Skip to content
File

Blob: src/client/components/editor/extensions/table-extensions.ts

typescript137 lines
1import type { AnyExtension } from "@tiptap/core";
2import { Extension } from "@tiptap/core";
3import { TableView } from "@tiptap/extension-table";
4import { TableMap } from "@tiptap/pm/tables";
5import {
6 SharedTable,
7 SharedTableCell,
8 SharedTableHeader,
9 SharedTableRow,
10 SharedTableRowHeightAttribute,
11 TABLE_CELL_MIN_WIDTH,
12} from "@/shared/editor/schema";
13import { TableHandles } from "./table/overlay-extension";
14import { TableWidthNormalization } from "./table/normalization-extension";
15import {
16 applyExplicitColumnWidths,
17 buildEvenWidths,
18 clearExplicitColumnWidths,
19 measureCanonicalTableWidth,
20 measureAutoColumnWidths,
21 measureRenderedTableWidth,
22 measureWrapperContentWidth,
23} from "./table/widths";
24 
25const CollaborationSafeTable = SharedTable.extend({
26 addOptions() {
27 const parent = this.parent?.();
28 return {
29 HTMLAttributes: parent?.HTMLAttributes ?? {},
30 resizable: parent?.resizable ?? true,
31 renderWrapper: parent?.renderWrapper ?? true,
32 handleWidth: parent?.handleWidth ?? 5,
33 cellMinWidth: parent?.cellMinWidth ?? TABLE_CELL_MIN_WIDTH,
34 View: TableView,
35 lastColumnResizable: parent?.lastColumnResizable ?? true,
36 allowTableNodeSelection: parent?.allowTableNodeSelection ?? true,
37 };
38 },
39 
40 addNodeView() {
41 const View = this.options.View;
42 if (this.editor.isEditable || !View) return null;
43 
44 return ({ node, view }) => new View(node, this.options.cellMinWidth, view);
45 },
46});
47 
48declare module "@tiptap/core" {
49 interface Commands<ReturnType> {
50 tableWidths: {
51 /**
52 * Clear explicit colwidth on every cell of the table at the given doc position.
53 * After this runs the table returns to responsive layout (width: 100%).
54 */
55 resetTableColumnWidths: (tablePos: number) => ReturnType;
56 /**
57 * Persist explicit widths derived from rendered content, clamped to the
58 * editor's minimum cell width.
59 */
60 fitTableColumnsToContent: (tablePos: number) => ReturnType;
61 /**
62 * Persist explicit widths that distribute the table's columns evenly
63 * across the available wrapper width, clamped to the minimum cell width.
64 */
65 distributeTableColumnsEvenly: (tablePos: number) => ReturnType;
66 };
67 }
68}
69 
70const TableWidthCommands = Extension.create({
71 name: "tableWidthCommands",
72 
73 addCommands() {
74 return {
75 resetTableColumnWidths:
76 (tablePos: number) =>
77 ({ state, tr, dispatch }) => {
78 if (!clearExplicitColumnWidths(state, tr, tablePos)) return false;
79 if (dispatch) dispatch(tr);
80 return true;
81 },
82 fitTableColumnsToContent:
83 (tablePos: number) =>
84 ({ state, tr, dispatch }) => {
85 const table = state.doc.nodeAt(tablePos);
86 if (!table || table.type.spec.tableRole !== "table") return false;
87 
88 const widths = measureAutoColumnWidths(this.editor.view, tablePos);
89 if (!widths) return false;
90 if (!dispatch) return true;
91 if (!applyExplicitColumnWidths(state, tr, tablePos, widths)) return false;
92 dispatch(tr);
93 return true;
94 },
95 distributeTableColumnsEvenly:
96 (tablePos: number) =>
97 ({ state, tr, dispatch }) => {
98 const table = state.doc.nodeAt(tablePos);
99 if (!table || table.type.spec.tableRole !== "table") return false;
100 
101 const totalWidth =
102 measureWrapperContentWidth(this.editor.view, tablePos) ??
103 measureCanonicalTableWidth(state.doc, table, tablePos) ??
104 measureRenderedTableWidth(this.editor.view, tablePos);
105 if (!totalWidth) return false;
106 if (!dispatch) return true;
107 
108 const map = TableMap.get(table);
109 const widths = buildEvenWidths(map.width, totalWidth);
110 if (!applyExplicitColumnWidths(state, tr, tablePos, widths)) return false;
111 dispatch(tr);
112 return true;
113 },
114 };
115 },
116});
117 
118export function createTableExtensions(): AnyExtension[] {
119 return [
120 CollaborationSafeTable.configure({
121 resizable: true,
122 allowTableNodeSelection: true,
123 cellMinWidth: TABLE_CELL_MIN_WIDTH,
124 lastColumnResizable: true,
125 renderWrapper: true,
126 View: TableView,
127 }),
128 SharedTableRow,
129 SharedTableCell,
130 SharedTableHeader,
131 SharedTableRowHeightAttribute,
132 TableWidthCommands,
133 TableWidthNormalization,
134 TableHandles,
135 ];
136}