@pie-players/pie-tool-answer-eliminator 0.3.50 → 0.3.52

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 CHANGED
@@ -179,18 +179,21 @@ if (saved) {
179
179
 
180
180
  ### 1. Choice Detection
181
181
 
182
- The tool automatically detects answer choices within the scoped element:
182
+ The tool detects choices through an **adapter registry** (`AdapterRegistry`), not a fixed selector list. Each adapter knows how to find and operate on choices for a specific PIE element type, and the registry runs them in priority order (`canHandle()`, then `findChoices()`), so a single question can mix element types:
183
+
184
+ | Adapter | Element type | How it finds choices |
185
+ | --- | --- | --- |
186
+ | `MultipleChoiceAdapter` | `multiple-choice` (single- or multi-select) | `.corespring-checkbox` / `.corespring-radio-button` choices |
187
+ | `EBSRAdapter` | `ebsr` | delegates to the multiple-choice adapter for each `ebsr-multiple-choice` part (Part A/B), prefixing choice IDs with the part |
188
+ | `InlineDropdownAdapter` | `inline-dropdown` | dropdown items with `role="option"` |
189
+
190
+ Support more element types at runtime by registering your own adapter (any object implementing the `ChoiceAdapter` interface: `canHandle`, `findChoices`, `getChoiceId`, `getChoiceLabel`, `canEliminate`, `createChoiceRange`, `getButtonContainer`):
183
191
 
184
192
  ```typescript
185
- // Searches for choice elements with these patterns
186
- const choiceSelectors = [
187
- '[data-choice-id]', // PIE standard
188
- '.choice', // Common class
189
- '[role="radio"]', // Accessibility
190
- '[role="checkbox"]', // Accessibility
191
- 'input[type="radio"]', // Native inputs
192
- 'input[type="checkbox"]' // Native inputs
193
- ];
193
+ import { AdapterRegistry } from '@pie-players/pie-tool-answer-eliminator';
194
+
195
+ const registry = new AdapterRegistry();
196
+ registry.registerAdapter(myCustomChoiceAdapter);
194
197
  ```
195
198
 
196
199
  ### 2. State Storage
@@ -205,26 +208,16 @@ Eliminated choices are stored by choice ID:
205
208
 
206
209
  ### 3. Visual Feedback
207
210
 
208
- Eliminated choices receive the `eliminated` CSS class:
209
-
210
- ```css
211
- .choice.eliminated {
212
- text-decoration: line-through;
213
- opacity: 0.5;
214
- }
215
- ```
211
+ Elimination styling is applied by a pluggable `EliminationStrategy` built on the **CSS Custom Highlight API** (zero DOM mutation, so the choice text and structure stay intact for screen readers). Two strategies ship today:
216
212
 
217
- ### 4. Toggle Behavior
213
+ - `strikethrough` (default): a `::highlight(pie-answer-eliminated-<id>)` rule renders a line-through over the choice label.
214
+ - `mask`: a `::highlight(pie-answer-masked-<id>)` rule dims and blurs the choice.
218
215
 
219
- Clicking a choice toggles its elimination state:
216
+ For browsers without the Highlight API, each strategy falls back to a class on the choice container. Either way the eliminated choice also receives ARIA hooks (`data-pie-answer-eliminated`, plus `aria-disabled`/`aria-hidden` and an offscreen "(eliminated)" announcement) for assistive technology.
220
217
 
221
- ```typescript
222
- // First click: eliminate
223
- choiceElement.classList.add('eliminated');
218
+ ### 4. Toggle Behavior
224
219
 
225
- // Second click: restore
226
- choiceElement.classList.remove('eliminated');
227
- ```
220
+ Each detected choice gets its own elimination toggle button (class `pie-answer-eliminator-toggle`, rendered as `⊗`) placed via the adapter's `getButtonContainer()`. Clicking the button toggles that choice through the active strategy. Eliminating is blocked when the adapter's `canEliminate()` returns false — for example when the choice is already selected, disabled, or the item is in evaluate/view mode. An active button is marked with `pie-answer-eliminator-toggle--active` and `aria-pressed="true"`; clicking again restores the choice.
228
221
 
229
222
  ## Cleanup
230
223
 
@@ -37,22 +37,20 @@ function R(t) {
37
37
  var I = (t) => {
38
38
  const e = (t.providerOptions && typeof t.providerOptions == "object" ? t.providerOptions : {}).__pieTelemetry;
39
39
  return typeof e == "function" ? e : void 0;
40
- }, E = {
40
+ }, M = {
41
41
  pie: 3e3,
42
42
  custom: 3e3
43
- }, M = /* @__PURE__ */ new Set(["polly", "google"]), P = (t) => {
43
+ }, P = /* @__PURE__ */ new Set(["polly", "google"]), O = (t) => {
44
44
  if (!t || b(t) !== "pie") return !1;
45
45
  const e = (t.provider || "polly").toLowerCase();
46
- return M.has(e);
47
- }, S = (t) => t.replace(/\/+$/, ""), O = (t) => {
46
+ return P.has(e);
47
+ }, S = (t) => t.replace(/\/+$/, ""), U = (t) => {
48
48
  const e = S(t.apiEndpoint), r = (t.provider || "").toLowerCase();
49
49
  return r === "polly" || r === "google" ? `${e}/${r}/voices` : `${e}/voices`;
50
- }, b = (t) => t.transportMode === "custom" ? "custom" : t.transportMode === "pie" ? "pie" : t.provider === "custom" ? "custom" : "pie", k = (t, e) => t.endpointMode ? t.endpointMode : e === "custom" ? "rootPost" : "synthesizePath", U = (t, e) => t.endpointValidationMode ? t.endpointValidationMode : e === "custom" ? "none" : "voices", L = (t) => {
50
+ }, b = (t) => t.transportMode === "custom" ? "custom" : t.transportMode === "pie" ? "pie" : t.provider === "custom" ? "custom" : "pie", k = (t, e) => t.endpointMode ? t.endpointMode : e === "custom" ? "rootPost" : "synthesizePath", L = (t, e) => t.endpointValidationMode ? t.endpointValidationMode : e === "custom" ? "none" : "voices", E = (t) => Number.isFinite(t) ? t < 1 ? "slow" : t > 1 ? "fast" : "medium" : "medium", B = (t) => {
51
51
  const e = t.providerOptions || {};
52
- if (typeof e.speedRate == "string") return e.speedRate;
53
- const r = Number(t.rate ?? 1);
54
- return !Number.isFinite(r) || r <= 0.95 ? "slow" : r >= 1.5 ? "fast" : "medium";
55
- }, B = (t) => {
52
+ return typeof e.speedRate == "string" ? e.speedRate : E(Number(t.rate ?? 1));
53
+ }, C = (t) => {
56
54
  const e = [];
57
55
  let r = 0;
58
56
  const i = t.split(`
@@ -69,7 +67,7 @@ var I = (t) => {
69
67
  } catch {
70
68
  }
71
69
  return e;
72
- }, C = (t) => {
70
+ }, z = (t) => {
73
71
  if (!Array.isArray(t)) return [];
74
72
  const e = [];
75
73
  let r = 0;
@@ -85,7 +83,7 @@ var I = (t) => {
85
83
  });
86
84
  }
87
85
  return e.sort((i, o) => i.time !== o.time ? i.time - o.time : i.start !== o.start ? i.start - o.start : i.end - o.end);
88
- }, z = {
86
+ }, x = {
89
87
  pie: {
90
88
  id: "pie",
91
89
  resolveSynthesisUrl: (t) => {
@@ -129,7 +127,7 @@ var I = (t) => {
129
127
  const r = e.providerOptions || {}, i = typeof r.lang_id == "string" ? r.lang_id : e.language || "en-US", o = typeof r.cache == "boolean" ? r.cache : !0;
130
128
  return {
131
129
  text: t,
132
- speedRate: L(e),
130
+ speedRate: B(e),
133
131
  lang_id: i,
134
132
  cache: o
135
133
  };
@@ -139,7 +137,7 @@ var I = (t) => {
139
137
  if (e.includeAuthOnAssetFetch)
140
138
  for (const [l, u] of Object.entries(r)) l.toLowerCase() === "authorization" && (s[l] = u);
141
139
  let a = [];
142
- const n = C(o.speechMarks);
140
+ const n = z(o.speechMarks);
143
141
  if (n.length > 0) a = n;
144
142
  else if (typeof o.word == "string" && o.word.length > 0) {
145
143
  const l = T(o.word, e);
@@ -149,7 +147,7 @@ var I = (t) => {
149
147
  signal: i,
150
148
  redirect: "error"
151
149
  });
152
- y.ok && (a = B(await y.text()));
150
+ y.ok && (a = C(await y.text()));
153
151
  }
154
152
  }
155
153
  const h = T(o.audioContent, e);
@@ -163,7 +161,7 @@ var I = (t) => {
163
161
  };
164
162
  }
165
163
  }
166
- }, x = class {
164
+ }, N = class {
167
165
  constructor(t, e) {
168
166
  d(this, "config", void 0), d(this, "adapter", void 0), d(this, "currentAudio", null), d(this, "pausedState", !1), d(this, "wordTimings", []), d(this, "highlightInterval", null), d(this, "intentionallyStopped", !1), d(this, "activeSynthesisController", null), d(this, "synthesisRunId", 0), d(this, "telemetryReporter", void 0), d(this, "onWordBoundary", void 0), this.config = t, this.adapter = e, this.telemetryReporter = I(t);
169
167
  }
@@ -390,9 +388,12 @@ var I = (t) => {
390
388
  return this.pausedState;
391
389
  }
392
390
  updateSettings(t) {
393
- t.rate !== void 0 && (this.config.rate = t.rate), t.pitch !== void 0 && (this.config.pitch = t.pitch), t.voice !== void 0 && (this.config.voice = t.voice);
391
+ t.rate !== void 0 && (this.config.rate = t.rate, this.config.providerOptions = {
392
+ ...this.config.providerOptions || {},
393
+ speedRate: E(Number(t.rate))
394
+ }), t.pitch !== void 0 && (this.config.pitch = t.pitch), t.voice !== void 0 && (this.config.voice = t.voice);
394
395
  }
395
- }, N = class {
396
+ }, F = class {
396
397
  constructor() {
397
398
  d(this, "providerId", "server-tts"), d(this, "providerName", "Server TTS"), d(this, "version", "1.0.0"), d(this, "config", null), d(this, "adapter", null), d(this, "telemetryReporter", void 0);
398
399
  }
@@ -408,7 +409,7 @@ var I = (t) => {
408
409
  if (!e.apiEndpoint) throw new Error("apiEndpoint is required for ServerTTSProvider");
409
410
  this.config = e, this.telemetryReporter = I(e);
410
411
  const r = b(e);
411
- if (this.adapter = z[r], e.validateEndpoint) {
412
+ if (this.adapter = x[r], e.validateEndpoint) {
412
413
  const i = Date.now();
413
414
  if (await this.emitTelemetry("pie-tool-backend-call-start", {
414
415
  toolId: "tts",
@@ -430,17 +431,17 @@ var I = (t) => {
430
431
  duration: Date.now() - i
431
432
  });
432
433
  }
433
- return new x(e, this.adapter);
434
+ return new N(e, this.adapter);
434
435
  }
435
436
  async testAPIAvailability() {
436
437
  if (!this.config || !this.adapter) return !1;
437
438
  try {
438
439
  const t = { ...this.config.headers };
439
440
  this.config.authToken && (t.Authorization = `Bearer ${this.config.authToken}`);
440
- const e = new AbortController(), r = setTimeout(() => e.abort(), 5e3), i = U(this.config, this.adapter.id);
441
+ const e = new AbortController(), r = setTimeout(() => e.abort(), 5e3), i = L(this.config, this.adapter.id);
441
442
  if (i === "none")
442
443
  return clearTimeout(r), !0;
443
- const o = i === "voices" ? O(this.config) : this.adapter.resolveSynthesisUrl(this.config), s = i === "voices" ? "GET" : "OPTIONS";
444
+ const o = i === "voices" ? U(this.config) : this.adapter.resolveSynthesisUrl(this.config), s = i === "voices" ? "GET" : "OPTIONS";
444
445
  try {
445
446
  const a = await fetch(o, {
446
447
  method: s,
@@ -478,8 +479,8 @@ var I = (t) => {
478
479
  supportsVoiceSelection: !0,
479
480
  supportsRateControl: !0,
480
481
  supportsPitchControl: !1,
481
- supportsSSML: P(this.config),
482
- maxTextLength: E[t]
482
+ supportsSSML: O(this.config),
483
+ maxTextLength: M[t]
483
484
  };
484
485
  }
485
486
  destroy() {
@@ -487,5 +488,5 @@ var I = (t) => {
487
488
  }
488
489
  };
489
490
  export {
490
- N as ServerTTSProvider
491
+ F as ServerTTSProvider
491
492
  };