@kerebron/extension-tables 0.4.28 → 0.4.30

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,394 @@
1
+ // Utilities used for copy/paste handling.
2
+ //
3
+ // This module handles pasting cell content into tables, or pasting
4
+ // anything into a cell selection, as replacing a block of cells with
5
+ // the content of the selection. When pasting cells into a cell, that
6
+ // involves placing the block of pasted content so that its top left
7
+ // aligns with the selection cell, optionally extending the table to
8
+ // the right or bottom to make sure it is large enough. Pasting into a
9
+ // cell selection is different, here the cells in the selection are
10
+ // clipped to the selection's rectangle, optionally repeating the
11
+ // pasted cells when they are smaller than the selection.
12
+
13
+ import { Fragment, Node, NodeType, Schema, Slice } from 'prosemirror-model';
14
+ import { Transform } from 'prosemirror-transform';
15
+
16
+ import { EditorState, Transaction } from 'prosemirror-state';
17
+ import { CellSelection } from './CellSelection.js';
18
+ import { tableNodeTypes } from './tableNodeTypes.js';
19
+ import { ColWidths, Rect, TableMap } from './TableMap.js';
20
+ import { CellAttrs, removeColSpan } from './util.js';
21
+
22
+ /**
23
+ * @internal
24
+ */
25
+ export type Area = { width: number; height: number; rows: Fragment[] };
26
+
27
+ // Utilities to help with copying and pasting table cells
28
+
29
+ /**
30
+ * Get a rectangular area of cells from a slice, or null if the outer
31
+ * nodes of the slice aren't table cells or rows.
32
+ *
33
+ * @internal
34
+ */
35
+ export function pastedCells(slice: Slice): Area | null {
36
+ if (!slice.size) return null;
37
+ let { content, openStart, openEnd } = slice;
38
+ while (
39
+ content.childCount == 1 &&
40
+ ((openStart > 0 && openEnd > 0) ||
41
+ content.child(0).type.spec.tableRole == 'table')
42
+ ) {
43
+ openStart--;
44
+ openEnd--;
45
+ content = content.child(0).content;
46
+ }
47
+ const first = content.child(0);
48
+ const role = first.type.spec.tableRole;
49
+ const schema = first.type.schema,
50
+ rows = [];
51
+ if (role == 'row') {
52
+ for (let i = 0; i < content.childCount; i++) {
53
+ let cells = content.child(i).content;
54
+ const left = i ? 0 : Math.max(0, openStart - 1);
55
+ const right = i < content.childCount - 1 ? 0 : Math.max(0, openEnd - 1);
56
+ if (left || right) {
57
+ cells = fitSlice(
58
+ tableNodeTypes(schema).row,
59
+ new Slice(cells, left, right),
60
+ ).content;
61
+ }
62
+ rows.push(cells);
63
+ }
64
+ } else if (role == 'cell' || role == 'header_cell') {
65
+ rows.push(
66
+ openStart || openEnd
67
+ ? fitSlice(
68
+ tableNodeTypes(schema).row,
69
+ new Slice(content, openStart, openEnd),
70
+ ).content
71
+ : content,
72
+ );
73
+ } else {
74
+ return null;
75
+ }
76
+ return ensureRectangular(schema, rows);
77
+ }
78
+
79
+ // Compute the width and height of a set of cells, and make sure each
80
+ // row has the same number of cells.
81
+ function ensureRectangular(schema: Schema, rows: Fragment[]): Area {
82
+ const widths: ColWidths = [];
83
+ for (let i = 0; i < rows.length; i++) {
84
+ const row = rows[i];
85
+ for (let j = row.childCount - 1; j >= 0; j--) {
86
+ const { rowspan, colspan } = row.child(j).attrs;
87
+ for (let r = i; r < i + rowspan; r++) {
88
+ widths[r] = (widths[r] || 0) + colspan;
89
+ }
90
+ }
91
+ }
92
+ let width = 0;
93
+ for (let r = 0; r < widths.length; r++) width = Math.max(width, widths[r]);
94
+ for (let r = 0; r < widths.length; r++) {
95
+ if (r >= rows.length) rows.push(Fragment.empty);
96
+ if (widths[r] < width) {
97
+ const empty = tableNodeTypes(schema).cell.createAndFill()!;
98
+ const cells = [];
99
+ for (let i = widths[r]; i < width; i++) {
100
+ cells.push(empty);
101
+ }
102
+ rows[r] = rows[r].append(Fragment.from(cells));
103
+ }
104
+ }
105
+ return { height: rows.length, width, rows };
106
+ }
107
+
108
+ export function fitSlice(nodeType: NodeType, slice: Slice): Node {
109
+ const node = nodeType.createAndFill()!;
110
+ const tr = new Transform(node).replace(0, node.content.size, slice);
111
+ return tr.doc;
112
+ }
113
+
114
+ /**
115
+ * Clip or extend (repeat) the given set of cells to cover the given
116
+ * width and height. Will clip rowspan/colspan cells at the edges when
117
+ * they stick out.
118
+ *
119
+ * @internal
120
+ */
121
+ export function clipCells(
122
+ { width, height, rows }: Area,
123
+ newWidth: number,
124
+ newHeight: number,
125
+ ): Area {
126
+ if (width != newWidth) {
127
+ const added: number[] = [];
128
+ const newRows: Fragment[] = [];
129
+ for (let row = 0; row < rows.length; row++) {
130
+ const frag = rows[row],
131
+ cells = [];
132
+ for (let col = added[row] || 0, i = 0; col < newWidth; i++) {
133
+ let cell = frag.child(i % frag.childCount);
134
+ if (col + cell.attrs.colspan > newWidth) {
135
+ cell = cell.type.createChecked(
136
+ removeColSpan(
137
+ cell.attrs as CellAttrs,
138
+ cell.attrs.colspan,
139
+ col + cell.attrs.colspan - newWidth,
140
+ ),
141
+ cell.content,
142
+ );
143
+ }
144
+ cells.push(cell);
145
+ col += cell.attrs.colspan;
146
+ for (let j = 1; j < cell.attrs.rowspan; j++) {
147
+ added[row + j] = (added[row + j] || 0) + cell.attrs.colspan;
148
+ }
149
+ }
150
+ newRows.push(Fragment.from(cells));
151
+ }
152
+ rows = newRows;
153
+ width = newWidth;
154
+ }
155
+
156
+ if (height != newHeight) {
157
+ const newRows = [];
158
+ for (let row = 0, i = 0; row < newHeight; row++, i++) {
159
+ const cells = [],
160
+ source = rows[i % height];
161
+ for (let j = 0; j < source.childCount; j++) {
162
+ let cell = source.child(j);
163
+ if (row + cell.attrs.rowspan > newHeight) {
164
+ cell = cell.type.create(
165
+ {
166
+ ...cell.attrs,
167
+ rowspan: Math.max(1, newHeight - cell.attrs.rowspan),
168
+ },
169
+ cell.content,
170
+ );
171
+ }
172
+ cells.push(cell);
173
+ }
174
+ newRows.push(Fragment.from(cells));
175
+ }
176
+ rows = newRows;
177
+ height = newHeight;
178
+ }
179
+
180
+ return { width, height, rows };
181
+ }
182
+
183
+ // Make sure a table has at least the given width and height. Return
184
+ // true if something was changed.
185
+ function growTable(
186
+ tr: Transaction,
187
+ map: TableMap,
188
+ table: Node,
189
+ start: number,
190
+ width: number,
191
+ height: number,
192
+ mapFrom: number,
193
+ ): boolean {
194
+ const schema = tr.doc.type.schema;
195
+ const types = tableNodeTypes(schema);
196
+ let empty;
197
+ let emptyHead;
198
+ if (width > map.width) {
199
+ for (let row = 0, rowEnd = 0; row < map.height; row++) {
200
+ const rowNode = table.child(row);
201
+ rowEnd += rowNode.nodeSize;
202
+ const cells: Node[] = [];
203
+ let add: Node;
204
+ if (rowNode.lastChild == null || rowNode.lastChild.type == types.cell) {
205
+ add = empty || (empty = types.cell.createAndFill()!);
206
+ } else {add = emptyHead ||
207
+ (emptyHead = types.header_cell.createAndFill()!);}
208
+ for (let i = map.width; i < width; i++) cells.push(add);
209
+ tr.insert(tr.mapping.slice(mapFrom).map(rowEnd - 1 + start), cells);
210
+ }
211
+ }
212
+ if (height > map.height) {
213
+ const cells = [];
214
+ for (
215
+ let i = 0, start = (map.height - 1) * map.width;
216
+ i < Math.max(map.width, width);
217
+ i++
218
+ ) {
219
+ const header = i >= map.width
220
+ ? false
221
+ : table.nodeAt(map.map[start + i])!.type == types.header_cell;
222
+ cells.push(
223
+ header
224
+ ? emptyHead || (emptyHead = types.header_cell.createAndFill()!)
225
+ : empty || (empty = types.cell.createAndFill()!),
226
+ );
227
+ }
228
+
229
+ const emptyRow = types.row.create(null, Fragment.from(cells)),
230
+ rows = [];
231
+ for (let i = map.height; i < height; i++) rows.push(emptyRow);
232
+ tr.insert(tr.mapping.slice(mapFrom).map(start + table.nodeSize - 2), rows);
233
+ }
234
+ return !!(empty || emptyHead);
235
+ }
236
+
237
+ // Make sure the given line (left, top) to (right, top) doesn't cross
238
+ // any rowspan cells by splitting cells that cross it. Return true if
239
+ // something changed.
240
+ function isolateHorizontal(
241
+ tr: Transaction,
242
+ map: TableMap,
243
+ table: Node,
244
+ start: number,
245
+ left: number,
246
+ right: number,
247
+ top: number,
248
+ mapFrom: number,
249
+ ): boolean {
250
+ if (top == 0 || top == map.height) return false;
251
+ let found = false;
252
+ for (let col = left; col < right; col++) {
253
+ const index = top * map.width + col,
254
+ pos = map.map[index];
255
+ if (map.map[index - map.width] == pos) {
256
+ found = true;
257
+ const cell = table.nodeAt(pos)!;
258
+ const { top: cellTop, left: cellLeft } = map.findCell(pos);
259
+ tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + start), null, {
260
+ ...cell.attrs,
261
+ rowspan: top - cellTop,
262
+ });
263
+ tr.insert(
264
+ tr.mapping.slice(mapFrom).map(map.positionAt(top, cellLeft, table)),
265
+ cell.type.createAndFill({
266
+ ...cell.attrs,
267
+ rowspan: cellTop + cell.attrs.rowspan - top,
268
+ })!,
269
+ );
270
+ col += cell.attrs.colspan - 1;
271
+ }
272
+ }
273
+ return found;
274
+ }
275
+
276
+ // Make sure the given line (left, top) to (left, bottom) doesn't
277
+ // cross any colspan cells by splitting cells that cross it. Return
278
+ // true if something changed.
279
+ function isolateVertical(
280
+ tr: Transaction,
281
+ map: TableMap,
282
+ table: Node,
283
+ start: number,
284
+ top: number,
285
+ bottom: number,
286
+ left: number,
287
+ mapFrom: number,
288
+ ): boolean {
289
+ if (left == 0 || left == map.width) return false;
290
+ let found = false;
291
+ for (let row = top; row < bottom; row++) {
292
+ const index = row * map.width + left,
293
+ pos = map.map[index];
294
+ if (map.map[index - 1] == pos) {
295
+ found = true;
296
+ const cell = table.nodeAt(pos)!;
297
+ const cellLeft = map.colCount(pos);
298
+ const updatePos = tr.mapping.slice(mapFrom).map(pos + start);
299
+ tr.setNodeMarkup(
300
+ updatePos,
301
+ null,
302
+ removeColSpan(
303
+ cell.attrs as CellAttrs,
304
+ left - cellLeft,
305
+ cell.attrs.colspan - (left - cellLeft),
306
+ ),
307
+ );
308
+ tr.insert(
309
+ updatePos + cell.nodeSize,
310
+ cell.type.createAndFill(
311
+ removeColSpan(cell.attrs as CellAttrs, 0, left - cellLeft),
312
+ )!,
313
+ );
314
+ row += cell.attrs.rowspan - 1;
315
+ }
316
+ }
317
+ return found;
318
+ }
319
+
320
+ /**
321
+ * Insert the given set of cells (as returned by `pastedCells`) into a
322
+ * table, at the position pointed at by rect.
323
+ *
324
+ * @internal
325
+ */
326
+ export function insertCells(
327
+ state: EditorState,
328
+ dispatch: (tr: Transaction) => void,
329
+ tableStart: number,
330
+ rect: Rect,
331
+ cells: Area,
332
+ ): void {
333
+ let table = tableStart ? state.doc.nodeAt(tableStart - 1) : state.doc;
334
+ if (!table) {
335
+ throw new Error('No table found');
336
+ }
337
+ let map = TableMap.get(table);
338
+ const { top, left } = rect;
339
+ const right = left + cells.width,
340
+ bottom = top + cells.height;
341
+ const tr = state.tr;
342
+ let mapFrom = 0;
343
+
344
+ function recomp(): void {
345
+ table = tableStart ? tr.doc.nodeAt(tableStart - 1) : tr.doc;
346
+ if (!table) {
347
+ throw new Error('No table found');
348
+ }
349
+ map = TableMap.get(table);
350
+ mapFrom = tr.mapping.maps.length;
351
+ }
352
+
353
+ // Prepare the table to be large enough and not have any cells
354
+ // crossing the boundaries of the rectangle that we want to
355
+ // insert into. If anything about it changes, recompute the table
356
+ // map so that subsequent operations can see the current shape.
357
+ if (growTable(tr, map, table, tableStart, right, bottom, mapFrom)) recomp();
358
+ if (
359
+ isolateHorizontal(tr, map, table, tableStart, left, right, top, mapFrom)
360
+ ) {
361
+ recomp();
362
+ }
363
+ if (
364
+ isolateHorizontal(tr, map, table, tableStart, left, right, bottom, mapFrom)
365
+ ) {
366
+ recomp();
367
+ }
368
+ if (isolateVertical(tr, map, table, tableStart, top, bottom, left, mapFrom)) {
369
+ recomp();
370
+ }
371
+ if (
372
+ isolateVertical(tr, map, table, tableStart, top, bottom, right, mapFrom)
373
+ ) {
374
+ recomp();
375
+ }
376
+
377
+ for (let row = top; row < bottom; row++) {
378
+ const from = map.positionAt(row, left, table),
379
+ to = map.positionAt(row, right, table);
380
+ tr.replace(
381
+ tr.mapping.slice(mapFrom).map(from + tableStart),
382
+ tr.mapping.slice(mapFrom).map(to + tableStart),
383
+ new Slice(cells.rows[row - top], 0, 0),
384
+ );
385
+ }
386
+ recomp();
387
+ tr.setSelection(
388
+ new CellSelection(
389
+ tr.doc.resolve(tableStart + map.positionAt(top, left, table)),
390
+ tr.doc.resolve(tableStart + map.positionAt(bottom - 1, right - 1, table)),
391
+ ),
392
+ );
393
+ dispatch(tr);
394
+ }
@@ -0,0 +1,12 @@
1
+ import { Fragment, Node as ProsemirrorNode, NodeType } from 'prosemirror-model';
2
+
3
+ export function createCell(
4
+ cellType: NodeType,
5
+ cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
6
+ ): ProsemirrorNode | null | undefined {
7
+ if (cellContent) {
8
+ return cellType.createChecked(null, cellContent);
9
+ }
10
+
11
+ return cellType.createAndFill();
12
+ }
@@ -0,0 +1,53 @@
1
+ import { Fragment, Node as ProsemirrorNode, Schema } from 'prosemirror-model';
2
+
3
+ import { createCell } from './createCell.js';
4
+ import { getTableNodeTypes } from './getTableNodeTypes.js';
5
+
6
+ export function createTable(
7
+ schema: Schema,
8
+ rowsCount: number,
9
+ colsCount: number,
10
+ withHeaderRow: boolean,
11
+ cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
12
+ ): ProsemirrorNode {
13
+ const types = getTableNodeTypes(schema);
14
+ const headerCells: ProsemirrorNode[] = [];
15
+ const cells: ProsemirrorNode[] = [];
16
+
17
+ for (let index = 0; index < colsCount; index += 1) {
18
+ const cell = createCell(types.cell, cellContent);
19
+
20
+ if (cell) {
21
+ cells.push(cell);
22
+ }
23
+
24
+ if (withHeaderRow) {
25
+ const headerCell = createCell(types.header_cell, cellContent);
26
+
27
+ if (headerCell) {
28
+ headerCells.push(headerCell);
29
+ }
30
+ }
31
+ }
32
+
33
+ const rows: ProsemirrorNode[] = [];
34
+
35
+ for (let index = 0; index < rowsCount; index += 1) {
36
+ rows.push(
37
+ types.row.createChecked(
38
+ null,
39
+ withHeaderRow && index === 0 ? headerCells : cells,
40
+ ),
41
+ );
42
+ }
43
+
44
+ const attrs: Record<string, string> = {};
45
+ const tableSpec = schema.spec.nodes.get('table')!;
46
+ for (const [key, attrSpec] of Object.entries(tableSpec.attrs || {})) {
47
+ if ('undefined' !== typeof attrSpec.default) {
48
+ attrs[key] = attrSpec.default;
49
+ }
50
+ }
51
+
52
+ return types.table.createChecked(attrs, rows);
53
+ }
@@ -0,0 +1,156 @@
1
+ // This file defines helpers for normalizing tables, making sure no
2
+ // cells overlap (which can happen, if you have the wrong col- and
3
+ // rowspans) and that each row has the same width. Uses the problems
4
+ // reported by `TableMap`.
5
+
6
+ import { Node } from 'prosemirror-model';
7
+ import { EditorState, PluginKey, Transaction } from 'prosemirror-state';
8
+ import { tableNodeTypes, TableRole } from './tableNodeTypes.js';
9
+ import { TableMap } from './TableMap.js';
10
+ import { CellAttrs, removeColSpan } from './util.js';
11
+
12
+ /**
13
+ * @public
14
+ */
15
+ export const fixTablesKey = new PluginKey<{ fixTables: boolean }>('fix-tables');
16
+
17
+ /**
18
+ * Helper for iterating through the nodes in a document that changed
19
+ * compared to the given previous document. Useful for avoiding
20
+ * duplicate work on each transaction.
21
+ *
22
+ * @public
23
+ */
24
+ function changedDescendants(
25
+ old: Node,
26
+ cur: Node,
27
+ offset: number,
28
+ f: (node: Node, pos: number) => void,
29
+ ): void {
30
+ const oldSize = old.childCount,
31
+ curSize = cur.childCount;
32
+ outer: for (let i = 0, j = 0; i < curSize; i++) {
33
+ const child = cur.child(i);
34
+ for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) {
35
+ if (old.child(scan) == child) {
36
+ j = scan + 1;
37
+ offset += child.nodeSize;
38
+ continue outer;
39
+ }
40
+ }
41
+ f(child, offset);
42
+ if (j < oldSize && old.child(j).sameMarkup(child)) {
43
+ changedDescendants(old.child(j), child, offset + 1, f);
44
+ } else child.nodesBetween(0, child.content.size, f, offset + 1);
45
+ offset += child.nodeSize;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Inspect all tables in the given state's document and return a
51
+ * transaction that fixes them, if necessary. If `oldState` was
52
+ * provided, that is assumed to hold a previous, known-good state,
53
+ * which will be used to avoid re-scanning unchanged parts of the
54
+ * document.
55
+ *
56
+ * @public
57
+ */
58
+ export function fixTables(
59
+ state: EditorState,
60
+ oldState?: EditorState,
61
+ ): Transaction | undefined {
62
+ let tr: Transaction | undefined;
63
+ const check = (node: Node, pos: number) => {
64
+ if (node.type.spec.tableRole == 'table') {
65
+ tr = fixTable(state, node, pos, tr);
66
+ }
67
+ };
68
+ if (!oldState) state.doc.descendants(check);
69
+ else if (oldState.doc != state.doc) {
70
+ changedDescendants(oldState.doc, state.doc, 0, check);
71
+ }
72
+ return tr;
73
+ }
74
+
75
+ // Fix the given table, if necessary. Will append to the transaction
76
+ // it was given, if non-null, or create a new one if necessary.
77
+ export function fixTable(
78
+ state: EditorState,
79
+ table: Node,
80
+ tablePos: number,
81
+ tr: Transaction | undefined,
82
+ ): Transaction | undefined {
83
+ const map = TableMap.get(table);
84
+ if (!map.problems) return tr;
85
+ if (!tr) tr = state.tr;
86
+
87
+ // Track which rows we must add cells to, so that we can adjust that
88
+ // when fixing collisions.
89
+ const mustAdd: number[] = [];
90
+ for (let i = 0; i < map.height; i++) mustAdd.push(0);
91
+ for (let i = 0; i < map.problems.length; i++) {
92
+ const prob = map.problems[i];
93
+ if (prob.type == 'collision') {
94
+ const cell = table.nodeAt(prob.pos);
95
+ if (!cell) continue;
96
+ const attrs = cell.attrs as CellAttrs;
97
+ for (let j = 0; j < attrs.rowspan; j++) mustAdd[prob.row + j] += prob.n;
98
+ tr.setNodeMarkup(
99
+ tr.mapping.map(tablePos + 1 + prob.pos),
100
+ null,
101
+ removeColSpan(attrs, attrs.colspan - prob.n, prob.n),
102
+ );
103
+ } else if (prob.type == 'missing') {
104
+ mustAdd[prob.row] += prob.n;
105
+ } else if (prob.type == 'overlong_rowspan') {
106
+ const cell = table.nodeAt(prob.pos);
107
+ if (!cell) continue;
108
+ tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, {
109
+ ...cell.attrs,
110
+ rowspan: cell.attrs.rowspan - prob.n,
111
+ });
112
+ } else if (prob.type == 'colwidth mismatch') {
113
+ const cell = table.nodeAt(prob.pos);
114
+ if (!cell) continue;
115
+ tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, {
116
+ ...cell.attrs,
117
+ colwidth: prob.colwidth,
118
+ });
119
+ } else if (prob.type == 'zero_sized') {
120
+ const pos = tr.mapping.map(tablePos);
121
+ tr.delete(pos, pos + table.nodeSize);
122
+ }
123
+ }
124
+ let first, last;
125
+ for (let i = 0; i < mustAdd.length; i++) {
126
+ if (mustAdd[i]) {
127
+ if (first == null) first = i;
128
+ last = i;
129
+ }
130
+ }
131
+ // Add the necessary cells, using a heuristic for whether to add the
132
+ // cells at the start or end of the rows (if it looks like a 'bite'
133
+ // was taken out of the table, add cells at the start of the row
134
+ // after the bite. Otherwise add them at the end).
135
+ for (let i = 0, pos = tablePos + 1; i < map.height; i++) {
136
+ const row = table.child(i);
137
+ const end = pos + row.nodeSize;
138
+ const add = mustAdd[i];
139
+ if (add > 0) {
140
+ let role: TableRole = 'cell';
141
+ if (row.firstChild) {
142
+ role = row.firstChild.type.spec.tableRole;
143
+ }
144
+ const nodes: Node[] = [];
145
+ for (let j = 0; j < add; j++) {
146
+ const node = tableNodeTypes(state.schema)[role].createAndFill();
147
+
148
+ if (node) nodes.push(node);
149
+ }
150
+ const side = (i == 0 || first == i - 1) && last == i ? pos + 1 : end - 1;
151
+ tr.insert(tr.mapping.map(side), nodes);
152
+ }
153
+ pos = end;
154
+ }
155
+ return tr.setMeta(fixTablesKey, { fixTables: true });
156
+ }
@@ -0,0 +1,21 @@
1
+ import { NodeType, Schema } from 'prosemirror-model';
2
+
3
+ export function getTableNodeTypes(schema: Schema): { [key: string]: NodeType } {
4
+ if (schema.cached.tableNodeTypes) {
5
+ return schema.cached.tableNodeTypes;
6
+ }
7
+
8
+ const roles: { [key: string]: NodeType } = {};
9
+
10
+ Object.keys(schema.nodes).forEach((type) => {
11
+ const nodeType = schema.nodes[type];
12
+
13
+ if (nodeType.spec.tableRole) {
14
+ roles[nodeType.spec.tableRole] = nodeType;
15
+ }
16
+ });
17
+
18
+ schema.cached.tableNodeTypes = roles;
19
+
20
+ return roles;
21
+ }