feat: support plus menu in table cell on mobile (#7048)

* feat: support plus menu in table cell on mobile

* test: support plus menu in table cell on mobile

* feat: add lightImpact feedback

* chore: optimize the action sheet
This commit is contained in:
Lucas 2024-12-26 11:09:56 +08:00 committed by GitHub
parent 802a667907
commit 83e50d376e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 578 additions and 334 deletions

View File

@ -385,5 +385,56 @@ void main() {
expect(paragraph.delta!, isEmpty);
}
});
testWidgets('''
1. insert a simple table via + menu
2. insert a heading block in table cell
''', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile('simple table');
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [0])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
final firstParagraphPath = [0, 0, 0, 0];
// open the plus menu and select the table block
{
await tester.openPlusMenuAndClickButton(
LocaleKeys.document_slashMenu_name_table.tr(),
);
// check the block is inserted
final table = editorState.getNodeAtPath([0])!;
expect(table.type, equals(SimpleTableBlockKeys.type));
expect(table.rowLength, equals(2));
expect(table.columnLength, equals(2));
// focus on the first cell
final selection = editorState.selection!;
expect(selection.isCollapsed, isTrue);
expect(selection.start.path, equals(firstParagraphPath));
}
// open the plus menu and select the heading block
{
await tester.openPlusMenuAndClickButton(
LocaleKeys.editor_toggleHeading1ShortForm.tr(),
);
// check the heading block is inserted
final heading = editorState.getNodeAtPath([0, 0, 0, 0])!;
expect(heading.type, equals(HeadingBlockKeys.type));
expect(heading.level, equals(1));
}
});
});
}

View File

@ -138,7 +138,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
_divider(),
..._buildPublishActions(context),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
@ -191,6 +190,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
MobileViewBottomSheetBodyAction.unpublish,
),
),
_divider(),
];
} else {
return [
@ -201,6 +201,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
MobileViewBottomSheetBodyAction.publish,
),
),
_divider(),
];
}
}

View File

@ -675,7 +675,12 @@ CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder(
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
return CalloutBlockComponentBuilder(
configuration: configuration.copyWith(
padding: (node) => const EdgeInsets.symmetric(vertical: 10),
padding: (node) {
if (UniversalPlatform.isMobile) {
return configuration.padding(node);
}
return const EdgeInsets.symmetric(vertical: 10);
},
textAlign: (node) => _buildTextAlignInTableCell(
context,
node: node,
@ -725,6 +730,7 @@ CodeBlockComponentBuilder _buildCodeBlockComponentBuilder(
) {
return CodeBlockComponentBuilder(
styleBuilder: styleCustomizer.codeBlockStyleBuilder,
configuration: configuration,
padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34),
languagePickerBuilder: codeBlockLanguagePickerBuilder,
copyButtonBuilder: codeBlockCopyBuilder,
@ -763,9 +769,10 @@ ToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder(
final factor = pageStyle.fontLayout.factor;
final headingPaddings =
pageStyle.lineHeightLayout.headingPaddings.map((e) => e * factor);
int level = node.attributes[HeadingBlockKeys.level] ?? 6;
level = level.clamp(1, 6);
return EdgeInsets.only(top: headingPaddings.elementAt(level - 1));
final level =
(node.attributes[HeadingBlockKeys.level] ?? 6).clamp(1, 6);
final top = headingPaddings.elementAt(level - 1);
return configuration.padding(node).copyWith(top: top);
}
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
@ -846,7 +853,9 @@ FileBlockComponentBuilder _buildFileBlockComponentBuilder(
BuildContext context,
BlockComponentConfiguration configuration,
) {
return FileBlockComponentBuilder(configuration: configuration);
return FileBlockComponentBuilder(
configuration: configuration,
);
}
SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder(

View File

@ -281,9 +281,13 @@ class FileBlockComponentState extends State<FileBlockComponent>
listenable: editorState.selectionNotifier,
blockColor: editorState.editorStyle.selectionColor,
supportTypes: const [BlockSelectionType.block],
child: Padding(key: fileKey, padding: padding, child: child),
child: Padding(
key: fileKey,
padding: padding,
child: child,
),
);
} else if (url == null || url.isEmpty) {
} else {
return Padding(
key: fileKey,
padding: padding,
@ -384,6 +388,9 @@ class FileBlockComponentState extends State<FileBlockComponent>
),
const HSpace(8),
],
if (UniversalPlatform.isMobile) ...[
const HSpace(36),
],
];
} else {
return [

View File

@ -0,0 +1,480 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class AddBlockMenuItemBuilder {
AddBlockMenuItemBuilder({
required this.editorState,
required this.selection,
});
final EditorState editorState;
final Selection selection;
List<TypeOptionMenuItemValue<String>> buildTypeOptionMenuItemValues(
BuildContext context,
) {
if (selection.isCollapsed) {
final node = editorState.getNodeAtPath(selection.end.path);
if (node?.parentTableCellNode != null) {
return _buildTableTypeOptionMenuItemValues(context);
}
}
return _buildDefaultTypeOptionMenuItemValues(context);
}
/// Build the default type option menu item values.
List<TypeOptionMenuItemValue<String>> _buildDefaultTypeOptionMenuItemValues(
BuildContext context,
) {
final colorMap = _colorMap(context);
return [
..._buildHeadingMenuItems(colorMap),
..._buildParagraphMenuItems(colorMap),
..._buildTodoListMenuItems(colorMap),
..._buildTableMenuItems(colorMap),
..._buildQuoteMenuItems(colorMap),
..._buildListMenuItems(colorMap),
..._buildToggleHeadingMenuItems(colorMap),
..._buildImageMenuItems(colorMap),
..._buildPhotoGalleryMenuItems(colorMap),
..._buildFileMenuItems(colorMap),
..._buildMentionMenuItems(context, colorMap),
..._buildDividerMenuItems(colorMap),
..._buildCalloutMenuItems(colorMap),
..._buildCodeMenuItems(colorMap),
..._buildMathEquationMenuItems(colorMap),
];
}
/// Build the table type option menu item values.
List<TypeOptionMenuItemValue<String>> _buildTableTypeOptionMenuItemValues(
BuildContext context,
) {
final colorMap = _colorMap(context);
return [
..._buildHeadingMenuItems(colorMap),
..._buildParagraphMenuItems(colorMap),
..._buildTodoListMenuItems(colorMap),
..._buildQuoteMenuItems(colorMap),
..._buildListMenuItems(colorMap),
..._buildToggleHeadingMenuItems(colorMap),
..._buildImageMenuItems(colorMap),
..._buildFileMenuItems(colorMap),
..._buildMentionMenuItems(context, colorMap),
..._buildDividerMenuItems(colorMap),
..._buildCalloutMenuItems(colorMap),
..._buildCodeMenuItems(colorMap),
..._buildMathEquationMenuItems(colorMap),
];
}
List<TypeOptionMenuItemValue<String>> _buildHeadingMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: HeadingBlockKeys.type,
backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading1.tr(),
icon: FlowySvgs.m_add_block_h1_s,
onTap: (_, __) => _insertBlock(headingNode(level: 1)),
),
TypeOptionMenuItemValue(
value: HeadingBlockKeys.type,
backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading2.tr(),
icon: FlowySvgs.m_add_block_h2_s,
onTap: (_, __) => _insertBlock(headingNode(level: 2)),
),
TypeOptionMenuItemValue(
value: HeadingBlockKeys.type,
backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading3.tr(),
icon: FlowySvgs.m_add_block_h3_s,
onTap: (_, __) => _insertBlock(headingNode(level: 3)),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildParagraphMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
backgroundColor: colorMap[ParagraphBlockKeys.type]!,
text: LocaleKeys.editor_text.tr(),
icon: FlowySvgs.m_add_block_paragraph_s,
onTap: (_, __) => _insertBlock(paragraphNode()),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildTodoListMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: TodoListBlockKeys.type,
backgroundColor: colorMap[TodoListBlockKeys.type]!,
text: LocaleKeys.editor_checkbox.tr(),
icon: FlowySvgs.m_add_block_checkbox_s,
onTap: (_, __) => _insertBlock(todoListNode(checked: false)),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildTableMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: SimpleTableBlockKeys.type,
backgroundColor: colorMap[SimpleTableBlockKeys.type]!,
text: LocaleKeys.editor_table.tr(),
icon: FlowySvgs.slash_menu_icon_simple_table_s,
onTap: (_, __) => _insertBlock(
createSimpleTableBlockNode(columnCount: 2, rowCount: 2),
),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildQuoteMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: QuoteBlockKeys.type,
backgroundColor: colorMap[QuoteBlockKeys.type]!,
text: LocaleKeys.editor_quote.tr(),
icon: FlowySvgs.m_add_block_quote_s,
onTap: (_, __) => _insertBlock(quoteNode()),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildListMenuItems(
Map<String, Color> colorMap,
) {
return [
// bulleted list, numbered list, toggle list
TypeOptionMenuItemValue(
value: BulletedListBlockKeys.type,
backgroundColor: colorMap[BulletedListBlockKeys.type]!,
text: LocaleKeys.editor_bulletedListShortForm.tr(),
icon: FlowySvgs.m_add_block_bulleted_list_s,
onTap: (_, __) => _insertBlock(bulletedListNode()),
),
TypeOptionMenuItemValue(
value: NumberedListBlockKeys.type,
backgroundColor: colorMap[NumberedListBlockKeys.type]!,
text: LocaleKeys.editor_numberedListShortForm.tr(),
icon: FlowySvgs.m_add_block_numbered_list_s,
onTap: (_, __) => _insertBlock(numberedListNode()),
),
TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type,
backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleListShortForm.tr(),
icon: FlowySvgs.m_add_block_toggle_s,
onTap: (_, __) => _insertBlock(toggleListBlockNode()),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildToggleHeadingMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type,
backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleHeading1ShortForm.tr(),
icon: FlowySvgs.toggle_heading1_s,
iconPadding: const EdgeInsets.all(3),
onTap: (_, __) => _insertBlock(toggleHeadingNode()),
),
TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type,
backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleHeading2ShortForm.tr(),
icon: FlowySvgs.toggle_heading2_s,
iconPadding: const EdgeInsets.all(3),
onTap: (_, __) => _insertBlock(toggleHeadingNode(level: 2)),
),
TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type,
backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleHeading3ShortForm.tr(),
icon: FlowySvgs.toggle_heading3_s,
iconPadding: const EdgeInsets.all(3),
onTap: (_, __) => _insertBlock(toggleHeadingNode(level: 3)),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildImageMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: ImageBlockKeys.type,
backgroundColor: colorMap[ImageBlockKeys.type]!,
text: LocaleKeys.editor_image.tr(),
icon: FlowySvgs.m_add_block_image_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 400), () async {
final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();
await editorState.insertEmptyImageBlock(imagePlaceholderKey);
});
},
),
];
}
List<TypeOptionMenuItemValue<String>> _buildPhotoGalleryMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: MultiImageBlockKeys.type,
backgroundColor: colorMap[ImageBlockKeys.type]!,
text: LocaleKeys.document_plugins_photoGallery_name.tr(),
icon: FlowySvgs.m_add_block_photo_gallery_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 400), () async {
final imagePlaceholderKey = GlobalKey<MultiImagePlaceholderState>();
await editorState.insertEmptyMultiImageBlock(imagePlaceholderKey);
});
},
),
];
}
List<TypeOptionMenuItemValue<String>> _buildFileMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: FileBlockKeys.type,
backgroundColor: colorMap[ImageBlockKeys.type]!,
text: LocaleKeys.document_plugins_file_name.tr(),
icon: FlowySvgs.media_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 400), () async {
final fileGlobalKey = GlobalKey<FileBlockComponentState>();
await editorState.insertEmptyFileBlock(fileGlobalKey);
});
},
),
];
}
List<TypeOptionMenuItemValue<String>> _buildMentionMenuItems(
BuildContext context,
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
backgroundColor: colorMap[MentionBlockKeys.type]!,
text: LocaleKeys.editor_date.tr(),
icon: FlowySvgs.m_add_block_date_s,
onTap: (_, __) => _insertBlock(dateMentionNode()),
),
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
backgroundColor: colorMap[MentionBlockKeys.type]!,
text: LocaleKeys.editor_page.tr(),
icon: FlowySvgs.icon_document_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
final currentViewId = getIt<MenuSharedState>().latestOpenView?.id;
final view = await showPageSelectorSheet(
context,
currentViewId: currentViewId,
);
if (view != null) {
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertBlockAfterCurrentSelection(
selection,
pageMentionNode(view.id),
);
});
}
},
),
];
}
List<TypeOptionMenuItemValue<String>> _buildDividerMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: DividerBlockKeys.type,
backgroundColor: colorMap[DividerBlockKeys.type]!,
text: LocaleKeys.editor_divider.tr(),
icon: FlowySvgs.m_add_block_divider_s,
onTap: (_, __) {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertDivider(selection);
});
},
),
];
}
// callout, code, math equation
List<TypeOptionMenuItemValue<String>> _buildCalloutMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: CalloutBlockKeys.type,
backgroundColor: colorMap[CalloutBlockKeys.type]!,
text: LocaleKeys.document_plugins_callout.tr(),
icon: FlowySvgs.m_add_block_callout_s,
onTap: (_, __) => _insertBlock(calloutNode()),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildCodeMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: CodeBlockKeys.type,
backgroundColor: colorMap[CodeBlockKeys.type]!,
text: LocaleKeys.editor_codeBlockShortForm.tr(),
icon: FlowySvgs.m_add_block_code_s,
onTap: (_, __) => _insertBlock(codeBlockNode()),
),
];
}
List<TypeOptionMenuItemValue<String>> _buildMathEquationMenuItems(
Map<String, Color> colorMap,
) {
return [
TypeOptionMenuItemValue(
value: MathEquationBlockKeys.type,
backgroundColor: colorMap[MathEquationBlockKeys.type]!,
text: LocaleKeys.editor_mathEquationShortForm.tr(),
icon: FlowySvgs.m_add_block_formula_s,
onTap: (_, __) {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertMathEquation(selection);
});
},
),
];
}
Map<String, Color> _colorMap(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
if (isDarkMode) {
return {
HeadingBlockKeys.type: const Color(0xFF5465A1),
ParagraphBlockKeys.type: const Color(0xFF5465A1),
TodoListBlockKeys.type: const Color(0xFF4BB299),
SimpleTableBlockKeys.type: const Color(0xFF4BB299),
QuoteBlockKeys.type: const Color(0xFFBAAC74),
BulletedListBlockKeys.type: const Color(0xFFA35F94),
NumberedListBlockKeys.type: const Color(0xFFA35F94),
ToggleListBlockKeys.type: const Color(0xFFA35F94),
ImageBlockKeys.type: const Color(0xFFBAAC74),
MentionBlockKeys.type: const Color(0xFF40AAB8),
DividerBlockKeys.type: const Color(0xFF4BB299),
CalloutBlockKeys.type: const Color(0xFF66599B),
CodeBlockKeys.type: const Color(0xFF66599B),
MathEquationBlockKeys.type: const Color(0xFF66599B),
};
}
return {
HeadingBlockKeys.type: const Color(0xFFBECCFF),
ParagraphBlockKeys.type: const Color(0xFFBECCFF),
TodoListBlockKeys.type: const Color(0xFF98F4CD),
SimpleTableBlockKeys.type: const Color(0xFF98F4CD),
QuoteBlockKeys.type: const Color(0xFFFDEDA7),
BulletedListBlockKeys.type: const Color(0xFFFFB9EF),
NumberedListBlockKeys.type: const Color(0xFFFFB9EF),
ToggleListBlockKeys.type: const Color(0xFFFFB9EF),
ImageBlockKeys.type: const Color(0xFFFDEDA7),
MentionBlockKeys.type: const Color(0xFF91EAF5),
DividerBlockKeys.type: const Color(0xFF98F4CD),
CalloutBlockKeys.type: const Color(0xFFCABDFF),
CodeBlockKeys.type: const Color(0xFFCABDFF),
MathEquationBlockKeys.type: const Color(0xFFCABDFF),
};
}
Future<void> _insertBlock(Node node) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(
const Duration(milliseconds: 100),
() async {
// if current selected block is a empty paragraph block, replace it with the new block.
if (selection.isCollapsed) {
final currentNode = editorState.getNodeAtPath(selection.end.path);
final text = currentNode?.delta?.toPlainText();
if (currentNode != null &&
currentNode.type == ParagraphBlockKeys.type &&
text != null &&
text.isEmpty) {
final transaction = editorState.transaction;
transaction.insertNode(
selection.end.path.next,
node,
);
transaction.deleteNode(currentNode);
if (node.type == SimpleTableBlockKeys.type) {
transaction.afterSelection = Selection.collapsed(
Position(
// table -> row -> cell -> paragraph
path: selection.end.path + [0, 0, 0],
),
);
} else {
transaction.afterSelection = Selection.collapsed(
Position(path: selection.end.path),
);
}
transaction.selectionExtraInfo = {};
await editorState.apply(transaction);
return;
}
}
await editorState.insertBlockAfterCurrentSelection(selection, node);
},
);
}
}

View File

@ -4,21 +4,14 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'add_block_menu_item_builder.dart';
@visibleForTesting
const addBlockToolbarItemKey = ValueKey('add_block_toolbar_item');
@ -94,323 +87,13 @@ class AddBlockMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
final builder = AddBlockMenuItemBuilder(
editorState: editorState,
selection: selection,
);
return TypeOptionMenu<String>(
values: buildTypeOptionMenuItemValues(context),
values: builder.buildTypeOptionMenuItemValues(context),
scaleFactor: context.scale,
);
}
Future<void> _insertBlock(Node node) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(
const Duration(milliseconds: 100),
() async {
// if current selected block is a empty paragraph block, replace it with the new block.
if (selection.isCollapsed) {
final currentNode = editorState.getNodeAtPath(selection.end.path);
final text = currentNode?.delta?.toPlainText();
if (currentNode != null &&
currentNode.type == ParagraphBlockKeys.type &&
text != null &&
text.isEmpty) {
final transaction = editorState.transaction;
transaction.insertNode(
selection.end.path.next,
node,
);
transaction.deleteNode(currentNode);
if (node.type == SimpleTableBlockKeys.type) {
transaction.afterSelection = Selection.collapsed(
Position(
// table -> row -> cell -> paragraph
path: selection.end.path + [0, 0, 0],
),
);
} else {
transaction.afterSelection = Selection.collapsed(
Position(path: selection.end.path),
);
}
transaction.selectionExtraInfo = {};
await editorState.apply(transaction);
return;
}
}
await editorState.insertBlockAfterCurrentSelection(selection, node);
},
);
}
List<TypeOptionMenuItemValue<String>> buildTypeOptionMenuItemValues(
BuildContext context,
) {
final colorMap = _colorMap(context);
return [
// heading 1 - 3
TypeOptionMenuItemValue(
value: HeadingBlockKeys.type,
backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading1.tr(),
icon: FlowySvgs.m_add_block_h1_s,
onTap: (_, __) => _insertBlock(headingNode(level: 1)),
),
TypeOptionMenuItemValue(
value: HeadingBlockKeys.type,
backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading2.tr(),
icon: FlowySvgs.m_add_block_h2_s,
onTap: (_, __) => _insertBlock(headingNode(level: 2)),
),
TypeOptionMenuItemValue(
value: HeadingBlockKeys.type,
backgroundColor: colorMap[HeadingBlockKeys.type]!,
text: LocaleKeys.editor_heading3.tr(),
icon: FlowySvgs.m_add_block_h3_s,
onTap: (_, __) => _insertBlock(headingNode(level: 3)),
),
// paragraph
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
backgroundColor: colorMap[ParagraphBlockKeys.type]!,
text: LocaleKeys.editor_text.tr(),
icon: FlowySvgs.m_add_block_paragraph_s,
onTap: (_, __) => _insertBlock(paragraphNode()),
),
// checkbox
TypeOptionMenuItemValue(
value: TodoListBlockKeys.type,
backgroundColor: colorMap[TodoListBlockKeys.type]!,
text: LocaleKeys.editor_checkbox.tr(),
icon: FlowySvgs.m_add_block_checkbox_s,
onTap: (_, __) => _insertBlock(todoListNode(checked: false)),
),
// table
TypeOptionMenuItemValue(
value: SimpleTableBlockKeys.type,
backgroundColor: colorMap[SimpleTableBlockKeys.type]!,
text: LocaleKeys.editor_table.tr(),
icon: FlowySvgs.slash_menu_icon_simple_table_s,
onTap: (_, __) => _insertBlock(
createSimpleTableBlockNode(columnCount: 2, rowCount: 2),
),
),
// quote
TypeOptionMenuItemValue(
value: QuoteBlockKeys.type,
backgroundColor: colorMap[QuoteBlockKeys.type]!,
text: LocaleKeys.editor_quote.tr(),
icon: FlowySvgs.m_add_block_quote_s,
onTap: (_, __) => _insertBlock(quoteNode()),
),
// bulleted list, numbered list, toggle list
TypeOptionMenuItemValue(
value: BulletedListBlockKeys.type,
backgroundColor: colorMap[BulletedListBlockKeys.type]!,
text: LocaleKeys.editor_bulletedListShortForm.tr(),
icon: FlowySvgs.m_add_block_bulleted_list_s,
onTap: (_, __) => _insertBlock(bulletedListNode()),
),
TypeOptionMenuItemValue(
value: NumberedListBlockKeys.type,
backgroundColor: colorMap[NumberedListBlockKeys.type]!,
text: LocaleKeys.editor_numberedListShortForm.tr(),
icon: FlowySvgs.m_add_block_numbered_list_s,
onTap: (_, __) => _insertBlock(numberedListNode()),
),
TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type,
backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleListShortForm.tr(),
icon: FlowySvgs.m_add_block_toggle_s,
onTap: (_, __) => _insertBlock(toggleListBlockNode()),
),
// toggle headings
TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type,
backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleHeading1ShortForm.tr(),
icon: FlowySvgs.toggle_heading1_s,
iconPadding: const EdgeInsets.all(3),
onTap: (_, __) => _insertBlock(toggleHeadingNode()),
),
TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type,
backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleHeading2ShortForm.tr(),
icon: FlowySvgs.toggle_heading2_s,
iconPadding: const EdgeInsets.all(3),
onTap: (_, __) => _insertBlock(toggleHeadingNode(level: 2)),
),
TypeOptionMenuItemValue(
value: ToggleListBlockKeys.type,
backgroundColor: colorMap[ToggleListBlockKeys.type]!,
text: LocaleKeys.editor_toggleHeading3ShortForm.tr(),
icon: FlowySvgs.toggle_heading3_s,
iconPadding: const EdgeInsets.all(3),
onTap: (_, __) => _insertBlock(toggleHeadingNode(level: 3)),
),
// image
TypeOptionMenuItemValue(
value: ImageBlockKeys.type,
backgroundColor: colorMap[ImageBlockKeys.type]!,
text: LocaleKeys.editor_image.tr(),
icon: FlowySvgs.m_add_block_image_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 400), () async {
final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();
await editorState.insertEmptyImageBlock(imagePlaceholderKey);
});
},
),
TypeOptionMenuItemValue(
value: MultiImageBlockKeys.type,
backgroundColor: colorMap[ImageBlockKeys.type]!,
text: LocaleKeys.document_plugins_photoGallery_name.tr(),
icon: FlowySvgs.m_add_block_photo_gallery_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 400), () async {
final imagePlaceholderKey = GlobalKey<MultiImagePlaceholderState>();
await editorState.insertEmptyMultiImageBlock(imagePlaceholderKey);
});
},
),
TypeOptionMenuItemValue(
value: FileBlockKeys.type,
backgroundColor: colorMap[ImageBlockKeys.type]!,
text: LocaleKeys.document_plugins_file_name.tr(),
icon: FlowySvgs.media_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 400), () async {
final fileGlobalKey = GlobalKey<FileBlockComponentState>();
await editorState.insertEmptyFileBlock(fileGlobalKey);
});
},
),
// date
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
backgroundColor: colorMap[MentionBlockKeys.type]!,
text: LocaleKeys.editor_date.tr(),
icon: FlowySvgs.m_add_block_date_s,
onTap: (_, __) => _insertBlock(dateMentionNode()),
),
// page
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
backgroundColor: colorMap[MentionBlockKeys.type]!,
text: LocaleKeys.editor_page.tr(),
icon: FlowySvgs.icon_document_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
final currentViewId = getIt<MenuSharedState>().latestOpenView?.id;
final view = await showPageSelectorSheet(
context,
currentViewId: currentViewId,
);
if (view != null) {
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertBlockAfterCurrentSelection(
selection,
pageMentionNode(view.id),
);
});
}
},
),
// divider
TypeOptionMenuItemValue(
value: DividerBlockKeys.type,
backgroundColor: colorMap[DividerBlockKeys.type]!,
text: LocaleKeys.editor_divider.tr(),
icon: FlowySvgs.m_add_block_divider_s,
onTap: (_, __) {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertDivider(selection);
});
},
),
// callout, code, math equation
TypeOptionMenuItemValue(
value: CalloutBlockKeys.type,
backgroundColor: colorMap[CalloutBlockKeys.type]!,
text: LocaleKeys.document_plugins_callout.tr(),
icon: FlowySvgs.m_add_block_callout_s,
onTap: (_, __) => _insertBlock(calloutNode()),
),
TypeOptionMenuItemValue(
value: CodeBlockKeys.type,
backgroundColor: colorMap[CodeBlockKeys.type]!,
text: LocaleKeys.editor_codeBlockShortForm.tr(),
icon: FlowySvgs.m_add_block_code_s,
onTap: (_, __) => _insertBlock(codeBlockNode()),
),
TypeOptionMenuItemValue(
value: MathEquationBlockKeys.type,
backgroundColor: colorMap[MathEquationBlockKeys.type]!,
text: LocaleKeys.editor_mathEquationShortForm.tr(),
icon: FlowySvgs.m_add_block_formula_s,
onTap: (_, __) {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertMathEquation(selection);
});
},
),
];
}
Map<String, Color> _colorMap(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
if (isDarkMode) {
return {
HeadingBlockKeys.type: const Color(0xFF5465A1),
ParagraphBlockKeys.type: const Color(0xFF5465A1),
TodoListBlockKeys.type: const Color(0xFF4BB299),
SimpleTableBlockKeys.type: const Color(0xFF4BB299),
QuoteBlockKeys.type: const Color(0xFFBAAC74),
BulletedListBlockKeys.type: const Color(0xFFA35F94),
NumberedListBlockKeys.type: const Color(0xFFA35F94),
ToggleListBlockKeys.type: const Color(0xFFA35F94),
ImageBlockKeys.type: const Color(0xFFBAAC74),
MentionBlockKeys.type: const Color(0xFF40AAB8),
DividerBlockKeys.type: const Color(0xFF4BB299),
CalloutBlockKeys.type: const Color(0xFF66599B),
CodeBlockKeys.type: const Color(0xFF66599B),
MathEquationBlockKeys.type: const Color(0xFF66599B),
};
}
return {
HeadingBlockKeys.type: const Color(0xFFBECCFF),
ParagraphBlockKeys.type: const Color(0xFFBECCFF),
TodoListBlockKeys.type: const Color(0xFF98F4CD),
SimpleTableBlockKeys.type: const Color(0xFF98F4CD),
QuoteBlockKeys.type: const Color(0xFFFDEDA7),
BulletedListBlockKeys.type: const Color(0xFFFFB9EF),
NumberedListBlockKeys.type: const Color(0xFFFFB9EF),
ToggleListBlockKeys.type: const Color(0xFFFFB9EF),
ImageBlockKeys.type: const Color(0xFFFDEDA7),
MentionBlockKeys.type: const Color(0xFF91EAF5),
DividerBlockKeys.type: const Color(0xFF98F4CD),
CalloutBlockKeys.type: const Color(0xFFCABDFF),
CodeBlockKeys.type: const Color(0xFFCABDFF),
MathEquationBlockKeys.type: const Color(0xFFCABDFF),
};
}
}

View File

@ -2,6 +2,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:universal_platform/universal_platform.dart';
@ -546,9 +547,17 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
isReorderingHitCellNotifier.value = isHitCurrentCell;
if (isHitCurrentCell) {
if (isReorderingColumn) {
simpleTableContext.isReorderingHitIndex.value = node.columnIndex;
if (simpleTableContext.isReorderingHitIndex.value != node.columnIndex) {
HapticFeedback.lightImpact();
simpleTableContext.isReorderingHitIndex.value = node.columnIndex;
}
} else if (isReorderingRow) {
simpleTableContext.isReorderingHitIndex.value = node.rowIndex;
if (simpleTableContext.isReorderingHitIndex.value != node.rowIndex) {
HapticFeedback.lightImpact();
simpleTableContext.isReorderingHitIndex.value = node.rowIndex;
}
}
}
}

View File

@ -3,6 +3,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class SimpleTableMobileDraggableReorderButton extends StatelessWidget {
@ -46,6 +47,8 @@ class SimpleTableMobileDraggableReorderButton extends StatelessWidget {
}
void _startDragging() {
HapticFeedback.lightImpact();
isShowingMenu.value = true;
editorState.selection = null;

View File

@ -65,6 +65,7 @@ class _SimpleTableFeedbackState extends State<SimpleTableFeedback> {
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Provider.value(
value: widget.editorState,
child: SimpleTableWidget(