@pie-players/pie-tool-answer-eliminator 0.3.59 → 0.3.60
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/dist/adapters/choice-adapter.d.ts +13 -0
- package/dist/adapters/ebsr-adapter.d.ts +2 -0
- package/dist/adapters/ebsr-adapter.js +6 -0
- package/dist/adapters/multiple-choice-adapter.d.ts +4 -0
- package/dist/adapters/multiple-choice-adapter.js +34 -1
- package/dist/answer-eliminator-core.d.ts +62 -3
- package/dist/tool-answer-eliminator.js +1420 -1355
- package/package.json +4 -4
|
@@ -30,11 +30,24 @@ export interface ChoiceAdapter {
|
|
|
30
30
|
* Get human-readable label (for screen readers)
|
|
31
31
|
*/
|
|
32
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;
|
|
33
39
|
/**
|
|
34
40
|
* Check if choice can be eliminated
|
|
35
41
|
* (not already selected, not in evaluate mode, etc.)
|
|
36
42
|
*/
|
|
37
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;
|
|
38
51
|
/**
|
|
39
52
|
* Get the container element to attach elimination button
|
|
40
53
|
*/
|
|
@@ -14,6 +14,8 @@ export declare class EBSRAdapter implements ChoiceAdapter {
|
|
|
14
14
|
createChoiceRange(choice: HTMLElement): Range | null;
|
|
15
15
|
getChoiceId(choice: HTMLElement): string;
|
|
16
16
|
getChoiceLabel(choice: HTMLElement): string;
|
|
17
|
+
isSelected(choice: HTMLElement): boolean;
|
|
17
18
|
canEliminate(choice: HTMLElement): boolean;
|
|
19
|
+
setSelectable(choice: HTMLElement, selectable: boolean): void;
|
|
18
20
|
getButtonContainer(choice: HTMLElement): HTMLElement | null;
|
|
19
21
|
}
|
|
@@ -59,9 +59,15 @@ export class EBSRAdapter {
|
|
|
59
59
|
getChoiceLabel(choice) {
|
|
60
60
|
return this.mcAdapter.getChoiceLabel(choice);
|
|
61
61
|
}
|
|
62
|
+
isSelected(choice) {
|
|
63
|
+
return this.mcAdapter.isSelected(choice);
|
|
64
|
+
}
|
|
62
65
|
canEliminate(choice) {
|
|
63
66
|
return this.mcAdapter.canEliminate(choice);
|
|
64
67
|
}
|
|
68
|
+
setSelectable(choice, selectable) {
|
|
69
|
+
this.mcAdapter.setSelectable(choice, selectable);
|
|
70
|
+
}
|
|
65
71
|
getButtonContainer(choice) {
|
|
66
72
|
return this.mcAdapter.getButtonContainer(choice);
|
|
67
73
|
}
|
|
@@ -13,12 +13,16 @@ export declare class MultipleChoiceAdapter implements ChoiceAdapter {
|
|
|
13
13
|
private static readonly ROOT_HOOK_ATTR;
|
|
14
14
|
private static readonly FEEDBACK_HOOK_ATTR;
|
|
15
15
|
private static readonly CHOICE_SELECTOR;
|
|
16
|
+
private static readonly MUI_BUTTON_SELECTOR;
|
|
17
|
+
private static readonly MUI_DISABLED_CLASS;
|
|
16
18
|
canHandle(element: HTMLElement): boolean;
|
|
17
19
|
findChoices(root: HTMLElement): HTMLElement[];
|
|
18
20
|
createChoiceRange(choice: HTMLElement): Range | null;
|
|
19
21
|
getChoiceId(choice: HTMLElement): string;
|
|
20
22
|
getChoiceLabel(choice: HTMLElement): string;
|
|
23
|
+
isSelected(choice: HTMLElement): boolean;
|
|
21
24
|
canEliminate(choice: HTMLElement): boolean;
|
|
25
|
+
setSelectable(choice: HTMLElement, selectable: boolean): void;
|
|
22
26
|
getButtonContainer(choice: HTMLElement): HTMLElement | null;
|
|
23
27
|
private generateFallbackId;
|
|
24
28
|
private annotateChoice;
|
|
@@ -12,6 +12,11 @@ export class MultipleChoiceAdapter {
|
|
|
12
12
|
static ROOT_HOOK_ATTR = "data-pie-answer-eliminator-root";
|
|
13
13
|
static FEEDBACK_HOOK_ATTR = "data-pie-answer-eliminator-feedback-tick";
|
|
14
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";
|
|
15
20
|
canHandle(element) {
|
|
16
21
|
return (element.tagName.toLowerCase() === "multiple-choice" ||
|
|
17
22
|
element.classList.contains("multiple-choice"));
|
|
@@ -47,13 +52,21 @@ export class MultipleChoiceAdapter {
|
|
|
47
52
|
const label = this.resolveLabelElement(choice);
|
|
48
53
|
return label?.textContent?.trim() || "Unlabeled choice";
|
|
49
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
|
+
}
|
|
50
63
|
canEliminate(choice) {
|
|
51
64
|
const input = choice.querySelector('input[type="checkbox"], input[type="radio"]');
|
|
52
65
|
if (!input)
|
|
53
66
|
return false;
|
|
54
67
|
// Can't eliminate if:
|
|
55
68
|
// 1. Already selected (checked)
|
|
56
|
-
if (
|
|
69
|
+
if (this.isSelected(choice))
|
|
57
70
|
return false;
|
|
58
71
|
// 2. Disabled
|
|
59
72
|
if (input.disabled)
|
|
@@ -65,6 +78,26 @@ export class MultipleChoiceAdapter {
|
|
|
65
78
|
return false;
|
|
66
79
|
return true;
|
|
67
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
|
+
}
|
|
68
101
|
getButtonContainer(choice) {
|
|
69
102
|
// Return the choice-input container
|
|
70
103
|
return choice;
|
|
@@ -14,6 +14,11 @@ export declare class AnswerEliminatorCore {
|
|
|
14
14
|
private choiceAdapters;
|
|
15
15
|
private buttonAlignment;
|
|
16
16
|
private shouldRestoreState;
|
|
17
|
+
private active;
|
|
18
|
+
private questionRoot;
|
|
19
|
+
private selectionChangeHandler;
|
|
20
|
+
private selectionObserver;
|
|
21
|
+
private selectionRefreshFrame;
|
|
17
22
|
private storeIntegration;
|
|
18
23
|
constructor(strategyType?: "strikethrough" | "mask" | "gray", buttonAlignment?: "left" | "right" | "inline");
|
|
19
24
|
private createStrategy;
|
|
@@ -63,6 +68,56 @@ export declare class AnswerEliminatorCore {
|
|
|
63
68
|
* Restore state from ElementToolStateStore
|
|
64
69
|
*/
|
|
65
70
|
private restoreState;
|
|
71
|
+
/**
|
|
72
|
+
* Re-enable selection for every currently-tracked struck choice.
|
|
73
|
+
* Must run before the element/adapter maps are cleared, otherwise the
|
|
74
|
+
* inputs would be left disabled after the tool is turned off.
|
|
75
|
+
*/
|
|
76
|
+
private restoreAllSelectable;
|
|
77
|
+
/**
|
|
78
|
+
* Show or hide a choice's strikethrough button, preserving the visible
|
|
79
|
+
* `display` value chosen by the alignment configuration.
|
|
80
|
+
*/
|
|
81
|
+
private setButtonHidden;
|
|
82
|
+
/**
|
|
83
|
+
* The single rule for whether a choice's eliminate button should be hidden:
|
|
84
|
+
* - A struck-through choice always keeps its button (so it can be undone),
|
|
85
|
+
* even when the feature is toggled off.
|
|
86
|
+
* - When the feature is off, a non-struck choice hides its button.
|
|
87
|
+
* - When the feature is on, a non-struck choice hides its button only while
|
|
88
|
+
* it is selected (a selected answer must not be eliminable).
|
|
89
|
+
*/
|
|
90
|
+
private shouldHideButton;
|
|
91
|
+
/**
|
|
92
|
+
* Re-apply the visibility rule to a single choice's button.
|
|
93
|
+
*/
|
|
94
|
+
private updateButtonVisibility;
|
|
95
|
+
/**
|
|
96
|
+
* Re-evaluate button visibility for every tracked choice. Driven by live
|
|
97
|
+
* selection changes and by toggling the feature on/off.
|
|
98
|
+
*/
|
|
99
|
+
private refreshSelectionState;
|
|
100
|
+
/**
|
|
101
|
+
* Schedule a selection refresh on the next frame. Deliberately does NOT
|
|
102
|
+
* cancel-and-reschedule: a burst of mutations coalesces into one pending
|
|
103
|
+
* refresh, and mutations in later frames each get their own refresh, so a
|
|
104
|
+
* multi-render commit settles instead of being starved by continuous
|
|
105
|
+
* churn (e.g. ripple animations).
|
|
106
|
+
*/
|
|
107
|
+
private scheduleSelectionRefresh;
|
|
108
|
+
/**
|
|
109
|
+
* Track live selection changes on the question root so button visibility
|
|
110
|
+
* follows the selection even when no full re-render occurs.
|
|
111
|
+
*
|
|
112
|
+
* Controlled widgets (PIE multiple-choice) update the input's `checked`
|
|
113
|
+
* *property* on their own render — invisible to a MutationObserver and
|
|
114
|
+
* later than the native `change` event — so we observe every DOM change
|
|
115
|
+
* the widget makes (childList + attributes) and re-read on the next frame,
|
|
116
|
+
* by which point the property has settled. Mutations caused by our own
|
|
117
|
+
* buttons are ignored so the refresh can't loop.
|
|
118
|
+
*/
|
|
119
|
+
private attachSelectionListener;
|
|
120
|
+
private detachSelectionListener;
|
|
66
121
|
/**
|
|
67
122
|
* Cleanup buttons from previous element
|
|
68
123
|
*/
|
|
@@ -80,9 +135,13 @@ export declare class AnswerEliminatorCore {
|
|
|
80
135
|
*/
|
|
81
136
|
disableStateRestoration(): void;
|
|
82
137
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
138
|
+
* Turn the feature off at the question level.
|
|
139
|
+
*
|
|
140
|
+
* Struck-through choices stay struck: their strikethrough, their disabled
|
|
141
|
+
* (non-selectable) input, and a visible/usable eliminate button all remain
|
|
142
|
+
* so the student can still undo them. Only the eliminate buttons for
|
|
143
|
+
* choices that are NOT struck through are hidden. Nothing is destroyed, so
|
|
144
|
+
* toggling the feature back on simply re-reveals the hidden buttons.
|
|
86
145
|
*/
|
|
87
146
|
cleanup(): void;
|
|
88
147
|
/**
|