diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart index a6bc269284..3d7f895fd9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart @@ -43,7 +43,11 @@ class RichTextNodeWidget extends BuiltInTextWidget { // customize class _RichTextNodeWidgetState extends State - with SelectableMixin, DefaultSelectable, BuiltInStyleMixin { + with + SelectableMixin, + DefaultSelectable, + BuiltInStyleMixin, + BuiltInTextWidgetMixin { @override GlobalKey? get iconKey => null; @@ -59,7 +63,7 @@ class _RichTextNodeWidgetState extends State } @override - Widget build(BuildContext context) { + Widget buildWithSingle(BuildContext context) { return Padding( padding: padding, child: FlowyRichText( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 3df482012f..fad5bb7337 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -288,3 +288,55 @@ Node? _closestTextNode(Node? node) { } return null; } + +TextNode? findLastTextNode(Node node) { + final children = node.children.toList(growable: false).reversed; + for (final child in children) { + if (child.children.isNotEmpty) { + final result = findLastTextNode(child); + if (result != null) { + return result; + } + } + if (child is TextNode) { + return child; + } + } + if (node is TextNode) { + return node; + } + return null; +} + +// find the forward nearest text node +TextNode? forwardNearestTextNode(Node node) { + var previous = node.previous; + while (previous != null) { + final lastTextNode = findLastTextNode(previous); + if (lastTextNode != null) { + return lastTextNode; + } + if (previous is TextNode) { + return previous; + } + previous = previous.previous; + } + final parent = node.parent; + if (parent != null) { + if (parent is TextNode) { + return parent; + } + return forwardNearestTextNode(parent); + } + return null; +} + +Node? _forwardNearestTextNode(Node node) { + if (node is TextNode) { + return node; + } + if (node.next != null) { + return _forwardNearestTextNode(node.next!); + } + return null; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index f40e8a8fa5..246c6ecb27 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -1,5 +1,9 @@ +import 'dart:collection'; + import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; +import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:network_image_mock/network_image_mock.dart'; @@ -320,6 +324,133 @@ void main() async { ); expect((editor.nodeAtPath([0, 0]) as TextNode).toRawString(), text * 2); }); + + testWidgets('Delete the complicated nested bulleted list', (tester) async { + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + const text = 'Welcome to Appflowy 😁'; + final node = TextNode( + type: 'text', + delta: Delta()..insert(text), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, + }, + ); + + node + ..insert( + node.copyWith(children: LinkedList()), + ) + ..insert( + node.copyWith(children: LinkedList()) + ..insert( + node.copyWith(children: LinkedList()), + ) + ..insert( + node.copyWith(children: LinkedList()), + ), + ); + + final editor = tester.editor..insert(node); + await editor.startTesting(); + + await editor.updateSelection( + Selection.single(path: [0, 1], startOffset: 0), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( + editor.nodeAtPath([0, 1])!.subtype != BuiltInAttributeKey.bulletedList, + true, + ); + expect( + editor.nodeAtPath([0, 1, 0])!.subtype, + BuiltInAttributeKey.bulletedList, + ); + expect( + editor.nodeAtPath([0, 1, 1])!.subtype, + BuiltInAttributeKey.bulletedList, + ); + expect(find.byType(FlowyRichText), findsNWidgets(5)); + + // Before + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // After + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( + editor.nodeAtPath([1])!.subtype != BuiltInAttributeKey.bulletedList, + true, + ); + expect( + editor.nodeAtPath([1, 0])!.subtype == BuiltInAttributeKey.bulletedList, + true, + ); + expect( + editor.nodeAtPath([1, 1])!.subtype == BuiltInAttributeKey.bulletedList, + true, + ); + + // After + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + }); + + test('find the last text node', () { + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + // * Welcome to Appflowy 😁 + const text = 'Welcome to Appflowy 😁'; + TextNode textNode() { + return TextNode( + type: 'text', + delta: Delta()..insert(text), + ); + } + + final node110 = textNode(); + final node111 = textNode(); + final node11 = textNode() + ..insert(node110) + ..insert(node111); + final node10 = textNode(); + final node1 = textNode() + ..insert(node10) + ..insert(node11); + final node0 = textNode(); + final node = textNode() + ..insert(node0) + ..insert(node1); + + expect(findLastTextNode(node)?.path, [1, 1, 1]); + expect(findLastTextNode(node0)?.path, [0]); + expect(findLastTextNode(node1)?.path, [1, 1, 1]); + expect(findLastTextNode(node10)?.path, [1, 0]); + expect(findLastTextNode(node11)?.path, [1, 1, 1]); + + expect(forwardNearestTextNode(node111)?.path, [1, 1, 0]); + expect(forwardNearestTextNode(node110)?.path, [1, 1]); + expect(forwardNearestTextNode(node11)?.path, [1, 0]); + expect(forwardNearestTextNode(node10)?.path, [1]); + expect(forwardNearestTextNode(node1)?.path, [0]); + expect(forwardNearestTextNode(node0)?.path, []); + }); } Future _deleteFirstImage(WidgetTester tester, bool isBackward) async {