feat: Add node validator and update op methods

This commit is contained in:
Lucas.Xu 2022-07-14 13:59:05 +08:00
parent 00c628437d
commit 6eb347a096
7 changed files with 120 additions and 99 deletions

View File

@ -31,7 +31,7 @@ class MyApp extends StatelessWidget {
// is not restarted. // is not restarted.
primarySwatch: Colors.blue, 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. // the App.build method, and use it to set our appbar title.
title: Text(widget.title), title: Text(widget.title),
), ),
body: FutureBuilder<String>( body: Column(
future: rootBundle.loadString('assets/document.json'), crossAxisAlignment: CrossAxisAlignment.start,
builder: (context, snapshot) { children: [
if (!snapshot.hasData) { FutureBuilder<String>(
return const Center( future: rootBundle.loadString('assets/document.json'),
child: CircularProgressIndicator(), builder: (context, snapshot) {
); if (!snapshot.hasData) {
} else { return const Center(
final data = Map<String, Object>.from(json.decode(snapshot.data!)); child: CircularProgressIndicator(),
final document = StateTree.fromJson(data); );
final editorState = EditorState( } else {
document: document, final data =
renderPlugins: renderPlugins, Map<String, Object>.from(json.decode(snapshot.data!));
); final document = StateTree.fromJson(data);
return editorState.build(context); 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,
),
)
],
), ),
); );
} }

View File

@ -39,16 +39,10 @@ class _ImageNodeWidget extends StatelessWidget {
), ),
), ),
onTap: () { onTap: () {
editorState.update( editorState.update(node, {
node, 'image_src':
Attributes.from(node.attributes) "https://images.pexels.com/photos/9995076/pexels-photo-9995076.png?cs=srgb&dl=pexels-temmuz-uzun-9995076.jpg&fm=jpg&w=640&h=400"
..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",
},
),
);
}, },
); );
} }

View File

@ -7,7 +7,11 @@ class TextNodeBuilder extends NodeWidgetBuilder {
TextNodeBuilder.create({ TextNodeBuilder.create({
required super.node, required super.node,
required super.editorState, 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; String get content => node.attributes['content'] as String;
@ -51,41 +55,35 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final editableRichText = ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: node, value: node,
builder: (_, __) => Consumer<Node>( builder: (_, __) => Consumer<Node>(
builder: ((context, value, child) => SelectableText.rich( builder: ((context, value, child) {
TextSpan( return Column(
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(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: node.children children: [
.map( 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( (e) => editorState.renderPlugins.buildWidget(
context: NodeWidgetContext( context: NodeWidgetContext(
buildContext: context, buildContext: context,
@ -94,11 +92,11 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
), ),
), ),
) )
.toList(), ],
), );
], }),
),
); );
return child;
} }
@override @override
@ -147,15 +145,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
@override @override
void updateEditingValue(TextEditingValue value) { void updateEditingValue(TextEditingValue value) {
debugPrint(value.text); debugPrint(value.text);
editorState.update( editorState.update(node, {'content': value.text});
node,
Attributes.from(node.attributes)
..addAll(
{
'content': value.text,
},
),
);
} }
@override @override

View File

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

View File

@ -36,18 +36,25 @@ class EditorState {
} }
// TODO: move to a better place. // TODO: move to a better place.
void update( void update(Node node, Attributes attributes) {
Node node,
Attributes attributes,
) {
_applyOperation(UpdateOperation( _applyOperation(UpdateOperation(
path: node.path(), path: node.path,
attributes: attributes, attributes: Attributes.from(attributes)..addAll(attributes),
oldAttributes: node.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) { if (op is InsertOperation) {
document.insert(op.path, op.value); document.insert(op.path, op.value);
} else if (op is UpdateOperation) { } else if (op is UpdateOperation) {

View File

@ -3,9 +3,12 @@ import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/render/render_plugins.dart'; import 'package:flowy_editor/render/render_plugins.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
typedef NodeValidator<T extends Node> = bool Function(T node);
class NodeWidgetBuilder<T extends Node> { class NodeWidgetBuilder<T extends Node> {
final EditorState editorState; final EditorState editorState;
final T node; final T node;
NodeValidator<T>? nodeValidator;
RenderPlugins get renderPlugins => editorState.renderPlugins; RenderPlugins get renderPlugins => editorState.renderPlugins;
@ -18,5 +21,14 @@ class NodeWidgetBuilder<T extends Node> {
/// and the layout style of [Node.Children]. /// and the layout style of [Node.Children].
Widget build(BuildContext buildContext) => throw UnimplementedError(); 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);
}
} }

View File

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