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 {
|
testWidgets('paste image(png) from memory', (tester) async {
|
||||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||||
final bytes = image.buffer.asUint8List();
|
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/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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// cut.
|
/// cut.
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
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/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_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_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_in_app_json.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.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/startup/startup.dart';
|
||||||
import 'package:appflowy_backend/log.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:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide EditorCopyPaste;
|
|
||||||
|
|
||||||
extension PasteFromHtml on EditorState {
|
extension PasteFromHtml on EditorState {
|
||||||
Future<bool> pasteHtml(String html) async {
|
Future<bool> pasteHtml(String html) async {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import 'dart:convert';
|
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_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 {
|
extension PasteFromInAppJson on EditorState {
|
||||||
Future<bool> pasteInAppJson(String inAppJson) async {
|
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/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 {
|
extension PasteFromPlainText on EditorState {
|
||||||
Future<void> pastePlainText(String plainText) async {
|
Future<void> pastePlainText(String plainText) async {
|
||||||
|
@ -53,8 +53,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: dc10742
|
ref: "9d3e854"
|
||||||
resolved-ref: dc10742ba559e445e2ba1bd1b295cbf4758ccf3d
|
resolved-ref: "9d3e854f11fd9d732535ce5f5b1c8f41517479a1"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
|
@ -199,7 +199,7 @@ dependency_overrides:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: "dc10742"
|
ref: "9d3e854"
|
||||||
|
|
||||||
appflowy_editor_plugins:
|
appflowy_editor_plugins:
|
||||||
git:
|
git:
|
||||||
|
Loading…
Reference in New Issue
Block a user