fix: Exporting document with table causes crash (#11422)

* fix: Exporting document with table causes crash

* fix: Same issue for checkbox lists
This commit is contained in:
Tom Moor
2026-02-12 18:27:42 -05:00
committed by GitHub
parent a52391842f
commit 66f9113975
2 changed files with 65 additions and 22 deletions
+26 -8
View File
@@ -1,6 +1,7 @@
import type { Node as ProsemirrorNode } from "prosemirror-model"; import type { Node as ProsemirrorNode } from "prosemirror-model";
import type { EditorView, NodeView } from "prosemirror-view"; import type { EditorView, NodeView } from "prosemirror-view";
import type { Dictionary } from "../../../app/hooks/useDictionary"; import type { Dictionary } from "../../../app/hooks/useDictionary";
import { isBrowser } from "../../utils/browser";
import Storage from "../../utils/Storage"; import Storage from "../../utils/Storage";
import { EditorStyleHelper } from "../styles/EditorStyleHelper"; import { EditorStyleHelper } from "../styles/EditorStyleHelper";
@@ -11,23 +12,20 @@ import { EditorStyleHelper } from "../styles/EditorStyleHelper";
export class CheckboxListView implements NodeView { export class CheckboxListView implements NodeView {
dom: HTMLElement; dom: HTMLElement;
contentDOM: HTMLElement; contentDOM: HTMLElement;
private toggleControl: HTMLButtonElement; private toggleControl: HTMLButtonElement;
private node: ProsemirrorNode; private node: ProsemirrorNode;
private view: EditorView;
private getPos: () => number | undefined;
private userIdentifier: string; private userIdentifier: string;
private dictionary: Dictionary; private dictionary: Dictionary;
constructor( constructor(
node: ProsemirrorNode, node: ProsemirrorNode,
view: EditorView, _view: EditorView,
getPos: () => number | undefined, _getPos: () => number | undefined,
userIdentifier: string, userIdentifier: string,
dictionary: Dictionary dictionary: Dictionary
) { ) {
this.node = node; this.node = node;
this.view = view;
this.getPos = getPos;
this.userIdentifier = userIdentifier; this.userIdentifier = userIdentifier;
this.dictionary = dictionary; this.dictionary = dictionary;
@@ -40,7 +38,10 @@ export class CheckboxListView implements NodeView {
EditorStyleHelper.checklistCompletedToggle EditorStyleHelper.checklistCompletedToggle
); );
this.toggleControl.contentEditable = "false"; 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 = document.createElement("ul");
this.contentDOM.classList.add("checkbox_list"); this.contentDOM.classList.add("checkbox_list");
@@ -49,10 +50,16 @@ export class CheckboxListView implements NodeView {
wrapperElement.appendChild(this.contentDOM); wrapperElement.appendChild(this.contentDOM);
this.dom = wrapperElement; this.dom = wrapperElement;
this.updateToggleState(); if (isBrowser) {
this.updateToggleState();
}
} }
private handleToggleClick = (clickEvent: Event) => { private handleToggleClick = (clickEvent: Event) => {
if (!isBrowser) {
return;
}
clickEvent.preventDefault(); clickEvent.preventDefault();
clickEvent.stopPropagation(); clickEvent.stopPropagation();
@@ -68,6 +75,10 @@ export class CheckboxListView implements NodeView {
}; };
private updateToggleState() { private updateToggleState() {
if (!isBrowser) {
return;
}
const listId = this.node.attrs.id; const listId = this.node.attrs.id;
if (!listId) { if (!listId) {
this.toggleControl.style.display = "none"; this.toggleControl.style.display = "none";
@@ -104,15 +115,22 @@ export class CheckboxListView implements NodeView {
} }
update(node: ProsemirrorNode) { update(node: ProsemirrorNode) {
if (!isBrowser) {
return false;
}
if (node.type.name !== "checkbox_list") { if (node.type.name !== "checkbox_list") {
return false; return false;
} }
this.node = node; this.node = node;
this.updateToggleState(); this.updateToggleState();
return true; return true;
} }
destroy() { destroy() {
if (!isBrowser) {
return;
}
this.toggleControl.removeEventListener("click", this.handleToggleClick); this.toggleControl.removeEventListener("click", this.handleToggleClick);
} }
} }
+39 -14
View File
@@ -2,6 +2,7 @@ import type { Node } from "prosemirror-model";
import { TableView as ProsemirrorTableView } from "prosemirror-tables"; import { TableView as ProsemirrorTableView } from "prosemirror-tables";
import { EditorStyleHelper } from "../styles/EditorStyleHelper"; import { EditorStyleHelper } from "../styles/EditorStyleHelper";
import { TableLayout } from "../types"; import { TableLayout } from "../types";
import { isBrowser } from "../../utils/browser";
export class TableView extends ProsemirrorTableView { export class TableView extends ProsemirrorTableView {
public constructor( public constructor(
@@ -18,24 +19,28 @@ export class TableView extends ProsemirrorTableView {
this.scrollable.appendChild(this.table); this.scrollable.appendChild(this.table);
this.scrollable.classList.add(EditorStyleHelper.tableScrollable); this.scrollable.classList.add(EditorStyleHelper.tableScrollable);
this.scrollable.addEventListener( if (isBrowser) {
"scroll", this.scrollable.addEventListener(
() => { "scroll",
this.updateClassList(this.node); () => {
}, this.updateClassList(this.node);
{ },
passive: true, {
} passive: true,
); }
);
}
this.updateClassList(node); this.updateClassList(node);
// We need to wait for the next tick to ensure dom is rendered and scroll shadows are correct. // We need to wait for the next tick to ensure dom is rendered and scroll shadows are correct.
setTimeout(() => { if (isBrowser) {
if (this.dom) { setTimeout(() => {
this.updateClassList(node); if (this.dom) {
} this.updateClassList(node);
}, 0); }
}, 0);
}
// Set up sticky header handling // Set up sticky header handling
this.setupStickyHeader(); this.setupStickyHeader();
@@ -66,6 +71,10 @@ export class TableView extends ProsemirrorTableView {
} }
private updateClassList(node: Node) { private updateClassList(node: Node) {
if (!isBrowser) {
return;
}
this.dom.classList.toggle( this.dom.classList.toggle(
EditorStyleHelper.tableFullWidth, EditorStyleHelper.tableFullWidth,
node.attrs.layout === TableLayout.fullWidth node.attrs.layout === TableLayout.fullWidth
@@ -108,6 +117,10 @@ export class TableView extends ProsemirrorTableView {
* Sets up the scroll listener for sticky header behavior. * Sets up the scroll listener for sticky header behavior.
*/ */
private setupStickyHeader() { private setupStickyHeader() {
if (!isBrowser) {
return;
}
// Defer setup to ensure DOM is fully rendered // Defer setup to ensure DOM is fully rendered
setTimeout(() => { setTimeout(() => {
this.scrollHandler = () => { this.scrollHandler = () => {
@@ -129,6 +142,10 @@ export class TableView extends ProsemirrorTableView {
* Cleans up the scroll listener and resets header styles. * Cleans up the scroll listener and resets header styles.
*/ */
private cleanupStickyHeader() { private cleanupStickyHeader() {
if (!isBrowser) {
return;
}
if (this.scrollHandler) { if (this.scrollHandler) {
document.removeEventListener("scroll", this.scrollHandler, { document.removeEventListener("scroll", this.scrollHandler, {
capture: true, capture: true,
@@ -145,6 +162,10 @@ export class TableView extends ProsemirrorTableView {
* Updates the header row transform to create a sticky effect. * Updates the header row transform to create a sticky effect.
*/ */
private updateStickyHeader() { private updateStickyHeader() {
if (!isBrowser) {
return;
}
const headerRow = this.table.querySelector("tr") as HTMLElement | null; const headerRow = this.table.querySelector("tr") as HTMLElement | null;
if (!headerRow) { if (!headerRow) {
return; return;
@@ -179,6 +200,10 @@ export class TableView extends ProsemirrorTableView {
* @returns the offset in pixels from the top of the viewport. * @returns the offset in pixels from the top of the viewport.
*/ */
private getHeaderOffset(): number { private getHeaderOffset(): number {
if (!isBrowser) {
return TableView.HEADER_HEIGHT;
}
const value = getComputedStyle(document.documentElement).getPropertyValue( const value = getComputedStyle(document.documentElement).getPropertyValue(
"--header-offset" "--header-offset"
); );