Merge pull request #609 from LucasXu0/feat/flowy_editor

combine render plugins and editor state
This commit is contained in:
Nathan.fooo 2022-07-13 21:29:53 +08:00 committed by GitHub
commit a4c66c4db0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 215 additions and 94 deletions

View File

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

View File

@ -20,7 +20,7 @@
"subtype": "with-checkbox",
"text-type": "heading1",
"font-size": 30,
"content": "bbbbbbbbbbbbbbbbbbbbbbb",
"content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"checkbox": false
}
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import './operation.dart';
class Transaction {
final List<Operation> operations = [];
final List<Operation> operations;
Transaction([this.operations = const []]);
}

View File

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

View File

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

View File

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

View File

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

View File

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