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 593cfa2871..4c1cd079b1 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/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 { // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), - body: FutureBuilder( - future: rootBundle.loadString('assets/document.json'), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - final data = Map.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( + future: rootBundle.loadString('assets/document.json'), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + final data = + Map.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, + ), + ) + ], ), ); } 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 33425eccd5..5084b6333c 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 @@ -40,16 +40,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" + }); }, ); } 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 1af9c7ee7e..59e466c92e 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 @@ -8,7 +8,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; @@ -52,41 +56,35 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override Widget build(BuildContext context) { - final editableRichText = ChangeNotifierProvider.value( + return ChangeNotifierProvider.value( value: node, builder: (_, __) => Consumer( - 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, @@ -95,11 +93,11 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> ), ), ) - .toList(), - ), - ], + ], + ); + }), + ), ); - return child; } @override @@ -148,15 +146,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 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 84493de0f9..cf706df856 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -12,12 +12,15 @@ class Node extends ChangeNotifier with LinkedListEntry { 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; + assert(attributes['subtype'] is String?, + 'subtype must be a [String] or [null]'); + return attributes['subtype'] as String?; } return null; } + Path get path => _path(); + Node({ required this.type, required this.children, @@ -60,12 +63,16 @@ class Node extends ChangeNotifier with LinkedListEntry { } void updateAttributes(Attributes attributes) { + bool shouldNotifyParent = + this.attributes['subtype'] != attributes['subtype']; + for (final attribute in attributes.entries) { this.attributes[attribute.key] = attribute.value; } - // Notify the new attributes - notifyListeners(); + // if attributes contains 'subtype', should notify parent to rebuild node + // else, just notify current node. + shouldNotifyParent ? parent?.notifyListeners() : notifyListeners(); } Node? childAtIndex(int index) { @@ -84,20 +91,6 @@ class Node extends ChangeNotifier with LinkedListEntry { 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; @@ -118,8 +111,10 @@ class Node extends ChangeNotifier with LinkedListEntry { @override void unlink() { - parent = null; super.unlink(); + + parent?.notifyListeners(); + parent = null; } Map toJson() { @@ -134,4 +129,18 @@ class Node extends ChangeNotifier with LinkedListEntry { } 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]); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 6fe58d2886..1b85eb515f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -52,7 +52,7 @@ class StateTree { if (updatedNode == null) { return null; } - final previousAttributes = {...updatedNode.attributes}; + final previousAttributes = Attributes.from(updatedNode.attributes); updatedNode.updateAttributes(attributes); return previousAttributes; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 30ffb68009..a5fbb9e40d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -37,18 +37,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(node.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) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index 0dad14f821..4fb99419ce 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/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 = bool Function(T node); + class NodeWidgetBuilder { final EditorState editorState; final T node; + NodeValidator? nodeValidator; RenderPlugins get renderPlugins => editorState.renderPlugins; @@ -18,5 +21,14 @@ class NodeWidgetBuilder { /// 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); + } } 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 6635f46827..d272364b44 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 @@ -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); });