@kerebron/extension-tables 0.4.28 → 0.4.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/esm/ExtensionTables.js +1 -0
  2. package/esm/ExtensionTables.js.map +1 -0
  3. package/esm/NodeTable.js +1 -0
  4. package/esm/NodeTable.js.map +1 -0
  5. package/esm/NodeTableCell.js +1 -0
  6. package/esm/NodeTableCell.js.map +1 -0
  7. package/esm/NodeTableHeader.js +1 -0
  8. package/esm/NodeTableHeader.js.map +1 -0
  9. package/esm/NodeTableRow.js +1 -0
  10. package/esm/NodeTableRow.js.map +1 -0
  11. package/esm/_dnt.shims.js +1 -0
  12. package/esm/_dnt.shims.js.map +1 -0
  13. package/esm/utilities/CellSelection.js +1 -0
  14. package/esm/utilities/CellSelection.js.map +1 -0
  15. package/esm/utilities/TableMap.js +1 -0
  16. package/esm/utilities/TableMap.js.map +1 -0
  17. package/esm/utilities/TableView.js +1 -0
  18. package/esm/utilities/TableView.js.map +1 -0
  19. package/esm/utilities/columnResizing.js +1 -0
  20. package/esm/utilities/columnResizing.js.map +1 -0
  21. package/esm/utilities/commands.js +1 -0
  22. package/esm/utilities/commands.js.map +1 -0
  23. package/esm/utilities/copypaste.js +1 -0
  24. package/esm/utilities/copypaste.js.map +1 -0
  25. package/esm/utilities/createCell.js +1 -0
  26. package/esm/utilities/createCell.js.map +1 -0
  27. package/esm/utilities/createTable.js +1 -0
  28. package/esm/utilities/createTable.js.map +1 -0
  29. package/esm/utilities/fixTables.js +1 -0
  30. package/esm/utilities/fixTables.js.map +1 -0
  31. package/esm/utilities/getTableNodeTypes.js +1 -0
  32. package/esm/utilities/getTableNodeTypes.js.map +1 -0
  33. package/esm/utilities/input.js +1 -0
  34. package/esm/utilities/input.js.map +1 -0
  35. package/esm/utilities/tableEditing.js +1 -0
  36. package/esm/utilities/tableEditing.js.map +1 -0
  37. package/esm/utilities/tableNodeTypes.js +1 -0
  38. package/esm/utilities/tableNodeTypes.js.map +1 -0
  39. package/esm/utilities/util.js +1 -0
  40. package/esm/utilities/util.js.map +1 -0
  41. package/package.json +6 -2
  42. package/src/ExtensionTables.ts +16 -0
  43. package/src/NodeTable.ts +139 -0
  44. package/src/NodeTableCell.ts +70 -0
  45. package/src/NodeTableHeader.ts +49 -0
  46. package/src/NodeTableRow.ts +41 -0
  47. package/src/_dnt.shims.ts +60 -0
  48. package/src/utilities/CellSelection.ts +477 -0
  49. package/src/utilities/TableMap.ts +392 -0
  50. package/src/utilities/TableView.ts +102 -0
  51. package/src/utilities/columnResizing.ts +437 -0
  52. package/src/utilities/commands.ts +896 -0
  53. package/src/utilities/copypaste.ts +394 -0
  54. package/src/utilities/createCell.ts +12 -0
  55. package/src/utilities/createTable.ts +53 -0
  56. package/src/utilities/fixTables.ts +156 -0
  57. package/src/utilities/getTableNodeTypes.ts +21 -0
  58. package/src/utilities/input.ts +299 -0
  59. package/src/utilities/tableEditing.ts +90 -0
  60. package/src/utilities/tableNodeTypes.ts +32 -0
  61. package/src/utilities/util.ts +204 -0
  62. package/assets/tables.css +0 -85
@@ -0,0 +1,392 @@
1
+ // Because working with row and column-spanning cells is not quite
2
+ // trivial, this code builds up a descriptive structure for a given
3
+ // table node. The structures are cached with the (persistent) table
4
+ // nodes as key, so that they only have to be recomputed when the
5
+ // content of the table changes.
6
+ //
7
+ // This does mean that they have to store table-relative, not
8
+ // document-relative positions. So code that uses them will typically
9
+ // compute the start position of the table and offset positions passed
10
+ // to or gotten from this structure by that amount.
11
+ import { Attrs, Node } from 'prosemirror-model';
12
+ import { CellAttrs } from './util.js';
13
+
14
+ /**
15
+ * @public
16
+ */
17
+ export type ColWidths = number[];
18
+
19
+ /**
20
+ * @public
21
+ */
22
+ export type Problem =
23
+ | {
24
+ type: 'colwidth mismatch';
25
+ pos: number;
26
+ colwidth: ColWidths;
27
+ }
28
+ | {
29
+ type: 'collision';
30
+ pos: number;
31
+ row: number;
32
+ n: number;
33
+ }
34
+ | {
35
+ type: 'missing';
36
+ row: number;
37
+ n: number;
38
+ }
39
+ | {
40
+ type: 'overlong_rowspan';
41
+ pos: number;
42
+ n: number;
43
+ }
44
+ | {
45
+ type: 'zero_sized';
46
+ };
47
+
48
+ let readFromCache: (key: Node) => TableMap | undefined;
49
+ let addToCache: (key: Node, value: TableMap) => TableMap;
50
+
51
+ // Prefer using a weak map to cache table maps. Fall back on a
52
+ // fixed-size cache if that's not supported.
53
+ if (typeof WeakMap != 'undefined') {
54
+ // eslint-disable-next-line
55
+ const cache = new WeakMap<Node, TableMap>();
56
+ readFromCache = (key) => cache.get(key);
57
+ addToCache = (key, value) => {
58
+ cache.set(key, value);
59
+ return value;
60
+ };
61
+ } else {
62
+ const cache: (Node | TableMap)[] = [];
63
+ const cacheSize = 10;
64
+ let cachePos = 0;
65
+ readFromCache = (key) => {
66
+ for (let i = 0; i < cache.length; i += 2) {
67
+ if (cache[i] == key) return cache[i + 1] as TableMap;
68
+ }
69
+ };
70
+ addToCache = (key, value) => {
71
+ if (cachePos == cacheSize) cachePos = 0;
72
+ cache[cachePos++] = key;
73
+ return (cache[cachePos++] = value);
74
+ };
75
+ }
76
+
77
+ /**
78
+ * @public
79
+ */
80
+ export interface Rect {
81
+ left: number;
82
+ top: number;
83
+ right: number;
84
+ bottom: number;
85
+ }
86
+
87
+ /**
88
+ * A table map describes the structure of a given table. To avoid
89
+ * recomputing them all the time, they are cached per table node. To
90
+ * be able to do that, positions saved in the map are relative to the
91
+ * start of the table, rather than the start of the document.
92
+ *
93
+ * @public
94
+ */
95
+ export class TableMap {
96
+ constructor(
97
+ /**
98
+ * The number of columns
99
+ */
100
+ public width: number,
101
+ /**
102
+ * The number of rows
103
+ */
104
+ public height: number,
105
+ /**
106
+ * A width * height array with the start position of
107
+ * the cell covering that part of the table in each slot
108
+ */
109
+ public map: number[],
110
+ /**
111
+ * An optional array of problems (cell overlap or non-rectangular
112
+ * shape) for the table, used by the table normalizer.
113
+ */
114
+ public problems: Problem[] | null,
115
+ ) {}
116
+
117
+ // Find the dimensions of the cell at the given position.
118
+ findCell(pos: number): Rect {
119
+ for (let i = 0; i < this.map.length; i++) {
120
+ const curPos = this.map[i];
121
+ if (curPos != pos) continue;
122
+
123
+ const left = i % this.width;
124
+ const top = (i / this.width) | 0;
125
+ let right = left + 1;
126
+ let bottom = top + 1;
127
+
128
+ for (let j = 1; right < this.width && this.map[i + j] == curPos; j++) {
129
+ right++;
130
+ }
131
+ for (
132
+ let j = 1;
133
+ bottom < this.height && this.map[i + this.width * j] == curPos;
134
+ j++
135
+ ) {
136
+ bottom++;
137
+ }
138
+
139
+ return { left, top, right, bottom };
140
+ }
141
+ throw new RangeError(`No cell with offset ${pos} found`);
142
+ }
143
+
144
+ // Find the left side of the cell at the given position.
145
+ colCount(pos: number): number {
146
+ for (let i = 0; i < this.map.length; i++) {
147
+ if (this.map[i] == pos) {
148
+ return i % this.width;
149
+ }
150
+ }
151
+ throw new RangeError(`No cell with offset ${pos} found`);
152
+ }
153
+
154
+ // Find the next cell in the given direction, starting from the cell
155
+ // at `pos`, if any.
156
+ nextCell(pos: number, axis: 'horiz' | 'vert', dir: number): null | number {
157
+ const { left, right, top, bottom } = this.findCell(pos);
158
+ if (axis == 'horiz') {
159
+ if (dir < 0 ? left == 0 : right == this.width) return null;
160
+ return this.map[top * this.width + (dir < 0 ? left - 1 : right)];
161
+ } else {
162
+ if (dir < 0 ? top == 0 : bottom == this.height) return null;
163
+ return this.map[left + this.width * (dir < 0 ? top - 1 : bottom)];
164
+ }
165
+ }
166
+
167
+ // Get the rectangle spanning the two given cells.
168
+ rectBetween(a: number, b: number): Rect {
169
+ const {
170
+ left: leftA,
171
+ right: rightA,
172
+ top: topA,
173
+ bottom: bottomA,
174
+ } = this.findCell(a);
175
+ const {
176
+ left: leftB,
177
+ right: rightB,
178
+ top: topB,
179
+ bottom: bottomB,
180
+ } = this.findCell(b);
181
+ return {
182
+ left: Math.min(leftA, leftB),
183
+ top: Math.min(topA, topB),
184
+ right: Math.max(rightA, rightB),
185
+ bottom: Math.max(bottomA, bottomB),
186
+ };
187
+ }
188
+
189
+ // Return the position of all cells that have the top left corner in
190
+ // the given rectangle.
191
+ cellsInRect(rect: Rect): number[] {
192
+ const result: number[] = [];
193
+ const seen: Record<number, boolean> = {};
194
+ for (let row = rect.top; row < rect.bottom; row++) {
195
+ for (let col = rect.left; col < rect.right; col++) {
196
+ const index = row * this.width + col;
197
+ const pos = this.map[index];
198
+
199
+ if (seen[pos]) continue;
200
+ seen[pos] = true;
201
+
202
+ if (
203
+ (col == rect.left && col && this.map[index - 1] == pos) ||
204
+ (row == rect.top && row && this.map[index - this.width] == pos)
205
+ ) {
206
+ continue;
207
+ }
208
+ result.push(pos);
209
+ }
210
+ }
211
+ return result;
212
+ }
213
+
214
+ // Return the position at which the cell at the given row and column
215
+ // starts, or would start, if a cell started there.
216
+ positionAt(row: number, col: number, table: Node): number {
217
+ for (let i = 0, rowStart = 0;; i++) {
218
+ const rowEnd = rowStart + table.child(i).nodeSize;
219
+ if (i == row) {
220
+ let index = col + row * this.width;
221
+ const rowEndIndex = (row + 1) * this.width;
222
+ // Skip past cells from previous rows (via rowspan)
223
+ while (index < rowEndIndex && this.map[index] < rowStart) index++;
224
+ return index == rowEndIndex ? rowEnd - 1 : this.map[index];
225
+ }
226
+ rowStart = rowEnd;
227
+ }
228
+ }
229
+
230
+ // Find the table map for the given table node.
231
+ static get(table: Node): TableMap {
232
+ return readFromCache(table) || addToCache(table, computeMap(table));
233
+ }
234
+ }
235
+
236
+ // Compute a table map.
237
+ function computeMap(table: Node): TableMap {
238
+ if (table.type.spec.tableRole != 'table') {
239
+ throw new RangeError('Not a table node: ' + table.type.name);
240
+ }
241
+ const width = findWidth(table),
242
+ height = table.childCount;
243
+ const map = [];
244
+ let mapPos = 0;
245
+ let problems: Problem[] | null = null;
246
+ const colWidths: ColWidths = [];
247
+ for (let i = 0, e = width * height; i < e; i++) map[i] = 0;
248
+
249
+ for (let row = 0, pos = 0; row < height; row++) {
250
+ const rowNode = table.child(row);
251
+ pos++;
252
+ for (let i = 0;; i++) {
253
+ while (mapPos < map.length && map[mapPos] != 0) mapPos++;
254
+ if (i == rowNode.childCount) break;
255
+ const cellNode = rowNode.child(i);
256
+ const { colspan, rowspan, colwidth } = cellNode.attrs;
257
+ for (let h = 0; h < rowspan; h++) {
258
+ if (h + row >= height) {
259
+ (problems || (problems = [])).push({
260
+ type: 'overlong_rowspan',
261
+ pos,
262
+ n: rowspan - h,
263
+ });
264
+ break;
265
+ }
266
+ const start = mapPos + h * width;
267
+ for (let w = 0; w < colspan; w++) {
268
+ if (map[start + w] == 0) map[start + w] = pos;
269
+ else {
270
+ (problems || (problems = [])).push({
271
+ type: 'collision',
272
+ row,
273
+ pos,
274
+ n: colspan - w,
275
+ });
276
+ }
277
+ const colW = colwidth && colwidth[w];
278
+ if (colW) {
279
+ const widthIndex = ((start + w) % width) * 2,
280
+ prev = colWidths[widthIndex];
281
+ if (
282
+ prev == null ||
283
+ (prev != colW && colWidths[widthIndex + 1] == 1)
284
+ ) {
285
+ colWidths[widthIndex] = colW;
286
+ colWidths[widthIndex + 1] = 1;
287
+ } else if (prev == colW) {
288
+ colWidths[widthIndex + 1]++;
289
+ }
290
+ }
291
+ }
292
+ }
293
+ mapPos += colspan;
294
+ pos += cellNode.nodeSize;
295
+ }
296
+ const expectedPos = (row + 1) * width;
297
+ let missing = 0;
298
+ while (mapPos < expectedPos) if (map[mapPos++] == 0) missing++;
299
+ if (missing) {
300
+ (problems || (problems = [])).push({ type: 'missing', row, n: missing });
301
+ }
302
+ pos++;
303
+ }
304
+
305
+ if (width === 0 || height === 0) {
306
+ (problems || (problems = [])).push({ type: 'zero_sized' });
307
+ }
308
+
309
+ const tableMap = new TableMap(width, height, map, problems);
310
+ let badWidths = false;
311
+
312
+ // For columns that have defined widths, but whose widths disagree
313
+ // between rows, fix up the cells whose width doesn't match the
314
+ // computed one.
315
+ for (let i = 0; !badWidths && i < colWidths.length; i += 2) {
316
+ if (colWidths[i] != null && colWidths[i + 1] < height) badWidths = true;
317
+ }
318
+ if (badWidths) findBadColWidths(tableMap, colWidths, table);
319
+
320
+ return tableMap;
321
+ }
322
+
323
+ function findWidth(table: Node): number {
324
+ let width = -1;
325
+ let hasRowSpan = false;
326
+ for (let row = 0; row < table.childCount; row++) {
327
+ const rowNode = table.child(row);
328
+ let rowWidth = 0;
329
+ if (hasRowSpan) {
330
+ for (let j = 0; j < row; j++) {
331
+ const prevRow = table.child(j);
332
+ for (let i = 0; i < prevRow.childCount; i++) {
333
+ const cell = prevRow.child(i);
334
+ if (j + cell.attrs.rowspan > row) rowWidth += cell.attrs.colspan;
335
+ }
336
+ }
337
+ }
338
+ for (let i = 0; i < rowNode.childCount; i++) {
339
+ const cell = rowNode.child(i);
340
+ rowWidth += cell.attrs.colspan;
341
+ if (cell.attrs.rowspan > 1) hasRowSpan = true;
342
+ }
343
+ if (width == -1) width = rowWidth;
344
+ else if (width != rowWidth) width = Math.max(width, rowWidth);
345
+ }
346
+ return width;
347
+ }
348
+
349
+ function findBadColWidths(
350
+ map: TableMap,
351
+ colWidths: ColWidths,
352
+ table: Node,
353
+ ): void {
354
+ if (!map.problems) map.problems = [];
355
+ const seen: Record<number, boolean> = {};
356
+ for (let i = 0; i < map.map.length; i++) {
357
+ const pos = map.map[i];
358
+ if (seen[pos]) continue;
359
+ seen[pos] = true;
360
+ const node = table.nodeAt(pos);
361
+ if (!node) {
362
+ throw new RangeError(`No cell with offset ${pos} found`);
363
+ }
364
+
365
+ let updated = null;
366
+ const attrs = node.attrs as CellAttrs;
367
+ for (let j = 0; j < attrs.colspan; j++) {
368
+ const col = (i + j) % map.width;
369
+ const colWidth = colWidths[col * 2];
370
+ if (
371
+ colWidth != null &&
372
+ (!attrs.colwidth || attrs.colwidth[j] != colWidth)
373
+ ) {
374
+ (updated || (updated = freshColWidth(attrs)))[j] = colWidth;
375
+ }
376
+ }
377
+ if (updated) {
378
+ map.problems.unshift({
379
+ type: 'colwidth mismatch',
380
+ pos,
381
+ colwidth: updated,
382
+ });
383
+ }
384
+ }
385
+ }
386
+
387
+ function freshColWidth(attrs: Attrs): ColWidths {
388
+ if (attrs.colwidth) return attrs.colwidth.slice();
389
+ const result: ColWidths = [];
390
+ for (let i = 0; i < attrs.colspan; i++) result.push(0);
391
+ return result;
392
+ }
@@ -0,0 +1,102 @@
1
+ import { Node } from 'prosemirror-model';
2
+ import { NodeView, ViewMutationRecord } from 'prosemirror-view';
3
+ import { CellAttrs } from './util.js';
4
+
5
+ /**
6
+ * @public
7
+ */
8
+ export class TableView implements NodeView {
9
+ public dom: HTMLDivElement;
10
+ public table: HTMLTableElement;
11
+ public colgroup: HTMLTableColElement;
12
+ public contentDOM: HTMLTableSectionElement;
13
+
14
+ constructor(
15
+ public node: Node,
16
+ public defaultCellMinWidth: number,
17
+ ) {
18
+ this.dom = document.createElement('div');
19
+ this.dom.className = 'tableWrapper';
20
+ this.table = this.dom.appendChild(document.createElement('table'));
21
+ this.table.style.setProperty(
22
+ '--default-cell-min-width',
23
+ `${defaultCellMinWidth}px`,
24
+ );
25
+ this.colgroup = this.table.appendChild(document.createElement('colgroup'));
26
+ updateColumnsOnResize(node, this.colgroup, this.table, defaultCellMinWidth);
27
+ this.contentDOM = this.table.appendChild(document.createElement('tbody'));
28
+ }
29
+
30
+ update(node: Node): boolean {
31
+ if (node.type != this.node.type) return false;
32
+ this.node = node;
33
+ updateColumnsOnResize(
34
+ node,
35
+ this.colgroup,
36
+ this.table,
37
+ this.defaultCellMinWidth,
38
+ );
39
+ return true;
40
+ }
41
+
42
+ ignoreMutation(record: ViewMutationRecord): boolean {
43
+ return (
44
+ record.type == 'attributes' &&
45
+ (record.target == this.table || this.colgroup.contains(record.target))
46
+ );
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @public
52
+ */
53
+ export function updateColumnsOnResize(
54
+ node: Node,
55
+ colgroup: HTMLTableColElement,
56
+ table: HTMLTableElement,
57
+ defaultCellMinWidth: number,
58
+ overrideCol?: number,
59
+ overrideValue?: number,
60
+ ): void {
61
+ let totalWidth = 0;
62
+ let fixedWidth = true;
63
+ let nextDOM = colgroup.firstChild as HTMLElement;
64
+ const row = node.firstChild;
65
+ if (!row) return;
66
+
67
+ for (let i = 0, col = 0; i < row.childCount; i++) {
68
+ const { colspan, colwidth } = row.child(i).attrs as CellAttrs;
69
+ for (let j = 0; j < colspan; j++, col++) {
70
+ const hasWidth = overrideCol == col
71
+ ? overrideValue
72
+ : colwidth && colwidth[j];
73
+ const cssWidth = hasWidth ? hasWidth + 'px' : '';
74
+ totalWidth += hasWidth || defaultCellMinWidth;
75
+ if (!hasWidth) fixedWidth = false;
76
+ if (!nextDOM) {
77
+ const col = document.createElement('col');
78
+ col.style.width = cssWidth;
79
+ colgroup.appendChild(col);
80
+ } else {
81
+ if (nextDOM.style.width != cssWidth) {
82
+ nextDOM.style.width = cssWidth;
83
+ }
84
+ nextDOM = nextDOM.nextSibling as HTMLElement;
85
+ }
86
+ }
87
+ }
88
+
89
+ while (nextDOM) {
90
+ const after = nextDOM.nextSibling;
91
+ nextDOM.parentNode?.removeChild(nextDOM);
92
+ nextDOM = after as HTMLElement;
93
+ }
94
+
95
+ if (fixedWidth) {
96
+ table.style.width = totalWidth + 'px';
97
+ table.style.minWidth = '';
98
+ } else {
99
+ table.style.width = '';
100
+ table.style.minWidth = totalWidth + 'px';
101
+ }
102
+ }