浏览代码

feat: insert / delelte / update / search node in state tree

Lucas.Xu 2 年之前
父节点
当前提交
59d92a8ced

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/assets/document.json

@@ -40,6 +40,7 @@
         "attributes": {
           "url": "x.mp4"
         }
+        
       }
     ]
   }

+ 31 - 2
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -1,5 +1,4 @@
 import 'dart:collection';
-
 import 'package:flowy_editor/document/path.dart';
 
 class Node extends LinkedListEntry<Node> {
@@ -35,11 +34,23 @@ class Node extends LinkedListEntry<Node> {
       );
     }
 
-    return Node(
+    final node = Node(
       type: jType,
       children: children,
       attributes: jAttributes,
     );
+
+    for (final child in children) {
+      child.parent = node;
+    }
+
+    return node;
+  }
+
+  void updateAttributes(Map<String, Object> attributes) {
+    for (final attribute in attributes.entries) {
+      this.attributes[attribute.key] = attribute.value;
+    }
   }
 
   Node? childAtIndex(int index) {
@@ -58,6 +69,24 @@ class Node extends LinkedListEntry<Node> {
     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() {
     var map = <String, Object>{
       'type': type,

+ 41 - 5
frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart

@@ -1,7 +1,8 @@
 import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/path.dart';
 
 class StateTree {
-  Node root;
+  final Node root;
 
   StateTree({required this.root});
 
@@ -13,8 +14,43 @@ class StateTree {
     return StateTree(root: root);
   }
 
-  // bool insert(Path path, Node node) {
-  //   final insertedNode = root
-  //   return false;
-  // }
+  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;
+  }
+
+  Map<String, Object>? update(Path path, Map<String, Object> 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;
+  }
 }

+ 39 - 1
frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart

@@ -1,5 +1,6 @@
 import 'dart:convert';
 
+import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/state_tree.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -13,11 +14,48 @@ void main() {
     final stateTree = StateTree.fromJson(data);
     expect(stateTree.root.type, 'root');
     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, 0]);
+    expect(deletedNode != null, true);
+    expect(deletedNode!.attributes['text-type'], 'check-box');
+  });
+
+  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, 0], {'text-type': 'heading1'});
+    expect(attributes != null, true);
+    expect(attributes!['text-type'], 'check-box');
+    final updatedNode = stateTree.nodeAtPath([1, 0]);
+    expect(updatedNode != null, true);
+    expect(updatedNode!.attributes['text-type'], 'heading1');
+  });
 }