Merge pull request #991 from AppFlowy-IO/feat/number-list

Feat: number list
This commit is contained in:
Lucas.Xu 2022-09-14 09:42:01 +08:00 committed by GitHub
commit 84a71674f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 479 additions and 110 deletions

View File

@ -193,7 +193,7 @@ class TransactionBuilder {
///
/// Also, this method will transform the path of the operations
/// to avoid conflicts.
add(Operation op) {
add(Operation op, {bool transform = true}) {
final Operation? last = operations.isEmpty ? null : operations.last;
if (last != null) {
if (op is TextEditOperation &&
@ -208,8 +208,10 @@ class TransactionBuilder {
return;
}
}
for (var i = 0; i < operations.length; i++) {
op = transformOperation(operations[i], op);
if (transform) {
for (var i = 0; i < operations.length; i++) {
op = transformOperation(operations[i], op);
}
}
if (op is TextEditOperation && op.delta.isEmpty) {
return;

View File

@ -40,6 +40,7 @@ class NumberListTextNodeWidget extends StatefulWidget {
}
// customize
const double _numberHorizontalPadding = 8;
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
with SelectableMixin, DefaultSelectable {
@ -47,8 +48,6 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'number_list_text');
final _iconWidth = 20.0;
final _iconRightPadding = 5.0;
@override
SelectableMixin<StatefulWidget> get forward =>
@ -61,12 +60,14 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowySvg(
Padding(
key: iconKey,
width: _iconWidth,
height: _iconWidth,
padding: EdgeInsets.only(right: _iconRightPadding),
number: widget.textNode.attributes.number,
padding: const EdgeInsets.symmetric(
horizontal: _numberHorizontalPadding, vertical: 0),
child: Text(
'${widget.textNode.attributes.number.toString()}.',
style: const TextStyle(fontSize: 16),
),
),
Flexible(
child: FlowyRichText(

View File

@ -1,4 +1,5 @@
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -25,10 +26,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
selection = selection.isBackward ? selection : selection.reversed;
final textNodes = nodes.whereType<TextNode>().toList();
final nonTextNodes =
final List<Node> nonTextNodes =
nodes.where((node) => node is! TextNode).toList(growable: false);
final transactionBuilder = TransactionBuilder(editorState);
List<int>? cancelNumberListPath;
if (nonTextNodes.isNotEmpty) {
transactionBuilder.deleteNodes(nonTextNodes);
@ -40,6 +42,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
if (index < 0 && selection.isCollapsed) {
// 1. style
if (textNode.subtype != null) {
if (textNode.subtype == StyleKey.numberList) {
cancelNumberListPath = textNode.path;
}
transactionBuilder
..updateNode(textNode, {
StyleKey.subtype: null,
@ -54,23 +59,13 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
} else {
// 2. non-style
// find previous text node.
var previous = textNode.previous;
while (previous != null) {
if (previous is TextNode) {
transactionBuilder
..mergeText(previous, textNode)
..deleteNode(textNode)
..afterSelection = Selection.collapsed(
Position(
path: previous.path,
offset: previous.toRawString().length,
),
);
break;
} else {
previous = previous.previous;
}
}
return _backDeleteToPreviousTextNode(
editorState,
textNode,
transactionBuilder,
nonTextNodes,
selection,
);
}
} else {
if (selection.isCollapsed) {
@ -88,8 +83,69 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
}
}
} else {
if (textNodes.isNotEmpty) {
_deleteTextNodes(transactionBuilder, textNodes, selection);
if (textNodes.isEmpty) {
return KeyEventResult.handled;
}
final startPosition = selection.start;
final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
_deleteTextNodes(transactionBuilder, textNodes, selection);
transactionBuilder.commit();
if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) {
makeFollowingNodesIncremental(
editorState,
startPosition.path,
transactionBuilder.afterSelection!,
);
}
return KeyEventResult.handled;
}
if (transactionBuilder.operations.isNotEmpty) {
if (nonTextNodes.isNotEmpty) {
transactionBuilder.afterSelection = Selection.collapsed(selection.start);
}
transactionBuilder.commit();
}
if (cancelNumberListPath != null) {
makeFollowingNodesIncremental(
editorState,
cancelNumberListPath,
Selection.collapsed(selection.start),
beginNum: 0,
);
}
return KeyEventResult.handled;
}
KeyEventResult _backDeleteToPreviousTextNode(
EditorState editorState,
TextNode textNode,
TransactionBuilder transactionBuilder,
List<Node> nonTextNodes,
Selection selection) {
var previous = textNode.previous;
bool prevIsNumberList = false;
while (previous != null) {
if (previous is TextNode) {
if (previous.subtype == StyleKey.numberList) {
prevIsNumberList = true;
}
transactionBuilder
..mergeText(previous, textNode)
..deleteNode(textNode)
..afterSelection = Selection.collapsed(
Position(
path: previous.path,
offset: previous.toRawString().length,
),
);
break;
} else {
previous = previous.previous;
}
}
@ -100,6 +156,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
transactionBuilder.commit();
}
if (prevIsNumberList) {
makeFollowingNodesIncremental(
editorState, previous!.path, transactionBuilder.afterSelection!);
}
return KeyEventResult.handled;
}
@ -120,37 +181,65 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
final transactionBuilder = TransactionBuilder(editorState);
if (textNodes.length == 1) {
final textNode = textNodes.first;
// The cursor is at the end of the line,
// merge next line into this line.
if (selection.start.offset >= textNode.delta.length) {
final nextNode = textNode.next;
if (nextNode == null) {
return KeyEventResult.ignored;
}
if (nextNode is TextNode) {
transactionBuilder.mergeText(textNode, nextNode);
}
transactionBuilder.deleteNode(nextNode);
} else {
final index = textNode.delta.nextRunePosition(selection.start.offset);
if (selection.isCollapsed) {
transactionBuilder.deleteText(
textNode,
selection.start.offset,
index - selection.start.offset,
);
} else {
transactionBuilder.deleteText(
textNode,
selection.start.offset,
selection.end.offset - selection.start.offset,
);
}
return _mergeNextLineIntoThisLine(
editorState,
textNode,
transactionBuilder,
selection,
);
}
final index = textNode.delta.nextRunePosition(selection.start.offset);
if (selection.isCollapsed) {
transactionBuilder.deleteText(
textNode,
selection.start.offset,
index - selection.start.offset,
);
} else {
transactionBuilder.deleteText(
textNode,
selection.start.offset,
selection.end.offset - selection.start.offset,
);
}
transactionBuilder.commit();
} else {
final startPosition = selection.start;
final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
_deleteTextNodes(transactionBuilder, textNodes, selection);
transactionBuilder.commit();
if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) {
makeFollowingNodesIncremental(
editorState, startPosition.path, transactionBuilder.afterSelection!);
}
}
return KeyEventResult.handled;
}
KeyEventResult _mergeNextLineIntoThisLine(
EditorState editorState,
TextNode textNode,
TransactionBuilder transactionBuilder,
Selection selection) {
final nextNode = textNode.next;
if (nextNode == null) {
return KeyEventResult.ignored;
}
if (nextNode is TextNode) {
transactionBuilder.mergeText(textNode, nextNode);
}
transactionBuilder.deleteNode(nextNode);
transactionBuilder.commit();
if (textNode.subtype == StyleKey.numberList) {
makeFollowingNodesIncremental(editorState, textNode.path, selection);
}
return KeyEventResult.handled;
}

View File

@ -1,10 +1,31 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/html_converter.dart';
import 'package:appflowy_editor/src/document/node_iterator.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
import 'package:flutter/material.dart';
import 'package:rich_clipboard/rich_clipboard.dart';
_handleCopy(EditorState editorState) async {
int _textLengthOfNode(Node node) {
if (node is TextNode) {
return node.delta.length;
}
return 0;
}
Selection _computeSelectionAfterPasteMultipleNodes(
EditorState editorState, List<Node> nodes) {
final currentSelection = editorState.cursorSelection!;
final currentCursor = currentSelection.start;
final currentPath = [...currentCursor.path];
currentPath[currentPath.length - 1] += nodes.length;
int lenOfLastNode = _textLengthOfNode(nodes.last);
return Selection.collapsed(
Position(path: currentPath, offset: lenOfLastNode));
}
void _handleCopy(EditorState editorState) async {
final selection = editorState.cursorSelection?.normalize;
if (selection == null || selection.isCollapsed) {
return;
@ -40,7 +61,7 @@ _handleCopy(EditorState editorState) async {
RichClipboard.setData(RichClipboardData(html: copyString));
}
_pasteHTML(EditorState editorState, String html) {
void _pasteHTML(EditorState editorState, String html) {
final selection = editorState.cursorSelection?.normalize;
if (selection == null) {
return;
@ -78,7 +99,7 @@ _pasteHTML(EditorState editorState, String html) {
_pasteMultipleLinesInText(editorState, path, selection.start.offset, nodes);
}
_pasteMultipleLinesInText(
void _pasteMultipleLinesInText(
EditorState editorState, List<int> path, int offset, List<Node> nodes) {
final tb = TransactionBuilder(editorState);
@ -86,6 +107,11 @@ _pasteMultipleLinesInText(
final nodeAtPath = editorState.document.nodeAtPath(path)!;
if (nodeAtPath.type == "text" && firstNode.type == "text") {
int? startNumber;
if (nodeAtPath.subtype == StyleKey.numberList) {
startNumber = nodeAtPath.attributes[StyleKey.number] as int;
}
// split and merge
final textNodeAtPath = nodeAtPath as TextNode;
final firstTextNode = firstNode as TextNode;
@ -100,7 +126,12 @@ _pasteMultipleLinesInText(
firstTextNode.delta);
final tailNodes = nodes.sublist(1);
final originalPath = [...path];
path[path.length - 1]++;
final afterSelection =
_computeSelectionAfterPasteMultipleNodes(editorState, tailNodes);
if (tailNodes.isNotEmpty) {
if (tailNodes.last.type == "text") {
final tailTextNode = tailNodes.last as TextNode;
@ -112,17 +143,27 @@ _pasteMultipleLinesInText(
tailNodes.add(TextNode(type: "text", delta: remain));
}
tb.setAfterSelection(afterSelection);
tb.insertNodes(path, tailNodes);
tb.commit();
if (startNumber != null) {
makeFollowingNodesIncremental(editorState, originalPath, afterSelection,
beginNum: startNumber);
}
return;
}
final afterSelection =
_computeSelectionAfterPasteMultipleNodes(editorState, nodes);
path[path.length - 1]++;
tb.setAfterSelection(afterSelection);
tb.insertNodes(path, nodes);
tb.commit();
}
_handlePaste(EditorState editorState) async {
void _handlePaste(EditorState editorState) async {
final data = await RichClipboard.getData();
if (editorState.cursorSelection?.isCollapsed ?? false) {
@ -137,7 +178,7 @@ _handlePaste(EditorState editorState) async {
});
}
_pastRichClipboard(EditorState editorState, RichClipboardData data) {
void _pastRichClipboard(EditorState editorState, RichClipboardData data) {
if (data.html != null) {
_pasteHTML(editorState, data.html!);
return;
@ -148,7 +189,8 @@ _pastRichClipboard(EditorState editorState, RichClipboardData data) {
}
}
_pasteSingleLine(EditorState editorState, Selection selection, String line) {
void _pasteSingleLine(
EditorState editorState, Selection selection, String line) {
final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode;
final beginOffset = selection.end.offset;
TransactionBuilder(editorState)
@ -188,7 +230,7 @@ Delta _lineContentToDelta(String lineContent) {
return delta;
}
_handlePastePlainText(EditorState editorState, String plainText) {
void _handlePastePlainText(EditorState editorState, String plainText) {
final selection = editorState.cursorSelection?.normalize;
if (selection == null) {
return;
@ -219,16 +261,21 @@ _handlePastePlainText(EditorState editorState, String plainText) {
final insertedLineSuffix = node.delta.slice(beginOffset);
path[path.length - 1]++;
var index = 0;
final tb = TransactionBuilder(editorState);
final nodes = remains.map((e) {
if (index++ == remains.length - 1) {
return TextNode(
type: "text",
delta: _lineContentToDelta(e)..addAll(insertedLineSuffix));
}
return TextNode(type: "text", delta: _lineContentToDelta(e));
}).toList();
final List<TextNode> nodes = remains
.map((e) => TextNode(type: "text", delta: _lineContentToDelta(e)))
.toList();
final afterSelection =
_computeSelectionAfterPasteMultipleNodes(editorState, nodes);
// append remain text to the last line
if (nodes.isNotEmpty) {
final last = nodes.last;
nodes[nodes.length - 1] =
TextNode(type: "text", delta: last.delta..addAll(insertedLineSuffix));
}
// insert first line
tb.textEdit(
node,
@ -238,22 +285,19 @@ _handlePastePlainText(EditorState editorState, String plainText) {
..delete(node.delta.length - beginOffset));
// insert remains
tb.insertNodes(path, nodes);
tb.setAfterSelection(afterSelection);
tb.commit();
// fixme: don't set the cursor manually
editorState.updateCursorSelection(Selection.collapsed(
Position(path: nodes.last.path, offset: lines.last.length)));
}
}
/// 1. copy the selected content
/// 2. delete selected content
_handleCut(EditorState editorState) {
void _handleCut(EditorState editorState) {
_handleCopy(editorState);
_deleteSelectedContent(editorState);
}
_deleteSelectedContent(EditorState editorState) {
void _deleteSelectedContent(EditorState editorState) {
final selection = editorState.cursorSelection?.normalize;
if (selection == null || selection.isCollapsed) {
return;

View File

@ -1,14 +1,10 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy_editor/src/document/attributes.dart';
import 'package:appflowy_editor/src/document/node.dart';
import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
import './number_list_helper.dart';
/// Handle some cases where enter is pressed and shift is not pressed.
///
@ -41,6 +37,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
// Multiple selection
if (!selection.isSingle) {
final startNode = editorState.document.nodeAtPath(selection.start.path)!;
final length = textNodes.length;
final List<TextNode> subTextNodes =
length >= 3 ? textNodes.sublist(1, textNodes.length - 1) : [];
@ -61,6 +58,12 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
)
..afterSelection = afterSelection
..commit();
if (startNode is TextNode && startNode.subtype == StyleKey.numberList) {
makeFollowingNodesIncremental(
editorState, selection.start.path, afterSelection);
}
return KeyEventResult.handled;
}
@ -87,36 +90,57 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
))
..afterSelection = afterSelection
..commit();
final nextNode = textNode.next;
if (nextNode is TextNode && nextNode.subtype == StyleKey.numberList) {
makeFollowingNodesIncremental(
editorState, textNode.path, afterSelection,
beginNum: 0);
}
} else {
final subtype = textNode.subtype;
final afterSelection = Selection.collapsed(
Position(path: textNode.path.next, offset: 0),
);
TransactionBuilder(editorState)
..insertNode(
textNode.path,
TextNode.empty(),
)
..afterSelection = afterSelection
..commit();
if (subtype == StyleKey.numberList) {
final prevNumber = textNode.attributes[StyleKey.number] as int;
final newNode = TextNode.empty();
newNode.attributes[StyleKey.subtype] = StyleKey.numberList;
newNode.attributes[StyleKey.number] = prevNumber;
final insertPath = textNode.path;
TransactionBuilder(editorState)
..insertNode(
insertPath,
newNode,
)
..afterSelection = afterSelection
..commit();
makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
beginNum: prevNumber);
} else {
TransactionBuilder(editorState)
..insertNode(
textNode.path,
TextNode.empty(),
)
..afterSelection = afterSelection
..commit();
}
}
return KeyEventResult.handled;
}
// Otherwise,
// split the node into two nodes with style
final needCopyAttributes = StyleKey.globalStyleKeys
.where((key) => key != StyleKey.heading)
.contains(textNode.subtype);
Attributes attributes = {};
if (needCopyAttributes) {
attributes = Attributes.from(textNode.attributes);
if (attributes.check) {
attributes[StyleKey.checkbox] = false;
}
}
Attributes attributes = _attributesFromPreviousLine(textNode);
final nextPath = textNode.path.next;
final afterSelection = Selection.collapsed(
Position(path: textNode.path.next, offset: 0),
Position(path: nextPath, offset: 0),
);
TransactionBuilder(editorState)
..insertNode(
textNode.path.next,
@ -132,5 +156,39 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
)
..afterSelection = afterSelection
..commit();
// If the new type of a text node is number list,
// the numbers of the following nodes should be incremental.
if (textNode.subtype == StyleKey.numberList) {
makeFollowingNodesIncremental(editorState, nextPath, afterSelection);
}
return KeyEventResult.handled;
};
Attributes _attributesFromPreviousLine(TextNode textNode) {
final prevAttributes = textNode.attributes;
final subType = textNode.subtype;
if (subType == null || subType == StyleKey.heading) {
return {};
}
final copy = Attributes.from(prevAttributes);
if (subType == StyleKey.numberList) {
return _nextNumberAttributesFromPreviousLine(copy, textNode);
}
if (subType == StyleKey.checkbox) {
copy[StyleKey.checkbox] = false;
return copy;
}
return copy;
}
Attributes _nextNumberAttributesFromPreviousLine(
Attributes copy, TextNode textNode) {
final prevNum = textNode.attributes[StyleKey.number] as int?;
copy[StyleKey.number] = prevNum == null ? 1 : prevNum + 1;
return copy;
}

View File

@ -0,0 +1,38 @@
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/document/attributes.dart';
void makeFollowingNodesIncremental(
EditorState editorState, List<int> insertPath, Selection afterSelection,
{int? beginNum}) {
final insertNode = editorState.document.nodeAtPath(insertPath);
if (insertNode == null) {
return;
}
beginNum ??= insertNode.attributes[StyleKey.number] as int;
int numPtr = beginNum + 1;
var ptr = insertNode.next;
final builder = TransactionBuilder(editorState);
while (ptr != null) {
if (ptr.subtype != StyleKey.numberList) {
break;
}
final currentNum = ptr.attributes[StyleKey.number] as int;
if (currentNum != numPtr) {
Attributes updateAttributes = {};
updateAttributes[StyleKey.number] = numPtr;
builder.updateNode(ptr, updateAttributes);
}
ptr = ptr.next;
numPtr++;
}
builder.afterSelection = afterSelection;
builder.commit();
}

View File

@ -8,6 +8,7 @@ import 'package:appflowy_editor/src/document/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
import './number_list_helper.dart';
@visibleForTesting
List<String> get checkboxListSymbols => _checkboxListSymbols;
@ -20,6 +21,8 @@ const _bulletedListSymbols = ['*', '-'];
const _checkboxListSymbols = ['[x]', '-[x]'];
const _unCheckboxListSymbols = ['[]', '-[]'];
final _numberRegex = RegExp(r'^(\d+)\.');
ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
if (event.logicalKey != LogicalKeyboardKey.space) {
return KeyEventResult.ignored;
@ -42,6 +45,16 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
final textNode = textNodes.first;
final text = textNode.toRawString();
final numberMatch = _numberRegex.firstMatch(text);
if (numberMatch != null) {
final matchText = numberMatch.group(0);
final numText = numberMatch.group(1);
if (matchText != null && numText != null) {
return _toNumberList(editorState, textNode, matchText, numText);
}
}
if ((_checkboxListSymbols + _unCheckboxListSymbols).any(text.startsWith)) {
return _toCheckboxList(editorState, textNode);
} else if (_bulletedListSymbols.any(text.startsWith)) {
@ -53,6 +66,52 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
return KeyEventResult.ignored;
};
KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
String matchText, String numText) {
if (textNode.subtype == StyleKey.bulletedList) {
return KeyEventResult.ignored;
}
final numValue = int.tryParse(numText);
if (numValue == null) {
return KeyEventResult.ignored;
}
// The user types number + . + space, he wants to turn
// this line into number list, but we should check if previous line
// is number list.
//
// Check whether the number input by the user is the successor of the previous
// line. If it's not, ignore it.
final prevNode = textNode.previous;
if (prevNode != null &&
prevNode is TextNode &&
prevNode.attributes[StyleKey.subtype] == StyleKey.numberList) {
final prevNumber = prevNode.attributes[StyleKey.number] as int;
if (numValue != prevNumber + 1) {
return KeyEventResult.ignored;
}
}
final afterSelection = Selection.collapsed(Position(
path: textNode.path,
offset: 0,
));
final insertPath = textNode.path;
TransactionBuilder(editorState)
..deleteText(textNode, 0, matchText.length)
..updateNode(textNode,
{StyleKey.subtype: StyleKey.numberList, StyleKey.number: numValue})
..afterSelection = afterSelection
..commit();
makeFollowingNodesIncremental(editorState, insertPath, afterSelection);
return KeyEventResult.handled;
}
KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
if (textNode.subtype == StyleKey.bulletedList) {
return KeyEventResult.ignored;

View File

@ -43,7 +43,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
for (var i = operations.length - 1; i >= 0; i--) {
final operation = operations[i];
final inverted = operation.invert();
builder.add(inverted);
builder.add(inverted, transform: false);
}
builder.afterSelection = beforeSelection;
builder.beforeSelection = afterSelection;
@ -123,11 +123,12 @@ class UndoManager {
}
final transaction = historyItem.toTransaction(s);
s.apply(
transaction,
const ApplyOptions(
recordUndo: false,
recordRedo: true,
));
transaction,
const ApplyOptions(
recordUndo: false,
recordRedo: true,
),
);
}
redo() {
@ -142,10 +143,11 @@ class UndoManager {
}
final transaction = historyItem.toTransaction(s);
s.apply(
transaction,
const ApplyOptions(
recordUndo: true,
recordRedo: false,
));
transaction,
const ApplyOptions(
recordUndo: true,
recordRedo: false,
),
);
}
}

View File

@ -0,0 +1,76 @@
import 'dart:collection';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/undo_manager.dart';
import 'package:flutter_test/flutter_test.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
Node _createEmptyEditorRoot() {
return Node(
type: 'editor',
children: LinkedList(),
attributes: {},
);
}
test("HistoryItem #1", () {
final document = StateTree(root: _createEmptyEditorRoot());
final editorState = EditorState(document: document);
final historyItem = HistoryItem();
historyItem.add(DeleteOperation(
[0], [TextNode(type: 'text', delta: Delta()..insert('0'))]));
historyItem.add(DeleteOperation(
[0], [TextNode(type: 'text', delta: Delta()..insert('1'))]));
historyItem.add(DeleteOperation(
[0], [TextNode(type: 'text', delta: Delta()..insert('2'))]));
final transaction = historyItem.toTransaction(editorState);
assert(isInsertAndPathEqual(transaction.operations[0], [0], '2'));
assert(isInsertAndPathEqual(transaction.operations[1], [0], '1'));
assert(isInsertAndPathEqual(transaction.operations[2], [0], '0'));
});
test("HistoryItem #2", () {
final document = StateTree(root: _createEmptyEditorRoot());
final editorState = EditorState(document: document);
final historyItem = HistoryItem();
historyItem.add(DeleteOperation(
[0], [TextNode(type: 'text', delta: Delta()..insert('0'))]));
historyItem
.add(UpdateOperation([0], {"subType": "number"}, {"subType": null}));
historyItem.add(DeleteOperation([0], [TextNode.empty(), TextNode.empty()]));
historyItem.add(DeleteOperation([0], [TextNode.empty()]));
final transaction = historyItem.toTransaction(editorState);
assert(isInsertAndPathEqual(transaction.operations[0], [0]));
assert(isInsertAndPathEqual(transaction.operations[1], [0]));
assert(transaction.operations[2] is UpdateOperation);
assert(isInsertAndPathEqual(transaction.operations[3], [0], '0'));
});
}
bool isInsertAndPathEqual(Operation operation, Path path, [String? content]) {
if (operation is! InsertOperation) {
return false;
}
if (!pathEquals(operation.path, path)) {
return false;
}
final firstNode = operation.nodes[0];
if (firstNode is! TextNode) {
return false;
}
if (content == null) {
return true;
}
return firstNode.delta.toRawString() == content;
}