richTextInput.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { Editor, EditorContent, Extension, useEditor } from '@tiptap/react';
  2. import React, { useCallback, useEffect, useMemo, useRef } from 'react';
  3. import Document from '@tiptap/extension-document';
  4. import Text from '@tiptap/extension-text';
  5. import { UndoRedo } from '@tiptap/extensions';
  6. import Paragraph from '@tiptap/extension-paragraph';
  7. import HardBreak from '@tiptap/extension-hard-break';
  8. import { Placeholder } from '@tiptap/extensions';
  9. import InputSlot from './extension/inputSlot';
  10. import SelectSlot from './extension/selectSlot';
  11. import SkillSlot from './extension/skillSlot';
  12. import { strings } from '@douyinfe/semi-foundation/aiChatInput/constants';
  13. import { Content as TiptapContent } from "@tiptap/core";
  14. import { cssClasses } from '@douyinfe/semi-foundation/aiChatInput/constants';
  15. import { EditorView } from '@tiptap/pm/view';
  16. import { handleCompositionEndLogic, handlePasteLogic, handleTextInputLogic, handleZeroWidthCharLogic } from './extension/plugins';
  17. import SemiStatusExtension from './extension/statusExtension';
  18. const PREFIX = cssClasses.PREFIX;
  19. export default (props: {
  20. innerRef?: React.Ref<HTMLDivElement>;
  21. defaultContent?: TiptapContent;
  22. placeholder?: string;
  23. setEditor?: (editor: Editor) => void;
  24. onKeyDown?: (e: KeyboardEvent) => void;
  25. onChange?: (content: string) => void;
  26. extensions?: Extension[];
  27. handleKeyDown?: (view: any, event: KeyboardEvent) => boolean;
  28. onPaste?: (files: File[]) => void;
  29. onFocus?: (event: FocusEvent) => void;
  30. onBlur?: (event: FocusEvent) => void;
  31. handleCreate?: () => void
  32. }) => {
  33. const { setEditor, onKeyDown, onChange, placeholder, extensions = [],
  34. defaultContent, onPaste, innerRef, handleKeyDown, onFocus, onBlur, handleCreate } = props;
  35. const isComposing = useRef(false);
  36. const handleCompositionStart = useCallback((view: EditorView) => {
  37. isComposing.current = true;
  38. }, []);
  39. const handleCompositionEnd = useCallback((view: EditorView) => {
  40. isComposing.current = false;
  41. handleCompositionEndLogic(view);
  42. }, []);
  43. const handleTextInput = useCallback((view: EditorView, from: number, to: number, text: string) => {
  44. if (isComposing.current) {
  45. return false;
  46. }
  47. return handleTextInputLogic(view, from, to, text);
  48. }, []);
  49. const allExtensions = useMemo(() => {
  50. return [
  51. Document, Paragraph, Text, UndoRedo, HardBreak,
  52. InputSlot, SelectSlot, SkillSlot,
  53. Placeholder.configure({
  54. placeholder: placeholder,
  55. }),
  56. SemiStatusExtension,
  57. ...extensions,
  58. ];
  59. }, [extensions, placeholder]);
  60. const editorProps = useMemo(() => {
  61. return {
  62. handleKeyDown: handleKeyDown,
  63. handlePaste: handlePasteLogic,
  64. handleTextInput,
  65. handleDOMEvents: {
  66. compositionstart: handleCompositionStart,
  67. compositionend: handleCompositionEnd,
  68. }
  69. };
  70. }, [handleKeyDown, handleTextInput, handleCompositionStart, handleCompositionEnd]);
  71. // const onSelectionUpdate = useCallback(({ editor }) => {
  72. // // For debug
  73. // const fromPos = editor.state.selection.from;
  74. // const { $from } = editor.state.selection;
  75. // console.log('光标/选区位置', fromPos, editor.state.selection, editor.state.doc);
  76. // // console.log('before', $from.nodeBefore, $from.nodeAfter);
  77. // }, []);
  78. const onCreate = useCallback(({ editor }) => {
  79. const { state, view } = editor;
  80. const tr = handleZeroWidthCharLogic(state);
  81. if (tr) {
  82. // 一次性触发,避免多次触发导致 appendTransaction 被多次调用
  83. view.dispatch(tr);
  84. }
  85. handleCreate();
  86. }, [handleCreate]);
  87. const onUpdate = useCallback(({ editor }) => {
  88. // The content has changed.
  89. const content = editor.getText();
  90. onChange(content);
  91. }, [onChange]);
  92. const handlePaste = useCallback((e) => {
  93. // To support file paste
  94. const items = e.clipboardData?.items as any;
  95. let files = [];
  96. if (items) {
  97. for (const it of items) {
  98. const file = it.getAsFile();
  99. file && files.push(it.getAsFile());
  100. }
  101. }
  102. if (files.length) {
  103. onPaste?.(files);
  104. }
  105. }, [onPaste]);
  106. const editor = useEditor({
  107. extensions: allExtensions as Extension[],
  108. content: defaultContent ?? ``,
  109. editorProps: editorProps,
  110. // onSelectionUpdate,
  111. onCreate,
  112. onUpdate,
  113. onPaste: handlePaste,
  114. });
  115. useEffect(() => {
  116. setEditor(editor);
  117. }, [editor, setEditor]);
  118. return (<>
  119. <EditorContent
  120. editor={editor}
  121. onKeyDown={onKeyDown as any}
  122. onFocus={onFocus as any}
  123. onBlur={onBlur as any}
  124. ref={innerRef}
  125. className={`${PREFIX}-editor-content`}
  126. />
  127. </>);
  128. };