소스 검색

Merge pull request #608 from LucasXu0/feat/flowy_editor

feat: support subtype render plugin and add text with check-box example
Nathan.fooo 2 년 전
부모
커밋
ea23739df4

+ 2 - 2
frontend/app_flowy/packages/flowy_editor/assets/document.json

@@ -5,7 +5,7 @@
       {
         "type": "text",
         "attributes": {
-          "text-type": "heading1"
+          "subtype": "with-heading"
         }
       },
       {
@@ -24,7 +24,7 @@
           {
             "type": "text",
             "attributes": {
-              "text-type": "check-box",
+              "text-type": "checkbox",
               "check": true
             }
           },

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

@@ -8,17 +8,20 @@
         {
           "type": "text",
           "attributes": {
-            "text-type": "heading1",
+            "subtype": "with-checkbox",
             "font-size": 30,
-            "content": "aaaaaaaaaaaaaaaaaaaaaaaa"
+            "content": "aaaaaaaaaaaaaaaaaaaaaaaa",
+            "checkbox": false
           }
         },
         {
           "type": "text",
           "attributes": {
+            "subtype": "with-checkbox",
             "text-type": "heading1",
             "font-size": 30,
-            "content": "bbbbbbbbbbbbbbbbbbbbbbb"
+            "content": "bbbbbbbbbbbbbbbbbbbbbbb",
+            "checkbox": false
           }
         },
         {

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

@@ -2,6 +2,7 @@ import 'dart:convert';
 
 import 'package:example/plugin/image_node_widget.dart';
 import 'package:example/plugin/text_node_widget.dart';
+import 'package:example/plugin/text_with_check_box_node_widget.dart';
 import 'package:flutter/material.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flutter/services.dart';
@@ -67,6 +68,10 @@ class _MyHomePageState extends State<MyHomePage> {
       ..register(
         'image',
         ImageNodeBuilder.create,
+      )
+      ..register(
+        'text/with-checkbox',
+        TextWithCheckBoxNodeBuilder.create,
       );
   }
 
@@ -89,7 +94,7 @@ class _MyHomePageState extends State<MyHomePage> {
             final data = Map<String, Object>.from(json.decode(snapshot.data!));
             final stateTree = StateTree.fromJson(data);
             return renderPlugins.buildWidget(
-              NodeWidgetContext(
+              context: NodeWidgetContext(
                 buildContext: context,
                 node: stateTree.root,
               ),

+ 25 - 3
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart

@@ -1,14 +1,36 @@
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
 
 class ImageNodeBuilder extends NodeWidgetBuilder {
-  ImageNodeBuilder.create({required super.node, required super.renderPlugins})
-      : super.create();
+  ImageNodeBuilder.create({
+    required super.node,
+    required super.renderPlugins,
+  }) : super.create();
 
   String get src => node.attributes['image_src'] as String;
 
   @override
   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);
+          },
+        );
+      },
+    );
+  }
+
+  Widget _build(BuildContext buildContext) {
     final image = Image.network(src);
     Widget? children;
     if (node.children.isNotEmpty) {
@@ -17,7 +39,7 @@ class ImageNodeBuilder extends NodeWidgetBuilder {
         children: node.children
             .map(
               (e) => renderPlugins.buildWidget(
-                NodeWidgetContext(buildContext: buildContext, node: e),
+                context: NodeWidgetContext(buildContext: buildContext, node: e),
               ),
             )
             .toList(),

+ 5 - 3
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart

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

+ 27 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart

@@ -0,0 +1,27 @@
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flutter/material.dart';
+
+class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
+  TextWithCheckBoxNodeBuilder.create({
+    required super.node,
+    required super.renderPlugins,
+  }) : super.create();
+
+  // TODO: check the type
+  bool get isCompleted => node.attributes['checkbox'] as bool;
+
+  @override
+  Widget build(BuildContext buildContext) {
+    return Row(
+      children: [
+        Checkbox(value: isCompleted, onChanged: (value) {}),
+        Expanded(
+          child: renderPlugins.buildWidget(
+            context: NodeWidgetContext(buildContext: buildContext, node: node),
+            withSubtype: false,
+          ),
+        )
+      ],
+    );
+  }
+}

+ 14 - 0
frontend/app_flowy/packages/flowy_editor/example/pubspec.lock

@@ -109,6 +109,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.7.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   path:
     dependency: transitive
     description:
@@ -116,6 +123,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.8.1"
+  provider:
+    dependency: "direct main"
+    description:
+      name: provider
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.3"
   sky_engine:
     dependency: transitive
     description: flutter

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml

@@ -36,6 +36,7 @@ dependencies:
   cupertino_icons: ^1.0.2
   flowy_editor:
     path: ../
+  provider: ^6.0.3
 
 dev_dependencies:
   flutter_test:

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

@@ -1,14 +1,24 @@
 import 'dart:collection';
 import 'package:flowy_editor/document/path.dart';
+import 'package:flutter/material.dart';
 
 typedef Attributes = Map<String, dynamic>;
 
-class Node extends LinkedListEntry<Node> {
+class Node extends ChangeNotifier with LinkedListEntry<Node> {
   Node? parent;
   final String type;
   final LinkedList<Node> children;
   final Attributes attributes;
 
+  String? get subtype {
+    // TODO: make 'subtype' as a const value.
+    if (attributes.containsKey('subtype')) {
+      assert(attributes['subtype'] is String, 'subtype must be a [String]');
+      return attributes['subtype'] as String;
+    }
+    return null;
+  }
+
   Node({
     required this.type,
     required this.children,
@@ -53,6 +63,9 @@ class Node extends LinkedListEntry<Node> {
     for (final attribute in attributes.entries) {
       this.attributes[attribute.key] = attribute.value;
     }
+
+    // Notify the new attributes
+    notifyListeners();
   }
 
   Node? childAtIndex(int index) {
@@ -75,12 +88,18 @@ class Node extends LinkedListEntry<Node> {
   void insertAfter(Node entry) {
     entry.parent = parent;
     super.insertAfter(entry);
+
+    // Notify the new node.
+    parent?.notifyListeners();
   }
 
   @override
   void insertBefore(Node entry) {
     entry.parent = parent;
     super.insertBefore(entry);
+
+    // Notify the new node.
+    parent?.notifyListeners();
   }
 
   @override

+ 37 - 10
frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart

@@ -18,31 +18,58 @@ typedef NodeWidgetBuilderF<T extends Node, A extends NodeWidgetBuilder> = A
 // typedef NodeBuilder<T extends Node> = T Function(Node node);
 
 class RenderPlugins {
-  Map<String, NodeWidgetBuilderF> nodeWidgetBuilders = {};
+  final Map<String, NodeWidgetBuilderF> _nodeWidgetBuilders = {};
   // unused
   // Map<String, NodeBuilder> nodeBuilders = {};
 
-  /// register plugin to render specified [name].
-  /// [name] should be correspond to the [type] in [Node].
+  /// Register plugin to render specified [name].
+  ///
+  /// [name] should be [Node].type
+  ///   or [Node].type + '/' + [Node].attributes['subtype'].
+  ///
+  /// e.g. 'text', 'text/with-checkbox', or 'text/with-heading'
+  ///
   /// [name] could be empty.
   void register(String name, NodeWidgetBuilderF builder) {
-    nodeWidgetBuilders[name] = builder;
+    _validatePluginName(name);
+
+    _nodeWidgetBuilders[name] = builder;
   }
 
-  /// unRegister plugin with specified [name].
+  /// UnRegister plugin with specified [name].
   void unRegister(String name) {
-    nodeWidgetBuilders.removeWhere((key, _) => key == name);
+    _validatePluginName(name);
+
+    _nodeWidgetBuilders.removeWhere((key, _) => key == name);
   }
 
-  Widget buildWidget(NodeWidgetContext context) {
-    final nodeWidgetBuilder = _nodeWidgetBuilder(context.node.type);
+  Widget buildWidget({
+    required NodeWidgetContext context,
+    bool withSubtype = true,
+  }) {
+    /// Find node widget builder
+    /// 1. If node's attributes contains subtype, return.
+    /// 2. If node's attributes do no contains substype, return.
+    final node = context.node;
+    var name = node.type;
+    if (withSubtype && node.subtype != null) {
+      name += '/${node.subtype}';
+    }
+    final nodeWidgetBuilder = _nodeWidgetBuilder(name);
     return nodeWidgetBuilder(node: context.node, renderPlugins: this)(
         context.buildContext);
   }
 
   NodeWidgetBuilderF _nodeWidgetBuilder(String name) {
-    assert(nodeWidgetBuilders.containsKey(name),
+    assert(_nodeWidgetBuilders.containsKey(name),
         'Could not query the builder with this $name');
-    return nodeWidgetBuilders[name]!;
+    return _nodeWidgetBuilders[name]!;
+  }
+
+  void _validatePluginName(String name) {
+    final paths = name.split('/');
+    if (paths.length > 2) {
+      throw Exception('[Name] must contains zero or one slash("/")');
+    }
   }
 }

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

@@ -48,7 +48,7 @@ void main() {
     final stateTree = StateTree.fromJson(data);
     final deletedNode = stateTree.delete([1, 1]);
     expect(deletedNode != null, true);
-    expect(deletedNode!.attributes['text-type'], 'check-box');
+    expect(deletedNode!.attributes['text-type'], 'checkbox');
     final node = stateTree.nodeAtPath([1, 1]);
     expect(node != null, true);
     expect(node!.attributes['tag'], '**');
@@ -60,7 +60,7 @@ void main() {
     final stateTree = StateTree.fromJson(data);
     final attributes = stateTree.update([1, 1], {'text-type': 'heading1'});
     expect(attributes != null, true);
-    expect(attributes!['text-type'], 'check-box');
+    expect(attributes!['text-type'], 'checkbox');
     final updatedNode = stateTree.nodeAtPath([1, 1]);
     expect(updatedNode != null, true);
     expect(updatedNode!.attributes['text-type'], 'heading1');