Преглед на файлове

feat: Add node validator and update op methods

Lucas.Xu преди 2 години
родител
ревизия
6eb347a096

+ 32 - 18
frontend/app_flowy/packages/flowy_editor/example/lib/main.dart

@@ -31,7 +31,7 @@ class MyApp extends StatelessWidget {
         // is not restarted.
         primarySwatch: Colors.blue,
       ),
-      home: const MyHomePage(title: 'Flutter Demo Home Page'),
+      home: const MyHomePage(title: 'FlowyEditor Example'),
     );
   }
 }
@@ -83,23 +83,37 @@ class _MyHomePageState extends State<MyHomePage> {
         // the App.build method, and use it to set our appbar title.
         title: Text(widget.title),
       ),
-      body: FutureBuilder<String>(
-        future: rootBundle.loadString('assets/document.json'),
-        builder: (context, snapshot) {
-          if (!snapshot.hasData) {
-            return const Center(
-              child: CircularProgressIndicator(),
-            );
-          } else {
-            final data = Map<String, Object>.from(json.decode(snapshot.data!));
-            final document = StateTree.fromJson(data);
-            final editorState = EditorState(
-              document: document,
-              renderPlugins: renderPlugins,
-            );
-            return editorState.build(context);
-          }
-        },
+      body: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          FutureBuilder<String>(
+            future: rootBundle.loadString('assets/document.json'),
+            builder: (context, snapshot) {
+              if (!snapshot.hasData) {
+                return const Center(
+                  child: CircularProgressIndicator(),
+                );
+              } else {
+                final data =
+                    Map<String, Object>.from(json.decode(snapshot.data!));
+                final document = StateTree.fromJson(data);
+                print(document.root.toString());
+                final editorState = EditorState(
+                  document: document,
+                  renderPlugins: renderPlugins,
+                );
+                return editorState.build(context);
+              }
+            },
+          ),
+          SizedBox(
+            height: 50,
+            width: MediaQuery.of(context).size.width,
+            child: Container(
+              color: Colors.red,
+            ),
+          )
+        ],
       ),
     );
   }

+ 4 - 10
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart

@@ -39,16 +39,10 @@ class _ImageNodeWidget extends StatelessWidget {
         ),
       ),
       onTap: () {
-        editorState.update(
-          node,
-          Attributes.from(node.attributes)
-            ..addAll(
-              {
-                '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",
-              },
-            ),
-        );
+        editorState.update(node, {
+          '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"
+        });
       },
     );
   }

+ 36 - 46
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart

@@ -7,7 +7,11 @@ class TextNodeBuilder extends NodeWidgetBuilder {
   TextNodeBuilder.create({
     required super.node,
     required super.editorState,
-  }) : super.create();
+  }) : super.create() {
+    nodeValidator = ((node) {
+      return node.type == 'text' && node.attributes.containsKey('content');
+    });
+  }
 
   String get content => node.attributes['content'] as String;
 
@@ -51,41 +55,35 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
 
   @override
   Widget build(BuildContext context) {
-    final editableRichText = ChangeNotifierProvider.value(
+    return ChangeNotifierProvider.value(
       value: node,
       builder: (_, __) => Consumer<Node>(
-        builder: ((context, value, child) => SelectableText.rich(
-              TextSpan(
-                text: content,
-                style: node.attributes.toTextStyle(),
-              ),
-              onTap: () {
-                _textInputConnection?.close();
-                _textInputConnection = TextInput.attach(
-                  this,
-                  const TextInputConfiguration(
-                    enableDeltaModel: false,
-                    inputType: TextInputType.multiline,
-                    textCapitalization: TextCapitalization.sentences,
-                  ),
-                );
-                _textInputConnection
-                  ?..show()
-                  ..setEditingState(textEditingValue);
-              },
-            )),
-      ),
-    );
-
-    final child = Column(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        editableRichText,
-        if (node.children.isNotEmpty)
-          Column(
+        builder: ((context, value, child) {
+          return Column(
             crossAxisAlignment: CrossAxisAlignment.start,
-            children: node.children
-                .map(
+            children: [
+              SelectableText.rich(
+                TextSpan(
+                  text: content,
+                  style: node.attributes.toTextStyle(),
+                ),
+                onTap: () {
+                  _textInputConnection?.close();
+                  _textInputConnection = TextInput.attach(
+                    this,
+                    const TextInputConfiguration(
+                      enableDeltaModel: false,
+                      inputType: TextInputType.multiline,
+                      textCapitalization: TextCapitalization.sentences,
+                    ),
+                  );
+                  _textInputConnection
+                    ?..show()
+                    ..setEditingState(textEditingValue);
+                },
+              ),
+              if (node.children.isNotEmpty)
+                ...node.children.map(
                   (e) => editorState.renderPlugins.buildWidget(
                     context: NodeWidgetContext(
                       buildContext: context,
@@ -94,11 +92,11 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
                     ),
                   ),
                 )
-                .toList(),
-          ),
-      ],
+            ],
+          );
+        }),
+      ),
     );
-    return child;
   }
 
   @override
@@ -147,15 +145,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
   @override
   void updateEditingValue(TextEditingValue value) {
     debugPrint(value.text);
-    editorState.update(
-      node,
-      Attributes.from(node.attributes)
-        ..addAll(
-          {
-            'content': value.text,
-          },
-        ),
-    );
+    editorState.update(node, {'content': value.text});
   }
 
   @override

+ 20 - 16
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -19,6 +19,8 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     return null;
   }
 
+  Path get path => _path();
+
   Node({
     required this.type,
     required this.children,
@@ -66,7 +68,7 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     }
 
     // Notify the new attributes
-    notifyListeners();
+    parent?.notifyListeners();
   }
 
   Node? childAtIndex(int index) {
@@ -85,20 +87,6 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     return childAtIndex(path.first)?.childAtPath(path.sublist(1));
   }
 
-  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
   void insertAfter(Node entry) {
     entry.parent = parent;
@@ -119,8 +107,10 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
 
   @override
   void unlink() {
-    parent = null;
     super.unlink();
+
+    parent?.notifyListeners();
+    parent = null;
   }
 
   Map<String, Object> toJson() {
@@ -135,4 +125,18 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     }
     return map;
   }
+
+  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]);
+  }
 }

+ 14 - 7
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -36,18 +36,25 @@ class EditorState {
   }
 
   // TODO: move to a better place.
-  void update(
-    Node node,
-    Attributes attributes,
-  ) {
+  void update(Node node, Attributes attributes) {
     _applyOperation(UpdateOperation(
-      path: node.path(),
-      attributes: attributes,
+      path: node.path,
+      attributes: Attributes.from(attributes)..addAll(attributes),
       oldAttributes: node.attributes,
     ));
   }
 
-  _applyOperation(Operation op) {
+  // TODO: move to a better place.
+  void delete(Node node) {
+    _applyOperation(
+      DeleteOperation(
+        path: node.path,
+        removedValue: node,
+      ),
+    );
+  }
+
+  void _applyOperation(Operation op) {
     if (op is InsertOperation) {
       document.insert(op.path, op.value);
     } else if (op is UpdateOperation) {

+ 13 - 1
frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart

@@ -3,9 +3,12 @@ import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/render/render_plugins.dart';
 import 'package:flutter/material.dart';
 
+typedef NodeValidator<T extends Node> = bool Function(T node);
+
 class NodeWidgetBuilder<T extends Node> {
   final EditorState editorState;
   final T node;
+  NodeValidator<T>? nodeValidator;
 
   RenderPlugins get renderPlugins => editorState.renderPlugins;
 
@@ -18,5 +21,14 @@ class NodeWidgetBuilder<T extends Node> {
   /// and the layout style of [Node.Children].
   Widget build(BuildContext buildContext) => throw UnimplementedError();
 
-  Widget call(BuildContext buildContext) => build(buildContext);
+  Widget call(BuildContext buildContext) {
+    /// TODO: Validate the node
+    /// if failed, stop call build function,
+    ///   return Empty widget, and throw Error.
+    if (nodeValidator != null && nodeValidator!(node) != true) {
+      throw Exception(
+          'Node validate failure, node = { type: ${node.type}, attributes: ${node.attributes} }');
+    }
+    return build(buildContext);
+  }
 }

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

@@ -39,7 +39,7 @@ void main() {
     expect(checkBoxNode != null, true);
     final textType = checkBoxNode!.attributes['text-type'];
     expect(textType != null, true);
-    final path = checkBoxNode.path([]);
+    final path = checkBoxNode.path;
     expect(pathEquals(path, [1, 0]), true);
   });