diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 23be85aa1..095adecb9 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -56,6 +56,7 @@ import { Status, TransclusionSource, TransclusionReference, + TableView, } from "@docmost/editor-ext"; import { randomElement, @@ -259,6 +260,7 @@ export const mainExtensions = [ resizable: true, lastColumnResizable: true, allowTableNodeSelection: true, + View: TableView, }), TableRow, TableCell, diff --git a/packages/editor-ext/src/lib/table/index.ts b/packages/editor-ext/src/lib/table/index.ts index 9e5a92651..784b71928 100644 --- a/packages/editor-ext/src/lib/table/index.ts +++ b/packages/editor-ext/src/lib/table/index.ts @@ -2,4 +2,5 @@ export * from "./row"; export * from "./cell"; export * from "./header"; export * from "./table"; -export * from "./dnd"; \ No newline at end of file +export * from "./dnd"; +export * from "./table-view"; diff --git a/packages/editor-ext/src/lib/table/table-view.ts b/packages/editor-ext/src/lib/table/table-view.ts new file mode 100644 index 000000000..733b04089 --- /dev/null +++ b/packages/editor-ext/src/lib/table/table-view.ts @@ -0,0 +1,145 @@ +import type { Node as ProseMirrorNode } from '@tiptap/pm/model'; +import type { NodeView, ViewMutationRecord } from '@tiptap/pm/view'; +import { getColStyleDeclaration } from './utils/col-style'; + +export function updateColumns( + node: ProseMirrorNode, + colgroup: HTMLTableColElement, // has the same prototype as + table: HTMLTableElement, + cellMinWidth: number, + overrideCol?: number, + overrideValue?: number, +) { + let totalWidth = 0; + let fixedWidth = true; + let nextDOM = colgroup.firstChild; + const row = node.firstChild; + + if (row !== null) { + for (let i = 0, col = 0; i < row.childCount; i += 1) { + const { colspan, colwidth } = row.child(i).attrs; + + for (let j = 0; j < colspan; j += 1, col += 1) { + const hasWidth = + overrideCol === col + ? overrideValue + : ((colwidth && colwidth[j]) as number | undefined); + const cssWidth = hasWidth ? `${hasWidth}px` : ''; + + totalWidth += hasWidth || cellMinWidth; + + if (!hasWidth) { + fixedWidth = false; + } + + if (!nextDOM) { + const colElement = document.createElement('col'); + + const [propertyKey, propertyValue] = getColStyleDeclaration( + cellMinWidth, + hasWidth, + ); + + colElement.style.setProperty(propertyKey, propertyValue); + + colgroup.appendChild(colElement); + } else { + if ((nextDOM as HTMLTableColElement).style.width !== cssWidth) { + const [propertyKey, propertyValue] = getColStyleDeclaration( + cellMinWidth, + hasWidth, + ); + + (nextDOM as HTMLTableColElement).style.setProperty( + propertyKey, + propertyValue, + ); + } + + nextDOM = nextDOM.nextSibling; + } + } + } + } + + while (nextDOM) { + const after = nextDOM.nextSibling; + + nextDOM.parentNode?.removeChild(nextDOM); + nextDOM = after; + } + + // Check if user has set a width style on the table node + const hasUserWidth = + node.attrs.style && + typeof node.attrs.style === 'string' && + /\bwidth\s*:/i.test(node.attrs.style); + + if (fixedWidth && !hasUserWidth) { + table.style.width = `${totalWidth}px`; + table.style.minWidth = ''; + } else { + table.style.width = ''; + table.style.minWidth = `${totalWidth}px`; + } +} + +export class TableView implements NodeView { + node: ProseMirrorNode; + + cellMinWidth: number; + + dom: HTMLDivElement; + + table: HTMLTableElement; + + colgroup: HTMLTableColElement; + + contentDOM: HTMLTableSectionElement; + + constructor(node: ProseMirrorNode, cellMinWidth: number) { + this.node = node; + this.cellMinWidth = cellMinWidth; + this.dom = document.createElement('div'); + this.dom.className = 'tableWrapper'; + this.table = this.dom.appendChild(document.createElement('table')); + + // Apply user styles to the table element + if (node.attrs.style) { + this.table.style.cssText = node.attrs.style; + } + + this.colgroup = this.table.appendChild(document.createElement('colgroup')); + updateColumns(node, this.colgroup, this.table, cellMinWidth); + this.contentDOM = this.table.appendChild(document.createElement('tbody')); + } + + update(node: ProseMirrorNode) { + if (node.type !== this.node.type) { + return false; + } + + this.node = node; + updateColumns(node, this.colgroup, this.table, this.cellMinWidth); + + return true; + } + + ignoreMutation(mutation: ViewMutationRecord) { + const target = mutation.target as Node; + const isInsideWrapper = this.dom.contains(target); + const isInsideContent = this.contentDOM.contains(target); + + if (isInsideWrapper && !isInsideContent) { + if ( + mutation.type === 'attributes' || + mutation.type === 'childList' || + mutation.type === 'characterData' + ) { + return true; + } + } + + return false; + } +} diff --git a/packages/editor-ext/src/lib/table/table.ts b/packages/editor-ext/src/lib/table/table.ts index f1436c28d..e93a1836c 100644 --- a/packages/editor-ext/src/lib/table/table.ts +++ b/packages/editor-ext/src/lib/table/table.ts @@ -32,6 +32,7 @@ function handleListOutdent(editor: Editor): boolean { } export const CustomTable = Table.extend({ + addKeyboardShortcuts() { return { ...this.parent?.(), diff --git a/packages/editor-ext/src/lib/table/utils/col-style.ts b/packages/editor-ext/src/lib/table/utils/col-style.ts new file mode 100644 index 000000000..d54a259fd --- /dev/null +++ b/packages/editor-ext/src/lib/table/utils/col-style.ts @@ -0,0 +1,9 @@ +export function getColStyleDeclaration(minWidth: number, width: number | undefined): [string, string] { + if (width) { + // apply the stored width unless it is below the configured minimum cell width + return ['width', `${Math.max(width, minWidth)}px`] + } + + // set the minimum with on the column if it has no stored width + return ['min-width', `${minWidth}px`] +}