@pie-players/pie-tool-answer-eliminator 0.2.8 → 0.2.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"vite.config.d.ts","sourceRoot":"","sources":["../vite.config.ts"],"names":[],"mappings":";AAKA,wBAmCG"}
1
+ {"version":3,"file":"vite.config.d.ts","sourceRoot":"","sources":["../vite.config.ts"],"names":[],"mappings":";AAKA,wBAkCG"}
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@pie-players/pie-tool-answer-eliminator",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "description": "Answer eliminator tool for PIE assessment player - supports process-of-elimination test-taking strategy",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/pie-framework/pie-players.git"
8
+ "url": "git+https://github.com/pie-framework/pie-players.git",
9
+ "directory": "packages/tool-answer-eliminator"
9
10
  },
10
11
  "publishConfig": {
11
12
  "access": "public"
@@ -48,9 +49,9 @@
48
49
  "unpkg": "./dist/tool-answer-eliminator.js",
49
50
  "jsdelivr": "./dist/tool-answer-eliminator.js",
50
51
  "dependencies": {
51
- "@pie-players/pie-assessment-toolkit": "0.2.7",
52
- "@pie-players/pie-players-shared": "0.2.4",
53
- "@sveltejs/kit": "^2.52.0",
52
+ "@pie-players/pie-assessment-toolkit": "0.2.10",
53
+ "@pie-players/pie-context": "0.1.2",
54
+ "@pie-players/pie-players-shared": "0.2.6",
54
55
  "daisyui": "^5.5.18"
55
56
  },
56
57
  "types": "./dist/index.d.ts",
@@ -67,5 +68,13 @@
67
68
  "typescript": "^5.7.0",
68
69
  "vite": "^7.0.8",
69
70
  "vite-plugin-dts": "^4.5.3"
70
- }
71
+ },
72
+ "homepage": "https://github.com/pie-framework/pie-players/tree/master/packages/tool-answer-eliminator#readme",
73
+ "bugs": {
74
+ "url": "https://github.com/pie-framework/pie-players/issues"
75
+ },
76
+ "engines": {
77
+ "node": ">=18.0.0"
78
+ },
79
+ "sideEffects": true
71
80
  }
@@ -5,18 +5,27 @@ 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>();
11
20
  private ranges = new Map<string, Range>();
21
+ private fallbackContainers = new Map<string, HTMLElement>();
12
22
 
13
23
  initialize(): void {
14
- this.injectCSS();
24
+ // No-op: shared fallback styles are owned by @pie-players/pie-theme/components.css.
15
25
  }
16
26
 
17
27
  destroy(): void {
18
28
  this.clearAll();
19
- this.removeCSS();
20
29
  }
21
30
 
22
31
  apply(choiceId: string, range: Range): void {
@@ -29,7 +38,10 @@ export class MaskStrategy implements EliminationStrategy {
29
38
  this.injectHighlightCSS(choiceId);
30
39
 
31
40
  const highlight = new Highlight(range);
32
- CSS.highlights.set(`answer-masked-${choiceId}`, highlight);
41
+ CSS.highlights.set(
42
+ `${MaskStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`,
43
+ highlight,
44
+ );
33
45
 
34
46
  this.highlights.set(choiceId, highlight);
35
47
  this.ranges.set(choiceId, range);
@@ -43,7 +55,7 @@ export class MaskStrategy implements EliminationStrategy {
43
55
  return;
44
56
  }
45
57
 
46
- CSS.highlights.delete(`answer-masked-${choiceId}`);
58
+ CSS.highlights.delete(`${MaskStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`);
47
59
 
48
60
  // Remove CSS for this specific highlight
49
61
  this.removeHighlightCSS(choiceId);
@@ -55,6 +67,7 @@ export class MaskStrategy implements EliminationStrategy {
55
67
 
56
68
  this.highlights.delete(choiceId);
57
69
  this.ranges.delete(choiceId);
70
+ this.fallbackContainers.delete(choiceId);
58
71
  }
59
72
 
60
73
  isEliminated(choiceId: string): boolean {
@@ -65,6 +78,7 @@ export class MaskStrategy implements EliminationStrategy {
65
78
  for (const choiceId of this.highlights.keys()) {
66
79
  this.remove(choiceId);
67
80
  }
81
+ this.fallbackContainers.clear();
68
82
  }
69
83
 
70
84
  getEliminatedIds(): string[] {
@@ -75,33 +89,14 @@ export class MaskStrategy implements EliminationStrategy {
75
89
  return typeof CSS !== "undefined" && "highlights" in CSS;
76
90
  }
77
91
 
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
92
  private injectHighlightCSS(choiceId: string): void {
98
- const styleId = `answer-eliminator-mask-highlight-${choiceId}`;
93
+ const styleId = `${MaskStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`;
99
94
  if (document.getElementById(styleId)) return;
100
95
 
101
96
  const style = document.createElement("style");
102
97
  style.id = styleId;
103
98
  style.textContent = `
104
- ::highlight(answer-masked-${choiceId}) {
99
+ ::highlight(pie-answer-masked-${choiceId}) {
105
100
  opacity: 0.2;
106
101
  filter: blur(2px);
107
102
  }
@@ -111,19 +106,15 @@ export class MaskStrategy implements EliminationStrategy {
111
106
 
112
107
  private removeHighlightCSS(choiceId: string): void {
113
108
  document
114
- .getElementById(`answer-eliminator-mask-highlight-${choiceId}`)
109
+ .getElementById(`${MaskStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`)
115
110
  ?.remove();
116
111
  }
117
112
 
118
- private removeCSS(): void {
119
- document.getElementById("answer-eliminator-mask-styles")?.remove();
120
- }
121
-
122
113
  private addAriaAttributes(range: Range): void {
123
114
  const container = this.findChoiceContainer(range);
124
115
  if (!container) return;
125
116
 
126
- container.setAttribute("data-eliminated", "true");
117
+ container.setAttribute(MaskStrategy.ELIMINATED_ATTR, "true");
127
118
  container.setAttribute("aria-hidden", "true");
128
119
  }
129
120
 
@@ -131,7 +122,7 @@ export class MaskStrategy implements EliminationStrategy {
131
122
  const container = this.findChoiceContainer(range);
132
123
  if (!container) return;
133
124
 
134
- container.removeAttribute("data-eliminated");
125
+ container.removeAttribute(MaskStrategy.ELIMINATED_ATTR);
135
126
  container.removeAttribute("aria-hidden");
136
127
  }
137
128
 
@@ -144,7 +135,7 @@ export class MaskStrategy implements EliminationStrategy {
144
135
  }
145
136
 
146
137
  while (element && element !== document.body) {
147
- if (element.classList?.contains("choice-input")) {
138
+ if (element.getAttribute(MaskStrategy.CHOICE_HOOK_ATTR) === "true") {
148
139
  return element;
149
140
  }
150
141
  element = element.parentElement;
@@ -157,24 +148,24 @@ export class MaskStrategy implements EliminationStrategy {
157
148
  const container = this.findChoiceContainer(range);
158
149
  if (!container) return;
159
150
 
160
- container.classList.add("answer-masked-fallback");
161
- container.setAttribute("data-eliminated", "true");
162
- 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);
154
+ this.fallbackContainers.set(choiceId, container);
163
155
  this.addAriaAttributes(range);
164
156
  }
165
157
 
166
158
  private removeFallback(choiceId: string): void {
167
- const container = document.querySelector(
168
- `[data-eliminated-id="${choiceId}"]`,
169
- );
159
+ const container = this.fallbackContainers.get(choiceId);
170
160
  if (!container) return;
171
161
 
172
- container.classList.remove("answer-masked-fallback");
173
- container.removeAttribute("data-eliminated");
174
- 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);
175
165
 
176
166
  const range = document.createRange();
177
167
  range.selectNodeContents(container);
178
168
  this.removeAriaAttributes(range);
169
+ this.fallbackContainers.delete(choiceId);
179
170
  }
180
171
  }
@@ -8,25 +8,33 @@ 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>();
14
26
  private ranges = new Map<string, Range>();
27
+ private fallbackContainers = new Map<string, HTMLElement>();
15
28
 
16
29
  initialize(): void {
17
30
  // Check browser support
18
31
  if (!this.isSupported()) {
19
32
  console.warn("CSS Custom Highlight API not supported, using fallback");
20
- return;
21
33
  }
22
-
23
- // Inject CSS for highlight styling
24
- this.injectCSS();
25
34
  }
26
35
 
27
36
  destroy(): void {
28
37
  this.clearAll();
29
- this.removeCSS();
30
38
  }
31
39
 
32
40
  apply(choiceId: string, range: Range): void {
@@ -42,7 +50,10 @@ export class StrikethroughStrategy implements EliminationStrategy {
42
50
  const highlight = new Highlight(range);
43
51
 
44
52
  // Register in CSS.highlights with unique name
45
- CSS.highlights.set(`answer-eliminated-${choiceId}`, highlight);
53
+ CSS.highlights.set(
54
+ `${StrikethroughStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`,
55
+ highlight,
56
+ );
46
57
 
47
58
  // Track internally
48
59
  this.highlights.set(choiceId, highlight);
@@ -59,7 +70,9 @@ export class StrikethroughStrategy implements EliminationStrategy {
59
70
  }
60
71
 
61
72
  // Remove from CSS.highlights
62
- CSS.highlights.delete(`answer-eliminated-${choiceId}`);
73
+ CSS.highlights.delete(
74
+ `${StrikethroughStrategy.HIGHLIGHT_NAME_PREFIX}${choiceId}`,
75
+ );
63
76
 
64
77
  // Remove CSS for this specific highlight
65
78
  this.removeHighlightCSS(choiceId);
@@ -72,6 +85,7 @@ export class StrikethroughStrategy implements EliminationStrategy {
72
85
 
73
86
  this.highlights.delete(choiceId);
74
87
  this.ranges.delete(choiceId);
88
+ this.fallbackContainers.delete(choiceId);
75
89
  }
76
90
 
77
91
  isEliminated(choiceId: string): boolean {
@@ -83,6 +97,7 @@ export class StrikethroughStrategy implements EliminationStrategy {
83
97
  for (const choiceId of this.highlights.keys()) {
84
98
  this.remove(choiceId);
85
99
  }
100
+ this.fallbackContainers.clear();
86
101
  }
87
102
 
88
103
  getEliminatedIds(): string[] {
@@ -93,48 +108,14 @@ export class StrikethroughStrategy implements EliminationStrategy {
93
108
  return typeof CSS !== "undefined" && "highlights" in CSS;
94
109
  }
95
110
 
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
111
  private injectHighlightCSS(choiceId: string): void {
131
- const styleId = `answer-eliminator-highlight-${choiceId}`;
112
+ const styleId = `${StrikethroughStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`;
132
113
  if (document.getElementById(styleId)) return;
133
114
 
134
115
  const style = document.createElement("style");
135
116
  style.id = styleId;
136
117
  style.textContent = `
137
- ::highlight(answer-eliminated-${choiceId}) {
118
+ ::highlight(pie-answer-eliminated-${choiceId}) {
138
119
  text-decoration: line-through;
139
120
  text-decoration-thickness: 2px;
140
121
  text-decoration-color: var(--pie-incorrect, #ff9800);
@@ -146,30 +127,25 @@ export class StrikethroughStrategy implements EliminationStrategy {
146
127
 
147
128
  private removeHighlightCSS(choiceId: string): void {
148
129
  document
149
- .getElementById(`answer-eliminator-highlight-${choiceId}`)
130
+ .getElementById(
131
+ `${StrikethroughStrategy.HIGHLIGHT_STYLE_PREFIX}${choiceId}`,
132
+ )
150
133
  ?.remove();
151
134
  }
152
135
 
153
- private removeCSS(): void {
154
- const style = document.getElementById(
155
- "answer-eliminator-strikethrough-styles",
156
- );
157
- style?.remove();
158
- }
159
-
160
136
  private addAriaAttributes(range: Range): void {
161
137
  // Find the choice container element
162
138
  const container = this.findChoiceContainer(range);
163
139
  if (!container) return;
164
140
 
165
- container.setAttribute("data-eliminated", "true");
141
+ container.setAttribute(StrikethroughStrategy.ELIMINATED_ATTR, "true");
166
142
  container.setAttribute("aria-disabled", "true");
167
143
 
168
144
  // Add screen reader announcement
169
- const label = container.querySelector(".label");
170
- if (label && !label.querySelector(".sr-announcement")) {
145
+ const label = this.resolveLabelElement(container);
146
+ if (label && !label.querySelector(`.${StrikethroughStrategy.SR_CLASS}`)) {
171
147
  const announcement = document.createElement("span");
172
- announcement.className = "sr-announcement";
148
+ announcement.className = StrikethroughStrategy.SR_CLASS;
173
149
  announcement.textContent = " (eliminated)";
174
150
  label.appendChild(announcement);
175
151
  }
@@ -179,11 +155,13 @@ export class StrikethroughStrategy implements EliminationStrategy {
179
155
  const container = this.findChoiceContainer(range);
180
156
  if (!container) return;
181
157
 
182
- container.removeAttribute("data-eliminated");
158
+ container.removeAttribute(StrikethroughStrategy.ELIMINATED_ATTR);
183
159
  container.removeAttribute("aria-disabled");
184
160
 
185
161
  // Remove screen reader announcement
186
- const announcement = container.querySelector(".sr-announcement");
162
+ const announcement = container.querySelector(
163
+ `.${StrikethroughStrategy.SR_CLASS}`,
164
+ );
187
165
  announcement?.remove();
188
166
  }
189
167
 
@@ -198,9 +176,7 @@ export class StrikethroughStrategy implements EliminationStrategy {
198
176
 
199
177
  while (element && element !== document.body) {
200
178
  if (
201
- element.classList?.contains("choice-input") ||
202
- element.classList?.contains("corespring-checkbox") ||
203
- element.classList?.contains("corespring-radio-button")
179
+ element.getAttribute(StrikethroughStrategy.CHOICE_HOOK_ATTR) === "true"
204
180
  ) {
205
181
  return element;
206
182
  }
@@ -215,25 +191,33 @@ export class StrikethroughStrategy implements EliminationStrategy {
215
191
  const container = this.findChoiceContainer(range);
216
192
  if (!container) return;
217
193
 
218
- container.classList.add("answer-eliminated-fallback");
219
- container.setAttribute("data-eliminated", "true");
220
- 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);
197
+ this.fallbackContainers.set(choiceId, container);
221
198
  this.addAriaAttributes(range);
222
199
  }
223
200
 
224
201
  private removeFallback(choiceId: string): void {
225
- const container = document.querySelector(
226
- `[data-eliminated-id="${choiceId}"]`,
227
- );
202
+ const container = this.fallbackContainers.get(choiceId);
228
203
  if (!container) return;
229
204
 
230
- container.classList.remove("answer-eliminated-fallback");
231
- container.removeAttribute("data-eliminated");
232
- 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);
233
208
 
234
209
  // Create fake range for aria removal
235
210
  const range = document.createRange();
236
211
  range.selectNodeContents(container);
237
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
+ );
238
222
  }
239
223
  }