Преглед изворни кода

Merge pull request #605 from LucasXu0/feat/flowy_editor

Support delete / insert / update / search in State Tree
Nathan.fooo пре 2 година
родитељ
комит
6bfc5c3fd4

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

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

+ 55 - 6
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -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)
-        : <String, Object>{};
+        ? Attributes.from(json['attributes'] as Map)
+        : 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(
-            Map<String, Object>.from(jnode),
+          (jChild) => Node.fromJson(
+            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() {

+ 43 - 2
frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart

@@ -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;
+  }
 }
 }

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

@@ -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');
   });
   });
 }
 }