diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index 5ad966af9..a1283e34a 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -23,7 +23,7 @@ import { } from "@/features/comment/atoms/comment-atom"; import { useAtom, useAtomValue } from "jotai"; import { v7 as uuid7 } from "uuid"; -import { isCellSelection, isTextSelected } from "@docmost/editor-ext"; +import { isCellSelection, isEditorReady, isTextSelected } from "@docmost/editor-ext"; import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx"; import { useTranslation } from "react-i18next"; import { showAiMenuAtom, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms"; @@ -226,7 +226,7 @@ export const EditorBubbleMenu: FC = (props) => { aria-label={t(item.name)} className={clsx({ [classes.active]: item.isActive() })} style={{ border: "none" }} - onClick={item.command} + onClick={() => isEditorReady(props.editor) && item.command()} > @@ -256,7 +256,7 @@ export const EditorBubbleMenu: FC = (props) => { radius="6px" aria-label={t(commentItem.name)} style={{ border: "none" }} - onClick={commentItem.command} + onClick={() => isEditorReady(props.editor) && commentItem.command()} > diff --git a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx index d9a643b8a..a228dc645 100644 --- a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx @@ -13,6 +13,7 @@ import { import type { Editor } from "@tiptap/react"; import { useEditorState } from "@tiptap/react"; import { useTranslation } from "react-i18next"; +import { isEditorReady } from "@docmost/editor-ext"; import clsx from "clsx"; import classes from "./bubble-menu.module.css"; @@ -253,6 +254,7 @@ export const ColorSelector: FC = ({ {TEXT_COLORS.map(({ name, color }, index) => { const applyTextColor = () => { + if (!isEditorReady(editor)) return; if (name === "Default") { editor.commands.unsetColor(); } else { @@ -316,6 +318,7 @@ export const ColorSelector: FC = ({ {HIGHLIGHT_COLORS.map(({ name, color }, index) => { const applyHighlight = () => { + if (!isEditorReady(editor)) return; if (name === "Default") { editor.commands.unsetHighlight(); } else { @@ -386,8 +389,10 @@ export const ColorSelector: FC = ({ data-color-grid="remove" className={classes.removeColor} onClick={() => { - editor.commands.unsetColor(); - editor.commands.unsetHighlight(); + if (isEditorReady(editor)) { + editor.commands.unsetColor(); + editor.commands.unsetHighlight(); + } setIsOpen(false); }} onKeyDown={(e) => { diff --git a/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx index 73fe722ad..3db4452b1 100644 --- a/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx @@ -19,6 +19,7 @@ import { Popover, Button, ScrollArea, Tooltip } from "@mantine/core"; import type { Editor } from "@tiptap/react"; import { useEditorState } from "@tiptap/react"; import { useTranslation } from "react-i18next"; +import { isEditorReady } from "@docmost/editor-ext"; import classes from "./bubble-menu.module.css"; interface NodeSelectorProps { @@ -193,7 +194,7 @@ export const NodeSelector: FC = ({ justify="left" fullWidth onClick={() => { - item.command(); + if (isEditorReady(editor)) item.command(); setIsOpen(false); }} style={{ border: "none" }} diff --git a/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx index ccfc48382..e5e419ae8 100644 --- a/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx @@ -11,6 +11,7 @@ import { Menu, Button, Tooltip, rem } from "@mantine/core"; import type { Editor } from "@tiptap/react"; import { useEditorState } from "@tiptap/react"; import { useTranslation } from "react-i18next"; +import { isEditorReady } from "@docmost/editor-ext"; interface TextAlignmentProps { editor: Editor | null; @@ -117,7 +118,7 @@ export const TextAlignmentSelector: FC = ({ activeItem.name === item.name ? : null } onClick={() => { - item.command(); + if (isEditorReady(editor)) item.command(); setIsOpen(false); }} > diff --git a/apps/client/src/features/editor/components/common/editor-paste-handler.tsx b/apps/client/src/features/editor/components/common/editor-paste-handler.tsx index b91770111..b01e2b810 100644 --- a/apps/client/src/features/editor/components/common/editor-paste-handler.tsx +++ b/apps/client/src/features/editor/components/common/editor-paste-handler.tsx @@ -183,6 +183,7 @@ async function reuploadPastedAttachments( ); if (reuploadResults.size === 0) return; + if (editor.isDestroyed) return; editor.chain().command(({ tr }) => { const sorted = [...nodesToReupload].sort((a, b) => b.pos - a.pos); diff --git a/apps/client/src/features/editor/components/fixed-toolbar/groups/block-type-group.tsx b/apps/client/src/features/editor/components/fixed-toolbar/groups/block-type-group.tsx index 69911f7cb..53192d5ec 100644 --- a/apps/client/src/features/editor/components/fixed-toolbar/groups/block-type-group.tsx +++ b/apps/client/src/features/editor/components/fixed-toolbar/groups/block-type-group.tsx @@ -25,11 +25,11 @@ export const BlockTypeGroup: FC = ({ editor }) => { const state = useEditorState({ editor, selector: (ctx) => ({ - isHeading1: ctx.editor.isActive("heading", { level: 1 }), - isHeading2: ctx.editor.isActive("heading", { level: 2 }), - isHeading3: ctx.editor.isActive("heading", { level: 3 }), - isBlockquote: ctx.editor.isActive("blockquote"), - isCodeBlock: ctx.editor.isActive("codeBlock"), + isHeading1: !!ctx.editor?.isActive("heading", { level: 1 }), + isHeading2: !!ctx.editor?.isActive("heading", { level: 2 }), + isHeading3: !!ctx.editor?.isActive("heading", { level: 3 }), + isBlockquote: !!ctx.editor?.isActive("blockquote"), + isCodeBlock: !!ctx.editor?.isActive("codeBlock"), }), }); diff --git a/apps/client/src/features/editor/components/fixed-toolbar/use-toolbar-state.ts b/apps/client/src/features/editor/components/fixed-toolbar/use-toolbar-state.ts index 589dbeea0..44b599c1b 100644 --- a/apps/client/src/features/editor/components/fixed-toolbar/use-toolbar-state.ts +++ b/apps/client/src/features/editor/components/fixed-toolbar/use-toolbar-state.ts @@ -21,7 +21,7 @@ export interface ToolbarState { // static editor (mainExtensions only, undoRedo disabled), neither is loaded // and editor.can().undo/redo is undefined. function safeCan(editor: Editor, command: "undo" | "redo"): boolean { - const can = editor.can() as Record; + const can = editor?.can() as Record; const fn = can[command]; return typeof fn === "function" ? (fn as () => boolean)() : false; } @@ -30,7 +30,7 @@ export function useToolbarState(editor: Editor | null): ToolbarState | null { return useEditorState({ editor, selector: (ctx) => { - if (!ctx.editor) return null; + if (!ctx.editor || ctx.editor.isDestroyed) return null; return { isBold: ctx.editor.isActive("bold"), isItalic: ctx.editor.isActive("italic"), diff --git a/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx b/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx index 2f2922e26..ba2bed40e 100644 --- a/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx +++ b/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx @@ -25,7 +25,7 @@ const recalculateLinks = (nodePos: NodePos[]) => { (acc, item) => { const label = item.node.textContent; const level = Number(item.node.attrs.level); - if (label.length && level <= 4) { + if (label.length && level <= 6) { acc.push({ label, level, diff --git a/apps/client/src/features/editor/components/table/handle/column-handle.tsx b/apps/client/src/features/editor/components/table/handle/column-handle.tsx index a46ac50d5..ee29a7d99 100644 --- a/apps/client/src/features/editor/components/table/handle/column-handle.tsx +++ b/apps/client/src/features/editor/components/table/handle/column-handle.tsx @@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next"; import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; import { ColumnHandleMenu } from "./menus/column-handle-menu"; +import { isEditorReady } from "@docmost/editor-ext"; import classes from "./handle.module.css"; interface ColumnHandleProps { @@ -35,7 +36,9 @@ export const ColumnHandle = React.memo(function ColumnHandle({ // an external drop reflows the doc before the plugin re-emits // hoveringCell), it can resolve to a Text node, on which `.closest` is // undefined. Filter to HTMLElement so downstream consumers stay safe. - const lookupDom = editor.view.nodeDOM(anchorPos); + const lookupDom = isEditorReady(editor) + ? editor.view.nodeDOM(anchorPos) + : null; const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null; const [cellDom, setCellDom] = useState(lookupCellDom); const lastCellDomRef = useRef(lookupCellDom); diff --git a/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx index 84f904ca7..6bc85608f 100644 --- a/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx +++ b/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx @@ -101,14 +101,14 @@ export const CellChevronMenu = React.memo(function CellChevronMenu({ } onClick={() => editor.chain().focus().mergeCells().run()} - disabled={!editor.can().mergeCells()} + disabled={!editor?.can().mergeCells()} > {t("Merge cells")} } onClick={() => editor.chain().focus().splitCell().run()} - disabled={!editor.can().splitCell()} + disabled={!editor?.can().splitCell()} > {t("Split cell")} diff --git a/apps/client/src/features/editor/components/table/handle/row-handle.tsx b/apps/client/src/features/editor/components/table/handle/row-handle.tsx index 1f3e3cc51..18ba480cb 100644 --- a/apps/client/src/features/editor/components/table/handle/row-handle.tsx +++ b/apps/client/src/features/editor/components/table/handle/row-handle.tsx @@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next"; import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; import { RowHandleMenu } from "./menus/row-handle-menu"; +import { isEditorReady } from "@docmost/editor-ext"; import classes from "./handle.module.css"; interface RowHandleProps { @@ -33,7 +34,9 @@ export const RowHandle = React.memo(function RowHandle({ // an external drop reflows the doc before the plugin re-emits // hoveringCell), it can resolve to a Text node, on which `.closest` is // undefined. Filter to HTMLElement so downstream consumers stay safe. - const lookupDom = editor.view.nodeDOM(anchorPos); + const lookupDom = isEditorReady(editor) + ? editor.view.nodeDOM(anchorPos) + : null; const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null; const [cellDom, setCellDom] = useState(lookupCellDom); const lastCellDomRef = useRef(lookupCellDom); diff --git a/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx b/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx index e50793149..06d05bd21 100644 --- a/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx +++ b/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx @@ -105,6 +105,7 @@ function TransclusionReferenceBody({ sourcePageId, transclusionId, }); + if (editor.isDestroyed) return; const pos = getPos(); if (typeof pos !== "number") return; const from = pos; diff --git a/apps/client/src/features/editor/hooks/use-editor-scroll.ts b/apps/client/src/features/editor/hooks/use-editor-scroll.ts index cfd5a6921..0f1759cf6 100644 --- a/apps/client/src/features/editor/hooks/use-editor-scroll.ts +++ b/apps/client/src/features/editor/hooks/use-editor-scroll.ts @@ -42,6 +42,10 @@ export const useEditorScroll = ({ return; } + if (editor.isDestroyed) { + resolve(false); + return; + } const dom = editor.view.dom.querySelector(`[id="${targetId}"], [data-id="${targetId}"]`); if (dom) { dom.scrollIntoView({ behavior: 'smooth', block: 'start' }); diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 9d1316943..31657ea7b 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -458,7 +458,9 @@ export default function PageEditor({ )}
editor.commands.focus("end")} + onClick={() => { + if (editor && !editor.isDestroyed) editor.commands.focus("end"); + }} style={{ paddingBottom: "20vh" }} >
diff --git a/apps/client/src/features/page-history/hooks/use-history-restore.tsx b/apps/client/src/features/page-history/hooks/use-history-restore.tsx index fbeb4e6ed..f457c696a 100644 --- a/apps/client/src/features/page-history/hooks/use-history-restore.tsx +++ b/apps/client/src/features/page-history/hooks/use-history-restore.tsx @@ -42,6 +42,14 @@ export function useHistoryRestore() { const handleRestore = useCallback(() => { if (!activeHistoryData) return; + if ( + !mainEditor || + mainEditor.isDestroyed || + !mainEditorTitle || + mainEditorTitle.isDestroyed + ) { + return; + } mainEditorTitle .chain()