@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,477 @@
1
+ // This file defines a ProseMirror selection subclass that models
2
+ // table cell selections. The table plugin needs to be active to wire
3
+ // in the user interaction part of table selections (so that you
4
+ // actually get such selections when you select across cells).
5
+
6
+ import { Fragment, Node, ResolvedPos, Slice } from 'prosemirror-model';
7
+ import {
8
+ EditorState,
9
+ NodeSelection,
10
+ Selection,
11
+ SelectionRange,
12
+ TextSelection,
13
+ Transaction,
14
+ } from 'prosemirror-state';
15
+ import { Decoration, DecorationSet, DecorationSource } from 'prosemirror-view';
16
+
17
+ import { Mappable } from 'prosemirror-transform';
18
+ import { TableMap } from './TableMap.js';
19
+ import { CellAttrs, inSameTable, pointsAtCell, removeColSpan } from './util.js';
20
+
21
+ /**
22
+ * @public
23
+ */
24
+ export interface CellSelectionJSON {
25
+ type: string;
26
+ anchor: number;
27
+ head: number;
28
+ }
29
+
30
+ /**
31
+ * A [`Selection`](http://prosemirror.net/docs/ref/#state.Selection)
32
+ * subclass that represents a cell selection spanning part of a table.
33
+ * With the plugin enabled, these will be created when the user
34
+ * selects across cells, and will be drawn by giving selected cells a
35
+ * `selectedCell` CSS class.
36
+ *
37
+ * @public
38
+ */
39
+ export class CellSelection extends Selection {
40
+ // A resolved position pointing _in front of_ the anchor cell (the one
41
+ // that doesn't move when extending the selection).
42
+ public $anchorCell: ResolvedPos;
43
+
44
+ // A resolved position pointing in front of the head cell (the one
45
+ // moves when extending the selection).
46
+ public $headCell: ResolvedPos;
47
+
48
+ // A table selection is identified by its anchor and head cells. The
49
+ // positions given to this constructor should point _before_ two
50
+ // cells in the same table. They may be the same, to select a single
51
+ // cell.
52
+ constructor($anchorCell: ResolvedPos, $headCell: ResolvedPos = $anchorCell) {
53
+ const table = $anchorCell.node(-1);
54
+ const map = TableMap.get(table);
55
+ const tableStart = $anchorCell.start(-1);
56
+ const rect = map.rectBetween(
57
+ $anchorCell.pos - tableStart,
58
+ $headCell.pos - tableStart,
59
+ );
60
+
61
+ const doc = $anchorCell.node(0);
62
+ const cells = map
63
+ .cellsInRect(rect)
64
+ .filter((p) => p != $headCell.pos - tableStart);
65
+ // Make the head cell the first range, so that it counts as the
66
+ // primary part of the selection
67
+ cells.unshift($headCell.pos - tableStart);
68
+ const ranges = cells.map((pos) => {
69
+ const cell = table.nodeAt(pos);
70
+ if (!cell) {
71
+ throw RangeError(`No cell with offset ${pos} found`);
72
+ }
73
+ const from = tableStart + pos + 1;
74
+ return new SelectionRange(
75
+ doc.resolve(from),
76
+ doc.resolve(from + cell.content.size),
77
+ );
78
+ });
79
+ super(ranges[0].$from, ranges[0].$to, ranges);
80
+ this.$anchorCell = $anchorCell;
81
+ this.$headCell = $headCell;
82
+ }
83
+
84
+ public map(doc: Node, mapping: Mappable): CellSelection | Selection {
85
+ const $anchorCell = doc.resolve(mapping.map(this.$anchorCell.pos));
86
+ const $headCell = doc.resolve(mapping.map(this.$headCell.pos));
87
+ if (
88
+ pointsAtCell($anchorCell) &&
89
+ pointsAtCell($headCell) &&
90
+ inSameTable($anchorCell, $headCell)
91
+ ) {
92
+ const tableChanged = this.$anchorCell.node(-1) != $anchorCell.node(-1);
93
+ if (tableChanged && this.isRowSelection()) {
94
+ return CellSelection.rowSelection($anchorCell, $headCell);
95
+ } else if (tableChanged && this.isColSelection()) {
96
+ return CellSelection.colSelection($anchorCell, $headCell);
97
+ } else return new CellSelection($anchorCell, $headCell);
98
+ }
99
+ return TextSelection.between($anchorCell, $headCell);
100
+ }
101
+
102
+ // Returns a rectangular slice of table rows containing the selected
103
+ // cells.
104
+ public override content(): Slice {
105
+ const table = this.$anchorCell.node(-1);
106
+ const map = TableMap.get(table);
107
+ const tableStart = this.$anchorCell.start(-1);
108
+
109
+ const rect = map.rectBetween(
110
+ this.$anchorCell.pos - tableStart,
111
+ this.$headCell.pos - tableStart,
112
+ );
113
+ const seen: Record<number, boolean> = {};
114
+ const rows = [];
115
+ for (let row = rect.top; row < rect.bottom; row++) {
116
+ const rowContent = [];
117
+ for (
118
+ let index = row * map.width + rect.left, col = rect.left;
119
+ col < rect.right;
120
+ col++, index++
121
+ ) {
122
+ const pos = map.map[index];
123
+ if (seen[pos]) continue;
124
+ seen[pos] = true;
125
+
126
+ const cellRect = map.findCell(pos);
127
+ let cell = table.nodeAt(pos);
128
+ if (!cell) {
129
+ throw RangeError(`No cell with offset ${pos} found`);
130
+ }
131
+
132
+ const extraLeft = rect.left - cellRect.left;
133
+ const extraRight = cellRect.right - rect.right;
134
+
135
+ if (extraLeft > 0 || extraRight > 0) {
136
+ let attrs = cell.attrs as CellAttrs;
137
+ if (extraLeft > 0) {
138
+ attrs = removeColSpan(attrs, 0, extraLeft);
139
+ }
140
+ if (extraRight > 0) {
141
+ attrs = removeColSpan(
142
+ attrs,
143
+ attrs.colspan - extraRight,
144
+ extraRight,
145
+ );
146
+ }
147
+ if (cellRect.left < rect.left) {
148
+ cell = cell.type.createAndFill(attrs);
149
+ if (!cell) {
150
+ throw RangeError(
151
+ `Could not create cell with attrs ${JSON.stringify(attrs)}`,
152
+ );
153
+ }
154
+ } else {
155
+ cell = cell.type.create(attrs, cell.content);
156
+ }
157
+ }
158
+ if (cellRect.top < rect.top || cellRect.bottom > rect.bottom) {
159
+ const attrs = {
160
+ ...cell.attrs,
161
+ rowspan: Math.min(cellRect.bottom, rect.bottom) -
162
+ Math.max(cellRect.top, rect.top),
163
+ };
164
+ if (cellRect.top < rect.top) {
165
+ cell = cell.type.createAndFill(attrs)!;
166
+ } else {
167
+ cell = cell.type.create(attrs, cell.content);
168
+ }
169
+ }
170
+ rowContent.push(cell);
171
+ }
172
+ rows.push(table.child(row).copy(Fragment.from(rowContent)));
173
+ }
174
+
175
+ const fragment = this.isColSelection() && this.isRowSelection()
176
+ ? table
177
+ : rows;
178
+ return new Slice(Fragment.from(fragment), 1, 1);
179
+ }
180
+
181
+ public override replace(tr: Transaction, content: Slice = Slice.empty): void {
182
+ const mapFrom = tr.steps.length,
183
+ ranges = this.ranges;
184
+ for (let i = 0; i < ranges.length; i++) {
185
+ const { $from, $to } = ranges[i],
186
+ mapping = tr.mapping.slice(mapFrom);
187
+ tr.replace(
188
+ mapping.map($from.pos),
189
+ mapping.map($to.pos),
190
+ i ? Slice.empty : content,
191
+ );
192
+ }
193
+ const sel = Selection.findFrom(
194
+ tr.doc.resolve(tr.mapping.slice(mapFrom).map(this.to)),
195
+ -1,
196
+ );
197
+ if (sel) tr.setSelection(sel);
198
+ }
199
+
200
+ public override replaceWith(tr: Transaction, node: Node): void {
201
+ this.replace(tr, new Slice(Fragment.from(node), 0, 0));
202
+ }
203
+
204
+ public forEachCell(f: (node: Node, pos: number) => void): void {
205
+ const table = this.$anchorCell.node(-1);
206
+ const map = TableMap.get(table);
207
+ const tableStart = this.$anchorCell.start(-1);
208
+
209
+ const cells = map.cellsInRect(
210
+ map.rectBetween(
211
+ this.$anchorCell.pos - tableStart,
212
+ this.$headCell.pos - tableStart,
213
+ ),
214
+ );
215
+ for (let i = 0; i < cells.length; i++) {
216
+ f(table.nodeAt(cells[i])!, tableStart + cells[i]);
217
+ }
218
+ }
219
+
220
+ // True if this selection goes all the way from the top to the
221
+ // bottom of the table.
222
+ public isColSelection(): boolean {
223
+ const anchorTop = this.$anchorCell.index(-1);
224
+ const headTop = this.$headCell.index(-1);
225
+ if (Math.min(anchorTop, headTop) > 0) return false;
226
+
227
+ const anchorBottom = anchorTop + this.$anchorCell.nodeAfter!.attrs.rowspan;
228
+ const headBottom = headTop + this.$headCell.nodeAfter!.attrs.rowspan;
229
+
230
+ return (
231
+ Math.max(anchorBottom, headBottom) == this.$headCell.node(-1).childCount
232
+ );
233
+ }
234
+
235
+ // Returns the smallest column selection that covers the given anchor
236
+ // and head cell.
237
+ public static colSelection(
238
+ $anchorCell: ResolvedPos,
239
+ $headCell: ResolvedPos = $anchorCell,
240
+ ): CellSelection {
241
+ const table = $anchorCell.node(-1);
242
+ const map = TableMap.get(table);
243
+ const tableStart = $anchorCell.start(-1);
244
+
245
+ const anchorRect = map.findCell($anchorCell.pos - tableStart);
246
+ const headRect = map.findCell($headCell.pos - tableStart);
247
+ const doc = $anchorCell.node(0);
248
+
249
+ if (anchorRect.top <= headRect.top) {
250
+ if (anchorRect.top > 0) {
251
+ $anchorCell = doc.resolve(tableStart + map.map[anchorRect.left]);
252
+ }
253
+ if (headRect.bottom < map.height) {
254
+ $headCell = doc.resolve(
255
+ tableStart +
256
+ map.map[map.width * (map.height - 1) + headRect.right - 1],
257
+ );
258
+ }
259
+ } else {
260
+ if (headRect.top > 0) {
261
+ $headCell = doc.resolve(tableStart + map.map[headRect.left]);
262
+ }
263
+ if (anchorRect.bottom < map.height) {
264
+ $anchorCell = doc.resolve(
265
+ tableStart +
266
+ map.map[map.width * (map.height - 1) + anchorRect.right - 1],
267
+ );
268
+ }
269
+ }
270
+ return new CellSelection($anchorCell, $headCell);
271
+ }
272
+
273
+ // True if this selection goes all the way from the left to the
274
+ // right of the table.
275
+ public isRowSelection(): boolean {
276
+ const table = this.$anchorCell.node(-1);
277
+ const map = TableMap.get(table);
278
+ const tableStart = this.$anchorCell.start(-1);
279
+
280
+ const anchorLeft = map.colCount(this.$anchorCell.pos - tableStart);
281
+ const headLeft = map.colCount(this.$headCell.pos - tableStart);
282
+ if (Math.min(anchorLeft, headLeft) > 0) return false;
283
+
284
+ const anchorRight = anchorLeft + this.$anchorCell.nodeAfter!.attrs.colspan;
285
+ const headRight = headLeft + this.$headCell.nodeAfter!.attrs.colspan;
286
+ return Math.max(anchorRight, headRight) == map.width;
287
+ }
288
+
289
+ public eq(other: unknown): boolean {
290
+ return (
291
+ other instanceof CellSelection &&
292
+ other.$anchorCell.pos == this.$anchorCell.pos &&
293
+ other.$headCell.pos == this.$headCell.pos
294
+ );
295
+ }
296
+
297
+ // Returns the smallest row selection that covers the given anchor
298
+ // and head cell.
299
+ public static rowSelection(
300
+ $anchorCell: ResolvedPos,
301
+ $headCell: ResolvedPos = $anchorCell,
302
+ ): CellSelection {
303
+ const table = $anchorCell.node(-1);
304
+ const map = TableMap.get(table);
305
+ const tableStart = $anchorCell.start(-1);
306
+
307
+ const anchorRect = map.findCell($anchorCell.pos - tableStart);
308
+ const headRect = map.findCell($headCell.pos - tableStart);
309
+ const doc = $anchorCell.node(0);
310
+
311
+ if (anchorRect.left <= headRect.left) {
312
+ if (anchorRect.left > 0) {
313
+ $anchorCell = doc.resolve(
314
+ tableStart + map.map[anchorRect.top * map.width],
315
+ );
316
+ }
317
+ if (headRect.right < map.width) {
318
+ $headCell = doc.resolve(
319
+ tableStart + map.map[map.width * (headRect.top + 1) - 1],
320
+ );
321
+ }
322
+ } else {
323
+ if (headRect.left > 0) {
324
+ $headCell = doc.resolve(tableStart + map.map[headRect.top * map.width]);
325
+ }
326
+ if (anchorRect.right < map.width) {
327
+ $anchorCell = doc.resolve(
328
+ tableStart + map.map[map.width * (anchorRect.top + 1) - 1],
329
+ );
330
+ }
331
+ }
332
+ return new CellSelection($anchorCell, $headCell);
333
+ }
334
+
335
+ public toJSON(): CellSelectionJSON {
336
+ return {
337
+ type: 'cell',
338
+ anchor: this.$anchorCell.pos,
339
+ head: this.$headCell.pos,
340
+ };
341
+ }
342
+
343
+ static override fromJSON(doc: Node, json: CellSelectionJSON): CellSelection {
344
+ return new CellSelection(doc.resolve(json.anchor), doc.resolve(json.head));
345
+ }
346
+
347
+ static create(
348
+ doc: Node,
349
+ anchorCell: number,
350
+ headCell: number = anchorCell,
351
+ ): CellSelection {
352
+ return new CellSelection(doc.resolve(anchorCell), doc.resolve(headCell));
353
+ }
354
+
355
+ override getBookmark(): CellBookmark {
356
+ return new CellBookmark(this.$anchorCell.pos, this.$headCell.pos);
357
+ }
358
+ }
359
+
360
+ CellSelection.prototype.visible = false;
361
+
362
+ Selection.jsonID('cell', CellSelection);
363
+
364
+ /**
365
+ * @public
366
+ */
367
+ export class CellBookmark {
368
+ constructor(
369
+ public anchor: number,
370
+ public head: number,
371
+ ) {}
372
+
373
+ map(mapping: Mappable): CellBookmark {
374
+ return new CellBookmark(mapping.map(this.anchor), mapping.map(this.head));
375
+ }
376
+
377
+ resolve(doc: Node): CellSelection | Selection {
378
+ const $anchorCell = doc.resolve(this.anchor),
379
+ $headCell = doc.resolve(this.head);
380
+ if (
381
+ $anchorCell.parent.type.spec.tableRole == 'row' &&
382
+ $headCell.parent.type.spec.tableRole == 'row' &&
383
+ $anchorCell.index() < $anchorCell.parent.childCount &&
384
+ $headCell.index() < $headCell.parent.childCount &&
385
+ inSameTable($anchorCell, $headCell)
386
+ ) {
387
+ return new CellSelection($anchorCell, $headCell);
388
+ } else return Selection.near($headCell, 1);
389
+ }
390
+ }
391
+
392
+ export function drawCellSelection(state: EditorState): DecorationSource | null {
393
+ if (!(state.selection instanceof CellSelection)) return null;
394
+ const cells: Decoration[] = [];
395
+ state.selection.forEachCell((node, pos) => {
396
+ cells.push(
397
+ Decoration.node(pos, pos + node.nodeSize, { class: 'selectedCell' }),
398
+ );
399
+ });
400
+ return DecorationSet.create(state.doc, cells);
401
+ }
402
+
403
+ function isCellBoundarySelection({ $from, $to }: TextSelection) {
404
+ if ($from.pos == $to.pos || $from.pos < $to.pos - 6) return false; // Cheap elimination
405
+ let afterFrom = $from.pos;
406
+ let beforeTo = $to.pos;
407
+ let depth = $from.depth;
408
+ for (; depth >= 0; depth--, afterFrom++) {
409
+ if ($from.after(depth + 1) < $from.end(depth)) break;
410
+ }
411
+ for (let d = $to.depth; d >= 0; d--, beforeTo--) {
412
+ if ($to.before(d + 1) > $to.start(d)) break;
413
+ }
414
+ return (
415
+ afterFrom == beforeTo &&
416
+ /row|table/.test($from.node(depth).type.spec.tableRole)
417
+ );
418
+ }
419
+
420
+ function isTextSelectionAcrossCells({ $from, $to }: TextSelection) {
421
+ let fromCellBoundaryNode: Node | undefined;
422
+ let toCellBoundaryNode: Node | undefined;
423
+
424
+ for (let i = $from.depth; i > 0; i--) {
425
+ const node = $from.node(i);
426
+ if (
427
+ node.type.spec.tableRole === 'cell' ||
428
+ node.type.spec.tableRole === 'header_cell'
429
+ ) {
430
+ fromCellBoundaryNode = node;
431
+ break;
432
+ }
433
+ }
434
+
435
+ for (let i = $to.depth; i > 0; i--) {
436
+ const node = $to.node(i);
437
+ if (
438
+ node.type.spec.tableRole === 'cell' ||
439
+ node.type.spec.tableRole === 'header_cell'
440
+ ) {
441
+ toCellBoundaryNode = node;
442
+ break;
443
+ }
444
+ }
445
+
446
+ return fromCellBoundaryNode !== toCellBoundaryNode && $to.parentOffset === 0;
447
+ }
448
+
449
+ export function normalizeSelection(
450
+ state: EditorState,
451
+ tr: Transaction | undefined,
452
+ allowTableNodeSelection: boolean,
453
+ ): Transaction | undefined {
454
+ const sel = (tr || state).selection;
455
+ const doc = (tr || state).doc;
456
+ let normalize: Selection | undefined;
457
+ let role: string | undefined;
458
+ if (sel instanceof NodeSelection && (role = sel.node.type.spec.tableRole)) {
459
+ if (role == 'cell' || role == 'header_cell') {
460
+ normalize = CellSelection.create(doc, sel.from);
461
+ } else if (role == 'row') {
462
+ const $cell = doc.resolve(sel.from + 1);
463
+ normalize = CellSelection.rowSelection($cell, $cell);
464
+ } else if (!allowTableNodeSelection) {
465
+ const map = TableMap.get(sel.node);
466
+ const start = sel.from + 1;
467
+ const lastCell = start + map.map[map.width * map.height - 1];
468
+ normalize = CellSelection.create(doc, start + 1, lastCell);
469
+ }
470
+ } else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) {
471
+ normalize = TextSelection.create(doc, sel.from);
472
+ } else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel)) {
473
+ normalize = TextSelection.create(doc, sel.$from.start(), sel.$from.end());
474
+ }
475
+ if (normalize) (tr || (tr = state.tr)).setSelection(normalize);
476
+ return tr;
477
+ }