@pie-players/pie-tool-answer-eliminator 1.0.0-beta.0 → 1.0.0-beta.2

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": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
4
4
  "type": "module",
5
5
  "description": "Answer eliminator tool for PIE assessment player - supports process-of-elimination test-taking strategy",
6
6
  "repository": {
@@ -25,10 +25,6 @@
25
25
  ".": {
26
26
  "types": "./dist/index.d.ts",
27
27
  "import": "./dist/tool-answer-eliminator.js"
28
- },
29
- "./adapters/adapter-registry": {
30
- "types": "./dist/adapters/adapter-registry.d.ts",
31
- "import": "./dist/adapters/adapter-registry.js"
32
28
  }
33
29
  },
34
30
  "customElements": "custom-elements.json",
@@ -41,12 +37,12 @@
41
37
  "unpkg": "./dist/tool-answer-eliminator.js",
42
38
  "jsdelivr": "./dist/tool-answer-eliminator.js",
43
39
  "dependencies": {
44
- "@pie-players/pie-assessment-toolkit": "1.0.0-beta.0",
45
- "@pie-players/pie-players-shared": "1.0.0-beta.0"
40
+ "@pie-players/pie-assessment-toolkit": "1.0.0-beta.2",
41
+ "@pie-players/pie-players-shared": "1.0.0-beta.2"
46
42
  },
47
43
  "types": "./dist/index.d.ts",
48
44
  "scripts": {
49
- "build": "vite build && tsc -p tsconfig.adapters.json",
45
+ "build": "vite build",
50
46
  "dev": "vite build --watch",
51
47
  "test": "bun test tests",
52
48
  "typecheck": "tsc --noEmit",
@@ -57,7 +53,7 @@
57
53
  "@biomejs/biome": "^2.5.15",
58
54
  "@happy-dom/global-registrator": "^20.14.5",
59
55
  "@sveltejs/vite-plugin-svelte": "^7.3.1",
60
- "svelte": "^5.57.1",
56
+ "svelte": "^5.57.2",
61
57
  "svelte-check": "^4.7.6",
62
58
  "typescript": "^5.9.3",
63
59
  "vite": "^8.3.3",
@@ -1,25 +0,0 @@
1
- import { ChoiceAdapter } from './choice-adapter.js';
2
- export interface ChoiceWithAdapter {
3
- choice: HTMLElement;
4
- adapter: ChoiceAdapter;
5
- }
6
- /**
7
- * Registry for choice adapters
8
- * Automatically detects which adapter to use for different PIE elements
9
- */
10
- export declare class AdapterRegistry {
11
- private adapters;
12
- constructor();
13
- /**
14
- * Find the appropriate adapter for an element
15
- */
16
- findAdapter(element: HTMLElement): ChoiceAdapter | null;
17
- /**
18
- * Find all choices with their adapters within a root element
19
- */
20
- findAllChoicesWithAdapters(root: HTMLElement): ChoiceWithAdapter[];
21
- /**
22
- * Register a custom adapter
23
- */
24
- registerAdapter(adapter: ChoiceAdapter): void;
25
- }
@@ -1,50 +0,0 @@
1
- import { EBSRAdapter } from "./ebsr-adapter.js";
2
- import { InlineDropdownAdapter } from "./inline-dropdown-adapter.js";
3
- import { MultipleChoiceAdapter } from "./multiple-choice-adapter.js";
4
- /**
5
- * Registry for choice adapters
6
- * Automatically detects which adapter to use for different PIE elements
7
- */
8
- export class AdapterRegistry {
9
- adapters;
10
- constructor() {
11
- // Register adapters in priority order (higher priority first)
12
- this.adapters = [
13
- new MultipleChoiceAdapter(),
14
- new EBSRAdapter(),
15
- new InlineDropdownAdapter(),
16
- ].sort((a, b) => b.priority - a.priority);
17
- }
18
- /**
19
- * Find the appropriate adapter for an element
20
- */
21
- findAdapter(element) {
22
- return this.adapters.find((adapter) => adapter.canHandle(element)) || null;
23
- }
24
- /**
25
- * Find all choices with their adapters within a root element
26
- */
27
- findAllChoicesWithAdapters(root) {
28
- const results = [];
29
- const processedChoices = new Set();
30
- // Try each adapter
31
- for (const adapter of this.adapters) {
32
- const choices = adapter.findChoices(root);
33
- for (const choice of choices) {
34
- // Avoid duplicates (in case adapters overlap)
35
- if (processedChoices.has(choice))
36
- continue;
37
- processedChoices.add(choice);
38
- results.push({ choice, adapter });
39
- }
40
- }
41
- return results;
42
- }
43
- /**
44
- * Register a custom adapter
45
- */
46
- registerAdapter(adapter) {
47
- this.adapters.push(adapter);
48
- this.adapters.sort((a, b) => b.priority - a.priority);
49
- }
50
- }
@@ -1,55 +0,0 @@
1
- /**
2
- * Adapter interface for detecting and working with choice elements
3
- * from different PIE element types.
4
- *
5
- * Adapters enable the answer eliminator to work generically with
6
- * multiple-choice, EBSR, inline-dropdown, and future choice elements.
7
- */
8
- export interface ChoiceAdapter {
9
- /** Element type this adapter handles */
10
- readonly elementType: string;
11
- /** Priority (higher = checked first) */
12
- readonly priority: number;
13
- /**
14
- * Check if this adapter can handle the given element
15
- */
16
- canHandle(element: HTMLElement): boolean;
17
- /**
18
- * Find all choice elements within the root
19
- */
20
- findChoices(root: HTMLElement): HTMLElement[];
21
- /**
22
- * Create a Range covering the choice content (for CSS Highlight API)
23
- */
24
- createChoiceRange(choice: HTMLElement): Range | null;
25
- /**
26
- * Get unique identifier for this choice (for persistence)
27
- */
28
- getChoiceId(choice: HTMLElement): string;
29
- /**
30
- * Get human-readable label (for screen readers)
31
- */
32
- getChoiceLabel(choice: HTMLElement): string;
33
- /**
34
- * Check if the choice is currently selected by the student
35
- * (radio/checkbox checked). Optional: adapters that support hiding the
36
- * eliminate control on a selected choice implement this.
37
- */
38
- isSelected?(choice: HTMLElement): boolean;
39
- /**
40
- * Check if choice can be eliminated
41
- * (not already selected, not in evaluate mode, etc.)
42
- */
43
- canEliminate(choice: HTMLElement): boolean;
44
- /**
45
- * Enable or disable the student's ability to select this choice.
46
- * A struck-through choice is made non-selectable (grayed out) so a
47
- * student cannot pick an answer they have eliminated. Optional: only
48
- * adapters backed by a real input (radio/checkbox) implement this.
49
- */
50
- setSelectable?(choice: HTMLElement, selectable: boolean): void;
51
- /**
52
- * Get the container element to attach elimination button
53
- */
54
- getButtonContainer(choice: HTMLElement): HTMLElement | null;
55
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,21 +0,0 @@
1
- import { ChoiceAdapter } from './choice-adapter.js';
2
- /**
3
- * Adapter for EBSR (Evidence-Based Selected Response) elements
4
- *
5
- * EBSR contains two multiple-choice questions (Part A and Part B),
6
- * so we delegate to MultipleChoiceAdapter and prefix IDs with part identifier
7
- */
8
- export declare class EBSRAdapter implements ChoiceAdapter {
9
- readonly elementType = "ebsr";
10
- readonly priority = 110;
11
- private mcAdapter;
12
- canHandle(element: HTMLElement): boolean;
13
- findChoices(root: HTMLElement): HTMLElement[];
14
- createChoiceRange(choice: HTMLElement): Range | null;
15
- getChoiceId(choice: HTMLElement): string;
16
- getChoiceLabel(choice: HTMLElement): string;
17
- isSelected(choice: HTMLElement): boolean;
18
- canEliminate(choice: HTMLElement): boolean;
19
- setSelectable(choice: HTMLElement, selectable: boolean): void;
20
- getButtonContainer(choice: HTMLElement): HTMLElement | null;
21
- }
@@ -1,69 +0,0 @@
1
- import { parseVersionedTagName } from "@pie-players/pie-players-shared/pie/tag-names";
2
- import { MultipleChoiceAdapter } from "./multiple-choice-adapter.js";
3
- const EBSR_MULTIPLE_CHOICE_TAG = "ebsr-multiple-choice";
4
- const isEbsrMultipleChoiceElement = (element) => element instanceof HTMLElement &&
5
- parseVersionedTagName(element.tagName.toLowerCase()).baseName ===
6
- EBSR_MULTIPLE_CHOICE_TAG;
7
- const findEbsrMultipleChoicePart = (root, id) => Array.from(root.querySelectorAll(`[id="${id}"]`)).find(isEbsrMultipleChoiceElement) ?? null;
8
- const findClosestEbsrMultipleChoicePart = (choice) => {
9
- let element = choice.parentElement;
10
- while (element !== null) {
11
- if (isEbsrMultipleChoiceElement(element)) {
12
- return element;
13
- }
14
- element = element.parentElement;
15
- }
16
- return null;
17
- };
18
- /**
19
- * Adapter for EBSR (Evidence-Based Selected Response) elements
20
- *
21
- * EBSR contains two multiple-choice questions (Part A and Part B),
22
- * so we delegate to MultipleChoiceAdapter and prefix IDs with part identifier
23
- */
24
- export class EBSRAdapter {
25
- elementType = "ebsr";
26
- priority = 110;
27
- mcAdapter = new MultipleChoiceAdapter();
28
- canHandle(element) {
29
- return (element.tagName.toLowerCase() === "ebsr" ||
30
- Array.from(element.querySelectorAll("*")).some(isEbsrMultipleChoiceElement));
31
- }
32
- findChoices(root) {
33
- // EBSR contains private multiple-choice elements, version-scoped in modern bundles.
34
- // Find choices in both Part A and Part B
35
- const partA = findEbsrMultipleChoicePart(root, "a");
36
- const partB = findEbsrMultipleChoicePart(root, "b");
37
- const choices = [];
38
- if (partA)
39
- choices.push(...this.mcAdapter.findChoices(partA));
40
- if (partB)
41
- choices.push(...this.mcAdapter.findChoices(partB));
42
- return choices;
43
- }
44
- // Delegate all other methods to MultipleChoiceAdapter
45
- createChoiceRange(choice) {
46
- return this.mcAdapter.createChoiceRange(choice);
47
- }
48
- getChoiceId(choice) {
49
- // Prefix with part ID (a or b)
50
- const part = findClosestEbsrMultipleChoicePart(choice)?.id || "unknown";
51
- const choiceId = this.mcAdapter.getChoiceId(choice);
52
- return `${part}-${choiceId}`;
53
- }
54
- getChoiceLabel(choice) {
55
- return this.mcAdapter.getChoiceLabel(choice);
56
- }
57
- isSelected(choice) {
58
- return this.mcAdapter.isSelected(choice);
59
- }
60
- canEliminate(choice) {
61
- return this.mcAdapter.canEliminate(choice);
62
- }
63
- setSelectable(choice, selectable) {
64
- this.mcAdapter.setSelectable(choice, selectable);
65
- }
66
- getButtonContainer(choice) {
67
- return this.mcAdapter.getButtonContainer(choice);
68
- }
69
- }
@@ -1,17 +0,0 @@
1
- import { ChoiceAdapter } from './choice-adapter.js';
2
- /**
3
- * Adapter for PIE inline-dropdown elements
4
- *
5
- * Works with dropdown menu items (role="option")
6
- */
7
- export declare class InlineDropdownAdapter implements ChoiceAdapter {
8
- readonly elementType = "inline-dropdown";
9
- readonly priority = 90;
10
- canHandle(element: HTMLElement): boolean;
11
- findChoices(root: HTMLElement): HTMLElement[];
12
- createChoiceRange(choice: HTMLElement): Range | null;
13
- getChoiceId(choice: HTMLElement): string;
14
- getChoiceLabel(choice: HTMLElement): string;
15
- canEliminate(choice: HTMLElement): boolean;
16
- getButtonContainer(choice: HTMLElement): HTMLElement | null;
17
- }
@@ -1,35 +0,0 @@
1
- /**
2
- * Adapter for PIE inline-dropdown elements
3
- *
4
- * Works with dropdown menu items (role="option")
5
- */
6
- export class InlineDropdownAdapter {
7
- elementType = "inline-dropdown";
8
- priority = 90;
9
- canHandle(element) {
10
- return (element.tagName.toLowerCase() === "inline-dropdown" ||
11
- element.querySelector('[role="combobox"]') !== null);
12
- }
13
- findChoices(root) {
14
- // Find dropdown menu items
15
- return Array.from(root.querySelectorAll('[role="option"]'));
16
- }
17
- createChoiceRange(choice) {
18
- const range = document.createRange();
19
- range.selectNodeContents(choice);
20
- return range;
21
- }
22
- getChoiceId(choice) {
23
- return choice.id || choice.getAttribute("data-value") || "";
24
- }
25
- getChoiceLabel(choice) {
26
- return choice.textContent?.trim() || "Unlabeled option";
27
- }
28
- canEliminate(choice) {
29
- // Can't eliminate if already selected
30
- return choice.getAttribute("aria-selected") !== "true";
31
- }
32
- getButtonContainer(choice) {
33
- return choice;
34
- }
35
- }
@@ -1,31 +0,0 @@
1
- import { ChoiceAdapter } from './choice-adapter.js';
2
- /**
3
- * Adapter for PIE multiple-choice elements
4
- *
5
- * Works with both single-select (radio) and multiple-select (checkbox) modes
6
- * Detects PIE's corespring-checkbox and corespring-radio-button classes
7
- */
8
- export declare class MultipleChoiceAdapter implements ChoiceAdapter {
9
- readonly elementType = "multiple-choice";
10
- readonly priority = 100;
11
- private static readonly CHOICE_HOOK_ATTR;
12
- private static readonly LABEL_HOOK_ATTR;
13
- private static readonly ROOT_HOOK_ATTR;
14
- private static readonly FEEDBACK_HOOK_ATTR;
15
- private static readonly CHOICE_SELECTOR;
16
- private static readonly MUI_BUTTON_SELECTOR;
17
- private static readonly MUI_DISABLED_CLASS;
18
- canHandle(element: HTMLElement): boolean;
19
- findChoices(root: HTMLElement): HTMLElement[];
20
- createChoiceRange(choice: HTMLElement): Range | null;
21
- getChoiceId(choice: HTMLElement): string;
22
- getChoiceLabel(choice: HTMLElement): string;
23
- isSelected(choice: HTMLElement): boolean;
24
- canEliminate(choice: HTMLElement): boolean;
25
- setSelectable(choice: HTMLElement, selectable: boolean): void;
26
- getButtonContainer(choice: HTMLElement): HTMLElement | null;
27
- private generateFallbackId;
28
- private annotateChoice;
29
- private resolveLabelElement;
30
- private annotateFeedbackTicks;
31
- }
@@ -1,130 +0,0 @@
1
- /**
2
- * Adapter for PIE multiple-choice elements
3
- *
4
- * Works with both single-select (radio) and multiple-select (checkbox) modes
5
- * Detects PIE's corespring-checkbox and corespring-radio-button classes
6
- */
7
- export class MultipleChoiceAdapter {
8
- elementType = "multiple-choice";
9
- priority = 100;
10
- static CHOICE_HOOK_ATTR = "data-pie-answer-eliminator-choice";
11
- static LABEL_HOOK_ATTR = "data-pie-answer-eliminator-label";
12
- static ROOT_HOOK_ATTR = "data-pie-answer-eliminator-root";
13
- static FEEDBACK_HOOK_ATTR = "data-pie-answer-eliminator-feedback-tick";
14
- static CHOICE_SELECTOR = `[${MultipleChoiceAdapter.CHOICE_HOOK_ATTR}="true"], .corespring-checkbox, .corespring-radio-button`;
15
- // Material-UI radio/checkbox controls gray out via the `Mui-disabled`
16
- // class on the button root; toggling the native `disabled` alone doesn't
17
- // restyle them.
18
- static MUI_BUTTON_SELECTOR = ".MuiButtonBase-root, .MuiRadio-root, .MuiCheckbox-root";
19
- static MUI_DISABLED_CLASS = "Mui-disabled";
20
- canHandle(element) {
21
- return (element.tagName.toLowerCase() === "multiple-choice" ||
22
- element.classList.contains("multiple-choice"));
23
- }
24
- findChoices(root) {
25
- root.setAttribute(MultipleChoiceAdapter.ROOT_HOOK_ATTR, "true");
26
- const choices = Array.from(root.querySelectorAll(MultipleChoiceAdapter.CHOICE_SELECTOR));
27
- for (const choice of choices) {
28
- this.annotateChoice(choice);
29
- }
30
- this.annotateFeedbackTicks(root);
31
- return choices;
32
- }
33
- createChoiceRange(choice) {
34
- // Create range covering the label content
35
- // Try multiple possible selectors for the label
36
- const labelElement = this.resolveLabelElement(choice);
37
- if (!labelElement) {
38
- return null;
39
- }
40
- const range = document.createRange();
41
- range.selectNodeContents(labelElement);
42
- return range;
43
- }
44
- getChoiceId(choice) {
45
- // Get value from input element
46
- const input = choice.querySelector('input[type="checkbox"], input[type="radio"]');
47
- return (input?.getAttribute("value") ||
48
- input?.id ||
49
- this.generateFallbackId(choice));
50
- }
51
- getChoiceLabel(choice) {
52
- const label = this.resolveLabelElement(choice);
53
- return label?.textContent?.trim() || "Unlabeled choice";
54
- }
55
- isSelected(choice) {
56
- const input = choice.querySelector('input[type="checkbox"], input[type="radio"]');
57
- if (!input)
58
- return false;
59
- // Prefer the live `checked` property (works for radio and checkbox);
60
- // fall back to the aria-checked attribute for custom/ARIA widgets.
61
- return input.checked || input.getAttribute("aria-checked") === "true";
62
- }
63
- canEliminate(choice) {
64
- const input = choice.querySelector('input[type="checkbox"], input[type="radio"]');
65
- if (!input)
66
- return false;
67
- // Can't eliminate if:
68
- // 1. Already selected (checked)
69
- if (this.isSelected(choice))
70
- return false;
71
- // 2. Disabled
72
- if (input.disabled)
73
- return false;
74
- // 3. In evaluate/view mode (has feedback tick)
75
- const root = choice.closest(`[${MultipleChoiceAdapter.ROOT_HOOK_ATTR}="true"]`) ||
76
- choice.closest("multiple-choice");
77
- if (root?.querySelector(`[${MultipleChoiceAdapter.FEEDBACK_HOOK_ATTR}="true"]`))
78
- return false;
79
- return true;
80
- }
81
- setSelectable(choice, selectable) {
82
- const input = choice.querySelector('input[type="checkbox"], input[type="radio"]');
83
- if (!input)
84
- return;
85
- // Native `disabled` makes the control non-selectable and grays it out;
86
- // aria-disabled keeps the state exposed to assistive tech.
87
- input.disabled = !selectable;
88
- if (selectable) {
89
- input.removeAttribute("aria-disabled");
90
- }
91
- else {
92
- input.setAttribute("aria-disabled", "true");
93
- }
94
- // If the input lives inside a Material-UI control, mirror MUI's own
95
- // disabled styling by toggling `Mui-disabled` on the button root.
96
- const muiButton = input.closest(MultipleChoiceAdapter.MUI_BUTTON_SELECTOR);
97
- if (muiButton) {
98
- muiButton.classList.toggle(MultipleChoiceAdapter.MUI_DISABLED_CLASS, !selectable);
99
- }
100
- }
101
- getButtonContainer(choice) {
102
- // Return the choice-input container
103
- return choice;
104
- }
105
- generateFallbackId(choice) {
106
- // Generate stable ID based on choice position
107
- const parent = choice.closest(`[${MultipleChoiceAdapter.ROOT_HOOK_ATTR}="true"]`) ||
108
- choice.closest("multiple-choice");
109
- const choices = parent?.querySelectorAll(`[${MultipleChoiceAdapter.CHOICE_HOOK_ATTR}="true"]`) || [];
110
- const index = Array.from(choices).indexOf(choice);
111
- return `choice-${index}`;
112
- }
113
- annotateChoice(choice) {
114
- choice.setAttribute(MultipleChoiceAdapter.CHOICE_HOOK_ATTR, "true");
115
- const label = this.resolveLabelElement(choice);
116
- if (label) {
117
- label.setAttribute(MultipleChoiceAdapter.LABEL_HOOK_ATTR, "true");
118
- }
119
- }
120
- resolveLabelElement(choice) {
121
- return (choice.querySelector(`[${MultipleChoiceAdapter.LABEL_HOOK_ATTR}="true"]`) ||
122
- choice.querySelector("label") ||
123
- choice.querySelector("span"));
124
- }
125
- annotateFeedbackTicks(root) {
126
- for (const feedbackTick of root.querySelectorAll(".feedback-tick")) {
127
- feedbackTick.setAttribute(MultipleChoiceAdapter.FEEDBACK_HOOK_ATTR, "true");
128
- }
129
- }
130
- }