@tiptap/react
Advanced tools
+355
| import type { ReactNode } from 'react' | ||
| import { createContext, useContext, useEffect, useMemo, useState } from 'react' | ||
| import { EditorContext } from './Context.js' | ||
| import type { Editor, EditorContentProps, EditorStateSnapshot } from './index.js' | ||
| import { EditorContent, useEditorState } from './index.js' | ||
| import { type BubbleMenuProps, BubbleMenu } from './menus/BubbleMenu.js' | ||
| import { type FloatingMenuProps, FloatingMenu } from './menus/FloatingMenu.js' | ||
| /** | ||
| * The shape of the React context used by the `<Tiptap />` components. | ||
| * | ||
| * This object exposes the editor instance and a simple readiness flag. | ||
| */ | ||
| export type TiptapContextType = { | ||
| /** The Tiptap editor instance. May be null during SSR or before initialization. */ | ||
| editor: Editor | null | ||
| /** True when the editor has finished initializing and is ready for user interaction. */ | ||
| isReady: boolean | ||
| } | ||
| /** | ||
| * React context that stores the current editor instance and readiness flag. | ||
| * | ||
| * Use `useTiptap()` to read from this context in child components. | ||
| */ | ||
| export const TiptapContext = createContext<TiptapContextType>({ | ||
| editor: null, | ||
| isReady: false, | ||
| }) | ||
| TiptapContext.displayName = 'TiptapContext' | ||
| /** | ||
| * Hook to read the Tiptap context (`editor` + `isReady`). | ||
| * | ||
| * This is a small convenience wrapper around `useContext(TiptapContext)`. | ||
| * | ||
| * @returns The current `TiptapContextType` value from the provider. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useTiptap } from '@tiptap/react' | ||
| * | ||
| * function Status() { | ||
| * const { isReady } = useTiptap() | ||
| * return <div>{isReady ? 'Editor ready' : 'Loading editor...'}</div> | ||
| * } | ||
| * ``` | ||
| */ | ||
| export const useTiptap = () => useContext(TiptapContext) | ||
| /** | ||
| * Select a slice of the editor state using the context-provided editor. | ||
| * | ||
| * This is a thin wrapper around `useEditorState` that reads the `editor` | ||
| * instance from `useTiptap()` so callers don't have to pass it manually. | ||
| * | ||
| * Important: This hook should only be used when the editor is available. | ||
| * Use the `isReady` flag from `useTiptap()` to guard against null editor, | ||
| * or ensure your component only renders after the editor is initialized. | ||
| * | ||
| * @typeParam TSelectorResult - The type returned by the selector. | ||
| * @param selector - Function that receives the editor state snapshot and | ||
| * returns the piece of state you want to subscribe to. | ||
| * @param equalityFn - Optional function to compare previous/next selected | ||
| * values and avoid unnecessary updates. | ||
| * @returns The selected slice of the editor state. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * function WordCount() { | ||
| * const { isReady } = useTiptap() | ||
| * | ||
| * // Only use useTiptapState when the editor is ready | ||
| * const wordCount = useTiptapState(state => { | ||
| * const text = state.editor.state.doc.textContent | ||
| * return text.split(/\s+/).filter(Boolean).length | ||
| * }) | ||
| * | ||
| * if (!isReady) return null | ||
| * | ||
| * return <span>{wordCount} words</span> | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function useTiptapState<TSelectorResult>( | ||
| selector: (context: EditorStateSnapshot<Editor>) => TSelectorResult, | ||
| equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean, | ||
| ) { | ||
| const { editor } = useTiptap() | ||
| return useEditorState({ | ||
| editor: editor as Editor, | ||
| selector, | ||
| equalityFn, | ||
| }) | ||
| } | ||
| /** | ||
| * Props for the `Tiptap` root/provider component. | ||
| */ | ||
| export type TiptapWrapperProps = { | ||
| /** | ||
| * The editor instance to provide to child components. | ||
| * Can be null during SSR or before initialization. | ||
| */ | ||
| instance: Editor | null | ||
| children: ReactNode | ||
| } | ||
| /** | ||
| * Top-level provider component that makes the editor instance available via | ||
| * React context and tracks when the editor becomes ready. | ||
| * | ||
| * The component listens to the editor's `create` event and flips the | ||
| * `isReady` flag once initialization completes. | ||
| * | ||
| * This component also provides backwards compatibility with the legacy | ||
| * `EditorContext`, so components using `useCurrentEditor()` will work | ||
| * inside a `<Tiptap>` provider. | ||
| * | ||
| * @param props - Component props. | ||
| * @returns A context provider element wrapping `children`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { Tiptap, useEditor } from '@tiptap/react' | ||
| * | ||
| * function App() { | ||
| * const editor = useEditor({ extensions: [...] }) | ||
| * | ||
| * return ( | ||
| * <Tiptap instance={editor}> | ||
| * <Toolbar /> | ||
| * <Tiptap.Content /> | ||
| * </Tiptap> | ||
| * ) | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function TiptapWrapper({ instance, children }: TiptapWrapperProps) { | ||
| const [isReady, setIsReady] = useState(instance?.isInitialized ?? false) | ||
| useEffect(() => { | ||
| if (!instance) { | ||
| setIsReady(false) | ||
| return | ||
| } | ||
| // If the editor is already initialized, set isReady to true | ||
| if (instance.isInitialized) { | ||
| setIsReady(true) | ||
| return | ||
| } | ||
| const handleCreate = () => { | ||
| setIsReady(true) | ||
| } | ||
| instance.on('create', handleCreate) | ||
| return () => { | ||
| instance.off('create', handleCreate) | ||
| } | ||
| }, [instance]) | ||
| // Memoize context values to prevent unnecessary re-renders | ||
| const tiptapContextValue = useMemo<TiptapContextType>(() => ({ editor: instance, isReady }), [instance, isReady]) | ||
| // Provide backwards compatibility with the legacy EditorContext | ||
| // so components using useCurrentEditor() work inside <Tiptap> | ||
| const legacyContextValue = useMemo(() => ({ editor: instance }), [instance]) | ||
| return ( | ||
| <EditorContext.Provider value={legacyContextValue}> | ||
| <TiptapContext.Provider value={tiptapContextValue}>{children}</TiptapContext.Provider> | ||
| </EditorContext.Provider> | ||
| ) | ||
| } | ||
| TiptapWrapper.displayName = 'Tiptap' | ||
| /** | ||
| * Convenience component that renders `EditorContent` using the context-provided | ||
| * editor instance. Use this instead of manually passing the `editor` prop. | ||
| * | ||
| * @param props - All `EditorContent` props except `editor` and `ref`. | ||
| * @returns An `EditorContent` element bound to the context editor. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // inside a Tiptap provider | ||
| * <Tiptap.Content className="editor" /> | ||
| * ``` | ||
| */ | ||
| export function TiptapContent({ ...rest }: Omit<EditorContentProps, 'editor' | 'ref'>) { | ||
| const { editor } = useTiptap() | ||
| return <EditorContent editor={editor} {...rest} /> | ||
| } | ||
| TiptapContent.displayName = 'Tiptap.Content' | ||
| export type TiptapLoadingProps = { | ||
| children: ReactNode | ||
| } | ||
| /** | ||
| * Component that renders its children only when the editor is not ready. | ||
| * | ||
| * This is useful for displaying loading states or placeholders during | ||
| * editor initialization, especially with SSR. | ||
| * | ||
| * @param props - The props for the TiptapLoading component. | ||
| * @returns The children when editor is not ready, or null when ready. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap instance={editor}> | ||
| * <Tiptap.Loading> | ||
| * <div className="skeleton">Loading editor...</div> | ||
| * </Tiptap.Loading> | ||
| * <Tiptap.Content /> | ||
| * </Tiptap> | ||
| * ``` | ||
| */ | ||
| export function TiptapLoading({ children }: TiptapLoadingProps) { | ||
| const { isReady } = useTiptap() | ||
| if (isReady) { | ||
| return null | ||
| } | ||
| return children | ||
| } | ||
| TiptapLoading.displayName = 'Tiptap.Loading' | ||
| /** | ||
| * A wrapper around the library `BubbleMenu` that injects the editor from | ||
| * context so callers don't need to pass the `editor` prop. | ||
| * | ||
| * Returns `null` when the editor is not available (for example during SSR). | ||
| * | ||
| * @param props - Props for the underlying `BubbleMenu` (except `editor`). | ||
| * @returns A `BubbleMenu` bound to the context editor, or `null`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap.BubbleMenu tippyOptions={{ duration: 100 }}> | ||
| * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button> | ||
| * </Tiptap.BubbleMenu> | ||
| * ``` | ||
| */ | ||
| export function TiptapBubbleMenu({ children, ...rest }: { children: ReactNode } & Omit<BubbleMenuProps, 'editor'>) { | ||
| const { editor } = useTiptap() | ||
| if (!editor) { | ||
| return null | ||
| } | ||
| return ( | ||
| <BubbleMenu editor={editor} {...rest}> | ||
| {children} | ||
| </BubbleMenu> | ||
| ) | ||
| } | ||
| TiptapBubbleMenu.displayName = 'Tiptap.BubbleMenu' | ||
| /** | ||
| * A wrapper around the library `FloatingMenu` that injects the editor from | ||
| * context so callers don't need to pass the `editor` prop. | ||
| * | ||
| * Returns `null` when the editor is not available. | ||
| * | ||
| * @param props - Props for the underlying `FloatingMenu` (except `editor`). | ||
| * @returns A `FloatingMenu` bound to the context editor, or `null`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap.FloatingMenu placement="top"> | ||
| * <button onClick={() => editor.chain().focus().toggleItalic().run()}>Italic</button> | ||
| * </Tiptap.FloatingMenu> | ||
| * ``` | ||
| */ | ||
| export function TiptapFloatingMenu({ children, ...rest }: { children: ReactNode } & Omit<FloatingMenuProps, 'editor'>) { | ||
| const { editor } = useTiptap() | ||
| if (!editor) { | ||
| return null | ||
| } | ||
| return ( | ||
| <FloatingMenu {...rest} editor={editor}> | ||
| {children} | ||
| </FloatingMenu> | ||
| ) | ||
| } | ||
| TiptapFloatingMenu.displayName = 'Tiptap.FloatingMenu' | ||
| /** | ||
| * Root `Tiptap` component. Use it as the provider for all child components. | ||
| * | ||
| * The exported object includes several helper subcomponents for common use | ||
| * cases: `Content`, `Loading`, `BubbleMenu`, and `FloatingMenu`. | ||
| * | ||
| * This component provides both the new `TiptapContext` (accessed via `useTiptap()`) | ||
| * and the legacy `EditorContext` (accessed via `useCurrentEditor()`) for | ||
| * backwards compatibility. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const editor = useEditor({ extensions: [...] }) | ||
| * | ||
| * return ( | ||
| * <Tiptap instance={editor}> | ||
| * <Tiptap.Loading>Initializing editor...</Tiptap.Loading> | ||
| * <Tiptap.Content /> | ||
| * <Tiptap.BubbleMenu> | ||
| * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button> | ||
| * </Tiptap.BubbleMenu> | ||
| * </Tiptap> | ||
| * ) | ||
| * ``` | ||
| */ | ||
| export const Tiptap = Object.assign(TiptapWrapper, { | ||
| /** | ||
| * The Tiptap Content component that renders the EditorContent with the editor instance from the context. | ||
| * @see TiptapContent | ||
| */ | ||
| Content: TiptapContent, | ||
| /** | ||
| * The Tiptap Loading component that renders its children only when the editor is not ready. | ||
| * @see TiptapLoading | ||
| */ | ||
| Loading: TiptapLoading, | ||
| /** | ||
| * The Tiptap BubbleMenu component that wraps the BubbleMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapBubbleMenu | ||
| */ | ||
| BubbleMenu: TiptapBubbleMenu, | ||
| /** | ||
| * The Tiptap FloatingMenu component that wraps the FloatingMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapFloatingMenu | ||
| */ | ||
| FloatingMenu: TiptapFloatingMenu, | ||
| }) | ||
| export default Tiptap |
+327
-4
@@ -50,6 +50,15 @@ "use strict"; | ||
| ReactRenderer: () => ReactRenderer, | ||
| Tiptap: () => Tiptap, | ||
| TiptapBubbleMenu: () => TiptapBubbleMenu, | ||
| TiptapContent: () => TiptapContent, | ||
| TiptapContext: () => TiptapContext, | ||
| TiptapFloatingMenu: () => TiptapFloatingMenu, | ||
| TiptapLoading: () => TiptapLoading, | ||
| TiptapWrapper: () => TiptapWrapper, | ||
| useCurrentEditor: () => useCurrentEditor, | ||
| useEditor: () => useEditor, | ||
| useEditorState: () => useEditorState, | ||
| useReactNodeView: () => useReactNodeView | ||
| useReactNodeView: () => useReactNodeView, | ||
| useTiptap: () => useTiptap, | ||
| useTiptapState: () => useTiptapState | ||
| }); | ||
@@ -875,2 +884,3 @@ module.exports = __toCommonJS(index_exports); | ||
| this.selectionRafId = null; | ||
| this.cachedExtensionWithSyncedStorage = null; | ||
| if (!this.node.isLeaf) { | ||
@@ -893,2 +903,23 @@ if (this.options.contentDOMElementTag) { | ||
| /** | ||
| * Returns a proxy of the extension that redirects storage access to the editor's mutable storage. | ||
| * This preserves the original prototype chain (instanceof checks, methods like configure/extend work). | ||
| * Cached to avoid proxy creation on every update. | ||
| */ | ||
| get extensionWithSyncedStorage() { | ||
| if (!this.cachedExtensionWithSyncedStorage) { | ||
| const editor = this.editor; | ||
| const extension = this.extension; | ||
| this.cachedExtensionWithSyncedStorage = new Proxy(extension, { | ||
| get(target, prop, receiver) { | ||
| var _a; | ||
| if (prop === "storage") { | ||
| return (_a = editor.storage[extension.name]) != null ? _a : {}; | ||
| } | ||
| return Reflect.get(target, prop, receiver); | ||
| } | ||
| }); | ||
| } | ||
| return this.cachedExtensionWithSyncedStorage; | ||
| } | ||
| /** | ||
| * Setup the React component. | ||
@@ -905,3 +936,3 @@ * Called on initialization. | ||
| selected: false, | ||
| extension: this.extension, | ||
| extension: this.extensionWithSyncedStorage, | ||
| HTMLAttributes: this.HTMLAttributes, | ||
@@ -1027,3 +1058,3 @@ getPos: () => this.getPos(), | ||
| innerDecorations, | ||
| updateProps: () => rerenderComponent({ node, decorations, innerDecorations }) | ||
| updateProps: () => rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }) | ||
| }); | ||
@@ -1037,3 +1068,3 @@ } | ||
| this.innerDecorations = innerDecorations; | ||
| rerenderComponent({ node, decorations, innerDecorations }); | ||
| rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }); | ||
| return true; | ||
@@ -1100,2 +1131,285 @@ } | ||
| // src/Tiptap.tsx | ||
| var import_react14 = require("react"); | ||
| // src/menus/BubbleMenu.tsx | ||
| var import_extension_bubble_menu = require("@tiptap/extension-bubble-menu"); | ||
| var import_react10 = require("@tiptap/react"); | ||
| var import_react11 = __toESM(require("react"), 1); | ||
| var import_react_dom3 = require("react-dom"); | ||
| var import_jsx_runtime8 = require("react/jsx-runtime"); | ||
| var BubbleMenu = import_react11.default.forwardRef( | ||
| ({ | ||
| pluginKey = "bubbleMenu", | ||
| editor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| shouldShow = null, | ||
| getReferencedVirtualElement, | ||
| options, | ||
| children, | ||
| ...restProps | ||
| }, ref) => { | ||
| const menuEl = (0, import_react11.useRef)(document.createElement("div")); | ||
| if (typeof ref === "function") { | ||
| ref(menuEl.current); | ||
| } else if (ref) { | ||
| ref.current = menuEl.current; | ||
| } | ||
| const { editor: currentEditor } = (0, import_react10.useCurrentEditor)(); | ||
| const pluginEditor = editor || currentEditor; | ||
| const bubbleMenuPluginProps = { | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| pluginKey, | ||
| shouldShow, | ||
| getReferencedVirtualElement, | ||
| options | ||
| }; | ||
| const bubbleMenuPluginPropsRef = (0, import_react11.useRef)(bubbleMenuPluginProps); | ||
| bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps; | ||
| const [pluginInitialized, setPluginInitialized] = (0, import_react11.useState)(false); | ||
| const skipFirstUpdateRef = (0, import_react11.useRef)(true); | ||
| (0, import_react11.useEffect)(() => { | ||
| if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (!pluginEditor) { | ||
| console.warn("BubbleMenu component is not rendered inside of an editor component or does not have editor prop."); | ||
| return; | ||
| } | ||
| const bubbleMenuElement = menuEl.current; | ||
| bubbleMenuElement.style.visibility = "hidden"; | ||
| bubbleMenuElement.style.position = "absolute"; | ||
| const plugin = (0, import_extension_bubble_menu.BubbleMenuPlugin)({ | ||
| ...bubbleMenuPluginPropsRef.current, | ||
| editor: pluginEditor, | ||
| element: bubbleMenuElement | ||
| }); | ||
| pluginEditor.registerPlugin(plugin); | ||
| const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey; | ||
| skipFirstUpdateRef.current = true; | ||
| setPluginInitialized(true); | ||
| return () => { | ||
| setPluginInitialized(false); | ||
| pluginEditor.unregisterPlugin(createdPluginKey); | ||
| window.requestAnimationFrame(() => { | ||
| if (bubbleMenuElement.parentNode) { | ||
| bubbleMenuElement.parentNode.removeChild(bubbleMenuElement); | ||
| } | ||
| }); | ||
| }; | ||
| }, [pluginEditor]); | ||
| (0, import_react11.useEffect)(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false; | ||
| return; | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta("bubbleMenu", { | ||
| type: "updateOptions", | ||
| options: bubbleMenuPluginPropsRef.current | ||
| }) | ||
| ); | ||
| }, [ | ||
| pluginInitialized, | ||
| pluginEditor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| shouldShow, | ||
| options, | ||
| appendTo, | ||
| getReferencedVirtualElement | ||
| ]); | ||
| return (0, import_react_dom3.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { ...restProps, children }), menuEl.current); | ||
| } | ||
| ); | ||
| // src/menus/FloatingMenu.tsx | ||
| var import_extension_floating_menu = require("@tiptap/extension-floating-menu"); | ||
| var import_react12 = require("@tiptap/react"); | ||
| var import_react13 = __toESM(require("react"), 1); | ||
| var import_react_dom4 = require("react-dom"); | ||
| var import_jsx_runtime9 = require("react/jsx-runtime"); | ||
| var FloatingMenu = import_react13.default.forwardRef( | ||
| ({ | ||
| pluginKey = "floatingMenu", | ||
| editor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| shouldShow = null, | ||
| options, | ||
| children, | ||
| ...restProps | ||
| }, ref) => { | ||
| const menuEl = (0, import_react13.useRef)(document.createElement("div")); | ||
| if (typeof ref === "function") { | ||
| ref(menuEl.current); | ||
| } else if (ref) { | ||
| ref.current = menuEl.current; | ||
| } | ||
| const { editor: currentEditor } = (0, import_react12.useCurrentEditor)(); | ||
| const pluginEditor = editor || currentEditor; | ||
| const floatingMenuPluginProps = { | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| pluginKey, | ||
| shouldShow, | ||
| options | ||
| }; | ||
| const floatingMenuPluginPropsRef = (0, import_react13.useRef)(floatingMenuPluginProps); | ||
| floatingMenuPluginPropsRef.current = floatingMenuPluginProps; | ||
| const [pluginInitialized, setPluginInitialized] = (0, import_react13.useState)(false); | ||
| const skipFirstUpdateRef = (0, import_react13.useRef)(true); | ||
| (0, import_react13.useEffect)(() => { | ||
| if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (!pluginEditor) { | ||
| console.warn( | ||
| "FloatingMenu component is not rendered inside of an editor component or does not have editor prop." | ||
| ); | ||
| return; | ||
| } | ||
| const floatingMenuElement = menuEl.current; | ||
| floatingMenuElement.style.visibility = "hidden"; | ||
| floatingMenuElement.style.position = "absolute"; | ||
| const plugin = (0, import_extension_floating_menu.FloatingMenuPlugin)({ | ||
| ...floatingMenuPluginPropsRef.current, | ||
| editor: pluginEditor, | ||
| element: floatingMenuElement | ||
| }); | ||
| pluginEditor.registerPlugin(plugin); | ||
| const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey; | ||
| skipFirstUpdateRef.current = true; | ||
| setPluginInitialized(true); | ||
| return () => { | ||
| setPluginInitialized(false); | ||
| pluginEditor.unregisterPlugin(createdPluginKey); | ||
| window.requestAnimationFrame(() => { | ||
| if (floatingMenuElement.parentNode) { | ||
| floatingMenuElement.parentNode.removeChild(floatingMenuElement); | ||
| } | ||
| }); | ||
| }; | ||
| }, [pluginEditor]); | ||
| (0, import_react13.useEffect)(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false; | ||
| return; | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta("floatingMenu", { | ||
| type: "updateOptions", | ||
| options: floatingMenuPluginPropsRef.current | ||
| }) | ||
| ); | ||
| }, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo]); | ||
| return (0, import_react_dom4.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ...restProps, children }), menuEl.current); | ||
| } | ||
| ); | ||
| // src/Tiptap.tsx | ||
| var import_jsx_runtime10 = require("react/jsx-runtime"); | ||
| var TiptapContext = (0, import_react14.createContext)({ | ||
| editor: null, | ||
| isReady: false | ||
| }); | ||
| TiptapContext.displayName = "TiptapContext"; | ||
| var useTiptap = () => (0, import_react14.useContext)(TiptapContext); | ||
| function useTiptapState(selector, equalityFn) { | ||
| const { editor } = useTiptap(); | ||
| return useEditorState({ | ||
| editor, | ||
| selector, | ||
| equalityFn | ||
| }); | ||
| } | ||
| function TiptapWrapper({ instance, children }) { | ||
| var _a; | ||
| const [isReady, setIsReady] = (0, import_react14.useState)((_a = instance == null ? void 0 : instance.isInitialized) != null ? _a : false); | ||
| (0, import_react14.useEffect)(() => { | ||
| if (!instance) { | ||
| setIsReady(false); | ||
| return; | ||
| } | ||
| if (instance.isInitialized) { | ||
| setIsReady(true); | ||
| return; | ||
| } | ||
| const handleCreate = () => { | ||
| setIsReady(true); | ||
| }; | ||
| instance.on("create", handleCreate); | ||
| return () => { | ||
| instance.off("create", handleCreate); | ||
| }; | ||
| }, [instance]); | ||
| const tiptapContextValue = (0, import_react14.useMemo)(() => ({ editor: instance, isReady }), [instance, isReady]); | ||
| const legacyContextValue = (0, import_react14.useMemo)(() => ({ editor: instance }), [instance]); | ||
| return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(EditorContext.Provider, { value: legacyContextValue, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(TiptapContext.Provider, { value: tiptapContextValue, children }) }); | ||
| } | ||
| TiptapWrapper.displayName = "Tiptap"; | ||
| function TiptapContent({ ...rest }) { | ||
| const { editor } = useTiptap(); | ||
| return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(EditorContent, { editor, ...rest }); | ||
| } | ||
| TiptapContent.displayName = "Tiptap.Content"; | ||
| function TiptapLoading({ children }) { | ||
| const { isReady } = useTiptap(); | ||
| if (isReady) { | ||
| return null; | ||
| } | ||
| return children; | ||
| } | ||
| TiptapLoading.displayName = "Tiptap.Loading"; | ||
| function TiptapBubbleMenu({ children, ...rest }) { | ||
| const { editor } = useTiptap(); | ||
| if (!editor) { | ||
| return null; | ||
| } | ||
| return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(BubbleMenu, { editor, ...rest, children }); | ||
| } | ||
| TiptapBubbleMenu.displayName = "Tiptap.BubbleMenu"; | ||
| function TiptapFloatingMenu({ children, ...rest }) { | ||
| const { editor } = useTiptap(); | ||
| if (!editor) { | ||
| return null; | ||
| } | ||
| return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(FloatingMenu, { ...rest, editor, children }); | ||
| } | ||
| TiptapFloatingMenu.displayName = "Tiptap.FloatingMenu"; | ||
| var Tiptap = Object.assign(TiptapWrapper, { | ||
| /** | ||
| * The Tiptap Content component that renders the EditorContent with the editor instance from the context. | ||
| * @see TiptapContent | ||
| */ | ||
| Content: TiptapContent, | ||
| /** | ||
| * The Tiptap Loading component that renders its children only when the editor is not ready. | ||
| * @see TiptapLoading | ||
| */ | ||
| Loading: TiptapLoading, | ||
| /** | ||
| * The Tiptap BubbleMenu component that wraps the BubbleMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapBubbleMenu | ||
| */ | ||
| BubbleMenu: TiptapBubbleMenu, | ||
| /** | ||
| * The Tiptap FloatingMenu component that wraps the FloatingMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapFloatingMenu | ||
| */ | ||
| FloatingMenu: TiptapFloatingMenu | ||
| }); | ||
| // src/index.ts | ||
@@ -1121,2 +1435,9 @@ __reExport(index_exports, require("@tiptap/core"), module.exports); | ||
| ReactRenderer, | ||
| Tiptap, | ||
| TiptapBubbleMenu, | ||
| TiptapContent, | ||
| TiptapContext, | ||
| TiptapFloatingMenu, | ||
| TiptapLoading, | ||
| TiptapWrapper, | ||
| useCurrentEditor, | ||
@@ -1126,4 +1447,6 @@ useEditor, | ||
| useReactNodeView, | ||
| useTiptap, | ||
| useTiptapState, | ||
| ...require("@tiptap/core") | ||
| }); | ||
| //# sourceMappingURL=index.cjs.map |
+269
-1
@@ -8,2 +8,4 @@ import * as react_jsx_runtime from 'react/jsx-runtime'; | ||
| import { Decoration, DecorationSource } from '@tiptap/pm/view'; | ||
| import { BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'; | ||
| import { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'; | ||
@@ -248,3 +250,10 @@ /** | ||
| constructor(component: Component, props: NodeViewRendererProps, options?: Partial<Options>); | ||
| private cachedExtensionWithSyncedStorage; | ||
| /** | ||
| * Returns a proxy of the extension that redirects storage access to the editor's mutable storage. | ||
| * This preserves the original prototype chain (instanceof checks, methods like configure/extend work). | ||
| * Cached to avoid proxy creation on every update. | ||
| */ | ||
| get extensionWithSyncedStorage(): NodeViewRendererProps['extension']; | ||
| /** | ||
| * Setup the React component. | ||
@@ -299,2 +308,261 @@ * Called on initialization. | ||
| type Optional$1<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; | ||
| type BubbleMenuProps = Optional$1<Omit<Optional$1<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> & React__default.HTMLAttributes<HTMLDivElement>; | ||
| type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; | ||
| type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & { | ||
| editor: FloatingMenuPluginProps['editor'] | null; | ||
| options?: FloatingMenuPluginProps['options']; | ||
| } & React__default.HTMLAttributes<HTMLDivElement>; | ||
| /** | ||
| * The shape of the React context used by the `<Tiptap />` components. | ||
| * | ||
| * This object exposes the editor instance and a simple readiness flag. | ||
| */ | ||
| type TiptapContextType = { | ||
| /** The Tiptap editor instance. May be null during SSR or before initialization. */ | ||
| editor: Editor | null; | ||
| /** True when the editor has finished initializing and is ready for user interaction. */ | ||
| isReady: boolean; | ||
| }; | ||
| /** | ||
| * React context that stores the current editor instance and readiness flag. | ||
| * | ||
| * Use `useTiptap()` to read from this context in child components. | ||
| */ | ||
| declare const TiptapContext: React.Context<TiptapContextType>; | ||
| /** | ||
| * Hook to read the Tiptap context (`editor` + `isReady`). | ||
| * | ||
| * This is a small convenience wrapper around `useContext(TiptapContext)`. | ||
| * | ||
| * @returns The current `TiptapContextType` value from the provider. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useTiptap } from '@tiptap/react' | ||
| * | ||
| * function Status() { | ||
| * const { isReady } = useTiptap() | ||
| * return <div>{isReady ? 'Editor ready' : 'Loading editor...'}</div> | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare const useTiptap: () => TiptapContextType; | ||
| /** | ||
| * Select a slice of the editor state using the context-provided editor. | ||
| * | ||
| * This is a thin wrapper around `useEditorState` that reads the `editor` | ||
| * instance from `useTiptap()` so callers don't have to pass it manually. | ||
| * | ||
| * Important: This hook should only be used when the editor is available. | ||
| * Use the `isReady` flag from `useTiptap()` to guard against null editor, | ||
| * or ensure your component only renders after the editor is initialized. | ||
| * | ||
| * @typeParam TSelectorResult - The type returned by the selector. | ||
| * @param selector - Function that receives the editor state snapshot and | ||
| * returns the piece of state you want to subscribe to. | ||
| * @param equalityFn - Optional function to compare previous/next selected | ||
| * values and avoid unnecessary updates. | ||
| * @returns The selected slice of the editor state. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * function WordCount() { | ||
| * const { isReady } = useTiptap() | ||
| * | ||
| * // Only use useTiptapState when the editor is ready | ||
| * const wordCount = useTiptapState(state => { | ||
| * const text = state.editor.state.doc.textContent | ||
| * return text.split(/\s+/).filter(Boolean).length | ||
| * }) | ||
| * | ||
| * if (!isReady) return null | ||
| * | ||
| * return <span>{wordCount} words</span> | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare function useTiptapState<TSelectorResult>(selector: (context: EditorStateSnapshot<Editor>) => TSelectorResult, equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean): TSelectorResult; | ||
| /** | ||
| * Props for the `Tiptap` root/provider component. | ||
| */ | ||
| type TiptapWrapperProps = { | ||
| /** | ||
| * The editor instance to provide to child components. | ||
| * Can be null during SSR or before initialization. | ||
| */ | ||
| instance: Editor | null; | ||
| children: ReactNode; | ||
| }; | ||
| /** | ||
| * Top-level provider component that makes the editor instance available via | ||
| * React context and tracks when the editor becomes ready. | ||
| * | ||
| * The component listens to the editor's `create` event and flips the | ||
| * `isReady` flag once initialization completes. | ||
| * | ||
| * This component also provides backwards compatibility with the legacy | ||
| * `EditorContext`, so components using `useCurrentEditor()` will work | ||
| * inside a `<Tiptap>` provider. | ||
| * | ||
| * @param props - Component props. | ||
| * @returns A context provider element wrapping `children`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { Tiptap, useEditor } from '@tiptap/react' | ||
| * | ||
| * function App() { | ||
| * const editor = useEditor({ extensions: [...] }) | ||
| * | ||
| * return ( | ||
| * <Tiptap instance={editor}> | ||
| * <Toolbar /> | ||
| * <Tiptap.Content /> | ||
| * </Tiptap> | ||
| * ) | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare function TiptapWrapper({ instance, children }: TiptapWrapperProps): react_jsx_runtime.JSX.Element; | ||
| declare namespace TiptapWrapper { | ||
| var displayName: string; | ||
| } | ||
| /** | ||
| * Convenience component that renders `EditorContent` using the context-provided | ||
| * editor instance. Use this instead of manually passing the `editor` prop. | ||
| * | ||
| * @param props - All `EditorContent` props except `editor` and `ref`. | ||
| * @returns An `EditorContent` element bound to the context editor. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // inside a Tiptap provider | ||
| * <Tiptap.Content className="editor" /> | ||
| * ``` | ||
| */ | ||
| declare function TiptapContent({ ...rest }: Omit<EditorContentProps, 'editor' | 'ref'>): react_jsx_runtime.JSX.Element; | ||
| declare namespace TiptapContent { | ||
| var displayName: string; | ||
| } | ||
| type TiptapLoadingProps = { | ||
| children: ReactNode; | ||
| }; | ||
| /** | ||
| * Component that renders its children only when the editor is not ready. | ||
| * | ||
| * This is useful for displaying loading states or placeholders during | ||
| * editor initialization, especially with SSR. | ||
| * | ||
| * @param props - The props for the TiptapLoading component. | ||
| * @returns The children when editor is not ready, or null when ready. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap instance={editor}> | ||
| * <Tiptap.Loading> | ||
| * <div className="skeleton">Loading editor...</div> | ||
| * </Tiptap.Loading> | ||
| * <Tiptap.Content /> | ||
| * </Tiptap> | ||
| * ``` | ||
| */ | ||
| declare function TiptapLoading({ children }: TiptapLoadingProps): ReactNode; | ||
| declare namespace TiptapLoading { | ||
| var displayName: string; | ||
| } | ||
| /** | ||
| * A wrapper around the library `BubbleMenu` that injects the editor from | ||
| * context so callers don't need to pass the `editor` prop. | ||
| * | ||
| * Returns `null` when the editor is not available (for example during SSR). | ||
| * | ||
| * @param props - Props for the underlying `BubbleMenu` (except `editor`). | ||
| * @returns A `BubbleMenu` bound to the context editor, or `null`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap.BubbleMenu tippyOptions={{ duration: 100 }}> | ||
| * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button> | ||
| * </Tiptap.BubbleMenu> | ||
| * ``` | ||
| */ | ||
| declare function TiptapBubbleMenu({ children, ...rest }: { | ||
| children: ReactNode; | ||
| } & Omit<BubbleMenuProps, 'editor'>): react_jsx_runtime.JSX.Element | null; | ||
| declare namespace TiptapBubbleMenu { | ||
| var displayName: string; | ||
| } | ||
| /** | ||
| * A wrapper around the library `FloatingMenu` that injects the editor from | ||
| * context so callers don't need to pass the `editor` prop. | ||
| * | ||
| * Returns `null` when the editor is not available. | ||
| * | ||
| * @param props - Props for the underlying `FloatingMenu` (except `editor`). | ||
| * @returns A `FloatingMenu` bound to the context editor, or `null`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap.FloatingMenu placement="top"> | ||
| * <button onClick={() => editor.chain().focus().toggleItalic().run()}>Italic</button> | ||
| * </Tiptap.FloatingMenu> | ||
| * ``` | ||
| */ | ||
| declare function TiptapFloatingMenu({ children, ...rest }: { | ||
| children: ReactNode; | ||
| } & Omit<FloatingMenuProps, 'editor'>): react_jsx_runtime.JSX.Element | null; | ||
| declare namespace TiptapFloatingMenu { | ||
| var displayName: string; | ||
| } | ||
| /** | ||
| * Root `Tiptap` component. Use it as the provider for all child components. | ||
| * | ||
| * The exported object includes several helper subcomponents for common use | ||
| * cases: `Content`, `Loading`, `BubbleMenu`, and `FloatingMenu`. | ||
| * | ||
| * This component provides both the new `TiptapContext` (accessed via `useTiptap()`) | ||
| * and the legacy `EditorContext` (accessed via `useCurrentEditor()`) for | ||
| * backwards compatibility. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const editor = useEditor({ extensions: [...] }) | ||
| * | ||
| * return ( | ||
| * <Tiptap instance={editor}> | ||
| * <Tiptap.Loading>Initializing editor...</Tiptap.Loading> | ||
| * <Tiptap.Content /> | ||
| * <Tiptap.BubbleMenu> | ||
| * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button> | ||
| * </Tiptap.BubbleMenu> | ||
| * </Tiptap> | ||
| * ) | ||
| * ``` | ||
| */ | ||
| declare const Tiptap: typeof TiptapWrapper & { | ||
| /** | ||
| * The Tiptap Content component that renders the EditorContent with the editor instance from the context. | ||
| * @see TiptapContent | ||
| */ | ||
| Content: typeof TiptapContent; | ||
| /** | ||
| * The Tiptap Loading component that renders its children only when the editor is not ready. | ||
| * @see TiptapLoading | ||
| */ | ||
| Loading: typeof TiptapLoading; | ||
| /** | ||
| * The Tiptap BubbleMenu component that wraps the BubbleMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapBubbleMenu | ||
| */ | ||
| BubbleMenu: typeof TiptapBubbleMenu; | ||
| /** | ||
| * The Tiptap FloatingMenu component that wraps the FloatingMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapFloatingMenu | ||
| */ | ||
| FloatingMenu: typeof TiptapFloatingMenu; | ||
| }; | ||
| type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = { | ||
@@ -360,2 +628,2 @@ editor: TEditor; | ||
| export { EditorConsumer, EditorContent, type EditorContentProps, EditorContext, type EditorContextValue, EditorProvider, type EditorProviderProps, type EditorStateSnapshot, MarkViewContent, type MarkViewContentProps, type MarkViewContextProps, NodeViewContent, type NodeViewContentProps, NodeViewWrapper, type NodeViewWrapperProps, PureEditorContent, ReactMarkView, ReactMarkViewContext, ReactMarkViewRenderer, type ReactMarkViewRendererOptions, ReactNodeView, ReactNodeViewContentProvider, ReactNodeViewContext, type ReactNodeViewContextProps, type ReactNodeViewProps, ReactNodeViewRenderer, type ReactNodeViewRendererOptions, ReactRenderer, type ReactRendererOptions, type UseEditorOptions, type UseEditorStateOptions, useCurrentEditor, useEditor, useEditorState, useReactNodeView }; | ||
| export { EditorConsumer, EditorContent, type EditorContentProps, EditorContext, type EditorContextValue, EditorProvider, type EditorProviderProps, type EditorStateSnapshot, MarkViewContent, type MarkViewContentProps, type MarkViewContextProps, NodeViewContent, type NodeViewContentProps, NodeViewWrapper, type NodeViewWrapperProps, PureEditorContent, ReactMarkView, ReactMarkViewContext, ReactMarkViewRenderer, type ReactMarkViewRendererOptions, ReactNodeView, ReactNodeViewContentProvider, ReactNodeViewContext, type ReactNodeViewContextProps, type ReactNodeViewProps, ReactNodeViewRenderer, type ReactNodeViewRendererOptions, ReactRenderer, type ReactRendererOptions, Tiptap, TiptapBubbleMenu, TiptapContent, TiptapContext, type TiptapContextType, TiptapFloatingMenu, TiptapLoading, type TiptapLoadingProps, TiptapWrapper, type TiptapWrapperProps, type UseEditorOptions, type UseEditorStateOptions, useCurrentEditor, useEditor, useEditorState, useReactNodeView, useTiptap, useTiptapState }; |
+269
-1
@@ -8,2 +8,4 @@ import * as react_jsx_runtime from 'react/jsx-runtime'; | ||
| import { Decoration, DecorationSource } from '@tiptap/pm/view'; | ||
| import { BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'; | ||
| import { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'; | ||
@@ -248,3 +250,10 @@ /** | ||
| constructor(component: Component, props: NodeViewRendererProps, options?: Partial<Options>); | ||
| private cachedExtensionWithSyncedStorage; | ||
| /** | ||
| * Returns a proxy of the extension that redirects storage access to the editor's mutable storage. | ||
| * This preserves the original prototype chain (instanceof checks, methods like configure/extend work). | ||
| * Cached to avoid proxy creation on every update. | ||
| */ | ||
| get extensionWithSyncedStorage(): NodeViewRendererProps['extension']; | ||
| /** | ||
| * Setup the React component. | ||
@@ -299,2 +308,261 @@ * Called on initialization. | ||
| type Optional$1<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; | ||
| type BubbleMenuProps = Optional$1<Omit<Optional$1<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> & React__default.HTMLAttributes<HTMLDivElement>; | ||
| type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; | ||
| type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & { | ||
| editor: FloatingMenuPluginProps['editor'] | null; | ||
| options?: FloatingMenuPluginProps['options']; | ||
| } & React__default.HTMLAttributes<HTMLDivElement>; | ||
| /** | ||
| * The shape of the React context used by the `<Tiptap />` components. | ||
| * | ||
| * This object exposes the editor instance and a simple readiness flag. | ||
| */ | ||
| type TiptapContextType = { | ||
| /** The Tiptap editor instance. May be null during SSR or before initialization. */ | ||
| editor: Editor | null; | ||
| /** True when the editor has finished initializing and is ready for user interaction. */ | ||
| isReady: boolean; | ||
| }; | ||
| /** | ||
| * React context that stores the current editor instance and readiness flag. | ||
| * | ||
| * Use `useTiptap()` to read from this context in child components. | ||
| */ | ||
| declare const TiptapContext: React.Context<TiptapContextType>; | ||
| /** | ||
| * Hook to read the Tiptap context (`editor` + `isReady`). | ||
| * | ||
| * This is a small convenience wrapper around `useContext(TiptapContext)`. | ||
| * | ||
| * @returns The current `TiptapContextType` value from the provider. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useTiptap } from '@tiptap/react' | ||
| * | ||
| * function Status() { | ||
| * const { isReady } = useTiptap() | ||
| * return <div>{isReady ? 'Editor ready' : 'Loading editor...'}</div> | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare const useTiptap: () => TiptapContextType; | ||
| /** | ||
| * Select a slice of the editor state using the context-provided editor. | ||
| * | ||
| * This is a thin wrapper around `useEditorState` that reads the `editor` | ||
| * instance from `useTiptap()` so callers don't have to pass it manually. | ||
| * | ||
| * Important: This hook should only be used when the editor is available. | ||
| * Use the `isReady` flag from `useTiptap()` to guard against null editor, | ||
| * or ensure your component only renders after the editor is initialized. | ||
| * | ||
| * @typeParam TSelectorResult - The type returned by the selector. | ||
| * @param selector - Function that receives the editor state snapshot and | ||
| * returns the piece of state you want to subscribe to. | ||
| * @param equalityFn - Optional function to compare previous/next selected | ||
| * values and avoid unnecessary updates. | ||
| * @returns The selected slice of the editor state. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * function WordCount() { | ||
| * const { isReady } = useTiptap() | ||
| * | ||
| * // Only use useTiptapState when the editor is ready | ||
| * const wordCount = useTiptapState(state => { | ||
| * const text = state.editor.state.doc.textContent | ||
| * return text.split(/\s+/).filter(Boolean).length | ||
| * }) | ||
| * | ||
| * if (!isReady) return null | ||
| * | ||
| * return <span>{wordCount} words</span> | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare function useTiptapState<TSelectorResult>(selector: (context: EditorStateSnapshot<Editor>) => TSelectorResult, equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean): TSelectorResult; | ||
| /** | ||
| * Props for the `Tiptap` root/provider component. | ||
| */ | ||
| type TiptapWrapperProps = { | ||
| /** | ||
| * The editor instance to provide to child components. | ||
| * Can be null during SSR or before initialization. | ||
| */ | ||
| instance: Editor | null; | ||
| children: ReactNode; | ||
| }; | ||
| /** | ||
| * Top-level provider component that makes the editor instance available via | ||
| * React context and tracks when the editor becomes ready. | ||
| * | ||
| * The component listens to the editor's `create` event and flips the | ||
| * `isReady` flag once initialization completes. | ||
| * | ||
| * This component also provides backwards compatibility with the legacy | ||
| * `EditorContext`, so components using `useCurrentEditor()` will work | ||
| * inside a `<Tiptap>` provider. | ||
| * | ||
| * @param props - Component props. | ||
| * @returns A context provider element wrapping `children`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { Tiptap, useEditor } from '@tiptap/react' | ||
| * | ||
| * function App() { | ||
| * const editor = useEditor({ extensions: [...] }) | ||
| * | ||
| * return ( | ||
| * <Tiptap instance={editor}> | ||
| * <Toolbar /> | ||
| * <Tiptap.Content /> | ||
| * </Tiptap> | ||
| * ) | ||
| * } | ||
| * ``` | ||
| */ | ||
| declare function TiptapWrapper({ instance, children }: TiptapWrapperProps): react_jsx_runtime.JSX.Element; | ||
| declare namespace TiptapWrapper { | ||
| var displayName: string; | ||
| } | ||
| /** | ||
| * Convenience component that renders `EditorContent` using the context-provided | ||
| * editor instance. Use this instead of manually passing the `editor` prop. | ||
| * | ||
| * @param props - All `EditorContent` props except `editor` and `ref`. | ||
| * @returns An `EditorContent` element bound to the context editor. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // inside a Tiptap provider | ||
| * <Tiptap.Content className="editor" /> | ||
| * ``` | ||
| */ | ||
| declare function TiptapContent({ ...rest }: Omit<EditorContentProps, 'editor' | 'ref'>): react_jsx_runtime.JSX.Element; | ||
| declare namespace TiptapContent { | ||
| var displayName: string; | ||
| } | ||
| type TiptapLoadingProps = { | ||
| children: ReactNode; | ||
| }; | ||
| /** | ||
| * Component that renders its children only when the editor is not ready. | ||
| * | ||
| * This is useful for displaying loading states or placeholders during | ||
| * editor initialization, especially with SSR. | ||
| * | ||
| * @param props - The props for the TiptapLoading component. | ||
| * @returns The children when editor is not ready, or null when ready. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap instance={editor}> | ||
| * <Tiptap.Loading> | ||
| * <div className="skeleton">Loading editor...</div> | ||
| * </Tiptap.Loading> | ||
| * <Tiptap.Content /> | ||
| * </Tiptap> | ||
| * ``` | ||
| */ | ||
| declare function TiptapLoading({ children }: TiptapLoadingProps): ReactNode; | ||
| declare namespace TiptapLoading { | ||
| var displayName: string; | ||
| } | ||
| /** | ||
| * A wrapper around the library `BubbleMenu` that injects the editor from | ||
| * context so callers don't need to pass the `editor` prop. | ||
| * | ||
| * Returns `null` when the editor is not available (for example during SSR). | ||
| * | ||
| * @param props - Props for the underlying `BubbleMenu` (except `editor`). | ||
| * @returns A `BubbleMenu` bound to the context editor, or `null`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap.BubbleMenu tippyOptions={{ duration: 100 }}> | ||
| * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button> | ||
| * </Tiptap.BubbleMenu> | ||
| * ``` | ||
| */ | ||
| declare function TiptapBubbleMenu({ children, ...rest }: { | ||
| children: ReactNode; | ||
| } & Omit<BubbleMenuProps, 'editor'>): react_jsx_runtime.JSX.Element | null; | ||
| declare namespace TiptapBubbleMenu { | ||
| var displayName: string; | ||
| } | ||
| /** | ||
| * A wrapper around the library `FloatingMenu` that injects the editor from | ||
| * context so callers don't need to pass the `editor` prop. | ||
| * | ||
| * Returns `null` when the editor is not available. | ||
| * | ||
| * @param props - Props for the underlying `FloatingMenu` (except `editor`). | ||
| * @returns A `FloatingMenu` bound to the context editor, or `null`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap.FloatingMenu placement="top"> | ||
| * <button onClick={() => editor.chain().focus().toggleItalic().run()}>Italic</button> | ||
| * </Tiptap.FloatingMenu> | ||
| * ``` | ||
| */ | ||
| declare function TiptapFloatingMenu({ children, ...rest }: { | ||
| children: ReactNode; | ||
| } & Omit<FloatingMenuProps, 'editor'>): react_jsx_runtime.JSX.Element | null; | ||
| declare namespace TiptapFloatingMenu { | ||
| var displayName: string; | ||
| } | ||
| /** | ||
| * Root `Tiptap` component. Use it as the provider for all child components. | ||
| * | ||
| * The exported object includes several helper subcomponents for common use | ||
| * cases: `Content`, `Loading`, `BubbleMenu`, and `FloatingMenu`. | ||
| * | ||
| * This component provides both the new `TiptapContext` (accessed via `useTiptap()`) | ||
| * and the legacy `EditorContext` (accessed via `useCurrentEditor()`) for | ||
| * backwards compatibility. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const editor = useEditor({ extensions: [...] }) | ||
| * | ||
| * return ( | ||
| * <Tiptap instance={editor}> | ||
| * <Tiptap.Loading>Initializing editor...</Tiptap.Loading> | ||
| * <Tiptap.Content /> | ||
| * <Tiptap.BubbleMenu> | ||
| * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button> | ||
| * </Tiptap.BubbleMenu> | ||
| * </Tiptap> | ||
| * ) | ||
| * ``` | ||
| */ | ||
| declare const Tiptap: typeof TiptapWrapper & { | ||
| /** | ||
| * The Tiptap Content component that renders the EditorContent with the editor instance from the context. | ||
| * @see TiptapContent | ||
| */ | ||
| Content: typeof TiptapContent; | ||
| /** | ||
| * The Tiptap Loading component that renders its children only when the editor is not ready. | ||
| * @see TiptapLoading | ||
| */ | ||
| Loading: typeof TiptapLoading; | ||
| /** | ||
| * The Tiptap BubbleMenu component that wraps the BubbleMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapBubbleMenu | ||
| */ | ||
| BubbleMenu: typeof TiptapBubbleMenu; | ||
| /** | ||
| * The Tiptap FloatingMenu component that wraps the FloatingMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapFloatingMenu | ||
| */ | ||
| FloatingMenu: typeof TiptapFloatingMenu; | ||
| }; | ||
| type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = { | ||
@@ -360,2 +628,2 @@ editor: TEditor; | ||
| export { EditorConsumer, EditorContent, type EditorContentProps, EditorContext, type EditorContextValue, EditorProvider, type EditorProviderProps, type EditorStateSnapshot, MarkViewContent, type MarkViewContentProps, type MarkViewContextProps, NodeViewContent, type NodeViewContentProps, NodeViewWrapper, type NodeViewWrapperProps, PureEditorContent, ReactMarkView, ReactMarkViewContext, ReactMarkViewRenderer, type ReactMarkViewRendererOptions, ReactNodeView, ReactNodeViewContentProvider, ReactNodeViewContext, type ReactNodeViewContextProps, type ReactNodeViewProps, ReactNodeViewRenderer, type ReactNodeViewRendererOptions, ReactRenderer, type ReactRendererOptions, type UseEditorOptions, type UseEditorStateOptions, useCurrentEditor, useEditor, useEditorState, useReactNodeView }; | ||
| export { EditorConsumer, EditorContent, type EditorContentProps, EditorContext, type EditorContextValue, EditorProvider, type EditorProviderProps, type EditorStateSnapshot, MarkViewContent, type MarkViewContentProps, type MarkViewContextProps, NodeViewContent, type NodeViewContentProps, NodeViewWrapper, type NodeViewWrapperProps, PureEditorContent, ReactMarkView, ReactMarkViewContext, ReactMarkViewRenderer, type ReactMarkViewRendererOptions, ReactNodeView, ReactNodeViewContentProvider, ReactNodeViewContext, type ReactNodeViewContextProps, type ReactNodeViewProps, ReactNodeViewRenderer, type ReactNodeViewRendererOptions, ReactRenderer, type ReactRendererOptions, Tiptap, TiptapBubbleMenu, TiptapContent, TiptapContext, type TiptapContextType, TiptapFloatingMenu, TiptapLoading, type TiptapLoadingProps, TiptapWrapper, type TiptapWrapperProps, type UseEditorOptions, type UseEditorStateOptions, useCurrentEditor, useEditor, useEditorState, useReactNodeView, useTiptap, useTiptapState }; |
+318
-4
@@ -818,2 +818,3 @@ // src/Context.tsx | ||
| this.selectionRafId = null; | ||
| this.cachedExtensionWithSyncedStorage = null; | ||
| if (!this.node.isLeaf) { | ||
@@ -836,2 +837,23 @@ if (this.options.contentDOMElementTag) { | ||
| /** | ||
| * Returns a proxy of the extension that redirects storage access to the editor's mutable storage. | ||
| * This preserves the original prototype chain (instanceof checks, methods like configure/extend work). | ||
| * Cached to avoid proxy creation on every update. | ||
| */ | ||
| get extensionWithSyncedStorage() { | ||
| if (!this.cachedExtensionWithSyncedStorage) { | ||
| const editor = this.editor; | ||
| const extension = this.extension; | ||
| this.cachedExtensionWithSyncedStorage = new Proxy(extension, { | ||
| get(target, prop, receiver) { | ||
| var _a; | ||
| if (prop === "storage") { | ||
| return (_a = editor.storage[extension.name]) != null ? _a : {}; | ||
| } | ||
| return Reflect.get(target, prop, receiver); | ||
| } | ||
| }); | ||
| } | ||
| return this.cachedExtensionWithSyncedStorage; | ||
| } | ||
| /** | ||
| * Setup the React component. | ||
@@ -848,3 +870,3 @@ * Called on initialization. | ||
| selected: false, | ||
| extension: this.extension, | ||
| extension: this.extensionWithSyncedStorage, | ||
| HTMLAttributes: this.HTMLAttributes, | ||
@@ -970,3 +992,3 @@ getPos: () => this.getPos(), | ||
| innerDecorations, | ||
| updateProps: () => rerenderComponent({ node, decorations, innerDecorations }) | ||
| updateProps: () => rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }) | ||
| }); | ||
@@ -980,3 +1002,3 @@ } | ||
| this.innerDecorations = innerDecorations; | ||
| rerenderComponent({ node, decorations, innerDecorations }); | ||
| rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }); | ||
| return true; | ||
@@ -1043,2 +1065,285 @@ } | ||
| // src/Tiptap.tsx | ||
| import { createContext as createContext3, useContext as useContext3, useEffect as useEffect5, useMemo as useMemo2, useState as useState5 } from "react"; | ||
| // src/menus/BubbleMenu.tsx | ||
| import { BubbleMenuPlugin } from "@tiptap/extension-bubble-menu"; | ||
| import { useCurrentEditor as useCurrentEditor2 } from "@tiptap/react"; | ||
| import React5, { useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react"; | ||
| import { createPortal } from "react-dom"; | ||
| import { jsx as jsx8 } from "react/jsx-runtime"; | ||
| var BubbleMenu = React5.forwardRef( | ||
| ({ | ||
| pluginKey = "bubbleMenu", | ||
| editor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| shouldShow = null, | ||
| getReferencedVirtualElement, | ||
| options, | ||
| children, | ||
| ...restProps | ||
| }, ref) => { | ||
| const menuEl = useRef2(document.createElement("div")); | ||
| if (typeof ref === "function") { | ||
| ref(menuEl.current); | ||
| } else if (ref) { | ||
| ref.current = menuEl.current; | ||
| } | ||
| const { editor: currentEditor } = useCurrentEditor2(); | ||
| const pluginEditor = editor || currentEditor; | ||
| const bubbleMenuPluginProps = { | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| pluginKey, | ||
| shouldShow, | ||
| getReferencedVirtualElement, | ||
| options | ||
| }; | ||
| const bubbleMenuPluginPropsRef = useRef2(bubbleMenuPluginProps); | ||
| bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps; | ||
| const [pluginInitialized, setPluginInitialized] = useState3(false); | ||
| const skipFirstUpdateRef = useRef2(true); | ||
| useEffect3(() => { | ||
| if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (!pluginEditor) { | ||
| console.warn("BubbleMenu component is not rendered inside of an editor component or does not have editor prop."); | ||
| return; | ||
| } | ||
| const bubbleMenuElement = menuEl.current; | ||
| bubbleMenuElement.style.visibility = "hidden"; | ||
| bubbleMenuElement.style.position = "absolute"; | ||
| const plugin = BubbleMenuPlugin({ | ||
| ...bubbleMenuPluginPropsRef.current, | ||
| editor: pluginEditor, | ||
| element: bubbleMenuElement | ||
| }); | ||
| pluginEditor.registerPlugin(plugin); | ||
| const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey; | ||
| skipFirstUpdateRef.current = true; | ||
| setPluginInitialized(true); | ||
| return () => { | ||
| setPluginInitialized(false); | ||
| pluginEditor.unregisterPlugin(createdPluginKey); | ||
| window.requestAnimationFrame(() => { | ||
| if (bubbleMenuElement.parentNode) { | ||
| bubbleMenuElement.parentNode.removeChild(bubbleMenuElement); | ||
| } | ||
| }); | ||
| }; | ||
| }, [pluginEditor]); | ||
| useEffect3(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false; | ||
| return; | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta("bubbleMenu", { | ||
| type: "updateOptions", | ||
| options: bubbleMenuPluginPropsRef.current | ||
| }) | ||
| ); | ||
| }, [ | ||
| pluginInitialized, | ||
| pluginEditor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| shouldShow, | ||
| options, | ||
| appendTo, | ||
| getReferencedVirtualElement | ||
| ]); | ||
| return createPortal(/* @__PURE__ */ jsx8("div", { ...restProps, children }), menuEl.current); | ||
| } | ||
| ); | ||
| // src/menus/FloatingMenu.tsx | ||
| import { FloatingMenuPlugin } from "@tiptap/extension-floating-menu"; | ||
| import { useCurrentEditor as useCurrentEditor3 } from "@tiptap/react"; | ||
| import React6, { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react"; | ||
| import { createPortal as createPortal2 } from "react-dom"; | ||
| import { jsx as jsx9 } from "react/jsx-runtime"; | ||
| var FloatingMenu = React6.forwardRef( | ||
| ({ | ||
| pluginKey = "floatingMenu", | ||
| editor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| shouldShow = null, | ||
| options, | ||
| children, | ||
| ...restProps | ||
| }, ref) => { | ||
| const menuEl = useRef3(document.createElement("div")); | ||
| if (typeof ref === "function") { | ||
| ref(menuEl.current); | ||
| } else if (ref) { | ||
| ref.current = menuEl.current; | ||
| } | ||
| const { editor: currentEditor } = useCurrentEditor3(); | ||
| const pluginEditor = editor || currentEditor; | ||
| const floatingMenuPluginProps = { | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| pluginKey, | ||
| shouldShow, | ||
| options | ||
| }; | ||
| const floatingMenuPluginPropsRef = useRef3(floatingMenuPluginProps); | ||
| floatingMenuPluginPropsRef.current = floatingMenuPluginProps; | ||
| const [pluginInitialized, setPluginInitialized] = useState4(false); | ||
| const skipFirstUpdateRef = useRef3(true); | ||
| useEffect4(() => { | ||
| if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (!pluginEditor) { | ||
| console.warn( | ||
| "FloatingMenu component is not rendered inside of an editor component or does not have editor prop." | ||
| ); | ||
| return; | ||
| } | ||
| const floatingMenuElement = menuEl.current; | ||
| floatingMenuElement.style.visibility = "hidden"; | ||
| floatingMenuElement.style.position = "absolute"; | ||
| const plugin = FloatingMenuPlugin({ | ||
| ...floatingMenuPluginPropsRef.current, | ||
| editor: pluginEditor, | ||
| element: floatingMenuElement | ||
| }); | ||
| pluginEditor.registerPlugin(plugin); | ||
| const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey; | ||
| skipFirstUpdateRef.current = true; | ||
| setPluginInitialized(true); | ||
| return () => { | ||
| setPluginInitialized(false); | ||
| pluginEditor.unregisterPlugin(createdPluginKey); | ||
| window.requestAnimationFrame(() => { | ||
| if (floatingMenuElement.parentNode) { | ||
| floatingMenuElement.parentNode.removeChild(floatingMenuElement); | ||
| } | ||
| }); | ||
| }; | ||
| }, [pluginEditor]); | ||
| useEffect4(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false; | ||
| return; | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta("floatingMenu", { | ||
| type: "updateOptions", | ||
| options: floatingMenuPluginPropsRef.current | ||
| }) | ||
| ); | ||
| }, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo]); | ||
| return createPortal2(/* @__PURE__ */ jsx9("div", { ...restProps, children }), menuEl.current); | ||
| } | ||
| ); | ||
| // src/Tiptap.tsx | ||
| import { jsx as jsx10 } from "react/jsx-runtime"; | ||
| var TiptapContext = createContext3({ | ||
| editor: null, | ||
| isReady: false | ||
| }); | ||
| TiptapContext.displayName = "TiptapContext"; | ||
| var useTiptap = () => useContext3(TiptapContext); | ||
| function useTiptapState(selector, equalityFn) { | ||
| const { editor } = useTiptap(); | ||
| return useEditorState({ | ||
| editor, | ||
| selector, | ||
| equalityFn | ||
| }); | ||
| } | ||
| function TiptapWrapper({ instance, children }) { | ||
| var _a; | ||
| const [isReady, setIsReady] = useState5((_a = instance == null ? void 0 : instance.isInitialized) != null ? _a : false); | ||
| useEffect5(() => { | ||
| if (!instance) { | ||
| setIsReady(false); | ||
| return; | ||
| } | ||
| if (instance.isInitialized) { | ||
| setIsReady(true); | ||
| return; | ||
| } | ||
| const handleCreate = () => { | ||
| setIsReady(true); | ||
| }; | ||
| instance.on("create", handleCreate); | ||
| return () => { | ||
| instance.off("create", handleCreate); | ||
| }; | ||
| }, [instance]); | ||
| const tiptapContextValue = useMemo2(() => ({ editor: instance, isReady }), [instance, isReady]); | ||
| const legacyContextValue = useMemo2(() => ({ editor: instance }), [instance]); | ||
| return /* @__PURE__ */ jsx10(EditorContext.Provider, { value: legacyContextValue, children: /* @__PURE__ */ jsx10(TiptapContext.Provider, { value: tiptapContextValue, children }) }); | ||
| } | ||
| TiptapWrapper.displayName = "Tiptap"; | ||
| function TiptapContent({ ...rest }) { | ||
| const { editor } = useTiptap(); | ||
| return /* @__PURE__ */ jsx10(EditorContent, { editor, ...rest }); | ||
| } | ||
| TiptapContent.displayName = "Tiptap.Content"; | ||
| function TiptapLoading({ children }) { | ||
| const { isReady } = useTiptap(); | ||
| if (isReady) { | ||
| return null; | ||
| } | ||
| return children; | ||
| } | ||
| TiptapLoading.displayName = "Tiptap.Loading"; | ||
| function TiptapBubbleMenu({ children, ...rest }) { | ||
| const { editor } = useTiptap(); | ||
| if (!editor) { | ||
| return null; | ||
| } | ||
| return /* @__PURE__ */ jsx10(BubbleMenu, { editor, ...rest, children }); | ||
| } | ||
| TiptapBubbleMenu.displayName = "Tiptap.BubbleMenu"; | ||
| function TiptapFloatingMenu({ children, ...rest }) { | ||
| const { editor } = useTiptap(); | ||
| if (!editor) { | ||
| return null; | ||
| } | ||
| return /* @__PURE__ */ jsx10(FloatingMenu, { ...rest, editor, children }); | ||
| } | ||
| TiptapFloatingMenu.displayName = "Tiptap.FloatingMenu"; | ||
| var Tiptap = Object.assign(TiptapWrapper, { | ||
| /** | ||
| * The Tiptap Content component that renders the EditorContent with the editor instance from the context. | ||
| * @see TiptapContent | ||
| */ | ||
| Content: TiptapContent, | ||
| /** | ||
| * The Tiptap Loading component that renders its children only when the editor is not ready. | ||
| * @see TiptapLoading | ||
| */ | ||
| Loading: TiptapLoading, | ||
| /** | ||
| * The Tiptap BubbleMenu component that wraps the BubbleMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapBubbleMenu | ||
| */ | ||
| BubbleMenu: TiptapBubbleMenu, | ||
| /** | ||
| * The Tiptap FloatingMenu component that wraps the FloatingMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapFloatingMenu | ||
| */ | ||
| FloatingMenu: TiptapFloatingMenu | ||
| }); | ||
| // src/index.ts | ||
@@ -1063,7 +1368,16 @@ export * from "@tiptap/core"; | ||
| ReactRenderer, | ||
| Tiptap, | ||
| TiptapBubbleMenu, | ||
| TiptapContent, | ||
| TiptapContext, | ||
| TiptapFloatingMenu, | ||
| TiptapLoading, | ||
| TiptapWrapper, | ||
| useCurrentEditor, | ||
| useEditor, | ||
| useEditorState, | ||
| useReactNodeView | ||
| useReactNodeView, | ||
| useTiptap, | ||
| useTiptapState | ||
| }; | ||
| //# sourceMappingURL=index.js.map |
+72
-17
@@ -76,2 +76,4 @@ "use strict"; | ||
| bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps; | ||
| const [pluginInitialized, setPluginInitialized] = (0, import_react2.useState)(false); | ||
| const skipFirstUpdateRef = (0, import_react2.useRef)(true); | ||
| (0, import_react2.useEffect)(() => { | ||
@@ -95,3 +97,6 @@ if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) { | ||
| const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey; | ||
| skipFirstUpdateRef.current = true; | ||
| setPluginInitialized(true); | ||
| return () => { | ||
| setPluginInitialized(false); | ||
| pluginEditor.unregisterPlugin(createdPluginKey); | ||
@@ -105,2 +110,26 @@ window.requestAnimationFrame(() => { | ||
| }, [pluginEditor]); | ||
| (0, import_react2.useEffect)(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false; | ||
| return; | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta("bubbleMenu", { | ||
| type: "updateOptions", | ||
| options: bubbleMenuPluginPropsRef.current | ||
| }) | ||
| ); | ||
| }, [ | ||
| pluginInitialized, | ||
| pluginEditor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| shouldShow, | ||
| options, | ||
| appendTo, | ||
| getReferencedVirtualElement | ||
| ]); | ||
| return (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...restProps, children }), menuEl.current); | ||
@@ -135,11 +164,20 @@ } | ||
| const { editor: currentEditor } = (0, import_react3.useCurrentEditor)(); | ||
| const pluginEditor = editor || currentEditor; | ||
| const floatingMenuPluginProps = { | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| pluginKey, | ||
| shouldShow, | ||
| options | ||
| }; | ||
| const floatingMenuPluginPropsRef = (0, import_react4.useRef)(floatingMenuPluginProps); | ||
| floatingMenuPluginPropsRef.current = floatingMenuPluginProps; | ||
| const [pluginInitialized, setPluginInitialized] = (0, import_react4.useState)(false); | ||
| const skipFirstUpdateRef = (0, import_react4.useRef)(true); | ||
| (0, import_react4.useEffect)(() => { | ||
| const floatingMenuElement = menuEl.current; | ||
| floatingMenuElement.style.visibility = "hidden"; | ||
| floatingMenuElement.style.position = "absolute"; | ||
| if ((editor == null ? void 0 : editor.isDestroyed) || (currentEditor == null ? void 0 : currentEditor.isDestroyed)) { | ||
| if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| const attachToEditor = editor || currentEditor; | ||
| if (!attachToEditor) { | ||
| if (!pluginEditor) { | ||
| console.warn( | ||
@@ -150,15 +188,17 @@ "FloatingMenu component is not rendered inside of an editor component or does not have editor prop." | ||
| } | ||
| const floatingMenuElement = menuEl.current; | ||
| floatingMenuElement.style.visibility = "hidden"; | ||
| floatingMenuElement.style.position = "absolute"; | ||
| const plugin = (0, import_extension_floating_menu.FloatingMenuPlugin)({ | ||
| editor: attachToEditor, | ||
| element: floatingMenuElement, | ||
| pluginKey, | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| shouldShow, | ||
| options | ||
| ...floatingMenuPluginPropsRef.current, | ||
| editor: pluginEditor, | ||
| element: floatingMenuElement | ||
| }); | ||
| attachToEditor.registerPlugin(plugin); | ||
| pluginEditor.registerPlugin(plugin); | ||
| const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey; | ||
| skipFirstUpdateRef.current = true; | ||
| setPluginInitialized(true); | ||
| return () => { | ||
| attachToEditor.unregisterPlugin(pluginKey); | ||
| setPluginInitialized(false); | ||
| pluginEditor.unregisterPlugin(createdPluginKey); | ||
| window.requestAnimationFrame(() => { | ||
@@ -170,3 +210,18 @@ if (floatingMenuElement.parentNode) { | ||
| }; | ||
| }, [editor, currentEditor, appendTo, pluginKey, shouldShow, options, updateDelay, resizeDelay]); | ||
| }, [pluginEditor]); | ||
| (0, import_react4.useEffect)(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false; | ||
| return; | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta("floatingMenu", { | ||
| type: "updateOptions", | ||
| options: floatingMenuPluginPropsRef.current | ||
| }) | ||
| ); | ||
| }, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo]); | ||
| return (0, import_react_dom2.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ...restProps, children }), menuEl.current); | ||
@@ -173,0 +228,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../../src/menus/index.ts","../../src/menus/BubbleMenu.tsx","../../src/menus/FloatingMenu.tsx"],"sourcesContent":["export * from './BubbleMenu.js'\nexport * from './FloatingMenu.js'\n","import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &\n React.HTMLAttributes<HTMLDivElement>\n\nexport const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(\n (\n {\n pluginKey = 'bubbleMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n getReferencedVirtualElement,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the bubble menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const bubbleMenuPluginProps: Omit<BubbleMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n getReferencedVirtualElement,\n options,\n }\n /**\n * The props for the bubble menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const bubbleMenuPluginPropsRef = useRef(bubbleMenuPluginProps)\n bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')\n return\n }\n\n const bubbleMenuElement = menuEl.current\n bubbleMenuElement.style.visibility = 'hidden'\n bubbleMenuElement.style.position = 'absolute'\n\n const plugin = BubbleMenuPlugin({\n ...bubbleMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: bubbleMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey\n\n return () => {\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (bubbleMenuElement.parentNode) {\n bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n","import type { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'\nimport { FloatingMenuPlugin } from '@tiptap/extension-floating-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {\n editor: FloatingMenuPluginProps['editor'] | null\n options?: FloatingMenuPluginProps['options']\n} & React.HTMLAttributes<HTMLDivElement>\n\nexport const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(\n (\n {\n pluginKey = 'floatingMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n useEffect(() => {\n const floatingMenuElement = menuEl.current\n\n floatingMenuElement.style.visibility = 'hidden'\n floatingMenuElement.style.position = 'absolute'\n\n if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {\n return\n }\n\n const attachToEditor = editor || currentEditor\n\n if (!attachToEditor) {\n console.warn(\n 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',\n )\n return\n }\n\n const plugin = FloatingMenuPlugin({\n editor: attachToEditor,\n element: floatingMenuElement,\n pluginKey,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow,\n options,\n })\n\n attachToEditor.registerPlugin(plugin)\n\n return () => {\n attachToEditor.unregisterPlugin(pluginKey)\n window.requestAnimationFrame(() => {\n if (floatingMenuElement.parentNode) {\n floatingMenuElement.parentNode.removeChild(floatingMenuElement)\n }\n })\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editor, currentEditor, appendTo, pluginKey, shouldShow, options, updateDelay, resizeDelay])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n"],"mappings":"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","names":["import_react","React","import_react","import_react_dom","import_jsx_runtime","React"]} | ||
| {"version":3,"sources":["../../src/menus/index.ts","../../src/menus/BubbleMenu.tsx","../../src/menus/FloatingMenu.tsx"],"sourcesContent":["export * from './BubbleMenu.js'\nexport * from './FloatingMenu.js'\n","import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &\n React.HTMLAttributes<HTMLDivElement>\n\nexport const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(\n (\n {\n pluginKey = 'bubbleMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n getReferencedVirtualElement,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the bubble menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const bubbleMenuPluginProps: Omit<BubbleMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n getReferencedVirtualElement,\n options,\n }\n /**\n * The props for the bubble menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const bubbleMenuPluginPropsRef = useRef(bubbleMenuPluginProps)\n bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps\n\n /**\n * Track whether the plugin has been initialized, so we only send updates\n * after the initial registration.\n */\n const [pluginInitialized, setPluginInitialized] = useState(false)\n\n /**\n * Track whether we need to skip the first options update dispatch.\n * This prevents unnecessary updates right after plugin initialization.\n */\n const skipFirstUpdateRef = useRef(true)\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')\n return\n }\n\n const bubbleMenuElement = menuEl.current\n bubbleMenuElement.style.visibility = 'hidden'\n bubbleMenuElement.style.position = 'absolute'\n\n const plugin = BubbleMenuPlugin({\n ...bubbleMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: bubbleMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey\n\n skipFirstUpdateRef.current = true\n setPluginInitialized(true)\n\n return () => {\n setPluginInitialized(false)\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (bubbleMenuElement.parentNode) {\n bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n /**\n * Update the plugin options when props change after the plugin has been initialized.\n * This allows dynamic updates to options like scrollTarget without re-registering the entire plugin.\n */\n useEffect(() => {\n if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {\n return\n }\n\n // Skip the first update right after initialization since the plugin was just created with these options\n if (skipFirstUpdateRef.current) {\n skipFirstUpdateRef.current = false\n return\n }\n\n pluginEditor.view.dispatch(\n pluginEditor.state.tr.setMeta('bubbleMenu', {\n type: 'updateOptions',\n options: bubbleMenuPluginPropsRef.current,\n }),\n )\n }, [\n pluginInitialized,\n pluginEditor,\n updateDelay,\n resizeDelay,\n shouldShow,\n options,\n appendTo,\n getReferencedVirtualElement,\n ])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n","import type { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'\nimport { FloatingMenuPlugin } from '@tiptap/extension-floating-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {\n editor: FloatingMenuPluginProps['editor'] | null\n options?: FloatingMenuPluginProps['options']\n} & React.HTMLAttributes<HTMLDivElement>\n\nexport const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(\n (\n {\n pluginKey = 'floatingMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the floating menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const floatingMenuPluginProps: Omit<FloatingMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n options,\n }\n\n /**\n * The props for the floating menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const floatingMenuPluginPropsRef = useRef(floatingMenuPluginProps)\n floatingMenuPluginPropsRef.current = floatingMenuPluginProps\n\n /**\n * Track whether the plugin has been initialized, so we only send updates\n * after the initial registration.\n */\n const [pluginInitialized, setPluginInitialized] = useState(false)\n\n /**\n * Track whether we need to skip the first options update dispatch.\n * This prevents unnecessary updates right after plugin initialization.\n */\n const skipFirstUpdateRef = useRef(true)\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn(\n 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',\n )\n return\n }\n\n const floatingMenuElement = menuEl.current\n floatingMenuElement.style.visibility = 'hidden'\n floatingMenuElement.style.position = 'absolute'\n\n const plugin = FloatingMenuPlugin({\n ...floatingMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: floatingMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey\n\n skipFirstUpdateRef.current = true\n setPluginInitialized(true)\n\n return () => {\n setPluginInitialized(false)\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (floatingMenuElement.parentNode) {\n floatingMenuElement.parentNode.removeChild(floatingMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n /**\n * Update the plugin options when props change after the plugin has been initialized.\n * This allows dynamic updates to options like scrollTarget without re-registering the entire plugin.\n */\n useEffect(() => {\n if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {\n return\n }\n\n // Skip the first update right after initialization since the plugin was just created with these options\n if (skipFirstUpdateRef.current) {\n skipFirstUpdateRef.current = false\n return\n }\n\n pluginEditor.view.dispatch(\n pluginEditor.state.tr.setMeta('floatingMenu', {\n type: 'updateOptions',\n options: floatingMenuPluginPropsRef.current,\n }),\n )\n }, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n"],"mappings":"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","names":["import_react","React","import_react","import_react_dom","import_jsx_runtime","React"]} |
+74
-19
| // src/menus/BubbleMenu.tsx | ||
| import { BubbleMenuPlugin } from "@tiptap/extension-bubble-menu"; | ||
| import { useCurrentEditor } from "@tiptap/react"; | ||
| import React, { useEffect, useRef } from "react"; | ||
| import React, { useEffect, useRef, useState } from "react"; | ||
| import { createPortal } from "react-dom"; | ||
@@ -39,2 +39,4 @@ import { jsx } from "react/jsx-runtime"; | ||
| bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps; | ||
| const [pluginInitialized, setPluginInitialized] = useState(false); | ||
| const skipFirstUpdateRef = useRef(true); | ||
| useEffect(() => { | ||
@@ -58,3 +60,6 @@ if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) { | ||
| const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey; | ||
| skipFirstUpdateRef.current = true; | ||
| setPluginInitialized(true); | ||
| return () => { | ||
| setPluginInitialized(false); | ||
| pluginEditor.unregisterPlugin(createdPluginKey); | ||
@@ -68,2 +73,26 @@ window.requestAnimationFrame(() => { | ||
| }, [pluginEditor]); | ||
| useEffect(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false; | ||
| return; | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta("bubbleMenu", { | ||
| type: "updateOptions", | ||
| options: bubbleMenuPluginPropsRef.current | ||
| }) | ||
| ); | ||
| }, [ | ||
| pluginInitialized, | ||
| pluginEditor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| shouldShow, | ||
| options, | ||
| appendTo, | ||
| getReferencedVirtualElement | ||
| ]); | ||
| return createPortal(/* @__PURE__ */ jsx("div", { ...restProps, children }), menuEl.current); | ||
@@ -76,3 +105,3 @@ } | ||
| import { useCurrentEditor as useCurrentEditor2 } from "@tiptap/react"; | ||
| import React2, { useEffect as useEffect2, useRef as useRef2 } from "react"; | ||
| import React2, { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react"; | ||
| import { createPortal as createPortal2 } from "react-dom"; | ||
@@ -99,11 +128,20 @@ import { jsx as jsx2 } from "react/jsx-runtime"; | ||
| const { editor: currentEditor } = useCurrentEditor2(); | ||
| const pluginEditor = editor || currentEditor; | ||
| const floatingMenuPluginProps = { | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| pluginKey, | ||
| shouldShow, | ||
| options | ||
| }; | ||
| const floatingMenuPluginPropsRef = useRef2(floatingMenuPluginProps); | ||
| floatingMenuPluginPropsRef.current = floatingMenuPluginProps; | ||
| const [pluginInitialized, setPluginInitialized] = useState2(false); | ||
| const skipFirstUpdateRef = useRef2(true); | ||
| useEffect2(() => { | ||
| const floatingMenuElement = menuEl.current; | ||
| floatingMenuElement.style.visibility = "hidden"; | ||
| floatingMenuElement.style.position = "absolute"; | ||
| if ((editor == null ? void 0 : editor.isDestroyed) || (currentEditor == null ? void 0 : currentEditor.isDestroyed)) { | ||
| if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| const attachToEditor = editor || currentEditor; | ||
| if (!attachToEditor) { | ||
| if (!pluginEditor) { | ||
| console.warn( | ||
@@ -114,15 +152,17 @@ "FloatingMenu component is not rendered inside of an editor component or does not have editor prop." | ||
| } | ||
| const floatingMenuElement = menuEl.current; | ||
| floatingMenuElement.style.visibility = "hidden"; | ||
| floatingMenuElement.style.position = "absolute"; | ||
| const plugin = FloatingMenuPlugin({ | ||
| editor: attachToEditor, | ||
| element: floatingMenuElement, | ||
| pluginKey, | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| shouldShow, | ||
| options | ||
| ...floatingMenuPluginPropsRef.current, | ||
| editor: pluginEditor, | ||
| element: floatingMenuElement | ||
| }); | ||
| attachToEditor.registerPlugin(plugin); | ||
| pluginEditor.registerPlugin(plugin); | ||
| const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey; | ||
| skipFirstUpdateRef.current = true; | ||
| setPluginInitialized(true); | ||
| return () => { | ||
| attachToEditor.unregisterPlugin(pluginKey); | ||
| setPluginInitialized(false); | ||
| pluginEditor.unregisterPlugin(createdPluginKey); | ||
| window.requestAnimationFrame(() => { | ||
@@ -134,3 +174,18 @@ if (floatingMenuElement.parentNode) { | ||
| }; | ||
| }, [editor, currentEditor, appendTo, pluginKey, shouldShow, options, updateDelay, resizeDelay]); | ||
| }, [pluginEditor]); | ||
| useEffect2(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return; | ||
| } | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false; | ||
| return; | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta("floatingMenu", { | ||
| type: "updateOptions", | ||
| options: floatingMenuPluginPropsRef.current | ||
| }) | ||
| ); | ||
| }, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo]); | ||
| return createPortal2(/* @__PURE__ */ jsx2("div", { ...restProps, children }), menuEl.current); | ||
@@ -137,0 +192,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../../src/menus/BubbleMenu.tsx","../../src/menus/FloatingMenu.tsx"],"sourcesContent":["import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &\n React.HTMLAttributes<HTMLDivElement>\n\nexport const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(\n (\n {\n pluginKey = 'bubbleMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n getReferencedVirtualElement,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the bubble menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const bubbleMenuPluginProps: Omit<BubbleMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n getReferencedVirtualElement,\n options,\n }\n /**\n * The props for the bubble menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const bubbleMenuPluginPropsRef = useRef(bubbleMenuPluginProps)\n bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')\n return\n }\n\n const bubbleMenuElement = menuEl.current\n bubbleMenuElement.style.visibility = 'hidden'\n bubbleMenuElement.style.position = 'absolute'\n\n const plugin = BubbleMenuPlugin({\n ...bubbleMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: bubbleMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey\n\n return () => {\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (bubbleMenuElement.parentNode) {\n bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n","import type { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'\nimport { FloatingMenuPlugin } from '@tiptap/extension-floating-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {\n editor: FloatingMenuPluginProps['editor'] | null\n options?: FloatingMenuPluginProps['options']\n} & React.HTMLAttributes<HTMLDivElement>\n\nexport const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(\n (\n {\n pluginKey = 'floatingMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n useEffect(() => {\n const floatingMenuElement = menuEl.current\n\n floatingMenuElement.style.visibility = 'hidden'\n floatingMenuElement.style.position = 'absolute'\n\n if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {\n return\n }\n\n const attachToEditor = editor || currentEditor\n\n if (!attachToEditor) {\n console.warn(\n 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',\n )\n return\n }\n\n const plugin = FloatingMenuPlugin({\n editor: attachToEditor,\n element: floatingMenuElement,\n pluginKey,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow,\n options,\n })\n\n attachToEditor.registerPlugin(plugin)\n\n return () => {\n attachToEditor.unregisterPlugin(pluginKey)\n window.requestAnimationFrame(() => {\n if (floatingMenuElement.parentNode) {\n floatingMenuElement.parentNode.removeChild(floatingMenuElement)\n }\n })\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editor, currentEditor, appendTo, pluginKey, shouldShow, options, updateDelay, resizeDelay])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n"],"mappings":"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","names":["useCurrentEditor","React","useEffect","useRef","createPortal","jsx"]} | ||
| {"version":3,"sources":["../../src/menus/BubbleMenu.tsx","../../src/menus/FloatingMenu.tsx"],"sourcesContent":["import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &\n React.HTMLAttributes<HTMLDivElement>\n\nexport const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(\n (\n {\n pluginKey = 'bubbleMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n getReferencedVirtualElement,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the bubble menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const bubbleMenuPluginProps: Omit<BubbleMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n getReferencedVirtualElement,\n options,\n }\n /**\n * The props for the bubble menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const bubbleMenuPluginPropsRef = useRef(bubbleMenuPluginProps)\n bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps\n\n /**\n * Track whether the plugin has been initialized, so we only send updates\n * after the initial registration.\n */\n const [pluginInitialized, setPluginInitialized] = useState(false)\n\n /**\n * Track whether we need to skip the first options update dispatch.\n * This prevents unnecessary updates right after plugin initialization.\n */\n const skipFirstUpdateRef = useRef(true)\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')\n return\n }\n\n const bubbleMenuElement = menuEl.current\n bubbleMenuElement.style.visibility = 'hidden'\n bubbleMenuElement.style.position = 'absolute'\n\n const plugin = BubbleMenuPlugin({\n ...bubbleMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: bubbleMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey\n\n skipFirstUpdateRef.current = true\n setPluginInitialized(true)\n\n return () => {\n setPluginInitialized(false)\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (bubbleMenuElement.parentNode) {\n bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n /**\n * Update the plugin options when props change after the plugin has been initialized.\n * This allows dynamic updates to options like scrollTarget without re-registering the entire plugin.\n */\n useEffect(() => {\n if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {\n return\n }\n\n // Skip the first update right after initialization since the plugin was just created with these options\n if (skipFirstUpdateRef.current) {\n skipFirstUpdateRef.current = false\n return\n }\n\n pluginEditor.view.dispatch(\n pluginEditor.state.tr.setMeta('bubbleMenu', {\n type: 'updateOptions',\n options: bubbleMenuPluginPropsRef.current,\n }),\n )\n }, [\n pluginInitialized,\n pluginEditor,\n updateDelay,\n resizeDelay,\n shouldShow,\n options,\n appendTo,\n getReferencedVirtualElement,\n ])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n","import type { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'\nimport { FloatingMenuPlugin } from '@tiptap/extension-floating-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {\n editor: FloatingMenuPluginProps['editor'] | null\n options?: FloatingMenuPluginProps['options']\n} & React.HTMLAttributes<HTMLDivElement>\n\nexport const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(\n (\n {\n pluginKey = 'floatingMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the floating menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const floatingMenuPluginProps: Omit<FloatingMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n options,\n }\n\n /**\n * The props for the floating menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const floatingMenuPluginPropsRef = useRef(floatingMenuPluginProps)\n floatingMenuPluginPropsRef.current = floatingMenuPluginProps\n\n /**\n * Track whether the plugin has been initialized, so we only send updates\n * after the initial registration.\n */\n const [pluginInitialized, setPluginInitialized] = useState(false)\n\n /**\n * Track whether we need to skip the first options update dispatch.\n * This prevents unnecessary updates right after plugin initialization.\n */\n const skipFirstUpdateRef = useRef(true)\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn(\n 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',\n )\n return\n }\n\n const floatingMenuElement = menuEl.current\n floatingMenuElement.style.visibility = 'hidden'\n floatingMenuElement.style.position = 'absolute'\n\n const plugin = FloatingMenuPlugin({\n ...floatingMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: floatingMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey\n\n skipFirstUpdateRef.current = true\n setPluginInitialized(true)\n\n return () => {\n setPluginInitialized(false)\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (floatingMenuElement.parentNode) {\n floatingMenuElement.parentNode.removeChild(floatingMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n /**\n * Update the plugin options when props change after the plugin has been initialized.\n * This allows dynamic updates to options like scrollTarget without re-registering the entire plugin.\n */\n useEffect(() => {\n if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {\n return\n }\n\n // Skip the first update right after initialization since the plugin was just created with these options\n if (skipFirstUpdateRef.current) {\n skipFirstUpdateRef.current = false\n return\n }\n\n pluginEditor.view.dispatch(\n pluginEditor.state.tr.setMeta('floatingMenu', {\n type: 'updateOptions',\n options: floatingMenuPluginPropsRef.current,\n }),\n )\n }, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n"],"mappings":"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","names":["useCurrentEditor","React","useEffect","useRef","useState","createPortal","jsx"]} |
+7
-7
| { | ||
| "name": "@tiptap/react", | ||
| "description": "React components for tiptap", | ||
| "version": "3.17.1", | ||
| "version": "3.18.0", | ||
| "homepage": "https://tiptap.dev", | ||
@@ -51,8 +51,8 @@ "keywords": [ | ||
| "react-dom": "^19.0.0", | ||
| "@tiptap/core": "^3.17.1", | ||
| "@tiptap/pm": "^3.17.1" | ||
| "@tiptap/core": "^3.18.0", | ||
| "@tiptap/pm": "^3.18.0" | ||
| }, | ||
| "optionalDependencies": { | ||
| "@tiptap/extension-bubble-menu": "^3.17.1", | ||
| "@tiptap/extension-floating-menu": "^3.17.1" | ||
| "@tiptap/extension-bubble-menu": "^3.18.0", | ||
| "@tiptap/extension-floating-menu": "^3.18.0" | ||
| }, | ||
@@ -64,4 +64,4 @@ "peerDependencies": { | ||
| "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", | ||
| "@tiptap/pm": "^3.17.1", | ||
| "@tiptap/core": "^3.17.1" | ||
| "@tiptap/core": "^3.18.0", | ||
| "@tiptap/pm": "^3.18.0" | ||
| }, | ||
@@ -68,0 +68,0 @@ "repository": { |
+1
-0
@@ -8,2 +8,3 @@ export * from './Context.js' | ||
| export * from './ReactRenderer.js' | ||
| export * from './Tiptap.js' | ||
| export * from './types.js' | ||
@@ -10,0 +11,0 @@ export * from './useEditor.js' |
| import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu' | ||
| import { useCurrentEditor } from '@tiptap/react' | ||
| import React, { useEffect, useRef } from 'react' | ||
| import React, { useEffect, useRef, useState } from 'react' | ||
| import { createPortal } from 'react-dom' | ||
@@ -61,2 +61,14 @@ | ||
| /** | ||
| * Track whether the plugin has been initialized, so we only send updates | ||
| * after the initial registration. | ||
| */ | ||
| const [pluginInitialized, setPluginInitialized] = useState(false) | ||
| /** | ||
| * Track whether we need to skip the first options update dispatch. | ||
| * This prevents unnecessary updates right after plugin initialization. | ||
| */ | ||
| const skipFirstUpdateRef = useRef(true) | ||
| useEffect(() => { | ||
@@ -86,3 +98,7 @@ if (pluginEditor?.isDestroyed) { | ||
| skipFirstUpdateRef.current = true | ||
| setPluginInitialized(true) | ||
| return () => { | ||
| setPluginInitialized(false) | ||
| pluginEditor.unregisterPlugin(createdPluginKey) | ||
@@ -97,4 +113,36 @@ window.requestAnimationFrame(() => { | ||
| /** | ||
| * Update the plugin options when props change after the plugin has been initialized. | ||
| * This allows dynamic updates to options like scrollTarget without re-registering the entire plugin. | ||
| */ | ||
| useEffect(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return | ||
| } | ||
| // Skip the first update right after initialization since the plugin was just created with these options | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false | ||
| return | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta('bubbleMenu', { | ||
| type: 'updateOptions', | ||
| options: bubbleMenuPluginPropsRef.current, | ||
| }), | ||
| ) | ||
| }, [ | ||
| pluginInitialized, | ||
| pluginEditor, | ||
| updateDelay, | ||
| resizeDelay, | ||
| shouldShow, | ||
| options, | ||
| appendTo, | ||
| getReferencedVirtualElement, | ||
| ]) | ||
| return createPortal(<div {...restProps}>{children}</div>, menuEl.current) | ||
| }, | ||
| ) |
| import type { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu' | ||
| import { FloatingMenuPlugin } from '@tiptap/extension-floating-menu' | ||
| import { useCurrentEditor } from '@tiptap/react' | ||
| import React, { useEffect, useRef } from 'react' | ||
| import React, { useEffect, useRef, useState } from 'react' | ||
| import { createPortal } from 'react-dom' | ||
@@ -39,15 +39,44 @@ | ||
| useEffect(() => { | ||
| const floatingMenuElement = menuEl.current | ||
| /** | ||
| * The editor instance where the floating menu plugin will be registered. | ||
| */ | ||
| const pluginEditor = editor || currentEditor | ||
| floatingMenuElement.style.visibility = 'hidden' | ||
| floatingMenuElement.style.position = 'absolute' | ||
| // Creating a useMemo would be more computationally expensive than just | ||
| // re-creating this object on every render. | ||
| const floatingMenuPluginProps: Omit<FloatingMenuPluginProps, 'editor' | 'element'> = { | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| pluginKey, | ||
| shouldShow, | ||
| options, | ||
| } | ||
| if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) { | ||
| /** | ||
| * The props for the floating menu plugin. They are accessed inside a ref to | ||
| * avoid running the useEffect hook and re-registering the plugin when the | ||
| * props change. | ||
| */ | ||
| const floatingMenuPluginPropsRef = useRef(floatingMenuPluginProps) | ||
| floatingMenuPluginPropsRef.current = floatingMenuPluginProps | ||
| /** | ||
| * Track whether the plugin has been initialized, so we only send updates | ||
| * after the initial registration. | ||
| */ | ||
| const [pluginInitialized, setPluginInitialized] = useState(false) | ||
| /** | ||
| * Track whether we need to skip the first options update dispatch. | ||
| * This prevents unnecessary updates right after plugin initialization. | ||
| */ | ||
| const skipFirstUpdateRef = useRef(true) | ||
| useEffect(() => { | ||
| if (pluginEditor?.isDestroyed) { | ||
| return | ||
| } | ||
| const attachToEditor = editor || currentEditor | ||
| if (!attachToEditor) { | ||
| if (!pluginEditor) { | ||
| console.warn( | ||
@@ -59,17 +88,22 @@ 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.', | ||
| const floatingMenuElement = menuEl.current | ||
| floatingMenuElement.style.visibility = 'hidden' | ||
| floatingMenuElement.style.position = 'absolute' | ||
| const plugin = FloatingMenuPlugin({ | ||
| editor: attachToEditor, | ||
| ...floatingMenuPluginPropsRef.current, | ||
| editor: pluginEditor, | ||
| element: floatingMenuElement, | ||
| pluginKey, | ||
| updateDelay, | ||
| resizeDelay, | ||
| appendTo, | ||
| shouldShow, | ||
| options, | ||
| }) | ||
| attachToEditor.registerPlugin(plugin) | ||
| pluginEditor.registerPlugin(plugin) | ||
| const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey | ||
| skipFirstUpdateRef.current = true | ||
| setPluginInitialized(true) | ||
| return () => { | ||
| attachToEditor.unregisterPlugin(pluginKey) | ||
| setPluginInitialized(false) | ||
| pluginEditor.unregisterPlugin(createdPluginKey) | ||
| window.requestAnimationFrame(() => { | ||
@@ -81,7 +115,29 @@ if (floatingMenuElement.parentNode) { | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [editor, currentEditor, appendTo, pluginKey, shouldShow, options, updateDelay, resizeDelay]) | ||
| }, [pluginEditor]) | ||
| /** | ||
| * Update the plugin options when props change after the plugin has been initialized. | ||
| * This allows dynamic updates to options like scrollTarget without re-registering the entire plugin. | ||
| */ | ||
| useEffect(() => { | ||
| if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) { | ||
| return | ||
| } | ||
| // Skip the first update right after initialization since the plugin was just created with these options | ||
| if (skipFirstUpdateRef.current) { | ||
| skipFirstUpdateRef.current = false | ||
| return | ||
| } | ||
| pluginEditor.view.dispatch( | ||
| pluginEditor.state.tr.setMeta('floatingMenu', { | ||
| type: 'updateOptions', | ||
| options: floatingMenuPluginPropsRef.current, | ||
| }), | ||
| ) | ||
| }, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo]) | ||
| return createPortal(<div {...restProps}>{children}</div>, menuEl.current) | ||
| }, | ||
| ) |
@@ -103,3 +103,28 @@ import type { | ||
| private cachedExtensionWithSyncedStorage: NodeViewRendererProps['extension'] | null = null | ||
| /** | ||
| * Returns a proxy of the extension that redirects storage access to the editor's mutable storage. | ||
| * This preserves the original prototype chain (instanceof checks, methods like configure/extend work). | ||
| * Cached to avoid proxy creation on every update. | ||
| */ | ||
| get extensionWithSyncedStorage(): NodeViewRendererProps['extension'] { | ||
| if (!this.cachedExtensionWithSyncedStorage) { | ||
| const editor = this.editor | ||
| const extension = this.extension | ||
| this.cachedExtensionWithSyncedStorage = new Proxy(extension, { | ||
| get(target, prop, receiver) { | ||
| if (prop === 'storage') { | ||
| return editor.storage[extension.name as keyof typeof editor.storage] ?? {} | ||
| } | ||
| return Reflect.get(target, prop, receiver) | ||
| }, | ||
| }) | ||
| } | ||
| return this.cachedExtensionWithSyncedStorage | ||
| } | ||
| /** | ||
| * Setup the React component. | ||
@@ -116,3 +141,3 @@ * Called on initialization. | ||
| selected: false, | ||
| extension: this.extension, | ||
| extension: this.extensionWithSyncedStorage, | ||
| HTMLAttributes: this.HTMLAttributes, | ||
@@ -271,3 +296,4 @@ getPos: () => this.getPos(), | ||
| innerDecorations, | ||
| updateProps: () => rerenderComponent({ node, decorations, innerDecorations }), | ||
| updateProps: () => | ||
| rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }), | ||
| }) | ||
@@ -284,3 +310,3 @@ } | ||
| rerenderComponent({ node, decorations, innerDecorations }) | ||
| rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }) | ||
@@ -287,0 +313,0 @@ return true |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
455866
36.89%32
3.23%5871
32.32%