@quillmark/svelte 0.26.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -4
- package/README.md +26 -8
- package/THEMING.md +40 -4
- package/dist/core/address.d.ts +35 -64
- package/dist/core/address.js +35 -55
- package/dist/core/bloom.d.ts +4 -10
- package/dist/core/bloom.js +14 -27
- package/dist/core/codec/blocks.d.ts +41 -43
- package/dist/core/codec/blocks.js +97 -130
- package/dist/core/codec/code.d.ts +7 -10
- package/dist/core/codec/code.js +37 -46
- package/dist/core/codec/decode.d.ts +19 -23
- package/dist/core/codec/decode.js +76 -111
- package/dist/core/codec/encode.d.ts +11 -15
- package/dist/core/codec/encode.js +60 -67
- package/dist/core/codec/field.d.ts +62 -95
- package/dist/core/codec/field.js +107 -179
- package/dist/core/codec/islands.d.ts +33 -41
- package/dist/core/codec/islands.js +76 -86
- package/dist/core/codec/lists.d.ts +17 -26
- package/dist/core/codec/lists.js +117 -157
- package/dist/core/codec/marks.d.ts +4 -3
- package/dist/core/codec/marks.js +8 -3
- package/dist/core/codec/prose.css +84 -46
- package/dist/core/codec/schema.d.ts +18 -23
- package/dist/core/codec/schema.js +84 -76
- package/dist/core/codec/slash.d.ts +42 -48
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-copy.d.ts +4 -3
- package/dist/core/codec/table-copy.js +25 -7
- package/dist/core/codec/table-view.d.ts +12 -12
- package/dist/core/codec/table-view.js +460 -383
- package/dist/core/codec/table.d.ts +94 -61
- package/dist/core/codec/table.js +331 -83
- package/dist/core/codec/tasks.d.ts +6 -0
- package/dist/core/codec/tasks.js +30 -3
- package/dist/core/errors.d.ts +15 -30
- package/dist/core/errors.js +2 -6
- package/dist/core/index.js +7 -12
- package/dist/core/lifecycle.d.ts +7 -16
- package/dist/core/lifecycle.js +10 -21
- package/dist/core/rebind.svelte.d.ts +10 -20
- package/dist/core/rebind.svelte.js +15 -27
- package/dist/core/rungs.d.ts +14 -11
- package/dist/core/rungs.js +17 -19
- package/dist/core/teardown.d.ts +4 -6
- package/dist/core/teardown.js +7 -10
- package/dist/core/theme.css +74 -54
- package/dist/index.js +3 -8
- package/dist/preset/app.css +71 -0
- package/dist/preset/index.css +8 -7
- package/dist/preset/recipes.css +19 -164
- package/dist/preset/scale.css +28 -18
- package/dist/preset/shell.css +157 -0
- package/dist/preview/Preview.svelte +8 -0
- package/dist/preview/bridge.d.ts +18 -22
- package/dist/preview/bridge.js +29 -58
- package/dist/preview/controller.d.ts +21 -29
- package/dist/preview/controller.js +14 -39
- package/dist/preview/geometry.d.ts +13 -34
- package/dist/preview/geometry.js +13 -34
- package/dist/preview/index.js +4 -16
- package/dist/preview/paint.d.ts +3 -3
- package/dist/preview/paint.js +28 -68
- package/dist/preview/strings.d.ts +1 -1
- package/dist/preview/strings.js +2 -14
- package/dist/preview/viewport.js +20 -15
- package/dist/preview/zoom.d.ts +21 -22
- package/dist/preview/zoom.js +53 -30
- package/dist/visual/ArrayField.svelte +77 -16
- package/dist/visual/BooleanField.svelte +28 -15
- package/dist/visual/Card.svelte +66 -51
- package/dist/visual/CardControls.svelte +5 -11
- package/dist/visual/DateField.svelte +20 -4
- package/dist/visual/EnumField.svelte +3 -0
- package/dist/visual/FieldHint.svelte +2 -1
- package/dist/visual/FormatPopover.svelte +82 -15
- package/dist/visual/FormatPopover.svelte.d.ts +4 -0
- package/dist/visual/MatrixAdd.svelte +30 -12
- package/dist/visual/MatrixField.svelte +24 -16
- package/dist/visual/NumberField.svelte +7 -0
- package/dist/visual/ObjectField.svelte +4 -4
- package/dist/visual/ProseField.svelte +0 -5
- package/dist/visual/TipsCard.svelte +11 -10
- package/dist/visual/VariantField.svelte +3 -1
- package/dist/visual/VisualEditorInner.svelte +19 -3
- package/dist/visual/controls.css +7 -2
- package/dist/visual/fonts.d.ts +18 -0
- package/dist/visual/fonts.js +59 -0
- package/dist/visual/icons/nodes.d.ts +0 -5
- package/dist/visual/icons/nodes.js +0 -4
- package/dist/visual/props.d.ts +14 -1
- package/dist/visual/strings.d.ts +7 -0
- package/dist/visual/strings.js +5 -2
- package/dist/visual/structure.d.ts +43 -87
- package/dist/visual/structure.js +42 -83
- package/package.json +2 -2
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/* The shell: the screen an app mounting two surfaces over one session is, drawn once
|
|
2
|
+
(THEMING §"The shell, if you are building a tool"). `recipes.css` dresses the page a
|
|
3
|
+
surface sits on; this is what it is mounted in, a tool where `.qm-page` is a document.
|
|
4
|
+
|
|
5
|
+
Every selector here computes at specificity zero, as the surfaces' own do: a host's
|
|
6
|
+
rule on the same element wins whatever order the sheets land in, and an app whose
|
|
7
|
+
stylesheet someone else writes (quillkit's app client, under an author's `app.css`)
|
|
8
|
+
needs no knowledge of which of these the client happened to set. `check:style` holds
|
|
9
|
+
the rank. This file mints nothing. */
|
|
10
|
+
|
|
11
|
+
/* Pinned to the viewport rather than sized to it: a page-height box still leaves the
|
|
12
|
+
root scroller counting the overflow its panes clip, and a wheel over the chrome drags
|
|
13
|
+
the whole app off the top. Three bands (a head, the body, an optional foot), and the
|
|
14
|
+
body takes what is left and may shrink below its content, so the panes in it own
|
|
15
|
+
their overflow and hold still while their contents move.
|
|
16
|
+
|
|
17
|
+
The one column is stated for the same reason the rows are: an implicit track is `auto`,
|
|
18
|
+
which is sized by what a band holds, and a `.qm-split` under one contributes its items'
|
|
19
|
+
max-content through its `fr` tracks. The shell would take the width of the widest thing
|
|
20
|
+
mounted in it instead of the viewport's, and every band would be that wide with the
|
|
21
|
+
second pane past the edge. A host holding its content to a maximum never sees it; one
|
|
22
|
+
spending the whole screen sees it on the first surface it mounts. */
|
|
23
|
+
:where(.qm-workspace) {
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset: 0;
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: minmax(0, 1fr);
|
|
28
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* A gesture past the end of a pane chains out to the viewport and bounces the empty
|
|
33
|
+
document, which is the last way a pinned shell moves. Conditioned on the workspace, so
|
|
34
|
+
a host's other routes keep a page's own feel. */
|
|
35
|
+
:where(html:has(.qm-workspace)) {
|
|
36
|
+
overscroll-behavior: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* A band's line: the row a head or a foot puts its parts on, wrapping, centred. That is
|
|
40
|
+
the whole of it. What a band is made of is the host's: the rule under it, the gutter
|
|
41
|
+
beside it, and the depth of it, because where a band ends and how tall it stands are
|
|
42
|
+
page-shell decisions: a site holding its content to a maximum runs the rule past it at
|
|
43
|
+
its own rhythm, and a one-screen tool has no maximum to hold to. */
|
|
44
|
+
:where(.qm-bar) {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-wrap: wrap;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: var(--qmh-space-2) var(--qmh-space-4);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Two even tracks for two surfaces, and no resizer, since a divider that can be dragged
|
|
52
|
+
is a control competing with what it divides. Each track may shrink below its content,
|
|
53
|
+
which is what lets the frame in it clip rather than grow.
|
|
54
|
+
|
|
55
|
+
The gap is the endorsed default and not a fact about splitting: two surfaces standing
|
|
56
|
+
apart on a page read as two things, which is what a host showing its instruments wants.
|
|
57
|
+
A host that spends the whole screen on the mounts closes it to nothing and carries a
|
|
58
|
+
hairline on one pane instead: one property, and the tracks are untouched. */
|
|
59
|
+
:where(.qm-split) {
|
|
60
|
+
display: grid;
|
|
61
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
62
|
+
grid-template-rows: minmax(0, 1fr);
|
|
63
|
+
gap: var(--qmh-space-4);
|
|
64
|
+
min-height: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* The band that switches them, and nothing at the widths where both tracks are showing
|
|
68
|
+
and there is nothing to switch. Where it stands is the host's, like any other band; what
|
|
69
|
+
goes in it is a pair of `.qm-control`s, which already carry the on-state a pressed
|
|
70
|
+
control takes. Its visibility is the preset's because the threshold is: a host hiding
|
|
71
|
+
its own tab row writes this file's number a second time, which is the one thing the
|
|
72
|
+
single source is for. */
|
|
73
|
+
:where(.qm-switch) {
|
|
74
|
+
display: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* One mount at a time under the threshold, rather than two of them abreast. Half a narrow
|
|
78
|
+
viewport is under the width either surface reads at, so the split stops being one and
|
|
79
|
+
the tracks become views of the same room: the host says which with `data-qm-show`, and
|
|
80
|
+
the attribute names the track rather than what is in it, so nothing here knows an editor
|
|
81
|
+
from a preview. No attribute is the first track.
|
|
82
|
+
|
|
83
|
+
The other track is `display: none` and not a clip, which is the whole of the hiding: it
|
|
84
|
+
leaves the tab order, hit-testing and the accessibility tree in one property, so the
|
|
85
|
+
pane needs no `inert` and the host needs no breakpoint in JS. Both stay mounted (a
|
|
86
|
+
preview binds its handle once, an editor holds a caret, a history and a scroll), so
|
|
87
|
+
the switch is presentational, never conditional. A pane hidden this way keeps the
|
|
88
|
+
pixels it had: the paint loop's band sweep no-ops on an empty visible set, and its
|
|
89
|
+
resize observer holds the last real width rather than rastering at the 0×0 box a hidden
|
|
90
|
+
pane reports, so what comes back at the width it left needs no paint at all. The
|
|
91
|
+
preview's caret follow spans the same window — a trip asked for against a pane with no
|
|
92
|
+
box is held and run when it has one (`preview/bridge.ts`), the pane opening at the
|
|
93
|
+
caret rather than several edits behind it.
|
|
94
|
+
|
|
95
|
+
Height is the second clause because a landscape phone passes the first one: it clears
|
|
96
|
+
the width with room for two tracks and has no body left to put them in. A desktop window
|
|
97
|
+
that short wants one mount for the same reason. */
|
|
98
|
+
@media (width < 50rem), (height < 30rem) {
|
|
99
|
+
:where(.qm-switch) {
|
|
100
|
+
display: flex;
|
|
101
|
+
gap: var(--qmh-space);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Even halves of the band, at the height a thumb needs. The floor is the recipe's and
|
|
105
|
+
not the host's, because the band exists only at the sizes where the pointer is a
|
|
106
|
+
finger: a host is free to stand the band deeper, and has nothing to gain from
|
|
107
|
+
standing it shallower than the thing it is tapping. */
|
|
108
|
+
:where(.qm-switch > *) {
|
|
109
|
+
flex: 1 1 0;
|
|
110
|
+
min-height: var(--qmh-tap);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:where(.qm-split) {
|
|
114
|
+
grid-template-columns: minmax(0, 1fr);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:where(.qm-split:not([data-qm-show='2']) > :nth-child(2)),
|
|
118
|
+
:where(.qm-split[data-qm-show='2'] > :nth-child(1)) {
|
|
119
|
+
display: none;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* The mounting site: an edge, a corner, and the clip that holds a surface to them, which
|
|
124
|
+
is all a surface painting its own column needs. No tone and no gutter: those are the
|
|
125
|
+
surface's own (THEMING §"Drop it in"). `border-box`, so a stated height is the height
|
|
126
|
+
it draws.
|
|
127
|
+
|
|
128
|
+
Positioned, because a clip reaches only what it is the containing block for: a
|
|
129
|
+
surface's out-of-flow parts (a parked description, an overlay) resolve against the
|
|
130
|
+
nearest positioned ancestor, and with none between them and the document they land at
|
|
131
|
+
their static position unclipped, leaving the page to scroll past its own foot by
|
|
132
|
+
however far the frame's contents run over.
|
|
133
|
+
|
|
134
|
+
The two minimums are what let a frame in a track clip rather than push: a grid or flex
|
|
135
|
+
item floors at its content, so a mount that paints wider than its column widens the
|
|
136
|
+
column instead. They ride the frame rather than the split's children, which keeps every
|
|
137
|
+
rule in this file landing where the host put a class. Outside a track they are the
|
|
138
|
+
initial value and do nothing. */
|
|
139
|
+
:where(.qm-frame) {
|
|
140
|
+
box-sizing: border-box;
|
|
141
|
+
position: relative;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
min-height: 0;
|
|
144
|
+
border: var(--qmh-border-width) solid var(--qmh-border);
|
|
145
|
+
border-radius: var(--qmh-radius);
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* One track is the whole width under the threshold, so a frame's sides and corners would
|
|
150
|
+
frame the screen rather than a pane. It keeps the rules above and below it, which part
|
|
151
|
+
it from the bands. After the frame, since both rank zero and the later one wins. */
|
|
152
|
+
@media (width < 50rem), (height < 30rem) {
|
|
153
|
+
:where(.qm-frame) {
|
|
154
|
+
border-inline: none;
|
|
155
|
+
border-radius: 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -178,6 +178,14 @@
|
|
|
178
178
|
pointer-events: none;
|
|
179
179
|
transition: opacity var(--_qm-reveal);
|
|
180
180
|
}
|
|
181
|
+
/* A screen with no hover has no pointer to bring it back, and the pill is the way back
|
|
182
|
+
to the fit there, so it stays. */
|
|
183
|
+
@media (hover: none) {
|
|
184
|
+
:global(:where(.qm-preview-zoom[data-idle])) {
|
|
185
|
+
opacity: 1;
|
|
186
|
+
pointer-events: auto;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
181
189
|
:global(:where(.qm-preview-zoom:focus-visible)) {
|
|
182
190
|
opacity: 1;
|
|
183
191
|
pointer-events: auto;
|
package/dist/preview/bridge.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { LiveSession } from '@quillmark/wasm';
|
|
2
2
|
import type { Landing } from '../core/address.js';
|
|
3
3
|
import type { PageSlot } from './paint.js';
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
4
|
+
/**
|
|
5
|
+
* Both trips are held while the pane has no box to measure against; the last one held runs when it
|
|
6
|
+
* has one again.
|
|
7
|
+
*/
|
|
6
8
|
export interface BridgeController {
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
+
/**
|
|
10
|
+
* Scroll `field`'s first box to centre; `false` when this compile places none for that address.
|
|
11
|
+
*/
|
|
9
12
|
scrollToField(field: string): boolean;
|
|
10
13
|
/**
|
|
11
|
-
* Bring the caret rect at `field`/`pos` into view
|
|
12
|
-
*
|
|
14
|
+
* Bring the caret rect at `field`/`pos` into view; a no-op off-content or when already clear of
|
|
15
|
+
* the fold.
|
|
13
16
|
*/
|
|
14
17
|
focusPosition(field: string, pos: number): void;
|
|
15
18
|
destroy(): void;
|
|
@@ -21,24 +24,17 @@ export interface VerticalSpan {
|
|
|
21
24
|
height: number;
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
|
-
* Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* The floor is what a zero-height rect gets, which bare intersection would score as
|
|
31
|
-
* clear at either edge. The cap is a third of the room the port has to spare, so a
|
|
32
|
-
* target too tall for its own height of clearance — a located line at zoom, a short
|
|
33
|
-
* split track — passes once centred and holds a band around it, rather than being
|
|
34
|
-
* re-centred by every keystroke. A target taller than the port is clear while it covers
|
|
35
|
-
* the port: no scroll shows more of it.
|
|
27
|
+
* Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the target's own
|
|
28
|
+
* height, so it scales with zoom as the caret does. The floor is {@link MIN_CLEARANCE}, for a
|
|
29
|
+
* zero-height rect. The cap is a third of the room the port has to spare, so a target too tall for
|
|
30
|
+
* its own clearance passes once centred rather than being re-centred by every keystroke. A target
|
|
31
|
+
* taller than the port is clear while it covers the port.
|
|
36
32
|
*/
|
|
37
33
|
export declare function clearOfTheFold(target: VerticalSpan, port: VerticalSpan): boolean;
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The last trip a pane with no box could not run. A cell so a rebuilt bridge inherits it
|
|
36
|
+
* (`controller.ts`).
|
|
37
|
+
*/
|
|
42
38
|
export interface HeldTrip {
|
|
43
39
|
run?: () => void;
|
|
44
40
|
}
|
package/dist/preview/bridge.js
CHANGED
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
import { boxesForField, rectToPercent, clickToPdfPt, pxToPt, applyPercentRect } from './geometry.js';
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* A pane hidden by the shell's narrow switch (`preset/shell.css`) measures 0×0, as does every box
|
|
4
|
+
* on it, which the clearance arithmetic would read as a caret clear of both edges.
|
|
5
|
+
*/
|
|
6
6
|
function hasBox(port) {
|
|
7
7
|
return port.width > 0 && port.height > 0;
|
|
8
8
|
}
|
|
9
|
-
/** What a rect reporting no height clears an edge by, in CSS px
|
|
10
|
-
* smallest zoom a page still reads at. */
|
|
9
|
+
/** What a rect reporting no height clears an edge by, in CSS px. */
|
|
11
10
|
const MIN_CLEARANCE = 8;
|
|
12
|
-
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
11
|
+
/**
|
|
12
|
+
* What a click may miss a field's ink by and still land on it, in CSS px of the page as drawn
|
|
13
|
+
* (PREVIEW.md §"Click bridge").
|
|
14
|
+
*/
|
|
16
15
|
const CLICK_SLACK_PX = 2;
|
|
17
16
|
/**
|
|
18
|
-
* Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* The floor is what a zero-height rect gets, which bare intersection would score as
|
|
25
|
-
* clear at either edge. The cap is a third of the room the port has to spare, so a
|
|
26
|
-
* target too tall for its own height of clearance — a located line at zoom, a short
|
|
27
|
-
* split track — passes once centred and holds a band around it, rather than being
|
|
28
|
-
* re-centred by every keystroke. A target taller than the port is clear while it covers
|
|
29
|
-
* the port: no scroll shows more of it.
|
|
17
|
+
* Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the target's own
|
|
18
|
+
* height, so it scales with zoom as the caret does. The floor is {@link MIN_CLEARANCE}, for a
|
|
19
|
+
* zero-height rect. The cap is a third of the room the port has to spare, so a target too tall for
|
|
20
|
+
* its own clearance passes once centred rather than being re-centred by every keystroke. A target
|
|
21
|
+
* taller than the port is clear while it covers the port.
|
|
30
22
|
*/
|
|
31
23
|
export function clearOfTheFold(target, port) {
|
|
32
24
|
if (target.height >= port.height)
|
|
@@ -36,19 +28,13 @@ export function clearOfTheFold(target, port) {
|
|
|
36
28
|
}
|
|
37
29
|
export function createBridge(session, container, slots, onPick, held = {}) {
|
|
38
30
|
const unlisten = [];
|
|
39
|
-
// Two rungs: `positionAt` over span-tracked content, `fieldAt` over every
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// being tracked, and the pick carries no `pos` there: a fabricated `0` would be an
|
|
43
|
-
// invented offset wearing a real one's type. No third rung hit-tests `regions()`,
|
|
44
|
-
// whose rects bound ink the field does not fill; the slack both rungs carry is what
|
|
45
|
-
// that rung would have been reached for.
|
|
31
|
+
// Two rungs: `positionAt` over span-tracked content, then `fieldAt` over every tracked
|
|
32
|
+
// placement (PREVIEW.md §"Click bridge"). The second carries no `pos`: a fabricated `0` would
|
|
33
|
+
// be an invented offset.
|
|
46
34
|
if (onPick) {
|
|
47
35
|
for (const { page, el } of slots) {
|
|
48
36
|
const handleClick = (ev) => {
|
|
49
|
-
// Re-read through the live array: a
|
|
50
|
-
// slot objects to re-cache `size`, and the click math must use the
|
|
51
|
-
// current compile's PageSize, not the one captured at bridge build.
|
|
37
|
+
// Re-read through the live array: a refresh swaps slot objects to re-cache `size`.
|
|
52
38
|
const slot = slots[page];
|
|
53
39
|
if (!slot)
|
|
54
40
|
return;
|
|
@@ -68,9 +54,8 @@ export function createBridge(session, container, slots, onPick, held = {}) {
|
|
|
68
54
|
unlisten.push(() => el.removeEventListener('click', handleClick));
|
|
69
55
|
}
|
|
70
56
|
}
|
|
71
|
-
// Where the % rect `pct` on `slot` currently sits
|
|
72
|
-
//
|
|
73
|
-
// so it displaces nothing and no layout survives the read.
|
|
57
|
+
// Where the % rect `pct` on `slot` currently sits: a throwaway absolute marker, measured and
|
|
58
|
+
// removed.
|
|
74
59
|
function measure(slot, pct) {
|
|
75
60
|
const marker = document.createElement('div');
|
|
76
61
|
applyPercentRect(marker, pct);
|
|
@@ -79,11 +64,7 @@ export function createBridge(session, container, slots, onPick, held = {}) {
|
|
|
79
64
|
marker.remove();
|
|
80
65
|
return rect;
|
|
81
66
|
}
|
|
82
|
-
// Centre `target` vertically in `port
|
|
83
|
-
// minimum trip instead, since the container is a vertical scroller and only a page
|
|
84
|
-
// wider than the port has anything to move there. `port` is the caller's own read: the
|
|
85
|
-
// marker `measure` places is absolute inside the slot, so nothing between that read
|
|
86
|
-
// and this one moves the container's box, and one forced layout serves the whole hop.
|
|
67
|
+
// Centre `target` vertically in `port`; the cross axis takes the minimum trip.
|
|
87
68
|
function centre(target, port) {
|
|
88
69
|
container.scrollTop += target.top + target.height / 2 - (port.top + port.height / 2);
|
|
89
70
|
reveal(target, port);
|
|
@@ -94,10 +75,8 @@ export function createBridge(session, container, slots, onPick, held = {}) {
|
|
|
94
75
|
else if (target.right > port.right)
|
|
95
76
|
container.scrollLeft += target.right - port.right;
|
|
96
77
|
}
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// would open where the caret was several edits ago. Guarded for jsdom, which ships no
|
|
100
|
-
// ResizeObserver and lays out nothing for one to report.
|
|
78
|
+
// Runs the held trip once the pane has a box again. Guarded for jsdom, which has no
|
|
79
|
+
// ResizeObserver.
|
|
101
80
|
if (typeof ResizeObserver !== 'undefined') {
|
|
102
81
|
const observer = new ResizeObserver(() => {
|
|
103
82
|
if (!held.run || !hasBox(container.getBoundingClientRect()))
|
|
@@ -117,14 +96,12 @@ export function createBridge(session, container, slots, onPick, held = {}) {
|
|
|
117
96
|
if (!slot)
|
|
118
97
|
return false;
|
|
119
98
|
const port = container.getBoundingClientRect();
|
|
120
|
-
//
|
|
121
|
-
// with no box yet is not a second no.
|
|
99
|
+
// Placed, so `true`; a pane with no box yet is not a no.
|
|
122
100
|
if (!hasBox(port)) {
|
|
123
101
|
held.run = () => void scrollToField(field);
|
|
124
102
|
return true;
|
|
125
103
|
}
|
|
126
|
-
// A discrete act
|
|
127
|
-
// the continuous hop below.
|
|
104
|
+
// A discrete act, so it centres every time.
|
|
128
105
|
centre(measure(slot, rectToPercent(box.rect, slot.size)), port);
|
|
129
106
|
return true;
|
|
130
107
|
}
|
|
@@ -135,21 +112,15 @@ export function createBridge(session, container, slots, onPick, held = {}) {
|
|
|
135
112
|
const slot = slots[region.page];
|
|
136
113
|
if (!slot)
|
|
137
114
|
return;
|
|
138
|
-
// Held before the
|
|
139
|
-
//
|
|
140
|
-
// a track is hidden. What is held is the request, not the trip, so it re-locates
|
|
141
|
-
// against whatever compile is current when the pane comes back.
|
|
115
|
+
// Held before the fold test, which would call a zero-sized pane clear. The request is held,
|
|
116
|
+
// so it re-locates against the compile current on return.
|
|
142
117
|
const port = container.getBoundingClientRect();
|
|
143
118
|
if (!hasBox(port)) {
|
|
144
119
|
held.run = () => focusPosition(field, pos);
|
|
145
120
|
return;
|
|
146
121
|
}
|
|
147
|
-
//
|
|
148
|
-
// the
|
|
149
|
-
// takes the scrollport back from the user on every one of them, including
|
|
150
|
-
// the ones that changed nothing about where the caret already was.
|
|
151
|
-
// Across, a page zoomed wider than the pane has a caret to keep in view too, and
|
|
152
|
-
// the least trip does it without moving the line the reader is on.
|
|
122
|
+
// Called per keystroke, so it moves the pane only when the caret has left the fold; across,
|
|
123
|
+
// the least trip.
|
|
153
124
|
const target = measure(slot, rectToPercent(region.rect, slot.size));
|
|
154
125
|
if (clearOfTheFold(target, port))
|
|
155
126
|
reveal(target, port);
|
|
@@ -7,15 +7,18 @@ export interface PreviewOptions {
|
|
|
7
7
|
container: HTMLElement;
|
|
8
8
|
/** Pages kept painted beyond the visible band. Default 1. */
|
|
9
9
|
margin?: number;
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
/**
|
|
11
|
+
* A click resolved to an address ({@link Landing}): a caret where the compile tracks the
|
|
12
|
+
* content under the point, the field alone where it tracks only the placement. Does not fire
|
|
13
|
+
* where it tracks neither.
|
|
14
|
+
*/
|
|
13
15
|
onPick?(at: Landing): void;
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
+
/**
|
|
17
|
+
* A page paint the backend refused ({@link EditorErrorHandler}); the error message state shows
|
|
18
|
+
* either way.
|
|
19
|
+
*/
|
|
16
20
|
onError?: EditorErrorHandler;
|
|
17
|
-
/** The surface's strings
|
|
18
|
-
* English. */
|
|
21
|
+
/** The surface's strings; unset keys take the package's English. */
|
|
19
22
|
strings?: PreviewStringsInput;
|
|
20
23
|
}
|
|
21
24
|
export interface PreviewController {
|
|
@@ -23,37 +26,26 @@ export interface PreviewController {
|
|
|
23
26
|
* apply-driven hop. */
|
|
24
27
|
refresh(change: ChangeSet): void;
|
|
25
28
|
/**
|
|
26
|
-
* Scroll `field`'s first box into view, and end any follow it outranks. `false`
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* that from a field the host misnamed. The editor's `focusField` is what
|
|
30
|
-
* distinguishes them.
|
|
29
|
+
* Scroll `field`'s first box into view, and end any follow it outranks. `false` when this
|
|
30
|
+
* compile places nothing at that address; the preview has no schema to tell that from a
|
|
31
|
+
* misnamed field.
|
|
31
32
|
*/
|
|
32
33
|
scrollToField(field: DocPath): boolean;
|
|
33
34
|
/**
|
|
34
|
-
* Bring the caret at `at` into view
|
|
35
|
-
* the
|
|
36
|
-
*
|
|
37
|
-
* Takes the editor's own `onCaretMove` payload, so the editor→preview half of
|
|
38
|
-
* the bridge is `onCaretMove={preview.focusPosition}` and translates nothing. A
|
|
39
|
-
* `ContentHit` carries both members and fits here too.
|
|
35
|
+
* Bring the caret at `at` into view; the pane moves only when the caret has left the fold.
|
|
36
|
+
* Takes the editor's `onCaretMove` payload, so `onCaretMove={preview.focusPosition}` wires it.
|
|
40
37
|
*/
|
|
41
38
|
focusPosition(at: Place): void;
|
|
42
39
|
/**
|
|
43
|
-
* End the follow: the pane stays where the user left it until the next
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
46
|
-
* The editor's `onActiveLeafChange` is the signal, firing for a form control as for
|
|
47
|
-
* a prose leaf: `onActiveLeafChange={preview.endFollow}`. A control has no caret
|
|
48
|
-
* coordinate to report, so unwired the follow keeps naming the leaf the focus left
|
|
49
|
-
* and every recompile pulls the pane back there. A prose leaf restarts it with its
|
|
50
|
-
* own next caret.
|
|
40
|
+
* End the follow: the pane stays where the user left it until the next `focusPosition`. Wire
|
|
41
|
+
* `onActiveLeafChange={preview.endFollow}`; a form control reports no caret, so unwired the
|
|
42
|
+
* follow keeps pulling the pane back to the leaf the focus left.
|
|
51
43
|
*/
|
|
52
44
|
endFollow(): void;
|
|
53
45
|
/**
|
|
54
|
-
* Draw every page at `zoom`, a fraction of actual size (`1` is 100%), held between
|
|
55
|
-
*
|
|
56
|
-
*
|
|
46
|
+
* Draw every page at `zoom`, a fraction of actual size (`1` is 100%), held between the pane's
|
|
47
|
+
* floor and 200%; `undefined` is the default, the widest page fitting the pane's width up to
|
|
48
|
+
* 125%.
|
|
57
49
|
*/
|
|
58
50
|
setZoom(zoom: number | undefined): void;
|
|
59
51
|
destroy(): void;
|
|
@@ -4,8 +4,6 @@ import { createBridge } from './bridge.js';
|
|
|
4
4
|
import { createViewport } from './viewport.js';
|
|
5
5
|
import { mergePreviewStrings } from './strings.js';
|
|
6
6
|
const CONTAINER_CLASS = 'qm-preview';
|
|
7
|
-
// The message states share one element; each carries `MESSAGE_CLASS` plus a state
|
|
8
|
-
// class, the hooks a consumer and the tests target.
|
|
9
7
|
const MESSAGE_CLASS = 'qm-preview-message';
|
|
10
8
|
const EMPTY_CLASS = 'qm-preview-empty';
|
|
11
9
|
const ERROR_CLASS = 'qm-preview-error';
|
|
@@ -14,8 +12,7 @@ export function createPreview(session, opts) {
|
|
|
14
12
|
const margin = opts.margin ?? 1;
|
|
15
13
|
const t = mergePreviewStrings(opts.strings);
|
|
16
14
|
container.classList.add(CONTAINER_CLASS);
|
|
17
|
-
// One element, restamped: the
|
|
18
|
-
// threw) are mutually exclusive.
|
|
15
|
+
// One element, restamped: the message states are mutually exclusive.
|
|
19
16
|
let message;
|
|
20
17
|
function showMessage(text, state) {
|
|
21
18
|
if (!message) {
|
|
@@ -29,12 +26,8 @@ export function createPreview(session, opts) {
|
|
|
29
26
|
message?.remove();
|
|
30
27
|
message = undefined;
|
|
31
28
|
}
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// there. A paint that unexpectedly throws is caught per-slot and surfaced
|
|
35
|
-
// through the shared message rather than aborting the observer callback
|
|
36
|
-
// mid-sweep (a paint is the one surface that can still refuse a counted page;
|
|
37
|
-
// brittle to leave uncaught).
|
|
29
|
+
// A paint that throws is caught per slot and surfaced through the message rather than aborting
|
|
30
|
+
// the observer callback mid-sweep.
|
|
38
31
|
const paintLoop = createPaintLoop(session, container, margin, (page, err) => {
|
|
39
32
|
reportError(opts.onError, {
|
|
40
33
|
code: 'paint-failed',
|
|
@@ -45,14 +38,10 @@ export function createPreview(session, opts) {
|
|
|
45
38
|
});
|
|
46
39
|
showMessage(t.renderFailed, ERROR_CLASS);
|
|
47
40
|
});
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// slots array is a stable identity but its members are swapped on reconcile, and
|
|
51
|
-
// the listeners are per slot.
|
|
41
|
+
// Rebuilt whenever the page count changes: the listeners are per slot and slots are swapped on
|
|
42
|
+
// reconcile.
|
|
52
43
|
let bridge;
|
|
53
|
-
// Outlives
|
|
54
|
-
// follow is re-asserted after a recompile, and a `scrollToField` the pane could not
|
|
55
|
-
// run has nothing else that would ask again for it.
|
|
44
|
+
// Outlives the bridge rebuild, so a trip held for a pane with no box crosses it.
|
|
56
45
|
const held = {};
|
|
57
46
|
function attach() {
|
|
58
47
|
bridge = createBridge(session, container, paintLoop.slots, opts.onPick, held);
|
|
@@ -61,9 +50,6 @@ export function createPreview(session, opts) {
|
|
|
61
50
|
bridge?.destroy();
|
|
62
51
|
bridge = undefined;
|
|
63
52
|
}
|
|
64
|
-
// Called at construction and after every `apply`, so a 0-page compile that later
|
|
65
|
-
// gains pages recovers. A counted page is the whole condition: every backend
|
|
66
|
-
// paints, and `paint`/`pageSize` refuse only a page the count excludes.
|
|
67
53
|
function render(pageCount, dirtyPages) {
|
|
68
54
|
if (pageCount === 0) {
|
|
69
55
|
paintLoop.refresh([], 0);
|
|
@@ -71,46 +57,35 @@ export function createPreview(session, opts) {
|
|
|
71
57
|
showMessage(t.noPages, EMPTY_CLASS);
|
|
72
58
|
return;
|
|
73
59
|
}
|
|
74
|
-
//
|
|
75
|
-
// clear any prior message before painting so a paint that throws mid-refresh
|
|
76
|
-
// leaves its error surfaced rather than have `hideMessage` clobber it.
|
|
60
|
+
// Clear the message before painting so an error from this paint is not clobbered.
|
|
77
61
|
const countChanged = pageCount !== paintLoop.slots.length;
|
|
78
62
|
hideMessage();
|
|
79
63
|
paintLoop.refresh(dirtyPages, pageCount);
|
|
80
|
-
// The slot set moved under the bridge's feet, or we are leaving a message state.
|
|
81
64
|
if (countChanged || !bridge) {
|
|
82
65
|
detach();
|
|
83
66
|
attach();
|
|
84
67
|
}
|
|
85
68
|
}
|
|
86
|
-
// Built before the first bridge
|
|
87
|
-
//
|
|
69
|
+
// Built before the first bridge so its ResizeObserver fires first: a re-shown pane re-fits
|
|
70
|
+
// before the bridge runs a held trip.
|
|
88
71
|
const viewport = createViewport(container, paintLoop.slots, paintLoop.setScale, t.resetZoom);
|
|
89
72
|
render(session.pageCount, []);
|
|
90
73
|
viewport.update();
|
|
91
|
-
// The last
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// layout's content is off-content for the whole burst: `focusPosition` no-ops and
|
|
95
|
-
// the pane sits still, and a caret event is otherwise the only thing that asks
|
|
96
|
-
// again. The re-locate is the preview's, not the consumer's: the staleness sits
|
|
97
|
-
// between two session queries this module owns both ends of.
|
|
74
|
+
// The last caret the editor reported, re-located after every recompile: `session.locate`
|
|
75
|
+
// answers against the last compiled layout, so a caret typed past it no-ops until the recompile
|
|
76
|
+
// lands.
|
|
98
77
|
let followed;
|
|
99
78
|
return {
|
|
100
79
|
refresh(change) {
|
|
101
80
|
render(change.pageCount, change.dirtyPages);
|
|
102
|
-
// The default fits the widest page, which a recompile can move.
|
|
103
81
|
viewport.update();
|
|
104
|
-
// Guarded like any other caret hop, so a caret already clear of the fold
|
|
105
|
-
// leaves a pane the user scrolled where they left it.
|
|
106
82
|
if (followed)
|
|
107
83
|
bridge?.focusPosition(followed.field, followed.pos);
|
|
108
84
|
},
|
|
109
85
|
scrollToField(field) {
|
|
110
86
|
const placed = bridge?.scrollToField(field) ?? false;
|
|
111
|
-
// The
|
|
112
|
-
// recompile
|
|
113
|
-
// for. A hop that placed nothing moved nothing, so the follow stands.
|
|
87
|
+
// The hop outranks the follow, which would otherwise pull the pane back at the next
|
|
88
|
+
// recompile.
|
|
114
89
|
if (placed)
|
|
115
90
|
followed = undefined;
|
|
116
91
|
return placed;
|