@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,896 @@
1
+ // This file defines a number of table-related commands.
2
+
3
+ import {
4
+ Fragment,
5
+ Node,
6
+ NodeType,
7
+ ResolvedPos,
8
+ Slice,
9
+ } from 'prosemirror-model';
10
+ import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
11
+
12
+ import type { Command, CommandFactory } from '@kerebron/editor/commands';
13
+
14
+ import { CellSelection } from './CellSelection.js';
15
+ import type { Direction } from './input.js';
16
+ import { tableNodeTypes, TableRole } from './tableNodeTypes.js';
17
+ import { Rect, TableMap } from './TableMap.js';
18
+ import {
19
+ addColSpan,
20
+ cellAround,
21
+ CellAttrs,
22
+ cellWrapping,
23
+ columnIsHeader,
24
+ isInTable,
25
+ moveCellForward,
26
+ removeColSpan,
27
+ selectionCell,
28
+ } from './util.js';
29
+
30
+ /**
31
+ * @public
32
+ */
33
+ export type TableRect = Rect & {
34
+ tableStart: number;
35
+ map: TableMap;
36
+ table: Node;
37
+ };
38
+
39
+ /**
40
+ * Helper to get the selected rectangle in a table, if any. Adds table
41
+ * map, table node, and table start offset to the object for
42
+ * convenience.
43
+ *
44
+ * @public
45
+ */
46
+ export function selectedRect(state: EditorState): TableRect {
47
+ const sel = state.selection;
48
+ const $pos = selectionCell(state);
49
+ const table = $pos.node(-1);
50
+ const tableStart = $pos.start(-1);
51
+ const map = TableMap.get(table);
52
+ const rect = sel instanceof CellSelection
53
+ ? map.rectBetween(
54
+ sel.$anchorCell.pos - tableStart,
55
+ sel.$headCell.pos - tableStart,
56
+ )
57
+ : map.findCell($pos.pos - tableStart);
58
+ return { ...rect, tableStart, map, table };
59
+ }
60
+
61
+ /**
62
+ * Add a column at the given position in a table.
63
+ *
64
+ * @public
65
+ */
66
+ export function addColumn(
67
+ tr: Transaction,
68
+ { map, tableStart, table }: TableRect,
69
+ col: number,
70
+ ): Transaction {
71
+ let refColumn: number | null = col > 0 ? -1 : 0;
72
+ if (columnIsHeader(map, table, col + refColumn)) {
73
+ refColumn = col == 0 || col == map.width ? null : 0;
74
+ }
75
+
76
+ for (let row = 0; row < map.height; row++) {
77
+ const index = row * map.width + col;
78
+ // If this position falls inside a col-spanning cell
79
+ if (col > 0 && col < map.width && map.map[index - 1] == map.map[index]) {
80
+ const pos = map.map[index];
81
+ const cell = table.nodeAt(pos)!;
82
+ tr.setNodeMarkup(
83
+ tr.mapping.map(tableStart + pos),
84
+ null,
85
+ addColSpan(cell.attrs as CellAttrs, col - map.colCount(pos)),
86
+ );
87
+ // Skip ahead if rowspan > 1
88
+ row += cell.attrs.rowspan - 1;
89
+ } else {
90
+ const type = refColumn == null
91
+ ? tableNodeTypes(table.type.schema).cell
92
+ : table.nodeAt(map.map[index + refColumn])!.type;
93
+ const pos = map.positionAt(row, col, table);
94
+ tr.insert(tr.mapping.map(tableStart + pos), type.createAndFill()!);
95
+ }
96
+ }
97
+ return tr;
98
+ }
99
+
100
+ /**
101
+ * Command to add a column before the column with the selection.
102
+ *
103
+ * @public
104
+ */
105
+ export function addColumnBefore(
106
+ state: EditorState,
107
+ dispatch?: (tr: Transaction) => void,
108
+ ): boolean {
109
+ if (!isInTable(state)) return false;
110
+ if (dispatch) {
111
+ const rect = selectedRect(state);
112
+ dispatch(addColumn(state.tr, rect, rect.left));
113
+ }
114
+ return true;
115
+ }
116
+
117
+ /**
118
+ * Command to add a column after the column with the selection.
119
+ *
120
+ * @public
121
+ */
122
+ export function addColumnAfter(
123
+ state: EditorState,
124
+ dispatch?: (tr: Transaction) => void,
125
+ ): boolean {
126
+ if (!isInTable(state)) return false;
127
+ if (dispatch) {
128
+ const rect = selectedRect(state);
129
+ dispatch(addColumn(state.tr, rect, rect.right));
130
+ }
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * @public
136
+ */
137
+ export function removeColumn(
138
+ tr: Transaction,
139
+ { map, table, tableStart }: TableRect,
140
+ col: number,
141
+ ) {
142
+ const mapStart = tr.mapping.maps.length;
143
+ for (let row = 0; row < map.height;) {
144
+ const index = row * map.width + col;
145
+ const pos = map.map[index];
146
+ const cell = table.nodeAt(pos)!;
147
+ const attrs = cell.attrs as CellAttrs;
148
+ // If this is part of a col-spanning cell
149
+ if (
150
+ (col > 0 && map.map[index - 1] == pos) ||
151
+ (col < map.width - 1 && map.map[index + 1] == pos)
152
+ ) {
153
+ tr.setNodeMarkup(
154
+ tr.mapping.slice(mapStart).map(tableStart + pos),
155
+ null,
156
+ removeColSpan(attrs, col - map.colCount(pos)),
157
+ );
158
+ } else {
159
+ const start = tr.mapping.slice(mapStart).map(tableStart + pos);
160
+ tr.delete(start, start + cell.nodeSize);
161
+ }
162
+ row += attrs.rowspan;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Command function that removes the selected columns from a table.
168
+ *
169
+ * @public
170
+ */
171
+ export function deleteColumn(
172
+ state: EditorState,
173
+ dispatch?: (tr: Transaction) => void,
174
+ ): boolean {
175
+ if (!isInTable(state)) return false;
176
+ if (dispatch) {
177
+ const rect = selectedRect(state);
178
+ const tr = state.tr;
179
+ if (rect.left == 0 && rect.right == rect.map.width) return false;
180
+ for (let i = rect.right - 1;; i--) {
181
+ removeColumn(tr, rect, i);
182
+ if (i == rect.left) break;
183
+ const table = rect.tableStart
184
+ ? tr.doc.nodeAt(rect.tableStart - 1)
185
+ : tr.doc;
186
+ if (!table) {
187
+ throw RangeError('No table found');
188
+ }
189
+ rect.table = table;
190
+ rect.map = TableMap.get(table);
191
+ }
192
+ dispatch(tr);
193
+ }
194
+ return true;
195
+ }
196
+
197
+ /**
198
+ * @public
199
+ */
200
+ export function rowIsHeader(map: TableMap, table: Node, row: number): boolean {
201
+ const headerCell = tableNodeTypes(table.type.schema).header_cell;
202
+ for (let col = 0; col < map.width; col++) {
203
+ if (table.nodeAt(map.map[col + row * map.width])?.type != headerCell) {
204
+ return false;
205
+ }
206
+ }
207
+ return true;
208
+ }
209
+
210
+ /**
211
+ * @public
212
+ */
213
+ export function addRow(
214
+ tr: Transaction,
215
+ { map, tableStart, table }: TableRect,
216
+ row: number,
217
+ ): Transaction {
218
+ let rowPos = tableStart;
219
+ for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize;
220
+ const cells = [];
221
+ let refRow: number | null = row > 0 ? -1 : 0;
222
+ if (rowIsHeader(map, table, row + refRow)) {
223
+ refRow = row == 0 || row == map.height ? null : 0;
224
+ }
225
+ for (let col = 0, index = map.width * row; col < map.width; col++, index++) {
226
+ // Covered by a rowspan cell
227
+ if (
228
+ row > 0 &&
229
+ row < map.height &&
230
+ map.map[index] == map.map[index - map.width]
231
+ ) {
232
+ const pos = map.map[index];
233
+ const attrs = table.nodeAt(pos)!.attrs;
234
+ tr.setNodeMarkup(tableStart + pos, null, {
235
+ ...attrs,
236
+ rowspan: attrs.rowspan + 1,
237
+ });
238
+ col += attrs.colspan - 1;
239
+ } else {
240
+ const type = refRow == null
241
+ ? tableNodeTypes(table.type.schema).cell
242
+ : table.nodeAt(map.map[index + refRow * map.width])?.type;
243
+ const node = type?.createAndFill();
244
+ if (node) cells.push(node);
245
+ }
246
+ }
247
+ tr.insert(rowPos, tableNodeTypes(table.type.schema).row.create(null, cells));
248
+ return tr;
249
+ }
250
+
251
+ /**
252
+ * Add a table row before the selection.
253
+ *
254
+ * @public
255
+ */
256
+ export function addRowBefore(
257
+ state: EditorState,
258
+ dispatch?: (tr: Transaction) => void,
259
+ ): boolean {
260
+ if (!isInTable(state)) return false;
261
+ if (dispatch) {
262
+ const rect = selectedRect(state);
263
+ dispatch(addRow(state.tr, rect, rect.top));
264
+ }
265
+ return true;
266
+ }
267
+
268
+ /**
269
+ * Add a table row after the selection.
270
+ *
271
+ * @public
272
+ */
273
+ export function addRowAfter(
274
+ state: EditorState,
275
+ dispatch?: (tr: Transaction) => void,
276
+ ): boolean {
277
+ if (!isInTable(state)) return false;
278
+ if (dispatch) {
279
+ const rect = selectedRect(state);
280
+ dispatch(addRow(state.tr, rect, rect.bottom));
281
+ }
282
+ return true;
283
+ }
284
+
285
+ /**
286
+ * @public
287
+ */
288
+ export function removeRow(
289
+ tr: Transaction,
290
+ { map, table, tableStart }: TableRect,
291
+ row: number,
292
+ ): void {
293
+ let rowPos = 0;
294
+ for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize;
295
+ const nextRow = rowPos + table.child(row).nodeSize;
296
+
297
+ const mapFrom = tr.mapping.maps.length;
298
+ tr.delete(rowPos + tableStart, nextRow + tableStart);
299
+
300
+ const seen = new Set<number>();
301
+
302
+ for (let col = 0, index = row * map.width; col < map.width; col++, index++) {
303
+ const pos = map.map[index];
304
+
305
+ // Skip cells that are checked already
306
+ if (seen.has(pos)) continue;
307
+ seen.add(pos);
308
+
309
+ if (row > 0 && pos == map.map[index - map.width]) {
310
+ // If this cell starts in the row above, simply reduce its rowspan
311
+ const attrs = table.nodeAt(pos)!.attrs as CellAttrs;
312
+ tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + tableStart), null, {
313
+ ...attrs,
314
+ rowspan: attrs.rowspan - 1,
315
+ });
316
+ col += attrs.colspan - 1;
317
+ } else if (row < map.height && pos == map.map[index + map.width]) {
318
+ // Else, if it continues in the row below, it has to be moved down
319
+ const cell = table.nodeAt(pos)!;
320
+ const attrs = cell.attrs as CellAttrs;
321
+ const copy = cell.type.create(
322
+ { ...attrs, rowspan: cell.attrs.rowspan - 1 },
323
+ cell.content,
324
+ );
325
+ const newPos = map.positionAt(row + 1, col, table);
326
+ tr.insert(tr.mapping.slice(mapFrom).map(tableStart + newPos), copy);
327
+ col += attrs.colspan - 1;
328
+ }
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Remove the selected rows from a table.
334
+ *
335
+ * @public
336
+ */
337
+ export function deleteRow(
338
+ state: EditorState,
339
+ dispatch?: (tr: Transaction) => void,
340
+ ): boolean {
341
+ if (!isInTable(state)) return false;
342
+ if (dispatch) {
343
+ const rect = selectedRect(state),
344
+ tr = state.tr;
345
+ if (rect.top == 0 && rect.bottom == rect.map.height) return false;
346
+ for (let i = rect.bottom - 1;; i--) {
347
+ removeRow(tr, rect, i);
348
+ if (i == rect.top) break;
349
+ const table = rect.tableStart
350
+ ? tr.doc.nodeAt(rect.tableStart - 1)
351
+ : tr.doc;
352
+ if (!table) {
353
+ throw RangeError('No table found');
354
+ }
355
+ rect.table = table;
356
+ rect.map = TableMap.get(rect.table);
357
+ }
358
+ dispatch(tr);
359
+ }
360
+ return true;
361
+ }
362
+
363
+ function isEmpty(cell: Node): boolean {
364
+ const c = cell.content;
365
+
366
+ return (
367
+ c.childCount == 1 && c.child(0).isTextblock && c.child(0).childCount == 0
368
+ );
369
+ }
370
+
371
+ function cellsOverlapRectangle({ width, height, map }: TableMap, rect: Rect) {
372
+ let indexTop = rect.top * width + rect.left,
373
+ indexLeft = indexTop;
374
+ let indexBottom = (rect.bottom - 1) * width + rect.left,
375
+ indexRight = indexTop + (rect.right - rect.left - 1);
376
+ for (let i = rect.top; i < rect.bottom; i++) {
377
+ if (
378
+ (rect.left > 0 && map[indexLeft] == map[indexLeft - 1]) ||
379
+ (rect.right < width && map[indexRight] == map[indexRight + 1])
380
+ ) {
381
+ return true;
382
+ }
383
+ indexLeft += width;
384
+ indexRight += width;
385
+ }
386
+ for (let i = rect.left; i < rect.right; i++) {
387
+ if (
388
+ (rect.top > 0 && map[indexTop] == map[indexTop - width]) ||
389
+ (rect.bottom < height && map[indexBottom] == map[indexBottom + width])
390
+ ) {
391
+ return true;
392
+ }
393
+ indexTop++;
394
+ indexBottom++;
395
+ }
396
+ return false;
397
+ }
398
+
399
+ /**
400
+ * Merge the selected cells into a single cell. Only available when
401
+ * the selected cells' outline forms a rectangle.
402
+ *
403
+ * @public
404
+ */
405
+ export function mergeCells(
406
+ state: EditorState,
407
+ dispatch?: (tr: Transaction) => void,
408
+ ): boolean {
409
+ const sel = state.selection;
410
+ if (
411
+ !(sel instanceof CellSelection) ||
412
+ sel.$anchorCell.pos == sel.$headCell.pos
413
+ ) {
414
+ return false;
415
+ }
416
+ const rect = selectedRect(state),
417
+ { map } = rect;
418
+ if (cellsOverlapRectangle(map, rect)) return false;
419
+ if (dispatch) {
420
+ const tr = state.tr;
421
+ const seen: Record<number, boolean> = {};
422
+ let content = Fragment.empty;
423
+ let mergedPos: number | undefined;
424
+ let mergedCell: Node | undefined;
425
+ for (let row = rect.top; row < rect.bottom; row++) {
426
+ for (let col = rect.left; col < rect.right; col++) {
427
+ const cellPos = map.map[row * map.width + col];
428
+ const cell = rect.table.nodeAt(cellPos);
429
+ if (seen[cellPos] || !cell) continue;
430
+ seen[cellPos] = true;
431
+ if (mergedPos == null) {
432
+ mergedPos = cellPos;
433
+ mergedCell = cell;
434
+ } else {
435
+ if (!isEmpty(cell)) content = content.append(cell.content);
436
+ const mapped = tr.mapping.map(cellPos + rect.tableStart);
437
+ tr.delete(mapped, mapped + cell.nodeSize);
438
+ }
439
+ }
440
+ }
441
+ if (mergedPos == null || mergedCell == null) {
442
+ return true;
443
+ }
444
+
445
+ tr.setNodeMarkup(mergedPos + rect.tableStart, null, {
446
+ ...addColSpan(
447
+ mergedCell.attrs as CellAttrs,
448
+ mergedCell.attrs.colspan,
449
+ rect.right - rect.left - mergedCell.attrs.colspan,
450
+ ),
451
+ rowspan: rect.bottom - rect.top,
452
+ });
453
+ if (content.size) {
454
+ const end = mergedPos + 1 + mergedCell.content.size;
455
+ const start = isEmpty(mergedCell) ? mergedPos + 1 : end;
456
+ tr.replaceWith(start + rect.tableStart, end + rect.tableStart, content);
457
+ }
458
+ tr.setSelection(
459
+ new CellSelection(tr.doc.resolve(mergedPos + rect.tableStart)),
460
+ );
461
+ dispatch(tr);
462
+ }
463
+ return true;
464
+ }
465
+
466
+ /**
467
+ * Split a selected cell, whose rowpan or colspan is greater than one,
468
+ * into smaller cells. Use the first cell type for the new cells.
469
+ *
470
+ * @public
471
+ */
472
+ export function splitCell(
473
+ state: EditorState,
474
+ dispatch?: (tr: Transaction) => void,
475
+ ): boolean {
476
+ const nodeTypes = tableNodeTypes(state.schema);
477
+ return splitCellWithType(({ node }: { node: Node }) => {
478
+ return nodeTypes[node.type.spec.tableRole as TableRole];
479
+ })(state, dispatch);
480
+ }
481
+
482
+ /**
483
+ * @public
484
+ */
485
+ export interface GetCellTypeOptions {
486
+ node: Node;
487
+ row: number;
488
+ col: number;
489
+ }
490
+
491
+ /**
492
+ * Split a selected cell, whose rowpan or colspan is greater than one,
493
+ * into smaller cells with the cell type (th, td) returned by getType function.
494
+ *
495
+ * @public
496
+ */
497
+ export const splitCellWithType: CommandFactory = (
498
+ getCellType: (options: GetCellTypeOptions) => NodeType,
499
+ ) => {
500
+ return (state, dispatch) => {
501
+ const sel = state.selection;
502
+ let cellNode: Node | null | undefined;
503
+ let cellPos: number | undefined;
504
+ if (!(sel instanceof CellSelection)) {
505
+ cellNode = cellWrapping(sel.$from);
506
+ if (!cellNode) return false;
507
+ cellPos = cellAround(sel.$from)?.pos;
508
+ } else {
509
+ if (sel.$anchorCell.pos != sel.$headCell.pos) return false;
510
+ cellNode = sel.$anchorCell.nodeAfter;
511
+ cellPos = sel.$anchorCell.pos;
512
+ }
513
+ if (cellNode == null || cellPos == null) {
514
+ return false;
515
+ }
516
+ if (cellNode.attrs.colspan == 1 && cellNode.attrs.rowspan == 1) {
517
+ return false;
518
+ }
519
+ if (dispatch) {
520
+ let baseAttrs = cellNode.attrs;
521
+ const attrs = [];
522
+ const colwidth = baseAttrs.colwidth;
523
+ if (baseAttrs.rowspan > 1) baseAttrs = { ...baseAttrs, rowspan: 1 };
524
+ if (baseAttrs.colspan > 1) baseAttrs = { ...baseAttrs, colspan: 1 };
525
+ const rect = selectedRect(state),
526
+ tr = state.tr;
527
+ for (let i = 0; i < rect.right - rect.left; i++) {
528
+ attrs.push(
529
+ colwidth
530
+ ? {
531
+ ...baseAttrs,
532
+ colwidth: colwidth && colwidth[i] ? [colwidth[i]] : null,
533
+ }
534
+ : baseAttrs,
535
+ );
536
+ }
537
+ let lastCell;
538
+ for (let row = rect.top; row < rect.bottom; row++) {
539
+ let pos = rect.map.positionAt(row, rect.left, rect.table);
540
+ if (row == rect.top) pos += cellNode.nodeSize;
541
+ for (let col = rect.left, i = 0; col < rect.right; col++, i++) {
542
+ if (col == rect.left && row == rect.top) continue;
543
+ tr.insert(
544
+ lastCell = tr.mapping.map(pos + rect.tableStart, 1),
545
+ getCellType({ node: cellNode, row, col }).createAndFill(attrs[i])!,
546
+ );
547
+ }
548
+ }
549
+ tr.setNodeMarkup(
550
+ cellPos,
551
+ getCellType({ node: cellNode, row: rect.top, col: rect.left }),
552
+ attrs[0],
553
+ );
554
+ if (sel instanceof CellSelection) {
555
+ tr.setSelection(
556
+ new CellSelection(
557
+ tr.doc.resolve(sel.$anchorCell.pos),
558
+ lastCell ? tr.doc.resolve(lastCell) : undefined,
559
+ ),
560
+ );
561
+ }
562
+ dispatch(tr);
563
+ }
564
+ return true;
565
+ };
566
+ };
567
+
568
+ /**
569
+ * Returns a command that sets the given attribute to the given value,
570
+ * and is only available when the currently selected cell doesn't
571
+ * already have that attribute set to that value.
572
+ *
573
+ * @public
574
+ */
575
+ export const setCellAttr: CommandFactory = (name: string, value: unknown) => {
576
+ return function (state, dispatch) {
577
+ if (!isInTable(state)) return false;
578
+ const $cell = selectionCell(state);
579
+ if ($cell.nodeAfter!.attrs[name] === value) return false;
580
+ if (dispatch) {
581
+ const tr = state.tr;
582
+ if (state.selection instanceof CellSelection) {
583
+ state.selection.forEachCell((node, pos) => {
584
+ if (node.attrs[name] !== value) {
585
+ tr.setNodeMarkup(pos, null, {
586
+ ...node.attrs,
587
+ [name]: value,
588
+ });
589
+ }
590
+ });
591
+ } else {
592
+ tr.setNodeMarkup($cell.pos, null, {
593
+ ...$cell.nodeAfter!.attrs,
594
+ [name]: value,
595
+ });
596
+ }
597
+ dispatch(tr);
598
+ }
599
+ return true;
600
+ };
601
+ };
602
+
603
+ function deprecated_toggleHeader(type: ToggleHeaderType): Command {
604
+ return function (state, dispatch) {
605
+ if (!isInTable(state)) return false;
606
+ if (dispatch) {
607
+ const types = tableNodeTypes(state.schema);
608
+ const rect = selectedRect(state),
609
+ tr = state.tr;
610
+ const cells = rect.map.cellsInRect(
611
+ type == 'column'
612
+ ? {
613
+ left: rect.left,
614
+ top: 0,
615
+ right: rect.right,
616
+ bottom: rect.map.height,
617
+ }
618
+ : type == 'row'
619
+ ? {
620
+ left: 0,
621
+ top: rect.top,
622
+ right: rect.map.width,
623
+ bottom: rect.bottom,
624
+ }
625
+ : rect,
626
+ );
627
+ const nodes = cells.map((pos) => rect.table.nodeAt(pos)!);
628
+ for (
629
+ let i = 0;
630
+ i < cells.length;
631
+ i++ // Remove headers, if any
632
+ ) {
633
+ if (nodes[i].type == types.header_cell) {
634
+ tr.setNodeMarkup(
635
+ rect.tableStart + cells[i],
636
+ types.cell,
637
+ nodes[i].attrs,
638
+ );
639
+ }
640
+ }
641
+ if (tr.steps.length == 0) {
642
+ for (
643
+ let i = 0;
644
+ i < cells.length;
645
+ i++ // No headers removed, add instead
646
+ ) {
647
+ tr.setNodeMarkup(
648
+ rect.tableStart + cells[i],
649
+ types.header_cell,
650
+ nodes[i].attrs,
651
+ );
652
+ }
653
+ }
654
+ dispatch(tr);
655
+ }
656
+ return true;
657
+ };
658
+ }
659
+
660
+ function isHeaderEnabledByType(
661
+ type: 'row' | 'column',
662
+ rect: TableRect,
663
+ types: Record<string, NodeType>,
664
+ ): boolean {
665
+ // Get cell positions for first row or first column
666
+ const cellPositions = rect.map.cellsInRect({
667
+ left: 0,
668
+ top: 0,
669
+ right: type == 'row' ? rect.map.width : 1,
670
+ bottom: type == 'column' ? rect.map.height : 1,
671
+ });
672
+
673
+ for (let i = 0; i < cellPositions.length; i++) {
674
+ const cell = rect.table.nodeAt(cellPositions[i]);
675
+ if (cell && cell.type !== types.header_cell) {
676
+ return false;
677
+ }
678
+ }
679
+
680
+ return true;
681
+ }
682
+
683
+ /**
684
+ * @public
685
+ */
686
+ export type ToggleHeaderType = 'column' | 'row' | 'cell';
687
+
688
+ /**
689
+ * Toggles between row/column header and normal cells (Only applies to first row/column).
690
+ * For deprecated behavior pass `useDeprecatedLogic` in options with true.
691
+ *
692
+ * @public
693
+ */
694
+ export const toggleHeader: CommandFactory = (
695
+ type: ToggleHeaderType,
696
+ options?: { useDeprecatedLogic: boolean } | undefined,
697
+ ) => {
698
+ options = options || { useDeprecatedLogic: false };
699
+
700
+ if (options.useDeprecatedLogic) return deprecated_toggleHeader(type);
701
+
702
+ return function (state, dispatch) {
703
+ if (!isInTable(state)) return false;
704
+ if (dispatch) {
705
+ const types = tableNodeTypes(state.schema);
706
+ const rect = selectedRect(state),
707
+ tr = state.tr;
708
+
709
+ const isHeaderRowEnabled = isHeaderEnabledByType('row', rect, types);
710
+ const isHeaderColumnEnabled = isHeaderEnabledByType(
711
+ 'column',
712
+ rect,
713
+ types,
714
+ );
715
+
716
+ const isHeaderEnabled = type === 'column'
717
+ ? isHeaderRowEnabled
718
+ : type === 'row'
719
+ ? isHeaderColumnEnabled
720
+ : false;
721
+
722
+ const selectionStartsAt = isHeaderEnabled ? 1 : 0;
723
+
724
+ const cellsRect = type == 'column'
725
+ ? {
726
+ left: 0,
727
+ top: selectionStartsAt,
728
+ right: 1,
729
+ bottom: rect.map.height,
730
+ }
731
+ : type == 'row'
732
+ ? {
733
+ left: selectionStartsAt,
734
+ top: 0,
735
+ right: rect.map.width,
736
+ bottom: 1,
737
+ }
738
+ : rect;
739
+
740
+ const newType = type == 'column'
741
+ ? isHeaderColumnEnabled ? types.cell : types.header_cell
742
+ : type == 'row'
743
+ ? isHeaderRowEnabled ? types.cell : types.header_cell
744
+ : types.cell;
745
+
746
+ rect.map.cellsInRect(cellsRect).forEach((relativeCellPos) => {
747
+ const cellPos = relativeCellPos + rect.tableStart;
748
+ const cell = tr.doc.nodeAt(cellPos);
749
+
750
+ if (cell) {
751
+ tr.setNodeMarkup(cellPos, newType, cell.attrs);
752
+ }
753
+ });
754
+
755
+ dispatch(tr);
756
+ }
757
+ return true;
758
+ };
759
+ };
760
+
761
+ /**
762
+ * Toggles whether the selected row contains header cells.
763
+ *
764
+ * @public
765
+ */
766
+ export const toggleHeaderRow: Command = toggleHeader('row', {
767
+ useDeprecatedLogic: true,
768
+ });
769
+
770
+ /**
771
+ * Toggles whether the selected column contains header cells.
772
+ *
773
+ * @public
774
+ */
775
+ export const toggleHeaderColumn: Command = toggleHeader('column', {
776
+ useDeprecatedLogic: true,
777
+ });
778
+
779
+ /**
780
+ * Toggles whether the selected cells are header cells.
781
+ *
782
+ * @public
783
+ */
784
+ export const toggleHeaderCell: Command = toggleHeader('cell', {
785
+ useDeprecatedLogic: true,
786
+ });
787
+
788
+ function findNextCell($cell: ResolvedPos, dir: Direction): number | null {
789
+ if (dir < 0) {
790
+ const before = $cell.nodeBefore;
791
+ if (before) return $cell.pos - before.nodeSize;
792
+ for (
793
+ let row = $cell.index(-1) - 1, rowEnd = $cell.before();
794
+ row >= 0;
795
+ row--
796
+ ) {
797
+ const rowNode = $cell.node(-1).child(row);
798
+ const lastChild = rowNode.lastChild;
799
+ if (lastChild) {
800
+ return rowEnd - 1 - lastChild.nodeSize;
801
+ }
802
+ rowEnd -= rowNode.nodeSize;
803
+ }
804
+ } else {
805
+ if ($cell.index() < $cell.parent.childCount - 1) {
806
+ return $cell.pos + $cell.nodeAfter!.nodeSize;
807
+ }
808
+ const table = $cell.node(-1);
809
+ for (
810
+ let row = $cell.indexAfter(-1), rowStart = $cell.after();
811
+ row < table.childCount;
812
+ row++
813
+ ) {
814
+ const rowNode = table.child(row);
815
+ if (rowNode.childCount) return rowStart + 1;
816
+ rowStart += rowNode.nodeSize;
817
+ }
818
+ }
819
+ return null;
820
+ }
821
+
822
+ /**
823
+ * Returns a command for selecting the next (direction=1) or previous
824
+ * (direction=-1) cell in a table.
825
+ *
826
+ * @public
827
+ */
828
+ export const goToNextCell: CommandFactory = (direction: Direction) => {
829
+ return function (state, dispatch) {
830
+ if (!isInTable(state)) return false;
831
+ const cell = findNextCell(selectionCell(state), direction);
832
+ if (cell == null) return false;
833
+ if (dispatch) {
834
+ const $cell = state.doc.resolve(cell);
835
+ dispatch(
836
+ state.tr
837
+ .setSelection(TextSelection.between($cell, moveCellForward($cell)))
838
+ .scrollIntoView(),
839
+ );
840
+ }
841
+ return true;
842
+ };
843
+ };
844
+
845
+ /**
846
+ * Deletes the table around the selection, if any.
847
+ *
848
+ * @public
849
+ */
850
+ export function deleteTable(
851
+ state: EditorState,
852
+ dispatch?: (tr: Transaction) => void,
853
+ ): boolean {
854
+ const $pos = state.selection.$anchor;
855
+ for (let d = $pos.depth; d > 0; d--) {
856
+ const node = $pos.node(d);
857
+ if (node.type.spec.tableRole == 'table') {
858
+ if (dispatch) {
859
+ dispatch(
860
+ state.tr.delete($pos.before(d), $pos.after(d)).scrollIntoView(),
861
+ );
862
+ }
863
+ return true;
864
+ }
865
+ }
866
+ return false;
867
+ }
868
+
869
+ /**
870
+ * Deletes the content of the selected cells, if they are not empty.
871
+ *
872
+ * @public
873
+ */
874
+ export function deleteCellSelection(
875
+ state: EditorState,
876
+ dispatch?: (tr: Transaction) => void,
877
+ ): boolean {
878
+ const sel = state.selection;
879
+ if (!(sel instanceof CellSelection)) return false;
880
+ if (dispatch) {
881
+ const tr = state.tr;
882
+ const baseContent = tableNodeTypes(state.schema).cell.createAndFill()!
883
+ .content;
884
+ sel.forEachCell((cell, pos) => {
885
+ if (!cell.content.eq(baseContent)) {
886
+ tr.replace(
887
+ tr.mapping.map(pos + 1),
888
+ tr.mapping.map(pos + cell.nodeSize - 1),
889
+ new Slice(baseContent, 0, 0),
890
+ );
891
+ }
892
+ });
893
+ if (tr.docChanged) dispatch(tr);
894
+ }
895
+ return true;
896
+ }