editor_page.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. import 'package:appflowy/plugins/document/application/doc_bloc.dart';
  2. import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';
  3. import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
  4. import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
  5. import 'package:appflowy/plugins/document/presentation/editor_style.dart';
  6. import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';
  7. import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';
  8. import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
  9. import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
  10. import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
  11. import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
  12. import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
  13. import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
  14. import 'package:appflowy_editor/appflowy_editor.dart';
  15. import 'package:collection/collection.dart';
  16. import 'package:flowy_infra/theme_extension.dart';
  17. import 'package:flowy_infra_ui/flowy_infra_ui.dart';
  18. import 'package:flutter/material.dart';
  19. import 'package:flutter_bloc/flutter_bloc.dart';
  20. final List<CommandShortcutEvent> commandShortcutEvents = [
  21. toggleToggleListCommand,
  22. ...codeBlockCommands,
  23. customCopyCommand,
  24. customPasteCommand,
  25. customCutCommand,
  26. ...standardCommandShortcutEvents,
  27. ];
  28. final List<CommandShortcutEvent> defaultCommandShortcutEvents = [
  29. ...commandShortcutEvents.map((e) => e.copyWith()).toList(),
  30. ];
  31. /// Wrapper for the appflowy editor.
  32. class AppFlowyEditorPage extends StatefulWidget {
  33. const AppFlowyEditorPage({
  34. super.key,
  35. required this.editorState,
  36. this.header,
  37. this.shrinkWrap = false,
  38. this.scrollController,
  39. this.autoFocus,
  40. required this.styleCustomizer,
  41. });
  42. final Widget? header;
  43. final EditorState editorState;
  44. final ScrollController? scrollController;
  45. final bool shrinkWrap;
  46. final bool? autoFocus;
  47. final EditorStyleCustomizer styleCustomizer;
  48. @override
  49. State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
  50. }
  51. class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
  52. late final ScrollController effectiveScrollController;
  53. late final InlineActionsService inlineActionsService = InlineActionsService(
  54. context: context,
  55. handlers: [
  56. InlinePageReferenceService(
  57. currentViewId: documentBloc.view.id,
  58. ).inlinePageReferenceDelegate,
  59. DateReferenceService(context).dateReferenceDelegate,
  60. ReminderReferenceService(context).reminderReferenceDelegate,
  61. ],
  62. );
  63. late final List<CommandShortcutEvent> commandShortcutEvents = [
  64. toggleToggleListCommand,
  65. ...codeBlockCommands,
  66. customCopyCommand,
  67. customPasteCommand,
  68. customCutCommand,
  69. ...standardCommandShortcutEvents,
  70. ..._buildFindAndReplaceCommands(),
  71. ];
  72. final List<ToolbarItem> toolbarItems = [
  73. smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
  74. paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
  75. ...(headingItems
  76. ..forEach(
  77. (e) => e.isActive = onlyShowInSingleSelectionAndTextType,
  78. )),
  79. ...markdownFormatItems,
  80. quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
  81. bulletedListItem
  82. ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
  83. numberedListItem
  84. ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
  85. inlineMathEquationItem,
  86. linkItem,
  87. alignToolbarItem,
  88. buildTextColorItem(),
  89. buildHighlightColorItem(),
  90. customizeFontToolbarItem,
  91. ];
  92. late final List<SelectionMenuItem> slashMenuItems;
  93. late final Map<String, BlockComponentBuilder> blockComponentBuilders =
  94. _customAppFlowyBlockComponentBuilders();
  95. List<CharacterShortcutEvent> get characterShortcutEvents => [
  96. // code block
  97. ...codeBlockCharacterEvents,
  98. // toggle list
  99. formatGreaterToToggleList,
  100. insertChildNodeInsideToggleList,
  101. // customize the slash menu command
  102. customSlashCommand(
  103. slashMenuItems,
  104. style: styleCustomizer.selectionMenuStyleBuilder(),
  105. ),
  106. ...standardCharacterShortcutEvents
  107. ..removeWhere(
  108. (element) => element == slashCommand,
  109. ), // remove the default slash command.
  110. /// Inline Actions
  111. /// - Reminder
  112. /// - Inline-page reference
  113. inlineActionsCommand(
  114. inlineActionsService,
  115. style: styleCustomizer.inlineActionsMenuStyleBuilder(),
  116. ),
  117. ];
  118. late final showSlashMenu = customSlashCommand(
  119. slashMenuItems,
  120. shouldInsertSlash: false,
  121. style: styleCustomizer.selectionMenuStyleBuilder(),
  122. ).handler;
  123. EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer;
  124. DocumentBloc get documentBloc => context.read<DocumentBloc>();
  125. @override
  126. void initState() {
  127. super.initState();
  128. _initializeShortcuts();
  129. indentableBlockTypes.add(ToggleListBlockKeys.type);
  130. convertibleBlockTypes.add(ToggleListBlockKeys.type);
  131. slashMenuItems = _customSlashMenuItems();
  132. effectiveScrollController = widget.scrollController ?? ScrollController();
  133. // keep the previous font style when typing new text.
  134. supportSlashMenuNodeWhiteList.addAll([
  135. ToggleListBlockKeys.type,
  136. ]);
  137. toolbarItemWhiteList.addAll([
  138. ToggleListBlockKeys.type,
  139. CalloutBlockKeys.type,
  140. TableBlockKeys.type,
  141. ]);
  142. AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily);
  143. // customize the dynamic theme color
  144. _customizeBlockComponentBackgroundColorDecorator();
  145. }
  146. @override
  147. void dispose() {
  148. if (widget.scrollController == null) {
  149. effectiveScrollController.dispose();
  150. }
  151. inlineActionsService.dispose();
  152. widget.editorState.dispose();
  153. super.dispose();
  154. }
  155. @override
  156. Widget build(BuildContext context) {
  157. final (bool autoFocus, Selection? selection) =
  158. _computeAutoFocusParameters();
  159. final isRTL =
  160. context.read<AppearanceSettingsCubit>().state.layoutDirection ==
  161. LayoutDirection.rtlLayout;
  162. final textDirection = isRTL ? TextDirection.rtl : TextDirection.ltr;
  163. _setRTLToolbarItems(isRTL);
  164. final editorScrollController = EditorScrollController(
  165. editorState: widget.editorState,
  166. shrinkWrap: widget.shrinkWrap,
  167. scrollController: effectiveScrollController,
  168. );
  169. final editor = AppFlowyEditor(
  170. editorState: widget.editorState,
  171. editable: true,
  172. editorScrollController: editorScrollController,
  173. // setup the auto focus parameters
  174. autoFocus: widget.autoFocus ?? autoFocus,
  175. focusedSelection: selection,
  176. // setup the theme
  177. editorStyle: styleCustomizer.style(),
  178. // customize the block builders
  179. blockComponentBuilders: blockComponentBuilders,
  180. // customize the shortcuts
  181. characterShortcutEvents: characterShortcutEvents,
  182. commandShortcutEvents: commandShortcutEvents,
  183. // customize the context menu items
  184. contextMenuItems: customContextMenuItems,
  185. // customize the header and footer.
  186. header: widget.header,
  187. footer: const VSpace(200),
  188. );
  189. return Center(
  190. child: FloatingToolbar(
  191. style: styleCustomizer.floatingToolbarStyleBuilder(),
  192. items: toolbarItems,
  193. editorState: widget.editorState,
  194. editorScrollController: editorScrollController,
  195. textDirection: textDirection,
  196. child: Directionality(
  197. textDirection: textDirection,
  198. child: editor,
  199. ),
  200. ),
  201. );
  202. }
  203. Map<String, BlockComponentBuilder> _customAppFlowyBlockComponentBuilders() {
  204. final standardActions = [
  205. OptionAction.delete,
  206. OptionAction.duplicate,
  207. // OptionAction.divider,
  208. // OptionAction.moveUp,
  209. // OptionAction.moveDown,
  210. ];
  211. final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
  212. final configuration = BlockComponentConfiguration(
  213. padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
  214. );
  215. final customBlockComponentBuilderMap = {
  216. PageBlockKeys.type: PageBlockComponentBuilder(),
  217. ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
  218. configuration: configuration,
  219. ),
  220. TodoListBlockKeys.type: TodoListBlockComponentBuilder(
  221. configuration: configuration.copyWith(
  222. placeholderText: (_) => 'To-do',
  223. ),
  224. ),
  225. BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
  226. configuration: configuration.copyWith(
  227. placeholderText: (_) => 'List',
  228. ),
  229. ),
  230. NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
  231. configuration: configuration.copyWith(
  232. placeholderText: (_) => 'List',
  233. ),
  234. ),
  235. QuoteBlockKeys.type: QuoteBlockComponentBuilder(
  236. configuration: configuration.copyWith(
  237. placeholderText: (_) => 'Quote',
  238. ),
  239. ),
  240. HeadingBlockKeys.type: HeadingBlockComponentBuilder(
  241. configuration: configuration.copyWith(
  242. padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
  243. placeholderText: (node) =>
  244. 'Heading ${node.attributes[HeadingBlockKeys.level]}',
  245. ),
  246. textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
  247. ),
  248. ImageBlockKeys.type: CustomImageBlockComponentBuilder(
  249. configuration: configuration,
  250. showMenu: true,
  251. menuBuilder: (Node node, CustomImageBlockComponentState state) =>
  252. Positioned(
  253. top: 0,
  254. right: 10,
  255. child: ImageMenu(
  256. node: node,
  257. state: state,
  258. ),
  259. ),
  260. ),
  261. TableBlockKeys.type: TableBlockComponentBuilder(
  262. menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
  263. TableMenu(
  264. node: node,
  265. editorState: editorState,
  266. position: position,
  267. dir: dir,
  268. onBuild: onBuild,
  269. onClose: onClose,
  270. ),
  271. ),
  272. TableCellBlockKeys.type: TableCellBlockComponentBuilder(
  273. menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
  274. TableMenu(
  275. node: node,
  276. editorState: editorState,
  277. position: position,
  278. dir: dir,
  279. onBuild: onBuild,
  280. onClose: onClose,
  281. ),
  282. ),
  283. DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
  284. configuration: configuration.copyWith(
  285. padding: (_) => const EdgeInsets.symmetric(vertical: 10),
  286. ),
  287. ),
  288. DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
  289. configuration: configuration.copyWith(
  290. padding: (_) => const EdgeInsets.symmetric(vertical: 10),
  291. ),
  292. ),
  293. DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
  294. configuration: configuration.copyWith(
  295. padding: (_) => const EdgeInsets.symmetric(vertical: 10),
  296. ),
  297. ),
  298. CalloutBlockKeys.type: CalloutBlockComponentBuilder(
  299. configuration: configuration,
  300. defaultColor: calloutBGColor,
  301. ),
  302. DividerBlockKeys.type: DividerBlockComponentBuilder(
  303. configuration: configuration,
  304. height: 28.0,
  305. ),
  306. MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
  307. configuration: configuration.copyWith(
  308. padding: (_) => const EdgeInsets.symmetric(vertical: 20),
  309. ),
  310. ),
  311. CodeBlockKeys.type: CodeBlockComponentBuilder(
  312. configuration: configuration.copyWith(
  313. textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
  314. placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
  315. ),
  316. padding: const EdgeInsets.only(
  317. left: 30,
  318. right: 30,
  319. bottom: 36,
  320. ),
  321. ),
  322. AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
  323. SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
  324. ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
  325. configuration: configuration,
  326. ),
  327. OutlineBlockKeys.type: OutlineBlockComponentBuilder(
  328. configuration: configuration.copyWith(
  329. placeholderTextStyle: (_) =>
  330. styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
  331. ),
  332. ),
  333. errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
  334. configuration: configuration.copyWith(
  335. padding: (_) => const EdgeInsets.symmetric(vertical: 10),
  336. ),
  337. ),
  338. };
  339. final builders = {
  340. ...standardBlockComponentBuilderMap,
  341. ...customBlockComponentBuilderMap,
  342. };
  343. // customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
  344. for (final entry in builders.entries) {
  345. if (entry.key == PageBlockKeys.type) {
  346. continue;
  347. }
  348. final builder = entry.value;
  349. // customize the action builder.
  350. final supportColorBuilderTypes = [
  351. ParagraphBlockKeys.type,
  352. HeadingBlockKeys.type,
  353. BulletedListBlockKeys.type,
  354. NumberedListBlockKeys.type,
  355. QuoteBlockKeys.type,
  356. TodoListBlockKeys.type,
  357. CalloutBlockKeys.type,
  358. OutlineBlockKeys.type,
  359. ToggleListBlockKeys.type,
  360. ];
  361. final supportAlignBuilderType = [
  362. ImageBlockKeys.type,
  363. ];
  364. final colorAction = [
  365. OptionAction.divider,
  366. OptionAction.color,
  367. ];
  368. final alignAction = [
  369. OptionAction.divider,
  370. OptionAction.align,
  371. ];
  372. final List<OptionAction> actions = [
  373. ...standardActions,
  374. if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
  375. if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
  376. ];
  377. builder.showActions =
  378. (node) => node.parent?.type != TableCellBlockKeys.type;
  379. builder.actionBuilder = (context, state) {
  380. final top = builder.configuration.padding(context.node).top;
  381. final padding = context.node.type == HeadingBlockKeys.type
  382. ? EdgeInsets.only(top: top + 8.0)
  383. : EdgeInsets.only(top: top + 2.0);
  384. return Padding(
  385. padding: padding,
  386. child: BlockActionList(
  387. blockComponentContext: context,
  388. blockComponentState: state,
  389. editorState: widget.editorState,
  390. actions: actions,
  391. showSlashMenu: () => showSlashMenu(widget.editorState),
  392. ),
  393. );
  394. };
  395. }
  396. return builders;
  397. }
  398. List<SelectionMenuItem> _customSlashMenuItems() {
  399. final items = [...standardSelectionMenuItems];
  400. final imageItem = items.firstWhereOrNull(
  401. (element) => element.name == AppFlowyEditorLocalizations.current.image,
  402. );
  403. if (imageItem != null) {
  404. final imageItemIndex = items.indexOf(imageItem);
  405. if (imageItemIndex != -1) {
  406. items[imageItemIndex] = customImageMenuItem;
  407. }
  408. }
  409. return [
  410. ...items,
  411. inlineGridMenuItem(documentBloc),
  412. referencedGridMenuItem,
  413. inlineBoardMenuItem(documentBloc),
  414. referencedBoardMenuItem,
  415. inlineCalendarMenuItem(documentBloc),
  416. referencedCalendarMenuItem,
  417. calloutItem,
  418. outlineItem,
  419. mathEquationItem,
  420. codeBlockItem,
  421. toggleListBlockItem,
  422. emojiMenuItem,
  423. autoGeneratorMenuItem,
  424. ];
  425. }
  426. (bool, Selection?) _computeAutoFocusParameters() {
  427. if (widget.editorState.document.isEmpty) {
  428. return (
  429. true,
  430. Selection.collapsed(
  431. Position(path: [0], offset: 0),
  432. ),
  433. );
  434. }
  435. final nodes = widget.editorState.document.root.children
  436. .where((element) => element.delta != null);
  437. final isAllEmpty =
  438. nodes.isNotEmpty && nodes.every((element) => element.delta!.isEmpty);
  439. if (isAllEmpty) {
  440. return (
  441. true,
  442. Selection.collapsed(
  443. Position(path: nodes.first.path, offset: 0),
  444. )
  445. );
  446. }
  447. return const (false, null);
  448. }
  449. Future<void> _initializeShortcuts() async {
  450. // TODO(Xazin): Refactor lazy initialization
  451. defaultCommandShortcutEvents;
  452. final settingsShortcutService = SettingsShortcutService();
  453. final customizeShortcuts =
  454. await settingsShortcutService.getCustomizeShortcuts();
  455. await settingsShortcutService.updateCommandShortcuts(
  456. commandShortcutEvents,
  457. customizeShortcuts,
  458. );
  459. }
  460. void _setRTLToolbarItems(bool isRTL) {
  461. final textDirectionItemIds = textDirectionItems.map((e) => e.id);
  462. // clear all the text direction items
  463. toolbarItems.removeWhere(
  464. (item) => textDirectionItemIds.contains(item.id),
  465. );
  466. // only show the rtl item when the layout direction is ltr.
  467. if (isRTL) {
  468. toolbarItems.addAll(textDirectionItems);
  469. }
  470. }
  471. List<CommandShortcutEvent> _buildFindAndReplaceCommands() {
  472. return findAndReplaceCommands(
  473. context: context,
  474. style: FindReplaceStyle(
  475. findMenuBuilder: (
  476. context,
  477. editorState,
  478. localizations,
  479. style,
  480. showReplaceMenu,
  481. onDismiss,
  482. ) {
  483. return Material(
  484. child: DecoratedBox(
  485. decoration: BoxDecoration(
  486. color: Theme.of(context).colorScheme.surfaceVariant,
  487. borderRadius: BorderRadius.circular(4),
  488. ),
  489. child: FindAndReplaceMenuWidget(
  490. editorState: editorState,
  491. onDismiss: onDismiss,
  492. ),
  493. ),
  494. );
  495. },
  496. ),
  497. );
  498. }
  499. void _customizeBlockComponentBackgroundColorDecorator() {
  500. blockComponentBackgroundColorDecorator = (Node node, String colorString) {
  501. // the color string is from FlowyTint.
  502. final tintColor = FlowyTint.values.firstWhereOrNull(
  503. (e) => e.id == colorString,
  504. );
  505. if (tintColor != null) {
  506. return tintColor.color(context);
  507. }
  508. final themeColor = themeBackgroundColors[colorString];
  509. if (themeColor != null) {
  510. return themeColor.color(context);
  511. }
  512. if (colorString == optionActionColorDefaultColor) {
  513. final defaultColor = node.type == CalloutBlockKeys.type
  514. ? AFThemeExtension.of(context).calloutBGColor
  515. : Colors.transparent;
  516. return defaultColor;
  517. }
  518. if (colorString == tableCellDefaultColor) {
  519. return AFThemeExtension.of(context).tableCellBGColor;
  520. }
  521. return null;
  522. };
  523. }
  524. }