Skip to content

Commit

Permalink
feat: migrate copy paste command from AppFlowy (AppFlowy-IO#413)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasXu0 authored Aug 25, 2023
1 parent 687af49 commit 1922e56
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 192 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ CommandShortcutEventHandler _copyCommandHandler = (editorState) {
final document = Document.blank()..insert([0], nodes);
final html = documentToHTML(document);

AppFlowyClipboard.setData(
text: text.isEmpty ? null : text,
html: html.isEmpty ? null : html,
);
() async {
await AppFlowyClipboard.setData(
text: text.isEmpty ? null : text,
html: html.isEmpty ? null : html,
);
}();

return KeyEventResult.handled;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import 'package:appflowy_editor/appflowy_editor.dart';

extension EditorCopyPaste 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, replace it with the inserted node.
if (delta.isEmpty) {
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 (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),
insertAfter: true,
);
}

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) {
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(),
},
);
}
}
}
Loading

0 comments on commit 1922e56

Please sign in to comment.