fix: remove keyword when click selection menu item

This commit is contained in:
Haoran Lin
2022-09-23 01:38:33 +08:00
parent 206b06f023
commit 5782dec45c
6 changed files with 91 additions and 52 deletions

View File

@ -29,7 +29,7 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3

View File

@ -37,7 +37,7 @@ class SelectionMenuItemWidget extends StatelessWidget {
: MaterialStateProperty.all(Colors.transparent), : MaterialStateProperty.all(Colors.transparent),
), ),
label: Text( label: Text(
item.name, item.name(),
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,

View File

@ -124,7 +124,7 @@ List<SelectionMenuItem> get defaultSelectionMenuItems =>
_defaultSelectionMenuItems; _defaultSelectionMenuItems;
final List<SelectionMenuItem> _defaultSelectionMenuItems = [ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.text, name: () => AppFlowyEditorLocalizations.current.text,
icon: _selectionMenuIcon('text'), icon: _selectionMenuIcon('text'),
keywords: ['text'], keywords: ['text'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -132,7 +132,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.heading1, name: () => AppFlowyEditorLocalizations.current.heading1,
icon: _selectionMenuIcon('h1'), icon: _selectionMenuIcon('h1'),
keywords: ['heading 1, h1'], keywords: ['heading 1, h1'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -140,7 +140,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.heading2, name: () => AppFlowyEditorLocalizations.current.heading2,
icon: _selectionMenuIcon('h2'), icon: _selectionMenuIcon('h2'),
keywords: ['heading 2, h2'], keywords: ['heading 2, h2'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -148,7 +148,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.heading3, name: () => AppFlowyEditorLocalizations.current.heading3,
icon: _selectionMenuIcon('h3'), icon: _selectionMenuIcon('h3'),
keywords: ['heading 3, h3'], keywords: ['heading 3, h3'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -156,13 +156,13 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.image, name: () => AppFlowyEditorLocalizations.current.image,
icon: _selectionMenuIcon('image'), icon: _selectionMenuIcon('image'),
keywords: ['image'], keywords: ['image'],
handler: showImageUploadMenu, handler: showImageUploadMenu,
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.bulletedList, name: () => AppFlowyEditorLocalizations.current.bulletedList,
icon: _selectionMenuIcon('bulleted_list'), icon: _selectionMenuIcon('bulleted_list'),
keywords: ['bulleted list', 'list', 'unordered list'], keywords: ['bulleted list', 'list', 'unordered list'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -170,7 +170,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.checkbox, name: () => AppFlowyEditorLocalizations.current.checkbox,
icon: _selectionMenuIcon('checkbox'), icon: _selectionMenuIcon('checkbox'),
keywords: ['todo list', 'list', 'checkbox list'], keywords: ['todo list', 'list', 'checkbox list'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -178,7 +178,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.quote, name: () => AppFlowyEditorLocalizations.current.quote,
icon: _selectionMenuIcon('quote'), icon: _selectionMenuIcon('quote'),
keywords: ['quote', 'refer'], keywords: ['quote', 'refer'],
handler: (editorState, _, __) { handler: (editorState, _, __) {

View File

@ -6,27 +6,54 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
typedef SelectionMenuItemHandler = void Function(
EditorState editorState,
SelectionMenuService menuService,
BuildContext context,
);
/// Selection Menu Item /// Selection Menu Item
class SelectionMenuItem { class SelectionMenuItem {
SelectionMenuItem({ SelectionMenuItem({
required this.name, required this.name,
required this.icon, required this.icon,
required this.keywords, 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; final Widget icon;
/// Customizes keywords for item. /// Customizes keywords for item.
/// ///
/// The keywords are used to quickly retrieve items. /// The keywords are used to quickly retrieve items.
final List<String> keywords; final List<String> keywords;
final void Function( late final SelectionMenuItemHandler handler;
EditorState editorState,
SelectionMenuService menuService, void _deleteToSlash(EditorState editorState) {
BuildContext context, final selectionService = editorState.service.selectionService;
) handler; 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 { class SelectionMenuWidget extends StatefulWidget {
@ -204,11 +231,8 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
if (event.logicalKey == LogicalKeyboardKey.enter) { if (event.logicalKey == LogicalKeyboardKey.enter) {
if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) { if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) {
_deleteLastCharacters(length: keyword.length + 1);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_showingItems[_selectedIndex] _showingItems[_selectedIndex]
.handler(widget.editorState, widget.menuService, context); .handler(widget.editorState, widget.menuService, context);
});
return KeyEventResult.handled; return KeyEventResult.handled;
} }
} else if (event.logicalKey == LogicalKeyboardKey.escape) { } else if (event.logicalKey == LogicalKeyboardKey.escape) {

View File

@ -17,7 +17,7 @@ void main() async {
final menuService = _TestSelectionMenuService(); final menuService = _TestSelectionMenuService();
const icon = Icon(Icons.abc); const icon = Icon(Icons.abc);
final item = SelectionMenuItem( final item = SelectionMenuItem(
name: 'example', name: () => 'example',
icon: icon, icon: icon,
keywords: ['example A', 'example B'], keywords: ['example A', 'example B'],
handler: (editorState, menuService, context) { handler: (editorState, menuService, context) {

View File

@ -13,15 +13,9 @@ void main() async {
}); });
group('selection_menu_widget.dart', () { group('selection_menu_widget.dart', () {
// const i = defaultSelectionMenuItems.length; for (var i = 0; i < defaultSelectionMenuItems.length; i += 1) {
// testWidgets('Selects number.$i item in selection menu with enter', (
// Because the `defaultSelectionMenuItems` uses localization, tester) async {
// 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); final editor = await _prepare(tester);
for (var j = 0; j < i; j++) { for (var j = 0; j < i; j++) {
await editor.pressLogicKey(LogicalKeyboardKey.arrowDown); await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
@ -32,11 +26,28 @@ void main() async {
find.byType(SelectionMenuWidget, skipOffstage: false), find.byType(SelectionMenuWidget, skipOffstage: false),
findsNothing, findsNothing,
); );
if (defaultSelectionMenuItems[i].name != 'Image') { if (defaultSelectionMenuItems[i].name() != 'Image') {
await _testDefaultSelectionMenuItems(i, editor); 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', testWidgets('Search item in selection menu util no results',
(tester) async { (tester) async {
final editor = await _prepare(tester); final editor = await _prepare(tester);
@ -138,23 +149,27 @@ Future<void> _testDefaultSelectionMenuItems(
int index, EditorWidgetTester editor) async { int index, EditorWidgetTester editor) async {
expect(editor.documentLength, 4); expect(editor.documentLength, 4);
expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); 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 node = editor.nodeAtPath([2]);
final item = defaultSelectionMenuItems[index]; final item = defaultSelectionMenuItems[index];
if (item.name == 'Text') { final itemName = item.name();
if (itemName == 'Text') {
expect(node?.subtype == null, true); expect(node?.subtype == null, true);
} else if (item.name == 'Heading 1') { } else if (itemName == 'Heading 1') {
expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h1); expect(node?.attributes.heading, BuiltInAttributeKey.h1);
} else if (item.name == 'Heading 2') { } else if (itemName == 'Heading 2') {
expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h2); expect(node?.attributes.heading, BuiltInAttributeKey.h2);
} else if (item.name == 'Heading 3') { } else if (itemName == 'Heading 3') {
expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h3); expect(node?.attributes.heading, BuiltInAttributeKey.h3);
} else if (item.name == 'Bulleted list') { } else if (itemName == 'Bulleted list') {
expect(node?.subtype, BuiltInAttributeKey.bulletedList); expect(node?.subtype, BuiltInAttributeKey.bulletedList);
} else if (item.name == 'Checkbox') { } else if (itemName == 'Checkbox') {
expect(node?.subtype, BuiltInAttributeKey.checkbox); expect(node?.subtype, BuiltInAttributeKey.checkbox);
expect(node?.attributes.check, false); expect(node?.attributes.check, false);
} else if (itemName == 'Quote') {
expect(node?.subtype, BuiltInAttributeKey.quote);
} }
} }