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:
Ahad Patel 2024-10-08 17:21:18 +05:30 • committed by GitHub
commit 647a53e257
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 439 additions and 49 deletions

View file

@ -139,6 +139,151 @@ void main() async {
expect(nestedBlockAfter?.indentPadding.left, _padding);
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();
});
});
@ -170,3 +315,31 @@ Future<TestableEditor> indentTestHelper(
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;
}

View file

@ -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();
});
});
}