@quillmark/svelte 0.5.0 → 0.6.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/README.md +8 -2
- package/THEMING.md +22 -27
- package/dist/core/address.d.ts +17 -3
- package/dist/core/address.js +20 -3
- package/dist/core/codec/blocks.d.ts +37 -0
- package/dist/core/codec/blocks.js +163 -0
- package/dist/core/codec/field.js +3 -5
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +24 -144
- package/dist/core/codec/prose.css +14 -25
- package/dist/core/codec/schema.d.ts +8 -0
- package/dist/core/codec/schema.js +33 -1
- package/dist/core/codec/slash.d.ts +18 -16
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.js +127 -157
- package/dist/core/codec/table.d.ts +0 -10
- package/dist/core/codec/table.js +0 -15
- package/dist/core/theme.css +350 -347
- package/dist/preset/recipes.css +10 -21
- package/dist/preset/scale.css +6 -7
- package/dist/preview/Preview.svelte +4 -2
- package/dist/preview/geometry.d.ts +10 -9
- package/dist/preview/geometry.js +11 -13
- package/dist/visual/ArrayField.svelte +242 -63
- package/dist/visual/ArrayField.svelte.d.ts +5 -1
- package/dist/visual/Card.svelte +21 -2
- package/dist/visual/DiagnosticList.svelte +6 -11
- package/dist/visual/EnumField.svelte +33 -1
- package/dist/visual/Field.svelte +29 -5
- package/dist/visual/FieldLabel.svelte +2 -2
- package/dist/visual/FormatPopover.svelte +13 -1
- package/dist/visual/ObjectField.svelte +157 -35
- package/dist/visual/ObjectField.svelte.d.ts +13 -4
- package/dist/visual/ProseArrayElement.svelte +22 -2
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -0
- package/dist/visual/TipsCard.svelte +33 -13
- package/dist/visual/VariantField.svelte +98 -0
- package/dist/visual/VariantField.svelte.d.ts +21 -0
- package/dist/visual/VisualEditorInner.svelte +18 -15
- package/dist/visual/controls.css +282 -283
- package/dist/visual/diagnostics.d.ts +15 -24
- package/dist/visual/diagnostics.js +14 -30
- package/dist/visual/domid.d.ts +13 -0
- package/dist/visual/domid.js +16 -0
- package/dist/visual/leaves.d.ts +9 -7
- package/dist/visual/links.d.ts +5 -2
- package/dist/visual/links.js +9 -3
- package/dist/visual/props.d.ts +1 -1
- package/dist/visual/strings.d.ts +21 -6
- package/dist/visual/strings.js +3 -0
- package/dist/visual/structure.d.ts +32 -7
- package/dist/visual/structure.js +49 -3
- package/package.json +2 -2
- package/dist/core/codec/clipboard.d.ts +0 -59
- package/dist/core/codec/clipboard.js +0 -202
|
@@ -17,17 +17,16 @@
|
|
|
17
17
|
// The chrome is A band and A selection (CODEC §"The table island"), and it raises
|
|
18
18
|
// nothing. Every control is absolutely positioned out of the grid, so the band is in no
|
|
19
19
|
// row and no column of it; `codec/prose.css` draws the band and the selection wash both.
|
|
20
|
-
import { baseKeymap, toggleMark } from 'prosemirror-commands';
|
|
20
|
+
import { baseKeymap, chainCommands, toggleMark } from 'prosemirror-commands';
|
|
21
21
|
import { redo, undo } from 'prosemirror-history';
|
|
22
22
|
import { keymap } from 'prosemirror-keymap';
|
|
23
23
|
import { EditorState, NodeSelection, Selection, TextSelection } from 'prosemirror-state';
|
|
24
24
|
import { EditorView } from 'prosemirror-view';
|
|
25
|
-
import { tableFromClipboard, writeRectangle } from './clipboard.js';
|
|
26
25
|
import { decode } from './decode.js';
|
|
27
26
|
import { inputRulesPlugin } from './inputrules.js';
|
|
28
27
|
import { tablePropsOfNode } from './islands.js';
|
|
29
28
|
import { inlineSchema } from './schema.js';
|
|
30
|
-
import {
|
|
29
|
+
import { cellAt, cellContent, cellEqual, cellFromDoc, clearCells, columnCount, deleteColumn, deleteRow, insertColumn, insertRow, moveColumn, moveRow, normalizeTable, rowCells, rowCount, rowEmpty, shapeEqual, withCell } from './table.js';
|
|
31
30
|
/**
|
|
32
31
|
* The package's English for the island chrome. It lives here, beside the surface
|
|
33
32
|
* that draws it, rather than in the visual tier's table: the codec mounts this
|
|
@@ -83,8 +82,9 @@ function el(tag, className) {
|
|
|
83
82
|
}
|
|
84
83
|
/** A band button. It swallows its own `mousedown` (prosemirror-menu's trick, and
|
|
85
84
|
* the format popover's): without it the browser focuses the button, blurring the
|
|
86
|
-
* cell whose caret the op is about to be measured against.
|
|
87
|
-
*
|
|
85
|
+
* cell whose caret the op is about to be measured against. No control on the band ever
|
|
86
|
+
* holds the focus: a cell is the island's one host, and what a press acts on is where
|
|
87
|
+
* it lands the caret. */
|
|
88
88
|
function chromeButton(className, label, run) {
|
|
89
89
|
const btn = el('button', className);
|
|
90
90
|
btn.type = 'button';
|
|
@@ -161,13 +161,6 @@ class TableIslandView {
|
|
|
161
161
|
this.dom.setAttribute('data-island', node.attrs.islandType);
|
|
162
162
|
this.dom.setAttribute('data-island-id', node.attrs.id);
|
|
163
163
|
this.dom.addEventListener('mousedown', this.onPointerDown);
|
|
164
|
-
// On the document rather than on the island: a `copy` is delivered to the DOM
|
|
165
|
-
// selection's own node, and a held rectangle has cleared that selection (a sweep
|
|
166
|
-
// removes the ranges itself, a grip press moves the focus to a button), so the
|
|
167
|
-
// event never crosses this subtree at all. What makes one this island's is
|
|
168
|
-
// {@link TableIslandView.onCopy}'s guard.
|
|
169
|
-
this.dom.ownerDocument.addEventListener('copy', this.onCopy);
|
|
170
|
-
this.dom.ownerDocument.addEventListener('cut', this.onCopy);
|
|
171
164
|
this.render();
|
|
172
165
|
}
|
|
173
166
|
// ── PM's NodeView contract ────────────────────────────────────────────────
|
|
@@ -220,8 +213,6 @@ class TableIslandView {
|
|
|
220
213
|
}
|
|
221
214
|
destroy() {
|
|
222
215
|
this.dom.removeEventListener('mousedown', this.onPointerDown);
|
|
223
|
-
this.dom.ownerDocument.removeEventListener('copy', this.onCopy);
|
|
224
|
-
this.dom.ownerDocument.removeEventListener('cut', this.onCopy);
|
|
225
216
|
this.endDrag();
|
|
226
217
|
this.endSweep();
|
|
227
218
|
this.teardownCells();
|
|
@@ -317,10 +308,10 @@ class TableIslandView {
|
|
|
317
308
|
this.outer.focus();
|
|
318
309
|
this.outer.dispatch(this.outer.state.tr.setSelection(selection));
|
|
319
310
|
}
|
|
320
|
-
/** The island as the selection: Escape out of a cell or a
|
|
321
|
-
* what selects it. Backspace there deletes the
|
|
322
|
-
* island; the cell selection covering every rank
|
|
323
|
-
* leaving the grid (CODEC §"The table island"). */
|
|
311
|
+
/** The island as the selection: the Escape that climbs out of a cell or a held
|
|
312
|
+
* rectangle, which is the whole of what selects it. Backspace there deletes the
|
|
313
|
+
* table, as it does over every other island; the cell selection covering every rank
|
|
314
|
+
* reaches the same delete without leaving the grid (CODEC §"The table island"). */
|
|
324
315
|
selectIsland() {
|
|
325
316
|
const pos = this.getPos();
|
|
326
317
|
if (pos == null)
|
|
@@ -396,10 +387,6 @@ class TableIslandView {
|
|
|
396
387
|
return best;
|
|
397
388
|
}
|
|
398
389
|
// ── The selection ─────────────────────────────────────────────────────────
|
|
399
|
-
/** Take the line's grip: the focus a selection's keys land on. */
|
|
400
|
-
gripFor(line) {
|
|
401
|
-
return this.grips.get(lineKey(line));
|
|
402
|
-
}
|
|
403
390
|
/** The rectangle a line covers, which is what selecting one means: a grip draws no
|
|
404
391
|
* second kind of selection, it draws this one. */
|
|
405
392
|
lineCells(line) {
|
|
@@ -416,11 +403,43 @@ class TableIslandView {
|
|
|
416
403
|
? { floor: 0, limit: rowCount(props) - 1 }
|
|
417
404
|
: { floor: 0, limit: columnCount(props) - 1 };
|
|
418
405
|
}
|
|
419
|
-
/** Select a line
|
|
420
|
-
*
|
|
406
|
+
/** Select a line, and land the caret on it. The line verbs bind in the cell
|
|
407
|
+
* (CODEC §"The table island"), so the rectangle has to run through the caret for the
|
|
408
|
+
* next key to read a line off it. The focus moves first and the paint follows: a cell
|
|
409
|
+
* taking the focus retires whatever was held. */
|
|
421
410
|
selectLine(line) {
|
|
411
|
+
this.carryCaret(line);
|
|
422
412
|
this.select(this.lineCells(line));
|
|
423
|
-
|
|
413
|
+
}
|
|
414
|
+
/** Put the caret on the line, keeping its other coordinate, and leave it exactly
|
|
415
|
+
* where it is on a line it is already in: a walk down the rows stays in its column,
|
|
416
|
+
* and a grip press on the caret's own row does not shunt it to a cell's end. */
|
|
417
|
+
carryCaret(line) {
|
|
418
|
+
const held = this.cells.find((m) => m.view.hasFocus());
|
|
419
|
+
if (line.axis === 'row') {
|
|
420
|
+
if (held?.r !== line.index)
|
|
421
|
+
this.focusCell(line.index, held?.c ?? 0);
|
|
422
|
+
}
|
|
423
|
+
else if (held?.c !== line.index)
|
|
424
|
+
this.focusCell(held?.r ?? 0, line.index);
|
|
425
|
+
}
|
|
426
|
+
/** Which line the held rectangle is, on one axis: a row spanning every column, a
|
|
427
|
+
* column spanning every row, and nothing at all for a block that spans neither. The
|
|
428
|
+
* extent rule reaching the line verbs ({@link TableIslandView.deleteSelection}) — a
|
|
429
|
+
* rectangle answers by what it covers, never by the gesture that drew it, so a row
|
|
430
|
+
* swept cell by cell moves exactly as the row a grip named does. */
|
|
431
|
+
lineOn(axis) {
|
|
432
|
+
const held = this.selected;
|
|
433
|
+
if (!held)
|
|
434
|
+
return undefined;
|
|
435
|
+
const props = this.props();
|
|
436
|
+
if (axis === 'row')
|
|
437
|
+
return held.r0 === held.r1 && held.c0 === 0 && held.c1 === columnCount(props) - 1
|
|
438
|
+
? held.r0
|
|
439
|
+
: undefined;
|
|
440
|
+
return held.c0 === held.c1 && held.r0 === 0 && held.r1 === rowCount(props) - 1
|
|
441
|
+
? held.c0
|
|
442
|
+
: undefined;
|
|
424
443
|
}
|
|
425
444
|
select(cells) {
|
|
426
445
|
// One subject at a time: a cell selection retires an island one, or the surface
|
|
@@ -457,94 +476,6 @@ class TableIslandView {
|
|
|
457
476
|
for (const cell of this.cells)
|
|
458
477
|
cell.box.toggleAttribute('data-selected', !!held && cell.r >= held.r0 && cell.r <= held.r1 && cell.c >= held.c0 && cell.c <= held.c1);
|
|
459
478
|
}
|
|
460
|
-
/**
|
|
461
|
-
* A selected line's keys, bound to the line its own grip names rather than read off
|
|
462
|
-
* the selection: focus and selection are separate, and a Tab between grips would
|
|
463
|
-
* otherwise aim the next key at whichever line was last pressed.
|
|
464
|
-
*
|
|
465
|
-
* Undo is forwarded for the reason a cell forwards it: one undo stack per leaf, and
|
|
466
|
-
* a `button` has no history of its own.
|
|
467
|
-
*/
|
|
468
|
-
lineKeys(line) {
|
|
469
|
-
return (event) => {
|
|
470
|
-
const key = event.key;
|
|
471
|
-
const mod = event.ctrlKey || event.metaKey;
|
|
472
|
-
if (mod && (key === 'z' || key === 'Z' || key === 'y')) {
|
|
473
|
-
event.preventDefault();
|
|
474
|
-
const redoing = key === 'y' || (key === 'Z' && event.shiftKey);
|
|
475
|
-
(redoing ? redo : undo)(this.outer.state, this.outer.dispatch);
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
const along = line.axis === 'row' ? ['ArrowUp', 'ArrowDown'] : ['ArrowLeft', 'ArrowRight'];
|
|
479
|
-
const acts = along.includes(key) || ['Backspace', 'Delete', 'Enter', 'Escape'].includes(key);
|
|
480
|
-
if (!acts)
|
|
481
|
-
return;
|
|
482
|
-
event.preventDefault();
|
|
483
|
-
event.stopPropagation();
|
|
484
|
-
if (key === 'Escape')
|
|
485
|
-
return this.selectIsland();
|
|
486
|
-
if (key === 'Enter') {
|
|
487
|
-
this.clearSelection();
|
|
488
|
-
return line.axis === 'row' ? this.focusCell(line.index, 0) : this.focusCell(0, line.index);
|
|
489
|
-
}
|
|
490
|
-
if (key === 'Backspace' || key === 'Delete')
|
|
491
|
-
return this.deleteSelection();
|
|
492
|
-
const by = key === 'ArrowUp' || key === 'ArrowLeft' ? -1 : 1;
|
|
493
|
-
if (event.altKey)
|
|
494
|
-
return this.moveLine(line, by);
|
|
495
|
-
const { floor, limit } = this.bounds(line.axis, this.props());
|
|
496
|
-
this.selectLine({
|
|
497
|
-
axis: line.axis,
|
|
498
|
-
index: Math.max(floor, Math.min(line.index + by, limit))
|
|
499
|
-
});
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
// ── The clipboard ─────────────────────────────────────────────────────────
|
|
503
|
-
/** The held rectangle's cells, read off the stored props: the unit that crosses the
|
|
504
|
-
* clipboard (`clipboard.ts`), so the unit the verbs here read. */
|
|
505
|
-
heldBlock(held) {
|
|
506
|
-
const props = this.props();
|
|
507
|
-
return Array.from({ length: held.r1 - held.r0 + 1 }, (_, i) => Array.from({ length: held.c1 - held.c0 + 1 }, (_, j) => cellAt(props, held.r0 + i, held.c0 + j)));
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Copy or cut the held rectangle (`clipboard.ts` writes both wire formats). A cut is
|
|
511
|
-
* a copy plus the delete Backspace already means over that rectangle, so what it
|
|
512
|
-
* takes out is read by the same extent rule: a covered rank goes, a sub-rectangle
|
|
513
|
-
* clears.
|
|
514
|
-
*
|
|
515
|
-
* The guard is the whole of what makes a document-level listener this island's. With
|
|
516
|
-
* no rectangle the event belongs to whatever holds the caret. With the focus outside,
|
|
517
|
-
* it is another surface's copy over a rectangle this island still happens to hold.
|
|
518
|
-
*/
|
|
519
|
-
onCopy = (event) => {
|
|
520
|
-
const held = this.selected;
|
|
521
|
-
const data = event.clipboardData;
|
|
522
|
-
const owner = this.dom.ownerDocument;
|
|
523
|
-
if (!held || !data || !this.dom.contains(owner.activeElement))
|
|
524
|
-
return;
|
|
525
|
-
writeRectangle(data, this.heldBlock(held), this.props().aligns.slice(held.c0, held.c1 + 1), owner);
|
|
526
|
-
event.preventDefault();
|
|
527
|
-
if (event.type === 'cut')
|
|
528
|
-
this.deleteSelection();
|
|
529
|
-
};
|
|
530
|
-
/**
|
|
531
|
-
* A table pasted into a cell is written in at that cell, growing the table to hold
|
|
532
|
-
* it (`table.ts` §{@link pasteCells}); anything else is the cell's own paste and
|
|
533
|
-
* this declines. It lands at the caret's cell, which over a swept block is the cell
|
|
534
|
-
* the press started in — the block's anchor rather than its first corner, a sweep
|
|
535
|
-
* running up or left putting those at opposite ends.
|
|
536
|
-
*
|
|
537
|
-
* Without this arm the cell's inline schema reads the same clipboard as text and
|
|
538
|
-
* flattens the table into one cell, which is the destructive case one level down.
|
|
539
|
-
*/
|
|
540
|
-
pasteIntoCell(r, c, event) {
|
|
541
|
-
const props = tableFromClipboard(event.clipboardData);
|
|
542
|
-
if (!props)
|
|
543
|
-
return false;
|
|
544
|
-
this.clearSelection();
|
|
545
|
-
this.write(pasteCells(this.props(), r, c, allRows(props)));
|
|
546
|
-
return true;
|
|
547
|
-
}
|
|
548
479
|
/**
|
|
549
480
|
* What Backspace means over the selection, decided by its extent rather than by the
|
|
550
481
|
* gesture that drew it (CODEC §"The table island").
|
|
@@ -826,8 +757,9 @@ class TableIslandView {
|
|
|
826
757
|
this.reseat(seat);
|
|
827
758
|
}
|
|
828
759
|
/**
|
|
829
|
-
* Where the focus sits, in terms a rebuild can restore it by:
|
|
830
|
-
*
|
|
760
|
+
* Where the focus sits, in terms a rebuild can restore it by: the caret's cell by
|
|
761
|
+
* position, that being the one seat the island has and not an element that survives
|
|
762
|
+
* one.
|
|
831
763
|
*
|
|
832
764
|
* A rebuild is what a changed rectangle costs, and the op that changed it usually
|
|
833
765
|
* says where the caret lands ({@link TableIslandView.write}'s `focus`). An undo says
|
|
@@ -837,28 +769,15 @@ class TableIslandView {
|
|
|
837
769
|
*/
|
|
838
770
|
focusedSeat() {
|
|
839
771
|
const held = this.cells.find((m) => m.view.hasFocus());
|
|
840
|
-
|
|
841
|
-
return { cell: { r: held.r, c: held.c } };
|
|
842
|
-
const active = this.dom.ownerDocument.activeElement;
|
|
843
|
-
for (const [key, grip] of this.grips) {
|
|
844
|
-
if (grip !== active)
|
|
845
|
-
continue;
|
|
846
|
-
const [axis, index] = key.split(':');
|
|
847
|
-
return { line: { axis: axis, index: Number(index) } };
|
|
848
|
-
}
|
|
849
|
-
return undefined;
|
|
772
|
+
return held && { r: held.r, c: held.c };
|
|
850
773
|
}
|
|
851
|
-
/** Put the
|
|
852
|
-
* the rectangle that is there now: the seat
|
|
853
|
-
* removed. A caller that has a landing of its own overrides this by running
|
|
774
|
+
/** Put the caret back where {@link TableIslandView.focusedSeat} found it, clamped by
|
|
775
|
+
* `focusCell` into the rectangle that is there now: the seat may be the cell the
|
|
776
|
+
* rebuild removed. A caller that has a landing of its own overrides this by running
|
|
777
|
+
* after. */
|
|
854
778
|
reseat(seat) {
|
|
855
|
-
if (seat
|
|
856
|
-
|
|
857
|
-
if (!seat?.line)
|
|
858
|
-
return;
|
|
859
|
-
const { floor, limit } = this.bounds(seat.line.axis, this.props());
|
|
860
|
-
const index = Math.max(floor, Math.min(seat.line.index, limit));
|
|
861
|
-
this.gripFor({ axis: seat.line.axis, index })?.focus();
|
|
779
|
+
if (seat)
|
|
780
|
+
this.focusCell(seat.r, seat.c);
|
|
862
781
|
}
|
|
863
782
|
/** One table row: its cells, each carrying whatever chrome hangs off it. */
|
|
864
783
|
row(props, r, s) {
|
|
@@ -894,12 +813,12 @@ class TableIslandView {
|
|
|
894
813
|
* press that travels drags it. Both are "this line", asked once with the pointer,
|
|
895
814
|
* and the dead zone is what tells them apart.
|
|
896
815
|
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
*
|
|
816
|
+
* Pointer chrome and nothing else: no key binds here and no route focuses it, the
|
|
817
|
+
* line verbs binding in the cell the selection runs through
|
|
818
|
+
* (§{@link TableIslandView.cellKeys}). Out of the tab order at no cost in reach,
|
|
819
|
+
* since a grip follows the cell it hangs in: a forward Tab is the cell traversal's
|
|
820
|
+
* before the browser gets there, and a backward one leaves the grid off cell (0,0)
|
|
821
|
+
* without passing a grip (§{@link TableIslandView.step}). */
|
|
903
822
|
grip(line, label) {
|
|
904
823
|
const btn = chromeButton('qm-table-grip', label, () => {
|
|
905
824
|
if (this.suppressClick)
|
|
@@ -914,7 +833,6 @@ class TableIslandView {
|
|
|
914
833
|
bar.appendChild(svg(GRIP[line.axis]));
|
|
915
834
|
btn.appendChild(bar);
|
|
916
835
|
btn.addEventListener('pointerdown', (e) => this.onGripDown(line, btn, e));
|
|
917
|
-
btn.addEventListener('keydown', this.lineKeys(line));
|
|
918
836
|
this.grips.set(lineKey(line), btn);
|
|
919
837
|
return btn;
|
|
920
838
|
}
|
|
@@ -924,11 +842,12 @@ class TableIslandView {
|
|
|
924
842
|
* the pointer out past the last line is the claim — so its name carries the verb for
|
|
925
843
|
* everything that does not read position.
|
|
926
844
|
*
|
|
927
|
-
* In the tab order, where a grip is not:
|
|
928
|
-
*
|
|
929
|
-
* at the last row and Tab past the last
|
|
930
|
-
*
|
|
931
|
-
* the focus rung draws the one it lands
|
|
845
|
+
* In the tab order, where a grip is not: growth is what the line verbs do not carry,
|
|
846
|
+
* so the column bar is the keyboard's only route to a new column, and the row bar is
|
|
847
|
+
* that control on the other axis, where Enter at the last row and Tab past the last
|
|
848
|
+
* cell reach the verb from inside a cell. They sit after the grid, so the Tab that
|
|
849
|
+
* declines off the last cell lands on them, and the focus rung draws the one it lands
|
|
850
|
+
* on (`codec/prose.css`). */
|
|
932
851
|
addBar(axis, label) {
|
|
933
852
|
const btn = chromeButton('qm-table-add', label, () => {
|
|
934
853
|
const props = this.props();
|
|
@@ -955,10 +874,14 @@ class TableIslandView {
|
|
|
955
874
|
view.updateState(next);
|
|
956
875
|
if (!tr.docChanged)
|
|
957
876
|
return;
|
|
877
|
+
// A cell edit is the caret's, so it retires the rectangle: a wash left up
|
|
878
|
+
// over text being typed says the next Backspace takes a rank, and it does
|
|
879
|
+
// not. The clear op is not this path — it writes through the leaf, and the
|
|
880
|
+
// reseed it comes back as changes no cell's own doc.
|
|
881
|
+
this.clearSelection();
|
|
958
882
|
const now = this.props();
|
|
959
883
|
this.write(withCell(now, r, c, cellFromDoc(next.doc, cellAt(now, r, c))));
|
|
960
884
|
},
|
|
961
|
-
handlePaste: (_view, event) => this.pasteIntoCell(r, c, event),
|
|
962
885
|
handleDOMEvents: {
|
|
963
886
|
focus: () => {
|
|
964
887
|
this.clearSelection();
|
|
@@ -1026,10 +949,10 @@ class TableIslandView {
|
|
|
1026
949
|
if (inlineSchema.marks.underline)
|
|
1027
950
|
marks['Mod-u'] = toggleMark(inlineSchema.marks.underline);
|
|
1028
951
|
// Up and down are the grid's own walk: nothing else moves the caret vertically,
|
|
1029
|
-
// and `focusCell` clamps, so neither can grow the table. Left and right
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
952
|
+
// and `focusCell` clamps, so neither can grow the table. Left and right do not
|
|
953
|
+
// traverse at all: at a text edge they would call what Tab and Shift-Tab already
|
|
954
|
+
// call, and inherit the append-past-the-last-cell that makes Tab a growth
|
|
955
|
+
// affordance and would make a caret key one.
|
|
1033
956
|
const walk = (dir) => {
|
|
1034
957
|
return (_state, _dispatch, view) => {
|
|
1035
958
|
const { selection } = view?.state ?? {};
|
|
@@ -1050,6 +973,34 @@ class TableIslandView {
|
|
|
1050
973
|
this.deleteSelection();
|
|
1051
974
|
return true;
|
|
1052
975
|
};
|
|
976
|
+
// The line verbs, over the held rectangle and nothing else: with none held every
|
|
977
|
+
// one of these declines and the key is the caret's, which is what lets them sit on
|
|
978
|
+
// keys a cell is already typing under. An arrow names the line on its own axis
|
|
979
|
+
// through the caret — the one already held steps to its neighbour and carries the
|
|
980
|
+
// caret, and any other rectangle turns into it, which is how a row becomes the
|
|
981
|
+
// column the caret is in. Alt moves the line instead, and over a rectangle that is
|
|
982
|
+
// not one it does nothing but keep the key: while a rectangle is held every arrow
|
|
983
|
+
// is its own, and an Alt+arrow handed back is the browser's Back on two platforms,
|
|
984
|
+
// mid-gesture.
|
|
985
|
+
const line = (axis, by, move) => {
|
|
986
|
+
return () => {
|
|
987
|
+
if (!this.selected)
|
|
988
|
+
return false;
|
|
989
|
+
const on = this.lineOn(axis);
|
|
990
|
+
if (move) {
|
|
991
|
+
if (on !== undefined)
|
|
992
|
+
this.moveLine({ axis, index: on }, by);
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
if (on === undefined) {
|
|
996
|
+
this.selectLine({ axis, index: axis === 'row' ? r : c });
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
const { floor, limit } = this.bounds(axis, this.props());
|
|
1000
|
+
this.selectLine({ axis, index: Math.max(floor, Math.min(on + by, limit)) });
|
|
1001
|
+
return true;
|
|
1002
|
+
};
|
|
1003
|
+
};
|
|
1053
1004
|
return {
|
|
1054
1005
|
...marks,
|
|
1055
1006
|
// One undo stack per leaf: a cell carries no history of its own, so Mod-z
|
|
@@ -1059,11 +1010,23 @@ class TableIslandView {
|
|
|
1059
1010
|
'Shift-Mod-z': () => redo(this.outer.state, this.outer.dispatch),
|
|
1060
1011
|
Backspace: erase,
|
|
1061
1012
|
Delete: erase,
|
|
1062
|
-
ArrowUp: walk('up'),
|
|
1063
|
-
ArrowDown: walk('down'),
|
|
1013
|
+
ArrowUp: chainCommands(line('row', -1, false), walk('up')),
|
|
1014
|
+
ArrowDown: chainCommands(line('row', 1, false), walk('down')),
|
|
1015
|
+
ArrowLeft: line('column', -1, false),
|
|
1016
|
+
ArrowRight: line('column', 1, false),
|
|
1017
|
+
'Alt-ArrowUp': line('row', -1, true),
|
|
1018
|
+
'Alt-ArrowDown': line('row', 1, true),
|
|
1019
|
+
'Alt-ArrowLeft': line('column', -1, true),
|
|
1020
|
+
'Alt-ArrowRight': line('column', 1, true),
|
|
1064
1021
|
Tab: () => this.step(r, c, 1),
|
|
1065
1022
|
'Shift-Tab': () => this.step(r, c, -1),
|
|
1066
1023
|
Enter: () => {
|
|
1024
|
+
// Over a rectangle it hands the caret back, which is the line verbs' own
|
|
1025
|
+
// exit: the caret never left the cell, so there is nowhere else to put it.
|
|
1026
|
+
if (this.selected) {
|
|
1027
|
+
this.clearSelection();
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1067
1030
|
const props = this.props();
|
|
1068
1031
|
if (r === rowCount(props) - 1)
|
|
1069
1032
|
this.write(insertRow(props, r), { r: r + 1, c });
|
|
@@ -1071,10 +1034,17 @@ class TableIslandView {
|
|
|
1071
1034
|
this.focusCell(r + 1, c);
|
|
1072
1035
|
return true;
|
|
1073
1036
|
},
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1037
|
+
// Escape climbs a rung a press: the caret's own row, then the island. The row
|
|
1038
|
+
// is the entry the band has no other route to — every line verb reads a held
|
|
1039
|
+
// rectangle, and this is the gesture that draws one from the keyboard — and it
|
|
1040
|
+
// is the row rather than the column because the column is one arrow further
|
|
1041
|
+
// on. What the press past the island means is the shell's
|
|
1042
|
+
// (VISUAL_EDITOR §"Settled and open").
|
|
1076
1043
|
Escape: () => {
|
|
1077
|
-
this.
|
|
1044
|
+
if (this.selected)
|
|
1045
|
+
this.selectIsland();
|
|
1046
|
+
else
|
|
1047
|
+
this.selectLine({ axis: 'row', index: r });
|
|
1078
1048
|
return true;
|
|
1079
1049
|
}
|
|
1080
1050
|
};
|
|
@@ -71,16 +71,6 @@ export declare function moveColumn(props: TableProps, c: number, by: number): Ta
|
|
|
71
71
|
* Alignment is the column's rather than the cells', so it survives a clear that spans one.
|
|
72
72
|
*/
|
|
73
73
|
export declare function clearCells(props: TableProps, r0: number, c0: number, r1: number, c1: number): TableProps;
|
|
74
|
-
/**
|
|
75
|
-
* A rectangle written in at `(r, c)`, the table growing to hold whatever runs past its
|
|
76
|
-
* edges: what a paste into a cell means (`clipboard.ts`). The alternative is clipping
|
|
77
|
-
* to the rectangle that is there, which silently drops the tail of what the author
|
|
78
|
-
* copied; growth drops nothing and every added rank is one the ops already remove.
|
|
79
|
-
*
|
|
80
|
-
* `aligns` is the table's own throughout: a column keeps its alignment under a paste,
|
|
81
|
-
* and a column the paste added arrives `none` like any other fresh one.
|
|
82
|
-
*/
|
|
83
|
-
export declare function pasteCells(props: TableProps, r: number, c: number, block: TableCell[][]): TableProps;
|
|
84
74
|
/** Set column `c`'s alignment: the one table capability the content round-trips
|
|
85
75
|
* today and nothing in the editor could reach. */
|
|
86
76
|
export declare function setAlign(props: TableProps, c: number, align: TableAlign): TableProps;
|
package/dist/core/codec/table.js
CHANGED
|
@@ -189,21 +189,6 @@ export function clearCells(props, r0, c0, r1, c1) {
|
|
|
189
189
|
aligns: props.aligns
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
|
-
/**
|
|
193
|
-
* A rectangle written in at `(r, c)`, the table growing to hold whatever runs past its
|
|
194
|
-
* edges: what a paste into a cell means (`clipboard.ts`). The alternative is clipping
|
|
195
|
-
* to the rectangle that is there, which silently drops the tail of what the author
|
|
196
|
-
* copied; growth drops nothing and every added rank is one the ops already remove.
|
|
197
|
-
*
|
|
198
|
-
* `aligns` is the table's own throughout: a column keeps its alignment under a paste,
|
|
199
|
-
* and a column the paste added arrives `none` like any other fresh one.
|
|
200
|
-
*/
|
|
201
|
-
export function pasteCells(props, r, c, block) {
|
|
202
|
-
const rows = allRows(props);
|
|
203
|
-
const height = Math.max(rows.length, r + block.length);
|
|
204
|
-
const width = Math.max(columnCount(props), ...block.map((row) => c + row.length));
|
|
205
|
-
return fromRows(Array.from({ length: height }, (_, i) => Array.from({ length: width }, (_, j) => block[i - r]?.[j - c] ?? rows[i]?.[j] ?? emptyCell())), props.aligns);
|
|
206
|
-
}
|
|
207
192
|
/** Set column `c`'s alignment: the one table capability the content round-trips
|
|
208
193
|
* today and nothing in the editor could reach. */
|
|
209
194
|
export function setAlign(props, c, align) {
|