@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.
- package/NOTICE +6 -7
- package/README.md +1 -1
- package/dist/core/address.d.ts +6 -1
- package/dist/core/address.js +12 -3
- package/dist/core/bloom.d.ts +9 -4
- package/dist/core/bloom.js +18 -49
- package/dist/core/codec/atoms.d.ts +1 -1
- package/dist/core/codec/atoms.js +1 -1
- package/dist/core/codec/blocks.js +4 -2
- package/dist/core/codec/code.d.ts +1 -1
- package/dist/core/codec/code.js +1 -1
- package/dist/core/codec/decode.d.ts +5 -5
- package/dist/core/codec/decode.js +18 -12
- package/dist/core/codec/encode.js +6 -1
- package/dist/core/codec/field.d.ts +15 -10
- package/dist/core/codec/field.js +46 -17
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +2 -2
- package/dist/core/codec/inputrules.js +1 -1
- package/dist/core/codec/lists.d.ts +3 -2
- package/dist/core/codec/lists.js +21 -5
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +74 -20
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +16 -7
- package/dist/core/codec/table-copy.d.ts +17 -0
- package/dist/core/codec/table-copy.js +84 -0
- package/dist/core/codec/table-view.d.ts +1 -7
- package/dist/core/codec/table-view.js +70 -119
- package/dist/core/codec/table.d.ts +2 -9
- package/dist/core/codec/table.js +10 -19
- package/dist/core/codec/tasks.d.ts +19 -0
- package/dist/core/codec/tasks.js +140 -0
- package/dist/core/errors.d.ts +2 -2
- package/dist/visual/ArrayField.svelte +101 -63
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/Card.svelte +9 -3
- package/dist/visual/Card.svelte.d.ts +2 -2
- package/dist/visual/Field.svelte +20 -16
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldLabel.svelte +5 -33
- package/dist/visual/FieldLabel.svelte.d.ts +0 -2
- package/dist/visual/FormatPopover.svelte +54 -73
- package/dist/visual/MatrixField.svelte +18 -12
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +38 -20
- package/dist/visual/ObjectField.svelte.d.ts +6 -0
- package/dist/visual/ProseField.svelte +10 -13
- package/dist/visual/ProseField.svelte.d.ts +5 -2
- package/dist/visual/ProseValue.svelte +25 -19
- package/dist/visual/ProseValue.svelte.d.ts +4 -4
- package/dist/visual/TextField.svelte +3 -2
- package/dist/visual/TipsCard.svelte +5 -2
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +13 -13
- package/dist/visual/controls.css +19 -1
- package/dist/visual/icons/nodes.d.ts +0 -21
- package/dist/visual/icons/nodes.js +1 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/strings.d.ts +10 -5
- package/dist/visual/strings.js +16 -9
- package/dist/visual/structure.d.ts +21 -31
- package/dist/visual/structure.js +54 -51
- 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
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
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
|
|
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(
|
|
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
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
* cell
|
|
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,
|
|
1373
|
-
* where a row or column op puts it,
|
|
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
|
|
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
|
|
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;
|
package/dist/core/codec/table.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -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 `
|
|
15
|
-
*
|
|
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
|