From 55ca05f30eb1b06f2fa3cfe9eade8c4648782eff Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 13 Jul 2022 16:09:41 +0800 Subject: [PATCH] feat: support subtype render plugin and add text with check-box example --- .../flowy_editor/assets/document.json | 4 +- .../flowy_editor/example/assets/document.json | 9 ++-- .../flowy_editor/example/lib/main.dart | 7 ++- .../example/lib/plugin/image_node_widget.dart | 28 +++++++++-- .../example/lib/plugin/text_node_widget.dart | 8 ++-- .../text_with_check_box_node_widget.dart | 27 +++++++++++ .../flowy_editor/example/pubspec.lock | 14 ++++++ .../flowy_editor/example/pubspec.yaml | 1 + .../flowy_editor/lib/document/node.dart | 21 ++++++++- .../lib/render/render_plugins.dart | 47 +++++++++++++++---- .../flowy_editor/test/flowy_editor_test.dart | 4 +- 11 files changed, 145 insertions(+), 25 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 8aa75717ac..31092286f8 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/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 } }, diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index 49c082f461..f41ec18130 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/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 } }, { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index d697fe1a9c..845b352a23 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/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 { ..register( 'image', ImageNodeBuilder.create, + ) + ..register( + 'text/with-checkbox', + TextWithCheckBoxNodeBuilder.create, ); } @@ -89,7 +94,7 @@ class _MyHomePageState extends State { final data = Map.from(json.decode(snapshot.data!)); final stateTree = StateTree.fromJson(data); return renderPlugins.buildWidget( - NodeWidgetContext( + context: NodeWidgetContext( buildContext: context, node: stateTree.root, ), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index cb9d44da60..f960f877df 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/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( + 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(), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 7acf35cec9..825f55a4ab 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/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(), diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_with_check_box_node_widget.dart new file mode 100644 index 0000000000..79dddaa665 --- /dev/null +++ b/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, + ), + ) + ], + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock index cce4d72430..63ade5e65c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ b/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 diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index 2149f712a8..1a788cfb7d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/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: diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index e4ff84b99c..07c0c13ade 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/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; -class Node extends LinkedListEntry { +class Node extends ChangeNotifier with LinkedListEntry { Node? parent; final String type; final LinkedList 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 { 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 { 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 diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart index 49d5dc9e1f..4dfe3a88ba 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/render_plugins.dart @@ -18,31 +18,58 @@ typedef NodeWidgetBuilderF = A // typedef NodeBuilder = T Function(Node node); class RenderPlugins { - Map nodeWidgetBuilders = {}; + final Map _nodeWidgetBuilders = {}; // unused // Map 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("/")'); + } } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index e8f14bc9c7..6e98376748 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -44,7 +44,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'], '**'); @@ -56,7 +56,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');