From 14b60fb9b03d923839de85b055117db0cb11e469 Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Mon, 12 Aug 2024 11:16:45 +0330 Subject: [PATCH] fix: list paste bug #5846 (#5917) * fix: list paste bug #5846 selecting a single word on a bullet or number list and pasting it from the clipboard would clear the whole text of that node and replace it with clipboard text. resolves #5846 * chore: update editor version --------- Co-authored-by: Lucas.Xu --- .../document_copy_and_paste_test.dart | 38 ++++ .../copy_and_paste/custom_cut_command.dart | 3 +- .../copy_and_paste/custom_paste_command.dart | 3 +- .../editor_state_paste_node_extension.dart | 188 ------------------ .../copy_and_paste/paste_from_html.dart | 3 +- .../paste_from_in_app_json.dart | 3 +- .../copy_and_paste/paste_from_plain_text.dart | 3 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 9 files changed, 46 insertions(+), 201 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart index 9fe8d37070..457934dff4 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -165,6 +165,44 @@ void main() { }); }); + testWidgets('paste text on part of bullet list', (tester) async { + const plainText = 'test'; + + await tester.pasteContent( + plainText: plainText, + beforeTest: (editorState) async { + final transaction = editorState.transaction; + transaction.insertNodes( + [0], + [ + Node( + type: BulletedListBlockKeys.type, + attributes: { + 'delta': [ + {"insert": "bullet list"}, + ], + }, + ), + ], + ); + + // Set the selection to the second numbered list node (which has empty delta) + transaction.afterSelection = Selection( + start: Position(path: [0], offset: 7), + end: Position(path: [0], offset: 11), + ); + + await editorState.apply(transaction); + await tester.pumpAndSettle(); + }, + (editorState) { + final node = editorState.getNodeAtPath([0]); + expect(node?.delta?.toPlainText(), 'bullet test'); + expect(node?.type, BulletedListBlockKeys.type); + }, + ); + }); + testWidgets('paste image(png) from memory', (tester) async { final image = await rootBundle.load('assets/test/images/sample.png'); final bytes = image.buffer.asUint8List(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart index db83e8e5f6..a2f4442bd0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart @@ -1,6 +1,5 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide EditorCopyPaste; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; /// cut. diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart index 85b290863e..067d6b766c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart @@ -1,13 +1,12 @@ import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide Log, EditorCopyPaste; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart deleted file mode 100644 index 6cee6f1db7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; - -final _listTypes = [ - BulletedListBlockKeys.type, - TodoListBlockKeys.type, - NumberedListBlockKeys.type, -]; - -extension PasteNodes on EditorState { - Future pasteSingleLineNode(Node insertedNode) async { - final selection = await deleteSelectionIfNeeded(); - if (selection == null) { - return; - } - final node = getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - final transaction = this.transaction; - final insertedDelta = insertedNode.delta; - // if the node is empty and its type is paragprah, replace it with the inserted node. - if (delta.isEmpty && node.type == ParagraphBlockKeys.type) { - transaction.insertNode( - selection.end.path.next, - insertedNode, - ); - transaction.deleteNode(node); - final path = calculatePath(selection.end.path, [insertedNode]); - final offset = calculateLength([insertedNode]); - transaction.afterSelection = Selection.collapsed( - Position( - path: path, - offset: offset, - ), - ); - } else if (_listTypes.contains(node.type)) { - final convertedNode = insertedNode.copyWith(type: node.type); - final path = selection.start.path; - transaction - ..insertNode(path, convertedNode) - ..deleteNodesAtPath(path); - - // Set the afterSelection to the last child of the inserted node - final lastChildPath = calculatePath(path, [convertedNode]); - final lastChildOffset = calculateLength([convertedNode]); - transaction.afterSelection = Selection.collapsed( - Position(path: lastChildPath, offset: lastChildOffset), - ); - } else if (insertedDelta != null) { - // if the node is not empty, insert the delta from inserted node after the selection. - transaction.insertTextDelta(node, selection.endIndex, insertedDelta); - } - await apply(transaction); - } - - Future pasteMultiLineNodes(List nodes) async { - assert(nodes.length > 1); - - final selection = await deleteSelectionIfNeeded(); - if (selection == null) { - return; - } - final node = getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - final transaction = this.transaction; - - final lastNodeLength = calculateLength(nodes); - // merge the current selected node delta into the nodes. - if (delta.isNotEmpty) { - nodes.first.insertDelta( - delta.slice(0, selection.startIndex), - insertAfter: false, - ); - - nodes.last.insertDelta( - delta.slice(selection.endIndex), - ); - } - - if (delta.isEmpty && node.type != ParagraphBlockKeys.type) { - nodes[0] = nodes.first.copyWith( - type: node.type, - attributes: { - ...node.attributes, - ...nodes.first.attributes, - }, - ); - } - - for (final child in node.children) { - nodes.last.insert(child); - } - - transaction.insertNodes(selection.end.path, nodes); - - // delete the current node. - transaction.deleteNode(node); - - final path = calculatePath(selection.start.path, nodes); - transaction.afterSelection = Selection.collapsed( - Position( - path: path, - offset: lastNodeLength, - ), - ); - - await apply(transaction); - } - - // delete the selection if it's not collapsed. - Future deleteSelectionIfNeeded() async { - final selection = this.selection; - if (selection == null) { - return null; - } - - // delete the selection first. - if (!selection.isCollapsed) { - await deleteSelection(selection); - } - - // fetch selection again.selection = editorState.selection; - assert(this.selection?.isCollapsed == true); - return this.selection; - } - - Path calculatePath(Path start, List nodes) { - var path = start; - for (var i = 0; i < nodes.length; i++) { - path = path.next; - } - path = path.previous; - if (nodes.last.children.isNotEmpty) { - return [ - ...path, - ...calculatePath([0], nodes.last.children.toList()), - ]; - } - return path; - } - - int calculateLength(List nodes) { - if (nodes.last.children.isNotEmpty) { - return calculateLength(nodes.last.children.toList()); - } - return nodes.last.delta?.length ?? 0; - } -} - -extension on Node { - void insertDelta(Delta delta, {bool insertAfter = true}) { - assert(delta.every((element) => element is TextInsert)); - if (this.delta == null) { - updateAttributes({ - blockComponentDelta: delta.toJson(), - }); - } else if (insertAfter) { - updateAttributes( - { - blockComponentDelta: this - .delta! - .compose( - Delta() - ..retain(this.delta!.length) - ..addAll(delta), - ) - .toJson(), - }, - ); - } else { - updateAttributes( - { - blockComponentDelta: delta - .compose( - Delta() - ..retain(delta.length) - ..addAll(this.delta!), - ) - .toJson(), - }, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart index 2047e6dd47..87259d5981 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart @@ -1,5 +1,4 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide EditorCopyPaste; +import 'package:appflowy_editor/appflowy_editor.dart'; extension PasteFromHtml on EditorState { Future pasteHtml(String html) async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart index 8e425aeab6..00a2a6c2f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart @@ -1,8 +1,7 @@ import 'dart:convert'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide Log, EditorCopyPaste; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; extension PasteFromInAppJson on EditorState { Future pasteInAppJson(String inAppJson) async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart index 6100a0056d..b09c8c0dc4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart @@ -1,6 +1,5 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide EditorCopyPaste; +import 'package:appflowy_editor/appflowy_editor.dart'; extension PasteFromPlainText on EditorState { Future pastePlainText(String plainText) async { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 21b0791d8d..9b6b2102ef 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,8 +53,8 @@ packages: dependency: "direct main" description: path: "." - ref: dc10742 - resolved-ref: dc10742ba559e445e2ba1bd1b295cbf4758ccf3d + ref: "9d3e854" + resolved-ref: "9d3e854f11fd9d732535ce5f5b1c8f41517479a1" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "3.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0389146a42..896bb4c73d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -199,7 +199,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "dc10742" + ref: "9d3e854" appflowy_editor_plugins: git: