diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 350764f769..00ef06da5d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -74,7 +74,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "1. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -83,7 +83,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "2. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -92,7 +92,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "3. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -101,7 +101,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support." + "insert": "4. Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -110,7 +110,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "5. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -119,7 +119,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "6. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -128,7 +128,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "7. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -137,7 +137,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "8. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -146,7 +146,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "9. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -155,7 +155,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "10. Click the '?' at the bottom right for help and support." } ], "attributes": {} @@ -164,7 +164,7 @@ "type": "text", "delta": [ { - "insert": "Click the '?' at the bottom right for help and support." + "insert": "11. Click the '?' at the bottom right for help and support." } ], "attributes": {} 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 1e047a23b4..83960275e6 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -96,9 +96,7 @@ class _MyHomePageState extends State { ); return FlowyEditor( editorState: _editorState, - keyEventHandler: [ - deleteSingleImageNode, - ], + keyEventHandler: const [], ); } }, diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 4b63e77f51..389bfed320 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,17 +1,6 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; -FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) { - final selectNodes = editorState.selectedNodes; - if (selectNodes.length != 1 || selectNodes.first.type != 'image') { - return KeyEventResult.ignored; - } - TransactionBuilder(editorState) - ..deleteNode(selectNodes.first) - ..commit(); - return KeyEventResult.handled; -}; - class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ required super.node, @@ -67,6 +56,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { return null; } + @override + Offset getOffsetByTextSelection(TextSelection textSelection) { + return Offset.zero; + } + @override Widget build(BuildContext context) { return _build(context); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 3783eab4fa..1124ec3cbb 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -93,6 +93,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); _textSelection = textSelection; + print('text selection = $textSelection'); return _computeCursorRect(textSelection.baseOffset); } @@ -101,6 +102,12 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _textSelection; } + @override + Offset getOffsetByTextSelection(TextSelection textSelection) { + final offset = _computeCursorRect(textSelection.baseOffset).center; + return _renderParagraph.localToGlobal(offset); + } + @override Widget build(BuildContext context) { Widget richText; @@ -148,6 +155,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); final cursorHeight = _renderParagraph.getFullHeightForCaret(position); + print('offset = $offset, cursorHeight = $cursorHeight'); if (cursorHeight != null) { const cursorWidth = 2; return Rect.fromLTWH( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index f1fa65b33d..04a5721ed9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -19,6 +19,9 @@ class ApplyOptions { }); } +// TODO +final selectionServiceKey = GlobalKey(); + class EditorState { final StateTree document; final RenderPlugins renderPlugins; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart new file mode 100644 index 0000000000..b1b6e53512 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart @@ -0,0 +1,8 @@ +extension FlowyObjectExtensions on Object { + T? unwrapOrNull() { + if (this is T) { + return this as T; + } + return null; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 19c94ef327..3f8510d8b3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -11,5 +11,4 @@ export 'package:flowy_editor/operation/transaction.dart'; export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; export 'package:flowy_editor/editor_state.dart'; -export 'package:flowy_editor/service/flowy_editor_service.dart'; -export 'package:flowy_editor/service/flowy_keyboard_service.dart'; +export 'package:flowy_editor/service/editor_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 1ba8f32b53..59849c1a6a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -13,4 +13,7 @@ mixin Selectable on State { /// For [TextNode] only. TextSelection? getTextSelection(); + + /// For [TextNode] only. + Offset getOffsetByTextSelection(TextSelection textSelection); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart similarity index 64% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 0703e75022..d0efac2a0f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,5 +1,7 @@ -import 'package:flowy_editor/service/flowy_keyboard_service.dart'; -import 'package:flowy_editor/service/flowy_selection_service.dart'; +import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/service/selection_service.dart'; import '../editor_state.dart'; import 'package:flutter/material.dart'; @@ -23,11 +25,13 @@ class _FlowyEditorState extends State { @override Widget build(BuildContext context) { - return FlowySelectionService( + return FlowySelection( + key: selectionServiceKey, editorState: editorState, - child: FlowyKeyboardWidget( + child: FlowyKeyboard( handlers: [ flowyDeleteNodesHandler, + deleteSingleTextNodeHandler, ...widget.keyEventHandler, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart new file mode 100644 index 0000000000..dda52612e9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart @@ -0,0 +1,21 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; + +FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { + // Handle delete nodes. + final nodes = editorState.selectedNodes; + if (nodes.length <= 1) { + return KeyEventResult.ignored; + } + + debugPrint('delete nodes = $nodes'); + + nodes + .fold( + TransactionBuilder(editorState), + (previousValue, node) => previousValue..deleteNode(node), + ) + .commit(); + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart new file mode 100644 index 0000000000..3c1c1c9e95 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart @@ -0,0 +1,73 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.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:flowy_editor/service/selection_service.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.getTextSelection(); + 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 = + selectionServiceKey.currentState as FlowySelectionService; + 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 = + selectionServiceKey.currentState as FlowySelectionService; + 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/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart similarity index 65% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart index 68f295e0bd..060a9c98fb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart @@ -1,4 +1,3 @@ -import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flutter/services.dart'; import '../editor_state.dart'; @@ -9,27 +8,9 @@ typedef FlowyKeyEventHandler = KeyEventResult Function( RawKeyEvent event, ); -FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { - // Handle delete nodes. - final nodes = editorState.selectedNodes; - if (nodes.length <= 1) { - return KeyEventResult.ignored; - } - - debugPrint('delete nodes = $nodes'); - - nodes - .fold( - TransactionBuilder(editorState), - (previousValue, node) => previousValue..deleteNode(node), - ) - .commit(); - return KeyEventResult.handled; -}; - /// Process keyboard events -class FlowyKeyboardWidget extends StatefulWidget { - const FlowyKeyboardWidget({ +class FlowyKeyboard extends StatefulWidget { + const FlowyKeyboard({ Key? key, required this.handlers, required this.editorState, @@ -41,10 +22,10 @@ class FlowyKeyboardWidget extends StatefulWidget { final List handlers; @override - State createState() => _FlowyKeyboardWidgetState(); + State createState() => _FlowyKeyboardState(); } -class _FlowyKeyboardWidgetState extends State { +class _FlowyKeyboardState extends State { final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service'); @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart similarity index 82% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index b75ea5703b..99b0efb467 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -8,7 +8,7 @@ import '../document/node.dart'; import '../render/selection/selectable.dart'; /// Process selection and cursor -mixin _FlowySelectionService on State { +mixin FlowySelectionService on State { /// [Pan] and [Tap] must be mutually exclusive. /// Pan Offset? panStartOffset; @@ -19,20 +19,20 @@ mixin _FlowySelectionService on State { void updateSelection(Offset start, Offset end); - void updateCursor(Offset offset); + void updateCursor(Offset start); /// Returns selected node(s) /// Returns empty list if no nodes are being selected. - List get selectedNodes; + List getSelectedNodes(Offset start, [Offset? end]); /// Compute selected node triggered by [Tap] - Node? computeSelectedNodeByTap( + Node? computeSelectedNodeInOffset( Node node, Offset offset, ); /// Compute selected nodes triggered by [Pan] - List computeSelectedNodesByPan( + List computeSelectedNodesInRange( Node node, Offset start, Offset end, @@ -52,8 +52,8 @@ mixin _FlowySelectionService on State { ); } -class FlowySelectionService extends StatefulWidget { - const FlowySelectionService({ +class FlowySelection extends StatefulWidget { + const FlowySelection({ Key? key, required this.editorState, required this.child, @@ -63,11 +63,11 @@ class FlowySelectionService extends StatefulWidget { final Widget child; @override - State createState() => _FlowySelectionServiceState(); + State createState() => _FlowySelectionState(); } -class _FlowySelectionServiceState extends State - with _FlowySelectionService { +class _FlowySelectionState extends State + with FlowySelectionService { final _cursorKey = GlobalKey(debugLabel: 'cursor'); final List _selectionOverlays = []; @@ -106,7 +106,7 @@ class _FlowySelectionServiceState extends State void updateSelection(Offset start, Offset end) { _clearAllOverlayEntries(); - final nodes = selectedNodes; + final nodes = getSelectedNodes(start, end); editorState.selectedNodes = nodes; if (nodes.isEmpty) { return; @@ -133,10 +133,10 @@ class _FlowySelectionServiceState extends State } @override - void updateCursor(Offset offset) { + void updateCursor(Offset start) { _clearAllOverlayEntries(); - final nodes = selectedNodes; + final nodes = getSelectedNodes(start); editorState.selectedNodes = nodes; if (nodes.isEmpty) { return; @@ -147,7 +147,7 @@ class _FlowySelectionServiceState extends State return; } final selectable = selectedNode.key?.currentState as Selectable; - final rect = selectable.getCursorRect(offset); + final rect = selectable.getCursorRect(start); final cursor = OverlayEntry( builder: ((context) => FlowyCursorWidget( key: _cursorKey, @@ -161,13 +161,18 @@ class _FlowySelectionServiceState extends State } @override - List get selectedNodes { - if (panStartOffset != null && panEndOffset != null) { - return computeSelectedNodesByPan( - editorState.document.root, panStartOffset!, panEndOffset!); - } else if (tapOffset != null) { - final reuslt = - computeSelectedNodeByTap(editorState.document.root, tapOffset!); + List getSelectedNodes(Offset start, [Offset? end]) { + if (end != null) { + return computeSelectedNodesInRange( + editorState.document.root, + start, + end, + ); + } else { + final reuslt = computeSelectedNodeInOffset( + editorState.document.root, + start, + ); if (reuslt != null) { return [reuslt]; } @@ -176,13 +181,9 @@ class _FlowySelectionServiceState extends State } @override - Node? computeSelectedNodeByTap(Node node, Offset offset) { - assert(this.tapOffset != null); - final tapOffset = this.tapOffset; - if (tapOffset != null) {} - + Node? computeSelectedNodeInOffset(Node node, Offset offset) { for (final child in node.children) { - final result = computeSelectedNodeByTap(child, offset); + final result = computeSelectedNodeInOffset(child, offset); if (result != null) { return result; } @@ -198,7 +199,7 @@ class _FlowySelectionServiceState extends State } @override - List computeSelectedNodesByPan(Node node, Offset start, Offset end) { + List computeSelectedNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { if (isNodeInSelection(node, start, end)) { @@ -206,7 +207,7 @@ class _FlowySelectionServiceState extends State } } for (final child in node.children) { - result.addAll(computeSelectedNodesByPan(child, start, end)); + result.addAll(computeSelectedNodesInRange(child, start, end)); } // TODO: sort the result return result;