@milkdown/preset-gfm 6.1.4 → 6.3.0

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 (85) hide show
  1. package/lib/auto-link.d.ts.map +1 -1
  2. package/lib/footnote/definition.d.ts +1 -5
  3. package/lib/footnote/definition.d.ts.map +1 -1
  4. package/lib/footnote/reference.d.ts +1 -5
  5. package/lib/footnote/reference.d.ts.map +1 -1
  6. package/lib/index.d.ts +1 -3
  7. package/lib/index.d.ts.map +1 -1
  8. package/lib/index.es.js +1979 -891
  9. package/lib/index.es.js.map +1 -1
  10. package/lib/strike-through.d.ts +1 -5
  11. package/lib/strike-through.d.ts.map +1 -1
  12. package/lib/supported-keys.d.ts +1 -0
  13. package/lib/supported-keys.d.ts.map +1 -1
  14. package/lib/table/command.d.ts +1 -1
  15. package/lib/table/command.d.ts.map +1 -1
  16. package/lib/table/nodes/index.d.ts +2 -23
  17. package/lib/table/nodes/index.d.ts.map +1 -1
  18. package/lib/table/operator-plugin/actions.d.ts +1 -1
  19. package/lib/table/operator-plugin/actions.d.ts.map +1 -1
  20. package/lib/table/operator-plugin/calc-pos.d.ts.map +1 -1
  21. package/lib/table/operator-plugin/helper.d.ts +1 -1
  22. package/lib/table/operator-plugin/helper.d.ts.map +1 -1
  23. package/lib/table/operator-plugin/index.d.ts +1 -1
  24. package/lib/table/operator-plugin/index.d.ts.map +1 -1
  25. package/lib/table/operator-plugin/widget.d.ts +4 -4
  26. package/lib/table/operator-plugin/widget.d.ts.map +1 -1
  27. package/lib/table/plugin/auto-insert-zero-space.d.ts +3 -0
  28. package/lib/table/plugin/auto-insert-zero-space.d.ts.map +1 -0
  29. package/lib/table/plugin/cell-selection.d.ts +38 -0
  30. package/lib/table/plugin/cell-selection.d.ts.map +1 -0
  31. package/lib/table/plugin/column-resizing.d.ts +17 -0
  32. package/lib/table/plugin/column-resizing.d.ts.map +1 -0
  33. package/lib/table/plugin/commands.d.ts +30 -0
  34. package/lib/table/plugin/commands.d.ts.map +1 -0
  35. package/lib/table/plugin/copy-paste.d.ts +13 -0
  36. package/lib/table/plugin/copy-paste.d.ts.map +1 -0
  37. package/lib/table/plugin/fix-tables.d.ts +6 -0
  38. package/lib/table/plugin/fix-tables.d.ts.map +1 -0
  39. package/lib/table/plugin/index.d.ts +4 -0
  40. package/lib/table/plugin/index.d.ts.map +1 -0
  41. package/lib/table/plugin/schema.d.ts +4 -0
  42. package/lib/table/plugin/schema.d.ts.map +1 -0
  43. package/lib/table/plugin/table-editing.d.ts +9 -0
  44. package/lib/table/plugin/table-editing.d.ts.map +1 -0
  45. package/lib/table/plugin/table-map.d.ts +44 -0
  46. package/lib/table/plugin/table-map.d.ts.map +1 -0
  47. package/lib/table/plugin/table-view.d.ts +15 -0
  48. package/lib/table/plugin/table-view.d.ts.map +1 -0
  49. package/lib/table/plugin/types.d.ts +15 -0
  50. package/lib/table/plugin/types.d.ts.map +1 -0
  51. package/lib/table/plugin/util.d.ts +16 -0
  52. package/lib/table/plugin/util.d.ts.map +1 -0
  53. package/lib/table/utils.d.ts +6 -6
  54. package/lib/table/utils.d.ts.map +1 -1
  55. package/lib/task-list-item.d.ts +1 -5
  56. package/lib/task-list-item.d.ts.map +1 -1
  57. package/package.json +8 -7
  58. package/src/auto-link.ts +4 -3
  59. package/src/footnote/definition.ts +3 -1
  60. package/src/footnote/reference.ts +2 -1
  61. package/src/table/command.ts +3 -3
  62. package/src/table/nodes/index.ts +7 -3
  63. package/src/table/operator-plugin/actions.ts +4 -4
  64. package/src/table/operator-plugin/calc-pos.ts +4 -2
  65. package/src/table/operator-plugin/helper.ts +2 -1
  66. package/src/table/operator-plugin/index.ts +1 -1
  67. package/src/table/operator-plugin/widget.ts +4 -14
  68. package/src/table/plugin/auto-insert-zero-space.ts +51 -0
  69. package/src/table/plugin/cell-selection.ts +352 -0
  70. package/src/table/plugin/column-resizing.ts +260 -0
  71. package/src/table/plugin/commands.ts +551 -0
  72. package/src/table/plugin/copy-paste.ts +306 -0
  73. package/src/table/plugin/fix-tables.ts +117 -0
  74. package/src/table/plugin/index.ts +4 -0
  75. package/src/table/plugin/schema.ts +114 -0
  76. package/src/table/plugin/table-editing.ts +275 -0
  77. package/src/table/plugin/table-map.ts +280 -0
  78. package/src/table/plugin/table-view.ts +76 -0
  79. package/src/table/plugin/types.ts +16 -0
  80. package/src/table/plugin/util.ts +107 -0
  81. package/src/table/utils.ts +31 -22
  82. package/src/task-list-item.ts +4 -2
  83. package/lib/table/nodes/schema.d.ts +0 -2
  84. package/lib/table/nodes/schema.d.ts.map +0 -1
  85. package/src/table/nodes/schema.ts +0 -16
@@ -0,0 +1,551 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+
3
+ import { Fragment, Node, NodeSpec, NodeType, ResolvedPos } from '@milkdown/prose/model';
4
+ import { Command, EditorState, TextSelection, Transaction } from '@milkdown/prose/state';
5
+
6
+ import { CellSelection } from './cell-selection';
7
+ import { tableNodeTypes } from './schema';
8
+ import { Rect, TableMap } from './table-map';
9
+ import {
10
+ addColSpan,
11
+ cellAround,
12
+ cellWrapping,
13
+ columnIsHeader,
14
+ isInTable,
15
+ moveCellForward,
16
+ removeColSpan,
17
+ selectionCell,
18
+ setAttr,
19
+ } from './util';
20
+
21
+ // Helper to get the selected rectangle in a table, if any. Adds table
22
+ // map, table node, and table start offset to the object for
23
+ // convenience.
24
+ export function selectedRect(state: EditorState): Required<Rect> {
25
+ const sel = state.selection,
26
+ $pos = selectionCell(state) as ResolvedPos;
27
+ const table = $pos.node(-1),
28
+ tableStart = $pos.start(-1),
29
+ map = TableMap.get(table);
30
+ let rect;
31
+ if (sel instanceof CellSelection)
32
+ rect = map.rectBetween(sel.$anchorCell.pos - tableStart, sel.$headCell.pos - tableStart);
33
+ else rect = map.findCell($pos.pos - tableStart);
34
+ rect.tableStart = tableStart;
35
+ rect.map = map;
36
+ rect.table = table;
37
+ return rect as Required<Rect>;
38
+ }
39
+
40
+ // Add a column at the given position in a table.
41
+ export function addColumn(tr: Transaction, { map, tableStart, table }: Rect, col: number) {
42
+ map = map as TableMap;
43
+ table = table as Node;
44
+ tableStart = tableStart as number;
45
+ let refColumn: number | null = col > 0 ? -1 : 0;
46
+ if (columnIsHeader(map, table, col + refColumn)) refColumn = col == 0 || col == map.width ? null : 0;
47
+
48
+ for (let row = 0; row < map.height; row++) {
49
+ const index = row * map.width + col;
50
+ // If this position falls inside a col-spanning cell
51
+ if (col > 0 && col < map.width && map.map[index - 1] == map.map[index]) {
52
+ const pos = map.map[index] as number,
53
+ cell = table.nodeAt(pos) as Node;
54
+ tr.setNodeMarkup(tr.mapping.map(tableStart + pos), null, addColSpan(cell.attrs, col - map.colCount(pos)));
55
+ // Skip ahead if rowspan > 1
56
+ row += cell.attrs['rowspan'] - 1;
57
+ } else {
58
+ const offset = map.map[index + (refColumn as number)] as number;
59
+ const type =
60
+ refColumn == null ? tableNodeTypes(table.type.schema).cell : (table.nodeAt(offset) as Node).type;
61
+ const pos = map.positionAt(row, col, table);
62
+ tr.insert(tr.mapping.map(tableStart + pos), type.createAndFill());
63
+ }
64
+ }
65
+ return tr;
66
+ }
67
+
68
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
69
+ // Command to add a column before the column with the selection.
70
+ export const addColumnBefore: Command = (state, dispatch) => {
71
+ if (!isInTable(state)) return false;
72
+ if (dispatch) {
73
+ const rect = selectedRect(state);
74
+ dispatch(addColumn(state.tr, rect, rect.left));
75
+ }
76
+ return true;
77
+ };
78
+
79
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
80
+ // Command to add a column after the column with the selection.
81
+ export const addColumnAfter: Command = (state, dispatch) => {
82
+ if (!isInTable(state)) return false;
83
+ if (dispatch) {
84
+ const rect = selectedRect(state);
85
+ dispatch(addColumn(state.tr, rect, rect.right));
86
+ }
87
+ return true;
88
+ };
89
+
90
+ export function removeColumn(tr: Transaction, { map, table, tableStart }: Rect, col: number) {
91
+ map = map as TableMap;
92
+ table = table as Node;
93
+ tableStart = tableStart as number;
94
+ const mapStart = tr.mapping.maps.length;
95
+ for (let row = 0; row < map.height; ) {
96
+ const index = row * map.width + col,
97
+ pos = map.map[index] as number,
98
+ cell = table.nodeAt(pos) as Node;
99
+ // If this is part of a col-spanning cell
100
+ if ((col > 0 && map.map[index - 1] == pos) || (col < map.width - 1 && map.map[index + 1] == pos)) {
101
+ tr.setNodeMarkup(
102
+ tr.mapping.slice(mapStart).map(tableStart + pos),
103
+ null,
104
+ removeColSpan(cell.attrs, col - map.colCount(pos)),
105
+ );
106
+ } else {
107
+ const start = tr.mapping.slice(mapStart).map(tableStart + pos);
108
+ tr.delete(start, start + cell.nodeSize);
109
+ }
110
+ row += cell.attrs['rowspan'];
111
+ }
112
+ }
113
+
114
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
115
+ // Command function that removes the selected columns from a table.
116
+ export const deleteColumn: Command = (state, dispatch) => {
117
+ if (!isInTable(state)) return false;
118
+ if (dispatch) {
119
+ const rect = selectedRect(state) as Required<Rect>,
120
+ tr = state.tr;
121
+ if (rect.left == 0 && rect.right == rect.map.width) return false;
122
+ for (let i = rect.right - 1; ; i--) {
123
+ removeColumn(tr, rect, i);
124
+ if (i == rect.left) break;
125
+ rect.table = (rect.tableStart ? tr.doc.nodeAt(rect.tableStart - 1) : tr.doc) as Node;
126
+ rect.map = TableMap.get(rect.table);
127
+ }
128
+ dispatch(tr);
129
+ }
130
+ return true;
131
+ };
132
+
133
+ export function rowIsHeader(map: TableMap, table: Node, row: number) {
134
+ const headerCell = tableNodeTypes(table.type.schema).header_cell;
135
+ for (let col = 0; col < map.width; col++) {
136
+ const offset = map.map[col + row * map.width] as number;
137
+ if ((table.nodeAt(offset) as Node).type != headerCell) return false;
138
+ }
139
+ return true;
140
+ }
141
+
142
+ export function addRow(tr: Transaction, { map, tableStart, table }: Rect, row: number) {
143
+ map = map as TableMap;
144
+ table = table as Node;
145
+ tableStart = tableStart as number;
146
+ let rowPos = tableStart;
147
+ for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize;
148
+ const cells = [];
149
+ let refRow: number | null = row > 0 ? -1 : 0;
150
+ if (rowIsHeader(map, table, row + refRow)) refRow = row == 0 || row == map.height ? null : 0;
151
+ for (let col = 0, index = map.width * row; col < map.width; col++, index++) {
152
+ // Covered by a rowspan cell
153
+ if (row > 0 && row < map.height && map.map[index] == map.map[index - map.width]) {
154
+ const pos = map.map[index] as number,
155
+ attrs = (table.nodeAt(pos) as Node).attrs;
156
+ tr.setNodeMarkup(tableStart + pos, null, setAttr(attrs, 'rowspan', attrs['rowspan'] + 1));
157
+ col += attrs['colspan'] - 1;
158
+ } else {
159
+ const type =
160
+ refRow == null
161
+ ? tableNodeTypes(table.type.schema).cell
162
+ : (table.nodeAt(map.map[index + refRow * map.width] as number) as Node).type;
163
+ cells.push(type.createAndFill());
164
+ }
165
+ }
166
+ tr.insert(rowPos, tableNodeTypes(table.type.schema).row.create(null, cells));
167
+ return tr;
168
+ }
169
+
170
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
171
+ // Add a table row before the selection.
172
+ export function addRowBefore(state: EditorState, dispatch?: (tr: Transaction) => boolean) {
173
+ if (!isInTable(state)) return false;
174
+ if (dispatch) {
175
+ const rect = selectedRect(state);
176
+ dispatch(addRow(state.tr, rect, rect.top));
177
+ }
178
+ return true;
179
+ }
180
+
181
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
182
+ // Add a table row after the selection.
183
+ export function addRowAfter(state: EditorState, dispatch?: (tr: Transaction) => boolean) {
184
+ if (!isInTable(state)) return false;
185
+ if (dispatch) {
186
+ const rect = selectedRect(state);
187
+ dispatch(addRow(state.tr, rect, rect.bottom));
188
+ }
189
+ return true;
190
+ }
191
+
192
+ export function removeRow(tr: Transaction, { map, tableStart, table }: Rect, row: number) {
193
+ map = map as TableMap;
194
+ table = table as Node;
195
+ tableStart = tableStart as number;
196
+ let rowPos = 0;
197
+ for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize;
198
+ const nextRow = rowPos + table.child(row).nodeSize;
199
+
200
+ const mapFrom = tr.mapping.maps.length;
201
+ tr.delete(rowPos + tableStart, nextRow + tableStart);
202
+
203
+ for (let col = 0, index = row * map.width; col < map.width; col++, index++) {
204
+ const pos = map.map[index] as number;
205
+ if (row > 0 && pos == map.map[index - map.width]) {
206
+ // If this cell starts in the row above, simply reduce its rowspan
207
+ const attrs = (table.nodeAt(pos) as Node).attrs;
208
+ tr.setNodeMarkup(
209
+ tr.mapping.slice(mapFrom).map(pos + tableStart),
210
+ null,
211
+ setAttr(attrs, 'rowspan', attrs['rowspan'] - 1),
212
+ );
213
+ col += attrs['colspan'] - 1;
214
+ } else if (row < map.width && pos == map.map[index + map.width]) {
215
+ // Else, if it continues in the row below, it has to be moved down
216
+ const cell = table.nodeAt(pos) as Node;
217
+ const copy = cell.type.create(setAttr(cell.attrs, 'rowspan', cell.attrs['rowspan'] - 1), cell.content);
218
+ const newPos = map.positionAt(row + 1, col, table);
219
+ tr.insert(tr.mapping.slice(mapFrom).map(tableStart + newPos), copy);
220
+ col += cell.attrs['colspan'] - 1;
221
+ }
222
+ }
223
+ }
224
+
225
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
226
+ // Remove the selected rows from a table.
227
+ export const deleteRow: Command = (state, dispatch) => {
228
+ if (!isInTable(state)) return false;
229
+ if (dispatch) {
230
+ const rect = selectedRect(state) as Required<Rect>,
231
+ tr = state.tr;
232
+ if (rect.top == 0 && rect.bottom == rect.map.height) return false;
233
+ for (let i = rect.bottom - 1; ; i--) {
234
+ removeRow(tr, rect, i);
235
+ if (i == rect.top) break;
236
+ rect.table = rect.tableStart ? (tr.doc.nodeAt(rect.tableStart - 1) as Node) : tr.doc;
237
+ rect.map = TableMap.get(rect.table);
238
+ }
239
+ dispatch(tr);
240
+ }
241
+ return true;
242
+ };
243
+
244
+ function isEmpty(cell: Node) {
245
+ const c = cell.content;
246
+ return c.childCount == 1 && (c.firstChild as Node).isTextblock && (c.firstChild as Node).childCount == 0;
247
+ }
248
+
249
+ function cellsOverlapRectangle({ width, height, map }: TableMap, rect: Rect) {
250
+ let indexTop = rect.top * width + rect.left,
251
+ indexLeft = indexTop;
252
+ let indexBottom = (rect.bottom - 1) * width + rect.left,
253
+ indexRight = indexTop + (rect.right - rect.left - 1);
254
+ for (let i = rect.top; i < rect.bottom; i++) {
255
+ if (
256
+ (rect.left > 0 && map[indexLeft] == map[indexLeft - 1]) ||
257
+ (rect.right < width && map[indexRight] == map[indexRight + 1])
258
+ )
259
+ return true;
260
+ indexLeft += width;
261
+ indexRight += width;
262
+ }
263
+ for (let i = rect.left; i < rect.right; i++) {
264
+ if (
265
+ (rect.top > 0 && map[indexTop] == map[indexTop - width]) ||
266
+ (rect.bottom < height && map[indexBottom] == map[indexBottom + width])
267
+ )
268
+ return true;
269
+ indexTop++;
270
+ indexBottom++;
271
+ }
272
+ return false;
273
+ }
274
+
275
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
276
+ // Merge the selected cells into a single cell. Only available when
277
+ // the selected cells' outline forms a rectangle.
278
+ export function mergeCells(state: EditorState, dispatch?: (tr: Transaction) => boolean) {
279
+ const sel = state.selection;
280
+ if (!(sel instanceof CellSelection) || sel.$anchorCell.pos == sel.$headCell.pos) return false;
281
+ const rect = selectedRect(state) as Required<Rect>,
282
+ { map } = rect;
283
+ if (cellsOverlapRectangle(map as TableMap, rect)) return false;
284
+ if (dispatch) {
285
+ const tr = state.tr,
286
+ seen: Record<number, boolean> = {};
287
+ let content = Fragment.empty,
288
+ mergedPos,
289
+ mergedCell;
290
+ for (let row = rect.top; row < rect.bottom; row++) {
291
+ for (let col = rect.left; col < rect.right; col++) {
292
+ const cellPos = map.map[row * map.width + col] as number,
293
+ cell = rect.table.nodeAt(cellPos) as Node;
294
+ if (seen[cellPos]) continue;
295
+ seen[cellPos] = true;
296
+ if (mergedPos == null) {
297
+ mergedPos = cellPos;
298
+ mergedCell = cell;
299
+ } else {
300
+ if (!isEmpty(cell)) content = content.append(cell.content);
301
+ const mapped = tr.mapping.map(cellPos + rect.tableStart);
302
+ tr.delete(mapped, mapped + cell.nodeSize);
303
+ }
304
+ }
305
+ }
306
+ mergedCell = mergedCell as Node;
307
+ mergedPos = mergedPos as number;
308
+ tr.setNodeMarkup(
309
+ mergedPos + rect.tableStart,
310
+ null,
311
+ setAttr(
312
+ addColSpan(
313
+ mergedCell.attrs,
314
+ mergedCell.attrs['colspan'],
315
+ rect.right - rect.left - mergedCell.attrs['colspan'],
316
+ ),
317
+ 'rowspan',
318
+ rect.bottom - rect.top,
319
+ ),
320
+ );
321
+ if (content.size) {
322
+ const end = mergedPos + 1 + mergedCell.content.size;
323
+ const start = isEmpty(mergedCell) ? mergedPos + 1 : end;
324
+ tr.replaceWith(start + rect.tableStart, end + rect.tableStart, content);
325
+ }
326
+ tr.setSelection(new CellSelection(tr.doc.resolve(mergedPos + rect.tableStart)));
327
+ dispatch(tr);
328
+ }
329
+ return true;
330
+ }
331
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
332
+ // Split a selected cell, whose rowpan or colspan is greater than one,
333
+ // into smaller cells. Use the first cell type for the new cells.
334
+ export function splitCell(state: EditorState, dispatch?: (tr: Transaction) => boolean) {
335
+ const nodeTypes = tableNodeTypes(state.schema);
336
+ return splitCellWithType(({ node }) => {
337
+ return nodeTypes[node.type.spec['tableRole']];
338
+ })(state, dispatch);
339
+ }
340
+
341
+ // :: (getCellType: ({ row: number, col: number, node: Node}) → NodeType) → (EditorState, dispatch: ?(tr: Transaction)) → bool
342
+ // Split a selected cell, whose rowpan or colspan is greater than one,
343
+ // into smaller cells with the cell type (th, td) returned by getType function.
344
+ export function splitCellWithType(getCellType: (pos: { row: number; col: number; node: Node }) => NodeType) {
345
+ return (state: EditorState, dispatch?: (tr: Transaction) => boolean) => {
346
+ const sel = state.selection;
347
+ let cellNode, cellPos;
348
+ if (!(sel instanceof CellSelection)) {
349
+ cellNode = cellWrapping(sel.$from);
350
+ if (!cellNode) return false;
351
+ cellPos = (cellAround(sel.$from) as ResolvedPos).pos;
352
+ } else {
353
+ if (sel.$anchorCell.pos != sel.$headCell.pos) return false;
354
+ cellNode = sel.$anchorCell.nodeAfter as Node;
355
+ cellPos = sel.$anchorCell.pos;
356
+ }
357
+ if (cellNode.attrs['colspan'] == 1 && cellNode.attrs['rowspan'] == 1) {
358
+ return false;
359
+ }
360
+ if (dispatch) {
361
+ let baseAttrs = cellNode.attrs;
362
+ const attrs = [],
363
+ colwidth = baseAttrs['colwidth'];
364
+ if (baseAttrs['rowspan'] > 1) baseAttrs = setAttr(baseAttrs, 'rowspan', 1);
365
+ if (baseAttrs['colspan'] > 1) baseAttrs = setAttr(baseAttrs, 'colspan', 1);
366
+ const rect = selectedRect(state) as Required<Rect>,
367
+ tr = state.tr;
368
+ for (let i = 0; i < rect.right - rect.left; i++)
369
+ attrs.push(
370
+ colwidth
371
+ ? setAttr(baseAttrs, 'colwidth', colwidth && colwidth[i] ? [colwidth[i]] : null)
372
+ : baseAttrs,
373
+ );
374
+ let lastCell;
375
+ for (let row = rect.top; row < rect.bottom; row++) {
376
+ let pos = rect.map.positionAt(row, rect.left, rect.table);
377
+ if (row == rect.top) pos += cellNode.nodeSize;
378
+ for (let col = rect.left, i = 0; col < rect.right; col++, i++) {
379
+ if (col == rect.left && row == rect.top) continue;
380
+ tr.insert(
381
+ (lastCell = tr.mapping.map(pos + rect.tableStart, 1)),
382
+ getCellType({ node: cellNode, row, col }).createAndFill(attrs[i]) as Node,
383
+ );
384
+ }
385
+ }
386
+ tr.setNodeMarkup(cellPos, getCellType({ node: cellNode, row: rect.top, col: rect.left }), attrs[0]);
387
+ if (sel instanceof CellSelection) {
388
+ let pos: ResolvedPos | undefined = undefined;
389
+ if (lastCell) {
390
+ pos = tr.doc.resolve(lastCell);
391
+ }
392
+ tr.setSelection(new CellSelection(tr.doc.resolve(sel.$anchorCell.pos), pos));
393
+ }
394
+ dispatch(tr);
395
+ }
396
+ return true;
397
+ };
398
+ }
399
+
400
+ // :: (string, any) → (EditorState, dispatch: ?(tr: Transaction)) → bool
401
+ // Returns a command that sets the given attribute to the given value,
402
+ // and is only available when the currently selected cell doesn't
403
+ // already have that attribute set to that value.
404
+ export function setCellAttr<T>(name: string, value: T): Command {
405
+ return (state, dispatch) => {
406
+ if (!isInTable(state)) return false;
407
+ const $cell = selectionCell(state) as ResolvedPos;
408
+ if (($cell.nodeAfter as Node).attrs[name] === value) return false;
409
+ if (dispatch) {
410
+ const tr = state.tr;
411
+ if (state.selection instanceof CellSelection)
412
+ state.selection.forEachCell((node, pos) => {
413
+ if (node.attrs[name] !== value) tr.setNodeMarkup(pos, null, setAttr(node.attrs, name, value));
414
+ });
415
+ else tr.setNodeMarkup($cell.pos, null, setAttr(($cell.nodeAfter as Node).attrs, name, value));
416
+ dispatch(tr);
417
+ }
418
+ return true;
419
+ };
420
+ }
421
+
422
+ function isHeaderEnabledByType(type: string, rect: Required<Rect>, types: Record<string, NodeSpec>) {
423
+ // Get cell positions for first row or first column
424
+ const cellPositions = rect.map.cellsInRect({
425
+ left: 0,
426
+ top: 0,
427
+ right: type == 'row' ? rect.map.width : 1,
428
+ bottom: type == 'column' ? rect.map.height : 1,
429
+ });
430
+
431
+ for (let i = 0; i < cellPositions.length; i++) {
432
+ const cell = rect.table.nodeAt(cellPositions[i] as number);
433
+ if (cell && cell.type !== types['header_cell']) {
434
+ return false;
435
+ }
436
+ }
437
+
438
+ return true;
439
+ }
440
+
441
+ // Toggles between row/column header and normal cells (Only applies to first row/column).
442
+ // For deprecated behavior pass `useDeprecatedLogic` in options with true.
443
+ export function toggleHeader(type: string) {
444
+ return function (state: EditorState, dispatch?: (tr: Transaction) => boolean) {
445
+ if (!isInTable(state)) return false;
446
+ if (dispatch) {
447
+ const types = tableNodeTypes(state.schema);
448
+ const rect = selectedRect(state),
449
+ tr = state.tr;
450
+
451
+ const isHeaderRowEnabled = isHeaderEnabledByType('row', rect, types);
452
+ const isHeaderColumnEnabled = isHeaderEnabledByType('column', rect, types);
453
+
454
+ const isHeaderEnabled =
455
+ type === 'column' ? isHeaderRowEnabled : type === 'row' ? isHeaderColumnEnabled : false;
456
+
457
+ const selectionStartsAt = isHeaderEnabled ? 1 : 0;
458
+
459
+ const cellsRect =
460
+ type == 'column'
461
+ ? new Rect(0, selectionStartsAt, 1, rect.map.height)
462
+ : type == 'row'
463
+ ? new Rect(selectionStartsAt, 0, rect.map.width, 1)
464
+ : rect;
465
+
466
+ const newType =
467
+ type == 'column'
468
+ ? isHeaderColumnEnabled
469
+ ? types.cell
470
+ : types.header_cell
471
+ : type == 'row'
472
+ ? isHeaderRowEnabled
473
+ ? types.cell
474
+ : types.header_cell
475
+ : types.cell;
476
+
477
+ rect.map.cellsInRect(cellsRect).forEach((relativeCellPos) => {
478
+ const cellPos = relativeCellPos + rect.tableStart;
479
+ const cell = tr.doc.nodeAt(cellPos);
480
+
481
+ if (cell) {
482
+ tr.setNodeMarkup(cellPos, newType, cell.attrs);
483
+ }
484
+ });
485
+
486
+ dispatch(tr);
487
+ }
488
+ return true;
489
+ };
490
+ }
491
+
492
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
493
+ // Toggles whether the selected row contains header cells.
494
+ export const toggleHeaderRow = toggleHeader('row');
495
+
496
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
497
+ // Toggles whether the selected column contains header cells.
498
+ export const toggleHeaderColumn = toggleHeader('column');
499
+
500
+ // :: (EditorState, dispatch: ?(tr: Transaction)) → bool
501
+ // Toggles whether the selected cells are header cells.
502
+ export const toggleHeaderCell = toggleHeader('cell');
503
+
504
+ function findNextCell($cell: ResolvedPos, dir: number) {
505
+ if (dir < 0) {
506
+ const before = $cell.nodeBefore;
507
+ if (before) return $cell.pos - before.nodeSize;
508
+ for (let row = $cell.index(-1) - 1, rowEnd = $cell.before(); row >= 0; row--) {
509
+ const rowNode = $cell.node(-1).child(row);
510
+ if (rowNode.childCount) return rowEnd - 1 - (rowNode.lastChild as Node).nodeSize;
511
+ rowEnd -= rowNode.nodeSize;
512
+ }
513
+ } else {
514
+ if ($cell.index() < $cell.parent.childCount - 1) return $cell.pos + ($cell.nodeAfter as Node).nodeSize;
515
+ const table = $cell.node(-1);
516
+ for (let row = $cell.indexAfter(-1), rowStart = $cell.after(); row < table.childCount; row++) {
517
+ const rowNode = table.child(row);
518
+ if (rowNode.childCount) return rowStart + 1;
519
+ rowStart += rowNode.nodeSize;
520
+ }
521
+ }
522
+ return;
523
+ }
524
+
525
+ // Returns a command for selecting the next (direction=1) or previous
526
+ // (direction=-1) cell in a table.
527
+ export function goToNextCell(direction: number): Command {
528
+ return (state, dispatch) => {
529
+ if (!isInTable(state)) return false;
530
+ const cell = findNextCell(selectionCell(state) as ResolvedPos, direction);
531
+ if (cell == null) return false;
532
+ if (dispatch) {
533
+ const $cell = state.doc.resolve(cell);
534
+ dispatch(state.tr.setSelection(TextSelection.between($cell, moveCellForward($cell))).scrollIntoView());
535
+ }
536
+ return true;
537
+ };
538
+ }
539
+
540
+ // Deletes the table around the selection, if any.
541
+ export const deleteTable: Command = (state, dispatch) => {
542
+ const $pos = state.selection.$anchor;
543
+ for (let d = $pos.depth; d > 0; d--) {
544
+ const node = $pos.node(d);
545
+ if (node.type.spec['tableRole'] == 'table') {
546
+ if (dispatch) dispatch(state.tr.delete($pos.before(d), $pos.after(d)).scrollIntoView());
547
+ return true;
548
+ }
549
+ }
550
+ return false;
551
+ };