mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
refactor: move transaction.dart to core/transform
This commit is contained in:
parent
319875529f
commit
19bf8e3b7a
@ -26,9 +26,9 @@ ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (selection.isCollapsed) {
|
||||
TransactionBuilder(editorState)
|
||||
..insertText(codeBlockNode.first, selection.end.offset, '\n')
|
||||
..commit();
|
||||
editorState.transaction
|
||||
.insertText(codeBlockNode.first, selection.end.offset, '\n');
|
||||
editorState.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
@ -61,16 +61,16 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
|
||||
return;
|
||||
}
|
||||
if (textNodes.first.toPlainText().isEmpty) {
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..updateNode(textNodes.first, {
|
||||
'subtype': 'code_block',
|
||||
'theme': 'vs',
|
||||
'language': null,
|
||||
})
|
||||
..afterSelection = selection
|
||||
..commit();
|
||||
..afterSelection = selection;
|
||||
editorState.commit();
|
||||
} else {
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..insertNode(
|
||||
selection.end.path.next,
|
||||
TextNode(
|
||||
@ -83,8 +83,8 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
|
||||
delta: Delta()..insert('\n'),
|
||||
),
|
||||
)
|
||||
..afterSelection = selection
|
||||
..commit();
|
||||
..afterSelection = selection;
|
||||
editorState.commit();
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -181,11 +181,10 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
|
||||
child: DropdownButton<String>(
|
||||
value: _detectLanguage,
|
||||
onChanged: (value) {
|
||||
TransactionBuilder(widget.editorState)
|
||||
..updateNode(widget.textNode, {
|
||||
'language': value,
|
||||
})
|
||||
..commit();
|
||||
widget.editorState.transaction.updateNode(widget.textNode, {
|
||||
'language': value,
|
||||
});
|
||||
widget.editorState.commit();
|
||||
},
|
||||
items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
|
@ -18,7 +18,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
|
||||
}
|
||||
final textNode = textNodes.first;
|
||||
if (textNode.toPlainText() == '--') {
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, 0, 2)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
@ -29,8 +29,8 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
|
||||
),
|
||||
)
|
||||
..afterSelection =
|
||||
Selection.single(path: textNode.path.next, startOffset: 0)
|
||||
..commit();
|
||||
Selection.single(path: textNode.path.next, startOffset: 0);
|
||||
editorState.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
@ -54,7 +54,7 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||
}
|
||||
final textNode = textNodes.first;
|
||||
if (textNode.toPlainText().isEmpty) {
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
Node(
|
||||
@ -64,10 +64,10 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||
),
|
||||
)
|
||||
..afterSelection =
|
||||
Selection.single(path: textNode.path.next, startOffset: 0)
|
||||
..commit();
|
||||
Selection.single(path: textNode.path.next, startOffset: 0);
|
||||
editorState.commit();
|
||||
} else {
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..insertNode(
|
||||
selection.end.path.next,
|
||||
TextNode(
|
||||
@ -78,8 +78,8 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||
delta: Delta()..insert('---'),
|
||||
),
|
||||
)
|
||||
..afterSelection = selection
|
||||
..commit();
|
||||
..afterSelection = selection;
|
||||
editorState.commit();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
|
||||
final Path texNodePath;
|
||||
if (textNodes.first.toPlainText().isEmpty) {
|
||||
texNodePath = selection.end.path;
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..insertNode(
|
||||
selection.end.path,
|
||||
Node(
|
||||
@ -33,11 +33,11 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
|
||||
),
|
||||
)
|
||||
..deleteNode(textNodes.first)
|
||||
..afterSelection = selection
|
||||
..commit();
|
||||
..afterSelection = selection;
|
||||
editorState.commit();
|
||||
} else {
|
||||
texNodePath = selection.end.path.next;
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..insertNode(
|
||||
selection.end.path.next,
|
||||
Node(
|
||||
@ -46,8 +46,8 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
|
||||
attributes: {'tex': ''},
|
||||
),
|
||||
)
|
||||
..afterSelection = selection
|
||||
..commit();
|
||||
..afterSelection = selection;
|
||||
editorState.commit();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final texState =
|
||||
@ -142,9 +142,8 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
TransactionBuilder(widget.editorState)
|
||||
..deleteNode(widget.node)
|
||||
..commit();
|
||||
widget.editorState.transaction.deleteNode(widget.node);
|
||||
widget.editorState.commit();
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -175,12 +174,11 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
if (controller.text != _tex) {
|
||||
TransactionBuilder(widget.editorState)
|
||||
..updateNode(
|
||||
widget.node,
|
||||
{'tex': controller.text},
|
||||
)
|
||||
..commit();
|
||||
widget.editorState.transaction.updateNode(
|
||||
widget.node,
|
||||
{'tex': controller.text},
|
||||
);
|
||||
widget.editorState.commit();
|
||||
}
|
||||
},
|
||||
child: const Text('OK'),
|
||||
|
@ -31,7 +31,7 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
|
||||
// Delete the previous 'underscore',
|
||||
// update the style of the text surrounded by the two underscores to 'italic',
|
||||
// and update the cursor position.
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, firstUnderscore, 1)
|
||||
..formatText(
|
||||
textNode,
|
||||
@ -46,8 +46,8 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
|
||||
path: textNode.path,
|
||||
offset: selection.end.offset - 1,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
@ -13,8 +13,7 @@ export 'src/core/document/attributes.dart';
|
||||
export 'src/core/legacy/built_in_attribute_keys.dart';
|
||||
export 'src/editor_state.dart';
|
||||
export 'src/core/transform/operation.dart';
|
||||
export 'src/operation/transaction.dart';
|
||||
export 'src/operation/transaction_builder.dart';
|
||||
export 'src/core/transform/transaction.dart';
|
||||
export 'src/render/selection/selectable.dart';
|
||||
export 'src/service/editor_service.dart';
|
||||
export 'src/service/render_plugin_service.dart';
|
||||
|
@ -4,7 +4,7 @@ import 'package:appflowy_editor/src/commands/text_command_infra.dart';
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
Future<void> insertContextInText(
|
||||
@ -22,9 +22,8 @@ Future<void> insertContextInText(
|
||||
|
||||
final completer = Completer<void>();
|
||||
|
||||
TransactionBuilder(editorState)
|
||||
..insertText(result, index, content)
|
||||
..commit();
|
||||
editorState.transaction.insertText(result, index, content);
|
||||
editorState.commit();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
completer.complete();
|
||||
|
@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
Future<void> updateTextNodeAttributes(
|
||||
@ -23,9 +23,8 @@ Future<void> updateTextNodeAttributes(
|
||||
|
||||
final completer = Completer<void>();
|
||||
|
||||
TransactionBuilder(editorState)
|
||||
..updateNode(result, attributes)
|
||||
..commit();
|
||||
editorState.transaction.updateNode(result, attributes);
|
||||
editorState.commit();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
completer.complete();
|
||||
@ -49,15 +48,13 @@ Future<void> updateTextNodeDeltaAttributes(
|
||||
final newSelection = getSelection(editorState, selection: selection);
|
||||
|
||||
final completer = Completer<void>();
|
||||
|
||||
TransactionBuilder(editorState)
|
||||
..formatText(
|
||||
result,
|
||||
newSelection.startIndex,
|
||||
newSelection.length,
|
||||
attributes,
|
||||
)
|
||||
..commit();
|
||||
editorState.transaction.formatText(
|
||||
result,
|
||||
newSelection.startIndex,
|
||||
newSelection.length,
|
||||
attributes,
|
||||
);
|
||||
editorState.commit();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
completer.complete();
|
||||
|
@ -0,0 +1,267 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
||||
import 'package:appflowy_editor/src/core/document/document.dart';
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/core/document/text_delta.dart';
|
||||
import 'package:appflowy_editor/src/core/location/position.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/operation.dart';
|
||||
|
||||
/// A [Transaction] has a list of [Operation] objects that will be applied
|
||||
/// to the editor.
|
||||
///
|
||||
/// 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.
|
||||
class Transaction {
|
||||
Transaction({
|
||||
required this.document,
|
||||
});
|
||||
|
||||
final Document document;
|
||||
|
||||
/// The operations to be applied.
|
||||
final List<Operation> operations = [];
|
||||
|
||||
/// The selection to be applied.
|
||||
Selection? afterSelection;
|
||||
|
||||
/// The before selection is to be recovered if needed.
|
||||
Selection? beforeSelection;
|
||||
|
||||
/// Inserts the [Node] at the given [Path].
|
||||
void insertNode(
|
||||
Path path,
|
||||
Node node, {
|
||||
bool deepCopy = true,
|
||||
}) {
|
||||
insertNodes(path, [node], deepCopy: deepCopy);
|
||||
}
|
||||
|
||||
/// Inserts a sequence of [Node]s at the given [Path].
|
||||
void insertNodes(
|
||||
Path path,
|
||||
Iterable<Node> nodes, {
|
||||
bool deepCopy = true,
|
||||
}) {
|
||||
if (deepCopy) {
|
||||
add(InsertOperation(path, nodes.map((e) => e.copyWith())));
|
||||
} else {
|
||||
add(InsertOperation(path, nodes));
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the attributes of the [Node].
|
||||
///
|
||||
/// The [attributes] will be merged into the existing attributes.
|
||||
void updateNode(Node node, Attributes attributes) {
|
||||
final inverted = invertAttributes(node.attributes, attributes);
|
||||
add(UpdateOperation(
|
||||
node.path,
|
||||
{...attributes},
|
||||
inverted,
|
||||
));
|
||||
}
|
||||
|
||||
/// Deletes the [Node] in the document.
|
||||
void deleteNode(Node node) {
|
||||
deleteNodesAtPath(node.path);
|
||||
}
|
||||
|
||||
/// Deletes the [Node]s in the document.
|
||||
void deleteNodes(Iterable<Node> nodes) {
|
||||
nodes.forEach(deleteNode);
|
||||
}
|
||||
|
||||
/// Deletes the [Node]s at the given [Path].
|
||||
///
|
||||
/// The [length] indicates the number of consecutive deletions,
|
||||
/// including the node of the current path.
|
||||
void deleteNodesAtPath(Path path, [int length = 1]) {
|
||||
if (path.isEmpty) return;
|
||||
final nodes = <Node>[];
|
||||
final parent = path.parent;
|
||||
for (var i = 0; i < length; i++) {
|
||||
final node = document.nodeAtPath(parent + [path.last + i]);
|
||||
if (node == null) {
|
||||
break;
|
||||
}
|
||||
nodes.add(node);
|
||||
}
|
||||
add(DeleteOperation(path, nodes));
|
||||
}
|
||||
|
||||
/// Update the [TextNode]s with the given [Delta].
|
||||
void updateText(TextNode textNode, Delta delta) {
|
||||
final inverted = delta.invert(textNode.delta);
|
||||
add(UpdateTextOperation(textNode.path, delta, inverted));
|
||||
}
|
||||
|
||||
/// Returns the JSON representation of the transaction.
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (operations.isNotEmpty) {
|
||||
json['operations'] = operations.map((o) => o.toJson());
|
||||
}
|
||||
if (afterSelection != null) {
|
||||
json['after_selection'] = afterSelection!.toJson();
|
||||
}
|
||||
if (beforeSelection != null) {
|
||||
json['before_selection'] = beforeSelection!.toJson();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Adds 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, {bool transform = true}) {
|
||||
final Operation? last = operations.isEmpty ? null : operations.last;
|
||||
if (last != null) {
|
||||
if (op is UpdateTextOperation &&
|
||||
last is UpdateTextOperation &&
|
||||
op.path.equals(last.path)) {
|
||||
final newOp = UpdateTextOperation(
|
||||
op.path,
|
||||
last.delta.compose(op.delta),
|
||||
op.inverted.compose(last.inverted),
|
||||
);
|
||||
operations[operations.length - 1] = newOp;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (transform) {
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
op = transformOperation(operations[i], op);
|
||||
}
|
||||
}
|
||||
if (op is UpdateTextOperation && op.delta.isEmpty) {
|
||||
return;
|
||||
}
|
||||
operations.add(op);
|
||||
}
|
||||
}
|
||||
|
||||
extension TextTransaction on Transaction {
|
||||
void mergeText(
|
||||
TextNode first,
|
||||
TextNode second, {
|
||||
int? firstOffset,
|
||||
int secondOffset = 0,
|
||||
}) {
|
||||
final firstLength = first.delta.length;
|
||||
final secondLength = second.delta.length;
|
||||
firstOffset ??= firstLength;
|
||||
updateText(
|
||||
first,
|
||||
Delta()
|
||||
..retain(firstOffset)
|
||||
..delete(firstLength - firstOffset)
|
||||
..addAll(second.delta.slice(secondOffset, secondLength)),
|
||||
);
|
||||
afterSelection = Selection.collapsed(Position(
|
||||
path: first.path,
|
||||
offset: firstOffset,
|
||||
));
|
||||
}
|
||||
|
||||
/// Inserts the text 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 reused.
|
||||
void insertText(
|
||||
TextNode textNode,
|
||||
int index,
|
||||
String text, {
|
||||
Attributes? attributes,
|
||||
}) {
|
||||
var newAttributes = attributes;
|
||||
if (index != 0 && attributes == null) {
|
||||
newAttributes =
|
||||
textNode.delta.slice(max(index - 1, 0), index).first.attributes;
|
||||
if (newAttributes != null) {
|
||||
newAttributes = {...newAttributes}; // make a copy
|
||||
}
|
||||
}
|
||||
updateText(
|
||||
textNode,
|
||||
Delta()
|
||||
..retain(index)
|
||||
..insert(text, attributes: newAttributes),
|
||||
);
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.path, offset: index + text.length),
|
||||
);
|
||||
}
|
||||
|
||||
/// Assigns a formatting attributes to a range of text.
|
||||
formatText(
|
||||
TextNode textNode,
|
||||
int index,
|
||||
int length,
|
||||
Attributes attributes,
|
||||
) {
|
||||
afterSelection = beforeSelection;
|
||||
updateText(
|
||||
textNode,
|
||||
Delta()
|
||||
..retain(index)
|
||||
..retain(length, attributes: attributes),
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes the text of specified length starting at index.
|
||||
deleteText(
|
||||
TextNode textNode,
|
||||
int index,
|
||||
int length,
|
||||
) {
|
||||
updateText(
|
||||
textNode,
|
||||
Delta()
|
||||
..retain(index)
|
||||
..delete(length),
|
||||
);
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.path, offset: index),
|
||||
);
|
||||
}
|
||||
|
||||
/// Replaces the text of specified length starting at 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 reused.
|
||||
replaceText(
|
||||
TextNode textNode,
|
||||
int index,
|
||||
int length,
|
||||
String text, {
|
||||
Attributes? attributes,
|
||||
}) {
|
||||
var newAttributes = attributes;
|
||||
if (index != 0 && attributes == null) {
|
||||
newAttributes =
|
||||
textNode.delta.slice(max(index - 1, 0), index).first.attributes;
|
||||
if (newAttributes != null) {
|
||||
newAttributes = {...newAttributes}; // make a copy
|
||||
}
|
||||
}
|
||||
updateText(
|
||||
textNode,
|
||||
Delta()
|
||||
..retain(index)
|
||||
..delete(length)
|
||||
..insert(text, attributes: newAttributes),
|
||||
);
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: textNode.path,
|
||||
offset: index + text.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/core/document/document.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/operation.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||
import 'package:appflowy_editor/src/undo_manager.dart';
|
||||
|
||||
class ApplyOptions {
|
||||
@ -74,6 +74,24 @@ class EditorState {
|
||||
|
||||
bool editable = true;
|
||||
|
||||
Transaction get transaction {
|
||||
if (_transaction != null) {
|
||||
return _transaction!;
|
||||
}
|
||||
_transaction = Transaction(document: document);
|
||||
_transaction!.beforeSelection = _cursorSelection;
|
||||
return _transaction!;
|
||||
}
|
||||
|
||||
Transaction? _transaction;
|
||||
|
||||
void commit() {
|
||||
if (_transaction != null) {
|
||||
apply(_transaction!, const ApplyOptions(recordUndo: true));
|
||||
_transaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
Selection? get cursorSelection {
|
||||
return _cursorSelection;
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import '../core/transform/operation.dart';
|
||||
|
||||
/// A [Transaction] has a list of [Operation] objects that will be applied
|
||||
/// to the editor. It is an immutable class and used to store and transmit.
|
||||
///
|
||||
/// If you want to build a new [Transaction], use [TransactionBuilder] 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. Used by the UndoManager to implement redo/undo.
|
||||
@immutable
|
||||
class Transaction {
|
||||
final UnmodifiableListView<Operation> operations;
|
||||
final Selection? beforeSelection;
|
||||
final Selection? afterSelection;
|
||||
|
||||
const Transaction({
|
||||
required this.operations,
|
||||
this.beforeSelection,
|
||||
this.afterSelection,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> result = {
|
||||
"operations": operations.map((e) => e.toJson()),
|
||||
};
|
||||
if (beforeSelection != null) {
|
||||
result["beforeSelection"] = beforeSelection!.toJson();
|
||||
}
|
||||
if (afterSelection != null) {
|
||||
result["afterSelection"] = afterSelection!.toJson();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/core/location/position.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/core/document/text_delta.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/operation.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction.dart';
|
||||
|
||||
/// A [TransactionBuilder] is used to build the transaction from the state.
|
||||
/// It will save a snapshot of the cursor selection state automatically.
|
||||
/// The cursor can be restored if the transaction is undo.
|
||||
class TransactionBuilder {
|
||||
final List<Operation> operations = [];
|
||||
EditorState state;
|
||||
Selection? beforeSelection;
|
||||
Selection? afterSelection;
|
||||
|
||||
TransactionBuilder(this.state);
|
||||
|
||||
/// Commits the operations to the state
|
||||
Future<void> commit() async {
|
||||
final transaction = finish();
|
||||
state.apply(transaction);
|
||||
}
|
||||
|
||||
/// Inserts the nodes at the position of path.
|
||||
insertNode(Path path, Node node) {
|
||||
insertNodes(path, [node]);
|
||||
}
|
||||
|
||||
/// Inserts a sequence of nodes at the position of path.
|
||||
insertNodes(Path path, List<Node> nodes) {
|
||||
beforeSelection = state.cursorSelection;
|
||||
add(InsertOperation(path, nodes.map((node) => node.copyWith()).toList()));
|
||||
}
|
||||
|
||||
/// Updates the attributes of nodes.
|
||||
updateNode(Node node, Attributes attributes) {
|
||||
beforeSelection = state.cursorSelection;
|
||||
final inverted = invertAttributes(node.attributes, attributes);
|
||||
add(UpdateOperation(
|
||||
node.path,
|
||||
{...attributes},
|
||||
inverted,
|
||||
));
|
||||
}
|
||||
|
||||
/// Deletes a node in the document.
|
||||
deleteNode(Node node) {
|
||||
deleteNodesAtPath(node.path);
|
||||
}
|
||||
|
||||
deleteNodes(List<Node> nodes) {
|
||||
nodes.forEach(deleteNode);
|
||||
}
|
||||
|
||||
/// Deletes a sequence of nodes at the path of the document.
|
||||
/// The length specifies the length of the following nodes to delete(
|
||||
/// including the start one).
|
||||
deleteNodesAtPath(Path path, [int length = 1]) {
|
||||
if (path.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final nodes = <Node>[];
|
||||
final prefix = path.sublist(0, path.length - 1);
|
||||
final last = path.last;
|
||||
for (var i = 0; i < length; i++) {
|
||||
final node = state.document.nodeAtPath(prefix + [last + i])!;
|
||||
nodes.add(node);
|
||||
}
|
||||
|
||||
add(DeleteOperation(path, nodes.map((node) => node.copyWith()).toList()));
|
||||
}
|
||||
|
||||
textEdit(TextNode node, Delta Function() f) {
|
||||
beforeSelection = state.cursorSelection;
|
||||
final path = node.path;
|
||||
|
||||
final delta = f();
|
||||
|
||||
final inverted = delta.invert(node.delta);
|
||||
|
||||
add(UpdateTextOperation(path, delta, inverted));
|
||||
}
|
||||
|
||||
setAfterSelection(Selection sel) {
|
||||
afterSelection = sel;
|
||||
}
|
||||
|
||||
mergeText(TextNode firstNode, TextNode secondNode,
|
||||
{int? firstOffset, int secondOffset = 0}) {
|
||||
final firstLength = firstNode.delta.length;
|
||||
final secondLength = secondNode.delta.length;
|
||||
textEdit(
|
||||
firstNode,
|
||||
() => Delta()
|
||||
..retain(firstOffset ?? firstLength)
|
||||
..delete(firstLength - (firstOffset ?? firstLength))
|
||||
..addAll(secondNode.delta.slice(secondOffset, secondLength)),
|
||||
);
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: firstNode.path,
|
||||
offset: firstOffset ?? firstLength,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts 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;
|
||||
if (index != 0 && attributes == null) {
|
||||
newAttributes =
|
||||
node.delta.slice(max(index - 1, 0), index).first.attributes;
|
||||
if (newAttributes != null) {
|
||||
newAttributes = Attributes.from(newAttributes);
|
||||
}
|
||||
}
|
||||
textEdit(
|
||||
node,
|
||||
() => Delta()
|
||||
..retain(index)
|
||||
..insert(
|
||||
content,
|
||||
attributes: newAttributes,
|
||||
),
|
||||
);
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(path: node.path, offset: index + content.length),
|
||||
);
|
||||
}
|
||||
|
||||
/// Assigns formatting attributes to a range of text.
|
||||
formatText(TextNode node, int index, int length, Attributes attributes) {
|
||||
textEdit(
|
||||
node,
|
||||
() => Delta()
|
||||
..retain(index)
|
||||
..retain(length, attributes: attributes));
|
||||
afterSelection = beforeSelection;
|
||||
}
|
||||
|
||||
/// Deletes length characters starting from index.
|
||||
deleteText(TextNode node, int index, int length) {
|
||||
textEdit(
|
||||
node,
|
||||
() => Delta()
|
||||
..retain(index)
|
||||
..delete(length));
|
||||
afterSelection =
|
||||
Selection.collapsed(Position(path: node.path, offset: index));
|
||||
}
|
||||
|
||||
replaceText(TextNode node, int index, int length, String content,
|
||||
[Attributes? attributes]) {
|
||||
var newAttributes = attributes;
|
||||
if (attributes == null) {
|
||||
final ops = node.delta.slice(index, index + length);
|
||||
if (ops.isNotEmpty) {
|
||||
newAttributes = ops.first.attributes;
|
||||
}
|
||||
}
|
||||
textEdit(
|
||||
node,
|
||||
() => Delta()
|
||||
..retain(index)
|
||||
..delete(length)
|
||||
..insert(content, attributes: newAttributes),
|
||||
);
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: node.path,
|
||||
offset: index + content.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds 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, {bool transform = true}) {
|
||||
final Operation? last = operations.isEmpty ? null : operations.last;
|
||||
if (last != null) {
|
||||
if (op is UpdateTextOperation &&
|
||||
last is UpdateTextOperation &&
|
||||
op.path.equals(last.path)) {
|
||||
final newOp = UpdateTextOperation(
|
||||
op.path,
|
||||
last.delta.compose(op.delta),
|
||||
op.inverted.compose(last.inverted),
|
||||
);
|
||||
operations[operations.length - 1] = newOp;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (transform) {
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
op = transformOperation(operations[i], op);
|
||||
}
|
||||
}
|
||||
if (op is UpdateTextOperation && op.delta.isEmpty) {
|
||||
return;
|
||||
}
|
||||
operations.add(op);
|
||||
}
|
||||
|
||||
/// Generates a immutable [Transaction] to apply or transmit.
|
||||
Transaction finish() {
|
||||
return Transaction(
|
||||
operations: UnmodifiableListView(operations),
|
||||
beforeSelection: beforeSelection,
|
||||
afterSelection: afterSelection,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||
@ -25,23 +24,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
|
||||
RichClipboard.setData(RichClipboardData(text: src));
|
||||
},
|
||||
onDelete: () {
|
||||
TransactionBuilder(context.editorState)
|
||||
..deleteNode(context.node)
|
||||
..commit();
|
||||
context.editorState.transaction.deleteNode(context.node);
|
||||
context.editorState.commit();
|
||||
},
|
||||
onAlign: (alignment) {
|
||||
TransactionBuilder(context.editorState)
|
||||
..updateNode(context.node, {
|
||||
'align': _alignmentToText(alignment),
|
||||
})
|
||||
..commit();
|
||||
context.editorState.transaction.updateNode(context.node, {
|
||||
'align': _alignmentToText(alignment),
|
||||
});
|
||||
context.editorState.commit();
|
||||
},
|
||||
onResize: (width) {
|
||||
TransactionBuilder(context.editorState)
|
||||
..updateNode(context.node, {
|
||||
'width': width,
|
||||
})
|
||||
..commit();
|
||||
context.editorState.transaction.updateNode(context.node, {
|
||||
'width': width,
|
||||
});
|
||||
context.editorState.commit();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import 'dart:collection';
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -192,11 +192,10 @@ extension on EditorState {
|
||||
'align': 'center',
|
||||
},
|
||||
);
|
||||
TransactionBuilder(this)
|
||||
..insertNode(
|
||||
selection.start.path,
|
||||
imageNode,
|
||||
)
|
||||
..commit();
|
||||
transaction.insertNode(
|
||||
selection.start.path,
|
||||
imageNode,
|
||||
);
|
||||
commit();
|
||||
}
|
||||
}
|
||||
|
@ -45,13 +45,12 @@ class SelectionMenuItem {
|
||||
final node = nodes.first as TextNode;
|
||||
final end = selection.start.offset;
|
||||
final start = node.toPlainText().substring(0, end).lastIndexOf('/');
|
||||
TransactionBuilder(editorState)
|
||||
..deleteText(
|
||||
node,
|
||||
start,
|
||||
selection.start.offset - start,
|
||||
)
|
||||
..commit();
|
||||
editorState.transaction.deleteText(
|
||||
node,
|
||||
start,
|
||||
selection.start.offset - start,
|
||||
);
|
||||
editorState.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -278,13 +277,12 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
|
||||
final nodes = selectionService.currentSelectedNodes;
|
||||
if (selection != null && nodes.length == 1) {
|
||||
widget.onSelectionUpdate();
|
||||
TransactionBuilder(widget.editorState)
|
||||
..deleteText(
|
||||
nodes.first as TextNode,
|
||||
selection.start.offset - length,
|
||||
length,
|
||||
)
|
||||
..commit();
|
||||
widget.editorState.transaction.deleteText(
|
||||
nodes.first as TextNode,
|
||||
selection.start.offset - length,
|
||||
length,
|
||||
);
|
||||
widget.editorState.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@ -295,13 +293,12 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
|
||||
widget.editorState.service.selectionService.currentSelectedNodes;
|
||||
if (selection != null && nodes.length == 1) {
|
||||
widget.onSelectionUpdate();
|
||||
TransactionBuilder(widget.editorState)
|
||||
..insertText(
|
||||
nodes.first as TextNode,
|
||||
selection.end.offset,
|
||||
text,
|
||||
)
|
||||
..commit();
|
||||
widget.editorState.transaction.insertText(
|
||||
nodes.first as TextNode,
|
||||
selection.end.offset,
|
||||
text,
|
||||
);
|
||||
widget.editorState.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -357,10 +357,9 @@ void showLinkMenu(
|
||||
_dismissLinkMenu();
|
||||
},
|
||||
onRemoveLink: () {
|
||||
TransactionBuilder(editorState)
|
||||
..formatText(
|
||||
textNode, index, length, {BuiltInAttributeKey.href: null})
|
||||
..commit();
|
||||
editorState.transaction.formatText(
|
||||
textNode, index, length, {BuiltInAttributeKey.href: null});
|
||||
editorState.commit();
|
||||
_dismissLinkMenu();
|
||||
},
|
||||
onFocusChange: (value) {
|
||||
|
@ -1,12 +1,5 @@
|
||||
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/core/location/position.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
|
||||
|
||||
void insertHeadingAfterSelection(EditorState editorState, String heading) {
|
||||
insertTextNodeAfterSelection(editorState, {
|
||||
@ -54,16 +47,15 @@ bool insertTextNodeAfterSelection(
|
||||
formatTextNodes(editorState, attributes);
|
||||
} else {
|
||||
final next = selection.end.path.next;
|
||||
final builder = TransactionBuilder(editorState);
|
||||
builder
|
||||
editorState.transaction
|
||||
..insertNode(
|
||||
next,
|
||||
TextNode.empty(attributes: attributes),
|
||||
)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: next, offset: 0),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -107,7 +99,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final builder = TransactionBuilder(editorState);
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
for (final textNode in textNodes) {
|
||||
var newAttributes = {...textNode.attributes};
|
||||
@ -117,7 +109,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
||||
}
|
||||
}
|
||||
newAttributes.addAll(attributes);
|
||||
builder
|
||||
transaction
|
||||
..updateNode(
|
||||
textNode,
|
||||
newAttributes,
|
||||
@ -130,7 +122,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
||||
);
|
||||
}
|
||||
|
||||
builder.commit();
|
||||
editorState.commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -216,13 +208,13 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final builder = TransactionBuilder(editorState);
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
// 1. All nodes are text nodes.
|
||||
// 2. The first node is not TextNode.
|
||||
// 3. The last node is not TextNode.
|
||||
if (nodes.length == textNodes.length && textNodes.length == 1) {
|
||||
builder.formatText(
|
||||
transaction.formatText(
|
||||
textNodes.first,
|
||||
selection.start.offset,
|
||||
selection.end.offset - selection.start.offset,
|
||||
@ -239,7 +231,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
|
||||
} else if (i == textNodes.length - 1 && textNode == nodes.last) {
|
||||
length = selection.end.offset;
|
||||
}
|
||||
builder.formatText(
|
||||
transaction.formatText(
|
||||
textNode,
|
||||
index,
|
||||
length,
|
||||
@ -248,7 +240,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
|
||||
}
|
||||
}
|
||||
|
||||
builder.commit();
|
||||
editorState.commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_editor/src/infra/log.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -7,7 +8,6 @@ import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
|
||||
/// [AppFlowyInputService] is responsible for processing text input,
|
||||
/// including text insertion, deletion and replacement.
|
||||
@ -160,13 +160,12 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
||||
}
|
||||
if (currentSelection.isSingle) {
|
||||
final textNode = selectionService.currentSelectedNodes.first as TextNode;
|
||||
TransactionBuilder(_editorState)
|
||||
..insertText(
|
||||
textNode,
|
||||
delta.insertionOffset,
|
||||
delta.textInserted,
|
||||
)
|
||||
..commit();
|
||||
_editorState.transaction.insertText(
|
||||
textNode,
|
||||
delta.insertionOffset,
|
||||
delta.textInserted,
|
||||
);
|
||||
_editorState.commit();
|
||||
} else {
|
||||
// TODO: implement
|
||||
}
|
||||
@ -181,9 +180,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
||||
if (currentSelection.isSingle) {
|
||||
final textNode = selectionService.currentSelectedNodes.first as TextNode;
|
||||
final length = delta.deletedRange.end - delta.deletedRange.start;
|
||||
TransactionBuilder(_editorState)
|
||||
..deleteText(textNode, delta.deletedRange.start, length)
|
||||
..commit();
|
||||
_editorState.transaction
|
||||
.deleteText(textNode, delta.deletedRange.start, length);
|
||||
_editorState.commit();
|
||||
} else {
|
||||
// TODO: implement
|
||||
}
|
||||
@ -198,10 +197,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
||||
if (currentSelection.isSingle) {
|
||||
final textNode = selectionService.currentSelectedNodes.first as TextNode;
|
||||
final length = delta.replacedRange.end - delta.replacedRange.start;
|
||||
TransactionBuilder(_editorState)
|
||||
..replaceText(
|
||||
textNode, delta.replacedRange.start, length, delta.replacementText)
|
||||
..commit();
|
||||
_editorState.transaction.replaceText(
|
||||
textNode, delta.replacedRange.start, length, delta.replacementText);
|
||||
_editorState.commit();
|
||||
} else {
|
||||
// TODO: implement
|
||||
}
|
||||
|
@ -28,11 +28,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
||||
final List<Node> nonTextNodes =
|
||||
nodes.where((node) => node is! TextNode).toList(growable: false);
|
||||
|
||||
final transactionBuilder = TransactionBuilder(editorState);
|
||||
final transaction = editorState.transaction;
|
||||
List<int>? cancelNumberListPath;
|
||||
|
||||
if (nonTextNodes.isNotEmpty) {
|
||||
transactionBuilder.deleteNodes(nonTextNodes);
|
||||
transaction.deleteNodes(nonTextNodes);
|
||||
}
|
||||
|
||||
if (textNodes.length == 1) {
|
||||
@ -44,7 +44,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
||||
if (textNode.subtype == BuiltInAttributeKey.numberList) {
|
||||
cancelNumberListPath = textNode.path;
|
||||
}
|
||||
transactionBuilder
|
||||
transaction
|
||||
..updateNode(textNode, {
|
||||
BuiltInAttributeKey.subtype: null,
|
||||
textNode.subtype!: null,
|
||||
@ -61,20 +61,20 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
||||
return _backDeleteToPreviousTextNode(
|
||||
editorState,
|
||||
textNode,
|
||||
transactionBuilder,
|
||||
transaction,
|
||||
nonTextNodes,
|
||||
selection,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (selection.isCollapsed) {
|
||||
transactionBuilder.deleteText(
|
||||
transaction.deleteText(
|
||||
textNode,
|
||||
index,
|
||||
selection.start.offset - index,
|
||||
);
|
||||
} else {
|
||||
transactionBuilder.deleteText(
|
||||
transaction.deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
selection.end.offset - selection.start.offset,
|
||||
@ -84,33 +84,32 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
||||
} else {
|
||||
if (textNodes.isEmpty) {
|
||||
if (nonTextNodes.isNotEmpty) {
|
||||
transactionBuilder.afterSelection =
|
||||
Selection.collapsed(selection.start);
|
||||
transaction.afterSelection = Selection.collapsed(selection.start);
|
||||
}
|
||||
transactionBuilder.commit();
|
||||
editorState.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
final startPosition = selection.start;
|
||||
final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
|
||||
_deleteTextNodes(transactionBuilder, textNodes, selection);
|
||||
transactionBuilder.commit();
|
||||
_deleteTextNodes(transaction, textNodes, selection);
|
||||
editorState.commit();
|
||||
|
||||
if (nodeAtStart is TextNode &&
|
||||
nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
|
||||
makeFollowingNodesIncremental(
|
||||
editorState,
|
||||
startPosition.path,
|
||||
transactionBuilder.afterSelection!,
|
||||
transaction.afterSelection!,
|
||||
);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (transactionBuilder.operations.isNotEmpty) {
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
if (nonTextNodes.isNotEmpty) {
|
||||
transactionBuilder.afterSelection = Selection.collapsed(selection.start);
|
||||
transaction.afterSelection = Selection.collapsed(selection.start);
|
||||
}
|
||||
transactionBuilder.commit();
|
||||
editorState.commit();
|
||||
}
|
||||
|
||||
if (cancelNumberListPath != null) {
|
||||
@ -128,20 +127,20 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
||||
KeyEventResult _backDeleteToPreviousTextNode(
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
TransactionBuilder transactionBuilder,
|
||||
Transaction transaction,
|
||||
List<Node> nonTextNodes,
|
||||
Selection selection,
|
||||
) {
|
||||
if (textNode.next == null &&
|
||||
textNode.children.isEmpty &&
|
||||
textNode.parent?.parent != null) {
|
||||
transactionBuilder
|
||||
transaction
|
||||
..deleteNode(textNode)
|
||||
..insertNode(textNode.parent!.path.next, textNode)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.parent!.path.next, offset: 0),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@ -152,15 +151,15 @@ KeyEventResult _backDeleteToPreviousTextNode(
|
||||
prevIsNumberList = true;
|
||||
}
|
||||
|
||||
transactionBuilder.mergeText(previousTextNode, textNode);
|
||||
transaction.mergeText(previousTextNode, textNode);
|
||||
if (textNode.children.isNotEmpty) {
|
||||
transactionBuilder.insertNodes(
|
||||
transaction.insertNodes(
|
||||
previousTextNode.path.next,
|
||||
textNode.children.toList(growable: false),
|
||||
);
|
||||
}
|
||||
transactionBuilder.deleteNode(textNode);
|
||||
transactionBuilder.afterSelection = Selection.collapsed(
|
||||
transaction.deleteNode(textNode);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: previousTextNode.path,
|
||||
offset: previousTextNode.toPlainText().length,
|
||||
@ -168,16 +167,16 @@ KeyEventResult _backDeleteToPreviousTextNode(
|
||||
);
|
||||
}
|
||||
|
||||
if (transactionBuilder.operations.isNotEmpty) {
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
if (nonTextNodes.isNotEmpty) {
|
||||
transactionBuilder.afterSelection = Selection.collapsed(selection.start);
|
||||
transaction.afterSelection = Selection.collapsed(selection.start);
|
||||
}
|
||||
transactionBuilder.commit();
|
||||
editorState.commit();
|
||||
}
|
||||
|
||||
if (prevIsNumberList) {
|
||||
makeFollowingNodesIncremental(editorState, previousTextNode!.path,
|
||||
transactionBuilder.afterSelection!);
|
||||
makeFollowingNodesIncremental(
|
||||
editorState, previousTextNode!.path, transaction.afterSelection!);
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
@ -197,7 +196,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final transactionBuilder = TransactionBuilder(editorState);
|
||||
final transaction = editorState.transaction;
|
||||
if (textNodes.length == 1) {
|
||||
final textNode = textNodes.first;
|
||||
// The cursor is at the end of the line,
|
||||
@ -206,55 +205,52 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
|
||||
return _mergeNextLineIntoThisLine(
|
||||
editorState,
|
||||
textNode,
|
||||
transactionBuilder,
|
||||
transaction,
|
||||
selection,
|
||||
);
|
||||
}
|
||||
final index = textNode.delta.nextRunePosition(selection.start.offset);
|
||||
if (selection.isCollapsed) {
|
||||
transactionBuilder.deleteText(
|
||||
transaction.deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
index - selection.start.offset,
|
||||
);
|
||||
} else {
|
||||
transactionBuilder.deleteText(
|
||||
transaction.deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
selection.end.offset - selection.start.offset,
|
||||
);
|
||||
}
|
||||
transactionBuilder.commit();
|
||||
editorState.commit();
|
||||
} else {
|
||||
final startPosition = selection.start;
|
||||
final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
|
||||
_deleteTextNodes(transactionBuilder, textNodes, selection);
|
||||
transactionBuilder.commit();
|
||||
_deleteTextNodes(transaction, textNodes, selection);
|
||||
editorState.commit();
|
||||
|
||||
if (nodeAtStart is TextNode &&
|
||||
nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
|
||||
makeFollowingNodesIncremental(
|
||||
editorState, startPosition.path, transactionBuilder.afterSelection!);
|
||||
editorState, startPosition.path, transaction.afterSelection!);
|
||||
}
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
KeyEventResult _mergeNextLineIntoThisLine(
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
TransactionBuilder transactionBuilder,
|
||||
Selection selection) {
|
||||
KeyEventResult _mergeNextLineIntoThisLine(EditorState editorState,
|
||||
TextNode textNode, Transaction transaction, Selection selection) {
|
||||
final nextNode = textNode.next;
|
||||
if (nextNode == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (nextNode is TextNode) {
|
||||
transactionBuilder.mergeText(textNode, nextNode);
|
||||
transaction.mergeText(textNode, nextNode);
|
||||
}
|
||||
transactionBuilder.deleteNode(nextNode);
|
||||
transactionBuilder.commit();
|
||||
transaction.deleteNode(nextNode);
|
||||
editorState.commit();
|
||||
|
||||
if (textNode.subtype == BuiltInAttributeKey.numberList) {
|
||||
makeFollowingNodesIncremental(editorState, textNode.path, selection);
|
||||
@ -263,15 +259,15 @@ KeyEventResult _mergeNextLineIntoThisLine(
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
void _deleteTextNodes(TransactionBuilder transactionBuilder,
|
||||
List<TextNode> textNodes, Selection selection) {
|
||||
void _deleteTextNodes(
|
||||
Transaction transaction, List<TextNode> textNodes, Selection selection) {
|
||||
final first = textNodes.first;
|
||||
final last = textNodes.last;
|
||||
var content = textNodes.last.toPlainText();
|
||||
content = content.substring(selection.end.offset, content.length);
|
||||
// Merge the fist and the last text node content,
|
||||
// and delete the all nodes expect for the first.
|
||||
transactionBuilder
|
||||
transaction
|
||||
..deleteNodes(textNodes.sublist(1))
|
||||
..mergeText(
|
||||
first,
|
||||
|
@ -85,16 +85,16 @@ void _pasteHTML(EditorState editorState, String html) {
|
||||
} else if (nodes.length == 1) {
|
||||
final firstNode = nodes[0];
|
||||
final nodeAtPath = editorState.document.nodeAtPath(path)!;
|
||||
final tb = TransactionBuilder(editorState);
|
||||
final tb = editorState.transaction;
|
||||
final startOffset = selection.start.offset;
|
||||
if (nodeAtPath.type == "text" && firstNode.type == "text") {
|
||||
final textNodeAtPath = nodeAtPath as TextNode;
|
||||
final firstTextNode = firstNode as TextNode;
|
||||
tb.textEdit(textNodeAtPath,
|
||||
() => (Delta()..retain(startOffset)) + firstTextNode.delta);
|
||||
tb.setAfterSelection(Selection.collapsed(Position(
|
||||
tb.updateText(
|
||||
textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta);
|
||||
tb.afterSelection = (Selection.collapsed(Position(
|
||||
path: path, offset: startOffset + firstTextNode.delta.length)));
|
||||
tb.commit();
|
||||
editorState.commit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -104,7 +104,7 @@ void _pasteHTML(EditorState editorState, String html) {
|
||||
|
||||
void _pasteMultipleLinesInText(
|
||||
EditorState editorState, List<int> path, int offset, List<Node> nodes) {
|
||||
final tb = TransactionBuilder(editorState);
|
||||
final tb = editorState.transaction;
|
||||
|
||||
final firstNode = nodes[0];
|
||||
final nodeAtPath = editorState.document.nodeAtPath(path)!;
|
||||
@ -120,10 +120,9 @@ void _pasteMultipleLinesInText(
|
||||
final firstTextNode = firstNode as TextNode;
|
||||
final remain = textNodeAtPath.delta.slice(offset);
|
||||
|
||||
tb.textEdit(
|
||||
tb.updateText(
|
||||
textNodeAtPath,
|
||||
() =>
|
||||
(Delta()
|
||||
(Delta()
|
||||
..retain(offset)
|
||||
..delete(remain.length)) +
|
||||
firstTextNode.delta);
|
||||
@ -146,9 +145,9 @@ void _pasteMultipleLinesInText(
|
||||
tailNodes.add(TextNode(delta: remain));
|
||||
}
|
||||
|
||||
tb.setAfterSelection(afterSelection);
|
||||
tb.afterSelection = afterSelection;
|
||||
tb.insertNodes(path, tailNodes);
|
||||
tb.commit();
|
||||
editorState.commit();
|
||||
|
||||
if (startNumber != null) {
|
||||
makeFollowingNodesIncremental(editorState, originalPath, afterSelection,
|
||||
@ -161,9 +160,9 @@ void _pasteMultipleLinesInText(
|
||||
_computeSelectionAfterPasteMultipleNodes(editorState, nodes);
|
||||
|
||||
path[path.length - 1]++;
|
||||
tb.setAfterSelection(afterSelection);
|
||||
tb.afterSelection = afterSelection;
|
||||
tb.insertNodes(path, nodes);
|
||||
tb.commit();
|
||||
editorState.commit();
|
||||
}
|
||||
|
||||
void _handlePaste(EditorState editorState) async {
|
||||
@ -196,15 +195,15 @@ void _pasteSingleLine(
|
||||
EditorState editorState, Selection selection, String line) {
|
||||
final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode;
|
||||
final beginOffset = selection.end.offset;
|
||||
TransactionBuilder(editorState)
|
||||
..textEdit(
|
||||
editorState.transaction
|
||||
..updateText(
|
||||
node,
|
||||
() => Delta()
|
||||
Delta()
|
||||
..retain(beginOffset)
|
||||
..addAll(_lineContentToDelta(line)))
|
||||
..setAfterSelection(Selection.collapsed(
|
||||
Position(path: selection.end.path, offset: beginOffset + line.length)))
|
||||
..commit();
|
||||
..afterSelection = (Selection.collapsed(
|
||||
Position(path: selection.end.path, offset: beginOffset + line.length)));
|
||||
editorState.commit();
|
||||
}
|
||||
|
||||
/// parse url from the line text
|
||||
@ -264,7 +263,7 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
|
||||
final insertedLineSuffix = node.delta.slice(beginOffset);
|
||||
|
||||
path[path.length - 1]++;
|
||||
final tb = TransactionBuilder(editorState);
|
||||
final tb = editorState.transaction;
|
||||
final List<TextNode> nodes =
|
||||
remains.map((e) => TextNode(delta: _lineContentToDelta(e))).toList();
|
||||
|
||||
@ -279,16 +278,16 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
|
||||
}
|
||||
|
||||
// insert first line
|
||||
tb.textEdit(
|
||||
tb.updateText(
|
||||
node,
|
||||
() => Delta()
|
||||
Delta()
|
||||
..retain(beginOffset)
|
||||
..insert(firstLine)
|
||||
..delete(node.delta.length - beginOffset));
|
||||
// insert remains
|
||||
tb.insertNodes(path, nodes);
|
||||
tb.setAfterSelection(afterSelection);
|
||||
tb.commit();
|
||||
tb.afterSelection = afterSelection;
|
||||
editorState.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@ -309,15 +308,15 @@ void _deleteSelectedContent(EditorState editorState) {
|
||||
if (selection.start.path.equals(selection.end.path) &&
|
||||
beginNode.type == "text") {
|
||||
final textItem = beginNode as TextNode;
|
||||
final tb = TransactionBuilder(editorState);
|
||||
final tb = editorState.transaction;
|
||||
final len = selection.end.offset - selection.start.offset;
|
||||
tb.textEdit(
|
||||
tb.updateText(
|
||||
textItem,
|
||||
() => Delta()
|
||||
Delta()
|
||||
..retain(selection.start.offset)
|
||||
..delete(len));
|
||||
tb.setAfterSelection(Selection.collapsed(selection.start));
|
||||
tb.commit();
|
||||
tb.afterSelection = Selection.collapsed(selection.start);
|
||||
editorState.commit();
|
||||
return;
|
||||
}
|
||||
final traverser = NodeIterator(
|
||||
@ -325,13 +324,13 @@ void _deleteSelectedContent(EditorState editorState) {
|
||||
startNode: beginNode,
|
||||
endNode: endNode,
|
||||
);
|
||||
final tb = TransactionBuilder(editorState);
|
||||
final tb = editorState.transaction;
|
||||
while (traverser.moveNext()) {
|
||||
final item = traverser.current;
|
||||
if (item.type == "text" && beginNode == item) {
|
||||
final textItem = item as TextNode;
|
||||
final deleteLen = textItem.delta.length - selection.start.offset;
|
||||
tb.textEdit(textItem, () {
|
||||
tb.updateText(textItem, () {
|
||||
final delta = Delta()
|
||||
..retain(selection.start.offset)
|
||||
..delete(deleteLen);
|
||||
@ -342,13 +341,13 @@ void _deleteSelectedContent(EditorState editorState) {
|
||||
}
|
||||
|
||||
return delta;
|
||||
});
|
||||
}());
|
||||
} else {
|
||||
tb.deleteNode(item);
|
||||
}
|
||||
}
|
||||
tb.setAfterSelection(Selection.collapsed(selection.start));
|
||||
tb.commit();
|
||||
tb.afterSelection = Selection.collapsed(selection.start);
|
||||
editorState.commit();
|
||||
}
|
||||
|
||||
ShortcutEventHandler copyEventHandler = (editorState, event) {
|
||||
|
@ -39,7 +39,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
final afterSelection = Selection.collapsed(
|
||||
Position(path: textNodes.first.path.next, offset: 0),
|
||||
);
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(
|
||||
textNodes.first,
|
||||
selection.start.offset,
|
||||
@ -51,8 +51,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
0,
|
||||
selection.end.offset,
|
||||
)
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
..afterSelection = afterSelection;
|
||||
editorState.commit();
|
||||
|
||||
if (startNode is TextNode &&
|
||||
startNode.subtype == BuiltInAttributeKey.numberList) {
|
||||
@ -77,12 +77,12 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
final afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.path, offset: 0),
|
||||
);
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..updateNode(textNode, {
|
||||
BuiltInAttributeKey.subtype: null,
|
||||
})
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
..afterSelection = afterSelection;
|
||||
editorState.commit();
|
||||
|
||||
final nextNode = textNode.next;
|
||||
if (nextNode is TextNode &&
|
||||
@ -105,13 +105,13 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
BuiltInAttributeKey.numberList;
|
||||
newNode.attributes[BuiltInAttributeKey.number] = prevNumber;
|
||||
final insertPath = textNode.path;
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..insertNode(
|
||||
insertPath,
|
||||
newNode,
|
||||
)
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
..afterSelection = afterSelection;
|
||||
editorState.commit();
|
||||
|
||||
makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
|
||||
beginNum: prevNumber);
|
||||
@ -120,7 +120,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
BuiltInAttributeKey.heading,
|
||||
BuiltInAttributeKey.quote,
|
||||
].contains(subtype);
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
textNode.copyWith(
|
||||
@ -129,8 +129,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
attributes: needCopyAttributes ? null : {},
|
||||
),
|
||||
)
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
..afterSelection = afterSelection;
|
||||
editorState.commit();
|
||||
}
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
@ -145,25 +145,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
Position(path: nextPath, offset: 0),
|
||||
);
|
||||
|
||||
final transactionBuilder = TransactionBuilder(editorState);
|
||||
transactionBuilder.insertNode(
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNode(
|
||||
textNode.path.next,
|
||||
textNode.copyWith(
|
||||
attributes: attributes,
|
||||
delta: textNode.delta.slice(selection.end.offset),
|
||||
),
|
||||
);
|
||||
transactionBuilder.deleteText(
|
||||
transaction.deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
textNode.toPlainText().length - selection.start.offset,
|
||||
);
|
||||
if (textNode.children.isNotEmpty) {
|
||||
final children = textNode.children.toList(growable: false);
|
||||
transactionBuilder.deleteNodes(children);
|
||||
transaction.deleteNodes(children);
|
||||
}
|
||||
transactionBuilder.afterSelection = afterSelection;
|
||||
transactionBuilder.commit();
|
||||
transaction.afterSelection = afterSelection;
|
||||
editorState.commit();
|
||||
|
||||
// If the new type of a text node is number list,
|
||||
// the numbers of the following nodes should be incremental.
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
||||
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
bool _isCodeStyle(TextNode textNode, int index) {
|
||||
@ -72,7 +73,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, lastBackquoteIndex, 1)
|
||||
..deleteText(textNode, firstBackquoteIndex, 2)
|
||||
..formatText(
|
||||
@ -88,8 +89,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
|
||||
path: textNode.path,
|
||||
offset: endIndex - 3,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
@ -103,7 +104,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
|
||||
// delete the backquote.
|
||||
// update the style of the text surround by ` ` to code.
|
||||
// and update the cursor position.
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, startIndex, 1)
|
||||
..formatText(
|
||||
textNode,
|
||||
@ -118,8 +119,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
|
||||
path: textNode.path,
|
||||
offset: endIndex - 1,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
@ -165,7 +166,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
|
||||
// delete the last three tildes.
|
||||
// update the style of the text surround by `~~ ~~` to strikethrough.
|
||||
// and update the cursor position.
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, lastTildeIndex, 1)
|
||||
..deleteText(textNode, thirdToLastTildeIndex, 2)
|
||||
..formatText(
|
||||
@ -181,8 +182,8 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
|
||||
path: textNode.path,
|
||||
offset: selection.end.offset - 3,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
@ -219,7 +220,7 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
|
||||
// update the href attribute of the text surrounded by [ ] to the url,
|
||||
// delete everything after the text,
|
||||
// and update the cursor position.
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, firstOpeningBracket, 1)
|
||||
..formatText(
|
||||
textNode,
|
||||
@ -236,8 +237,8 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
|
||||
path: textNode.path,
|
||||
offset: firstOpeningBracket + linkText!.length,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
@ -42,7 +42,7 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
|
||||
// delete the last three asterisks.
|
||||
// update the style of the text surround by `** **` to bold.
|
||||
// and update the cursor position.
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, lastAsterisIndex, 1)
|
||||
..deleteText(textNode, thirdToLastAsteriskIndex, 2)
|
||||
..formatText(
|
||||
@ -59,8 +59,8 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
|
||||
path: textNode.path,
|
||||
offset: selection.end.offset - 3,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
@ -108,7 +108,7 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
|
||||
// delete the last three underscores.
|
||||
// update the style of the text surround by `__ __` to bold.
|
||||
// and update the cursor position.
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, lastAsterisIndex, 1)
|
||||
..deleteText(textNode, thirdToLastUnderscoreIndex, 2)
|
||||
..formatText(
|
||||
@ -125,8 +125,8 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
|
||||
path: textNode.path,
|
||||
offset: selection.end.offset - 3,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
||||
|
||||
void makeFollowingNodesIncremental(
|
||||
@ -16,7 +15,7 @@ void makeFollowingNodesIncremental(
|
||||
int numPtr = beginNum + 1;
|
||||
var ptr = insertNode.next;
|
||||
|
||||
final builder = TransactionBuilder(editorState);
|
||||
final builder = editorState.transaction;
|
||||
|
||||
while (ptr != null) {
|
||||
if (ptr.subtype != BuiltInAttributeKey.numberList) {
|
||||
@ -34,5 +33,5 @@ void makeFollowingNodesIncremental(
|
||||
}
|
||||
|
||||
builder.afterSelection = afterSelection;
|
||||
builder.commit();
|
||||
editorState.commit();
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
|
||||
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
||||
@ -25,10 +25,9 @@ ShortcutEventHandler slashShortcutHandler = (editorState, event) {
|
||||
if (selection == null || context == null || selectable == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
TransactionBuilder(editorState)
|
||||
..replaceText(textNode, selection.start.offset,
|
||||
selection.end.offset - selection.start.offset, event.character ?? '')
|
||||
..commit();
|
||||
editorState.transaction.replaceText(textNode, selection.start.offset,
|
||||
selection.end.offset - selection.start.offset, event.character ?? '');
|
||||
editorState.commit();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_selectionMenuService =
|
||||
|
@ -15,9 +15,8 @@ ShortcutEventHandler tabHandler = (editorState, event) {
|
||||
final previous = textNode.previous;
|
||||
|
||||
if (textNode.subtype != BuiltInAttributeKey.bulletedList) {
|
||||
TransactionBuilder(editorState)
|
||||
..insertText(textNode, selection.end.offset, ' ' * 4)
|
||||
..commit();
|
||||
editorState.transaction.insertText(textNode, selection.end.offset, ' ' * 4);
|
||||
editorState.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@ -31,11 +30,11 @@ ShortcutEventHandler tabHandler = (editorState, event) {
|
||||
start: selection.start.copyWith(path: path),
|
||||
end: selection.end.copyWith(path: path),
|
||||
);
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteNode(textNode)
|
||||
..insertNode(path, textNode)
|
||||
..setAfterSelection(afterSelection)
|
||||
..commit();
|
||||
..afterSelection = afterSelection;
|
||||
editorState.commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -6,7 +7,6 @@ import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/location/position.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import './number_list_helper.dart';
|
||||
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
|
||||
|
||||
@ -99,15 +99,14 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
|
||||
));
|
||||
|
||||
final insertPath = textNode.path;
|
||||
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, 0, matchText.length)
|
||||
..updateNode(textNode, {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
|
||||
BuiltInAttributeKey.number: numValue
|
||||
})
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
..afterSelection = afterSelection;
|
||||
editorState.commit();
|
||||
|
||||
makeFollowingNodesIncremental(editorState, insertPath, afterSelection);
|
||||
|
||||
@ -118,7 +117,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
|
||||
if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, 0, 1)
|
||||
..updateNode(textNode, {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
|
||||
@ -128,8 +127,8 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
|
||||
path: textNode.path,
|
||||
offset: 0,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@ -151,7 +150,7 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
|
||||
check = false;
|
||||
}
|
||||
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, 0, symbol.length)
|
||||
..updateNode(textNode, {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
|
||||
@ -162,8 +161,8 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
|
||||
path: textNode.path,
|
||||
offset: 0,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@ -177,7 +176,7 @@ KeyEventResult _toHeadingStyle(
|
||||
if (textNode.attributes.heading == hX) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
TransactionBuilder(editorState)
|
||||
editorState.transaction
|
||||
..deleteText(textNode, 0, x)
|
||||
..updateNode(textNode, {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
|
||||
@ -188,8 +187,8 @@ KeyEventResult _toHeadingStyle(
|
||||
path: textNode.path,
|
||||
offset: 0,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
);
|
||||
editorState.commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,7 @@ import 'dart:collection';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/infra/log.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/operation.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/transaction.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
|
||||
/// A [HistoryItem] contains list of operations committed by users.
|
||||
@ -39,7 +38,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
|
||||
|
||||
/// Create a new [Transaction] by inverting the operations.
|
||||
Transaction toTransaction(EditorState state) {
|
||||
final builder = TransactionBuilder(state);
|
||||
final builder = Transaction(document: state.document);
|
||||
for (var i = operations.length - 1; i >= 0; i--) {
|
||||
final operation = operations[i];
|
||||
final inverted = operation.invert();
|
||||
@ -47,7 +46,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
|
||||
}
|
||||
builder.afterSelection = beforeSelection;
|
||||
builder.beforeSelection = afterSelection;
|
||||
return builder.finish();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ import 'dart:collection';
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:appflowy_editor/src/core/transform/operation.dart';
|
||||
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:appflowy_editor/src/core/document/document.dart';
|
||||
|
||||
@ -48,25 +47,26 @@ void main() {
|
||||
final item2 = Node(type: "node", attributes: {}, children: LinkedList());
|
||||
final item3 = Node(type: "node", attributes: {}, children: LinkedList());
|
||||
final root = Node(
|
||||
type: "root",
|
||||
attributes: {},
|
||||
children: LinkedList()
|
||||
..addAll([
|
||||
item1,
|
||||
item2,
|
||||
item3,
|
||||
]));
|
||||
type: "root",
|
||||
attributes: {},
|
||||
children: LinkedList()
|
||||
..addAll([
|
||||
item1,
|
||||
item2,
|
||||
item3,
|
||||
]),
|
||||
);
|
||||
final state = EditorState(document: Document(root: root));
|
||||
|
||||
expect(item1.path, [0]);
|
||||
expect(item2.path, [1]);
|
||||
expect(item3.path, [2]);
|
||||
|
||||
final tb = TransactionBuilder(state);
|
||||
tb.deleteNode(item1);
|
||||
tb.deleteNode(item2);
|
||||
tb.deleteNode(item3);
|
||||
final transaction = tb.finish();
|
||||
final transaction = state.transaction;
|
||||
transaction.deleteNode(item1);
|
||||
transaction.deleteNode(item2);
|
||||
transaction.deleteNode(item3);
|
||||
state.commit();
|
||||
expect(transaction.operations[0].path, [0]);
|
||||
expect(transaction.operations[1].path, [0]);
|
||||
expect(transaction.operations[2].path, [0]);
|
||||
@ -77,10 +77,9 @@ void main() {
|
||||
final state = EditorState(document: Document(root: root));
|
||||
|
||||
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
|
||||
final tb = TransactionBuilder(state);
|
||||
tb.insertNode([0], item1);
|
||||
|
||||
final transaction = tb.finish();
|
||||
final transaction = state.transaction;
|
||||
transaction.insertNode([0], item1);
|
||||
state.commit();
|
||||
expect(transaction.toJson(), {
|
||||
"operations": [
|
||||
{
|
||||
@ -94,16 +93,17 @@ void main() {
|
||||
test("delete", () {
|
||||
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
|
||||
final root = Node(
|
||||
type: "root",
|
||||
attributes: {},
|
||||
children: LinkedList()
|
||||
..addAll([
|
||||
item1,
|
||||
]));
|
||||
type: "root",
|
||||
attributes: {},
|
||||
children: LinkedList()
|
||||
..addAll([
|
||||
item1,
|
||||
]),
|
||||
);
|
||||
final state = EditorState(document: Document(root: root));
|
||||
final tb = TransactionBuilder(state);
|
||||
tb.deleteNode(item1);
|
||||
final transaction = tb.finish();
|
||||
final transaction = state.transaction;
|
||||
transaction.deleteNode(item1);
|
||||
state.commit();
|
||||
expect(transaction.toJson(), {
|
||||
"operations": [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user