mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #605 from LucasXu0/feat/flowy_editor
Support delete / insert / update / search in State Tree
This commit is contained in:
commit
6bfc5c3fd4
@ -14,6 +14,13 @@
|
|||||||
"tag": "*"
|
"tag": "*"
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"attributes": {
|
||||||
|
"text-type": "heading2",
|
||||||
|
"check": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
|
@ -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() {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user