mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-11-04 12:03:28 +00:00 
			
		
		
		
	feat: support slash menu on mobile (#7368)
* feat: support slash menu on mobile * feat: support at menu on mobile * feat: support plus menu on mobile
This commit is contained in:
		
							parent
							
								
									b75fd673cd
								
							
						
					
					
						commit
						9e98680861
					
				@ -0,0 +1,41 @@
 | 
			
		||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
 | 
			
		||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';
 | 
			
		||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
 | 
			
		||||
import 'package:flutter_test/flutter_test.dart';
 | 
			
		||||
import 'package:integration_test/integration_test.dart';
 | 
			
		||||
 | 
			
		||||
import '../../shared/util.dart';
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
 | 
			
		||||
  const title = 'Test At Menu';
 | 
			
		||||
 | 
			
		||||
  group('at menu', () {
 | 
			
		||||
    testWidgets('show at menu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createPageAndShowAtMenu(title);
 | 
			
		||||
      final menuWidget = find.byType(MobileInlineActionsMenu);
 | 
			
		||||
      expect(menuWidget, findsOneWidget);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    testWidgets('search by at menu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createPageAndShowAtMenu(title);
 | 
			
		||||
      const searchText = gettingStarted;
 | 
			
		||||
      await tester.ime.insertText(searchText);
 | 
			
		||||
      final actionWidgets = find.byType(MobileInlineActionsWidget);
 | 
			
		||||
      expect(actionWidgets, findsNWidgets(2));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    testWidgets('tap at menu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createPageAndShowAtMenu(title);
 | 
			
		||||
      const searchText = gettingStarted;
 | 
			
		||||
      await tester.ime.insertText(searchText);
 | 
			
		||||
      final actionWidgets = find.byType(MobileInlineActionsWidget);
 | 
			
		||||
      await tester.tap(actionWidgets.last);
 | 
			
		||||
      expect(find.byType(MentionPageBlock), findsOneWidget);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,11 @@
 | 
			
		||||
import 'package:integration_test/integration_test.dart';
 | 
			
		||||
 | 
			
		||||
import 'at_menu_test.dart' as at_menu;
 | 
			
		||||
import 'at_menu_test.dart' as at_menu_test;
 | 
			
		||||
import 'page_style_test.dart' as page_style_test;
 | 
			
		||||
import 'plus_menu_test.dart' as plus_menu_test;
 | 
			
		||||
import 'simple_table_test.dart' as simple_table_test;
 | 
			
		||||
import 'slash_menu_test.dart' as slash_menu;
 | 
			
		||||
import 'title_test.dart' as title_test;
 | 
			
		||||
import 'toolbar_test.dart' as toolbar_test;
 | 
			
		||||
 | 
			
		||||
@ -13,6 +16,9 @@ void main() {
 | 
			
		||||
  title_test.main();
 | 
			
		||||
  page_style_test.main();
 | 
			
		||||
  plus_menu_test.main();
 | 
			
		||||
  at_menu_test.main();
 | 
			
		||||
  simple_table_test.main();
 | 
			
		||||
  toolbar_test.main();
 | 
			
		||||
  slash_menu.main();
 | 
			
		||||
  at_menu.main();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,9 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:appflowy/generated/locale_keys.g.dart';
 | 
			
		||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
 | 
			
		||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';
 | 
			
		||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
 | 
			
		||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
@ -85,5 +88,32 @@ void main() {
 | 
			
		||||
        equals(Selection.collapsed(Position(path: [2]))),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const title = 'Test Plus Menu';
 | 
			
		||||
    testWidgets('show plus menu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createPageAndShowPlusMenu(title);
 | 
			
		||||
      final menuWidget = find.byType(MobileInlineActionsMenu);
 | 
			
		||||
      expect(menuWidget, findsOneWidget);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    testWidgets('search by plus menu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createPageAndShowPlusMenu(title);
 | 
			
		||||
      const searchText = gettingStarted;
 | 
			
		||||
      await tester.ime.insertText(searchText);
 | 
			
		||||
      final actionWidgets = find.byType(MobileInlineActionsWidget);
 | 
			
		||||
      expect(actionWidgets, findsNWidgets(2));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    testWidgets('tap plus menu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createPageAndShowPlusMenu(title);
 | 
			
		||||
      const searchText = gettingStarted;
 | 
			
		||||
      await tester.ime.insertText(searchText);
 | 
			
		||||
      final actionWidgets = find.byType(MobileInlineActionsWidget);
 | 
			
		||||
      await tester.tap(actionWidgets.last);
 | 
			
		||||
      expect(find.byType(MentionPageBlock), findsOneWidget);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,74 @@
 | 
			
		||||
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart';
 | 
			
		||||
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart';
 | 
			
		||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart';
 | 
			
		||||
import 'package:flutter/cupertino.dart';
 | 
			
		||||
import 'package:flutter_test/flutter_test.dart';
 | 
			
		||||
import 'package:integration_test/integration_test.dart';
 | 
			
		||||
 | 
			
		||||
import '../../shared/util.dart';
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
 | 
			
		||||
  const title = 'Test Slash Menu';
 | 
			
		||||
 | 
			
		||||
  group('slash menu', () {
 | 
			
		||||
    testWidgets('show slash menu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createPageAndShowSlashMenu(title);
 | 
			
		||||
      final menuWidget = find.byType(MobileSelectionMenuWidget);
 | 
			
		||||
      expect(menuWidget, findsOneWidget);
 | 
			
		||||
      final items =
 | 
			
		||||
          (menuWidget.evaluate().first.widget as MobileSelectionMenuWidget)
 | 
			
		||||
              .items;
 | 
			
		||||
      int i = 0;
 | 
			
		||||
      for (final item in items) {
 | 
			
		||||
        final localItem = mobileItems[i];
 | 
			
		||||
        expect(item.name, localItem.name);
 | 
			
		||||
        i++;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    testWidgets('search by slash menu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createPageAndShowSlashMenu(title);
 | 
			
		||||
      const searchText = 'Heading';
 | 
			
		||||
      await tester.ime.insertText(searchText);
 | 
			
		||||
      final itemWidgets = find.byType(MobileSelectionMenuItemWidget);
 | 
			
		||||
      int number = 0;
 | 
			
		||||
      for (final mobileItem in mobileItems) {
 | 
			
		||||
        for (final item in mobileItem.children) {
 | 
			
		||||
          if (item.name.toLowerCase().contains(searchText.toLowerCase())) {
 | 
			
		||||
            number++;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      expect(itemWidgets, findsNWidgets(number));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    testWidgets('tap to show submenu', (tester) async {
 | 
			
		||||
      await tester.launchInAnonymousMode();
 | 
			
		||||
      await tester.createNewDocumentOnMobile(title);
 | 
			
		||||
      await tester.editor.tapLineOfEditorAt(0);
 | 
			
		||||
      final listview = find.descendant(
 | 
			
		||||
        of: find.byType(MobileSelectionMenuWidget),
 | 
			
		||||
        matching: find.byType(ListView),
 | 
			
		||||
      );
 | 
			
		||||
      for (final item in mobileItems) {
 | 
			
		||||
        await tester.editor.showSlashMenu();
 | 
			
		||||
        await tester.scrollUntilVisible(
 | 
			
		||||
          find.text(item.name),
 | 
			
		||||
          50,
 | 
			
		||||
          scrollable: listview,
 | 
			
		||||
          duration: const Duration(milliseconds: 250),
 | 
			
		||||
        );
 | 
			
		||||
        await tester.tap(find.text(item.name));
 | 
			
		||||
        final childrenLength = ((listview.evaluate().first.widget as ListView)
 | 
			
		||||
                .childrenDelegate as SliverChildListDelegate)
 | 
			
		||||
            .children
 | 
			
		||||
            .length;
 | 
			
		||||
        expect(childrenLength, item.children.length);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@ -974,6 +974,27 @@ extension CommonOperations on WidgetTester {
 | 
			
		||||
      ..writeAsBytesSync(imagePath.buffer.asUint8List());
 | 
			
		||||
    return EmojiIconData.custom(imageFile.path);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// create new page and show slash menu
 | 
			
		||||
  Future<void> createPageAndShowSlashMenu(String title) async {
 | 
			
		||||
    await createNewDocumentOnMobile(title);
 | 
			
		||||
    await editor.tapLineOfEditorAt(0);
 | 
			
		||||
    await editor.showSlashMenu();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// create new page and show at menu
 | 
			
		||||
  Future<void> createPageAndShowAtMenu(String title) async {
 | 
			
		||||
    await createNewDocumentOnMobile(title);
 | 
			
		||||
    await editor.tapLineOfEditorAt(0);
 | 
			
		||||
    await editor.showAtMenu();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// create new page and show plus menu
 | 
			
		||||
  Future<void> createPageAndShowPlusMenu(String title) async {
 | 
			
		||||
    await createNewDocumentOnMobile(title);
 | 
			
		||||
    await editor.tapLineOfEditorAt(0);
 | 
			
		||||
    await editor.showPlusMenu();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension SettingsFinder on CommonFinders {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,253 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:appflowy/generated/locale_keys.g.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flowy_infra_ui/style_widget/text.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'mobile_inline_actions_menu_group.dart';
 | 
			
		||||
 | 
			
		||||
extension _StartWithsSort on List<InlineActionsResult> {
 | 
			
		||||
  void sortByStartsWithKeyword(String search) => sort(
 | 
			
		||||
        (a, b) {
 | 
			
		||||
          final aCount = a.startsWithKeywords
 | 
			
		||||
                  ?.where(
 | 
			
		||||
                    (key) => search.toLowerCase().startsWith(key),
 | 
			
		||||
                  )
 | 
			
		||||
                  .length ??
 | 
			
		||||
              0;
 | 
			
		||||
 | 
			
		||||
          final bCount = b.startsWithKeywords
 | 
			
		||||
                  ?.where(
 | 
			
		||||
                    (key) => search.toLowerCase().startsWith(key),
 | 
			
		||||
                  )
 | 
			
		||||
                  .length ??
 | 
			
		||||
              0;
 | 
			
		||||
 | 
			
		||||
          if (aCount > bCount) {
 | 
			
		||||
            return -1;
 | 
			
		||||
          } else if (bCount > aCount) {
 | 
			
		||||
            return 1;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return 0;
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const _invalidSearchesAmount = 10;
 | 
			
		||||
 | 
			
		||||
class MobileInlineActionsHandler extends StatefulWidget {
 | 
			
		||||
  const MobileInlineActionsHandler({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.results,
 | 
			
		||||
    required this.editorState,
 | 
			
		||||
    required this.menuService,
 | 
			
		||||
    required this.onDismiss,
 | 
			
		||||
    required this.style,
 | 
			
		||||
    required this.service,
 | 
			
		||||
    this.startCharAmount = 1,
 | 
			
		||||
    this.startOffset = 0,
 | 
			
		||||
    this.cancelBySpaceHandler,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final List<InlineActionsResult> results;
 | 
			
		||||
  final EditorState editorState;
 | 
			
		||||
  final InlineActionsMenuService menuService;
 | 
			
		||||
  final VoidCallback onDismiss;
 | 
			
		||||
  final InlineActionsMenuStyle style;
 | 
			
		||||
  final int startCharAmount;
 | 
			
		||||
  final InlineActionsService service;
 | 
			
		||||
  final bool Function()? cancelBySpaceHandler;
 | 
			
		||||
  final int startOffset;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<MobileInlineActionsHandler> createState() =>
 | 
			
		||||
      _MobileInlineActionsHandlerState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MobileInlineActionsHandlerState
 | 
			
		||||
    extends State<MobileInlineActionsHandler> {
 | 
			
		||||
  final _focusNode =
 | 
			
		||||
      FocusNode(debugLabel: 'mobile_inline_actions_menu_handler');
 | 
			
		||||
 | 
			
		||||
  late List<InlineActionsResult> results = widget.results;
 | 
			
		||||
  int invalidCounter = 0;
 | 
			
		||||
  late int startOffset;
 | 
			
		||||
 | 
			
		||||
  String _search = '';
 | 
			
		||||
 | 
			
		||||
  set search(String search) {
 | 
			
		||||
    _search = search;
 | 
			
		||||
    _doSearch();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _doSearch() async {
 | 
			
		||||
    final List<InlineActionsResult> newResults = [];
 | 
			
		||||
    for (final handler in widget.service.handlers) {
 | 
			
		||||
      final group = await handler.search(_search);
 | 
			
		||||
 | 
			
		||||
      if (group.results.isNotEmpty) {
 | 
			
		||||
        newResults.add(group);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    invalidCounter = results.every((group) => group.results.isEmpty)
 | 
			
		||||
        ? invalidCounter + 1
 | 
			
		||||
        : 0;
 | 
			
		||||
 | 
			
		||||
    if (invalidCounter >= _invalidSearchesAmount) {
 | 
			
		||||
      widget.onDismiss();
 | 
			
		||||
 | 
			
		||||
      // Workaround to bring focus back to editor
 | 
			
		||||
      await editorState.updateSelectionWithReason(editorState.selection);
 | 
			
		||||
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _resetSelection();
 | 
			
		||||
 | 
			
		||||
    newResults.sortByStartsWithKeyword(_search);
 | 
			
		||||
    setState(() => results = newResults);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _resetSelection() {
 | 
			
		||||
    _selectedGroup = 0;
 | 
			
		||||
    _selectedIndex = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int _selectedGroup = 0;
 | 
			
		||||
  int _selectedIndex = 0;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback(
 | 
			
		||||
      (_) => _focusNode.requestFocus(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    startOffset = editorState.selection?.endIndex ?? 0;
 | 
			
		||||
    keepEditorFocusNotifier.increase();
 | 
			
		||||
    editorState.selectionNotifier.addListener(onSelectionChanged);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    editorState.selectionNotifier.removeListener(onSelectionChanged);
 | 
			
		||||
    _focusNode.dispose();
 | 
			
		||||
    keepEditorFocusNotifier.decrease();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final width = editorState.renderBox!.size.width - 24 * 2;
 | 
			
		||||
    return Focus(
 | 
			
		||||
      focusNode: _focusNode,
 | 
			
		||||
      child: Container(
 | 
			
		||||
        constraints: BoxConstraints(
 | 
			
		||||
          maxHeight: 192,
 | 
			
		||||
          minWidth: width,
 | 
			
		||||
          maxWidth: width,
 | 
			
		||||
        ),
 | 
			
		||||
        margin: EdgeInsets.symmetric(horizontal: 24.0),
 | 
			
		||||
        decoration: BoxDecoration(
 | 
			
		||||
          color: widget.style.backgroundColor,
 | 
			
		||||
          borderRadius: BorderRadius.circular(6.0),
 | 
			
		||||
          boxShadow: [
 | 
			
		||||
            BoxShadow(
 | 
			
		||||
              blurRadius: 5,
 | 
			
		||||
              spreadRadius: 1,
 | 
			
		||||
              color: Colors.black.withValues(alpha: 0.1),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        child: noResults
 | 
			
		||||
            ? SizedBox(
 | 
			
		||||
                width: 150,
 | 
			
		||||
                child: FlowyText.regular(
 | 
			
		||||
                  LocaleKeys.inlineActions_noResults.tr(),
 | 
			
		||||
                ),
 | 
			
		||||
              )
 | 
			
		||||
            : SingleChildScrollView(
 | 
			
		||||
                physics: const ClampingScrollPhysics(),
 | 
			
		||||
                child: Material(
 | 
			
		||||
                  color: Colors.transparent,
 | 
			
		||||
                  child: Padding(
 | 
			
		||||
                    padding: EdgeInsets.all(6.0),
 | 
			
		||||
                    child: Column(
 | 
			
		||||
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                      children: results
 | 
			
		||||
                          .where((g) => g.results.isNotEmpty)
 | 
			
		||||
                          .mapIndexed(
 | 
			
		||||
                            (index, group) => MobileInlineActionsGroup(
 | 
			
		||||
                              result: group,
 | 
			
		||||
                              editorState: editorState,
 | 
			
		||||
                              menuService: menuService,
 | 
			
		||||
                              style: widget.style,
 | 
			
		||||
                              onSelected: widget.onDismiss,
 | 
			
		||||
                              startOffset: startOffset - widget.startCharAmount,
 | 
			
		||||
                              endOffset:
 | 
			
		||||
                                  _search.length + widget.startCharAmount,
 | 
			
		||||
                              isLastGroup: index == results.length - 1,
 | 
			
		||||
                              isGroupSelected: _selectedGroup == index,
 | 
			
		||||
                              selectedIndex: _selectedIndex,
 | 
			
		||||
                              onPreSelect: (int value) {
 | 
			
		||||
                                setState(() {
 | 
			
		||||
                                  _selectedGroup = index;
 | 
			
		||||
                                  _selectedIndex = value;
 | 
			
		||||
                                });
 | 
			
		||||
                              },
 | 
			
		||||
                            ),
 | 
			
		||||
                          )
 | 
			
		||||
                          .toList(),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get noResults =>
 | 
			
		||||
      results.isEmpty || results.every((e) => e.results.isEmpty);
 | 
			
		||||
 | 
			
		||||
  int get groupLength => results.length;
 | 
			
		||||
 | 
			
		||||
  int lengthOfGroup(int index) =>
 | 
			
		||||
      results.length > index ? results[index].results.length : -1;
 | 
			
		||||
 | 
			
		||||
  InlineActionsMenuItem handlerOf(int groupIndex, int handlerIndex) =>
 | 
			
		||||
      results[groupIndex].results[handlerIndex];
 | 
			
		||||
 | 
			
		||||
  EditorState get editorState => widget.editorState;
 | 
			
		||||
 | 
			
		||||
  InlineActionsMenuService get menuService => widget.menuService;
 | 
			
		||||
 | 
			
		||||
  void onSelectionChanged() {
 | 
			
		||||
    final selection = editorState.selection;
 | 
			
		||||
    if (selection == null) {
 | 
			
		||||
      menuService.dismiss();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!selection.isCollapsed) {
 | 
			
		||||
      menuService.dismiss();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    final startOffset = widget.startOffset;
 | 
			
		||||
    final endOffset = selection.end.offset;
 | 
			
		||||
    if (endOffset < startOffset) {
 | 
			
		||||
      menuService.dismiss();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    final node = editorState.getNodeAtPath(selection.start.path);
 | 
			
		||||
    final text = node?.delta?.toPlainText() ?? '';
 | 
			
		||||
    final search = text.substring(startOffset, endOffset);
 | 
			
		||||
    this.search = search;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,151 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'mobile_inline_actions_handler.dart';
 | 
			
		||||
 | 
			
		||||
class MobileInlineActionsMenu extends InlineActionsMenuService {
 | 
			
		||||
  MobileInlineActionsMenu({
 | 
			
		||||
    required this.context,
 | 
			
		||||
    required this.editorState,
 | 
			
		||||
    required this.initialResults,
 | 
			
		||||
    required this.style,
 | 
			
		||||
    required this.service,
 | 
			
		||||
    this.startCharAmount = 1,
 | 
			
		||||
    this.cancelBySpaceHandler,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final BuildContext context;
 | 
			
		||||
  final EditorState editorState;
 | 
			
		||||
  final List<InlineActionsResult> initialResults;
 | 
			
		||||
  final bool Function()? cancelBySpaceHandler;
 | 
			
		||||
  final InlineActionsService service;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  final InlineActionsMenuStyle style;
 | 
			
		||||
 | 
			
		||||
  final int startCharAmount;
 | 
			
		||||
 | 
			
		||||
  OverlayEntry? _menuEntry;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dismiss() {
 | 
			
		||||
    if (_menuEntry != null) {
 | 
			
		||||
      editorState.service.keyboardService?.enable();
 | 
			
		||||
      editorState.service.scrollService?.enable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _menuEntry?.remove();
 | 
			
		||||
    _menuEntry = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> show() {
 | 
			
		||||
    final completer = Completer<void>();
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
 | 
			
		||||
      _show();
 | 
			
		||||
      completer.complete();
 | 
			
		||||
    });
 | 
			
		||||
    return completer.future;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _show() {
 | 
			
		||||
    final selectionRects = editorState.selectionRects();
 | 
			
		||||
    if (selectionRects.isEmpty) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const double menuHeight = 192.0;
 | 
			
		||||
    const Offset menuOffset = Offset(0, 10);
 | 
			
		||||
    final Offset editorOffset =
 | 
			
		||||
        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
 | 
			
		||||
    final Size editorSize = editorState.renderBox!.size;
 | 
			
		||||
 | 
			
		||||
    // Default to opening the overlay below
 | 
			
		||||
    Alignment alignment = Alignment.topLeft;
 | 
			
		||||
 | 
			
		||||
    final firstRect = selectionRects.first;
 | 
			
		||||
    Offset offset = firstRect.bottomRight + menuOffset;
 | 
			
		||||
 | 
			
		||||
    // Show above
 | 
			
		||||
    if (offset.dy + menuHeight >= editorOffset.dy + editorSize.height) {
 | 
			
		||||
      offset = firstRect.topRight - menuOffset;
 | 
			
		||||
      alignment = Alignment.bottomLeft;
 | 
			
		||||
 | 
			
		||||
      offset = Offset(
 | 
			
		||||
        offset.dx,
 | 
			
		||||
        MediaQuery.of(context).size.height - offset.dy,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final (left, top, right, bottom) = _getPosition(alignment, offset);
 | 
			
		||||
 | 
			
		||||
    _menuEntry = OverlayEntry(
 | 
			
		||||
      builder: (context) => SizedBox(
 | 
			
		||||
        width: editorSize.width,
 | 
			
		||||
        height: editorSize.height,
 | 
			
		||||
        child: GestureDetector(
 | 
			
		||||
          behavior: HitTestBehavior.opaque,
 | 
			
		||||
          onTap: dismiss,
 | 
			
		||||
          child: Stack(
 | 
			
		||||
            children: [
 | 
			
		||||
              Positioned(
 | 
			
		||||
                top: top,
 | 
			
		||||
                bottom: bottom,
 | 
			
		||||
                left: left,
 | 
			
		||||
                right: right,
 | 
			
		||||
                child: MobileInlineActionsHandler(
 | 
			
		||||
                  service: service,
 | 
			
		||||
                  results: initialResults,
 | 
			
		||||
                  editorState: editorState,
 | 
			
		||||
                  menuService: this,
 | 
			
		||||
                  onDismiss: dismiss,
 | 
			
		||||
                  style: style,
 | 
			
		||||
                  startCharAmount: startCharAmount,
 | 
			
		||||
                  cancelBySpaceHandler: cancelBySpaceHandler,
 | 
			
		||||
                  startOffset: editorState.selection?.start.offset ?? 0,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Overlay.of(context).insert(_menuEntry!);
 | 
			
		||||
 | 
			
		||||
    editorState.service.keyboardService?.disable(showCursor: true);
 | 
			
		||||
    editorState.service.scrollService?.disable();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  (double? left, double? top, double? right, double? bottom) _getPosition(
 | 
			
		||||
    Alignment alignment,
 | 
			
		||||
    Offset offset,
 | 
			
		||||
  ) {
 | 
			
		||||
    double? left, top, right, bottom;
 | 
			
		||||
    switch (alignment) {
 | 
			
		||||
      case Alignment.topLeft:
 | 
			
		||||
        left = 0;
 | 
			
		||||
        top = offset.dy;
 | 
			
		||||
        break;
 | 
			
		||||
      case Alignment.bottomLeft:
 | 
			
		||||
        left = 0;
 | 
			
		||||
        bottom = offset.dy;
 | 
			
		||||
        break;
 | 
			
		||||
      case Alignment.topRight:
 | 
			
		||||
        right = offset.dx;
 | 
			
		||||
        top = offset.dy;
 | 
			
		||||
        break;
 | 
			
		||||
      case Alignment.bottomRight:
 | 
			
		||||
        right = offset.dx;
 | 
			
		||||
        bottom = offset.dy;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (left, top, right, bottom);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,150 @@
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flowy_infra_ui/style_widget/button.dart';
 | 
			
		||||
import 'package:flowy_infra_ui/style_widget/text.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
class MobileInlineActionsGroup extends StatelessWidget {
 | 
			
		||||
  const MobileInlineActionsGroup({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.result,
 | 
			
		||||
    required this.editorState,
 | 
			
		||||
    required this.menuService,
 | 
			
		||||
    required this.style,
 | 
			
		||||
    required this.onSelected,
 | 
			
		||||
    required this.startOffset,
 | 
			
		||||
    required this.endOffset,
 | 
			
		||||
    required this.onPreSelect,
 | 
			
		||||
    this.isLastGroup = false,
 | 
			
		||||
    this.isGroupSelected = false,
 | 
			
		||||
    this.selectedIndex = 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final InlineActionsResult result;
 | 
			
		||||
  final EditorState editorState;
 | 
			
		||||
  final InlineActionsMenuService menuService;
 | 
			
		||||
  final InlineActionsMenuStyle style;
 | 
			
		||||
  final VoidCallback onSelected;
 | 
			
		||||
  final ValueChanged<int> onPreSelect;
 | 
			
		||||
  final int startOffset;
 | 
			
		||||
  final int endOffset;
 | 
			
		||||
 | 
			
		||||
  final bool isLastGroup;
 | 
			
		||||
  final bool isGroupSelected;
 | 
			
		||||
  final int selectedIndex;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      children: [
 | 
			
		||||
        if (result.title != null) ...[
 | 
			
		||||
          SizedBox(
 | 
			
		||||
            height: 36,
 | 
			
		||||
            child: Padding(
 | 
			
		||||
              padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
              child: Align(
 | 
			
		||||
                alignment: Alignment.centerLeft,
 | 
			
		||||
                child: FlowyText.medium(
 | 
			
		||||
                  result.title!,
 | 
			
		||||
                  color: style.groupTextColor,
 | 
			
		||||
                  fontSize: 12,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
        ...result.results.mapIndexed(
 | 
			
		||||
          (index, item) => GestureDetector(
 | 
			
		||||
            onTapDown: (e) {
 | 
			
		||||
              onPreSelect.call(index);
 | 
			
		||||
            },
 | 
			
		||||
            child: MobileInlineActionsWidget(
 | 
			
		||||
              item: item,
 | 
			
		||||
              editorState: editorState,
 | 
			
		||||
              menuService: menuService,
 | 
			
		||||
              isSelected: isGroupSelected && index == selectedIndex,
 | 
			
		||||
              style: style,
 | 
			
		||||
              onSelected: onSelected,
 | 
			
		||||
              startOffset: startOffset,
 | 
			
		||||
              endOffset: endOffset,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MobileInlineActionsWidget extends StatelessWidget {
 | 
			
		||||
  const MobileInlineActionsWidget({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.item,
 | 
			
		||||
    required this.editorState,
 | 
			
		||||
    required this.menuService,
 | 
			
		||||
    required this.isSelected,
 | 
			
		||||
    required this.style,
 | 
			
		||||
    required this.onSelected,
 | 
			
		||||
    required this.startOffset,
 | 
			
		||||
    required this.endOffset,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final InlineActionsMenuItem item;
 | 
			
		||||
  final EditorState editorState;
 | 
			
		||||
  final InlineActionsMenuService menuService;
 | 
			
		||||
  final bool isSelected;
 | 
			
		||||
  final InlineActionsMenuStyle style;
 | 
			
		||||
  final VoidCallback onSelected;
 | 
			
		||||
  final int startOffset;
 | 
			
		||||
  final int endOffset;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final hasIcon = item.icon != null;
 | 
			
		||||
    return Container(
 | 
			
		||||
      height: 36,
 | 
			
		||||
      decoration: BoxDecoration(
 | 
			
		||||
        color: isSelected ? style.menuItemSelectedColor : null,
 | 
			
		||||
        borderRadius: BorderRadius.circular(6.0),
 | 
			
		||||
      ),
 | 
			
		||||
      child: FlowyButton(
 | 
			
		||||
        expand: true,
 | 
			
		||||
        isSelected: isSelected,
 | 
			
		||||
        text: Padding(
 | 
			
		||||
          padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
          child: Align(
 | 
			
		||||
            alignment: Alignment.centerLeft,
 | 
			
		||||
            child: Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                if (hasIcon) ...[
 | 
			
		||||
                  item.icon!.call(isSelected),
 | 
			
		||||
                  SizedBox(width: 12),
 | 
			
		||||
                ],
 | 
			
		||||
                FlowyText.regular(
 | 
			
		||||
                  item.label,
 | 
			
		||||
                  figmaLineHeight: 18,
 | 
			
		||||
                  overflow: TextOverflow.ellipsis,
 | 
			
		||||
                  fontSize: 16,
 | 
			
		||||
                  color: style.menuItemSelectedTextColor,
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        onTap: () => _onPressed(context),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onPressed(BuildContext context) {
 | 
			
		||||
    onSelected();
 | 
			
		||||
    item.onSelected?.call(
 | 
			
		||||
      context,
 | 
			
		||||
      editorState,
 | 
			
		||||
      menuService,
 | 
			
		||||
      (startOffset, endOffset),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,215 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'mobile_selection_menu_widget.dart';
 | 
			
		||||
 | 
			
		||||
class MobileSelectionMenu extends SelectionMenuService {
 | 
			
		||||
  MobileSelectionMenu({
 | 
			
		||||
    required this.context,
 | 
			
		||||
    required this.editorState,
 | 
			
		||||
    required this.selectionMenuItems,
 | 
			
		||||
    this.deleteSlashByDefault = false,
 | 
			
		||||
    this.deleteKeywordsByDefault = false,
 | 
			
		||||
    this.style = SelectionMenuStyle.light,
 | 
			
		||||
    this.itemCountFilter = 0,
 | 
			
		||||
    this.startOffset = 0,
 | 
			
		||||
    this.singleColumn = false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final BuildContext context;
 | 
			
		||||
  final EditorState editorState;
 | 
			
		||||
  final List<SelectionMenuItem> selectionMenuItems;
 | 
			
		||||
  final bool deleteSlashByDefault;
 | 
			
		||||
  final bool deleteKeywordsByDefault;
 | 
			
		||||
  final bool singleColumn;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  final SelectionMenuStyle style;
 | 
			
		||||
 | 
			
		||||
  OverlayEntry? _selectionMenuEntry;
 | 
			
		||||
  Offset _offset = Offset.zero;
 | 
			
		||||
  Alignment _alignment = Alignment.topLeft;
 | 
			
		||||
  final int itemCountFilter;
 | 
			
		||||
  final int startOffset;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dismiss() {
 | 
			
		||||
    if (_selectionMenuEntry != null) {
 | 
			
		||||
      editorState.service.keyboardService?.enable();
 | 
			
		||||
      editorState.service.scrollService?.enable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _selectionMenuEntry?.remove();
 | 
			
		||||
    _selectionMenuEntry = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> show() async {
 | 
			
		||||
    final completer = Completer<void>();
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
 | 
			
		||||
      _show();
 | 
			
		||||
      completer.complete();
 | 
			
		||||
    });
 | 
			
		||||
    return completer.future;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _show() {
 | 
			
		||||
    final selectionRects = editorState.selectionRects();
 | 
			
		||||
    if (selectionRects.isEmpty) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    calculateSelectionMenuOffset(selectionRects.first);
 | 
			
		||||
    final (left, top, right, bottom) = getPosition();
 | 
			
		||||
 | 
			
		||||
    final editorHeight = editorState.renderBox!.size.height;
 | 
			
		||||
    final editorWidth = editorState.renderBox!.size.width;
 | 
			
		||||
 | 
			
		||||
    _selectionMenuEntry = OverlayEntry(
 | 
			
		||||
      builder: (context) {
 | 
			
		||||
        return SizedBox(
 | 
			
		||||
          width: editorWidth,
 | 
			
		||||
          height: editorHeight,
 | 
			
		||||
          child: GestureDetector(
 | 
			
		||||
            behavior: HitTestBehavior.opaque,
 | 
			
		||||
            onTap: dismiss,
 | 
			
		||||
            child: Stack(
 | 
			
		||||
              children: [
 | 
			
		||||
                Positioned(
 | 
			
		||||
                  top: top,
 | 
			
		||||
                  bottom: bottom,
 | 
			
		||||
                  left: left,
 | 
			
		||||
                  right: right,
 | 
			
		||||
                  child: SingleChildScrollView(
 | 
			
		||||
                    scrollDirection: Axis.horizontal,
 | 
			
		||||
                    child: MobileSelectionMenuWidget(
 | 
			
		||||
                      selectionMenuStyle: style,
 | 
			
		||||
                      singleColumn: singleColumn,
 | 
			
		||||
                      items: selectionMenuItems
 | 
			
		||||
                        ..forEach((element) {
 | 
			
		||||
                          if (element is MobileSelectionMenuItem) {
 | 
			
		||||
                            element.deleteSlash = false;
 | 
			
		||||
                            element.deleteKeywords = deleteKeywordsByDefault;
 | 
			
		||||
                            for (final e in element.children) {
 | 
			
		||||
                              e.deleteSlash = deleteSlashByDefault;
 | 
			
		||||
                              e.deleteKeywords = deleteKeywordsByDefault;
 | 
			
		||||
                              e.onSelected = () {
 | 
			
		||||
                                dismiss();
 | 
			
		||||
                              };
 | 
			
		||||
                            }
 | 
			
		||||
                          } else {
 | 
			
		||||
                            element.deleteSlash = deleteSlashByDefault;
 | 
			
		||||
                            element.deleteKeywords = deleteKeywordsByDefault;
 | 
			
		||||
                            element.onSelected = () {
 | 
			
		||||
                              dismiss();
 | 
			
		||||
                            };
 | 
			
		||||
                          }
 | 
			
		||||
                        }),
 | 
			
		||||
                      maxItemInRow: 5,
 | 
			
		||||
                      editorState: editorState,
 | 
			
		||||
                      itemCountFilter: itemCountFilter,
 | 
			
		||||
                      startOffset: startOffset,
 | 
			
		||||
                      menuService: this,
 | 
			
		||||
                      onExit: () {
 | 
			
		||||
                        dismiss();
 | 
			
		||||
                      },
 | 
			
		||||
                      deleteSlashByDefault: deleteSlashByDefault,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Overlay.of(context, rootOverlay: true).insert(_selectionMenuEntry!);
 | 
			
		||||
 | 
			
		||||
    editorState.service.keyboardService?.disable(showCursor: true);
 | 
			
		||||
    editorState.service.scrollService?.disable();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Alignment get alignment {
 | 
			
		||||
    return _alignment;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Offset get offset {
 | 
			
		||||
    return _offset;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  (double? left, double? top, double? right, double? bottom) getPosition() {
 | 
			
		||||
    double? left, top, right, bottom;
 | 
			
		||||
    switch (alignment) {
 | 
			
		||||
      case Alignment.topLeft:
 | 
			
		||||
        left = offset.dx;
 | 
			
		||||
        top = offset.dy;
 | 
			
		||||
        break;
 | 
			
		||||
      case Alignment.bottomLeft:
 | 
			
		||||
        left = offset.dx;
 | 
			
		||||
        bottom = offset.dy;
 | 
			
		||||
        break;
 | 
			
		||||
      case Alignment.topRight:
 | 
			
		||||
        right = offset.dx;
 | 
			
		||||
        top = offset.dy;
 | 
			
		||||
        break;
 | 
			
		||||
      case Alignment.bottomRight:
 | 
			
		||||
        right = offset.dx;
 | 
			
		||||
        bottom = offset.dy;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (left, top, right, bottom);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void calculateSelectionMenuOffset(Rect rect) {
 | 
			
		||||
    // Workaround: We can customize the padding through the [EditorStyle],
 | 
			
		||||
    // but the coordinates of overlay are not properly converted currently.
 | 
			
		||||
    // Just subtract the padding here as a result.
 | 
			
		||||
    const menuHeight = 192.0;
 | 
			
		||||
    const menuOffset = Offset(0, 10);
 | 
			
		||||
    final editorOffset =
 | 
			
		||||
        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
 | 
			
		||||
    final editorHeight = editorState.renderBox!.size.height;
 | 
			
		||||
    final editorWidth = editorState.renderBox!.size.width;
 | 
			
		||||
 | 
			
		||||
    // show below default
 | 
			
		||||
    _alignment = Alignment.topLeft;
 | 
			
		||||
    final bottomRight = rect.bottomRight;
 | 
			
		||||
    final topRight = rect.topRight;
 | 
			
		||||
    var offset = bottomRight + menuOffset;
 | 
			
		||||
    _offset = Offset(
 | 
			
		||||
      offset.dx,
 | 
			
		||||
      offset.dy,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // show above
 | 
			
		||||
    if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {
 | 
			
		||||
      offset = topRight - menuOffset;
 | 
			
		||||
      _alignment = Alignment.bottomLeft;
 | 
			
		||||
 | 
			
		||||
      _offset = Offset(
 | 
			
		||||
        offset.dx,
 | 
			
		||||
        MediaQuery.of(context).size.height - offset.dy,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // show on left
 | 
			
		||||
    if (_offset.dx - editorOffset.dx > editorWidth / 2) {
 | 
			
		||||
      _alignment = _alignment == Alignment.topLeft
 | 
			
		||||
          ? Alignment.topRight
 | 
			
		||||
          : Alignment.bottomRight;
 | 
			
		||||
 | 
			
		||||
      _offset = Offset(
 | 
			
		||||
        editorWidth - _offset.dx + editorOffset.dx,
 | 
			
		||||
        _offset.dy,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
 | 
			
		||||
class MobileSelectionMenuItem extends SelectionMenuItem {
 | 
			
		||||
  MobileSelectionMenuItem({
 | 
			
		||||
    required super.getName,
 | 
			
		||||
    required super.icon,
 | 
			
		||||
    super.keywords = const [],
 | 
			
		||||
    required super.handler,
 | 
			
		||||
    this.children = const [],
 | 
			
		||||
    super.nameBuilder,
 | 
			
		||||
    super.deleteKeywords,
 | 
			
		||||
    super.deleteSlash,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final List<SelectionMenuItem> children;
 | 
			
		||||
 | 
			
		||||
  bool get isNotEmpty => children.isNotEmpty;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,76 @@
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'mobile_selection_menu_item.dart';
 | 
			
		||||
 | 
			
		||||
class MobileSelectionMenuItemWidget extends StatelessWidget {
 | 
			
		||||
  const MobileSelectionMenuItemWidget({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.editorState,
 | 
			
		||||
    required this.menuService,
 | 
			
		||||
    required this.item,
 | 
			
		||||
    required this.isSelected,
 | 
			
		||||
    required this.selectionMenuStyle,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final EditorState editorState;
 | 
			
		||||
  final SelectionMenuService menuService;
 | 
			
		||||
  final SelectionMenuItem item;
 | 
			
		||||
  final bool isSelected;
 | 
			
		||||
  final SelectionMenuStyle selectionMenuStyle;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final style = selectionMenuStyle;
 | 
			
		||||
    final showRightArrow = item is MobileSelectionMenuItem &&
 | 
			
		||||
        (item as MobileSelectionMenuItem).isNotEmpty;
 | 
			
		||||
    return Container(
 | 
			
		||||
      padding: const EdgeInsets.symmetric(horizontal: 6),
 | 
			
		||||
      child: TextButton.icon(
 | 
			
		||||
        icon: item.icon(
 | 
			
		||||
          editorState,
 | 
			
		||||
          false,
 | 
			
		||||
          selectionMenuStyle,
 | 
			
		||||
        ),
 | 
			
		||||
        style: ButtonStyle(
 | 
			
		||||
          alignment: Alignment.centerLeft,
 | 
			
		||||
          overlayColor: WidgetStateProperty.all(
 | 
			
		||||
            style.selectionMenuItemSelectedColor,
 | 
			
		||||
          ),
 | 
			
		||||
          backgroundColor: isSelected
 | 
			
		||||
              ? WidgetStateProperty.all(
 | 
			
		||||
                  style.selectionMenuItemSelectedColor,
 | 
			
		||||
                )
 | 
			
		||||
              : WidgetStateProperty.all(Colors.transparent),
 | 
			
		||||
        ),
 | 
			
		||||
        label: Row(
 | 
			
		||||
          children: [
 | 
			
		||||
            item.nameBuilder?.call(item.name, style, false) ??
 | 
			
		||||
                Text(
 | 
			
		||||
                  item.name,
 | 
			
		||||
                  textAlign: TextAlign.left,
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                    color: style.selectionMenuItemTextColor,
 | 
			
		||||
                    fontSize: 16.0,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
            if (showRightArrow) ...[
 | 
			
		||||
              Spacer(),
 | 
			
		||||
              Icon(
 | 
			
		||||
                Icons.keyboard_arrow_right_rounded,
 | 
			
		||||
                color: Color(0xff1E2022).withValues(alpha: 0.3),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          item.handler(
 | 
			
		||||
            editorState,
 | 
			
		||||
            menuService,
 | 
			
		||||
            context,
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,298 @@
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'mobile_selection_menu_item.dart';
 | 
			
		||||
import 'mobile_selection_menu_item_widget.dart';
 | 
			
		||||
 | 
			
		||||
class MobileSelectionMenuWidget extends StatefulWidget {
 | 
			
		||||
  const MobileSelectionMenuWidget({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.items,
 | 
			
		||||
    required this.itemCountFilter,
 | 
			
		||||
    required this.maxItemInRow,
 | 
			
		||||
    required this.menuService,
 | 
			
		||||
    required this.editorState,
 | 
			
		||||
    required this.onExit,
 | 
			
		||||
    required this.selectionMenuStyle,
 | 
			
		||||
    required this.deleteSlashByDefault,
 | 
			
		||||
    required this.singleColumn,
 | 
			
		||||
    required this.startOffset,
 | 
			
		||||
    this.nameBuilder,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final List<SelectionMenuItem> items;
 | 
			
		||||
  final int itemCountFilter;
 | 
			
		||||
  final int maxItemInRow;
 | 
			
		||||
 | 
			
		||||
  final SelectionMenuService menuService;
 | 
			
		||||
  final EditorState editorState;
 | 
			
		||||
 | 
			
		||||
  final VoidCallback onExit;
 | 
			
		||||
 | 
			
		||||
  final SelectionMenuStyle selectionMenuStyle;
 | 
			
		||||
 | 
			
		||||
  final bool deleteSlashByDefault;
 | 
			
		||||
  final bool singleColumn;
 | 
			
		||||
  final int startOffset;
 | 
			
		||||
 | 
			
		||||
  final SelectionMenuItemNameBuilder? nameBuilder;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<MobileSelectionMenuWidget> createState() =>
 | 
			
		||||
      _MobileSelectionMenuWidgetState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
 | 
			
		||||
  final _focusNode = FocusNode(debugLabel: 'popup_list_widget');
 | 
			
		||||
 | 
			
		||||
  List<SelectionMenuItem> _showingItems = [];
 | 
			
		||||
 | 
			
		||||
  int _searchCounter = 0;
 | 
			
		||||
 | 
			
		||||
  EditorState get editorState => widget.editorState;
 | 
			
		||||
 | 
			
		||||
  SelectionMenuService get menuService => widget.menuService;
 | 
			
		||||
 | 
			
		||||
  String _keyword = '';
 | 
			
		||||
 | 
			
		||||
  String get keyword => _keyword;
 | 
			
		||||
 | 
			
		||||
  int selectedIndex = 0;
 | 
			
		||||
 | 
			
		||||
  List<SelectionMenuItem> get filterItems {
 | 
			
		||||
    final List<SelectionMenuItem> items = [];
 | 
			
		||||
    for (final item in widget.items) {
 | 
			
		||||
      if (item is MobileSelectionMenuItem) {
 | 
			
		||||
        for (final childItem in item.children) {
 | 
			
		||||
          items.add(childItem);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        items.add(item);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return items;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set keyword(String newKeyword) {
 | 
			
		||||
    _keyword = newKeyword;
 | 
			
		||||
 | 
			
		||||
    // Search items according to the keyword, and calculate the length of
 | 
			
		||||
    //  the longest keyword, which is used to dismiss the selection_service.
 | 
			
		||||
    var maxKeywordLength = 0;
 | 
			
		||||
 | 
			
		||||
    final items = newKeyword.isEmpty
 | 
			
		||||
        ? widget.items
 | 
			
		||||
        : filterItems
 | 
			
		||||
            .where(
 | 
			
		||||
              (item) => item.allKeywords.any((keyword) {
 | 
			
		||||
                final value = keyword.contains(newKeyword.toLowerCase());
 | 
			
		||||
                if (value) {
 | 
			
		||||
                  maxKeywordLength = max(maxKeywordLength, keyword.length);
 | 
			
		||||
                }
 | 
			
		||||
                return value;
 | 
			
		||||
              }),
 | 
			
		||||
            )
 | 
			
		||||
            .toList(growable: false);
 | 
			
		||||
 | 
			
		||||
    AppFlowyEditorLog.ui.debug('$items');
 | 
			
		||||
 | 
			
		||||
    if (keyword.length >= maxKeywordLength + 2 &&
 | 
			
		||||
        !(widget.deleteSlashByDefault && _searchCounter < 2)) {
 | 
			
		||||
      return widget.onExit();
 | 
			
		||||
    }
 | 
			
		||||
    setState(() {
 | 
			
		||||
      selectedIndex = 0;
 | 
			
		||||
      _showingItems = items;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (_showingItems.isEmpty) {
 | 
			
		||||
      _searchCounter++;
 | 
			
		||||
    } else {
 | 
			
		||||
      _searchCounter = 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
 | 
			
		||||
    final List<SelectionMenuItem> items = [];
 | 
			
		||||
    for (final item in widget.items) {
 | 
			
		||||
      if (item is MobileSelectionMenuItem) {
 | 
			
		||||
        item.onSelected = () {
 | 
			
		||||
          if (mounted) {
 | 
			
		||||
            setState(() {
 | 
			
		||||
              _showingItems = item.children
 | 
			
		||||
                  .map((e) => e..onSelected = widget.onExit)
 | 
			
		||||
                  .toList();
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      items.add(item);
 | 
			
		||||
    }
 | 
			
		||||
    _showingItems = items;
 | 
			
		||||
 | 
			
		||||
    keepEditorFocusNotifier.increase();
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
      _focusNode.requestFocus();
 | 
			
		||||
    });
 | 
			
		||||
    editorState.selectionNotifier.addListener(onSelectionChanged);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    editorState.selectionNotifier.removeListener(onSelectionChanged);
 | 
			
		||||
    _focusNode.dispose();
 | 
			
		||||
    keepEditorFocusNotifier.decrease();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Focus(
 | 
			
		||||
      focusNode: _focusNode,
 | 
			
		||||
      child: DecoratedBox(
 | 
			
		||||
        decoration: BoxDecoration(
 | 
			
		||||
          color: widget.selectionMenuStyle.selectionMenuBackgroundColor,
 | 
			
		||||
          boxShadow: [
 | 
			
		||||
            BoxShadow(
 | 
			
		||||
              blurRadius: 5,
 | 
			
		||||
              spreadRadius: 1,
 | 
			
		||||
              color: Colors.black.withValues(alpha: 0.1),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
          borderRadius: BorderRadius.circular(6.0),
 | 
			
		||||
        ),
 | 
			
		||||
        child: _showingItems.isEmpty
 | 
			
		||||
            ? _buildNoResultsWidget(context)
 | 
			
		||||
            : _buildResultsWidget(
 | 
			
		||||
                context,
 | 
			
		||||
                _showingItems,
 | 
			
		||||
                widget.itemCountFilter,
 | 
			
		||||
              ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void onSelectionChanged() {
 | 
			
		||||
    final selection = editorState.selection;
 | 
			
		||||
    if (selection == null) {
 | 
			
		||||
      widget.onExit();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!selection.isCollapsed) {
 | 
			
		||||
      widget.onExit();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    final startOffset = widget.startOffset;
 | 
			
		||||
    final endOffset = selection.end.offset;
 | 
			
		||||
    if (endOffset < startOffset) {
 | 
			
		||||
      widget.onExit();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    final node = editorState.getNodeAtPath(selection.start.path);
 | 
			
		||||
    final text = node?.delta?.toPlainText() ?? '';
 | 
			
		||||
    final search = text.substring(startOffset, endOffset);
 | 
			
		||||
    keyword = search;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildResultsWidget(
 | 
			
		||||
    BuildContext buildContext,
 | 
			
		||||
    List<SelectionMenuItem> items,
 | 
			
		||||
    int itemCountFilter,
 | 
			
		||||
  ) {
 | 
			
		||||
    if (widget.singleColumn) {
 | 
			
		||||
      final List<Widget> itemWidgets = [];
 | 
			
		||||
      for (var i = 0; i < items.length; i++) {
 | 
			
		||||
        itemWidgets.add(
 | 
			
		||||
          GestureDetector(
 | 
			
		||||
            onTapDown: (e) {
 | 
			
		||||
              setState(() {
 | 
			
		||||
                selectedIndex = i;
 | 
			
		||||
              });
 | 
			
		||||
            },
 | 
			
		||||
            child: MobileSelectionMenuItemWidget(
 | 
			
		||||
              item: items[i],
 | 
			
		||||
              isSelected: i == selectedIndex,
 | 
			
		||||
              editorState: editorState,
 | 
			
		||||
              menuService: menuService,
 | 
			
		||||
              selectionMenuStyle: widget.selectionMenuStyle,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return ConstrainedBox(
 | 
			
		||||
        constraints: const BoxConstraints(
 | 
			
		||||
          maxHeight: 192,
 | 
			
		||||
          minWidth: 240,
 | 
			
		||||
          maxWidth: 240,
 | 
			
		||||
        ),
 | 
			
		||||
        child: ListView(
 | 
			
		||||
          shrinkWrap: true,
 | 
			
		||||
          padding: EdgeInsets.zero,
 | 
			
		||||
          children: itemWidgets,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      final List<Widget> columns = [];
 | 
			
		||||
      List<Widget> itemWidgets = [];
 | 
			
		||||
      // apply item count filter
 | 
			
		||||
      if (itemCountFilter > 0) {
 | 
			
		||||
        items = items.take(itemCountFilter).toList();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (var i = 0; i < items.length; i++) {
 | 
			
		||||
        if (i != 0 && i % (widget.maxItemInRow) == 0) {
 | 
			
		||||
          columns.add(
 | 
			
		||||
            Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: itemWidgets,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
          itemWidgets = [];
 | 
			
		||||
        }
 | 
			
		||||
        itemWidgets.add(
 | 
			
		||||
          MobileSelectionMenuItemWidget(
 | 
			
		||||
            item: items[i],
 | 
			
		||||
            isSelected: false,
 | 
			
		||||
            editorState: editorState,
 | 
			
		||||
            menuService: menuService,
 | 
			
		||||
            selectionMenuStyle: widget.selectionMenuStyle,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (itemWidgets.isNotEmpty) {
 | 
			
		||||
        columns.add(
 | 
			
		||||
          Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: itemWidgets,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
        itemWidgets = [];
 | 
			
		||||
      }
 | 
			
		||||
      return Row(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
        children: columns,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildNoResultsWidget(BuildContext context) {
 | 
			
		||||
    return const Padding(
 | 
			
		||||
      padding: EdgeInsets.all(8.0),
 | 
			
		||||
      child: SizedBox(
 | 
			
		||||
        width: 140,
 | 
			
		||||
        child: Material(
 | 
			
		||||
          child: Text(
 | 
			
		||||
            "No results",
 | 
			
		||||
            style: TextStyle(fontSize: 18.0, color: Colors.grey),
 | 
			
		||||
            textAlign: TextAlign.center,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -9,6 +9,7 @@ import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
InlineActionsMenuService? _actionsMenuService;
 | 
			
		||||
 | 
			
		||||
Future<void> showLinkToPageMenu(
 | 
			
		||||
  EditorState editorState,
 | 
			
		||||
  SelectionMenuService menuService, {
 | 
			
		||||
@ -60,7 +61,7 @@ Future<void> showLinkToPageMenu(
 | 
			
		||||
      startCharAmount: 0,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    _actionsMenuService?.show();
 | 
			
		||||
    await _actionsMenuService?.show();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/handlers/child_page.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
 | 
			
		||||
@ -47,6 +48,7 @@ CharacterShortcutEvent pageReferenceShortcutPlusSign(
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
InlineActionsMenuService? selectionMenuService;
 | 
			
		||||
 | 
			
		||||
Future<bool> inlinePageReferenceCommandHandler(
 | 
			
		||||
  String character,
 | 
			
		||||
  BuildContext context,
 | 
			
		||||
@ -56,7 +58,7 @@ Future<bool> inlinePageReferenceCommandHandler(
 | 
			
		||||
  String? previousChar,
 | 
			
		||||
}) async {
 | 
			
		||||
  final selection = editorState.selection;
 | 
			
		||||
  if (UniversalPlatform.isMobile || selection == null) {
 | 
			
		||||
  if (selection == null) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -110,32 +112,47 @@ Future<bool> inlinePageReferenceCommandHandler(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (context.mounted) {
 | 
			
		||||
    selectionMenuService = InlineActionsMenu(
 | 
			
		||||
      context: service.context!,
 | 
			
		||||
      editorState: editorState,
 | 
			
		||||
      service: service,
 | 
			
		||||
      initialResults: initialResults,
 | 
			
		||||
      style: style,
 | 
			
		||||
      startCharAmount: previousChar != null ? 2 : 1,
 | 
			
		||||
      cancelBySpaceHandler: () {
 | 
			
		||||
        if (character == _plusChar) {
 | 
			
		||||
          final currentSelection = editorState.selection;
 | 
			
		||||
          if (currentSelection == null) {
 | 
			
		||||
            return false;
 | 
			
		||||
          }
 | 
			
		||||
          // check if the space is after the character
 | 
			
		||||
          if (currentSelection.isCollapsed &&
 | 
			
		||||
              currentSelection.start.offset ==
 | 
			
		||||
                  selection.start.offset + character.length) {
 | 
			
		||||
            _cancelInlinePageReferenceMenu(editorState);
 | 
			
		||||
            return true;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
    keepEditorFocusNotifier.increase();
 | 
			
		||||
    selectionMenuService?.dismiss();
 | 
			
		||||
    selectionMenuService = UniversalPlatform.isMobile
 | 
			
		||||
        ? MobileInlineActionsMenu(
 | 
			
		||||
            context: service.context!,
 | 
			
		||||
            editorState: editorState,
 | 
			
		||||
            service: service,
 | 
			
		||||
            initialResults: initialResults,
 | 
			
		||||
            style: style,
 | 
			
		||||
          )
 | 
			
		||||
        : InlineActionsMenu(
 | 
			
		||||
            context: service.context!,
 | 
			
		||||
            editorState: editorState,
 | 
			
		||||
            service: service,
 | 
			
		||||
            initialResults: initialResults,
 | 
			
		||||
            style: style,
 | 
			
		||||
            startCharAmount: previousChar != null ? 2 : 1,
 | 
			
		||||
            cancelBySpaceHandler: () {
 | 
			
		||||
              if (character == _plusChar) {
 | 
			
		||||
                final currentSelection = editorState.selection;
 | 
			
		||||
                if (currentSelection == null) {
 | 
			
		||||
                  return false;
 | 
			
		||||
                }
 | 
			
		||||
                // check if the space is after the character
 | 
			
		||||
                if (currentSelection.isCollapsed &&
 | 
			
		||||
                    currentSelection.start.offset ==
 | 
			
		||||
                        selection.start.offset + character.length) {
 | 
			
		||||
                  _cancelInlinePageReferenceMenu(editorState);
 | 
			
		||||
                  return true;
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              return false;
 | 
			
		||||
            },
 | 
			
		||||
          );
 | 
			
		||||
    // disable the keyboard service
 | 
			
		||||
    editorState.service.keyboardService?.disable();
 | 
			
		||||
 | 
			
		||||
    selectionMenuService?.show();
 | 
			
		||||
    await selectionMenuService?.show();
 | 
			
		||||
 | 
			
		||||
    // enable the keyboard service
 | 
			
		||||
    editorState.service.keyboardService?.enable();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,6 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu.dart';
 | 
			
		||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:universal_platform/universal_platform.dart';
 | 
			
		||||
 | 
			
		||||
@ -50,6 +48,7 @@ CharacterShortcutEvent customAppFlowySlashCommand({
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SelectionMenuService? _selectionMenuService;
 | 
			
		||||
 | 
			
		||||
Future<bool> _showSlashMenu(
 | 
			
		||||
  EditorState editorState, {
 | 
			
		||||
  required SlashMenuItemsBuilder itemsBuilder,
 | 
			
		||||
@ -59,10 +58,6 @@ Future<bool> _showSlashMenu(
 | 
			
		||||
  SelectionMenuStyle style = SelectionMenuStyle.light,
 | 
			
		||||
  required Set<String> supportSlashMenuNodeTypes,
 | 
			
		||||
}) async {
 | 
			
		||||
  if (UniversalPlatform.isMobile) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final selection = editorState.selection;
 | 
			
		||||
  if (selection == null) {
 | 
			
		||||
    return false;
 | 
			
		||||
@ -99,25 +94,32 @@ Future<bool> _showSlashMenu(
 | 
			
		||||
 | 
			
		||||
  final context = editorState.getNodeAtPath(selection.start.path)?.context;
 | 
			
		||||
  if (context != null && context.mounted) {
 | 
			
		||||
    _selectionMenuService = SelectionMenu(
 | 
			
		||||
      context: context,
 | 
			
		||||
      editorState: editorState,
 | 
			
		||||
      selectionMenuItems: items,
 | 
			
		||||
      deleteSlashByDefault: shouldInsertSlash,
 | 
			
		||||
      deleteKeywordsByDefault: deleteKeywordsByDefault,
 | 
			
		||||
      singleColumn: singleColumn,
 | 
			
		||||
      style: style,
 | 
			
		||||
    );
 | 
			
		||||
    _selectionMenuService?.dismiss();
 | 
			
		||||
    _selectionMenuService = UniversalPlatform.isMobile
 | 
			
		||||
        ? MobileSelectionMenu(
 | 
			
		||||
            context: context,
 | 
			
		||||
            editorState: editorState,
 | 
			
		||||
            selectionMenuItems: items,
 | 
			
		||||
            deleteSlashByDefault: shouldInsertSlash,
 | 
			
		||||
            deleteKeywordsByDefault: deleteKeywordsByDefault,
 | 
			
		||||
            singleColumn: singleColumn,
 | 
			
		||||
            style: style,
 | 
			
		||||
            startOffset: editorState.selection?.start.offset ?? 0,
 | 
			
		||||
          )
 | 
			
		||||
        : SelectionMenu(
 | 
			
		||||
            context: context,
 | 
			
		||||
            editorState: editorState,
 | 
			
		||||
            selectionMenuItems: items,
 | 
			
		||||
            deleteSlashByDefault: shouldInsertSlash,
 | 
			
		||||
            deleteKeywordsByDefault: deleteKeywordsByDefault,
 | 
			
		||||
            singleColumn: singleColumn,
 | 
			
		||||
            style: style,
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
    // disable the keyboard service
 | 
			
		||||
    editorState.service.keyboardService?.disable();
 | 
			
		||||
 | 
			
		||||
    if (!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST')) {
 | 
			
		||||
      await _selectionMenuService?.show();
 | 
			
		||||
    } else {
 | 
			
		||||
      await _selectionMenuService?.show();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await _selectionMenuService?.show();
 | 
			
		||||
    // enable the keyboard service
 | 
			
		||||
    editorState.service.keyboardService?.enable();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,156 @@
 | 
			
		||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
 | 
			
		||||
import 'package:appflowy/generated/locale_keys.g.dart';
 | 
			
		||||
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';
 | 
			
		||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
 | 
			
		||||
import 'slash_menu_items.dart';
 | 
			
		||||
 | 
			
		||||
final List<MobileSelectionMenuItem> mobileItems = [
 | 
			
		||||
  textStyleMobileSlashMenuItem,
 | 
			
		||||
  listMobileSlashMenuItem,
 | 
			
		||||
  toggleListMobileSlashMenuItem,
 | 
			
		||||
  fileOrMediaMobileSlashMenuItem,
 | 
			
		||||
  decorationsMobileSlashMenuItem,
 | 
			
		||||
  tableMobileSlashMenuItem,
 | 
			
		||||
  dateOrReminderMobileSlashMenuItem,
 | 
			
		||||
  advancedMobileSlashMenuItem,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
final List<MobileSelectionMenuItem> mobileItemsInTale = [
 | 
			
		||||
  textStyleMobileSlashMenuItem,
 | 
			
		||||
  listMobileSlashMenuItem,
 | 
			
		||||
  toggleListMobileSlashMenuItem,
 | 
			
		||||
  fileOrMediaMobileSlashMenuItem,
 | 
			
		||||
  decorationsMobileSlashMenuItem,
 | 
			
		||||
  dateOrReminderMobileSlashMenuItem,
 | 
			
		||||
  advancedMobileSlashMenuItem,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
SelectionMenuItemHandler _handler = (_, __, ___) {};
 | 
			
		||||
 | 
			
		||||
MobileSelectionMenuItem textStyleMobileSlashMenuItem = MobileSelectionMenuItem(
 | 
			
		||||
  getName: LocaleKeys.document_slashMenu_name_textStyle.tr,
 | 
			
		||||
  handler: _handler,
 | 
			
		||||
  icon: (_, isSelected, style) => SelectableSvgWidget(
 | 
			
		||||
    data: FlowySvgs.slash_menu_icon_text_s,
 | 
			
		||||
    isSelected: isSelected,
 | 
			
		||||
    style: style,
 | 
			
		||||
  ),
 | 
			
		||||
  nameBuilder: slashMenuItemNameBuilder,
 | 
			
		||||
  children: [
 | 
			
		||||
    paragraphSlashMenuItem,
 | 
			
		||||
    heading1SlashMenuItem,
 | 
			
		||||
    heading2SlashMenuItem,
 | 
			
		||||
    heading3SlashMenuItem,
 | 
			
		||||
  ],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
MobileSelectionMenuItem listMobileSlashMenuItem = MobileSelectionMenuItem(
 | 
			
		||||
  getName: LocaleKeys.document_slashMenu_name_list.tr,
 | 
			
		||||
  handler: _handler,
 | 
			
		||||
  icon: (_, isSelected, style) => SelectableSvgWidget(
 | 
			
		||||
    data: FlowySvgs.slash_menu_icon_bulleted_list_s,
 | 
			
		||||
    isSelected: isSelected,
 | 
			
		||||
    style: style,
 | 
			
		||||
  ),
 | 
			
		||||
  nameBuilder: slashMenuItemNameBuilder,
 | 
			
		||||
  children: [
 | 
			
		||||
    todoListSlashMenuItem,
 | 
			
		||||
    bulletedListSlashMenuItem,
 | 
			
		||||
    numberedListSlashMenuItem,
 | 
			
		||||
  ],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
MobileSelectionMenuItem toggleListMobileSlashMenuItem = MobileSelectionMenuItem(
 | 
			
		||||
  getName: LocaleKeys.document_slashMenu_name_toggleList.tr,
 | 
			
		||||
  handler: _handler,
 | 
			
		||||
  icon: (_, isSelected, style) => SelectableSvgWidget(
 | 
			
		||||
    data: FlowySvgs.slash_menu_icon_toggle_s,
 | 
			
		||||
    isSelected: isSelected,
 | 
			
		||||
    style: style,
 | 
			
		||||
  ),
 | 
			
		||||
  nameBuilder: slashMenuItemNameBuilder,
 | 
			
		||||
  children: [
 | 
			
		||||
    toggleListSlashMenuItem,
 | 
			
		||||
    toggleHeading1SlashMenuItem,
 | 
			
		||||
    toggleHeading2SlashMenuItem,
 | 
			
		||||
    toggleHeading3SlashMenuItem,
 | 
			
		||||
  ],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
MobileSelectionMenuItem fileOrMediaMobileSlashMenuItem =
 | 
			
		||||
    MobileSelectionMenuItem(
 | 
			
		||||
  getName: LocaleKeys.document_slashMenu_name_fileOrMedia.tr,
 | 
			
		||||
  handler: _handler,
 | 
			
		||||
  icon: (_, isSelected, style) => SelectableSvgWidget(
 | 
			
		||||
    data: FlowySvgs.slash_menu_icon_file_s,
 | 
			
		||||
    isSelected: isSelected,
 | 
			
		||||
    style: style,
 | 
			
		||||
  ),
 | 
			
		||||
  nameBuilder: slashMenuItemNameBuilder,
 | 
			
		||||
  children: [
 | 
			
		||||
    imageSlashMenuItem,
 | 
			
		||||
    photoGallerySlashMenuItem,
 | 
			
		||||
    fileSlashMenuItem,
 | 
			
		||||
  ],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
MobileSelectionMenuItem decorationsMobileSlashMenuItem =
 | 
			
		||||
    MobileSelectionMenuItem(
 | 
			
		||||
  getName: LocaleKeys.document_slashMenu_name_decorations.tr,
 | 
			
		||||
  handler: _handler,
 | 
			
		||||
  icon: (_, isSelected, style) => SelectableSvgWidget(
 | 
			
		||||
    data: FlowySvgs.slash_menu_icon_simple_table_s,
 | 
			
		||||
    isSelected: isSelected,
 | 
			
		||||
    style: style,
 | 
			
		||||
  ),
 | 
			
		||||
  nameBuilder: slashMenuItemNameBuilder,
 | 
			
		||||
  children: [
 | 
			
		||||
    quoteSlashMenuItem,
 | 
			
		||||
    dividerSlashMenuItem,
 | 
			
		||||
  ],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
MobileSelectionMenuItem tableMobileSlashMenuItem = MobileSelectionMenuItem(
 | 
			
		||||
  getName: LocaleKeys.document_slashMenu_name_table.tr,
 | 
			
		||||
  handler: _handler,
 | 
			
		||||
  icon: (_, isSelected, style) => SelectableSvgWidget(
 | 
			
		||||
    data: FlowySvgs.slash_menu_icon_simple_table_s,
 | 
			
		||||
    isSelected: isSelected,
 | 
			
		||||
    style: style,
 | 
			
		||||
  ),
 | 
			
		||||
  nameBuilder: slashMenuItemNameBuilder,
 | 
			
		||||
  children: [tableSlashMenuItem],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
MobileSelectionMenuItem dateOrReminderMobileSlashMenuItem =
 | 
			
		||||
    MobileSelectionMenuItem(
 | 
			
		||||
  getName: LocaleKeys.document_slashMenu_name_dateOrReminder.tr,
 | 
			
		||||
  handler: _handler,
 | 
			
		||||
  icon: (_, isSelected, style) => SelectableSvgWidget(
 | 
			
		||||
    data: FlowySvgs.slash_menu_icon_date_or_reminder_s,
 | 
			
		||||
    isSelected: isSelected,
 | 
			
		||||
    style: style,
 | 
			
		||||
  ),
 | 
			
		||||
  nameBuilder: slashMenuItemNameBuilder,
 | 
			
		||||
  children: [dateOrReminderSlashMenuItem],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
MobileSelectionMenuItem advancedMobileSlashMenuItem = MobileSelectionMenuItem(
 | 
			
		||||
  getName: LocaleKeys.document_slashMenu_name_advanced.tr,
 | 
			
		||||
  handler: _handler,
 | 
			
		||||
  icon: (_, isSelected, style) => SelectableSvgWidget(
 | 
			
		||||
    data: FlowySvgs.m_aa_font_color_m,
 | 
			
		||||
    isSelected: isSelected,
 | 
			
		||||
    style: style,
 | 
			
		||||
  ),
 | 
			
		||||
  nameBuilder: slashMenuItemNameBuilder,
 | 
			
		||||
  children: [
 | 
			
		||||
    subPageSlashMenuItem,
 | 
			
		||||
    calloutSlashMenuItem,
 | 
			
		||||
    codeBlockSlashMenuItem,
 | 
			
		||||
    mathEquationSlashMenuItem,
 | 
			
		||||
  ],
 | 
			
		||||
);
 | 
			
		||||
@ -3,6 +3,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selec
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:flowy_infra_ui/style_widget/text.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:universal_platform/universal_platform.dart';
 | 
			
		||||
 | 
			
		||||
/// Builder function for the slash menu item.
 | 
			
		||||
Widget slashMenuItemNameBuilder(
 | 
			
		||||
@ -49,9 +50,10 @@ class SlashMenuItemNameBuilder extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final isMobile = UniversalPlatform.isMobile;
 | 
			
		||||
    return FlowyText.regular(
 | 
			
		||||
      name,
 | 
			
		||||
      fontSize: 12.0,
 | 
			
		||||
      fontSize: isMobile ? 16.0 : 12.0,
 | 
			
		||||
      figmaLineHeight: 15.0,
 | 
			
		||||
      color: isSelected
 | 
			
		||||
          ? style.selectionMenuItemSelectedTextColor
 | 
			
		||||
@ -80,9 +82,11 @@ class SlashMenuIconBuilder extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final isMobile = UniversalPlatform.isMobile;
 | 
			
		||||
    return SelectableSvgWidget(
 | 
			
		||||
      data: data,
 | 
			
		||||
      isSelected: isSelected,
 | 
			
		||||
      size: isMobile ? Size.square(20) : null,
 | 
			
		||||
      style: style,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,9 @@
 | 
			
		||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
 | 
			
		||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
 | 
			
		||||
import 'package:appflowy_editor/appflowy_editor.dart';
 | 
			
		||||
import 'package:universal_platform/universal_platform.dart';
 | 
			
		||||
 | 
			
		||||
import 'slash_menu_items/mobile_items.dart';
 | 
			
		||||
import 'slash_menu_items/slash_menu_items.dart';
 | 
			
		||||
 | 
			
		||||
/// Build slash menu items
 | 
			
		||||
@ -13,14 +15,22 @@ List<SelectionMenuItem> slashMenuItemsBuilder({
 | 
			
		||||
  Node? node,
 | 
			
		||||
}) {
 | 
			
		||||
  final isInTable = node != null && node.parentTableCellNode != null;
 | 
			
		||||
 | 
			
		||||
  if (isInTable) {
 | 
			
		||||
    return _simpleTableSlashMenuItems();
 | 
			
		||||
  final isMobile = UniversalPlatform.isMobile;
 | 
			
		||||
  if (isMobile) {
 | 
			
		||||
    if (isInTable) {
 | 
			
		||||
      return mobileItemsInTale;
 | 
			
		||||
    } else {
 | 
			
		||||
      return mobileItems;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    return _defaultSlashMenuItems(
 | 
			
		||||
      isLocalMode: isLocalMode,
 | 
			
		||||
      documentBloc: documentBloc,
 | 
			
		||||
    );
 | 
			
		||||
    if (isInTable) {
 | 
			
		||||
      return _simpleTableSlashMenuItems();
 | 
			
		||||
    } else {
 | 
			
		||||
      return _defaultSlashMenuItems(
 | 
			
		||||
        isLocalMode: isLocalMode,
 | 
			
		||||
        documentBloc: documentBloc,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
 | 
			
		||||
@ -21,13 +22,14 @@ CharacterShortcutEvent inlineActionsCommand(
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
InlineActionsMenuService? selectionMenuService;
 | 
			
		||||
 | 
			
		||||
Future<bool> inlineActionsCommandHandler(
 | 
			
		||||
  EditorState editorState,
 | 
			
		||||
  InlineActionsService service,
 | 
			
		||||
  InlineActionsMenuStyle style,
 | 
			
		||||
) async {
 | 
			
		||||
  final selection = editorState.selection;
 | 
			
		||||
  if (UniversalPlatform.isMobile || selection == null) {
 | 
			
		||||
  if (selection == null) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -50,15 +52,31 @@ Future<bool> inlineActionsCommandHandler(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (service.context != null) {
 | 
			
		||||
    selectionMenuService = InlineActionsMenu(
 | 
			
		||||
      context: service.context!,
 | 
			
		||||
      editorState: editorState,
 | 
			
		||||
      service: service,
 | 
			
		||||
      initialResults: initialResults,
 | 
			
		||||
      style: style,
 | 
			
		||||
    );
 | 
			
		||||
    keepEditorFocusNotifier.increase();
 | 
			
		||||
    selectionMenuService?.dismiss();
 | 
			
		||||
    selectionMenuService = UniversalPlatform.isMobile
 | 
			
		||||
        ? MobileInlineActionsMenu(
 | 
			
		||||
            context: service.context!,
 | 
			
		||||
            editorState: editorState,
 | 
			
		||||
            service: service,
 | 
			
		||||
            initialResults: initialResults,
 | 
			
		||||
            style: style,
 | 
			
		||||
          )
 | 
			
		||||
        : InlineActionsMenu(
 | 
			
		||||
            context: service.context!,
 | 
			
		||||
            editorState: editorState,
 | 
			
		||||
            service: service,
 | 
			
		||||
            initialResults: initialResults,
 | 
			
		||||
            style: style,
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
    selectionMenuService?.show();
 | 
			
		||||
    // disable the keyboard service
 | 
			
		||||
    editorState.service.keyboardService?.disable();
 | 
			
		||||
 | 
			
		||||
    await selectionMenuService?.show();
 | 
			
		||||
 | 
			
		||||
    // enable the keyboard service
 | 
			
		||||
    editorState.service.keyboardService?.enable();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
 | 
			
		||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
 | 
			
		||||
@ -7,7 +9,8 @@ import 'package:flutter/material.dart';
 | 
			
		||||
abstract class InlineActionsMenuService {
 | 
			
		||||
  InlineActionsMenuStyle get style;
 | 
			
		||||
 | 
			
		||||
  void show();
 | 
			
		||||
  Future<void> show();
 | 
			
		||||
 | 
			
		||||
  void dismiss();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -59,8 +62,13 @@ class InlineActionsMenu extends InlineActionsMenuService {
 | 
			
		||||
  void _onSelectionUpdate() => selectionChangedByMenu = true;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void show() {
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((_) => _show());
 | 
			
		||||
  Future<void> show() {
 | 
			
		||||
    final completer = Completer<void>();
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
 | 
			
		||||
      _show();
 | 
			
		||||
      completer.complete();
 | 
			
		||||
    });
 | 
			
		||||
    return completer.future;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _show() {
 | 
			
		||||
 | 
			
		||||
@ -1684,6 +1684,11 @@
 | 
			
		||||
        "selectADocumentToLinkTo": "Select a Document to link to"
 | 
			
		||||
      },
 | 
			
		||||
      "name": {
 | 
			
		||||
        "textStyle": "Text Style",
 | 
			
		||||
        "list": "List",
 | 
			
		||||
        "fileOrMedia": "File or Media",
 | 
			
		||||
        "decorations": "Decorations",
 | 
			
		||||
        "advanced": "Advanced",
 | 
			
		||||
        "text": "Text",
 | 
			
		||||
        "heading1": "Heading 1",
 | 
			
		||||
        "heading2": "Heading 2",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user