refactor: abstract the common transaction to commands

This commit is contained in:
Lucas.Xu 2022-10-13 19:46:22 +08:00
parent 71b1769eee
commit f3eeb471e7
32 changed files with 470 additions and 510 deletions

View File

@ -26,9 +26,9 @@ ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
if (selection.isCollapsed) { if (selection.isCollapsed) {
editorState.transaction final transaction = editorState.transaction
.insertText(codeBlockNode.first, selection.end.offset, '\n'); ..insertText(codeBlockNode.first, selection.end.offset, '\n');
editorState.commit(); editorState.apply(transaction);
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) {
editorState.transaction final transaction = 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;
editorState.commit(); editorState.apply(transaction);
} else { } else {
editorState.transaction final transaction = editorState.transaction
..insertNode( ..insertNode(
selection.end.path.next, selection.end.path.next,
TextNode( TextNode(
@ -84,7 +84,7 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
), ),
) )
..afterSelection = selection; ..afterSelection = selection;
editorState.commit(); editorState.apply(transaction);
} }
}, },
); );
@ -181,10 +181,11 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
child: DropdownButton<String>( child: DropdownButton<String>(
value: _detectLanguage, value: _detectLanguage,
onChanged: (value) { onChanged: (value) {
widget.editorState.transaction.updateNode(widget.textNode, { final transaction = widget.editorState.transaction
..updateNode(widget.textNode, {
'language': value, 'language': value,
}); });
widget.editorState.commit(); widget.editorState.apply(transaction);
}, },
items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) { items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(

View File

@ -18,7 +18,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
} }
final textNode = textNodes.first; final textNode = textNodes.first;
if (textNode.toPlainText() == '--') { if (textNode.toPlainText() == '--') {
editorState.transaction final transaction = editorState.transaction
..deleteText(textNode, 0, 2) ..deleteText(textNode, 0, 2)
..insertNode( ..insertNode(
textNode.path, textNode.path,
@ -30,7 +30,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
) )
..afterSelection = ..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0); Selection.single(path: textNode.path.next, startOffset: 0);
editorState.commit(); editorState.apply(transaction);
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) {
editorState.transaction final transaction = editorState.transaction
..insertNode( ..insertNode(
textNode.path, textNode.path,
Node( Node(
@ -65,9 +65,9 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
) )
..afterSelection = ..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0); Selection.single(path: textNode.path.next, startOffset: 0);
editorState.commit(); editorState.apply(transaction);
} else { } else {
editorState.transaction final transaction = editorState.transaction
..insertNode( ..insertNode(
selection.end.path.next, selection.end.path.next,
TextNode( TextNode(
@ -79,7 +79,7 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
), ),
) )
..afterSelection = selection; ..afterSelection = selection;
editorState.commit(); editorState.apply(transaction);
} }
}, },
); );

View File

@ -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;
editorState.transaction final transaction = editorState.transaction
..insertNode( ..insertNode(
selection.end.path, selection.end.path,
Node( Node(
@ -34,10 +34,10 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
) )
..deleteNode(textNodes.first) ..deleteNode(textNodes.first)
..afterSelection = selection; ..afterSelection = selection;
editorState.commit(); editorState.apply(transaction);
} else { } else {
texNodePath = selection.end.path.next; texNodePath = selection.end.path.next;
editorState.transaction final transaction = editorState.transaction
..insertNode( ..insertNode(
selection.end.path.next, selection.end.path.next,
Node( Node(
@ -47,7 +47,7 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
), ),
) )
..afterSelection = selection; ..afterSelection = selection;
editorState.commit(); editorState.apply(transaction);
} }
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final texState = final texState =
@ -142,8 +142,9 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
size: 16, size: 16,
), ),
onPressed: () { onPressed: () {
widget.editorState.transaction.deleteNode(widget.node); final transaction = widget.editorState.transaction
widget.editorState.commit(); ..deleteNode(widget.node);
widget.editorState.apply(transaction);
}, },
), ),
); );
@ -174,11 +175,12 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
if (controller.text != _tex) { if (controller.text != _tex) {
widget.editorState.transaction.updateNode( final transaction = widget.editorState.transaction
..updateNode(
widget.node, widget.node,
{'tex': controller.text}, {'tex': controller.text},
); );
widget.editorState.commit(); widget.editorState.apply(transaction);
} }
}, },
child: const Text('OK'), child: const Text('OK'),

View File

@ -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.
editorState.transaction final transaction = editorState.transaction
..deleteText(textNode, firstUnderscore, 1) ..deleteText(textNode, firstUnderscore, 1)
..formatText( ..formatText(
textNode, textNode,
@ -47,7 +47,7 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
offset: selection.end.offset - 1, offset: selection.end.offset - 1,
), ),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -0,0 +1,20 @@
import 'package:appflowy_editor/src/commands/command_extension.dart';
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/editor_state.dart';
extension TextCommands on EditorState {
Future<void> updateNodeAttributes(
Attributes attributes, {
Path? path,
Node? node,
}) {
return futureCommand(() {
final n = getNode(path: path, node: node);
apply(
transaction..updateNode(n, attributes),
);
});
}
}

View File

@ -0,0 +1,54 @@
import 'dart:async';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/document/path.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:flutter/widgets.dart';
extension CommandExtension on EditorState {
Future<void> futureCommand(void Function() fn) async {
final completer = Completer<void>();
fn();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}
Node getNode({
Path? path,
Node? node,
}) {
if (node != null) {
return node;
} else if (path != null) {
return document.nodeAtPath(path)!;
}
throw Exception('path and node cannot be null at the same time');
}
TextNode getTextNode({
Path? path,
TextNode? textNode,
}) {
if (textNode != null) {
return textNode;
} else if (path != null) {
return document.nodeAtPath(path)! as TextNode;
}
throw Exception('path and node cannot be null at the same time');
}
Selection getSelection(
Selection? selection,
) {
final currentSelection = service.selectionService.currentSelection.value;
if (selection != null) {
return selection;
} else if (currentSelection != null) {
return currentSelection;
}
throw Exception('path and textNode cannot be null at the same time');
}
}

View File

@ -1,33 +0,0 @@
import 'dart:async';
import 'package:appflowy_editor/src/commands/text_command_infra.dart';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/document/path.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:flutter/widgets.dart';
Future<void> insertContextInText(
EditorState editorState,
int index,
String content, {
Path? path,
TextNode? textNode,
}) async {
final result = getTextNodeToBeFormatted(
editorState,
path: path,
textNode: textNode,
);
final completer = Completer<void>();
editorState.transaction.insertText(result, index, content);
editorState.commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}

View File

@ -1,83 +0,0 @@
import 'package:appflowy_editor/src/commands/format_text.dart';
import 'package:appflowy_editor/src/commands/text_command_infra.dart';
import 'package:appflowy_editor/src/core/document/attributes.dart';
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.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/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
Future<void> formatBuiltInTextAttributes(
EditorState editorState,
String key,
Attributes attributes, {
Selection? selection,
Path? path,
TextNode? textNode,
}) async {
final result = getTextNodeToBeFormatted(
editorState,
path: path,
textNode: textNode,
);
if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
// remove all the existing style
final newAttributes = result.attributes
..removeWhere((key, value) {
if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
return true;
}
return false;
})
..addAll(attributes)
..addAll({
BuiltInAttributeKey.subtype: key,
});
return updateTextNodeAttributes(
editorState,
newAttributes,
textNode: textNode,
);
} else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) {
return updateTextNodeDeltaAttributes(
editorState,
selection,
attributes,
textNode: textNode,
);
}
}
Future<void> formatTextToCheckbox(
EditorState editorState,
bool check, {
Path? path,
TextNode? textNode,
}) async {
return formatBuiltInTextAttributes(
editorState,
BuiltInAttributeKey.checkbox,
{
BuiltInAttributeKey.checkbox: check,
},
path: path,
textNode: textNode,
);
}
Future<void> formatLinkInText(
EditorState editorState,
String? link, {
Path? path,
TextNode? textNode,
}) async {
return formatBuiltInTextAttributes(
editorState,
BuiltInAttributeKey.href,
{
BuiltInAttributeKey.href: link,
},
path: path,
textNode: textNode,
);
}

View File

@ -1,64 +0,0 @@
import 'dart:async';
import 'package:appflowy_editor/src/commands/text_command_infra.dart';
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/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:flutter/widgets.dart';
Future<void> updateTextNodeAttributes(
EditorState editorState,
Attributes attributes, {
Path? path,
TextNode? textNode,
}) async {
final result = getTextNodeToBeFormatted(
editorState,
path: path,
textNode: textNode,
);
final completer = Completer<void>();
editorState.transaction.updateNode(result, attributes);
editorState.commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}
Future<void> updateTextNodeDeltaAttributes(
EditorState editorState,
Selection? selection,
Attributes attributes, {
Path? path,
TextNode? textNode,
}) {
final result = getTextNodeToBeFormatted(
editorState,
path: path,
textNode: textNode,
);
final newSelection = getSelection(editorState, selection: selection);
final completer = Completer<void>();
editorState.transaction.formatText(
result,
newSelection.startIndex,
newSelection.length,
attributes,
);
editorState.commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}

View File

@ -0,0 +1,98 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/commands/command_extension.dart';
extension TextCommands on EditorState {
/// Insert text at the given index of the given [TextNode] or the [Path].
///
/// [Path] and [TextNode] are mutually exclusive.
/// One of these two parameters must have a value.
Future<void> insertText(
int index,
String text, {
Path? path,
TextNode? textNode,
}) async {
return futureCommand(() {
final n = getTextNode(path: path, textNode: textNode);
apply(
transaction..insertText(n, index, text),
);
});
}
Future<void> formatText(
EditorState editorState,
Selection? selection,
Attributes attributes, {
Path? path,
TextNode? textNode,
}) async {
return futureCommand(() {
final n = getTextNode(path: path, textNode: textNode);
final s = getSelection(selection);
apply(
transaction..formatText(n, s.startIndex, s.length, attributes),
);
});
}
Future<void> formatTextWithBuiltInAttribute(
EditorState editorState,
String key,
Attributes attributes, {
Selection? selection,
Path? path,
TextNode? textNode,
}) async {
return futureCommand(() {
final n = getTextNode(path: path, textNode: textNode);
if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
final attr = n.attributes
..removeWhere(
(key, _) => BuiltInAttributeKey.globalStyleKeys.contains(key))
..addAll(attributes)
..addAll({
BuiltInAttributeKey.subtype: key,
});
apply(
transaction..updateNode(n, attr),
);
} else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) {
final s = getSelection(selection);
apply(
transaction..formatText(n, s.startIndex, s.length, attributes),
);
}
});
}
Future<void> formatTextToCheckbox(
EditorState editorState,
bool check, {
Path? path,
TextNode? textNode,
}) async {
return formatTextWithBuiltInAttribute(
editorState,
BuiltInAttributeKey.checkbox,
{BuiltInAttributeKey.checkbox: check},
path: path,
textNode: textNode,
);
}
Future<void> formatLinkInText(
EditorState editorState,
String? link, {
Path? path,
TextNode? textNode,
}) async {
return formatTextWithBuiltInAttribute(
editorState,
BuiltInAttributeKey.href,
{BuiltInAttributeKey.href: link},
path: path,
textNode: textNode,
);
}
}

View File

@ -1,43 +0,0 @@
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/document/path.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
// get formatted [TextNode]
TextNode getTextNodeToBeFormatted(
EditorState editorState, {
Path? path,
TextNode? textNode,
}) {
final currentSelection =
editorState.service.selectionService.currentSelection.value;
TextNode result;
if (textNode != null) {
result = textNode;
} else if (path != null) {
result = editorState.document.nodeAtPath(path) as TextNode;
} else if (currentSelection != null && currentSelection.isCollapsed) {
result = editorState.document.nodeAtPath(currentSelection.start.path)
as TextNode;
} else {
throw Exception('path and textNode cannot be null at the same time');
}
return result;
}
Selection getSelection(
EditorState editorState, {
Selection? selection,
}) {
final currentSelection =
editorState.service.selectionService.currentSelection.value;
Selection result;
if (selection != null) {
result = selection;
} else if (currentSelection != null) {
result = currentSelection;
} else {
throw Exception('path and textNode cannot be null at the same time');
}
return result;
}

View File

@ -119,7 +119,7 @@ class Transaction {
/// ///
/// Also, this method will transform the path of the operations /// Also, this method will transform the path of the operations
/// to avoid conflicts. /// to avoid conflicts.
add(Operation op, {bool transform = true}) { void add(Operation op, {bool transform = true}) {
final Operation? last = operations.isEmpty ? null : operations.last; final Operation? last = operations.isEmpty ? null : operations.last;
if (last != null) { if (last != null) {
if (op is UpdateTextOperation && if (op is UpdateTextOperation &&
@ -199,7 +199,7 @@ extension TextTransaction on Transaction {
} }
/// Assigns a formatting attributes to a range of text. /// Assigns a formatting attributes to a range of text.
formatText( void formatText(
TextNode textNode, TextNode textNode,
int index, int index,
int length, int length,
@ -215,7 +215,7 @@ extension TextTransaction on Transaction {
} }
/// Deletes the text of specified length starting at index. /// Deletes the text of specified length starting at index.
deleteText( void deleteText(
TextNode textNode, TextNode textNode,
int index, int index,
int length, int length,
@ -235,7 +235,7 @@ extension TextTransaction on Transaction {
/// ///
/// Optionally, you may specify formatting attributes that are applied to the inserted string. /// 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. /// By default, the formatting attributes before the insert position will be reused.
replaceText( void replaceText(
TextNode textNode, TextNode textNode,
int index, int index,
int length, int length,

View File

@ -75,21 +75,9 @@ class EditorState {
bool editable = true; bool editable = true;
Transaction get transaction { Transaction get transaction {
if (_transaction != null) { final transaction = Transaction(document: document);
return _transaction!; transaction.beforeSelection = _cursorSelection;
} 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 {
@ -131,7 +119,7 @@ class EditorState {
/// The options can be used to determine whether the editor /// The options can be used to determine whether the editor
/// should record the transaction in undo/redo stack. /// should record the transaction in undo/redo stack.
apply(Transaction transaction, apply(Transaction transaction,
[ApplyOptions options = const ApplyOptions()]) { [ApplyOptions options = const ApplyOptions(recordUndo: true)]) {
if (!editable) { if (!editable) {
return; return;
} }

View File

@ -24,20 +24,23 @@ class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
RichClipboard.setData(RichClipboardData(text: src)); RichClipboard.setData(RichClipboardData(text: src));
}, },
onDelete: () { onDelete: () {
context.editorState.transaction.deleteNode(context.node); final transaction = context.editorState.transaction
context.editorState.commit(); ..deleteNode(context.node);
context.editorState.apply(transaction);
}, },
onAlign: (alignment) { onAlign: (alignment) {
context.editorState.transaction.updateNode(context.node, { final transaction = context.editorState.transaction
..updateNode(context.node, {
'align': _alignmentToText(alignment), 'align': _alignmentToText(alignment),
}); });
context.editorState.commit(); context.editorState.apply(transaction);
}, },
onResize: (width) { onResize: (width) {
context.editorState.transaction.updateNode(context.node, { final transaction = context.editorState.transaction
..updateNode(context.node, {
'width': width, 'width': width,
}); });
context.editorState.commit(); context.editorState.apply(transaction);
}, },
); );
} }

View File

@ -195,6 +195,6 @@ extension on EditorState {
selection.start.path, selection.start.path,
imageNode, imageNode,
); );
commit(); apply(transaction);
} }
} }

View File

@ -1,5 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/commands/format_built_in_text.dart'; import 'package:appflowy_editor/src/commands/text/text_commands.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
@ -75,7 +75,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
name: check ? 'check' : 'uncheck', name: check ? 'check' : 'uncheck',
), ),
onTap: () async { onTap: () async {
await formatTextToCheckbox( await widget.editorState.formatTextToCheckbox(
widget.editorState, widget.editorState,
!check, !check,
textNode: widget.textNode, textNode: widget.textNode,

View File

@ -45,12 +45,13 @@ 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('/');
editorState.transaction.deleteText( final transaction = editorState.transaction
..deleteText(
node, node,
start, start,
selection.start.offset - start, selection.start.offset - start,
); );
editorState.commit(); editorState.apply(transaction);
} }
} }
} }
@ -277,12 +278,13 @@ 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();
widget.editorState.transaction.deleteText( final transaction = widget.editorState.transaction
..deleteText(
nodes.first as TextNode, nodes.first as TextNode,
selection.start.offset - length, selection.start.offset - length,
length, length,
); );
widget.editorState.commit(); widget.editorState.apply(transaction);
} }
} }
@ -293,12 +295,13 @@ 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();
widget.editorState.transaction.insertText( final transaction = widget.editorState.transaction
..insertText(
nodes.first as TextNode, nodes.first as TextNode,
selection.end.offset, selection.end.offset,
text, text,
); );
widget.editorState.commit(); widget.editorState.apply(transaction);
} }
} }
} }

View File

@ -1,5 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/commands/format_built_in_text.dart'; import 'package:appflowy_editor/src/commands/text/text_commands.dart';
import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
@ -349,7 +349,11 @@ void showLinkMenu(
await safeLaunchUrl(linkText); await safeLaunchUrl(linkText);
}, },
onSubmitted: (text) async { onSubmitted: (text) async {
await formatLinkInText(editorState, text, textNode: textNode); await editorState.formatLinkInText(
editorState,
text,
textNode: textNode,
);
_dismissLinkMenu(); _dismissLinkMenu();
}, },
onCopyLink: () { onCopyLink: () {
@ -357,9 +361,14 @@ void showLinkMenu(
_dismissLinkMenu(); _dismissLinkMenu();
}, },
onRemoveLink: () { onRemoveLink: () {
editorState.transaction.formatText( final transaction = editorState.transaction
textNode, index, length, {BuiltInAttributeKey.href: null}); ..formatText(
editorState.commit(); textNode,
index,
length,
{BuiltInAttributeKey.href: null},
);
editorState.apply(transaction);
_dismissLinkMenu(); _dismissLinkMenu();
}, },
onFocusChange: (value) { onFocusChange: (value) {

View File

@ -47,7 +47,7 @@ bool insertTextNodeAfterSelection(
formatTextNodes(editorState, attributes); formatTextNodes(editorState, attributes);
} else { } else {
final next = selection.end.path.next; final next = selection.end.path.next;
editorState.transaction final transaction = editorState.transaction
..insertNode( ..insertNode(
next, next,
TextNode.empty(attributes: attributes), TextNode.empty(attributes: attributes),
@ -55,7 +55,7 @@ bool insertTextNodeAfterSelection(
..afterSelection = Selection.collapsed( ..afterSelection = Selection.collapsed(
Position(path: next, offset: 0), Position(path: next, offset: 0),
); );
editorState.commit(); editorState.apply(transaction);
} }
return true; return true;
@ -122,7 +122,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
); );
} }
editorState.commit(); editorState.apply(transaction);
return true; return true;
} }
@ -240,7 +240,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
} }
} }
editorState.commit(); editorState.apply(transaction);
return true; return true;
} }

View File

@ -160,12 +160,13 @@ 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;
_editorState.transaction.insertText( final transaction = _editorState.transaction;
transaction.insertText(
textNode, textNode,
delta.insertionOffset, delta.insertionOffset,
delta.textInserted, delta.textInserted,
); );
_editorState.commit(); _editorState.apply(transaction);
} else { } else {
// TODO: implement // TODO: implement
} }
@ -180,9 +181,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;
_editorState.transaction final transaction = _editorState.transaction;
.deleteText(textNode, delta.deletedRange.start, length); transaction.deleteText(textNode, delta.deletedRange.start, length);
_editorState.commit(); _editorState.apply(transaction);
} else { } else {
// TODO: implement // TODO: implement
} }
@ -197,9 +198,10 @@ 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;
_editorState.transaction.replaceText( final transaction = _editorState.transaction;
transaction.replaceText(
textNode, delta.replacedRange.start, length, delta.replacementText); textNode, delta.replacedRange.start, length, delta.replacementText);
_editorState.commit(); _editorState.apply(transaction);
} else { } else {
// TODO: implement // TODO: implement
} }

View File

@ -86,13 +86,13 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
if (nonTextNodes.isNotEmpty) { if (nonTextNodes.isNotEmpty) {
transaction.afterSelection = Selection.collapsed(selection.start); transaction.afterSelection = Selection.collapsed(selection.start);
} }
editorState.commit(); editorState.apply(transaction);
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(transaction, textNodes, selection); _deleteTextNodes(transaction, textNodes, selection);
editorState.commit(); editorState.apply(transaction);
if (nodeAtStart is TextNode && if (nodeAtStart is TextNode &&
nodeAtStart.subtype == BuiltInAttributeKey.numberList) { nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
@ -109,7 +109,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
if (nonTextNodes.isNotEmpty) { if (nonTextNodes.isNotEmpty) {
transaction.afterSelection = Selection.collapsed(selection.start); transaction.afterSelection = Selection.collapsed(selection.start);
} }
editorState.commit(); editorState.apply(transaction);
} }
if (cancelNumberListPath != null) { if (cancelNumberListPath != null) {
@ -140,7 +140,7 @@ KeyEventResult _backDeleteToPreviousTextNode(
..afterSelection = Selection.collapsed( ..afterSelection = Selection.collapsed(
Position(path: textNode.parent!.path.next, offset: 0), Position(path: textNode.parent!.path.next, offset: 0),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -171,7 +171,7 @@ KeyEventResult _backDeleteToPreviousTextNode(
if (nonTextNodes.isNotEmpty) { if (nonTextNodes.isNotEmpty) {
transaction.afterSelection = Selection.collapsed(selection.start); transaction.afterSelection = Selection.collapsed(selection.start);
} }
editorState.commit(); editorState.apply(transaction);
} }
if (prevIsNumberList) { if (prevIsNumberList) {
@ -223,12 +223,12 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
selection.end.offset - selection.start.offset, selection.end.offset - selection.start.offset,
); );
} }
editorState.commit(); editorState.apply(transaction);
} 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(transaction, textNodes, selection); _deleteTextNodes(transaction, textNodes, selection);
editorState.commit(); editorState.apply(transaction);
if (nodeAtStart is TextNode && if (nodeAtStart is TextNode &&
nodeAtStart.subtype == BuiltInAttributeKey.numberList) { nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
@ -250,7 +250,7 @@ KeyEventResult _mergeNextLineIntoThisLine(EditorState editorState,
transaction.mergeText(textNode, nextNode); transaction.mergeText(textNode, nextNode);
} }
transaction.deleteNode(nextNode); transaction.deleteNode(nextNode);
editorState.commit(); editorState.apply(transaction);
if (textNode.subtype == BuiltInAttributeKey.numberList) { if (textNode.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental(editorState, textNode.path, selection); makeFollowingNodesIncremental(editorState, textNode.path, selection);

View File

@ -94,7 +94,7 @@ void _pasteHTML(EditorState editorState, String html) {
textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta); textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta);
tb.afterSelection = (Selection.collapsed(Position( tb.afterSelection = (Selection.collapsed(Position(
path: path, offset: startOffset + firstTextNode.delta.length))); path: path, offset: startOffset + firstTextNode.delta.length)));
editorState.commit(); editorState.apply(tb);
return; return;
} }
} }
@ -147,7 +147,7 @@ void _pasteMultipleLinesInText(
tb.afterSelection = afterSelection; tb.afterSelection = afterSelection;
tb.insertNodes(path, tailNodes); tb.insertNodes(path, tailNodes);
editorState.commit(); editorState.apply(tb);
if (startNumber != null) { if (startNumber != null) {
makeFollowingNodesIncremental(editorState, originalPath, afterSelection, makeFollowingNodesIncremental(editorState, originalPath, afterSelection,
@ -162,7 +162,7 @@ void _pasteMultipleLinesInText(
path[path.length - 1]++; path[path.length - 1]++;
tb.afterSelection = afterSelection; tb.afterSelection = afterSelection;
tb.insertNodes(path, nodes); tb.insertNodes(path, nodes);
editorState.commit(); editorState.apply(tb);
} }
void _handlePaste(EditorState editorState) async { void _handlePaste(EditorState editorState) async {
@ -195,7 +195,7 @@ 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;
editorState.transaction final transaction = editorState.transaction
..updateText( ..updateText(
node, node,
Delta() Delta()
@ -203,7 +203,7 @@ void _pasteSingleLine(
..addAll(_lineContentToDelta(line))) ..addAll(_lineContentToDelta(line)))
..afterSelection = (Selection.collapsed( ..afterSelection = (Selection.collapsed(
Position(path: selection.end.path, offset: beginOffset + line.length))); Position(path: selection.end.path, offset: beginOffset + line.length)));
editorState.commit(); editorState.apply(transaction);
} }
/// parse url from the line text /// parse url from the line text
@ -287,7 +287,7 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
// insert remains // insert remains
tb.insertNodes(path, nodes); tb.insertNodes(path, nodes);
tb.afterSelection = afterSelection; tb.afterSelection = afterSelection;
editorState.commit(); editorState.apply(tb);
} }
} }
@ -316,7 +316,7 @@ void _deleteSelectedContent(EditorState editorState) {
..retain(selection.start.offset) ..retain(selection.start.offset)
..delete(len)); ..delete(len));
tb.afterSelection = Selection.collapsed(selection.start); tb.afterSelection = Selection.collapsed(selection.start);
editorState.commit(); editorState.apply(tb);
return; return;
} }
final traverser = NodeIterator( final traverser = NodeIterator(
@ -347,7 +347,7 @@ void _deleteSelectedContent(EditorState editorState) {
} }
} }
tb.afterSelection = Selection.collapsed(selection.start); tb.afterSelection = Selection.collapsed(selection.start);
editorState.commit(); editorState.apply(tb);
} }
ShortcutEventHandler copyEventHandler = (editorState, event) { ShortcutEventHandler copyEventHandler = (editorState, event) {

View File

@ -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),
); );
editorState.transaction final transaction = editorState.transaction
..deleteText( ..deleteText(
textNodes.first, textNodes.first,
selection.start.offset, selection.start.offset,
@ -52,7 +52,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
selection.end.offset, selection.end.offset,
) )
..afterSelection = afterSelection; ..afterSelection = afterSelection;
editorState.commit(); editorState.apply(transaction);
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),
); );
editorState.transaction final transaction = editorState.transaction
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: null, BuiltInAttributeKey.subtype: null,
}) })
..afterSelection = afterSelection; ..afterSelection = afterSelection;
editorState.commit(); editorState.apply(transaction);
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;
editorState.transaction final transaction = editorState.transaction
..insertNode( ..insertNode(
insertPath, insertPath,
newNode, newNode,
) )
..afterSelection = afterSelection; ..afterSelection = afterSelection;
editorState.commit(); editorState.apply(transaction);
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);
editorState.transaction final transaction = editorState.transaction
..insertNode( ..insertNode(
textNode.path, textNode.path,
textNode.copyWith( textNode.copyWith(
@ -130,7 +130,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
), ),
) )
..afterSelection = afterSelection; ..afterSelection = afterSelection;
editorState.commit(); editorState.apply(transaction);
} }
} }
return KeyEventResult.handled; return KeyEventResult.handled;
@ -163,7 +163,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
transaction.deleteNodes(children); transaction.deleteNodes(children);
} }
transaction.afterSelection = afterSelection; transaction.afterSelection = afterSelection;
editorState.commit(); editorState.apply(transaction);
// 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.

View File

@ -73,7 +73,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
editorState.transaction final transaction = editorState.transaction
..deleteText(textNode, lastBackquoteIndex, 1) ..deleteText(textNode, lastBackquoteIndex, 1)
..deleteText(textNode, firstBackquoteIndex, 2) ..deleteText(textNode, firstBackquoteIndex, 2)
..formatText( ..formatText(
@ -90,7 +90,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
offset: endIndex - 3, offset: endIndex - 3,
), ),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -104,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.
editorState.transaction final transaction = editorState.transaction
..deleteText(textNode, startIndex, 1) ..deleteText(textNode, startIndex, 1)
..formatText( ..formatText(
textNode, textNode,
@ -120,7 +120,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
offset: endIndex - 1, offset: endIndex - 1,
), ),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
}; };
@ -166,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.
editorState.transaction final transaction = editorState.transaction
..deleteText(textNode, lastTildeIndex, 1) ..deleteText(textNode, lastTildeIndex, 1)
..deleteText(textNode, thirdToLastTildeIndex, 2) ..deleteText(textNode, thirdToLastTildeIndex, 2)
..formatText( ..formatText(
@ -183,7 +183,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
offset: selection.end.offset - 3, offset: selection.end.offset - 3,
), ),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
}; };
@ -220,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.
editorState.transaction final transaction = editorState.transaction
..deleteText(textNode, firstOpeningBracket, 1) ..deleteText(textNode, firstOpeningBracket, 1)
..formatText( ..formatText(
textNode, textNode,
@ -238,7 +238,137 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
offset: firstOpeningBracket + linkText!.length, offset: firstOpeningBracket + linkText!.length,
), ),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled;
};
// convert **abc** to bold abc.
ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
if (selection == null || !selection.isSingle || textNodes.length != 1) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
final text = textNode.toPlainText().substring(0, selection.end.offset);
// make sure the last two characters are **.
if (text.length < 2 || text[selection.end.offset - 1] != '*') {
return KeyEventResult.ignored;
}
// find all the index of `*`.
final asteriskIndexes = <int>[];
for (var i = 0; i < text.length; i++) {
if (text[i] == '*') {
asteriskIndexes.add(i);
}
}
if (asteriskIndexes.length < 3) {
return KeyEventResult.ignored;
}
// make sure the second to last and third to last asterisks are connected.
final thirdToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 3];
final secondToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 2];
final lastAsterisIndex = asteriskIndexes[asteriskIndexes.length - 1];
if (secondToLastAsteriskIndex != thirdToLastAsteriskIndex + 1 ||
lastAsterisIndex == secondToLastAsteriskIndex + 1) {
return KeyEventResult.ignored;
}
// delete the last three asterisks.
// update the style of the text surround by `** **` to bold.
// and update the cursor position.
final transaction = editorState.transaction
..deleteText(textNode, lastAsterisIndex, 1)
..deleteText(textNode, thirdToLastAsteriskIndex, 2)
..formatText(
textNode,
thirdToLastAsteriskIndex,
selection.end.offset - thirdToLastAsteriskIndex - 3,
{
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.defaultFormating: true,
},
)
..afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: selection.end.offset - 3,
),
);
editorState.apply(transaction);
return KeyEventResult.handled;
};
// convert __abc__ to bold abc.
ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
if (selection == null || !selection.isSingle || textNodes.length != 1) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
final text = textNode.toPlainText().substring(0, selection.end.offset);
// make sure the last two characters are __.
if (text.length < 2 || text[selection.end.offset - 1] != '_') {
return KeyEventResult.ignored;
}
// find all the index of `_`.
final underscoreIndexes = <int>[];
for (var i = 0; i < text.length; i++) {
if (text[i] == '_') {
underscoreIndexes.add(i);
}
}
if (underscoreIndexes.length < 3) {
return KeyEventResult.ignored;
}
// make sure the second to last and third to last underscores are connected.
final thirdToLastUnderscoreIndex =
underscoreIndexes[underscoreIndexes.length - 3];
final secondToLastUnderscoreIndex =
underscoreIndexes[underscoreIndexes.length - 2];
final lastAsterisIndex = underscoreIndexes[underscoreIndexes.length - 1];
if (secondToLastUnderscoreIndex != thirdToLastUnderscoreIndex + 1 ||
lastAsterisIndex == secondToLastUnderscoreIndex + 1) {
return KeyEventResult.ignored;
}
// delete the last three underscores.
// update the style of the text surround by `__ __` to bold.
// and update the cursor position.
final transaction = editorState.transaction
..deleteText(textNode, lastAsterisIndex, 1)
..deleteText(textNode, thirdToLastUnderscoreIndex, 2)
..formatText(
textNode,
thirdToLastUnderscoreIndex,
selection.end.offset - thirdToLastUnderscoreIndex - 3,
{
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.defaultFormating: true,
},
)
..afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: selection.end.offset - 3,
),
);
editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -1,132 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
// convert **abc** to bold abc.
ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
if (selection == null || !selection.isSingle || textNodes.length != 1) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
final text = textNode.toPlainText().substring(0, selection.end.offset);
// make sure the last two characters are **.
if (text.length < 2 || text[selection.end.offset - 1] != '*') {
return KeyEventResult.ignored;
}
// find all the index of `*`.
final asteriskIndexes = <int>[];
for (var i = 0; i < text.length; i++) {
if (text[i] == '*') {
asteriskIndexes.add(i);
}
}
if (asteriskIndexes.length < 3) {
return KeyEventResult.ignored;
}
// make sure the second to last and third to last asterisks are connected.
final thirdToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 3];
final secondToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 2];
final lastAsterisIndex = asteriskIndexes[asteriskIndexes.length - 1];
if (secondToLastAsteriskIndex != thirdToLastAsteriskIndex + 1 ||
lastAsterisIndex == secondToLastAsteriskIndex + 1) {
return KeyEventResult.ignored;
}
// delete the last three asterisks.
// update the style of the text surround by `** **` to bold.
// and update the cursor position.
editorState.transaction
..deleteText(textNode, lastAsterisIndex, 1)
..deleteText(textNode, thirdToLastAsteriskIndex, 2)
..formatText(
textNode,
thirdToLastAsteriskIndex,
selection.end.offset - thirdToLastAsteriskIndex - 3,
{
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.defaultFormating: true,
},
)
..afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: selection.end.offset - 3,
),
);
editorState.commit();
return KeyEventResult.handled;
};
// convert __abc__ to bold abc.
ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
if (selection == null || !selection.isSingle || textNodes.length != 1) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
final text = textNode.toPlainText().substring(0, selection.end.offset);
// make sure the last two characters are __.
if (text.length < 2 || text[selection.end.offset - 1] != '_') {
return KeyEventResult.ignored;
}
// find all the index of `_`.
final underscoreIndexes = <int>[];
for (var i = 0; i < text.length; i++) {
if (text[i] == '_') {
underscoreIndexes.add(i);
}
}
if (underscoreIndexes.length < 3) {
return KeyEventResult.ignored;
}
// make sure the second to last and third to last underscores are connected.
final thirdToLastUnderscoreIndex =
underscoreIndexes[underscoreIndexes.length - 3];
final secondToLastUnderscoreIndex =
underscoreIndexes[underscoreIndexes.length - 2];
final lastAsterisIndex = underscoreIndexes[underscoreIndexes.length - 1];
if (secondToLastUnderscoreIndex != thirdToLastUnderscoreIndex + 1 ||
lastAsterisIndex == secondToLastUnderscoreIndex + 1) {
return KeyEventResult.ignored;
}
// delete the last three underscores.
// update the style of the text surround by `__ __` to bold.
// and update the cursor position.
editorState.transaction
..deleteText(textNode, lastAsterisIndex, 1)
..deleteText(textNode, thirdToLastUnderscoreIndex, 2)
..formatText(
textNode,
thirdToLastUnderscoreIndex,
selection.end.offset - thirdToLastUnderscoreIndex - 3,
{
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.defaultFormating: true,
},
)
..afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: selection.end.offset - 3,
),
);
editorState.commit();
return KeyEventResult.handled;
};

View File

@ -15,7 +15,7 @@ void makeFollowingNodesIncremental(
int numPtr = beginNum + 1; int numPtr = beginNum + 1;
var ptr = insertNode.next; var ptr = insertNode.next;
final builder = editorState.transaction; final transaction = editorState.transaction;
while (ptr != null) { while (ptr != null) {
if (ptr.subtype != BuiltInAttributeKey.numberList) { if (ptr.subtype != BuiltInAttributeKey.numberList) {
@ -25,13 +25,13 @@ void makeFollowingNodesIncremental(
if (currentNum != numPtr) { if (currentNum != numPtr) {
Attributes updateAttributes = {}; Attributes updateAttributes = {};
updateAttributes[BuiltInAttributeKey.number] = numPtr; updateAttributes[BuiltInAttributeKey.number] = numPtr;
builder.updateNode(ptr, updateAttributes); transaction.updateNode(ptr, updateAttributes);
} }
ptr = ptr.next; ptr = ptr.next;
numPtr++; numPtr++;
} }
builder.afterSelection = afterSelection; transaction.afterSelection = afterSelection;
editorState.commit(); editorState.apply(transaction);
} }

View File

@ -25,9 +25,10 @@ ShortcutEventHandler slashShortcutHandler = (editorState, event) {
if (selection == null || context == null || selectable == null) { if (selection == null || context == null || selectable == null) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
editorState.transaction.replaceText(textNode, selection.start.offset, final transaction = editorState.transaction
..replaceText(textNode, selection.start.offset,
selection.end.offset - selection.start.offset, event.character ?? ''); selection.end.offset - selection.start.offset, event.character ?? '');
editorState.commit(); editorState.apply(transaction);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_selectionMenuService = _selectionMenuService =

View File

@ -1,5 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/commands/edit_text.dart'; import 'package:appflowy_editor/src/commands/text/text_commands.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -15,7 +15,11 @@ ShortcutEventHandler spaceOnWebHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
insertContextInText(editorState, selection.startIndex, ' '); editorState.insertText(
selection.startIndex,
' ',
textNode: textNodes.first,
);
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -15,8 +15,9 @@ ShortcutEventHandler tabHandler = (editorState, event) {
final previous = textNode.previous; final previous = textNode.previous;
if (textNode.subtype != BuiltInAttributeKey.bulletedList) { if (textNode.subtype != BuiltInAttributeKey.bulletedList) {
editorState.transaction.insertText(textNode, selection.end.offset, ' ' * 4); final transaction = editorState.transaction
editorState.commit(); ..insertText(textNode, selection.end.offset, ' ' * 4);
editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -30,11 +31,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),
); );
editorState.transaction final transaction = editorState.transaction
..deleteNode(textNode) ..deleteNode(textNode)
..insertNode(path, textNode) ..insertNode(path, textNode)
..afterSelection = afterSelection; ..afterSelection = afterSelection;
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -99,14 +99,14 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
)); ));
final insertPath = textNode.path; final insertPath = textNode.path;
editorState.transaction final transaction = editorState.transaction
..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;
editorState.commit(); editorState.apply(transaction);
makeFollowingNodesIncremental(editorState, insertPath, afterSelection); makeFollowingNodesIncremental(editorState, insertPath, afterSelection);
@ -117,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;
} }
editorState.transaction final transaction = editorState.transaction
..deleteText(textNode, 0, 1) ..deleteText(textNode, 0, 1)
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
@ -128,7 +128,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
offset: 0, offset: 0,
), ),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -150,7 +150,7 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
check = false; check = false;
} }
editorState.transaction final transaction = 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,7 +162,7 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
offset: 0, offset: 0,
), ),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -176,7 +176,7 @@ KeyEventResult _toHeadingStyle(
if (textNode.attributes.heading == hX) { if (textNode.attributes.heading == hX) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
editorState.transaction final transaction = editorState.transaction
..deleteText(textNode, 0, x) ..deleteText(textNode, 0, x)
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
@ -188,7 +188,7 @@ KeyEventResult _toHeadingStyle(
offset: 0, offset: 0,
), ),
); );
editorState.commit(); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
} }

View File

@ -4,7 +4,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_ke
import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';

View File

@ -66,7 +66,7 @@ void main() {
transaction.deleteNode(item1); transaction.deleteNode(item1);
transaction.deleteNode(item2); transaction.deleteNode(item2);
transaction.deleteNode(item3); transaction.deleteNode(item3);
state.commit(); state.apply(transaction);
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]);
@ -79,7 +79,7 @@ void main() {
final item1 = Node(type: "node", attributes: {}, children: LinkedList()); final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final transaction = state.transaction; final transaction = state.transaction;
transaction.insertNode([0], item1); transaction.insertNode([0], item1);
state.commit(); state.apply(transaction);
expect(transaction.toJson(), { expect(transaction.toJson(), {
"operations": [ "operations": [
{ {
@ -103,7 +103,7 @@ void main() {
final state = EditorState(document: Document(root: root)); final state = EditorState(document: Document(root: root));
final transaction = state.transaction; final transaction = state.transaction;
transaction.deleteNode(item1); transaction.deleteNode(item1);
state.commit(); state.apply(transaction);
expect(transaction.toJson(), { expect(transaction.toJson(), {
"operations": [ "operations": [
{ {