@quillmark/svelte 0.20.1 → 0.22.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/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/THEMING.md +1 -1
  4. package/dist/core/address.d.ts +6 -1
  5. package/dist/core/address.js +12 -3
  6. package/dist/core/bloom.d.ts +9 -4
  7. package/dist/core/bloom.js +18 -49
  8. package/dist/core/codec/atoms.d.ts +1 -1
  9. package/dist/core/codec/atoms.js +26 -11
  10. package/dist/core/codec/blocks.d.ts +5 -5
  11. package/dist/core/codec/blocks.js +27 -8
  12. package/dist/core/codec/code.d.ts +4 -4
  13. package/dist/core/codec/code.js +52 -22
  14. package/dist/core/codec/decode.d.ts +5 -5
  15. package/dist/core/codec/decode.js +18 -12
  16. package/dist/core/codec/encode.js +6 -1
  17. package/dist/core/codec/field.d.ts +22 -15
  18. package/dist/core/codec/field.js +66 -26
  19. package/dist/core/codec/index.d.ts +1 -1
  20. package/dist/core/codec/index.js +2 -2
  21. package/dist/core/codec/inputrules.d.ts +4 -0
  22. package/dist/core/codec/inputrules.js +44 -6
  23. package/dist/core/codec/link-paste.d.ts +11 -0
  24. package/dist/core/codec/link-paste.js +32 -0
  25. package/dist/core/codec/lists.d.ts +5 -4
  26. package/dist/core/codec/lists.js +87 -31
  27. package/dist/core/codec/nesting.d.ts +20 -0
  28. package/dist/core/codec/nesting.js +62 -0
  29. package/dist/core/codec/prose.css +145 -63
  30. package/dist/core/codec/schema.d.ts +2 -2
  31. package/dist/core/codec/schema.js +71 -18
  32. package/dist/core/codec/slash.js +0 -0
  33. package/dist/core/codec/table-copy.d.ts +17 -0
  34. package/dist/core/codec/table-copy.js +84 -0
  35. package/dist/core/codec/table-view.d.ts +6 -7
  36. package/dist/core/codec/table-view.js +281 -201
  37. package/dist/core/codec/table.d.ts +2 -9
  38. package/dist/core/codec/table.js +10 -19
  39. package/dist/core/codec/tasks.d.ts +19 -0
  40. package/dist/core/codec/tasks.js +140 -0
  41. package/dist/core/errors.d.ts +2 -2
  42. package/dist/core/rungs.d.ts +6 -0
  43. package/dist/core/rungs.js +11 -0
  44. package/dist/core/theme.css +24 -12
  45. package/dist/visual/ArrayField.svelte +236 -78
  46. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  47. package/dist/visual/BooleanField.svelte +12 -1
  48. package/dist/visual/Card.svelte +41 -23
  49. package/dist/visual/Card.svelte.d.ts +3 -2
  50. package/dist/visual/CardControls.svelte +15 -15
  51. package/dist/visual/DateField.svelte +93 -64
  52. package/dist/visual/EnumField.svelte +9 -4
  53. package/dist/visual/Field.svelte +20 -16
  54. package/dist/visual/Field.svelte.d.ts +4 -2
  55. package/dist/visual/FieldHint.svelte +22 -5
  56. package/dist/visual/FieldLabel.svelte +5 -33
  57. package/dist/visual/FieldLabel.svelte.d.ts +0 -2
  58. package/dist/visual/FormatPopover.svelte +78 -81
  59. package/dist/visual/MatrixField.svelte +28 -19
  60. package/dist/visual/NumberField.svelte +32 -3
  61. package/dist/visual/ObjectField.svelte +40 -24
  62. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  63. package/dist/visual/ProseField.svelte +10 -13
  64. package/dist/visual/ProseField.svelte.d.ts +5 -2
  65. package/dist/visual/ProseValue.svelte +25 -19
  66. package/dist/visual/ProseValue.svelte.d.ts +4 -4
  67. package/dist/visual/TextField.svelte +3 -2
  68. package/dist/visual/TipsCard.svelte +7 -7
  69. package/dist/visual/VariantField.svelte +1 -1
  70. package/dist/visual/VariantField.svelte.d.ts +1 -1
  71. package/dist/visual/VisualEditorInner.svelte +121 -39
  72. package/dist/visual/controls.css +21 -2
  73. package/dist/visual/icons/Icon.svelte +3 -2
  74. package/dist/visual/icons/Icon.svelte.d.ts +3 -2
  75. package/dist/visual/icons/nodes.d.ts +25 -21
  76. package/dist/visual/icons/nodes.js +9 -7
  77. package/dist/visual/leaves.d.ts +12 -9
  78. package/dist/visual/leaves.js +19 -0
  79. package/dist/visual/motion.d.ts +25 -3
  80. package/dist/visual/motion.js +58 -9
  81. package/dist/visual/strings.d.ts +15 -9
  82. package/dist/visual/strings.js +19 -12
  83. package/dist/visual/structure.d.ts +21 -31
  84. package/dist/visual/structure.js +54 -51
  85. package/package.json +3 -3
@@ -15,20 +15,23 @@
15
15
  // is preserved, never minted), and no placeholder.
16
16
  //
17
17
  // The chrome is a band and a selection (CODEC §"The table island"), and the band raises
18
- // nothing: what floats over the grid is the held column's alignment cluster alone. Every
19
- // control is absolutely positioned out of the grid, so none is in a row or a column of
20
- // it; `codec/prose.css` draws the band, the cluster and the selection wash.
21
- import { baseKeymap, chainCommands, toggleMark } from 'prosemirror-commands';
18
+ // nothing: what floats over the grid is a held column's alignment or the held first
19
+ // row's header toggle. Every control is absolutely positioned out of the grid, so none
20
+ // is in a row or a column of it; `codec/prose.css` draws the band, the clusters and the
21
+ // selection wash.
22
+ import { baseKeymap, chainCommands, selectAll, toggleMark } from 'prosemirror-commands';
22
23
  import { redo, undo } from 'prosemirror-history';
23
24
  import { keymap } from 'prosemirror-keymap';
24
25
  import { EditorState, NodeSelection, Selection, TextSelection } from 'prosemirror-state';
25
26
  import { EditorView } from 'prosemirror-view';
26
27
  import { decode } from './decode.js';
27
- import { inputRulesPlugin } from './inputrules.js';
28
+ import { inputRulesPlugin, undoRuleKeymap } from './inputrules.js';
29
+ import { linkPastePlugin } from './link-paste.js';
28
30
  import { tablePropsOfNode } from './islands.js';
31
+ import { tableElement, tableText } from './table-copy.js';
29
32
  import { breakKeymap } from './breaks.js';
30
33
  import { cellSchema } from './schema.js';
31
- import { ALIGNS, cellAt, cellContent, cellEqual, cellFromDoc, clearCells, columnCount, deleteColumn, deleteRow, insertColumn, insertRow, moveColumn, moveRow, normalizeTable, rowCells, rowCount, rowEmpty, setAlign, setHeadless, setCellLayout, columnShares, setWidths, shapeEqual, withCell } from './table.js';
34
+ import { ALIGNS, cellAt, cellContent, cellEqual, cellFromDoc, clearCells, columnCount, deleteColumn, deleteRow, insertColumn, insertRow, moveColumn, moveRow, normalizeTable, rowCells, rowCount, rowEmpty, setAlign, setHeadless, columnShares, setWidths, shapeEqual, withCell } from './table.js';
32
35
  /**
33
36
  * The package's English for the island chrome. It lives here, beside the surface
34
37
  * that draws it, rather than in the visual tier's table: the codec mounts this
@@ -50,11 +53,7 @@ export const DEFAULT_TABLE_STRINGS = {
50
53
  tableAlignLeft: 'Align left',
51
54
  tableAlignCenter: 'Align center',
52
55
  tableAlignRight: 'Align right',
53
- tableHeaderToggle: 'Header row',
54
- tableAlignTop: 'Align top',
55
- tableAlignMiddle: 'Align middle',
56
- tableAlignBottom: 'Align bottom',
57
- tableCellAlignment: 'Cell alignment'
56
+ tableHeaderToggle: 'Header row'
58
57
  };
59
58
  /** The grip's glyph, as the path data a DOM node can carry — this chrome's own set rather
60
59
  * than `visual/icons/nodes.ts`, this being the one place chrome is built without Svelte,
@@ -79,28 +78,6 @@ const HEADER_GLYPH = [
79
78
  'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z',
80
79
  'M3 9h18'
81
80
  ];
82
- /** The held cells' vertical toggles' glyphs, off the same release. */
83
- const VALIGN_GLYPH = {
84
- top: [
85
- 'M6 6h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z',
86
- 'M16 6h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z',
87
- 'M22 2H2'
88
- ],
89
- middle: [
90
- 'M2 12h20',
91
- 'M10 16v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4',
92
- 'M10 8V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4',
93
- 'M20 16v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-1',
94
- 'M14 8V7c0-1.1.9-2 2-2h2a2 2 0 0 1 2 2v1'
95
- ],
96
- bottom: [
97
- 'M6 2h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z',
98
- 'M16 9h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2z',
99
- 'M22 22H2'
100
- ]
101
- };
102
- /** The vertical alignments a cell takes, in the cluster's order. */
103
- const VALIGNS = ['top', 'middle', 'bottom'];
104
81
  /** Draw a cell's alignment on its box: its own where it holds one, its column's
105
82
  * otherwise, and the vertical one where it holds one. */
106
83
  function layCell(box, props, r, c) {
@@ -158,10 +135,16 @@ function chromeButton(className, label, run) {
158
135
  });
159
136
  return btn;
160
137
  }
161
- /** A cell's plugin stack: marks and the markdown shorthands, and nothing that
162
- * belongs to the field (history, anchors, the ghost). */
138
+ /** A cell's plugin stack: marks, the markdown shorthands and a pasted URL's link, and
139
+ * nothing that belongs to the field (history, anchors, the ghost). */
163
140
  function cellPlugins(keys) {
164
- return [inputRulesPlugin(cellSchema), keymap(keys), keymap(baseKeymap)];
141
+ return [
142
+ inputRulesPlugin(cellSchema),
143
+ undoRuleKeymap(),
144
+ linkPastePlugin(cellSchema.marks.link),
145
+ keymap(keys),
146
+ keymap(baseKeymap)
147
+ ];
165
148
  }
166
149
  /** The controls, each of which answers for its own press. Spelled apart from
167
150
  * {@link owned} because the pointer router needs them before it needs the cells: a grip
@@ -181,14 +164,26 @@ const within = (rect, p) => p.x >= rect.left && p.x <= rect.right && p.y >= rect
181
164
  function distance(rect, p) {
182
165
  return Math.hypot(Math.max(rect.left - p.x, 0, p.x - rect.right), Math.max(rect.top - p.y, 0, p.y - rect.bottom));
183
166
  }
184
- const sameCells = (a, b) => a.r0 === b.r0 && a.c0 === b.c0 && a.r1 === b.r1 && a.c1 === b.c1;
185
- /** The rectangle two corners span, in either order. */
167
+ const sameCells = (a, b) => a.r0 === b.r0 && a.c0 === b.c0 && a.r1 === b.r1 && a.c1 === b.c1 && a.swept === b.swept;
168
+ /** The block two corners span, in either order: what a sweep holds. */
186
169
  const spanCells = (a, b) => ({
187
170
  r0: Math.min(a.r, b.r),
188
171
  c0: Math.min(a.c, b.c),
189
172
  r1: Math.max(a.r, b.r),
190
- c1: Math.max(a.c, b.c)
173
+ c1: Math.max(a.c, b.c),
174
+ swept: true
191
175
  });
176
+ /** Each mounted island drawing a table, by its `dom`, which is what the outer view hands
177
+ * back for a position: how a key in the field reaches into a table ({@link tableAt}). A
178
+ * placeholder is absent, so it answers as the atom it draws. */
179
+ const mounted = new WeakMap();
180
+ /**
181
+ * Where the caret stood as each table value was written over, keyed by that value. An
182
+ * undo hands the node back the very `props` object the undone edit replaced, so the seat
183
+ * held against it is where the edit was made from: where PM's history puts the caret
184
+ * back in a field's own text.
185
+ */
186
+ const departures = new WeakMap();
192
187
  class TableIslandView {
193
188
  node;
194
189
  outer;
@@ -210,9 +205,6 @@ class TableIslandView {
210
205
  clusters = [];
211
206
  /** The first row's header toggle, up while that row is held. */
212
207
  headerCluster;
213
- /** The held cells' cluster, up over the first cell of a held rectangle that is no
214
- * column, and built for each paint since what it presses is the cells'. */
215
- cellCluster;
216
208
  /** The grid's own box, and the containing block every out-of-flow control is placed
217
209
  * against. Not the scroller: an absolute inside a scroll container is placed
218
210
  * against a padding box the scroll then slides out from under, so a control at the
@@ -225,6 +217,8 @@ class TableIslandView {
225
217
  resize;
226
218
  /** A drag's trailing `click`, which would otherwise re-select the moved line. */
227
219
  suppressClick = false;
220
+ /** The grips of the row and column under the pointer, drawn up. */
221
+ near = [];
228
222
  constructor(node, outer, getPos, deps) {
229
223
  this.node = node;
230
224
  this.outer = outer;
@@ -234,6 +228,9 @@ class TableIslandView {
234
228
  this.dom.setAttribute('data-qm-island', node.attrs.islandType);
235
229
  this.dom.setAttribute('data-qm-island-id', node.attrs.id);
236
230
  this.dom.addEventListener('mousedown', this.onPointerDown);
231
+ this.dom.addEventListener('mouseover', this.onHover);
232
+ this.dom.addEventListener('mouseleave', this.onLeave);
233
+ this.dom.addEventListener('copy', this.onCopy, true);
237
234
  this.render();
238
235
  }
239
236
  // ── PM's NodeView contract ────────────────────────────────────────────────
@@ -256,6 +253,7 @@ class TableIslandView {
256
253
  // transaction, which is every keystroke in the table, and projecting each cell
257
254
  // costs a `mapMarks` across the WASM boundary wherever one holds an anchor.
258
255
  this.rendered = props;
256
+ const departure = departures.get(props);
259
257
  for (const mounted of this.cells) {
260
258
  const stored = cellAt(props, mounted.r, mounted.c);
261
259
  if (cellEqual(stored, mounted.shown))
@@ -267,13 +265,14 @@ class TableIslandView {
267
265
  });
268
266
  mounted.view.updateState(fresh);
269
267
  mounted.shown = stored;
270
- // Best-effort caret continuity, the rule a field's own re-hydrate takes: keep
271
- // the offset, clamped into the text that is there now. A fresh state resolves
272
- // its selection to the start of the cell, so an undo would otherwise put the
273
- // caret somewhere the edit it undid never was.
268
+ // The rule a field's own re-hydrate takes: keep the offset, clamped into the
269
+ // text that is there now, a fresh state resolving to the cell's start. An undo
270
+ // outranks it below, the departure being where the edit was.
274
271
  const at = Math.min(head, fresh.doc.content.size);
275
272
  mounted.view.dispatch(fresh.tr.setSelection(Selection.near(fresh.doc.resolve(at))));
276
273
  }
274
+ if (departure && this.focusedSeat())
275
+ this.focusCell(departure.r, departure.c, departure.head);
277
276
  // A cell's alignment is no part of its text, so it is restated on every box: the
278
277
  // reseed above compares text and marks alone.
279
278
  for (const mounted of this.cells)
@@ -296,6 +295,9 @@ class TableIslandView {
296
295
  }
297
296
  destroy() {
298
297
  this.dom.removeEventListener('mousedown', this.onPointerDown);
298
+ this.dom.removeEventListener('mouseover', this.onHover);
299
+ this.dom.removeEventListener('mouseleave', this.onLeave);
300
+ this.dom.removeEventListener('copy', this.onCopy, true);
299
301
  this.endDrag();
300
302
  this.endSweep();
301
303
  this.endResize();
@@ -355,6 +357,43 @@ class TableIslandView {
355
357
  else
356
358
  this.caretBeside(point);
357
359
  };
360
+ /** A cell under the pointer draws its row's and its column's grips, so a line's
361
+ * handle is found from the line rather than only from the band beside it. A control
362
+ * under the pointer is its own reveal, and draws no other. */
363
+ onHover = (event) => {
364
+ const target = event.target;
365
+ const box = target?.closest?.(CONTROLS) ? null : target?.closest?.('.qm-table-cell');
366
+ const cell = box ? this.cells.find((m) => m.box === box) : undefined;
367
+ const next = cell
368
+ ? [
369
+ this.grips.get(lineKey({ axis: 'row', index: cell.r }))?.grip,
370
+ this.grips.get(lineKey({ axis: 'column', index: cell.c }))?.grip
371
+ ].filter((grip) => !!grip)
372
+ : [];
373
+ for (const grip of this.near)
374
+ if (!next.includes(grip))
375
+ grip.removeAttribute('data-near');
376
+ for (const grip of next)
377
+ grip.setAttribute('data-near', '');
378
+ this.near = next;
379
+ };
380
+ onLeave = () => {
381
+ for (const grip of this.near)
382
+ grip.removeAttribute('data-near');
383
+ this.near = [];
384
+ };
385
+ /** A copy over a held rectangle writes its cells (`table-copy.ts`). Captured, since the
386
+ * caret's cell would otherwise copy its own text selection over them. */
387
+ onCopy = (event) => {
388
+ const held = this.selected;
389
+ if (!held || !event.clipboardData)
390
+ return;
391
+ const props = this.props();
392
+ event.clipboardData.setData('text/html', tableElement(props, held).outerHTML);
393
+ event.clipboardData.setData('text/plain', tableText(props, held));
394
+ event.preventDefault();
395
+ event.stopPropagation();
396
+ };
358
397
  /** The cell a press inside the frame belongs to: the nearest by rect, which for a
359
398
  * press on a border or a cell's own padding is the cell it is against.
360
399
  *
@@ -399,18 +438,32 @@ class TableIslandView {
399
438
  * leaf mounted without that plugin then answers "no gap" instead of dispatching
400
439
  * a selection nothing draws. */
401
440
  caretBeside(point) {
441
+ const box = this.dom.getBoundingClientRect();
442
+ this.leave(point.y < (box.top + box.bottom) / 2 ? -1 : 1);
443
+ }
444
+ /** Land the document's caret on one side of the island, by
445
+ * {@link TableIslandView.caretBeside}'s rule; false where that side holds no caret. */
446
+ leave(side) {
402
447
  const pos = this.getPos();
403
448
  if (pos == null)
404
- return;
405
- const box = this.dom.getBoundingClientRect();
406
- const before = point.y < (box.top + box.bottom) / 2;
407
- const $at = this.outer.state.doc.resolve(before ? pos : pos + this.node.nodeSize);
449
+ return false;
450
+ const $at = this.outer.state.doc.resolve(side < 0 ? pos : pos + this.node.nodeSize);
408
451
  const selection = this.outer.someProp('createSelectionBetween', (f) => f(this.outer, $at, $at)) ??
409
- Selection.findFrom($at, before ? -1 : 1, true);
452
+ Selection.findFrom($at, side, true);
410
453
  if (!selection)
411
- return;
454
+ return false;
412
455
  this.outer.focus();
413
456
  this.outer.dispatch(this.outer.state.tr.setSelection(selection));
457
+ return true;
458
+ }
459
+ /** Land the caret in the table from the block beside it: its last cell's end from
460
+ * below, its first cell's start from above. */
461
+ enter(side) {
462
+ const props = this.props();
463
+ if (side < 0)
464
+ this.focusCell(rowCount(props) - 1, columnCount(props) - 1, 'end');
465
+ else
466
+ this.focusCell(0, 0, 'start');
414
467
  }
415
468
  /** The island as the selection: the Escape that climbs out of a cell or a held
416
469
  * rectangle, which is the whole of what selects it. Backspace there deletes the
@@ -497,8 +550,8 @@ class TableIslandView {
497
550
  * revalidates the whole table. */
498
551
  lineCells(line, props = this.props()) {
499
552
  return line.axis === 'row'
500
- ? { r0: line.index, c0: 0, r1: line.index, c1: columnCount(props) - 1 }
501
- : { r0: 0, c0: line.index, r1: rowCount(props) - 1, c1: line.index };
553
+ ? { r0: line.index, c0: 0, r1: line.index, c1: columnCount(props) - 1, swept: false }
554
+ : { r0: 0, c0: line.index, r1: rowCount(props) - 1, c1: line.index, swept: false };
502
555
  }
503
556
  /** The index space an axis allows, which is the whole of it on both. The header is
504
557
  * row 0 rather than a line above the floor: a walk reaches it, a drag lands on it,
@@ -528,14 +581,12 @@ class TableIslandView {
528
581
  else if (held?.c !== line.index)
529
582
  this.focusCell(held?.r ?? 0, line.index);
530
583
  }
531
- /** Which line the held rectangle is, on one axis: a row spanning every column, a
532
- * column spanning every row, and nothing at all for a block that spans neither. The
533
- * extent rule reaching the line verbs ({@link TableIslandView.deleteSelection}) — a
534
- * rectangle answers by what it covers, never by the gesture that drew it, so a row
535
- * swept cell by cell moves exactly as the row a grip named does. */
584
+ /** Which line the held rectangle names, on one axis: a row spanning every column, a
585
+ * column spanning every row, and nothing for a block that spans neither or for cells
586
+ * a sweep holds, which are text however many lines they cross. */
536
587
  lineOn(axis) {
537
588
  const held = this.selected;
538
- if (!held)
589
+ if (!held || held.swept)
539
590
  return undefined;
540
591
  const props = this.props();
541
592
  if (axis === 'row')
@@ -554,18 +605,24 @@ class TableIslandView {
554
605
  // One subject at a time: a cell selection retires an island one, or the surface
555
606
  // paints a washed row inside an outlined table and the next Backspace has two
556
607
  // honest readings.
557
- const pos = this.getPos();
558
- const outer = this.outer.state.selection;
559
- if (pos != null && outer instanceof NodeSelection && outer.from === pos) {
560
- const $at = this.outer.state.doc.resolve(pos);
561
- const beside = this.outer.someProp('createSelectionBetween', (f) => f(this.outer, $at, $at)) ??
562
- Selection.findFrom($at, -1, true);
563
- if (beside)
564
- this.outer.dispatch(this.outer.state.tr.setSelection(beside));
565
- }
608
+ this.retireIsland();
566
609
  this.selected = cells;
567
610
  this.paintSelection();
568
611
  }
612
+ /** Move the outer selection off this island when it holds it: a caret or a rectangle
613
+ * inside the grid is the subject from then on, and an outline left on the table says
614
+ * the next Backspace deletes it. */
615
+ retireIsland() {
616
+ const pos = this.getPos();
617
+ const outer = this.outer.state.selection;
618
+ if (pos == null || !(outer instanceof NodeSelection) || outer.from !== pos)
619
+ return;
620
+ const $at = this.outer.state.doc.resolve(pos);
621
+ const beside = this.outer.someProp('createSelectionBetween', (f) => f(this.outer, $at, $at)) ??
622
+ Selection.findFrom($at, -1, true);
623
+ if (beside)
624
+ this.outer.dispatch(this.outer.state.tr.setSelection(beside));
625
+ }
569
626
  clearSelection() {
570
627
  if (!this.selected)
571
628
  return;
@@ -588,24 +645,25 @@ class TableIslandView {
588
645
  if (line.axis === 'row' && line.index === 0 && this.headerCluster)
589
646
  this.headerCluster.hidden = !named;
590
647
  }
591
- this.paintCellCluster(props);
592
648
  for (const cell of this.cells)
593
649
  cell.box.toggleAttribute('data-selected', !!held && cell.r >= held.r0 && cell.r <= held.r1 && cell.c >= held.c0 && cell.c <= held.c1);
594
650
  }
595
651
  /**
596
- * What Backspace means over the selection, decided by its extent rather than by the
597
- * gesture that drew it (CODEC §"The table island").
598
- *
599
- * The both-axes arm is the rule's own limit rather than an exception to it: every rank
600
- * going at once leaves no table for a rank rule to have produced. It is the whole of
601
- * how a pointer deletes a table, and on a one-column or one-row table a single grip
602
- * draws it.
652
+ * What Backspace means over the selection (CODEC §"The table island"). Swept cells
653
+ * clear and keep their lines. Named lines go, read off their extent: every rank on
654
+ * both axes is the table, which goes as the island, and on a one-column or one-row
655
+ * table a single grip draws that.
603
656
  */
604
657
  deleteSelection() {
605
658
  const held = this.selected;
606
659
  if (!held)
607
660
  return;
608
661
  const props = this.props();
662
+ if (held.swept) {
663
+ if (allCells(props, held).some((cell) => cell.text !== ''))
664
+ this.write(clearCells(props, held.r0, held.c0, held.r1, held.c1));
665
+ return;
666
+ }
609
667
  const wide = held.c0 === 0 && held.c1 === columnCount(props) - 1;
610
668
  const tall = held.r0 === 0 && held.r1 === rowCount(props) - 1;
611
669
  if (tall && wide)
@@ -663,73 +721,17 @@ class TableIslandView {
663
721
  this.write(line.axis === 'row' ? moveRow(props, line.index, by) : moveColumn(props, line.index, by));
664
722
  this.selectLine({ axis: line.axis, index: to });
665
723
  }
666
- /** Set column `c`'s alignment and keep the column held: a changed alignment rebuilds
667
- * the views, which retires the selection as a move's rebuild does. The alignment the
668
- * column already holds writes nothing. */
724
+ /** Set column `c`'s alignment, clear its cells' own, which would outrank it, and keep
725
+ * the column held: a changed alignment rebuilds the views, which retires the selection
726
+ * as a move's rebuild does. One `set` for both, and a column already at `to` with no
727
+ * cell of its own writes nothing. */
669
728
  align(c, to) {
670
729
  const props = this.props();
671
- if (props.aligns[c] !== to)
730
+ const own = Array.from({ length: rowCount(props) }, (_, r) => cellAt(props, r, c)).some((cell) => cell.align !== undefined);
731
+ if (props.aligns[c] !== to || own)
672
732
  this.write(setAlign(props, c, to));
673
733
  this.selectLine({ axis: 'column', index: c });
674
734
  }
675
- /** The held cells' cluster: up over the first cell of a held rectangle, where no
676
- * column's own cluster is, each toggle pressed where every cell in the rectangle
677
- * holds it. It floats over the cell under that one, as the column's floats over the
678
- * first row, and is out of the tab order for the reason a grip is, its keyboard twin
679
- * being `Mod-Shift-l`, `-e` and `-r` (§{@link TableIslandView.cellKeys}). */
680
- paintCellCluster(props) {
681
- this.cellCluster?.remove();
682
- this.cellCluster = undefined;
683
- const held = this.selected;
684
- if (!held || this.lineOn('column') !== undefined)
685
- return;
686
- const first = this.cells.find((m) => m.r === held.r0 && m.c === held.c0);
687
- if (!first)
688
- return;
689
- const s = this.deps.strings();
690
- const covered = allCells(props, held);
691
- const holds = (key, value) => covered.every((cell) => cell[key] === value);
692
- const cluster = el('div', 'qm-table-align');
693
- cluster.setAttribute('role', 'group');
694
- cluster.setAttribute('aria-label', s.tableCellAlignment);
695
- cluster.setAttribute('data-cells', '');
696
- cluster.addEventListener('mousedown', (e) => e.preventDefault());
697
- const toggle = (label, glyph, pressed, run) => {
698
- const btn = chromeButton('qm-table-align-option', label, run);
699
- btn.tabIndex = -1;
700
- btn.setAttribute('aria-pressed', String(pressed));
701
- btn.appendChild(svg(glyph, 2));
702
- cluster.appendChild(btn);
703
- return btn;
704
- };
705
- const names = {
706
- left: s.tableAlignLeft,
707
- center: s.tableAlignCenter,
708
- right: s.tableAlignRight
709
- };
710
- for (const align of SETTABLE)
711
- toggle(names[align], ALIGN_GLYPH[align], holds('align', align), () => this.layCells('align', align)).setAttribute('data-align', align);
712
- const vnames = {
713
- top: s.tableAlignTop,
714
- middle: s.tableAlignMiddle,
715
- bottom: s.tableAlignBottom
716
- };
717
- for (const valign of VALIGNS)
718
- toggle(vnames[valign], VALIGN_GLYPH[valign], holds('valign', valign), () => this.layCells('valign', valign)).setAttribute('data-valign', valign);
719
- first.box.appendChild(cluster);
720
- this.cellCluster = cluster;
721
- }
722
- /** Lay every held cell, or the caret's where none is held, at `value`, and clear the
723
- * key from them where every one already holds it: the press that set an alignment
724
- * takes it back to the column's. One `set`, and the rectangle stays held. */
725
- layCells(key, value, at) {
726
- const held = this.selected ?? (at && { r0: at.r, c0: at.c, r1: at.r, c1: at.c });
727
- if (!held)
728
- return;
729
- const props = this.props();
730
- const every = allCells(props, held).every((cell) => cell[key] === value);
731
- this.write(setCellLayout(props, held.r0, held.c0, held.r1, held.c1, key, every ? undefined : value));
732
- }
733
735
  // ── Drag to reorder ───────────────────────────────────────────────────────
734
736
  /**
735
737
  * Press-and-drag a grip moves its line. The press still selects: the dead zone is
@@ -1013,11 +1015,17 @@ class TableIslandView {
1013
1015
  this.endResize();
1014
1016
  this.teardownCells();
1015
1017
  this.grips.clear();
1018
+ this.near = [];
1016
1019
  this.clusters = [];
1017
1020
  this.headerCluster = undefined;
1018
1021
  this.dom.textContent = '';
1019
1022
  const props = tablePropsOfNode(this.node);
1023
+ const departure = props && departures.get(props);
1020
1024
  this.rendered = props;
1025
+ if (props)
1026
+ mounted.set(this.dom, this);
1027
+ else
1028
+ mounted.delete(this.dom);
1021
1029
  if (!props) {
1022
1030
  // Any other island type: the literal placeholder `toDOM` draws (islands.ts).
1023
1031
  this.dom.appendChild(document.createTextNode(`[${this.node.attrs.islandType || 'island'}]`));
@@ -1059,7 +1067,7 @@ class TableIslandView {
1059
1067
  scroller.appendChild(frame);
1060
1068
  this.dom.appendChild(scroller);
1061
1069
  this.paintSelection();
1062
- this.reseat(seat);
1070
+ this.reseat(seat, departure);
1063
1071
  }
1064
1072
  /**
1065
1073
  * Where the focus sits, in terms a rebuild can restore it by: the caret's cell by
@@ -1074,14 +1082,19 @@ class TableIslandView {
1074
1082
  */
1075
1083
  focusedSeat() {
1076
1084
  const held = this.cells.find((m) => m.view.hasFocus());
1077
- return held && { r: held.r, c: held.c };
1085
+ return held && { r: held.r, c: held.c, head: held.view.state.selection.head };
1078
1086
  }
1079
- /** Put the caret back where {@link TableIslandView.focusedSeat} found it, clamped by
1080
- * `focusCell` into the rectangle that is there now: the seat may be the cell the
1081
- * rebuild removed. A caller that has a landing of its own overrides this by running
1082
- * after. */
1083
- reseat(seat) {
1084
- if (seat)
1087
+ /** Put the focus back after a rebuild: at the departure where the table returns to a
1088
+ * value an edit was made from, which is an undo, else at the end of the cell
1089
+ * {@link TableIslandView.focusedSeat} found, clamped by `focusCell` into the
1090
+ * rectangle that is there now, the seat being perhaps a cell the rebuild removed. A
1091
+ * caller that has a landing of its own overrides this by running after. */
1092
+ reseat(seat, departure) {
1093
+ if (!seat)
1094
+ return;
1095
+ if (departure)
1096
+ this.focusCell(departure.r, departure.c, departure.head);
1097
+ else
1085
1098
  this.focusCell(seat.r, seat.c);
1086
1099
  }
1087
1100
  /** One table row: its cells, each carrying whatever chrome hangs off it. */
@@ -1146,11 +1159,11 @@ class TableIslandView {
1146
1159
  return btn;
1147
1160
  }
1148
1161
  /** A column's alignment cluster: a toggle per alignment, pressed on the one the column
1149
- * holds. Up only while the held rectangle is this column
1162
+ * holds. Up only while this column is the named line
1150
1163
  * ({@link TableIslandView.paintSelection}), and absent rather than transparent the
1151
1164
  * rest of the time: it floats over the first row, and a box hit-tested there at rest
1152
1165
  * would take that cell's presses. Out of the tab order for the reason a grip is, its
1153
- * keyboard twin being Shift+Left and Shift+Right (§{@link TableIslandView.cellKeys}). */
1166
+ * keyboard twins being Shift+Left and Shift+Right (§{@link TableIslandView.cellKeys}). */
1154
1167
  alignCluster(c, held, s) {
1155
1168
  const cluster = el('div', 'qm-table-align');
1156
1169
  cluster.setAttribute('role', 'group');
@@ -1201,12 +1214,10 @@ class TableIslandView {
1201
1214
  * the pointer out past the last line is the claim — so its name carries the verb for
1202
1215
  * everything that does not read position.
1203
1216
  *
1204
- * In the tab order, where a grip is not: growth is what the line verbs do not carry,
1205
- * so the column bar is the keyboard's only route to a new column, and the row bar is
1206
- * that control on the other axis, where Enter at the last row and Tab past the last
1207
- * cell reach the verb from inside a cell. They sit after the grid, so the Tab that
1208
- * declines off the last cell lands on them, and the focus rung draws the one it lands
1209
- * on (`codec/prose.css`). */
1217
+ * Out of the tab order, as a grip is: the Tab that declines off the last cell leaves
1218
+ * the island rather than landing on two controls drawn only under the pointer. A row
1219
+ * is the keyboard's from inside a cell, by Enter at the last row and Tab past the
1220
+ * last cell, and a column is the pointer's alone. */
1210
1221
  addBar(axis, label) {
1211
1222
  const btn = chromeButton('qm-table-add', label, () => {
1212
1223
  const props = this.props();
@@ -1216,6 +1227,7 @@ class TableIslandView {
1216
1227
  this.write(insertColumn(props, columnCount(props) - 1), { r: 0, c: columnCount(props) });
1217
1228
  });
1218
1229
  btn.setAttribute('data-axis', axis);
1230
+ btn.tabIndex = -1;
1219
1231
  btn.appendChild(el('span', 'qm-table-add-bar'));
1220
1232
  return btn;
1221
1233
  }
@@ -1236,6 +1248,7 @@ class TableIslandView {
1236
1248
  scrollThreshold: this.deps.clearance,
1237
1249
  scrollMargin: this.deps.clearance,
1238
1250
  dispatchTransaction: (tr) => {
1251
+ const from = { r, c, head: view.state.selection.head };
1239
1252
  const next = view.state.apply(tr);
1240
1253
  view.updateState(next);
1241
1254
  if (!tr.docChanged)
@@ -1248,11 +1261,12 @@ class TableIslandView {
1248
1261
  const now = this.props();
1249
1262
  const cell = cellFromDoc(next.doc, cellAt(now, r, c));
1250
1263
  mounted.shown = cell;
1251
- this.write(withCell(now, r, c, cell));
1264
+ this.write(withCell(now, r, c, cell), undefined, from);
1252
1265
  },
1253
1266
  handleDOMEvents: {
1254
1267
  focus: () => {
1255
1268
  this.clearSelection();
1269
+ this.retireIsland();
1256
1270
  this.deps.onCellFocus();
1257
1271
  return false;
1258
1272
  }
@@ -1283,13 +1297,16 @@ class TableIslandView {
1283
1297
  * op is one commit and one undo step, which is why every op writes a whole
1284
1298
  * table rather than mutating the props in place.
1285
1299
  */
1286
- write(next, focus) {
1300
+ write(next, focus, from = this.focusedSeat()) {
1287
1301
  const pos = this.getPos();
1288
1302
  if (pos == null)
1289
1303
  return;
1290
1304
  const node = this.outer.state.doc.nodeAt(pos);
1291
1305
  if (!node || node.type !== this.node.type)
1292
1306
  return;
1307
+ const left = tablePropsOfNode(node);
1308
+ if (left && from)
1309
+ departures.set(left, from);
1293
1310
  this.outer.dispatch(this.outer.state.tr.setNodeMarkup(pos, undefined, {
1294
1311
  ...node.attrs,
1295
1312
  props: normalizeTable(next)
@@ -1297,9 +1314,10 @@ class TableIslandView {
1297
1314
  if (focus)
1298
1315
  this.focusCell(focus.r, focus.c);
1299
1316
  }
1300
- /** Land the caret at the end of a cell, clamped into the rectangle: where a row
1301
- * or column op puts it, since the op rebuilt the views the caret was in. */
1302
- focusCell(r, c) {
1317
+ /** Land the caret at the end of a cell, at its start, or at an offset clamped into its
1318
+ * text, the cell itself clamped into the rectangle: where a row or column op puts it,
1319
+ * since the op rebuilt the views the caret was in. */
1320
+ focusCell(r, c, edge = 'end') {
1303
1321
  const props = this.props();
1304
1322
  const row = Math.max(0, Math.min(r, rowCount(props) - 1));
1305
1323
  const col = Math.max(0, Math.min(c, columnCount(props) - 1));
@@ -1307,12 +1325,29 @@ class TableIslandView {
1307
1325
  if (!mounted)
1308
1326
  return;
1309
1327
  const { view } = mounted;
1328
+ const { doc } = view.state;
1329
+ const at = edge === 'start'
1330
+ ? Selection.atStart(doc)
1331
+ : edge === 'end'
1332
+ ? Selection.atEnd(doc)
1333
+ : Selection.near(doc.resolve(Math.min(edge, doc.content.size)));
1310
1334
  // Flagged, for the reason the leaf's own landing is (`field.ts`, `setCaret`): PM
1311
1335
  // focuses with `preventScroll`, so an unflagged dispatch lands the caret where no
1312
1336
  // scroller has moved to — a Tab past the right edge of the horizontal scroller a
1313
1337
  // wide table lives in, or the row an Enter on the last one appends below the fold.
1314
1338
  view.focus();
1315
- view.dispatch(view.state.tr.setSelection(Selection.atEnd(view.state.doc)).scrollIntoView());
1339
+ view.dispatch(view.state.tr.setSelection(at).scrollIntoView());
1340
+ }
1341
+ /** The cell beside `(r, c)` in reading order, a row's end wrapping to the next row's
1342
+ * start; past the first or last cell its row is outside the table. */
1343
+ reading(r, c, dir) {
1344
+ const cols = columnCount(this.props());
1345
+ const next = c + dir;
1346
+ if (next >= cols)
1347
+ return { r: r + 1, c: 0 };
1348
+ if (next < 0)
1349
+ return { r: r - 1, c: cols - 1 };
1350
+ return { r, c: next };
1316
1351
  }
1317
1352
  /**
1318
1353
  * A cell's keys. Traversal is the island's link in the leaf's chain
@@ -1330,22 +1365,35 @@ class TableIslandView {
1330
1365
  marks['Mod-i'] = toggleMark(cellSchema.marks.em);
1331
1366
  if (cellSchema.marks.underline)
1332
1367
  marks['Mod-u'] = toggleMark(cellSchema.marks.underline);
1333
- // Up and down are the grid's own walk: nothing else moves the caret vertically,
1334
- // and `focusCell` clamps, so neither can grow the table. Left and right do not
1335
- // traverse at all: at a text edge they would call what Tab and Shift-Tab already
1336
- // call, and inherit the append-past-the-last-cell that makes Tab a growth
1337
- // affordance and would make a caret key one.
1368
+ // The arrows are the grid's own walk, each leaving a cell only from the text's edge
1369
+ // on its side. Up and down reach the cell above or below, `focusCell` clamping at
1370
+ // the top and bottom. Left and right reach the neighbour in reading order, landing
1371
+ // at the edge they entered by and stopping at the first and last cells. No arrow
1372
+ // grows the table, growth being Tab's and Enter's: on a caret key it would make a
1373
+ // walk a growth affordance.
1374
+ const caretAtEdge = (view, dir) => {
1375
+ const { selection } = view?.state ?? {};
1376
+ return (!!view && selection instanceof TextSelection && selection.empty && view.endOfTextblock(dir));
1377
+ };
1338
1378
  const walk = (dir) => {
1339
1379
  return (_state, _dispatch, view) => {
1340
- const { selection } = view?.state ?? {};
1341
- if (!view || !(selection instanceof TextSelection) || !selection.empty)
1342
- return false;
1343
- if (!view.endOfTextblock(dir))
1380
+ if (!caretAtEdge(view, dir))
1344
1381
  return false;
1345
1382
  this.focusCell(dir === 'up' ? r - 1 : r + 1, c);
1346
1383
  return true;
1347
1384
  };
1348
1385
  };
1386
+ const cross = (dir) => {
1387
+ return (_state, _dispatch, view) => {
1388
+ if (!caretAtEdge(view, dir < 0 ? 'left' : 'right'))
1389
+ return false;
1390
+ const to = this.reading(r, c, dir);
1391
+ if (to.r < 0 || to.r >= rowCount(this.props()))
1392
+ return false;
1393
+ this.focusCell(to.r, to.c, dir < 0 ? 'end' : 'start');
1394
+ return true;
1395
+ };
1396
+ };
1349
1397
  // A block selection outranks the caret it was swept from: the origin cell still
1350
1398
  // holds the focus, so its own view is where the block's Backspace lands. Declining
1351
1399
  // when no block is held is what leaves an ordinary Backspace to `baseKeymap`.
@@ -1357,17 +1405,22 @@ class TableIslandView {
1357
1405
  };
1358
1406
  // The line verbs, over the held rectangle and nothing else: with none held every
1359
1407
  // one of these declines and the key is the caret's, which is what lets them sit on
1360
- // keys a cell is already typing under. An arrow names the line on its own axis
1361
- // through the caret — the one already held steps to its neighbour and carries the
1362
- // caret, and any other rectangle turns into it, which is how a row becomes the
1363
- // column the caret is in. Alt moves the line instead, and over a rectangle that is
1364
- // not one it does nothing but keep the key: while a rectangle is held every arrow
1365
- // is its own, and an Alt+arrow handed back is the browser's Back on two platforms,
1366
- // mid-gesture.
1408
+ // keys a cell is already typing under. An arrow over swept cells lets go of them,
1409
+ // leaving the caret where it is, as an arrow collapses a text selection. Over named
1410
+ // lines it names the line on its own axis through the caret — the one already held
1411
+ // steps to its neighbour and carries the caret, and any other turns into it, which
1412
+ // is how a row becomes the column the caret is in. Alt moves the line instead, and
1413
+ // over a rectangle that is not one it does nothing but keep the key: while a
1414
+ // rectangle is held every arrow is its own, and an Alt+arrow handed back is the
1415
+ // browser's Back on two platforms, mid-gesture.
1367
1416
  const line = (axis, by, move) => {
1368
1417
  return () => {
1369
1418
  if (!this.selected)
1370
1419
  return false;
1420
+ if (this.selected.swept && !move) {
1421
+ this.clearSelection();
1422
+ return true;
1423
+ }
1371
1424
  const on = this.lineOn(axis);
1372
1425
  if (move) {
1373
1426
  if (on !== undefined)
@@ -1399,6 +1452,34 @@ class TableIslandView {
1399
1452
  return true;
1400
1453
  };
1401
1454
  };
1455
+ // Select-all grows a rung a press: the cell's text, which the base keymap's
1456
+ // `selectAll` takes, then the caret's row, then every cell, then the document. Any
1457
+ // held rectangle short of every cell grows to every cell.
1458
+ const grow = (state) => {
1459
+ const props = this.props();
1460
+ const every = {
1461
+ r0: 0,
1462
+ c0: 0,
1463
+ r1: rowCount(props) - 1,
1464
+ c1: columnCount(props) - 1,
1465
+ swept: false
1466
+ };
1467
+ const held = this.selected;
1468
+ if (held && sameCells(held, every)) {
1469
+ this.clearSelection();
1470
+ this.outer.focus();
1471
+ return selectAll(this.outer.state, this.outer.dispatch);
1472
+ }
1473
+ if (held)
1474
+ this.select(every);
1475
+ else {
1476
+ const { from, to } = state.selection;
1477
+ if (from > Selection.atStart(state.doc).from || to < Selection.atEnd(state.doc).to)
1478
+ return false;
1479
+ this.selectLine({ axis: 'row', index: r });
1480
+ }
1481
+ return true;
1482
+ };
1402
1483
  return {
1403
1484
  ...marks,
1404
1485
  ...breakKeymap(cellSchema),
@@ -1411,17 +1492,15 @@ class TableIslandView {
1411
1492
  Delete: erase,
1412
1493
  ArrowUp: chainCommands(line('row', -1, false), walk('up')),
1413
1494
  ArrowDown: chainCommands(line('row', 1, false), walk('down')),
1414
- ArrowLeft: line('column', -1, false),
1415
- ArrowRight: line('column', 1, false),
1495
+ ArrowLeft: chainCommands(line('column', -1, false), cross(-1)),
1496
+ ArrowRight: chainCommands(line('column', 1, false), cross(1)),
1416
1497
  'Alt-ArrowUp': line('row', -1, true),
1417
1498
  'Alt-ArrowDown': line('row', 1, true),
1418
1499
  'Alt-ArrowLeft': line('column', -1, true),
1419
1500
  'Alt-ArrowRight': line('column', 1, true),
1420
1501
  'Shift-ArrowLeft': slide(-1),
1421
1502
  'Shift-ArrowRight': slide(1),
1422
- 'Shift-Mod-l': () => (this.layCells('align', 'left', { r, c }), true),
1423
- 'Shift-Mod-e': () => (this.layCells('align', 'center', { r, c }), true),
1424
- 'Shift-Mod-r': () => (this.layCells('align', 'right', { r, c }), true),
1503
+ 'Mod-a': grow,
1425
1504
  Tab: () => this.step(r, c, 1),
1426
1505
  'Shift-Tab': () => this.step(r, c, -1),
1427
1506
  Enter: () => {
@@ -1431,11 +1510,17 @@ class TableIslandView {
1431
1510
  this.clearSelection();
1432
1511
  return true;
1433
1512
  }
1513
+ // Past the last row it appends one, and on an empty trailing row it refuses the
1514
+ // row on offer, as Tab's decline does and an empty item's Enter leaves a list:
1515
+ // the row goes and the caret lands past the table. Row 1 is never the one
1516
+ // refused, so the table keeps a row under its first.
1434
1517
  const props = this.props();
1435
- if (r === rowCount(props) - 1)
1436
- this.write(insertRow(props, r), { r: r + 1, c });
1437
- else
1518
+ if (r < rowCount(props) - 1)
1438
1519
  this.focusCell(r + 1, c);
1520
+ else if (r > 1 && rowEmpty(props, r) && this.leave(1))
1521
+ this.write(deleteRow(props, r));
1522
+ else
1523
+ this.write(insertRow(props, r), { r: r + 1, c });
1439
1524
  return true;
1440
1525
  },
1441
1526
  // Escape climbs a rung a press: the caret's own row, then the island. The row
@@ -1467,30 +1552,25 @@ class TableIslandView {
1467
1552
  */
1468
1553
  step(r, c, dir) {
1469
1554
  const props = this.props();
1470
- const cols = columnCount(props);
1471
- const rows = rowCount(props);
1472
- let nr = r;
1473
- let nc = c + dir;
1474
- if (nc >= cols) {
1475
- nr = r + 1;
1476
- nc = 0;
1477
- }
1478
- else if (nc < 0) {
1479
- nr = r - 1;
1480
- nc = cols - 1;
1481
- }
1482
- if (nr < 0)
1555
+ const to = this.reading(r, c, dir);
1556
+ if (to.r < 0)
1483
1557
  return false;
1484
- if (nr >= rows) {
1558
+ if (to.r >= rowCount(props)) {
1485
1559
  if (rowEmpty(props, r))
1486
1560
  return false;
1487
- this.write(insertRow(props, r), { r: nr, c: 0 });
1561
+ this.write(insertRow(props, r), to);
1488
1562
  }
1489
1563
  else
1490
- this.focusCell(nr, nc);
1564
+ this.focusCell(to.r, to.c);
1491
1565
  return true;
1492
1566
  }
1493
1567
  }
1568
+ /** The table island mounted at `pos` in `view`, which a caret can step into; `undefined`
1569
+ * for any other node, or where no view is mounted. */
1570
+ export function tableAt(view, pos) {
1571
+ const dom = view.nodeDOM(pos);
1572
+ return dom ? mounted.get(dom) : undefined;
1573
+ }
1494
1574
  /** The `island_block` node view: a table island's editing surface, and the literal
1495
1575
  * placeholder for every other island type. */
1496
1576
  export function tableNodeView(deps) {