diff --git a/demos/src/Examples/Default/React/index.jsx b/demos/src/Examples/Default/React/index.jsx index 00c9f575486..18d22551932 100644 --- a/demos/src/Examples/Default/React/index.jsx +++ b/demos/src/Examples/Default/React/index.jsx @@ -228,6 +228,6 @@ display: none; export default () => { return ( - } extensions={extensions} content={content}> + } extensions={extensions} content={content} useImmediateRender> ) } diff --git a/docs/api/utilities.md b/docs/api/utilities.md new file mode 100644 index 00000000000..d4d98288239 --- /dev/null +++ b/docs/api/utilities.md @@ -0,0 +1,11 @@ +# Editor API Utility Overview + +Welcome to the Editor API Utility section. Here, you'll discover essential tools to enhance your Tiptap experience: + +- **Render JSON as HTML**: Learn to convert JSON content to HTML, even without an editor instance, simplifying content management. + +- **Tiptap for PHP**: Explore PHP integration for Tiptap, enabling seamless content transformation and modification. + +- **Suggestions**: Enhance your editor with suggestions like mentions and emojis, tailored to your needs. + +Explore subpages for in-depth guidance and examples. diff --git a/docs/links.yaml b/docs/links.yaml index 947f61b4bef..68b97a7996b 100644 --- a/docs/links.yaml +++ b/docs/links.yaml @@ -333,8 +333,7 @@ link: /api/extensions/unique-id type: pro - title: Utilities - link: /utilities - redirect: /api/utilities/html + link: /api/utilities items: - title: HTML link: /api/utilities/html diff --git a/packages/react/src/Context.tsx b/packages/react/src/Context.tsx index 7a47ed354f6..b15ed9bbdf9 100644 --- a/packages/react/src/Context.tsx +++ b/packages/react/src/Context.tsx @@ -3,7 +3,7 @@ import React, { createContext, ReactNode, useContext } from 'react' import { Editor } from './Editor.js' import { EditorContent } from './EditorContent.js' -import { useEditor } from './useEditor.js' +import { useEditor, useEditorForImmediateRender } from './useEditor.js' export type EditorContextValue = { editor: Editor | null; @@ -19,18 +19,38 @@ export const useCurrentEditor = () => useContext(EditorContext) export type EditorProviderProps = { children: ReactNode; + /** + * This option will create and immediately return a defined editor instance. The editor returned in the context consumer will never be null if + * this is enabled. In future major versions, this property will be removed and this behavior will be the defualt. + */ + useImmediateRender?: boolean; slotBefore?: ReactNode; slotAfter?: ReactNode; } & Partial -export const EditorProvider = ({ +const EditorProviderNoImmediateRender = ({ children, slotAfter, slotBefore, ...editorOptions -}: EditorProviderProps) => { +}: Omit) => { const editor = useEditor(editorOptions) - if (!editor) { - return null - } + return ( + + {slotBefore} + + {({ editor: currentEditor }) => ( + + )} + + {children} + {slotAfter} + + ) +} + +const EditorProviderImmediateRender = ({ + children, slotAfter, slotBefore, ...editorOptions +}: Omit) => { + const editor = useEditorForImmediateRender(editorOptions) return ( @@ -45,3 +65,15 @@ export const EditorProvider = ({ ) } + +export const EditorProvider = ({ useImmediateRender, ...providerOptions }: EditorProviderProps) => { + if (useImmediateRender) { + return ( + + ) + } + + return ( + + ) +} diff --git a/packages/react/src/useEditor.ts b/packages/react/src/useEditor.ts index e80ca4a49a3..6fa8e110d6e 100644 --- a/packages/react/src/useEditor.ts +++ b/packages/react/src/useEditor.ts @@ -2,6 +2,7 @@ import { EditorOptions } from '@tiptap/core' import { DependencyList, useEffect, + useMemo, useRef, useState, } from 'react' @@ -113,14 +114,130 @@ export const useEditor = (options: Partial = {}, deps: Dependency return () => { isMounted = false + editorRef.current?.destroy() } }, deps) + return editorRef.current +} + +/** + * This hook will create and immediately return a defined editor instance. In future major versions, this + * hook will be removed and useEditor will be changed to behave like this hook. + */ +export const useEditorForImmediateRender = (options: Partial = {}, deps: DependencyList = []) => { + const { + onBeforeCreate, + onBlur, + onCreate, + onDestroy, + onFocus, + onSelectionUpdate, + onTransaction, + onUpdate, + } = options + + const onBeforeCreateRef = useRef(onBeforeCreate) + const onBlurRef = useRef(onBlur) + const onCreateRef = useRef(onCreate) + const onDestroyRef = useRef(onDestroy) + const onFocusRef = useRef(onFocus) + const onSelectionUpdateRef = useRef(onSelectionUpdate) + const onTransactionRef = useRef(onTransaction) + const onUpdateRef = useRef(onUpdate) + + const isMounted = useRef(false) + useEffect(() => { + isMounted.current = true + return () => { - return editorRef.current?.destroy() + isMounted.current = false } }, []) - return editorRef.current + const [, forceUpdate] = useState({}) + const editor = useMemo(() => { + const instance = new Editor(options) + + instance.on('transaction', () => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (isMounted.current) { + forceUpdate({}) + } + }) + }) + }) + + return instance + }, deps) + + useEffect(() => { + return () => { + editor.destroy() + } + }, [editor]) + + // This effect will handle updating the editor instance + // when the event handlers change. + useEffect(() => { + if (onBeforeCreate) { + editor.off('beforeCreate', onBeforeCreateRef.current) + editor.on('beforeCreate', onBeforeCreate) + + onBeforeCreateRef.current = onBeforeCreate + } + + if (onBlur) { + editor.off('blur', onBlurRef.current) + editor.on('blur', onBlur) + + onBlurRef.current = onBlur + } + + if (onCreate) { + editor.off('create', onCreateRef.current) + editor.on('create', onCreate) + + onCreateRef.current = onCreate + } + + if (onDestroy) { + editor.off('destroy', onDestroyRef.current) + editor.on('destroy', onDestroy) + + onDestroyRef.current = onDestroy + } + + if (onFocus) { + editor.off('focus', onFocusRef.current) + editor.on('focus', onFocus) + + onFocusRef.current = onFocus + } + + if (onSelectionUpdate) { + editor.off('selectionUpdate', onSelectionUpdateRef.current) + editor.on('selectionUpdate', onSelectionUpdate) + + onSelectionUpdateRef.current = onSelectionUpdate + } + + if (onTransaction) { + editor.off('transaction', onTransactionRef.current) + editor.on('transaction', onTransaction) + + onTransactionRef.current = onTransaction + } + + if (onUpdate) { + editor.off('update', onUpdateRef.current) + editor.on('update', onUpdate) + + onUpdateRef.current = onUpdate + } + }, [onBeforeCreate, onBlur, onCreate, onDestroy, onFocus, onSelectionUpdate, onTransaction, onUpdate, editor]) + + return editor }