@quillmark/svelte 0.21.0 → 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 (67) hide show
  1. package/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/dist/core/address.d.ts +6 -1
  4. package/dist/core/address.js +12 -3
  5. package/dist/core/bloom.d.ts +9 -4
  6. package/dist/core/bloom.js +18 -49
  7. package/dist/core/codec/atoms.d.ts +1 -1
  8. package/dist/core/codec/atoms.js +1 -1
  9. package/dist/core/codec/blocks.js +4 -2
  10. package/dist/core/codec/code.d.ts +1 -1
  11. package/dist/core/codec/code.js +1 -1
  12. package/dist/core/codec/decode.d.ts +5 -5
  13. package/dist/core/codec/decode.js +18 -12
  14. package/dist/core/codec/encode.js +6 -1
  15. package/dist/core/codec/field.d.ts +15 -10
  16. package/dist/core/codec/field.js +46 -17
  17. package/dist/core/codec/index.d.ts +1 -1
  18. package/dist/core/codec/index.js +2 -2
  19. package/dist/core/codec/inputrules.js +1 -1
  20. package/dist/core/codec/lists.d.ts +3 -2
  21. package/dist/core/codec/lists.js +21 -5
  22. package/dist/core/codec/nesting.d.ts +20 -0
  23. package/dist/core/codec/nesting.js +62 -0
  24. package/dist/core/codec/prose.css +74 -20
  25. package/dist/core/codec/schema.d.ts +2 -2
  26. package/dist/core/codec/schema.js +16 -7
  27. package/dist/core/codec/table-copy.d.ts +17 -0
  28. package/dist/core/codec/table-copy.js +84 -0
  29. package/dist/core/codec/table-view.d.ts +1 -7
  30. package/dist/core/codec/table-view.js +70 -119
  31. package/dist/core/codec/table.d.ts +2 -9
  32. package/dist/core/codec/table.js +10 -19
  33. package/dist/core/codec/tasks.d.ts +19 -0
  34. package/dist/core/codec/tasks.js +140 -0
  35. package/dist/core/errors.d.ts +2 -2
  36. package/dist/visual/ArrayField.svelte +101 -63
  37. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  38. package/dist/visual/Card.svelte +9 -3
  39. package/dist/visual/Card.svelte.d.ts +2 -2
  40. package/dist/visual/Field.svelte +20 -16
  41. package/dist/visual/Field.svelte.d.ts +4 -2
  42. package/dist/visual/FieldLabel.svelte +5 -33
  43. package/dist/visual/FieldLabel.svelte.d.ts +0 -2
  44. package/dist/visual/FormatPopover.svelte +54 -73
  45. package/dist/visual/MatrixField.svelte +18 -12
  46. package/dist/visual/NumberField.svelte +32 -3
  47. package/dist/visual/ObjectField.svelte +38 -20
  48. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  49. package/dist/visual/ProseField.svelte +10 -13
  50. package/dist/visual/ProseField.svelte.d.ts +5 -2
  51. package/dist/visual/ProseValue.svelte +25 -19
  52. package/dist/visual/ProseValue.svelte.d.ts +4 -4
  53. package/dist/visual/TextField.svelte +3 -2
  54. package/dist/visual/TipsCard.svelte +5 -2
  55. package/dist/visual/VariantField.svelte +1 -1
  56. package/dist/visual/VariantField.svelte.d.ts +1 -1
  57. package/dist/visual/VisualEditorInner.svelte +13 -13
  58. package/dist/visual/controls.css +19 -1
  59. package/dist/visual/icons/nodes.d.ts +0 -21
  60. package/dist/visual/icons/nodes.js +1 -7
  61. package/dist/visual/leaves.d.ts +12 -9
  62. package/dist/visual/leaves.js +19 -0
  63. package/dist/visual/strings.d.ts +10 -5
  64. package/dist/visual/strings.js +16 -9
  65. package/dist/visual/structure.d.ts +21 -31
  66. package/dist/visual/structure.js +54 -51
  67. package/package.json +3 -3
@@ -15,10 +15,10 @@
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 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.
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
22
  import { baseKeymap, chainCommands, selectAll, toggleMark } from 'prosemirror-commands';
23
23
  import { redo, undo } from 'prosemirror-history';
24
24
  import { keymap } from 'prosemirror-keymap';
@@ -28,9 +28,10 @@ import { decode } from './decode.js';
28
28
  import { inputRulesPlugin, undoRuleKeymap } from './inputrules.js';
29
29
  import { linkPastePlugin } from './link-paste.js';
30
30
  import { tablePropsOfNode } from './islands.js';
31
+ import { tableElement, tableText } from './table-copy.js';
31
32
  import { breakKeymap } from './breaks.js';
32
33
  import { cellSchema } from './schema.js';
33
- 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';
34
35
  /**
35
36
  * The package's English for the island chrome. It lives here, beside the surface
36
37
  * that draws it, rather than in the visual tier's table: the codec mounts this
@@ -52,11 +53,7 @@ export const DEFAULT_TABLE_STRINGS = {
52
53
  tableAlignLeft: 'Align left',
53
54
  tableAlignCenter: 'Align center',
54
55
  tableAlignRight: 'Align right',
55
- tableHeaderToggle: 'Header row',
56
- tableAlignTop: 'Align top',
57
- tableAlignMiddle: 'Align middle',
58
- tableAlignBottom: 'Align bottom',
59
- tableCellAlignment: 'Cell alignment'
56
+ tableHeaderToggle: 'Header row'
60
57
  };
61
58
  /** The grip's glyph, as the path data a DOM node can carry — this chrome's own set rather
62
59
  * than `visual/icons/nodes.ts`, this being the one place chrome is built without Svelte,
@@ -81,28 +78,6 @@ const HEADER_GLYPH = [
81
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',
82
79
  'M3 9h18'
83
80
  ];
84
- /** The held cells' vertical toggles' glyphs, off the same release. */
85
- const VALIGN_GLYPH = {
86
- top: [
87
- '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',
88
- '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',
89
- 'M22 2H2'
90
- ],
91
- middle: [
92
- 'M2 12h20',
93
- 'M10 16v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4',
94
- 'M10 8V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4',
95
- 'M20 16v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-1',
96
- 'M14 8V7c0-1.1.9-2 2-2h2a2 2 0 0 1 2 2v1'
97
- ],
98
- bottom: [
99
- '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',
100
- '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',
101
- 'M22 22H2'
102
- ]
103
- };
104
- /** The vertical alignments a cell takes, in the cluster's order. */
105
- const VALIGNS = ['top', 'middle', 'bottom'];
106
81
  /** Draw a cell's alignment on its box: its own where it holds one, its column's
107
82
  * otherwise, and the vertical one where it holds one. */
108
83
  function layCell(box, props, r, c) {
@@ -202,6 +177,13 @@ const spanCells = (a, b) => ({
202
177
  * back for a position: how a key in the field reaches into a table ({@link tableAt}). A
203
178
  * placeholder is absent, so it answers as the atom it draws. */
204
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();
205
187
  class TableIslandView {
206
188
  node;
207
189
  outer;
@@ -223,9 +205,6 @@ class TableIslandView {
223
205
  clusters = [];
224
206
  /** The first row's header toggle, up while that row is held. */
225
207
  headerCluster;
226
- /** The held cells' cluster, up over the first cell of a held rectangle that is no
227
- * column, and built for each paint since what it presses is the cells'. */
228
- cellCluster;
229
208
  /** The grid's own box, and the containing block every out-of-flow control is placed
230
209
  * against. Not the scroller: an absolute inside a scroll container is placed
231
210
  * against a padding box the scroll then slides out from under, so a control at the
@@ -251,6 +230,7 @@ class TableIslandView {
251
230
  this.dom.addEventListener('mousedown', this.onPointerDown);
252
231
  this.dom.addEventListener('mouseover', this.onHover);
253
232
  this.dom.addEventListener('mouseleave', this.onLeave);
233
+ this.dom.addEventListener('copy', this.onCopy, true);
254
234
  this.render();
255
235
  }
256
236
  // ── PM's NodeView contract ────────────────────────────────────────────────
@@ -273,6 +253,7 @@ class TableIslandView {
273
253
  // transaction, which is every keystroke in the table, and projecting each cell
274
254
  // costs a `mapMarks` across the WASM boundary wherever one holds an anchor.
275
255
  this.rendered = props;
256
+ const departure = departures.get(props);
276
257
  for (const mounted of this.cells) {
277
258
  const stored = cellAt(props, mounted.r, mounted.c);
278
259
  if (cellEqual(stored, mounted.shown))
@@ -284,13 +265,14 @@ class TableIslandView {
284
265
  });
285
266
  mounted.view.updateState(fresh);
286
267
  mounted.shown = stored;
287
- // Best-effort caret continuity, the rule a field's own re-hydrate takes: keep
288
- // the offset, clamped into the text that is there now. A fresh state resolves
289
- // its selection to the start of the cell, so an undo would otherwise put the
290
- // 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.
291
271
  const at = Math.min(head, fresh.doc.content.size);
292
272
  mounted.view.dispatch(fresh.tr.setSelection(Selection.near(fresh.doc.resolve(at))));
293
273
  }
274
+ if (departure && this.focusedSeat())
275
+ this.focusCell(departure.r, departure.c, departure.head);
294
276
  // A cell's alignment is no part of its text, so it is restated on every box: the
295
277
  // reseed above compares text and marks alone.
296
278
  for (const mounted of this.cells)
@@ -315,6 +297,7 @@ class TableIslandView {
315
297
  this.dom.removeEventListener('mousedown', this.onPointerDown);
316
298
  this.dom.removeEventListener('mouseover', this.onHover);
317
299
  this.dom.removeEventListener('mouseleave', this.onLeave);
300
+ this.dom.removeEventListener('copy', this.onCopy, true);
318
301
  this.endDrag();
319
302
  this.endSweep();
320
303
  this.endResize();
@@ -399,6 +382,18 @@ class TableIslandView {
399
382
  grip.removeAttribute('data-near');
400
383
  this.near = [];
401
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
+ };
402
397
  /** The cell a press inside the frame belongs to: the nearest by rect, which for a
403
398
  * press on a border or a cell's own padding is the cell it is against.
404
399
  *
@@ -650,7 +645,6 @@ class TableIslandView {
650
645
  if (line.axis === 'row' && line.index === 0 && this.headerCluster)
651
646
  this.headerCluster.hidden = !named;
652
647
  }
653
- this.paintCellCluster(props);
654
648
  for (const cell of this.cells)
655
649
  cell.box.toggleAttribute('data-selected', !!held && cell.r >= held.r0 && cell.r <= held.r1 && cell.c >= held.c0 && cell.c <= held.c1);
656
650
  }
@@ -733,69 +727,11 @@ class TableIslandView {
733
727
  * cell of its own writes nothing. */
734
728
  align(c, to) {
735
729
  const props = this.props();
736
- const last = rowCount(props) - 1;
737
- const own = Array.from({ length: last + 1 }, (_, r) => cellAt(props, r, c)).some((cell) => cell.align !== undefined);
730
+ const own = Array.from({ length: rowCount(props) }, (_, r) => cellAt(props, r, c)).some((cell) => cell.align !== undefined);
738
731
  if (props.aligns[c] !== to || own)
739
- this.write(setCellLayout(setAlign(props, c, to), 0, c, last, c, 'align', undefined));
732
+ this.write(setAlign(props, c, to));
740
733
  this.selectLine({ axis: 'column', index: c });
741
734
  }
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. */
746
- paintCellCluster(props) {
747
- this.cellCluster?.remove();
748
- this.cellCluster = undefined;
749
- const held = this.selected;
750
- if (!held?.swept)
751
- return;
752
- const first = this.cells.find((m) => m.r === held.r0 && m.c === held.c0);
753
- if (!first)
754
- return;
755
- const s = this.deps.strings();
756
- const covered = allCells(props, held);
757
- const holds = (key, value) => covered.every((cell) => cell[key] === value);
758
- const cluster = el('div', 'qm-table-align');
759
- cluster.setAttribute('role', 'group');
760
- cluster.setAttribute('aria-label', s.tableCellAlignment);
761
- cluster.setAttribute('data-cells', '');
762
- cluster.addEventListener('mousedown', (e) => e.preventDefault());
763
- const toggle = (label, glyph, pressed, run) => {
764
- const btn = chromeButton('qm-table-align-option', label, run);
765
- btn.tabIndex = -1;
766
- btn.setAttribute('aria-pressed', String(pressed));
767
- btn.appendChild(svg(glyph, 2));
768
- cluster.appendChild(btn);
769
- return btn;
770
- };
771
- const names = {
772
- left: s.tableAlignLeft,
773
- center: s.tableAlignCenter,
774
- right: s.tableAlignRight
775
- };
776
- for (const align of SETTABLE)
777
- toggle(names[align], ALIGN_GLYPH[align], holds('align', align), () => this.layCells('align', align)).setAttribute('data-align', align);
778
- const vnames = {
779
- top: s.tableAlignTop,
780
- middle: s.tableAlignMiddle,
781
- bottom: s.tableAlignBottom
782
- };
783
- for (const valign of VALIGNS)
784
- toggle(vnames[valign], VALIGN_GLYPH[valign], holds('valign', valign), () => this.layCells('valign', valign)).setAttribute('data-valign', valign);
785
- first.box.appendChild(cluster);
786
- this.cellCluster = cluster;
787
- }
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;
793
- if (!held)
794
- return;
795
- const props = this.props();
796
- const every = allCells(props, held).every((cell) => cell[key] === value);
797
- this.write(setCellLayout(props, held.r0, held.c0, held.r1, held.c1, key, every ? undefined : value));
798
- }
799
735
  // ── Drag to reorder ───────────────────────────────────────────────────────
800
736
  /**
801
737
  * Press-and-drag a grip moves its line. The press still selects: the dead zone is
@@ -1084,6 +1020,7 @@ class TableIslandView {
1084
1020
  this.headerCluster = undefined;
1085
1021
  this.dom.textContent = '';
1086
1022
  const props = tablePropsOfNode(this.node);
1023
+ const departure = props && departures.get(props);
1087
1024
  this.rendered = props;
1088
1025
  if (props)
1089
1026
  mounted.set(this.dom, this);
@@ -1130,7 +1067,7 @@ class TableIslandView {
1130
1067
  scroller.appendChild(frame);
1131
1068
  this.dom.appendChild(scroller);
1132
1069
  this.paintSelection();
1133
- this.reseat(seat);
1070
+ this.reseat(seat, departure);
1134
1071
  }
1135
1072
  /**
1136
1073
  * Where the focus sits, in terms a rebuild can restore it by: the caret's cell by
@@ -1145,14 +1082,19 @@ class TableIslandView {
1145
1082
  */
1146
1083
  focusedSeat() {
1147
1084
  const held = this.cells.find((m) => m.view.hasFocus());
1148
- return held && { r: held.r, c: held.c };
1085
+ return held && { r: held.r, c: held.c, head: held.view.state.selection.head };
1149
1086
  }
1150
- /** Put the caret back where {@link TableIslandView.focusedSeat} found it, clamped by
1151
- * `focusCell` into the rectangle that is there now: the seat may be the cell the
1152
- * rebuild removed. A caller that has a landing of its own overrides this by running
1153
- * after. */
1154
- reseat(seat) {
1155
- 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
1156
1098
  this.focusCell(seat.r, seat.c);
1157
1099
  }
1158
1100
  /** One table row: its cells, each carrying whatever chrome hangs off it. */
@@ -1272,12 +1214,10 @@ class TableIslandView {
1272
1214
  * the pointer out past the last line is the claim — so its name carries the verb for
1273
1215
  * everything that does not read position.
1274
1216
  *
1275
- * In the tab order, where a grip is not: growth is what the line verbs do not carry,
1276
- * so the column bar is the keyboard's only route to a new column, and the row bar is
1277
- * that control on the other axis, where Enter at the last row and Tab past the last
1278
- * cell reach the verb from inside a cell. They sit after the grid, so the Tab that
1279
- * declines off the last cell lands on them, and the focus rung draws the one it lands
1280
- * 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. */
1281
1221
  addBar(axis, label) {
1282
1222
  const btn = chromeButton('qm-table-add', label, () => {
1283
1223
  const props = this.props();
@@ -1287,6 +1227,7 @@ class TableIslandView {
1287
1227
  this.write(insertColumn(props, columnCount(props) - 1), { r: 0, c: columnCount(props) });
1288
1228
  });
1289
1229
  btn.setAttribute('data-axis', axis);
1230
+ btn.tabIndex = -1;
1290
1231
  btn.appendChild(el('span', 'qm-table-add-bar'));
1291
1232
  return btn;
1292
1233
  }
@@ -1307,6 +1248,7 @@ class TableIslandView {
1307
1248
  scrollThreshold: this.deps.clearance,
1308
1249
  scrollMargin: this.deps.clearance,
1309
1250
  dispatchTransaction: (tr) => {
1251
+ const from = { r, c, head: view.state.selection.head };
1310
1252
  const next = view.state.apply(tr);
1311
1253
  view.updateState(next);
1312
1254
  if (!tr.docChanged)
@@ -1319,7 +1261,7 @@ class TableIslandView {
1319
1261
  const now = this.props();
1320
1262
  const cell = cellFromDoc(next.doc, cellAt(now, r, c));
1321
1263
  mounted.shown = cell;
1322
- this.write(withCell(now, r, c, cell));
1264
+ this.write(withCell(now, r, c, cell), undefined, from);
1323
1265
  },
1324
1266
  handleDOMEvents: {
1325
1267
  focus: () => {
@@ -1355,13 +1297,16 @@ class TableIslandView {
1355
1297
  * op is one commit and one undo step, which is why every op writes a whole
1356
1298
  * table rather than mutating the props in place.
1357
1299
  */
1358
- write(next, focus) {
1300
+ write(next, focus, from = this.focusedSeat()) {
1359
1301
  const pos = this.getPos();
1360
1302
  if (pos == null)
1361
1303
  return;
1362
1304
  const node = this.outer.state.doc.nodeAt(pos);
1363
1305
  if (!node || node.type !== this.node.type)
1364
1306
  return;
1307
+ const left = tablePropsOfNode(node);
1308
+ if (left && from)
1309
+ departures.set(left, from);
1365
1310
  this.outer.dispatch(this.outer.state.tr.setNodeMarkup(pos, undefined, {
1366
1311
  ...node.attrs,
1367
1312
  props: normalizeTable(next)
@@ -1369,8 +1314,9 @@ class TableIslandView {
1369
1314
  if (focus)
1370
1315
  this.focusCell(focus.r, focus.c);
1371
1316
  }
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. */
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. */
1374
1320
  focusCell(r, c, edge = 'end') {
1375
1321
  const props = this.props();
1376
1322
  const row = Math.max(0, Math.min(r, rowCount(props) - 1));
@@ -1379,7 +1325,12 @@ class TableIslandView {
1379
1325
  if (!mounted)
1380
1326
  return;
1381
1327
  const { view } = mounted;
1382
- const at = edge === 'start' ? Selection.atStart(view.state.doc) : Selection.atEnd(view.state.doc);
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)));
1383
1334
  // Flagged, for the reason the leaf's own landing is (`field.ts`, `setCaret`): PM
1384
1335
  // focuses with `preventScroll`, so an unflagged dispatch lands the caret where no
1385
1336
  // scroller has moved to — a Tab past the right edge of the horizontal scroller a
@@ -73,19 +73,12 @@ export declare function moveColumn(props: TableProps, c: number, by: number): Ta
73
73
  * empties is text.
74
74
  */
75
75
  export declare function clearCells(props: TableProps, r0: number, c0: number, r1: number, c1: number): TableProps;
76
- /** Set column `c`'s alignment: what the held column's cluster and Shift+arrow write
77
- * (`table-view.ts`). */
76
+ /** Set column `c`'s alignment and clear its cells' own `align`, which would outrank it:
77
+ * what the held column's cluster and Shift+arrow write (`table-view.ts`). */
78
78
  export declare function setAlign(props: TableProps, c: number, align: TableAlign): TableProps;
79
79
  /** Draw the first row as a header or as a body row: what the header toggle writes
80
80
  * (`table-view.ts`). */
81
81
  export declare function setHeadless(props: TableProps, headless: boolean): TableProps;
82
- /** A cell's horizontal alignment, and its vertical one: the keys a cell holds beside
83
- * its text, each absent at the default the column or the quill supplies. */
84
- export type CellAlign = NonNullable<TableCell['align']>;
85
- export type CellValign = NonNullable<TableCell['valign']>;
86
- /** Every cell in the inclusive rectangle given `key`'s `value`, or cleared of it where
87
- * `value` is absent, which returns the cell to its column's alignment or the quill's. */
88
- export declare function setCellLayout<K extends 'align' | 'valign'>(props: TableProps, r0: number, c0: number, r1: number, c1: number, key: K, value: TableCell[K] | undefined): TableProps;
89
82
  /** Set or clear the column weights: what a border drag and its double-click write
90
83
  * (`table-view.ts`). `undefined` returns every column to auto-fit. */
91
84
  export declare function setWidths(props: TableProps, widths: (number | null)[] | undefined): TableProps;
@@ -184,11 +184,19 @@ export function clearCells(props, r0, c0, r1, c1) {
184
184
  rows: props.rows.map((row, i) => blank(row, i + 1))
185
185
  });
186
186
  }
187
- /** Set column `c`'s alignment: what the held column's cluster and Shift+arrow write
188
- * (`table-view.ts`). */
187
+ /** Set column `c`'s alignment and clear its cells' own `align`, which would outrank it:
188
+ * what the held column's cluster and Shift+arrow write (`table-view.ts`). */
189
189
  export function setAlign(props, c, align) {
190
+ const release = (cells) => cells.map((cell, i) => {
191
+ if (i !== c || cell.align === undefined)
192
+ return cell;
193
+ const { align: _align, ...rest } = cell;
194
+ return rest;
195
+ });
190
196
  return normalizeTable({
191
197
  ...props,
198
+ header: release(props.header),
199
+ rows: props.rows.map(release),
192
200
  aligns: props.aligns.map((a, i) => (i === c ? align : a))
193
201
  });
194
202
  }
@@ -197,23 +205,6 @@ export function setAlign(props, c, align) {
197
205
  export function setHeadless(props, headless) {
198
206
  return normalizeTable({ ...props, headless: headless || undefined });
199
207
  }
200
- /** Every cell in the inclusive rectangle given `key`'s `value`, or cleared of it where
201
- * `value` is absent, which returns the cell to its column's alignment or the quill's. */
202
- export function setCellLayout(props, r0, c0, r1, c1, key, value) {
203
- const lay = (cells, r) => cells.map((cell, c) => {
204
- if (r < r0 || r > r1 || c < c0 || c > c1)
205
- return cell;
206
- const next = { ...cell, [key]: value };
207
- if (value === undefined)
208
- delete next[key];
209
- return next;
210
- });
211
- return normalizeTable({
212
- ...props,
213
- header: lay(props.header, 0),
214
- rows: props.rows.map((row, i) => lay(row, i + 1))
215
- });
216
- }
217
208
  /** Set or clear the column weights: what a border drag and its double-click write
218
209
  * (`table-view.ts`). `undefined` returns every column to auto-fit. */
219
210
  export function setWidths(props, widths) {
@@ -0,0 +1,19 @@
1
+ import { type DOMOutputSpec, type NodeType } from 'prosemirror-model';
2
+ import { Plugin, type Command } from 'prosemirror-state';
3
+ /** The `checked` an `<li>` states: `data-qm-checked`, which `toDOM` writes, else none. */
4
+ export declare function taskChecked(el: HTMLElement): boolean | null;
5
+ /** Where an `<li>`'s blocks are: a task item's body, past its checkbox; the item itself
6
+ * otherwise. */
7
+ export declare function taskBody(el: Node): HTMLElement;
8
+ /** A task item's `<li>`: the checkbox, unreachable by the caret, then the body holding
9
+ * the item's blocks, which names it. The mark is the check glyph a matrix tick draws. */
10
+ export declare function taskDOM(checked: boolean): DOMOutputSpec;
11
+ /** The node view a block leaf mounts its list items through. */
12
+ export declare function taskPlugin(): Plugin;
13
+ /**
14
+ * `splitListItem`, leaving the item that takes none of a task item's text an open task:
15
+ * the one below, or the one above where Enter is pressed at the head of the item's text.
16
+ * An empty item the split lifts out of a nested list keeps its own `checked`, as a
17
+ * Shift-Tab lift keeps it.
18
+ */
19
+ export declare function splitTaskItem(itemType: NodeType): Command;
@@ -0,0 +1,140 @@
1
+ // A task item is a `list_item` whose `checked` is a boolean (CODEC §"Task items"). Its
2
+ // `toDOM` draws a checkbox ahead of the item's blocks, disabled, which is the drawing a
3
+ // held leaf, a copy and `renderContent` take; a leaf that edits mounts the item through
4
+ // `taskItemView`, whose checkbox toggles. A task item is minted by source mode and by a
5
+ // paste alone: no shorthand, command or key makes one.
6
+ import { DOMSerializer } from 'prosemirror-model';
7
+ import { Plugin } from 'prosemirror-state';
8
+ import { splitListItem } from 'prosemirror-schema-list';
9
+ const SVG = 'http://www.w3.org/2000/svg';
10
+ /** The id the checkbox names its item's blocks by, fresh per drawing. */
11
+ let drawn = 0;
12
+ /** The `checked` an `<li>` states: `data-qm-checked`, which `toDOM` writes, else none. */
13
+ export function taskChecked(el) {
14
+ const stated = el.getAttribute('data-qm-checked');
15
+ return stated === null ? null : stated === 'true';
16
+ }
17
+ /** Where an `<li>`'s blocks are: a task item's body, past its checkbox; the item itself
18
+ * otherwise. */
19
+ export function taskBody(el) {
20
+ const li = el;
21
+ const body = li.hasAttribute('data-qm-checked')
22
+ ? li.querySelector(':scope > [data-qm-task-body]')
23
+ : null;
24
+ return body ?? li;
25
+ }
26
+ /** A task item's `<li>`: the checkbox, unreachable by the caret, then the body holding
27
+ * the item's blocks, which names it. The mark is the check glyph a matrix tick draws. */
28
+ export function taskDOM(checked) {
29
+ const body = `qm-task-${++drawn}`;
30
+ return [
31
+ 'li',
32
+ { 'data-qm-checked': String(checked) },
33
+ [
34
+ 'span',
35
+ { class: 'qm-task', contenteditable: 'false' },
36
+ [
37
+ 'input',
38
+ {
39
+ type: 'checkbox',
40
+ class: 'qm-task-box',
41
+ 'aria-labelledby': body,
42
+ checked: checked ? '' : null,
43
+ disabled: ''
44
+ }
45
+ ],
46
+ [
47
+ `${SVG} svg`,
48
+ {
49
+ class: 'qm-task-mark',
50
+ viewBox: '0 0 24 24',
51
+ fill: 'none',
52
+ stroke: 'currentColor',
53
+ 'stroke-width': '2',
54
+ 'stroke-linecap': 'round',
55
+ 'stroke-linejoin': 'round',
56
+ 'aria-hidden': 'true'
57
+ },
58
+ [`${SVG} path`, { d: 'M20 6 9 17l-5-5' }]
59
+ ]
60
+ ],
61
+ ['div', { id: body, 'data-qm-task-body': '' }, 0]
62
+ ];
63
+ }
64
+ const isTask = (node) => node.attrs.checked !== null;
65
+ /**
66
+ * Every `list_item` in a leaf that edits, drawn by its own `toDOM`. A task item's checkbox
67
+ * is enabled, and a change to it is one `checked` transaction, so one `Mod-z` takes it
68
+ * back; where the leaf declines the edit the box returns to the item's value. The box
69
+ * keeps the press from the caret: a mousedown leaves the selection and the focus where
70
+ * they were, and ProseMirror reads no event aimed at it.
71
+ */
72
+ function taskItemView(node, view, getPos) {
73
+ const { dom, contentDOM } = DOMSerializer.renderSpec(document, node.type.spec.toDOM(node));
74
+ const box = isTask(node)
75
+ ? dom.querySelector(':scope > .qm-task > input')
76
+ : null;
77
+ if (box) {
78
+ box.disabled = false;
79
+ box.addEventListener('mousedown', (e) => e.preventDefault());
80
+ box.addEventListener('change', () => {
81
+ const pos = getPos();
82
+ if (pos === undefined || !view.editable) {
83
+ box.checked = node.attrs.checked;
84
+ return;
85
+ }
86
+ view.dispatch(view.state.tr.setNodeAttribute(pos, 'checked', box.checked));
87
+ });
88
+ }
89
+ return {
90
+ dom,
91
+ contentDOM,
92
+ update(next) {
93
+ if (next.type !== node.type || isTask(next) !== isTask(node))
94
+ return false;
95
+ node = next;
96
+ if (box) {
97
+ box.checked = next.attrs.checked;
98
+ dom.setAttribute('data-qm-checked', String(next.attrs.checked));
99
+ }
100
+ return true;
101
+ },
102
+ stopEvent: (e) => box !== null && e.target === box,
103
+ ignoreMutation: (m) => m.type !== 'selection' && !contentDOM.contains(m.target)
104
+ };
105
+ }
106
+ /** The node view a block leaf mounts its list items through. */
107
+ export function taskPlugin() {
108
+ return new Plugin({ props: { nodeViews: { list_item: taskItemView } } });
109
+ }
110
+ /**
111
+ * `splitListItem`, leaving the item that takes none of a task item's text an open task:
112
+ * the one below, or the one above where Enter is pressed at the head of the item's text.
113
+ * An empty item the split lifts out of a nested list keeps its own `checked`, as a
114
+ * Shift-Tab lift keeps it.
115
+ */
116
+ export function splitTaskItem(itemType) {
117
+ const split = splitListItem(itemType);
118
+ return (state, dispatch, view) => {
119
+ const { $from, empty } = state.selection;
120
+ const item = $from.depth >= 2 ? $from.node(-1) : null;
121
+ if (!dispatch || item?.type !== itemType || !isTask(item))
122
+ return split(state, dispatch, view);
123
+ const lifted = $from.parent.content.size === 0 && item.childCount === 1;
124
+ const atHead = empty && $from.parentOffset === 0 && $from.index(-1) === 0;
125
+ return split(state, (tr) => {
126
+ const $at = tr.selection.$from;
127
+ const below = $at.before($at.depth - 1);
128
+ if (lifted)
129
+ tr.setNodeAttribute(below, 'checked', item.attrs.checked);
130
+ else if (atHead) {
131
+ const above = tr.doc.resolve(below).nodeBefore;
132
+ if (above)
133
+ tr.setNodeAttribute(below - above.nodeSize, 'checked', false);
134
+ }
135
+ else
136
+ tr.setNodeAttribute(below, 'checked', false);
137
+ dispatch(tr);
138
+ });
139
+ };
140
+ }
@@ -11,8 +11,8 @@ export type EditorErrorCode =
11
11
  * the whole projection instead, so the store is correct and this field's
12
12
  * identity anchors were paid. */
13
13
  | 'commit-fallback'
14
- /** The overwrite fallback also failed, or the typed writer refused a `plaintext`
15
- * leaf's text: the optimistic PM state stands and the store is stale for this
14
+ /** The overwrite fallback also failed, or the typed writer refused a `text` leaf's
15
+ * text: the optimistic PM state stands and the store is stale for this
16
16
  * field. The one code here that leaves damage. */
17
17
  | 'commit-lost'
18
18
  /** A card operation (add, move, remove, retype) that threw. The document is