Merge pull request #608 from LucasXu0/feat/flowy_editor

feat: support subtype render plugin and add text with check-box example
This commit is contained in:
Nathan.fooo 2022-07-13 16:32:10 +08:00 committed by GitHub
commit ea23739df4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 145 additions and 25 deletions

View File

@ -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
}
},

View File

@ -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
}
},
{

View File

@ -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,
),

View File

@ -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(),

View File

@ -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(),

View File

@ -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,
),
)
],
);
}
}

View File

@ -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

View File

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

View File

@ -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

View File

@ -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("/")');
}
}
}

View File

@ -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');