diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock index 49a5879fa6..79143cb186 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock @@ -24,7 +24,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart index 3b7307f039..a4322c59fe 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart @@ -37,7 +37,7 @@ class SelectionMenuItemWidget extends StatelessWidget { : MaterialStateProperty.all(Colors.transparent), ), label: Text( - item.name, + item.name(), textAlign: TextAlign.left, style: const TextStyle( color: Colors.black, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 8d3d1e3453..4fbc480dfd 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -124,7 +124,7 @@ List get defaultSelectionMenuItems => _defaultSelectionMenuItems; final List _defaultSelectionMenuItems = [ SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.text, + name: () => AppFlowyEditorLocalizations.current.text, icon: _selectionMenuIcon('text'), keywords: ['text'], handler: (editorState, _, __) { @@ -132,7 +132,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.heading1, + name: () => AppFlowyEditorLocalizations.current.heading1, icon: _selectionMenuIcon('h1'), keywords: ['heading 1, h1'], handler: (editorState, _, __) { @@ -140,7 +140,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.heading2, + name: () => AppFlowyEditorLocalizations.current.heading2, icon: _selectionMenuIcon('h2'), keywords: ['heading 2, h2'], handler: (editorState, _, __) { @@ -148,7 +148,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.heading3, + name: () => AppFlowyEditorLocalizations.current.heading3, icon: _selectionMenuIcon('h3'), keywords: ['heading 3, h3'], handler: (editorState, _, __) { @@ -156,13 +156,13 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.image, + name: () => AppFlowyEditorLocalizations.current.image, icon: _selectionMenuIcon('image'), keywords: ['image'], handler: showImageUploadMenu, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.bulletedList, + name: () => AppFlowyEditorLocalizations.current.bulletedList, icon: _selectionMenuIcon('bulleted_list'), keywords: ['bulleted list', 'list', 'unordered list'], handler: (editorState, _, __) { @@ -170,7 +170,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.checkbox, + name: () => AppFlowyEditorLocalizations.current.checkbox, icon: _selectionMenuIcon('checkbox'), keywords: ['todo list', 'list', 'checkbox list'], handler: (editorState, _, __) { @@ -178,7 +178,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.quote, + name: () => AppFlowyEditorLocalizations.current.quote, icon: _selectionMenuIcon('quote'), keywords: ['quote', 'refer'], handler: (editorState, _, __) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index 1553085349..c5a35ef73c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -6,27 +6,54 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +typedef SelectionMenuItemHandler = void Function( + EditorState editorState, + SelectionMenuService menuService, + BuildContext context, + ); + /// Selection Menu Item class SelectionMenuItem { SelectionMenuItem({ required this.name, required this.icon, required this.keywords, - required this.handler, - }); + required SelectionMenuItemHandler handler, + }) { + this.handler = (editorState, menuService, context) { + _deleteToSlash(editorState); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + handler(editorState, menuService, context); + }); + }; + } - final String name; + final String Function() name; final Widget icon; /// Customizes keywords for item. /// /// The keywords are used to quickly retrieve items. final List keywords; - final void Function( - EditorState editorState, - SelectionMenuService menuService, - BuildContext context, - ) handler; + late final SelectionMenuItemHandler handler; + + void _deleteToSlash(EditorState editorState) { + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final nodes = selectionService.currentSelectedNodes; + if (selection != null && nodes.length == 1) { + final node = nodes.first as TextNode; + final end = selection.start.offset; + final start = node.toRawString().substring(0, end).lastIndexOf('/'); + TransactionBuilder(editorState) + ..deleteText( + node, + start, + selection.start.offset - start, + ) + ..commit(); + } + } } class SelectionMenuWidget extends StatefulWidget { @@ -204,11 +231,8 @@ class _SelectionMenuWidgetState extends State { if (event.logicalKey == LogicalKeyboardKey.enter) { if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) { - _deleteLastCharacters(length: keyword.length + 1); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _showingItems[_selectedIndex] - .handler(widget.editorState, widget.menuService, context); - }); + _showingItems[_selectedIndex] + .handler(widget.editorState, widget.menuService, context); return KeyEventResult.handled; } } else if (event.logicalKey == LogicalKeyboardKey.escape) { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index 6ac10d593a..1495105746 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -12,29 +12,40 @@ void main() async { }); group('selection_menu_widget.dart', () { - // const i = defaultSelectionMenuItems.length; - // - // Because the `defaultSelectionMenuItems` uses localization, - // and the MaterialApp has not been initialized at the time of getting the value, - // it will crash. - // - // Use const value temporarily instead. - const i = 7; - testWidgets('Selects number.$i item in selection menu', (tester) async { - final editor = await _prepare(tester); - for (var j = 0; j < i; j++) { - await editor.pressLogicKey(LogicalKeyboardKey.arrowDown); - } + for (var i = 0; i < defaultSelectionMenuItems.length; i += 1) { + testWidgets('Selects number.$i item in selection menu with enter', ( + tester) async { + final editor = await _prepare(tester); + for (var j = 0; j < i; j++) { + await editor.pressLogicKey(LogicalKeyboardKey.arrowDown); + } - await editor.pressLogicKey(LogicalKeyboardKey.enter); - expect( - find.byType(SelectionMenuWidget, skipOffstage: false), - findsNothing, - ); - if (defaultSelectionMenuItems[i].name != 'Image') { - await _testDefaultSelectionMenuItems(i, editor); - } - }); + await editor.pressLogicKey(LogicalKeyboardKey.enter); + expect( + find.byType(SelectionMenuWidget, skipOffstage: false), + findsNothing, + ); + if (defaultSelectionMenuItems[i].name() != 'Image') { + await _testDefaultSelectionMenuItems(i, editor); + } + }); + + testWidgets('Selects number.$i item in selection menu with click', ( + tester) async { + final editor = await _prepare(tester); + + await tester.tap(find.byType(SelectionMenuItemWidget).at(i)); + await tester.pumpAndSettle(); + + expect( + find.byType(SelectionMenuWidget, skipOffstage: false), + findsNothing, + ); + if (defaultSelectionMenuItems[i].name() != 'Image') { + await _testDefaultSelectionMenuItems(i, editor); + } + }); + } testWidgets('Search item in selection menu util no results', (tester) async { @@ -137,23 +148,27 @@ Future _testDefaultSelectionMenuItems( int index, EditorWidgetTester editor) async { expect(editor.documentLength, 4); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), 'Welcome to Appflowy 😁'); final node = editor.nodeAtPath([2]); final item = defaultSelectionMenuItems[index]; - if (item.name == 'Text') { + final itemName = item.name(); + if (itemName == 'Text') { expect(node?.subtype == null, true); - } else if (item.name == 'Heading 1') { + } else if (itemName == 'Heading 1') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h1); - } else if (item.name == 'Heading 2') { + } else if (itemName == 'Heading 2') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h2); - } else if (item.name == 'Heading 3') { + } else if (itemName == 'Heading 3') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h3); - } else if (item.name == 'Bulleted list') { + } else if (itemName == 'Bulleted list') { expect(node?.subtype, BuiltInAttributeKey.bulletedList); - } else if (item.name == 'Checkbox') { + } else if (itemName == 'Checkbox') { expect(node?.subtype, BuiltInAttributeKey.checkbox); expect(node?.attributes.check, false); + } else if (itemName == 'Quote') { + expect(node?.subtype, BuiltInAttributeKey.quote); } }