diff --git a/apps/client/src/features/editor/components/audio/audio-menu.tsx b/apps/client/src/features/editor/components/audio/audio-menu.tsx index 3ca1950da..eadc1afe5 100644 --- a/apps/client/src/features/editor/components/audio/audio-menu.tsx +++ b/apps/client/src/features/editor/components/audio/audio-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -46,7 +47,7 @@ export function AudioMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "audio"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/callout/callout-menu.tsx b/apps/client/src/features/editor/components/callout/callout-menu.tsx index 69c836934..3ce022dae 100644 --- a/apps/client/src/features/editor/components/callout/callout-menu.tsx +++ b/apps/client/src/features/editor/components/callout/callout-menu.tsx @@ -16,7 +16,7 @@ import { IconMoodSmile, IconNotes, } from "@tabler/icons-react"; -import { CalloutType, isTextSelected } from "@docmost/editor-ext"; +import { CalloutType, isEditorReady, isTextSelected } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import EmojiPicker from "@/components/ui/emoji-picker.tsx"; import classes from "../common/toolbar-menu.module.css"; @@ -55,7 +55,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { }); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "callout"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/columns/columns-menu.tsx b/apps/client/src/features/editor/components/columns/columns-menu.tsx index 0ee99508c..4a1f041eb 100644 --- a/apps/client/src/features/editor/components/columns/columns-menu.tsx +++ b/apps/client/src/features/editor/components/columns/columns-menu.tsx @@ -19,7 +19,7 @@ import { IconCopy, IconTrash, } from "@tabler/icons-react"; -import { isTextSelected } from "@docmost/editor-ext"; +import { isEditorReady, isTextSelected } from "@docmost/editor-ext"; import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import classes from "../common/toolbar-menu.module.css"; @@ -82,7 +82,7 @@ export function ColumnsMenu({ editor }: EditorMenuProps) { const shouldShow = useCallback( ({ state }: ShouldShowProps) => { - if (!state) return false; + if (!state || !isEditorReady(editor)) return false; if (!editor.isActive("columns")) return false; if (isTextSelected(editor)) return false; if (nodesWithMenus.some((name) => editor.isActive(name))) return false; @@ -121,7 +121,7 @@ export function ColumnsMenu({ editor }: EditorMenuProps) { }); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "columns"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx index 869decd71..877911750 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -81,7 +82,7 @@ export function DrawioMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "drawio"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx index fd3128062..823c2c213 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -94,7 +95,7 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "excalidraw"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/image/image-menu.tsx b/apps/client/src/features/editor/components/image/image-menu.tsx index 666fab7dc..1b2d00e7e 100644 --- a/apps/client/src/features/editor/components/image/image-menu.tsx +++ b/apps/client/src/features/editor/components/image/image-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback, useRef } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -56,7 +57,7 @@ export function ImageMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "image"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/pdf/pdf-menu.tsx b/apps/client/src/features/editor/components/pdf/pdf-menu.tsx index 2104bfbc6..3fc8b6fd1 100644 --- a/apps/client/src/features/editor/components/pdf/pdf-menu.tsx +++ b/apps/client/src/features/editor/components/pdf/pdf-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -37,9 +38,8 @@ export function PdfMenu({ editor }: EditorMenuProps) { const shouldShow = useCallback( ({ state }: ShouldShowProps) => { - if (!state || !editor.isActive("pdf")) { - return false; - } + if (!state || !isEditorReady(editor)) return false; + if (!editor.isActive("pdf")) return false; const { selection } = state; const dom = editor.view.nodeDOM(selection.from) as HTMLElement | null; @@ -51,7 +51,7 @@ export function PdfMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "pdf"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx index 9f0544e67..a626e1ee2 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx @@ -6,6 +6,7 @@ import { ActionIcon, Tooltip } from "@mantine/core"; import { IconTrash } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { Editor } from "@tiptap/core"; +import { isEditorReady } from "@docmost/editor-ext"; interface SubpagesMenuProps { editor: Editor; @@ -33,6 +34,7 @@ export const SubpagesMenu = React.memo( ); const getReferenceClientRect = useCallback(() => { + if (!isEditorReady(editor)) return new DOMRect(); const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "subpages"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index 3be7ec539..92cc318e9 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -18,7 +18,7 @@ import { IconTrashX, } from "@tabler/icons-react"; import { BubbleMenu } from "@tiptap/react/menus"; -import { isCellSelection, isTextSelected } from "@docmost/editor-ext"; +import { isCellSelection, isEditorReady, isTextSelected } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import classes from "../common/toolbar-menu.module.css"; @@ -38,6 +38,7 @@ export const TableMenu = React.memo( ); const getReferencedVirtualElement = useCallback(() => { + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "table"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index 3f232625f..429e02f87 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -53,7 +54,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "video"; const parent = findParentNode(predicate)(selection); diff --git a/packages/editor-ext/src/lib/utils.ts b/packages/editor-ext/src/lib/utils.ts index 3bfd01778..8d03577c7 100644 --- a/packages/editor-ext/src/lib/utils.ts +++ b/packages/editor-ext/src/lib/utils.ts @@ -338,6 +338,15 @@ export const isRowGripSelected = ({ return !!gripRow; }; +// TipTap's `editor.view` proxy throws if accessed before mount or after destroy. +// Guard floating-menu callbacks (getReferencedVirtualElement, shouldShow) with +// this before touching `editor.view.nodeDOM(...)`. +export function isEditorReady( + editor: Editor | null | undefined, +): editor is Editor { + return !!editor && editor.isInitialized; +} + export function isTextSelected(editor: Editor) { const { state: {