mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-10-27 16:11:13 +00:00
feat: turn into multiple lines (#6558)
* feat: select multiple lines with block selection style * feat: multiple nodes conversion * fix: exclude children for the block can't contain children * chore: update editor version * fix: unit test * test: convert nested list to heading/quote/callout * test: transform nodes at the same level into another block type * test: add undo redo for turn into * test: add multi lines integration test * chore: remove debug logs * fix: integration test
This commit is contained in:
parent
b1682e4f54
commit
a8bcab7770
@ -59,7 +59,7 @@ void main() {
|
||||
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('turn into', (tester) async {
|
||||
testWidgets('turn into - single line', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
@ -97,5 +97,51 @@ void main() {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('turn into - multi lines', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
const name = 'Test Document';
|
||||
await tester.createNewPageWithNameUnderParent(name: name);
|
||||
await tester.openPage(name);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText('turn into 1');
|
||||
await tester.ime.insertCharacter('\n');
|
||||
await tester.ime.insertText('turn into 2');
|
||||
|
||||
// click the block option button to convert it to another blocks
|
||||
final values = {
|
||||
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_bulletedList.tr():
|
||||
BulletedListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_numberedList.tr():
|
||||
NumberedListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_todoList.tr():
|
||||
TodoListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
|
||||
};
|
||||
|
||||
for (final value in values.entries) {
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
editorState.selection = Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [1], offset: 2),
|
||||
);
|
||||
final menuText = value.key;
|
||||
final afterType = value.value;
|
||||
await turnIntoBlock(
|
||||
tester,
|
||||
[0],
|
||||
menuText: menuText,
|
||||
afterType: afterType,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -132,6 +132,11 @@ void _customBlockOptionActions(
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
builder.showActions =
|
||||
(node) => node.parent?.type != TableCellBlockKeys.type;
|
||||
builder.configuration = builder.configuration.copyWith(
|
||||
blockSelectionAreaMargin: (_) => const EdgeInsets.symmetric(
|
||||
vertical: 1,
|
||||
),
|
||||
);
|
||||
|
||||
builder.actionBuilder = (context, state) {
|
||||
final top = builder.configuration.padding(context.node).top;
|
||||
|
||||
@ -209,44 +209,60 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
|
||||
Node node, {
|
||||
int? level,
|
||||
}) async {
|
||||
final toType = type;
|
||||
|
||||
Log.info(
|
||||
'Turn into block: from ${node.type} to $type',
|
||||
);
|
||||
|
||||
if (type == node.type && type != HeadingBlockKeys.type) {
|
||||
Log.info('Block type is the same');
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Node afterNode = node.copyWith(
|
||||
type: type,
|
||||
attributes: {
|
||||
if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level,
|
||||
if (toType == TodoListBlockKeys.type) TodoListBlockKeys.checked: false,
|
||||
blockComponentBackgroundColor:
|
||||
node.attributes[blockComponentBackgroundColor],
|
||||
blockComponentTextDirection:
|
||||
node.attributes[blockComponentTextDirection],
|
||||
blockComponentDelta: (node.delta ?? Delta()).toJson(),
|
||||
},
|
||||
);
|
||||
final insertedNode = [];
|
||||
// heading block and callout block should not have children
|
||||
if ([HeadingBlockKeys.type, CalloutBlockKeys.type].contains(toType)) {
|
||||
afterNode = afterNode.copyWith(
|
||||
children: [],
|
||||
final toType = type;
|
||||
|
||||
// only handle the node in the same depth
|
||||
final selectedNodes = editorState
|
||||
.getNodesInSelection(selection.normalized)
|
||||
.where((e) => e.path.length == node.path.length)
|
||||
.toList();
|
||||
Log.info('turnIntoBlock selectedNodes $selectedNodes');
|
||||
|
||||
final insertedNode = <Node>[];
|
||||
|
||||
for (final node in selectedNodes) {
|
||||
Log.info(
|
||||
'Turn into block: from ${node.type} to $type',
|
||||
);
|
||||
insertedNode.addAll(node.children.map((e) => e.copyWith()));
|
||||
|
||||
Node afterNode = node.copyWith(
|
||||
type: type,
|
||||
attributes: {
|
||||
if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level,
|
||||
if (toType == TodoListBlockKeys.type)
|
||||
TodoListBlockKeys.checked: false,
|
||||
blockComponentBackgroundColor:
|
||||
node.attributes[blockComponentBackgroundColor],
|
||||
blockComponentTextDirection:
|
||||
node.attributes[blockComponentTextDirection],
|
||||
blockComponentDelta: (node.delta ?? Delta()).toJson(),
|
||||
},
|
||||
);
|
||||
|
||||
// heading block and callout block should not have children
|
||||
if ([HeadingBlockKeys.type, CalloutBlockKeys.type, QuoteBlockKeys.type]
|
||||
.contains(toType)) {
|
||||
afterNode = afterNode.copyWith(
|
||||
children: [],
|
||||
);
|
||||
insertedNode.add(afterNode);
|
||||
insertedNode.addAll(node.children.map((e) => e.copyWith()));
|
||||
} else {
|
||||
insertedNode.add(afterNode);
|
||||
}
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNodes(node.path, [
|
||||
afterNode,
|
||||
...insertedNode,
|
||||
]);
|
||||
transaction.deleteNode(node);
|
||||
transaction.insertNodes(
|
||||
node.path,
|
||||
insertedNode,
|
||||
);
|
||||
transaction.deleteNodes(selectedNodes);
|
||||
await editorState.apply(transaction);
|
||||
|
||||
return true;
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||
@ -16,6 +14,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
// this flag is used to disable the tooltip of the block when it is dragged
|
||||
@ -248,7 +247,7 @@ class _OptionButtonFeedbackState extends State<_OptionButtonFeedback> {
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionButton extends StatelessWidget {
|
||||
class _OptionButton extends StatefulWidget {
|
||||
const _OptionButton({
|
||||
required this.controller,
|
||||
required this.editorState,
|
||||
@ -261,10 +260,45 @@ class _OptionButton extends StatelessWidget {
|
||||
final BlockComponentContext blockComponentContext;
|
||||
final ValueNotifier<bool> isDragging;
|
||||
|
||||
@override
|
||||
State<_OptionButton> createState() => _OptionButtonState();
|
||||
}
|
||||
|
||||
const _interceptorKey = 'document_option_button_interceptor';
|
||||
|
||||
class _OptionButtonState extends State<_OptionButton> {
|
||||
late final gestureInterceptor = SelectionGestureInterceptor(
|
||||
key: _interceptorKey,
|
||||
canTap: (details) => !_isTapInBounds(details.globalPosition),
|
||||
);
|
||||
|
||||
// the selection will be cleared when tap the option button
|
||||
// so we need to restore the selection after tap the option button
|
||||
Selection? beforeSelection;
|
||||
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.editorState.service.selectionService.registerGestureInterceptor(
|
||||
gestureInterceptor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.editorState.service.selectionService.unregisterGestureInterceptor(
|
||||
_interceptorKey,
|
||||
);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: isDragging,
|
||||
valueListenable: widget.isDragging,
|
||||
builder: (context, isDragging, child) {
|
||||
return BlockActionButton(
|
||||
svg: FlowySvgs.drag_element_s,
|
||||
@ -291,7 +325,11 @@ class _OptionButton extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
controller.show();
|
||||
if (widget.editorState.selection != null) {
|
||||
beforeSelection = widget.editorState.selection;
|
||||
}
|
||||
|
||||
widget.controller.show();
|
||||
|
||||
// update selection
|
||||
_updateBlockSelection();
|
||||
@ -302,23 +340,35 @@ class _OptionButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _updateBlockSelection() {
|
||||
final startNode = blockComponentContext.node;
|
||||
var endNode = startNode;
|
||||
while (endNode.children.isNotEmpty) {
|
||||
endNode = endNode.children.last;
|
||||
if (beforeSelection == null) {
|
||||
final path = widget.blockComponentContext.node.path;
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: path),
|
||||
);
|
||||
widget.editorState.updateSelectionWithReason(
|
||||
selection,
|
||||
customSelectionType: SelectionType.block,
|
||||
);
|
||||
} else {
|
||||
widget.editorState.updateSelectionWithReason(
|
||||
beforeSelection!,
|
||||
customSelectionType: SelectionType.block,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isTapInBounds(Offset offset) {
|
||||
if (renderBox == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final start = Position(path: startNode.path);
|
||||
final end = endNode.selectable?.end() ??
|
||||
Position(
|
||||
path: endNode.path,
|
||||
offset: endNode.delta?.length ?? 0,
|
||||
);
|
||||
|
||||
editorState.selectionType = SelectionType.block;
|
||||
editorState.selection = Selection(
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
final localPosition = renderBox!.globalToLocal(offset);
|
||||
final result = renderBox!.paintBounds.contains(localPosition);
|
||||
if (result) {
|
||||
beforeSelection = widget.editorState.selection;
|
||||
} else {
|
||||
beforeSelection = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,8 +53,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: a0b3c72
|
||||
resolved-ref: a0b3c7289b8c4073b47793f665e70a511324f9b9
|
||||
ref: bcd1208
|
||||
resolved-ref: bcd12082fea75cdbbea8b090bf938aae7dc9f4ad
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "4.0.0"
|
||||
@ -1535,10 +1535,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -1933,10 +1933,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
string_validator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2238,10 +2238,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.1"
|
||||
version: "14.2.5"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -43,7 +43,7 @@ dependencies:
|
||||
|
||||
# Desktop Drop uses Cross File (XFile) data type
|
||||
desktop_drop: ^0.4.4
|
||||
device_info_plus: ^10.1.0
|
||||
device_info_plus:
|
||||
dotted_border: ^2.0.0+3
|
||||
easy_localization: ^3.0.2
|
||||
envied: ^0.5.2
|
||||
@ -161,6 +161,7 @@ dev_dependencies:
|
||||
|
||||
dependency_overrides:
|
||||
http: ^1.0.0
|
||||
device_info_plus: ^10.1.0
|
||||
|
||||
url_protocol:
|
||||
git:
|
||||
@ -170,7 +171,7 @@ dependency_overrides:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "a0b3c72"
|
||||
ref: "bcd1208"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/bl
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
@ -16,6 +17,7 @@ void main() {
|
||||
Document document,
|
||||
String originalType,
|
||||
String originalText, {
|
||||
Selection? selection,
|
||||
String? toType,
|
||||
void Function(EditorState editorState, Node node)? afterTurnInto,
|
||||
}) async {
|
||||
@ -33,6 +35,12 @@ void main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
editorState.selectionType = SelectionType.block;
|
||||
editorState.selection = selection ??
|
||||
Selection.collapsed(
|
||||
Position(path: [0]),
|
||||
);
|
||||
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, originalType);
|
||||
final result = await cubit.turnIntoBlock(
|
||||
@ -49,6 +57,11 @@ void main() {
|
||||
);
|
||||
|
||||
// turn it back the originalType for the next test
|
||||
editorState.selectionType = SelectionType.block;
|
||||
editorState.selection = selection ??
|
||||
Selection.collapsed(
|
||||
Position(path: [0]),
|
||||
);
|
||||
await cubit.turnIntoBlock(
|
||||
originalType,
|
||||
newNode,
|
||||
@ -165,122 +178,358 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('from nested list to heading', () async {
|
||||
const text = 'bulleted list';
|
||||
const nestedText1 = 'nested bulleted list 1';
|
||||
const nestedText2 = 'nested bulleted list 2';
|
||||
const nestedText3 = 'nested bulleted list 3';
|
||||
final document = createDocument([
|
||||
bulletedListNode(
|
||||
text: text,
|
||||
children: [
|
||||
bulletedListNode(
|
||||
text: nestedText1,
|
||||
),
|
||||
bulletedListNode(
|
||||
text: nestedText2,
|
||||
),
|
||||
bulletedListNode(
|
||||
text: nestedText3,
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
await checkTurnInto(
|
||||
document,
|
||||
BulletedListBlockKeys.type,
|
||||
text,
|
||||
toType: HeadingBlockKeys.type,
|
||||
afterTurnInto: (editorState, node) {
|
||||
expect(node.type, HeadingBlockKeys.type);
|
||||
expect(node.children.length, 0);
|
||||
expect(node.delta!.toPlainText(), text);
|
||||
for (final type in [
|
||||
HeadingBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
CalloutBlockKeys.type,
|
||||
]) {
|
||||
test('from nested bulleted list to $type', () async {
|
||||
const text = 'bulleted list';
|
||||
const nestedText1 = 'nested bulleted list 1';
|
||||
const nestedText2 = 'nested bulleted list 2';
|
||||
const nestedText3 = 'nested bulleted list 3';
|
||||
final document = createDocument([
|
||||
bulletedListNode(
|
||||
text: text,
|
||||
children: [
|
||||
bulletedListNode(
|
||||
text: nestedText1,
|
||||
),
|
||||
bulletedListNode(
|
||||
text: nestedText2,
|
||||
),
|
||||
bulletedListNode(
|
||||
text: nestedText3,
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
await checkTurnInto(
|
||||
document,
|
||||
BulletedListBlockKeys.type,
|
||||
text,
|
||||
toType: type,
|
||||
afterTurnInto: (editorState, node) {
|
||||
expect(node.type, type);
|
||||
expect(node.children.length, 0);
|
||||
expect(node.delta!.toPlainText(), text);
|
||||
|
||||
expect(editorState.document.root.children.length, 4);
|
||||
expect(
|
||||
editorState.document.root.children[1].type,
|
||||
BulletedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[1].delta!.toPlainText(),
|
||||
nestedText1,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[2].type,
|
||||
BulletedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[2].delta!.toPlainText(),
|
||||
nestedText2,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[3].type,
|
||||
BulletedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[3].delta!.toPlainText(),
|
||||
nestedText3,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
expect(editorState.document.root.children.length, 4);
|
||||
expect(
|
||||
editorState.document.root.children[1].type,
|
||||
BulletedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[1].delta!.toPlainText(),
|
||||
nestedText1,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[2].type,
|
||||
BulletedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[2].delta!.toPlainText(),
|
||||
nestedText2,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[3].type,
|
||||
BulletedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[3].delta!.toPlainText(),
|
||||
nestedText3,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('from numbered list to heading', () async {
|
||||
const text = 'numbered list';
|
||||
const nestedText1 = 'nested numbered list 1';
|
||||
const nestedText2 = 'nested numbered list 2';
|
||||
const nestedText3 = 'nested numbered list 3';
|
||||
for (final type in [
|
||||
HeadingBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
CalloutBlockKeys.type,
|
||||
]) {
|
||||
test('from nested numbered list to $type', () async {
|
||||
const text = 'numbered list';
|
||||
const nestedText1 = 'nested numbered list 1';
|
||||
const nestedText2 = 'nested numbered list 2';
|
||||
const nestedText3 = 'nested numbered list 3';
|
||||
final document = createDocument([
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(text),
|
||||
children: [
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(nestedText1),
|
||||
),
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(nestedText2),
|
||||
),
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(nestedText3),
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
await checkTurnInto(
|
||||
document,
|
||||
NumberedListBlockKeys.type,
|
||||
text,
|
||||
toType: type,
|
||||
afterTurnInto: (editorState, node) {
|
||||
expect(node.type, type);
|
||||
expect(node.children.length, 0);
|
||||
expect(node.delta!.toPlainText(), text);
|
||||
|
||||
expect(editorState.document.root.children.length, 4);
|
||||
expect(
|
||||
editorState.document.root.children[1].type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[1].delta!.toPlainText(),
|
||||
nestedText1,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[2].type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[2].delta!.toPlainText(),
|
||||
nestedText2,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[3].type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[3].delta!.toPlainText(),
|
||||
nestedText3,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
for (final type in [
|
||||
HeadingBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
CalloutBlockKeys.type,
|
||||
]) {
|
||||
// numbered list, bulleted list, todo list
|
||||
// before
|
||||
// - numbered list 1
|
||||
// - nested list 1
|
||||
// - bulleted list 2
|
||||
// - nested list 2
|
||||
// - todo list 3
|
||||
// - nested list 3
|
||||
// after
|
||||
// - heading 1
|
||||
// - nested list 1
|
||||
// - heading 2
|
||||
// - nested list 2
|
||||
// - heading 3
|
||||
// - nested list 3
|
||||
test('from nested mixed list to $type', () async {
|
||||
const text1 = 'numbered list 1';
|
||||
const text2 = 'bulleted list 2';
|
||||
const text3 = 'todo list 3';
|
||||
const nestedText1 = 'nested list 1';
|
||||
const nestedText2 = 'nested list 2';
|
||||
const nestedText3 = 'nested list 3';
|
||||
final document = createDocument([
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(text1),
|
||||
children: [
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(nestedText1),
|
||||
),
|
||||
],
|
||||
),
|
||||
bulletedListNode(
|
||||
delta: Delta()..insert(text2),
|
||||
children: [
|
||||
bulletedListNode(
|
||||
delta: Delta()..insert(nestedText2),
|
||||
),
|
||||
],
|
||||
),
|
||||
todoListNode(
|
||||
checked: false,
|
||||
text: text3,
|
||||
children: [
|
||||
todoListNode(
|
||||
checked: false,
|
||||
text: nestedText3,
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
await checkTurnInto(
|
||||
document,
|
||||
NumberedListBlockKeys.type,
|
||||
text1,
|
||||
toType: type,
|
||||
selection: Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [2]),
|
||||
),
|
||||
afterTurnInto: (editorState, node) {
|
||||
final nodes = editorState.document.root.children;
|
||||
expect(nodes.length, 6);
|
||||
final texts = [
|
||||
text1,
|
||||
nestedText1,
|
||||
text2,
|
||||
nestedText2,
|
||||
text3,
|
||||
nestedText3,
|
||||
];
|
||||
final types = [
|
||||
type,
|
||||
NumberedListBlockKeys.type,
|
||||
type,
|
||||
BulletedListBlockKeys.type,
|
||||
type,
|
||||
TodoListBlockKeys.type,
|
||||
];
|
||||
for (var i = 0; i < 6; i++) {
|
||||
expect(nodes[i].type, types[i]);
|
||||
expect(nodes[i].children.length, 0);
|
||||
expect(nodes[i].delta!.toPlainText(), texts[i]);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
for (final type in [
|
||||
ParagraphBlockKeys.type,
|
||||
BulletedListBlockKeys.type,
|
||||
NumberedListBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
]) {
|
||||
// numbered list, bulleted list, todo list
|
||||
// before
|
||||
// - numbered list 1
|
||||
// - nested list 1
|
||||
// - bulleted list 2
|
||||
// - nested list 2
|
||||
// - todo list 3
|
||||
// - nested list 3
|
||||
// after
|
||||
// - new_list_type
|
||||
// - nested list 1
|
||||
// - new_list_type
|
||||
// - nested list 2
|
||||
// - new_list_type
|
||||
// - nested list 3
|
||||
test('from nested mixed list to $type', () async {
|
||||
const text1 = 'numbered list 1';
|
||||
const text2 = 'bulleted list 2';
|
||||
const text3 = 'todo list 3';
|
||||
const nestedText1 = 'nested list 1';
|
||||
const nestedText2 = 'nested list 2';
|
||||
const nestedText3 = 'nested list 3';
|
||||
final document = createDocument([
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(text1),
|
||||
children: [
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(nestedText1),
|
||||
),
|
||||
],
|
||||
),
|
||||
bulletedListNode(
|
||||
delta: Delta()..insert(text2),
|
||||
children: [
|
||||
bulletedListNode(
|
||||
delta: Delta()..insert(nestedText2),
|
||||
),
|
||||
],
|
||||
),
|
||||
todoListNode(
|
||||
checked: false,
|
||||
text: text3,
|
||||
children: [
|
||||
todoListNode(
|
||||
checked: false,
|
||||
text: nestedText3,
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
await checkTurnInto(
|
||||
document,
|
||||
NumberedListBlockKeys.type,
|
||||
text1,
|
||||
toType: type,
|
||||
selection: Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [2]),
|
||||
),
|
||||
afterTurnInto: (editorState, node) {
|
||||
final nodes = editorState.document.root.children;
|
||||
expect(nodes.length, 3);
|
||||
final texts = [
|
||||
text1,
|
||||
text2,
|
||||
text3,
|
||||
];
|
||||
final nestedTexts = [
|
||||
nestedText1,
|
||||
nestedText2,
|
||||
nestedText3,
|
||||
];
|
||||
final types = [
|
||||
NumberedListBlockKeys.type,
|
||||
BulletedListBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
];
|
||||
for (var i = 0; i < 3; i++) {
|
||||
expect(nodes[i].type, type);
|
||||
expect(nodes[i].children.length, 1);
|
||||
expect(nodes[i].delta!.toPlainText(), texts[i]);
|
||||
expect(nodes[i].children[0].type, types[i]);
|
||||
expect(nodes[i].children[0].delta!.toPlainText(), nestedTexts[i]);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('undo, redo', () async {
|
||||
const text1 = 'numbered list 1';
|
||||
const nestedText1 = 'nested list 1';
|
||||
final document = createDocument([
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(text),
|
||||
delta: Delta()..insert(text1),
|
||||
children: [
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(nestedText1),
|
||||
),
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(nestedText2),
|
||||
),
|
||||
numberedListNode(
|
||||
delta: Delta()..insert(nestedText3),
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
await checkTurnInto(
|
||||
document,
|
||||
NumberedListBlockKeys.type,
|
||||
text,
|
||||
text1,
|
||||
toType: HeadingBlockKeys.type,
|
||||
afterTurnInto: (editorState, node) {
|
||||
expect(node.type, HeadingBlockKeys.type);
|
||||
expect(node.children.length, 0);
|
||||
expect(node.delta!.toPlainText(), text);
|
||||
|
||||
expect(editorState.document.root.children.length, 4);
|
||||
expect(
|
||||
editorState.document.root.children[1].type,
|
||||
NumberedListBlockKeys.type,
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(path: [0]),
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[1].delta!.toPlainText(),
|
||||
nestedText1,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[2].type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[2].delta!.toPlainText(),
|
||||
nestedText2,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[3].type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
editorState.document.root.children[3].delta!.toPlainText(),
|
||||
nestedText3,
|
||||
KeyEventResult result = undoCommand.execute(editorState);
|
||||
expect(result, KeyEventResult.handled);
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(path: [0]),
|
||||
);
|
||||
result = redoCommand.execute(editorState);
|
||||
expect(result, KeyEventResult.handled);
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user