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 <lucas.xu@appflowy.io>
This commit is contained in:
Mohammad Zolfaghari 2024-08-12 11:16:45 +03:30 committed by GitHub
parent 1c638dd930
commit 14b60fb9b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 46 additions and 201 deletions

View File

@ -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();

View File

@ -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.

View File

@ -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';

View File

@ -1,188 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
final _listTypes = [
BulletedListBlockKeys.type,
TodoListBlockKeys.type,
NumberedListBlockKeys.type,
];
extension PasteNodes on EditorState {
Future<void> 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<void> pasteMultiLineNodes(List<Node> 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<Selection?> 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<Node> 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<Node> 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(),
},
);
}
}
}

View File

@ -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<bool> pasteHtml(String html) async {

View File

@ -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<bool> pasteInAppJson(String inAppJson) async {

View File

@ -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<void> pastePlainText(String plainText) async {

View File

@ -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"

View File

@ -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: