mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-10-31 18:15:09 +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,22 +209,33 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> { | |||||||
|     Node node, { |     Node node, { | ||||||
|     int? level, |     int? level, | ||||||
|   }) async { |   }) async { | ||||||
|  |     final selection = editorState.selection; | ||||||
|  |     if (selection == null) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     final toType = type; |     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( |       Log.info( | ||||||
|         'Turn into block: from ${node.type} to $type', |         'Turn into block: from ${node.type} to $type', | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|     if (type == node.type && type != HeadingBlockKeys.type) { |  | ||||||
|       Log.info('Block type is the same'); |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|       Node afterNode = node.copyWith( |       Node afterNode = node.copyWith( | ||||||
|         type: type, |         type: type, | ||||||
|         attributes: { |         attributes: { | ||||||
|           if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level, |           if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level, | ||||||
|         if (toType == TodoListBlockKeys.type) TodoListBlockKeys.checked: false, |           if (toType == TodoListBlockKeys.type) | ||||||
|  |             TodoListBlockKeys.checked: false, | ||||||
|           blockComponentBackgroundColor: |           blockComponentBackgroundColor: | ||||||
|               node.attributes[blockComponentBackgroundColor], |               node.attributes[blockComponentBackgroundColor], | ||||||
|           blockComponentTextDirection: |           blockComponentTextDirection: | ||||||
| @ -232,21 +243,26 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> { | |||||||
|           blockComponentDelta: (node.delta ?? Delta()).toJson(), |           blockComponentDelta: (node.delta ?? Delta()).toJson(), | ||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|     final insertedNode = []; | 
 | ||||||
|       // heading block and callout block should not have children |       // heading block and callout block should not have children | ||||||
|     if ([HeadingBlockKeys.type, CalloutBlockKeys.type].contains(toType)) { |       if ([HeadingBlockKeys.type, CalloutBlockKeys.type, QuoteBlockKeys.type] | ||||||
|  |           .contains(toType)) { | ||||||
|         afterNode = afterNode.copyWith( |         afterNode = afterNode.copyWith( | ||||||
|           children: [], |           children: [], | ||||||
|         ); |         ); | ||||||
|  |         insertedNode.add(afterNode); | ||||||
|         insertedNode.addAll(node.children.map((e) => e.copyWith())); |         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), | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     final start = Position(path: startNode.path); |  | ||||||
|     final end = endNode.selectable?.end() ?? |  | ||||||
|         Position( |  | ||||||
|           path: endNode.path, |  | ||||||
|           offset: endNode.delta?.length ?? 0, |  | ||||||
|       ); |       ); | ||||||
| 
 |       widget.editorState.updateSelectionWithReason( | ||||||
|     editorState.selectionType = SelectionType.block; |         selection, | ||||||
|     editorState.selection = Selection( |         customSelectionType: SelectionType.block, | ||||||
|       start: start, |       ); | ||||||
|       end: end, |     } else { | ||||||
|  |       widget.editorState.updateSelectionWithReason( | ||||||
|  |         beforeSelection!, | ||||||
|  |         customSelectionType: SelectionType.block, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   bool _isTapInBounds(Offset offset) { | ||||||
|  |     if (renderBox == null) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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" |     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,7 +178,12 @@ void main() { | |||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     test('from nested list to heading', () async { |     for (final type in [ | ||||||
|  |       HeadingBlockKeys.type, | ||||||
|  |       QuoteBlockKeys.type, | ||||||
|  |       CalloutBlockKeys.type, | ||||||
|  |     ]) { | ||||||
|  |       test('from nested bulleted list to $type', () async { | ||||||
|         const text = 'bulleted list'; |         const text = 'bulleted list'; | ||||||
|         const nestedText1 = 'nested bulleted list 1'; |         const nestedText1 = 'nested bulleted list 1'; | ||||||
|         const nestedText2 = 'nested bulleted list 2'; |         const nestedText2 = 'nested bulleted list 2'; | ||||||
| @ -190,9 +208,9 @@ void main() { | |||||||
|           document, |           document, | ||||||
|           BulletedListBlockKeys.type, |           BulletedListBlockKeys.type, | ||||||
|           text, |           text, | ||||||
|         toType: HeadingBlockKeys.type, |           toType: type, | ||||||
|           afterTurnInto: (editorState, node) { |           afterTurnInto: (editorState, node) { | ||||||
|           expect(node.type, HeadingBlockKeys.type); |             expect(node.type, type); | ||||||
|             expect(node.children.length, 0); |             expect(node.children.length, 0); | ||||||
|             expect(node.delta!.toPlainText(), text); |             expect(node.delta!.toPlainText(), text); | ||||||
| 
 | 
 | ||||||
| @ -224,8 +242,14 @@ void main() { | |||||||
|           }, |           }, | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     test('from numbered list to heading', () async { |     for (final type in [ | ||||||
|  |       HeadingBlockKeys.type, | ||||||
|  |       QuoteBlockKeys.type, | ||||||
|  |       CalloutBlockKeys.type, | ||||||
|  |     ]) { | ||||||
|  |       test('from nested numbered list to $type', () async { | ||||||
|         const text = 'numbered list'; |         const text = 'numbered list'; | ||||||
|         const nestedText1 = 'nested numbered list 1'; |         const nestedText1 = 'nested numbered list 1'; | ||||||
|         const nestedText2 = 'nested numbered list 2'; |         const nestedText2 = 'nested numbered list 2'; | ||||||
| @ -250,9 +274,9 @@ void main() { | |||||||
|           document, |           document, | ||||||
|           NumberedListBlockKeys.type, |           NumberedListBlockKeys.type, | ||||||
|           text, |           text, | ||||||
|         toType: HeadingBlockKeys.type, |           toType: type, | ||||||
|           afterTurnInto: (editorState, node) { |           afterTurnInto: (editorState, node) { | ||||||
|           expect(node.type, HeadingBlockKeys.type); |             expect(node.type, type); | ||||||
|             expect(node.children.length, 0); |             expect(node.children.length, 0); | ||||||
|             expect(node.delta!.toPlainText(), text); |             expect(node.delta!.toPlainText(), text); | ||||||
| 
 | 
 | ||||||
| @ -284,5 +308,230 @@ void main() { | |||||||
|           }, |           }, | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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(text1), | ||||||
|  |           children: [ | ||||||
|  |             numberedListNode( | ||||||
|  |               delta: Delta()..insert(nestedText1), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ]); | ||||||
|  |       await checkTurnInto( | ||||||
|  |         document, | ||||||
|  |         NumberedListBlockKeys.type, | ||||||
|  |         text1, | ||||||
|  |         toType: HeadingBlockKeys.type, | ||||||
|  |         afterTurnInto: (editorState, node) { | ||||||
|  |           expect(editorState.document.root.children.length, 2); | ||||||
|  |           editorState.selection = Selection.collapsed( | ||||||
|  |             Position(path: [0]), | ||||||
|  |           ); | ||||||
|  |           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
	 Lucas
						Lucas