mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: improve reference menus (#4301)
* feat: improve reference menus * fix: limit page results in reference menus * fix: custom title for specific type refs * fix: insert pages * fix: enable scrolling on item focus change * fix: enable shift+tab to navigate * fix: properly offset menu * fix: review comments * fix: remove bottom padding on last group
This commit is contained in:
@ -1,9 +1,10 @@
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||||
|
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ void main() {
|
|||||||
|
|
||||||
// Select result
|
// Select result
|
||||||
final optionFinder = find.descendant(
|
final optionFinder = find.descendant(
|
||||||
of: find.byType(LinkToPageMenu),
|
of: find.byType(InlineActionsHandler),
|
||||||
matching: find.text(name),
|
matching: find.text(name),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||||
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
||||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
|
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell.dart';
|
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
@ -155,7 +155,7 @@ Future<void> insertReferenceDatabase(
|
|||||||
layout.referencedMenuName,
|
layout.referencedMenuName,
|
||||||
);
|
);
|
||||||
|
|
||||||
final linkToPageMenu = find.byType(LinkToPageMenu);
|
final linkToPageMenu = find.byType(InlineActionsHandler);
|
||||||
expect(linkToPageMenu, findsOneWidget);
|
expect(linkToPageMenu, findsOneWidget);
|
||||||
final referencedDatabase = find.descendant(
|
final referencedDatabase = find.descendant(
|
||||||
of: linkToPageMenu,
|
of: linkToPageMenu,
|
||||||
|
@ -73,6 +73,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
handlers: [
|
handlers: [
|
||||||
InlinePageReferenceService(
|
InlinePageReferenceService(
|
||||||
currentViewId: documentBloc.view.id,
|
currentViewId: documentBloc.view.id,
|
||||||
|
limitResults: 5,
|
||||||
).inlinePageReferenceDelegate,
|
).inlinePageReferenceDelegate,
|
||||||
DateReferenceService(context).dateReferenceDelegate,
|
DateReferenceService(context).dateReferenceDelegate,
|
||||||
ReminderReferenceService(context).reminderReferenceDelegate,
|
ReminderReferenceService(context).reminderReferenceDelegate,
|
||||||
|
@ -14,6 +14,7 @@ extension InsertDatabase on EditorState {
|
|||||||
if (selection == null || !selection.isCollapsed) {
|
if (selection == null || !selection.isCollapsed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final node = getNodeAtPath(selection.end.path);
|
final node = getNodeAtPath(selection.end.path);
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
return;
|
return;
|
||||||
@ -52,19 +53,9 @@ extension InsertDatabase on EditorState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late Transaction transaction;
|
final Transaction transaction = viewType == ViewLayoutPB.Document
|
||||||
if (viewType == ViewLayoutPB.Document) {
|
? await _insertDocumentReference(childView, selection, node)
|
||||||
transaction = await _insertDocumentReference(
|
: await _insertDatabaseReference(childView, selection.end.path);
|
||||||
childView,
|
|
||||||
selection,
|
|
||||||
node,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
transaction = await _insertDatabaseReference(
|
|
||||||
childView,
|
|
||||||
selection.end.path,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await apply(transaction);
|
await apply(transaction);
|
||||||
}
|
}
|
||||||
|
@ -1,252 +1,71 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
void showLinkToPageMenu(
|
InlineActionsMenuService? _actionsMenuService;
|
||||||
OverlayState container,
|
Future<void> showLinkToPageMenu(
|
||||||
EditorState editorState,
|
EditorState editorState,
|
||||||
SelectionMenuService menuService,
|
SelectionMenuService menuService,
|
||||||
ViewLayoutPB pageType,
|
ViewLayoutPB pageType,
|
||||||
) {
|
) async {
|
||||||
menuService.dismiss();
|
menuService.dismiss();
|
||||||
|
_actionsMenuService?.dismiss();
|
||||||
|
|
||||||
final alignment = menuService.alignment;
|
final rootContext = editorState.document.root.context;
|
||||||
final offset = menuService.offset;
|
if (rootContext == null) {
|
||||||
final top = alignment == Alignment.topLeft ? offset.dy : null;
|
return;
|
||||||
final bottom = alignment == Alignment.bottomLeft ? offset.dy : null;
|
|
||||||
|
|
||||||
keepEditorFocusNotifier.increase();
|
|
||||||
late OverlayEntry linkToPageMenuEntry;
|
|
||||||
linkToPageMenuEntry = FullScreenOverlayEntry(
|
|
||||||
top: top,
|
|
||||||
bottom: bottom,
|
|
||||||
left: offset.dx,
|
|
||||||
dismissCallback: () => keepEditorFocusNotifier.decrease(),
|
|
||||||
builder: (context) => Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: LinkToPageMenu(
|
|
||||||
editorState: editorState,
|
|
||||||
layoutType: pageType,
|
|
||||||
hintText: pageType.toHintText(),
|
|
||||||
onSelected: (appPB, viewPB) async {
|
|
||||||
try {
|
|
||||||
await editorState.insertReferencePage(viewPB, pageType);
|
|
||||||
linkToPageMenuEntry.remove();
|
|
||||||
} on FlowyError catch (e) {
|
|
||||||
if (context.mounted) {
|
|
||||||
Dialogs.show(
|
|
||||||
child: FlowyErrorPage.message(
|
|
||||||
e.msg,
|
|
||||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
|
||||||
),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).build();
|
|
||||||
container.insert(linkToPageMenuEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
class LinkToPageMenu extends StatefulWidget {
|
|
||||||
const LinkToPageMenu({
|
|
||||||
super.key,
|
|
||||||
required this.editorState,
|
|
||||||
required this.layoutType,
|
|
||||||
required this.hintText,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
final EditorState editorState;
|
|
||||||
final ViewLayoutPB layoutType;
|
|
||||||
final String hintText;
|
|
||||||
final void Function(ViewPB view, ViewPB childView) onSelected;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<LinkToPageMenu> createState() => _LinkToPageMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|
||||||
final _focusNode = FocusNode(debugLabel: 'reference_list_widget');
|
|
||||||
EditorStyle get style => widget.editorState.editorStyle;
|
|
||||||
int _selectedIndex = 0;
|
|
||||||
final int _totalItems = 0;
|
|
||||||
Future<List<ViewPB>>? _availableLayout;
|
|
||||||
final List<ViewPB> _items = [];
|
|
||||||
|
|
||||||
Future<List<ViewPB>> fetchItems() async {
|
|
||||||
final items =
|
|
||||||
await ViewBackendService().fetchViewsWithLayoutType(widget.layoutType);
|
|
||||||
_items
|
|
||||||
..clear()
|
|
||||||
..addAll(items);
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
final service = InlineActionsService(
|
||||||
void initState() {
|
context: rootContext,
|
||||||
_availableLayout = fetchItems();
|
handlers: [
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
InlinePageReferenceService(
|
||||||
_focusNode.requestFocus();
|
currentViewId: "",
|
||||||
});
|
viewLayout: pageType,
|
||||||
super.initState();
|
customTitle: titleFromPageType(pageType),
|
||||||
|
insertPage: pageType != ViewLayoutPB.Document,
|
||||||
|
limitResults: 15,
|
||||||
|
).inlinePageReferenceDelegate,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<InlineActionsResult> initialResults = [];
|
||||||
|
for (final handler in service.handlers) {
|
||||||
|
final group = await handler();
|
||||||
|
|
||||||
|
if (group.results.isNotEmpty) {
|
||||||
|
initialResults.add(group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
if (rootContext.mounted) {
|
||||||
void dispose() {
|
_actionsMenuService = InlineActionsMenu(
|
||||||
_focusNode.dispose();
|
context: rootContext,
|
||||||
super.dispose();
|
editorState: editorState,
|
||||||
}
|
service: service,
|
||||||
|
initialResults: initialResults,
|
||||||
@override
|
style: Theme.of(editorState.document.root.context!).brightness ==
|
||||||
Widget build(BuildContext context) {
|
Brightness.light
|
||||||
final theme = Theme.of(context);
|
? const InlineActionsMenuStyle.light()
|
||||||
return Focus(
|
: const InlineActionsMenuStyle.dark(),
|
||||||
focusNode: _focusNode,
|
startCharAmount: 0,
|
||||||
onKey: _onKey,
|
|
||||||
child: Container(
|
|
||||||
width: 300,
|
|
||||||
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.cardColor,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
blurRadius: 5,
|
|
||||||
spreadRadius: 1,
|
|
||||||
color: Colors.black.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
borderRadius: BorderRadius.circular(6.0),
|
|
||||||
),
|
|
||||||
child: _buildListWidget(
|
|
||||||
context,
|
|
||||||
_selectedIndex,
|
|
||||||
_availableLayout,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
_actionsMenuService?.show();
|
||||||
if (event is! RawKeyDownEvent ||
|
|
||||||
_availableLayout == null ||
|
|
||||||
_items.isEmpty) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
final acceptedKeys = [
|
|
||||||
LogicalKeyboardKey.arrowUp,
|
|
||||||
LogicalKeyboardKey.arrowDown,
|
|
||||||
LogicalKeyboardKey.tab,
|
|
||||||
LogicalKeyboardKey.enter,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!acceptedKeys.contains(event.logicalKey)) {
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newSelectedIndex = _selectedIndex;
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.arrowDown &&
|
|
||||||
newSelectedIndex != _totalItems - 1) {
|
|
||||||
newSelectedIndex += 1;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp &&
|
|
||||||
newSelectedIndex != 0) {
|
|
||||||
newSelectedIndex -= 1;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.tab) {
|
|
||||||
newSelectedIndex += 1;
|
|
||||||
newSelectedIndex %= _totalItems;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
|
||||||
widget.onSelected(
|
|
||||||
_items[_selectedIndex],
|
|
||||||
_items[_selectedIndex],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_selectedIndex = newSelectedIndex;
|
|
||||||
});
|
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildListWidget(
|
|
||||||
BuildContext context,
|
|
||||||
int selectedIndex,
|
|
||||||
Future<List<ViewPB>>? items,
|
|
||||||
) {
|
|
||||||
int index = 0;
|
|
||||||
return FutureBuilder<List<ViewPB>>(
|
|
||||||
future: items,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData &&
|
|
||||||
snapshot.connectionState == ConnectionState.done) {
|
|
||||||
final views = snapshot.data;
|
|
||||||
final List<Widget> children = [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
widget.hintText,
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (views != null && views.isNotEmpty) {
|
|
||||||
for (final view in views) {
|
|
||||||
children.add(
|
|
||||||
FlowyButton(
|
|
||||||
isSelected: index == _selectedIndex,
|
|
||||||
leftIcon: view.defaultIcon(),
|
|
||||||
text: FlowyText.regular(view.name),
|
|
||||||
onTap: () => widget.onSelected(view, view),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on ViewLayoutPB {
|
String titleFromPageType(ViewLayoutPB layout) => switch (layout) {
|
||||||
String toHintText() {
|
ViewLayoutPB.Grid => LocaleKeys.inlineActions_gridReference.tr(),
|
||||||
switch (this) {
|
ViewLayoutPB.Document => LocaleKeys.inlineActions_docReference.tr(),
|
||||||
case ViewLayoutPB.Grid:
|
ViewLayoutPB.Board => LocaleKeys.inlineActions_boardReference.tr(),
|
||||||
return LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
|
ViewLayoutPB.Calendar => LocaleKeys.inlineActions_calReference.tr(),
|
||||||
case ViewLayoutPB.Board:
|
_ => LocaleKeys.inlineActions_pageReference.tr(),
|
||||||
return LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
|
};
|
||||||
case ViewLayoutPB.Calendar:
|
|
||||||
return LocaleKeys.document_slashMenu_calendar_selectACalendarToLinkTo
|
|
||||||
.tr();
|
|
||||||
case ViewLayoutPB.Document:
|
|
||||||
return LocaleKeys.document_slashMenu_document_selectADocumentToLinkTo
|
|
||||||
.tr();
|
|
||||||
default:
|
|
||||||
throw Exception('Unknown layout type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -85,6 +85,7 @@ Future<bool> inlinePageReferenceCommandHandler(
|
|||||||
handlers: [
|
handlers: [
|
||||||
InlinePageReferenceService(
|
InlinePageReferenceService(
|
||||||
currentViewId: currentViewId,
|
currentViewId: currentViewId,
|
||||||
|
limitResults: 10,
|
||||||
).inlinePageReferenceDelegate,
|
).inlinePageReferenceDelegate,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -100,7 +101,7 @@ Future<bool> inlinePageReferenceCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (service.context != null) {
|
if (context.mounted) {
|
||||||
selectionMenuService = InlineActionsMenu(
|
selectionMenuService = InlineActionsMenu(
|
||||||
context: service.context!,
|
context: service.context!,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
@ -5,7 +5,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selec
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
// Document Reference
|
// Document Reference
|
||||||
|
|
||||||
@ -17,14 +16,8 @@ SelectionMenuItem referencedDocumentMenuItem = SelectionMenuItem(
|
|||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
keywords: ['page', 'notes', 'referenced page', 'referenced document'],
|
keywords: ['page', 'notes', 'referenced page', 'referenced document'],
|
||||||
handler: (editorState, menuService, context) {
|
handler: (editorState, menuService, context) =>
|
||||||
showLinkToPageMenu(
|
showLinkToPageMenu(editorState, menuService, ViewLayoutPB.Document),
|
||||||
Overlay.of(context),
|
|
||||||
editorState,
|
|
||||||
menuService,
|
|
||||||
ViewLayoutPB.Document,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Database References
|
// Database References
|
||||||
@ -37,15 +30,8 @@ SelectionMenuItem referencedGridMenuItem = SelectionMenuItem(
|
|||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
keywords: ['referenced', 'grid', 'database'],
|
keywords: ['referenced', 'grid', 'database'],
|
||||||
handler: (editorState, menuService, context) {
|
handler: (editorState, menuService, context) =>
|
||||||
final container = Overlay.of(context);
|
showLinkToPageMenu(editorState, menuService, ViewLayoutPB.Grid),
|
||||||
showLinkToPageMenu(
|
|
||||||
container,
|
|
||||||
editorState,
|
|
||||||
menuService,
|
|
||||||
ViewLayoutPB.Grid,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
SelectionMenuItem referencedBoardMenuItem = SelectionMenuItem(
|
SelectionMenuItem referencedBoardMenuItem = SelectionMenuItem(
|
||||||
@ -56,15 +42,8 @@ SelectionMenuItem referencedBoardMenuItem = SelectionMenuItem(
|
|||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
keywords: ['referenced', 'board', 'kanban'],
|
keywords: ['referenced', 'board', 'kanban'],
|
||||||
handler: (editorState, menuService, context) {
|
handler: (editorState, menuService, context) =>
|
||||||
final container = Overlay.of(context);
|
showLinkToPageMenu(editorState, menuService, ViewLayoutPB.Board),
|
||||||
showLinkToPageMenu(
|
|
||||||
container,
|
|
||||||
editorState,
|
|
||||||
menuService,
|
|
||||||
ViewLayoutPB.Board,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
SelectionMenuItem referencedCalendarMenuItem = SelectionMenuItem(
|
SelectionMenuItem referencedCalendarMenuItem = SelectionMenuItem(
|
||||||
@ -75,12 +54,6 @@ SelectionMenuItem referencedCalendarMenuItem = SelectionMenuItem(
|
|||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
keywords: ['referenced', 'calendar', 'database'],
|
keywords: ['referenced', 'calendar', 'database'],
|
||||||
handler: (editorState, menuService, context) {
|
handler: (editorState, menuService, context) =>
|
||||||
showLinkToPageMenu(
|
showLinkToPageMenu(editorState, menuService, ViewLayoutPB.Calendar),
|
||||||
Overlay.of(context),
|
|
||||||
editorState,
|
|
||||||
menuService,
|
|
||||||
ViewLayoutPB.Calendar,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
@ -1,19 +1,30 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.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_result.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
|
|
||||||
class InlinePageReferenceService {
|
class InlinePageReferenceService {
|
||||||
InlinePageReferenceService({
|
InlinePageReferenceService({
|
||||||
required this.currentViewId,
|
required this.currentViewId,
|
||||||
|
this.viewLayout,
|
||||||
|
this.customTitle,
|
||||||
|
this.insertPage = false,
|
||||||
|
this.limitResults = 0,
|
||||||
}) {
|
}) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
@ -21,38 +32,48 @@ class InlinePageReferenceService {
|
|||||||
final Completer _initCompleter = Completer<void>();
|
final Completer _initCompleter = Completer<void>();
|
||||||
|
|
||||||
final String currentViewId;
|
final String currentViewId;
|
||||||
|
final ViewLayoutPB? viewLayout;
|
||||||
|
final String? customTitle;
|
||||||
|
|
||||||
late final ViewBackendService service;
|
/// Defaults to false, if set to true the Page
|
||||||
|
/// will be inserted as a Reference
|
||||||
|
/// When false, a link to the view will be inserted
|
||||||
|
///
|
||||||
|
final bool insertPage;
|
||||||
|
|
||||||
|
/// Defaults to 0 where there are no limits
|
||||||
|
/// Anything above 0 will limit the page reference results
|
||||||
|
/// to [limitResults].
|
||||||
|
///
|
||||||
|
final int limitResults;
|
||||||
|
|
||||||
|
final ViewBackendService service = ViewBackendService();
|
||||||
List<InlineActionsMenuItem> _items = [];
|
List<InlineActionsMenuItem> _items = [];
|
||||||
List<InlineActionsMenuItem> _filtered = [];
|
List<InlineActionsMenuItem> _filtered = [];
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
service = ViewBackendService();
|
_items = await _generatePageItems(currentViewId, viewLayout);
|
||||||
|
_filtered = limitResults > 0 ? _items.take(limitResults).toList() : _items;
|
||||||
_generatePageItems(currentViewId).then((value) {
|
_initCompleter.complete();
|
||||||
_items = value;
|
|
||||||
_filtered = value;
|
|
||||||
_initCompleter.complete();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<InlineActionsMenuItem>> _filterItems(String? search) async {
|
Future<List<InlineActionsMenuItem>> _filterItems(String? search) async {
|
||||||
await _initCompleter.future;
|
await _initCompleter.future;
|
||||||
|
|
||||||
if (search == null || search.isEmpty) {
|
final items = (search == null || search.isEmpty)
|
||||||
return _items;
|
? _items
|
||||||
}
|
: _items.where(
|
||||||
|
(item) =>
|
||||||
|
item.keywords != null &&
|
||||||
|
item.keywords!.isNotEmpty &&
|
||||||
|
item.keywords!.any(
|
||||||
|
(keyword) => keyword.contains(search.toLowerCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return _items
|
return limitResults > 0
|
||||||
.where(
|
? items.take(limitResults).toList()
|
||||||
(item) =>
|
: items.toList();
|
||||||
item.keywords != null &&
|
|
||||||
item.keywords!.isNotEmpty &&
|
|
||||||
item.keywords!.any(
|
|
||||||
(keyword) => keyword.contains(search.toLowerCase()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<InlineActionsResult> inlinePageReferenceDelegate([
|
Future<InlineActionsResult> inlinePageReferenceDelegate([
|
||||||
@ -61,15 +82,24 @@ class InlinePageReferenceService {
|
|||||||
_filtered = await _filterItems(search);
|
_filtered = await _filterItems(search);
|
||||||
|
|
||||||
return InlineActionsResult(
|
return InlineActionsResult(
|
||||||
title: LocaleKeys.inlineActions_pageReference.tr(),
|
title: customTitle?.isNotEmpty == true
|
||||||
|
? customTitle!
|
||||||
|
: LocaleKeys.inlineActions_pageReference.tr(),
|
||||||
results: _filtered,
|
results: _filtered,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<InlineActionsMenuItem>> _generatePageItems(
|
Future<List<InlineActionsMenuItem>> _generatePageItems(
|
||||||
String currentViewId,
|
String currentViewId,
|
||||||
|
ViewLayoutPB? viewLayout,
|
||||||
) async {
|
) async {
|
||||||
final views = await service.fetchViews();
|
late List<ViewPB> views;
|
||||||
|
if (viewLayout != null) {
|
||||||
|
views = await service.fetchViewsWithLayoutType(viewLayout);
|
||||||
|
} else {
|
||||||
|
views = await service.fetchViews();
|
||||||
|
}
|
||||||
|
|
||||||
if (views.isEmpty) {
|
if (views.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -93,37 +123,15 @@ class InlinePageReferenceService {
|
|||||||
lineHeight: 1.3,
|
lineHeight: 1.3,
|
||||||
)
|
)
|
||||||
: view.defaultIcon(),
|
: view.defaultIcon(),
|
||||||
onSelected: (context, editorState, menuService, replace) async {
|
onSelected: (context, editorState, menuService, replace) => insertPage
|
||||||
final selection = editorState.selection;
|
? _onInsertPageRef(view, context, editorState, replace)
|
||||||
if (selection == null || !selection.isCollapsed) {
|
: _onInsertLinkRef(
|
||||||
return;
|
view,
|
||||||
}
|
context,
|
||||||
|
editorState,
|
||||||
final node = editorState.getNodeAtPath(selection.end.path);
|
menuService,
|
||||||
final delta = node?.delta;
|
replace,
|
||||||
if (node == null || delta == null) {
|
),
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @page name -> $
|
|
||||||
// preload the page infos
|
|
||||||
pageMemorizer[view.id] = view;
|
|
||||||
final transaction = editorState.transaction
|
|
||||||
..replaceText(
|
|
||||||
node,
|
|
||||||
replace.$1,
|
|
||||||
replace.$2,
|
|
||||||
'\$',
|
|
||||||
attributes: {
|
|
||||||
MentionBlockKeys.mention: {
|
|
||||||
MentionBlockKeys.type: MentionType.page.name,
|
|
||||||
MentionBlockKeys.pageId: view.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await editorState.apply(transaction);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
pages.add(pageSelectionMenuItem);
|
pages.add(pageSelectionMenuItem);
|
||||||
@ -131,4 +139,84 @@ class InlinePageReferenceService {
|
|||||||
|
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onInsertPageRef(
|
||||||
|
ViewPB view,
|
||||||
|
BuildContext context,
|
||||||
|
EditorState editorState,
|
||||||
|
(int, int) replace,
|
||||||
|
) async {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null || !selection.isCollapsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path);
|
||||||
|
|
||||||
|
if (node != null) {
|
||||||
|
// Delete search term
|
||||||
|
if (replace.$2 > 0) {
|
||||||
|
final transaction = editorState.transaction
|
||||||
|
..deleteText(node, replace.$1, replace.$2);
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert newline before inserting referenced database
|
||||||
|
if (node.delta?.toPlainText().isNotEmpty == true) {
|
||||||
|
await editorState.insertNewLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await editorState.insertReferencePage(view, view.layout);
|
||||||
|
} on FlowyError catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
Dialogs.show(
|
||||||
|
context,
|
||||||
|
child: FlowyErrorPage.message(
|
||||||
|
e.msg,
|
||||||
|
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onInsertLinkRef(
|
||||||
|
ViewPB view,
|
||||||
|
BuildContext context,
|
||||||
|
EditorState editorState,
|
||||||
|
InlineActionsMenuService menuService,
|
||||||
|
(int, int) replace,
|
||||||
|
) async {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null || !selection.isCollapsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
if (node == null || delta == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @page name -> $
|
||||||
|
// preload the page infos
|
||||||
|
pageMemorizer[view.id] = view;
|
||||||
|
final transaction = editorState.transaction
|
||||||
|
..replaceText(
|
||||||
|
node,
|
||||||
|
replace.$1,
|
||||||
|
replace.$2,
|
||||||
|
'\$',
|
||||||
|
attributes: {
|
||||||
|
MentionBlockKeys.mention: {
|
||||||
|
MentionBlockKeys.type: MentionType.page.name,
|
||||||
|
MentionBlockKeys.pageId: view.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
|
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/inline_actions_service.dart';
|
||||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
abstract class InlineActionsMenuService {
|
abstract class InlineActionsMenuService {
|
||||||
InlineActionsMenuStyle get style;
|
InlineActionsMenuStyle get style;
|
||||||
@ -69,7 +70,8 @@ class InlineActionsMenu extends InlineActionsMenuService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const double menuHeight = 200.0;
|
const double menuHeight = 300.0;
|
||||||
|
const double menuWidth = 200.0;
|
||||||
const Offset menuOffset = Offset(0, 10);
|
const Offset menuOffset = Offset(0, 10);
|
||||||
final Offset editorOffset =
|
final Offset editorOffset =
|
||||||
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
||||||
@ -93,13 +95,14 @@ class InlineActionsMenu extends InlineActionsMenuService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show on the left
|
// Show on the left
|
||||||
if (offset.dx > editorSize.width / 2) {
|
final windowWidth = MediaQuery.of(context).size.width;
|
||||||
|
if (offset.dx > (windowWidth - menuWidth)) {
|
||||||
alignment = alignment == Alignment.topLeft
|
alignment = alignment == Alignment.topLeft
|
||||||
? Alignment.topRight
|
? Alignment.topRight
|
||||||
: Alignment.bottomRight;
|
: Alignment.bottomRight;
|
||||||
|
|
||||||
offset = Offset(
|
offset = Offset(
|
||||||
editorSize.width - offset.dx,
|
windowWidth - offset.dx,
|
||||||
offset.dy,
|
offset.dy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
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_menu.dart';
|
||||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
|
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
|
||||||
@ -7,8 +10,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
/// All heights are in physical pixels
|
||||||
|
const double _groupTextHeight = 14; // 12 height + 2 bottom spacing
|
||||||
|
const double _groupBottomSpacing = 6;
|
||||||
|
const double _itemHeight = 30; // 26 height + 4 vertical spacing (2*2)
|
||||||
|
|
||||||
|
const double _menuHeight = 300;
|
||||||
|
const double _contentHeight = 260;
|
||||||
|
|
||||||
extension _StartWithsSort on List<InlineActionsResult> {
|
extension _StartWithsSort on List<InlineActionsResult> {
|
||||||
void sortByStartsWithKeyword(String search) => sort(
|
void sortByStartsWithKeyword(String search) => sort(
|
||||||
@ -68,6 +77,7 @@ class InlineActionsHandler extends StatefulWidget {
|
|||||||
|
|
||||||
class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
||||||
final _focusNode = FocusNode(debugLabel: 'inline_actions_menu_handler');
|
final _focusNode = FocusNode(debugLabel: 'inline_actions_menu_handler');
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
late List<InlineActionsResult> results = widget.results;
|
late List<InlineActionsResult> results = widget.results;
|
||||||
int invalidCounter = 0;
|
int invalidCounter = 0;
|
||||||
@ -126,7 +136,8 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
|||||||
return Focus(
|
return Focus(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
onKey: onKey,
|
onKey: onKey,
|
||||||
child: DecoratedBox(
|
child: Container(
|
||||||
|
constraints: BoxConstraints.loose(const Size(200, _menuHeight)),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: widget.style.backgroundColor,
|
color: widget.style.backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(6.0),
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
@ -147,24 +158,29 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
|||||||
LocaleKeys.inlineActions_noResults.tr(),
|
LocaleKeys.inlineActions_noResults.tr(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Column(
|
: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
controller: _scrollController,
|
||||||
children: results
|
physics: const ClampingScrollPhysics(),
|
||||||
.where((g) => g.results.isNotEmpty)
|
child: Column(
|
||||||
.mapIndexed(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
(index, group) => InlineActionsGroup(
|
children: results
|
||||||
result: group,
|
.where((g) => g.results.isNotEmpty)
|
||||||
editorState: widget.editorState,
|
.mapIndexed(
|
||||||
menuService: widget.menuService,
|
(index, group) => InlineActionsGroup(
|
||||||
style: widget.style,
|
result: group,
|
||||||
isGroupSelected: _selectedGroup == index,
|
editorState: widget.editorState,
|
||||||
selectedIndex: _selectedIndex,
|
menuService: widget.menuService,
|
||||||
onSelected: widget.onDismiss,
|
style: widget.style,
|
||||||
startOffset: startOffset - widget.startCharAmount,
|
onSelected: widget.onDismiss,
|
||||||
endOffset: _search.length + widget.startCharAmount,
|
startOffset: startOffset - widget.startCharAmount,
|
||||||
),
|
endOffset: _search.length + widget.startCharAmount,
|
||||||
)
|
isLastGroup: index == results.length - 1,
|
||||||
.toList(),
|
isGroupSelected: _selectedGroup == index,
|
||||||
|
selectedIndex: _selectedIndex,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -210,11 +226,18 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
|||||||
}
|
}
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
widget.onDismiss();
|
widget.onDismiss();
|
||||||
return KeyEventResult.handled;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||||
if (_search.isEmpty) {
|
if (_search.isEmpty) {
|
||||||
|
if (_canDeleteLastCharacter()) {
|
||||||
|
widget.editorState.deleteBackward();
|
||||||
|
} else {
|
||||||
|
// Workaround for editor regaining focus
|
||||||
|
widget.editorState.apply(
|
||||||
|
widget.editorState.transaction
|
||||||
|
..afterSelection = widget.editorState.selection,
|
||||||
|
);
|
||||||
|
}
|
||||||
widget.onDismiss();
|
widget.onDismiss();
|
||||||
widget.editorState.deleteBackward();
|
|
||||||
} else {
|
} else {
|
||||||
widget.onSelectionUpdate();
|
widget.onSelectionUpdate();
|
||||||
widget.editorState.deleteBackward();
|
widget.editorState.deleteBackward();
|
||||||
@ -296,24 +319,78 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _moveSelection(LogicalKeyboardKey key) {
|
void _moveSelection(LogicalKeyboardKey key) {
|
||||||
if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.tab].contains(key)) {
|
bool didChange = false;
|
||||||
if (_selectedIndex < lengthOfGroup(_selectedGroup) - 1) {
|
|
||||||
_selectedIndex += 1;
|
if (key == LogicalKeyboardKey.arrowUp ||
|
||||||
} else if (_selectedGroup < groupLength - 1) {
|
(key == LogicalKeyboardKey.tab &&
|
||||||
_selectedGroup += 1;
|
RawKeyboard.instance.isShiftPressed)) {
|
||||||
_selectedIndex = 0;
|
|
||||||
}
|
|
||||||
} else if (key == LogicalKeyboardKey.arrowUp) {
|
|
||||||
if (_selectedIndex == 0 && _selectedGroup > 0) {
|
if (_selectedIndex == 0 && _selectedGroup > 0) {
|
||||||
_selectedGroup -= 1;
|
_selectedGroup -= 1;
|
||||||
_selectedIndex = lengthOfGroup(_selectedGroup) - 1;
|
_selectedIndex = lengthOfGroup(_selectedGroup) - 1;
|
||||||
|
didChange = true;
|
||||||
} else if (_selectedIndex > 0) {
|
} else if (_selectedIndex > 0) {
|
||||||
_selectedIndex -= 1;
|
_selectedIndex -= 1;
|
||||||
|
didChange = true;
|
||||||
|
}
|
||||||
|
} else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.tab]
|
||||||
|
.contains(key)) {
|
||||||
|
if (_selectedIndex < lengthOfGroup(_selectedGroup) - 1) {
|
||||||
|
_selectedIndex += 1;
|
||||||
|
didChange = true;
|
||||||
|
} else if (_selectedGroup < groupLength - 1) {
|
||||||
|
_selectedGroup += 1;
|
||||||
|
_selectedIndex = 0;
|
||||||
|
didChange = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted && didChange) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
_scrollToItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToItem() {
|
||||||
|
final groups = _selectedGroup + 1;
|
||||||
|
|
||||||
|
int items = 0;
|
||||||
|
for (int i = 0; i <= _selectedGroup; i++) {
|
||||||
|
items += lengthOfGroup(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the leftover items
|
||||||
|
items -= lengthOfGroup(_selectedGroup) - (_selectedIndex + 1);
|
||||||
|
|
||||||
|
/// The offset is roughly calculated by:
|
||||||
|
/// - Amount of Groups passed
|
||||||
|
/// - Amount of Items passed
|
||||||
|
final double offset =
|
||||||
|
(_groupTextHeight + _groupBottomSpacing) * groups + _itemHeight * items;
|
||||||
|
|
||||||
|
// We have a buffer so that when moving up, we show items above the currently
|
||||||
|
// selected item. The buffer is the height of 2 items
|
||||||
|
if (offset <= _scrollController.offset + _itemHeight * 2) {
|
||||||
|
// We want to show the user some options above the newly
|
||||||
|
// focused one, therefore we take the offset and subtract
|
||||||
|
// the height of three items (current + 2)
|
||||||
|
_scrollController.animateTo(
|
||||||
|
offset - _itemHeight * 3,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
);
|
||||||
|
} else if (offset >
|
||||||
|
_scrollController.offset +
|
||||||
|
_contentHeight -
|
||||||
|
_itemHeight -
|
||||||
|
_groupTextHeight) {
|
||||||
|
// The same here, we want to show the options below the
|
||||||
|
// newly focused item when moving downwards, therefore we add
|
||||||
|
// 2 times the item height to the offset
|
||||||
|
_scrollController.animateTo(
|
||||||
|
offset - _contentHeight + _itemHeight * 2,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,4 +411,18 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
|||||||
startOffset - 1 + _search.length,
|
startOffset - 1 + _search.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _canDeleteLastCharacter() {
|
||||||
|
final selection = widget.editorState.selection;
|
||||||
|
if (selection == null || !selection.isCollapsed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final delta = widget.editorState.getNodeAtPath(selection.start.path)?.delta;
|
||||||
|
if (delta == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta.isNotEmpty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ class InlineActionsGroup extends StatelessWidget {
|
|||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
required this.startOffset,
|
required this.startOffset,
|
||||||
required this.endOffset,
|
required this.endOffset,
|
||||||
|
this.isLastGroup = false,
|
||||||
this.isGroupSelected = false,
|
this.isGroupSelected = false,
|
||||||
this.selectedIndex = 0,
|
this.selectedIndex = 0,
|
||||||
});
|
});
|
||||||
@ -28,13 +29,14 @@ class InlineActionsGroup extends StatelessWidget {
|
|||||||
final int startOffset;
|
final int startOffset;
|
||||||
final int endOffset;
|
final int endOffset;
|
||||||
|
|
||||||
|
final bool isLastGroup;
|
||||||
final bool isGroupSelected;
|
final bool isGroupSelected;
|
||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 6),
|
padding: isLastGroup ? EdgeInsets.zero : const EdgeInsets.only(bottom: 6),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -1000,6 +1000,10 @@
|
|||||||
"inlineActions": {
|
"inlineActions": {
|
||||||
"noResults": "No results",
|
"noResults": "No results",
|
||||||
"pageReference": "Page reference",
|
"pageReference": "Page reference",
|
||||||
|
"docReference": "Document reference",
|
||||||
|
"boardReference": "Board reference",
|
||||||
|
"calReference": "Calendar reference",
|
||||||
|
"gridReference": "Grid reference",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"reminder": {
|
"reminder": {
|
||||||
"groupTitle": "Reminder",
|
"groupTitle": "Reminder",
|
||||||
|
Reference in New Issue
Block a user