diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 6a4e4f3c80..05f0d61b8d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -256,7 +256,12 @@ TextOperation? _textOperationFromJson(Map json) { return result; } -// basically copy from: https://github.com/quilljs/delta +/// Deltas are a simple, yet expressive format that can be used to describe contents and changes. +/// The format is JSON based, and is human readable, yet easily parsible by machines. +/// Deltas can describe any rich text document, includes all text and formatting information, without the ambiguity and complexity of HTML. +/// + +/// Basically borrowed from: https://github.com/quilljs/delta class Delta extends Iterable { final List _operations; String? _rawString; @@ -316,6 +321,9 @@ class Delta extends Iterable { _operations.add(textOp); } + /// The slice() method does not change the original string. + /// The start and end parameters specifies the part of the string to extract. + /// The end position is optional. Delta slice(int start, [int? end]) { final result = Delta(); final iterator = _OpIterator(_operations); @@ -336,19 +344,29 @@ class Delta extends Iterable { return result; } + /// Insert operations have an `insert` key defined. + /// A String value represents inserting text. void insert(String content, [Attributes? attributes]) => add(TextInsert(content, attributes)); + /// Retain operations have a Number `retain` key defined representing the number of characters to keep (other libraries might use the name keep or skip). + /// An optional `attributes` key can be defined with an Object to describe formatting changes to the character range. + /// A value of `null` in the `attributes` Object represents removal of that key. + /// + /// *Note: It is not necessary to retain the last characters of a document as this is implied.* void retain(int length, [Attributes? attributes]) => add(TextRetain(length, attributes)); + /// Delete operations have a Number `delete` key defined representing the number of characters to delete. void delete(int length) => add(TextDelete(length)); + /// The length of the string fo the [Delta]. int get length { return _operations.fold( 0, (previousValue, element) => previousValue + element.length); } + /// Returns a Delta that is equivalent to applying the operations of own Delta, followed by another Delta. Delta compose(Delta other) { final thisIter = _OpIterator(_operations); final otherIter = _OpIterator(other._operations); @@ -412,6 +430,7 @@ class Delta extends Iterable { return delta..chop(); } + /// This method joins two Delta together. Delta operator +(Delta other) { var ops = [..._operations]; if (other._operations.isNotEmpty) { @@ -445,6 +464,7 @@ class Delta extends Iterable { return hashList(_operations); } + /// Returned an inverted delta that has the opposite effect of against a base document delta. Delta invert(Delta base) { final inverted = Delta(); _operations.fold(0, (int previousValue, op) { @@ -475,6 +495,13 @@ class Delta extends Iterable { return _operations.map((e) => e.toJson()).toList(); } + /// This method will return the position of the previous rune. + /// + /// Since the encoding of the [String] in Dart is UTF-16. + /// If you want to find the previous character of a position, + /// you can' just use the `position - 1` simply. + /// + /// This method can help you to compute the position of the previous character. int prevRunePosition(int pos) { if (pos == 0) { return pos - 1; @@ -485,6 +512,13 @@ class Delta extends Iterable { return _runeIndexes![pos - 1]; } + /// This method will return the position of the next rune. + /// + /// Since the encoding of the [String] in Dart is UTF-16. + /// If you want to find the previous character of a position, + /// you can' just use the `position + 1` simply. + /// + /// This method can help you to compute the position of the next character. int nextRunePosition(int pos) { final stringContent = toRawString(); if (pos >= stringContent.length - 1) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart index 31a321a3c0..6b7210cd7a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flowy_editor/src/service/service.dart'; import 'package:flutter/material.dart'; -import 'package:flowy_editor/src/document/node.dart'; import 'package:flowy_editor/src/document/selection.dart'; import 'package:flowy_editor/src/document/state_tree.dart'; import 'package:flowy_editor/src/operation/operation.dart'; @@ -26,11 +25,26 @@ enum CursorUpdateReason { others, } +/// The state of the editor. +/// +/// The state including: +/// - The document to render +/// - The state of the selection. +/// +/// [EditorState] also includes the services of the editor: +/// - Selection service +/// - Scroll service +/// - Keyboard service +/// - Input service +/// - Toolbar service +/// +/// In consideration of collaborative editing. +/// All the mutations should be applied through [Transaction]. +/// +/// Mutating the document with document's API is not recommended. class EditorState { final StateTree document; - List selectedNodes = []; - // Service reference. final service = FlowyService(); @@ -41,7 +55,6 @@ class EditorState { return _cursorSelection; } - /// add the set reason in the future, don't use setter updateCursorSelection(Selection? cursorSelection, [CursorUpdateReason reason = CursorUpdateReason.others]) { // broadcast to other users here @@ -59,8 +72,13 @@ class EditorState { undoManager.state = this; } + /// Apply the transaction to the state. + /// + /// The options can be used to determine whether the editor + /// should record the transaction in undo/redo stack. apply(Transaction transaction, [ApplyOptions options = const ApplyOptions()]) { + // TODO: validate the transation. for (final op in transaction.operations) { _applyOperation(op); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index 5a8dc68b3b..f8cc80b161 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -14,7 +14,6 @@ import 'package:flowy_editor/src/operation/transaction.dart'; /// A [TransactionBuilder] is used to build the transaction from the state. /// It will save make a snapshot of the cursor selection state automatically. /// The cursor can be resorted if the transaction is undo. - class TransactionBuilder { final List operations = []; EditorState state; @@ -29,15 +28,18 @@ class TransactionBuilder { state.apply(transaction); } + /// Insert the nodes at the position of path. insertNode(Path path, Node node) { insertNodes(path, [node]); } + /// Insert a sequence of nodes at the position of path. insertNodes(Path path, List nodes) { beforeSelection = state.cursorSelection; add(InsertOperation(path, nodes)); } + /// Update the attributes of nodes. updateNode(Node node, Attributes attributes) { beforeSelection = state.cursorSelection; @@ -49,6 +51,7 @@ class TransactionBuilder { )); } + /// Delete a node in the document. deleteNode(Node node) { deleteNodesAtPath(node.path); } @@ -57,6 +60,9 @@ class TransactionBuilder { nodes.forEach(deleteNode); } + /// Delete a sequence of nodes at the path of the document. + /// The length specific the length of the following nodes to delete( + /// including the start one). deleteNodesAtPath(Path path, [int length = 1]) { if (path.isEmpty) { return; @@ -106,6 +112,9 @@ class TransactionBuilder { ); } + /// Insert content at a specified index. + /// Optionally, you may specify formatting attributes that are applied to the inserted string. + /// By default, the formatting attributes before the insert position will be used. insertText(TextNode node, int index, String content, [Attributes? attributes]) { var newAttributes = attributes; @@ -126,6 +135,7 @@ class TransactionBuilder { Position(path: node.path, offset: index + content.length)); } + /// Assign formatting attributes to a range of text. formatText(TextNode node, int index, int length, Attributes attributes) { textEdit( node, @@ -135,6 +145,7 @@ class TransactionBuilder { afterSelection = beforeSelection; } + /// Delete length characters starting from index. deleteText(TextNode node, int index, int length) { textEdit( node, @@ -169,6 +180,11 @@ class TransactionBuilder { ); } + /// Add an operation to the transaction. + /// This method will merge operations if they are both TextEdits. + /// + /// Also, this method will transform the path of the operations + /// to avoid conflicts. add(Operation op) { final Operation? last = operations.isEmpty ? null : operations.last; if (last != null) { @@ -190,6 +206,7 @@ class TransactionBuilder { operations.add(op); } + /// Generate a immutable [Transaction] to apply or transmit. Transaction finish() { return Transaction( operations: UnmodifiableListView(operations), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart index e617804a77..a327206c26 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart @@ -1,6 +1,5 @@ import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; @@ -14,7 +13,6 @@ import 'package:flowy_editor/src/service/keyboard_service.dart'; List defaultKeyEventHandlers = [ deleteTextHandler, slashShortcutHandler, - flowyDeleteNodesHandler, arrowKeysHandler, copyPasteKeysHandler, redoUndoKeysHandler, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart deleted file mode 100644 index 132f88854a..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/src/service/keyboard_service.dart'; -import 'package:flutter/material.dart'; - -FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { - // Handle delete nodes. - final nodes = editorState.selectedNodes; - if (nodes.length <= 1) { - return KeyEventResult.ignored; - } - - debugPrint('delete nodes = $nodes'); - - nodes - .fold( - TransactionBuilder(editorState), - (previousValue, node) => previousValue..deleteNode(node), - ) - .commit(); - return KeyEventResult.handled; -}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart index b81e91481f..e49debdd09 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart @@ -18,6 +18,11 @@ class HistoryItem extends LinkedListEntry { HistoryItem(); + /// Seal the history item. + /// When an item is sealed, no more operations can be added + /// to the item. + /// + /// The caller should create a new [HistoryItem]. seal() { _sealed = true; } @@ -32,6 +37,7 @@ class HistoryItem extends LinkedListEntry { operations.addAll(iterable); } + /// Create a new [Transaction] by inverting the operations. Transaction toTransaction(EditorState state) { final builder = TransactionBuilder(state); for (var i = operations.length - 1; i >= 0; i--) {