Browse Source

feat: 1. move render plugins to editor state 2. support get node's path 3. format code

Lucas.Xu 2 years ago
parent
commit
5ad7845189

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

@@ -20,7 +20,7 @@
             "subtype": "with-checkbox",
             "subtype": "with-checkbox",
             "text-type": "heading1",
             "text-type": "heading1",
             "font-size": 30,
             "font-size": 30,
-            "content": "bbbbbbbbbbbbbbbbbbbbbbb",
+            "content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
             "checkbox": false
             "checkbox": false
           }
           }
         },
         },

+ 5 - 6
frontend/app_flowy/packages/flowy_editor/example/lib/main.dart

@@ -92,13 +92,12 @@ class _MyHomePageState extends State<MyHomePage> {
             );
             );
           } else {
           } else {
             final data = Map<String, Object>.from(json.decode(snapshot.data!));
             final data = Map<String, Object>.from(json.decode(snapshot.data!));
-            final stateTree = StateTree.fromJson(data);
-            return renderPlugins.buildWidget(
-              context: NodeWidgetContext(
-                buildContext: context,
-                node: stateTree.root,
-              ),
+            final document = StateTree.fromJson(data);
+            final editorState = EditorState(
+              document: document,
+              renderPlugins: renderPlugins,
             );
             );
+            return editorState.build(context);
           }
           }
         },
         },
       ),
       ),

+ 32 - 16
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart

@@ -5,27 +5,39 @@ import 'package:provider/provider.dart';
 class ImageNodeBuilder extends NodeWidgetBuilder {
 class ImageNodeBuilder extends NodeWidgetBuilder {
   ImageNodeBuilder.create({
   ImageNodeBuilder.create({
     required super.node,
     required super.node,
-    required super.renderPlugins,
+    required super.editorState,
   }) : super.create();
   }) : super.create();
 
 
   String get src => node.attributes['image_src'] as String;
   String get src => node.attributes['image_src'] as String;
 
 
   @override
   @override
   Widget build(BuildContext buildContext) {
   Widget build(BuildContext buildContext) {
-    Future.delayed(const Duration(seconds: 5), () {
-      node.updateAttributes({
-        'image_src':
-            "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400"
-      });
-    });
-    return ChangeNotifierProvider.value(
-      value: node,
-      builder: (context, child) {
-        return Consumer<Node>(
-          builder: (context, value, child) {
-            return _build(context);
-          },
-        );
+    // Future.delayed(const Duration(seconds: 5), () {
+    //   node.updateAttributes({
+    //     'image_src':
+    //         "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400"
+    //   });
+    // });
+    return GestureDetector(
+      child: ChangeNotifierProvider.value(
+        value: node,
+        builder: (context, child) {
+          return Consumer<Node>(
+            builder: (context, value, child) {
+              return _build(context);
+            },
+          );
+        },
+      ),
+      onTap: () {
+        const newImageSrc =
+            "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400";
+        final newAttribute = Attributes.from(node.attributes)
+          ..update(
+            'image_src',
+            (value) => newImageSrc,
+          );
+        editorState.update(node, newAttribute);
       },
       },
     );
     );
   }
   }
@@ -39,7 +51,11 @@ class ImageNodeBuilder extends NodeWidgetBuilder {
         children: node.children
         children: node.children
             .map(
             .map(
               (e) => renderPlugins.buildWidget(
               (e) => renderPlugins.buildWidget(
-                context: NodeWidgetContext(buildContext: buildContext, node: e),
+                context: NodeWidgetContext(
+                  buildContext: buildContext,
+                  node: e,
+                  editorState: editorState,
+                ),
               ),
               ),
             )
             )
             .toList(),
             .toList(),

+ 6 - 2
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart

@@ -4,7 +4,7 @@ import 'package:flowy_editor/flowy_editor.dart';
 class TextNodeBuilder extends NodeWidgetBuilder {
 class TextNodeBuilder extends NodeWidgetBuilder {
   TextNodeBuilder.create({
   TextNodeBuilder.create({
     required super.node,
     required super.node,
-    required super.renderPlugins,
+    required super.editorState,
   }) : super.create();
   }) : super.create();
 
 
   String get content => node.attributes['content'] as String;
   String get content => node.attributes['content'] as String;
@@ -25,7 +25,11 @@ class TextNodeBuilder extends NodeWidgetBuilder {
         children: node.children
         children: node.children
             .map(
             .map(
               (e) => renderPlugins.buildWidget(
               (e) => renderPlugins.buildWidget(
-                context: NodeWidgetContext(buildContext: buildContext, node: e),
+                context: NodeWidgetContext(
+                  buildContext: buildContext,
+                  node: e,
+                  editorState: editorState,
+                ),
               ),
               ),
             )
             )
             .toList(),
             .toList(),

+ 7 - 2
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
 class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
 class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
   TextWithCheckBoxNodeBuilder.create({
   TextWithCheckBoxNodeBuilder.create({
     required super.node,
     required super.node,
-    required super.renderPlugins,
+    required super.editorState,
   }) : super.create();
   }) : super.create();
 
 
   // TODO: check the type
   // TODO: check the type
@@ -13,11 +13,16 @@ class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
   @override
   @override
   Widget build(BuildContext buildContext) {
   Widget build(BuildContext buildContext) {
     return Row(
     return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
       children: [
       children: [
         Checkbox(value: isCompleted, onChanged: (value) {}),
         Checkbox(value: isCompleted, onChanged: (value) {}),
         Expanded(
         Expanded(
           child: renderPlugins.buildWidget(
           child: renderPlugins.buildWidget(
-            context: NodeWidgetContext(buildContext: buildContext, node: node),
+            context: NodeWidgetContext(
+              buildContext: buildContext,
+              node: node,
+              editorState: editorState,
+            ),
             withSubtype: false,
             withSubtype: false,
           ),
           ),
         )
         )

+ 21 - 0
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -84,6 +84,27 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     return childAtIndex(path.first)?.childAtPath(path.sublist(1));
     return childAtIndex(path.first)?.childAtPath(path.sublist(1));
   }
   }
 
 
+  Node root() {
+    if (parent != null) {
+      return parent!.root();
+    }
+    return this;
+  }
+
+  Path path([Path previous = const []]) {
+    if (parent == null) {
+      return previous;
+    }
+    var index = 0;
+    for (var child in parent!.children) {
+      if (child == this) {
+        break;
+      }
+      index += 1;
+    }
+    return parent!.path([index, ...previous]);
+  }
+
   @override
   @override
   void insertAfter(Node entry) {
   void insertAfter(Node entry) {
     entry.parent = parent;
     entry.parent = parent;

+ 9 - 1
frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart

@@ -4,7 +4,9 @@ import 'package:flowy_editor/document/path.dart';
 class StateTree {
 class StateTree {
   final Node root;
   final Node root;
 
 
-  StateTree({required this.root});
+  StateTree({
+    required this.root,
+  });
 
 
   factory StateTree.fromJson(Attributes json) {
   factory StateTree.fromJson(Attributes json) {
     assert(json['document'] is Map);
     assert(json['document'] is Map);
@@ -14,6 +16,12 @@ class StateTree {
     return StateTree(root: root);
     return StateTree(root: root);
   }
   }
 
 
+  // Path pathForNode(Node node) {
+  //   var nodeRoot = node.root();
+  //   assert(nodeRoot == root, "Every node's root must be same as root");
+
+  // }
+
   Node? nodeAtPath(Path path) {
   Node? nodeAtPath(Path path) {
     return root.childAtPath(path);
     return root.childAtPath(path);
   }
   }

+ 16 - 5
frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart

@@ -24,7 +24,10 @@ class TextOperation {
 
 
 int _hashAttributes(Attributes attributes) {
 int _hashAttributes(Attributes attributes) {
   return Object.hashAllUnordered(
   return Object.hashAllUnordered(
-      attributes.entries.map((e) => Object.hash(e.key, e.value)));
+    attributes.entries.map(
+      (e) => Object.hash(e.key, e.value),
+    ),
+  );
 }
 }
 
 
 class TextInsert extends TextOperation {
 class TextInsert extends TextOperation {
@@ -145,7 +148,8 @@ class _OpIterator {
   int _index = 0;
   int _index = 0;
   int _offset = 0;
   int _offset = 0;
 
 
-  _OpIterator(List<TextOperation> operations) : _operations = UnmodifiableListView(operations);
+  _OpIterator(List<TextOperation> operations)
+      : _operations = UnmodifiableListView(operations);
 
 
   bool get hasNext {
   bool get hasNext {
     return peekLength() < _maxInt;
     return peekLength() < _maxInt;
@@ -186,16 +190,23 @@ class _OpIterator {
       _offset += length;
       _offset += length;
     }
     }
     if (nextOp is TextDelete) {
     if (nextOp is TextDelete) {
-      return TextDelete(length: length);
+      return TextDelete(
+        length: length,
+      );
     }
     }
 
 
     if (nextOp is TextRetain) {
     if (nextOp is TextRetain) {
-      return TextRetain(length: length, attributes: nextOp.attributes);
+      return TextRetain(
+        length: length,
+        attributes: nextOp.attributes,
+      );
     }
     }
 
 
     if (nextOp is TextInsert) {
     if (nextOp is TextInsert) {
       return TextInsert(
       return TextInsert(
-          nextOp.content.substring(offset, offset + length), nextOp.attributes);
+        nextOp.content.substring(offset, offset + length),
+        nextOp.attributes,
+      );
     }
     }
 
 
     return TextRetain(length: _maxInt);
     return TextRetain(length: _maxInt);

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

@@ -1,13 +1,13 @@
-
 import './text_delta.dart';
 import './text_delta.dart';
 import './node.dart';
 import './node.dart';
 
 
 class TextNode extends Node {
 class TextNode extends Node {
   final Delta delta;
   final Delta delta;
 
 
-  TextNode(
-      {required super.type,
-      required super.children,
-      required super.attributes,
-      required this.delta});
+  TextNode({
+    required super.type,
+    required super.children,
+    required super.attributes,
+    required this.delta,
+  });
 }
 }

+ 29 - 1
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -1,24 +1,52 @@
+import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/operation/operation.dart';
 import 'package:flowy_editor/operation/operation.dart';
+import 'package:flutter/material.dart';
 
 
 import './document/state_tree.dart';
 import './document/state_tree.dart';
 import './document/selection.dart';
 import './document/selection.dart';
 import './operation/operation.dart';
 import './operation/operation.dart';
 import './operation/transaction.dart';
 import './operation/transaction.dart';
+import './render/render_plugins.dart';
 
 
 class EditorState {
 class EditorState {
   final StateTree document;
   final StateTree document;
+  final RenderPlugins renderPlugins;
   Selection? cursorSelection;
   Selection? cursorSelection;
 
 
   EditorState({
   EditorState({
     required this.document,
     required this.document,
+    required this.renderPlugins,
   });
   });
 
 
-  apply(Transaction transaction) {
+  /// TODO: move to a better place.
+  Widget build(BuildContext context) {
+    return renderPlugins.buildWidget(
+      context: NodeWidgetContext(
+        buildContext: context,
+        node: document.root,
+        editorState: this,
+      ),
+    );
+  }
+
+  void apply(Transaction transaction) {
     for (final op in transaction.operations) {
     for (final op in transaction.operations) {
       _applyOperation(op);
       _applyOperation(op);
     }
     }
   }
   }
 
 
+  // TODO: move to a better place.
+  void update(
+    Node node,
+    Attributes attributes,
+  ) {
+    _applyOperation(UpdateOperation(
+      path: node.path(),
+      attributes: attributes,
+      oldAttributes: node.attributes,
+    ));
+  }
+
   _applyOperation(Operation op) {
   _applyOperation(Operation op) {
     if (op is InsertOperation) {
     if (op is InsertOperation) {
       document.insert(op.path, op.value);
       document.insert(op.path, op.value);

+ 3 - 0
frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart

@@ -5,3 +5,6 @@ export 'package:flowy_editor/document/node.dart';
 export 'package:flowy_editor/document/path.dart';
 export 'package:flowy_editor/document/path.dart';
 export 'package:flowy_editor/render/render_plugins.dart';
 export 'package:flowy_editor/render/render_plugins.dart';
 export 'package:flowy_editor/render/node_widget_builder.dart';
 export 'package:flowy_editor/render/node_widget_builder.dart';
+export 'package:flowy_editor/operation/transaction.dart';
+export 'package:flowy_editor/operation/operation.dart';
+export 'package:flowy_editor/editor_state.dart';

+ 13 - 8
frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart

@@ -2,9 +2,7 @@ import 'package:flowy_editor/document/path.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/node.dart';
 
 
 abstract class Operation {
 abstract class Operation {
-
   Operation invert();
   Operation invert();
-
 }
 }
 
 
 class InsertOperation extends Operation {
 class InsertOperation extends Operation {
@@ -18,9 +16,11 @@ class InsertOperation extends Operation {
 
 
   @override
   @override
   Operation invert() {
   Operation invert() {
-    return DeleteOperation(path: path, removedValue: value);
+    return DeleteOperation(
+      path: path,
+      removedValue: value,
+    );
   }
   }
-
 }
 }
 
 
 class UpdateOperation extends Operation {
 class UpdateOperation extends Operation {
@@ -36,9 +36,12 @@ class UpdateOperation extends Operation {
 
 
   @override
   @override
   Operation invert() {
   Operation invert() {
-    return UpdateOperation(path: path, attributes: oldAttributes, oldAttributes: attributes);
+    return UpdateOperation(
+      path: path,
+      attributes: oldAttributes,
+      oldAttributes: attributes,
+    );
   }
   }
-
 }
 }
 
 
 class DeleteOperation extends Operation {
 class DeleteOperation extends Operation {
@@ -52,7 +55,9 @@ class DeleteOperation extends Operation {
 
 
   @override
   @override
   Operation invert() {
   Operation invert() {
-    return InsertOperation(path: path, value: removedValue);
+    return InsertOperation(
+      path: path,
+      value: removedValue,
+    );
   }
   }
-
 }
 }

+ 2 - 2
frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart

@@ -1,6 +1,6 @@
 import './operation.dart';
 import './operation.dart';
 
 
 class Transaction {
 class Transaction {
-  final List<Operation> operations = [];
-
+  final List<Operation> operations;
+  Transaction([this.operations = const []]);
 }
 }

+ 11 - 6
frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart

@@ -1,17 +1,22 @@
+import 'package:flowy_editor/editor_state.dart';
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/render/render_plugins.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
-import '../document/node.dart';
-import '../render/render_plugins.dart';
-
 class NodeWidgetBuilder<T extends Node> {
 class NodeWidgetBuilder<T extends Node> {
+  final EditorState editorState;
   final T node;
   final T node;
-  final RenderPlugins renderPlugins;
 
 
-  NodeWidgetBuilder.create({required this.node, required this.renderPlugins});
+  RenderPlugins get renderPlugins => editorState.renderPlugins;
 
 
-  Widget call(BuildContext buildContext) => build(buildContext);
+  NodeWidgetBuilder.create({
+    required this.editorState,
+    required this.node,
+  });
 
 
   /// Render the current [Node]
   /// Render the current [Node]
   /// and the layout style of [Node.Children].
   /// and the layout style of [Node.Children].
   Widget build(BuildContext buildContext) => throw UnimplementedError();
   Widget build(BuildContext buildContext) => throw UnimplementedError();
+
+  Widget call(BuildContext buildContext) => build(buildContext);
 }
 }

+ 16 - 7
frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart

@@ -1,17 +1,24 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import '../document/node.dart';
 import '../document/node.dart';
-import 'node_widget_builder.dart';
+import './node_widget_builder.dart';
+import 'package:flowy_editor/editor_state.dart';
 
 
 class NodeWidgetContext {
 class NodeWidgetContext {
-  BuildContext buildContext;
-  Node node;
-  NodeWidgetContext({required this.buildContext, required this.node});
+  final BuildContext buildContext;
+  final Node node;
+  final EditorState editorState;
+
+  NodeWidgetContext({
+    required this.buildContext,
+    required this.node,
+    required this.editorState,
+  });
 }
 }
 
 
 typedef NodeWidgetBuilderF<T extends Node, A extends NodeWidgetBuilder> = A
 typedef NodeWidgetBuilderF<T extends Node, A extends NodeWidgetBuilder> = A
     Function({
     Function({
   required T node,
   required T node,
-  required RenderPlugins renderPlugins,
+  required EditorState editorState,
 });
 });
 
 
 // unused
 // unused
@@ -56,8 +63,10 @@ class RenderPlugins {
       name += '/${node.subtype}';
       name += '/${node.subtype}';
     }
     }
     final nodeWidgetBuilder = _nodeWidgetBuilder(name);
     final nodeWidgetBuilder = _nodeWidgetBuilder(name);
-    return nodeWidgetBuilder(node: context.node, renderPlugins: this)(
-        context.buildContext);
+    return nodeWidgetBuilder(
+      node: context.node,
+      editorState: context.editorState,
+    )(context.buildContext);
   }
   }
 
 
   NodeWidgetBuilderF _nodeWidgetBuilder(String name) {
   NodeWidgetBuilderF _nodeWidgetBuilder(String name) {

+ 1 - 3
frontend/app_flowy/packages/flowy_editor/test/delta_test.dart

@@ -74,9 +74,7 @@ void main() {
     expect(a.compose(b), expected);
     expect(a.compose(b), expected);
   });
   });
   test('retain + insert', () {
   test('retain + insert', () {
-    final a = Delta().retain(1, {
-      'color': 'blue'
-    });
+    final a = Delta().retain(1, {'color': 'blue'});
     final b = Delta().insert('B');
     final b = Delta().insert('B');
     final expected = Delta().insert('B').retain(1, {
     final expected = Delta().insert('B').retain(1, {
       'color': 'blue',
       'color': 'blue',

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

@@ -1,4 +1,5 @@
 import 'dart:convert';
 import 'dart:convert';
+import 'dart:math';
 
 
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/state_tree.dart';
 import 'package:flowy_editor/document/state_tree.dart';
@@ -20,7 +21,7 @@ void main() {
     expect(stateTree.root.toJson(), data['document']);
     expect(stateTree.root.toJson(), data['document']);
   });
   });
 
 
-  test('search node in state tree', () async {
+  test('search node by Path in state tree', () async {
     final String response = await rootBundle.loadString('assets/document.json');
     final String response = await rootBundle.loadString('assets/document.json');
     final data = Map<String, Object>.from(json.decode(response));
     final data = Map<String, Object>.from(json.decode(response));
     final stateTree = StateTree.fromJson(data);
     final stateTree = StateTree.fromJson(data);
@@ -30,6 +31,18 @@ void main() {
     expect(textType != null, true);
     expect(textType != null, true);
   });
   });
 
 
+  test('search node by Self 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);
+    final path = checkBoxNode.path([]);
+    expect(pathEquals(path, [1, 0]), true);
+  });
+
   test('insert node in state tree', () async {
   test('insert node in state tree', () async {
     final String response = await rootBundle.loadString('assets/document.json');
     final String response = await rootBundle.loadString('assets/document.json');
     final data = Map<String, Object>.from(json.decode(response));
     final data = Map<String, Object>.from(json.decode(response));