@mathscan/math-exercise-engine 1.2.12 → 1.2.13
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 +3 -1
- package/dist/{GeometryWidget-BMOiQMvj.js → GeometryWidget-CMJTNSAG.js} +2 -2
- package/dist/{GeometryWidget-BMOiQMvj.js.map → GeometryWidget-CMJTNSAG.js.map} +1 -1
- package/dist/{GeometryWidget-Qi5s7Lql.mjs → GeometryWidget-h2yNZsas.mjs} +2 -2
- package/dist/{GeometryWidget-Qi5s7Lql.mjs.map → GeometryWidget-h2yNZsas.mjs.map} +1 -1
- package/dist/{index-BtJRsuwL.mjs → index-CAvdtiDQ.mjs} +70 -12
- package/dist/index-CAvdtiDQ.mjs.map +1 -0
- package/dist/{index-Di53h19p.js → index-CbKXPRsS.js} +70 -12
- package/dist/index-CbKXPRsS.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/index-BtJRsuwL.mjs.map +0 -1
- package/dist/index-Di53h19p.js.map +0 -1
package/README.md
CHANGED
|
@@ -120,11 +120,13 @@ exerciseRef.current?.focusFirstInput(); // Focus first editable input field
|
|
|
120
120
|
- **`submit()`**: Performs validation, returns `ValidationResult`, triggers `onSubmit`, and **locks the exercise** (see below). Calling it again while locked is a no-op that replays the same `ValidationResult` — no sound, no `onSubmit`
|
|
121
121
|
- **`getValues()`**: Returns `Record<string, UserAnswerValue>` of all current input values
|
|
122
122
|
- **`getCompletion()`**: Returns `{ total, filled, missing, filledIds, isComplete, isAnyFilled }` — use it to decide when to enable your submit button
|
|
123
|
+
- A typed input (or fraction slot) counts as filled once it has a value **and** the student finished it: the box is full (`digits` / `length`), the student moved to another input, or 1 s (`INPUT_FINISH_IDLE_MS`) passed without a keystroke. It never depends on the answer, so a wrong answer of any length can be submitted and the button can't hint at the answer's length. `isAnyFilled` only needs a value.
|
|
124
|
+
- When an input finishes without a value change (pause or moving on), `onChange` fires again with the same values so a host reading `getCompletion()` there sees the update.
|
|
123
125
|
- **`revealAnswers()`**: Fills every widget with its correct answer and **locks the exercise**. No-op for display-only exercises
|
|
124
126
|
- **`reset()`**: Clears all input fields and **unlocks** the exercise
|
|
125
127
|
- **`focusFirstInput()`**: Focuses the first editable input of *this* renderer. Skips read-only/disabled fields, so it does nothing while the exercise is locked
|
|
126
128
|
|
|
127
|
-
**Auto-advance:** when a typed input's box is full (`digits` / `length` characters), the caret moves to the next editable input of the exercise, in on-screen order. It never depends on the answer, so it can't hint at the answer's length. It moves only on the keystroke that fills the box, so replacing a full value doesn't move it again. It doesn't move on a dropped value, from a fraction slot (no box length), or on the last input.
|
|
129
|
+
**Auto-advance:** when a typed input's box is full (`digits` / `length` characters), the caret moves to the next editable input of the exercise, in on-screen order. It never depends on the answer, so it can't hint at the answer's length. It moves only on the keystroke that fills the box, so replacing a full value doesn't move it again. It doesn't move on a dropped value, from a fraction slot (no box length), or on the last input. With a `NUMERIC_KEYPAD`, inputs get `inputMode="none"`, so neither a tap nor auto-advance opens the device keyboard over the keypad.
|
|
128
130
|
|
|
129
131
|
## 🔒 Locking After a Check
|
|
130
132
|
|
|
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
4
4
|
const React = require("react");
|
|
5
5
|
const react = require("@mathscan/geometry-engine/react");
|
|
6
6
|
const validation = require("@mathscan/geometry-engine/validation");
|
|
7
|
-
const index = require("./index-
|
|
7
|
+
const index = require("./index-CbKXPRsS.js");
|
|
8
8
|
const GeometryWidget = ({
|
|
9
9
|
widgetId,
|
|
10
10
|
widget,
|
|
@@ -47,4 +47,4 @@ const GeometryWidget = ({
|
|
|
47
47
|
) });
|
|
48
48
|
};
|
|
49
49
|
exports.GeometryWidget = GeometryWidget;
|
|
50
|
-
//# sourceMappingURL=GeometryWidget-
|
|
50
|
+
//# sourceMappingURL=GeometryWidget-CMJTNSAG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeometryWidget-
|
|
1
|
+
{"version":3,"file":"GeometryWidget-CMJTNSAG.js","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["useExerciseLocked","useMemo","readGeometryAttempt","useEffect","logRejectedGeometryAttempt","jsx","StudentGeometryExercise","serializeGeometryState","issues"],"mappings":";;;;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiBA,MAAAA,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAWC,MAAAA,QAAQ,MAAMC,MAAAA,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7EC,QAAAA,UAAU,MAAM;AACd,QAAI,OAAQC,OAAAA,2BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACEC,2BAAAA,IAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAAA,2BAAAA;AAAAA,IAACC,MAAAA;AAAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAUC,WAAAA,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACC,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;;"}
|
|
@@ -2,7 +2,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMemo, useEffect } from "react";
|
|
3
3
|
import { StudentGeometryExercise } from "@mathscan/geometry-engine/react";
|
|
4
4
|
import { serializeGeometryState } from "@mathscan/geometry-engine/validation";
|
|
5
|
-
import { u as useExerciseLocked, r as readGeometryAttempt, l as logRejectedGeometryAttempt } from "./index-
|
|
5
|
+
import { u as useExerciseLocked, r as readGeometryAttempt, l as logRejectedGeometryAttempt } from "./index-CAvdtiDQ.mjs";
|
|
6
6
|
const GeometryWidget = ({
|
|
7
7
|
widgetId,
|
|
8
8
|
widget,
|
|
@@ -47,4 +47,4 @@ const GeometryWidget = ({
|
|
|
47
47
|
export {
|
|
48
48
|
GeometryWidget
|
|
49
49
|
};
|
|
50
|
-
//# sourceMappingURL=GeometryWidget-
|
|
50
|
+
//# sourceMappingURL=GeometryWidget-h2yNZsas.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GeometryWidget-
|
|
1
|
+
{"version":3,"file":"GeometryWidget-h2yNZsas.mjs","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["issues"],"mappings":";;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiB,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAW,QAAQ,MAAM,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7E,YAAU,MAAM;AACd,QAAI,OAAQ,4BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACE,oBAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAU,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACA,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;"}
|
|
@@ -17159,12 +17159,17 @@ function getTextInputs(container) {
|
|
|
17159
17159
|
function canTakeInput(el) {
|
|
17160
17160
|
return !el.readOnly && !el.disabled;
|
|
17161
17161
|
}
|
|
17162
|
+
function canAdvanceTo(el) {
|
|
17163
|
+
return !el.disabled && el.tabIndex >= 0;
|
|
17164
|
+
}
|
|
17162
17165
|
function findNextInput(container, currentKey) {
|
|
17163
17166
|
const inputs = getTextInputs(container);
|
|
17164
17167
|
const currentIndex = inputs.findIndex((el) => el.id === getInputElementId(currentKey));
|
|
17165
17168
|
if (currentIndex === -1) return null;
|
|
17166
|
-
return inputs.slice(currentIndex + 1).find(
|
|
17169
|
+
return inputs.slice(currentIndex + 1).find(canAdvanceTo) ?? null;
|
|
17167
17170
|
}
|
|
17171
|
+
const KeypadContext = createContext(false);
|
|
17172
|
+
const useHasKeypad = () => useContext(KeypadContext);
|
|
17168
17173
|
const InlineInput = ({
|
|
17169
17174
|
widgetId,
|
|
17170
17175
|
value,
|
|
@@ -17181,6 +17186,7 @@ const InlineInput = ({
|
|
|
17181
17186
|
}) => {
|
|
17182
17187
|
const playSound = useSound();
|
|
17183
17188
|
const locked = useExerciseLocked();
|
|
17189
|
+
const hasKeypad = useHasKeypad();
|
|
17184
17190
|
const handleChange = useCallback((newValue, fromDrop = false, sourceOptionId) => {
|
|
17185
17191
|
onChange(widgetId, newValue, fromDrop, sourceOptionId);
|
|
17186
17192
|
}, [onChange, widgetId]);
|
|
@@ -17235,6 +17241,7 @@ const InlineInput = ({
|
|
|
17235
17241
|
},
|
|
17236
17242
|
onFocus: () => onFocus?.(widgetId),
|
|
17237
17243
|
readOnly: locked,
|
|
17244
|
+
inputMode: hasKeypad ? "none" : void 0,
|
|
17238
17245
|
disabled,
|
|
17239
17246
|
maxLength: expectedLength,
|
|
17240
17247
|
style: { width: dynamicWidth },
|
|
@@ -17437,6 +17444,7 @@ const FractionInput = ({
|
|
|
17437
17444
|
}) => {
|
|
17438
17445
|
const playSound = useSound();
|
|
17439
17446
|
const locked = useExerciseLocked();
|
|
17447
|
+
const hasKeypad = useHasKeypad();
|
|
17440
17448
|
const numKey = getFractionKey(widgetId, "num");
|
|
17441
17449
|
const denKey = getFractionKey(widgetId, "den");
|
|
17442
17450
|
const numValue = values[numKey] ?? "";
|
|
@@ -17490,6 +17498,7 @@ const FractionInput = ({
|
|
|
17490
17498
|
},
|
|
17491
17499
|
onFocus: () => onFocus?.(subKey),
|
|
17492
17500
|
readOnly: locked,
|
|
17501
|
+
inputMode: hasKeypad ? "none" : void 0,
|
|
17493
17502
|
maxLength: placeholder ? Math.max(20, placeholder.length) : 4,
|
|
17494
17503
|
size: placeholder ? placeholder.length : void 0,
|
|
17495
17504
|
"aria-label": label,
|
|
@@ -19700,7 +19709,7 @@ function isGeometryWidget(widget) {
|
|
|
19700
19709
|
return widget.type === JSON_TYPES.GEOMETRY;
|
|
19701
19710
|
}
|
|
19702
19711
|
const GeometryWidget = React.lazy(async () => ({
|
|
19703
|
-
default: (await import("./GeometryWidget-
|
|
19712
|
+
default: (await import("./GeometryWidget-h2yNZsas.mjs")).GeometryWidget
|
|
19704
19713
|
}));
|
|
19705
19714
|
const WidgetRenderer = ({
|
|
19706
19715
|
widget,
|
|
@@ -22396,6 +22405,7 @@ const TOOL_ASPECT_RATIO = {
|
|
|
22396
22405
|
"set-square": SET_SQUARE_ASPECT_RATIO
|
|
22397
22406
|
};
|
|
22398
22407
|
const FLIP_BTN_EXTRA = 44;
|
|
22408
|
+
const INPUT_FINISH_IDLE_MS = 1e3;
|
|
22399
22409
|
function warnOnInvalidThemeColors(theme) {
|
|
22400
22410
|
if (process.env.NODE_ENV === "production" || !theme) return;
|
|
22401
22411
|
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return;
|
|
@@ -22433,6 +22443,9 @@ function hasJustFilledBox(json, key2, previous, value) {
|
|
|
22433
22443
|
if (maxLength === void 0) return false;
|
|
22434
22444
|
return String(previous ?? "").length < maxLength && String(value).length >= maxLength;
|
|
22435
22445
|
}
|
|
22446
|
+
function isTypedInputKey(json, key2) {
|
|
22447
|
+
return json.widgets[key2]?.type === JSON_TYPES.INLINE_INPUT || Boolean(parseFractionKey(key2));
|
|
22448
|
+
}
|
|
22436
22449
|
function buildInitialValuesSignature(json) {
|
|
22437
22450
|
return JSON.stringify({
|
|
22438
22451
|
widgets: json.widgets,
|
|
@@ -22518,6 +22531,24 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22518
22531
|
const valuesRef = useRef(values);
|
|
22519
22532
|
valuesRef.current = values;
|
|
22520
22533
|
const touchedRef = useRef(/* @__PURE__ */ new Set());
|
|
22534
|
+
const pendingRef = useRef(/* @__PURE__ */ new Map());
|
|
22535
|
+
const onChangeRef = useRef(onChange);
|
|
22536
|
+
onChangeRef.current = onChange;
|
|
22537
|
+
const clearPending = (key2) => {
|
|
22538
|
+
const timer = pendingRef.current.get(key2);
|
|
22539
|
+
if (timer === void 0) return false;
|
|
22540
|
+
clearTimeout(timer);
|
|
22541
|
+
pendingRef.current.delete(key2);
|
|
22542
|
+
return true;
|
|
22543
|
+
};
|
|
22544
|
+
const clearAllPending = () => {
|
|
22545
|
+
pendingRef.current.forEach((timer) => clearTimeout(timer));
|
|
22546
|
+
pendingRef.current = /* @__PURE__ */ new Map();
|
|
22547
|
+
};
|
|
22548
|
+
const finishPending = (key2) => {
|
|
22549
|
+
if (clearPending(key2)) onChangeRef.current?.(valuesRef.current);
|
|
22550
|
+
};
|
|
22551
|
+
useEffect(() => clearAllPending, []);
|
|
22521
22552
|
const [loadedValuesSignature, setLoadedValuesSignature] = useState(initialValuesSignature);
|
|
22522
22553
|
const [droppedSources, setDroppedSources] = useState(/* @__PURE__ */ new Map());
|
|
22523
22554
|
const [focusedInputId, setFocusedInputId] = useState(null);
|
|
@@ -22548,11 +22579,13 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22548
22579
|
setLoadedValuesSignature(initialValuesSignature);
|
|
22549
22580
|
touchedRef.current = /* @__PURE__ */ new Set();
|
|
22550
22581
|
setTutorialRun(null);
|
|
22582
|
+
clearAllPending();
|
|
22551
22583
|
}, [json, initialValuesSignature, loadedValuesSignature]);
|
|
22552
22584
|
const locked = validationDetails !== null || externallyLocked;
|
|
22553
22585
|
const lastResultRef = useRef(null);
|
|
22554
22586
|
const runSubmit = () => {
|
|
22555
22587
|
if (locked) return lastResultRef.current ?? validate(json, values);
|
|
22588
|
+
clearAllPending();
|
|
22556
22589
|
const result = validate(json, values);
|
|
22557
22590
|
playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
|
|
22558
22591
|
const details = { ...result.details };
|
|
@@ -22590,6 +22623,11 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22590
22623
|
for (const id of answerable) {
|
|
22591
22624
|
if (!touchedRef.current.has(id)) missingSet.add(id);
|
|
22592
22625
|
}
|
|
22626
|
+
const isAnyFilled = answerable.some((id) => !missingSet.has(id));
|
|
22627
|
+
for (const key2 of pendingRef.current.keys()) {
|
|
22628
|
+
const id = toBaseId(key2);
|
|
22629
|
+
if (answerable.includes(id)) missingSet.add(id);
|
|
22630
|
+
}
|
|
22593
22631
|
const missing = [...missingSet];
|
|
22594
22632
|
const filledIds = answerable.filter((id) => !missingSet.has(id));
|
|
22595
22633
|
return {
|
|
@@ -22598,7 +22636,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22598
22636
|
missing,
|
|
22599
22637
|
filledIds,
|
|
22600
22638
|
isComplete: answerable.length > 0 && missing.length === 0,
|
|
22601
|
-
isAnyFilled
|
|
22639
|
+
isAnyFilled
|
|
22602
22640
|
};
|
|
22603
22641
|
},
|
|
22604
22642
|
reset: () => {
|
|
@@ -22612,10 +22650,12 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22612
22650
|
setRevealed(false);
|
|
22613
22651
|
setLoadedValuesSignature(initialValuesSignature);
|
|
22614
22652
|
touchedRef.current = /* @__PURE__ */ new Set();
|
|
22653
|
+
clearAllPending();
|
|
22615
22654
|
},
|
|
22616
22655
|
revealAnswers: () => {
|
|
22617
22656
|
const revealedValues = buildRevealedValues(json);
|
|
22618
22657
|
if (Object.keys(revealedValues).length === 0) return;
|
|
22658
|
+
clearAllPending();
|
|
22619
22659
|
playSound("answer-reveal");
|
|
22620
22660
|
const details = {};
|
|
22621
22661
|
for (const [widgetId, userValue] of Object.entries(revealedValues)) {
|
|
@@ -22675,11 +22715,28 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22675
22715
|
}
|
|
22676
22716
|
return next2;
|
|
22677
22717
|
});
|
|
22718
|
+
if (!silent && isTypedInputKey(json, widgetId)) {
|
|
22719
|
+
clearPending(widgetId);
|
|
22720
|
+
const maxLength = getMaxInputLength(json, widgetId);
|
|
22721
|
+
const isFull = maxLength !== void 0 && String(value).length >= maxLength;
|
|
22722
|
+
if (!fromDrop && value !== "" && !isFull) {
|
|
22723
|
+
pendingRef.current.set(
|
|
22724
|
+
widgetId,
|
|
22725
|
+
setTimeout(() => finishPending(widgetId), INPUT_FINISH_IDLE_MS)
|
|
22726
|
+
);
|
|
22727
|
+
}
|
|
22728
|
+
}
|
|
22678
22729
|
onChange?.(next);
|
|
22679
22730
|
if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
|
|
22680
22731
|
focusNextInput(widgetId);
|
|
22681
22732
|
}
|
|
22682
22733
|
};
|
|
22734
|
+
const handleInputFocus = (key2) => {
|
|
22735
|
+
for (const pendingKey of [...pendingRef.current.keys()]) {
|
|
22736
|
+
if (pendingKey !== key2) finishPending(pendingKey);
|
|
22737
|
+
}
|
|
22738
|
+
setFocusedInputId(key2);
|
|
22739
|
+
};
|
|
22683
22740
|
const handleSubmit = () => {
|
|
22684
22741
|
runSubmit();
|
|
22685
22742
|
};
|
|
@@ -22812,7 +22869,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22812
22869
|
if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
|
|
22813
22870
|
return { tools: list, toolsMinHeight: maxH };
|
|
22814
22871
|
}, [rulerWidget, protractorWidget, setSquareWidget]);
|
|
22815
|
-
return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
|
|
22872
|
+
return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsx(KeypadContext.Provider, { value: Boolean(numericKeypadWidget), children: /* @__PURE__ */ jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
|
|
22816
22873
|
/* @__PURE__ */ jsxs(
|
|
22817
22874
|
"div",
|
|
22818
22875
|
{
|
|
@@ -22837,7 +22894,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22837
22894
|
droppedInputs,
|
|
22838
22895
|
validationDetails,
|
|
22839
22896
|
onChange: handleChange,
|
|
22840
|
-
onInputFocus:
|
|
22897
|
+
onInputFocus: handleInputFocus,
|
|
22841
22898
|
resetKey
|
|
22842
22899
|
},
|
|
22843
22900
|
item.id
|
|
@@ -22853,7 +22910,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22853
22910
|
droppedInputs,
|
|
22854
22911
|
validationDetails,
|
|
22855
22912
|
onChange: handleChange,
|
|
22856
|
-
onInputFocus:
|
|
22913
|
+
onInputFocus: handleInputFocus,
|
|
22857
22914
|
resetKey
|
|
22858
22915
|
},
|
|
22859
22916
|
item.id
|
|
@@ -22870,7 +22927,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22870
22927
|
droppedInputs,
|
|
22871
22928
|
validationDetails,
|
|
22872
22929
|
onChange: handleChange,
|
|
22873
|
-
onInputFocus:
|
|
22930
|
+
onInputFocus: handleInputFocus,
|
|
22874
22931
|
resetKey
|
|
22875
22932
|
},
|
|
22876
22933
|
item.id
|
|
@@ -22888,9 +22945,10 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22888
22945
|
onChange: () => {
|
|
22889
22946
|
},
|
|
22890
22947
|
onKeypadChange: (newValue) => {
|
|
22891
|
-
if (focusedInputId)
|
|
22892
|
-
|
|
22893
|
-
|
|
22948
|
+
if (!focusedInputId) return;
|
|
22949
|
+
const maxLength = getMaxInputLength(json, focusedInputId);
|
|
22950
|
+
if (maxLength !== void 0 && newValue.length > maxLength) return;
|
|
22951
|
+
handleChange(focusedInputId, newValue, false);
|
|
22894
22952
|
}
|
|
22895
22953
|
}
|
|
22896
22954
|
) }),
|
|
@@ -22923,7 +22981,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
22923
22981
|
},
|
|
22924
22982
|
tutorialRun.run
|
|
22925
22983
|
)
|
|
22926
|
-
] }) }) }) });
|
|
22984
|
+
] }) }) }) }) });
|
|
22927
22985
|
});
|
|
22928
22986
|
ExerciseRenderer.displayName = "ExerciseRenderer";
|
|
22929
22987
|
function renderExercise(options) {
|
|
@@ -23269,4 +23327,4 @@ export {
|
|
|
23269
23327
|
extractVariables as y,
|
|
23270
23328
|
validateBooleanExpression as z
|
|
23271
23329
|
};
|
|
23272
|
-
//# sourceMappingURL=index-
|
|
23330
|
+
//# sourceMappingURL=index-CAvdtiDQ.mjs.map
|