mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #609 from LucasXu0/feat/flowy_editor
combine render plugins and editor state
This commit is contained in:
commit
a4c66c4db0
6
.github/workflows/dart_test.yml
vendored
6
.github/workflows/dart_test.yml
vendored
@ -8,6 +8,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'feat/flowy_editor'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@ -71,3 +72,8 @@ jobs:
|
||||
flutter pub get
|
||||
flutter test
|
||||
|
||||
- name: Run FlowyEditor tests
|
||||
working-directory: frontend/app_flowy/packages/flowy_editor
|
||||
run: |
|
||||
flutter pub get
|
||||
flutter test
|
@ -20,7 +20,7 @@
|
||||
"subtype": "with-checkbox",
|
||||
"text-type": "heading1",
|
||||
"font-size": 30,
|
||||
"content": "bbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"checkbox": false
|
||||
}
|
||||
},
|
||||
|
@ -92,13 +92,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
);
|
||||
} else {
|
||||
final data = Map<String, Object>.from(json.decode(snapshot.data!));
|
||||
final stateTree = StateTree.fromJson(data);
|
||||
return renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(
|
||||
buildContext: context,
|
||||
node: stateTree.root,
|
||||
),
|
||||
final document = StateTree.fromJson(data);
|
||||
final editorState = EditorState(
|
||||
document: document,
|
||||
renderPlugins: renderPlugins,
|
||||
);
|
||||
return editorState.build(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -5,55 +5,74 @@ import 'package:provider/provider.dart';
|
||||
class ImageNodeBuilder extends NodeWidgetBuilder {
|
||||
ImageNodeBuilder.create({
|
||||
required super.node,
|
||||
required super.renderPlugins,
|
||||
required super.editorState,
|
||||
}) : super.create();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext buildContext) {
|
||||
return _ImageNodeWidget(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageNodeWidget extends StatelessWidget {
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
const _ImageNodeWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
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 context) {
|
||||
return GestureDetector(
|
||||
child: ChangeNotifierProvider.value(
|
||||
value: node,
|
||||
builder: (_, __) => Consumer<Node>(
|
||||
builder: ((context, value, child) => _build(context)),
|
||||
),
|
||||
),
|
||||
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",
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _build(BuildContext buildContext) {
|
||||
final image = Image.network(src);
|
||||
Widget? children;
|
||||
if (node.children.isNotEmpty) {
|
||||
children = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: node.children
|
||||
.map(
|
||||
(e) => renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(buildContext: buildContext, node: e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
if (children != null) {
|
||||
return Column(
|
||||
children: [
|
||||
image,
|
||||
children,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return image;
|
||||
}
|
||||
Widget _build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Image.network(src),
|
||||
if (node.children.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: node.children
|
||||
.map(
|
||||
(e) => editorState.renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(
|
||||
buildContext: context,
|
||||
node: e,
|
||||
editorState: editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import 'package:flowy_editor/flowy_editor.dart';
|
||||
class TextNodeBuilder extends NodeWidgetBuilder {
|
||||
TextNodeBuilder.create({
|
||||
required super.node,
|
||||
required super.renderPlugins,
|
||||
required super.editorState,
|
||||
}) : super.create();
|
||||
|
||||
String get content => node.attributes['content'] as String;
|
||||
@ -25,7 +25,11 @@ class TextNodeBuilder extends NodeWidgetBuilder {
|
||||
children: node.children
|
||||
.map(
|
||||
(e) => renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(buildContext: buildContext, node: e),
|
||||
context: NodeWidgetContext(
|
||||
buildContext: buildContext,
|
||||
node: e,
|
||||
editorState: editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
|
||||
TextWithCheckBoxNodeBuilder.create({
|
||||
required super.node,
|
||||
required super.renderPlugins,
|
||||
required super.editorState,
|
||||
}) : super.create();
|
||||
|
||||
// TODO: check the type
|
||||
@ -13,11 +13,16 @@ class TextWithCheckBoxNodeBuilder extends NodeWidgetBuilder {
|
||||
@override
|
||||
Widget build(BuildContext buildContext) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Checkbox(value: isCompleted, onChanged: (value) {}),
|
||||
Expanded(
|
||||
child: renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(buildContext: buildContext, node: node),
|
||||
context: NodeWidgetContext(
|
||||
buildContext: buildContext,
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
),
|
||||
withSubtype: false,
|
||||
),
|
||||
)
|
||||
|
@ -185,5 +185,5 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
sdks:
|
||||
dart: ">=2.17.3 <3.0.0"
|
||||
dart: ">=2.17.0 <3.0.0"
|
||||
flutter: ">=1.17.0"
|
||||
|
@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.3 <3.0.0"
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
|
@ -84,6 +84,20 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||
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;
|
||||
|
@ -4,7 +4,9 @@ import 'package:flowy_editor/document/path.dart';
|
||||
class StateTree {
|
||||
final Node root;
|
||||
|
||||
StateTree({required this.root});
|
||||
StateTree({
|
||||
required this.root,
|
||||
});
|
||||
|
||||
factory StateTree.fromJson(Attributes json) {
|
||||
assert(json['document'] is Map);
|
||||
|
@ -24,7 +24,10 @@ class TextOperation {
|
||||
|
||||
int _hashAttributes(Attributes attributes) {
|
||||
return Object.hashAllUnordered(
|
||||
attributes.entries.map((e) => Object.hash(e.key, e.value)));
|
||||
attributes.entries.map(
|
||||
(e) => Object.hash(e.key, e.value),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class TextInsert extends TextOperation {
|
||||
@ -145,7 +148,8 @@ class _OpIterator {
|
||||
int _index = 0;
|
||||
int _offset = 0;
|
||||
|
||||
_OpIterator(List<TextOperation> operations) : _operations = UnmodifiableListView(operations);
|
||||
_OpIterator(List<TextOperation> operations)
|
||||
: _operations = UnmodifiableListView(operations);
|
||||
|
||||
bool get hasNext {
|
||||
return peekLength() < _maxInt;
|
||||
@ -186,16 +190,23 @@ class _OpIterator {
|
||||
_offset += length;
|
||||
}
|
||||
if (nextOp is TextDelete) {
|
||||
return TextDelete(length: length);
|
||||
return TextDelete(
|
||||
length: length,
|
||||
);
|
||||
}
|
||||
|
||||
if (nextOp is TextRetain) {
|
||||
return TextRetain(length: length, attributes: nextOp.attributes);
|
||||
return TextRetain(
|
||||
length: length,
|
||||
attributes: nextOp.attributes,
|
||||
);
|
||||
}
|
||||
|
||||
if (nextOp is TextInsert) {
|
||||
return TextInsert(
|
||||
nextOp.content.substring(offset, offset + length), nextOp.attributes);
|
||||
nextOp.content.substring(offset, offset + length),
|
||||
nextOp.attributes,
|
||||
);
|
||||
}
|
||||
|
||||
return TextRetain(length: _maxInt);
|
||||
|
@ -1,13 +1,13 @@
|
||||
|
||||
import './text_delta.dart';
|
||||
import './node.dart';
|
||||
|
||||
class TextNode extends Node {
|
||||
final Delta delta;
|
||||
|
||||
TextNode(
|
||||
{required super.type,
|
||||
required super.children,
|
||||
required super.attributes,
|
||||
required this.delta});
|
||||
TextNode({
|
||||
required super.type,
|
||||
required super.children,
|
||||
required super.attributes,
|
||||
required this.delta,
|
||||
});
|
||||
}
|
||||
|
@ -1,24 +1,52 @@
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/operation/operation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import './document/state_tree.dart';
|
||||
import './document/selection.dart';
|
||||
import './operation/operation.dart';
|
||||
import './operation/transaction.dart';
|
||||
import './render/render_plugins.dart';
|
||||
|
||||
class EditorState {
|
||||
final StateTree document;
|
||||
final RenderPlugins renderPlugins;
|
||||
Selection? cursorSelection;
|
||||
|
||||
EditorState({
|
||||
required this.document,
|
||||
required this.renderPlugins,
|
||||
});
|
||||
|
||||
apply(Transaction transaction) {
|
||||
/// TODO: move to a better place.
|
||||
Widget build(BuildContext context) {
|
||||
return renderPlugins.buildWidget(
|
||||
context: NodeWidgetContext(
|
||||
buildContext: context,
|
||||
node: document.root,
|
||||
editorState: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void apply(Transaction transaction) {
|
||||
for (final op in transaction.operations) {
|
||||
_applyOperation(op);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to a better place.
|
||||
void update(
|
||||
Node node,
|
||||
Attributes attributes,
|
||||
) {
|
||||
_applyOperation(UpdateOperation(
|
||||
path: node.path(),
|
||||
attributes: attributes,
|
||||
oldAttributes: node.attributes,
|
||||
));
|
||||
}
|
||||
|
||||
_applyOperation(Operation op) {
|
||||
if (op is InsertOperation) {
|
||||
document.insert(op.path, op.value);
|
||||
|
@ -5,3 +5,6 @@ export 'package:flowy_editor/document/node.dart';
|
||||
export 'package:flowy_editor/document/path.dart';
|
||||
export 'package:flowy_editor/render/render_plugins.dart';
|
||||
export 'package:flowy_editor/render/node_widget_builder.dart';
|
||||
export 'package:flowy_editor/operation/transaction.dart';
|
||||
export 'package:flowy_editor/operation/operation.dart';
|
||||
export 'package:flowy_editor/editor_state.dart';
|
||||
|
@ -2,9 +2,7 @@ import 'package:flowy_editor/document/path.dart';
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
|
||||
abstract class Operation {
|
||||
|
||||
Operation invert();
|
||||
|
||||
}
|
||||
|
||||
class InsertOperation extends Operation {
|
||||
@ -18,9 +16,11 @@ class InsertOperation extends Operation {
|
||||
|
||||
@override
|
||||
Operation invert() {
|
||||
return DeleteOperation(path: path, removedValue: value);
|
||||
return DeleteOperation(
|
||||
path: path,
|
||||
removedValue: value,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class UpdateOperation extends Operation {
|
||||
@ -36,9 +36,12 @@ class UpdateOperation extends Operation {
|
||||
|
||||
@override
|
||||
Operation invert() {
|
||||
return UpdateOperation(path: path, attributes: oldAttributes, oldAttributes: attributes);
|
||||
return UpdateOperation(
|
||||
path: path,
|
||||
attributes: oldAttributes,
|
||||
oldAttributes: attributes,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DeleteOperation extends Operation {
|
||||
@ -52,7 +55,9 @@ class DeleteOperation extends Operation {
|
||||
|
||||
@override
|
||||
Operation invert() {
|
||||
return InsertOperation(path: path, value: removedValue);
|
||||
return InsertOperation(
|
||||
path: path,
|
||||
value: removedValue,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import './operation.dart';
|
||||
|
||||
class Transaction {
|
||||
final List<Operation> operations = [];
|
||||
|
||||
final List<Operation> operations;
|
||||
Transaction([this.operations = const []]);
|
||||
}
|
||||
|
@ -1,17 +1,22 @@
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/render/render_plugins.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../document/node.dart';
|
||||
import '../render/render_plugins.dart';
|
||||
|
||||
class NodeWidgetBuilder<T extends Node> {
|
||||
final EditorState editorState;
|
||||
final T node;
|
||||
final RenderPlugins renderPlugins;
|
||||
|
||||
NodeWidgetBuilder.create({required this.node, required this.renderPlugins});
|
||||
RenderPlugins get renderPlugins => editorState.renderPlugins;
|
||||
|
||||
Widget call(BuildContext buildContext) => build(buildContext);
|
||||
NodeWidgetBuilder.create({
|
||||
required this.editorState,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
/// Render the current [Node]
|
||||
/// and the layout style of [Node.Children].
|
||||
Widget build(BuildContext buildContext) => throw UnimplementedError();
|
||||
|
||||
Widget call(BuildContext buildContext) => build(buildContext);
|
||||
}
|
||||
|
@ -1,17 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../document/node.dart';
|
||||
import 'node_widget_builder.dart';
|
||||
import './node_widget_builder.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
|
||||
class NodeWidgetContext {
|
||||
BuildContext buildContext;
|
||||
Node node;
|
||||
NodeWidgetContext({required this.buildContext, required this.node});
|
||||
final BuildContext buildContext;
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
NodeWidgetContext({
|
||||
required this.buildContext,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
});
|
||||
}
|
||||
|
||||
typedef NodeWidgetBuilderF<T extends Node, A extends NodeWidgetBuilder> = A
|
||||
Function({
|
||||
required T node,
|
||||
required RenderPlugins renderPlugins,
|
||||
required EditorState editorState,
|
||||
});
|
||||
|
||||
// unused
|
||||
@ -56,8 +63,10 @@ class RenderPlugins {
|
||||
name += '/${node.subtype}';
|
||||
}
|
||||
final nodeWidgetBuilder = _nodeWidgetBuilder(name);
|
||||
return nodeWidgetBuilder(node: context.node, renderPlugins: this)(
|
||||
context.buildContext);
|
||||
return nodeWidgetBuilder(
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
)(context.buildContext);
|
||||
}
|
||||
|
||||
NodeWidgetBuilderF _nodeWidgetBuilder(String name) {
|
||||
|
@ -4,7 +4,7 @@ version: 0.0.1
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.3 <3.0.0"
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
@ -74,9 +74,7 @@ void main() {
|
||||
expect(a.compose(b), expected);
|
||||
});
|
||||
test('retain + insert', () {
|
||||
final a = Delta().retain(1, {
|
||||
'color': 'blue'
|
||||
});
|
||||
final a = Delta().retain(1, {'color': 'blue'});
|
||||
final b = Delta().insert('B');
|
||||
final expected = Delta().insert('B').retain(1, {
|
||||
'color': 'blue',
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/document/state_tree.dart';
|
||||
@ -20,7 +21,7 @@ void main() {
|
||||
expect(stateTree.root.toJson(), data['document']);
|
||||
});
|
||||
|
||||
test('search node in state tree', () async {
|
||||
test('search node by Path in state tree', () async {
|
||||
final String response = await rootBundle.loadString('assets/document.json');
|
||||
final data = Map<String, Object>.from(json.decode(response));
|
||||
final stateTree = StateTree.fromJson(data);
|
||||
@ -30,6 +31,18 @@ void main() {
|
||||
expect(textType != null, true);
|
||||
});
|
||||
|
||||
test('search node by Self in state tree', () async {
|
||||
final String response = await rootBundle.loadString('assets/document.json');
|
||||
final data = Map<String, Object>.from(json.decode(response));
|
||||
final stateTree = StateTree.fromJson(data);
|
||||
final checkBoxNode = stateTree.root.childAtPath([1, 0]);
|
||||
expect(checkBoxNode != null, true);
|
||||
final textType = checkBoxNode!.attributes['text-type'];
|
||||
expect(textType != null, true);
|
||||
final path = checkBoxNode.path([]);
|
||||
expect(pathEquals(path, [1, 0]), true);
|
||||
});
|
||||
|
||||
test('insert node in state tree', () async {
|
||||
final String response = await rootBundle.loadString('assets/document.json');
|
||||
final data = Map<String, Object>.from(json.decode(response));
|
||||
|
Loading…
Reference in New Issue
Block a user