@quillmark/svelte 0.5.1 → 0.7.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 +6 -0
- package/THEMING.md +13 -17
- 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 +5 -6
- 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.d.ts +4 -0
- package/dist/core/codec/table-view.js +137 -158
- package/dist/core/codec/table.d.ts +0 -10
- package/dist/core/codec/table.js +0 -15
- package/dist/core/theme.css +357 -346
- package/dist/preset/recipes.css +25 -21
- package/dist/preview/Preview.svelte +4 -2
- package/dist/preview/bridge.d.ts +23 -0
- package/dist/preview/bridge.js +92 -35
- package/dist/preview/controller.d.ts +6 -5
- package/dist/preview/controller.js +13 -7
- package/dist/preview/geometry.d.ts +10 -9
- package/dist/preview/geometry.js +11 -13
- package/dist/preview/index.js +3 -2
- package/dist/preview/paint.js +9 -1
- package/dist/visual/ArrayField.svelte +246 -63
- package/dist/visual/ArrayField.svelte.d.ts +5 -1
- package/dist/visual/Card.svelte +84 -50
- package/dist/visual/EnumField.svelte +37 -3
- package/dist/visual/Field.svelte +28 -4
- package/dist/visual/FieldLabel.svelte +2 -2
- package/dist/visual/FormatPopover.svelte +13 -1
- package/dist/visual/ObjectField.svelte +128 -35
- package/dist/visual/ObjectField.svelte.d.ts +11 -4
- package/dist/visual/ProseArrayElement.svelte +22 -2
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -0
- package/dist/visual/SlashMenu.svelte +7 -0
- package/dist/visual/TipsCard.svelte +33 -13
- package/dist/visual/VariantField.svelte +95 -0
- package/dist/visual/VariantField.svelte.d.ts +21 -0
- package/dist/visual/VisualEditorInner.svelte +44 -21
- package/dist/visual/controls.css +282 -283
- package/dist/visual/diagnostics.d.ts +13 -11
- package/dist/visual/diagnostics.js +11 -9
- package/dist/visual/domid.d.ts +13 -0
- package/dist/visual/domid.js +16 -0
- package/dist/visual/hold.d.ts +17 -0
- package/dist/visual/hold.js +39 -0
- package/dist/visual/index.js +1 -1
- 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/motion.d.ts +9 -0
- package/dist/visual/motion.js +10 -2
- package/dist/visual/strings.d.ts +15 -0
- package/dist/visual/strings.js +3 -0
- package/dist/visual/structure.d.ts +32 -6
- 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
|
@@ -432,11 +432,11 @@ li.ProseMirror-selectednode::after {
|
|
|
432
432
|
which is drawn through a drag the pointer has travelled off — answering which line.
|
|
433
433
|
|
|
434
434
|
Opacity rather than absence, so every control keeps its box and is hit-tested wherever
|
|
435
|
-
it sits. A grip's reach is the pointer's
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
one a keyboard has reached. */
|
|
435
|
+
it sits. A grip's reach is the pointer's alone and it holds no focus, so it carries no
|
|
436
|
+
focus rung: what a keyboard draws a line with is the cell the selection runs through
|
|
437
|
+
(`table-view.ts` §`cellKeys`), and the wash and the selected grip's tint are what say
|
|
438
|
+
so. The two bars are in the tab order, the Tab that declines off the last cell landing
|
|
439
|
+
on them, so the rung there draws one a keyboard has reached. */
|
|
440
440
|
:is(.qm-table-grip, .qm-table-add) {
|
|
441
441
|
opacity: 0;
|
|
442
442
|
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
@@ -447,32 +447,21 @@ li.ProseMirror-selectednode::after {
|
|
|
447
447
|
:where(.qm-table-island:hover) :is(.qm-table-grip, .qm-table-add) {
|
|
448
448
|
opacity: var(--_qm-opacity-muted);
|
|
449
449
|
}
|
|
450
|
-
|
|
451
|
-
.qm-table-grip:is([aria-pressed='true'], .qm-table-lifted) {
|
|
450
|
+
.qm-table-add:is(:hover, :focus-visible),
|
|
451
|
+
.qm-table-grip:is(:hover, [aria-pressed='true'], .qm-table-lifted) {
|
|
452
452
|
opacity: 1;
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
/* The ring, on the
|
|
456
|
-
is its caret and a cell's is too, so the outline is dropped there; a control
|
|
457
|
-
up out of nothing has the reveal, and a ring under a keyboard is what says
|
|
458
|
-
there rather than on the paper behind it. Declared here rather than
|
|
459
|
-
`.qm-focus-ring`: that recipe is the visual tier's stylesheet, and the
|
|
460
|
-
the leaf, which is the codec's.
|
|
461
|
-
|
|
462
|
-
A selected grip takes none. Focus and selection arrive together there — the grip takes
|
|
463
|
-
the focus because the selection's keys bind on it — and the tint plus the wash already
|
|
464
|
-
say where the next command lands. A ring on top is a second box around the loudest
|
|
465
|
-
thing on the island, drawn on the press that made the selection: a pointer gesture
|
|
466
|
-
answering with a keyboard's indicator. Stated as `none` rather than left unset, because
|
|
467
|
-
what fills the gap is the browser's own ring, which is the same box in a colour the
|
|
468
|
-
surface does not own. */
|
|
469
|
-
:is(.qm-table-grip, .qm-table-add):focus-visible {
|
|
455
|
+
/* The ring, on the two add bars and nowhere else in this file. A prose leaf's focus
|
|
456
|
+
indicator is its caret and a cell's is too, so the outline is dropped there; a control
|
|
457
|
+
that comes up out of nothing has the reveal, and a ring under a keyboard is what says
|
|
458
|
+
the focus is there rather than on the paper behind it. Declared here rather than
|
|
459
|
+
reached for as `.qm-focus-ring`: that recipe is the visual tier's stylesheet, and the
|
|
460
|
+
island is inside the leaf, which is the codec's. */
|
|
461
|
+
.qm-table-add:focus-visible {
|
|
470
462
|
outline: var(--_qm-ring);
|
|
471
463
|
outline-offset: var(--_qm-ring-offset);
|
|
472
464
|
}
|
|
473
|
-
.qm-table-grip[aria-pressed='true']:focus-visible {
|
|
474
|
-
outline: none;
|
|
475
|
-
}
|
|
476
465
|
|
|
477
466
|
/* A drag in flight. The lifted line reads as picked up by tone, never a shadow:
|
|
478
467
|
elevation is a surface rung here, which says the same thing at both poles. The drop
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { Schema } from 'prosemirror-model';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a link carrying `href` renders as one. A value with no scheme is relative
|
|
4
|
+
* to the embedding page and has none to refuse.
|
|
5
|
+
*
|
|
6
|
+
* A refused href is unchanged: it stays on the mark and round-trips, so a document
|
|
7
|
+
* survives an editor that declines to make it clickable.
|
|
8
|
+
*/
|
|
9
|
+
export declare function rendersHref(href: string): boolean;
|
|
2
10
|
/** The full field schema: every block kind, container, mark, and island. */
|
|
3
11
|
export declare const blockSchema: Schema<string, string>;
|
|
4
12
|
/** The constrained inline schema: one paragraph, no block splitting, no
|
|
@@ -10,6 +10,33 @@
|
|
|
10
10
|
// not marks here (decorations).
|
|
11
11
|
import { Schema } from 'prosemirror-model';
|
|
12
12
|
import { islandBlockSpec, islandInlineSpec } from './islands.js';
|
|
13
|
+
// ── The href gate ───────────────────────────────────────────────────────────
|
|
14
|
+
// An `href` is an attribute value and not markup, so the markdown → typed node → DOM
|
|
15
|
+
// path that keeps a document's text from becoming tags never reaches it: what the
|
|
16
|
+
// mark carries is what `toDOM` emits, and `renderContent` paints marks outside a
|
|
17
|
+
// `contenteditable` (the tips card), where a click is a plain one.
|
|
18
|
+
//
|
|
19
|
+
// An allowlist, because the set a document can spell is open: naming the dangerous
|
|
20
|
+
// schemes instead loses to the first one this has not heard of. It holds what the
|
|
21
|
+
// surface has a caller for; a scheme reaching it later is a line.
|
|
22
|
+
const RENDERED_SCHEMES = new Set(['http', 'https', 'mailto', 'tel', 'ftp']);
|
|
23
|
+
/** A scheme and its colon: a letter, then letters, digits, `+`, `-` or `.` (RFC 3986). */
|
|
24
|
+
const SCHEME = /^([a-z][a-z0-9+.-]*):/i;
|
|
25
|
+
/** Dropped before the scheme is read, so a tab spliced into `javascript:` is tested
|
|
26
|
+
* as what it navigates to. Wider than the URL parser's own tab/newline rule: no
|
|
27
|
+
* scheme carries a control character, and the strip decides without rewriting. */
|
|
28
|
+
const IGNORED = /[\u0000-\u0020]/g;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a link carrying `href` renders as one. A value with no scheme is relative
|
|
31
|
+
* to the embedding page and has none to refuse.
|
|
32
|
+
*
|
|
33
|
+
* A refused href is unchanged: it stays on the mark and round-trips, so a document
|
|
34
|
+
* survives an editor that declines to make it clickable.
|
|
35
|
+
*/
|
|
36
|
+
export function rendersHref(href) {
|
|
37
|
+
const scheme = SCHEME.exec(href.replace(IGNORED, ''));
|
|
38
|
+
return scheme === null || RENDERED_SCHEMES.has(scheme[1].toLowerCase());
|
|
39
|
+
}
|
|
13
40
|
// ── Marks (the block and inline schemas share them; plaintext declares none) ─
|
|
14
41
|
const marks = {
|
|
15
42
|
// Order matters: it fixes mark-set sort order and parse precedence. `link`
|
|
@@ -26,7 +53,12 @@ const marks = {
|
|
|
26
53
|
parseDOM: [
|
|
27
54
|
{ tag: 'a[href]', getAttrs: (el) => ({ href: el.getAttribute('href') }) }
|
|
28
55
|
],
|
|
29
|
-
|
|
56
|
+
// A refused href draws as a bare span: the text stands, unstyled and
|
|
57
|
+
// unclickable, and the mark keeps its value for encode.
|
|
58
|
+
toDOM: (mark) => {
|
|
59
|
+
const href = mark.attrs.href;
|
|
60
|
+
return rendersHref(href) ? ['a', { href }, 0] : ['span', 0];
|
|
61
|
+
}
|
|
30
62
|
},
|
|
31
63
|
// The open-set escape hatch: an inert mark that renders as a bare span and
|
|
32
64
|
// re-emits its stored `type`/`attrs` on encode. `excludes: ''` lets marks of
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Plugin, type Command } from 'prosemirror-state';
|
|
1
|
+
import { EditorState, Plugin, type Command } from 'prosemirror-state';
|
|
2
2
|
import type { EditorView } from 'prosemirror-view';
|
|
3
3
|
import { type RangeAnchor } from './anchor.js';
|
|
4
4
|
/** The menu as the chrome sees it: the command names the query matched, the keyboard's
|
|
@@ -25,18 +25,21 @@ export interface SlashStrings {
|
|
|
25
25
|
* chrome's wording follows, and the visual `strings` set extends both. */
|
|
26
26
|
export declare const DEFAULT_SLASH_STRINGS: SlashStrings;
|
|
27
27
|
/**
|
|
28
|
-
* The vocabulary, in menu order: the names `SLASH_COMMANDS` is keyed
|
|
29
|
-
*
|
|
28
|
+
* The vocabulary a caret can run, in menu order: the names `SLASH_COMMANDS` is keyed
|
|
29
|
+
* by, each asked whether it would fire here. A row that declines when picked is worse
|
|
30
|
+
* than an absent one, and asking the command rather than restating its guards is what
|
|
31
|
+
* keeps the menu from drifting off what the shorthand does. So the offers narrow as the
|
|
32
|
+
* caret moves: the island alone mid-paragraph, no list at an item's head.
|
|
30
33
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* is, and a second door onto the same construct is a second thing to keep true. A table
|
|
34
|
-
* has no prefix a rule can fire on (a pipe row is not one), so it is the menu's alone.
|
|
34
|
+
* `state` is the state a pick leaves behind — the run already consumed — since that is
|
|
35
|
+
* the caret every command answers for (`blocks.ts`).
|
|
35
36
|
*/
|
|
36
|
-
export declare function slashItems(): string[];
|
|
37
|
-
/** The query filter: a case-insensitive prefix of the name
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
export declare function slashItems(state: EditorState): string[];
|
|
38
|
+
/** The query filter: a case-insensitive prefix of the name or of any of its words, which
|
|
39
|
+
* is how a command line completes and how a two-word name stays reachable by the noun a
|
|
40
|
+
* writer has — `/list` offers both lists. A prefix and not a substring: `note` is inside
|
|
41
|
+
* `footnote` and completes nothing. An empty query offers everything, and the
|
|
42
|
+
* vocabulary's order survives, so the menu never reorders under a keystroke. */
|
|
40
43
|
export declare function filterItems(items: string[], query: string): string[];
|
|
41
44
|
/** The live trigger: where the `/` sits, what has been typed after it, and which
|
|
42
45
|
* offer the keyboard is on. */
|
|
@@ -67,11 +70,10 @@ export declare function slashKeymap(): Record<string, Command>;
|
|
|
67
70
|
* highlight has one lane rather than a hover painting a second one. */
|
|
68
71
|
export declare function focusSlashItem(view: EditorView, name: string): void;
|
|
69
72
|
/**
|
|
70
|
-
* Run a command: one transaction that deletes the trigger run and applies the
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* exactly what they were computed for.
|
|
73
|
+
* Run a command: one transaction that deletes the trigger run and applies the command's
|
|
74
|
+
* own edit (`blocks.ts` §`consuming`). A command that declines consumes nothing. The
|
|
75
|
+
* menu never picks one, offering only what would run; `slashPick` is a public seam and
|
|
76
|
+
* can.
|
|
75
77
|
*/
|
|
76
78
|
export declare function runSlashItem(view: EditorView, name: string): void;
|
|
77
79
|
export {};
|
package/dist/core/codec/slash.js
CHANGED
|
Binary file
|
|
@@ -40,6 +40,10 @@ export interface TableViewDeps {
|
|
|
40
40
|
/** A cell took focus: the leaf's own `focus` handler never fires for one (a focus
|
|
41
41
|
* event does not bubble), so the active address would not follow the caret. */
|
|
42
42
|
onCellFocus: () => void;
|
|
43
|
+
/** The clearance a revealed caret keeps, in the leaf's own line box (`field.ts`), which
|
|
44
|
+
* is a cell's line box too. `undefined` where the derivation is out of reach (jsdom),
|
|
45
|
+
* which is PM's 5px default. */
|
|
46
|
+
clearance: number | undefined;
|
|
43
47
|
}
|
|
44
48
|
/** The `island_block` node view: a table island's editing surface, and the literal
|
|
45
49
|
* placeholder for every other island type. */
|
|
@@ -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();
|
|
@@ -950,15 +869,24 @@ class TableIslandView {
|
|
|
950
869
|
plugins: cellPlugins(this.cellKeys(r, c))
|
|
951
870
|
}),
|
|
952
871
|
attributes: { 'aria-label': name, class: 'qm-table-cell-editor' },
|
|
872
|
+
// A cell is one of the leaf's lines, so a reveal here keeps the leaf's
|
|
873
|
+
// clearance: PM's 5px default is the caret visible and unusable that rung
|
|
874
|
+
// exists to refuse (`field.ts`).
|
|
875
|
+
scrollThreshold: this.deps.clearance,
|
|
876
|
+
scrollMargin: this.deps.clearance,
|
|
953
877
|
dispatchTransaction: (tr) => {
|
|
954
878
|
const next = view.state.apply(tr);
|
|
955
879
|
view.updateState(next);
|
|
956
880
|
if (!tr.docChanged)
|
|
957
881
|
return;
|
|
882
|
+
// A cell edit is the caret's, so it retires the rectangle: a wash left up
|
|
883
|
+
// over text being typed says the next Backspace takes a rank, and it does
|
|
884
|
+
// not. The clear op is not this path — it writes through the leaf, and the
|
|
885
|
+
// reseed it comes back as changes no cell's own doc.
|
|
886
|
+
this.clearSelection();
|
|
958
887
|
const now = this.props();
|
|
959
888
|
this.write(withCell(now, r, c, cellFromDoc(next.doc, cellAt(now, r, c))));
|
|
960
889
|
},
|
|
961
|
-
handlePaste: (_view, event) => this.pasteIntoCell(r, c, event),
|
|
962
890
|
handleDOMEvents: {
|
|
963
891
|
focus: () => {
|
|
964
892
|
this.clearSelection();
|
|
@@ -1005,8 +933,12 @@ class TableIslandView {
|
|
|
1005
933
|
if (!mounted)
|
|
1006
934
|
return;
|
|
1007
935
|
const { view } = mounted;
|
|
936
|
+
// Flagged, for the reason the leaf's own landing is (`field.ts`, `setCaret`): PM
|
|
937
|
+
// focuses with `preventScroll`, so an unflagged dispatch lands the caret where no
|
|
938
|
+
// scroller has moved to — a Tab past the right edge of the horizontal scroller a
|
|
939
|
+
// wide table lives in, or the row an Enter on the last one appends below the fold.
|
|
1008
940
|
view.focus();
|
|
1009
|
-
view.dispatch(view.state.tr.setSelection(Selection.atEnd(view.state.doc)));
|
|
941
|
+
view.dispatch(view.state.tr.setSelection(Selection.atEnd(view.state.doc)).scrollIntoView());
|
|
1010
942
|
}
|
|
1011
943
|
/**
|
|
1012
944
|
* A cell's keys. Traversal is the island's link in the leaf's chain
|
|
@@ -1026,10 +958,10 @@ class TableIslandView {
|
|
|
1026
958
|
if (inlineSchema.marks.underline)
|
|
1027
959
|
marks['Mod-u'] = toggleMark(inlineSchema.marks.underline);
|
|
1028
960
|
// 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
|
-
//
|
|
961
|
+
// and `focusCell` clamps, so neither can grow the table. Left and right do not
|
|
962
|
+
// traverse at all: at a text edge they would call what Tab and Shift-Tab already
|
|
963
|
+
// call, and inherit the append-past-the-last-cell that makes Tab a growth
|
|
964
|
+
// affordance and would make a caret key one.
|
|
1033
965
|
const walk = (dir) => {
|
|
1034
966
|
return (_state, _dispatch, view) => {
|
|
1035
967
|
const { selection } = view?.state ?? {};
|
|
@@ -1050,6 +982,34 @@ class TableIslandView {
|
|
|
1050
982
|
this.deleteSelection();
|
|
1051
983
|
return true;
|
|
1052
984
|
};
|
|
985
|
+
// The line verbs, over the held rectangle and nothing else: with none held every
|
|
986
|
+
// one of these declines and the key is the caret's, which is what lets them sit on
|
|
987
|
+
// keys a cell is already typing under. An arrow names the line on its own axis
|
|
988
|
+
// through the caret — the one already held steps to its neighbour and carries the
|
|
989
|
+
// caret, and any other rectangle turns into it, which is how a row becomes the
|
|
990
|
+
// column the caret is in. Alt moves the line instead, and over a rectangle that is
|
|
991
|
+
// not one it does nothing but keep the key: while a rectangle is held every arrow
|
|
992
|
+
// is its own, and an Alt+arrow handed back is the browser's Back on two platforms,
|
|
993
|
+
// mid-gesture.
|
|
994
|
+
const line = (axis, by, move) => {
|
|
995
|
+
return () => {
|
|
996
|
+
if (!this.selected)
|
|
997
|
+
return false;
|
|
998
|
+
const on = this.lineOn(axis);
|
|
999
|
+
if (move) {
|
|
1000
|
+
if (on !== undefined)
|
|
1001
|
+
this.moveLine({ axis, index: on }, by);
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
if (on === undefined) {
|
|
1005
|
+
this.selectLine({ axis, index: axis === 'row' ? r : c });
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
const { floor, limit } = this.bounds(axis, this.props());
|
|
1009
|
+
this.selectLine({ axis, index: Math.max(floor, Math.min(on + by, limit)) });
|
|
1010
|
+
return true;
|
|
1011
|
+
};
|
|
1012
|
+
};
|
|
1053
1013
|
return {
|
|
1054
1014
|
...marks,
|
|
1055
1015
|
// One undo stack per leaf: a cell carries no history of its own, so Mod-z
|
|
@@ -1059,11 +1019,23 @@ class TableIslandView {
|
|
|
1059
1019
|
'Shift-Mod-z': () => redo(this.outer.state, this.outer.dispatch),
|
|
1060
1020
|
Backspace: erase,
|
|
1061
1021
|
Delete: erase,
|
|
1062
|
-
ArrowUp: walk('up'),
|
|
1063
|
-
ArrowDown: walk('down'),
|
|
1022
|
+
ArrowUp: chainCommands(line('row', -1, false), walk('up')),
|
|
1023
|
+
ArrowDown: chainCommands(line('row', 1, false), walk('down')),
|
|
1024
|
+
ArrowLeft: line('column', -1, false),
|
|
1025
|
+
ArrowRight: line('column', 1, false),
|
|
1026
|
+
'Alt-ArrowUp': line('row', -1, true),
|
|
1027
|
+
'Alt-ArrowDown': line('row', 1, true),
|
|
1028
|
+
'Alt-ArrowLeft': line('column', -1, true),
|
|
1029
|
+
'Alt-ArrowRight': line('column', 1, true),
|
|
1064
1030
|
Tab: () => this.step(r, c, 1),
|
|
1065
1031
|
'Shift-Tab': () => this.step(r, c, -1),
|
|
1066
1032
|
Enter: () => {
|
|
1033
|
+
// Over a rectangle it hands the caret back, which is the line verbs' own
|
|
1034
|
+
// exit: the caret never left the cell, so there is nowhere else to put it.
|
|
1035
|
+
if (this.selected) {
|
|
1036
|
+
this.clearSelection();
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1067
1039
|
const props = this.props();
|
|
1068
1040
|
if (r === rowCount(props) - 1)
|
|
1069
1041
|
this.write(insertRow(props, r), { r: r + 1, c });
|
|
@@ -1071,10 +1043,17 @@ class TableIslandView {
|
|
|
1071
1043
|
this.focusCell(r + 1, c);
|
|
1072
1044
|
return true;
|
|
1073
1045
|
},
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1046
|
+
// Escape climbs a rung a press: the caret's own row, then the island. The row
|
|
1047
|
+
// is the entry the band has no other route to — every line verb reads a held
|
|
1048
|
+
// rectangle, and this is the gesture that draws one from the keyboard — and it
|
|
1049
|
+
// is the row rather than the column because the column is one arrow further
|
|
1050
|
+
// on. What the press past the island means is the shell's
|
|
1051
|
+
// (VISUAL_EDITOR §"Settled and open").
|
|
1076
1052
|
Escape: () => {
|
|
1077
|
-
this.
|
|
1053
|
+
if (this.selected)
|
|
1054
|
+
this.selectIsland();
|
|
1055
|
+
else
|
|
1056
|
+
this.selectLine({ axis: 'row', index: r });
|
|
1078
1057
|
return true;
|
|
1079
1058
|
}
|
|
1080
1059
|
};
|