@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.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 +658 -714
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +1 -21
- package/dist/attempt/TestSession.js +16 -102
- package/dist/components/ItemScope.custom-element.d.ts +1 -0
- package/dist/components/ItemScope.custom-element.js +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
- package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
- package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/components/item-scope-element.d.ts +2 -0
- package/dist/components/item-scope-element.js +2 -0
- package/dist/context/assessment-toolkit-context.d.ts +33 -6
- package/dist/index.d.ts +25 -50
- package/dist/index.js +15 -45
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
- package/dist/policy/core/ToolPolicyEngine.js +203 -56
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +95 -74
- package/dist/policy/core/decision-types.d.ts +98 -55
- package/dist/policy/core/feature-decision.d.ts +28 -19
- package/dist/policy/core/feature-decision.js +15 -11
- package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
- package/dist/policy/core/pnp-policy-inputs.js +40 -88
- package/dist/policy/core/provenance.d.ts +10 -5
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/core/tool-parameters.d.ts +11 -0
- package/dist/policy/core/tool-parameters.js +15 -0
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
- package/dist/policy/sources/PnpPolicySource.js +133 -132
- package/dist/runtime/SectionControllerBinding.d.ts +283 -0
- package/dist/runtime/SectionControllerBinding.js +456 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
- package/dist/runtime/SectionRuntimeEngine.js +30 -372
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
- package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
- package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
- package/dist/runtime/adapter/dom-event-bridge.js +17 -44
- package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
- package/dist/runtime/adapter/subscriber-fanout.js +2 -6
- package/dist/runtime/catalog-context.d.ts +19 -0
- package/dist/runtime/catalog-context.js +28 -0
- package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
- package/dist/runtime/composition-emit-scheduler.js +10 -16
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +13 -0
- package/dist/runtime/content-region.js +29 -0
- package/dist/runtime/content-registration.d.ts +13 -0
- package/dist/runtime/content-registration.js +25 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
- package/dist/runtime/core/SectionEngineCore.js +2 -6
- package/dist/runtime/core/cohort.d.ts +1 -6
- package/dist/runtime/core/cohort.js +1 -10
- package/dist/runtime/core/engine-input.d.ts +18 -30
- package/dist/runtime/core/engine-input.js +11 -12
- package/dist/runtime/core/engine-output.d.ts +17 -40
- package/dist/runtime/core/engine-output.js +9 -21
- package/dist/runtime/core/engine-readiness.d.ts +5 -37
- package/dist/runtime/core/engine-readiness.js +8 -41
- package/dist/runtime/core/engine-resolver.d.ts +46 -74
- package/dist/runtime/core/engine-resolver.js +18 -43
- package/dist/runtime/core/engine-state.d.ts +28 -36
- package/dist/runtime/core/engine-state.js +18 -21
- package/dist/runtime/core/engine-transition.d.ts +14 -19
- package/dist/runtime/core/engine-transition.js +74 -53
- package/dist/runtime/engine.d.ts +16 -19
- package/dist/runtime/engine.js +12 -19
- package/dist/runtime/page-warnings.d.ts +13 -0
- package/dist/runtime/page-warnings.js +22 -0
- package/dist/runtime/policy-input-forwarding.d.ts +25 -0
- package/dist/runtime/policy-input-forwarding.js +25 -0
- package/dist/runtime/registration-events.d.ts +22 -14
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-event-bridge.d.ts +45 -0
- package/dist/runtime/shell-event-bridge.js +127 -0
- package/dist/runtime/shell-registration.d.ts +73 -0
- package/dist/runtime/shell-registration.js +95 -0
- package/dist/runtime/shell-scope.d.ts +68 -0
- package/dist/runtime/shell-scope.js +194 -0
- package/dist/runtime/tool-host-contract.d.ts +8 -14
- package/dist/runtime/tool-host-contract.js +12 -12
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +42 -67
- package/dist/services/ElementToolStateStore.js +65 -71
- package/dist/services/HighlightCoordinator.d.ts +28 -40
- package/dist/services/HighlightCoordinator.js +459 -381
- package/dist/services/RangeSerializer.d.ts +11 -33
- package/dist/services/RangeSerializer.js +88 -59
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +159 -119
- package/dist/services/TTSService.js +937 -822
- package/dist/services/ToolCoordinator.d.ts +42 -102
- package/dist/services/ToolCoordinator.js +178 -256
- package/dist/services/ToolRegistry.d.ts +152 -133
- package/dist/services/ToolRegistry.js +192 -178
- package/dist/services/ToolkitCoordinator.d.ts +233 -237
- package/dist/services/ToolkitCoordinator.js +615 -557
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +0 -1
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +51 -13
- package/dist/services/framework-error.js +19 -20
- package/dist/services/interfaces.d.ts +189 -184
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +80 -0
- package/dist/services/spoken-audio-cards.d.ts +2 -6
- package/dist/services/spoken-audio-cards.js +2 -6
- package/dist/services/tool-config-defaults.d.ts +3 -3
- package/dist/services/tool-config-defaults.js +3 -3
- package/dist/services/tool-config-validation.d.ts +9 -12
- package/dist/services/tool-config-validation.js +47 -87
- package/dist/services/tool-context.d.ts +7 -31
- package/dist/services/tool-context.js +99 -65
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +5 -5
- package/dist/services/tool-icons.js +6 -5
- package/dist/services/tool-instance-id.d.ts +8 -7
- package/dist/services/tool-instance-id.js +12 -24
- package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
- package/dist/services/tool-providers/TTSToolProvider.js +32 -85
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
- package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
- package/dist/services/tool-request.d.ts +31 -12
- package/dist/services/tool-request.js +31 -16
- package/dist/services/toolbar-decided-tools.d.ts +16 -0
- package/dist/services/toolbar-decided-tools.js +29 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +33 -29
- package/dist/services/tools-config-normalizer.js +26 -28
- package/dist/services/tts/browser-provider.d.ts +39 -6
- package/dist/services/tts/browser-provider.js +280 -160
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +64 -0
- package/dist/services/tts/flat-tree.js +269 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
- package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
- package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
- package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +42 -47
- package/dist/services/tts/math-control-names.d.ts +21 -0
- package/dist/services/tts/math-control-names.js +142 -0
- package/dist/services/tts/math-speech.d.ts +12 -1
- package/dist/services/tts/math-speech.js +54 -18
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +52 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +4 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -0
- package/dist/services/tts/text-processing.js +46 -33
- package/dist/services/tts/text-segmentation.d.ts +12 -0
- package/dist/services/tts/text-segmentation.js +60 -10
- package/dist/services/tts-runtime-config.d.ts +54 -26
- package/dist/services/tts-runtime-config.js +27 -112
- package/dist/tools/content-capability-resolution.d.ts +9 -11
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +59 -0
- package/dist/tools/registration.js +55 -0
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +45 -50
- package/dist/tools/tool-tag-map.d.ts +5 -15
- package/dist/tools/tool-tag-map.js +6 -20
- package/dist/utils/structural-equality.d.ts +11 -0
- package/dist/utils/structural-equality.js +47 -0
- package/package.json +15 -29
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
- package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -18
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
- package/dist/runtime/section-runtime-engine-host-context.js +0 -57
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -10
- package/dist/tools/internal.d.ts +0 -44
- package/dist/tools/internal.js +0 -39
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.js +0 -10
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { warnOncePerDocument } from "./page-warnings.js";
|
|
2
|
+
import { PIE_REGISTER_EVENT, } from "./registration-events.js";
|
|
3
|
+
/**
|
|
4
|
+
* A toolkit stops every registration it claims, so one that reaches the
|
|
5
|
+
* document went unclaimed: its item's catalogs are not filed and it does not
|
|
6
|
+
* count toward its section's loading. Installed from a toolkit's host at
|
|
7
|
+
* connect, once per document across every copy of the toolkit.
|
|
8
|
+
*/
|
|
9
|
+
export function watchForUnclaimedRegistrations(doc) {
|
|
10
|
+
if (!doc)
|
|
11
|
+
return;
|
|
12
|
+
const slot = Symbol.for("pie.assessmentToolkit.unclaimedRegistrationWatch");
|
|
13
|
+
const slots = doc;
|
|
14
|
+
if (slots[slot])
|
|
15
|
+
return;
|
|
16
|
+
slots[slot] = true;
|
|
17
|
+
doc.addEventListener(PIE_REGISTER_EVENT, (event) => {
|
|
18
|
+
const detail = event
|
|
19
|
+
.detail;
|
|
20
|
+
const itemId = typeof detail?.itemId === "string" ? detail.itemId : "";
|
|
21
|
+
warnOncePerDocument(doc, "unclaimedRegistration", `[pie-assessment-toolkit] Item "${itemId}" registered, but no toolkit claimed the registration, so its accessibility catalogs are not filed and it does not count toward its section's loading. Render its shell or scope inside the <pie-assessment-toolkit> that answered it. Reported once per page.`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -55,25 +55,17 @@ export type CatalogType = "spoken" | "sign-language" | "transcript" | "braille"
|
|
|
55
55
|
/**
|
|
56
56
|
* The catalog types PIE names, plus the rule for the ones it does not.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
* extensible, and
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* though — the named literals were documentation only, so a card written
|
|
63
|
-
* `"spokn"` was a perfectly valid `CatalogType` that no reader would ever ask
|
|
64
|
-
* for, and it failed by being invisible rather than by failing. That is what
|
|
65
|
-
* `isKnownCatalogType` and the warnings below are for: the openness stays, the
|
|
66
|
-
* silence does not.
|
|
58
|
+
* `CatalogType` stays open because QTI treats the support vocabulary as
|
|
59
|
+
* extensible, and catalogs arrive as authored JSON this type never validates.
|
|
60
|
+
* The trade: a card written `"spokn"` is a valid `CatalogType` no reader asks
|
|
61
|
+
* for, so `isKnownCatalogType` and the warnings below report it.
|
|
67
62
|
*/
|
|
68
63
|
export declare const KNOWN_CATALOG_TYPES: ReadonlySet<string>;
|
|
69
64
|
export declare function isKnownCatalogType(type: string): boolean;
|
|
70
65
|
/**
|
|
71
|
-
* Which of a card's two content slots it fills.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* says which form it is by carrying `content` or `payload`, and the
|
|
75
|
-
* exactly-one-of invariant is what makes that unambiguous. This names the
|
|
76
|
-
* distinction so a lookup can ask for one.
|
|
66
|
+
* Which of a card's two content slots it fills. The card says so by carrying
|
|
67
|
+
* `content` or `payload`, and the exactly-one-of invariant makes that
|
|
68
|
+
* unambiguous; this type names the distinction so a lookup can ask for one.
|
|
77
69
|
*/
|
|
78
70
|
export type CatalogCardForm = "content" | "payload";
|
|
79
71
|
export declare const catalogCardForm: (card: CatalogCard) => CatalogCardForm;
|
|
@@ -93,17 +85,15 @@ export interface CatalogLookupOptions {
|
|
|
93
85
|
* Preferred content form, when one catalog type legitimately has both on the
|
|
94
86
|
* same node.
|
|
95
87
|
*
|
|
96
|
-
* The case
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* so whichever of the two was written second in the array was unreachable and
|
|
102
|
-
* nothing said so.
|
|
88
|
+
* The case is a `spoken` node carrying both a reading script and a recording
|
|
89
|
+
* of it, which is APIP's authoring pattern and what QTI 3's migration
|
|
90
|
+
* guidance keeps: the script is the source the audio was generated from and
|
|
91
|
+
* the fallback when it cannot play. Without a form, a lookup returns the
|
|
92
|
+
* first card matching type and language.
|
|
103
93
|
*
|
|
104
94
|
* A preference, not a filter: if the requested form is not present, the other
|
|
105
|
-
* one is still returned. Callers that cannot use a form
|
|
106
|
-
*
|
|
95
|
+
* one is still returned. Callers that cannot use a form check what they got,
|
|
96
|
+
* as they do for a card of a type they did not expect.
|
|
107
97
|
*/
|
|
108
98
|
form?: CatalogCardForm;
|
|
109
99
|
}
|
|
@@ -241,9 +231,11 @@ export declare class AccessibilityCatalogResolver {
|
|
|
241
231
|
*/
|
|
242
232
|
hasCatalog(catalogId: string): boolean;
|
|
243
233
|
/**
|
|
244
|
-
* Get alternative content for a catalog identifier
|
|
234
|
+
* Get alternative content for a catalog identifier.
|
|
245
235
|
*
|
|
246
|
-
*
|
|
236
|
+
* With a `context`, the catalog registered for exactly that owner comes
|
|
237
|
+
* first, then the one compatible scoped catalog (several are ambiguous and
|
|
238
|
+
* resolve to null). Item-level catalogs come next, then assessment-level.
|
|
247
239
|
*/
|
|
248
240
|
getAlternative(catalogId: string, options: CatalogLookupOptions): ResolvedCatalog | null;
|
|
249
241
|
private resolveCard;
|
|
@@ -264,11 +256,9 @@ export declare class AccessibilityCatalogResolver {
|
|
|
264
256
|
* Get all available alternatives for a catalog identifier
|
|
265
257
|
*
|
|
266
258
|
* Every card goes through `resolveCard`, the same projection `getAlternative`
|
|
267
|
-
* uses, so enumeration
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* that arrived under the alias rendered correctly and was still reported as
|
|
271
|
-
* carrying no payload by anything asking what alternates exist.
|
|
259
|
+
* uses, so enumeration describes a card exactly as the resolution that
|
|
260
|
+
* renders it. A hand-rolled copy here drifted: the `signLanguage` alias was
|
|
261
|
+
* folded in on the resolution path only.
|
|
272
262
|
*/
|
|
273
263
|
getAllAlternatives(catalogId: string): ResolvedCatalog[];
|
|
274
264
|
/**
|
|
@@ -4,15 +4,10 @@ import { sanitizeSsmlString } from "./SSMLExtractor.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* The catalog types PIE names, plus the rule for the ones it does not.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* extensible, and
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* though — the named literals were documentation only, so a card written
|
|
12
|
-
* `"spokn"` was a perfectly valid `CatalogType` that no reader would ever ask
|
|
13
|
-
* for, and it failed by being invisible rather than by failing. That is what
|
|
14
|
-
* `isKnownCatalogType` and the warnings below are for: the openness stays, the
|
|
15
|
-
* silence does not.
|
|
7
|
+
* `CatalogType` stays open because QTI treats the support vocabulary as
|
|
8
|
+
* extensible, and catalogs arrive as authored JSON this type never validates.
|
|
9
|
+
* The trade: a card written `"spokn"` is a valid `CatalogType` no reader asks
|
|
10
|
+
* for, so `isKnownCatalogType` and the warnings below report it.
|
|
16
11
|
*/
|
|
17
12
|
export const KNOWN_CATALOG_TYPES = new Set([
|
|
18
13
|
"spoken",
|
|
@@ -336,9 +331,11 @@ export class AccessibilityCatalogResolver {
|
|
|
336
331
|
Array.from(this.scopedCatalogs.values()).some((owner) => owner.catalogs.has(catalogId)));
|
|
337
332
|
}
|
|
338
333
|
/**
|
|
339
|
-
* Get alternative content for a catalog identifier
|
|
334
|
+
* Get alternative content for a catalog identifier.
|
|
340
335
|
*
|
|
341
|
-
*
|
|
336
|
+
* With a `context`, the catalog registered for exactly that owner comes
|
|
337
|
+
* first, then the one compatible scoped catalog (several are ambiguous and
|
|
338
|
+
* resolve to null). Item-level catalogs come next, then assessment-level.
|
|
342
339
|
*/
|
|
343
340
|
getAlternative(catalogId, options) {
|
|
344
341
|
// A typo on this side is as silent as one on a card: the lookup simply finds
|
|
@@ -513,10 +510,10 @@ export class AccessibilityCatalogResolver {
|
|
|
513
510
|
}
|
|
514
511
|
return false;
|
|
515
512
|
}
|
|
516
|
-
// The single funnel every registration path runs through
|
|
517
|
-
// `addItemCatalogs` by way of `indexCatalogs`,
|
|
518
|
-
//
|
|
519
|
-
//
|
|
513
|
+
// The single funnel every registration path runs through (the constructor and
|
|
514
|
+
// `addItemCatalogs` by way of `indexCatalogs`, `registerCatalogs` and
|
|
515
|
+
// `registerOwner` by way of `insertScopedCatalogs`), so the unknown-type
|
|
516
|
+
// report lives here.
|
|
520
517
|
sanitizeCatalogs(catalogs) {
|
|
521
518
|
return catalogs.map((catalog) => ({
|
|
522
519
|
...catalog,
|
|
@@ -541,11 +538,10 @@ export class AccessibilityCatalogResolver {
|
|
|
541
538
|
// Language rungs, most specific first: the requested language, then the
|
|
542
539
|
// default language, then any.
|
|
543
540
|
//
|
|
544
|
-
// Each requested tag expands into its RFC 4647 lookup sequence
|
|
545
|
-
// tries `es-mx` and then `es` before falling
|
|
546
|
-
//
|
|
547
|
-
// emits
|
|
548
|
-
// no-constraint rung, i.e. by accident.
|
|
541
|
+
// Each requested tag expands into its RFC 4647 lookup sequence over
|
|
542
|
+
// normalized tags, so `es-MX` tries `es-mx` and then `es` before falling
|
|
543
|
+
// through to the default, and a POSIX `es_ES` card (what the Learnosity
|
|
544
|
+
// transform emits) matches an `es-ES` request on its own rung.
|
|
549
545
|
const languageRungs = [];
|
|
550
546
|
const pushLookupRungs = (tag) => {
|
|
551
547
|
for (const step of languageTagLookupSequence(tag)) {
|
|
@@ -563,18 +559,16 @@ export class AccessibilityCatalogResolver {
|
|
|
563
559
|
const candidates = catalog.cards.filter((card) => card.catalog === type && matchesLanguage(card));
|
|
564
560
|
if (candidates.length === 0)
|
|
565
561
|
continue;
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
// audio, which is worse than answering it with Spanish text.
|
|
562
|
+
// Language outranks form: form is preferred inside a language rung and
|
|
563
|
+
// never across them, so a Spanish lookup preferring a recording gets a
|
|
564
|
+
// Spanish recording first, then Spanish text, and never English audio
|
|
565
|
+
// ahead of either.
|
|
571
566
|
if (form) {
|
|
572
567
|
const preferred = candidates.find((card) => catalogCardForm(card) === form);
|
|
573
568
|
if (preferred)
|
|
574
569
|
return preferred;
|
|
575
570
|
}
|
|
576
|
-
// No preference
|
|
577
|
-
// which is what every caller got before form preference existed.
|
|
571
|
+
// No preference, or the preferred form is absent: the first match.
|
|
578
572
|
return candidates[0];
|
|
579
573
|
}
|
|
580
574
|
return null;
|
|
@@ -583,25 +577,20 @@ export class AccessibilityCatalogResolver {
|
|
|
583
577
|
* Get all available alternatives for a catalog identifier
|
|
584
578
|
*
|
|
585
579
|
* Every card goes through `resolveCard`, the same projection `getAlternative`
|
|
586
|
-
* uses, so enumeration
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* that arrived under the alias rendered correctly and was still reported as
|
|
590
|
-
* carrying no payload by anything asking what alternates exist.
|
|
580
|
+
* uses, so enumeration describes a card exactly as the resolution that
|
|
581
|
+
* renders it. A hand-rolled copy here drifted: the `signLanguage` alias was
|
|
582
|
+
* folded in on the resolution path only.
|
|
591
583
|
*/
|
|
592
584
|
getAllAlternatives(catalogId) {
|
|
593
585
|
const results = [];
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
// and language alone dropped the second of those on the floor, so anything
|
|
598
|
-
// asking what alternates exist under-reported them.
|
|
586
|
+
// Keyed on type, language and form: one catalog identifier can carry cards
|
|
587
|
+
// of one type in several languages, and a script and a recording of one
|
|
588
|
+
// type in the same language.
|
|
599
589
|
const claimed = new Set();
|
|
600
590
|
const add = (card, source) => {
|
|
601
591
|
// The language part is normalized, so a POSIX `es_ES` card and a BCP-47
|
|
602
|
-
// `es-ES` card collapse to one entry
|
|
603
|
-
//
|
|
604
|
-
// alternates where resolution can only ever return one.
|
|
592
|
+
// `es-ES` card collapse to one entry, as on the resolution path, which
|
|
593
|
+
// only ever returns one of them.
|
|
605
594
|
const key = `${card.catalog}|${normalizeLanguageTag(card.language)}|${catalogCardForm(card)}`;
|
|
606
595
|
if (claimed.has(key))
|
|
607
596
|
return;
|
|
@@ -646,7 +635,6 @@ export class AccessibilityCatalogResolver {
|
|
|
646
635
|
getStatistics() {
|
|
647
636
|
const allTypes = new Set();
|
|
648
637
|
const allLanguages = new Set();
|
|
649
|
-
// Collect from assessment catalogs
|
|
650
638
|
for (const catalog of this.assessmentCatalogs.values()) {
|
|
651
639
|
for (const card of catalog.cards) {
|
|
652
640
|
allTypes.add(card.catalog);
|
|
@@ -654,7 +642,6 @@ export class AccessibilityCatalogResolver {
|
|
|
654
642
|
allLanguages.add(card.language);
|
|
655
643
|
}
|
|
656
644
|
}
|
|
657
|
-
// Collect from item catalogs
|
|
658
645
|
for (const catalog of this.itemCatalogs.values()) {
|
|
659
646
|
for (const card of catalog.cards) {
|
|
660
647
|
allTypes.add(card.catalog);
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ElementToolStateStore
|
|
2
|
+
* ElementToolStateStore: ephemeral UI tool state (answer eliminations, flags and
|
|
3
|
+
* the like), kept out of the session data sent to the server for scoring.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - Generic solution for any tool
|
|
5
|
+
* State is keyed by a composite global element id,
|
|
6
|
+
* `assessmentId:sectionId:attemptId:itemId:elementId`, unique across the
|
|
7
|
+
* assessment and per attempt: two attempts at one section never share a key.
|
|
8
|
+
* Subscribers see every change, and an optional persistence callback
|
|
9
|
+
* (`setOnStateChange`) receives the serialized state.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The parts of a global element id. `attemptId` is `""` when the host names no
|
|
13
|
+
* attempt. A tool whose state belongs to an item or a section rather than to
|
|
14
|
+
* one element leaves `elementId`, and for a section `itemId`, as `""`.
|
|
15
15
|
*/
|
|
16
16
|
export interface ElementIdComponents {
|
|
17
17
|
assessmentId: string;
|
|
18
18
|
sectionId: string;
|
|
19
|
+
attemptId: string;
|
|
19
20
|
itemId: string;
|
|
20
21
|
elementId: string;
|
|
21
22
|
}
|
|
@@ -24,96 +25,70 @@ export declare class ElementToolStateStore {
|
|
|
24
25
|
private listeners;
|
|
25
26
|
private onStateChange;
|
|
26
27
|
/**
|
|
27
|
-
* Generates a globally unique element ID
|
|
28
|
-
* Format: `${assessmentId}:${sectionId}:${itemId}:${elementId}
|
|
28
|
+
* Generates a globally unique element ID from its parts.
|
|
29
|
+
* Format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`,
|
|
30
|
+
* each part with `%` written as `%25` and `:` as `%3A`, so an id containing
|
|
31
|
+
* `:` round-trips and cannot collide with another split of the same characters.
|
|
29
32
|
*
|
|
30
33
|
* @example
|
|
31
|
-
* getGlobalElementId('demo-assessment', 'section-1',
|
|
32
|
-
*
|
|
34
|
+
* getGlobalElementId({ assessmentId: 'demo-assessment', sectionId: 'section-1',
|
|
35
|
+
* attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' })
|
|
36
|
+
* // Returns: "demo-assessment:section-1:attempt-1:q1:mc1"
|
|
33
37
|
*/
|
|
34
|
-
getGlobalElementId(
|
|
38
|
+
getGlobalElementId(parts: ElementIdComponents): string;
|
|
35
39
|
/**
|
|
36
40
|
* Parses a global element ID into its component parts.
|
|
37
41
|
*
|
|
38
42
|
* @example
|
|
39
|
-
* parseGlobalElementId('demo-assessment:section-1:q1:mc1')
|
|
40
|
-
* // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
|
|
43
|
+
* parseGlobalElementId('demo-assessment:section-1:attempt-1:q1:mc1')
|
|
44
|
+
* // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
|
|
45
|
+
* // attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' }
|
|
41
46
|
*/
|
|
42
47
|
parseGlobalElementId(globalElementId: string): ElementIdComponents | null;
|
|
43
48
|
/**
|
|
44
49
|
* Sets tool state for a specific element.
|
|
45
50
|
* Notifies all subscribers and triggers persistence callback.
|
|
46
|
-
*
|
|
47
|
-
* @param globalElementId Composite key identifying the element
|
|
48
|
-
* @param toolId Tool identifier (e.g., 'answerEliminator', 'flagging')
|
|
49
|
-
* @param state Tool-specific state object
|
|
50
|
-
*/
|
|
51
|
-
setState(globalElementId: string, toolId: string, state: any): void;
|
|
52
|
-
/**
|
|
53
|
-
* Gets tool state for a specific element.
|
|
54
|
-
*
|
|
55
|
-
* @param globalElementId Composite key identifying the element
|
|
56
|
-
* @param toolId Tool identifier
|
|
57
|
-
* @returns Tool state or undefined if not found
|
|
58
51
|
*/
|
|
59
|
-
|
|
52
|
+
setState(globalElementId: string, toolId: string, state: unknown): void;
|
|
53
|
+
/** One tool's state for an element, or `undefined` when none is set. */
|
|
54
|
+
getState(globalElementId: string, toolId: string): unknown;
|
|
55
|
+
/** Every tool's state for an element, keyed by toolId. */
|
|
56
|
+
getElementState(globalElementId: string): Record<string, unknown>;
|
|
60
57
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param globalElementId Composite key identifying the element
|
|
64
|
-
* @returns Object with tool states keyed by toolId
|
|
58
|
+
* All state as `{ globalElementId: { toolId: state } }`, for persistence and
|
|
59
|
+
* debugging.
|
|
65
60
|
*/
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Gets all tool states for all elements.
|
|
69
|
-
* Used for persistence and debugging.
|
|
70
|
-
*
|
|
71
|
-
* @returns Nested object: { globalElementId: { toolId: state } }
|
|
72
|
-
*/
|
|
73
|
-
getAllState(): Record<string, Record<string, any>>;
|
|
61
|
+
getAllState(): Record<string, Record<string, unknown>>;
|
|
74
62
|
/**
|
|
75
63
|
* Subscribes to state changes.
|
|
76
|
-
* Callback is invoked whenever any element's tool state changes
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* @returns Unsubscribe function
|
|
64
|
+
* Callback is invoked whenever any element's tool state changes, including
|
|
65
|
+
* a `loadState` restore, with a copy of the state that it may keep. Returns
|
|
66
|
+
* the unsubscribe function.
|
|
80
67
|
*/
|
|
81
|
-
subscribe(callback: (state: Map<string, Map<string,
|
|
68
|
+
subscribe(callback: (state: Map<string, Map<string, unknown>>) => void): () => void;
|
|
82
69
|
private _notifyListeners;
|
|
83
70
|
/**
|
|
84
71
|
* Sets a callback to be invoked on state changes for persistence integration.
|
|
85
72
|
* Used by demo/app layer to persist tool state to localStorage or server.
|
|
86
|
-
*
|
|
87
|
-
* @param callback Function to call with serialized state on changes
|
|
88
73
|
*/
|
|
89
|
-
setOnStateChange(callback: (state: Record<string, Record<string,
|
|
74
|
+
setOnStateChange(callback: (state: Record<string, Record<string, unknown>>) => void): void;
|
|
90
75
|
private _notifyStateChange;
|
|
91
76
|
/**
|
|
92
77
|
* Loads tool state from serialized format.
|
|
93
|
-
* Used to restore state from localStorage or server.
|
|
94
|
-
*
|
|
95
|
-
* @param state Serialized state object
|
|
96
|
-
*/
|
|
97
|
-
loadState(state: Record<string, Record<string, any>>): void;
|
|
98
|
-
/**
|
|
99
|
-
* Clears all tool state for a specific element.
|
|
100
|
-
*
|
|
101
|
-
* @param globalElementId Composite key identifying the element
|
|
78
|
+
* Used to restore state from localStorage or server. Subscribers are
|
|
79
|
+
* notified; the persistence callback is not, so a restore never writes back.
|
|
102
80
|
*/
|
|
81
|
+
loadState(state: Record<string, Record<string, unknown>>): void;
|
|
82
|
+
/** Clears all tool state for a specific element. */
|
|
103
83
|
clearElement(globalElementId: string): void;
|
|
104
84
|
/**
|
|
105
85
|
* Clears state for a specific tool across all elements.
|
|
106
86
|
* Useful when disabling a tool globally.
|
|
107
|
-
*
|
|
108
|
-
* @param toolId Tool identifier
|
|
109
87
|
*/
|
|
110
88
|
clearTool(toolId: string): void;
|
|
111
89
|
/**
|
|
112
|
-
* Clears all tool state for a specific section.
|
|
90
|
+
* Clears all tool state for a specific section, across its attempts.
|
|
113
91
|
* Useful when unmounting/disposing a section.
|
|
114
|
-
*
|
|
115
|
-
* @param assessmentId Assessment identifier
|
|
116
|
-
* @param sectionId Section identifier
|
|
117
92
|
*/
|
|
118
93
|
clearSection(assessmentId: string, sectionId: string): void;
|
|
119
94
|
/**
|
|
@@ -1,58 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ElementToolStateStore
|
|
2
|
+
* ElementToolStateStore: ephemeral UI tool state (answer eliminations, flags and
|
|
3
|
+
* the like), kept out of the session data sent to the server for scoring.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - Element-level granularity (not item-level)
|
|
10
|
-
* - Composite keys for global uniqueness: `assessmentId:sectionId:itemId:elementId`
|
|
11
|
-
* - Cross-section state persistence
|
|
12
|
-
* - Reactive subscriptions
|
|
13
|
-
* - Optional persistence callback integration
|
|
14
|
-
* - Generic solution for any tool
|
|
5
|
+
* State is keyed by a composite global element id,
|
|
6
|
+
* `assessmentId:sectionId:attemptId:itemId:elementId`, unique across the
|
|
7
|
+
* assessment and per attempt: two attempts at one section never share a key.
|
|
8
|
+
* Subscribers see every change, and an optional persistence callback
|
|
9
|
+
* (`setOnStateChange`) receives the serialized state.
|
|
15
10
|
*/
|
|
11
|
+
const ID_PARTS = [
|
|
12
|
+
"assessmentId",
|
|
13
|
+
"sectionId",
|
|
14
|
+
"attemptId",
|
|
15
|
+
"itemId",
|
|
16
|
+
"elementId",
|
|
17
|
+
];
|
|
18
|
+
function escapeIdPart(part) {
|
|
19
|
+
return part.replace(/%/g, "%25").replace(/:/g, "%3A");
|
|
20
|
+
}
|
|
21
|
+
function unescapeIdPart(part) {
|
|
22
|
+
return part.replace(/%(25|3A)/g, (_, code) => (code === "25" ? "%" : ":"));
|
|
23
|
+
}
|
|
16
24
|
export class ElementToolStateStore {
|
|
17
25
|
elementStates = new Map();
|
|
18
26
|
listeners = new Set();
|
|
19
27
|
onStateChange = null;
|
|
20
28
|
/**
|
|
21
|
-
* Generates a globally unique element ID
|
|
22
|
-
* Format: `${assessmentId}:${sectionId}:${itemId}:${elementId}
|
|
29
|
+
* Generates a globally unique element ID from its parts.
|
|
30
|
+
* Format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`,
|
|
31
|
+
* each part with `%` written as `%25` and `:` as `%3A`, so an id containing
|
|
32
|
+
* `:` round-trips and cannot collide with another split of the same characters.
|
|
23
33
|
*
|
|
24
34
|
* @example
|
|
25
|
-
* getGlobalElementId('demo-assessment', 'section-1',
|
|
26
|
-
*
|
|
35
|
+
* getGlobalElementId({ assessmentId: 'demo-assessment', sectionId: 'section-1',
|
|
36
|
+
* attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' })
|
|
37
|
+
* // Returns: "demo-assessment:section-1:attempt-1:q1:mc1"
|
|
27
38
|
*/
|
|
28
|
-
getGlobalElementId(
|
|
29
|
-
return
|
|
39
|
+
getGlobalElementId(parts) {
|
|
40
|
+
return ID_PARTS.map((name) => escapeIdPart(parts[name])).join(":");
|
|
30
41
|
}
|
|
31
42
|
/**
|
|
32
43
|
* Parses a global element ID into its component parts.
|
|
33
44
|
*
|
|
34
45
|
* @example
|
|
35
|
-
* parseGlobalElementId('demo-assessment:section-1:q1:mc1')
|
|
36
|
-
* // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
|
|
46
|
+
* parseGlobalElementId('demo-assessment:section-1:attempt-1:q1:mc1')
|
|
47
|
+
* // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
|
|
48
|
+
* // attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' }
|
|
37
49
|
*/
|
|
38
50
|
parseGlobalElementId(globalElementId) {
|
|
39
51
|
const parts = globalElementId.split(":");
|
|
40
|
-
if (parts.length !==
|
|
52
|
+
if (parts.length !== ID_PARTS.length)
|
|
41
53
|
return null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
sectionId: parts[1],
|
|
45
|
-
itemId: parts[2],
|
|
46
|
-
elementId: parts[3],
|
|
47
|
-
};
|
|
54
|
+
const [assessmentId, sectionId, attemptId, itemId, elementId] = parts.map(unescapeIdPart);
|
|
55
|
+
return { assessmentId, sectionId, attemptId, itemId, elementId };
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
50
58
|
* Sets tool state for a specific element.
|
|
51
59
|
* Notifies all subscribers and triggers persistence callback.
|
|
52
|
-
*
|
|
53
|
-
* @param globalElementId Composite key identifying the element
|
|
54
|
-
* @param toolId Tool identifier (e.g., 'answerEliminator', 'flagging')
|
|
55
|
-
* @param state Tool-specific state object
|
|
56
60
|
*/
|
|
57
61
|
setState(globalElementId, toolId, state) {
|
|
58
62
|
let elementState = this.elementStates.get(globalElementId);
|
|
@@ -64,22 +68,11 @@ export class ElementToolStateStore {
|
|
|
64
68
|
this._notifyListeners();
|
|
65
69
|
this._notifyStateChange();
|
|
66
70
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Gets tool state for a specific element.
|
|
69
|
-
*
|
|
70
|
-
* @param globalElementId Composite key identifying the element
|
|
71
|
-
* @param toolId Tool identifier
|
|
72
|
-
* @returns Tool state or undefined if not found
|
|
73
|
-
*/
|
|
71
|
+
/** One tool's state for an element, or `undefined` when none is set. */
|
|
74
72
|
getState(globalElementId, toolId) {
|
|
75
73
|
return this.elementStates.get(globalElementId)?.get(toolId);
|
|
76
74
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Gets all tool states for a specific element.
|
|
79
|
-
*
|
|
80
|
-
* @param globalElementId Composite key identifying the element
|
|
81
|
-
* @returns Object with tool states keyed by toolId
|
|
82
|
-
*/
|
|
75
|
+
/** Every tool's state for an element, keyed by toolId. */
|
|
83
76
|
getElementState(globalElementId) {
|
|
84
77
|
const elementState = this.elementStates.get(globalElementId);
|
|
85
78
|
if (!elementState)
|
|
@@ -87,10 +80,8 @@ export class ElementToolStateStore {
|
|
|
87
80
|
return Object.fromEntries(elementState.entries());
|
|
88
81
|
}
|
|
89
82
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @returns Nested object: { globalElementId: { toolId: state } }
|
|
83
|
+
* All state as `{ globalElementId: { toolId: state } }`, for persistence and
|
|
84
|
+
* debugging.
|
|
94
85
|
*/
|
|
95
86
|
getAllState() {
|
|
96
87
|
const result = {};
|
|
@@ -101,39 +92,51 @@ export class ElementToolStateStore {
|
|
|
101
92
|
}
|
|
102
93
|
/**
|
|
103
94
|
* Subscribes to state changes.
|
|
104
|
-
* Callback is invoked whenever any element's tool state changes
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* @returns Unsubscribe function
|
|
95
|
+
* Callback is invoked whenever any element's tool state changes, including
|
|
96
|
+
* a `loadState` restore, with a copy of the state that it may keep. Returns
|
|
97
|
+
* the unsubscribe function.
|
|
108
98
|
*/
|
|
109
99
|
subscribe(callback) {
|
|
110
100
|
this.listeners.add(callback);
|
|
111
101
|
return () => this.listeners.delete(callback);
|
|
112
102
|
}
|
|
113
103
|
_notifyListeners() {
|
|
104
|
+
if (this.listeners.size === 0)
|
|
105
|
+
return;
|
|
106
|
+
const snapshot = new Map([...this.elementStates].map(([key, toolStates]) => [
|
|
107
|
+
key,
|
|
108
|
+
new Map(toolStates),
|
|
109
|
+
]));
|
|
114
110
|
for (const listener of this.listeners) {
|
|
115
|
-
|
|
111
|
+
try {
|
|
112
|
+
listener(snapshot);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.warn("[ElementToolStateStore] listener failed:", error);
|
|
116
|
+
}
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
/**
|
|
119
120
|
* Sets a callback to be invoked on state changes for persistence integration.
|
|
120
121
|
* Used by demo/app layer to persist tool state to localStorage or server.
|
|
121
|
-
*
|
|
122
|
-
* @param callback Function to call with serialized state on changes
|
|
123
122
|
*/
|
|
124
123
|
setOnStateChange(callback) {
|
|
125
124
|
this.onStateChange = callback;
|
|
126
125
|
}
|
|
127
126
|
_notifyStateChange() {
|
|
128
|
-
if (this.onStateChange)
|
|
127
|
+
if (!this.onStateChange)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
129
130
|
this.onStateChange(this.getAllState());
|
|
130
131
|
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.warn("[ElementToolStateStore] state-change callback failed:", error);
|
|
134
|
+
}
|
|
131
135
|
}
|
|
132
136
|
/**
|
|
133
137
|
* Loads tool state from serialized format.
|
|
134
|
-
* Used to restore state from localStorage or server.
|
|
135
|
-
*
|
|
136
|
-
* @param state Serialized state object
|
|
138
|
+
* Used to restore state from localStorage or server. Subscribers are
|
|
139
|
+
* notified; the persistence callback is not, so a restore never writes back.
|
|
137
140
|
*/
|
|
138
141
|
loadState(state) {
|
|
139
142
|
this.elementStates.clear();
|
|
@@ -143,11 +146,7 @@ export class ElementToolStateStore {
|
|
|
143
146
|
}
|
|
144
147
|
this._notifyListeners();
|
|
145
148
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Clears all tool state for a specific element.
|
|
148
|
-
*
|
|
149
|
-
* @param globalElementId Composite key identifying the element
|
|
150
|
-
*/
|
|
149
|
+
/** Clears all tool state for a specific element. */
|
|
151
150
|
clearElement(globalElementId) {
|
|
152
151
|
this.elementStates.delete(globalElementId);
|
|
153
152
|
this._notifyListeners();
|
|
@@ -156,8 +155,6 @@ export class ElementToolStateStore {
|
|
|
156
155
|
/**
|
|
157
156
|
* Clears state for a specific tool across all elements.
|
|
158
157
|
* Useful when disabling a tool globally.
|
|
159
|
-
*
|
|
160
|
-
* @param toolId Tool identifier
|
|
161
158
|
*/
|
|
162
159
|
clearTool(toolId) {
|
|
163
160
|
for (const elementState of this.elementStates.values()) {
|
|
@@ -167,14 +164,11 @@ export class ElementToolStateStore {
|
|
|
167
164
|
this._notifyStateChange();
|
|
168
165
|
}
|
|
169
166
|
/**
|
|
170
|
-
* Clears all tool state for a specific section.
|
|
167
|
+
* Clears all tool state for a specific section, across its attempts.
|
|
171
168
|
* Useful when unmounting/disposing a section.
|
|
172
|
-
*
|
|
173
|
-
* @param assessmentId Assessment identifier
|
|
174
|
-
* @param sectionId Section identifier
|
|
175
169
|
*/
|
|
176
170
|
clearSection(assessmentId, sectionId) {
|
|
177
|
-
const prefix = `${assessmentId}:${sectionId}:`;
|
|
171
|
+
const prefix = `${escapeIdPart(assessmentId)}:${escapeIdPart(sectionId)}:`;
|
|
178
172
|
const keysToDelete = [];
|
|
179
173
|
for (const key of this.elementStates.keys()) {
|
|
180
174
|
if (key.startsWith(prefix)) {
|