feat: allow passing in a context menu builder (#1152)

* feat: allow passing in a context menu builder

* test: fix test

* chore: dart analyzer
This commit is contained in:
Richard Shiue 2025-11-06 11:48:13 +08:00 • committed by GitHub
commit 387a90ce39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 39 additions and 24 deletions

View file

@ -26,7 +26,7 @@ class AppFlowyEditor extends StatefulWidget {
Map<String, BlockComponentBuilder>? blockComponentBuilders, Map<String, BlockComponentBuilder>? blockComponentBuilders,
List<CharacterShortcutEvent>? characterShortcutEvents, List<CharacterShortcutEvent>? characterShortcutEvents,
List<CommandShortcutEvent>? commandShortcutEvents, List<CommandShortcutEvent>? commandShortcutEvents,
List<List<ContextMenuItem>>? contextMenuItems, this.contextMenuBuilder,
this.contentInsertionConfiguration, this.contentInsertionConfiguration,
this.editable = true, this.editable = true,
this.autoFocus = false, this.autoFocus = false,
@ -53,8 +53,7 @@ class AppFlowyEditor extends StatefulWidget {
characterShortcutEvents = characterShortcutEvents =
characterShortcutEvents ?? standardCharacterShortcutEvents, characterShortcutEvents ?? standardCharacterShortcutEvents,
commandShortcutEvents = commandShortcutEvents =
commandShortcutEvents ?? standardCommandShortcutEvents, commandShortcutEvents ?? standardCommandShortcutEvents;
contextMenuItems = contextMenuItems ?? standardContextMenuItems;
final EditorState editorState; final EditorState editorState;
@ -122,16 +121,14 @@ class AppFlowyEditor extends StatefulWidget {
/// ``` /// ```
final List<CommandShortcutEvent> commandShortcutEvents; final List<CommandShortcutEvent> commandShortcutEvents;
/// The context menu items. /// The context menu builder.
/// ///
/// They will be shown when the user right click on the editor. /// It will be shown when the user right click on the editor.
/// Each item will be separated by a divider.
/// ///
/// Defaults to [standardContextMenuItems]. /// Defaults to a standard implementation containing
/// [standardContextMenuItems].
/// ///
/// If empty the context menu won't appear. final ContextMenuWidgetBuilder? contextMenuBuilder;
///
final List<List<ContextMenuItem>>? contextMenuItems;
/// Provide a editorScrollController to control the scroll behavior /// Provide a editorScrollController to control the scroll behavior
/// ///
@ -345,7 +342,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
cursorColor: widget.editorStyle.cursorColor, cursorColor: widget.editorStyle.cursorColor,
selectionColor: widget.editorStyle.selectionColor, selectionColor: widget.editorStyle.selectionColor,
showMagnifier: widget.showMagnifier, showMagnifier: widget.showMagnifier,
contextMenuItems: widget.contextMenuItems, contextMenuBuilder: widget.contextMenuBuilder,
dropTargetStyle: widget.dropTargetStyle, dropTargetStyle: widget.dropTargetStyle,
child: child, child: child,
); );

View file

@ -11,7 +11,7 @@ class DesktopSelectionServiceWidget extends StatefulWidget {
super.key, super.key,
this.cursorColor = const Color(0xFF00BCF0), this.cursorColor = const Color(0xFF00BCF0),
this.selectionColor = const Color(0xFF00BCF0), this.selectionColor = const Color(0xFF00BCF0),
this.contextMenuItems, this.contextMenuBuilder,
required this.child, required this.child,
this.dropTargetStyle = const AppFlowyDropTargetStyle(), this.dropTargetStyle = const AppFlowyDropTargetStyle(),
}); });
@ -19,7 +19,7 @@ class DesktopSelectionServiceWidget extends StatefulWidget {
final Widget child; final Widget child;
final Color cursorColor; final Color cursorColor;
final Color selectionColor; final Color selectionColor;
final List<List<ContextMenuItem>>? contextMenuItems; final ContextMenuWidgetBuilder? contextMenuBuilder;
final AppFlowyDropTargetStyle dropTargetStyle; final AppFlowyDropTargetStyle dropTargetStyle;
@override @override
@ -425,8 +425,8 @@ class _DesktopSelectionServiceWidgetState
void _showContextMenu(TapDownDetails details) { void _showContextMenu(TapDownDetails details) {
_clearContextMenu(); _clearContextMenu();
// Don't trigger the context menu if there are no items // Don't trigger the context menu if the builder is null
if (widget.contextMenuItems == null || widget.contextMenuItems!.isEmpty) { if (widget.contextMenuBuilder == null) {
return; return;
} }
@ -458,12 +458,14 @@ class _DesktopSelectionServiceWidgetState
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final offset = details.localPosition + const Offset(10, 10) + baseOffset; final offset = details.localPosition + const Offset(10, 10) + baseOffset;
final contextMenu = OverlayEntry( final contextMenu = OverlayEntry(
builder: (context) => ContextMenu( builder: (_) =>
position: offset, widget.contextMenuBuilder?.call(
editorState: editorState, context,
items: widget.contextMenuItems!, offset,
onPressed: () => _clearContextMenu(), editorState,
), () => _clearContextMenu(),
) ??
SizedBox.shrink(),
); );
_contextMenuAreas.add(contextMenu); _contextMenuAreas.add(contextMenu);

View file

@ -11,7 +11,7 @@ class SelectionServiceWidget extends StatefulWidget {
this.cursorColor = const Color(0xFF00BCF0), this.cursorColor = const Color(0xFF00BCF0),
this.selectionColor = const Color.fromARGB(53, 111, 201, 231), this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
this.showMagnifier = true, this.showMagnifier = true,
this.contextMenuItems, this.contextMenuBuilder,
this.dropTargetStyle, this.dropTargetStyle,
required this.child, required this.child,
}); });
@ -19,7 +19,7 @@ class SelectionServiceWidget extends StatefulWidget {
final Widget child; final Widget child;
final Color cursorColor; final Color cursorColor;
final Color selectionColor; final Color selectionColor;
final List<List<ContextMenuItem>>? contextMenuItems; final ContextMenuWidgetBuilder? contextMenuBuilder;
final AppFlowyDropTargetStyle? dropTargetStyle; final AppFlowyDropTargetStyle? dropTargetStyle;
/// Show the magnifier or not. /// Show the magnifier or not.
@ -47,7 +47,7 @@ class _SelectionServiceWidgetState extends State<SelectionServiceWidget>
key: forwardKey, key: forwardKey,
cursorColor: widget.cursorColor, cursorColor: widget.cursorColor,
selectionColor: widget.selectionColor, selectionColor: widget.selectionColor,
contextMenuItems: widget.contextMenuItems, contextMenuBuilder: widget.contextMenuBuilder,
dropTargetStyle: dropTargetStyle:
widget.dropTargetStyle ?? const AppFlowyDropTargetStyle(), widget.dropTargetStyle ?? const AppFlowyDropTargetStyle(),
child: widget.child, child: widget.child,

View file

@ -1,6 +1,13 @@
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
typedef ContextMenuWidgetBuilder = Widget Function(
BuildContext context,
Offset position,
EditorState editorState,
VoidCallback onPressed,
);
class ContextMenuItem { class ContextMenuItem {
ContextMenuItem({ ContextMenuItem({
required String Function() getName, required String Function() getName,

View file

@ -1,5 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/ime/text_input_service.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/ime/text_input_service.dart';
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -85,6 +86,14 @@ class TestableEditor {
...standardCharacterShortcutEvents, ...standardCharacterShortcutEvents,
formatGreaterHyphen, formatGreaterHyphen,
], ],
contextMenuBuilder: (context, position, editorState, onPressed) {
return ContextMenu(
position: position,
editorState: editorState,
items: standardContextMenuItems,
onPressed: onPressed,
);
},
editorStyle: inMobile editorStyle: inMobile
? EditorStyle.mobile( ? EditorStyle.mobile(
defaultTextDirection: defaultTextDirection, defaultTextDirection: defaultTextDirection,