editor_page.dart 17 KB

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