mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge remote-tracking branch 'origin/feat/flowy_editor' into feat/flowy_editor_input_service
This commit is contained in:
commit
b913db2301
@ -34,4 +34,11 @@ class Position {
|
||||
|
||||
@override
|
||||
String toString() => 'path = $path, offset = $offset';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"path": path.toList(),
|
||||
"offset": offset,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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'], '**');
|
||||
|
@ -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()],
|
||||
}
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user