@pie-players/pie-default-tool-loaders 0.3.64 → 0.3.66
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 +101 -7
- package/dist/index.d.ts +9 -16
- package/dist/index.js +10 -45
- package/dist/packaged-capability-composition.d.ts +73 -0
- package/dist/packaged-capability-composition.js +447 -0
- package/dist/packaged-tool-registry.d.ts +2 -0
- package/dist/packaged-tool-registry.js +1 -0
- package/dist/registrations/accessibility-tools.d.ts +34 -0
- package/dist/registrations/accessibility-tools.js +256 -0
- package/dist/registrations/audio-transcript.d.ts +75 -0
- package/dist/registrations/audio-transcript.js +183 -0
- package/dist/registrations/calculator.d.ts +20 -0
- package/dist/registrations/calculator.js +228 -0
- package/dist/registrations/interaction-tools.d.ts +27 -0
- package/dist/registrations/interaction-tools.js +143 -0
- package/dist/registrations/measurement-tools.d.ts +24 -0
- package/dist/registrations/measurement-tools.js +130 -0
- package/dist/registrations/subject-specific-tools.d.ts +27 -0
- package/dist/registrations/subject-specific-tools.js +158 -0
- package/dist/registrations/tts.d.ts +21 -0
- package/dist/registrations/tts.js +184 -0
- package/dist/tool-placement.d.ts +1 -0
- package/dist/tool-placement.js +1 -0
- package/dist/tool-tag-map.d.ts +1 -0
- package/dist/tool-tag-map.js +1 -0
- package/dist/universal-supports.d.ts +1 -0
- package/dist/universal-supports.js +1 -0
- package/package.json +17 -14
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility Tools Registrations
|
|
3
|
+
*
|
|
4
|
+
* Registers tools for visual accessibility and reading support:
|
|
5
|
+
* - Line Reader (reading guide)
|
|
6
|
+
* - Color Scheme (theme/contrast)
|
|
7
|
+
* - Annotation Toolbar (text highlighting)
|
|
8
|
+
*
|
|
9
|
+
* Maps to QTI 3.0 standard access features from:
|
|
10
|
+
* - visual category: highContrastDisplay, colorContrast
|
|
11
|
+
* - reading category: readingMask, readingGuide, highlighting
|
|
12
|
+
*/
|
|
13
|
+
import { hasReadableText } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
14
|
+
import { createToolElement, resolveToolTag, } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
15
|
+
import { applyOverlaySurface, createScopedVisibilityBinding, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
16
|
+
/**
|
|
17
|
+
* Line Reader tool registration
|
|
18
|
+
*
|
|
19
|
+
* Provides a reading guide overlay to help track reading position.
|
|
20
|
+
* Useful for text-heavy passages and questions.
|
|
21
|
+
*/
|
|
22
|
+
export const lineReaderToolRegistration = {
|
|
23
|
+
toolId: "lineReader",
|
|
24
|
+
name: "Line Reader",
|
|
25
|
+
description: "Reading guide overlay",
|
|
26
|
+
icon: "bars-3",
|
|
27
|
+
// Line reader appears where there's text to read
|
|
28
|
+
supportedLevels: ["section", "passage", "rubric", "item"],
|
|
29
|
+
// PNP support IDs
|
|
30
|
+
// Maps to QTI 3.0 standard features: readingMask, readingGuide, readingRuler
|
|
31
|
+
pnpSupportIds: [
|
|
32
|
+
"readingMask", // QTI 3.0 standard (reading.readingMask)
|
|
33
|
+
"readingGuide", // QTI 3.0 standard (reading.readingGuide)
|
|
34
|
+
"readingRuler", // QTI 3.0 standard (reading.readingRuler)
|
|
35
|
+
"lineReader", // Common variant
|
|
36
|
+
"trackingGuide", // Common variant
|
|
37
|
+
],
|
|
38
|
+
/**
|
|
39
|
+
* Pass 2: Line reader is relevant when readable text is present
|
|
40
|
+
*/
|
|
41
|
+
isVisibleInContext(context) {
|
|
42
|
+
return hasReadableText(context);
|
|
43
|
+
},
|
|
44
|
+
renderToolbar(context, toolbarContext) {
|
|
45
|
+
const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
|
|
46
|
+
const button = {
|
|
47
|
+
toolId: this.toolId,
|
|
48
|
+
label: this.name,
|
|
49
|
+
icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
|
|
50
|
+
disabled: false,
|
|
51
|
+
ariaLabel: "Line reader - Reading guide",
|
|
52
|
+
tooltip: "Line Reader",
|
|
53
|
+
onClick: () => toolbarContext.toggleTool(this.toolId),
|
|
54
|
+
active: visibility.isActive(),
|
|
55
|
+
};
|
|
56
|
+
const componentOverrides = toolbarContext.componentOverrides ?? {};
|
|
57
|
+
const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
|
|
58
|
+
overlay.setAttribute("tool-id", visibility.fullToolId);
|
|
59
|
+
applyOverlaySurface(overlay, "frameless");
|
|
60
|
+
return {
|
|
61
|
+
toolId: this.toolId,
|
|
62
|
+
button,
|
|
63
|
+
elements: [{ element: overlay, mount: "after-buttons" }],
|
|
64
|
+
sync: () => {
|
|
65
|
+
syncButtonAndOverlayVisibility({
|
|
66
|
+
button,
|
|
67
|
+
overlay,
|
|
68
|
+
isActive: visibility.isActive,
|
|
69
|
+
});
|
|
70
|
+
if (toolbarContext.toolkitCoordinator) {
|
|
71
|
+
overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
subscribeActive: visibility.subscribeActive,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Theme tool registration
|
|
80
|
+
*
|
|
81
|
+
* Provides accessible theme and contrast controls.
|
|
82
|
+
* Global tool that affects entire assessment.
|
|
83
|
+
*/
|
|
84
|
+
export const themeToolRegistration = {
|
|
85
|
+
toolId: "theme",
|
|
86
|
+
name: "Theme",
|
|
87
|
+
description: "Accessible themes and contrast",
|
|
88
|
+
icon: "swatch",
|
|
89
|
+
// Color scheme is assessment-wide
|
|
90
|
+
supportedLevels: ["assessment", "section"],
|
|
91
|
+
// PNP support IDs
|
|
92
|
+
// Maps to QTI 3.0 standard features: highContrastDisplay, colorContrast, invertColors
|
|
93
|
+
pnpSupportIds: [
|
|
94
|
+
"highContrastDisplay", // QTI 3.0 standard (visual.highContrastDisplay)
|
|
95
|
+
"colorContrast", // QTI 3.0 standard (visual.colorContrast)
|
|
96
|
+
"invertColors", // QTI 3.0 standard (visual.invertColors)
|
|
97
|
+
"theme", // Canonical id
|
|
98
|
+
"highContrast", // Common variant
|
|
99
|
+
"customColors", // Common variant
|
|
100
|
+
],
|
|
101
|
+
/**
|
|
102
|
+
* Pass 2: Color scheme is always relevant when allowed
|
|
103
|
+
*/
|
|
104
|
+
isVisibleInContext(context) {
|
|
105
|
+
return true; // Always show if allowed by orchestrator
|
|
106
|
+
},
|
|
107
|
+
renderToolbar(context, toolbarContext) {
|
|
108
|
+
const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
|
|
109
|
+
const button = {
|
|
110
|
+
toolId: this.toolId,
|
|
111
|
+
label: this.name,
|
|
112
|
+
icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
|
|
113
|
+
disabled: false,
|
|
114
|
+
ariaLabel: "Theme - Change colors and contrast",
|
|
115
|
+
tooltip: "Theme",
|
|
116
|
+
onClick: () => toolbarContext.toggleTool(this.toolId),
|
|
117
|
+
active: visibility.isActive(),
|
|
118
|
+
};
|
|
119
|
+
const componentOverrides = toolbarContext.componentOverrides ?? {};
|
|
120
|
+
const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
|
|
121
|
+
overlay.setAttribute("tool-id", visibility.fullToolId);
|
|
122
|
+
return {
|
|
123
|
+
toolId: this.toolId,
|
|
124
|
+
button,
|
|
125
|
+
elements: [
|
|
126
|
+
{
|
|
127
|
+
element: overlay,
|
|
128
|
+
mount: "after-buttons",
|
|
129
|
+
shell: {
|
|
130
|
+
title: this.name,
|
|
131
|
+
draggable: true,
|
|
132
|
+
resizable: false,
|
|
133
|
+
closeable: true,
|
|
134
|
+
initialWidth: 520,
|
|
135
|
+
initialHeight: 380,
|
|
136
|
+
minWidth: 420,
|
|
137
|
+
minHeight: 300,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
sync: () => {
|
|
142
|
+
syncButtonAndOverlayVisibility({
|
|
143
|
+
button,
|
|
144
|
+
overlay,
|
|
145
|
+
isActive: visibility.isActive,
|
|
146
|
+
});
|
|
147
|
+
if (toolbarContext.toolkitCoordinator) {
|
|
148
|
+
overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
subscribeActive: visibility.subscribeActive,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Annotation Toolbar registration
|
|
157
|
+
*
|
|
158
|
+
* Text highlighting with CSS Custom Highlight API.
|
|
159
|
+
* Zero DOM mutation, optimal performance.
|
|
160
|
+
*/
|
|
161
|
+
export const annotationToolbarRegistration = {
|
|
162
|
+
toolId: "annotationToolbar",
|
|
163
|
+
name: "Highlighter",
|
|
164
|
+
description: "Highlight and annotate text",
|
|
165
|
+
icon: "highlighter",
|
|
166
|
+
activation: "selection-gateway",
|
|
167
|
+
singletonScope: "section",
|
|
168
|
+
/**
|
|
169
|
+
* Section-scoped singleton surface. The gateway used to be mounted by
|
|
170
|
+
* `PieSectionPlayerBaseElement.svelte`, which named this tool id in three
|
|
171
|
+
* places — the policy check, the module load and the element — so no host
|
|
172
|
+
* could contribute a second section-scoped capability. Declaring the surface
|
|
173
|
+
* moves that wiring into this registration, and the renderer discovers it
|
|
174
|
+
* through `ToolRegistry.getToolsBySurface("section-overlay")`.
|
|
175
|
+
*/
|
|
176
|
+
surfaces: ["section-overlay"],
|
|
177
|
+
// Annotation appears where there's text content
|
|
178
|
+
supportedLevels: ["passage", "rubric", "item", "element"],
|
|
179
|
+
// PNP support IDs
|
|
180
|
+
// Maps to QTI 3.0 standard features: highlighting, annotations
|
|
181
|
+
pnpSupportIds: [
|
|
182
|
+
"highlighting", // QTI 3.0 standard (cognitive.highlighting / reading.wordHighlighting)
|
|
183
|
+
"annotations", // QTI 3.0 standard (cognitive.annotations)
|
|
184
|
+
"highlighter", // Common variant
|
|
185
|
+
"textHighlight", // Common variant
|
|
186
|
+
"annotation", // Common variant
|
|
187
|
+
],
|
|
188
|
+
/**
|
|
189
|
+
* Pass 2: Annotation is relevant when readable text is present
|
|
190
|
+
*/
|
|
191
|
+
isVisibleInContext(context) {
|
|
192
|
+
return hasReadableText(context);
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Mount the section-scoped gateway. The element positions itself (`position:
|
|
196
|
+
* fixed` inside its shadow root) and finds its scope through the toolkit's
|
|
197
|
+
* context, which bubbles from wherever the host mounted it, so this needs to
|
|
198
|
+
* know nothing about the host's layout.
|
|
199
|
+
*/
|
|
200
|
+
renderSurface(context) {
|
|
201
|
+
const componentOverrides = context.componentOverrides ?? {};
|
|
202
|
+
const tagName = resolveToolTag(context.toolId, componentOverrides);
|
|
203
|
+
if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
|
|
204
|
+
// The host loads the module before mounting, so reaching here means the
|
|
205
|
+
// optional package is absent. Declining is the right answer: mounting an
|
|
206
|
+
// undefined element would render an empty box the learner cannot use.
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const element = document.createElement(tagName);
|
|
210
|
+
// Reads the context it is handed. These were reactive props before the
|
|
211
|
+
// gateway moved behind `renderSurface`, and a host calling
|
|
212
|
+
// `updateAssessment(...)` mid-session swaps the coordinator without
|
|
213
|
+
// remounting — closing over the render-time services would leave the gateway
|
|
214
|
+
// highlighting into the previous session's coordinator.
|
|
215
|
+
const applyServices = (current) => {
|
|
216
|
+
element.enabled = true;
|
|
217
|
+
element.ttsService = current.services.ttsService;
|
|
218
|
+
element.highlightCoordinator =
|
|
219
|
+
current.services.toolkitCoordinator?.highlightCoordinator ?? null;
|
|
220
|
+
};
|
|
221
|
+
applyServices(context);
|
|
222
|
+
return { element, sync: applyServices };
|
|
223
|
+
},
|
|
224
|
+
renderToolbar(context, toolbarContext) {
|
|
225
|
+
const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
|
|
226
|
+
const button = {
|
|
227
|
+
toolId: this.toolId,
|
|
228
|
+
label: this.name,
|
|
229
|
+
icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
|
|
230
|
+
disabled: false,
|
|
231
|
+
ariaLabel: "Annotation toolbar - Highlight text",
|
|
232
|
+
tooltip: "Highlight",
|
|
233
|
+
onClick: () => toolbarContext.toggleTool(this.toolId),
|
|
234
|
+
active: visibility.isActive(),
|
|
235
|
+
};
|
|
236
|
+
const componentOverrides = toolbarContext.componentOverrides ?? {};
|
|
237
|
+
const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
|
|
238
|
+
overlay.setAttribute("tool-id", visibility.fullToolId);
|
|
239
|
+
return {
|
|
240
|
+
toolId: this.toolId,
|
|
241
|
+
button,
|
|
242
|
+
elements: [{ element: overlay, mount: "after-buttons" }],
|
|
243
|
+
sync: () => {
|
|
244
|
+
syncButtonAndOverlayVisibility({
|
|
245
|
+
button,
|
|
246
|
+
overlay,
|
|
247
|
+
isActive: visibility.isActive,
|
|
248
|
+
});
|
|
249
|
+
if (toolbarContext.toolkitCoordinator) {
|
|
250
|
+
overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
subscribeActive: visibility.subscribeActive,
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio transcript: the text of an audio prompt, rendered for the learners
|
|
3
|
+
* entitled to it.
|
|
4
|
+
*
|
|
5
|
+
* Two kinds of content wear the same shape, and the card says which:
|
|
6
|
+
*
|
|
7
|
+
* - `visibility: "always"` is authored presentation. The item family was
|
|
8
|
+
* designed to be delivered with its transcript on screen — Star Early
|
|
9
|
+
* Literacy sentence items, for instance — so no profile grants it and none
|
|
10
|
+
* revokes it.
|
|
11
|
+
* - anything else, `"onGrant"` included, is the accommodation. Policy decides
|
|
12
|
+
* against the `transcript` support id, and silence means no. For the three
|
|
13
|
+
* Star families where a visible transcript turns the item into a reading
|
|
14
|
+
* task, that gate is the whole point.
|
|
15
|
+
*
|
|
16
|
+
* The toolkit renders the text, not the element. Before this, the transcript rode
|
|
17
|
+
* on `model.audioTranscript` and `mc-populated-blank` revealed its own copy when
|
|
18
|
+
* an ancestor carried `.rli-with-audio-transcript` — which made delivering an
|
|
19
|
+
* accommodation the host's job, required every element carrying audio to
|
|
20
|
+
* reimplement the reveal, and put an element-specific CSS class in the contract.
|
|
21
|
+
* Rendering here is the same shape signing already has, and it is what makes one
|
|
22
|
+
* mechanism serve braille, simplified-language and the rest.
|
|
23
|
+
*
|
|
24
|
+
* No `aria-describedby` back to the element's audio control, deliberately. A
|
|
25
|
+
* description is announced as a flat string when a control takes focus, so
|
|
26
|
+
* pointing one at a multi-sentence transcript is worse to listen to than reading
|
|
27
|
+
* order: this renders a labelled region immediately before the content, where a
|
|
28
|
+
* screen-reader user meets it on the way to the control it belongs to.
|
|
29
|
+
*
|
|
30
|
+
* It ships in the packaged set, unlike signing, because its presentation half has
|
|
31
|
+
* to work with no host opt-in: an item authored to show its transcript must show
|
|
32
|
+
* it in every player, and a deployment that forgot an import would silently
|
|
33
|
+
* deliver a designed-for reading support as nothing. The accommodation half is
|
|
34
|
+
* still policy-gated exactly as signing is, and `transcript` is deliberately
|
|
35
|
+
* absent from the universal preset — a content-dependent support never belongs in
|
|
36
|
+
* a wholesale grant.
|
|
37
|
+
*/
|
|
38
|
+
import type { ToolContentDependencyContext, ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
39
|
+
/** QTI 3.0 / AfA support id gating an audio transcript. */
|
|
40
|
+
export declare const AUDIO_TRANSCRIPT_FEATURE_ID = "transcript";
|
|
41
|
+
/** Catalog type carrying the transcript text. */
|
|
42
|
+
export declare const AUDIO_TRANSCRIPT_CATALOG_TYPE = "transcript";
|
|
43
|
+
/** Accessible name of the rendered region, and its class hook for themes. */
|
|
44
|
+
export declare const AUDIO_TRANSCRIPT_REGION_LABEL = "Transcript";
|
|
45
|
+
export declare const AUDIO_TRANSCRIPT_REGION_CLASS = "pie-tool-audio-transcript";
|
|
46
|
+
/** The transcript to render, and why it is being rendered. */
|
|
47
|
+
export interface ResolvedAudioTranscript {
|
|
48
|
+
catalogId: string;
|
|
49
|
+
text: string;
|
|
50
|
+
language?: string;
|
|
51
|
+
/** True when the card declares itself authored presentation. */
|
|
52
|
+
always: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The transcript an entity carries, or `null`.
|
|
56
|
+
*
|
|
57
|
+
* Read from the owner snapshot the resolver produced. Item/passage/model
|
|
58
|
+
* traversal and registration precedence have already been applied, so this
|
|
59
|
+
* capability owns only transcript interpretation.
|
|
60
|
+
*
|
|
61
|
+
* The cards are read directly rather than through `AccessibilityCatalogResolver`,
|
|
62
|
+
* which flattens a card to type/language/content and so cannot carry
|
|
63
|
+
* `visibility` — the one attribute this decision turns on. Nothing is lost by it:
|
|
64
|
+
* the resolver's value is choosing between languages of the same alternate, and a
|
|
65
|
+
* transcript is in the language of the audio it transcribes, one per prompt.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveAudioTranscript(context: ToolContentDependencyContext): ResolvedAudioTranscript | null;
|
|
68
|
+
/**
|
|
69
|
+
* Host surface this capability fills: the full-width slot above a card's content.
|
|
70
|
+
*
|
|
71
|
+
* Item cards and passage cards open the same surface, so declaring it once
|
|
72
|
+
* reaches both — a passage can carry an audio prompt as an item can.
|
|
73
|
+
*/
|
|
74
|
+
export declare const CONTENT_LEAD_SURFACE = "content-lead";
|
|
75
|
+
export declare const audioTranscriptRegistration: ToolRegistration;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio transcript: the text of an audio prompt, rendered for the learners
|
|
3
|
+
* entitled to it.
|
|
4
|
+
*
|
|
5
|
+
* Two kinds of content wear the same shape, and the card says which:
|
|
6
|
+
*
|
|
7
|
+
* - `visibility: "always"` is authored presentation. The item family was
|
|
8
|
+
* designed to be delivered with its transcript on screen — Star Early
|
|
9
|
+
* Literacy sentence items, for instance — so no profile grants it and none
|
|
10
|
+
* revokes it.
|
|
11
|
+
* - anything else, `"onGrant"` included, is the accommodation. Policy decides
|
|
12
|
+
* against the `transcript` support id, and silence means no. For the three
|
|
13
|
+
* Star families where a visible transcript turns the item into a reading
|
|
14
|
+
* task, that gate is the whole point.
|
|
15
|
+
*
|
|
16
|
+
* The toolkit renders the text, not the element. Before this, the transcript rode
|
|
17
|
+
* on `model.audioTranscript` and `mc-populated-blank` revealed its own copy when
|
|
18
|
+
* an ancestor carried `.rli-with-audio-transcript` — which made delivering an
|
|
19
|
+
* accommodation the host's job, required every element carrying audio to
|
|
20
|
+
* reimplement the reveal, and put an element-specific CSS class in the contract.
|
|
21
|
+
* Rendering here is the same shape signing already has, and it is what makes one
|
|
22
|
+
* mechanism serve braille, simplified-language and the rest.
|
|
23
|
+
*
|
|
24
|
+
* No `aria-describedby` back to the element's audio control, deliberately. A
|
|
25
|
+
* description is announced as a flat string when a control takes focus, so
|
|
26
|
+
* pointing one at a multi-sentence transcript is worse to listen to than reading
|
|
27
|
+
* order: this renders a labelled region immediately before the content, where a
|
|
28
|
+
* screen-reader user meets it on the way to the control it belongs to.
|
|
29
|
+
*
|
|
30
|
+
* It ships in the packaged set, unlike signing, because its presentation half has
|
|
31
|
+
* to work with no host opt-in: an item authored to show its transcript must show
|
|
32
|
+
* it in every player, and a deployment that forgot an import would silently
|
|
33
|
+
* deliver a designed-for reading support as nothing. The accommodation half is
|
|
34
|
+
* still policy-gated exactly as signing is, and `transcript` is deliberately
|
|
35
|
+
* absent from the universal preset — a content-dependent support never belongs in
|
|
36
|
+
* a wholesale grant.
|
|
37
|
+
*/
|
|
38
|
+
/** QTI 3.0 / AfA support id gating an audio transcript. */
|
|
39
|
+
export const AUDIO_TRANSCRIPT_FEATURE_ID = "transcript";
|
|
40
|
+
/** Catalog type carrying the transcript text. */
|
|
41
|
+
export const AUDIO_TRANSCRIPT_CATALOG_TYPE = "transcript";
|
|
42
|
+
/** Accessible name of the rendered region, and its class hook for themes. */
|
|
43
|
+
export const AUDIO_TRANSCRIPT_REGION_LABEL = "Transcript";
|
|
44
|
+
export const AUDIO_TRANSCRIPT_REGION_CLASS = "pie-tool-audio-transcript";
|
|
45
|
+
/** Card value that makes a transcript authored presentation. */
|
|
46
|
+
const ALWAYS_VISIBLE = "always";
|
|
47
|
+
const isTranscriptCard = (card) => !!card && card.catalog === AUDIO_TRANSCRIPT_CATALOG_TYPE;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a card declares itself authored presentation.
|
|
50
|
+
*
|
|
51
|
+
* Absent reads as the accommodation, the same fail-closed reading the policy
|
|
52
|
+
* engine takes of a silent profile: a card that says nothing about presentation
|
|
53
|
+
* has not claimed to be presentation.
|
|
54
|
+
*/
|
|
55
|
+
const isAlwaysVisible = (card) => typeof card.visibility === "string" &&
|
|
56
|
+
card.visibility.trim().toLowerCase() === ALWAYS_VISIBLE;
|
|
57
|
+
const cardText = (card) => typeof card.content === "string" ? card.content.trim() : "";
|
|
58
|
+
/**
|
|
59
|
+
* The transcript an entity carries, or `null`.
|
|
60
|
+
*
|
|
61
|
+
* Read from the owner snapshot the resolver produced. Item/passage/model
|
|
62
|
+
* traversal and registration precedence have already been applied, so this
|
|
63
|
+
* capability owns only transcript interpretation.
|
|
64
|
+
*
|
|
65
|
+
* The cards are read directly rather than through `AccessibilityCatalogResolver`,
|
|
66
|
+
* which flattens a card to type/language/content and so cannot carry
|
|
67
|
+
* `visibility` — the one attribute this decision turns on. Nothing is lost by it:
|
|
68
|
+
* the resolver's value is choosing between languages of the same alternate, and a
|
|
69
|
+
* transcript is in the language of the audio it transcribes, one per prompt.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveAudioTranscript(context) {
|
|
72
|
+
if (!context.catalogs)
|
|
73
|
+
return null;
|
|
74
|
+
let firstOnGrant = null;
|
|
75
|
+
for (const { catalogId, card } of context.catalogs.cards) {
|
|
76
|
+
if (!isTranscriptCard(card))
|
|
77
|
+
continue;
|
|
78
|
+
const text = cardText(card);
|
|
79
|
+
// A card with no text is not a transcript; rendering an empty region
|
|
80
|
+
// would announce an alternate that is not there.
|
|
81
|
+
if (!text)
|
|
82
|
+
continue;
|
|
83
|
+
const resolved = {
|
|
84
|
+
catalogId,
|
|
85
|
+
text,
|
|
86
|
+
language: typeof card.language === "string" ? card.language : undefined,
|
|
87
|
+
always: isAlwaysVisible(card),
|
|
88
|
+
};
|
|
89
|
+
// An `always` card answers whether or not policy granted anything, so it
|
|
90
|
+
// wins over an accommodation card found earlier.
|
|
91
|
+
if (resolved.always)
|
|
92
|
+
return resolved;
|
|
93
|
+
firstOnGrant ??= resolved;
|
|
94
|
+
}
|
|
95
|
+
if (!firstOnGrant)
|
|
96
|
+
return null;
|
|
97
|
+
// The accommodation half: without a grant this learner is not entitled to it,
|
|
98
|
+
// and being asked anyway is the price of also serving the authored case.
|
|
99
|
+
return context.granted ? firstOnGrant : null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Host surface this capability fills: the full-width slot above a card's content.
|
|
103
|
+
*
|
|
104
|
+
* Item cards and passage cards open the same surface, so declaring it once
|
|
105
|
+
* reaches both — a passage can carry an audio prompt as an item can.
|
|
106
|
+
*/
|
|
107
|
+
export const CONTENT_LEAD_SURFACE = "content-lead";
|
|
108
|
+
/**
|
|
109
|
+
* A labelled region holding the transcript text.
|
|
110
|
+
*
|
|
111
|
+
* Plain DOM rather than a custom element: there is no state, no lifecycle and
|
|
112
|
+
* nothing to load, so a package and a registration ceremony would buy nothing.
|
|
113
|
+
* `renderSurface` promises an `HTMLElement`, not a custom one.
|
|
114
|
+
*/
|
|
115
|
+
function buildTranscriptRegion() {
|
|
116
|
+
const region = document.createElement("section");
|
|
117
|
+
region.className = AUDIO_TRANSCRIPT_REGION_CLASS;
|
|
118
|
+
// A region rather than a bare paragraph so it is reachable as a landmark and
|
|
119
|
+
// announces its purpose before its content.
|
|
120
|
+
region.setAttribute("role", "region");
|
|
121
|
+
const text = document.createElement("p");
|
|
122
|
+
text.className = `${AUDIO_TRANSCRIPT_REGION_CLASS}__text`;
|
|
123
|
+
region.appendChild(text);
|
|
124
|
+
return region;
|
|
125
|
+
}
|
|
126
|
+
const applyTranscript = (region, transcript) => {
|
|
127
|
+
const text = region.querySelector(`.${AUDIO_TRANSCRIPT_REGION_CLASS}__text`);
|
|
128
|
+
if (!text)
|
|
129
|
+
return;
|
|
130
|
+
text.textContent = transcript?.text ?? "";
|
|
131
|
+
if (transcript?.language) {
|
|
132
|
+
region.setAttribute("lang", transcript.language);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
region.removeAttribute("lang");
|
|
136
|
+
}
|
|
137
|
+
// Which half put it on screen, for a policy debugger and for hosts that theme
|
|
138
|
+
// authored presentation differently from an accommodation.
|
|
139
|
+
region.dataset.transcriptVisibility = transcript?.always
|
|
140
|
+
? "always"
|
|
141
|
+
: "onGrant";
|
|
142
|
+
};
|
|
143
|
+
export const audioTranscriptRegistration = {
|
|
144
|
+
toolId: AUDIO_TRANSCRIPT_FEATURE_ID,
|
|
145
|
+
name: "Audio Transcript",
|
|
146
|
+
description: "Text of an audio prompt, read before the content it transcribes",
|
|
147
|
+
supportedLevels: ["item", "passage"],
|
|
148
|
+
pnpSupportIds: [AUDIO_TRANSCRIPT_FEATURE_ID],
|
|
149
|
+
// No button to press: a transcript is either authored-visible, or granted and
|
|
150
|
+
// present, or absent. `region` is the activation for a capability with no
|
|
151
|
+
// toolbar presence, and tools-config validation reports a `tools.placement`
|
|
152
|
+
// entry naming it as unplaceable rather than silently doing nothing.
|
|
153
|
+
activation: "region",
|
|
154
|
+
surfaces: [CONTENT_LEAD_SURFACE],
|
|
155
|
+
// The resource half of the AfA pair, and the only reader of the card.
|
|
156
|
+
requiresAuthoredContent: {
|
|
157
|
+
resolve(context) {
|
|
158
|
+
return resolveAudioTranscript(context);
|
|
159
|
+
},
|
|
160
|
+
description: "An accessibility catalog card of type `transcript` on the item or passage",
|
|
161
|
+
},
|
|
162
|
+
// Authored presentation is not a grant, so the content has to be consulted even
|
|
163
|
+
// when policy granted nothing; `resolveAudioTranscript` returns null for an
|
|
164
|
+
// accommodation card in that case.
|
|
165
|
+
resolvesWithoutGrant: true,
|
|
166
|
+
renderSurface(context) {
|
|
167
|
+
const transcript = context.content;
|
|
168
|
+
// No content means the host asked before resolving, or resolved to nothing.
|
|
169
|
+
// Declining is the honest answer; an empty region is not.
|
|
170
|
+
if (!transcript)
|
|
171
|
+
return null;
|
|
172
|
+
const region = buildTranscriptRegion();
|
|
173
|
+
applyTranscript(region, transcript);
|
|
174
|
+
return {
|
|
175
|
+
element: region,
|
|
176
|
+
ariaLabel: AUDIO_TRANSCRIPT_REGION_LABEL,
|
|
177
|
+
// Reads the context it is handed, never the one captured above: on a
|
|
178
|
+
// re-resolve the host's context carries the current card, and a learner must
|
|
179
|
+
// not keep reading the previous item's transcript.
|
|
180
|
+
sync: (current) => applyTranscript(region, current.content),
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculator Tool Registration
|
|
3
|
+
*
|
|
4
|
+
* Registers the calculator tool with support for multiple calculator types
|
|
5
|
+
* (basic, scientific, graphing) via Desmos provider.
|
|
6
|
+
*
|
|
7
|
+
* Maps to QTI 3.0 standard access features:
|
|
8
|
+
* - calculator (cognitive support)
|
|
9
|
+
* - graphingCalculator (assessment tool)
|
|
10
|
+
*/
|
|
11
|
+
import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
12
|
+
/**
|
|
13
|
+
* Calculator tool registration
|
|
14
|
+
*
|
|
15
|
+
* Supports:
|
|
16
|
+
* - Basic, scientific, and graphing calculators via Desmos
|
|
17
|
+
* - Context-aware visibility (shows only when math content is detected)
|
|
18
|
+
* - Item level only
|
|
19
|
+
*/
|
|
20
|
+
export declare const calculatorToolRegistration: ToolRegistration;
|