@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 @@
|
|
|
1
|
+
export { SvelteComponent as default } from 'svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.config.d.ts","sourceRoot":"","sources":["../vite.config.ts"],"names":[],"mappings":";AAKA,wBAmCG"}
|
package/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pie-tool-answer-eliminator - PIE Assessment Tool
|
|
3
|
+
*
|
|
4
|
+
* This package exports a web component built from Svelte.
|
|
5
|
+
* Import the built version for CDN usage, or the .svelte source for Svelte projects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export AdapterRegistry for auto-hide detection
|
|
9
|
+
export { AdapterRegistry } from "./adapters/adapter-registry";
|
|
10
|
+
|
|
11
|
+
// Re-export any TypeScript types defined in the package
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pie-players/pie-tool-answer-eliminator",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Answer eliminator tool for PIE assessment player - supports process-of-elimination test-taking strategy",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/pie-framework/pie-players.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pie",
|
|
15
|
+
"assessment",
|
|
16
|
+
"tool",
|
|
17
|
+
"answer-eliminator",
|
|
18
|
+
"test-strategy",
|
|
19
|
+
"wcag",
|
|
20
|
+
"accessibility"
|
|
21
|
+
],
|
|
22
|
+
"svelte": "./tool-answer-eliminator.svelte",
|
|
23
|
+
"main": "./dist/tool-answer-eliminator.js",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/tool-answer-eliminator.js",
|
|
28
|
+
"svelte": "./tool-answer-eliminator.svelte"
|
|
29
|
+
},
|
|
30
|
+
"./adapters/adapter-registry": {
|
|
31
|
+
"types": "./adapters/adapter-registry.ts",
|
|
32
|
+
"import": "./adapters/adapter-registry.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"tool-answer-eliminator.svelte",
|
|
38
|
+
"index.ts",
|
|
39
|
+
"package.json",
|
|
40
|
+
"answer-eliminator-core.ts",
|
|
41
|
+
"adapters/",
|
|
42
|
+
"strategies/"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"svelte": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"unpkg": "./dist/tool-answer-eliminator.js",
|
|
49
|
+
"jsdelivr": "./dist/tool-answer-eliminator.js",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@pie-players/pie-assessment-toolkit": "workspace:*",
|
|
52
|
+
"@pie-players/pie-players-shared": "workspace:*"
|
|
53
|
+
},
|
|
54
|
+
"types": "./dist/index.d.ts",
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "vite build",
|
|
57
|
+
"dev": "vite build --watch",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"lint": "biome check ."
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@biomejs/biome": "^2.3.10",
|
|
63
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.4",
|
|
64
|
+
"svelte": "^5.16.1",
|
|
65
|
+
"typescript": "^5.7.0",
|
|
66
|
+
"vite": "^7.0.8",
|
|
67
|
+
"vite-plugin-dts": "^4.5.3"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy interface for visual elimination styles
|
|
3
|
+
* Uses CSS Custom Highlight API for zero DOM mutation
|
|
4
|
+
*/
|
|
5
|
+
export interface EliminationStrategy {
|
|
6
|
+
/** Strategy name */
|
|
7
|
+
readonly name: string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Apply elimination visual to a choice
|
|
11
|
+
* @param choiceId - Unique identifier for this choice
|
|
12
|
+
* @param range - DOM Range covering the choice content
|
|
13
|
+
*/
|
|
14
|
+
apply(choiceId: string, range: Range): void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Remove elimination visual from a choice
|
|
18
|
+
* @param choiceId - Unique identifier for this choice
|
|
19
|
+
*/
|
|
20
|
+
remove(choiceId: string): void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a choice is currently eliminated
|
|
24
|
+
* @param choiceId - Unique identifier for this choice
|
|
25
|
+
*/
|
|
26
|
+
isEliminated(choiceId: string): boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Clear all eliminations
|
|
30
|
+
*/
|
|
31
|
+
clearAll(): void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get all eliminated choice IDs
|
|
35
|
+
*/
|
|
36
|
+
getEliminatedIds(): string[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize strategy (inject CSS, etc.)
|
|
40
|
+
*/
|
|
41
|
+
initialize(): void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cleanup strategy (remove CSS, etc.)
|
|
45
|
+
*/
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { EliminationStrategy } from "./elimination-strategy";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mask strategy using CSS Custom Highlight API
|
|
5
|
+
* Partially hides/grays eliminated choices
|
|
6
|
+
*/
|
|
7
|
+
export class MaskStrategy implements EliminationStrategy {
|
|
8
|
+
readonly name = "mask";
|
|
9
|
+
|
|
10
|
+
private highlights = new Map<string, Highlight>();
|
|
11
|
+
private ranges = new Map<string, Range>();
|
|
12
|
+
|
|
13
|
+
initialize(): void {
|
|
14
|
+
this.injectCSS();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
destroy(): void {
|
|
18
|
+
this.clearAll();
|
|
19
|
+
this.removeCSS();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
apply(choiceId: string, range: Range): void {
|
|
23
|
+
if (!this.isSupported()) {
|
|
24
|
+
this.applyFallback(choiceId, range);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Inject CSS for this specific highlight
|
|
29
|
+
this.injectHighlightCSS(choiceId);
|
|
30
|
+
|
|
31
|
+
const highlight = new Highlight(range);
|
|
32
|
+
CSS.highlights.set(`answer-masked-${choiceId}`, highlight);
|
|
33
|
+
|
|
34
|
+
this.highlights.set(choiceId, highlight);
|
|
35
|
+
this.ranges.set(choiceId, range);
|
|
36
|
+
|
|
37
|
+
this.addAriaAttributes(range);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
remove(choiceId: string): void {
|
|
41
|
+
if (!this.isSupported()) {
|
|
42
|
+
this.removeFallback(choiceId);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
CSS.highlights.delete(`answer-masked-${choiceId}`);
|
|
47
|
+
|
|
48
|
+
// Remove CSS for this specific highlight
|
|
49
|
+
this.removeHighlightCSS(choiceId);
|
|
50
|
+
|
|
51
|
+
const range = this.ranges.get(choiceId);
|
|
52
|
+
if (range) {
|
|
53
|
+
this.removeAriaAttributes(range);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.highlights.delete(choiceId);
|
|
57
|
+
this.ranges.delete(choiceId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
isEliminated(choiceId: string): boolean {
|
|
61
|
+
return this.highlights.has(choiceId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clearAll(): void {
|
|
65
|
+
for (const choiceId of this.highlights.keys()) {
|
|
66
|
+
this.remove(choiceId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getEliminatedIds(): string[] {
|
|
71
|
+
return Array.from(this.highlights.keys());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private isSupported(): boolean {
|
|
75
|
+
return typeof CSS !== "undefined" && "highlights" in CSS;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private injectCSS(): void {
|
|
79
|
+
const styleId = "answer-eliminator-mask-styles";
|
|
80
|
+
if (document.getElementById(styleId)) return;
|
|
81
|
+
|
|
82
|
+
const style = document.createElement("style");
|
|
83
|
+
style.id = styleId;
|
|
84
|
+
// CSS Custom Highlight API: Each registered highlight gets its own ::highlight() selector
|
|
85
|
+
// We inject choice-specific styles dynamically in injectHighlightCSS()
|
|
86
|
+
style.textContent = `
|
|
87
|
+
/* Fallback */
|
|
88
|
+
.answer-masked-fallback {
|
|
89
|
+
opacity: 0.2;
|
|
90
|
+
filter: blur(2px);
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
document.head.appendChild(style);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private injectHighlightCSS(choiceId: string): void {
|
|
98
|
+
const styleId = `answer-eliminator-mask-highlight-${choiceId}`;
|
|
99
|
+
if (document.getElementById(styleId)) return;
|
|
100
|
+
|
|
101
|
+
const style = document.createElement("style");
|
|
102
|
+
style.id = styleId;
|
|
103
|
+
style.textContent = `
|
|
104
|
+
::highlight(answer-masked-${choiceId}) {
|
|
105
|
+
opacity: 0.2;
|
|
106
|
+
filter: blur(2px);
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
document.head.appendChild(style);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private removeHighlightCSS(choiceId: string): void {
|
|
113
|
+
document
|
|
114
|
+
.getElementById(`answer-eliminator-mask-highlight-${choiceId}`)
|
|
115
|
+
?.remove();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private removeCSS(): void {
|
|
119
|
+
document.getElementById("answer-eliminator-mask-styles")?.remove();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private addAriaAttributes(range: Range): void {
|
|
123
|
+
const container = this.findChoiceContainer(range);
|
|
124
|
+
if (!container) return;
|
|
125
|
+
|
|
126
|
+
container.setAttribute("data-eliminated", "true");
|
|
127
|
+
container.setAttribute("aria-hidden", "true");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private removeAriaAttributes(range: Range): void {
|
|
131
|
+
const container = this.findChoiceContainer(range);
|
|
132
|
+
if (!container) return;
|
|
133
|
+
|
|
134
|
+
container.removeAttribute("data-eliminated");
|
|
135
|
+
container.removeAttribute("aria-hidden");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private findChoiceContainer(range: Range): HTMLElement | null {
|
|
139
|
+
let element: HTMLElement | null = range.startContainer as HTMLElement;
|
|
140
|
+
|
|
141
|
+
// If startContainer is a text node, get its parent
|
|
142
|
+
if (element.nodeType === Node.TEXT_NODE) {
|
|
143
|
+
element = element.parentElement;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
while (element && element !== document.body) {
|
|
147
|
+
if (element.classList?.contains("choice-input")) {
|
|
148
|
+
return element;
|
|
149
|
+
}
|
|
150
|
+
element = element.parentElement;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private applyFallback(choiceId: string, range: Range): void {
|
|
157
|
+
const container = this.findChoiceContainer(range);
|
|
158
|
+
if (!container) return;
|
|
159
|
+
|
|
160
|
+
container.classList.add("answer-masked-fallback");
|
|
161
|
+
container.setAttribute("data-eliminated", "true");
|
|
162
|
+
container.setAttribute("data-eliminated-id", choiceId);
|
|
163
|
+
this.addAriaAttributes(range);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private removeFallback(choiceId: string): void {
|
|
167
|
+
const container = document.querySelector(
|
|
168
|
+
`[data-eliminated-id="${choiceId}"]`,
|
|
169
|
+
);
|
|
170
|
+
if (!container) return;
|
|
171
|
+
|
|
172
|
+
container.classList.remove("answer-masked-fallback");
|
|
173
|
+
container.removeAttribute("data-eliminated");
|
|
174
|
+
container.removeAttribute("data-eliminated-id");
|
|
175
|
+
|
|
176
|
+
const range = document.createRange();
|
|
177
|
+
range.selectNodeContents(container);
|
|
178
|
+
this.removeAriaAttributes(range);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { EliminationStrategy } from "./elimination-strategy";
|
|
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
|
+
readonly name = "strikethrough";
|
|
12
|
+
|
|
13
|
+
private highlights = new Map<string, Highlight>();
|
|
14
|
+
private ranges = new Map<string, Range>();
|
|
15
|
+
|
|
16
|
+
initialize(): void {
|
|
17
|
+
// Check browser support
|
|
18
|
+
if (!this.isSupported()) {
|
|
19
|
+
console.warn("CSS Custom Highlight API not supported, using fallback");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Inject CSS for highlight styling
|
|
24
|
+
this.injectCSS();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
destroy(): void {
|
|
28
|
+
this.clearAll();
|
|
29
|
+
this.removeCSS();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
apply(choiceId: string, range: Range): void {
|
|
33
|
+
if (!this.isSupported()) {
|
|
34
|
+
this.applyFallback(choiceId, range);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Inject CSS for this specific highlight
|
|
39
|
+
this.injectHighlightCSS(choiceId);
|
|
40
|
+
|
|
41
|
+
// Create highlight for this range
|
|
42
|
+
const highlight = new Highlight(range);
|
|
43
|
+
|
|
44
|
+
// Register in CSS.highlights with unique name
|
|
45
|
+
CSS.highlights.set(`answer-eliminated-${choiceId}`, highlight);
|
|
46
|
+
|
|
47
|
+
// Track internally
|
|
48
|
+
this.highlights.set(choiceId, highlight);
|
|
49
|
+
this.ranges.set(choiceId, range);
|
|
50
|
+
|
|
51
|
+
// Add ARIA attributes to the choice element for screen readers
|
|
52
|
+
this.addAriaAttributes(range);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
remove(choiceId: string): void {
|
|
56
|
+
if (!this.isSupported()) {
|
|
57
|
+
this.removeFallback(choiceId);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Remove from CSS.highlights
|
|
62
|
+
CSS.highlights.delete(`answer-eliminated-${choiceId}`);
|
|
63
|
+
|
|
64
|
+
// Remove CSS for this specific highlight
|
|
65
|
+
this.removeHighlightCSS(choiceId);
|
|
66
|
+
|
|
67
|
+
// Remove from internal tracking
|
|
68
|
+
const range = this.ranges.get(choiceId);
|
|
69
|
+
if (range) {
|
|
70
|
+
this.removeAriaAttributes(range);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.highlights.delete(choiceId);
|
|
74
|
+
this.ranges.delete(choiceId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
isEliminated(choiceId: string): boolean {
|
|
78
|
+
return this.highlights.has(choiceId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clearAll(): void {
|
|
82
|
+
// Remove all highlights
|
|
83
|
+
for (const choiceId of this.highlights.keys()) {
|
|
84
|
+
this.remove(choiceId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getEliminatedIds(): string[] {
|
|
89
|
+
return Array.from(this.highlights.keys());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private isSupported(): boolean {
|
|
93
|
+
return typeof CSS !== "undefined" && "highlights" in CSS;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private injectCSS(): void {
|
|
97
|
+
const styleId = "answer-eliminator-strikethrough-styles";
|
|
98
|
+
|
|
99
|
+
// Don't inject if already exists
|
|
100
|
+
if (document.getElementById(styleId)) return;
|
|
101
|
+
|
|
102
|
+
const style = document.createElement("style");
|
|
103
|
+
style.id = styleId;
|
|
104
|
+
// CSS Custom Highlight API: Each registered highlight gets its own ::highlight() selector
|
|
105
|
+
// Since we register highlights with names like 'answer-eliminated-{choiceId}',
|
|
106
|
+
// we need to inject CSS for each one dynamically, OR use a shared attribute approach.
|
|
107
|
+
// For performance, we inject a base style and add choice-specific styles dynamically.
|
|
108
|
+
style.textContent = `
|
|
109
|
+
/* Fallback for browsers without CSS Highlight API */
|
|
110
|
+
.answer-eliminated-fallback {
|
|
111
|
+
text-decoration: line-through;
|
|
112
|
+
text-decoration-thickness: 2px;
|
|
113
|
+
text-decoration-color: var(--pie-incorrect, #ff9800);
|
|
114
|
+
opacity: 0.6;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Screen reader only text */
|
|
118
|
+
.sr-announcement {
|
|
119
|
+
position: absolute;
|
|
120
|
+
left: -10000px;
|
|
121
|
+
width: 1px;
|
|
122
|
+
height: 1px;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
document.head.appendChild(style);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private injectHighlightCSS(choiceId: string): void {
|
|
131
|
+
const styleId = `answer-eliminator-highlight-${choiceId}`;
|
|
132
|
+
if (document.getElementById(styleId)) return;
|
|
133
|
+
|
|
134
|
+
const style = document.createElement("style");
|
|
135
|
+
style.id = styleId;
|
|
136
|
+
style.textContent = `
|
|
137
|
+
::highlight(answer-eliminated-${choiceId}) {
|
|
138
|
+
text-decoration: line-through;
|
|
139
|
+
text-decoration-thickness: 2px;
|
|
140
|
+
text-decoration-color: var(--pie-incorrect, #ff9800);
|
|
141
|
+
opacity: 0.6;
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
document.head.appendChild(style);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private removeHighlightCSS(choiceId: string): void {
|
|
148
|
+
document
|
|
149
|
+
.getElementById(`answer-eliminator-highlight-${choiceId}`)
|
|
150
|
+
?.remove();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private removeCSS(): void {
|
|
154
|
+
const style = document.getElementById(
|
|
155
|
+
"answer-eliminator-strikethrough-styles",
|
|
156
|
+
);
|
|
157
|
+
style?.remove();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private addAriaAttributes(range: Range): void {
|
|
161
|
+
// Find the choice container element
|
|
162
|
+
const container = this.findChoiceContainer(range);
|
|
163
|
+
if (!container) return;
|
|
164
|
+
|
|
165
|
+
container.setAttribute("data-eliminated", "true");
|
|
166
|
+
container.setAttribute("aria-disabled", "true");
|
|
167
|
+
|
|
168
|
+
// Add screen reader announcement
|
|
169
|
+
const label = container.querySelector(".label");
|
|
170
|
+
if (label && !label.querySelector(".sr-announcement")) {
|
|
171
|
+
const announcement = document.createElement("span");
|
|
172
|
+
announcement.className = "sr-announcement";
|
|
173
|
+
announcement.textContent = " (eliminated)";
|
|
174
|
+
label.appendChild(announcement);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private removeAriaAttributes(range: Range): void {
|
|
179
|
+
const container = this.findChoiceContainer(range);
|
|
180
|
+
if (!container) return;
|
|
181
|
+
|
|
182
|
+
container.removeAttribute("data-eliminated");
|
|
183
|
+
container.removeAttribute("aria-disabled");
|
|
184
|
+
|
|
185
|
+
// Remove screen reader announcement
|
|
186
|
+
const announcement = container.querySelector(".sr-announcement");
|
|
187
|
+
announcement?.remove();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private findChoiceContainer(range: Range): HTMLElement | null {
|
|
191
|
+
// Walk up from range start to find the choice container
|
|
192
|
+
let element: HTMLElement | null = range.startContainer as HTMLElement;
|
|
193
|
+
|
|
194
|
+
// If startContainer is a text node, get its parent
|
|
195
|
+
if (element.nodeType === Node.TEXT_NODE) {
|
|
196
|
+
element = element.parentElement;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
while (element && element !== document.body) {
|
|
200
|
+
if (
|
|
201
|
+
element.classList?.contains("choice-input") ||
|
|
202
|
+
element.classList?.contains("corespring-checkbox") ||
|
|
203
|
+
element.classList?.contains("corespring-radio-button")
|
|
204
|
+
) {
|
|
205
|
+
return element;
|
|
206
|
+
}
|
|
207
|
+
element = element.parentElement;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Fallback for browsers without CSS Highlight API
|
|
214
|
+
private applyFallback(choiceId: string, range: Range): void {
|
|
215
|
+
const container = this.findChoiceContainer(range);
|
|
216
|
+
if (!container) return;
|
|
217
|
+
|
|
218
|
+
container.classList.add("answer-eliminated-fallback");
|
|
219
|
+
container.setAttribute("data-eliminated", "true");
|
|
220
|
+
container.setAttribute("data-eliminated-id", choiceId);
|
|
221
|
+
this.addAriaAttributes(range);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private removeFallback(choiceId: string): void {
|
|
225
|
+
const container = document.querySelector(
|
|
226
|
+
`[data-eliminated-id="${choiceId}"]`,
|
|
227
|
+
);
|
|
228
|
+
if (!container) return;
|
|
229
|
+
|
|
230
|
+
container.classList.remove("answer-eliminated-fallback");
|
|
231
|
+
container.removeAttribute("data-eliminated");
|
|
232
|
+
container.removeAttribute("data-eliminated-id");
|
|
233
|
+
|
|
234
|
+
// Create fake range for aria removal
|
|
235
|
+
const range = document.createRange();
|
|
236
|
+
range.selectNodeContents(container);
|
|
237
|
+
this.removeAriaAttributes(range);
|
|
238
|
+
}
|
|
239
|
+
}
|