chore: support single column slash menu

This commit is contained in:
nathan 2024-07-27 13:17:58 +08:00
commit 218c7c82ba
4 changed files with 90 additions and 31 deletions

View file

@ -20,6 +20,7 @@ final CharacterShortcutEvent slashCommand = CharacterShortcutEvent(
CharacterShortcutEvent customSlashCommand( CharacterShortcutEvent customSlashCommand(
List<SelectionMenuItem> items, { List<SelectionMenuItem> items, {
bool shouldInsertSlash = true, bool shouldInsertSlash = true,
bool singleColumn = true,
SelectionMenuStyle style = SelectionMenuStyle.light, SelectionMenuStyle style = SelectionMenuStyle.light,
}) { }) {
return CharacterShortcutEvent( return CharacterShortcutEvent(
@ -29,6 +30,7 @@ CharacterShortcutEvent customSlashCommand(
editorState, editorState,
items, items,
shouldInsertSlash: shouldInsertSlash, shouldInsertSlash: shouldInsertSlash,
singleColumn: singleColumn,
style: style, style: style,
), ),
); );
@ -48,6 +50,7 @@ Future<bool> _showSlashMenu(
EditorState editorState, EditorState editorState,
List<SelectionMenuItem> items, { List<SelectionMenuItem> items, {
bool shouldInsertSlash = true, bool shouldInsertSlash = true,
bool singleColumn = true,
SelectionMenuStyle style = SelectionMenuStyle.light, SelectionMenuStyle style = SelectionMenuStyle.light,
}) async { }) async {
if (PlatformExtension.isMobile) { if (PlatformExtension.isMobile) {
@ -101,6 +104,7 @@ Future<bool> _showSlashMenu(
editorState: editorState, editorState: editorState,
selectionMenuItems: items, selectionMenuItems: items,
deleteSlashByDefault: shouldInsertSlash, deleteSlashByDefault: shouldInsertSlash,
singleColumn: singleColumn,
style: style, style: style,
); );
_selectionMenuService?.show(); _selectionMenuService?.show();

View file

@ -21,12 +21,14 @@ class SelectionMenu extends SelectionMenuService {
this.deleteSlashByDefault = true, this.deleteSlashByDefault = true,
this.style = SelectionMenuStyle.light, this.style = SelectionMenuStyle.light,
this.itemCountFilter = 0, this.itemCountFilter = 0,
this.singleColumn = false,
}); });
final BuildContext context; final BuildContext context;
final EditorState editorState; final EditorState editorState;
final List<SelectionMenuItem> selectionMenuItems; final List<SelectionMenuItem> selectionMenuItems;
final bool deleteSlashByDefault; final bool deleteSlashByDefault;
final bool singleColumn;
@override @override
final SelectionMenuStyle style; final SelectionMenuStyle style;
@ -100,6 +102,7 @@ class SelectionMenu extends SelectionMenuService {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: SelectionMenuWidget( child: SelectionMenuWidget(
selectionMenuStyle: style, selectionMenuStyle: style,
singleColumn: singleColumn,
items: selectionMenuItems items: selectionMenuItems
..forEach((element) { ..forEach((element) {
element.deleteSlash = deleteSlashByDefault; element.deleteSlash = deleteSlashByDefault;

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
typedef SelectionMenuItemHandler = void Function( typedef SelectionMenuItemHandler = void Function(
EditorState editorState, EditorState editorState,
@ -199,6 +200,7 @@ class SelectionMenuWidget extends StatefulWidget {
required this.selectionMenuStyle, required this.selectionMenuStyle,
required this.itemCountFilter, required this.itemCountFilter,
required this.deleteSlashByDefault, required this.deleteSlashByDefault,
this.singleColumn = false,
}); });
final List<SelectionMenuItem> items; final List<SelectionMenuItem> items;
@ -214,6 +216,7 @@ class SelectionMenuWidget extends StatefulWidget {
final SelectionMenuStyle selectionMenuStyle; final SelectionMenuStyle selectionMenuStyle;
final bool deleteSlashByDefault; final bool deleteSlashByDefault;
final bool singleColumn;
@override @override
State<SelectionMenuWidget> createState() => _SelectionMenuWidgetState(); State<SelectionMenuWidget> createState() => _SelectionMenuWidgetState();
@ -224,6 +227,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
int _selectedIndex = 0; int _selectedIndex = 0;
List<SelectionMenuItem> _showingItems = []; List<SelectionMenuItem> _showingItems = [];
AutoScrollController? _scrollController;
int _searchCounter = 0; int _searchCounter = 0;
@ -269,6 +273,9 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
super.initState(); super.initState();
_showingItems = widget.items; _showingItems = widget.items;
if (widget.singleColumn) {
_scrollController = AutoScrollController();
}
keepEditorFocusNotifier.increase(); keepEditorFocusNotifier.increase();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -280,6 +287,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
void dispose() { void dispose() {
_focusNode.dispose(); _focusNode.dispose();
keepEditorFocusNotifier.decrease(); keepEditorFocusNotifier.decrease();
_scrollController?.dispose();
super.dispose(); super.dispose();
} }
@ -313,23 +321,83 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
); );
} }
void _scrollToSelectedIndex() {
if (_scrollController == null) {
return;
}
_scrollController?.scrollToIndex(
_selectedIndex,
duration: const Duration(milliseconds: 200),
preferPosition: AutoScrollPosition.begin,
);
}
Widget _buildResultsWidget( Widget _buildResultsWidget(
BuildContext buildContext, BuildContext buildContext,
List<SelectionMenuItem> items, List<SelectionMenuItem> items,
int itemCountFilter, int itemCountFilter,
int selectedIndex, int selectedIndex,
) { ) {
List<Widget> columns = []; if (widget.singleColumn) {
List<Widget> itemWidgets = []; List<Widget> itemWidgets = [];
for (var i = 0; i < items.length; i++) {
itemWidgets.add(
AutoScrollTag(
key: ValueKey(i),
index: i,
controller: _scrollController!,
child: SelectionMenuItemWidget(
item: items[i],
isSelected: selectedIndex == i,
editorState: widget.editorState,
menuService: widget.menuService,
selectionMenuStyle: widget.selectionMenuStyle,
),
),
);
}
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 300,
minWidth: 300,
maxWidth: 300,
),
child: ListView(
shrinkWrap: true,
controller: _scrollController,
children: itemWidgets,
),
);
} else {
List<Widget> columns = [];
List<Widget> itemWidgets = [];
// apply item count filter
if (itemCountFilter > 0) {
items = items.take(itemCountFilter).toList();
}
// apply item count filter for (var i = 0; i < items.length; i++) {
if (i != 0 && i % (widget.maxItemInRow) == 0) {
if (itemCountFilter > 0) { columns.add(
items = items.take(itemCountFilter).toList(); Column(
} crossAxisAlignment: CrossAxisAlignment.start,
children: itemWidgets,
for (var i = 0; i < items.length; i++) { ),
if (i != 0 && i % (widget.maxItemInRow) == 0) { );
itemWidgets = [];
}
itemWidgets.add(
SelectionMenuItemWidget(
item: items[i],
isSelected: selectedIndex == i,
editorState: widget.editorState,
menuService: widget.menuService,
selectionMenuStyle: widget.selectionMenuStyle,
),
);
}
if (itemWidgets.isNotEmpty) {
columns.add( columns.add(
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -338,29 +406,11 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
); );
itemWidgets = []; itemWidgets = [];
} }
itemWidgets.add( return Row(
SelectionMenuItemWidget( crossAxisAlignment: CrossAxisAlignment.start,
item: items[i], children: columns,
isSelected: selectedIndex == i,
editorState: widget.editorState,
menuService: widget.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) { Widget _buildNoResultsWidget(BuildContext context) {
@ -444,6 +494,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
if (newSelectedIndex != _selectedIndex) { if (newSelectedIndex != _selectedIndex) {
setState(() { setState(() {
_selectedIndex = newSelectedIndex.clamp(0, _showingItems.length - 1); _selectedIndex = newSelectedIndex.clamp(0, _showingItems.length - 1);
_scrollToSelectedIndex();
}); });
return KeyEventResult.handled; return KeyEventResult.handled;
} }

View file

@ -51,6 +51,7 @@ dependencies:
printing: ^5.11.1 printing: ^5.11.1
url_launcher: ^6.1.11 url_launcher: ^6.1.11
visibility_detector: ^0.4.0+2 visibility_detector: ^0.4.0+2
scroll_to_index: ^3.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: