@mathscan/math-exercise-engine 1.1.17 → 1.2.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 +44 -11
- package/dist/index.d.ts +17 -0
- package/dist/index.js +230 -152
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -152
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +40 -0
- package/package.json +19 -20
package/dist/index.mjs
CHANGED
|
@@ -15449,13 +15449,18 @@ function applyDropToInput(targetId, item, onChange) {
|
|
|
15449
15449
|
function isUndoDrop(event, dragId) {
|
|
15450
15450
|
return event.active.id === dragId && !event.over;
|
|
15451
15451
|
}
|
|
15452
|
+
const LockContext = createContext(false);
|
|
15453
|
+
const ExerciseLockProvider = ({ locked, children }) => /* @__PURE__ */ jsx(LockContext.Provider, { value: locked, children });
|
|
15454
|
+
const useExerciseLocked = () => useContext(LockContext);
|
|
15452
15455
|
const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
|
|
15453
15456
|
const { droppedSources } = useExerciseDnd();
|
|
15457
|
+
const locked = useExerciseLocked();
|
|
15454
15458
|
const sourceOptionId = droppedSources.get(widgetId);
|
|
15455
15459
|
const dragId = `${widgetId}__chip`;
|
|
15456
15460
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
15457
15461
|
id: dragId,
|
|
15458
|
-
data: { type: "placed-chip", widgetId, label: value, sourceOptionId }
|
|
15462
|
+
data: { type: "placed-chip", widgetId, label: value, sourceOptionId },
|
|
15463
|
+
disabled: locked
|
|
15459
15464
|
});
|
|
15460
15465
|
useDndMonitor({
|
|
15461
15466
|
onDragEnd(event) {
|
|
@@ -15468,10 +15473,10 @@ const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
|
|
|
15468
15473
|
ref: setNodeRef,
|
|
15469
15474
|
className: `inline-input-chip ${className}${isDragging ? " dragging" : ""}`.trim(),
|
|
15470
15475
|
onClick: (e) => {
|
|
15471
|
-
if (e.detail !== 0) onRemove();
|
|
15476
|
+
if (!locked && e.detail !== 0) onRemove();
|
|
15472
15477
|
},
|
|
15473
|
-
title: "Drag to move, or click to remove",
|
|
15474
|
-
style: { touchAction: "none", cursor: "grab" },
|
|
15478
|
+
title: locked ? void 0 : "Drag to move, or click to remove",
|
|
15479
|
+
style: { touchAction: "none", cursor: locked ? "default" : "grab" },
|
|
15475
15480
|
...listeners,
|
|
15476
15481
|
...attributes,
|
|
15477
15482
|
children: value
|
|
@@ -15491,6 +15496,7 @@ const InlineInput = ({
|
|
|
15491
15496
|
onFocus
|
|
15492
15497
|
}) => {
|
|
15493
15498
|
const playSound = useSound();
|
|
15499
|
+
const locked = useExerciseLocked();
|
|
15494
15500
|
const handleChange = useCallback((newValue, fromDrop = false, sourceOptionId) => {
|
|
15495
15501
|
onChange(widgetId, newValue, fromDrop, sourceOptionId);
|
|
15496
15502
|
}, [onChange, widgetId]);
|
|
@@ -15506,6 +15512,7 @@ const InlineInput = ({
|
|
|
15506
15512
|
id: widgetId,
|
|
15507
15513
|
accepts: ["drag-option", "placed-chip"],
|
|
15508
15514
|
onDrop: handleDrop,
|
|
15515
|
+
disabled: locked,
|
|
15509
15516
|
className: "inline-widget inline-input-droppable",
|
|
15510
15517
|
children: isDropped && value !== "" ? (
|
|
15511
15518
|
// Show as chip only when value came from drag-drop
|
|
@@ -15528,8 +15535,11 @@ const InlineInput = ({
|
|
|
15528
15535
|
className: `inline-input${statusClass}`,
|
|
15529
15536
|
value,
|
|
15530
15537
|
onChange: (e) => handleChange(e.target.value, false),
|
|
15531
|
-
onMouseDown: () =>
|
|
15538
|
+
onMouseDown: () => {
|
|
15539
|
+
if (!locked) playSound("input-select");
|
|
15540
|
+
},
|
|
15532
15541
|
onFocus: () => onFocus?.(widgetId),
|
|
15542
|
+
readOnly: locked,
|
|
15533
15543
|
maxLength: expectedLength,
|
|
15534
15544
|
style: { width: dynamicWidth },
|
|
15535
15545
|
placeholder
|
|
@@ -15546,6 +15556,7 @@ const SingleDragOption = ({
|
|
|
15546
15556
|
className = ""
|
|
15547
15557
|
}) => {
|
|
15548
15558
|
const { usedOptionIds } = useExerciseDnd();
|
|
15559
|
+
const locked = useExerciseLocked();
|
|
15549
15560
|
const isUsed = usedOptionIds.has(optionId);
|
|
15550
15561
|
return /* @__PURE__ */ jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
|
|
15551
15562
|
Draggable,
|
|
@@ -15553,12 +15564,16 @@ const SingleDragOption = ({
|
|
|
15553
15564
|
id: optionId,
|
|
15554
15565
|
type: "drag-option",
|
|
15555
15566
|
data: { value, label: value },
|
|
15567
|
+
disabled: locked,
|
|
15556
15568
|
className: "drag-option-wrapper",
|
|
15557
15569
|
children: /* @__PURE__ */ jsx(
|
|
15558
15570
|
"button",
|
|
15559
15571
|
{
|
|
15560
15572
|
className: "drag-option",
|
|
15561
|
-
onClick: () =>
|
|
15573
|
+
onClick: () => {
|
|
15574
|
+
if (!locked) onOptionClick?.(value);
|
|
15575
|
+
},
|
|
15576
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
15562
15577
|
children: value
|
|
15563
15578
|
}
|
|
15564
15579
|
)
|
|
@@ -15672,31 +15687,34 @@ const NumericKeypad = ({
|
|
|
15672
15687
|
onChange
|
|
15673
15688
|
}) => {
|
|
15674
15689
|
const playSound = useSound();
|
|
15690
|
+
const locked = useExerciseLocked();
|
|
15675
15691
|
const handleDigit = (digit) => {
|
|
15692
|
+
if (locked) return;
|
|
15676
15693
|
playSound("keypad-press");
|
|
15677
15694
|
onChange(value + digit);
|
|
15678
15695
|
};
|
|
15679
15696
|
const handleBackspace = () => {
|
|
15697
|
+
if (locked) return;
|
|
15680
15698
|
playSound("keypad-press");
|
|
15681
15699
|
onChange(value.slice(0, -1));
|
|
15682
15700
|
};
|
|
15683
15701
|
return /* @__PURE__ */ jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, children: [
|
|
15684
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
|
|
15685
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
|
|
15686
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
|
|
15687
|
-
includeOperators && /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
|
|
15688
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
|
|
15689
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
|
|
15690
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
|
|
15691
|
-
includeOperators && /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
|
|
15692
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
|
|
15693
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
|
|
15694
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
|
|
15695
|
-
includeOperators && /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
|
|
15696
|
-
includeDecimal ? /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
|
|
15697
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
|
|
15698
|
-
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
|
|
15699
|
-
includeOperators && /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
|
|
15702
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
|
|
15703
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
|
|
15704
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
|
|
15705
|
+
includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
|
|
15706
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
|
|
15707
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
|
|
15708
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
|
|
15709
|
+
includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
|
|
15710
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
|
|
15711
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
|
|
15712
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
|
|
15713
|
+
includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
|
|
15714
|
+
includeDecimal ? /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
|
|
15715
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
|
|
15716
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
|
|
15717
|
+
includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
|
|
15700
15718
|
] });
|
|
15701
15719
|
};
|
|
15702
15720
|
const FractionInput = ({
|
|
@@ -15713,6 +15731,7 @@ const FractionInput = ({
|
|
|
15713
15731
|
placeholder
|
|
15714
15732
|
}) => {
|
|
15715
15733
|
const playSound = useSound();
|
|
15734
|
+
const locked = useExerciseLocked();
|
|
15716
15735
|
const numKey = getFractionKey(widgetId, "num");
|
|
15717
15736
|
const denKey = getFractionKey(widgetId, "den");
|
|
15718
15737
|
const numValue = values[numKey] ?? "";
|
|
@@ -15733,6 +15752,7 @@ const FractionInput = ({
|
|
|
15733
15752
|
id: subKey,
|
|
15734
15753
|
accepts: ["drag-option", "placed-chip"],
|
|
15735
15754
|
onDrop: handleDrop(subKey),
|
|
15755
|
+
disabled: locked,
|
|
15736
15756
|
className: "fraction-slot-droppable",
|
|
15737
15757
|
children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsx(
|
|
15738
15758
|
PlacedInputChip,
|
|
@@ -15750,8 +15770,11 @@ const FractionInput = ({
|
|
|
15750
15770
|
className: `inline-input fraction-input${slotClass}`,
|
|
15751
15771
|
value: currentValue,
|
|
15752
15772
|
onChange: (e) => onChange(subKey, e.target.value, false),
|
|
15753
|
-
onMouseDown: () =>
|
|
15773
|
+
onMouseDown: () => {
|
|
15774
|
+
if (!locked) playSound("input-select");
|
|
15775
|
+
},
|
|
15754
15776
|
onFocus: () => onFocus?.(subKey),
|
|
15777
|
+
readOnly: locked,
|
|
15755
15778
|
maxLength: placeholder ? Math.max(20, placeholder.length) : 4,
|
|
15756
15779
|
size: placeholder ? placeholder.length : void 0,
|
|
15757
15780
|
"aria-label": label,
|
|
@@ -15982,12 +16005,13 @@ function getEqualTrianglePartPath(index, divisions) {
|
|
|
15982
16005
|
function getGridColumnCount(divisions) {
|
|
15983
16006
|
return Math.ceil(Math.sqrt(divisions));
|
|
15984
16007
|
}
|
|
15985
|
-
function getInteractiveProps(part, onPartClick) {
|
|
16008
|
+
function getInteractiveProps(part, onPartClick, locked = false) {
|
|
15986
16009
|
if (!part.isInteractiveSlot) return {};
|
|
15987
16010
|
return {
|
|
15988
16011
|
onClick: () => onPartClick(part.index),
|
|
15989
16012
|
role: "button",
|
|
15990
16013
|
"aria-pressed": part.isSelected,
|
|
16014
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
15991
16015
|
"aria-label": getPartAriaLabel(part),
|
|
15992
16016
|
tabIndex: 0,
|
|
15993
16017
|
onKeyDown: (event) => {
|
|
@@ -16007,6 +16031,7 @@ const BarShape = ({
|
|
|
16007
16031
|
orientation,
|
|
16008
16032
|
onPartClick
|
|
16009
16033
|
}) => {
|
|
16034
|
+
const locked = useExerciseLocked();
|
|
16010
16035
|
const renderedParts = orientation === "vertical" ? [...parts].reverse() : parts;
|
|
16011
16036
|
return /* @__PURE__ */ jsx(
|
|
16012
16037
|
"div",
|
|
@@ -16018,7 +16043,7 @@ const BarShape = ({
|
|
|
16018
16043
|
{
|
|
16019
16044
|
className: part.className,
|
|
16020
16045
|
style: part.style,
|
|
16021
|
-
...getInteractiveProps(part, onPartClick)
|
|
16046
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
16022
16047
|
},
|
|
16023
16048
|
part.index
|
|
16024
16049
|
))
|
|
@@ -16030,87 +16055,96 @@ const GridShape = ({
|
|
|
16030
16055
|
parts,
|
|
16031
16056
|
divisions,
|
|
16032
16057
|
onPartClick
|
|
16033
|
-
}) =>
|
|
16034
|
-
|
|
16035
|
-
|
|
16036
|
-
|
|
16037
|
-
|
|
16038
|
-
"
|
|
16039
|
-
|
|
16040
|
-
|
|
16041
|
-
|
|
16042
|
-
|
|
16043
|
-
|
|
16044
|
-
"div",
|
|
16045
|
-
{
|
|
16046
|
-
className: part.className,
|
|
16047
|
-
style: part.style,
|
|
16048
|
-
...getInteractiveProps(part, onPartClick)
|
|
16058
|
+
}) => {
|
|
16059
|
+
const locked = useExerciseLocked();
|
|
16060
|
+
return /* @__PURE__ */ jsx(
|
|
16061
|
+
"div",
|
|
16062
|
+
{
|
|
16063
|
+
className: "visual-fraction-widget__parts visual-fraction-widget__parts--horizontal",
|
|
16064
|
+
style: {
|
|
16065
|
+
"--visual-fraction-divisions": divisions,
|
|
16066
|
+
"--visual-fraction-grid-cols": getGridColumnCount(divisions),
|
|
16067
|
+
// Safe integer division: zod schema enforces divisions = perfect square.
|
|
16068
|
+
"--visual-fraction-grid-rows": divisions / getGridColumnCount(divisions)
|
|
16049
16069
|
},
|
|
16050
|
-
part
|
|
16051
|
-
|
|
16052
|
-
|
|
16053
|
-
|
|
16070
|
+
children: parts.map((part) => /* @__PURE__ */ jsx(
|
|
16071
|
+
"div",
|
|
16072
|
+
{
|
|
16073
|
+
className: part.className,
|
|
16074
|
+
style: part.style,
|
|
16075
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
16076
|
+
},
|
|
16077
|
+
part.index
|
|
16078
|
+
))
|
|
16079
|
+
}
|
|
16080
|
+
);
|
|
16081
|
+
};
|
|
16054
16082
|
GridShape.displayName = "GridShape";
|
|
16055
16083
|
const CircleShape = ({
|
|
16056
16084
|
parts,
|
|
16057
16085
|
divisions,
|
|
16058
16086
|
onPartClick
|
|
16059
|
-
}) =>
|
|
16060
|
-
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16074
|
-
|
|
16075
|
-
|
|
16076
|
-
|
|
16077
|
-
|
|
16078
|
-
|
|
16079
|
-
|
|
16080
|
-
|
|
16081
|
-
|
|
16082
|
-
|
|
16083
|
-
|
|
16084
|
-
|
|
16085
|
-
|
|
16086
|
-
|
|
16087
|
-
|
|
16088
|
-
|
|
16089
|
-
|
|
16090
|
-
|
|
16091
|
-
)
|
|
16087
|
+
}) => {
|
|
16088
|
+
const locked = useExerciseLocked();
|
|
16089
|
+
return /* @__PURE__ */ jsx(
|
|
16090
|
+
"svg",
|
|
16091
|
+
{
|
|
16092
|
+
className: "visual-fraction-widget__shape visual-fraction-widget__circle",
|
|
16093
|
+
viewBox: "0 0 100 100",
|
|
16094
|
+
children: parts.map((part) => {
|
|
16095
|
+
const sectorProps = getCircleSectorProps(part.index, divisions);
|
|
16096
|
+
const className = `${part.className} visual-fraction-widget__sector`;
|
|
16097
|
+
const interactiveProps = getInteractiveProps(part, onPartClick, locked);
|
|
16098
|
+
return sectorProps.kind === "circle" ? /* @__PURE__ */ jsx(
|
|
16099
|
+
"circle",
|
|
16100
|
+
{
|
|
16101
|
+
className,
|
|
16102
|
+
style: part.style,
|
|
16103
|
+
cx: "50",
|
|
16104
|
+
cy: "50",
|
|
16105
|
+
r: "48",
|
|
16106
|
+
...interactiveProps
|
|
16107
|
+
},
|
|
16108
|
+
part.index
|
|
16109
|
+
) : /* @__PURE__ */ jsx(
|
|
16110
|
+
"path",
|
|
16111
|
+
{
|
|
16112
|
+
className,
|
|
16113
|
+
style: part.style,
|
|
16114
|
+
d: sectorProps.d,
|
|
16115
|
+
...interactiveProps
|
|
16116
|
+
},
|
|
16117
|
+
part.index
|
|
16118
|
+
);
|
|
16119
|
+
})
|
|
16120
|
+
}
|
|
16121
|
+
);
|
|
16122
|
+
};
|
|
16092
16123
|
CircleShape.displayName = "CircleShape";
|
|
16093
16124
|
const TriangleShape = ({
|
|
16094
16125
|
parts,
|
|
16095
16126
|
divisions,
|
|
16096
16127
|
onPartClick
|
|
16097
|
-
}) =>
|
|
16098
|
-
|
|
16099
|
-
|
|
16100
|
-
|
|
16101
|
-
|
|
16102
|
-
|
|
16103
|
-
"
|
|
16104
|
-
|
|
16105
|
-
|
|
16106
|
-
|
|
16107
|
-
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
|
|
16113
|
-
)
|
|
16128
|
+
}) => {
|
|
16129
|
+
const locked = useExerciseLocked();
|
|
16130
|
+
return /* @__PURE__ */ jsx(
|
|
16131
|
+
"svg",
|
|
16132
|
+
{
|
|
16133
|
+
className: "visual-fraction-widget__shape visual-fraction-widget__triangle",
|
|
16134
|
+
viewBox: "0 0 100 100",
|
|
16135
|
+
children: parts.map((part) => /* @__PURE__ */ jsx(
|
|
16136
|
+
"path",
|
|
16137
|
+
{
|
|
16138
|
+
className: `${part.className} visual-fraction-widget__sector`,
|
|
16139
|
+
style: part.style,
|
|
16140
|
+
d: getEqualTrianglePartPath(part.index, divisions),
|
|
16141
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
16142
|
+
},
|
|
16143
|
+
part.index
|
|
16144
|
+
))
|
|
16145
|
+
}
|
|
16146
|
+
);
|
|
16147
|
+
};
|
|
16114
16148
|
TriangleShape.displayName = "TriangleShape";
|
|
16115
16149
|
const ShapeBar = ({ parts, divisions, orientation, onPartClick }) => /* @__PURE__ */ jsx(BarShape, { parts, divisions, orientation, onPartClick });
|
|
16116
16150
|
const ShapeCircle = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsx(CircleShape, { parts, divisions, onPartClick });
|
|
@@ -16154,12 +16188,14 @@ const VisualFraction = ({
|
|
|
16154
16188
|
onChange
|
|
16155
16189
|
});
|
|
16156
16190
|
const playSound = useSound();
|
|
16191
|
+
const locked = useExerciseLocked();
|
|
16157
16192
|
const handlePartClickWithSound = useCallback(
|
|
16158
16193
|
(index) => {
|
|
16194
|
+
if (locked) return;
|
|
16159
16195
|
if (answer.length > 0 && index >= filledCount) playSound("fraction-shade");
|
|
16160
16196
|
handlePartClick(index);
|
|
16161
16197
|
},
|
|
16162
|
-
[answer.length, filledCount, playSound, handlePartClick]
|
|
16198
|
+
[locked, answer.length, filledCount, playSound, handlePartClick]
|
|
16163
16199
|
);
|
|
16164
16200
|
const parts = useMemo(
|
|
16165
16201
|
() => Array.from({ length: divisions }, (_, i) => {
|
|
@@ -16234,6 +16270,7 @@ const GridSymmetry = ({
|
|
|
16234
16270
|
validationStatus = null,
|
|
16235
16271
|
onChange
|
|
16236
16272
|
}) => {
|
|
16273
|
+
const exerciseLocked = useExerciseLocked();
|
|
16237
16274
|
const initialKeys = useMemo(() => new Set(widget.cells.map(gridCellKey)), [widget.cells]);
|
|
16238
16275
|
const initialInteractiveCells = useMemo(
|
|
16239
16276
|
() => widget.cells.filter(
|
|
@@ -16244,6 +16281,7 @@ const GridSymmetry = ({
|
|
|
16244
16281
|
const selectedCells = value ?? initialInteractiveCells;
|
|
16245
16282
|
const selectedKeys = useMemo(() => new Set(selectedCells.map(gridCellKey)), [selectedCells]);
|
|
16246
16283
|
const toggleCell = (cell) => {
|
|
16284
|
+
if (exerciseLocked) return;
|
|
16247
16285
|
const key2 = gridCellKey(cell);
|
|
16248
16286
|
if (initialKeys.has(key2)) return;
|
|
16249
16287
|
const next = selectedKeys.has(key2) ? selectedCells.filter((candidate) => gridCellKey(candidate) !== key2) : [...selectedCells, cell];
|
|
@@ -16284,6 +16322,7 @@ const GridSymmetry = ({
|
|
|
16284
16322
|
className,
|
|
16285
16323
|
"aria-label": label,
|
|
16286
16324
|
"aria-pressed": selected,
|
|
16325
|
+
"aria-disabled": exerciseLocked ? "true" : void 0,
|
|
16287
16326
|
onClick: () => toggleCell(cell)
|
|
16288
16327
|
},
|
|
16289
16328
|
key2
|
|
@@ -16323,6 +16362,7 @@ const RadioOptions = ({
|
|
|
16323
16362
|
onChange
|
|
16324
16363
|
}) => {
|
|
16325
16364
|
const playSound = useSound();
|
|
16365
|
+
const locked = useExerciseLocked();
|
|
16326
16366
|
return /* @__PURE__ */ jsx("div", { className: "radio-options", children: options.map((option, idx) => {
|
|
16327
16367
|
const isSelected = value === option;
|
|
16328
16368
|
const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
|
|
@@ -16331,9 +16371,11 @@ const RadioOptions = ({
|
|
|
16331
16371
|
{
|
|
16332
16372
|
className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
|
|
16333
16373
|
onClick: () => {
|
|
16374
|
+
if (locked) return;
|
|
16334
16375
|
playSound("radio-select");
|
|
16335
16376
|
onChange(name, option);
|
|
16336
16377
|
},
|
|
16378
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
16337
16379
|
type: "button",
|
|
16338
16380
|
children: option
|
|
16339
16381
|
},
|
|
@@ -16345,7 +16387,10 @@ const Btn = ({
|
|
|
16345
16387
|
label,
|
|
16346
16388
|
extraClass = "keypad-btn-digit",
|
|
16347
16389
|
onClick
|
|
16348
|
-
}) =>
|
|
16390
|
+
}) => {
|
|
16391
|
+
const locked = useExerciseLocked();
|
|
16392
|
+
return /* @__PURE__ */ jsx("button", { className: `keypad-btn ${extraClass}`, disabled: locked, onClick, children: /* @__PURE__ */ jsx("span", { children: label }) });
|
|
16393
|
+
};
|
|
16349
16394
|
const DiamondKeypad = ({ value, onChange }) => {
|
|
16350
16395
|
const playSound = useSound();
|
|
16351
16396
|
const append = (ch) => {
|
|
@@ -16432,8 +16477,10 @@ const MatcherWidget = ({
|
|
|
16432
16477
|
() => defaultValue2 && defaultValue2.length > 0 ? assignmentsFromValue(defaultValue2, itemIds, groupCount) : initialAssignments(itemIds)
|
|
16433
16478
|
);
|
|
16434
16479
|
const playSound = useSound();
|
|
16480
|
+
const locked = useExerciseLocked();
|
|
16435
16481
|
const handleCardClick = useCallback(
|
|
16436
16482
|
(itemId) => {
|
|
16483
|
+
if (locked) return;
|
|
16437
16484
|
playSound("matcher-select");
|
|
16438
16485
|
setAssignments((prev) => {
|
|
16439
16486
|
const current = prev[itemId] ?? null;
|
|
@@ -16441,7 +16488,7 @@ const MatcherWidget = ({
|
|
|
16441
16488
|
return { ...prev, [itemId]: next };
|
|
16442
16489
|
});
|
|
16443
16490
|
},
|
|
16444
|
-
[groupCount, playSound]
|
|
16491
|
+
[groupCount, playSound, locked]
|
|
16445
16492
|
);
|
|
16446
16493
|
const onChangeRef = useRef(onChange);
|
|
16447
16494
|
onChangeRef.current = onChange;
|
|
@@ -16478,6 +16525,7 @@ const MatcherWidget = ({
|
|
|
16478
16525
|
"--matcher-card-text-color": `var(--matcher-group-${colorIdx + 1}-text-color, ${textColors[colorIdx]})`
|
|
16479
16526
|
} : void 0,
|
|
16480
16527
|
onClick: () => handleCardClick(item.itemId),
|
|
16528
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
16481
16529
|
"aria-pressed": isSelected,
|
|
16482
16530
|
"aria-label": isSelected ? `Assigned to group ${colorIdx + 1}` : "Not assigned",
|
|
16483
16531
|
children: item.content.map((layoutItem) => /* @__PURE__ */ jsx(
|
|
@@ -16691,6 +16739,7 @@ const PairMatcherWidget = ({
|
|
|
16691
16739
|
shuffleItems = true
|
|
16692
16740
|
}) => {
|
|
16693
16741
|
const { topItems, bottomItems } = widget;
|
|
16742
|
+
const exerciseLocked = useExerciseLocked();
|
|
16694
16743
|
const [shuffledTop] = useState(
|
|
16695
16744
|
() => shuffleItems ? lodashExports.shuffle([...topItems]) : [...topItems]
|
|
16696
16745
|
);
|
|
@@ -16734,8 +16783,9 @@ const PairMatcherWidget = ({
|
|
|
16734
16783
|
}
|
|
16735
16784
|
soundPrevRef.current = { count, pendingId, flashId };
|
|
16736
16785
|
}, [state.pairs, state.pending, state.flashTopId, playSound]);
|
|
16786
|
+
const interactionLocked = validationStatus !== null || exerciseLocked;
|
|
16737
16787
|
const handleTopClick = useCallback((topId) => {
|
|
16738
|
-
if (
|
|
16788
|
+
if (interactionLocked) return;
|
|
16739
16789
|
if (flashTimerRef.current) {
|
|
16740
16790
|
clearTimeout(flashTimerRef.current);
|
|
16741
16791
|
flashTimerRef.current = null;
|
|
@@ -16777,9 +16827,9 @@ const PairMatcherWidget = ({
|
|
|
16777
16827
|
flashBottomId: null
|
|
16778
16828
|
};
|
|
16779
16829
|
});
|
|
16780
|
-
}, [
|
|
16830
|
+
}, [interactionLocked]);
|
|
16781
16831
|
const handleBottomClick = useCallback((bottomId) => {
|
|
16782
|
-
if (
|
|
16832
|
+
if (interactionLocked) return;
|
|
16783
16833
|
if (flashTimerRef.current) {
|
|
16784
16834
|
clearTimeout(flashTimerRef.current);
|
|
16785
16835
|
flashTimerRef.current = null;
|
|
@@ -16822,9 +16872,13 @@ const PairMatcherWidget = ({
|
|
|
16822
16872
|
flashBottomId: null
|
|
16823
16873
|
};
|
|
16824
16874
|
});
|
|
16825
|
-
}, [
|
|
16875
|
+
}, [interactionLocked]);
|
|
16826
16876
|
useEffect(() => {
|
|
16827
|
-
if (
|
|
16877
|
+
if (state.flashTopId === null || state.flashBottomId === null) return;
|
|
16878
|
+
if (interactionLocked) {
|
|
16879
|
+
setState((current) => ({ ...current, flashTopId: null, flashBottomId: null }));
|
|
16880
|
+
return;
|
|
16881
|
+
}
|
|
16828
16882
|
flashTimerRef.current = setTimeout(() => {
|
|
16829
16883
|
flashTimerRef.current = null;
|
|
16830
16884
|
setState((current) => {
|
|
@@ -16838,7 +16892,7 @@ const PairMatcherWidget = ({
|
|
|
16838
16892
|
flashTimerRef.current = null;
|
|
16839
16893
|
}
|
|
16840
16894
|
};
|
|
16841
|
-
}, [state.flashTopId, state.flashBottomId,
|
|
16895
|
+
}, [state.flashTopId, state.flashBottomId, interactionLocked]);
|
|
16842
16896
|
const onChangeRef = useRef(onChange);
|
|
16843
16897
|
onChangeRef.current = onChange;
|
|
16844
16898
|
const isMounted = useRef(false);
|
|
@@ -16898,7 +16952,7 @@ const PairMatcherWidget = ({
|
|
|
16898
16952
|
dir: isRtl ? "rtl" : void 0,
|
|
16899
16953
|
onClick: () => isTop ? handleTopClick(item.itemId) : handleBottomClick(item.itemId),
|
|
16900
16954
|
"aria-pressed": status !== "idle",
|
|
16901
|
-
"aria-disabled":
|
|
16955
|
+
"aria-disabled": interactionLocked ? "true" : void 0,
|
|
16902
16956
|
children: /* @__PURE__ */ jsx("span", { className: "pair-matcher-card-content", children: item.content.map((li) => renderPairMatcherItemContent(li, widgetsMap)) })
|
|
16903
16957
|
}
|
|
16904
16958
|
),
|
|
@@ -17437,6 +17491,7 @@ const DraggableCursor = ({
|
|
|
17437
17491
|
currentValue,
|
|
17438
17492
|
validationStatus
|
|
17439
17493
|
}) => {
|
|
17494
|
+
const locked = useExerciseLocked();
|
|
17440
17495
|
const [isDragging, setIsDragging] = useState(false);
|
|
17441
17496
|
const isDraggingRef = useRef(false);
|
|
17442
17497
|
const currentValueRef = useRef(currentValue);
|
|
@@ -17458,13 +17513,14 @@ const DraggableCursor = ({
|
|
|
17458
17513
|
return snapTicks.length > 0 ? snapToNearest(raw, snapTicks) : cleanFloat(raw);
|
|
17459
17514
|
}, [axisRowRef, min, max, snapTicks]);
|
|
17460
17515
|
const handlePointerDown = useCallback((e) => {
|
|
17516
|
+
if (locked) return;
|
|
17461
17517
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
17462
17518
|
e.stopPropagation();
|
|
17463
17519
|
isDraggingRef.current = true;
|
|
17464
17520
|
dragStartValueRef.current = currentValue;
|
|
17465
17521
|
lastSoundValueRef.current = currentValue;
|
|
17466
17522
|
setIsDragging(true);
|
|
17467
|
-
}, [currentValue]);
|
|
17523
|
+
}, [currentValue, locked]);
|
|
17468
17524
|
const updateCursorValue = useCallback((clientX) => {
|
|
17469
17525
|
const next = snap(clientX);
|
|
17470
17526
|
if (!almostEqual(next, lastSoundValueRef.current)) {
|
|
@@ -17492,7 +17548,7 @@ const DraggableCursor = ({
|
|
|
17492
17548
|
}
|
|
17493
17549
|
}, [updateCursorValue, playSound]);
|
|
17494
17550
|
const handleKeyDown = useCallback((e) => {
|
|
17495
|
-
if (!snapTicks.length) return;
|
|
17551
|
+
if (locked || !snapTicks.length) return;
|
|
17496
17552
|
const sorted = [...snapTicks].sort((a, b) => a - b);
|
|
17497
17553
|
const idx = sorted.findIndex((t) => t >= currentValue - 1e-9);
|
|
17498
17554
|
const first = sorted[0] ?? currentValue;
|
|
@@ -17514,7 +17570,7 @@ const DraggableCursor = ({
|
|
|
17514
17570
|
}
|
|
17515
17571
|
onChange(cursor.id, String(next));
|
|
17516
17572
|
}
|
|
17517
|
-
}, [snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
|
|
17573
|
+
}, [locked, snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
|
|
17518
17574
|
const ballClass = [
|
|
17519
17575
|
"number-line-cursor-ball",
|
|
17520
17576
|
isDragging ? "number-line-cursor-ball--dragging" : "",
|
|
@@ -17563,6 +17619,7 @@ const DraggableCursor = ({
|
|
|
17563
17619
|
"aria-valuemax": max,
|
|
17564
17620
|
"aria-valuenow": currentValue,
|
|
17565
17621
|
"aria-label": sliderAriaLabel,
|
|
17622
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
17566
17623
|
tabIndex: 0
|
|
17567
17624
|
}
|
|
17568
17625
|
)
|
|
@@ -17603,9 +17660,11 @@ const CursorBalls = ({
|
|
|
17603
17660
|
}) });
|
|
17604
17661
|
};
|
|
17605
17662
|
const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
17663
|
+
const locked = useExerciseLocked();
|
|
17606
17664
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
17607
17665
|
id: widgetId,
|
|
17608
|
-
data: { type: "placed-chip", widgetId, label }
|
|
17666
|
+
data: { type: "placed-chip", widgetId, label },
|
|
17667
|
+
disabled: locked
|
|
17609
17668
|
});
|
|
17610
17669
|
return /* @__PURE__ */ jsx(
|
|
17611
17670
|
"div",
|
|
@@ -17630,6 +17689,7 @@ const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
|
17630
17689
|
type: "button",
|
|
17631
17690
|
className: "number-line-placed-chip-remove",
|
|
17632
17691
|
"aria-label": "Remove",
|
|
17692
|
+
disabled: locked,
|
|
17633
17693
|
onClick: onRemove,
|
|
17634
17694
|
children: "✕"
|
|
17635
17695
|
}
|
|
@@ -17669,6 +17729,7 @@ const FreeDragAxis = ({
|
|
|
17669
17729
|
cursorValidationStatuses,
|
|
17670
17730
|
snapKey
|
|
17671
17731
|
}) => {
|
|
17732
|
+
const axisLocked = useExerciseLocked();
|
|
17672
17733
|
const axisRef = useRef(null);
|
|
17673
17734
|
const snapTicks = useMemo(
|
|
17674
17735
|
() => getSnapTicks(majorTicks, minorTicks),
|
|
@@ -17724,7 +17785,7 @@ const FreeDragAxis = ({
|
|
|
17724
17785
|
return changed ? next : prev;
|
|
17725
17786
|
});
|
|
17726
17787
|
}, [values]);
|
|
17727
|
-
const { setNodeRef } = useDroppable({ id: axisDropId });
|
|
17788
|
+
const { setNodeRef } = useDroppable({ id: axisDropId, disabled: axisLocked });
|
|
17728
17789
|
const mergedRef = useCallback((node) => {
|
|
17729
17790
|
axisRef.current = node;
|
|
17730
17791
|
setNodeRef(node);
|
|
@@ -18038,6 +18099,7 @@ const CounterControls = ({
|
|
|
18038
18099
|
onIncrement
|
|
18039
18100
|
}) => {
|
|
18040
18101
|
const playSound = useSound();
|
|
18102
|
+
const locked = useExerciseLocked();
|
|
18041
18103
|
return /* @__PURE__ */ jsxs("div", { className: "number-line-counter", "aria-label": counter.name ?? "Counter", children: [
|
|
18042
18104
|
/* @__PURE__ */ jsxs("div", { className: "number-line-counter-controls", children: [
|
|
18043
18105
|
/* @__PURE__ */ jsx(
|
|
@@ -18046,7 +18108,7 @@ const CounterControls = ({
|
|
|
18046
18108
|
type: "button",
|
|
18047
18109
|
className: "number-line-counter-btn",
|
|
18048
18110
|
"aria-label": "Decrease",
|
|
18049
|
-
disabled: count <= counter.min,
|
|
18111
|
+
disabled: locked || count <= counter.min,
|
|
18050
18112
|
onClick: () => {
|
|
18051
18113
|
playSound("counter-step");
|
|
18052
18114
|
onDecrement();
|
|
@@ -18061,7 +18123,7 @@ const CounterControls = ({
|
|
|
18061
18123
|
type: "button",
|
|
18062
18124
|
className: "number-line-counter-btn",
|
|
18063
18125
|
"aria-label": "Increase",
|
|
18064
|
-
disabled: count >= counter.max,
|
|
18126
|
+
disabled: locked || count >= counter.max,
|
|
18065
18127
|
onClick: () => {
|
|
18066
18128
|
playSound("counter-step");
|
|
18067
18129
|
onIncrement();
|
|
@@ -18645,6 +18707,7 @@ const ClockRenderer = ({
|
|
|
18645
18707
|
validationStatus,
|
|
18646
18708
|
onChange
|
|
18647
18709
|
}) => {
|
|
18710
|
+
const locked = useExerciseLocked();
|
|
18648
18711
|
const format = item.format ?? "24h";
|
|
18649
18712
|
const interactive = item.mode === "interactive";
|
|
18650
18713
|
const snap = interactive && "snap" in item ? item.snap ?? 1 : 1;
|
|
@@ -18690,18 +18753,18 @@ const ClockRenderer = ({
|
|
|
18690
18753
|
}
|
|
18691
18754
|
}, [snap, hours, minutes, item.mode, item.name, onChange, playSound]);
|
|
18692
18755
|
const onPointerDown = (hand) => (e) => {
|
|
18693
|
-
if (!interactive) return;
|
|
18756
|
+
if (!interactive || locked) return;
|
|
18694
18757
|
e.preventDefault();
|
|
18695
18758
|
e.target.setPointerCapture?.(e.pointerId);
|
|
18696
18759
|
draggingRef.current = hand;
|
|
18697
18760
|
updateFromPointer(e.clientX, e.clientY);
|
|
18698
18761
|
};
|
|
18699
18762
|
const onPointerMove = (e) => {
|
|
18700
|
-
if (!interactive || !draggingRef.current) return;
|
|
18763
|
+
if (!interactive || locked || !draggingRef.current) return;
|
|
18701
18764
|
updateFromPointer(e.clientX, e.clientY);
|
|
18702
18765
|
};
|
|
18703
18766
|
const onPointerUp = (e) => {
|
|
18704
|
-
if (!interactive) return;
|
|
18767
|
+
if (!interactive || locked) return;
|
|
18705
18768
|
e.target.releasePointerCapture?.(e.pointerId);
|
|
18706
18769
|
draggingRef.current = null;
|
|
18707
18770
|
};
|
|
@@ -19253,38 +19316,45 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19253
19316
|
const [droppedSources, setDroppedSources] = useState(/* @__PURE__ */ new Map());
|
|
19254
19317
|
const [focusedInputId, setFocusedInputId] = useState(null);
|
|
19255
19318
|
const [validationDetails, setValidationDetails] = useState(null);
|
|
19319
|
+
const rootRef = useRef(null);
|
|
19256
19320
|
const [resetKey, setResetKey] = useState(0);
|
|
19257
19321
|
const [revealed, setRevealed] = useState(false);
|
|
19258
19322
|
const playSound = useSoundPlayer(soundEnabled, sounds);
|
|
19259
|
-
|
|
19260
|
-
|
|
19261
|
-
|
|
19262
|
-
|
|
19263
|
-
|
|
19264
|
-
|
|
19265
|
-
|
|
19266
|
-
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
|
|
19270
|
-
|
|
19271
|
-
|
|
19272
|
-
|
|
19273
|
-
|
|
19274
|
-
|
|
19275
|
-
|
|
19276
|
-
|
|
19277
|
-
|
|
19278
|
-
|
|
19279
|
-
|
|
19280
|
-
|
|
19281
|
-
|
|
19282
|
-
|
|
19323
|
+
const externallyLocked = useExerciseLocked();
|
|
19324
|
+
const locked = validationDetails !== null || externallyLocked;
|
|
19325
|
+
const lastResultRef = useRef(null);
|
|
19326
|
+
const runSubmit = () => {
|
|
19327
|
+
if (locked) return lastResultRef.current ?? validate(json, values);
|
|
19328
|
+
const result = validate(json, values);
|
|
19329
|
+
playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
|
|
19330
|
+
const details = { ...result.details };
|
|
19331
|
+
const slotValidation = (id, raw, expected) => ({
|
|
19332
|
+
widgetId: id,
|
|
19333
|
+
userValue: typeof raw === "string" ? raw : void 0,
|
|
19334
|
+
isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
|
|
19335
|
+
});
|
|
19336
|
+
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
19337
|
+
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
19338
|
+
if (!(widgetId in details)) continue;
|
|
19339
|
+
const answer = json.validation?.answers?.[widgetId];
|
|
19340
|
+
if (!answer) continue;
|
|
19341
|
+
const [expectedNum, expectedDen] = answer.split("/", 2);
|
|
19342
|
+
if (!expectedNum || !expectedDen) continue;
|
|
19343
|
+
if (details[widgetId]?.isCorrect) {
|
|
19344
|
+
if (widget.numerator === null) details[`${widgetId}__num`] = { widgetId: `${widgetId}__num`, userValue: void 0, isCorrect: true };
|
|
19345
|
+
if (widget.denominator === null) details[`${widgetId}__den`] = { widgetId: `${widgetId}__den`, userValue: void 0, isCorrect: true };
|
|
19346
|
+
} else {
|
|
19347
|
+
if (widget.numerator === null) details[`${widgetId}__num`] = slotValidation(`${widgetId}__num`, values[`${widgetId}__num`], expectedNum);
|
|
19348
|
+
if (widget.denominator === null) details[`${widgetId}__den`] = slotValidation(`${widgetId}__den`, values[`${widgetId}__den`], expectedDen);
|
|
19283
19349
|
}
|
|
19284
|
-
|
|
19285
|
-
|
|
19286
|
-
|
|
19287
|
-
|
|
19350
|
+
}
|
|
19351
|
+
setValidationDetails(details);
|
|
19352
|
+
onSubmit?.(values);
|
|
19353
|
+
lastResultRef.current = result;
|
|
19354
|
+
return result;
|
|
19355
|
+
};
|
|
19356
|
+
useImperativeHandle(ref, () => ({
|
|
19357
|
+
submit: runSubmit,
|
|
19288
19358
|
getValues: () => values,
|
|
19289
19359
|
getCompletion: () => {
|
|
19290
19360
|
const answerable = getAnswerableInputs(json);
|
|
@@ -19327,14 +19397,20 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19327
19397
|
setResetKey((k) => k + 1);
|
|
19328
19398
|
},
|
|
19329
19399
|
focusFirstInput: () => {
|
|
19330
|
-
const container =
|
|
19331
|
-
|
|
19332
|
-
|
|
19333
|
-
|
|
19400
|
+
const container = rootRef.current;
|
|
19401
|
+
if (!container) return;
|
|
19402
|
+
const candidates = container.querySelectorAll(
|
|
19403
|
+
'input[type="text"], input[type="number"], textarea, select'
|
|
19404
|
+
);
|
|
19405
|
+
for (const el of Array.from(candidates)) {
|
|
19406
|
+
if (el.readOnly || el.disabled) continue;
|
|
19407
|
+
el.focus();
|
|
19408
|
+
return;
|
|
19334
19409
|
}
|
|
19335
19410
|
}
|
|
19336
|
-
}), [json, values, onSubmit, playSound]);
|
|
19411
|
+
}), [json, values, onSubmit, playSound, locked]);
|
|
19337
19412
|
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
19413
|
+
if (locked) return;
|
|
19338
19414
|
setRevealed(false);
|
|
19339
19415
|
const next = { ...valuesRef.current, [widgetId]: value };
|
|
19340
19416
|
valuesRef.current = next;
|
|
@@ -19351,7 +19427,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19351
19427
|
onChange?.(next);
|
|
19352
19428
|
};
|
|
19353
19429
|
const handleSubmit = () => {
|
|
19354
|
-
|
|
19430
|
+
runSubmit();
|
|
19355
19431
|
};
|
|
19356
19432
|
const lineGroups = useMemo(() => {
|
|
19357
19433
|
const isRTLInlineWidget = (item) => {
|
|
@@ -19466,7 +19542,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19466
19542
|
if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
|
|
19467
19543
|
return { tools: list, toolsMinHeight: maxH };
|
|
19468
19544
|
}, [rulerWidget, protractorWidget, setSquareWidget]);
|
|
19469
|
-
return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxs("div", { className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${className}`, children: [
|
|
19545
|
+
return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, children: [
|
|
19470
19546
|
/* @__PURE__ */ jsxs(
|
|
19471
19547
|
"div",
|
|
19472
19548
|
{
|
|
@@ -19562,8 +19638,8 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19562
19638
|
}
|
|
19563
19639
|
}
|
|
19564
19640
|
) }),
|
|
19565
|
-
showSubmitButton && /* @__PURE__ */ jsx("button", { className: "exercise-submit-btn", onClick: handleSubmit, children: "Check Answer" })
|
|
19566
|
-
] }) }) });
|
|
19641
|
+
showSubmitButton && /* @__PURE__ */ jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" })
|
|
19642
|
+
] }) }) }) });
|
|
19567
19643
|
});
|
|
19568
19644
|
ExerciseRenderer.displayName = "ExerciseRenderer";
|
|
19569
19645
|
function renderExercise(options) {
|
|
@@ -19620,6 +19696,7 @@ export {
|
|
|
19620
19696
|
EngineError,
|
|
19621
19697
|
ExerciseDocumentSchema,
|
|
19622
19698
|
ExerciseJSONSchema,
|
|
19699
|
+
ExerciseLockProvider,
|
|
19623
19700
|
ExerciseMetaSchema,
|
|
19624
19701
|
ExerciseRenderer,
|
|
19625
19702
|
ExpressionValidationSchema,
|
|
@@ -19842,6 +19919,7 @@ export {
|
|
|
19842
19919
|
toJSON,
|
|
19843
19920
|
tokenize,
|
|
19844
19921
|
tokenizeSafe,
|
|
19922
|
+
useExerciseLocked,
|
|
19845
19923
|
useSound,
|
|
19846
19924
|
validate,
|
|
19847
19925
|
validateBooleanExpression,
|