| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 | import 'package:appflowy/plugins/document/application/doc_bloc.dart';import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_list.dart';import 'package:appflowy/plugins/document/presentation/editor_plugins/database/referenced_database_menu_tem.dart';import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';import 'package:appflowy/plugins/document/presentation/editor_style.dart';import 'package:appflowy_editor/appflowy_editor.dart';import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';/// 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;  final List<CommandShortcutEvent> commandShortcutEvents = [    ...codeBlockCommands,    ...standardCommandShortcutEvents,  ];  final List<ToolbarItem> toolbarItems = [    smartEditItem,    paragraphItem,    ...headingItems,    ...markdownFormatItems,    quoteItem,    bulletedListItem,    numberedListItem,    linkItem,    textColorItem,    highlightColorItem,  ];  late final slashMenuItems = [    inlineGridMenuItem(documentBloc),    referencedGridMenuItem,    inlineBoardMenuItem(documentBloc),    referencedBoardMenuItem,    inlineCalendarMenuItem(documentBloc),    referencedCalendarMenuItem,    calloutItem,    mathEquationItem,    codeBlockItem,    emojiMenuItem,    autoGeneratorMenuItem,  ];  late final Map<String, BlockComponentBuilder> blockComponentBuilders =      _customAppFlowyBlockComponentBuilders();  List<CharacterShortcutEvent> get characterShortcutEvents => [        // code block        ...codeBlockCharacterEvents,        // toggle list        // formatGreaterToToggleList,        // customize the slash menu command        customSlashCommand(          slashMenuItems,          style: styleCustomizer.selectionMenuStyleBuilder(),        ),        ...standardCharacterShortcutEvents          ..removeWhere(            (element) => element == slashCommand,          ), // remove the default slash command.      ];  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();    effectiveScrollController = widget.scrollController ?? ScrollController();  }  @override  void dispose() {    if (widget.scrollController == null) {      effectiveScrollController.dispose();    }    super.dispose();  }  @override  Widget build(BuildContext context) {    final (bool autoFocus, Selection? selection) =        _computeAutoFocusParameters();    final editor = AppFlowyEditor.custom(      editorState: widget.editorState,      editable: true,      shrinkWrap: widget.shrinkWrap,      scrollController: effectiveScrollController,      // setup the auto focus parameters      autoFocus: widget.autoFocus ?? autoFocus,      focusedSelection: selection,      // setup the theme      editorStyle: styleCustomizer.style(),      // customize the block builder      blockComponentBuilders: blockComponentBuilders,      // customize the shortcuts      characterShortcutEvents: characterShortcutEvents,      commandShortcutEvents: commandShortcutEvents,      header: widget.header,    );    return Center(      child: ConstrainedBox(        constraints: const BoxConstraints(          maxWidth: double.infinity,          maxHeight: double.infinity,        ),        child: FloatingToolbar(          style: styleCustomizer.floatingToolbarStyleBuilder(),          items: toolbarItems,          editorState: widget.editorState,          scrollController: effectiveScrollController,          child: editor,        ),      ),    );  }  Map<String, BlockComponentBuilder> _customAppFlowyBlockComponentBuilders() {    final standardActions = [      OptionAction.delete,      OptionAction.duplicate,      // OptionAction.divider,      // OptionAction.moveUp,      // OptionAction.moveDown,    ];    final configuration = BlockComponentConfiguration(      padding: (_) => const EdgeInsets.symmetric(vertical: 4.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: ImageBlockComponentBuilder(        configuration: configuration,      ),      DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(        configuration: configuration,      ),      DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(        configuration: configuration,      ),      DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(        configuration: configuration,      ),      CalloutBlockKeys.type: CalloutBlockComponentBuilder(        configuration: configuration,      ),      DividerBlockKeys.type: DividerBlockComponentBuilder(),      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(),    };    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      ];      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 = (_) => true;      builder.actionBuilder = (context, state) => BlockActionList(            blockComponentContext: context,            blockComponentState: state,            editorState: widget.editorState,            actions: actions,            showSlashMenu: () => showSlashMenu(              widget.editorState,            ),          );    }    return builders;  }  (bool, Selection?) _computeAutoFocusParameters() {    if (widget.editorState.document.isEmpty) {      return (true, Selection.collapse([0], 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.collapse(nodes.first.path, 0));    }    return const (false, null);  }}
 |