@quillmark/svelte 0.20.0 → 0.21.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 (46) hide show
  1. package/THEMING.md +1 -1
  2. package/dist/core/codec/atoms.js +25 -10
  3. package/dist/core/codec/blocks.d.ts +5 -5
  4. package/dist/core/codec/blocks.js +23 -6
  5. package/dist/core/codec/code.d.ts +3 -3
  6. package/dist/core/codec/code.js +51 -21
  7. package/dist/core/codec/field.d.ts +9 -7
  8. package/dist/core/codec/field.js +22 -11
  9. package/dist/core/codec/inputrules.d.ts +4 -0
  10. package/dist/core/codec/inputrules.js +43 -5
  11. package/dist/core/codec/link-paste.d.ts +11 -0
  12. package/dist/core/codec/link-paste.js +32 -0
  13. package/dist/core/codec/lists.d.ts +2 -2
  14. package/dist/core/codec/lists.js +66 -26
  15. package/dist/core/codec/prose.css +71 -43
  16. package/dist/core/codec/schema.js +55 -11
  17. package/dist/core/codec/slash.js +0 -0
  18. package/dist/core/codec/table-view.d.ts +5 -0
  19. package/dist/core/codec/table-view.js +232 -103
  20. package/dist/core/rungs.d.ts +6 -0
  21. package/dist/core/rungs.js +11 -0
  22. package/dist/core/theme.css +24 -12
  23. package/dist/visual/ArrayField.svelte +168 -48
  24. package/dist/visual/BooleanField.svelte +12 -1
  25. package/dist/visual/Card.svelte +32 -20
  26. package/dist/visual/Card.svelte.d.ts +1 -0
  27. package/dist/visual/CardControls.svelte +15 -15
  28. package/dist/visual/DateField.svelte +93 -64
  29. package/dist/visual/EnumField.svelte +9 -4
  30. package/dist/visual/FieldHint.svelte +22 -5
  31. package/dist/visual/FieldLabel.svelte +3 -3
  32. package/dist/visual/FormatPopover.svelte +25 -9
  33. package/dist/visual/MatrixField.svelte +10 -7
  34. package/dist/visual/ObjectField.svelte +2 -4
  35. package/dist/visual/TipsCard.svelte +2 -5
  36. package/dist/visual/VisualEditorInner.svelte +108 -26
  37. package/dist/visual/controls.css +2 -1
  38. package/dist/visual/icons/Icon.svelte +3 -2
  39. package/dist/visual/icons/Icon.svelte.d.ts +3 -2
  40. package/dist/visual/icons/nodes.d.ts +25 -0
  41. package/dist/visual/icons/nodes.js +8 -0
  42. package/dist/visual/motion.d.ts +25 -3
  43. package/dist/visual/motion.js +58 -9
  44. package/dist/visual/strings.d.ts +6 -5
  45. package/dist/visual/strings.js +3 -3
  46. package/package.json +8 -8
@@ -15,16 +15,18 @@
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, the held first row's
19
+ // header toggle, or a sweep's cell alignment. Every control is absolutely positioned out
20
+ // of the grid, so none is in a row or a column of it; `codec/prose.css` draws the band,
21
+ // the clusters and the 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';
29
31
  import { breakKeymap } from './breaks.js';
30
32
  import { cellSchema } from './schema.js';
@@ -158,10 +160,16 @@ function chromeButton(className, label, run) {
158
160
  });
159
161
  return btn;
160
162
  }
161
- /** A cell's plugin stack: marks and the markdown shorthands, and nothing that
162
- * belongs to the field (history, anchors, the ghost). */
163
+ /** A cell's plugin stack: marks, the markdown shorthands and a pasted URL's link, and
164
+ * nothing that belongs to the field (history, anchors, the ghost). */
163
165
  function cellPlugins(keys) {
164
- return [inputRulesPlugin(cellSchema), keymap(keys), keymap(baseKeymap)];
166
+ return [
167
+ inputRulesPlugin(cellSchema),
168
+ undoRuleKeymap(),
169
+ linkPastePlugin(cellSchema.marks.link),
170
+ keymap(keys),
171
+ keymap(baseKeymap)
172
+ ];
165
173
  }
166
174
  /** The controls, each of which answers for its own press. Spelled apart from
167
175
  * {@link owned} because the pointer router needs them before it needs the cells: a grip
@@ -181,14 +189,19 @@ const within = (rect, p) => p.x >= rect.left && p.x <= rect.right && p.y >= rect
181
189
  function distance(rect, p) {
182
190
  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
191
  }
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. */
192
+ const sameCells = (a, b) => a.r0 === b.r0 && a.c0 === b.c0 && a.r1 === b.r1 && a.c1 === b.c1 && a.swept === b.swept;
193
+ /** The block two corners span, in either order: what a sweep holds. */
186
194
  const spanCells = (a, b) => ({
187
195
  r0: Math.min(a.r, b.r),
188
196
  c0: Math.min(a.c, b.c),
189
197
  r1: Math.max(a.r, b.r),
190
- c1: Math.max(a.c, b.c)
198
+ c1: Math.max(a.c, b.c),
199
+ swept: true
191
200
  });
201
+ /** Each mounted island drawing a table, by its `dom`, which is what the outer view hands
202
+ * back for a position: how a key in the field reaches into a table ({@link tableAt}). A
203
+ * placeholder is absent, so it answers as the atom it draws. */
204
+ const mounted = new WeakMap();
192
205
  class TableIslandView {
193
206
  node;
194
207
  outer;
@@ -225,6 +238,8 @@ class TableIslandView {
225
238
  resize;
226
239
  /** A drag's trailing `click`, which would otherwise re-select the moved line. */
227
240
  suppressClick = false;
241
+ /** The grips of the row and column under the pointer, drawn up. */
242
+ near = [];
228
243
  constructor(node, outer, getPos, deps) {
229
244
  this.node = node;
230
245
  this.outer = outer;
@@ -234,6 +249,8 @@ class TableIslandView {
234
249
  this.dom.setAttribute('data-qm-island', node.attrs.islandType);
235
250
  this.dom.setAttribute('data-qm-island-id', node.attrs.id);
236
251
  this.dom.addEventListener('mousedown', this.onPointerDown);
252
+ this.dom.addEventListener('mouseover', this.onHover);
253
+ this.dom.addEventListener('mouseleave', this.onLeave);
237
254
  this.render();
238
255
  }
239
256
  // ── PM's NodeView contract ────────────────────────────────────────────────
@@ -296,6 +313,8 @@ class TableIslandView {
296
313
  }
297
314
  destroy() {
298
315
  this.dom.removeEventListener('mousedown', this.onPointerDown);
316
+ this.dom.removeEventListener('mouseover', this.onHover);
317
+ this.dom.removeEventListener('mouseleave', this.onLeave);
299
318
  this.endDrag();
300
319
  this.endSweep();
301
320
  this.endResize();
@@ -355,6 +374,31 @@ class TableIslandView {
355
374
  else
356
375
  this.caretBeside(point);
357
376
  };
377
+ /** A cell under the pointer draws its row's and its column's grips, so a line's
378
+ * handle is found from the line rather than only from the band beside it. A control
379
+ * under the pointer is its own reveal, and draws no other. */
380
+ onHover = (event) => {
381
+ const target = event.target;
382
+ const box = target?.closest?.(CONTROLS) ? null : target?.closest?.('.qm-table-cell');
383
+ const cell = box ? this.cells.find((m) => m.box === box) : undefined;
384
+ const next = cell
385
+ ? [
386
+ this.grips.get(lineKey({ axis: 'row', index: cell.r }))?.grip,
387
+ this.grips.get(lineKey({ axis: 'column', index: cell.c }))?.grip
388
+ ].filter((grip) => !!grip)
389
+ : [];
390
+ for (const grip of this.near)
391
+ if (!next.includes(grip))
392
+ grip.removeAttribute('data-near');
393
+ for (const grip of next)
394
+ grip.setAttribute('data-near', '');
395
+ this.near = next;
396
+ };
397
+ onLeave = () => {
398
+ for (const grip of this.near)
399
+ grip.removeAttribute('data-near');
400
+ this.near = [];
401
+ };
358
402
  /** The cell a press inside the frame belongs to: the nearest by rect, which for a
359
403
  * press on a border or a cell's own padding is the cell it is against.
360
404
  *
@@ -399,18 +443,32 @@ class TableIslandView {
399
443
  * leaf mounted without that plugin then answers "no gap" instead of dispatching
400
444
  * a selection nothing draws. */
401
445
  caretBeside(point) {
446
+ const box = this.dom.getBoundingClientRect();
447
+ this.leave(point.y < (box.top + box.bottom) / 2 ? -1 : 1);
448
+ }
449
+ /** Land the document's caret on one side of the island, by
450
+ * {@link TableIslandView.caretBeside}'s rule; false where that side holds no caret. */
451
+ leave(side) {
402
452
  const pos = this.getPos();
403
453
  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);
454
+ return false;
455
+ const $at = this.outer.state.doc.resolve(side < 0 ? pos : pos + this.node.nodeSize);
408
456
  const selection = this.outer.someProp('createSelectionBetween', (f) => f(this.outer, $at, $at)) ??
409
- Selection.findFrom($at, before ? -1 : 1, true);
457
+ Selection.findFrom($at, side, true);
410
458
  if (!selection)
411
- return;
459
+ return false;
412
460
  this.outer.focus();
413
461
  this.outer.dispatch(this.outer.state.tr.setSelection(selection));
462
+ return true;
463
+ }
464
+ /** Land the caret in the table from the block beside it: its last cell's end from
465
+ * below, its first cell's start from above. */
466
+ enter(side) {
467
+ const props = this.props();
468
+ if (side < 0)
469
+ this.focusCell(rowCount(props) - 1, columnCount(props) - 1, 'end');
470
+ else
471
+ this.focusCell(0, 0, 'start');
414
472
  }
415
473
  /** The island as the selection: the Escape that climbs out of a cell or a held
416
474
  * rectangle, which is the whole of what selects it. Backspace there deletes the
@@ -497,8 +555,8 @@ class TableIslandView {
497
555
  * revalidates the whole table. */
498
556
  lineCells(line, props = this.props()) {
499
557
  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 };
558
+ ? { r0: line.index, c0: 0, r1: line.index, c1: columnCount(props) - 1, swept: false }
559
+ : { r0: 0, c0: line.index, r1: rowCount(props) - 1, c1: line.index, swept: false };
502
560
  }
503
561
  /** The index space an axis allows, which is the whole of it on both. The header is
504
562
  * row 0 rather than a line above the floor: a walk reaches it, a drag lands on it,
@@ -528,14 +586,12 @@ class TableIslandView {
528
586
  else if (held?.c !== line.index)
529
587
  this.focusCell(held?.r ?? 0, line.index);
530
588
  }
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. */
589
+ /** Which line the held rectangle names, on one axis: a row spanning every column, a
590
+ * column spanning every row, and nothing for a block that spans neither or for cells
591
+ * a sweep holds, which are text however many lines they cross. */
536
592
  lineOn(axis) {
537
593
  const held = this.selected;
538
- if (!held)
594
+ if (!held || held.swept)
539
595
  return undefined;
540
596
  const props = this.props();
541
597
  if (axis === 'row')
@@ -554,18 +610,24 @@ class TableIslandView {
554
610
  // One subject at a time: a cell selection retires an island one, or the surface
555
611
  // paints a washed row inside an outlined table and the next Backspace has two
556
612
  // 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
- }
613
+ this.retireIsland();
566
614
  this.selected = cells;
567
615
  this.paintSelection();
568
616
  }
617
+ /** Move the outer selection off this island when it holds it: a caret or a rectangle
618
+ * inside the grid is the subject from then on, and an outline left on the table says
619
+ * the next Backspace deletes it. */
620
+ retireIsland() {
621
+ const pos = this.getPos();
622
+ const outer = this.outer.state.selection;
623
+ if (pos == null || !(outer instanceof NodeSelection) || outer.from !== pos)
624
+ return;
625
+ const $at = this.outer.state.doc.resolve(pos);
626
+ const beside = this.outer.someProp('createSelectionBetween', (f) => f(this.outer, $at, $at)) ??
627
+ Selection.findFrom($at, -1, true);
628
+ if (beside)
629
+ this.outer.dispatch(this.outer.state.tr.setSelection(beside));
630
+ }
569
631
  clearSelection() {
570
632
  if (!this.selected)
571
633
  return;
@@ -593,19 +655,21 @@ class TableIslandView {
593
655
  cell.box.toggleAttribute('data-selected', !!held && cell.r >= held.r0 && cell.r <= held.r1 && cell.c >= held.c0 && cell.c <= held.c1);
594
656
  }
595
657
  /**
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.
658
+ * What Backspace means over the selection (CODEC §"The table island"). Swept cells
659
+ * clear and keep their lines. Named lines go, read off their extent: every rank on
660
+ * both axes is the table, which goes as the island, and on a one-column or one-row
661
+ * table a single grip draws that.
603
662
  */
604
663
  deleteSelection() {
605
664
  const held = this.selected;
606
665
  if (!held)
607
666
  return;
608
667
  const props = this.props();
668
+ if (held.swept) {
669
+ if (allCells(props, held).some((cell) => cell.text !== ''))
670
+ this.write(clearCells(props, held.r0, held.c0, held.r1, held.c1));
671
+ return;
672
+ }
609
673
  const wide = held.c0 === 0 && held.c1 === columnCount(props) - 1;
610
674
  const tall = held.r0 === 0 && held.r1 === rowCount(props) - 1;
611
675
  if (tall && wide)
@@ -663,25 +727,27 @@ class TableIslandView {
663
727
  this.write(line.axis === 'row' ? moveRow(props, line.index, by) : moveColumn(props, line.index, by));
664
728
  this.selectLine({ axis: line.axis, index: to });
665
729
  }
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. */
730
+ /** Set column `c`'s alignment, clear its cells' own, which would outrank it, and keep
731
+ * the column held: a changed alignment rebuilds the views, which retires the selection
732
+ * as a move's rebuild does. One `set` for both, and a column already at `to` with no
733
+ * cell of its own writes nothing. */
669
734
  align(c, to) {
670
735
  const props = this.props();
671
- if (props.aligns[c] !== to)
672
- this.write(setAlign(props, c, to));
736
+ const last = rowCount(props) - 1;
737
+ const own = Array.from({ length: last + 1 }, (_, r) => cellAt(props, r, c)).some((cell) => cell.align !== undefined);
738
+ if (props.aligns[c] !== to || own)
739
+ this.write(setCellLayout(setAlign(props, c, to), 0, c, last, c, 'align', undefined));
673
740
  this.selectLine({ axis: 'column', index: c });
674
741
  }
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}). */
742
+ /** The swept cells' cluster: up over the first cell of a sweep, each toggle pressed
743
+ * where every cell in it holds it. It floats over the cell under that one, as the
744
+ * column's floats over the first row, and is out of the tab order for the reason a
745
+ * grip is. A named line raises none: a grip is pressed to move or delete its line. */
680
746
  paintCellCluster(props) {
681
747
  this.cellCluster?.remove();
682
748
  this.cellCluster = undefined;
683
749
  const held = this.selected;
684
- if (!held || this.lineOn('column') !== undefined)
750
+ if (!held?.swept)
685
751
  return;
686
752
  const first = this.cells.find((m) => m.r === held.r0 && m.c === held.c0);
687
753
  if (!first)
@@ -719,11 +785,11 @@ class TableIslandView {
719
785
  first.box.appendChild(cluster);
720
786
  this.cellCluster = cluster;
721
787
  }
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 });
788
+ /** Lay every held cell at `value`, and clear the key from them where every one already
789
+ * holds it: the press that set an alignment takes it back to the column's. One
790
+ * `set`, and the rectangle stays held. */
791
+ layCells(key, value) {
792
+ const held = this.selected;
727
793
  if (!held)
728
794
  return;
729
795
  const props = this.props();
@@ -1013,11 +1079,16 @@ class TableIslandView {
1013
1079
  this.endResize();
1014
1080
  this.teardownCells();
1015
1081
  this.grips.clear();
1082
+ this.near = [];
1016
1083
  this.clusters = [];
1017
1084
  this.headerCluster = undefined;
1018
1085
  this.dom.textContent = '';
1019
1086
  const props = tablePropsOfNode(this.node);
1020
1087
  this.rendered = props;
1088
+ if (props)
1089
+ mounted.set(this.dom, this);
1090
+ else
1091
+ mounted.delete(this.dom);
1021
1092
  if (!props) {
1022
1093
  // Any other island type: the literal placeholder `toDOM` draws (islands.ts).
1023
1094
  this.dom.appendChild(document.createTextNode(`[${this.node.attrs.islandType || 'island'}]`));
@@ -1146,11 +1217,11 @@ class TableIslandView {
1146
1217
  return btn;
1147
1218
  }
1148
1219
  /** 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
1220
+ * holds. Up only while this column is the named line
1150
1221
  * ({@link TableIslandView.paintSelection}), and absent rather than transparent the
1151
1222
  * rest of the time: it floats over the first row, and a box hit-tested there at rest
1152
1223
  * 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}). */
1224
+ * keyboard twins being Shift+Left and Shift+Right (§{@link TableIslandView.cellKeys}). */
1154
1225
  alignCluster(c, held, s) {
1155
1226
  const cluster = el('div', 'qm-table-align');
1156
1227
  cluster.setAttribute('role', 'group');
@@ -1253,6 +1324,7 @@ class TableIslandView {
1253
1324
  handleDOMEvents: {
1254
1325
  focus: () => {
1255
1326
  this.clearSelection();
1327
+ this.retireIsland();
1256
1328
  this.deps.onCellFocus();
1257
1329
  return false;
1258
1330
  }
@@ -1297,9 +1369,9 @@ class TableIslandView {
1297
1369
  if (focus)
1298
1370
  this.focusCell(focus.r, focus.c);
1299
1371
  }
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) {
1372
+ /** Land the caret at the end of a cell, or at its start, clamped into the rectangle:
1373
+ * where a row or column op puts it, since the op rebuilt the views the caret was in. */
1374
+ focusCell(r, c, edge = 'end') {
1303
1375
  const props = this.props();
1304
1376
  const row = Math.max(0, Math.min(r, rowCount(props) - 1));
1305
1377
  const col = Math.max(0, Math.min(c, columnCount(props) - 1));
@@ -1307,12 +1379,24 @@ class TableIslandView {
1307
1379
  if (!mounted)
1308
1380
  return;
1309
1381
  const { view } = mounted;
1382
+ const at = edge === 'start' ? Selection.atStart(view.state.doc) : Selection.atEnd(view.state.doc);
1310
1383
  // Flagged, for the reason the leaf's own landing is (`field.ts`, `setCaret`): PM
1311
1384
  // focuses with `preventScroll`, so an unflagged dispatch lands the caret where no
1312
1385
  // scroller has moved to — a Tab past the right edge of the horizontal scroller a
1313
1386
  // wide table lives in, or the row an Enter on the last one appends below the fold.
1314
1387
  view.focus();
1315
- view.dispatch(view.state.tr.setSelection(Selection.atEnd(view.state.doc)).scrollIntoView());
1388
+ view.dispatch(view.state.tr.setSelection(at).scrollIntoView());
1389
+ }
1390
+ /** The cell beside `(r, c)` in reading order, a row's end wrapping to the next row's
1391
+ * start; past the first or last cell its row is outside the table. */
1392
+ reading(r, c, dir) {
1393
+ const cols = columnCount(this.props());
1394
+ const next = c + dir;
1395
+ if (next >= cols)
1396
+ return { r: r + 1, c: 0 };
1397
+ if (next < 0)
1398
+ return { r: r - 1, c: cols - 1 };
1399
+ return { r, c: next };
1316
1400
  }
1317
1401
  /**
1318
1402
  * A cell's keys. Traversal is the island's link in the leaf's chain
@@ -1330,22 +1414,35 @@ class TableIslandView {
1330
1414
  marks['Mod-i'] = toggleMark(cellSchema.marks.em);
1331
1415
  if (cellSchema.marks.underline)
1332
1416
  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.
1417
+ // The arrows are the grid's own walk, each leaving a cell only from the text's edge
1418
+ // on its side. Up and down reach the cell above or below, `focusCell` clamping at
1419
+ // the top and bottom. Left and right reach the neighbour in reading order, landing
1420
+ // at the edge they entered by and stopping at the first and last cells. No arrow
1421
+ // grows the table, growth being Tab's and Enter's: on a caret key it would make a
1422
+ // walk a growth affordance.
1423
+ const caretAtEdge = (view, dir) => {
1424
+ const { selection } = view?.state ?? {};
1425
+ return (!!view && selection instanceof TextSelection && selection.empty && view.endOfTextblock(dir));
1426
+ };
1338
1427
  const walk = (dir) => {
1339
1428
  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))
1429
+ if (!caretAtEdge(view, dir))
1344
1430
  return false;
1345
1431
  this.focusCell(dir === 'up' ? r - 1 : r + 1, c);
1346
1432
  return true;
1347
1433
  };
1348
1434
  };
1435
+ const cross = (dir) => {
1436
+ return (_state, _dispatch, view) => {
1437
+ if (!caretAtEdge(view, dir < 0 ? 'left' : 'right'))
1438
+ return false;
1439
+ const to = this.reading(r, c, dir);
1440
+ if (to.r < 0 || to.r >= rowCount(this.props()))
1441
+ return false;
1442
+ this.focusCell(to.r, to.c, dir < 0 ? 'end' : 'start');
1443
+ return true;
1444
+ };
1445
+ };
1349
1446
  // A block selection outranks the caret it was swept from: the origin cell still
1350
1447
  // holds the focus, so its own view is where the block's Backspace lands. Declining
1351
1448
  // when no block is held is what leaves an ordinary Backspace to `baseKeymap`.
@@ -1357,17 +1454,22 @@ class TableIslandView {
1357
1454
  };
1358
1455
  // The line verbs, over the held rectangle and nothing else: with none held every
1359
1456
  // 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.
1457
+ // keys a cell is already typing under. An arrow over swept cells lets go of them,
1458
+ // leaving the caret where it is, as an arrow collapses a text selection. Over named
1459
+ // lines it names the line on its own axis through the caret — the one already held
1460
+ // steps to its neighbour and carries the caret, and any other turns into it, which
1461
+ // is how a row becomes the column the caret is in. Alt moves the line instead, and
1462
+ // over a rectangle that is not one it does nothing but keep the key: while a
1463
+ // rectangle is held every arrow is its own, and an Alt+arrow handed back is the
1464
+ // browser's Back on two platforms, mid-gesture.
1367
1465
  const line = (axis, by, move) => {
1368
1466
  return () => {
1369
1467
  if (!this.selected)
1370
1468
  return false;
1469
+ if (this.selected.swept && !move) {
1470
+ this.clearSelection();
1471
+ return true;
1472
+ }
1371
1473
  const on = this.lineOn(axis);
1372
1474
  if (move) {
1373
1475
  if (on !== undefined)
@@ -1399,6 +1501,34 @@ class TableIslandView {
1399
1501
  return true;
1400
1502
  };
1401
1503
  };
1504
+ // Select-all grows a rung a press: the cell's text, which the base keymap's
1505
+ // `selectAll` takes, then the caret's row, then every cell, then the document. Any
1506
+ // held rectangle short of every cell grows to every cell.
1507
+ const grow = (state) => {
1508
+ const props = this.props();
1509
+ const every = {
1510
+ r0: 0,
1511
+ c0: 0,
1512
+ r1: rowCount(props) - 1,
1513
+ c1: columnCount(props) - 1,
1514
+ swept: false
1515
+ };
1516
+ const held = this.selected;
1517
+ if (held && sameCells(held, every)) {
1518
+ this.clearSelection();
1519
+ this.outer.focus();
1520
+ return selectAll(this.outer.state, this.outer.dispatch);
1521
+ }
1522
+ if (held)
1523
+ this.select(every);
1524
+ else {
1525
+ const { from, to } = state.selection;
1526
+ if (from > Selection.atStart(state.doc).from || to < Selection.atEnd(state.doc).to)
1527
+ return false;
1528
+ this.selectLine({ axis: 'row', index: r });
1529
+ }
1530
+ return true;
1531
+ };
1402
1532
  return {
1403
1533
  ...marks,
1404
1534
  ...breakKeymap(cellSchema),
@@ -1411,17 +1541,15 @@ class TableIslandView {
1411
1541
  Delete: erase,
1412
1542
  ArrowUp: chainCommands(line('row', -1, false), walk('up')),
1413
1543
  ArrowDown: chainCommands(line('row', 1, false), walk('down')),
1414
- ArrowLeft: line('column', -1, false),
1415
- ArrowRight: line('column', 1, false),
1544
+ ArrowLeft: chainCommands(line('column', -1, false), cross(-1)),
1545
+ ArrowRight: chainCommands(line('column', 1, false), cross(1)),
1416
1546
  'Alt-ArrowUp': line('row', -1, true),
1417
1547
  'Alt-ArrowDown': line('row', 1, true),
1418
1548
  'Alt-ArrowLeft': line('column', -1, true),
1419
1549
  'Alt-ArrowRight': line('column', 1, true),
1420
1550
  'Shift-ArrowLeft': slide(-1),
1421
1551
  '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),
1552
+ 'Mod-a': grow,
1425
1553
  Tab: () => this.step(r, c, 1),
1426
1554
  'Shift-Tab': () => this.step(r, c, -1),
1427
1555
  Enter: () => {
@@ -1431,11 +1559,17 @@ class TableIslandView {
1431
1559
  this.clearSelection();
1432
1560
  return true;
1433
1561
  }
1562
+ // Past the last row it appends one, and on an empty trailing row it refuses the
1563
+ // row on offer, as Tab's decline does and an empty item's Enter leaves a list:
1564
+ // the row goes and the caret lands past the table. Row 1 is never the one
1565
+ // refused, so the table keeps a row under its first.
1434
1566
  const props = this.props();
1435
- if (r === rowCount(props) - 1)
1436
- this.write(insertRow(props, r), { r: r + 1, c });
1437
- else
1567
+ if (r < rowCount(props) - 1)
1438
1568
  this.focusCell(r + 1, c);
1569
+ else if (r > 1 && rowEmpty(props, r) && this.leave(1))
1570
+ this.write(deleteRow(props, r));
1571
+ else
1572
+ this.write(insertRow(props, r), { r: r + 1, c });
1439
1573
  return true;
1440
1574
  },
1441
1575
  // Escape climbs a rung a press: the caret's own row, then the island. The row
@@ -1467,30 +1601,25 @@ class TableIslandView {
1467
1601
  */
1468
1602
  step(r, c, dir) {
1469
1603
  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)
1604
+ const to = this.reading(r, c, dir);
1605
+ if (to.r < 0)
1483
1606
  return false;
1484
- if (nr >= rows) {
1607
+ if (to.r >= rowCount(props)) {
1485
1608
  if (rowEmpty(props, r))
1486
1609
  return false;
1487
- this.write(insertRow(props, r), { r: nr, c: 0 });
1610
+ this.write(insertRow(props, r), to);
1488
1611
  }
1489
1612
  else
1490
- this.focusCell(nr, nc);
1613
+ this.focusCell(to.r, to.c);
1491
1614
  return true;
1492
1615
  }
1493
1616
  }
1617
+ /** The table island mounted at `pos` in `view`, which a caret can step into; `undefined`
1618
+ * for any other node, or where no view is mounted. */
1619
+ export function tableAt(view, pos) {
1620
+ const dom = view.nodeDOM(pos);
1621
+ return dom ? mounted.get(dom) : undefined;
1622
+ }
1494
1623
  /** The `island_block` node view: a table island's editing surface, and the literal
1495
1624
  * placeholder for every other island type. */
1496
1625
  export function tableNodeView(deps) {
@@ -6,3 +6,9 @@ export declare function rungStyle(node: Element): CSSStyleDeclaration;
6
6
  * empty). No rung, no motion: a fallback here would be the scale restated in the one
7
7
  * place `check:style` cannot read. */
8
8
  export declare function rungMs(style: CSSStyleDeclaration, rung: string): number | undefined;
9
+ /** `steps` of the space rung off `node`'s root, in px: a floating surface's offset from
10
+ * its anchor. `--_qm-space` is registered, so it computes to px where the rungs derived
11
+ * from it compute to their expressions; hence the multiple rather than a read of
12
+ * `--_qm-space-2`. `undefined` where the derivation is out of reach, as for
13
+ * {@link rungMs}. */
14
+ export declare function spacePx(node: Element | undefined, steps?: number): number | undefined;
@@ -22,3 +22,14 @@ export function rungMs(style, rung) {
22
22
  return undefined;
23
23
  return raw.endsWith('ms') ? n : n * 1000;
24
24
  }
25
+ /** `steps` of the space rung off `node`'s root, in px: a floating surface's offset from
26
+ * its anchor. `--_qm-space` is registered, so it computes to px where the rungs derived
27
+ * from it compute to their expressions; hence the multiple rather than a read of
28
+ * `--_qm-space-2`. `undefined` where the derivation is out of reach, as for
29
+ * {@link rungMs}. */
30
+ export function spacePx(node, steps = 1) {
31
+ if (!node)
32
+ return undefined;
33
+ const n = Number.parseFloat(rungStyle(node).getPropertyValue('--_qm-space'));
34
+ return Number.isFinite(n) ? n * steps : undefined;
35
+ }