@pie-players/pie-tool-answer-eliminator 0.2.9 → 0.2.11
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/adapters/multiple-choice-adapter.ts +62 -14
- package/answer-eliminator-core.ts +7 -50
- package/dist/adapters/multiple-choice-adapter.d.ts +8 -0
- package/dist/adapters/multiple-choice-adapter.d.ts.map +1 -1
- package/dist/answer-eliminator-core.d.ts +2 -0
- package/dist/answer-eliminator-core.d.ts.map +1 -1
- package/dist/strategies/mask-strategy.d.ts +6 -2
- package/dist/strategies/mask-strategy.d.ts.map +1 -1
- package/dist/strategies/strikethrough-strategy.d.ts +9 -2
- package/dist/strategies/strikethrough-strategy.d.ts.map +1 -1
- package/dist/tool-answer-eliminator.js +1460 -1325
- package/package.json +4 -4
- package/strategies/mask-strategy.ts +27 -39
- package/strategies/strikethrough-strategy.ts +47 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-tool-answer-eliminator",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Answer eliminator tool for PIE assessment player - supports process-of-elimination test-taking strategy",
|
|
6
6
|
"repository": {
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"unpkg": "./dist/tool-answer-eliminator.js",
|
|
50
50
|
"jsdelivr": "./dist/tool-answer-eliminator.js",
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@pie-players/pie-assessment-toolkit": "0.2.
|
|
53
|
-
"@pie-players/pie-context": "0.1.
|
|
54
|
-
"@pie-players/pie-players-shared": "0.2.
|
|
52
|
+
"@pie-players/pie-assessment-toolkit": "0.2.11",
|
|
53
|
+
"@pie-players/pie-context": "0.1.3",
|
|
54
|
+
"@pie-players/pie-players-shared": "0.2.7",
|
|
55
55
|
"daisyui": "^5.5.18"
|
|
56
56
|
},
|
|
57
57
|
"types": "./dist/index.d.ts",
|
|
@@ -5,6 +5,15 @@ import type { EliminationStrategy } from "./elimination-strategy.js";
|
|
|
5
5
|
* Partially hides/grays eliminated choices
|
|
6
6
|
*/
|
|
7
7
|
export class MaskStrategy implements EliminationStrategy {
|
|
8
|
+
private static readonly HIGHLIGHT_STYLE_PREFIX =
|
|
9
|
+
"pie-answer-eliminator-mask-highlight-";
|
|
10
|
+
private static readonly HIGHLIGHT_NAME_PREFIX = "pie-answer-masked-";
|
|
11
|
+
private static readonly FALLBACK_CLASS = "pie-answer-masked-fallback";
|
|
12
|
+
private static readonly CHOICE_HOOK_ATTR =
|
|
13
|
+
"data-pie-answer-eliminator-choice";
|
|
14
|
+
private static readonly ELIMINATED_ATTR = "data-pie-answer-eliminated";
|
|
15
|
+
private static readonly ELIMINATED_ID_ATTR = "data-pie-answer-eliminated-id";
|
|
16
|
+
|
|
8
17
|
readonly name = "mask";
|
|
9
18
|
|
|
10
19
|
private highlights = new Map<string, Highlight>();
|
|
@@ -12,12 +21,11 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
12
21
|
private fallbackContainers = new Map<string, HTMLElement>();
|
|
13
22
|
|
|
14
23
|
initialize(): void {
|
|
15
|
-
|
|
24
|
+
// No-op: shared fallback styles are owned by @pie-players/pie-theme/components.css.
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
destroy(): void {
|
|
19
28
|
this.clearAll();
|
|
20
|
-
this.removeCSS();
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
apply(choiceId: string, range: Range): void {
|
|
@@ -30,7 +38,10 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
30
38
|
this.injectHighlightCSS(choiceId);
|
|
31
39
|
|
|
32
40
|
const highlight = new Highlight(range);
|
|
33
|
-
CSS.highlights.set(
|
|
41
|
+
CSS.highlights.set(
|
|
42
|
+
`${MaskStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`,
|
|
43
|
+
highlight,
|
|
44
|
+
);
|
|
34
45
|
|
|
35
46
|
this.highlights.set(choiceId, highlight);
|
|
36
47
|
this.ranges.set(choiceId, range);
|
|
@@ -44,7 +55,7 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
44
55
|
return;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
CSS.highlights.delete(
|
|
58
|
+
CSS.highlights.delete(`${MaskStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`);
|
|
48
59
|
|
|
49
60
|
// Remove CSS for this specific highlight
|
|
50
61
|
this.removeHighlightCSS(choiceId);
|
|
@@ -78,33 +89,14 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
78
89
|
return typeof CSS !== "undefined" && "highlights" in CSS;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
|
-
private injectCSS(): void {
|
|
82
|
-
const styleId = "answer-eliminator-mask-styles";
|
|
83
|
-
if (document.getElementById(styleId)) return;
|
|
84
|
-
|
|
85
|
-
const style = document.createElement("style");
|
|
86
|
-
style.id = styleId;
|
|
87
|
-
// CSS Custom Highlight API: Each registered highlight gets its own ::highlight() selector
|
|
88
|
-
// We inject choice-specific styles dynamically in injectHighlightCSS()
|
|
89
|
-
style.textContent = `
|
|
90
|
-
/* Fallback */
|
|
91
|
-
.answer-masked-fallback {
|
|
92
|
-
opacity: 0.2;
|
|
93
|
-
filter: blur(2px);
|
|
94
|
-
}
|
|
95
|
-
`;
|
|
96
|
-
|
|
97
|
-
document.head.appendChild(style);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
92
|
private injectHighlightCSS(choiceId: string): void {
|
|
101
|
-
const styleId =
|
|
93
|
+
const styleId = `${MaskStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`;
|
|
102
94
|
if (document.getElementById(styleId)) return;
|
|
103
95
|
|
|
104
96
|
const style = document.createElement("style");
|
|
105
97
|
style.id = styleId;
|
|
106
98
|
style.textContent = `
|
|
107
|
-
::highlight(answer-masked-${choiceId}) {
|
|
99
|
+
::highlight(pie-answer-masked-${choiceId}) {
|
|
108
100
|
opacity: 0.2;
|
|
109
101
|
filter: blur(2px);
|
|
110
102
|
}
|
|
@@ -114,19 +106,15 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
114
106
|
|
|
115
107
|
private removeHighlightCSS(choiceId: string): void {
|
|
116
108
|
document
|
|
117
|
-
.getElementById(
|
|
109
|
+
.getElementById(`${MaskStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`)
|
|
118
110
|
?.remove();
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
private removeCSS(): void {
|
|
122
|
-
document.getElementById("answer-eliminator-mask-styles")?.remove();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
113
|
private addAriaAttributes(range: Range): void {
|
|
126
114
|
const container = this.findChoiceContainer(range);
|
|
127
115
|
if (!container) return;
|
|
128
116
|
|
|
129
|
-
container.setAttribute(
|
|
117
|
+
container.setAttribute(MaskStrategy.ELIMINATED_ATTR, "true");
|
|
130
118
|
container.setAttribute("aria-hidden", "true");
|
|
131
119
|
}
|
|
132
120
|
|
|
@@ -134,7 +122,7 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
134
122
|
const container = this.findChoiceContainer(range);
|
|
135
123
|
if (!container) return;
|
|
136
124
|
|
|
137
|
-
container.removeAttribute(
|
|
125
|
+
container.removeAttribute(MaskStrategy.ELIMINATED_ATTR);
|
|
138
126
|
container.removeAttribute("aria-hidden");
|
|
139
127
|
}
|
|
140
128
|
|
|
@@ -147,7 +135,7 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
147
135
|
}
|
|
148
136
|
|
|
149
137
|
while (element && element !== document.body) {
|
|
150
|
-
if (element.
|
|
138
|
+
if (element.getAttribute(MaskStrategy.CHOICE_HOOK_ATTR) === "true") {
|
|
151
139
|
return element;
|
|
152
140
|
}
|
|
153
141
|
element = element.parentElement;
|
|
@@ -160,9 +148,9 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
160
148
|
const container = this.findChoiceContainer(range);
|
|
161
149
|
if (!container) return;
|
|
162
150
|
|
|
163
|
-
container.classList.add(
|
|
164
|
-
container.setAttribute(
|
|
165
|
-
container.setAttribute(
|
|
151
|
+
container.classList.add(MaskStrategy.FALLBACK_CLASS);
|
|
152
|
+
container.setAttribute(MaskStrategy.ELIMINATED_ATTR, "true");
|
|
153
|
+
container.setAttribute(MaskStrategy.ELIMINATED_ID_ATTR, choiceId);
|
|
166
154
|
this.fallbackContainers.set(choiceId, container);
|
|
167
155
|
this.addAriaAttributes(range);
|
|
168
156
|
}
|
|
@@ -171,9 +159,9 @@ export class MaskStrategy implements EliminationStrategy {
|
|
|
171
159
|
const container = this.fallbackContainers.get(choiceId);
|
|
172
160
|
if (!container) return;
|
|
173
161
|
|
|
174
|
-
container.classList.remove(
|
|
175
|
-
container.removeAttribute(
|
|
176
|
-
container.removeAttribute(
|
|
162
|
+
container.classList.remove(MaskStrategy.FALLBACK_CLASS);
|
|
163
|
+
container.removeAttribute(MaskStrategy.ELIMINATED_ATTR);
|
|
164
|
+
container.removeAttribute(MaskStrategy.ELIMINATED_ID_ATTR);
|
|
177
165
|
|
|
178
166
|
const range = document.createRange();
|
|
179
167
|
range.selectNodeContents(container);
|
|
@@ -8,6 +8,18 @@ import type { EliminationStrategy } from "./elimination-strategy.js";
|
|
|
8
8
|
* WCAG Compliance: Maintains info structure (1.3.1), no layout shift (2.4.3)
|
|
9
9
|
*/
|
|
10
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
|
+
|
|
11
23
|
readonly name = "strikethrough";
|
|
12
24
|
|
|
13
25
|
private highlights = new Map<string, Highlight>();
|
|
@@ -18,16 +30,11 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
18
30
|
// Check browser support
|
|
19
31
|
if (!this.isSupported()) {
|
|
20
32
|
console.warn("CSS Custom Highlight API not supported, using fallback");
|
|
21
|
-
return;
|
|
22
33
|
}
|
|
23
|
-
|
|
24
|
-
// Inject CSS for highlight styling
|
|
25
|
-
this.injectCSS();
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
destroy(): void {
|
|
29
37
|
this.clearAll();
|
|
30
|
-
this.removeCSS();
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
apply(choiceId: string, range: Range): void {
|
|
@@ -43,7 +50,10 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
43
50
|
const highlight = new Highlight(range);
|
|
44
51
|
|
|
45
52
|
// Register in CSS.highlights with unique name
|
|
46
|
-
CSS.highlights.set(
|
|
53
|
+
CSS.highlights.set(
|
|
54
|
+
`${StrikethroughStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`,
|
|
55
|
+
highlight,
|
|
56
|
+
);
|
|
47
57
|
|
|
48
58
|
// Track internally
|
|
49
59
|
this.highlights.set(choiceId, highlight);
|
|
@@ -60,7 +70,9 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
// Remove from CSS.highlights
|
|
63
|
-
CSS.highlights.delete(
|
|
73
|
+
CSS.highlights.delete(
|
|
74
|
+
`${StrikethroughStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`,
|
|
75
|
+
);
|
|
64
76
|
|
|
65
77
|
// Remove CSS for this specific highlight
|
|
66
78
|
this.removeHighlightCSS(choiceId);
|
|
@@ -96,48 +108,14 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
96
108
|
return typeof CSS !== "undefined" && "highlights" in CSS;
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
private injectCSS(): void {
|
|
100
|
-
const styleId = "answer-eliminator-strikethrough-styles";
|
|
101
|
-
|
|
102
|
-
// Don't inject if already exists
|
|
103
|
-
if (document.getElementById(styleId)) return;
|
|
104
|
-
|
|
105
|
-
const style = document.createElement("style");
|
|
106
|
-
style.id = styleId;
|
|
107
|
-
// CSS Custom Highlight API: Each registered highlight gets its own ::highlight() selector
|
|
108
|
-
// Since we register highlights with names like 'answer-eliminated-{choiceId}',
|
|
109
|
-
// we need to inject CSS for each one dynamically, OR use a shared attribute approach.
|
|
110
|
-
// For performance, we inject a base style and add choice-specific styles dynamically.
|
|
111
|
-
style.textContent = `
|
|
112
|
-
/* Fallback for browsers without CSS Highlight API */
|
|
113
|
-
.answer-eliminated-fallback {
|
|
114
|
-
text-decoration: line-through;
|
|
115
|
-
text-decoration-thickness: 2px;
|
|
116
|
-
text-decoration-color: var(--pie-incorrect, #ff9800);
|
|
117
|
-
opacity: 0.6;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/* Screen reader only text */
|
|
121
|
-
.sr-announcement {
|
|
122
|
-
position: absolute;
|
|
123
|
-
left: -10000px;
|
|
124
|
-
width: 1px;
|
|
125
|
-
height: 1px;
|
|
126
|
-
overflow: hidden;
|
|
127
|
-
}
|
|
128
|
-
`;
|
|
129
|
-
|
|
130
|
-
document.head.appendChild(style);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
111
|
private injectHighlightCSS(choiceId: string): void {
|
|
134
|
-
const styleId =
|
|
112
|
+
const styleId = `${StrikethroughStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`;
|
|
135
113
|
if (document.getElementById(styleId)) return;
|
|
136
114
|
|
|
137
115
|
const style = document.createElement("style");
|
|
138
116
|
style.id = styleId;
|
|
139
117
|
style.textContent = `
|
|
140
|
-
::highlight(answer-eliminated-${choiceId}) {
|
|
118
|
+
::highlight(pie-answer-eliminated-${choiceId}) {
|
|
141
119
|
text-decoration: line-through;
|
|
142
120
|
text-decoration-thickness: 2px;
|
|
143
121
|
text-decoration-color: var(--pie-incorrect, #ff9800);
|
|
@@ -149,30 +127,25 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
149
127
|
|
|
150
128
|
private removeHighlightCSS(choiceId: string): void {
|
|
151
129
|
document
|
|
152
|
-
.getElementById(
|
|
130
|
+
.getElementById(
|
|
131
|
+
`${StrikethroughStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`,
|
|
132
|
+
)
|
|
153
133
|
?.remove();
|
|
154
134
|
}
|
|
155
135
|
|
|
156
|
-
private removeCSS(): void {
|
|
157
|
-
const style = document.getElementById(
|
|
158
|
-
"answer-eliminator-strikethrough-styles",
|
|
159
|
-
);
|
|
160
|
-
style?.remove();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
136
|
private addAriaAttributes(range: Range): void {
|
|
164
137
|
// Find the choice container element
|
|
165
138
|
const container = this.findChoiceContainer(range);
|
|
166
139
|
if (!container) return;
|
|
167
140
|
|
|
168
|
-
container.setAttribute(
|
|
141
|
+
container.setAttribute(StrikethroughStrategy.ELIMINATED_ATTR, "true");
|
|
169
142
|
container.setAttribute("aria-disabled", "true");
|
|
170
143
|
|
|
171
144
|
// Add screen reader announcement
|
|
172
|
-
const label =
|
|
173
|
-
if (label && !label.querySelector(
|
|
145
|
+
const label = this.resolveLabelElement(container);
|
|
146
|
+
if (label && !label.querySelector(`.${StrikethroughStrategy.SR_CLASS}`)) {
|
|
174
147
|
const announcement = document.createElement("span");
|
|
175
|
-
announcement.className =
|
|
148
|
+
announcement.className = StrikethroughStrategy.SR_CLASS;
|
|
176
149
|
announcement.textContent = " (eliminated)";
|
|
177
150
|
label.appendChild(announcement);
|
|
178
151
|
}
|
|
@@ -182,11 +155,13 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
182
155
|
const container = this.findChoiceContainer(range);
|
|
183
156
|
if (!container) return;
|
|
184
157
|
|
|
185
|
-
container.removeAttribute(
|
|
158
|
+
container.removeAttribute(StrikethroughStrategy.ELIMINATED_ATTR);
|
|
186
159
|
container.removeAttribute("aria-disabled");
|
|
187
160
|
|
|
188
161
|
// Remove screen reader announcement
|
|
189
|
-
const announcement = container.querySelector(
|
|
162
|
+
const announcement = container.querySelector(
|
|
163
|
+
`.${StrikethroughStrategy.SR_CLASS}`,
|
|
164
|
+
);
|
|
190
165
|
announcement?.remove();
|
|
191
166
|
}
|
|
192
167
|
|
|
@@ -201,9 +176,7 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
201
176
|
|
|
202
177
|
while (element && element !== document.body) {
|
|
203
178
|
if (
|
|
204
|
-
element.
|
|
205
|
-
element.classList?.contains("corespring-checkbox") ||
|
|
206
|
-
element.classList?.contains("corespring-radio-button")
|
|
179
|
+
element.getAttribute(StrikethroughStrategy.CHOICE_HOOK_ATTR) === "true"
|
|
207
180
|
) {
|
|
208
181
|
return element;
|
|
209
182
|
}
|
|
@@ -218,9 +191,9 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
218
191
|
const container = this.findChoiceContainer(range);
|
|
219
192
|
if (!container) return;
|
|
220
193
|
|
|
221
|
-
container.classList.add(
|
|
222
|
-
container.setAttribute(
|
|
223
|
-
container.setAttribute(
|
|
194
|
+
container.classList.add(StrikethroughStrategy.FALLBACK_CLASS);
|
|
195
|
+
container.setAttribute(StrikethroughStrategy.ELIMINATED_ATTR, "true");
|
|
196
|
+
container.setAttribute(StrikethroughStrategy.ELIMINATED_ID_ATTR, choiceId);
|
|
224
197
|
this.fallbackContainers.set(choiceId, container);
|
|
225
198
|
this.addAriaAttributes(range);
|
|
226
199
|
}
|
|
@@ -229,9 +202,9 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
229
202
|
const container = this.fallbackContainers.get(choiceId);
|
|
230
203
|
if (!container) return;
|
|
231
204
|
|
|
232
|
-
container.classList.remove(
|
|
233
|
-
container.removeAttribute(
|
|
234
|
-
container.removeAttribute(
|
|
205
|
+
container.classList.remove(StrikethroughStrategy.FALLBACK_CLASS);
|
|
206
|
+
container.removeAttribute(StrikethroughStrategy.ELIMINATED_ATTR);
|
|
207
|
+
container.removeAttribute(StrikethroughStrategy.ELIMINATED_ID_ATTR);
|
|
235
208
|
|
|
236
209
|
// Create fake range for aria removal
|
|
237
210
|
const range = document.createRange();
|
|
@@ -239,4 +212,12 @@ export class StrikethroughStrategy implements EliminationStrategy {
|
|
|
239
212
|
this.removeAriaAttributes(range);
|
|
240
213
|
this.fallbackContainers.delete(choiceId);
|
|
241
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
|
+
}
|
|
242
223
|
}
|