@pie-players/pie-tool-answer-eliminator 0.3.74 → 1.0.0-beta.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 +59 -78
- package/custom-elements.json +127 -0
- package/dist/tool-answer-eliminator.js +1087 -1109
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Test-taking strategy tool that allows students to eliminate answer choices they
|
|
|
7
7
|
- **Element-Level State**: Answer eliminations tracked per PIE element (not per item)
|
|
8
8
|
- **Visual Feedback**: Strikethrough styling for eliminated choices
|
|
9
9
|
- **Global Uniqueness**: Uses composite keys for state management across sections
|
|
10
|
-
- **
|
|
10
|
+
- **Separate from session data**: Eliminations never enter the PIE session; the coordinator's `saveToolState` hook persists them ([Persistence Integration](#persistence-integration))
|
|
11
11
|
- **ElementToolStateStore Integration**: Works with Assessment Toolkit's state management
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
@@ -49,93 +49,96 @@ The answer eliminator is automatically integrated when using the PIE Section Pla
|
|
|
49
49
|
|
|
50
50
|
The section player automatically:
|
|
51
51
|
- Renders answer eliminator buttons in question toolbars
|
|
52
|
-
-
|
|
53
|
-
- Passes ElementToolStateStore to the tool
|
|
52
|
+
- Passes each element's composite state key to the tool as `elementStateKeys`
|
|
54
53
|
- Manages state lifecycle
|
|
55
54
|
|
|
55
|
+
The tool reads its coordinator and the `ElementToolStateStore` from the toolkit
|
|
56
|
+
runtime context, so it has to sit inside `<pie-assessment-toolkit>`.
|
|
57
|
+
|
|
56
58
|
### Manual Integration (Advanced)
|
|
57
59
|
|
|
58
|
-
For custom
|
|
60
|
+
For a custom layout inside the toolkit. The tool detects choices under the item root its enclosing `<pie-item-scope>` provides, and with none it warns and injects nothing:
|
|
59
61
|
|
|
60
62
|
```html
|
|
61
63
|
<script type="module">
|
|
62
64
|
import '@pie-players/pie-tool-answer-eliminator';
|
|
63
|
-
import { ElementToolStateStore } from '@pie-players/pie-assessment-toolkit';
|
|
64
|
-
|
|
65
|
-
const store = new ElementToolStateStore();
|
|
66
|
-
const globalElementId = store.getGlobalElementId(
|
|
67
|
-
'my-assessment',
|
|
68
|
-
'section-1',
|
|
69
|
-
'question-1',
|
|
70
|
-
'mc1'
|
|
71
|
-
);
|
|
72
65
|
|
|
73
66
|
const tool = document.querySelector('pie-tool-answer-eliminator');
|
|
74
|
-
|
|
75
|
-
tool.
|
|
76
|
-
|
|
67
|
+
// Store key per PIE element in the question, by model id
|
|
68
|
+
tool.elementStateKeys = {
|
|
69
|
+
mc1: 'my-assessment:section-1:attempt-1:question-1:mc1',
|
|
70
|
+
mc2: 'my-assessment:section-1:attempt-1:question-1:mc2',
|
|
71
|
+
};
|
|
72
|
+
tool.visible = true; // or alwaysOn = true; the tool injects no buttons while neither is set
|
|
77
73
|
</script>
|
|
78
74
|
|
|
79
|
-
<pie-
|
|
75
|
+
<pie-assessment-toolkit>
|
|
76
|
+
<pie-item-scope item-id="question-1">
|
|
77
|
+
<!-- the item's rendered content -->
|
|
78
|
+
<pie-tool-answer-eliminator></pie-tool-answer-eliminator>
|
|
79
|
+
</pie-item-scope>
|
|
80
|
+
</pie-assessment-toolkit>
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
## Props/Attributes
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
`elementStateKeys` is a JS property only; the others also take the attribute shown.
|
|
85
86
|
|
|
86
|
-
| Property | Type |
|
|
87
|
-
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
87
|
+
| Property | Attribute | Type | Default | Description |
|
|
88
|
+
|----------|-----------|------|---------|-------------|
|
|
89
|
+
| `visible` | `visible` | `boolean` | `false` | Shows the elimination buttons; the toolbar toggle sets it |
|
|
90
|
+
| `alwaysOn` | `always-on` | `boolean` | `false` | Shows the buttons regardless of `visible`, for a profile-based accommodation |
|
|
91
|
+
| `strategy` | `strategy` | `'strikethrough' \| 'mask'` | `'strikethrough'` | Elimination styling; any other value uses `strikethrough` |
|
|
92
|
+
| `buttonAlignment` | `button-alignment` | `'left' \| 'right' \| 'inline'` | `'right'` | Button placement relative to the choice |
|
|
93
|
+
| `elementStateKeys` | | `Record<string, string>` | `{}` | Composite key `assessmentId:sectionId:attemptId:itemId:elementId` per PIE element, by model id, into the runtime context's element tool state store. A choice is tracked under its nearest ancestor with a listed id and persists under that key; choices outside a listed element are kept in memory only |
|
|
91
94
|
|
|
92
95
|
## Global Element ID Format
|
|
93
96
|
|
|
94
97
|
The tool uses globally unique composite keys for state management:
|
|
95
98
|
|
|
96
99
|
```
|
|
97
|
-
${assessmentId}:${sectionId}:${itemId}:${elementId}
|
|
100
|
+
${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}
|
|
98
101
|
```
|
|
99
102
|
|
|
100
103
|
**Example:**
|
|
101
104
|
```typescript
|
|
102
|
-
"demo-assessment:section-1:question-1:mc1"
|
|
103
|
-
"biology-exam:section-2:genetics-q1:
|
|
105
|
+
"demo-assessment:section-1:attempt-1:question-1:mc1"
|
|
106
|
+
"biology-exam:section-2:attempt-1:genetics-q1:ebsr1"
|
|
104
107
|
```
|
|
105
108
|
|
|
106
109
|
### Benefits of Composite Keys
|
|
107
110
|
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
111
|
+
- **Element-Level Granularity**: Each PIE element has independent eliminations
|
|
112
|
+
- **No Cross-Item Contamination**: Eliminations from question 1 don't appear on question 2
|
|
113
|
+
- **Cross-Section Persistence**: State persists when navigating between sections
|
|
114
|
+
- **Global Uniqueness**: No ID collisions across entire assessment
|
|
112
115
|
|
|
113
116
|
### Why Element-Level?
|
|
114
117
|
|
|
115
|
-
Items can contain **multiple interactive elements**
|
|
118
|
+
Items can contain **multiple interactive elements** whose choice ids repeat (two multiple-choice elements both have a choice `a`). Each element needs independent state:
|
|
116
119
|
|
|
117
120
|
```typescript
|
|
118
|
-
//
|
|
121
|
+
// Element-level state
|
|
119
122
|
{
|
|
120
|
-
"demo:section-1:question-1:
|
|
121
|
-
"answerEliminator": { "eliminatedChoices": ["
|
|
123
|
+
"demo:section-1:attempt-1:question-1:mc1": {
|
|
124
|
+
"answerEliminator": { "eliminatedChoices": ["a", "c"] }
|
|
122
125
|
},
|
|
123
|
-
"demo:section-1:question-1:
|
|
124
|
-
"answerEliminator": { "eliminatedChoices": ["
|
|
126
|
+
"demo:section-1:attempt-1:question-1:mc2": {
|
|
127
|
+
"answerEliminator": { "eliminatedChoices": ["a"] }
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
```
|
|
128
131
|
|
|
129
132
|
## State Management
|
|
130
133
|
|
|
131
|
-
###
|
|
134
|
+
### Tool state and session data
|
|
132
135
|
|
|
133
|
-
The answer eliminator stores
|
|
136
|
+
The answer eliminator stores eliminations in the toolkit's **ElementToolStateStore**, apart from the PIE session. They leave the browser only through the coordinator's `saveToolState` hook.
|
|
134
137
|
|
|
135
|
-
**Tool
|
|
138
|
+
**Tool state (not part of the session, never scored):**
|
|
136
139
|
```typescript
|
|
137
140
|
{
|
|
138
|
-
"demo:section-1:question-1:mc1": {
|
|
141
|
+
"demo:section-1:attempt-1:question-1:mc1": {
|
|
139
142
|
"answerEliminator": {
|
|
140
143
|
"eliminatedChoices": ["choice-b", "choice-d"]
|
|
141
144
|
}
|
|
@@ -160,26 +163,19 @@ The answer eliminator stores state in **ElementToolStateStore** (ephemeral, clie
|
|
|
160
163
|
To persist tool state across page refreshes:
|
|
161
164
|
|
|
162
165
|
```typescript
|
|
166
|
+
import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
|
|
163
167
|
import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
|
|
164
168
|
|
|
165
|
-
const
|
|
169
|
+
const storageKey = 'tool-state:my-assessment';
|
|
166
170
|
const coordinator = new ToolkitCoordinator({
|
|
167
171
|
assessmentId: 'my-assessment',
|
|
168
|
-
toolRegistry,
|
|
169
|
-
tools: { placement: { item: ['answerEliminator'] } }
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
coordinator.elementToolStateStore.setOnStateChange((state) => {
|
|
175
|
-
localStorage.setItem(storageKey, JSON.stringify(state));
|
|
172
|
+
toolRegistry: createPackagedToolRegistry(),
|
|
173
|
+
tools: { placement: { item: ['answerEliminator'] } },
|
|
174
|
+
hooks: {
|
|
175
|
+
loadToolState: () => JSON.parse(localStorage.getItem(storageKey) ?? 'null'),
|
|
176
|
+
saveToolState: (state) => localStorage.setItem(storageKey, JSON.stringify(state)),
|
|
177
|
+
},
|
|
176
178
|
});
|
|
177
|
-
|
|
178
|
-
// Load on mount
|
|
179
|
-
const saved = localStorage.getItem(storageKey);
|
|
180
|
-
if (saved) {
|
|
181
|
-
coordinator.elementToolStateStore.loadState(JSON.parse(saved));
|
|
182
|
-
}
|
|
183
179
|
```
|
|
184
180
|
|
|
185
181
|
## How It Works
|
|
@@ -194,14 +190,7 @@ The tool detects choices through an **adapter registry** (`AdapterRegistry`), no
|
|
|
194
190
|
| `EBSRAdapter` | `ebsr` | delegates to the multiple-choice adapter for each `ebsr-multiple-choice` part (Part A/B), prefixing choice IDs with the part |
|
|
195
191
|
| `InlineDropdownAdapter` | `inline-dropdown` | dropdown items with `role="option"` |
|
|
196
192
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
import { AdapterRegistry } from '@pie-players/pie-tool-answer-eliminator/adapters/adapter-registry';
|
|
201
|
-
|
|
202
|
-
const registry = new AdapterRegistry();
|
|
203
|
-
registry.registerAdapter(myCustomChoiceAdapter);
|
|
204
|
-
```
|
|
193
|
+
The tool builds its registry internally with these three adapters and has no injection point. The `./adapters/adapter-registry` export provides `AdapterRegistry` and `registerAdapter(...)`, and a registry a host constructs is not the one the tool reads, so supporting another element type is a change to this package. A `ChoiceAdapter` declares `elementType`, `priority`, `canHandle`, `findChoices`, `getChoiceId`, `getChoiceLabel`, `canEliminate`, `createChoiceRange` and `getButtonContainer`.
|
|
205
194
|
|
|
206
195
|
### 2. State Storage
|
|
207
196
|
|
|
@@ -220,22 +209,22 @@ Elimination styling is applied by a pluggable `EliminationStrategy` built on the
|
|
|
220
209
|
- `strikethrough` (default): a `::highlight(pie-answer-eliminated-<id>)` rule renders a line-through over the choice label.
|
|
221
210
|
- `mask`: a `::highlight(pie-answer-masked-<id>)` rule dims and blurs the choice.
|
|
222
211
|
|
|
223
|
-
Images need separate treatment: they are replaced elements, so neither `::highlight()` nor `text-decoration` paints on them and a picture choice would look untouched. The `strikethrough` strategy therefore wraps each `img` in the eliminated choice in a `span.pie-answer-eliminator-image-strike` (marked with `data-pie-answer-eliminator-image-strike`) and overlays a decorative, `pointer-events: none` SVG that draws an X corner to corner — upper-left to lower-right and lower-left to upper-right. Restoring the choice unwraps the image and returns the DOM to its original shape. Each diagonal is painted over a wider light casing line (`--pie-answer-eliminator-image-strike-casing-color
|
|
212
|
+
Images need separate treatment: they are replaced elements, so neither `::highlight()` nor `text-decoration` paints on them and a picture choice would look untouched. The `strikethrough` strategy therefore wraps each `img` in the eliminated choice in a `span.pie-answer-eliminator-image-strike` (marked with `data-pie-answer-eliminator-image-strike`) and overlays a decorative, `pointer-events: none` SVG that draws an X corner to corner — upper-left to lower-right and lower-left to upper-right. Restoring the choice unwraps the image and returns the DOM to its original shape. Each diagonal is painted over a wider light casing line (`--pie-answer-eliminator-image-strike-casing-color`, package-private with no compatibility guarantee) so it stays legible over dark artwork.
|
|
224
213
|
|
|
225
214
|
MathJax-rendered math needs its own mark for the same reason. MathJax's CHTML output draws each glyph as an `mjx-c` element with empty `textContent` (the character comes from `::before` generated content, which belongs to no Range), and its SVG output has no text at all — so the highlight had nothing to decorate and a math-only choice looked untouched. For each `mjx-container` in the eliminated choice, the `strikethrough` strategy marks the inner `mjx-math` box with `pie-answer-eliminator-math-strike` plus one of two modifiers, and the theme paints it:
|
|
226
215
|
|
|
227
216
|
| expression | mark | `data-pie-answer-eliminator-math-strike` |
|
|
228
217
|
| --- | --- | --- |
|
|
229
|
-
| single row of symbols |
|
|
218
|
+
| single row of symbols | centered line-through, as the prose gets | `line` |
|
|
230
219
|
| contains a fraction bar or table rule | diagonals, as an eliminated image gets | `cross` |
|
|
231
220
|
|
|
232
|
-
A
|
|
221
|
+
A centered line on a fraction lands on the math axis — exactly where the fraction bar already sits — so it reads as a recolored bar rather than an elimination. The split is structural (`mjx-mfrac`, `mjx-mtable`), not height-based: an inline `a/b` is only 1.16× its font size, indistinguishable in height from a radical or a parenthesised row, yet it is precisely the colliding case. Radicals and stacked limits keep the line, since their bars sit at the top or the strike simply crosses the base.
|
|
233
222
|
|
|
234
223
|
The inner `mjx-math` box is the paint target, not the container: for inline math `mjx-container` is `display: inline`, so its rect is the surrounding line box while the expression itself overflows it — a fraction sticks out several pixels above and below.
|
|
235
224
|
|
|
236
225
|
Only MathJax containers are marked: natively rendered MathML keeps real text in `mi`/`mn`/`mo`, so the highlight already strikes every token there.
|
|
237
226
|
|
|
238
|
-
All three treatments — the text line-through, the diagonals over an image, the line over math — are drawn in one
|
|
227
|
+
All three treatments — the text line-through, the diagonals over an image, the line over math — are drawn in one color, `--pie-answer-eliminator-strike-color` (a registered `component-public` token, defaulting to `--pie-incorrect`), so a choice mixing prose, pictures, and math reads as a single strike and can be restyled from one place.
|
|
239
228
|
|
|
240
229
|
For browsers without the Highlight API, each strategy falls back to a class on the choice container. Either way the eliminated choice also receives ARIA hooks (`data-pie-answer-eliminated`, plus `aria-disabled`/`aria-hidden` and an offscreen "(eliminated)" announcement) for assistive technology.
|
|
241
230
|
|
|
@@ -249,7 +238,7 @@ The ElementToolStateStore provides cleanup methods:
|
|
|
249
238
|
|
|
250
239
|
```typescript
|
|
251
240
|
// Clear state for a specific element
|
|
252
|
-
store.clearElement('demo:section-1:question-1:mc1');
|
|
241
|
+
store.clearElement('demo:section-1:attempt-1:question-1:mc1');
|
|
253
242
|
|
|
254
243
|
// Clear all answer eliminator state across all elements
|
|
255
244
|
store.clearTool('answerEliminator');
|
|
@@ -273,24 +262,16 @@ interface AnswerEliminatorState {
|
|
|
273
262
|
}
|
|
274
263
|
```
|
|
275
264
|
|
|
276
|
-
## Browser Support
|
|
277
|
-
|
|
278
|
-
- Chrome/Edge 90+
|
|
279
|
-
- Firefox 88+
|
|
280
|
-
- Safari 14+
|
|
281
|
-
|
|
282
|
-
Requires ES2020+ support (native ES modules, optional chaining, nullish coalescing).
|
|
283
|
-
|
|
284
265
|
## Examples
|
|
285
266
|
|
|
286
267
|
See the [section-demos](../../apps/section-demos/) for complete examples:
|
|
287
268
|
|
|
288
269
|
- **Three Questions Demo**: Element-level answer eliminator with state persistence
|
|
289
|
-
- **
|
|
270
|
+
- **Two Passages Demo**: Multi-section assessment with cross-section state
|
|
290
271
|
|
|
291
272
|
## Related Documentation
|
|
292
273
|
|
|
293
|
-
- [
|
|
274
|
+
- [Assessment toolkit architecture](../../docs/architecture/architecture.md#assessment-toolkit): the coordinator and element-level tool state
|
|
294
275
|
- [Assessment Toolkit README](../assessment-toolkit/README.md) - Toolkit overview
|
|
295
276
|
- [Section Player README](../section-player/README.md) - Integration guide
|
|
296
277
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "2.1.0",
|
|
3
|
+
"modules": [
|
|
4
|
+
{
|
|
5
|
+
"kind": "javascript-module",
|
|
6
|
+
"path": "dist/tool-answer-eliminator.js",
|
|
7
|
+
"declarations": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "class",
|
|
10
|
+
"description": "Process-of-elimination tool for the item in its enclosing `<pie-item-scope>`: it adds a toggle beside each multiple-choice, EBSR and inline-dropdown choice that strikes the choice through or masks it, per `strategy`. It renders nothing of its own and is active while `visible` or `always-on` is set. Eliminations persist in the runtime context's element tool state store, under the key `elementStateKeys` gives each PIE element.",
|
|
11
|
+
"name": "ToolAnswerEliminator",
|
|
12
|
+
"tagName": "pie-tool-answer-eliminator",
|
|
13
|
+
"customElement": true,
|
|
14
|
+
"members": [
|
|
15
|
+
{
|
|
16
|
+
"kind": "field",
|
|
17
|
+
"name": "visible",
|
|
18
|
+
"type": {
|
|
19
|
+
"text": "boolean"
|
|
20
|
+
},
|
|
21
|
+
"default": "false",
|
|
22
|
+
"attribute": "visible"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"kind": "field",
|
|
26
|
+
"name": "strategy",
|
|
27
|
+
"type": {
|
|
28
|
+
"text": "'strikethrough' | 'mask'"
|
|
29
|
+
},
|
|
30
|
+
"default": "'strikethrough'",
|
|
31
|
+
"attribute": "strategy"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"kind": "field",
|
|
35
|
+
"name": "alwaysOn",
|
|
36
|
+
"type": {
|
|
37
|
+
"text": "boolean"
|
|
38
|
+
},
|
|
39
|
+
"default": "false",
|
|
40
|
+
"attribute": "always-on"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"kind": "field",
|
|
44
|
+
"name": "buttonAlignment",
|
|
45
|
+
"type": {
|
|
46
|
+
"text": "'left' | 'right' | 'inline'"
|
|
47
|
+
},
|
|
48
|
+
"default": "'right'",
|
|
49
|
+
"attribute": "button-alignment"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"kind": "field",
|
|
53
|
+
"name": "elementStateKeys",
|
|
54
|
+
"type": {
|
|
55
|
+
"text": "ElementStateKeys"
|
|
56
|
+
},
|
|
57
|
+
"default": "{}",
|
|
58
|
+
"description": "Store key per PIE element, by model id (JS property only)",
|
|
59
|
+
"attribute": "elementstatekeys"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"attributes": [
|
|
63
|
+
{
|
|
64
|
+
"name": "visible",
|
|
65
|
+
"type": {
|
|
66
|
+
"text": "boolean"
|
|
67
|
+
},
|
|
68
|
+
"default": "false",
|
|
69
|
+
"fieldName": "visible"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "strategy",
|
|
73
|
+
"type": {
|
|
74
|
+
"text": "string"
|
|
75
|
+
},
|
|
76
|
+
"default": "'strikethrough'",
|
|
77
|
+
"fieldName": "strategy"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "always-on",
|
|
81
|
+
"type": {
|
|
82
|
+
"text": "boolean"
|
|
83
|
+
},
|
|
84
|
+
"default": "false",
|
|
85
|
+
"fieldName": "alwaysOn"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "button-alignment",
|
|
89
|
+
"type": {
|
|
90
|
+
"text": "string"
|
|
91
|
+
},
|
|
92
|
+
"default": "'right'",
|
|
93
|
+
"fieldName": "buttonAlignment"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "elementstatekeys",
|
|
97
|
+
"type": {
|
|
98
|
+
"text": "ElementStateKeys"
|
|
99
|
+
},
|
|
100
|
+
"default": "{}",
|
|
101
|
+
"description": "Store key per PIE element, by model id (JS property only)",
|
|
102
|
+
"fieldName": "elementStateKeys"
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"cssProperties": [
|
|
106
|
+
{
|
|
107
|
+
"name": "--pie-answer-eliminator-strike-color",
|
|
108
|
+
"description": "Single colour for every part of an elimination: the text line-through, the diagonals over an image, and the line or diagonals over rendered math, so one choice reads as one treatment. Must fall back through canonical --pie-incorrect so a host that themes only the canonical feedback tokens still gets a coherent strike."
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"x-pie": {
|
|
112
|
+
"shadow": "open"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"exports": [
|
|
117
|
+
{
|
|
118
|
+
"kind": "custom-element-definition",
|
|
119
|
+
"name": "pie-tool-answer-eliminator",
|
|
120
|
+
"declaration": {
|
|
121
|
+
"name": "ToolAnswerEliminator"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|