@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.
- package/THEMING.md +1 -1
- package/dist/core/codec/atoms.js +25 -10
- package/dist/core/codec/blocks.d.ts +5 -5
- package/dist/core/codec/blocks.js +23 -6
- package/dist/core/codec/code.d.ts +3 -3
- package/dist/core/codec/code.js +51 -21
- package/dist/core/codec/field.d.ts +9 -7
- package/dist/core/codec/field.js +22 -11
- package/dist/core/codec/inputrules.d.ts +4 -0
- package/dist/core/codec/inputrules.js +43 -5
- package/dist/core/codec/link-paste.d.ts +11 -0
- package/dist/core/codec/link-paste.js +32 -0
- package/dist/core/codec/lists.d.ts +2 -2
- package/dist/core/codec/lists.js +66 -26
- package/dist/core/codec/prose.css +71 -43
- package/dist/core/codec/schema.js +55 -11
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +5 -0
- package/dist/core/codec/table-view.js +232 -103
- package/dist/core/rungs.d.ts +6 -0
- package/dist/core/rungs.js +11 -0
- package/dist/core/theme.css +24 -12
- package/dist/visual/ArrayField.svelte +168 -48
- package/dist/visual/BooleanField.svelte +12 -1
- package/dist/visual/Card.svelte +32 -20
- package/dist/visual/Card.svelte.d.ts +1 -0
- package/dist/visual/CardControls.svelte +15 -15
- package/dist/visual/DateField.svelte +93 -64
- package/dist/visual/EnumField.svelte +9 -4
- package/dist/visual/FieldHint.svelte +22 -5
- package/dist/visual/FieldLabel.svelte +3 -3
- package/dist/visual/FormatPopover.svelte +25 -9
- package/dist/visual/MatrixField.svelte +10 -7
- package/dist/visual/ObjectField.svelte +2 -4
- package/dist/visual/TipsCard.svelte +2 -5
- package/dist/visual/VisualEditorInner.svelte +108 -26
- package/dist/visual/controls.css +2 -1
- package/dist/visual/icons/Icon.svelte +3 -2
- package/dist/visual/icons/Icon.svelte.d.ts +3 -2
- package/dist/visual/icons/nodes.d.ts +25 -0
- package/dist/visual/icons/nodes.js +8 -0
- package/dist/visual/motion.d.ts +25 -3
- package/dist/visual/motion.js +58 -9
- package/dist/visual/strings.d.ts +6 -5
- package/dist/visual/strings.js +3 -3
- 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
|
|
19
|
-
//
|
|
20
|
-
// it; `codec/prose.css` draws the band,
|
|
21
|
-
|
|
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
|
|
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 [
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
532
|
-
* column spanning every row, and nothing
|
|
533
|
-
*
|
|
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
|
-
|
|
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
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
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
|
|
667
|
-
* the
|
|
668
|
-
* column already
|
|
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
|
-
|
|
672
|
-
|
|
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
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
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
|
|
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
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
layCells(key, value
|
|
726
|
-
const held = this.selected
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
//
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
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
|
-
|
|
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
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
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
|
-
'
|
|
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
|
|
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
|
|
1471
|
-
|
|
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 (
|
|
1607
|
+
if (to.r >= rowCount(props)) {
|
|
1485
1608
|
if (rowEmpty(props, r))
|
|
1486
1609
|
return false;
|
|
1487
|
-
this.write(insertRow(props, r),
|
|
1610
|
+
this.write(insertRow(props, r), to);
|
|
1488
1611
|
}
|
|
1489
1612
|
else
|
|
1490
|
-
this.focusCell(
|
|
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) {
|
package/dist/core/rungs.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/rungs.js
CHANGED
|
@@ -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
|
+
}
|