123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- import 'package:appflowy/plugins/document/application/doc_bloc.dart';
- import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
- import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
- import 'package:appflowy/plugins/document/presentation/editor_style.dart';
- import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';
- import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';
- import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
- import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
- import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
- import 'package:appflowy/workspace/application/appearance.dart';
- import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
- import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
- import 'package:appflowy_editor/appflowy_editor.dart';
- import 'package:collection/collection.dart';
- import 'package:flowy_infra/theme_extension.dart';
- import 'package:flowy_infra_ui/flowy_infra_ui.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter_bloc/flutter_bloc.dart';
- final List<CommandShortcutEvent> commandShortcutEvents = [
- toggleToggleListCommand,
- ...codeBlockCommands,
- customCopyCommand,
- customPasteCommand,
- customCutCommand,
- ...standardCommandShortcutEvents,
- ];
- final List<CommandShortcutEvent> defaultCommandShortcutEvents = [
- ...commandShortcutEvents.map((e) => e.copyWith()).toList(),
- ];
- /// Wrapper for the appflowy editor.
- class AppFlowyEditorPage extends StatefulWidget {
- const AppFlowyEditorPage({
- super.key,
- required this.editorState,
- this.header,
- this.shrinkWrap = false,
- this.scrollController,
- this.autoFocus,
- required this.styleCustomizer,
- });
- final Widget? header;
- final EditorState editorState;
- final ScrollController? scrollController;
- final bool shrinkWrap;
- final bool? autoFocus;
- final EditorStyleCustomizer styleCustomizer;
- @override
- State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
- }
- class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
- late final ScrollController effectiveScrollController;
- late final InlineActionsService inlineActionsService = InlineActionsService(
- context: context,
- handlers: [
- InlinePageReferenceService(
- currentViewId: documentBloc.view.id,
- ).inlinePageReferenceDelegate,
- DateReferenceService(context).dateReferenceDelegate,
- ReminderReferenceService(context).reminderReferenceDelegate,
- ],
- );
- late final List<CommandShortcutEvent> commandShortcutEvents = [
- toggleToggleListCommand,
- ...codeBlockCommands,
- customCopyCommand,
- customPasteCommand,
- customCutCommand,
- ...standardCommandShortcutEvents,
- ..._buildFindAndReplaceCommands(),
- ];
- final List<ToolbarItem> toolbarItems = [
- smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
- paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
- ...(headingItems
- ..forEach(
- (e) => e.isActive = onlyShowInSingleSelectionAndTextType,
- )),
- ...markdownFormatItems,
- quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
- bulletedListItem
- ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
- numberedListItem
- ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
- inlineMathEquationItem,
- linkItem,
- alignToolbarItem,
- buildTextColorItem(),
- buildHighlightColorItem(),
- customizeFontToolbarItem,
- ];
- late final List<SelectionMenuItem> slashMenuItems;
- late final Map<String, BlockComponentBuilder> blockComponentBuilders =
- _customAppFlowyBlockComponentBuilders();
- List<CharacterShortcutEvent> get characterShortcutEvents => [
- // code block
- ...codeBlockCharacterEvents,
- // toggle list
- formatGreaterToToggleList,
- insertChildNodeInsideToggleList,
- // customize the slash menu command
- customSlashCommand(
- slashMenuItems,
- style: styleCustomizer.selectionMenuStyleBuilder(),
- ),
- ...standardCharacterShortcutEvents
- ..removeWhere(
- (element) => element == slashCommand,
- ), // remove the default slash command.
- /// Inline Actions
- /// - Reminder
- /// - Inline-page reference
- inlineActionsCommand(
- inlineActionsService,
- style: styleCustomizer.inlineActionsMenuStyleBuilder(),
- ),
- ];
- late final showSlashMenu = customSlashCommand(
- slashMenuItems,
- shouldInsertSlash: false,
- style: styleCustomizer.selectionMenuStyleBuilder(),
- ).handler;
- EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer;
- DocumentBloc get documentBloc => context.read<DocumentBloc>();
- @override
- void initState() {
- super.initState();
- _initializeShortcuts();
- indentableBlockTypes.add(ToggleListBlockKeys.type);
- convertibleBlockTypes.add(ToggleListBlockKeys.type);
- slashMenuItems = _customSlashMenuItems();
- effectiveScrollController = widget.scrollController ?? ScrollController();
- // keep the previous font style when typing new text.
- supportSlashMenuNodeWhiteList.addAll([
- ToggleListBlockKeys.type,
- ]);
- AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily);
- }
- @override
- void dispose() {
- if (widget.scrollController == null) {
- effectiveScrollController.dispose();
- }
- inlineActionsService.dispose();
- widget.editorState.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- final (bool autoFocus, Selection? selection) =
- _computeAutoFocusParameters();
- final isRTL =
- context.read<AppearanceSettingsCubit>().state.layoutDirection ==
- LayoutDirection.rtlLayout;
- final textDirection = isRTL ? TextDirection.rtl : TextDirection.ltr;
- _setRTLToolbarItems(isRTL);
- final editorScrollController = EditorScrollController(
- editorState: widget.editorState,
- shrinkWrap: widget.shrinkWrap,
- scrollController: effectiveScrollController,
- );
- final editor = AppFlowyEditor(
- editorState: widget.editorState,
- editable: true,
- editorScrollController: editorScrollController,
- // setup the auto focus parameters
- autoFocus: widget.autoFocus ?? autoFocus,
- focusedSelection: selection,
- // setup the theme
- editorStyle: styleCustomizer.style(),
- // customize the block builders
- blockComponentBuilders: blockComponentBuilders,
- // customize the shortcuts
- characterShortcutEvents: characterShortcutEvents,
- commandShortcutEvents: commandShortcutEvents,
- // customize the context menu items
- contextMenuItems: customContextMenuItems,
- // customize the header and footer.
- header: widget.header,
- footer: const VSpace(200),
- );
- return Center(
- child: FloatingToolbar(
- style: styleCustomizer.floatingToolbarStyleBuilder(),
- items: toolbarItems,
- editorState: widget.editorState,
- editorScrollController: editorScrollController,
- textDirection: textDirection,
- child: Directionality(
- textDirection: textDirection,
- child: editor,
- ),
- ),
- );
- }
- Map<String, BlockComponentBuilder> _customAppFlowyBlockComponentBuilders() {
- final standardActions = [
- OptionAction.delete,
- OptionAction.duplicate,
- // OptionAction.divider,
- // OptionAction.moveUp,
- // OptionAction.moveDown,
- ];
- final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
- final configuration = BlockComponentConfiguration(
- padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
- );
- final customBlockComponentBuilderMap = {
- PageBlockKeys.type: PageBlockComponentBuilder(),
- ParagraphBlockKeys.type: TextBlockComponentBuilder(
- configuration: configuration,
- ),
- TodoListBlockKeys.type: TodoListBlockComponentBuilder(
- configuration: configuration.copyWith(
- placeholderText: (_) => 'To-do',
- ),
- ),
- BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
- configuration: configuration.copyWith(
- placeholderText: (_) => 'List',
- ),
- ),
- NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
- configuration: configuration.copyWith(
- placeholderText: (_) => 'List',
- ),
- ),
- QuoteBlockKeys.type: QuoteBlockComponentBuilder(
- configuration: configuration.copyWith(
- placeholderText: (_) => 'Quote',
- ),
- ),
- HeadingBlockKeys.type: HeadingBlockComponentBuilder(
- configuration: configuration.copyWith(
- padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
- placeholderText: (node) =>
- 'Heading ${node.attributes[HeadingBlockKeys.level]}',
- ),
- textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
- ),
- ImageBlockKeys.type: CustomImageBlockComponentBuilder(
- configuration: configuration,
- showMenu: true,
- menuBuilder: (Node node, CustomImageBlockComponentState state) =>
- Positioned(
- top: 0,
- right: 10,
- child: ImageMenu(
- node: node,
- state: state,
- ),
- ),
- ),
- TableBlockKeys.type: TableBlockComponentBuilder(
- menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
- TableMenu(
- node: node,
- editorState: editorState,
- position: position,
- dir: dir,
- onBuild: onBuild,
- onClose: onClose,
- ),
- ),
- TableCellBlockKeys.type: TableCellBlockComponentBuilder(
- menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
- TableMenu(
- node: node,
- editorState: editorState,
- position: position,
- dir: dir,
- onBuild: onBuild,
- onClose: onClose,
- ),
- ),
- DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
- configuration: configuration.copyWith(
- padding: (_) => const EdgeInsets.symmetric(vertical: 10),
- ),
- ),
- DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
- configuration: configuration.copyWith(
- padding: (_) => const EdgeInsets.symmetric(vertical: 10),
- ),
- ),
- DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
- configuration: configuration.copyWith(
- padding: (_) => const EdgeInsets.symmetric(vertical: 10),
- ),
- ),
- CalloutBlockKeys.type: CalloutBlockComponentBuilder(
- configuration: configuration,
- defaultColor: calloutBGColor,
- ),
- DividerBlockKeys.type: DividerBlockComponentBuilder(
- configuration: configuration,
- height: 28.0,
- ),
- MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
- configuration: configuration.copyWith(
- padding: (_) => const EdgeInsets.symmetric(vertical: 20),
- ),
- ),
- CodeBlockKeys.type: CodeBlockComponentBuilder(
- configuration: configuration.copyWith(
- textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
- placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
- ),
- padding: const EdgeInsets.only(
- left: 30,
- right: 30,
- bottom: 36,
- ),
- ),
- AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
- SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
- ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
- configuration: configuration,
- ),
- OutlineBlockKeys.type: OutlineBlockComponentBuilder(
- configuration: configuration.copyWith(
- placeholderTextStyle: (_) =>
- styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
- ),
- ),
- errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
- configuration: configuration.copyWith(
- padding: (_) => const EdgeInsets.symmetric(vertical: 10),
- ),
- ),
- };
- final builders = {
- ...standardBlockComponentBuilderMap,
- ...customBlockComponentBuilderMap,
- };
- // customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
- for (final entry in builders.entries) {
- if (entry.key == PageBlockKeys.type) {
- continue;
- }
- final builder = entry.value;
- // customize the action builder.
- final supportColorBuilderTypes = [
- ParagraphBlockKeys.type,
- HeadingBlockKeys.type,
- BulletedListBlockKeys.type,
- NumberedListBlockKeys.type,
- QuoteBlockKeys.type,
- TodoListBlockKeys.type,
- CalloutBlockKeys.type,
- OutlineBlockKeys.type,
- ToggleListBlockKeys.type,
- ];
- final supportAlignBuilderType = [
- ImageBlockKeys.type,
- ];
- final colorAction = [
- OptionAction.divider,
- OptionAction.color,
- ];
- final alignAction = [
- OptionAction.divider,
- OptionAction.align,
- ];
- final List<OptionAction> actions = [
- ...standardActions,
- if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
- if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
- ];
- builder.showActions =
- (node) => node.parent?.type != TableCellBlockKeys.type;
- builder.actionBuilder = (context, state) {
- final top = builder.configuration.padding(context.node).top;
- final padding = context.node.type == HeadingBlockKeys.type
- ? EdgeInsets.only(top: top + 8.0)
- : EdgeInsets.only(top: top + 2.0);
- return Padding(
- padding: padding,
- child: BlockActionList(
- blockComponentContext: context,
- blockComponentState: state,
- editorState: widget.editorState,
- actions: actions,
- showSlashMenu: () => showSlashMenu(widget.editorState),
- ),
- );
- };
- }
- return builders;
- }
- List<SelectionMenuItem> _customSlashMenuItems() {
- final items = [...standardSelectionMenuItems];
- final imageItem = items.firstWhereOrNull(
- (element) => element.name == AppFlowyEditorLocalizations.current.image,
- );
- if (imageItem != null) {
- final imageItemIndex = items.indexOf(imageItem);
- if (imageItemIndex != -1) {
- items[imageItemIndex] = customImageMenuItem;
- }
- }
- return [
- ...items,
- inlineGridMenuItem(documentBloc),
- referencedGridMenuItem,
- inlineBoardMenuItem(documentBloc),
- referencedBoardMenuItem,
- inlineCalendarMenuItem(documentBloc),
- referencedCalendarMenuItem,
- calloutItem,
- outlineItem,
- mathEquationItem,
- codeBlockItem,
- toggleListBlockItem,
- emojiMenuItem,
- autoGeneratorMenuItem,
- ];
- }
- (bool, Selection?) _computeAutoFocusParameters() {
- if (widget.editorState.document.isEmpty) {
- return (
- true,
- Selection.collapsed(
- Position(path: [0], offset: 0),
- ),
- );
- }
- final nodes = widget.editorState.document.root.children
- .where((element) => element.delta != null);
- final isAllEmpty =
- nodes.isNotEmpty && nodes.every((element) => element.delta!.isEmpty);
- if (isAllEmpty) {
- return (
- true,
- Selection.collapsed(
- Position(path: nodes.first.path, offset: 0),
- )
- );
- }
- return const (false, null);
- }
- Future<void> _initializeShortcuts() async {
- // TODO(Xazin): Refactor lazy initialization
- defaultCommandShortcutEvents;
- final settingsShortcutService = SettingsShortcutService();
- final customizeShortcuts =
- await settingsShortcutService.getCustomizeShortcuts();
- await settingsShortcutService.updateCommandShortcuts(
- commandShortcutEvents,
- customizeShortcuts,
- );
- }
- void _setRTLToolbarItems(bool isRTL) {
- final textDirectionItemIds = textDirectionItems.map((e) => e.id);
- // clear all the text direction items
- toolbarItems.removeWhere(
- (item) => textDirectionItemIds.contains(item.id),
- );
- // only show the rtl item when the layout direction is ltr.
- if (isRTL) {
- toolbarItems.addAll(textDirectionItems);
- }
- }
- List<CommandShortcutEvent> _buildFindAndReplaceCommands() {
- return findAndReplaceCommands(
- context: context,
- style: FindReplaceStyle(
- findMenuBuilder: (
- context,
- editorState,
- localizations,
- style,
- showReplaceMenu,
- onDismiss,
- ) {
- return Material(
- child: DecoratedBox(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surfaceVariant,
- borderRadius: BorderRadius.circular(4),
- ),
- child: FindAndReplaceMenuWidget(
- editorState: editorState,
- onDismiss: onDismiss,
- ),
- ),
- );
- },
- ),
- );
- }
- }
|