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