@pie-players/pie-tool-answer-eliminator 0.2.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 +282 -0
- package/adapters/adapter-registry.ts +64 -0
- package/adapters/choice-adapter.ts +50 -0
- package/adapters/ebsr-adapter.ts +61 -0
- package/adapters/inline-dropdown-adapter.ts +46 -0
- package/adapters/multiple-choice-adapter.ts +96 -0
- package/answer-eliminator-core.ts +465 -0
- package/dist/adapters/adapter-registry.d.ts +26 -0
- package/dist/adapters/adapter-registry.d.ts.map +1 -0
- package/dist/adapters/choice-adapter.d.ts +43 -0
- package/dist/adapters/choice-adapter.d.ts.map +1 -0
- package/dist/adapters/ebsr-adapter.d.ts +20 -0
- package/dist/adapters/ebsr-adapter.d.ts.map +1 -0
- package/dist/adapters/inline-dropdown-adapter.d.ts +18 -0
- package/dist/adapters/inline-dropdown-adapter.d.ts.map +1 -0
- package/dist/adapters/multiple-choice-adapter.d.ts +20 -0
- package/dist/adapters/multiple-choice-adapter.d.ts.map +1 -0
- package/dist/answer-eliminator-core.d.ts +98 -0
- package/dist/answer-eliminator-core.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/strategies/elimination-strategy.d.ts +41 -0
- package/dist/strategies/elimination-strategy.d.ts.map +1 -0
- package/dist/strategies/mask-strategy.d.ts +28 -0
- package/dist/strategies/mask-strategy.d.ts.map +1 -0
- package/dist/strategies/strikethrough-strategy.d.ts +31 -0
- package/dist/strategies/strikethrough-strategy.d.ts.map +1 -0
- package/dist/tool-answer-eliminator.js +2838 -0
- package/dist/tool-answer-eliminator.js.map +1 -0
- package/dist/tool-answer-eliminator.svelte.d.ts +1 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.d.ts.map +1 -0
- package/index.ts +11 -0
- package/package.json +69 -0
- package/strategies/elimination-strategy.ts +47 -0
- package/strategies/mask-strategy.ts +180 -0
- package/strategies/strikethrough-strategy.ts +239 -0
- package/tool-answer-eliminator.svelte +250 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { AdapterRegistry } from "./adapters/adapter-registry";
|
|
2
|
+
import type { ChoiceAdapter } from "./adapters/choice-adapter";
|
|
3
|
+
import type { EliminationStrategy } from "./strategies/elimination-strategy";
|
|
4
|
+
import { MaskStrategy } from "./strategies/mask-strategy";
|
|
5
|
+
import { StrikethroughStrategy } from "./strategies/strikethrough-strategy";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Core engine for answer eliminator tool
|
|
9
|
+
* Coordinates adapters, strategies, and state management
|
|
10
|
+
*/
|
|
11
|
+
export class AnswerEliminatorCore {
|
|
12
|
+
private registry: AdapterRegistry;
|
|
13
|
+
private strategy: EliminationStrategy;
|
|
14
|
+
private eliminatedChoices = new Set<string>(); // Set<choiceId> for current element
|
|
15
|
+
private choiceElements = new Map<string, HTMLElement>(); // choiceId -> element
|
|
16
|
+
private choiceButtons = new Map<string, HTMLButtonElement>(); // choiceId -> button
|
|
17
|
+
private buttonAlignment: "left" | "right" | "inline" = "right";
|
|
18
|
+
private shouldRestoreState: boolean = true; // Whether to restore eliminations from state storage
|
|
19
|
+
|
|
20
|
+
// Store integration (replaces session/localStorage)
|
|
21
|
+
private storeIntegration: {
|
|
22
|
+
store: any; // ElementToolStateStore
|
|
23
|
+
globalElementId: string; // Composite key: "assessmentId:sectionId:itemId:elementId"
|
|
24
|
+
} | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
strategyType: "strikethrough" | "mask" | "gray" = "strikethrough",
|
|
28
|
+
buttonAlignment: "left" | "right" | "inline" = "right",
|
|
29
|
+
) {
|
|
30
|
+
this.registry = new AdapterRegistry();
|
|
31
|
+
this.strategy = this.createStrategy(strategyType);
|
|
32
|
+
this.strategy.initialize();
|
|
33
|
+
this.buttonAlignment = buttonAlignment;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private createStrategy(type: string): EliminationStrategy {
|
|
37
|
+
switch (type) {
|
|
38
|
+
case "mask":
|
|
39
|
+
return new MaskStrategy();
|
|
40
|
+
case "strikethrough":
|
|
41
|
+
default:
|
|
42
|
+
return new StrikethroughStrategy();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize eliminator for a question
|
|
48
|
+
*/
|
|
49
|
+
initializeForQuestion(questionRoot: HTMLElement): void {
|
|
50
|
+
// Clean up previous question
|
|
51
|
+
this.cleanupButtons();
|
|
52
|
+
|
|
53
|
+
// Find all choices with their adapters
|
|
54
|
+
const choicesWithAdapters =
|
|
55
|
+
this.registry.findAllChoicesWithAdapters(questionRoot);
|
|
56
|
+
|
|
57
|
+
// Attach elimination functionality to each choice
|
|
58
|
+
for (const { choice, adapter } of choicesWithAdapters) {
|
|
59
|
+
this.initializeChoice(choice, adapter);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Restore eliminated state from store (only if enabled)
|
|
63
|
+
if (this.shouldRestoreState) {
|
|
64
|
+
this.restoreState();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize a single choice
|
|
70
|
+
*/
|
|
71
|
+
private initializeChoice(choice: HTMLElement, adapter: ChoiceAdapter): void {
|
|
72
|
+
const choiceId = adapter.getChoiceId(choice);
|
|
73
|
+
|
|
74
|
+
// Track element
|
|
75
|
+
this.choiceElements.set(choiceId, choice);
|
|
76
|
+
|
|
77
|
+
// Create elimination toggle button
|
|
78
|
+
const button = this.createToggleButton(choice, adapter);
|
|
79
|
+
if (!button) return;
|
|
80
|
+
|
|
81
|
+
this.choiceButtons.set(choiceId, button);
|
|
82
|
+
|
|
83
|
+
// Attach button to choice
|
|
84
|
+
const container = adapter.getButtonContainer(choice);
|
|
85
|
+
if (container) {
|
|
86
|
+
// Position button within container
|
|
87
|
+
container.style.position = "relative";
|
|
88
|
+
container.appendChild(button);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create elimination toggle button
|
|
94
|
+
*/
|
|
95
|
+
private createToggleButton(
|
|
96
|
+
choice: HTMLElement,
|
|
97
|
+
adapter: ChoiceAdapter,
|
|
98
|
+
): HTMLButtonElement | null {
|
|
99
|
+
const choiceId = adapter.getChoiceId(choice);
|
|
100
|
+
const choiceLabel = adapter.getChoiceLabel(choice);
|
|
101
|
+
|
|
102
|
+
const button = document.createElement("button");
|
|
103
|
+
button.type = "button";
|
|
104
|
+
button.className = "answer-eliminator-toggle";
|
|
105
|
+
button.setAttribute("aria-label", `Toggle elimination for ${choiceLabel}`);
|
|
106
|
+
button.setAttribute("data-choice-id", choiceId);
|
|
107
|
+
button.textContent = "⊗"; // Cross mark (use textContent instead of innerHTML for better security)
|
|
108
|
+
|
|
109
|
+
// Apply positioning based on alignment configuration
|
|
110
|
+
this.applyButtonAlignment(button);
|
|
111
|
+
|
|
112
|
+
// Common button styling
|
|
113
|
+
Object.assign(button.style, {
|
|
114
|
+
width: "28px",
|
|
115
|
+
height: "28px",
|
|
116
|
+
padding: "0",
|
|
117
|
+
border: "1px solid #ccc",
|
|
118
|
+
borderRadius: "4px",
|
|
119
|
+
background: "white",
|
|
120
|
+
cursor: "pointer",
|
|
121
|
+
fontSize: "18px",
|
|
122
|
+
lineHeight: "1",
|
|
123
|
+
display: "flex",
|
|
124
|
+
alignItems: "center",
|
|
125
|
+
justifyContent: "center",
|
|
126
|
+
color: "#666",
|
|
127
|
+
transition: "all 0.2s ease",
|
|
128
|
+
zIndex: "10",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Add hover effect
|
|
132
|
+
button.addEventListener("mouseenter", () => {
|
|
133
|
+
button.style.background = "#f0f0f0";
|
|
134
|
+
button.style.borderColor = "#999";
|
|
135
|
+
button.style.color = "#333";
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
button.addEventListener("mouseleave", () => {
|
|
139
|
+
if (!this.strategy.isEliminated(choiceId)) {
|
|
140
|
+
button.style.background = "white";
|
|
141
|
+
button.style.borderColor = "#ccc";
|
|
142
|
+
button.style.color = "#666";
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
button.addEventListener("click", (e) => {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
this.toggleElimination(choice, adapter);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return button;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Toggle elimination for a choice
|
|
157
|
+
*/
|
|
158
|
+
toggleElimination(choice: HTMLElement, adapter: ChoiceAdapter): void {
|
|
159
|
+
const choiceId = adapter.getChoiceId(choice);
|
|
160
|
+
|
|
161
|
+
// Check if already eliminated
|
|
162
|
+
const isEliminated = this.strategy.isEliminated(choiceId);
|
|
163
|
+
|
|
164
|
+
if (isEliminated) {
|
|
165
|
+
// Restore
|
|
166
|
+
this.restoreChoice(choiceId);
|
|
167
|
+
} else {
|
|
168
|
+
// Eliminate
|
|
169
|
+
if (!adapter.canEliminate(choice)) {
|
|
170
|
+
console.warn(
|
|
171
|
+
"Cannot eliminate this choice (already selected or in evaluate mode)",
|
|
172
|
+
);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.eliminateChoice(choice, adapter);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Save state
|
|
180
|
+
this.saveState();
|
|
181
|
+
|
|
182
|
+
// Emit state change event
|
|
183
|
+
this.emitStateChange();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Eliminate a choice
|
|
188
|
+
*/
|
|
189
|
+
private eliminateChoice(choice: HTMLElement, adapter: ChoiceAdapter): void {
|
|
190
|
+
const choiceId = adapter.getChoiceId(choice);
|
|
191
|
+
|
|
192
|
+
// Create range for CSS Highlight API
|
|
193
|
+
const range = adapter.createChoiceRange(choice);
|
|
194
|
+
if (!range) {
|
|
195
|
+
console.error("Failed to create range for choice");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Apply strategy
|
|
200
|
+
this.strategy.apply(choiceId, range);
|
|
201
|
+
|
|
202
|
+
// Track in state
|
|
203
|
+
this.eliminatedChoices.add(choiceId);
|
|
204
|
+
|
|
205
|
+
// Update button appearance to show eliminated state
|
|
206
|
+
const button = this.choiceButtons.get(choiceId);
|
|
207
|
+
if (button) {
|
|
208
|
+
button.classList.add("active");
|
|
209
|
+
button.setAttribute("aria-pressed", "true");
|
|
210
|
+
// Visual feedback: filled/highlighted when eliminated
|
|
211
|
+
button.style.background = "#ff9800";
|
|
212
|
+
button.style.borderColor = "#ff9800";
|
|
213
|
+
button.style.color = "white";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Save to store
|
|
217
|
+
this.saveState();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Restore a choice
|
|
222
|
+
*/
|
|
223
|
+
private restoreChoice(choiceId: string): void {
|
|
224
|
+
// Remove from strategy
|
|
225
|
+
this.strategy.remove(choiceId);
|
|
226
|
+
|
|
227
|
+
// Remove from state
|
|
228
|
+
this.eliminatedChoices.delete(choiceId);
|
|
229
|
+
|
|
230
|
+
// Reset button appearance to default state
|
|
231
|
+
const button = this.choiceButtons.get(choiceId);
|
|
232
|
+
if (button) {
|
|
233
|
+
button.classList.remove("active");
|
|
234
|
+
button.setAttribute("aria-pressed", "false");
|
|
235
|
+
// Reset to default styling
|
|
236
|
+
button.style.background = "white";
|
|
237
|
+
button.style.borderColor = "#ccc";
|
|
238
|
+
button.style.color = "#666";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Save to store
|
|
242
|
+
this.saveState();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Reset all eliminations for current element
|
|
247
|
+
*/
|
|
248
|
+
resetAll(): void {
|
|
249
|
+
if (this.eliminatedChoices.size === 0) return;
|
|
250
|
+
|
|
251
|
+
// Restore all choices
|
|
252
|
+
for (const choiceId of Array.from(this.eliminatedChoices)) {
|
|
253
|
+
this.restoreChoice(choiceId);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Clear state
|
|
257
|
+
this.eliminatedChoices.clear();
|
|
258
|
+
this.saveState();
|
|
259
|
+
this.emitStateChange();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get count of eliminated choices for current element
|
|
264
|
+
*/
|
|
265
|
+
getEliminatedCount(): number {
|
|
266
|
+
return this.eliminatedChoices.size;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Set store integration for element-level state
|
|
271
|
+
* @param store ElementToolStateStore instance
|
|
272
|
+
* @param globalElementId Composite key: "assessmentId:sectionId:itemId:elementId"
|
|
273
|
+
*/
|
|
274
|
+
setStoreIntegration(store: any, globalElementId: string): void {
|
|
275
|
+
this.storeIntegration = { store, globalElementId };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Save state to ElementToolStateStore
|
|
280
|
+
*/
|
|
281
|
+
private saveState(): void {
|
|
282
|
+
if (!this.storeIntegration) return;
|
|
283
|
+
|
|
284
|
+
const state = {
|
|
285
|
+
eliminatedChoices: Array.from(this.eliminatedChoices),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
this.storeIntegration.store.setState(
|
|
289
|
+
this.storeIntegration.globalElementId,
|
|
290
|
+
"answerEliminator",
|
|
291
|
+
state,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Restore state from ElementToolStateStore
|
|
297
|
+
*/
|
|
298
|
+
private restoreState(): void {
|
|
299
|
+
if (!this.storeIntegration) return;
|
|
300
|
+
|
|
301
|
+
const state = this.storeIntegration.store.getState(
|
|
302
|
+
this.storeIntegration.globalElementId,
|
|
303
|
+
"answerEliminator",
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (!state || !state.eliminatedChoices) return;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const eliminated = state.eliminatedChoices;
|
|
310
|
+
|
|
311
|
+
if (!eliminated || eliminated.length === 0) return;
|
|
312
|
+
|
|
313
|
+
// Restore eliminated choices for current element
|
|
314
|
+
for (const choiceId of eliminated) {
|
|
315
|
+
const choice = this.choiceElements.get(choiceId);
|
|
316
|
+
if (!choice) continue;
|
|
317
|
+
|
|
318
|
+
// Find adapter for this choice
|
|
319
|
+
const adapter = this.findAdapterForChoice(choice);
|
|
320
|
+
if (!adapter) continue;
|
|
321
|
+
|
|
322
|
+
// Re-eliminate without saving (already in state)
|
|
323
|
+
const range = adapter.createChoiceRange(choice);
|
|
324
|
+
if (range) {
|
|
325
|
+
this.strategy.apply(choiceId, range);
|
|
326
|
+
|
|
327
|
+
// Track in memory
|
|
328
|
+
this.eliminatedChoices.add(choiceId);
|
|
329
|
+
|
|
330
|
+
// Update button appearance to show eliminated state
|
|
331
|
+
const button = this.choiceButtons.get(choiceId);
|
|
332
|
+
if (button) {
|
|
333
|
+
button.classList.add("active");
|
|
334
|
+
button.setAttribute("aria-pressed", "true");
|
|
335
|
+
// Apply eliminated styling
|
|
336
|
+
button.style.background = "#ff9800";
|
|
337
|
+
button.style.borderColor = "#ff9800";
|
|
338
|
+
button.style.color = "white";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("Failed to restore eliminator state:", error);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Find adapter for a choice element
|
|
349
|
+
*/
|
|
350
|
+
private findAdapterForChoice(choice: HTMLElement): ChoiceAdapter | null {
|
|
351
|
+
// Walk up to find PIE element root
|
|
352
|
+
let element: HTMLElement | null = choice;
|
|
353
|
+
|
|
354
|
+
while (element && element !== document.body) {
|
|
355
|
+
const adapter = this.registry.findAdapter(element);
|
|
356
|
+
if (adapter) return adapter;
|
|
357
|
+
element = element.parentElement;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Cleanup buttons from previous element
|
|
365
|
+
*/
|
|
366
|
+
private cleanupButtons(): void {
|
|
367
|
+
for (const button of this.choiceButtons.values()) {
|
|
368
|
+
button.remove();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.choiceButtons.clear();
|
|
372
|
+
this.choiceElements.clear();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Apply button positioning based on alignment configuration
|
|
377
|
+
*/
|
|
378
|
+
private applyButtonAlignment(button: HTMLButtonElement): void {
|
|
379
|
+
switch (this.buttonAlignment) {
|
|
380
|
+
case "right":
|
|
381
|
+
// Right-aligned (industry standard) - after choice text
|
|
382
|
+
Object.assign(button.style, {
|
|
383
|
+
position: "absolute",
|
|
384
|
+
right: "8px",
|
|
385
|
+
top: "50%",
|
|
386
|
+
transform: "translateY(-50%)",
|
|
387
|
+
});
|
|
388
|
+
break;
|
|
389
|
+
|
|
390
|
+
case "left":
|
|
391
|
+
// Left-aligned - before choice text
|
|
392
|
+
Object.assign(button.style, {
|
|
393
|
+
position: "absolute",
|
|
394
|
+
left: "8px",
|
|
395
|
+
top: "50%",
|
|
396
|
+
transform: "translateY(-50%)",
|
|
397
|
+
});
|
|
398
|
+
break;
|
|
399
|
+
|
|
400
|
+
case "inline":
|
|
401
|
+
// Inline with checkbox - no absolute positioning
|
|
402
|
+
Object.assign(button.style, {
|
|
403
|
+
position: "relative",
|
|
404
|
+
marginLeft: "8px",
|
|
405
|
+
marginRight: "8px",
|
|
406
|
+
display: "inline-flex",
|
|
407
|
+
verticalAlign: "middle",
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Enable state restoration from localStorage
|
|
415
|
+
*/
|
|
416
|
+
enableStateRestoration(): void {
|
|
417
|
+
this.shouldRestoreState = true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Disable state restoration from localStorage
|
|
422
|
+
*/
|
|
423
|
+
disableStateRestoration(): void {
|
|
424
|
+
this.shouldRestoreState = false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Cleanup when tool is turned off (but don't destroy strategy)
|
|
429
|
+
* Hides elimination buttons AND clears all visual eliminations
|
|
430
|
+
* Note: State is preserved in localStorage for when tool is turned back on
|
|
431
|
+
*/
|
|
432
|
+
cleanup(): void {
|
|
433
|
+
// Disable state restoration to prevent restoreState() from re-applying eliminations
|
|
434
|
+
this.disableStateRestoration();
|
|
435
|
+
|
|
436
|
+
// Remove all buttons
|
|
437
|
+
this.cleanupButtons();
|
|
438
|
+
|
|
439
|
+
// Clear all visual eliminations (strikethroughs)
|
|
440
|
+
// This removes the CSS highlights but keeps localStorage state
|
|
441
|
+
this.strategy.clearAll();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Emit state change event for UI updates
|
|
446
|
+
*/
|
|
447
|
+
private emitStateChange(): void {
|
|
448
|
+
const event = new CustomEvent("answer-eliminator-state-change", {
|
|
449
|
+
detail: {
|
|
450
|
+
questionId: this.currentQuestionId,
|
|
451
|
+
eliminatedCount: this.getEliminatedCount(),
|
|
452
|
+
},
|
|
453
|
+
bubbles: true,
|
|
454
|
+
});
|
|
455
|
+
document.dispatchEvent(event);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Destroy and cleanup
|
|
460
|
+
*/
|
|
461
|
+
destroy(): void {
|
|
462
|
+
this.cleanupButtons();
|
|
463
|
+
this.strategy.destroy();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ChoiceAdapter } from './choice-adapter';
|
|
2
|
+
export interface ChoiceWithAdapter {
|
|
3
|
+
choice: HTMLElement;
|
|
4
|
+
adapter: ChoiceAdapter;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Registry for choice adapters
|
|
8
|
+
* Automatically detects which adapter to use for different PIE elements
|
|
9
|
+
*/
|
|
10
|
+
export declare class AdapterRegistry {
|
|
11
|
+
private adapters;
|
|
12
|
+
constructor();
|
|
13
|
+
/**
|
|
14
|
+
* Find the appropriate adapter for an element
|
|
15
|
+
*/
|
|
16
|
+
findAdapter(element: HTMLElement): ChoiceAdapter | null;
|
|
17
|
+
/**
|
|
18
|
+
* Find all choices with their adapters within a root element
|
|
19
|
+
*/
|
|
20
|
+
findAllChoicesWithAdapters(root: HTMLElement): ChoiceWithAdapter[];
|
|
21
|
+
/**
|
|
22
|
+
* Register a custom adapter
|
|
23
|
+
*/
|
|
24
|
+
registerAdapter(adapter: ChoiceAdapter): void;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=adapter-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-registry.d.ts","sourceRoot":"","sources":["../../adapters/adapter-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAKtD,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,aAAa,CAAC;CACvB;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAkB;;IAWlC;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,aAAa,GAAG,IAAI;IAIvD;;OAEG;IACH,0BAA0B,CAAC,IAAI,EAAE,WAAW,GAAG,iBAAiB,EAAE;IAoBlE;;OAEG;IACH,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;CAI7C"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter interface for detecting and working with choice elements
|
|
3
|
+
* from different PIE element types.
|
|
4
|
+
*
|
|
5
|
+
* Adapters enable the answer eliminator to work generically with
|
|
6
|
+
* multiple-choice, EBSR, inline-dropdown, and future choice elements.
|
|
7
|
+
*/
|
|
8
|
+
export interface ChoiceAdapter {
|
|
9
|
+
/** Element type this adapter handles */
|
|
10
|
+
readonly elementType: string;
|
|
11
|
+
/** Priority (higher = checked first) */
|
|
12
|
+
readonly priority: number;
|
|
13
|
+
/**
|
|
14
|
+
* Check if this adapter can handle the given element
|
|
15
|
+
*/
|
|
16
|
+
canHandle(element: HTMLElement): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Find all choice elements within the root
|
|
19
|
+
*/
|
|
20
|
+
findChoices(root: HTMLElement): HTMLElement[];
|
|
21
|
+
/**
|
|
22
|
+
* Create a Range covering the choice content (for CSS Highlight API)
|
|
23
|
+
*/
|
|
24
|
+
createChoiceRange(choice: HTMLElement): Range | null;
|
|
25
|
+
/**
|
|
26
|
+
* Get unique identifier for this choice (for persistence)
|
|
27
|
+
*/
|
|
28
|
+
getChoiceId(choice: HTMLElement): string;
|
|
29
|
+
/**
|
|
30
|
+
* Get human-readable label (for screen readers)
|
|
31
|
+
*/
|
|
32
|
+
getChoiceLabel(choice: HTMLElement): string;
|
|
33
|
+
/**
|
|
34
|
+
* Check if choice can be eliminated
|
|
35
|
+
* (not already selected, not in evaluate mode, etc.)
|
|
36
|
+
*/
|
|
37
|
+
canEliminate(choice: HTMLElement): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Get the container element to attach elimination button
|
|
40
|
+
*/
|
|
41
|
+
getButtonContainer(choice: HTMLElement): HTMLElement | null;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=choice-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"choice-adapter.d.ts","sourceRoot":"","sources":["../../adapters/choice-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,wCAAwC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC;IAEzC;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,EAAE,CAAC;IAE9C;;OAEG;IACH,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,IAAI,CAAC;IAErD;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC;IAEzC;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC;IAE5C;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC;IAE3C;;OAEG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI,CAAC;CAC5D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ChoiceAdapter } from './choice-adapter';
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for EBSR (Evidence-Based Selected Response) elements
|
|
4
|
+
*
|
|
5
|
+
* EBSR contains two multiple-choice questions (Part A and Part B),
|
|
6
|
+
* so we delegate to MultipleChoiceAdapter and prefix IDs with part identifier
|
|
7
|
+
*/
|
|
8
|
+
export declare class EBSRAdapter implements ChoiceAdapter {
|
|
9
|
+
readonly elementType = "ebsr";
|
|
10
|
+
readonly priority = 95;
|
|
11
|
+
private mcAdapter;
|
|
12
|
+
canHandle(element: HTMLElement): boolean;
|
|
13
|
+
findChoices(root: HTMLElement): HTMLElement[];
|
|
14
|
+
createChoiceRange(choice: HTMLElement): Range | null;
|
|
15
|
+
getChoiceId(choice: HTMLElement): string;
|
|
16
|
+
getChoiceLabel(choice: HTMLElement): string;
|
|
17
|
+
canEliminate(choice: HTMLElement): boolean;
|
|
18
|
+
getButtonContainer(choice: HTMLElement): HTMLElement | null;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=ebsr-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ebsr-adapter.d.ts","sourceRoot":"","sources":["../../adapters/ebsr-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD;;;;;GAKG;AACH,qBAAa,WAAY,YAAW,aAAa;IAChD,QAAQ,CAAC,WAAW,UAAU;IAC9B,QAAQ,CAAC,QAAQ,MAAM;IAEvB,OAAO,CAAC,SAAS,CAA+B;IAEhD,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO;IAOxC,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,EAAE;IAgB7C,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,IAAI;IAIpD,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM;IAOxC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM;IAI3C,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO;IAI1C,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI;CAG3D"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ChoiceAdapter } from './choice-adapter';
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for PIE inline-dropdown elements
|
|
4
|
+
*
|
|
5
|
+
* Works with dropdown menu items (role="option")
|
|
6
|
+
*/
|
|
7
|
+
export declare class InlineDropdownAdapter implements ChoiceAdapter {
|
|
8
|
+
readonly elementType = "inline-dropdown";
|
|
9
|
+
readonly priority = 90;
|
|
10
|
+
canHandle(element: HTMLElement): boolean;
|
|
11
|
+
findChoices(root: HTMLElement): HTMLElement[];
|
|
12
|
+
createChoiceRange(choice: HTMLElement): Range | null;
|
|
13
|
+
getChoiceId(choice: HTMLElement): string;
|
|
14
|
+
getChoiceLabel(choice: HTMLElement): string;
|
|
15
|
+
canEliminate(choice: HTMLElement): boolean;
|
|
16
|
+
getButtonContainer(choice: HTMLElement): HTMLElement | null;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=inline-dropdown-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inline-dropdown-adapter.d.ts","sourceRoot":"","sources":["../../adapters/inline-dropdown-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;;;GAIG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IAC1D,QAAQ,CAAC,WAAW,qBAAqB;IACzC,QAAQ,CAAC,QAAQ,MAAM;IAEvB,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO;IAOxC,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,EAAE;IAK7C,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,IAAI;IAMpD,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM;IAIxC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM;IAI3C,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO;IAK1C,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI;CAG3D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ChoiceAdapter } from './choice-adapter';
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for PIE multiple-choice elements
|
|
4
|
+
*
|
|
5
|
+
* Works with both single-select (radio) and multiple-select (checkbox) modes
|
|
6
|
+
* Detects PIE's corespring-checkbox and corespring-radio-button classes
|
|
7
|
+
*/
|
|
8
|
+
export declare class MultipleChoiceAdapter implements ChoiceAdapter {
|
|
9
|
+
readonly elementType = "multiple-choice";
|
|
10
|
+
readonly priority = 100;
|
|
11
|
+
canHandle(element: HTMLElement): boolean;
|
|
12
|
+
findChoices(root: HTMLElement): HTMLElement[];
|
|
13
|
+
createChoiceRange(choice: HTMLElement): Range | null;
|
|
14
|
+
getChoiceId(choice: HTMLElement): string;
|
|
15
|
+
getChoiceLabel(choice: HTMLElement): string;
|
|
16
|
+
canEliminate(choice: HTMLElement): boolean;
|
|
17
|
+
getButtonContainer(choice: HTMLElement): HTMLElement | null;
|
|
18
|
+
private generateFallbackId;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=multiple-choice-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multiple-choice-adapter.d.ts","sourceRoot":"","sources":["../../adapters/multiple-choice-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;;;;GAKG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IAC1D,QAAQ,CAAC,WAAW,qBAAqB;IACzC,QAAQ,CAAC,QAAQ,OAAO;IAExB,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO;IAOxC,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,EAAE;IAS7C,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,IAAI;IAkBpD,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM;IAYxC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM;IAK3C,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO;IAoB1C,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI;IAK3D,OAAO,CAAC,kBAAkB;CAO1B"}
|