From 66f9113975ca7f91e724f7b31b8e445d3516410a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 12 Feb 2026 18:27:42 -0500 Subject: [PATCH] fix: Exporting document with table causes crash (#11422) * fix: Exporting document with table causes crash * fix: Same issue for checkbox lists --- shared/editor/nodes/CheckboxListView.ts | 34 ++++++++++++---- shared/editor/nodes/TableView.ts | 53 ++++++++++++++++++------- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/shared/editor/nodes/CheckboxListView.ts b/shared/editor/nodes/CheckboxListView.ts index 5d914634b0..1620109508 100644 --- a/shared/editor/nodes/CheckboxListView.ts +++ b/shared/editor/nodes/CheckboxListView.ts @@ -1,6 +1,7 @@ import type { Node as ProsemirrorNode } from "prosemirror-model"; import type { EditorView, NodeView } from "prosemirror-view"; import type { Dictionary } from "../../../app/hooks/useDictionary"; +import { isBrowser } from "../../utils/browser"; import Storage from "../../utils/Storage"; import { EditorStyleHelper } from "../styles/EditorStyleHelper"; @@ -11,23 +12,20 @@ import { EditorStyleHelper } from "../styles/EditorStyleHelper"; export class CheckboxListView implements NodeView { dom: HTMLElement; contentDOM: HTMLElement; + private toggleControl: HTMLButtonElement; private node: ProsemirrorNode; - private view: EditorView; - private getPos: () => number | undefined; private userIdentifier: string; private dictionary: Dictionary; constructor( node: ProsemirrorNode, - view: EditorView, - getPos: () => number | undefined, + _view: EditorView, + _getPos: () => number | undefined, userIdentifier: string, dictionary: Dictionary ) { this.node = node; - this.view = view; - this.getPos = getPos; this.userIdentifier = userIdentifier; this.dictionary = dictionary; @@ -40,7 +38,10 @@ export class CheckboxListView implements NodeView { EditorStyleHelper.checklistCompletedToggle ); this.toggleControl.contentEditable = "false"; - this.toggleControl.addEventListener("click", this.handleToggleClick); + + if (isBrowser) { + this.toggleControl.addEventListener("click", this.handleToggleClick); + } this.contentDOM = document.createElement("ul"); this.contentDOM.classList.add("checkbox_list"); @@ -49,10 +50,16 @@ export class CheckboxListView implements NodeView { wrapperElement.appendChild(this.contentDOM); this.dom = wrapperElement; - this.updateToggleState(); + if (isBrowser) { + this.updateToggleState(); + } } private handleToggleClick = (clickEvent: Event) => { + if (!isBrowser) { + return; + } + clickEvent.preventDefault(); clickEvent.stopPropagation(); @@ -68,6 +75,10 @@ export class CheckboxListView implements NodeView { }; private updateToggleState() { + if (!isBrowser) { + return; + } + const listId = this.node.attrs.id; if (!listId) { this.toggleControl.style.display = "none"; @@ -104,15 +115,22 @@ export class CheckboxListView implements NodeView { } update(node: ProsemirrorNode) { + if (!isBrowser) { + return false; + } if (node.type.name !== "checkbox_list") { return false; } + this.node = node; this.updateToggleState(); return true; } destroy() { + if (!isBrowser) { + return; + } this.toggleControl.removeEventListener("click", this.handleToggleClick); } } diff --git a/shared/editor/nodes/TableView.ts b/shared/editor/nodes/TableView.ts index 094b71c5d3..deb8e8619d 100644 --- a/shared/editor/nodes/TableView.ts +++ b/shared/editor/nodes/TableView.ts @@ -2,6 +2,7 @@ import type { Node } from "prosemirror-model"; import { TableView as ProsemirrorTableView } from "prosemirror-tables"; import { EditorStyleHelper } from "../styles/EditorStyleHelper"; import { TableLayout } from "../types"; +import { isBrowser } from "../../utils/browser"; export class TableView extends ProsemirrorTableView { public constructor( @@ -18,24 +19,28 @@ export class TableView extends ProsemirrorTableView { this.scrollable.appendChild(this.table); this.scrollable.classList.add(EditorStyleHelper.tableScrollable); - this.scrollable.addEventListener( - "scroll", - () => { - this.updateClassList(this.node); - }, - { - passive: true, - } - ); + if (isBrowser) { + this.scrollable.addEventListener( + "scroll", + () => { + this.updateClassList(this.node); + }, + { + passive: true, + } + ); + } this.updateClassList(node); // We need to wait for the next tick to ensure dom is rendered and scroll shadows are correct. - setTimeout(() => { - if (this.dom) { - this.updateClassList(node); - } - }, 0); + if (isBrowser) { + setTimeout(() => { + if (this.dom) { + this.updateClassList(node); + } + }, 0); + } // Set up sticky header handling this.setupStickyHeader(); @@ -66,6 +71,10 @@ export class TableView extends ProsemirrorTableView { } private updateClassList(node: Node) { + if (!isBrowser) { + return; + } + this.dom.classList.toggle( EditorStyleHelper.tableFullWidth, node.attrs.layout === TableLayout.fullWidth @@ -108,6 +117,10 @@ export class TableView extends ProsemirrorTableView { * Sets up the scroll listener for sticky header behavior. */ private setupStickyHeader() { + if (!isBrowser) { + return; + } + // Defer setup to ensure DOM is fully rendered setTimeout(() => { this.scrollHandler = () => { @@ -129,6 +142,10 @@ export class TableView extends ProsemirrorTableView { * Cleans up the scroll listener and resets header styles. */ private cleanupStickyHeader() { + if (!isBrowser) { + return; + } + if (this.scrollHandler) { document.removeEventListener("scroll", this.scrollHandler, { capture: true, @@ -145,6 +162,10 @@ export class TableView extends ProsemirrorTableView { * Updates the header row transform to create a sticky effect. */ private updateStickyHeader() { + if (!isBrowser) { + return; + } + const headerRow = this.table.querySelector("tr") as HTMLElement | null; if (!headerRow) { return; @@ -179,6 +200,10 @@ export class TableView extends ProsemirrorTableView { * @returns the offset in pixels from the top of the viewport. */ private getHeaderOffset(): number { + if (!isBrowser) { + return TableView.HEADER_HEIGHT; + } + const value = getComputedStyle(document.documentElement).getPropertyValue( "--header-offset" );