@quario/viewer 0.5.0 → 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/CHANGELOG.md +48 -0
- package/README.md +19 -11
- package/lib/check.js +17 -1
- package/lib/index.d.ts +10 -8
- package/lib/index.js +93 -91
- package/lib/panel.js +11 -4
- package/lib/stage.js +37 -315
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-09-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`fonts` is validated as a host property.** It was the one the viewer never
|
|
15
|
+
checked, so a malformed record reached the target inside the render and came
|
|
16
|
+
back as a render failure about a report that was never at fault. Its shape is
|
|
17
|
+
now checked with the other host properties and named on `fonts` rather than
|
|
18
|
+
on the target's `options.fonts`. A face that will not parse, or a missing
|
|
19
|
+
parser, is still found while the report is measured and remains a render
|
|
20
|
+
failure there.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **A rejected host property reports as `host-option`.** `ViewerErrorKind`
|
|
25
|
+
gains a fourth member, so a host switching exhaustively over the kind needs
|
|
26
|
+
a case for it. A `page`, `zoom`, `filename`, `colorScheme` or `fonts` the
|
|
27
|
+
viewer rejected was reported as `mount-render` or `update-render` — a render
|
|
28
|
+
that was never attempted — and the panel said "Could not render the report",
|
|
29
|
+
which reads as the report being at fault. It now says the viewer is
|
|
30
|
+
misconfigured and that the report was not the problem.
|
|
31
|
+
|
|
32
|
+
A malformed `report` or `targets` still reports as a render failure: those
|
|
33
|
+
are refused where the render begins rather than when the property is written,
|
|
34
|
+
so nothing yet tells them apart from a render that failed.
|
|
35
|
+
|
|
36
|
+
## [0.6.0] - 2026-09-07
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **Numbers presented through `format` now show a fixed two fraction digits**,
|
|
41
|
+
the same as every other target: `1,000.00` where the preview showed `1,000`,
|
|
42
|
+
`21.00%` where it showed `21%`, and a currency's own minor units in place of
|
|
43
|
+
a universal two. The digits come from the engine, so a page on screen and
|
|
44
|
+
the PDF of it agree. A formatted cell is also up to three characters wider,
|
|
45
|
+
so a line that just fitted can wrap and move a page break.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **A page the browser cannot fully paint no longer fails the render.** An
|
|
50
|
+
image whose pixel data is corrupt past the size in its header left that page
|
|
51
|
+
blank _and_ rejected `renderComplete`, so a host awaiting it saw an unhandled
|
|
52
|
+
rejection instead of an answer, and every page after it on screen went
|
|
53
|
+
unpainted too. Such an image is now drawn as nothing and the page is drawn
|
|
54
|
+
around it, marking and all; `renderComplete` resolves `true`, `rendered`
|
|
55
|
+
fires, and no `error` event is raised. It never rejects for this reason
|
|
56
|
+
again: a page that cannot be drawn is blank, not a failed render.
|
|
57
|
+
|
|
10
58
|
## [0.5.0] - 2026-09-05
|
|
11
59
|
|
|
12
60
|
### Changed
|
package/README.md
CHANGED
|
@@ -97,13 +97,20 @@ non-bubbling, like `<img>`'s. Listen on the element. (Because the event is named
|
|
|
97
97
|
inline `onerror` attribute on the element would fire too; `window.onerror` never sees it.)
|
|
98
98
|
|
|
99
99
|
`renderComplete` awaits the newest render settling: `true` when it landed on the sheet with the pages
|
|
100
|
-
on screen
|
|
101
|
-
arrive on the `error` event. `rendered` fires at that same moment. Like every
|
|
102
|
-
answers for the newest render only. A superseded render's failure is reported to no
|
|
100
|
+
on screen finished trying to paint, `false` when it failed or there was nothing to render. It never
|
|
101
|
+
rejects; failures arrive on the `error` event. `rendered` fires at that same moment. Like every
|
|
102
|
+
outcome here, it answers for the newest render only. A superseded render's failure is reported to no
|
|
103
|
+
one.
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
An image the browser cannot decode — pixel data corrupt past the size in its header, which is all
|
|
106
|
+
the engine reads — is drawn as nothing, and the page is drawn around it. That is not a render
|
|
107
|
+
failure: `renderComplete` answers `true`, `rendered` fires, and no `error` event is raised. A face in
|
|
108
|
+
`fonts` is a different story: it is parsed while the report is measured, so bytes that will not parse
|
|
109
|
+
are a render error with a panel.
|
|
110
|
+
|
|
111
|
+
Your own mistakes surface on the same channel: a report that is not compiled, or a malformed
|
|
112
|
+
option property, becomes a `TypeError` naming the property, on the `error` event and the
|
|
113
|
+
[error panel](#errors). No particular target is required — the sheet is the layout's own.
|
|
107
114
|
|
|
108
115
|
## Lifecycle
|
|
109
116
|
|
|
@@ -250,7 +257,8 @@ export marks every page: it rides on the layout, so what you see is what the doc
|
|
|
250
257
|
licensed render carries none.
|
|
251
258
|
|
|
252
259
|
While a render is in flight a thin indeterminate bar sits on the toolbar's bottom edge, and the
|
|
253
|
-
viewer
|
|
260
|
+
viewer's inner container — the `.qv-viewer` div in its shadow root, not the element itself —
|
|
261
|
+
reads `aria-busy="true"`. It reports that the viewer is working, not how far along. The
|
|
254
262
|
engine streams events and cannot know how many are still coming.
|
|
255
263
|
|
|
256
264
|
There is no Print button, because the sheet is the wrong thing to print. Your page's stylesheets do
|
|
@@ -261,9 +269,9 @@ document, the same bytes the PDF export hands over. A host that wants its own Pr
|
|
|
261
269
|
two lines:
|
|
262
270
|
|
|
263
271
|
```js
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
const bytes = await
|
|
272
|
+
// The same compiled report and data the element holds, through the pdf()
|
|
273
|
+
// target from the quick start — so what prints is exactly the PDF export.
|
|
274
|
+
const bytes = await view.report.render(pdf(), view.data);
|
|
267
275
|
window.open(URL.createObjectURL(new Blob([bytes], { type: "application/pdf" })));
|
|
268
276
|
```
|
|
269
277
|
|
|
@@ -273,7 +281,7 @@ the export is, watermark and all.
|
|
|
273
281
|
## Errors
|
|
274
282
|
|
|
275
283
|
When a render or an export fails, the viewer says so on the **error panel**, a strip across the top
|
|
276
|
-
of the sheet carrying a short label and the error's own message. It replaces rather than stacks, is
|
|
284
|
+
of the sheet carrying a short label — "Could not render the report", "Could not update the report — showing the previous version", or "Could not export PDF" — and the error's own message. It replaces rather than stacks, is
|
|
277
285
|
dismissed by its own button, and is cleared by the next render that lands on the sheet. A successful
|
|
278
286
|
export leaves it up, because the panel describes what you are looking at and a download says
|
|
279
287
|
nothing about that.
|
package/lib/check.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Pure policy, no DOM: this module is what the Node suite pins.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { pageBox } from "@quario/layout";
|
|
12
|
+
import { checkFonts, pageBox } from "@quario/layout";
|
|
13
13
|
import { STEPS } from "./zoom.js";
|
|
14
14
|
|
|
15
15
|
/** @type {(message: string) => never} */
|
|
@@ -55,6 +55,22 @@ export let geometry = (page) => {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The font mapping from the `fonts` property. Only the shape reaches here: a
|
|
60
|
+
* face that will not parse, or a missing parser, is found while the report is
|
|
61
|
+
* measured and is a render failure there. The layout names the mistake; this
|
|
62
|
+
* names the property, the way `geometry` does.
|
|
63
|
+
*
|
|
64
|
+
* @param {any} fonts
|
|
65
|
+
*/
|
|
66
|
+
export let faces = (fonts) => {
|
|
67
|
+
try {
|
|
68
|
+
checkFonts(fonts, "fonts");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
fail(/** @type {Error} */ (error).message);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
58
74
|
/** @type {(mode: any, floor: number, ceiling: number) => boolean} */
|
|
59
75
|
let inRange = (mode, floor, ceiling) =>
|
|
60
76
|
[Number.isFinite(mode), mode >= floor, mode <= ceiling].every(Boolean);
|
package/lib/index.d.ts
CHANGED
|
@@ -24,16 +24,17 @@ export type ViewerFonts = LayoutFonts;
|
|
|
24
24
|
export type ViewableReport = CompiledReport;
|
|
25
25
|
|
|
26
26
|
/** Which failure an `error` event names. */
|
|
27
|
-
export type ViewerErrorKind = "mount-render" | "update-render" | "export";
|
|
27
|
+
export type ViewerErrorKind = "mount-render" | "update-render" | "export" | "host-option";
|
|
28
28
|
|
|
29
29
|
/** The `error` event's payload. */
|
|
30
30
|
export interface ViewerErrorDetail {
|
|
31
31
|
/** The caught value, exactly as thrown. */
|
|
32
32
|
error: unknown;
|
|
33
33
|
/**
|
|
34
|
-
* Which failure occurred: `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Which failure occurred: `host-option` for a property the viewer rejected,
|
|
35
|
+
* which reached no render at all; otherwise `mount-render` until a render
|
|
36
|
+
* has ever landed on the sheet, `update-render` after, and `export` for a
|
|
37
|
+
* download that could not be produced.
|
|
37
38
|
*/
|
|
38
39
|
kind: ViewerErrorKind;
|
|
39
40
|
}
|
|
@@ -97,10 +98,11 @@ export class QuarioViewer extends LitElement {
|
|
|
97
98
|
colorScheme: "light" | "dark" | "auto" | undefined;
|
|
98
99
|
/**
|
|
99
100
|
* The newest render settling: `true` when it landed on the sheet with the
|
|
100
|
-
* pages on screen
|
|
101
|
-
* render.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
101
|
+
* pages on screen finished trying to paint, `false` when it failed or there
|
|
102
|
+
* was nothing to render. A page the browser could not draw is blank and the
|
|
103
|
+
* render still landed. Never rejects — failures are the
|
|
104
|
+
* `error` event's — and like every outcome here it answers for the newest
|
|
105
|
+
* render only. `rendered` fires at that same moment.
|
|
104
106
|
*/
|
|
105
107
|
get renderComplete(): Promise<boolean>;
|
|
106
108
|
|
package/lib/index.js
CHANGED
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { Task, TaskStatus } from "@lit/task";
|
|
28
28
|
import { LitElement, html } from "lit";
|
|
29
|
+
import { Landing, failures, options } from "@quario/landing";
|
|
29
30
|
import { layout } from "@quario/layout";
|
|
30
31
|
import { BUTTON } from "./button.js";
|
|
31
32
|
import { CHROME, progress } from "./chrome.js";
|
|
32
|
-
import { exports, geometry, level, name, scheme } from "./check.js";
|
|
33
|
+
import { exports, faces, geometry, level, name, scheme } from "./check.js";
|
|
33
34
|
import { MENU, zoomMenu } from "./menu.js";
|
|
34
35
|
import { PANEL, label, panel } from "./panel.js";
|
|
35
36
|
import { SURFACE, stage } from "./stage.js";
|
|
@@ -39,12 +40,6 @@ import { wanted } from "./zoom.js";
|
|
|
39
40
|
/** @type {(report: unknown, targets: unknown) => boolean} */
|
|
40
41
|
let vacant = (report, targets) => report === undefined && targets === undefined;
|
|
41
42
|
|
|
42
|
-
/** @type {(error: unknown) => unknown} */
|
|
43
|
-
let said = (error) => {
|
|
44
|
-
// oxlint-disable-next-line typescript/no-base-to-string
|
|
45
|
-
return error && String(error);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
43
|
// The properties are described once, in the hand-written public declarations,
|
|
49
44
|
// and read back here — a second copy in JSDoc is a copy that drifts.
|
|
50
45
|
/** @import { ViewableReport, ViewerFonts, ViewerPage } from './index.d.ts' */
|
|
@@ -89,30 +84,36 @@ export class QuarioViewer extends LitElement {
|
|
|
89
84
|
// fails and a disconnect that does not. The template interpolates its
|
|
90
85
|
// element as a node, which lit-html leaves untouched across re-renders.
|
|
91
86
|
#stage = stage();
|
|
92
|
-
/** Whether a render has ever landed on the sheet — the mount/update boundary. */
|
|
93
|
-
#landed = false;
|
|
94
|
-
/** Whether the sheet stopped matching the properties while disconnected. */
|
|
95
|
-
#stale = false;
|
|
96
87
|
/** @type {"fit" | number} The live zoom mode; the controls move it. */
|
|
97
88
|
#mode = "fit";
|
|
98
89
|
/** @type {{ width: number, height: number, margin: number }} */
|
|
99
90
|
#box = geometry(undefined);
|
|
100
91
|
#name = "report";
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
#
|
|
108
|
-
|
|
92
|
+
/** The option check across one update cycle: first-failure-wins, the
|
|
93
|
+
* re-validate-everything rule, and the commit gate that keeps a `filename`
|
|
94
|
+
* write from clobbering the mode a reader clicked to. */
|
|
95
|
+
#options = options();
|
|
96
|
+
/** The render boundary — landed, stale, and the epoch that re-runs the task
|
|
97
|
+
* when its real arguments did not change. */
|
|
98
|
+
#landing = new Landing(this, { abort: () => this.#task.abort() });
|
|
99
|
+
/** The panel's model, with this element's own announce policy: every
|
|
100
|
+
* failure is drawn, because the viewer only re-runs when a host property or
|
|
101
|
+
* the epoch moves, so its rate is already bounded. The panel is a state and
|
|
102
|
+
* the error event is the log. */
|
|
103
|
+
#failures = failures();
|
|
109
104
|
/** Re-applies geometry and scale after the update that changed them. */
|
|
110
105
|
#reapply = false;
|
|
111
106
|
/** @type {Set<string>} The exports in flight; their buttons disable. */
|
|
112
107
|
#exporting = new Set();
|
|
113
108
|
/** @type {(() => void) | undefined} */
|
|
114
109
|
#unwatch;
|
|
115
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* The newest swap's paint, which `renderComplete` waits behind. Bare, with
|
|
112
|
+
* no catch of its own: the stage settles a page it cannot draw rather than
|
|
113
|
+
* rejecting, so the chain below is reached whatever the pixels did.
|
|
114
|
+
*
|
|
115
|
+
* @type {Promise<void>}
|
|
116
|
+
*/
|
|
116
117
|
#painting = Promise.resolve();
|
|
117
118
|
|
|
118
119
|
// The whole async pipeline: keyed on the render properties, re-run when one
|
|
@@ -124,7 +125,7 @@ export class QuarioViewer extends LitElement {
|
|
|
124
125
|
// show" result: unlike the task primitive's own initial-state symbol it
|
|
125
126
|
// settles `taskComplete`, which is what lets `renderComplete` always answer.
|
|
126
127
|
#task = new Task(this, {
|
|
127
|
-
args: () => [this.report, this.targets, this.data, this.page, this.fonts, this.#epoch],
|
|
128
|
+
args: () => [this.report, this.targets, this.data, this.page, this.fonts, this.#landing.epoch],
|
|
128
129
|
task: async ([report, targets, data, page, fonts], { signal }) => {
|
|
129
130
|
if (!this.#begin(report, targets)) return null;
|
|
130
131
|
// The sheet's own target: the layout list, on the host's page and
|
|
@@ -135,7 +136,7 @@ export class QuarioViewer extends LitElement {
|
|
|
135
136
|
// The engine takes no signal, so abandonment is the guards around this
|
|
136
137
|
// body; the check only spares the swap when the answer arrives after a
|
|
137
138
|
// disconnect mid-render.
|
|
138
|
-
if (signal
|
|
139
|
+
if (this.#landing.dropped(signal, this.isConnected)) return this.#landing.abandon();
|
|
139
140
|
return { list, fonts };
|
|
140
141
|
},
|
|
141
142
|
onComplete: (result) => {
|
|
@@ -143,14 +144,12 @@ export class QuarioViewer extends LitElement {
|
|
|
143
144
|
// The swap sizes every page before it settles, so the reader's place
|
|
144
145
|
// is held; the paint it awaits is what `rendered` waits for.
|
|
145
146
|
this.#painting = this.#stage.swap(result.list, result.fonts).then(() => {
|
|
146
|
-
this.#
|
|
147
|
-
this.#stale = false;
|
|
147
|
+
this.#landing.land();
|
|
148
148
|
// A render that landed on the sheet takes the panel down: the panel
|
|
149
149
|
// says what is wrong with what the reader is looking at, and this is
|
|
150
150
|
// the moment that stops being true. A successful export is not that
|
|
151
151
|
// moment.
|
|
152
|
-
this.#
|
|
153
|
-
this.#dismissed = false;
|
|
152
|
+
this.#failures.clear();
|
|
154
153
|
// After the paint, so the update the task queued has already run.
|
|
155
154
|
this.requestUpdate();
|
|
156
155
|
this.dispatchEvent(new CustomEvent("rendered"));
|
|
@@ -158,7 +157,7 @@ export class QuarioViewer extends LitElement {
|
|
|
158
157
|
},
|
|
159
158
|
onError: (error) => {
|
|
160
159
|
if (!this.isConnected) return;
|
|
161
|
-
this.#announce(error, this.#
|
|
160
|
+
this.#announce(error, this.#kindOf(error));
|
|
162
161
|
},
|
|
163
162
|
});
|
|
164
163
|
|
|
@@ -186,19 +185,22 @@ export class QuarioViewer extends LitElement {
|
|
|
186
185
|
}
|
|
187
186
|
|
|
188
187
|
/**
|
|
189
|
-
* The newest render settling: `true` when it landed on the sheet
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
188
|
+
* The newest render settling: `true` when it landed on the sheet — after
|
|
189
|
+
* its pages have finished trying to paint — `false` when it failed or there
|
|
190
|
+
* was nothing to render. A page the browser could not draw is blank and the
|
|
191
|
+
* render still landed, so `true` promises the sheet is done changing, not
|
|
192
|
+
* that every page carries pixels. It never rejects — a failed
|
|
193
|
+
* render is handled, on the panel and through the error event — and like
|
|
194
|
+
* every outcome here it answers for the newest render only.
|
|
193
195
|
*
|
|
194
196
|
* @returns {Promise<boolean>}
|
|
195
197
|
*/
|
|
196
198
|
get renderComplete() {
|
|
197
199
|
return this.updateComplete.then(() =>
|
|
198
200
|
this.#task.status === TaskStatus.INITIAL
|
|
199
|
-
? this.#landed
|
|
201
|
+
? this.#landing.landed
|
|
200
202
|
: this.#task.taskComplete.then(
|
|
201
|
-
() => this.#painting.then(() => this.#landed),
|
|
203
|
+
() => this.#painting.then(() => this.#landing.landed),
|
|
202
204
|
() => false,
|
|
203
205
|
),
|
|
204
206
|
);
|
|
@@ -218,7 +220,7 @@ export class QuarioViewer extends LitElement {
|
|
|
218
220
|
this.#checkOptions(changed);
|
|
219
221
|
// A new document is a new subject: dismissing the last failure said
|
|
220
222
|
// nothing about this one.
|
|
221
|
-
if (["data", "report"].some((key) => changed.has(key))) this.#
|
|
223
|
+
if (["data", "report"].some((key) => changed.has(key))) this.#failures.reopen();
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
render() {
|
|
@@ -238,7 +240,7 @@ export class QuarioViewer extends LitElement {
|
|
|
238
240
|
})}
|
|
239
241
|
</div>
|
|
240
242
|
<div class="qv-body">
|
|
241
|
-
${this.#
|
|
243
|
+
${this.#failures.showing ? panel(this.#failures.showing, () => this.#dismiss()) : ""}
|
|
242
244
|
${this.#stage.element}
|
|
243
245
|
</div>
|
|
244
246
|
</div>
|
|
@@ -267,14 +269,12 @@ export class QuarioViewer extends LitElement {
|
|
|
267
269
|
// property write landed while disconnected. The task's arguments did not
|
|
268
270
|
// change, so the epoch is what re-runs it; left alone, reparenting a
|
|
269
271
|
// settled viewer costs nothing.
|
|
270
|
-
this.#wake();
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
disconnectedCallback() {
|
|
274
275
|
super.disconnectedCallback();
|
|
275
276
|
// Abandon in-flight work and release the observer. The properties and the
|
|
276
277
|
// sheet persist: removal is destruction only in the collector's sense.
|
|
277
|
-
this.#task.abort();
|
|
278
278
|
this.#unwatch?.();
|
|
279
279
|
this.#unwatch = undefined;
|
|
280
280
|
}
|
|
@@ -290,27 +290,13 @@ export class QuarioViewer extends LitElement {
|
|
|
290
290
|
* @param {unknown} targets
|
|
291
291
|
*/
|
|
292
292
|
#begin(report, targets) {
|
|
293
|
-
if (this.#invalid) throw this.#invalid;
|
|
293
|
+
if (this.#options.invalid) throw this.#options.invalid;
|
|
294
294
|
if (vacant(report, targets)) return null;
|
|
295
|
-
if (!this.isConnected) return this.#abandon();
|
|
295
|
+
if (!this.isConnected) return this.#landing.abandon();
|
|
296
296
|
exports(report, targets);
|
|
297
297
|
return true;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
#abandon() {
|
|
301
|
-
this.#stale = true;
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** @param {() => void} run */
|
|
306
|
-
#take(run) {
|
|
307
|
-
try {
|
|
308
|
-
run();
|
|
309
|
-
} catch (error) {
|
|
310
|
-
this.#invalid ??= error;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
300
|
/**
|
|
315
301
|
* Each property validates and commits on its own, stashing the first
|
|
316
302
|
* failure: one bad property must not block a good write to another. All
|
|
@@ -327,42 +313,59 @@ export class QuarioViewer extends LitElement {
|
|
|
327
313
|
* @param {Map<string, unknown>} changed
|
|
328
314
|
*/
|
|
329
315
|
#checkOptions(changed) {
|
|
330
|
-
let
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
316
|
+
let moved = this.#options.recheck(this.hasUpdated, changed, [
|
|
317
|
+
(gate) => {
|
|
318
|
+
let box = geometry(this.page);
|
|
319
|
+
if (gate("page")) {
|
|
320
|
+
this.#box = box;
|
|
321
|
+
this.#reapply = true;
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
(gate) => {
|
|
325
|
+
// Committed only when the host wrote `zoom`, so re-validating on a
|
|
326
|
+
// filename change cannot clobber the mode the reader clicked to.
|
|
327
|
+
let mode = level(this.zoom);
|
|
328
|
+
if (gate("zoom")) {
|
|
329
|
+
this.#mode = mode;
|
|
330
|
+
this.#reapply = true;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
() => void (this.#name = name(this.filename)),
|
|
334
|
+
// Nothing to commit: the shape is the whole answer, and the faces
|
|
335
|
+
// themselves are loaded inside the render where a bad one is a render
|
|
336
|
+
// failure. Checked here so a malformed record is a mistake named on the
|
|
337
|
+
// property rather than one the target names on `options.fonts`.
|
|
338
|
+
() => faces(this.fonts),
|
|
339
|
+
(gate) => {
|
|
340
|
+
// Same gate as zoom/page: re-validate always, write the CSSOM pin only
|
|
341
|
+
// when this property changed, so a filename write does not dirty
|
|
342
|
+
// inherited color-scheme before layout.
|
|
343
|
+
let used = scheme(this.colorScheme);
|
|
344
|
+
if (gate("colorScheme")) this.style.colorScheme = used;
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
if (moved) this.#landing.epoch++;
|
|
359
348
|
}
|
|
360
349
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Which failure a throw from the task is. Identity, not a tag: `#begin`
|
|
352
|
+
* throws the very value `#checkOptions` stashed, so the one it stashed is
|
|
353
|
+
* the one host mistake this can name for certain — and a property the
|
|
354
|
+
* viewer rejected reached no render at all, so it carries no
|
|
355
|
+
* mount-versus-update distinction.
|
|
356
|
+
*
|
|
357
|
+
* `exports()`'s `report` and `targets` are host mistakes too and still
|
|
358
|
+
* report as a render: they are thrown from `#begin` rather than stashed, so
|
|
359
|
+
* nothing here tells them from a render that failed. Closing that means the
|
|
360
|
+
* task returning failures instead of throwing them, which is the channel
|
|
361
|
+
* quario-70jg.27 reworks — not worth doing twice.
|
|
362
|
+
*
|
|
363
|
+
* @param {unknown} error
|
|
364
|
+
* @returns {import('./index.d.ts').ViewerErrorKind}
|
|
365
|
+
*/
|
|
366
|
+
#kindOf(error) {
|
|
367
|
+
if (error !== undefined && error === this.#options.invalid) return "host-option";
|
|
368
|
+
return this.#landing.landed ? "update-render" : "mount-render";
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
/** @param {() => void} run */
|
|
@@ -399,7 +402,7 @@ export class QuarioViewer extends LitElement {
|
|
|
399
402
|
}
|
|
400
403
|
|
|
401
404
|
#dismiss() {
|
|
402
|
-
this.#
|
|
405
|
+
this.#failures.dismiss();
|
|
403
406
|
this.requestUpdate();
|
|
404
407
|
}
|
|
405
408
|
|
|
@@ -409,12 +412,11 @@ export class QuarioViewer extends LitElement {
|
|
|
409
412
|
* or anything after a disconnect — never come through here.
|
|
410
413
|
*
|
|
411
414
|
* @param {unknown} error The caught value.
|
|
412
|
-
* @param {
|
|
415
|
+
* @param {import('./index.d.ts').ViewerErrorKind} kind
|
|
413
416
|
* @param {string} [format] The export format's name, for that kind alone.
|
|
414
417
|
*/
|
|
415
418
|
#announce(error, kind, format) {
|
|
416
|
-
this.#
|
|
417
|
-
this.#dismissed = false;
|
|
419
|
+
this.#failures.announce(label(kind, format), error);
|
|
418
420
|
this.requestUpdate();
|
|
419
421
|
this.dispatchEvent(new CustomEvent("error", { detail: { error, kind } }));
|
|
420
422
|
}
|
package/lib/panel.js
CHANGED
|
@@ -80,16 +80,23 @@ export let PANEL = css`
|
|
|
80
80
|
* its length: that failure leaves the previous report on the sheet, and
|
|
81
81
|
* silently stale content is the thing the panel exists to prevent.
|
|
82
82
|
*
|
|
83
|
-
* @param {
|
|
83
|
+
* @param {import('./index.d.ts').ViewerErrorKind} kind
|
|
84
84
|
* @param {string} [format] The export format's name, for that kind alone.
|
|
85
85
|
* @returns {string}
|
|
86
86
|
*/
|
|
87
87
|
export let label = (kind, format) =>
|
|
88
88
|
kind === "export"
|
|
89
89
|
? "Could not export " + format
|
|
90
|
-
: kind === "
|
|
91
|
-
?
|
|
92
|
-
|
|
90
|
+
: kind === "host-option"
|
|
91
|
+
? // Named apart because the panel speaks to the reader, and where that
|
|
92
|
+
// reader is a playground's visitor the panel is the product rather
|
|
93
|
+
// than chrome (CONTEXT.md, "Error panel"). Told the report could not
|
|
94
|
+
// be rendered, they read it as their own document failing. A property
|
|
95
|
+
// they cannot see and cannot fix is where that reading is wrong.
|
|
96
|
+
"This viewer is misconfigured — the report itself was not the problem."
|
|
97
|
+
: kind === "update-render"
|
|
98
|
+
? "Could not update the report — showing the previous version"
|
|
99
|
+
: "Could not render the report";
|
|
93
100
|
|
|
94
101
|
/**
|
|
95
102
|
* What an error says. Not every throw is an `Error` — a host's registry
|
package/lib/stage.js
CHANGED
|
@@ -21,6 +21,19 @@
|
|
|
21
21
|
* asked a thousand-page report for gigabytes of backing store, and past what
|
|
22
22
|
* the browser would grant the pages simply came up blank.
|
|
23
23
|
*
|
|
24
|
+
* **A page the browser will not paint is blank, not a failure.** `start`
|
|
25
|
+
* swallows and nothing here rejects: the report laid out and the list reached
|
|
26
|
+
* the sheet, which is all a render promised, so the surfaces have no channel
|
|
27
|
+
* for it and deliberately grow none.
|
|
28
|
+
*
|
|
29
|
+
* Little reaches it. An image that will not decode is the layout's own to
|
|
30
|
+
* absorb — `paint` draws it as nothing and draws the page around it — and a
|
|
31
|
+
* face is parsed with fontkit while the report is measured, so bytes that will
|
|
32
|
+
* not parse are a render error long before the sheet. What is left is the face
|
|
33
|
+
* fontkit accepted and the browser refuses, which the browser suite drives by
|
|
34
|
+
* making `FontFace.prototype.load` reject under a record handed straight to
|
|
35
|
+
* `swap`.
|
|
36
|
+
*
|
|
24
37
|
* **A canvas on the sheet is never re-pointed at a second paint.** `paint()`
|
|
25
38
|
* awaits before it draws, so a canvas whose paint is still in flight is
|
|
26
39
|
* *retired* rather than painted over or emptied: it comes off the sheet, its
|
|
@@ -57,17 +70,12 @@
|
|
|
57
70
|
*/
|
|
58
71
|
|
|
59
72
|
import { css } from "lit";
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Space around the sheet, in px. Read from here and nowhere else: the stage's
|
|
64
|
-
* margin in the CSS below, the width `fit()` measures against, and the offset
|
|
65
|
-
* `scale()` converts a scroll position through.
|
|
66
|
-
*/
|
|
67
|
-
let GUTTER = 28;
|
|
73
|
+
import { GAP, GUTTER, sheet } from "@quario/landing";
|
|
74
|
+
import { PX_PER_POINT } from "@quario/layout";
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
// `GUTTER` and `GAP` come from `@quario/landing`, which walks the reach over
|
|
77
|
+
// them: the CSS below and the sheet's own arithmetic have to agree about where
|
|
78
|
+
// a page sits, and two copies of that is a reach that paints the wrong pages.
|
|
71
79
|
|
|
72
80
|
export let SURFACE = css`
|
|
73
81
|
/* Where scrollbars take width, the gutter is held whether one is showing or
|
|
@@ -137,307 +145,24 @@ export let stage = () => {
|
|
|
137
145
|
// inside the viewer. The `qv-*` names are documented as stable, so the
|
|
138
146
|
// narrower one keeps its name.
|
|
139
147
|
wrapper.className = "qv-stage";
|
|
140
|
-
let
|
|
141
|
-
|
|
142
|
-
wrapper.append(
|
|
148
|
+
let sheetEl = document.createElement("div");
|
|
149
|
+
sheetEl.className = "qv-sheet";
|
|
150
|
+
wrapper.append(sheetEl);
|
|
143
151
|
scroll.append(wrapper);
|
|
144
152
|
|
|
145
153
|
/** The page width to fit against before a list arrives, in points. */
|
|
146
154
|
let fallback = 0;
|
|
147
|
-
/** @type {any} */
|
|
148
|
-
let list = null;
|
|
149
|
-
/** @type {any} */
|
|
150
|
-
let fonts;
|
|
151
155
|
/** The percentage currently on screen. */
|
|
152
156
|
let applied = 100;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* What each backed page is carrying: the scale it was painted at, so a page
|
|
156
|
-
* re-entering the reach at that scale is not repainted and a scale change
|
|
157
|
-
* invalidates it, and the paint itself, so whoever asks second waits behind
|
|
158
|
-
* the paint the first caller started rather than being told it is done.
|
|
159
|
-
* Keyed by the canvas, so a swap's new pages start with nothing remembered
|
|
160
|
-
* and the pages it replaced take their entries with them — which is why the
|
|
161
|
-
* map is weak: nothing has to remember to forget them.
|
|
162
|
-
*
|
|
163
|
-
* @type {WeakMap<HTMLCanvasElement, { scale: number, painted: Promise<void> }>}
|
|
164
|
-
*/
|
|
165
|
-
let backed = new WeakMap();
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* The canvases whose paint has finished. A promise's settled state is not
|
|
169
|
-
* synchronously observable and this is the one question the rule below
|
|
170
|
-
* turns on: a canvas in here has nothing left that could draw into it, so
|
|
171
|
-
* it can be emptied or re-sized in place; one that is backed but absent
|
|
172
|
-
* here is still being painted, and must be retired instead. Weak on the
|
|
173
|
-
* same key as `backed`, so a retired canvas takes its membership with it.
|
|
174
|
-
*
|
|
175
|
-
* Membership is per paint, not per canvas — `start` takes a canvas out
|
|
176
|
-
* before painting it again, or a page settled at one scale would count as
|
|
177
|
-
* settled the moment it began painting at the next.
|
|
178
|
-
*
|
|
179
|
-
* @type {WeakSet<HTMLCanvasElement>}
|
|
180
|
-
*/
|
|
181
|
-
let settled = new WeakSet();
|
|
182
|
-
|
|
183
157
|
/** CSS pixels per point at the applied percentage. */
|
|
184
158
|
let ratio = () => (PX_PER_POINT * applied) / 100;
|
|
185
159
|
|
|
186
|
-
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
|
|
192
|
-
*/
|
|
193
|
-
let pageAt = (i) => /** @type {HTMLCanvasElement} */ (sheet.children[i]);
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Give a page's canvas its size on screen. The one answer to how big page
|
|
197
|
-
* `i` is at the applied percentage: `pageOf` builds a canvas with it and
|
|
198
|
-
* `sizeAll` writes it over the sheet after a scale change, so a page
|
|
199
|
-
* retired between the two cannot arrive sizeless and move the extent the
|
|
200
|
-
* reader is scrolling through.
|
|
201
|
-
*
|
|
202
|
-
* @type {(canvas: HTMLElement, i: number, px?: number) => void}
|
|
203
|
-
*/
|
|
204
|
-
let sizePage = (canvas, i, px = ratio()) => {
|
|
205
|
-
let each = list.pages[i];
|
|
206
|
-
canvas.style.width = each.width * px + "px";
|
|
207
|
-
canvas.style.height = each.height * px + "px";
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* One page of the list as a canvas: sized on screen, carrying no pixels
|
|
212
|
-
* yet, and named for a screen reader. Every canvas on the sheet is built
|
|
213
|
-
* here — a swap's and a retirement's alike — so a replacement is the same
|
|
214
|
-
* element in every respect but identity.
|
|
215
|
-
*
|
|
216
|
-
* @type {(i: number) => HTMLCanvasElement}
|
|
217
|
-
*/
|
|
218
|
-
let pageOf = (i) => {
|
|
219
|
-
let canvas = document.createElement("canvas");
|
|
220
|
-
canvas.className = "qv-page";
|
|
221
|
-
// A canvas is 300 x 150 until told otherwise, and that store counts. A
|
|
222
|
-
// page starts with none and takes one once it is inside the reach.
|
|
223
|
-
canvas.width = 0;
|
|
224
|
-
canvas.height = 0;
|
|
225
|
-
canvas.setAttribute("role", "img");
|
|
226
|
-
canvas.setAttribute("aria-label", "Page " + (i + 1) + " of " + list.pages.length);
|
|
227
|
-
sizePage(canvas, i);
|
|
228
|
-
return canvas;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Take page `i`'s canvas off the sheet and stand a fresh one in its place,
|
|
233
|
-
* answering with the replacement. A paint still in flight holds the old
|
|
234
|
-
* canvas's context and draws into something nobody is looking at, which is
|
|
235
|
-
* how a superseded paint is stopped here — by construction, rather than by
|
|
236
|
-
* a check the painter would have to make above its own draw (ADR 0046).
|
|
237
|
-
* The retired canvas takes its entries in `backed` and `settled` with it.
|
|
238
|
-
*
|
|
239
|
-
* Its pixels go back at once, by the same 0 × 0 idiom `release` uses: the
|
|
240
|
-
* store is what ADR 0043 rations, and leaving it to be collected whenever
|
|
241
|
-
* the superseded paint lets go of the context is the timing that ADR
|
|
242
|
-
* refuses. `paint()` does its whole `save`/draw/`restore` after its awaits,
|
|
243
|
-
* so emptying the canvas between them unbalances nothing — it just leaves
|
|
244
|
-
* every op clipped to nothing, which spares the raster work too.
|
|
245
|
-
*
|
|
246
|
-
* @type {(i: number) => HTMLCanvasElement}
|
|
247
|
-
*/
|
|
248
|
-
let retire = (i) => {
|
|
249
|
-
let old = pageAt(i);
|
|
250
|
-
let fresh = pageOf(i);
|
|
251
|
-
old.replaceWith(fresh);
|
|
252
|
-
old.width = 0;
|
|
253
|
-
old.height = 0;
|
|
254
|
-
return fresh;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Give page `i`'s pixels back. A page whose paint has settled is emptied in
|
|
259
|
-
* place: sizing the canvas to 0 × 0 is the one idiom that frees the store
|
|
260
|
-
* synchronously in every engine the viewer runs in, and the CSS size is
|
|
261
|
-
* untouched, so the page keeps its place in the extent and shows the
|
|
262
|
-
* sheet's white. A page still painting is retired instead — emptying it
|
|
263
|
-
* would leave that paint pointed at a canvas the next pass over the reach
|
|
264
|
-
* re-sizes and re-paints.
|
|
265
|
-
*
|
|
266
|
-
* @type {(i: number) => void}
|
|
267
|
-
*/
|
|
268
|
-
let release = (i) => {
|
|
269
|
-
let canvas = pageAt(i);
|
|
270
|
-
if (!backed.has(canvas)) return;
|
|
271
|
-
if (!settled.has(canvas)) return void retire(i);
|
|
272
|
-
backed.delete(canvas);
|
|
273
|
-
canvas.width = 0;
|
|
274
|
-
canvas.height = 0;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
/** Device pixels per point at the applied percentage: what a page is
|
|
278
|
-
* painted at, and what its backing store is sized in. */
|
|
279
|
-
let deviceScale = () => ratio() * (globalThis.devicePixelRatio || 1);
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Start painting page `i`, and answer with that paint. **The one place a
|
|
283
|
-
* paint begins, and the one place the rule is enforced**: a canvas whose
|
|
284
|
-
* paint is still in flight is retired here before a second one is pointed
|
|
285
|
-
* at it, so no caller can reach a live canvas with a second paint by
|
|
286
|
-
* forgetting to ask first (ADR 0046). A paint that failed is forgotten, so
|
|
287
|
-
* the next pass over the reach tries again instead of counting the page as
|
|
288
|
-
* painted; one that settles joins `settled`.
|
|
289
|
-
*
|
|
290
|
-
* @type {(i: number) => Promise<void>}
|
|
291
|
-
*/
|
|
292
|
-
let start = (i) => {
|
|
293
|
-
let canvas = pageAt(i);
|
|
294
|
-
if (backed.has(canvas) && !settled.has(canvas)) canvas = retire(i);
|
|
295
|
-
settled.delete(canvas);
|
|
296
|
-
let scale = deviceScale();
|
|
297
|
-
let each = list.pages[i];
|
|
298
|
-
canvas.width = Math.round(each.width * scale);
|
|
299
|
-
canvas.height = Math.round(each.height * scale);
|
|
300
|
-
let ctx = /** @type {CanvasRenderingContext2D} */ (canvas.getContext("2d"));
|
|
301
|
-
let painted = paint(ctx, each, { scale, fonts }).then(
|
|
302
|
-
() => {
|
|
303
|
-
settled.add(canvas);
|
|
304
|
-
},
|
|
305
|
-
(failure) => {
|
|
306
|
-
backed.delete(canvas);
|
|
307
|
-
throw failure;
|
|
308
|
-
},
|
|
309
|
-
);
|
|
310
|
-
backed.set(canvas, { scale, painted });
|
|
311
|
-
return painted;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Paint one page and answer with that paint — or with the paint already
|
|
316
|
-
* under way at this scale, which is what makes scrolling back over ground
|
|
317
|
-
* already covered free. Answering with the paint rather than with a
|
|
318
|
-
* resolved promise is what lets two callers share one page's paint and both
|
|
319
|
-
* settle behind its pixels.
|
|
320
|
-
*
|
|
321
|
-
* Takes the index, not the canvas: the page it paints may be retired out
|
|
322
|
-
* from under a caller, so a caller that handed one in would be left holding
|
|
323
|
-
* an element that is no longer on the sheet.
|
|
324
|
-
*
|
|
325
|
-
* @type {(i: number) => Promise<void>}
|
|
326
|
-
*/
|
|
327
|
-
let paintPage = (i) => {
|
|
328
|
-
let carrying = backed.get(pageAt(i));
|
|
329
|
-
if (carrying && carrying.scale === deviceScale()) return carrying.painted;
|
|
330
|
-
return start(i);
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Bumped per repaint, so a repaint overtaken by the next stops walking.
|
|
335
|
-
* What that guards is the backing store, not the pixels: a superseded loop
|
|
336
|
-
* would paint at the *current* scale — `paintPage` reads it afresh — but
|
|
337
|
-
* onto pages the newer reach has since dropped, re-backing pages that
|
|
338
|
-
* should be blank (ADR 0043). Stale pixels are `retire`'s business, not
|
|
339
|
-
* this one, so neither guard stands in for the other.
|
|
340
|
-
*
|
|
341
|
-
* It guards repaint against repaint, and nothing else. A repaint chooses
|
|
342
|
-
* its pages once and holds that list across its awaits, so a scroll pass
|
|
343
|
-
* releasing a page mid-repaint is one this loop will paint anyway; the
|
|
344
|
-
* store that leaves behind is bounded by the reach and goes back on the
|
|
345
|
-
* next pass over it.
|
|
346
|
-
*/
|
|
347
|
-
let epoch = 0;
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Give every page its CSS size. Callers run this before writing the scroll
|
|
351
|
-
* offsets back: the extent those offsets are clamped against is this one,
|
|
352
|
-
* and the reach below is read from the offsets once they are in.
|
|
353
|
-
*/
|
|
354
|
-
let sizeAll = () => {
|
|
355
|
-
if (!list) return;
|
|
356
|
-
let px = ratio();
|
|
357
|
-
for (let i = 0; i < list.pages.length; i++) sizePage(pageAt(i), i, px);
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* The indices of the pages in the reach — the ones the viewport shows, and
|
|
362
|
-
* everything within one viewport height above or below. Walked over the
|
|
363
|
-
* list's own geometry (the gutter, each page's height at the applied
|
|
364
|
-
* scale, the gap), never a layout read, so it costs nothing to ask
|
|
365
|
-
* mid-scroll — the whole list is walked because a multiply and an add per
|
|
366
|
-
* page is beneath measuring, and stopping early would be a second rule
|
|
367
|
-
* about where the reach ends. `clientHeight` is read afresh each time, so a
|
|
368
|
-
* pane that changed size changes the reach with it.
|
|
369
|
-
*
|
|
370
|
-
* @returns {number[]}
|
|
371
|
-
*/
|
|
372
|
-
let inReach = () => {
|
|
373
|
-
let view = scroll.clientHeight;
|
|
374
|
-
let top = scroll.scrollTop - view;
|
|
375
|
-
let bottom = scroll.scrollTop + 2 * view;
|
|
376
|
-
let px = ratio();
|
|
377
|
-
let y = GUTTER;
|
|
378
|
-
let found = [];
|
|
379
|
-
for (let [i, each] of list.pages.entries()) {
|
|
380
|
-
let height = each.height * px;
|
|
381
|
-
if (y + height >= top && y <= bottom) found.push(i);
|
|
382
|
-
y += height + GAP;
|
|
383
|
-
}
|
|
384
|
-
return found;
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Release every page the reach has left behind, and answer with the indices
|
|
389
|
-
* of the ones to keep, in the order they are painted in. The single
|
|
390
|
-
* definition of which pages carry pixels: the repaint below and the scroll
|
|
391
|
-
* pass both go through here, and neither holds a page the other released.
|
|
392
|
-
*
|
|
393
|
-
* Indices rather than elements, for the reason `paintPage` takes one: a
|
|
394
|
-
* page can be retired between this pass and the paint that follows it.
|
|
395
|
-
*
|
|
396
|
-
* @type {() => number[]}
|
|
397
|
-
*/
|
|
398
|
-
let keep = () => {
|
|
399
|
-
let wanted = new Set(inReach());
|
|
400
|
-
let kept = [];
|
|
401
|
-
// `retire` replaces one for one, so the count is fixed across the walk.
|
|
402
|
-
for (let i = 0, total = sheet.children.length; i < total; i++) {
|
|
403
|
-
if (wanted.has(i)) kept.push(i);
|
|
404
|
-
else release(i);
|
|
405
|
-
}
|
|
406
|
-
return kept;
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
// Paint the reach in order, yielding between pages and giving way to any
|
|
410
|
-
// repaint that started since. Sizing is the caller's, and comes first.
|
|
411
|
-
let repaint = async () => {
|
|
412
|
-
if (!list) return;
|
|
413
|
-
let mine = ++epoch;
|
|
414
|
-
for (let i of keep()) {
|
|
415
|
-
if (mine !== epoch) return;
|
|
416
|
-
await paintPage(i);
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* One reach pass a frame, however many scrolls and resizes ask for one. It
|
|
422
|
-
* keeps no epoch of its own, so a swap painting behind it is never cut off
|
|
423
|
-
* part-painted, and a page it starts is one a swap arriving at the same page
|
|
424
|
-
* waits behind rather than skips. Nothing awaits these paints here: a scroll
|
|
425
|
-
* is not a render and has no failure channel of its own, and a paint that
|
|
426
|
-
* failed is forgotten, so the next pass over the reach reports it the way
|
|
427
|
-
* any other paint does.
|
|
428
|
-
*/
|
|
429
|
-
let pending = false;
|
|
430
|
-
let follow = () => {
|
|
431
|
-
if (pending) return;
|
|
432
|
-
pending = true;
|
|
433
|
-
requestAnimationFrame(() => {
|
|
434
|
-
pending = false;
|
|
435
|
-
if (!list) return;
|
|
436
|
-
for (let i of keep()) paintPage(i).catch(() => {});
|
|
437
|
-
});
|
|
438
|
-
};
|
|
439
|
-
scroll.addEventListener("scroll", follow);
|
|
440
|
-
|
|
160
|
+
/** The paged sheet: the page canvases, their backing-store lifetime, the
|
|
161
|
+
* reach and its painting, in `@quario/landing`. It asks `ratio()` afresh,
|
|
162
|
+
* so the percentage this element applies is the scale it paints at — and
|
|
163
|
+
* the elements above stay this file's, because the CSS is the viewer's own
|
|
164
|
+
* public surface. */
|
|
165
|
+
let paged = sheet(scroll, sheetEl, ratio);
|
|
441
166
|
return {
|
|
442
167
|
element: scroll,
|
|
443
168
|
|
|
@@ -450,7 +175,7 @@ export let stage = () => {
|
|
|
450
175
|
fit: () => {
|
|
451
176
|
let usable = scroll.clientWidth - 2 * GUTTER;
|
|
452
177
|
// The list's page width, or the geometry's before one arrives.
|
|
453
|
-
let width = (
|
|
178
|
+
let width = (paged.width() ?? fallback) * PX_PER_POINT;
|
|
454
179
|
if (usable <= 0 || !width) return null;
|
|
455
180
|
return (usable / width) * 100;
|
|
456
181
|
},
|
|
@@ -473,7 +198,7 @@ export let stage = () => {
|
|
|
473
198
|
* so a zoom step costs a handful of pages however long the report is.
|
|
474
199
|
*/
|
|
475
200
|
scale: (percent) => {
|
|
476
|
-
let held =
|
|
201
|
+
let held = sheetEl.firstChild && {
|
|
477
202
|
top: scroll.scrollTop,
|
|
478
203
|
left: scroll.scrollLeft,
|
|
479
204
|
height: scroll.clientHeight,
|
|
@@ -481,13 +206,13 @@ export let stage = () => {
|
|
|
481
206
|
};
|
|
482
207
|
let ratioOf = percent / applied;
|
|
483
208
|
applied = percent;
|
|
484
|
-
sizeAll();
|
|
209
|
+
paged.sizeAll();
|
|
485
210
|
if (held) {
|
|
486
211
|
let middle = held.top + held.height / 2 - GUTTER;
|
|
487
212
|
scroll.scrollTop = GUTTER + middle * ratioOf - held.height / 2;
|
|
488
213
|
scroll.scrollLeft = (held.left + held.width / 2) * ratioOf - held.width / 2;
|
|
489
214
|
}
|
|
490
|
-
void repaint();
|
|
215
|
+
void paged.repaint();
|
|
491
216
|
},
|
|
492
217
|
|
|
493
218
|
/**
|
|
@@ -510,25 +235,22 @@ export let stage = () => {
|
|
|
510
235
|
* — and the reach is read from those offsets, so it is chosen after they
|
|
511
236
|
* are in. What the
|
|
512
237
|
* returned promise settles behind is the reach, which is what the caller's
|
|
513
|
-
* `renderComplete` means by "the pages on screen
|
|
238
|
+
* `renderComplete` means by "the pages on screen have finished trying to
|
|
239
|
+
* paint" — and **it never rejects**, because `start` swallows a page the
|
|
240
|
+
* browser will not draw and the reach walks on past it.
|
|
514
241
|
*/
|
|
515
242
|
swap: async (next, faces) => {
|
|
516
243
|
// Reading the offsets flushes layout, so only an actual reswap pays for
|
|
517
244
|
// it: on an empty sheet there is nothing scrolled to preserve.
|
|
518
|
-
let held =
|
|
245
|
+
let held = sheetEl.firstChild && {
|
|
519
246
|
top: scroll.scrollTop,
|
|
520
247
|
left: scroll.scrollLeft,
|
|
521
248
|
};
|
|
522
|
-
|
|
523
|
-
fonts = faces;
|
|
524
|
-
sheet.replaceChildren(
|
|
525
|
-
...next.pages.map((/** @type {any} */ _, /** @type {number} */ i) => pageOf(i)),
|
|
526
|
-
);
|
|
249
|
+
await paged.swap(next, faces);
|
|
527
250
|
if (held) {
|
|
528
251
|
scroll.scrollTop = held.top;
|
|
529
252
|
scroll.scrollLeft = held.left;
|
|
530
253
|
}
|
|
531
|
-
await repaint();
|
|
532
254
|
},
|
|
533
255
|
|
|
534
256
|
/**
|
|
@@ -541,7 +263,7 @@ export let stage = () => {
|
|
|
541
263
|
let observer = new ResizeObserver(() => {
|
|
542
264
|
// A resize that changes the fit repaints through `changed()`; one
|
|
543
265
|
// that does not still moved the viewport the reach is measured in.
|
|
544
|
-
follow();
|
|
266
|
+
paged.follow();
|
|
545
267
|
changed();
|
|
546
268
|
});
|
|
547
269
|
observer.observe(scroll);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The embeddable report viewer shell for quario — in the makings, not yet released",
|
|
5
5
|
"homepage": "https://getquario.com",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@lit/task": "^1.0.3",
|
|
47
|
-
"@quario/
|
|
47
|
+
"@quario/landing": "^0.1.0",
|
|
48
|
+
"@quario/layout": "^0.4.0",
|
|
48
49
|
"lit": "^3.3.3"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
@@ -53,18 +54,19 @@
|
|
|
53
54
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
54
55
|
"esbuild": "^0.28.2",
|
|
55
56
|
"exceljs": "^4.4.0",
|
|
56
|
-
"quario": "^0.
|
|
57
|
+
"quario": "^0.7.0",
|
|
57
58
|
"size-limit": "^13.0.3",
|
|
58
59
|
"typescript": "^7.0.2"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
|
-
"quario": "^0.
|
|
62
|
+
"quario": "^0.7.0"
|
|
62
63
|
},
|
|
63
64
|
"size-limit": [
|
|
64
65
|
{
|
|
65
66
|
"path": "lib/index.js",
|
|
66
67
|
"ignore": [
|
|
67
68
|
"quario",
|
|
69
|
+
"@quario/landing",
|
|
68
70
|
"@quario/layout",
|
|
69
71
|
"lit",
|
|
70
72
|
"@lit/task"
|