Pārlūkot izejas kodu

Merge pull request #838 from AppFlowy-IO/feat/deep-copy-nodes

Feat: deep clone nodes
Vincent Chan 2 gadi atpakaļ
vecāks
revīzija
5b1c66ee1e

+ 28 - 0
frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart

@@ -163,6 +163,18 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     }
     }
     return parent!._path([index, ...previous]);
     return parent!._path([index, ...previous]);
   }
   }
+
+  Node deepClone() {
+    final newNode = Node(
+        type: type, children: LinkedList<Node>(), attributes: {...attributes});
+
+    for (final node in children) {
+      final newNode = node.deepClone();
+      newNode.parent = this;
+      newNode.children.add(newNode);
+    }
+    return newNode;
+  }
 }
 }
 
 
 class TextNode extends Node {
 class TextNode extends Node {
@@ -213,5 +225,21 @@ class TextNode extends Node {
         delta: delta ?? this.delta,
         delta: delta ?? this.delta,
       );
       );
 
 
+  @override
+  TextNode deepClone() {
+    final newNode = TextNode(
+        type: type,
+        children: LinkedList<Node>(),
+        delta: delta.slice(0),
+        attributes: {...attributes});
+
+    for (final node in children) {
+      final newNode = node.deepClone();
+      newNode.parent = this;
+      newNode.children.add(newNode);
+    }
+    return newNode;
+  }
+
   String toRawString() => _delta.toRawString();
   String toRawString() => _delta.toRawString();
 }
 }

+ 2 - 1
frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart

@@ -417,7 +417,8 @@ class Delta extends Iterable<TextOperation> {
 
 
           // Optimization if rest of other is just retain
           // Optimization if rest of other is just retain
           if (!otherIter.hasNext &&
           if (!otherIter.hasNext &&
-              delta._operations[delta._operations.length - 1] == newOp) {
+              delta._operations.isNotEmpty &&
+              delta._operations.last == newOp) {
             final rest = Delta(thisIter.rest());
             final rest = Delta(thisIter.rest());
             return (delta + rest)..chop();
             return (delta + rest)..chop();
           }
           }

+ 5 - 2
frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart

@@ -36,7 +36,7 @@ class TransactionBuilder {
   /// Insert a sequence of nodes at the position of path.
   /// Insert a sequence of nodes at the position of path.
   insertNodes(Path path, List<Node> nodes) {
   insertNodes(Path path, List<Node> nodes) {
     beforeSelection = state.cursorSelection;
     beforeSelection = state.cursorSelection;
-    add(InsertOperation(path, nodes));
+    add(InsertOperation(path, nodes.map((node) => node.deepClone()).toList()));
   }
   }
 
 
   /// Update the attributes of nodes.
   /// Update the attributes of nodes.
@@ -75,7 +75,7 @@ class TransactionBuilder {
       nodes.add(node);
       nodes.add(node);
     }
     }
 
 
-    add(DeleteOperation(path, nodes));
+    add(DeleteOperation(path, nodes.map((node) => node.deepClone()).toList()));
   }
   }
 
 
   textEdit(TextNode node, Delta Function() f) {
   textEdit(TextNode node, Delta Function() f) {
@@ -203,6 +203,9 @@ class TransactionBuilder {
     for (var i = 0; i < operations.length; i++) {
     for (var i = 0; i < operations.length; i++) {
       op = transformOperation(operations[i], op);
       op = transformOperation(operations[i], op);
     }
     }
+    if (op is TextEditOperation && op.delta.isEmpty) {
+      return;
+    }
     operations.add(op);
     operations.add(op);
   }
   }
 
 

+ 22 - 16
frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart

@@ -6,11 +6,10 @@ import 'package:flutter/services.dart';
 import 'package:rich_clipboard/rich_clipboard.dart';
 import 'package:rich_clipboard/rich_clipboard.dart';
 
 
 _handleCopy(EditorState editorState) async {
 _handleCopy(EditorState editorState) async {
-  var selection = editorState.cursorSelection;
+  final selection = editorState.cursorSelection?.normalize();
   if (selection == null || selection.isCollapsed) {
   if (selection == null || selection.isCollapsed) {
     return;
     return;
   }
   }
-  selection = selection.normalize();
   if (pathEquals(selection.start.path, selection.end.path)) {
   if (pathEquals(selection.start.path, selection.end.path)) {
     final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!;
     final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!;
     if (nodeAtPath.type == "text") {
     if (nodeAtPath.type == "text") {
@@ -43,11 +42,13 @@ _handleCopy(EditorState editorState) async {
 }
 }
 
 
 _pasteHTML(EditorState editorState, String html) {
 _pasteHTML(EditorState editorState, String html) {
-  final selection = editorState.cursorSelection;
+  final selection = editorState.cursorSelection?.normalize();
   if (selection == null) {
   if (selection == null) {
     return;
     return;
   }
   }
 
 
+  assert(selection.isCollapsed);
+
   final path = [...selection.end.path];
   final path = [...selection.end.path];
   if (path.isEmpty) {
   if (path.isEmpty) {
     return;
     return;
@@ -124,6 +125,20 @@ _pasteMultipleLinesInText(
 
 
 _handlePaste(EditorState editorState) async {
 _handlePaste(EditorState editorState) async {
   final data = await RichClipboard.getData();
   final data = await RichClipboard.getData();
+
+  if (editorState.cursorSelection?.isCollapsed ?? false) {
+    _pastRichClipboard(editorState, data);
+    return;
+  }
+
+  _deleteSelectedContent(editorState);
+
+  WidgetsBinding.instance.addPostFrameCallback((_) {
+    _pastRichClipboard(editorState, data);
+  });
+}
+
+_pastRichClipboard(EditorState editorState, RichClipboardData data) {
   if (data.html != null) {
   if (data.html != null) {
     _pasteHTML(editorState, data.html!);
     _pasteHTML(editorState, data.html!);
     return;
     return;
@@ -135,7 +150,7 @@ _handlePaste(EditorState editorState) async {
 }
 }
 
 
 _handlePastePlainText(EditorState editorState, String plainText) {
 _handlePastePlainText(EditorState editorState, String plainText) {
-  final selection = editorState.cursorSelection;
+  final selection = editorState.cursorSelection?.normalize();
   if (selection == null) {
   if (selection == null) {
     return;
     return;
   }
   }
@@ -208,22 +223,13 @@ _handlePastePlainText(EditorState editorState, String plainText) {
 /// 2. delete selected content
 /// 2. delete selected content
 _handleCut(EditorState editorState) {
 _handleCut(EditorState editorState) {
   debugPrint('cut');
   debugPrint('cut');
-  final selection = editorState.cursorSelection;
-  if (selection == null) {
-    return;
-  }
-
-  if (selection.isCollapsed) {
-    return;
-  }
-
   _handleCopy(editorState);
   _handleCopy(editorState);
   _deleteSelectedContent(editorState);
   _deleteSelectedContent(editorState);
 }
 }
 
 
 _deleteSelectedContent(EditorState editorState) {
 _deleteSelectedContent(EditorState editorState) {
-  final selection = editorState.cursorSelection;
-  if (selection == null) {
+  final selection = editorState.cursorSelection?.normalize();
+  if (selection == null || selection.isCollapsed) {
     return;
     return;
   }
   }
   final beginNode = editorState.document.nodeAtPath(selection.start.path)!;
   final beginNode = editorState.document.nodeAtPath(selection.start.path)!;
@@ -262,11 +268,11 @@ _deleteSelectedContent(EditorState editorState) {
 
 
         return delta;
         return delta;
       });
       });
-      tb.setAfterSelection(Selection.collapsed(selection.start));
     } else {
     } else {
       tb.deleteNode(item);
       tb.deleteNode(item);
     }
     }
   }
   }
+  tb.setAfterSelection(Selection.collapsed(selection.start));
   tb.commit();
   tb.commit();
 }
 }
 
 

+ 1 - 1
frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart

@@ -86,7 +86,7 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler =
       );
       );
       TransactionBuilder(editorState)
       TransactionBuilder(editorState)
         ..insertNode(
         ..insertNode(
-          textNode.path,
+          textNode.path.next,
           TextNode.empty(),
           TextNode.empty(),
         )
         )
         ..afterSelection = afterSelection
         ..afterSelection = afterSelection