diff --git a/lib/src/editor/editor_component/service/ime/delta_input_service.dart b/lib/src/editor/editor_component/service/ime/delta_input_service.dart index f8f24c63b..157356e4e 100644 --- a/lib/src/editor/editor_component/service/ime/delta_input_service.dart +++ b/lib/src/editor/editor_component/service/ime/delta_input_service.dart @@ -186,6 +186,10 @@ class DeltaTextInputService extends TextInputService with DeltaTextInputClient { } } } + + //new callback on flutter 3.44 + @override + bool onFocusReceived() => attached; } const String _whitespace = ' '; diff --git a/lib/src/editor/editor_component/service/shortcuts/command/arrow_left_command.dart b/lib/src/editor/editor_component/service/shortcuts/command/arrow_left_command.dart index 6be14fd23..5bcdfcb58 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command/arrow_left_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command/arrow_left_command.dart @@ -109,8 +109,16 @@ CommandShortcutEventHandler _moveCursorToLeftWordCommandHandler = selectionRange: SelectionRange.word, ); if (startOfWord == null) { + editorState.moveCursorForward(SelectionMoveRange.word); + return KeyEventResult.handled; + } + + // if 0, it means we are at the beginning of the line, move to the previous node + if (selection.end.offset <= 0) { + editorState.moveCursorForward(SelectionMoveRange.word); return KeyEventResult.handled; } + final selectedWord = delta.toPlainText().substring( startOfWord.offset, selection.end.offset, diff --git a/lib/src/editor/editor_component/service/shortcuts/command/arrow_right_command.dart b/lib/src/editor/editor_component/service/shortcuts/command/arrow_right_command.dart index b63422d5b..e087f9fde 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command/arrow_right_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command/arrow_right_command.dart @@ -112,6 +112,12 @@ CommandShortcutEventHandler _moveCursorToRightWordCommandHandler = selectionRange: SelectionRange.word, ); if (endOfWord == null) { + editorState.moveCursorBackward(SelectionMoveRange.line); + return KeyEventResult.handled; + } + // if the end of the word is less than or equal to 0, it means we are at the end of the line, move to the next node + if (endOfWord.offset <= 0) { + editorState.moveCursorBackward(SelectionMoveRange.line); return KeyEventResult.handled; } final selectedLine = delta.toPlainText(); diff --git a/lib/src/extensions/node_extensions.dart b/lib/src/extensions/node_extensions.dart index 35c121004..ed58dcf32 100644 --- a/lib/src/extensions/node_extensions.dart +++ b/lib/src/extensions/node_extensions.dart @@ -49,16 +49,20 @@ extension NodeExtensions on Node { } } - var next = this.next; - while (next != null) { - final nextDescendentMatch = next._thisOrDescendantMatching(test); - if (nextDescendentMatch != null) { - return nextDescendentMatch; + Node? current = this; + while (current != null) { + var next = current.next; + while (next != null) { + final nextDescendentMatch = next._thisOrDescendantMatching(test); + if (nextDescendentMatch != null) { + return nextDescendentMatch; + } + next = next.next; } - next = next.next; + current = current.parent; } - return parent?.next?._thisOrDescendantMatching(test); + return null; } Node? _thisOrDescendantMatching(bool Function(Node element) test) { diff --git a/lib/src/plugins/quill_delta/quill_delta_encoder.dart b/lib/src/plugins/quill_delta/quill_delta_encoder.dart index 684513c49..d2f669351 100644 --- a/lib/src/plugins/quill_delta/quill_delta_encoder.dart +++ b/lib/src/plugins/quill_delta/quill_delta_encoder.dart @@ -36,14 +36,21 @@ class QuillDeltaEncoder extends Converter { node = _applyHeadingStyleIfNeeded(node, attributes); node = _applyBlockquoteIfNeeded(node, attributes); _applyIndentIfNeeded(node, attributes); + _applyAlignIfNeeded(node, attributes); + } + if (!_isListItem(attributes)) { + nestedLists.clear(); } if (_isIndentBulletedList(attributes)) { final level = _indentLevel(attributes); - final path = [ - ...nestedLists[level - 1]!.last.path, - nestedLists[level]!.length - 1, - ]; - document.insert(path, [node]); + final inserted = _tryInsertNestedListNode( + document: document, + node: node, + level: level, + ); + if (!inserted) { + document.insert([index++], [node]); + } } else { document.insert([index++], [node]); } @@ -51,11 +58,34 @@ class QuillDeltaEncoder extends Converter { } else { final texts = op.text.split('\n'); if (texts.length > 1) { - node.delta?.insert(texts[0]); + // First segment appends to current node, then commit it + if (texts[0].isNotEmpty) { + _applyStyle(node, texts[0], attributes); + } document.insert([index++], [node]); - node = paragraphNode(delta: Delta()..insert(texts[1])); + + // Middle segments: each becomes a full committed paragraph + for (int i = 1; i < texts.length - 1; i++) { + node = paragraphNode(); + if (texts[i].isNotEmpty) { + _applyStyle(node, texts[i], attributes); + } + document.insert([index++], [node]); + } + + // Last segment starts a new open node (accumulates more ops) + node = paragraphNode(); + if (texts.last.isNotEmpty) { + _applyStyle(node, texts.last, attributes); + if (attributes != null) { + _applyAlignIfNeeded(node, attributes); + } + } } else { _applyStyle(node, op.text, attributes); + if (attributes != null) { + _applyAlignIfNeeded(node, attributes); + } } } } else { @@ -103,6 +133,13 @@ class QuillDeltaEncoder extends Converter { }); } + void _applyAlignIfNeeded(Node node, Map attributes) { + final align = attributes['align'] as String?; + if (align != null) { + node.updateAttributes({'align': align}); + } + } + void _applyIndentIfNeeded(Node node, Map attributes) { final indent = attributes[_indent] as int?; final list = attributes[_list] as String?; @@ -122,9 +159,7 @@ class QuillDeltaEncoder extends Converter { Node _applyBlockquoteIfNeeded(Node node, Map attributes) { final blockquote = attributes[_blockquote] as bool?; if (blockquote == true) { - return quoteNode( - delta: node.delta, - ); + return quoteNode(delta: node.delta); } return node; @@ -136,10 +171,7 @@ class QuillDeltaEncoder extends Converter { return node; } - return headingNode( - delta: node.delta, - level: header, - ); + return headingNode(delta: node.delta, level: header); } // If the attributes contains the list style, then apply the list style to the node. @@ -147,9 +179,7 @@ class QuillDeltaEncoder extends Converter { final list = attributes[_list] as String?; switch (list) { case _bulletedList: - final bulletedList = bulletedListNode( - delta: node.delta, - ); + final bulletedList = bulletedListNode(delta: node.delta); final indent = attributes[_indent] as int?; if (indent != null) { nestedLists[indent] ??= []; @@ -162,9 +192,7 @@ class QuillDeltaEncoder extends Converter { return bulletedList; case _orderedList: - final numberedList = numberedListNode( - delta: node.delta, - ); + final numberedList = numberedListNode(delta: node.delta); final indent = attributes[_indent] as int?; if (indent != null) { nestedLists[indent] ??= []; @@ -177,17 +205,11 @@ class QuillDeltaEncoder extends Converter { return numberedList; case _checkedList: - final checkedList = todoListNode( - delta: node.delta, - checked: true, - ); + final checkedList = todoListNode(delta: node.delta, checked: true); return checkedList; case _uncheckedList: - final uncheckedList = todoListNode( - delta: node.delta, - checked: false, - ); + final uncheckedList = todoListNode(delta: node.delta, checked: false); return uncheckedList; default: @@ -208,6 +230,48 @@ class QuillDeltaEncoder extends Converter { return [_bulletedList, _orderedList].contains(list) && indent != null; } + bool _isListItem(Map? attributes) { + final list = attributes?[_list] as String?; + + return [ + _bulletedList, + _orderedList, + _checkedList, + _uncheckedList, + ].contains(list); + } + + bool _tryInsertNestedListNode({ + required Document document, + required Node node, + required int level, + }) { + if (level <= 0) { + return false; + } + + final parent = _findNearestParentNode(level); + if (parent == null) { + return false; + } + + final path = [...parent.path, parent.children.length]; + document.insert(path, [node]); + + return true; + } + + Node? _findNearestParentNode(int level) { + for (int parentLevel = level - 1; parentLevel >= 0; parentLevel--) { + final candidates = nestedLists[parentLevel]; + if (candidates != null && candidates.isNotEmpty) { + return candidates.last; + } + } + + return null; + } + bool _containsStyle(Map? attributes, String key) { final value = attributes?[key] as bool?;