editor_page.dart 15 KB

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