@pie-players/pie-tool-answer-eliminator 0.3.43 → 0.3.44
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/dist/adapters/adapter-registry.d.ts +0 -1
- package/dist/adapters/choice-adapter.d.ts +0 -1
- package/dist/adapters/ebsr-adapter.d.ts +0 -1
- package/dist/adapters/inline-dropdown-adapter.d.ts +0 -1
- package/dist/adapters/multiple-choice-adapter.d.ts +0 -1
- package/dist/answer-eliminator-core.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/strategies/elimination-strategy.d.ts +0 -1
- package/dist/strategies/mask-strategy.d.ts +0 -1
- package/dist/strategies/strikethrough-strategy.d.ts +0 -1
- package/dist/vite.config.d.ts +0 -1
- package/package.json +6 -12
- package/adapters/adapter-registry.ts +0 -64
- package/adapters/choice-adapter.ts +0 -50
- package/adapters/ebsr-adapter.ts +0 -61
- package/adapters/inline-dropdown-adapter.ts +0 -46
- package/adapters/multiple-choice-adapter.ts +0 -144
- package/answer-eliminator-core.ts +0 -391
- package/dist/adapters/adapter-registry.d.ts.map +0 -1
- package/dist/adapters/choice-adapter.d.ts.map +0 -1
- package/dist/adapters/ebsr-adapter.d.ts.map +0 -1
- package/dist/adapters/inline-dropdown-adapter.d.ts.map +0 -1
- package/dist/adapters/multiple-choice-adapter.d.ts.map +0 -1
- package/dist/answer-eliminator-core.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/strategies/elimination-strategy.d.ts.map +0 -1
- package/dist/strategies/mask-strategy.d.ts.map +0 -1
- package/dist/strategies/strikethrough-strategy.d.ts.map +0 -1
- package/dist/vite.config.d.ts.map +0 -1
- package/strategies/elimination-strategy.ts +0 -47
- package/strategies/mask-strategy.ts +0 -171
- package/strategies/strikethrough-strategy.ts +0 -223
- package/tool-answer-eliminator.svelte +0 -223
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import type { EliminationStrategy } from "./elimination-strategy.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Strikethrough strategy using CSS Custom Highlight API
|
|
5
|
-
*
|
|
6
|
-
* Modern approach: Zero DOM mutation, uses browser-native highlighting
|
|
7
|
-
* Accessibility: Best for screen readers (text remains in DOM unchanged)
|
|
8
|
-
* WCAG Compliance: Maintains info structure (1.3.1), no layout shift (2.4.3)
|
|
9
|
-
*/
|
|
10
|
-
export class StrikethroughStrategy implements EliminationStrategy {
|
|
11
|
-
private static readonly HIGHLIGHT_STYLE_PREFIX =
|
|
12
|
-
"pie-answer-eliminator-highlight-";
|
|
13
|
-
private static readonly HIGHLIGHT_NAME_PREFIX = "pie-answer-eliminated-";
|
|
14
|
-
private static readonly FALLBACK_CLASS =
|
|
15
|
-
"pie-answer-eliminator-eliminated-fallback";
|
|
16
|
-
private static readonly SR_CLASS = "pie-answer-eliminator-sr-announcement";
|
|
17
|
-
private static readonly CHOICE_HOOK_ATTR =
|
|
18
|
-
"data-pie-answer-eliminator-choice";
|
|
19
|
-
private static readonly LABEL_HOOK_ATTR = "data-pie-answer-eliminator-label";
|
|
20
|
-
private static readonly ELIMINATED_ATTR = "data-pie-answer-eliminated";
|
|
21
|
-
private static readonly ELIMINATED_ID_ATTR = "data-pie-answer-eliminated-id";
|
|
22
|
-
|
|
23
|
-
readonly name = "strikethrough";
|
|
24
|
-
|
|
25
|
-
private highlights = new Map<string, Highlight>();
|
|
26
|
-
private ranges = new Map<string, Range>();
|
|
27
|
-
private fallbackContainers = new Map<string, HTMLElement>();
|
|
28
|
-
|
|
29
|
-
initialize(): void {
|
|
30
|
-
// Check browser support
|
|
31
|
-
if (!this.isSupported()) {
|
|
32
|
-
console.warn("CSS Custom Highlight API not supported, using fallback");
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
destroy(): void {
|
|
37
|
-
this.clearAll();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
apply(choiceId: string, range: Range): void {
|
|
41
|
-
if (!this.isSupported()) {
|
|
42
|
-
this.applyFallback(choiceId, range);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Inject CSS for this specific highlight
|
|
47
|
-
this.injectHighlightCSS(choiceId);
|
|
48
|
-
|
|
49
|
-
// Create highlight for this range
|
|
50
|
-
const highlight = new Highlight(range);
|
|
51
|
-
|
|
52
|
-
// Register in CSS.highlights with unique name
|
|
53
|
-
CSS.highlights.set(
|
|
54
|
-
`${StrikethroughStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`,
|
|
55
|
-
highlight,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
// Track internally
|
|
59
|
-
this.highlights.set(choiceId, highlight);
|
|
60
|
-
this.ranges.set(choiceId, range);
|
|
61
|
-
|
|
62
|
-
// Add ARIA attributes to the choice element for screen readers
|
|
63
|
-
this.addAriaAttributes(range);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
remove(choiceId: string): void {
|
|
67
|
-
if (!this.isSupported()) {
|
|
68
|
-
this.removeFallback(choiceId);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Remove from CSS.highlights
|
|
73
|
-
CSS.highlights.delete(
|
|
74
|
-
`${StrikethroughStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Remove CSS for this specific highlight
|
|
78
|
-
this.removeHighlightCSS(choiceId);
|
|
79
|
-
|
|
80
|
-
// Remove from internal tracking
|
|
81
|
-
const range = this.ranges.get(choiceId);
|
|
82
|
-
if (range) {
|
|
83
|
-
this.removeAriaAttributes(range);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
this.highlights.delete(choiceId);
|
|
87
|
-
this.ranges.delete(choiceId);
|
|
88
|
-
this.fallbackContainers.delete(choiceId);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
isEliminated(choiceId: string): boolean {
|
|
92
|
-
return this.highlights.has(choiceId);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
clearAll(): void {
|
|
96
|
-
// Remove all highlights
|
|
97
|
-
for (const choiceId of this.highlights.keys()) {
|
|
98
|
-
this.remove(choiceId);
|
|
99
|
-
}
|
|
100
|
-
this.fallbackContainers.clear();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
getEliminatedIds(): string[] {
|
|
104
|
-
return Array.from(this.highlights.keys());
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private isSupported(): boolean {
|
|
108
|
-
return typeof CSS !== "undefined" && "highlights" in CSS;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private injectHighlightCSS(choiceId: string): void {
|
|
112
|
-
const styleId = `${StrikethroughStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`;
|
|
113
|
-
if (document.getElementById(styleId)) return;
|
|
114
|
-
|
|
115
|
-
const style = document.createElement("style");
|
|
116
|
-
style.id = styleId;
|
|
117
|
-
style.textContent = `
|
|
118
|
-
::highlight(pie-answer-eliminated-${choiceId}) {
|
|
119
|
-
text-decoration: line-through;
|
|
120
|
-
text-decoration-thickness: 2px;
|
|
121
|
-
text-decoration-color: var(--pie-incorrect, #ff9800);
|
|
122
|
-
opacity: 0.6;
|
|
123
|
-
}
|
|
124
|
-
`;
|
|
125
|
-
document.head.appendChild(style);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private removeHighlightCSS(choiceId: string): void {
|
|
129
|
-
document
|
|
130
|
-
.getElementById(
|
|
131
|
-
`${StrikethroughStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`,
|
|
132
|
-
)
|
|
133
|
-
?.remove();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private addAriaAttributes(range: Range): void {
|
|
137
|
-
// Find the choice container element
|
|
138
|
-
const container = this.findChoiceContainer(range);
|
|
139
|
-
if (!container) return;
|
|
140
|
-
|
|
141
|
-
container.setAttribute(StrikethroughStrategy.ELIMINATED_ATTR, "true");
|
|
142
|
-
container.setAttribute("aria-disabled", "true");
|
|
143
|
-
|
|
144
|
-
// Add screen reader announcement
|
|
145
|
-
const label = this.resolveLabelElement(container);
|
|
146
|
-
if (label && !label.querySelector(`.${StrikethroughStrategy.SR_CLASS}`)) {
|
|
147
|
-
const announcement = document.createElement("span");
|
|
148
|
-
announcement.className = StrikethroughStrategy.SR_CLASS;
|
|
149
|
-
announcement.textContent = " (eliminated)";
|
|
150
|
-
label.appendChild(announcement);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private removeAriaAttributes(range: Range): void {
|
|
155
|
-
const container = this.findChoiceContainer(range);
|
|
156
|
-
if (!container) return;
|
|
157
|
-
|
|
158
|
-
container.removeAttribute(StrikethroughStrategy.ELIMINATED_ATTR);
|
|
159
|
-
container.removeAttribute("aria-disabled");
|
|
160
|
-
|
|
161
|
-
// Remove screen reader announcement
|
|
162
|
-
const announcement = container.querySelector(
|
|
163
|
-
`.${StrikethroughStrategy.SR_CLASS}`,
|
|
164
|
-
);
|
|
165
|
-
announcement?.remove();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private findChoiceContainer(range: Range): HTMLElement | null {
|
|
169
|
-
// Walk up from range start to find the choice container
|
|
170
|
-
let element: HTMLElement | null = range.startContainer as HTMLElement;
|
|
171
|
-
|
|
172
|
-
// If startContainer is a text node, get its parent
|
|
173
|
-
if (element.nodeType === Node.TEXT_NODE) {
|
|
174
|
-
element = element.parentElement;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
while (element && element !== document.body) {
|
|
178
|
-
if (
|
|
179
|
-
element.getAttribute(StrikethroughStrategy.CHOICE_HOOK_ATTR) === "true"
|
|
180
|
-
) {
|
|
181
|
-
return element;
|
|
182
|
-
}
|
|
183
|
-
element = element.parentElement;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Fallback for browsers without CSS Highlight API
|
|
190
|
-
private applyFallback(choiceId: string, range: Range): void {
|
|
191
|
-
const container = this.findChoiceContainer(range);
|
|
192
|
-
if (!container) return;
|
|
193
|
-
|
|
194
|
-
container.classList.add(StrikethroughStrategy.FALLBACK_CLASS);
|
|
195
|
-
container.setAttribute(StrikethroughStrategy.ELIMINATED_ATTR, "true");
|
|
196
|
-
container.setAttribute(StrikethroughStrategy.ELIMINATED_ID_ATTR, choiceId);
|
|
197
|
-
this.fallbackContainers.set(choiceId, container);
|
|
198
|
-
this.addAriaAttributes(range);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private removeFallback(choiceId: string): void {
|
|
202
|
-
const container = this.fallbackContainers.get(choiceId);
|
|
203
|
-
if (!container) return;
|
|
204
|
-
|
|
205
|
-
container.classList.remove(StrikethroughStrategy.FALLBACK_CLASS);
|
|
206
|
-
container.removeAttribute(StrikethroughStrategy.ELIMINATED_ATTR);
|
|
207
|
-
container.removeAttribute(StrikethroughStrategy.ELIMINATED_ID_ATTR);
|
|
208
|
-
|
|
209
|
-
// Create fake range for aria removal
|
|
210
|
-
const range = document.createRange();
|
|
211
|
-
range.selectNodeContents(container);
|
|
212
|
-
this.removeAriaAttributes(range);
|
|
213
|
-
this.fallbackContainers.delete(choiceId);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private resolveLabelElement(container: HTMLElement): HTMLElement | null {
|
|
217
|
-
return (
|
|
218
|
-
container.querySelector<HTMLElement>(
|
|
219
|
-
`[${StrikethroughStrategy.LABEL_HOOK_ATTR}="true"]`,
|
|
220
|
-
) || container.querySelector<HTMLElement>("label")
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
<svelte:options
|
|
2
|
-
customElement={{
|
|
3
|
-
tag: 'pie-tool-answer-eliminator',
|
|
4
|
-
shadow: 'open',
|
|
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
|
-
scopeElement: { type: 'Object', reflect: false },
|
|
12
|
-
|
|
13
|
-
// Store integration (JS properties only)
|
|
14
|
-
elementToolStateStore: { type: 'Object', reflect: false },
|
|
15
|
-
globalElementId: { type: 'String', reflect: false }
|
|
16
|
-
}
|
|
17
|
-
}}
|
|
18
|
-
/>
|
|
19
|
-
|
|
20
|
-
<!-- Answer Eliminator Tool - Process-of-Elimination Support (Inline Toggle Mode)
|
|
21
|
-
|
|
22
|
-
Allows students to mark answer choices as "eliminated" during test-taking,
|
|
23
|
-
supporting the process-of-elimination strategy.
|
|
24
|
-
|
|
25
|
-
**Interaction Pattern (Industry Standard):**
|
|
26
|
-
- Student toggles tool ON via toolbar button
|
|
27
|
-
- Small elimination buttons (X) appear next to each answer choice
|
|
28
|
-
- Student clicks X buttons to eliminate/restore choices
|
|
29
|
-
- Tool can be toggled OFF to hide all elimination buttons
|
|
30
|
-
- OR can be "always-on" via student profile (alwaysOn prop)
|
|
31
|
-
|
|
32
|
-
**Features:**
|
|
33
|
-
- Modern CSS Custom Highlight API (zero DOM mutation, 10-15x faster)
|
|
34
|
-
- Generic adapter pattern (works with multiple-choice, EBSR, inline-dropdown)
|
|
35
|
-
- Strikethrough visual (WCAG 2.2 AA compliant, best for accessibility)
|
|
36
|
-
- localStorage persistence across question navigation
|
|
37
|
-
- Keyboard accessible with proper ARIA attributes
|
|
38
|
-
|
|
39
|
-
**WCAG 2.2 Level AA Compliant:**
|
|
40
|
-
- 1.3.1 Info and Relationships (maintains structure)
|
|
41
|
-
- 2.4.3 Focus Order (no layout shift)
|
|
42
|
-
- 3.3.1 Error Identification (easy to identify/correct)
|
|
43
|
-
- 4.1.2 Name, Role, Value (proper ARIA)
|
|
44
|
-
-->
|
|
45
|
-
|
|
46
|
-
<script lang="ts">
|
|
47
|
-
|
|
48
|
-
import {
|
|
49
|
-
connectToolRuntimeContext,
|
|
50
|
-
connectToolShellContext,
|
|
51
|
-
ZIndexLayer,
|
|
52
|
-
} from '@pie-players/pie-assessment-toolkit';
|
|
53
|
-
import type {
|
|
54
|
-
AssessmentToolkitShellContext,
|
|
55
|
-
AssessmentToolkitRuntimeContext,
|
|
56
|
-
ToolCoordinatorApi,
|
|
57
|
-
} from '@pie-players/pie-assessment-toolkit';
|
|
58
|
-
import { onMount } from 'svelte';
|
|
59
|
-
import { AnswerEliminatorCore } from './answer-eliminator-core.js';
|
|
60
|
-
|
|
61
|
-
// Props
|
|
62
|
-
let {
|
|
63
|
-
visible = false,
|
|
64
|
-
toolId = 'answerEliminator',
|
|
65
|
-
strategy = 'strikethrough' as 'strikethrough' | 'mask' | 'gray',
|
|
66
|
-
alwaysOn = false, // Set true for profile-based accommodation
|
|
67
|
-
buttonAlignment = 'right' as 'left' | 'right' | 'inline', // Button placement: left, right, or inline with checkbox
|
|
68
|
-
scopeElement = null, // Container element to limit DOM queries (for multi-item pages)
|
|
69
|
-
|
|
70
|
-
// Store integration
|
|
71
|
-
elementToolStateStore = null, // ElementToolStateStore instance
|
|
72
|
-
globalElementId = '' // Composite key: "assessmentId:sectionId:itemId:elementId"
|
|
73
|
-
}: {
|
|
74
|
-
visible?: boolean;
|
|
75
|
-
toolId?: string;
|
|
76
|
-
strategy?: 'strikethrough' | 'mask' | 'gray';
|
|
77
|
-
alwaysOn?: boolean;
|
|
78
|
-
buttonAlignment?: 'left' | 'right' | 'inline';
|
|
79
|
-
scopeElement?: HTMLElement | null;
|
|
80
|
-
elementToolStateStore?: any;
|
|
81
|
-
globalElementId?: string;
|
|
82
|
-
} = $props();
|
|
83
|
-
|
|
84
|
-
// State
|
|
85
|
-
let contextHostElement = $state<HTMLElement | null>(null);
|
|
86
|
-
let runtimeContext = $state<AssessmentToolkitRuntimeContext | null>(null);
|
|
87
|
-
let shellContext = $state<AssessmentToolkitShellContext | null>(null);
|
|
88
|
-
const coordinator = $derived(
|
|
89
|
-
runtimeContext?.toolCoordinator as ToolCoordinatorApi | undefined,
|
|
90
|
-
);
|
|
91
|
-
let core = $state<AnswerEliminatorCore | null>(null);
|
|
92
|
-
let lastShellContextVersion = $state<number | null>(null);
|
|
93
|
-
|
|
94
|
-
// Track registration state
|
|
95
|
-
let registeredToolId = $state<string | null>(null);
|
|
96
|
-
let registeredCoordinator = $state<ToolCoordinatorApi | null>(null);
|
|
97
|
-
|
|
98
|
-
// Determine if tool should be active (either toggled on OR always-on mode)
|
|
99
|
-
let isActive = $derived(alwaysOn || visible);
|
|
100
|
-
|
|
101
|
-
$effect(() => {
|
|
102
|
-
if (!contextHostElement) return;
|
|
103
|
-
return connectToolRuntimeContext(contextHostElement, (value: AssessmentToolkitRuntimeContext) => {
|
|
104
|
-
runtimeContext = value;
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
$effect(() => {
|
|
109
|
-
if (!contextHostElement) return;
|
|
110
|
-
return connectToolShellContext(contextHostElement, (value: AssessmentToolkitShellContext) => {
|
|
111
|
-
shellContext = value;
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
function resolveQuestionRoot(): HTMLElement | null {
|
|
116
|
-
return scopeElement || shellContext?.scopeElement || null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function initializeForCurrentQuestion() {
|
|
120
|
-
if (!isActive || !core) return;
|
|
121
|
-
const questionRoot = resolveQuestionRoot();
|
|
122
|
-
|
|
123
|
-
if (!questionRoot) {
|
|
124
|
-
console.warn('[AnswerEliminator] Missing shell scope context for question root');
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
core.initializeForQuestion(questionRoot);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function handleItemChange() {
|
|
132
|
-
requestAnimationFrame(() => {
|
|
133
|
-
requestAnimationFrame(() => {
|
|
134
|
-
initializeForCurrentQuestion();
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Register with coordinator when it becomes available
|
|
140
|
-
$effect(() => {
|
|
141
|
-
if (!coordinator || !toolId) return;
|
|
142
|
-
if (
|
|
143
|
-
registeredCoordinator &&
|
|
144
|
-
registeredToolId &&
|
|
145
|
-
(registeredCoordinator !== coordinator || registeredToolId !== toolId)
|
|
146
|
-
) {
|
|
147
|
-
registeredCoordinator.unregisterTool(registeredToolId);
|
|
148
|
-
registeredCoordinator = null;
|
|
149
|
-
registeredToolId = null;
|
|
150
|
-
}
|
|
151
|
-
if (!registeredToolId) {
|
|
152
|
-
coordinator.registerTool(toolId, 'Answer Eliminator', undefined, ZIndexLayer.MODAL);
|
|
153
|
-
registeredCoordinator = coordinator;
|
|
154
|
-
registeredToolId = toolId;
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Update store integration when store props change
|
|
159
|
-
$effect(() => {
|
|
160
|
-
if (core && elementToolStateStore && globalElementId) {
|
|
161
|
-
core.setStoreIntegration(elementToolStateStore, globalElementId);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
onMount(() => {
|
|
166
|
-
// Initialize core engine with configuration
|
|
167
|
-
core = new AnswerEliminatorCore(strategy, buttonAlignment);
|
|
168
|
-
|
|
169
|
-
// Set up store integration if provided
|
|
170
|
-
if (core && elementToolStateStore && globalElementId) {
|
|
171
|
-
core.setStoreIntegration(elementToolStateStore, globalElementId);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Initialize for current question if active, otherwise ensure clean state
|
|
175
|
-
if (isActive) {
|
|
176
|
-
initializeForCurrentQuestion();
|
|
177
|
-
} else {
|
|
178
|
-
// If not active on mount, clear any leftover visual eliminations
|
|
179
|
-
core.cleanup();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return () => {
|
|
183
|
-
core?.destroy();
|
|
184
|
-
core = null;
|
|
185
|
-
if (registeredCoordinator && registeredToolId) {
|
|
186
|
-
registeredCoordinator.unregisterTool(registeredToolId);
|
|
187
|
-
registeredCoordinator = null;
|
|
188
|
-
registeredToolId = null;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
$effect(() => {
|
|
194
|
-
const shellVersion = shellContext?.contextVersion ?? null;
|
|
195
|
-
if (shellVersion === null) return;
|
|
196
|
-
if (lastShellContextVersion === null) {
|
|
197
|
-
lastShellContextVersion = shellVersion;
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (shellVersion === lastShellContextVersion) return;
|
|
201
|
-
lastShellContextVersion = shellVersion;
|
|
202
|
-
handleItemChange();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Watch for visibility changes to show/hide elimination buttons
|
|
206
|
-
$effect(() => {
|
|
207
|
-
if (core) {
|
|
208
|
-
if (isActive) {
|
|
209
|
-
// Re-enable state restoration when tool is activated
|
|
210
|
-
core.enableStateRestoration();
|
|
211
|
-
initializeForCurrentQuestion();
|
|
212
|
-
} else {
|
|
213
|
-
// Hide all elimination buttons when tool is turned off
|
|
214
|
-
// This also disables state restoration
|
|
215
|
-
core.cleanup();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
</script>
|
|
220
|
-
|
|
221
|
-
<!-- No visible UI - tool operates entirely through injected buttons next to choices -->
|
|
222
|
-
<!-- The toolbar button visibility is managed by tool-toolbar.svelte -->
|
|
223
|
-
<div bind:this={contextHostElement} style="display: none;" aria-hidden="true"></div>
|