TextBlock.hooks.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey';
  2. import { useCallback, useContext, useState } from 'react';
  3. import { Descendant, Range, Editor, Element, Text, Location } from 'slate';
  4. import { TextDelta } from '$app/interfaces/document';
  5. import { useTextInput } from '../_shared/TextInput.hooks';
  6. import { useAppDispatch } from '@/appflowy_app/stores/store';
  7. import { DocumentControllerContext } from '@/appflowy_app/stores/effects/document/document_controller';
  8. import {
  9. backspaceNodeThunk,
  10. indentNodeThunk,
  11. splitNodeThunk,
  12. } from '@/appflowy_app/stores/reducers/document/async_actions';
  13. import { TextSelection } from '@/appflowy_app/stores/reducers/document/slice';
  14. export function useTextBlock(id: string, delta: TextDelta[]) {
  15. const { editor, onSelectionChange } = useTextInput(id, delta);
  16. const [value, setValue] = useState<Descendant[]>([]);
  17. const { onTab, onBackSpace, onEnter } = useActions(id);
  18. const onChange = useCallback(
  19. (e: Descendant[]) => {
  20. setValue(e);
  21. editor.operations.forEach((op) => {
  22. if (op.type === 'set_selection') {
  23. onSelectionChange(op.newProperties as TextSelection);
  24. }
  25. });
  26. },
  27. [editor]
  28. );
  29. const onKeyDownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
  30. switch (event.key) {
  31. case 'Enter': {
  32. if (!editor.selection) return;
  33. event.stopPropagation();
  34. event.preventDefault();
  35. const retainRange = getRetainRangeBy(editor);
  36. const retain = getDelta(editor, retainRange);
  37. const insertRange = getInsertRangeBy(editor);
  38. const insert = getDelta(editor, insertRange);
  39. void (async () => {
  40. await onEnter(retain, insert);
  41. })();
  42. return;
  43. }
  44. case 'Backspace': {
  45. if (!editor.selection) return;
  46. const { anchor } = editor.selection;
  47. const isCollapsed = Range.isCollapsed(editor.selection);
  48. if (isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0') {
  49. event.stopPropagation();
  50. event.preventDefault();
  51. void (async () => {
  52. await onBackSpace();
  53. })();
  54. }
  55. return;
  56. }
  57. case 'Tab': {
  58. event.stopPropagation();
  59. event.preventDefault();
  60. void (async () => {
  61. await onTab();
  62. })();
  63. return;
  64. }
  65. }
  66. triggerHotkey(event, editor);
  67. };
  68. const onDOMBeforeInput = useCallback((e: InputEvent) => {
  69. // COMPAT: in Apple, `compositionend` is dispatched after the `beforeinput` for "insertFromComposition".
  70. // It will cause repeated characters when inputting Chinese.
  71. // Here, prevent the beforeInput event and wait for the compositionend event to take effect.
  72. if (e.inputType === 'insertFromComposition') {
  73. e.preventDefault();
  74. }
  75. }, []);
  76. return {
  77. onChange,
  78. onKeyDownCapture,
  79. onDOMBeforeInput,
  80. editor,
  81. value,
  82. };
  83. }
  84. function useActions(id: string) {
  85. const dispatch = useAppDispatch();
  86. const controller = useContext(DocumentControllerContext);
  87. const onTab = useCallback(async () => {
  88. if (!controller) return;
  89. await dispatch(
  90. indentNodeThunk({
  91. id,
  92. controller,
  93. })
  94. );
  95. }, [id, controller]);
  96. const onBackSpace = useCallback(async () => {
  97. if (!controller) return;
  98. await dispatch(backspaceNodeThunk({ id, controller }));
  99. }, [controller, id]);
  100. const onEnter = useCallback(
  101. async (retain: TextDelta[], insert: TextDelta[]) => {
  102. if (!controller) return;
  103. await dispatch(splitNodeThunk({ id, retain, insert, controller }));
  104. },
  105. [controller, id]
  106. );
  107. return {
  108. onTab,
  109. onBackSpace,
  110. onEnter,
  111. };
  112. }
  113. function getDelta(editor: Editor, at: Location): TextDelta[] {
  114. const baseElement = Editor.fragment(editor, at)[0] as Element;
  115. return baseElement.children.map((item) => {
  116. const { text, ...attributes } = item as Text;
  117. return {
  118. insert: text,
  119. attributes,
  120. };
  121. });
  122. }
  123. function getRetainRangeBy(editor: Editor) {
  124. const start = Editor.start(editor, editor.selection!);
  125. return {
  126. anchor: { path: [0, 0], offset: 0 },
  127. focus: start,
  128. };
  129. }
  130. function getInsertRangeBy(editor: Editor) {
  131. const end = Editor.end(editor, editor.selection!);
  132. const fragment = (editor.children[0] as Element).children;
  133. return {
  134. anchor: end,
  135. focus: { path: [0, fragment.length - 1], offset: (fragment[fragment.length - 1] as Text).text.length },
  136. };
  137. }