Skip to content

Commit

Permalink
feat: support dragging to reorder block (#887)
Browse files Browse the repository at this point in the history
* feat: support drag to reorder block

* feat: enable drag to reorder on home page

* fix: proper offset calculation for drop path

* docs: update doc comments for the drop target API

* fix: render issue

---------

Co-authored-by: Mathias Mogensen <[email protected]>
  • Loading branch information
LucasXu0 and Xazin authored Sep 12, 2024
1 parent 200b572 commit 5d1d311
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 33 deletions.
9 changes: 9 additions & 0 deletions example/lib/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:example/pages/auto_complete_editor.dart';
import 'package:example/pages/collab_editor.dart';
import 'package:example/pages/collab_selection_editor.dart';
import 'package:example/pages/customize_theme_for_editor.dart';
import 'package:example/pages/drag_to_reorder_editor.dart';
import 'package:example/pages/editor.dart';
import 'package:example/pages/editor_list.dart';
import 'package:example/pages/fixed_toolbar_editor.dart';
Expand Down Expand Up @@ -167,6 +168,14 @@ class _HomePageState extends State<HomePage> {

// Theme Demo
_buildSeparator(context, 'Showcases'),
_buildListTile(context, 'Drag to reorder', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DragToReorderEditor(),
),
);
}),
_buildListTile(context, 'Markdown Editor', () {
Navigator.push(
context,
Expand Down
5 changes: 3 additions & 2 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:example/home_page.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:example/home_page.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

void main() {
Expand Down
14 changes: 14 additions & 0 deletions example/lib/pages/desktop_editor.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:example/pages/drag_to_reorder_editor.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -93,6 +94,9 @@ class _DesktopEditorState extends State<DesktopEditor> {
editorStyle: editorStyle,
enableAutoComplete: true,
autoCompleteTextProvider: _buildAutoCompleteTextProvider,
dropTargetStyle: const AppFlowyDropTargetStyle(
color: Colors.red,
),
header: Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Image.asset(
Expand Down Expand Up @@ -164,6 +168,16 @@ class _DesktopEditorState extends State<DesktopEditor> {
value.configuration = value.configuration.copyWith(
padding: (_) => const EdgeInsets.symmetric(vertical: 8.0),
);

if (key != PageBlockKeys.type) {
value.showActions = (_) => true;
value.actionBuilder = (context, actionState) {
return DragToReorderAction(
blockComponentContext: context,
builder: value,
);
};
}
});
return map;
}
Expand Down
213 changes: 213 additions & 0 deletions example/lib/pages/drag_to_reorder_editor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

class DragToReorderEditor extends StatefulWidget {
const DragToReorderEditor({
super.key,
});

@override
State<DragToReorderEditor> createState() => _DragToReorderEditorState();
}

class _DragToReorderEditorState extends State<DragToReorderEditor> {
late final EditorState editorState;
late final EditorStyle editorStyle;
late final Map<String, BlockComponentBuilder> blockComponentBuilders;

@override
void initState() {
super.initState();

forceShowBlockAction = true;
editorState = _createEditorState();
editorStyle = _createEditorStyle();
blockComponentBuilders = _createBlockComponentBuilders();
}

@override
void dispose() {
forceShowBlockAction = false;
editorState.dispose();

super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Drag to reorder'),
),
body: AppFlowyEditor(
editorState: editorState,
editorStyle: editorStyle,
blockComponentBuilders: blockComponentBuilders,
dropTargetStyle: const AppFlowyDropTargetStyle(
color: Colors.red,
),
),
);
}

Map<String, BlockComponentBuilder> _createBlockComponentBuilders() {
final builders = {...standardBlockComponentBuilderMap};
for (final entry in builders.entries) {
if (entry.key == PageBlockKeys.type) {
continue;
}

final builder = entry.value;

// only customize the todo list block
if (entry.key == TodoListBlockKeys.type) {
builder.showActions = (_) => true;
builder.actionBuilder = (context, actionState) {
return DragToReorderAction(
blockComponentContext: context,
builder: builder,
);
};
}
}
return builders;
}

EditorState _createEditorState() {
final document = Document.blank()
..insert([
0,
], [
todoListNode(checked: false, text: 'Todo 1'),
todoListNode(checked: false, text: 'Todo 2'),
todoListNode(checked: false, text: 'Todo 3'),
]);
return EditorState(
document: document,
);
}

EditorStyle _createEditorStyle() {
return EditorStyle.desktop(
cursorWidth: 2.0,
cursorColor: Colors.black,
selectionColor: Colors.grey.shade300,
textStyleConfiguration: TextStyleConfiguration(
text: GoogleFonts.poppins(
fontSize: 16,
color: Colors.black,
),
code: GoogleFonts.architectsDaughter(),
bold: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
),
),
padding: const EdgeInsets.symmetric(horizontal: 200.0),
);
}
}

class DragToReorderAction extends StatefulWidget {
const DragToReorderAction({
super.key,
required this.blockComponentContext,
required this.builder,
});

final BlockComponentContext blockComponentContext;
final BlockComponentBuilder builder;

@override
State<DragToReorderAction> createState() => _DragToReorderActionState();
}

class _DragToReorderActionState extends State<DragToReorderAction> {
late final Node node;
late final BlockComponentContext blockComponentContext;

Offset? globalPosition;

@override
void initState() {
super.initState();

// copy the node to avoid the node in document being updated
node = widget.blockComponentContext.node.copyWith();
blockComponentContext = BlockComponentContext(
widget.blockComponentContext.buildContext,
node,
);
}

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 10.0, right: 4.0),
child: Draggable<Node>(
data: node,
feedback: Opacity(
opacity: 0.7,
child: Material(
color: Colors.transparent,
child: IntrinsicWidth(
child: IntrinsicHeight(
child: Provider.value(
value: context.read<EditorState>(),
child: widget.builder.build(blockComponentContext),
),
),
),
),
),
onDragStarted: () {
debugPrint('onDragStarted');
context.read<EditorState>().selectionService.removeDropTarget();
},
onDragUpdate: (details) {
context
.read<EditorState>()
.selectionService
.renderDropTargetForOffset(details.globalPosition);

globalPosition = details.globalPosition;
},
onDragEnd: (details) {
context.read<EditorState>().selectionService.removeDropTarget();

if (globalPosition == null) {
return;
}

final data = context
.read<EditorState>()
.selectionService
.getDropTargetRenderData(globalPosition!);
final acceptedPath = data?.dropPath;
debugPrint('onDragEnd, acceptedPath($acceptedPath)');
_moveNodeToNewPosition(node, acceptedPath);
},
child: const Icon(
Icons.drag_indicator_rounded,
size: 18,
),
),
);
}

Future<void> _moveNodeToNewPosition(Node node, Path? acceptedPath) async {
if (acceptedPath == null) {
debugPrint('acceptedPath is null');
return;
}

debugPrint('move node($node) to path($acceptedPath)');

final editorState = context.read<EditorState>();
final transaction = editorState.transaction;
transaction.insertNode(acceptedPath, node.copyWith());
transaction.deleteNode(widget.blockComponentContext.node);
await editorState.apply(transaction);
}
}
6 changes: 6 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ dependency_overrides:
appflowy_editor:
path: ../

appflowy_editor_plugins:
git:
url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git
path: "packages/appflowy_editor_plugins"
ref: "b228456"

dev_dependencies:
flutter_test:
sdk: flutter
Expand Down
9 changes: 3 additions & 6 deletions lib/src/core/document/node.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import 'dart:collection';

import 'package:flutter/material.dart';

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:nanoid/non_secure.dart';

abstract class NodeExternalValues {
Expand Down Expand Up @@ -225,11 +226,7 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {

@override
String toString() {
return '''Node(id: $id,
type: $type,
attributes: $attributes,
children: $children,
)''';
return 'Node(id: $id, type: $type, attributes: $attributes, children: $children)';
}

Delta? get delta {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ class _BlockComponentActionWrapperState
}
}

@override
void initState() {
super.initState();

if (forceShowBlockAction) {
alwaysShowActions = true;
showActionsNotifier.value = true;
}
}

@override
void dispose() {
showActionsNotifier.dispose();

super.dispose();
}

@override
Widget build(BuildContext context) {
return MouseRegion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
/// 1. used to update the child widget when node is changed
/// ~~2. used to show block component actions~~
/// 3. used to add the layer link to the child widget
class BlockComponentContainer extends StatefulWidget {
class BlockComponentContainer extends StatelessWidget {
const BlockComponentContainer({
super.key,
required this.configuration,
Expand All @@ -17,29 +17,24 @@ class BlockComponentContainer extends StatefulWidget {

final Node node;
final BlockComponentConfiguration configuration;

final WidgetBuilder builder;

@override
State<BlockComponentContainer> createState() =>
BlockComponentContainerState();
}

class BlockComponentContainerState extends State<BlockComponentContainer> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Node>.value(
value: widget.node,
final child = ChangeNotifierProvider<Node>.value(
value: node,
child: Consumer<Node>(
builder: (_, __, ___) {
AppFlowyEditorLog.editor
.debug('node is rebuilding...: type: ${widget.node.type} ');
.debug('node is rebuilding...: type: ${node.type} ');
return CompositedTransformTarget(
link: widget.node.layerLink,
child: widget.builder(context),
link: node.layerLink,
child: builder(context),
);
},
),
);

return child;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import 'package:flutter/material.dart';

const errorBlockComponentBuilderKey = 'errorBlockComponentBuilderKey';

// this value is used to force show the block action.
// it is only for test now.
bool forceShowBlockAction = false;

typedef BlockActionBuilder = Widget Function(
BlockComponentContext blockComponentContext,
BlockComponentActionState state,
Expand Down
Loading

0 comments on commit 5d1d311

Please sign in to comment.