@mathscan/math-exercise-engine 1.2.11 → 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.
@@ -17160,12 +17160,17 @@ function getTextInputs(container) {
17160
17160
  function canTakeInput(el) {
17161
17161
  return !el.readOnly && !el.disabled;
17162
17162
  }
17163
+ function canAdvanceTo(el) {
17164
+ return !el.disabled && el.tabIndex >= 0;
17165
+ }
17163
17166
  function findNextInput(container, currentKey) {
17164
17167
  const inputs = getTextInputs(container);
17165
17168
  const currentIndex = inputs.findIndex((el) => el.id === getInputElementId(currentKey));
17166
17169
  if (currentIndex === -1) return null;
17167
- return inputs.slice(currentIndex + 1).find(canTakeInput) ?? null;
17170
+ return inputs.slice(currentIndex + 1).find(canAdvanceTo) ?? null;
17168
17171
  }
17172
+ const KeypadContext = React.createContext(false);
17173
+ const useHasKeypad = () => React.useContext(KeypadContext);
17169
17174
  const InlineInput = ({
17170
17175
  widgetId,
17171
17176
  value,
@@ -17182,6 +17187,7 @@ const InlineInput = ({
17182
17187
  }) => {
17183
17188
  const playSound = useSound();
17184
17189
  const locked = useExerciseLocked();
17190
+ const hasKeypad = useHasKeypad();
17185
17191
  const handleChange = React.useCallback((newValue, fromDrop = false, sourceOptionId) => {
17186
17192
  onChange(widgetId, newValue, fromDrop, sourceOptionId);
17187
17193
  }, [onChange, widgetId]);
@@ -17236,6 +17242,7 @@ const InlineInput = ({
17236
17242
  },
17237
17243
  onFocus: () => onFocus?.(widgetId),
17238
17244
  readOnly: locked,
17245
+ inputMode: hasKeypad ? "none" : void 0,
17239
17246
  disabled,
17240
17247
  maxLength: expectedLength,
17241
17248
  style: { width: dynamicWidth },
@@ -17438,6 +17445,7 @@ const FractionInput = ({
17438
17445
  }) => {
17439
17446
  const playSound = useSound();
17440
17447
  const locked = useExerciseLocked();
17448
+ const hasKeypad = useHasKeypad();
17441
17449
  const numKey = getFractionKey(widgetId, "num");
17442
17450
  const denKey = getFractionKey(widgetId, "den");
17443
17451
  const numValue = values[numKey] ?? "";
@@ -17491,6 +17499,7 @@ const FractionInput = ({
17491
17499
  },
17492
17500
  onFocus: () => onFocus?.(subKey),
17493
17501
  readOnly: locked,
17502
+ inputMode: hasKeypad ? "none" : void 0,
17494
17503
  maxLength: placeholder ? Math.max(20, placeholder.length) : 4,
17495
17504
  size: placeholder ? placeholder.length : void 0,
17496
17505
  "aria-label": label,
@@ -19701,7 +19710,7 @@ function isGeometryWidget(widget) {
19701
19710
  return widget.type === JSON_TYPES.GEOMETRY;
19702
19711
  }
19703
19712
  const GeometryWidget = React.lazy(async () => ({
19704
- default: (await Promise.resolve().then(() => require("./GeometryWidget-BTPjXJz-.js"))).GeometryWidget
19713
+ default: (await Promise.resolve().then(() => require("./GeometryWidget-CMJTNSAG.js"))).GeometryWidget
19705
19714
  }));
19706
19715
  const WidgetRenderer = ({
19707
19716
  widget,
@@ -22397,6 +22406,7 @@ const TOOL_ASPECT_RATIO = {
22397
22406
  "set-square": SET_SQUARE_ASPECT_RATIO
22398
22407
  };
22399
22408
  const FLIP_BTN_EXTRA = 44;
22409
+ const INPUT_FINISH_IDLE_MS = 1e3;
22400
22410
  function warnOnInvalidThemeColors(theme) {
22401
22411
  if (process.env.NODE_ENV === "production" || !theme) return;
22402
22412
  if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return;
@@ -22434,6 +22444,9 @@ function hasJustFilledBox(json, key2, previous, value) {
22434
22444
  if (maxLength === void 0) return false;
22435
22445
  return String(previous ?? "").length < maxLength && String(value).length >= maxLength;
22436
22446
  }
22447
+ function isTypedInputKey(json, key2) {
22448
+ return json.widgets[key2]?.type === JSON_TYPES.INLINE_INPUT || Boolean(parseFractionKey(key2));
22449
+ }
22437
22450
  function buildInitialValuesSignature(json) {
22438
22451
  return JSON.stringify({
22439
22452
  widgets: json.widgets,
@@ -22519,6 +22532,24 @@ const ExerciseRenderer = React.forwardRef(({
22519
22532
  const valuesRef = React.useRef(values);
22520
22533
  valuesRef.current = values;
22521
22534
  const touchedRef = React.useRef(/* @__PURE__ */ new Set());
22535
+ const pendingRef = React.useRef(/* @__PURE__ */ new Map());
22536
+ const onChangeRef = React.useRef(onChange);
22537
+ onChangeRef.current = onChange;
22538
+ const clearPending = (key2) => {
22539
+ const timer = pendingRef.current.get(key2);
22540
+ if (timer === void 0) return false;
22541
+ clearTimeout(timer);
22542
+ pendingRef.current.delete(key2);
22543
+ return true;
22544
+ };
22545
+ const clearAllPending = () => {
22546
+ pendingRef.current.forEach((timer) => clearTimeout(timer));
22547
+ pendingRef.current = /* @__PURE__ */ new Map();
22548
+ };
22549
+ const finishPending = (key2) => {
22550
+ if (clearPending(key2)) onChangeRef.current?.(valuesRef.current);
22551
+ };
22552
+ React.useEffect(() => clearAllPending, []);
22522
22553
  const [loadedValuesSignature, setLoadedValuesSignature] = React.useState(initialValuesSignature);
22523
22554
  const [droppedSources, setDroppedSources] = React.useState(/* @__PURE__ */ new Map());
22524
22555
  const [focusedInputId, setFocusedInputId] = React.useState(null);
@@ -22549,11 +22580,13 @@ const ExerciseRenderer = React.forwardRef(({
22549
22580
  setLoadedValuesSignature(initialValuesSignature);
22550
22581
  touchedRef.current = /* @__PURE__ */ new Set();
22551
22582
  setTutorialRun(null);
22583
+ clearAllPending();
22552
22584
  }, [json, initialValuesSignature, loadedValuesSignature]);
22553
22585
  const locked = validationDetails !== null || externallyLocked;
22554
22586
  const lastResultRef = React.useRef(null);
22555
22587
  const runSubmit = () => {
22556
22588
  if (locked) return lastResultRef.current ?? validate(json, values);
22589
+ clearAllPending();
22557
22590
  const result = validate(json, values);
22558
22591
  playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
22559
22592
  const details = { ...result.details };
@@ -22591,6 +22624,11 @@ const ExerciseRenderer = React.forwardRef(({
22591
22624
  for (const id of answerable) {
22592
22625
  if (!touchedRef.current.has(id)) missingSet.add(id);
22593
22626
  }
22627
+ const isAnyFilled = answerable.some((id) => !missingSet.has(id));
22628
+ for (const key2 of pendingRef.current.keys()) {
22629
+ const id = toBaseId(key2);
22630
+ if (answerable.includes(id)) missingSet.add(id);
22631
+ }
22594
22632
  const missing = [...missingSet];
22595
22633
  const filledIds = answerable.filter((id) => !missingSet.has(id));
22596
22634
  return {
@@ -22599,7 +22637,7 @@ const ExerciseRenderer = React.forwardRef(({
22599
22637
  missing,
22600
22638
  filledIds,
22601
22639
  isComplete: answerable.length > 0 && missing.length === 0,
22602
- isAnyFilled: filledIds.length > 0
22640
+ isAnyFilled
22603
22641
  };
22604
22642
  },
22605
22643
  reset: () => {
@@ -22613,10 +22651,12 @@ const ExerciseRenderer = React.forwardRef(({
22613
22651
  setRevealed(false);
22614
22652
  setLoadedValuesSignature(initialValuesSignature);
22615
22653
  touchedRef.current = /* @__PURE__ */ new Set();
22654
+ clearAllPending();
22616
22655
  },
22617
22656
  revealAnswers: () => {
22618
22657
  const revealedValues = buildRevealedValues(json);
22619
22658
  if (Object.keys(revealedValues).length === 0) return;
22659
+ clearAllPending();
22620
22660
  playSound("answer-reveal");
22621
22661
  const details = {};
22622
22662
  for (const [widgetId, userValue] of Object.entries(revealedValues)) {
@@ -22645,6 +22685,7 @@ const ExerciseRenderer = React.forwardRef(({
22645
22685
  target?.focus();
22646
22686
  },
22647
22687
  hasTutorial: () => tutorial !== null,
22688
+ tutorialId: () => tutorial?.id ?? null,
22648
22689
  startTutorial: (options = {}) => {
22649
22690
  if (!tutorial || !rootRef.current) return false;
22650
22691
  setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
@@ -22675,11 +22716,28 @@ const ExerciseRenderer = React.forwardRef(({
22675
22716
  }
22676
22717
  return next2;
22677
22718
  });
22719
+ if (!silent && isTypedInputKey(json, widgetId)) {
22720
+ clearPending(widgetId);
22721
+ const maxLength = getMaxInputLength(json, widgetId);
22722
+ const isFull = maxLength !== void 0 && String(value).length >= maxLength;
22723
+ if (!fromDrop && value !== "" && !isFull) {
22724
+ pendingRef.current.set(
22725
+ widgetId,
22726
+ setTimeout(() => finishPending(widgetId), INPUT_FINISH_IDLE_MS)
22727
+ );
22728
+ }
22729
+ }
22678
22730
  onChange?.(next);
22679
22731
  if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
22680
22732
  focusNextInput(widgetId);
22681
22733
  }
22682
22734
  };
22735
+ const handleInputFocus = (key2) => {
22736
+ for (const pendingKey of [...pendingRef.current.keys()]) {
22737
+ if (pendingKey !== key2) finishPending(pendingKey);
22738
+ }
22739
+ setFocusedInputId(key2);
22740
+ };
22683
22741
  const handleSubmit = () => {
22684
22742
  runSubmit();
22685
22743
  };
@@ -22812,7 +22870,7 @@ const ExerciseRenderer = React.forwardRef(({
22812
22870
  if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
22813
22871
  return { tools: list, toolsMinHeight: maxH };
22814
22872
  }, [rulerWidget, protractorWidget, setSquareWidget]);
22815
- return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22873
+ return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsxRuntime.jsx(KeypadContext.Provider, { value: Boolean(numericKeypadWidget), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22816
22874
  /* @__PURE__ */ jsxRuntime.jsxs(
22817
22875
  "div",
22818
22876
  {
@@ -22837,7 +22895,7 @@ const ExerciseRenderer = React.forwardRef(({
22837
22895
  droppedInputs,
22838
22896
  validationDetails,
22839
22897
  onChange: handleChange,
22840
- onInputFocus: setFocusedInputId,
22898
+ onInputFocus: handleInputFocus,
22841
22899
  resetKey
22842
22900
  },
22843
22901
  item.id
@@ -22853,7 +22911,7 @@ const ExerciseRenderer = React.forwardRef(({
22853
22911
  droppedInputs,
22854
22912
  validationDetails,
22855
22913
  onChange: handleChange,
22856
- onInputFocus: setFocusedInputId,
22914
+ onInputFocus: handleInputFocus,
22857
22915
  resetKey
22858
22916
  },
22859
22917
  item.id
@@ -22870,7 +22928,7 @@ const ExerciseRenderer = React.forwardRef(({
22870
22928
  droppedInputs,
22871
22929
  validationDetails,
22872
22930
  onChange: handleChange,
22873
- onInputFocus: setFocusedInputId,
22931
+ onInputFocus: handleInputFocus,
22874
22932
  resetKey
22875
22933
  },
22876
22934
  item.id
@@ -22888,9 +22946,10 @@ const ExerciseRenderer = React.forwardRef(({
22888
22946
  onChange: () => {
22889
22947
  },
22890
22948
  onKeypadChange: (newValue) => {
22891
- if (focusedInputId) {
22892
- handleChange(focusedInputId, newValue, false);
22893
- }
22949
+ if (!focusedInputId) return;
22950
+ const maxLength = getMaxInputLength(json, focusedInputId);
22951
+ if (maxLength !== void 0 && newValue.length > maxLength) return;
22952
+ handleChange(focusedInputId, newValue, false);
22894
22953
  }
22895
22954
  }
22896
22955
  ) }),
@@ -22923,7 +22982,7 @@ const ExerciseRenderer = React.forwardRef(({
22923
22982
  },
22924
22983
  tutorialRun.run
22925
22984
  )
22926
- ] }) }) }) });
22985
+ ] }) }) }) }) });
22927
22986
  });
22928
22987
  ExerciseRenderer.displayName = "ExerciseRenderer";
22929
22988
  function renderExercise(options) {
@@ -23267,4 +23326,4 @@ exports.validateUniqueIds = validateUniqueIds;
23267
23326
  exports.validateVisualFractionCapacity = validateVisualFractionCapacity;
23268
23327
  exports.validateWidgetJSON = validateWidgetJSON;
23269
23328
  exports.validateWidgetJSONSafe = validateWidgetJSONSafe;
23270
- //# sourceMappingURL=index-C3qDr71I.js.map
23329
+ //# sourceMappingURL=index-CbKXPRsS.js.map