Browse Source

feat: implement find the forward nearest text node

Lucas.Xu 2 years ago
parent
commit
ab353551d1

+ 6 - 2
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<RichTextNodeWidget>
-    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
+    with
+        SelectableMixin,
+        DefaultSelectable,
+        BuiltInStyleMixin,
+        BuiltInTextWidgetMixin {
   @override
   GlobalKey? get iconKey => null;
 
@@ -59,7 +63,7 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
   }
 
   @override
-  Widget build(BuildContext context) {
+  Widget buildWithSingle(BuildContext context) {
     return Padding(
       padding: padding,
       child: FlowyRichText(

+ 52 - 0
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;
+}

+ 131 - 0
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<void> _deleteFirstImage(WidgetTester tester, bool isBackward) async {