@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-tool-answer-eliminator",
3
- "version": "0.2.9",
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.9",
53
- "@pie-players/pie-context": "0.1.1",
54
- "@pie-players/pie-players-shared": "0.2.5",
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
- this.injectCSS();
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(`answer-masked-${choiceId}`, highlight);
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(`answer-masked-${choiceId}`);
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 = `answer-eliminator-mask-highlight-${choiceId}`;
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(`answer-eliminator-mask-highlight-${choiceId}`)
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("data-eliminated", "true");
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("data-eliminated");
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.classList?.contains("choice-input")) {
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("answer-masked-fallback");
164
- container.setAttribute("data-eliminated", "true");
165
- container.setAttribute("data-eliminated-id", choiceId);
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("answer-masked-fallback");
175
- container.removeAttribute("data-eliminated");
176
- container.removeAttribute("data-eliminated-id");
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(`answer-eliminated-${choiceId}`, highlight);
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(`answer-eliminated-${choiceId}`);
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 = `answer-eliminator-highlight-${choiceId}`;
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(`answer-eliminator-highlight-${choiceId}`)
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("data-eliminated", "true");
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 = container.querySelector(".label");
173
- if (label && !label.querySelector(".sr-announcement")) {
145
+ const label = this.resolveLabelElement(container);
146
+ if (label && !label.querySelector(`.${StrikethroughStrategy.SR_CLASS}`)) {
174
147
  const announcement = document.createElement("span");
175
- announcement.className = "sr-announcement";
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("data-eliminated");
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(".sr-announcement");
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.classList?.contains("choice-input") ||
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("answer-eliminated-fallback");
222
- container.setAttribute("data-eliminated", "true");
223
- container.setAttribute("data-eliminated-id", choiceId);
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("answer-eliminated-fallback");
233
- container.removeAttribute("data-eliminated");
234
- container.removeAttribute("data-eliminated-id");
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
  }