Browse Source

feat: transaction builder

Vincent Chan 2 years ago
parent
commit
9307edb50d

+ 7 - 4
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:flowy_editor/flowy_editor.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
@@ -33,10 +34,12 @@ class _ImageNodeWidget extends StatelessWidget {
     return GestureDetector(
     return GestureDetector(
       child: _build(context),
       child: _build(context),
       onTap: () {
       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();
       },
       },
     );
     );
   }
   }

+ 1 - 1
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/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/operation/transaction_builder.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:flowy_editor/document/attributes.dart';
 import 'package:flowy_editor/document/attributes.dart';
 
 
@@ -187,7 +188,6 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
   @override
   @override
   void updateEditingValue(TextEditingValue value) {
   void updateEditingValue(TextEditingValue value) {
     debugPrint(value.text);
     debugPrint(value.text);
-    editorState.update(node, {'content': value.text});
   }
   }
 
 
   @override
   @override

+ 12 - 3
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -159,12 +159,21 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
 }
 }
 
 
 class TextNode extends Node {
 class TextNode extends Node {
-  final Delta delta;
+  Delta _delta;
 
 
   TextNode({
   TextNode({
     required super.type,
     required super.type,
     required super.children,
     required super.children,
     required super.attributes,
     required super.attributes,
-    required this.delta,
-  });
+    required Delta delta,
+  }) : _delta = delta;
+
+  Delta get delta {
+    return _delta;
+  }
+
+  set delta(Delta v) {
+    _delta = v;
+    notifyListeners();
+  }
 }
 }

+ 13 - 0
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/node.dart';
 import 'package:flowy_editor/document/path.dart';
 import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/text_delta.dart';
 import './attributes.dart';
 import './attributes.dart';
 
 
 class StateTree {
 class StateTree {
@@ -35,6 +36,18 @@ class StateTree {
     return true;
     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) {
   Node? delete(Path path) {
     if (path.isEmpty) {
     if (path.isEmpty) {
       return null;
       return null;

+ 2 - 19
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) {
   void _applyOperation(Operation op) {
     if (op is InsertOperation) {
     if (op is InsertOperation) {
       document.insert(op.path, op.value);
       document.insert(op.path, op.value);
@@ -62,6 +43,8 @@ class EditorState {
       document.update(op.path, op.attributes);
       document.update(op.path, op.attributes);
     } else if (op is DeleteOperation) {
     } else if (op is DeleteOperation) {
       document.delete(op.path);
       document.delete(op.path);
+    } else if (op is TextEditOperation) {
+      document.textEdit(op.path, op.delta);
     }
     }
   }
   }
 }
 }

+ 3 - 1
frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart

@@ -67,14 +67,16 @@ class DeleteOperation extends Operation {
 class TextEditOperation extends Operation {
 class TextEditOperation extends Operation {
   final Path path;
   final Path path;
   final Delta delta;
   final Delta delta;
+  final Delta inverted;
 
 
   TextEditOperation({
   TextEditOperation({
     required this.path,
     required this.path,
     required this.delta,
     required this.delta,
+    required this.inverted,
   });
   });
 
 
   @override
   @override
   Operation invert() {
   Operation invert() {
-    return TextEditOperation(path: path, delta: delta);
+    return TextEditOperation(path: path, delta: inverted, inverted: delta);
   }
   }
 }
 }

+ 24 - 2
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';
 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 {
 class Transaction {
-  final List<Operation> operations;
-  Transaction([this.operations = const []]);
+  final UnmodifiableListView<Operation> operations;
+  final Selection? cursorSelection;
+
+  const Transaction({
+    required this.operations,
+    this.cursorSelection,
+  });
 }
 }

+ 70 - 0
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<Operation> 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,
+    );
+  }
+}