From 966eea21791077231d0454ef05e66ef28af15bb6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 15:36:17 +0800 Subject: [PATCH 01/11] chore: format code --- .../flowy_editor/example/assets/example.json | 11 ---- .../lib/service/editor_service.dart | 53 +++++++++++-------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 2e982f98e4..6a0fba3021 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -3,17 +3,6 @@ "type": "editor", "attributes": {}, "children": [ - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "quote" - } - }, { "type": "image", "attributes": { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index f01cbe8a28..1eceb099f2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,24 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/render/editor/editor_entry.dart'; +import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; -import 'package:flowy_editor/service/input_service.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/shortcut_service.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/service/selection_service.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/render/rich_text/quoted_text.dart'; - -import 'package:flutter/material.dart'; +import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; +import 'package:flowy_editor/service/input_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/service/selection_service.dart'; +import 'package:flowy_editor/service/shortcut_service.dart'; NodeWidgetBuilders defaultBuilders = { 'editor': EditorEntryWidgetBuilder(), @@ -61,13 +61,14 @@ class _FlowyEditorState extends State { void initState() { super.initState(); - editorState.service.renderPluginService = FlowyRenderPlugin( - editorState: editorState, - builders: { - ...defaultBuilders, - ...widget.customBuilders, - }, - ); + editorState.service.renderPluginService = _createRenderPlugin(); + } + + @override + void didUpdateWidget(covariant FlowyEditor oldWidget) { + super.didUpdateWidget(oldWidget); + + editorState.service.renderPluginService = _createRenderPlugin(); } @override @@ -106,4 +107,12 @@ class _FlowyEditorState extends State { ), ); } + + FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin( + editorState: editorState, + builders: { + ...defaultBuilders, + ...widget.customBuilders, + }, + ); } From c4b3c54a7c117db07053d4f44743b703d3f3c408 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 15:45:49 +0800 Subject: [PATCH 02/11] chore: format code --- .../lib/service/editor_service.dart | 18 ++++++++++++------ .../lib/service/input_service.dart | 7 ++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 1eceb099f2..de5667fa10 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -30,6 +30,14 @@ NodeWidgetBuilders defaultBuilders = { 'text/quote': QuotedTextNodeWidgetBuilder(), }; +List defaultKeyEventHandler = [ + slashShortcutHandler, + flowyDeleteNodesHandler, + deleteSingleTextNodeHandler, + arrowKeysHandler, + enterInEdgeOfTextNodeHandler, +]; + class FlowyEditor extends StatefulWidget { const FlowyEditor({ Key? key, @@ -68,7 +76,9 @@ class _FlowyEditorState extends State { void didUpdateWidget(covariant FlowyEditor oldWidget) { super.didUpdateWidget(oldWidget); - editorState.service.renderPluginService = _createRenderPlugin(); + if (editorState.service != oldWidget.editorState.service) { + editorState.service.renderPluginService = _createRenderPlugin(); + } } @override @@ -82,11 +92,7 @@ class _FlowyEditorState extends State { child: FlowyKeyboard( key: editorState.service.keyboardServiceKey, handlers: [ - slashShortcutHandler, - flowyDeleteNodesHandler, - deleteSingleTextNodeHandler, - arrowKeysHandler, - enterInEdgeOfTextNodeHandler, + ...defaultKeyEventHandler, ...widget.keyEventHandlers, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index 38309414f4..9bc35f10ab 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -1,10 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; mixin FlowyInputService { void attach(TextEditingValue textEditingValue); From 55d46edeaf845eeb9a34c6f4f70a12c34466d66d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 23:16:56 +0800 Subject: [PATCH 03/11] fix: node change notifier doesn't work --- .../flowy_editor/example/lib/main.dart | 2 + .../lib/service/editor_service.dart | 2 - .../lib/service/input_service.dart | 22 ++++++ .../delete_single_text_node_handler.dart | 69 ------------------- .../lib/service/render_plugin_service.dart | 46 ++++++++----- 5 files changed, 54 insertions(+), 87 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 7ebb340f2d..a2fcc7a4df 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -55,6 +55,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { late EditorState _editorState; + final editorKey = GlobalKey(); int page = 0; @override @@ -116,6 +117,7 @@ class _MyHomePageState extends State { document: document, ); return FlowyEditor( + key: editorKey, editorState: _editorState, keyEventHandlers: const [], customBuilders: { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index de5667fa10..1d961737ff 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -12,7 +12,6 @@ import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; @@ -33,7 +32,6 @@ NodeWidgetBuilders defaultBuilders = { List defaultKeyEventHandler = [ slashShortcutHandler, flowyDeleteNodesHandler, - deleteSingleTextNodeHandler, arrowKeysHandler, enterInEdgeOfTextNodeHandler, ]; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index 9bc35f10ab..a28d572d30 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -94,6 +94,7 @@ class _FlowyInputState extends State // TODO: implement the detail for (final delta in deltas) { if (delta is TextEditingDeltaInsertion) { + _applyInsert(delta); } else if (delta is TextEditingDeltaDeletion) { } else if (delta is TextEditingDeltaReplacement) { } else if (delta is TextEditingDeltaNonTextUpdate) { @@ -103,6 +104,27 @@ class _FlowyInputState extends State } } + void _applyInsert(TextEditingDeltaInsertion delta) { + final selectionService = _editorState.service.selectionService; + final currentSelection = selectionService.currentSelection; + if (currentSelection == null) { + return; + } + if (currentSelection.isSingle) { + final textNode = + selectionService.currentSelectedNodes.value.first as TextNode; + TransactionBuilder(_editorState) + ..insertText( + textNode, + delta.insertionOffset, + delta.textInserted, + ) + ..commit(); + } else { + // TODO: implement + } + } + @override void close() { _textInputConnection?.close(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart deleted file mode 100644 index f5da6423ae..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -// TODO: need to be refactored, just a example code. -FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.backspace) { - return KeyEventResult.ignored; - } - - // final selectionNodes = editorState.selectedNodes; - // if (selectionNodes.length == 1 && selectionNodes.first is TextNode) { - // final node = selectionNodes.first.unwrapOrNull(); - // final selectable = node?.key?.currentState?.unwrapOrNull(); - // if (selectable != null) { - // final textSelection = selectable.getCurrentTextSelection(); - // if (textSelection != null) { - // if (textSelection.isCollapsed) { - // /// Three cases: - // /// Delete the zero character, - // /// 1. if there is still text node in front of it, then merge them. - // /// 2. if not, just ignore - // /// Delete the non-zero character, - // /// 3. delete the single character. - // if (textSelection.baseOffset == 0) { - // if (node?.previous != null && node?.previous is TextNode) { - // final previous = node!.previous! as TextNode; - // final newTextSelection = TextSelection.collapsed( - // offset: previous.toRawString().length); - // final selectionService = editorState.service.selectionService; - // final previousSelectable = - // previous.key?.currentState?.unwrapOrNull(); - // final newOfset = previousSelectable - // ?.getOffsetByTextSelection(newTextSelection); - // if (newOfset != null) { - // // selectionService.updateCursor(newOfset); - // } - // // merge - // TransactionBuilder(editorState) - // ..deleteNode(node) - // ..insertText( - // previous, previous.toRawString().length, node.toRawString()) - // ..commit(); - // return KeyEventResult.handled; - // } else { - // return KeyEventResult.ignored; - // } - // } else { - // TransactionBuilder(editorState) - // ..deleteText(node!, textSelection.baseOffset - 1, 1) - // ..commit(); - // final newTextSelection = - // TextSelection.collapsed(offset: textSelection.baseOffset - 1); - // final selectionService = editorState.service.selectionService; - // final newOfset = - // selectable.getOffsetByTextSelection(newTextSelection); - // // selectionService.updateCursor(newOfset); - // return KeyEventResult.handled; - // } - // } - // } - // } - // } - return KeyEventResult.ignored; -}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart index 47159097b5..8ac32ac66c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart @@ -75,10 +75,7 @@ class FlowyRenderPlugin extends FlowyRenderPluginService { if (builder != null && builder.nodeValidator(node)) { final key = GlobalKey(debugLabel: name); node.key = key; - return _wrap( - builder.build(context), - context, - ); + return _autoUpdateNodeWidget(builder, context); } else { assert(false, 'Could not query the builder with this $name'); // TODO: return a placeholder widget with tips. @@ -87,14 +84,14 @@ class FlowyRenderPlugin extends FlowyRenderPluginService { } @override - void register(String name, NodeWidgetBuilder builder) { + void register(String name, NodeWidgetBuilder builder) { debugPrint('[Plugins] registering $name...'); _validatePlugin(name); _builders[name] = builder; } @override - void registerAll(Map> builders) { + void registerAll(Map builders) { builders.forEach(register); } @@ -104,18 +101,35 @@ class FlowyRenderPlugin extends FlowyRenderPluginService { _builders.remove(name); } - Widget _wrap(Widget widget, NodeWidgetContext context) { + Widget _autoUpdateNodeWidget( + NodeWidgetBuilder builder, NodeWidgetContext context) { + Widget notifier; + if (context.node is TextNode) { + notifier = ChangeNotifierProvider.value( + value: context.node as TextNode, + builder: (_, child) { + return Consumer( + builder: ((_, value, child) { + debugPrint('Text Node is rebuilding...'); + return builder.build(context); + }), + ); + }); + } else { + notifier = ChangeNotifierProvider.value( + value: context.node, + builder: (_, child) { + return Consumer( + builder: ((_, value, child) { + debugPrint('Node is rebuilding...'); + return builder.build(context); + }), + ); + }); + } return CompositedTransformTarget( link: context.node.layerLink, - child: ChangeNotifierProvider.value( - value: context.node, - builder: (context, child) => Consumer( - builder: ((context, value, child) { - debugPrint('Node is rebuilding...'); - return widget; - }), - ), - ), + child: notifier, ); } From 575e01c9094c9e9e01ef2c3fe804ddda540b7491 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 29 Jul 2022 23:27:39 +0800 Subject: [PATCH 04/11] feat: implement text replacement in singe selection --- .../lib/operation/transaction_builder.dart | 13 ++++++++++++ .../lib/service/input_service.dart | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index fb042fe566..64fede87b3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -75,6 +75,19 @@ class TransactionBuilder { Selection.collapsed(Position(path: node.path, offset: index)); } + replaceText(TextNode node, int index, int length, String content) { + textEdit( + node, + () => Delta().retain(index).delete(length).insert(content), + ); + afterSelection = Selection.collapsed( + Position( + path: node.path, + offset: index + content.length, + ), + ); + } + add(Operation op) { final Operation? last = operations.isEmpty ? null : operations.last; if (last != null) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart index a28d572d30..ee570d902a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart @@ -97,6 +97,7 @@ class _FlowyInputState extends State _applyInsert(delta); } else if (delta is TextEditingDeltaDeletion) { } else if (delta is TextEditingDeltaReplacement) { + _applyReplacement(delta); } else if (delta is TextEditingDeltaNonTextUpdate) { // We don't need to care the [TextEditingDeltaNonTextUpdate]. // Do nothing. @@ -125,6 +126,25 @@ class _FlowyInputState extends State } } + void _applyReplacement(TextEditingDeltaReplacement delta) { + final selectionService = _editorState.service.selectionService; + final currentSelection = selectionService.currentSelection; + if (currentSelection == null) { + return; + } + if (currentSelection.isSingle) { + final textNode = + selectionService.currentSelectedNodes.value.first as TextNode; + final length = delta.replacedRange.end - delta.replacedRange.start; + TransactionBuilder(_editorState) + ..replaceText( + textNode, delta.replacedRange.start, length, delta.replacementText) + ..commit(); + } else { + // TODO: implement + } + } + @override void close() { _textInputConnection?.close(); From b245841ec3ac4ee89300a34b74033a12b93b1c13 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 30 Jul 2022 00:00:10 +0800 Subject: [PATCH 05/11] feat: implement text delete --- .../flowy_editor/example/assets/example.json | 6 +- .../lib/service/editor_service.dart | 2 + .../delele_text_handler.dart | 83 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 6a0fba3021..9f1d278e16 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -162,7 +162,11 @@ "type": "text", "delta": [ { - "insert": "Hello world" + "insert": "Hello " + }, + { + "insert": "world", + "attributes": { "bold": true } } ], "attributes": { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 1d961737ff..b7a64ffcc7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/service/internal_key_event_handlers/delele_text_handler.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/editor_state.dart'; @@ -30,6 +31,7 @@ NodeWidgetBuilders defaultBuilders = { }; List defaultKeyEventHandler = [ + deleteTextHandler, slashShortcutHandler, flowyDeleteNodesHandler, arrowKeysHandler, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart new file mode 100644 index 0000000000..b50d947d5c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart @@ -0,0 +1,83 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// Handle delete text. +FlowyKeyEventHandler deleteTextHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.backspace) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection; + if (selection == null) { + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + // make sure all nodes is [TextNode]. + final textNodes = nodes.whereType().toList(); + if (textNodes.length != nodes.length) { + return KeyEventResult.ignored; + } + + TransactionBuilder transactionBuilder = TransactionBuilder(editorState); + if (textNodes.length == 1) { + final textNode = textNodes.first; + final index = selection.start.offset - 1; + if (index < 0) { + // 1. style + if (textNode.subtype != null) { + transactionBuilder.updateNode(textNode, { + 'subtype': null, + }); + } else { + // 2. non-style + // find previous text node. + while (textNode.previous != null) { + if (textNode.previous is TextNode) { + final previous = textNode.previous as TextNode; + transactionBuilder + ..deleteNode(textNode) + ..insertText( + previous, + previous.toRawString().length, + textNode.toRawString(), + ); + // FIXME: keep the attributes. + break; + } + } + } + } else { + transactionBuilder.deleteText( + textNode, + selection.start.offset - 1, + 1, + ); + } + } else { + for (var i = 0; i < textNodes.length; i++) { + final textNode = textNodes[i]; + if (i == 0) { + transactionBuilder.deleteText( + textNode, + selection.start.offset, + textNode.toRawString().length - selection.start.offset, + ); + } else if (i == textNodes.length - 1) { + transactionBuilder.deleteText( + textNode, + 0, + selection.end.offset, + ); + } else { + transactionBuilder.deleteNode(textNode); + } + } + } + + transactionBuilder.commit(); + + return KeyEventResult.handled; +}; From 29fe4811c347cacf5501767d2e726e1505c56767 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 31 Jul 2022 15:57:22 +0800 Subject: [PATCH 06/11] fix: selection areas could not overlay --- ...{flowy_selection_widget.dart => selection_widget.dart} | 8 ++++++-- .../flowy_editor/lib/service/selection_service.dart | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/render/selection/{flowy_selection_widget.dart => selection_widget.dart} (76%) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart similarity index 76% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart index 96dd6a7759..e3dea7af34 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart @@ -25,8 +25,12 @@ class _SelectionWidgetState extends State { link: widget.layerLink, offset: widget.rect.topLeft, showWhenUnlinked: true, - child: Container( - color: widget.color, + // Ignore the gestures in selection overlays + // to solve the problem that selection areas cannot overlap. + child: IgnorePointer( + child: Container( + color: widget.color, + ), ), ), ); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 975677d508..375ebee31c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -5,7 +5,7 @@ import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart'; -import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; +import 'package:flowy_editor/render/selection/selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flutter/gestures.dart'; From 89a0a5599e40e15bcfaf6918552ad073860a6e72 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 31 Jul 2022 16:01:46 +0800 Subject: [PATCH 07/11] fix: cursor cannot be selected in same position. --- .../flowy_editor/lib/render/selection/cursor_widget.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart index 6a27eed855..19da4b55f4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart @@ -64,8 +64,12 @@ class CursorWidgetState extends State { link: widget.layerLink, offset: widget.rect.topCenter, showWhenUnlinked: true, - child: Container( - color: showCursor ? widget.color : Colors.transparent, + // Ignore the gestures in cursor + // to solve the problem that cursor area cannot be selected. + child: IgnorePointer( + child: Container( + color: showCursor ? widget.color : Colors.transparent, + ), ), ), ); From b577489c2f04b406a5fe30c69b5f3b0d9217f3ed Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 31 Jul 2022 16:14:12 +0800 Subject: [PATCH 08/11] feat: implement delete multiple text node and merge the text. --- .../lib/operation/transaction_builder.dart | 4 +++ .../delele_text_handler.dart | 31 ++++++++----------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 64fede87b3..5c50a19c42 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -48,6 +48,10 @@ class TransactionBuilder { add(DeleteOperation(path: node.path, removedValue: node)); } + deleteNodes(List nodes) { + nodes.forEach(deleteNode); + } + textEdit(TextNode node, Delta Function() f) { beforeSelection = state.cursorSelection; final path = node.path; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart index b50d947d5c..601c0ffef6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart @@ -57,24 +57,19 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { ); } } else { - for (var i = 0; i < textNodes.length; i++) { - final textNode = textNodes[i]; - if (i == 0) { - transactionBuilder.deleteText( - textNode, - selection.start.offset, - textNode.toRawString().length - selection.start.offset, - ); - } else if (i == textNodes.length - 1) { - transactionBuilder.deleteText( - textNode, - 0, - selection.end.offset, - ); - } else { - transactionBuilder.deleteNode(textNode); - } - } + final first = textNodes.first; + var content = textNodes.last.toRawString(); + content = content.substring(selection.end.offset, content.length); + // Merge the fist and the last text node content, + // and delete the all nodes expect for the first. + transactionBuilder + ..deleteNodes(textNodes.sublist(1)) + ..replaceText( + first, + selection.start.offset, + first.toRawString().length - selection.start.offset, + content, + ); } transactionBuilder.commit(); From d058f2d5914b39afb50c4fa6bb70369e9fef0f60 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 31 Jul 2022 17:16:07 +0800 Subject: [PATCH 09/11] feat: bold the text by command/control + b/B --- .../lib/service/editor_service.dart | 2 + ...pdate_text_style_by_command_x_handler.dart | 83 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index b7a64ffcc7..71143a3ede 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,4 +1,5 @@ import 'package:flowy_editor/service/internal_key_event_handlers/delele_text_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/editor_state.dart'; @@ -36,6 +37,7 @@ List defaultKeyEventHandler = [ flowyDeleteNodesHandler, arrowKeysHandler, enterInEdgeOfTextNodeHandler, + updateTextStyleByCommandXHandler, ]; class FlowyEditor extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart new file mode 100644 index 0000000000..5f13484442 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -0,0 +1,83 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flutter/material.dart'; + +FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { + if (!event.isMetaPressed || event.character == null) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection; + final nodes = editorState.service.selectionService.currentSelectedNodes.value + .whereType() + .toList(); + + if (selection == null || nodes.isEmpty) { + return KeyEventResult.ignored; + } + + switch (event.character!) { + // bold + case 'B': + case 'b': + _makeBold(editorState, nodes, selection); + return KeyEventResult.handled; + default: + break; + } + + return KeyEventResult.ignored; +}; + +// TODO: implement unBold. +void _makeBold( + EditorState editorState, List nodes, Selection selection) { + final builder = TransactionBuilder(editorState); + if (nodes.length == 1) { + builder.formatText( + nodes.first, + selection.start.offset, + selection.end.offset - selection.start.offset, + { + 'bold': true, + }, + ); + } else { + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + if (i == 0) { + builder.formatText( + node, + selection.start.offset, + node.toRawString().length - selection.start.offset, + { + 'bold': true, + }, + ); + } else if (i == nodes.length - 1) { + builder.formatText( + node, + 0, + selection.end.offset, + { + 'bold': true, + }, + ); + } else { + builder.formatText( + node, + 0, + node.toRawString().length, + { + 'bold': true, + }, + ); + } + } + } + builder.commit(); +} From c65f2e1b38b32674fbcaf17536d5cf5e4363bd84 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 1 Aug 2022 10:42:38 +0800 Subject: [PATCH 10/11] fix: delete text in single line --- .../delele_text_handler.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart index 601c0ffef6..4ad34b3c08 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart @@ -50,11 +50,19 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { } } } else { - transactionBuilder.deleteText( - textNode, - selection.start.offset - 1, - 1, - ); + if (selection.isCollapsed) { + transactionBuilder.deleteText( + textNode, + selection.start.offset - 1, + 1, + ); + } else { + transactionBuilder.deleteText( + textNode, + selection.start.offset, + selection.end.offset - selection.start.offset, + ); + } } } else { final first = textNodes.first; From 58856ccb1ec1b3162d2dd6cb18d400fade58fa43 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 1 Aug 2022 11:29:04 +0800 Subject: [PATCH 11/11] feat: implement deleting text in multiple lines. --- .../flowy_editor/lib/document/text_delta.dart | 5 ++++ .../lib/operation/transaction_builder.dart | 24 +++++++++++++++++-- .../delele_text_handler.dart | 16 +++++-------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index 2f3d194255..64335d4a05 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart @@ -275,6 +275,11 @@ class Delta { Delta([List? ops]) : operations = ops ?? []; + Delta addAll(List textOps) { + textOps.forEach(add); + return this; + } + Delta add(TextOperation textOp) { if (textOp.isEmpty) { return this; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 5c50a19c42..e70dfc411a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -63,8 +63,28 @@ class TransactionBuilder { add(TextEditOperation(path: path, delta: delta, inverted: inverted)); } - insertText(TextNode node, int index, String content) { - textEdit(node, () => Delta().retain(index).insert(content)); + mergeText(TextNode firstNode, TextNode secondNode, + {int? firstOffset, int secondOffset = 0}) { + final firstLength = firstNode.delta.length; + final secondLength = secondNode.delta.length; + textEdit( + firstNode, + () => Delta() + ..retain(firstOffset ?? firstLength) + ..delete(firstLength - (firstOffset ?? firstLength)) + ..addAll(secondNode.delta.slice(secondOffset, secondLength).operations), + ); + afterSelection = Selection.collapsed( + Position( + path: firstNode.path, + offset: firstOffset ?? firstLength, + ), + ); + } + + insertText(TextNode node, int index, String content, + [Attributes? attributes]) { + textEdit(node, () => Delta().retain(index).insert(content, attributes)); afterSelection = Selection.collapsed( Position(path: node.path, offset: index + content.length)); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart index 4ad34b3c08..498fd845b2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart @@ -39,12 +39,7 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { final previous = textNode.previous as TextNode; transactionBuilder ..deleteNode(textNode) - ..insertText( - previous, - previous.toRawString().length, - textNode.toRawString(), - ); - // FIXME: keep the attributes. + ..mergeText(previous, textNode); break; } } @@ -66,17 +61,18 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { } } else { final first = textNodes.first; + final last = textNodes.last; var content = textNodes.last.toRawString(); content = content.substring(selection.end.offset, content.length); // Merge the fist and the last text node content, // and delete the all nodes expect for the first. transactionBuilder ..deleteNodes(textNodes.sublist(1)) - ..replaceText( + ..mergeText( first, - selection.start.offset, - first.toRawString().length - selection.start.offset, - content, + last, + firstOffset: selection.start.offset, + secondOffset: selection.end.offset, ); }