mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-10-30 17:38:40 +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);
|
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('turn into', (tester) async {
|
testWidgets('turn into - single line', (tester) async {
|
||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
await tester.tapAnonymousSignInButton();
|
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) {
|
if (UniversalPlatform.isDesktop) {
|
||||||
builder.showActions =
|
builder.showActions =
|
||||||
(node) => node.parent?.type != TableCellBlockKeys.type;
|
(node) => node.parent?.type != TableCellBlockKeys.type;
|
||||||
|
builder.configuration = builder.configuration.copyWith(
|
||||||
|
blockSelectionAreaMargin: (_) => const EdgeInsets.symmetric(
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
builder.actionBuilder = (context, state) {
|
builder.actionBuilder = (context, state) {
|
||||||
final top = builder.configuration.padding(context.node).top;
|
final top = builder.configuration.padding(context.node).top;
|
||||||
|
|||||||
@ -209,44 +209,60 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
|
|||||||
Node node, {
|
Node node, {
|
||||||
int? level,
|
int? level,
|
||||||
}) async {
|
}) async {
|
||||||
final toType = type;
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
Log.info(
|
|
||||||
'Turn into block: from ${node.type} to $type',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (type == node.type && type != HeadingBlockKeys.type) {
|
|
||||||
Log.info('Block type is the same');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Node afterNode = node.copyWith(
|
final toType = type;
|
||||||
type: type,
|
|
||||||
attributes: {
|
// only handle the node in the same depth
|
||||||
if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level,
|
final selectedNodes = editorState
|
||||||
if (toType == TodoListBlockKeys.type) TodoListBlockKeys.checked: false,
|
.getNodesInSelection(selection.normalized)
|
||||||
blockComponentBackgroundColor:
|
.where((e) => e.path.length == node.path.length)
|
||||||
node.attributes[blockComponentBackgroundColor],
|
.toList();
|
||||||
blockComponentTextDirection:
|
Log.info('turnIntoBlock selectedNodes $selectedNodes');
|
||||||
node.attributes[blockComponentTextDirection],
|
|
||||||
blockComponentDelta: (node.delta ?? Delta()).toJson(),
|
final insertedNode = <Node>[];
|
||||||
},
|
|
||||||
);
|
for (final node in selectedNodes) {
|
||||||
final insertedNode = [];
|
Log.info(
|
||||||
// heading block and callout block should not have children
|
'Turn into block: from ${node.type} to $type',
|
||||||
if ([HeadingBlockKeys.type, CalloutBlockKeys.type].contains(toType)) {
|
|
||||||
afterNode = afterNode.copyWith(
|
|
||||||
children: [],
|
|
||||||
);
|
);
|
||||||
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;
|
final transaction = editorState.transaction;
|
||||||
transaction.insertNodes(node.path, [
|
transaction.insertNodes(
|
||||||
afterNode,
|
node.path,
|
||||||
...insertedNode,
|
insertedNode,
|
||||||
]);
|
);
|
||||||
transaction.deleteNode(node);
|
transaction.deleteNodes(selectedNodes);
|
||||||
await editorState.apply(transaction);
|
await editorState.apply(transaction);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_notification.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:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
// this flag is used to disable the tooltip of the block when it is dragged
|
// 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({
|
const _OptionButton({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
@ -261,10 +260,45 @@ class _OptionButton extends StatelessWidget {
|
|||||||
final BlockComponentContext blockComponentContext;
|
final BlockComponentContext blockComponentContext;
|
||||||
final ValueNotifier<bool> isDragging;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable: isDragging,
|
valueListenable: widget.isDragging,
|
||||||
builder: (context, isDragging, child) {
|
builder: (context, isDragging, child) {
|
||||||
return BlockActionButton(
|
return BlockActionButton(
|
||||||
svg: FlowySvgs.drag_element_s,
|
svg: FlowySvgs.drag_element_s,
|
||||||
@ -291,7 +325,11 @@ class _OptionButton extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
controller.show();
|
if (widget.editorState.selection != null) {
|
||||||
|
beforeSelection = widget.editorState.selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.controller.show();
|
||||||
|
|
||||||
// update selection
|
// update selection
|
||||||
_updateBlockSelection();
|
_updateBlockSelection();
|
||||||
@ -302,23 +340,35 @@ class _OptionButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateBlockSelection() {
|
void _updateBlockSelection() {
|
||||||
final startNode = blockComponentContext.node;
|
if (beforeSelection == null) {
|
||||||
var endNode = startNode;
|
final path = widget.blockComponentContext.node.path;
|
||||||
while (endNode.children.isNotEmpty) {
|
final selection = Selection.collapsed(
|
||||||
endNode = endNode.children.last;
|
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 localPosition = renderBox!.globalToLocal(offset);
|
||||||
final end = endNode.selectable?.end() ??
|
final result = renderBox!.paintBounds.contains(localPosition);
|
||||||
Position(
|
if (result) {
|
||||||
path: endNode.path,
|
beforeSelection = widget.editorState.selection;
|
||||||
offset: endNode.delta?.length ?? 0,
|
} else {
|
||||||
);
|
beforeSelection = null;
|
||||||
|
}
|
||||||
editorState.selectionType = SelectionType.block;
|
return result;
|
||||||
editorState.selection = Selection(
|
|
||||||
start: start,
|
|
||||||
end: end,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,8 +53,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: a0b3c72
|
ref: bcd1208
|
||||||
resolved-ref: a0b3c7289b8c4073b47793f665e70a511324f9b9
|
resolved-ref: bcd12082fea75cdbbea8b090bf938aae7dc9f4ad
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
@ -1535,10 +1535,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -1933,10 +1933,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
string_validator:
|
string_validator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -2238,10 +2238,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.1"
|
version: "14.2.5"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -43,7 +43,7 @@ dependencies:
|
|||||||
|
|
||||||
# Desktop Drop uses Cross File (XFile) data type
|
# Desktop Drop uses Cross File (XFile) data type
|
||||||
desktop_drop: ^0.4.4
|
desktop_drop: ^0.4.4
|
||||||
device_info_plus: ^10.1.0
|
device_info_plus:
|
||||||
dotted_border: ^2.0.0+3
|
dotted_border: ^2.0.0+3
|
||||||
easy_localization: ^3.0.2
|
easy_localization: ^3.0.2
|
||||||
envied: ^0.5.2
|
envied: ^0.5.2
|
||||||
@ -161,6 +161,7 @@ dev_dependencies:
|
|||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
http: ^1.0.0
|
http: ^1.0.0
|
||||||
|
device_info_plus: ^10.1.0
|
||||||
|
|
||||||
url_protocol:
|
url_protocol:
|
||||||
git:
|
git:
|
||||||
@ -170,7 +171,7 @@ dependency_overrides:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: "a0b3c72"
|
ref: "bcd1208"
|
||||||
|
|
||||||
appflowy_editor_plugins:
|
appflowy_editor_plugins:
|
||||||
git:
|
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/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -16,6 +17,7 @@ void main() {
|
|||||||
Document document,
|
Document document,
|
||||||
String originalType,
|
String originalType,
|
||||||
String originalText, {
|
String originalText, {
|
||||||
|
Selection? selection,
|
||||||
String? toType,
|
String? toType,
|
||||||
void Function(EditorState editorState, Node node)? afterTurnInto,
|
void Function(EditorState editorState, Node node)? afterTurnInto,
|
||||||
}) async {
|
}) async {
|
||||||
@ -33,6 +35,12 @@ void main() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editorState.selectionType = SelectionType.block;
|
||||||
|
editorState.selection = selection ??
|
||||||
|
Selection.collapsed(
|
||||||
|
Position(path: [0]),
|
||||||
|
);
|
||||||
|
|
||||||
final node = editorState.getNodeAtPath([0])!;
|
final node = editorState.getNodeAtPath([0])!;
|
||||||
expect(node.type, originalType);
|
expect(node.type, originalType);
|
||||||
final result = await cubit.turnIntoBlock(
|
final result = await cubit.turnIntoBlock(
|
||||||
@ -49,6 +57,11 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// turn it back the originalType for the next test
|
// turn it back the originalType for the next test
|
||||||
|
editorState.selectionType = SelectionType.block;
|
||||||
|
editorState.selection = selection ??
|
||||||
|
Selection.collapsed(
|
||||||
|
Position(path: [0]),
|
||||||
|
);
|
||||||
await cubit.turnIntoBlock(
|
await cubit.turnIntoBlock(
|
||||||
originalType,
|
originalType,
|
||||||
newNode,
|
newNode,
|
||||||
@ -165,122 +178,358 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('from nested list to heading', () async {
|
for (final type in [
|
||||||
const text = 'bulleted list';
|
HeadingBlockKeys.type,
|
||||||
const nestedText1 = 'nested bulleted list 1';
|
QuoteBlockKeys.type,
|
||||||
const nestedText2 = 'nested bulleted list 2';
|
CalloutBlockKeys.type,
|
||||||
const nestedText3 = 'nested bulleted list 3';
|
]) {
|
||||||
final document = createDocument([
|
test('from nested bulleted list to $type', () async {
|
||||||
bulletedListNode(
|
const text = 'bulleted list';
|
||||||
text: text,
|
const nestedText1 = 'nested bulleted list 1';
|
||||||
children: [
|
const nestedText2 = 'nested bulleted list 2';
|
||||||
bulletedListNode(
|
const nestedText3 = 'nested bulleted list 3';
|
||||||
text: nestedText1,
|
final document = createDocument([
|
||||||
),
|
bulletedListNode(
|
||||||
bulletedListNode(
|
text: text,
|
||||||
text: nestedText2,
|
children: [
|
||||||
),
|
bulletedListNode(
|
||||||
bulletedListNode(
|
text: nestedText1,
|
||||||
text: nestedText3,
|
),
|
||||||
),
|
bulletedListNode(
|
||||||
],
|
text: nestedText2,
|
||||||
),
|
),
|
||||||
]);
|
bulletedListNode(
|
||||||
await checkTurnInto(
|
text: nestedText3,
|
||||||
document,
|
),
|
||||||
BulletedListBlockKeys.type,
|
],
|
||||||
text,
|
),
|
||||||
toType: HeadingBlockKeys.type,
|
]);
|
||||||
afterTurnInto: (editorState, node) {
|
await checkTurnInto(
|
||||||
expect(node.type, HeadingBlockKeys.type);
|
document,
|
||||||
expect(node.children.length, 0);
|
BulletedListBlockKeys.type,
|
||||||
expect(node.delta!.toPlainText(), text);
|
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.length, 4);
|
||||||
expect(
|
expect(
|
||||||
editorState.document.root.children[1].type,
|
editorState.document.root.children[1].type,
|
||||||
BulletedListBlockKeys.type,
|
BulletedListBlockKeys.type,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
editorState.document.root.children[1].delta!.toPlainText(),
|
editorState.document.root.children[1].delta!.toPlainText(),
|
||||||
nestedText1,
|
nestedText1,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
editorState.document.root.children[2].type,
|
editorState.document.root.children[2].type,
|
||||||
BulletedListBlockKeys.type,
|
BulletedListBlockKeys.type,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
editorState.document.root.children[2].delta!.toPlainText(),
|
editorState.document.root.children[2].delta!.toPlainText(),
|
||||||
nestedText2,
|
nestedText2,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
editorState.document.root.children[3].type,
|
editorState.document.root.children[3].type,
|
||||||
BulletedListBlockKeys.type,
|
BulletedListBlockKeys.type,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
editorState.document.root.children[3].delta!.toPlainText(),
|
editorState.document.root.children[3].delta!.toPlainText(),
|
||||||
nestedText3,
|
nestedText3,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('from numbered list to heading', () async {
|
for (final type in [
|
||||||
const text = 'numbered list';
|
HeadingBlockKeys.type,
|
||||||
const nestedText1 = 'nested numbered list 1';
|
QuoteBlockKeys.type,
|
||||||
const nestedText2 = 'nested numbered list 2';
|
CalloutBlockKeys.type,
|
||||||
const nestedText3 = 'nested numbered list 3';
|
]) {
|
||||||
|
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([
|
final document = createDocument([
|
||||||
numberedListNode(
|
numberedListNode(
|
||||||
delta: Delta()..insert(text),
|
delta: Delta()..insert(text1),
|
||||||
children: [
|
children: [
|
||||||
numberedListNode(
|
numberedListNode(
|
||||||
delta: Delta()..insert(nestedText1),
|
delta: Delta()..insert(nestedText1),
|
||||||
),
|
),
|
||||||
numberedListNode(
|
|
||||||
delta: Delta()..insert(nestedText2),
|
|
||||||
),
|
|
||||||
numberedListNode(
|
|
||||||
delta: Delta()..insert(nestedText3),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
await checkTurnInto(
|
await checkTurnInto(
|
||||||
document,
|
document,
|
||||||
NumberedListBlockKeys.type,
|
NumberedListBlockKeys.type,
|
||||||
text,
|
text1,
|
||||||
toType: HeadingBlockKeys.type,
|
toType: HeadingBlockKeys.type,
|
||||||
afterTurnInto: (editorState, node) {
|
afterTurnInto: (editorState, node) {
|
||||||
expect(node.type, HeadingBlockKeys.type);
|
expect(editorState.document.root.children.length, 2);
|
||||||
expect(node.children.length, 0);
|
editorState.selection = Selection.collapsed(
|
||||||
expect(node.delta!.toPlainText(), text);
|
Position(path: [0]),
|
||||||
|
|
||||||
expect(editorState.document.root.children.length, 4);
|
|
||||||
expect(
|
|
||||||
editorState.document.root.children[1].type,
|
|
||||||
NumberedListBlockKeys.type,
|
|
||||||
);
|
);
|
||||||
expect(
|
KeyEventResult result = undoCommand.execute(editorState);
|
||||||
editorState.document.root.children[1].delta!.toPlainText(),
|
expect(result, KeyEventResult.handled);
|
||||||
nestedText1,
|
expect(editorState.document.root.children.length, 1);
|
||||||
);
|
editorState.selection = Selection.collapsed(
|
||||||
expect(
|
Position(path: [0]),
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
result = redoCommand.execute(editorState);
|
||||||
|
expect(result, KeyEventResult.handled);
|
||||||
|
expect(editorState.document.root.children.length, 2);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user