@qaiddev/quests-embed 1.3.0 → 1.5.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 +44 -0
- package/dist/embed.d.ts +32 -1
- package/dist/overflow.d.ts +49 -0
- package/dist/qaid-quests.js +357 -274
- package/dist/qaid-quests.js.map +1 -1
- package/dist/qaid-quests.umd.cjs +5 -5
- package/dist/qaid-quests.umd.cjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -348,8 +348,52 @@ const quests = new QaidQuests(config: QuestsConfig);
|
|
|
348
348
|
| Method | Description |
|
|
349
349
|
|--------|-------------|
|
|
350
350
|
| `getAnswers()` | Read-only snapshot of the answers collected so far, keyed by question id |
|
|
351
|
+
| `getCurrentQuestionId()` | Id of the question on screen, or `null` if the embed isn't on one (loading, finished, or unmounted) |
|
|
352
|
+
| `goToStep(questionId)` | Jump to a visible question. Returns `false` if it's unknown or hidden by an unmet `visibleIf`. Latched if the embed is still initializing |
|
|
353
|
+
| `update(questionnaire)` | Swap in a new questionnaire without re-mounting. Returns whether it was applied — see below |
|
|
351
354
|
| `destroy()` | Remove all DOM elements, event listeners, and injected styles. Safe to call multiple times |
|
|
352
355
|
|
|
356
|
+
#### `update(questionnaire)`
|
|
357
|
+
|
|
358
|
+
For hosts that render a questionnaire while it is being authored — a live
|
|
359
|
+
preview in an editor, say. Rebuilding the embed on every edit re-mounts the
|
|
360
|
+
shadow root, repaints the loading state, and re-runs both the theme fetch and
|
|
361
|
+
the create call; because that path awaits the network it always paints a blank
|
|
362
|
+
frame first, which is what makes a live preview flicker as the author types.
|
|
363
|
+
|
|
364
|
+
`update()` re-renders the header and the current step, synchronously. The
|
|
365
|
+
shadow root, the resolved theme, the response id and the answers so far all
|
|
366
|
+
survive, and focus is left wherever the host put it — it does not pull focus
|
|
367
|
+
into the form the way stepping through it does.
|
|
368
|
+
|
|
369
|
+
Answers are kept for questions that still exist and dropped for ones that
|
|
370
|
+
don't, so `getAnswers()` never reports an id the questionnaire has no question
|
|
371
|
+
for. The reader's place is kept the same way: if the question on screen is
|
|
372
|
+
still present and visible, the embed stays on it; otherwise the step index is
|
|
373
|
+
clamped into range.
|
|
374
|
+
|
|
375
|
+
It returns `false`, leaving the embed exactly as it was, when:
|
|
376
|
+
|
|
377
|
+
- the questionnaire has no questions,
|
|
378
|
+
- every question is gated off behind an unmet `visibleIf`, so there would be
|
|
379
|
+
nothing to render,
|
|
380
|
+
- or the reader has already completed the form — resuming a submitted response
|
|
381
|
+
isn't something the embed can decide for you, so rebuild if you want the new
|
|
382
|
+
form there.
|
|
383
|
+
|
|
384
|
+
Called before the embed has finished initializing, the update is latched and
|
|
385
|
+
applied as soon as it is ready (and reported as applied), the same way
|
|
386
|
+
`goToStep` is.
|
|
387
|
+
|
|
388
|
+
```js
|
|
389
|
+
const quests = new QaidQuests({ endpoint, questionnaire, container: "#preview" });
|
|
390
|
+
|
|
391
|
+
// later, as the author edits — no teardown, no refetch, no flicker
|
|
392
|
+
if (!quests.update(nextQuestionnaire)) {
|
|
393
|
+
// refused: rebuild if you need the new questionnaire on screen
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
353
397
|
### Server Protocol
|
|
354
398
|
|
|
355
399
|
The embed talks to your endpoint in three steps:
|
package/dist/embed.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Answers, QuestsConfig } from "./types";
|
|
1
|
+
import type { Answers, Questionnaire, QuestsConfig } from "./types";
|
|
2
2
|
export declare class QaidQuests {
|
|
3
3
|
private config;
|
|
4
4
|
private questionnaire;
|
|
@@ -8,6 +8,8 @@ export declare class QaidQuests {
|
|
|
8
8
|
private stepIndex;
|
|
9
9
|
private visibleQuestions;
|
|
10
10
|
private pendingGoToStep;
|
|
11
|
+
private pendingUpdate;
|
|
12
|
+
private suppressStepFocus;
|
|
11
13
|
private hasRenderedStep;
|
|
12
14
|
private answers;
|
|
13
15
|
private responseId;
|
|
@@ -26,6 +28,8 @@ export declare class QaidQuests {
|
|
|
26
28
|
private progressEl;
|
|
27
29
|
private progressFillEl;
|
|
28
30
|
private bodyEl;
|
|
31
|
+
private scrollCueEl;
|
|
32
|
+
private overflow;
|
|
29
33
|
private footerEl;
|
|
30
34
|
private savingEl;
|
|
31
35
|
private backdropEl;
|
|
@@ -99,6 +103,33 @@ export declare class QaidQuests {
|
|
|
99
103
|
* user's place after a forced re-mount.
|
|
100
104
|
*/
|
|
101
105
|
getCurrentQuestionId(): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* Swap in a new questionnaire without tearing the embed down.
|
|
108
|
+
*
|
|
109
|
+
* Built for editor previews, where the alternative — `destroy()` plus a
|
|
110
|
+
* fresh construction on every edit — re-mounts the shadow root, repaints
|
|
111
|
+
* the loading state, and re-runs both the theme fetch and the create
|
|
112
|
+
* call. Because that path awaits the network it always paints a blank
|
|
113
|
+
* frame first, which is what makes a live preview strobe while the author
|
|
114
|
+
* types. This re-renders the header and the current step and nothing
|
|
115
|
+
* else, synchronously: the shadow root, the resolved theme, the response
|
|
116
|
+
* id and the answers so far all survive.
|
|
117
|
+
*
|
|
118
|
+
* Answers are kept for questions that still exist and dropped for ones
|
|
119
|
+
* that don't, so `getAnswers()` never reports an id the questionnaire has
|
|
120
|
+
* no question for. The reader's place is kept the same way: if the
|
|
121
|
+
* question on screen is still present and visible, the embed stays on it,
|
|
122
|
+
* otherwise the step index is clamped into range.
|
|
123
|
+
*
|
|
124
|
+
* Returns whether the update was applied. It is refused, leaving the
|
|
125
|
+
* embed exactly as it was, when the questionnaire has no questions, or
|
|
126
|
+
* once the reader has completed the form — resuming a submitted response
|
|
127
|
+
* is not something this can decide on the host's behalf, so a host that
|
|
128
|
+
* wants the new form there should rebuild. Called before the embed has
|
|
129
|
+
* finished initializing, the update is latched and applied as soon as it
|
|
130
|
+
* is ready (and reported as applied), the same way `goToStep` is.
|
|
131
|
+
*/
|
|
132
|
+
update(questionnaire: Questionnaire): boolean;
|
|
102
133
|
/**
|
|
103
134
|
* Jump to the visible question with the given id. Returns true if
|
|
104
135
|
* the question is currently visible (and the embed navigated to
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping a step readable inside whatever box the host gives us.
|
|
3
|
+
*
|
|
4
|
+
* The card body is the scroll container. Two things were missing: it never
|
|
5
|
+
* adapted its own content to the height available (option tiles have width
|
|
6
|
+
* rules only, so a short container just clipped them), and when content did
|
|
7
|
+
* overflow there was no sign of it at all — the step simply looked cut off.
|
|
8
|
+
*
|
|
9
|
+
* This module owns both. It is DOM-only and free of embed state so it can be
|
|
10
|
+
* driven directly in tests.
|
|
11
|
+
*/
|
|
12
|
+
/** Where the scroll position currently sits. */
|
|
13
|
+
export type ScrollEdge = "none" | "top" | "middle" | "bottom";
|
|
14
|
+
/**
|
|
15
|
+
* Below this many pixels of available height, a step switches to its compact
|
|
16
|
+
* layout: smaller image tiles, tighter gaps. Chosen so the common 2-3 option
|
|
17
|
+
* step fits without scrolling in a ~360px tall container.
|
|
18
|
+
*/
|
|
19
|
+
export declare const COMPACT_HEIGHT = 360;
|
|
20
|
+
/** Which edges of `el` have content beyond them. */
|
|
21
|
+
export declare function scrollEdgeOf(el: {
|
|
22
|
+
scrollTop: number;
|
|
23
|
+
scrollHeight: number;
|
|
24
|
+
clientHeight: number;
|
|
25
|
+
}): ScrollEdge;
|
|
26
|
+
/**
|
|
27
|
+
* Reflect the scroll state onto the body as `data-qaid-scroll`, which the
|
|
28
|
+
* stylesheet turns into a fade mask, and onto the cue element's hidden state.
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyScrollState(body: HTMLElement, cue: HTMLElement | null): ScrollEdge;
|
|
31
|
+
/** Reflect the available height onto the card as `data-qaid-fit`. */
|
|
32
|
+
export declare function applyFitState(card: HTMLElement, availableHeight: number): "compact" | "roomy";
|
|
33
|
+
export interface OverflowWatcher {
|
|
34
|
+
/** Re-measure now — call after rendering a step. */
|
|
35
|
+
refresh(): void;
|
|
36
|
+
/** Detach every listener and observer. */
|
|
37
|
+
destroy(): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Watch `body` for scrolling and resizing, keeping the scroll and fit state
|
|
41
|
+
* attributes current.
|
|
42
|
+
*
|
|
43
|
+
* A ResizeObserver rather than a CSS container query on height: a container
|
|
44
|
+
* query needs `container-type: size`, which requires a definite height on both
|
|
45
|
+
* axes, and this card is deliberately content-sized when the host does not
|
|
46
|
+
* constrain it. Declaring it would collapse the body to nothing in exactly the
|
|
47
|
+
* inline case the embed supports.
|
|
48
|
+
*/
|
|
49
|
+
export declare function watchOverflow(body: HTMLElement, card: HTMLElement, cue: HTMLElement | null): OverflowWatcher;
|