useTurnIntoBlockEvents.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import { useCallback, useMemo } from 'react';
  2. import { BlockType } from '$app/interfaces/document';
  3. import { useAppDispatch } from '$app/stores/store';
  4. import { turnToBlockThunk } from '$app_reducers/document/async-actions';
  5. import { blockConfig } from '$app/constants/document/config';
  6. import Delta from 'quill-delta';
  7. import { useRangeRef } from '$app/components/document/_shared/SubscribeSelection.hooks';
  8. import { getBlockDelta } from '$app/components/document/_shared/SubscribeNode.hooks';
  9. import { getDeltaText } from '$app/utils/document/delta';
  10. import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
  11. import { turnIntoConfig } from './shortchut';
  12. export function useTurnIntoBlockEvents(id: string) {
  13. const { docId, controller } = useSubscribeDocument();
  14. const dispatch = useAppDispatch();
  15. const rangeRef = useRangeRef();
  16. const getFlag = useCallback(() => {
  17. const range = rangeRef.current?.caret;
  18. if (!range || range.id !== id) return;
  19. const delta = getBlockDelta(docId, id);
  20. if (!delta) return '';
  21. return getDeltaText(delta.slice(0, range.index));
  22. }, [docId, id, rangeRef]);
  23. const getDeltaContent = useCallback(() => {
  24. const range = rangeRef.current?.caret;
  25. if (!range || range.id !== id) return;
  26. const delta = getBlockDelta(docId, id);
  27. if (!delta) return '';
  28. const content = delta.slice(range.index);
  29. return new Delta(content);
  30. }, [docId, id, rangeRef]);
  31. const canHandle = useCallback(
  32. (event: React.KeyboardEvent<HTMLDivElement>, type: BlockType) => {
  33. {
  34. const triggerKey = event.key === turnIntoConfig[type].triggerKey ? event.key : undefined;
  35. if (!triggerKey) return false;
  36. const regex = turnIntoConfig[type].markdownRegexp;
  37. // This error will be thrown if the block type is not in the config, and it will happen in development environment
  38. if (!regex) {
  39. throw new Error(`canHandle: block type ${type} is not supported`);
  40. }
  41. const isTrigger = event.key === triggerKey;
  42. if (!isTrigger) {
  43. return false;
  44. }
  45. const flag = getFlag();
  46. if (!flag) return false;
  47. return regex.test(`${flag}${triggerKey}`);
  48. }
  49. },
  50. [getFlag]
  51. );
  52. const getTurnIntoBlockDelta = useCallback(() => {
  53. const content = getDeltaContent();
  54. if (!content) return;
  55. return {
  56. delta: content.ops,
  57. };
  58. }, [getDeltaContent]);
  59. const getAttrs = useCallback(
  60. (type: BlockType) => {
  61. const flag = getFlag();
  62. if (!flag) return;
  63. const triggerKey = turnIntoConfig[type].triggerKey;
  64. const regex = turnIntoConfig[type].markdownRegexp;
  65. const match = `${flag}${triggerKey}`.match(regex);
  66. return match?.[3];
  67. },
  68. [getFlag]
  69. );
  70. const spaceTriggerMap = useMemo(() => {
  71. return {
  72. [BlockType.HeadingBlock]: () => {
  73. const flag = getFlag();
  74. if (!flag) return;
  75. const level = flag.match(/#/g)?.length;
  76. if (!level || level > 3) return;
  77. return {
  78. level,
  79. ...getTurnIntoBlockDelta(),
  80. };
  81. },
  82. [BlockType.TodoListBlock]: () => {
  83. const flag = getFlag();
  84. if (!flag) return;
  85. return {
  86. checked: flag.includes('[x]'),
  87. ...getTurnIntoBlockDelta(),
  88. };
  89. },
  90. [BlockType.QuoteBlock]: getTurnIntoBlockDelta,
  91. [BlockType.BulletedListBlock]: getTurnIntoBlockDelta,
  92. [BlockType.NumberedListBlock]: getTurnIntoBlockDelta,
  93. [BlockType.ToggleListBlock]: getTurnIntoBlockDelta,
  94. [BlockType.CalloutBlock]: () => {
  95. const flag = getFlag();
  96. if (!flag) return;
  97. const tag = flag.match(/(TIP|INFO|WARNING|DANGER)/g)?.[0];
  98. if (!tag) return;
  99. const iconMap: Record<string, string> = {
  100. TIP: '💡',
  101. INFO: '❗',
  102. WARNING: '⚠️',
  103. DANGER: '‼️',
  104. };
  105. return {
  106. icon: iconMap[tag],
  107. ...getTurnIntoBlockDelta(),
  108. };
  109. },
  110. };
  111. }, [getFlag, getTurnIntoBlockDelta]);
  112. const turnIntoBlockEvents = useMemo(() => {
  113. const spaceTriggerEvents = Object.entries(spaceTriggerMap).map(([type, getData]) => {
  114. const blockType = type as BlockType;
  115. return {
  116. canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => canHandle(e, blockType),
  117. handler: (e: React.KeyboardEvent<HTMLDivElement>) => {
  118. e.preventDefault();
  119. if (!controller) return;
  120. const data = getData();
  121. if (!data) return;
  122. dispatch(turnToBlockThunk({ id, data, type: blockType, controller }));
  123. },
  124. };
  125. });
  126. return [
  127. ...spaceTriggerEvents,
  128. {
  129. canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => canHandle(e, BlockType.DividerBlock),
  130. handler: (e: React.KeyboardEvent<HTMLDivElement>) => {
  131. e.preventDefault();
  132. if (!controller) return;
  133. const delta = getDeltaContent();
  134. dispatch(
  135. turnToBlockThunk({
  136. id,
  137. controller,
  138. type: BlockType.DividerBlock,
  139. data: {},
  140. })
  141. );
  142. },
  143. },
  144. {
  145. canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => canHandle(e, BlockType.CodeBlock),
  146. handler: (e: React.KeyboardEvent<HTMLDivElement>) => {
  147. e.preventDefault();
  148. if (!controller) return;
  149. const defaultData = blockConfig[BlockType.CodeBlock].defaultData;
  150. dispatch(
  151. turnToBlockThunk({
  152. id,
  153. data: {
  154. ...defaultData,
  155. },
  156. type: BlockType.CodeBlock,
  157. controller,
  158. })
  159. );
  160. },
  161. },
  162. {
  163. canHandle: (e: React.KeyboardEvent<HTMLDivElement>) => canHandle(e, BlockType.EquationBlock),
  164. handler: (e: React.KeyboardEvent<HTMLDivElement>) => {
  165. e.preventDefault();
  166. const formula = getAttrs(BlockType.EquationBlock);
  167. const data = {
  168. formula,
  169. };
  170. dispatch(turnToBlockThunk({ id, data, type: BlockType.EquationBlock, controller }));
  171. },
  172. }
  173. ];
  174. }, [canHandle, controller, dispatch, getAttrs, getDeltaContent, id, spaceTriggerMap]);
  175. return turnIntoBlockEvents;
  176. }