feat: multi list indentation (#903)
* add muliple list indentation at once * feat: filter out the children node when executing indent command * feat: keeping only immediate children nodes when executing outdent command * feat: ignore outdent command if any of the selected nodes have path 1 * add test cases for indent and outdent commands * fix: linting issues and removed dead code * chore: remove unecessary comments in test file --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
parent
98356fac4c
commit
647a53e257
4 changed files with 439 additions and 49 deletions
|
|
@ -23,41 +23,69 @@ final CommandShortcutEvent indentCommand = CommandShortcutEvent(
|
||||||
|
|
||||||
bool isIndentable(EditorState editorState) {
|
bool isIndentable(EditorState editorState) {
|
||||||
final selection = editorState.selection;
|
final selection = editorState.selection;
|
||||||
if (selection == null || !selection.isSingle) {
|
if (selection == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final node = editorState.getNodeAtPath(selection.end.path);
|
|
||||||
final previous = node?.previous;
|
List<Node> nodes = editorState.getNodesInSelection(selection).normalized;
|
||||||
if (node == null ||
|
if (nodes.isEmpty) {
|
||||||
previous == null ||
|
|
||||||
!indentableBlockTypes.contains(previous.type) ||
|
|
||||||
!indentableBlockTypes.contains(node.type)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final previous = nodes.firstOrNull?.previous;
|
||||||
|
if (previous == null || !indentableBlockTypes.contains(previous.type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// there's no need to consider the child nodes
|
||||||
|
// since we are ignoring child nodes, all nodes will be on same level
|
||||||
|
nodes =
|
||||||
|
nodes.where((node) => node.path.length == previous.path.length).toList();
|
||||||
|
|
||||||
|
final isAllIndentable = nodes.every(
|
||||||
|
(node) => indentableBlockTypes.contains(node.type),
|
||||||
|
);
|
||||||
|
if (!isAllIndentable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandShortcutEventHandler _indentCommandHandler = (editorState) {
|
CommandShortcutEventHandler _indentCommandHandler = (editorState) {
|
||||||
final selection = editorState.selection;
|
final selection = editorState.selection?.normalized;
|
||||||
if (selection == null || !selection.isSingle) {
|
|
||||||
|
if (selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
final node = editorState.getNodeAtPath(selection.end.path);
|
|
||||||
final previous = node?.previous;
|
if (!isIndentable(editorState)) {
|
||||||
if (node == null ||
|
// ignore the system default tab behavior
|
||||||
previous == null ||
|
return KeyEventResult.handled;
|
||||||
!indentableBlockTypes.contains(previous.type) ||
|
|
||||||
!indentableBlockTypes.contains(node.type)) {
|
|
||||||
return KeyEventResult.handled; // ignore the system default tab behavior
|
|
||||||
}
|
}
|
||||||
final path = previous.path + [previous.children.length];
|
|
||||||
|
List<Node> nodes = editorState.getNodesInSelection(selection).normalized;
|
||||||
|
final previous = nodes.firstOrNull?.previous;
|
||||||
|
|
||||||
|
if (previous == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep the nodes in the same level as the previous block
|
||||||
|
nodes =
|
||||||
|
nodes.where((node) => node.path.length == previous.path.length).toList();
|
||||||
|
|
||||||
|
final startPath = previous.path + [previous.children.length];
|
||||||
|
final endPath = previous.path + [previous.children.length + nodes.length - 1];
|
||||||
|
|
||||||
final afterSelection = Selection(
|
final afterSelection = Selection(
|
||||||
start: selection.start.copyWith(path: path),
|
start: selection.start.copyWith(path: startPath),
|
||||||
end: selection.end.copyWith(path: path),
|
end: selection.end.copyWith(path: endPath),
|
||||||
);
|
);
|
||||||
|
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
..deleteNode(node)
|
..deleteNodes(nodes)
|
||||||
..insertNode(path, node, deepCopy: true)
|
..insertNodes(startPath, nodes, deepCopy: true)
|
||||||
..afterSelection = afterSelection;
|
..afterSelection = afterSelection;
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,50 +16,78 @@ final CommandShortcutEvent outdentCommand = CommandShortcutEvent(
|
||||||
|
|
||||||
bool isOutdentable(EditorState editorState) {
|
bool isOutdentable(EditorState editorState) {
|
||||||
final selection = editorState.selection;
|
final selection = editorState.selection;
|
||||||
if (selection == null || !selection.isSingle) {
|
if (selection == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final node = editorState.getNodeAtPath(selection.end.path);
|
|
||||||
final parent = node?.parent;
|
List<Node> nodes = editorState.getNodesInSelection(selection).normalized;
|
||||||
if (node == null ||
|
if (nodes.isEmpty) {
|
||||||
parent == null ||
|
return false;
|
||||||
!indentableBlockTypes.contains(node.type) ||
|
}
|
||||||
!indentableBlockTypes.contains(parent.type) ||
|
|
||||||
node.path.length == 1) {
|
final parent = nodes.firstOrNull?.parent;
|
||||||
// if the current node is having a path which is of size 1.
|
if (parent == null || !indentableBlockTypes.contains(parent.type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes.any((node) => node.path.length == 1)) {
|
||||||
|
// if the any nodes is having a path which is of size 1.
|
||||||
// for example [0], then that means, it is not indented
|
// for example [0], then that means, it is not indented
|
||||||
// thus we ignore this event.
|
// thus we ignore this event.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep only immediate children nodes of parent
|
||||||
|
// since we are keeping only immediate children nodes, all nodes will be on same level
|
||||||
|
nodes = nodes
|
||||||
|
.where((node) => node.path.length == parent.path.length + 1)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final isAllIndentable =
|
||||||
|
nodes.every((node) => indentableBlockTypes.contains(node.type));
|
||||||
|
if (!isAllIndentable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandShortcutEventHandler _outdentCommandHandler = (editorState) {
|
CommandShortcutEventHandler _outdentCommandHandler = (editorState) {
|
||||||
final selection = editorState.selection;
|
final selection = editorState.selection?.normalized;
|
||||||
if (selection == null || !selection.isSingle) {
|
if (selection == null) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
final node = editorState.getNodeAtPath(selection.end.path);
|
|
||||||
final parent = node?.parent;
|
if (!isOutdentable(editorState)) {
|
||||||
if (node == null ||
|
// ignore the system default tab behavior
|
||||||
parent == null ||
|
return KeyEventResult.handled;
|
||||||
!indentableBlockTypes.contains(node.type) ||
|
|
||||||
!indentableBlockTypes.contains(parent.type) ||
|
|
||||||
node.path.length == 1) {
|
|
||||||
// if the current node is having a path which is of size 1.
|
|
||||||
// for example [0], then that means, it is not indented
|
|
||||||
// thus we ignore this event.
|
|
||||||
return KeyEventResult.handled; // ignore the system default tab behavior
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final path = node.path.sublist(0, node.path.length - 1)..last += 1;
|
List<Node> nodes = editorState.getNodesInSelection(selection).normalized;
|
||||||
|
final parent = nodes.firstOrNull?.parent;
|
||||||
|
|
||||||
|
if (parent == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep the nodes of the immediate children of parent node
|
||||||
|
nodes = nodes
|
||||||
|
.where((node) => node.path.length == parent.path.length + 1)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final startPath = nodes.first.path.sublist(0, nodes.first.path.length - 1)
|
||||||
|
..last += 1;
|
||||||
|
final endPath = nodes.last.path.sublist(0, nodes.last.path.length - 1)
|
||||||
|
..last += nodes.length;
|
||||||
|
|
||||||
final afterSelection = Selection(
|
final afterSelection = Selection(
|
||||||
start: selection.start.copyWith(path: path),
|
start: selection.start.copyWith(path: startPath),
|
||||||
end: selection.end.copyWith(path: path),
|
end: selection.end.copyWith(path: endPath),
|
||||||
);
|
);
|
||||||
|
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
..deleteNode(node)
|
..deleteNodes(nodes)
|
||||||
..insertNode(path, node, deepCopy: true)
|
..insertNodes(startPath, nodes, deepCopy: true)
|
||||||
..afterSelection = afterSelection;
|
..afterSelection = afterSelection;
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,151 @@ void main() async {
|
||||||
expect(nestedBlockAfter?.indentPadding.left, _padding);
|
expect(nestedBlockAfter?.indentPadding.left, _padding);
|
||||||
expect(nestedBlockAfter?.indentPadding.right, 0);
|
expect(nestedBlockAfter?.indentPadding.right, 0);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
group('indentCommand (multi-line) - widget test multi-line indent padding',
|
||||||
|
() {
|
||||||
|
testWidgets("indent 2 LTR lines under LTR line", (tester) async {
|
||||||
|
final editor = await multiLineIndentTestHelper(
|
||||||
|
tester,
|
||||||
|
('Hello', blockComponentTextDirectionLTR),
|
||||||
|
('Will indent this', blockComponentTextDirectionLTR),
|
||||||
|
('Third Line', blockComponentTextDirectionLTR),
|
||||||
|
);
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0])!;
|
||||||
|
final nestedBlock = node.key.currentState!
|
||||||
|
.unwrapOrNull<NestedBlockComponentStatefulWidgetMixin>();
|
||||||
|
|
||||||
|
expect(nestedBlock?.indentPadding.left, _padding);
|
||||||
|
expect(nestedBlock?.indentPadding.right, 0);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("indent two LTR lines under RTL line", (tester) async {
|
||||||
|
final editor = await multiLineIndentTestHelper(
|
||||||
|
tester,
|
||||||
|
('سلام', blockComponentTextDirectionRTL),
|
||||||
|
('Will indent this', blockComponentTextDirectionLTR),
|
||||||
|
('Third Line', blockComponentTextDirectionLTR),
|
||||||
|
);
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0])!;
|
||||||
|
final nestedBlock = node.key.currentState!
|
||||||
|
.unwrapOrNull<NestedBlockComponentStatefulWidgetMixin>();
|
||||||
|
|
||||||
|
expect(nestedBlock?.indentPadding.left, _padding);
|
||||||
|
expect(nestedBlock?.indentPadding.right, 0);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("indent two RTL lines under RTL line", (tester) async {
|
||||||
|
final editor = await multiLineIndentTestHelper(
|
||||||
|
tester,
|
||||||
|
('سلام', blockComponentTextDirectionRTL),
|
||||||
|
('خط دوم', blockComponentTextDirectionRTL),
|
||||||
|
('خط سوم', blockComponentTextDirectionRTL),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0])!;
|
||||||
|
final nestedBlock = node.key.currentState!
|
||||||
|
.unwrapOrNull<NestedBlockComponentStatefulWidgetMixin>();
|
||||||
|
|
||||||
|
expect(nestedBlock?.indentPadding.left, 0);
|
||||||
|
expect(nestedBlock?.indentPadding.right, _padding);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("indent two RTL lines under LTR line", (tester) async {
|
||||||
|
final editor = await multiLineIndentTestHelper(
|
||||||
|
tester,
|
||||||
|
('Hello', blockComponentTextDirectionLTR),
|
||||||
|
('خط دوم', blockComponentTextDirectionRTL),
|
||||||
|
('خط سوم', blockComponentTextDirectionRTL),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0])!;
|
||||||
|
final nestedBlock = node.key.currentState!
|
||||||
|
.unwrapOrNull<NestedBlockComponentStatefulWidgetMixin>();
|
||||||
|
|
||||||
|
expect(nestedBlock?.indentPadding.left, 0);
|
||||||
|
expect(nestedBlock?.indentPadding.right, _padding);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("indent one RTL line and one LTR line under LTR line",
|
||||||
|
(tester) async {
|
||||||
|
final editor = await multiLineIndentTestHelper(
|
||||||
|
tester,
|
||||||
|
('Hello', blockComponentTextDirectionLTR),
|
||||||
|
('خط دوم', blockComponentTextDirectionRTL),
|
||||||
|
('Third Line', blockComponentTextDirectionLTR),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0])!;
|
||||||
|
final nestedBlock = node.key.currentState!
|
||||||
|
.unwrapOrNull<NestedBlockComponentStatefulWidgetMixin>();
|
||||||
|
|
||||||
|
expect(nestedBlock?.indentPadding.left, 0);
|
||||||
|
expect(nestedBlock?.indentPadding.right, _padding);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("indent one LTR line and one RTL lines under RTL line",
|
||||||
|
(tester) async {
|
||||||
|
final editor = await multiLineIndentTestHelper(
|
||||||
|
tester,
|
||||||
|
('سلام', blockComponentTextDirectionRTL),
|
||||||
|
('Will indent this', blockComponentTextDirectionLTR),
|
||||||
|
('خط سوم', blockComponentTextDirectionRTL),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0])!;
|
||||||
|
final nestedBlock = node.key.currentState!
|
||||||
|
.unwrapOrNull<NestedBlockComponentStatefulWidgetMixin>();
|
||||||
|
|
||||||
|
expect(nestedBlock?.indentPadding.left, _padding);
|
||||||
|
expect(nestedBlock?.indentPadding.right, 0);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("indent AUTO line under AUTO line", (tester) async {
|
||||||
|
final editor = await multiLineIndentTestHelper(
|
||||||
|
tester,
|
||||||
|
('سلام', blockComponentTextDirectionAuto),
|
||||||
|
('خط دوم', blockComponentTextDirectionAuto),
|
||||||
|
('خط سوم', blockComponentTextDirectionAuto),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0])!;
|
||||||
|
final nestedBlock = node.key.currentState!
|
||||||
|
.unwrapOrNull<NestedBlockComponentStatefulWidgetMixin>();
|
||||||
|
|
||||||
|
expect(nestedBlock?.indentPadding.left, 0);
|
||||||
|
expect(nestedBlock?.indentPadding.right, _padding);
|
||||||
|
|
||||||
await editor.dispose();
|
await editor.dispose();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -170,3 +315,31 @@ Future<TestableEditor> indentTestHelper(
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<TestableEditor> multiLineIndentTestHelper(
|
||||||
|
WidgetTester tester,
|
||||||
|
TestLine firstLine,
|
||||||
|
TestLine secondLine,
|
||||||
|
TestLine thirdLine,
|
||||||
|
) async {
|
||||||
|
final editor = tester.editor
|
||||||
|
..addNode(paragraphNode(text: firstLine.$1, textDirection: firstLine.$2))
|
||||||
|
..addNode(paragraphNode(text: secondLine.$1, textDirection: secondLine.$2))
|
||||||
|
..addNode(paragraphNode(text: thirdLine.$1, textDirection: thirdLine.$2));
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final selection = Selection(
|
||||||
|
start: Position(path: [1], offset: 1),
|
||||||
|
end: Position(path: [2], offset: 1),
|
||||||
|
);
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final node = editor.nodeAtPath([0])!;
|
||||||
|
expect(node.delta?.toPlainText(), firstLine.$1);
|
||||||
|
expect(node.children.every((element) => element.level == 2), true);
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import '../../../infra/testable_editor.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
setUpAll(() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('outdent_command.dart', () {
|
||||||
|
testWidgets("press shift tab in plain text", (tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final snapshotDocument = editor.document;
|
||||||
|
|
||||||
|
await editor.updateSelection(Selection.single(path: [0], startOffset: 0));
|
||||||
|
await editor.pressKey(
|
||||||
|
key: LogicalKeyboardKey.tab,
|
||||||
|
isShiftPressed: true,
|
||||||
|
);
|
||||||
|
// nothing happens
|
||||||
|
expect(
|
||||||
|
editor.selection,
|
||||||
|
Selection.single(path: [0], startOffset: 0),
|
||||||
|
);
|
||||||
|
expect(editor.document.toJson(), snapshotDocument.toJson());
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("press shift tab where previous element is not list element",
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor
|
||||||
|
..addParagraph(initialText: text)
|
||||||
|
..addNode(bulletedListNode(delta: Delta()..insert(text)))
|
||||||
|
..addNode(bulletedListNode(delta: Delta()..insert(text)));
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final snapshotDocument = editor.document;
|
||||||
|
|
||||||
|
final selection = Selection.single(path: [1], startOffset: 0);
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
await editor.pressKey(
|
||||||
|
key: LogicalKeyboardKey.tab,
|
||||||
|
isShiftPressed: true,
|
||||||
|
);
|
||||||
|
// nothing happens
|
||||||
|
expect(
|
||||||
|
editor.selection,
|
||||||
|
Selection.single(path: [1], startOffset: 0),
|
||||||
|
);
|
||||||
|
expect(editor.document.toJson(), snapshotDocument.toJson());
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
"press shift tab in indented list with multiple nodes in same sub-level",
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor
|
||||||
|
..addNode(todoListNode(checked: false, delta: Delta()..insert(text)))
|
||||||
|
..addNode(todoListNode(checked: false, delta: Delta()..insert(text)))
|
||||||
|
..addNode(todoListNode(checked: false, delta: Delta()..insert(text)));
|
||||||
|
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final selection = Selection.collapsed(Position(path: [1]));
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
|
// Before
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// After
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
|
||||||
|
expect(
|
||||||
|
editor.selection,
|
||||||
|
Selection.collapsed(Position(path: [0, 1])),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0])!.type,
|
||||||
|
'todo_list',
|
||||||
|
);
|
||||||
|
expect(editor.nodeAtPath([1]), null);
|
||||||
|
expect(editor.nodeAtPath([2]), null);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0, 0])!.type,
|
||||||
|
'todo_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0, 1])!.type,
|
||||||
|
'todo_list',
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.updateSelection(
|
||||||
|
Selection.single(path: [0, 1], startOffset: 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.pressKey(
|
||||||
|
key: LogicalKeyboardKey.tab,
|
||||||
|
isShiftPressed: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Before
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// After
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
// [] Welcome to Appflowy 😁
|
||||||
|
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([1])!.type,
|
||||||
|
'todo_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0, 0])!.type,
|
||||||
|
'todo_list',
|
||||||
|
);
|
||||||
|
expect(editor.nodeAtPath([0, 1]), null);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
"press shift tab in indented list with only one node in same sub-level",
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor
|
||||||
|
..addNode(bulletedListNode(delta: Delta()..insert(text)))
|
||||||
|
..addNode(bulletedListNode(delta: Delta()..insert(text)))
|
||||||
|
..addNode(bulletedListNode(delta: Delta()..insert(text)));
|
||||||
|
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
var selection = Selection.single(path: [1], startOffset: 0);
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
|
// Before
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// After
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
|
||||||
|
expect(
|
||||||
|
editor.selection,
|
||||||
|
Selection.single(path: [0, 0], startOffset: 0),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0, 0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([1])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(editor.nodeAtPath([2]), null);
|
||||||
|
|
||||||
|
await editor
|
||||||
|
.updateSelection(Selection.single(path: [0, 0], startOffset: 0));
|
||||||
|
|
||||||
|
await editor.pressKey(
|
||||||
|
key: LogicalKeyboardKey.tab,
|
||||||
|
isShiftPressed: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Before
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// After
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
// * Welcome to Appflowy 😁
|
||||||
|
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([1])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([2])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(editor.nodeAtPath([0, 0]), null);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('outdentCommand (multi-line) - widget test multi-line outdent padding',
|
||||||
|
() {
|
||||||
|
testWidgets("indent two items of same level then outdent it",
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor
|
||||||
|
..addNode(bulletedListNode(text: text))
|
||||||
|
..addNode(bulletedListNode(text: text))
|
||||||
|
..addNode(bulletedListNode(text: text));
|
||||||
|
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
var selection = Selection(
|
||||||
|
start: Position(path: [1], offset: 1),
|
||||||
|
end: Position(path: [2], offset: 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
|
// Before
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// After
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// ○ Welcome to Appflowy 😁
|
||||||
|
// ○ Welcome to Appflowy 😁
|
||||||
|
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0, 0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0, 1])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(editor.nodeAtPath([1]), null);
|
||||||
|
expect(editor.nodeAtPath([2]), null);
|
||||||
|
|
||||||
|
selection = Selection(
|
||||||
|
start: Position(path: [0, 0], offset: 1),
|
||||||
|
end: Position(path: [0, 1], offset: 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
|
||||||
|
await editor.pressKey(
|
||||||
|
key: LogicalKeyboardKey.tab,
|
||||||
|
isShiftPressed: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Before
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// ○ Welcome to Appflowy 😁
|
||||||
|
// ○ Welcome to Appflowy 😁
|
||||||
|
// After
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([1])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([2])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(editor.nodeAtPath([0, 0]), null);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("ignore if any bullet list of level 1 selected",
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor
|
||||||
|
..addNode(bulletedListNode(text: text))
|
||||||
|
..addNode(bulletedListNode(text: text))
|
||||||
|
..addNode(bulletedListNode(text: text));
|
||||||
|
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
var selection = Selection.single(path: [1], startOffset: 0);
|
||||||
|
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
|
||||||
|
await editor.pressKey(key: LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
|
// Before
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// After
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// ○ Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([1])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0, 0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(editor.nodeAtPath([2]), null);
|
||||||
|
|
||||||
|
selection = Selection(
|
||||||
|
start: Position(path: [1], offset: 1),
|
||||||
|
end: Position(path: [0, 0], offset: 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.updateSelection(selection);
|
||||||
|
|
||||||
|
await editor.pressKey(
|
||||||
|
key: LogicalKeyboardKey.tab,
|
||||||
|
isShiftPressed: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// list will remain same as node with path [1] which is on level = 1,
|
||||||
|
// Before
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// ○ Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// After
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
// ○ Welcome to Appflowy 😁
|
||||||
|
// • Welcome to Appflowy 😁
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([0, 0])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.nodeAtPath([1])!.type,
|
||||||
|
'bulleted_list',
|
||||||
|
);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue