@pie-players/pie-default-tool-loaders 1.0.0-beta.1 → 1.0.0-beta.10
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/LICENSE +21 -0
- package/README.md +98 -14
- package/dist/afa-profile.d.ts +17 -0
- package/dist/afa-profile.js +18 -0
- package/dist/calculator-providers/LazyCalculatorToolProvider.d.ts +32 -18
- package/dist/calculator-providers/LazyCalculatorToolProvider.js +24 -12
- package/dist/calculator-providers/packaged-calculator-providers.js +26 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +8 -5
- package/dist/packaged-capability-composition.js +11 -1
- package/dist/registrations/accessibility-tools.d.ts +8 -0
- package/dist/registrations/accessibility-tools.js +38 -0
- package/dist/registrations/audio-transcript.d.ts +0 -7
- package/dist/registrations/audio-transcript.js +1 -7
- package/dist/registrations/calculator.d.ts +16 -5
- package/dist/registrations/calculator.js +171 -176
- package/dist/registrations/interaction-tools.d.ts +14 -1
- package/dist/registrations/interaction-tools.js +91 -23
- package/dist/registrations/tts.d.ts +23 -2
- package/dist/registrations/tts.js +120 -98
- package/package.json +27 -25
- package/dist/calculator-providers/CortexToolProvider.d.ts +0 -10
- package/dist/calculator-providers/CortexToolProvider.js +0 -18
- package/dist/calculator-providers/DesmosToolProvider.d.ts +0 -11
- package/dist/calculator-providers/DesmosToolProvider.js +0 -18
- package/dist/calculator-providers/GeoGebraToolProvider.d.ts +0 -13
- package/dist/calculator-providers/GeoGebraToolProvider.js +0 -18
- package/dist/registrations/overlay-element-cache.d.ts +0 -23
- package/dist/registrations/overlay-toolbar-render.d.ts +0 -33
- package/dist/registrations/selection-actions.d.ts +0 -24
|
@@ -7,32 +7,21 @@
|
|
|
7
7
|
import { hasMathContent } from "@pie-players/pie-assessment-toolkit/tools/registration";
|
|
8
8
|
import { createScopedToolId } from "@pie-players/pie-assessment-toolkit/tools/registration";
|
|
9
9
|
import { createToolElement } from "@pie-players/pie-assessment-toolkit/tools/registration";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { GeoGebraToolProvider } from "../calculator-providers/GeoGebraToolProvider.js";
|
|
10
|
+
import { LazyCalculatorToolProvider, } from "../calculator-providers/LazyCalculatorToolProvider.js";
|
|
11
|
+
import { DEFAULT_CALCULATOR_PROVIDER_ID, PACKAGED_CALCULATOR_PROVIDERS, } from "../calculator-providers/packaged-calculator-providers.js";
|
|
13
12
|
import { resolveOverlayElement } from "./overlay-element-cache.js";
|
|
14
|
-
export
|
|
15
|
-
export function resolveCalculatorProviderId(config) {
|
|
13
|
+
export function resolveCalculatorProviderId(config, providers = PACKAGED_CALCULATOR_PROVIDERS) {
|
|
16
14
|
const configured = config?.provider?.id;
|
|
17
15
|
if (configured === undefined || configured === "") {
|
|
18
16
|
return DEFAULT_CALCULATOR_PROVIDER_ID;
|
|
19
17
|
}
|
|
20
|
-
if (configured === "
|
|
21
|
-
configured === "calculator-geogebra" ||
|
|
22
|
-
configured === "calculator-cortex") {
|
|
18
|
+
if (typeof configured === "string" && Object.hasOwn(providers, configured)) {
|
|
23
19
|
return configured;
|
|
24
20
|
}
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
case "calculator-cortex":
|
|
30
|
-
return new CortexToolProvider();
|
|
31
|
-
case "calculator-geogebra":
|
|
32
|
-
return new GeoGebraToolProvider();
|
|
33
|
-
default:
|
|
34
|
-
return new DesmosToolProvider();
|
|
35
|
-
}
|
|
21
|
+
const expected = Object.keys(providers)
|
|
22
|
+
.map((id) => `"${id}"`)
|
|
23
|
+
.join(", ");
|
|
24
|
+
throw new Error(`Unsupported calculator provider "${String(configured)}". Expected one of ${expected}.`);
|
|
36
25
|
}
|
|
37
26
|
function getCalculatorInstanceConfig(config) {
|
|
38
27
|
const theme = config?.theme;
|
|
@@ -104,169 +93,175 @@ function applyCalculatorParamsToElement(element, calculatorType, availableTypes,
|
|
|
104
93
|
}
|
|
105
94
|
}
|
|
106
95
|
/**
|
|
107
|
-
*
|
|
96
|
+
* Builds the calculator registration over the packaged backends plus any the
|
|
97
|
+
* host adds, so a host bundles a backend only by naming it.
|
|
108
98
|
*
|
|
109
99
|
* Supports:
|
|
110
|
-
* - Basic, scientific, and graphing calculators through
|
|
100
|
+
* - Basic, scientific, and graphing calculators through the backend that
|
|
101
|
+
* `provider.id` selects, Desmos by default
|
|
111
102
|
* - Context-aware visibility (shows only when math content is detected)
|
|
112
103
|
* - Item level only
|
|
113
104
|
*/
|
|
114
|
-
export
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
105
|
+
export function createCalculatorToolRegistration(options = {}) {
|
|
106
|
+
const providers = { ...PACKAGED_CALCULATOR_PROVIDERS, ...options.providers };
|
|
107
|
+
return {
|
|
108
|
+
toolId: "calculator",
|
|
109
|
+
name: "Calculator",
|
|
110
|
+
description: "Multi-type calculator (basic, scientific, graphing)",
|
|
111
|
+
nameKey: "tools.calculator.name",
|
|
112
|
+
descriptionKey: "tools.calculator.description",
|
|
113
|
+
icon: "calculator",
|
|
114
|
+
provider: {
|
|
115
|
+
createProvider: (config) => new LazyCalculatorToolProvider(providers[resolveCalculatorProviderId(config, providers)]),
|
|
116
|
+
getInitConfig: (config) => config?.provider?.init ?? {},
|
|
117
|
+
getAuthFetcher: (config) => {
|
|
118
|
+
const runtimeAuthFetcher = config?.provider?.runtime?.authFetcher;
|
|
119
|
+
return typeof runtimeAuthFetcher === "function"
|
|
120
|
+
? runtimeAuthFetcher
|
|
121
|
+
: undefined;
|
|
122
|
+
},
|
|
123
|
+
lazy: true,
|
|
124
|
+
},
|
|
125
|
+
// Calculator is item-level in this player architecture.
|
|
126
|
+
supportedLevels: ["item"],
|
|
127
|
+
/**
|
|
128
|
+
* Pass 2: Determine if calculator is relevant in this context
|
|
129
|
+
*
|
|
130
|
+
* Calculator is relevant when context contains mathematical content
|
|
131
|
+
* (MathML, LaTeX, arithmetic markers).
|
|
132
|
+
*/
|
|
133
|
+
isVisibleInContext(context) {
|
|
134
|
+
return hasMathContent(context);
|
|
129
135
|
},
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
*/
|
|
231
|
-
initialAlign: "bottom-left",
|
|
232
|
-
initialMargin: 16,
|
|
233
|
-
// Header controls in the host's design system, and the layout that
|
|
234
|
-
// goes with them.
|
|
235
|
-
ndsHeaderControls: true,
|
|
236
|
-
// A learner reads the question while using the calculator, so Tab and
|
|
237
|
-
// Shift+Tab cross between the page and the shell instead of cycling
|
|
238
|
-
// inside it.
|
|
239
|
-
pageTabOrder: true,
|
|
240
|
-
content: {
|
|
241
|
-
overflowY: "auto",
|
|
242
|
-
preserveMinHeight: true,
|
|
136
|
+
renderToolbar(context, toolbarContext) {
|
|
137
|
+
const { calculatorType, availableTypes, displayName } = getCalculatorRenderParams(toolbarContext);
|
|
138
|
+
const calculatorToolConfig = toolbarContext.toolkitCoordinator?.getToolConfig(this.toolId) ||
|
|
139
|
+
undefined;
|
|
140
|
+
const calculatorConfig = getCalculatorInstanceConfig(calculatorToolConfig);
|
|
141
|
+
const fullToolId = createScopedToolId(this.toolId, toolbarContext.scope.level, toolbarContext.scope.scopeId);
|
|
142
|
+
const componentOverrides = toolbarContext.componentOverrides;
|
|
143
|
+
// Reused across renders: besides keeping state, this spares a Desmos boot
|
|
144
|
+
// and container mount on every re-render.
|
|
145
|
+
const overlay = resolveOverlayElement(toolbarContext, fullToolId, () => createToolElement(this.toolId, context, toolbarContext, componentOverrides));
|
|
146
|
+
overlay.setAttribute("tool-id", fullToolId);
|
|
147
|
+
applyCalculatorParamsToElement(overlay, calculatorType, availableTypes, calculatorConfig);
|
|
148
|
+
const button = {
|
|
149
|
+
toolId: this.toolId,
|
|
150
|
+
label: displayName,
|
|
151
|
+
icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
|
|
152
|
+
// Routes this button through <pie-nds-icon-button> where the host enables NDS
|
|
153
|
+
// icons. Declared here because which capabilities render in the host's
|
|
154
|
+
// design system is a composition decision.
|
|
155
|
+
faIconName: "calculator",
|
|
156
|
+
disabled: false,
|
|
157
|
+
// The name stays put across open and closed. The toolbar mirrors
|
|
158
|
+
// `active` onto the button as `aria-pressed`, so the state is already
|
|
159
|
+
// exposed; a name that also swapped to "Close …" would contradict it.
|
|
160
|
+
ariaLabel: displayName,
|
|
161
|
+
tooltip: displayName,
|
|
162
|
+
onClick: () => toolbarContext.toggleTool(this.toolId),
|
|
163
|
+
active: toolbarContext.isToolVisible(this.toolId),
|
|
164
|
+
};
|
|
165
|
+
let lastVisibleState = button.active;
|
|
166
|
+
if (overlay.visible !== button.active) {
|
|
167
|
+
overlay.visible = button.active;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
toolId: this.toolId,
|
|
171
|
+
elements: [
|
|
172
|
+
{
|
|
173
|
+
element: overlay,
|
|
174
|
+
mount: "after-buttons",
|
|
175
|
+
shell: {
|
|
176
|
+
// The variant name the button already carries — "Basic Calculator",
|
|
177
|
+
// not the registration's generic one.
|
|
178
|
+
title: displayName,
|
|
179
|
+
draggable: true,
|
|
180
|
+
resizable: true,
|
|
181
|
+
closeable: true,
|
|
182
|
+
/*
|
|
183
|
+
* Sized per type from what each layout measures, rather than one
|
|
184
|
+
* size for all three. A shell subtracts ~74px of header from the
|
|
185
|
+
* height it is given, and the numbers below are the content each
|
|
186
|
+
* type needs plus room for its history tape:
|
|
187
|
+
*
|
|
188
|
+
* - basic: strip, display, edit row and a four-row keypad measure
|
|
189
|
+
* 398px. 560 left ~90px of blank above the entry line, which is
|
|
190
|
+
* the gap that made the panel look mis-sized.
|
|
191
|
+
* - scientific: the same plus a keypad layer tab row.
|
|
192
|
+
* - graphing: the expression rail beside the plot, whose column
|
|
193
|
+
* wants 486px on its own — viewport controls, a 14rem board floor
|
|
194
|
+
* and the readout that *is* the graph for assistive technology,
|
|
195
|
+
* since the board itself is `aria-hidden`. At 620 that column was
|
|
196
|
+
* 12px short and the readout's last line was cut off.
|
|
197
|
+
*
|
|
198
|
+
* The minimums stay shared across types and backends. One
|
|
199
|
+
* registration serves every backend, so a graphing-only floor
|
|
200
|
+
* would move the resize limit under vendors whose layouts were
|
|
201
|
+
* never measured for it. Cortex below 42rem stacks the rail above
|
|
202
|
+
* the plot and needs 701px there, which no spacing tier closes — it
|
|
203
|
+
* scrolls its own content instead of clipping it, which is the
|
|
204
|
+
* contract every size below the opening one already relies on.
|
|
205
|
+
*/
|
|
206
|
+
initialWidth: calculatorType === "graphing" ? 720 : 380,
|
|
207
|
+
initialHeight: calculatorType === "graphing"
|
|
208
|
+
? 660
|
|
209
|
+
: calculatorType === "basic"
|
|
210
|
+
? 500
|
|
211
|
+
: 560,
|
|
212
|
+
minWidth: 380,
|
|
213
|
+
minHeight: 480,
|
|
214
|
+
/*
|
|
215
|
+
* `bottom-right` put a 560-620px-tall shell over the items
|
|
216
|
+
* column it shares the viewport with — tall enough, at
|
|
217
|
+
* ordinary viewport heights, to sit on top of a sibling
|
|
218
|
+
* item's own toolbar row and block its button from mouse
|
|
219
|
+
* and touch input. `bottom-left` keeps the same footprint
|
|
220
|
+
* over the passage column instead, which carries no
|
|
221
|
+
* per-item controls to collide with.
|
|
222
|
+
*/
|
|
223
|
+
initialAlign: "bottom-left",
|
|
224
|
+
initialMargin: 16,
|
|
225
|
+
// Header controls in the host's design system, and the layout that
|
|
226
|
+
// goes with them.
|
|
227
|
+
ndsHeaderControls: true,
|
|
228
|
+
// A learner reads the question while using the calculator, so Tab and
|
|
229
|
+
// Shift+Tab cross between the page and the shell instead of cycling
|
|
230
|
+
// inside it.
|
|
231
|
+
pageTabOrder: true,
|
|
232
|
+
content: {
|
|
233
|
+
overflowY: "auto",
|
|
234
|
+
preserveMinHeight: true,
|
|
235
|
+
},
|
|
243
236
|
},
|
|
244
237
|
},
|
|
238
|
+
],
|
|
239
|
+
button,
|
|
240
|
+
sync: () => {
|
|
241
|
+
const active = toolbarContext.isToolVisible(this.toolId);
|
|
242
|
+
button.active = active;
|
|
243
|
+
button.label = displayName;
|
|
244
|
+
// Static across the toggle: `aria-pressed` carries the state, and a
|
|
245
|
+
// "Close …" template built from a localized noun mixes languages
|
|
246
|
+
// ("Close rekenmachine" under nl-NL).
|
|
247
|
+
button.ariaLabel = displayName;
|
|
248
|
+
button.tooltip = displayName;
|
|
249
|
+
if (lastVisibleState !== active) {
|
|
250
|
+
overlay.visible = active;
|
|
251
|
+
lastVisibleState = active;
|
|
252
|
+
}
|
|
253
|
+
applyCalculatorParamsToElement(overlay, calculatorType, availableTypes, calculatorConfig);
|
|
245
254
|
},
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
lastVisibleState = active;
|
|
260
|
-
}
|
|
261
|
-
applyCalculatorParamsToElement(overlay, calculatorType, availableTypes, calculatorConfig);
|
|
262
|
-
},
|
|
263
|
-
subscribeActive: (callback) => {
|
|
264
|
-
if (!toolbarContext.subscribeVisibility)
|
|
265
|
-
return () => { };
|
|
266
|
-
return toolbarContext.subscribeVisibility(() => {
|
|
267
|
-
callback(toolbarContext.isToolVisible(this.toolId));
|
|
268
|
-
});
|
|
269
|
-
},
|
|
270
|
-
};
|
|
271
|
-
},
|
|
272
|
-
};
|
|
255
|
+
subscribeActive: (callback) => {
|
|
256
|
+
if (!toolbarContext.subscribeVisibility)
|
|
257
|
+
return () => { };
|
|
258
|
+
return toolbarContext.subscribeVisibility(() => {
|
|
259
|
+
callback(toolbarContext.isToolVisible(this.toolId));
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** The calculator over the packaged Desmos and GeoGebra backends. */
|
|
267
|
+
export const calculatorToolRegistration = createCalculatorToolRegistration();
|
|
@@ -4,7 +4,20 @@
|
|
|
4
4
|
* Registers tools for interacting with question content:
|
|
5
5
|
* - Answer Eliminator (strike through answer choices)
|
|
6
6
|
*/
|
|
7
|
-
import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/registration";
|
|
7
|
+
import type { ToolProviderConfig, ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/registration";
|
|
8
|
+
/**
|
|
9
|
+
* The answer eliminator's `tools.providers.answerEliminator` entry.
|
|
10
|
+
*/
|
|
11
|
+
export interface AnswerEliminatorToolConfig extends ToolProviderConfig {
|
|
12
|
+
/**
|
|
13
|
+
* The elements the eliminator acts on, by the name `<name>` of their
|
|
14
|
+
* `@pie-element/<name>` package, such as `["multiple-choice"]`. An element
|
|
15
|
+
* must also support answer masking. Omitted, every element that supports it
|
|
16
|
+
* takes the eliminator; empty, none does. A host lists the elements whose
|
|
17
|
+
* support it has vetted.
|
|
18
|
+
*/
|
|
19
|
+
elements?: readonly string[];
|
|
20
|
+
}
|
|
8
21
|
/**
|
|
9
22
|
* Answer Eliminator tool registration
|
|
10
23
|
*
|
|
@@ -19,24 +19,84 @@ const ANSWER_MASKING = "answer-masking";
|
|
|
19
19
|
* its supports is decided by its declaration, these two included.
|
|
20
20
|
*/
|
|
21
21
|
const UNDECLARED_ANSWER_MASKING_ELEMENTS = new Set(["multiple-choice", "ebsr"]);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// tag, or any tag the item's `elements` map binds to `@pie-element/multiple-choice`.
|
|
25
|
-
function isUndeclaredAnswerMaskingElement(tag, packageSpec) {
|
|
26
|
-
const name = tag
|
|
22
|
+
function elementName(tag) {
|
|
23
|
+
return tag
|
|
27
24
|
.toLowerCase()
|
|
28
25
|
.replace(/--version-[0-9a-z-]+$/, "")
|
|
29
26
|
.replace(/^pie-(element-)?/, "");
|
|
30
|
-
if (UNDECLARED_ANSWER_MASKING_ELEMENTS.has(name))
|
|
31
|
-
return true;
|
|
32
|
-
const packageName = packageSpec?.match(/^@pie-element\/([^@]+)/)?.[1];
|
|
33
|
-
return !!packageName && UNDECLARED_ANSWER_MASKING_ELEMENTS.has(packageName);
|
|
34
27
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
// Content names one element several ways: `multiple-choice`,
|
|
29
|
+
// `pie-multiple-choice`, `pie-element-multiple-choice`, a runtime `--version-*`
|
|
30
|
+
// tag, or any tag the item's `elements` map binds to `@pie-element/multiple-choice`.
|
|
31
|
+
function elementNames(tag, packageSpec) {
|
|
32
|
+
const names = [elementName(tag)];
|
|
33
|
+
const packageName = pieElementPackageName(packageSpec);
|
|
34
|
+
if (packageName)
|
|
35
|
+
names.push(packageName);
|
|
36
|
+
return names;
|
|
37
|
+
}
|
|
38
|
+
function pieElementPackageName(packageSpec) {
|
|
39
|
+
return packageSpec?.match(/^@pie-element\/([^@]+)/)?.[1]?.toLowerCase() ?? null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The name an `elements` list matches: the `@pie-element/<name>` package the
|
|
43
|
+
* item binds the tag to, else the tag's own name. A tag bound to another
|
|
44
|
+
* package matches no name, whatever the tag says.
|
|
45
|
+
*/
|
|
46
|
+
function listedName(tag, packageSpec) {
|
|
47
|
+
return packageSpec ? pieElementPackageName(packageSpec) : elementName(tag);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The element names `config.elements` allows, or `null` for every element. An
|
|
51
|
+
* `elements` that is not a list allows none: validation reports it, and an
|
|
52
|
+
* element a host has not vetted stays without the eliminator.
|
|
53
|
+
*/
|
|
54
|
+
function allowedElementNames(config) {
|
|
55
|
+
const elements = config
|
|
56
|
+
?.elements;
|
|
57
|
+
if (elements === undefined)
|
|
58
|
+
return null;
|
|
59
|
+
if (!Array.isArray(elements))
|
|
60
|
+
return new Set();
|
|
61
|
+
return new Set(elements
|
|
62
|
+
.filter((name) => typeof name === "string")
|
|
63
|
+
.map(elementName));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The model ids of the context's elements that support answer masking and the
|
|
67
|
+
* host config allows.
|
|
68
|
+
*/
|
|
69
|
+
function answerMaskingElementIds(context, config) {
|
|
70
|
+
const allowed = allowedElementNames(config);
|
|
71
|
+
return elementSupports(context)
|
|
72
|
+
.filter(({ supports, tag, packageSpec }) => {
|
|
73
|
+
if (allowed) {
|
|
74
|
+
const name = listedName(tag, packageSpec);
|
|
75
|
+
if (name === null || !allowed.has(name))
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return supports
|
|
79
|
+
? supports.includes(ANSWER_MASKING)
|
|
80
|
+
: elementNames(tag, packageSpec).some((name) => UNDECLARED_ANSWER_MASKING_ELEMENTS.has(name));
|
|
81
|
+
})
|
|
82
|
+
.flatMap(({ elementId }) => (elementId ? [elementId] : []));
|
|
83
|
+
}
|
|
84
|
+
function validateAnswerEliminatorConfig(config) {
|
|
85
|
+
const { elements } = config;
|
|
86
|
+
if (elements === undefined)
|
|
87
|
+
return [];
|
|
88
|
+
if (Array.isArray(elements) &&
|
|
89
|
+
elements.every((name) => typeof name === "string" && name.trim() !== "")) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
code: "tools.invalidProviderConfig",
|
|
95
|
+
severity: "error",
|
|
96
|
+
path: "providers.answerEliminator.elements",
|
|
97
|
+
message: '`elements` must be a list of element names, such as ["multiple-choice"].',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
40
100
|
}
|
|
41
101
|
/** The state-store key of each PIE element in the item, by model id. */
|
|
42
102
|
function resolveElementStateKeys(context, toolbarContext) {
|
|
@@ -67,23 +127,25 @@ export const answerEliminatorToolRegistration = {
|
|
|
67
127
|
descriptionKey: "tools.answerEliminator.description",
|
|
68
128
|
icon: "strikethrough",
|
|
69
129
|
supportedLevels: ["item"],
|
|
130
|
+
validateConfig: validateAnswerEliminatorConfig,
|
|
70
131
|
/**
|
|
71
132
|
* Pass 2: relevant where an element supports answer masking. An element
|
|
72
133
|
* declares it on its class, and the answer eliminator's README says what the
|
|
73
134
|
* element then supplies.
|
|
74
135
|
*/
|
|
75
136
|
isVisibleInContext(context) {
|
|
76
|
-
return
|
|
137
|
+
return answerMaskingElementIds(context, null).length > 0;
|
|
77
138
|
},
|
|
78
139
|
/**
|
|
79
|
-
* Pass 3: the same question, asked as a capability
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* answer
|
|
140
|
+
* Pass 3: the same question, asked as a capability and narrowed to the
|
|
141
|
+
* elements the host config allows. Elimination controls are rendered per
|
|
142
|
+
* choice, so an item none of whose allowed elements supports answer masking
|
|
143
|
+
* gives this tool nothing to act on. Answering the relevance gate alone left
|
|
144
|
+
* the button on such items for any learner whose profile grants answer
|
|
145
|
+
* masking.
|
|
84
146
|
*/
|
|
85
|
-
isApplicableToContent(context) {
|
|
86
|
-
return
|
|
147
|
+
isApplicableToContent(context, config) {
|
|
148
|
+
return answerMaskingElementIds(context, config).length > 0;
|
|
87
149
|
},
|
|
88
150
|
renderToolbar(context, toolbarContext) {
|
|
89
151
|
const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
|
|
@@ -94,7 +156,7 @@ export const answerEliminatorToolRegistration = {
|
|
|
94
156
|
const button = {
|
|
95
157
|
toolId: this.toolId,
|
|
96
158
|
label: this.name,
|
|
97
|
-
icon:
|
|
159
|
+
icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
|
|
98
160
|
disabled: false,
|
|
99
161
|
ariaLabel: toolbarContext.i18n.t("tools.answerEliminator.buttonA11y"),
|
|
100
162
|
tooltip: toolbarContext.i18n.t("tools.answerEliminator.tooltip"),
|
|
@@ -112,6 +174,12 @@ export const answerEliminatorToolRegistration = {
|
|
|
112
174
|
isActive: visibility.isActive,
|
|
113
175
|
});
|
|
114
176
|
overlay.elementStateKeys = resolveElementStateKeys(context, toolbarContext);
|
|
177
|
+
const config = toolbarContext.toolkitCoordinator?.getToolConfig(this.toolId) ??
|
|
178
|
+
null;
|
|
179
|
+
overlay.elementIds =
|
|
180
|
+
allowedElementNames(config) === null
|
|
181
|
+
? undefined
|
|
182
|
+
: answerMaskingElementIds(context, config);
|
|
115
183
|
},
|
|
116
184
|
subscribeActive: visibility.subscribeActive,
|
|
117
185
|
};
|
|
@@ -4,12 +4,33 @@
|
|
|
4
4
|
* Registers the TTS tool for reading content aloud.
|
|
5
5
|
*/
|
|
6
6
|
import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/registration";
|
|
7
|
+
import { TTSToolProvider } from "@pie-players/pie-assessment-toolkit/tools/registration";
|
|
8
|
+
type TTSToolProviderOptions = NonNullable<ConstructorParameters<typeof TTSToolProvider>[1]>;
|
|
9
|
+
/** A host's own text-to-speech provider, selected by `provider.id`. */
|
|
10
|
+
export interface TTSHostProviderDescriptor {
|
|
11
|
+
/** Resolves the host's `ITTSProvider` class; imported only when selected. */
|
|
12
|
+
load: NonNullable<TTSToolProviderOptions["loadHostProvider"]>;
|
|
13
|
+
/** Wait on `provider.runtime.authFetcher` before initializing. Default `false`. */
|
|
14
|
+
requiresAuth?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface TTSRegistrationOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Host providers by `provider.id`. A `textToSpeech` config whose
|
|
19
|
+
* `provider.id` names none of them, or names none at all, keeps the browser
|
|
20
|
+
* or server backend its settings select.
|
|
21
|
+
*/
|
|
22
|
+
providers?: Readonly<Record<string, TTSHostProviderDescriptor>>;
|
|
23
|
+
}
|
|
7
24
|
/**
|
|
8
|
-
*
|
|
25
|
+
* Builds the text-to-speech registration over the browser and server backends
|
|
26
|
+
* plus any host providers, so a host bundles its provider only by naming it.
|
|
9
27
|
*
|
|
10
28
|
* Supports:
|
|
11
|
-
* - Reading content aloud using browser TTS or
|
|
29
|
+
* - Reading content aloud using browser TTS, a TTS server or a host provider
|
|
12
30
|
* - Context-aware visibility (shows when readable text is available)
|
|
13
31
|
* - Item and passage levels
|
|
14
32
|
*/
|
|
33
|
+
export declare function createTTSToolRegistration(options?: TTSRegistrationOptions): ToolRegistration;
|
|
34
|
+
/** The text-to-speech registration with the browser and server backends only. */
|
|
15
35
|
export declare const ttsToolRegistration: ToolRegistration;
|
|
36
|
+
export {};
|