123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- 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,
- });
- final Widget? header;
- final EditorState editorState;
- final ScrollController? scrollController;
- final bool shrinkWrap;
- final bool? autoFocus;
- @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 => EditorStyleCustomizer(
- context: context,
- );
- 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);
- }
- }
|