import { useCallback, useRef, useState } from 'react' import { type EditorState } from 'prosemirror-state' import { isRefObject } from '~components/utils/isRefObject' import { createRichTextEditor } from '../createRichTextEditor' import { type CommandOrTransaction } from '../types' type RTEOptions = { editable?: boolean inputRef?: React.Ref } type SetEditableStatus = (status: boolean) => void type UseRichTextEditorReturnValue = [ React.RefCallback, EditorState, (commandOrTransaction: CommandOrTransaction) => void, SetEditableStatus, ] /** * useRichTextEditor * React hook to initialize a ProseMirror editor, handle binding it to the DOM, * and updating the state within React’s lifecycle. * * @param {initialEditorState} ProseMirror state * @returns {Array} */ export const useRichTextEditor = ( initialEditorState: EditorState, /* * Pass in HTML attributes into the parent RTE node */ attributes?: Record, { editable = true, inputRef }: RTEOptions = {}, ): UseRichTextEditorReturnValue => { const [editorState, setEditorState] = useState(initialEditorState) // Refs to hold the methods returned from ProseMirror’s initialization const destroyEditorRef = useRef<() => void>() const dispatchTransactionRef = useRef<(commandOrTransaction: CommandOrTransaction) => void>( () => undefined, ) // Construct a consistent reference to call the dispatchTransactionRef without // forcing the consumer to unwind it const dispatchTransaction = useCallback( (commandOrTransaction: CommandOrTransaction) => { dispatchTransactionRef.current(commandOrTransaction) }, [dispatchTransactionRef], ) // Hold editableStatus as a ref so we can toggle its status const editableStatusRef = useRef(editable) // Stable ref to avoid recreating editorRef when inputRef callback identity changes const inputRefRef = useRef(inputRef) inputRefRef.current = inputRef const setEditableStatus = useCallback( (status) => { editableStatusRef.current = status // Trigger an update within ProseMirror by issuing a noop transaction dispatchTransaction((state, dispatch) => { if (!dispatch) return false dispatch(state.tr) return true }) }, // @todo: Fix if possible - avoiding breaking in eslint upgrade // eslint-disable-next-line react-hooks/exhaustive-deps [editableStatusRef], ) const editorRef = useCallback( (node: HTMLElement) => { if (node === null) { if (destroyEditorRef.current) { destroyEditorRef.current() destroyEditorRef.current = undefined } if (inputRefRef.current && isRefObject(inputRefRef.current)) { ;(inputRefRef.current as React.MutableRefObject).current = null } else { inputRefRef.current?.(null) } return } const instance = createRichTextEditor({ node, initialEditorState: editorState, onChange: setEditorState, isEditable: () => editableStatusRef.current, attributes, }) destroyEditorRef.current = instance.destroy dispatchTransactionRef.current = instance.dispatchTransaction if (inputRefRef.current && isRefObject(inputRefRef.current)) { ;(inputRefRef.current as React.MutableRefObject).current = instance.dom } else { inputRefRef.current?.(instance.dom) } }, // Including editorState in the dependencies here will cause an endless // loop as the initialization changes its value // @todo: Fix if possible - avoiding breaking in eslint upgrade // eslint-disable-next-line react-hooks/exhaustive-deps [setEditorState, editableStatusRef], ) return [editorRef, editorState, dispatchTransaction, setEditableStatus] }