mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
* 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:
parent
1c638dd930
commit
14b60fb9b0
@ -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();
|
||||
|
@ -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.
|
||||
|
@ -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';
|
||||
|
@ -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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user