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
2 changed files with 439 additions and 49 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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