@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,250 @@
|
|
|
1
|
+
<svelte:options
|
|
2
|
+
customElement={{
|
|
3
|
+
tag: 'pie-tool-answer-eliminator',
|
|
4
|
+
shadow: 'none',
|
|
5
|
+
props: {
|
|
6
|
+
visible: { type: 'Boolean', attribute: 'visible' },
|
|
7
|
+
toolId: { type: 'String', attribute: 'tool-id' },
|
|
8
|
+
strategy: { type: 'String', attribute: 'strategy' },
|
|
9
|
+
alwaysOn: { type: 'Boolean', attribute: 'always-on' },
|
|
10
|
+
buttonAlignment: { type: 'String', attribute: 'button-alignment' },
|
|
11
|
+
coordinator: { type: 'Object' },
|
|
12
|
+
scopeElement: { type: 'Object', reflect: false },
|
|
13
|
+
|
|
14
|
+
// Store integration (JS properties only)
|
|
15
|
+
elementToolStateStore: { type: 'Object', reflect: false },
|
|
16
|
+
globalElementId: { type: 'String', reflect: false }
|
|
17
|
+
}
|
|
18
|
+
}}
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<!-- Answer Eliminator Tool - Process-of-Elimination Support (Inline Toggle Mode)
|
|
22
|
+
|
|
23
|
+
Allows students to mark answer choices as "eliminated" during test-taking,
|
|
24
|
+
supporting the process-of-elimination strategy.
|
|
25
|
+
|
|
26
|
+
**Interaction Pattern (Industry Standard):**
|
|
27
|
+
- Student toggles tool ON via toolbar button
|
|
28
|
+
- Small elimination buttons (X) appear next to each answer choice
|
|
29
|
+
- Student clicks X buttons to eliminate/restore choices
|
|
30
|
+
- Tool can be toggled OFF to hide all elimination buttons
|
|
31
|
+
- OR can be "always-on" via student profile (alwaysOn prop)
|
|
32
|
+
|
|
33
|
+
**Features:**
|
|
34
|
+
- Modern CSS Custom Highlight API (zero DOM mutation, 10-15x faster)
|
|
35
|
+
- Generic adapter pattern (works with multiple-choice, EBSR, inline-dropdown)
|
|
36
|
+
- Strikethrough visual (WCAG 2.2 AA compliant, best for accessibility)
|
|
37
|
+
- localStorage persistence across question navigation
|
|
38
|
+
- Keyboard accessible with proper ARIA attributes
|
|
39
|
+
|
|
40
|
+
**WCAG 2.2 Level AA Compliant:**
|
|
41
|
+
- 1.3.1 Info and Relationships (maintains structure)
|
|
42
|
+
- 2.4.3 Focus Order (no layout shift)
|
|
43
|
+
- 3.3.1 Error Identification (easy to identify/correct)
|
|
44
|
+
- 4.1.2 Name, Role, Value (proper ARIA)
|
|
45
|
+
-->
|
|
46
|
+
|
|
47
|
+
<script lang="ts">
|
|
48
|
+
|
|
49
|
+
import type { IToolCoordinator } from '@pie-players/pie-assessment-toolkit';
|
|
50
|
+
import { ZIndexLayer } from '@pie-players/pie-assessment-toolkit';
|
|
51
|
+
import { onDestroy, onMount } from 'svelte';
|
|
52
|
+
import { AnswerEliminatorCore } from './answer-eliminator-core';
|
|
53
|
+
|
|
54
|
+
// Props
|
|
55
|
+
let {
|
|
56
|
+
visible = false,
|
|
57
|
+
toolId = 'answerEliminator',
|
|
58
|
+
strategy = 'strikethrough' as 'strikethrough' | 'mask' | 'gray',
|
|
59
|
+
alwaysOn = false, // Set true for profile-based accommodation
|
|
60
|
+
buttonAlignment = 'right' as 'left' | 'right' | 'inline', // Button placement: left, right, or inline with checkbox
|
|
61
|
+
coordinator,
|
|
62
|
+
scopeElement = null, // Container element to limit DOM queries (for multi-item pages)
|
|
63
|
+
|
|
64
|
+
// Store integration
|
|
65
|
+
elementToolStateStore = null, // ElementToolStateStore instance
|
|
66
|
+
globalElementId = '' // Composite key: "assessmentId:sectionId:itemId:elementId"
|
|
67
|
+
}: {
|
|
68
|
+
visible?: boolean;
|
|
69
|
+
toolId?: string;
|
|
70
|
+
strategy?: 'strikethrough' | 'mask' | 'gray';
|
|
71
|
+
alwaysOn?: boolean;
|
|
72
|
+
buttonAlignment?: 'left' | 'right' | 'inline';
|
|
73
|
+
coordinator?: IToolCoordinator;
|
|
74
|
+
scopeElement?: HTMLElement | null;
|
|
75
|
+
elementToolStateStore?: any;
|
|
76
|
+
globalElementId?: string;
|
|
77
|
+
} = $props();
|
|
78
|
+
|
|
79
|
+
// State
|
|
80
|
+
let core = $state<AnswerEliminatorCore | null>(null);
|
|
81
|
+
let eliminatedCount = $state(0);
|
|
82
|
+
let mutationObserver = $state<MutationObserver | null>(null);
|
|
83
|
+
|
|
84
|
+
// Track registration state
|
|
85
|
+
let registered = $state(false);
|
|
86
|
+
|
|
87
|
+
// Determine if tool should be active (either toggled on OR always-on mode)
|
|
88
|
+
let isActive = $derived(alwaysOn || visible);
|
|
89
|
+
|
|
90
|
+
function initializeForCurrentQuestion() {
|
|
91
|
+
if (!isActive || !core) return;
|
|
92
|
+
|
|
93
|
+
// Use scopeElement if provided (for multi-item pages), otherwise search globally
|
|
94
|
+
const searchRoot = scopeElement || document.body;
|
|
95
|
+
|
|
96
|
+
// Find the current question/item within the search scope
|
|
97
|
+
const questionRoot =
|
|
98
|
+
searchRoot.querySelector('pie-player') ||
|
|
99
|
+
searchRoot.querySelector('multiple-choice') ||
|
|
100
|
+
searchRoot.querySelector('ebsr') ||
|
|
101
|
+
searchRoot.querySelector('[data-pie-element]') ||
|
|
102
|
+
searchRoot;
|
|
103
|
+
|
|
104
|
+
if (!questionRoot) {
|
|
105
|
+
console.warn('[AnswerEliminator] Could not find question root within scope');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
core.initializeForQuestion(questionRoot as HTMLElement);
|
|
110
|
+
updateEliminatedCount();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function waitForPIEElements(callback: () => void, timeout: number = 5000) {
|
|
114
|
+
// Use scopeElement if provided, otherwise search globally
|
|
115
|
+
const searchRoot = scopeElement || document.body;
|
|
116
|
+
|
|
117
|
+
// Check if PIE elements already exist
|
|
118
|
+
const checkElements = () => {
|
|
119
|
+
const questionRoot =
|
|
120
|
+
searchRoot.querySelector('pie-player') ||
|
|
121
|
+
searchRoot.querySelector('multiple-choice') ||
|
|
122
|
+
searchRoot.querySelector('ebsr') ||
|
|
123
|
+
searchRoot.querySelector('[data-pie-element]');
|
|
124
|
+
|
|
125
|
+
if (questionRoot) {
|
|
126
|
+
// Elements found, clean up observer and execute callback
|
|
127
|
+
if (mutationObserver) {
|
|
128
|
+
mutationObserver.disconnect();
|
|
129
|
+
mutationObserver = null;
|
|
130
|
+
}
|
|
131
|
+
callback();
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Try immediately first
|
|
138
|
+
if (checkElements()) return;
|
|
139
|
+
|
|
140
|
+
// Set up MutationObserver to watch for elements within scope
|
|
141
|
+
mutationObserver = new MutationObserver(() => {
|
|
142
|
+
checkElements();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Observe the search root for added nodes
|
|
146
|
+
mutationObserver.observe(searchRoot, {
|
|
147
|
+
childList: true,
|
|
148
|
+
subtree: true
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Fallback timeout to prevent infinite observation
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
if (mutationObserver) {
|
|
154
|
+
mutationObserver.disconnect();
|
|
155
|
+
mutationObserver = null;
|
|
156
|
+
// Try one last time
|
|
157
|
+
checkElements();
|
|
158
|
+
}
|
|
159
|
+
}, timeout);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleItemChange() {
|
|
163
|
+
// Question changed, wait for PIE elements to be rendered using MutationObserver
|
|
164
|
+
waitForPIEElements(() => {
|
|
165
|
+
initializeForCurrentQuestion();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function updateEliminatedCount() {
|
|
170
|
+
eliminatedCount = core?.getEliminatedCount() || 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Register with coordinator when it becomes available
|
|
174
|
+
$effect(() => {
|
|
175
|
+
if (coordinator && toolId && !registered) {
|
|
176
|
+
coordinator.registerTool(toolId, 'Answer Eliminator', undefined, ZIndexLayer.MODAL);
|
|
177
|
+
registered = true;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Update store integration when store props change
|
|
182
|
+
$effect(() => {
|
|
183
|
+
if (core && elementToolStateStore && globalElementId) {
|
|
184
|
+
core.setStoreIntegration(elementToolStateStore, globalElementId);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
onMount(() => {
|
|
189
|
+
// Initialize core engine with configuration
|
|
190
|
+
core = new AnswerEliminatorCore(strategy, buttonAlignment);
|
|
191
|
+
|
|
192
|
+
// Set up store integration if provided
|
|
193
|
+
if (core && elementToolStateStore && globalElementId) {
|
|
194
|
+
core.setStoreIntegration(elementToolStateStore, globalElementId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Listen for question changes (PIE player emits this)
|
|
198
|
+
document.addEventListener('pie-item-changed', handleItemChange);
|
|
199
|
+
|
|
200
|
+
// Listen for custom events from answer-eliminator-core when state changes
|
|
201
|
+
document.addEventListener('answer-eliminator-state-change', () => {
|
|
202
|
+
updateEliminatedCount();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Initialize for current question if active, otherwise ensure clean state
|
|
206
|
+
if (isActive) {
|
|
207
|
+
// Wait for PIE elements to be mounted using MutationObserver
|
|
208
|
+
waitForPIEElements(() => {
|
|
209
|
+
initializeForCurrentQuestion();
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
// If not active on mount, clear any leftover visual eliminations
|
|
213
|
+
core.cleanup();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return () => {
|
|
217
|
+
// Clean up MutationObserver if still active
|
|
218
|
+
if (mutationObserver) {
|
|
219
|
+
mutationObserver.disconnect();
|
|
220
|
+
mutationObserver = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
core?.destroy();
|
|
224
|
+
core = null;
|
|
225
|
+
if (coordinator && toolId) {
|
|
226
|
+
coordinator.unregisterTool(toolId);
|
|
227
|
+
}
|
|
228
|
+
document.removeEventListener('pie-item-changed', handleItemChange);
|
|
229
|
+
document.removeEventListener('answer-eliminator-state-change', updateEliminatedCount);
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Watch for visibility changes to show/hide elimination buttons
|
|
234
|
+
$effect(() => {
|
|
235
|
+
if (core) {
|
|
236
|
+
if (isActive) {
|
|
237
|
+
// Re-enable state restoration when tool is activated
|
|
238
|
+
core.enableStateRestoration();
|
|
239
|
+
initializeForCurrentQuestion();
|
|
240
|
+
} else {
|
|
241
|
+
// Hide all elimination buttons when tool is turned off
|
|
242
|
+
// This also disables state restoration
|
|
243
|
+
core.cleanup();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
</script>
|
|
248
|
+
|
|
249
|
+
<!-- No visible UI - tool operates entirely through injected buttons next to choices -->
|
|
250
|
+
<!-- The toolbar button visibility is managed by tool-toolbar.svelte -->
|