Merge remote-tracking branch 'origin/feat/flowy_editor' into feat/flowy_editor_input_service

This commit is contained in:
Lucas.Xu 2022-08-02 15:15:48 +08:00
commit b913db2301
10 changed files with 241 additions and 84 deletions

View File

@ -34,4 +34,11 @@ class Position {
@override
String toString() => 'path = $path, offset = $offset';
Map<String, dynamic> toJson() {
return {
"path": path.toList(),
"offset": offset,
};
}
}

View File

@ -48,4 +48,11 @@ class Selection {
@override
String toString() => '[Selection] start = $start, end = $end';
Map<String, dynamic> toJson() {
return {
"start": start.toJson(),
"end": end.toJson(),
};
}
}

View File

@ -22,17 +22,21 @@ class StateTree {
return root.childAtPath(path);
}
bool insert(Path path, Node node) {
bool insert(Path path, List<Node> nodes) {
if (path.isEmpty) {
return false;
}
final insertedNode = root.childAtPath(
Node? insertedNode = root.childAtPath(
path.sublist(0, path.length - 1) + [path.last - 1],
);
if (insertedNode == null) {
return false;
}
insertedNode.insertAfter(node);
for (var i = 0; i < nodes.length; i++) {
final node = nodes[i];
insertedNode!.insertAfter(node);
insertedNode = node;
}
return true;
}
@ -48,13 +52,17 @@ class StateTree {
return false;
}
Node? delete(Path path) {
delete(Path path, [int length = 1]) {
if (path.isEmpty) {
return null;
}
final deletedNode = root.childAtPath(path);
deletedNode?.unlink();
return deletedNode;
var deletedNode = root.childAtPath(path);
while (deletedNode != null && length > 0) {
final next = deletedNode.next;
deletedNode.unlink();
length--;
deletedNode = next;
}
}
Attributes? update(Path path, Attributes attributes) {

View File

@ -94,11 +94,11 @@ class EditorState {
_applyOperation(Operation op) {
if (op is InsertOperation) {
document.insert(op.path, op.value);
document.insert(op.path, op.nodes);
} else if (op is UpdateOperation) {
document.update(op.path, op.attributes);
} else if (op is DeleteOperation) {
document.delete(op.path);
document.delete(op.path, op.nodes.length);
} else if (op is TextEditOperation) {
document.textEdit(op.path, op.delta);
}

View File

@ -2,22 +2,41 @@ import 'package:flowy_editor/document/attributes.dart';
import 'package:flowy_editor/flowy_editor.dart';
abstract class Operation {
factory Operation.fromJson(Map<String, dynamic> map) {
String t = map["type"] as String;
if (t == "insert-operation") {
return InsertOperation.fromJson(map);
} else if (t == "update-operation") {
return UpdateOperation.fromJson(map);
} else if (t == "delete-operation") {
return DeleteOperation.fromJson(map);
} else if (t == "text-edit-operation") {
return TextEditOperation.fromJson(map);
}
throw ArgumentError('unexpected type $t');
}
final Path path;
Operation({required this.path});
Operation(this.path);
Operation copyWithPath(Path path);
Operation invert();
Map<String, dynamic> toJson();
}
class InsertOperation extends Operation {
final Node value;
final List<Node> nodes;
InsertOperation({
required super.path,
required this.value,
});
factory InsertOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final value =
(map["nodes"] as List<dynamic>).map((n) => Node.fromJson(n)).toList();
return InsertOperation(path, value);
}
InsertOperation copyWith({Path? path, Node? value}) =>
InsertOperation(path: path ?? this.path, value: value ?? this.value);
InsertOperation(Path path, this.nodes) : super(path);
InsertOperation copyWith({Path? path, List<Node>? nodes}) =>
InsertOperation(path ?? this.path, nodes ?? this.nodes);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@ -25,28 +44,42 @@ class InsertOperation extends Operation {
@override
Operation invert() {
return DeleteOperation(
path: path,
removedValue: value,
path,
nodes,
);
}
@override
Map<String, dynamic> toJson() {
return {
"type": "insert-operation",
"path": path.toList(),
"nodes": nodes.map((n) => n.toJson()),
};
}
}
class UpdateOperation extends Operation {
final Attributes attributes;
final Attributes oldAttributes;
UpdateOperation({
required super.path,
required this.attributes,
required this.oldAttributes,
});
factory UpdateOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final attributes = map["attributes"] as Map<String, dynamic>;
final oldAttributes = map["oldAttributes"] as Map<String, dynamic>;
return UpdateOperation(path, attributes, oldAttributes);
}
UpdateOperation(
Path path,
this.attributes,
this.oldAttributes,
) : super(path);
UpdateOperation copyWith(
{Path? path, Attributes? attributes, Attributes? oldAttributes}) =>
UpdateOperation(
path: path ?? this.path,
attributes: attributes ?? this.attributes,
oldAttributes: oldAttributes ?? this.oldAttributes);
UpdateOperation(path ?? this.path, attributes ?? this.attributes,
oldAttributes ?? this.oldAttributes);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@ -54,33 +87,56 @@ class UpdateOperation extends Operation {
@override
Operation invert() {
return UpdateOperation(
path: path,
attributes: oldAttributes,
oldAttributes: attributes,
path,
oldAttributes,
attributes,
);
}
@override
Map<String, dynamic> toJson() {
return {
"type": "update-operation",
"path": path.toList(),
"attributes": {...attributes},
"oldAttributes": {...oldAttributes},
};
}
}
class DeleteOperation extends Operation {
final Node removedValue;
final List<Node> nodes;
DeleteOperation({
required super.path,
required this.removedValue,
});
factory DeleteOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final List<Node> nodes =
(map["nodes"] as List<dynamic>).map((e) => Node.fromJson(e)).toList();
return DeleteOperation(path, nodes);
}
DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation(
path: path ?? this.path, removedValue: removedValue ?? this.removedValue);
DeleteOperation(
Path path,
this.nodes,
) : super(path);
DeleteOperation copyWith({Path? path, List<Node>? nodes}) =>
DeleteOperation(path ?? this.path, nodes ?? this.nodes);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return InsertOperation(
path: path,
value: removedValue,
);
return InsertOperation(path, nodes);
}
@override
Map<String, dynamic> toJson() {
return {
"type": "delete-operation",
"path": path.toList(),
"nodes": nodes.map((n) => n.toJson()),
};
}
}
@ -88,24 +144,39 @@ class TextEditOperation extends Operation {
final Delta delta;
final Delta inverted;
TextEditOperation({
required super.path,
required this.delta,
required this.inverted,
});
factory TextEditOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final delta = Delta.fromJson(map["delta"]);
final invert = Delta.fromJson(map["invert"]);
return TextEditOperation(path, delta, invert);
}
TextEditOperation(
Path path,
this.delta,
this.inverted,
) : super(path);
TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) =>
TextEditOperation(
path: path ?? this.path,
delta: delta ?? this.delta,
inverted: inverted ?? this.inverted);
path ?? this.path, delta ?? this.delta, inverted ?? this.inverted);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return TextEditOperation(path: path, delta: inverted, inverted: delta);
return TextEditOperation(path, inverted, delta);
}
@override
Map<String, dynamic> toJson() {
return {
"type": "text-edit-operation",
"path": path.toList(),
"delta": delta.toJson(),
"invert": inverted.toJson(),
};
}
}

View File

@ -23,4 +23,17 @@ class Transaction {
this.beforeSelection,
this.afterSelection,
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> result = {
"operations": operations.map((e) => e.toJson()),
};
if (beforeSelection != null) {
result["beforeSelection"] = beforeSelection!.toJson();
}
if (afterSelection != null) {
result["afterSelection"] = afterSelection!.toJson();
}
return result;
}
}

View File

@ -29,32 +29,50 @@ class TransactionBuilder {
}
insertNode(Path path, Node node) {
beforeSelection = state.service.selectionService.currentSelection;
add(InsertOperation(path: path, value: node));
insertNodes(path, [node]);
// FIXME: Not exactly correct, needs to be customized.
afterSelection = Selection.collapsed(
Position(path: path, offset: 0),
);
}
insertNodes(Path path, List<Node> nodes) {
beforeSelection = state.service.selectionService.currentSelection;
add(InsertOperation(path, nodes));
}
updateNode(Node node, Attributes attributes) {
beforeSelection = state.cursorSelection;
add(UpdateOperation(
path: node.path,
attributes: Attributes.from(node.attributes)..addAll(attributes),
oldAttributes: node.attributes,
node.path,
Attributes.from(node.attributes)..addAll(attributes),
node.attributes,
));
}
deleteNode(Node node) {
beforeSelection = state.cursorSelection;
add(DeleteOperation(path: node.path, removedValue: node));
deleteNodesAtPath(node.path);
}
deleteNodes(List<Node> nodes) {
nodes.forEach(deleteNode);
}
deleteNodesAtPath(Path path, [int length = 1]) {
if (path.isEmpty) {
return;
}
final nodes = <Node>[];
final prefix = path.sublist(0, path.length - 1);
final last = path.last;
for (var i = 0; i < length; i++) {
final node = state.document.nodeAtPath(prefix + [last + i])!;
nodes.add(node);
}
add(DeleteOperation(path, nodes));
}
textEdit(TextNode node, Delta Function() f) {
beforeSelection = state.service.selectionService.currentSelection;
final path = node.path;
@ -63,7 +81,7 @@ class TransactionBuilder {
final inverted = delta.invert(node.delta);
add(TextEditOperation(path: path, delta: delta, inverted: inverted));
add(TextEditOperation(path, delta, inverted));
}
mergeText(TextNode firstNode, TextNode secondNode,
@ -123,9 +141,9 @@ class TransactionBuilder {
last is TextEditOperation &&
pathEquals(op.path, last.path)) {
final newOp = TextEditOperation(
path: op.path,
delta: last.delta.compose(op.delta),
inverted: op.inverted.compose(last.inverted),
op.path,
last.delta.compose(op.delta),
op.inverted.compose(last.inverted),
);
operations[operations.length - 1] = newOp;
return;

View File

@ -37,7 +37,7 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) {
children: LinkedList(),
delta: Delta([TextInsert(' ')]),
attributes:
needCopyAttributes ? {StyleKey.subtype: textNode.subtype} : null,
needCopyAttributes ? {StyleKey.subtype: textNode.subtype} : {},
),
)
..commit();

View File

@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:math';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/state_tree.dart';
@ -50,7 +49,7 @@ void main() {
final insertNode = Node.fromJson({
'type': 'text',
});
bool result = stateTree.insert([1, 1], insertNode);
bool result = stateTree.insert([1, 1], [insertNode]);
expect(result, true);
expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true);
});
@ -59,9 +58,7 @@ void main() {
final String response = await rootBundle.loadString('assets/document.json');
final data = Map<String, Object>.from(json.decode(response));
final stateTree = StateTree.fromJson(data);
final deletedNode = stateTree.delete([1, 1]);
expect(deletedNode != null, true);
expect(deletedNode!.attributes['text-type'], 'checkbox');
stateTree.delete([1, 1], 1);
final node = stateTree.nodeAtPath([1, 1]);
expect(node != null, true);
expect(node!.attributes['tag'], '**');

View File

@ -27,26 +27,18 @@ void main() {
group('transform operation', () {
test('insert + insert', () {
final t = transformOperation(
InsertOperation(path: [
0,
1
], value: Node(type: "node", attributes: {}, children: LinkedList())),
InsertOperation(
path: [0, 1],
value:
Node(type: "node", attributes: {}, children: LinkedList())));
InsertOperation([0, 1],
[Node(type: "node", attributes: {}, children: LinkedList())]),
InsertOperation([0, 1],
[Node(type: "node", attributes: {}, children: LinkedList())]));
expect(t.path, [0, 2]);
});
test('delete + delete', () {
final t = transformOperation(
DeleteOperation(
path: [0, 1],
removedValue:
Node(type: "node", attributes: {}, children: LinkedList())),
DeleteOperation(
path: [0, 2],
removedValue:
Node(type: "node", attributes: {}, children: LinkedList())));
DeleteOperation([0, 1],
[Node(type: "node", attributes: {}, children: LinkedList())]),
DeleteOperation([0, 2],
[Node(type: "node", attributes: {}, children: LinkedList())]));
expect(t.path, [0, 1]);
});
});
@ -78,4 +70,48 @@ void main() {
expect(transaction.operations[1].path, [0]);
expect(transaction.operations[2].path, [0]);
});
group("toJson", () {
test("insert", () {
final root = Node(type: "root", attributes: {}, children: LinkedList());
final state = EditorState(document: StateTree(root: root));
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final tb = TransactionBuilder(state);
tb.insertNode([0], item1);
final transaction = tb.finish();
expect(transaction.toJson(), {
"operations": [
{
"type": "insert-operation",
"path": [0],
"nodes": [item1.toJson()],
}
],
});
});
test("delete", () {
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final root = Node(
type: "root",
attributes: {},
children: LinkedList()
..addAll([
item1,
]));
final state = EditorState(document: StateTree(root: root));
final tb = TransactionBuilder(state);
tb.deleteNode(item1);
final transaction = tb.finish();
expect(transaction.toJson(), {
"operations": [
{
"type": "delete-operation",
"path": [0],
"nodes": [item1.toJson()],
}
],
});
});
});
}