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