mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: Add node validator and update op methods
This commit is contained in:
parent
00c628437d
commit
6eb347a096
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user