Merge pull request #605 from LucasXu0/feat/flowy_editor

Support delete / insert / update / search in State Tree
This commit is contained in:
Nathan.fooo 2022-07-11 22:19:25 +08:00 committed by GitHub
commit 6bfc5c3fd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 9 deletions

View File

@ -14,6 +14,13 @@
"tag": "*" "tag": "*"
}, },
"children": [ "children": [
{
"type": "text",
"attributes": {
"text-type": "heading2",
"check": true
}
},
{ {
"type": "text", "type": "text",
"attributes": { "attributes": {

View File

@ -1,10 +1,13 @@
import 'dart:collection'; import 'dart:collection';
import 'package:flowy_editor/document/path.dart';
typedef Attributes = Map<String, Object>;
class Node extends LinkedListEntry<Node> { class Node extends LinkedListEntry<Node> {
Node? parent; Node? parent;
final String type; final String type;
final LinkedList<Node> children; final LinkedList<Node> children;
final Map<String, Object> attributes; final Attributes attributes;
Node({ Node({
required this.type, required this.type,
@ -19,25 +22,71 @@ class Node extends LinkedListEntry<Node> {
final jType = json['type'] as String; final jType = json['type'] as String;
final jChildren = json['children'] as List?; final jChildren = json['children'] as List?;
final jAttributes = json['attributes'] != null final jAttributes = json['attributes'] != null
? Map<String, Object>.from(json['attributes'] as Map) ? Attributes.from(json['attributes'] as Map)
: <String, Object>{}; : Attributes.from({});
final LinkedList<Node> children = LinkedList(); final LinkedList<Node> children = LinkedList();
if (jChildren != null) { if (jChildren != null) {
children.addAll( children.addAll(
jChildren.map( jChildren.map(
(jnode) => Node.fromJson( (jChild) => Node.fromJson(
Map<String, Object>.from(jnode), Map<String, Object>.from(jChild),
), ),
), ),
); );
} }
return Node( final node = Node(
type: jType, type: jType,
children: children, children: children,
attributes: jAttributes, attributes: jAttributes,
); );
for (final child in children) {
child.parent = node;
}
return node;
}
void updateAttributes(Attributes attributes) {
for (final attribute in attributes.entries) {
this.attributes[attribute.key] = attribute.value;
}
}
Node? childAtIndex(int index) {
if (children.length <= index) {
return null;
}
return children.elementAt(index);
}
Node? childAtPath(Path path) {
if (path.isEmpty) {
return this;
}
return childAtIndex(path.first)?.childAtPath(path.sublist(1));
}
@override
void insertAfter(Node entry) {
entry.parent = parent;
super.insertAfter(entry);
}
@override
void insertBefore(Node entry) {
entry.parent = parent;
super.insertBefore(entry);
}
@override
void unlink() {
parent = null;
super.unlink();
} }
Map<String, Object> toJson() { Map<String, Object> toJson() {

View File

@ -1,15 +1,56 @@
import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/path.dart';
class StateTree { class StateTree {
Node root; final Node root;
StateTree({required this.root}); StateTree({required this.root});
factory StateTree.fromJson(Map<String, Object> json) { factory StateTree.fromJson(Attributes json) {
assert(json['document'] is Map); assert(json['document'] is Map);
final document = Map<String, Object>.from(json['document'] as Map); final document = Map<String, Object>.from(json['document'] as Map);
final root = Node.fromJson(document); final root = Node.fromJson(document);
return StateTree(root: root); return StateTree(root: root);
} }
Node? nodeAtPath(Path path) {
return root.childAtPath(path);
}
bool insert(Path path, Node node) {
if (path.isEmpty) {
return false;
}
final insertedNode = root.childAtPath(
path.sublist(0, path.length - 1) + [path.last - 1],
);
if (insertedNode == null) {
return false;
}
insertedNode.insertAfter(node);
return true;
}
Node? delete(Path path) {
if (path.isEmpty) {
return null;
}
final deletedNode = root.childAtPath(path);
deletedNode?.unlink();
return deletedNode;
}
Attributes? update(Path path, Attributes attributes) {
if (path.isEmpty) {
return null;
}
final updatedNode = root.childAtPath(path);
if (updatedNode == null) {
return null;
}
final previousAttributes = {...updatedNode.attributes};
updatedNode.updateAttributes(attributes);
return previousAttributes;
}
} }

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/state_tree.dart'; import 'package:flowy_editor/document/state_tree.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -13,6 +14,51 @@ void main() {
final stateTree = StateTree.fromJson(data); final stateTree = StateTree.fromJson(data);
expect(stateTree.root.type, 'root'); expect(stateTree.root.type, 'root');
expect(stateTree.root.toJson(), data['document']); expect(stateTree.root.toJson(), data['document']);
expect(stateTree.root.children.last.type, 'video'); });
test('search node in state tree', () async {
final String response = await rootBundle.loadString('assets/document.json');
final data = Map<String, Object>.from(json.decode(response));
final stateTree = StateTree.fromJson(data);
final checkBoxNode = stateTree.root.childAtPath([1, 0]);
expect(checkBoxNode != null, true);
final textType = checkBoxNode!.attributes['text-type'];
expect(textType != null, true);
});
test('insert node in state tree', () async {
final String response = await rootBundle.loadString('assets/document.json');
final data = Map<String, Object>.from(json.decode(response));
final stateTree = StateTree.fromJson(data);
final insertNode = Node.fromJson({
'type': 'text',
});
bool result = stateTree.insert([1, 1], insertNode);
expect(result, true);
expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true);
});
test('delete node in state tree', () async {
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'], 'check-box');
final node = stateTree.nodeAtPath([1, 1]);
expect(node != null, true);
expect(node!.attributes['tag'], '**');
});
test('update node in state tree', () async {
final String response = await rootBundle.loadString('assets/document.json');
final data = Map<String, Object>.from(json.decode(response));
final stateTree = StateTree.fromJson(data);
final attributes = stateTree.update([1, 1], {'text-type': 'heading1'});
expect(attributes != null, true);
expect(attributes!['text-type'], 'check-box');
final updatedNode = stateTree.nodeAtPath([1, 1]);
expect(updatedNode != null, true);
expect(updatedNode!.attributes['text-type'], 'heading1');
}); });
} }