diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index c48941011f..82d5b59ccb 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; @@ -33,10 +34,12 @@ class _ImageNodeWidget extends StatelessWidget { return GestureDetector( child: _build(context), onTap: () { - editorState.update(node, { - '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" - }); + TransactionBuilder(editorState) + ..updateNode(node, { + '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" + }) + ..commit(); }, ); } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 49dcfad8f4..5d75bd7ca4 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -2,6 +2,7 @@ import 'package:flowy_editor/document/text_delta.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flutter/services.dart'; import 'package:flowy_editor/document/attributes.dart'; @@ -187,7 +188,6 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override void updateEditingValue(TextEditingValue value) { debugPrint(value.text); - editorState.update(node, {'content': value.text}); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index ba4cb10525..4d2becf9fe 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -159,12 +159,21 @@ class Node extends ChangeNotifier with LinkedListEntry { } class TextNode extends Node { - final Delta delta; + Delta _delta; TextNode({ required super.type, required super.children, required super.attributes, - required this.delta, - }); + required Delta delta, + }) : _delta = delta; + + Delta get delta { + return _delta; + } + + set delta(Delta v) { + _delta = v; + notifyListeners(); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 1b85eb515f..b6dbd26fff 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -1,5 +1,6 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/text_delta.dart'; import './attributes.dart'; class StateTree { @@ -35,6 +36,18 @@ class StateTree { return true; } + bool textEdit(Path path, Delta delta) { + if (path.isEmpty) { + return false; + } + var node = root.childAtPath(path); + if (node == null || node is! TextNode) { + return false; + } + node.delta = node.delta.compose(delta); + return false; + } + Node? delete(Path path) { if (path.isEmpty) { return null; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index a5fbb9e40d..74c8a8e58c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -36,25 +36,6 @@ class EditorState { } } - // TODO: move to a better place. - void update(Node node, Attributes attributes) { - _applyOperation(UpdateOperation( - path: node.path, - attributes: Attributes.from(node.attributes)..addAll(attributes), - oldAttributes: node.attributes, - )); - } - - // 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) { document.insert(op.path, op.value); @@ -62,6 +43,8 @@ class EditorState { document.update(op.path, op.attributes); } else if (op is DeleteOperation) { document.delete(op.path); + } else if (op is TextEditOperation) { + document.textEdit(op.path, op.delta); } } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index 5fb9a523a8..e3710ddb3c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -67,14 +67,16 @@ class DeleteOperation extends Operation { class TextEditOperation extends Operation { final Path path; final Delta delta; + final Delta inverted; TextEditOperation({ required this.path, required this.delta, + required this.inverted, }); @override Operation invert() { - return TextEditOperation(path: path, delta: delta); + return TextEditOperation(path: path, delta: inverted, inverted: delta); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart index 1a2c4bcdb5..fa56484ae3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -1,6 +1,28 @@ +import 'dart:collection'; +import 'package:flutter/material.dart'; +import 'package:flowy_editor/document/selection.dart'; import './operation.dart'; +/// This class to use to store the **changes** +/// will be applied to the editor. +/// +/// This class is immutable version the the class +/// [[Transaction]]. Is used to stored and +/// transmit. If you want to build the transaction, +/// use [[Transaction]] directly. +/// +/// There will be several ways to consume the transaction: +/// 1. Apply to the state to update the UI. +/// 2. Send to the backend to store and do operation transforming. +/// 3. Stored by the UndoManager to implement redo/undo. +/// +@immutable class Transaction { - final List operations; - Transaction([this.operations = const []]); + final UnmodifiableListView operations; + final Selection? cursorSelection; + + const Transaction({ + required this.operations, + this.cursorSelection, + }); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart new file mode 100644 index 0000000000..3736d054a4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -0,0 +1,70 @@ +import 'dart:collection'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/document/selection.dart'; + +import './operation.dart'; +import './transaction.dart'; + +/// +/// This class is used to +/// build the transaction from the state. +/// +/// This class automatically save the +/// cursor from the state. +/// +/// When the transaction is undo, the +/// cursor can be restored. +/// +class TransactionBuilder { + final List operations = []; + EditorState state; + Selection? cursorSelection; + + TransactionBuilder(this.state); + + commit() { + final transaction = finish(); + state.apply(transaction); + } + + void insertNode(Path path, Node node) { + cursorSelection = state.cursorSelection; + operations.add(InsertOperation(path: path, value: node)); + } + + void updateNode(Node node, Attributes attributes) { + cursorSelection = state.cursorSelection; + operations.add(UpdateOperation( + path: node.path, + attributes: Attributes.from(node.attributes)..addAll(attributes), + oldAttributes: node.attributes, + )); + } + + void deleteNode(Node node) { + cursorSelection = state.cursorSelection; + operations.add(DeleteOperation(path: node.path, removedValue: node)); + } + + void textEdit(TextNode node, Delta Function() f) { + cursorSelection = state.cursorSelection; + final path = node.path; + + final delta = f(); + + final inverted = delta.invert(node.delta); + operations + .add(TextEditOperation(path: path, delta: delta, inverted: inverted)); + } + + Transaction finish() { + return Transaction( + operations: UnmodifiableListView(operations), + cursorSelection: cursorSelection, + ); + } +}