mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #831 from AppFlowy-IO/doc/transaction-and-deltas
Doc: transaction and deltas
This commit is contained in:
commit
68a1acc9f2
@ -256,7 +256,12 @@ TextOperation? _textOperationFromJson(Map<String, dynamic> 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<TextOperation> {
|
||||
final List<TextOperation> _operations;
|
||||
String? _rawString;
|
||||
@ -316,6 +321,9 @@ class Delta extends Iterable<TextOperation> {
|
||||
_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<TextOperation> {
|
||||
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<TextOperation> {
|
||||
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<TextOperation> {
|
||||
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<TextOperation> {
|
||||
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<TextOperation> {
|
||||
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) {
|
||||
|
@ -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<Node> 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);
|
||||
}
|
||||
|
@ -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<Operation> 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<Node> 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),
|
||||
|
@ -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<FlowyKeyEventHandler> defaultKeyEventHandlers = [
|
||||
deleteTextHandler,
|
||||
slashShortcutHandler,
|
||||
flowyDeleteNodesHandler,
|
||||
arrowKeysHandler,
|
||||
copyPasteKeysHandler,
|
||||
redoUndoKeysHandler,
|
||||
|
@ -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>(
|
||||
TransactionBuilder(editorState),
|
||||
(previousValue, node) => previousValue..deleteNode(node),
|
||||
)
|
||||
.commit();
|
||||
return KeyEventResult.handled;
|
||||
};
|
@ -18,6 +18,11 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
|
||||
|
||||
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<HistoryItem> {
|
||||
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--) {
|
||||
|
Loading…
Reference in New Issue
Block a user