@mathscan/math-exercise-engine 1.1.17 → 1.2.1
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 +186 -164
- package/dist/index.js +296 -239
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +296 -239
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +84 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -394,13 +394,9 @@ const MATCHER = chevrotain.createToken({
|
|
|
394
394
|
name: "MATCHER",
|
|
395
395
|
pattern: /MATCHER/
|
|
396
396
|
});
|
|
397
|
-
const
|
|
398
|
-
name: "
|
|
399
|
-
pattern: /
|
|
400
|
-
});
|
|
401
|
-
const BOTTOM_ROW = chevrotain.createToken({
|
|
402
|
-
name: "BOTTOM_ROW",
|
|
403
|
-
pattern: /BOTTOM_ROW/
|
|
397
|
+
const GROUP = chevrotain.createToken({
|
|
398
|
+
name: "GROUP",
|
|
399
|
+
pattern: /GROUP/
|
|
404
400
|
});
|
|
405
401
|
const ITEM = chevrotain.createToken({
|
|
406
402
|
name: "ITEM",
|
|
@@ -556,8 +552,7 @@ const allTokens = [
|
|
|
556
552
|
ARROW,
|
|
557
553
|
TABLE,
|
|
558
554
|
ROW,
|
|
559
|
-
|
|
560
|
-
BOTTOM_ROW,
|
|
555
|
+
GROUP,
|
|
561
556
|
CELL,
|
|
562
557
|
COUNTER,
|
|
563
558
|
CLOCK,
|
|
@@ -1065,39 +1060,19 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1065
1060
|
this.MANY3(() => {
|
|
1066
1061
|
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1067
1062
|
});
|
|
1068
|
-
this.SUBRULE(this.
|
|
1069
|
-
this.
|
|
1063
|
+
this.SUBRULE(this.pairMatcherGroupSection);
|
|
1064
|
+
this.SUBRULE2(this.pairMatcherGroupSection);
|
|
1070
1065
|
this.SUBRULE(this.matcherAnswers);
|
|
1071
1066
|
this.CONSUME(RBrace);
|
|
1072
1067
|
this.MANY4(() => {
|
|
1073
1068
|
this.OR4([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
1074
1069
|
});
|
|
1075
1070
|
});
|
|
1076
|
-
this.
|
|
1077
|
-
this.MANY(() => {
|
|
1078
|
-
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1079
|
-
});
|
|
1080
|
-
this.CONSUME(TOP_ROW);
|
|
1081
|
-
this.MANY2(() => {
|
|
1082
|
-
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1083
|
-
});
|
|
1084
|
-
this.CONSUME(LBrace);
|
|
1085
|
-
this.MANY3(() => {
|
|
1086
|
-
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1087
|
-
});
|
|
1088
|
-
this.AT_LEAST_ONE(() => {
|
|
1089
|
-
this.SUBRULE(this.matcherItem);
|
|
1090
|
-
});
|
|
1091
|
-
this.CONSUME(RBrace);
|
|
1092
|
-
this.MANY4(() => {
|
|
1093
|
-
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1094
|
-
});
|
|
1095
|
-
});
|
|
1096
|
-
this.pairMatcherBottomSection = this.RULE("pairMatcherBottomSection", () => {
|
|
1071
|
+
this.pairMatcherGroupSection = this.RULE("pairMatcherGroupSection", () => {
|
|
1097
1072
|
this.MANY(() => {
|
|
1098
1073
|
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1099
1074
|
});
|
|
1100
|
-
this.CONSUME(
|
|
1075
|
+
this.CONSUME(GROUP);
|
|
1101
1076
|
this.MANY2(() => {
|
|
1102
1077
|
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1103
1078
|
});
|
|
@@ -2590,20 +2565,24 @@ function isMatcherBlock(value) {
|
|
|
2590
2565
|
const PairMatcherBlockSchema = zod.z.object({
|
|
2591
2566
|
type: zod.z.literal(AST_TYPES.PAIR_MATCHER_BLOCK),
|
|
2592
2567
|
name: zod.z.string().min(1),
|
|
2593
|
-
/**
|
|
2594
|
-
|
|
2595
|
-
/**
|
|
2596
|
-
|
|
2568
|
+
/** Row layout (top/bottom, default) or column layout (left/right). */
|
|
2569
|
+
layout: zod.z.enum(["rows", "columns"]).optional(),
|
|
2570
|
+
/** Exactly two groups, in declaration order — neither more nor fewer. */
|
|
2571
|
+
groups: zod.z.tuple([
|
|
2572
|
+
zod.z.array(MatcherItemSchema).min(1),
|
|
2573
|
+
zod.z.array(MatcherItemSchema).min(1)
|
|
2574
|
+
]),
|
|
2597
2575
|
/**
|
|
2598
|
-
* Each answer pair is [
|
|
2576
|
+
* Each answer pair is [groups[0] id, groups[1] id].
|
|
2599
2577
|
* At least one pair is required.
|
|
2600
2578
|
*/
|
|
2601
2579
|
answers: zod.z.array(zod.z.tuple([zod.z.string().min(1), zod.z.string().min(1)])).min(1)
|
|
2602
2580
|
}).superRefine((data, ctx) => {
|
|
2581
|
+
const [groupAItems, groupBItems] = data.groups;
|
|
2603
2582
|
const idRegistry = /* @__PURE__ */ new Map();
|
|
2604
2583
|
const sections = [
|
|
2605
|
-
{ key:
|
|
2606
|
-
{ key:
|
|
2584
|
+
{ key: 0, items: groupAItems },
|
|
2585
|
+
{ key: 1, items: groupBItems }
|
|
2607
2586
|
];
|
|
2608
2587
|
for (const { key: key2, items } of sections) {
|
|
2609
2588
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -2613,59 +2592,59 @@ const PairMatcherBlockSchema = zod.z.object({
|
|
|
2613
2592
|
const isCrossSection = existing.section !== key2;
|
|
2614
2593
|
ctx.addIssue({
|
|
2615
2594
|
code: zod.z.ZodIssueCode.custom,
|
|
2616
|
-
message: isCrossSection ? `itemId "${itemId}" appears in both
|
|
2617
|
-
path: [key2, i, "itemId"]
|
|
2595
|
+
message: isCrossSection ? `itemId "${itemId}" appears in both groups — ids must be globally unique` : `Duplicate itemId "${itemId}" in group ${key2}`,
|
|
2596
|
+
path: ["groups", key2, i, "itemId"]
|
|
2618
2597
|
});
|
|
2619
2598
|
} else {
|
|
2620
2599
|
idRegistry.set(itemId, { section: key2, index: i });
|
|
2621
2600
|
}
|
|
2622
2601
|
}
|
|
2623
2602
|
}
|
|
2624
|
-
const
|
|
2625
|
-
const
|
|
2626
|
-
const
|
|
2627
|
-
const
|
|
2628
|
-
data.answers.forEach(([
|
|
2629
|
-
if (!
|
|
2603
|
+
const groupAIds = new Set(groupAItems.map((it) => it.itemId));
|
|
2604
|
+
const groupBIds = new Set(groupBItems.map((it) => it.itemId));
|
|
2605
|
+
const usedGroupAIds = /* @__PURE__ */ new Set();
|
|
2606
|
+
const usedGroupBIds = /* @__PURE__ */ new Set();
|
|
2607
|
+
data.answers.forEach(([aId, bId], i) => {
|
|
2608
|
+
if (!groupAIds.has(aId)) {
|
|
2630
2609
|
ctx.addIssue({
|
|
2631
2610
|
code: zod.z.ZodIssueCode.custom,
|
|
2632
|
-
message: `Answer pair [${
|
|
2611
|
+
message: `Answer pair [${aId}, ${bId}]: "${aId}" is not declared in groups[0]`,
|
|
2633
2612
|
path: ["answers", i, 0]
|
|
2634
2613
|
});
|
|
2635
2614
|
}
|
|
2636
|
-
if (!
|
|
2615
|
+
if (!groupBIds.has(bId)) {
|
|
2637
2616
|
ctx.addIssue({
|
|
2638
2617
|
code: zod.z.ZodIssueCode.custom,
|
|
2639
|
-
message: `Answer pair [${
|
|
2618
|
+
message: `Answer pair [${aId}, ${bId}]: "${bId}" is not declared in groups[1]`,
|
|
2640
2619
|
path: ["answers", i, 1]
|
|
2641
2620
|
});
|
|
2642
2621
|
}
|
|
2643
|
-
if (
|
|
2622
|
+
if (usedGroupAIds.has(aId)) {
|
|
2644
2623
|
ctx.addIssue({
|
|
2645
2624
|
code: zod.z.ZodIssueCode.custom,
|
|
2646
|
-
message: `
|
|
2625
|
+
message: `id "${aId}" appears in more than one answer pair`,
|
|
2647
2626
|
path: ["answers", i, 0]
|
|
2648
2627
|
});
|
|
2649
2628
|
} else {
|
|
2650
|
-
|
|
2629
|
+
usedGroupAIds.add(aId);
|
|
2651
2630
|
}
|
|
2652
|
-
if (
|
|
2631
|
+
if (usedGroupBIds.has(bId)) {
|
|
2653
2632
|
ctx.addIssue({
|
|
2654
2633
|
code: zod.z.ZodIssueCode.custom,
|
|
2655
|
-
message: `
|
|
2634
|
+
message: `id "${bId}" appears in more than one answer pair`,
|
|
2656
2635
|
path: ["answers", i, 1]
|
|
2657
2636
|
});
|
|
2658
2637
|
} else {
|
|
2659
|
-
|
|
2638
|
+
usedGroupBIds.add(bId);
|
|
2660
2639
|
}
|
|
2661
2640
|
});
|
|
2662
2641
|
});
|
|
2663
|
-
function createPairMatcherBlock(name,
|
|
2642
|
+
function createPairMatcherBlock(name, groups, answers, layout) {
|
|
2664
2643
|
return PairMatcherBlockSchema.parse({
|
|
2665
2644
|
type: AST_TYPES.PAIR_MATCHER_BLOCK,
|
|
2666
2645
|
name,
|
|
2667
|
-
|
|
2668
|
-
|
|
2646
|
+
layout,
|
|
2647
|
+
groups,
|
|
2669
2648
|
answers
|
|
2670
2649
|
});
|
|
2671
2650
|
}
|
|
@@ -2852,7 +2831,7 @@ const ExerciseDocumentSchema = zod.z.object({
|
|
|
2852
2831
|
} else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
|
|
2853
2832
|
const pm = block;
|
|
2854
2833
|
record(pm.name, ["lines", bi, "name"]);
|
|
2855
|
-
|
|
2834
|
+
pm.groups.flat().forEach((item, ii) => {
|
|
2856
2835
|
visitInlines(item.inlines, ["lines", bi, "items", ii, "inlines"]);
|
|
2857
2836
|
});
|
|
2858
2837
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
@@ -3065,24 +3044,14 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
3065
3044
|
pairMatcher(ctx) {
|
|
3066
3045
|
const params = this.visit(ctx.parameterList);
|
|
3067
3046
|
const name = params.name;
|
|
3068
|
-
const
|
|
3069
|
-
const
|
|
3047
|
+
const layout = params.layout;
|
|
3048
|
+
const [firstGroup, secondGroup] = ctx.pairMatcherGroupSection;
|
|
3049
|
+
const groupAItems = this.visit(firstGroup);
|
|
3050
|
+
const groupBItems = this.visit(secondGroup);
|
|
3070
3051
|
const rawAnswers = this.visit(ctx.matcherAnswers);
|
|
3071
|
-
return createPairMatcherBlock(name,
|
|
3052
|
+
return createPairMatcherBlock(name, [groupAItems, groupBItems], rawAnswers, layout);
|
|
3072
3053
|
}
|
|
3073
|
-
|
|
3074
|
-
return this.visitMatcherItemList(ctx);
|
|
3075
|
-
}
|
|
3076
|
-
pairMatcherBottomSection(ctx) {
|
|
3077
|
-
return this.visitMatcherItemList(ctx);
|
|
3078
|
-
}
|
|
3079
|
-
/**
|
|
3080
|
-
* Maps `ctx.matcherItem` CST nodes to MatcherItem AST nodes.
|
|
3081
|
-
* Shared by pairMatcherTopSection and pairMatcherBottomSection because their
|
|
3082
|
-
* grammar rules are structurally identical — both are `matcherItem+` lists
|
|
3083
|
-
* wrapped in a labelled section keyword.
|
|
3084
|
-
*/
|
|
3085
|
-
visitMatcherItemList(ctx) {
|
|
3054
|
+
pairMatcherGroupSection(ctx) {
|
|
3086
3055
|
return ctx.matcherItem ? ctx.matcherItem.map((i) => this.visit(i)) : [];
|
|
3087
3056
|
}
|
|
3088
3057
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -3874,8 +3843,12 @@ const MatcherWidgetJSONSchema = zod.z.object({
|
|
|
3874
3843
|
});
|
|
3875
3844
|
const PairMatcherWidgetJSONSchema = zod.z.object({
|
|
3876
3845
|
type: zod.z.literal(JSON_TYPES.PAIR_MATCHER),
|
|
3877
|
-
|
|
3878
|
-
|
|
3846
|
+
/** Row layout (top/bottom, default) or column layout (left/right). */
|
|
3847
|
+
layout: zod.z.enum(["rows", "columns"]).optional(),
|
|
3848
|
+
groups: zod.z.tuple([
|
|
3849
|
+
zod.z.array(MatcherItemContentSchema).min(1),
|
|
3850
|
+
zod.z.array(MatcherItemContentSchema).min(1)
|
|
3851
|
+
])
|
|
3879
3852
|
});
|
|
3880
3853
|
const VisualFractionJSONSchema = zod.z.object({
|
|
3881
3854
|
type: zod.z.literal(JSON_TYPES.VISUAL_FRACTION),
|
|
@@ -9835,7 +9808,7 @@ function collectAllInlines(blocks) {
|
|
|
9835
9808
|
}
|
|
9836
9809
|
} else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
|
|
9837
9810
|
const pm = block;
|
|
9838
|
-
for (const item of
|
|
9811
|
+
for (const item of pm.groups.flat()) {
|
|
9839
9812
|
result.push(...item.inlines);
|
|
9840
9813
|
}
|
|
9841
9814
|
}
|
|
@@ -9942,10 +9915,14 @@ function buildMatcherWidget(matcher, idGenerator, inlineDragIds) {
|
|
|
9942
9915
|
return result;
|
|
9943
9916
|
}
|
|
9944
9917
|
function buildPairMatcherWidget(pm, idGenerator, inlineDragIds) {
|
|
9918
|
+
const [groupAItems, groupBItems] = pm.groups;
|
|
9945
9919
|
return {
|
|
9946
9920
|
type: JSON_TYPES.PAIR_MATCHER,
|
|
9947
|
-
|
|
9948
|
-
|
|
9921
|
+
...pm.layout !== void 0 && { layout: pm.layout },
|
|
9922
|
+
groups: [
|
|
9923
|
+
mapMatcherItemList(groupAItems, idGenerator, inlineDragIds),
|
|
9924
|
+
mapMatcherItemList(groupBItems, idGenerator, inlineDragIds)
|
|
9925
|
+
]
|
|
9949
9926
|
};
|
|
9950
9927
|
}
|
|
9951
9928
|
function transformVerticalOperation(vop, idGenerator) {
|
|
@@ -15451,13 +15428,18 @@ function applyDropToInput(targetId, item, onChange) {
|
|
|
15451
15428
|
function isUndoDrop(event, dragId) {
|
|
15452
15429
|
return event.active.id === dragId && !event.over;
|
|
15453
15430
|
}
|
|
15431
|
+
const LockContext = React.createContext(false);
|
|
15432
|
+
const ExerciseLockProvider = ({ locked, children }) => /* @__PURE__ */ jsxRuntime.jsx(LockContext.Provider, { value: locked, children });
|
|
15433
|
+
const useExerciseLocked = () => React.useContext(LockContext);
|
|
15454
15434
|
const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
|
|
15455
15435
|
const { droppedSources } = useExerciseDnd();
|
|
15436
|
+
const locked = useExerciseLocked();
|
|
15456
15437
|
const sourceOptionId = droppedSources.get(widgetId);
|
|
15457
15438
|
const dragId = `${widgetId}__chip`;
|
|
15458
15439
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
15459
15440
|
id: dragId,
|
|
15460
|
-
data: { type: "placed-chip", widgetId, label: value, sourceOptionId }
|
|
15441
|
+
data: { type: "placed-chip", widgetId, label: value, sourceOptionId },
|
|
15442
|
+
disabled: locked
|
|
15461
15443
|
});
|
|
15462
15444
|
useDndMonitor({
|
|
15463
15445
|
onDragEnd(event) {
|
|
@@ -15470,10 +15452,10 @@ const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
|
|
|
15470
15452
|
ref: setNodeRef,
|
|
15471
15453
|
className: `inline-input-chip ${className}${isDragging ? " dragging" : ""}`.trim(),
|
|
15472
15454
|
onClick: (e) => {
|
|
15473
|
-
if (e.detail !== 0) onRemove();
|
|
15455
|
+
if (!locked && e.detail !== 0) onRemove();
|
|
15474
15456
|
},
|
|
15475
|
-
title: "Drag to move, or click to remove",
|
|
15476
|
-
style: { touchAction: "none", cursor: "grab" },
|
|
15457
|
+
title: locked ? void 0 : "Drag to move, or click to remove",
|
|
15458
|
+
style: { touchAction: "none", cursor: locked ? "default" : "grab" },
|
|
15477
15459
|
...listeners,
|
|
15478
15460
|
...attributes,
|
|
15479
15461
|
children: value
|
|
@@ -15493,6 +15475,7 @@ const InlineInput = ({
|
|
|
15493
15475
|
onFocus
|
|
15494
15476
|
}) => {
|
|
15495
15477
|
const playSound = useSound();
|
|
15478
|
+
const locked = useExerciseLocked();
|
|
15496
15479
|
const handleChange = React.useCallback((newValue, fromDrop = false, sourceOptionId) => {
|
|
15497
15480
|
onChange(widgetId, newValue, fromDrop, sourceOptionId);
|
|
15498
15481
|
}, [onChange, widgetId]);
|
|
@@ -15508,6 +15491,7 @@ const InlineInput = ({
|
|
|
15508
15491
|
id: widgetId,
|
|
15509
15492
|
accepts: ["drag-option", "placed-chip"],
|
|
15510
15493
|
onDrop: handleDrop,
|
|
15494
|
+
disabled: locked,
|
|
15511
15495
|
className: "inline-widget inline-input-droppable",
|
|
15512
15496
|
children: isDropped && value !== "" ? (
|
|
15513
15497
|
// Show as chip only when value came from drag-drop
|
|
@@ -15530,8 +15514,11 @@ const InlineInput = ({
|
|
|
15530
15514
|
className: `inline-input${statusClass}`,
|
|
15531
15515
|
value,
|
|
15532
15516
|
onChange: (e) => handleChange(e.target.value, false),
|
|
15533
|
-
onMouseDown: () =>
|
|
15517
|
+
onMouseDown: () => {
|
|
15518
|
+
if (!locked) playSound("input-select");
|
|
15519
|
+
},
|
|
15534
15520
|
onFocus: () => onFocus?.(widgetId),
|
|
15521
|
+
readOnly: locked,
|
|
15535
15522
|
maxLength: expectedLength,
|
|
15536
15523
|
style: { width: dynamicWidth },
|
|
15537
15524
|
placeholder
|
|
@@ -15548,6 +15535,7 @@ const SingleDragOption = ({
|
|
|
15548
15535
|
className = ""
|
|
15549
15536
|
}) => {
|
|
15550
15537
|
const { usedOptionIds } = useExerciseDnd();
|
|
15538
|
+
const locked = useExerciseLocked();
|
|
15551
15539
|
const isUsed = usedOptionIds.has(optionId);
|
|
15552
15540
|
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
15541
|
Draggable,
|
|
@@ -15555,12 +15543,16 @@ const SingleDragOption = ({
|
|
|
15555
15543
|
id: optionId,
|
|
15556
15544
|
type: "drag-option",
|
|
15557
15545
|
data: { value, label: value },
|
|
15546
|
+
disabled: locked,
|
|
15558
15547
|
className: "drag-option-wrapper",
|
|
15559
15548
|
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
15560
15549
|
"button",
|
|
15561
15550
|
{
|
|
15562
15551
|
className: "drag-option",
|
|
15563
|
-
onClick: () =>
|
|
15552
|
+
onClick: () => {
|
|
15553
|
+
if (!locked) onOptionClick?.(value);
|
|
15554
|
+
},
|
|
15555
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
15564
15556
|
children: value
|
|
15565
15557
|
}
|
|
15566
15558
|
)
|
|
@@ -15674,31 +15666,34 @@ const NumericKeypad = ({
|
|
|
15674
15666
|
onChange
|
|
15675
15667
|
}) => {
|
|
15676
15668
|
const playSound = useSound();
|
|
15669
|
+
const locked = useExerciseLocked();
|
|
15677
15670
|
const handleDigit = (digit) => {
|
|
15671
|
+
if (locked) return;
|
|
15678
15672
|
playSound("keypad-press");
|
|
15679
15673
|
onChange(value + digit);
|
|
15680
15674
|
};
|
|
15681
15675
|
const handleBackspace = () => {
|
|
15676
|
+
if (locked) return;
|
|
15682
15677
|
playSound("keypad-press");
|
|
15683
15678
|
onChange(value.slice(0, -1));
|
|
15684
15679
|
};
|
|
15685
15680
|
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: "÷" })
|
|
15681
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
|
|
15682
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
|
|
15683
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
|
|
15684
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
|
|
15685
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
|
|
15686
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
|
|
15687
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
|
|
15688
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
|
|
15689
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
|
|
15690
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
|
|
15691
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
|
|
15692
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
|
|
15693
|
+
includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
|
|
15694
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
|
|
15695
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
|
|
15696
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
|
|
15702
15697
|
] });
|
|
15703
15698
|
};
|
|
15704
15699
|
const FractionInput = ({
|
|
@@ -15715,6 +15710,7 @@ const FractionInput = ({
|
|
|
15715
15710
|
placeholder
|
|
15716
15711
|
}) => {
|
|
15717
15712
|
const playSound = useSound();
|
|
15713
|
+
const locked = useExerciseLocked();
|
|
15718
15714
|
const numKey = getFractionKey(widgetId, "num");
|
|
15719
15715
|
const denKey = getFractionKey(widgetId, "den");
|
|
15720
15716
|
const numValue = values[numKey] ?? "";
|
|
@@ -15735,6 +15731,7 @@ const FractionInput = ({
|
|
|
15735
15731
|
id: subKey,
|
|
15736
15732
|
accepts: ["drag-option", "placed-chip"],
|
|
15737
15733
|
onDrop: handleDrop(subKey),
|
|
15734
|
+
disabled: locked,
|
|
15738
15735
|
className: "fraction-slot-droppable",
|
|
15739
15736
|
children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
15740
15737
|
PlacedInputChip,
|
|
@@ -15752,8 +15749,11 @@ const FractionInput = ({
|
|
|
15752
15749
|
className: `inline-input fraction-input${slotClass}`,
|
|
15753
15750
|
value: currentValue,
|
|
15754
15751
|
onChange: (e) => onChange(subKey, e.target.value, false),
|
|
15755
|
-
onMouseDown: () =>
|
|
15752
|
+
onMouseDown: () => {
|
|
15753
|
+
if (!locked) playSound("input-select");
|
|
15754
|
+
},
|
|
15756
15755
|
onFocus: () => onFocus?.(subKey),
|
|
15756
|
+
readOnly: locked,
|
|
15757
15757
|
maxLength: placeholder ? Math.max(20, placeholder.length) : 4,
|
|
15758
15758
|
size: placeholder ? placeholder.length : void 0,
|
|
15759
15759
|
"aria-label": label,
|
|
@@ -15984,12 +15984,13 @@ function getEqualTrianglePartPath(index, divisions) {
|
|
|
15984
15984
|
function getGridColumnCount(divisions) {
|
|
15985
15985
|
return Math.ceil(Math.sqrt(divisions));
|
|
15986
15986
|
}
|
|
15987
|
-
function getInteractiveProps(part, onPartClick) {
|
|
15987
|
+
function getInteractiveProps(part, onPartClick, locked = false) {
|
|
15988
15988
|
if (!part.isInteractiveSlot) return {};
|
|
15989
15989
|
return {
|
|
15990
15990
|
onClick: () => onPartClick(part.index),
|
|
15991
15991
|
role: "button",
|
|
15992
15992
|
"aria-pressed": part.isSelected,
|
|
15993
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
15993
15994
|
"aria-label": getPartAriaLabel(part),
|
|
15994
15995
|
tabIndex: 0,
|
|
15995
15996
|
onKeyDown: (event) => {
|
|
@@ -16009,6 +16010,7 @@ const BarShape = ({
|
|
|
16009
16010
|
orientation,
|
|
16010
16011
|
onPartClick
|
|
16011
16012
|
}) => {
|
|
16013
|
+
const locked = useExerciseLocked();
|
|
16012
16014
|
const renderedParts = orientation === "vertical" ? [...parts].reverse() : parts;
|
|
16013
16015
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16014
16016
|
"div",
|
|
@@ -16020,7 +16022,7 @@ const BarShape = ({
|
|
|
16020
16022
|
{
|
|
16021
16023
|
className: part.className,
|
|
16022
16024
|
style: part.style,
|
|
16023
|
-
...getInteractiveProps(part, onPartClick)
|
|
16025
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
16024
16026
|
},
|
|
16025
16027
|
part.index
|
|
16026
16028
|
))
|
|
@@ -16032,87 +16034,96 @@ const GridShape = ({
|
|
|
16032
16034
|
parts,
|
|
16033
16035
|
divisions,
|
|
16034
16036
|
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)
|
|
16037
|
+
}) => {
|
|
16038
|
+
const locked = useExerciseLocked();
|
|
16039
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16040
|
+
"div",
|
|
16041
|
+
{
|
|
16042
|
+
className: "visual-fraction-widget__parts visual-fraction-widget__parts--horizontal",
|
|
16043
|
+
style: {
|
|
16044
|
+
"--visual-fraction-divisions": divisions,
|
|
16045
|
+
"--visual-fraction-grid-cols": getGridColumnCount(divisions),
|
|
16046
|
+
// Safe integer division: zod schema enforces divisions = perfect square.
|
|
16047
|
+
"--visual-fraction-grid-rows": divisions / getGridColumnCount(divisions)
|
|
16051
16048
|
},
|
|
16052
|
-
part.
|
|
16053
|
-
|
|
16054
|
-
|
|
16055
|
-
|
|
16049
|
+
children: parts.map((part) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
16050
|
+
"div",
|
|
16051
|
+
{
|
|
16052
|
+
className: part.className,
|
|
16053
|
+
style: part.style,
|
|
16054
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
16055
|
+
},
|
|
16056
|
+
part.index
|
|
16057
|
+
))
|
|
16058
|
+
}
|
|
16059
|
+
);
|
|
16060
|
+
};
|
|
16056
16061
|
GridShape.displayName = "GridShape";
|
|
16057
16062
|
const CircleShape = ({
|
|
16058
16063
|
parts,
|
|
16059
16064
|
divisions,
|
|
16060
16065
|
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
|
-
)
|
|
16066
|
+
}) => {
|
|
16067
|
+
const locked = useExerciseLocked();
|
|
16068
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16069
|
+
"svg",
|
|
16070
|
+
{
|
|
16071
|
+
className: "visual-fraction-widget__shape visual-fraction-widget__circle",
|
|
16072
|
+
viewBox: "0 0 100 100",
|
|
16073
|
+
children: parts.map((part) => {
|
|
16074
|
+
const sectorProps = getCircleSectorProps(part.index, divisions);
|
|
16075
|
+
const className = `${part.className} visual-fraction-widget__sector`;
|
|
16076
|
+
const interactiveProps = getInteractiveProps(part, onPartClick, locked);
|
|
16077
|
+
return sectorProps.kind === "circle" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16078
|
+
"circle",
|
|
16079
|
+
{
|
|
16080
|
+
className,
|
|
16081
|
+
style: part.style,
|
|
16082
|
+
cx: "50",
|
|
16083
|
+
cy: "50",
|
|
16084
|
+
r: "48",
|
|
16085
|
+
...interactiveProps
|
|
16086
|
+
},
|
|
16087
|
+
part.index
|
|
16088
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
16089
|
+
"path",
|
|
16090
|
+
{
|
|
16091
|
+
className,
|
|
16092
|
+
style: part.style,
|
|
16093
|
+
d: sectorProps.d,
|
|
16094
|
+
...interactiveProps
|
|
16095
|
+
},
|
|
16096
|
+
part.index
|
|
16097
|
+
);
|
|
16098
|
+
})
|
|
16099
|
+
}
|
|
16100
|
+
);
|
|
16101
|
+
};
|
|
16094
16102
|
CircleShape.displayName = "CircleShape";
|
|
16095
16103
|
const TriangleShape = ({
|
|
16096
16104
|
parts,
|
|
16097
16105
|
divisions,
|
|
16098
16106
|
onPartClick
|
|
16099
|
-
}) =>
|
|
16100
|
-
|
|
16101
|
-
|
|
16102
|
-
|
|
16103
|
-
|
|
16104
|
-
|
|
16105
|
-
"
|
|
16106
|
-
|
|
16107
|
-
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
|
|
16113
|
-
|
|
16114
|
-
|
|
16115
|
-
)
|
|
16107
|
+
}) => {
|
|
16108
|
+
const locked = useExerciseLocked();
|
|
16109
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16110
|
+
"svg",
|
|
16111
|
+
{
|
|
16112
|
+
className: "visual-fraction-widget__shape visual-fraction-widget__triangle",
|
|
16113
|
+
viewBox: "0 0 100 100",
|
|
16114
|
+
children: parts.map((part) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
16115
|
+
"path",
|
|
16116
|
+
{
|
|
16117
|
+
className: `${part.className} visual-fraction-widget__sector`,
|
|
16118
|
+
style: part.style,
|
|
16119
|
+
d: getEqualTrianglePartPath(part.index, divisions),
|
|
16120
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
16121
|
+
},
|
|
16122
|
+
part.index
|
|
16123
|
+
))
|
|
16124
|
+
}
|
|
16125
|
+
);
|
|
16126
|
+
};
|
|
16116
16127
|
TriangleShape.displayName = "TriangleShape";
|
|
16117
16128
|
const ShapeBar = ({ parts, divisions, orientation, onPartClick }) => /* @__PURE__ */ jsxRuntime.jsx(BarShape, { parts, divisions, orientation, onPartClick });
|
|
16118
16129
|
const ShapeCircle = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsxRuntime.jsx(CircleShape, { parts, divisions, onPartClick });
|
|
@@ -16156,12 +16167,14 @@ const VisualFraction = ({
|
|
|
16156
16167
|
onChange
|
|
16157
16168
|
});
|
|
16158
16169
|
const playSound = useSound();
|
|
16170
|
+
const locked = useExerciseLocked();
|
|
16159
16171
|
const handlePartClickWithSound = React.useCallback(
|
|
16160
16172
|
(index) => {
|
|
16173
|
+
if (locked) return;
|
|
16161
16174
|
if (answer.length > 0 && index >= filledCount) playSound("fraction-shade");
|
|
16162
16175
|
handlePartClick(index);
|
|
16163
16176
|
},
|
|
16164
|
-
[answer.length, filledCount, playSound, handlePartClick]
|
|
16177
|
+
[locked, answer.length, filledCount, playSound, handlePartClick]
|
|
16165
16178
|
);
|
|
16166
16179
|
const parts = React.useMemo(
|
|
16167
16180
|
() => Array.from({ length: divisions }, (_, i) => {
|
|
@@ -16236,6 +16249,7 @@ const GridSymmetry = ({
|
|
|
16236
16249
|
validationStatus = null,
|
|
16237
16250
|
onChange
|
|
16238
16251
|
}) => {
|
|
16252
|
+
const exerciseLocked = useExerciseLocked();
|
|
16239
16253
|
const initialKeys = React.useMemo(() => new Set(widget.cells.map(gridCellKey)), [widget.cells]);
|
|
16240
16254
|
const initialInteractiveCells = React.useMemo(
|
|
16241
16255
|
() => widget.cells.filter(
|
|
@@ -16246,6 +16260,7 @@ const GridSymmetry = ({
|
|
|
16246
16260
|
const selectedCells = value ?? initialInteractiveCells;
|
|
16247
16261
|
const selectedKeys = React.useMemo(() => new Set(selectedCells.map(gridCellKey)), [selectedCells]);
|
|
16248
16262
|
const toggleCell = (cell) => {
|
|
16263
|
+
if (exerciseLocked) return;
|
|
16249
16264
|
const key2 = gridCellKey(cell);
|
|
16250
16265
|
if (initialKeys.has(key2)) return;
|
|
16251
16266
|
const next = selectedKeys.has(key2) ? selectedCells.filter((candidate) => gridCellKey(candidate) !== key2) : [...selectedCells, cell];
|
|
@@ -16286,6 +16301,7 @@ const GridSymmetry = ({
|
|
|
16286
16301
|
className,
|
|
16287
16302
|
"aria-label": label,
|
|
16288
16303
|
"aria-pressed": selected,
|
|
16304
|
+
"aria-disabled": exerciseLocked ? "true" : void 0,
|
|
16289
16305
|
onClick: () => toggleCell(cell)
|
|
16290
16306
|
},
|
|
16291
16307
|
key2
|
|
@@ -16325,6 +16341,7 @@ const RadioOptions = ({
|
|
|
16325
16341
|
onChange
|
|
16326
16342
|
}) => {
|
|
16327
16343
|
const playSound = useSound();
|
|
16344
|
+
const locked = useExerciseLocked();
|
|
16328
16345
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "radio-options", children: options.map((option, idx) => {
|
|
16329
16346
|
const isSelected = value === option;
|
|
16330
16347
|
const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
|
|
@@ -16333,9 +16350,11 @@ const RadioOptions = ({
|
|
|
16333
16350
|
{
|
|
16334
16351
|
className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
|
|
16335
16352
|
onClick: () => {
|
|
16353
|
+
if (locked) return;
|
|
16336
16354
|
playSound("radio-select");
|
|
16337
16355
|
onChange(name, option);
|
|
16338
16356
|
},
|
|
16357
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
16339
16358
|
type: "button",
|
|
16340
16359
|
children: option
|
|
16341
16360
|
},
|
|
@@ -16347,7 +16366,10 @@ const Btn = ({
|
|
|
16347
16366
|
label,
|
|
16348
16367
|
extraClass = "keypad-btn-digit",
|
|
16349
16368
|
onClick
|
|
16350
|
-
}) =>
|
|
16369
|
+
}) => {
|
|
16370
|
+
const locked = useExerciseLocked();
|
|
16371
|
+
return /* @__PURE__ */ jsxRuntime.jsx("button", { className: `keypad-btn ${extraClass}`, disabled: locked, onClick, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }) });
|
|
16372
|
+
};
|
|
16351
16373
|
const DiamondKeypad = ({ value, onChange }) => {
|
|
16352
16374
|
const playSound = useSound();
|
|
16353
16375
|
const append = (ch) => {
|
|
@@ -16434,8 +16456,10 @@ const MatcherWidget = ({
|
|
|
16434
16456
|
() => defaultValue2 && defaultValue2.length > 0 ? assignmentsFromValue(defaultValue2, itemIds, groupCount) : initialAssignments(itemIds)
|
|
16435
16457
|
);
|
|
16436
16458
|
const playSound = useSound();
|
|
16459
|
+
const locked = useExerciseLocked();
|
|
16437
16460
|
const handleCardClick = React.useCallback(
|
|
16438
16461
|
(itemId) => {
|
|
16462
|
+
if (locked) return;
|
|
16439
16463
|
playSound("matcher-select");
|
|
16440
16464
|
setAssignments((prev) => {
|
|
16441
16465
|
const current = prev[itemId] ?? null;
|
|
@@ -16443,7 +16467,7 @@ const MatcherWidget = ({
|
|
|
16443
16467
|
return { ...prev, [itemId]: next };
|
|
16444
16468
|
});
|
|
16445
16469
|
},
|
|
16446
|
-
[groupCount, playSound]
|
|
16470
|
+
[groupCount, playSound, locked]
|
|
16447
16471
|
);
|
|
16448
16472
|
const onChangeRef = React.useRef(onChange);
|
|
16449
16473
|
onChangeRef.current = onChange;
|
|
@@ -16480,6 +16504,7 @@ const MatcherWidget = ({
|
|
|
16480
16504
|
"--matcher-card-text-color": `var(--matcher-group-${colorIdx + 1}-text-color, ${textColors[colorIdx]})`
|
|
16481
16505
|
} : void 0,
|
|
16482
16506
|
onClick: () => handleCardClick(item.itemId),
|
|
16507
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
16483
16508
|
"aria-pressed": isSelected,
|
|
16484
16509
|
"aria-label": isSelected ? `Assigned to group ${colorIdx + 1}` : "Not assigned",
|
|
16485
16510
|
children: item.content.map((layoutItem) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -16692,7 +16717,10 @@ const PairMatcherWidget = ({
|
|
|
16692
16717
|
widgets,
|
|
16693
16718
|
shuffleItems = true
|
|
16694
16719
|
}) => {
|
|
16695
|
-
const {
|
|
16720
|
+
const { groups, layout } = widget;
|
|
16721
|
+
const [topItems, bottomItems] = groups;
|
|
16722
|
+
const isColumns = layout === "columns";
|
|
16723
|
+
const exerciseLocked = useExerciseLocked();
|
|
16696
16724
|
const [shuffledTop] = React.useState(
|
|
16697
16725
|
() => shuffleItems ? lodashExports.shuffle([...topItems]) : [...topItems]
|
|
16698
16726
|
);
|
|
@@ -16736,8 +16764,9 @@ const PairMatcherWidget = ({
|
|
|
16736
16764
|
}
|
|
16737
16765
|
soundPrevRef.current = { count, pendingId, flashId };
|
|
16738
16766
|
}, [state.pairs, state.pending, state.flashTopId, playSound]);
|
|
16767
|
+
const interactionLocked = validationStatus !== null || exerciseLocked;
|
|
16739
16768
|
const handleTopClick = React.useCallback((topId) => {
|
|
16740
|
-
if (
|
|
16769
|
+
if (interactionLocked) return;
|
|
16741
16770
|
if (flashTimerRef.current) {
|
|
16742
16771
|
clearTimeout(flashTimerRef.current);
|
|
16743
16772
|
flashTimerRef.current = null;
|
|
@@ -16779,9 +16808,9 @@ const PairMatcherWidget = ({
|
|
|
16779
16808
|
flashBottomId: null
|
|
16780
16809
|
};
|
|
16781
16810
|
});
|
|
16782
|
-
}, [
|
|
16811
|
+
}, [interactionLocked]);
|
|
16783
16812
|
const handleBottomClick = React.useCallback((bottomId) => {
|
|
16784
|
-
if (
|
|
16813
|
+
if (interactionLocked) return;
|
|
16785
16814
|
if (flashTimerRef.current) {
|
|
16786
16815
|
clearTimeout(flashTimerRef.current);
|
|
16787
16816
|
flashTimerRef.current = null;
|
|
@@ -16824,9 +16853,13 @@ const PairMatcherWidget = ({
|
|
|
16824
16853
|
flashBottomId: null
|
|
16825
16854
|
};
|
|
16826
16855
|
});
|
|
16827
|
-
}, [
|
|
16856
|
+
}, [interactionLocked]);
|
|
16828
16857
|
React.useEffect(() => {
|
|
16829
|
-
if (
|
|
16858
|
+
if (state.flashTopId === null || state.flashBottomId === null) return;
|
|
16859
|
+
if (interactionLocked) {
|
|
16860
|
+
setState((current) => ({ ...current, flashTopId: null, flashBottomId: null }));
|
|
16861
|
+
return;
|
|
16862
|
+
}
|
|
16830
16863
|
flashTimerRef.current = setTimeout(() => {
|
|
16831
16864
|
flashTimerRef.current = null;
|
|
16832
16865
|
setState((current) => {
|
|
@@ -16840,7 +16873,7 @@ const PairMatcherWidget = ({
|
|
|
16840
16873
|
flashTimerRef.current = null;
|
|
16841
16874
|
}
|
|
16842
16875
|
};
|
|
16843
|
-
}, [state.flashTopId, state.flashBottomId,
|
|
16876
|
+
}, [state.flashTopId, state.flashBottomId, interactionLocked]);
|
|
16844
16877
|
const onChangeRef = React.useRef(onChange);
|
|
16845
16878
|
onChangeRef.current = onChange;
|
|
16846
16879
|
const isMounted = React.useRef(false);
|
|
@@ -16863,8 +16896,8 @@ const PairMatcherWidget = ({
|
|
|
16863
16896
|
pairStatusByTopId
|
|
16864
16897
|
});
|
|
16865
16898
|
const cols = React.useMemo(
|
|
16866
|
-
() => Math.max(topItems.length, bottomItems.length, 1),
|
|
16867
|
-
[topItems.length, bottomItems.length]
|
|
16899
|
+
() => isColumns ? null : Math.max(topItems.length, bottomItems.length, 1),
|
|
16900
|
+
[isColumns, topItems.length, bottomItems.length]
|
|
16868
16901
|
);
|
|
16869
16902
|
const renderCard = (item, isTop) => {
|
|
16870
16903
|
const isRtl = shouldItemsUseRTL(item.content);
|
|
@@ -16900,7 +16933,7 @@ const PairMatcherWidget = ({
|
|
|
16900
16933
|
dir: isRtl ? "rtl" : void 0,
|
|
16901
16934
|
onClick: () => isTop ? handleTopClick(item.itemId) : handleBottomClick(item.itemId),
|
|
16902
16935
|
"aria-pressed": status !== "idle",
|
|
16903
|
-
"aria-disabled":
|
|
16936
|
+
"aria-disabled": interactionLocked ? "true" : void 0,
|
|
16904
16937
|
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pair-matcher-card-content", children: item.content.map((li) => renderPairMatcherItemContent(li, widgetsMap)) })
|
|
16905
16938
|
}
|
|
16906
16939
|
),
|
|
@@ -16910,8 +16943,8 @@ const PairMatcherWidget = ({
|
|
|
16910
16943
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
16911
16944
|
"div",
|
|
16912
16945
|
{
|
|
16913
|
-
className: `pair-matcher-widget${validationStatus ? ` pair-matcher-widget--${validationStatus}` : ""}`,
|
|
16914
|
-
style: { "--pm-cols": cols },
|
|
16946
|
+
className: `pair-matcher-widget${isColumns ? " pair-matcher-widget--columns" : ""}${validationStatus ? ` pair-matcher-widget--${validationStatus}` : ""}`,
|
|
16947
|
+
style: cols === null ? {} : { "--pm-cols": cols },
|
|
16915
16948
|
ref: containerRef,
|
|
16916
16949
|
role: "group",
|
|
16917
16950
|
"aria-label": "Pair matcher exercise",
|
|
@@ -17439,6 +17472,7 @@ const DraggableCursor = ({
|
|
|
17439
17472
|
currentValue,
|
|
17440
17473
|
validationStatus
|
|
17441
17474
|
}) => {
|
|
17475
|
+
const locked = useExerciseLocked();
|
|
17442
17476
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
17443
17477
|
const isDraggingRef = React.useRef(false);
|
|
17444
17478
|
const currentValueRef = React.useRef(currentValue);
|
|
@@ -17460,13 +17494,14 @@ const DraggableCursor = ({
|
|
|
17460
17494
|
return snapTicks.length > 0 ? snapToNearest(raw, snapTicks) : cleanFloat(raw);
|
|
17461
17495
|
}, [axisRowRef, min, max, snapTicks]);
|
|
17462
17496
|
const handlePointerDown = React.useCallback((e) => {
|
|
17497
|
+
if (locked) return;
|
|
17463
17498
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
17464
17499
|
e.stopPropagation();
|
|
17465
17500
|
isDraggingRef.current = true;
|
|
17466
17501
|
dragStartValueRef.current = currentValue;
|
|
17467
17502
|
lastSoundValueRef.current = currentValue;
|
|
17468
17503
|
setIsDragging(true);
|
|
17469
|
-
}, [currentValue]);
|
|
17504
|
+
}, [currentValue, locked]);
|
|
17470
17505
|
const updateCursorValue = React.useCallback((clientX) => {
|
|
17471
17506
|
const next = snap(clientX);
|
|
17472
17507
|
if (!almostEqual(next, lastSoundValueRef.current)) {
|
|
@@ -17494,7 +17529,7 @@ const DraggableCursor = ({
|
|
|
17494
17529
|
}
|
|
17495
17530
|
}, [updateCursorValue, playSound]);
|
|
17496
17531
|
const handleKeyDown = React.useCallback((e) => {
|
|
17497
|
-
if (!snapTicks.length) return;
|
|
17532
|
+
if (locked || !snapTicks.length) return;
|
|
17498
17533
|
const sorted = [...snapTicks].sort((a, b) => a - b);
|
|
17499
17534
|
const idx = sorted.findIndex((t) => t >= currentValue - 1e-9);
|
|
17500
17535
|
const first = sorted[0] ?? currentValue;
|
|
@@ -17516,7 +17551,7 @@ const DraggableCursor = ({
|
|
|
17516
17551
|
}
|
|
17517
17552
|
onChange(cursor.id, String(next));
|
|
17518
17553
|
}
|
|
17519
|
-
}, [snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
|
|
17554
|
+
}, [locked, snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
|
|
17520
17555
|
const ballClass = [
|
|
17521
17556
|
"number-line-cursor-ball",
|
|
17522
17557
|
isDragging ? "number-line-cursor-ball--dragging" : "",
|
|
@@ -17565,6 +17600,7 @@ const DraggableCursor = ({
|
|
|
17565
17600
|
"aria-valuemax": max,
|
|
17566
17601
|
"aria-valuenow": currentValue,
|
|
17567
17602
|
"aria-label": sliderAriaLabel,
|
|
17603
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
17568
17604
|
tabIndex: 0
|
|
17569
17605
|
}
|
|
17570
17606
|
)
|
|
@@ -17605,9 +17641,11 @@ const CursorBalls = ({
|
|
|
17605
17641
|
}) });
|
|
17606
17642
|
};
|
|
17607
17643
|
const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
17644
|
+
const locked = useExerciseLocked();
|
|
17608
17645
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
17609
17646
|
id: widgetId,
|
|
17610
|
-
data: { type: "placed-chip", widgetId, label }
|
|
17647
|
+
data: { type: "placed-chip", widgetId, label },
|
|
17648
|
+
disabled: locked
|
|
17611
17649
|
});
|
|
17612
17650
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
17613
17651
|
"div",
|
|
@@ -17632,6 +17670,7 @@ const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
|
17632
17670
|
type: "button",
|
|
17633
17671
|
className: "number-line-placed-chip-remove",
|
|
17634
17672
|
"aria-label": "Remove",
|
|
17673
|
+
disabled: locked,
|
|
17635
17674
|
onClick: onRemove,
|
|
17636
17675
|
children: "✕"
|
|
17637
17676
|
}
|
|
@@ -17671,6 +17710,7 @@ const FreeDragAxis = ({
|
|
|
17671
17710
|
cursorValidationStatuses,
|
|
17672
17711
|
snapKey
|
|
17673
17712
|
}) => {
|
|
17713
|
+
const axisLocked = useExerciseLocked();
|
|
17674
17714
|
const axisRef = React.useRef(null);
|
|
17675
17715
|
const snapTicks = React.useMemo(
|
|
17676
17716
|
() => getSnapTicks(majorTicks, minorTicks),
|
|
@@ -17726,7 +17766,7 @@ const FreeDragAxis = ({
|
|
|
17726
17766
|
return changed ? next : prev;
|
|
17727
17767
|
});
|
|
17728
17768
|
}, [values]);
|
|
17729
|
-
const { setNodeRef } = useDroppable({ id: axisDropId });
|
|
17769
|
+
const { setNodeRef } = useDroppable({ id: axisDropId, disabled: axisLocked });
|
|
17730
17770
|
const mergedRef = React.useCallback((node) => {
|
|
17731
17771
|
axisRef.current = node;
|
|
17732
17772
|
setNodeRef(node);
|
|
@@ -18040,6 +18080,7 @@ const CounterControls = ({
|
|
|
18040
18080
|
onIncrement
|
|
18041
18081
|
}) => {
|
|
18042
18082
|
const playSound = useSound();
|
|
18083
|
+
const locked = useExerciseLocked();
|
|
18043
18084
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-counter", "aria-label": counter.name ?? "Counter", children: [
|
|
18044
18085
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-counter-controls", children: [
|
|
18045
18086
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -18048,7 +18089,7 @@ const CounterControls = ({
|
|
|
18048
18089
|
type: "button",
|
|
18049
18090
|
className: "number-line-counter-btn",
|
|
18050
18091
|
"aria-label": "Decrease",
|
|
18051
|
-
disabled: count <= counter.min,
|
|
18092
|
+
disabled: locked || count <= counter.min,
|
|
18052
18093
|
onClick: () => {
|
|
18053
18094
|
playSound("counter-step");
|
|
18054
18095
|
onDecrement();
|
|
@@ -18063,7 +18104,7 @@ const CounterControls = ({
|
|
|
18063
18104
|
type: "button",
|
|
18064
18105
|
className: "number-line-counter-btn",
|
|
18065
18106
|
"aria-label": "Increase",
|
|
18066
|
-
disabled: count >= counter.max,
|
|
18107
|
+
disabled: locked || count >= counter.max,
|
|
18067
18108
|
onClick: () => {
|
|
18068
18109
|
playSound("counter-step");
|
|
18069
18110
|
onIncrement();
|
|
@@ -18647,6 +18688,7 @@ const ClockRenderer = ({
|
|
|
18647
18688
|
validationStatus,
|
|
18648
18689
|
onChange
|
|
18649
18690
|
}) => {
|
|
18691
|
+
const locked = useExerciseLocked();
|
|
18650
18692
|
const format = item.format ?? "24h";
|
|
18651
18693
|
const interactive = item.mode === "interactive";
|
|
18652
18694
|
const snap = interactive && "snap" in item ? item.snap ?? 1 : 1;
|
|
@@ -18692,18 +18734,18 @@ const ClockRenderer = ({
|
|
|
18692
18734
|
}
|
|
18693
18735
|
}, [snap, hours, minutes, item.mode, item.name, onChange, playSound]);
|
|
18694
18736
|
const onPointerDown = (hand) => (e) => {
|
|
18695
|
-
if (!interactive) return;
|
|
18737
|
+
if (!interactive || locked) return;
|
|
18696
18738
|
e.preventDefault();
|
|
18697
18739
|
e.target.setPointerCapture?.(e.pointerId);
|
|
18698
18740
|
draggingRef.current = hand;
|
|
18699
18741
|
updateFromPointer(e.clientX, e.clientY);
|
|
18700
18742
|
};
|
|
18701
18743
|
const onPointerMove = (e) => {
|
|
18702
|
-
if (!interactive || !draggingRef.current) return;
|
|
18744
|
+
if (!interactive || locked || !draggingRef.current) return;
|
|
18703
18745
|
updateFromPointer(e.clientX, e.clientY);
|
|
18704
18746
|
};
|
|
18705
18747
|
const onPointerUp = (e) => {
|
|
18706
|
-
if (!interactive) return;
|
|
18748
|
+
if (!interactive || locked) return;
|
|
18707
18749
|
e.target.releasePointerCapture?.(e.pointerId);
|
|
18708
18750
|
draggingRef.current = null;
|
|
18709
18751
|
};
|
|
@@ -19255,38 +19297,45 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
19255
19297
|
const [droppedSources, setDroppedSources] = React.useState(/* @__PURE__ */ new Map());
|
|
19256
19298
|
const [focusedInputId, setFocusedInputId] = React.useState(null);
|
|
19257
19299
|
const [validationDetails, setValidationDetails] = React.useState(null);
|
|
19300
|
+
const rootRef = React.useRef(null);
|
|
19258
19301
|
const [resetKey, setResetKey] = React.useState(0);
|
|
19259
19302
|
const [revealed, setRevealed] = React.useState(false);
|
|
19260
19303
|
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
|
-
|
|
19304
|
+
const externallyLocked = useExerciseLocked();
|
|
19305
|
+
const locked = validationDetails !== null || externallyLocked;
|
|
19306
|
+
const lastResultRef = React.useRef(null);
|
|
19307
|
+
const runSubmit = () => {
|
|
19308
|
+
if (locked) return lastResultRef.current ?? validate(json, values);
|
|
19309
|
+
const result = validate(json, values);
|
|
19310
|
+
playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
|
|
19311
|
+
const details = { ...result.details };
|
|
19312
|
+
const slotValidation = (id, raw, expected) => ({
|
|
19313
|
+
widgetId: id,
|
|
19314
|
+
userValue: typeof raw === "string" ? raw : void 0,
|
|
19315
|
+
isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
|
|
19316
|
+
});
|
|
19317
|
+
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
19318
|
+
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
19319
|
+
if (!(widgetId in details)) continue;
|
|
19320
|
+
const answer = json.validation?.answers?.[widgetId];
|
|
19321
|
+
if (!answer) continue;
|
|
19322
|
+
const [expectedNum, expectedDen] = answer.split("/", 2);
|
|
19323
|
+
if (!expectedNum || !expectedDen) continue;
|
|
19324
|
+
if (details[widgetId]?.isCorrect) {
|
|
19325
|
+
if (widget.numerator === null) details[`${widgetId}__num`] = { widgetId: `${widgetId}__num`, userValue: void 0, isCorrect: true };
|
|
19326
|
+
if (widget.denominator === null) details[`${widgetId}__den`] = { widgetId: `${widgetId}__den`, userValue: void 0, isCorrect: true };
|
|
19327
|
+
} else {
|
|
19328
|
+
if (widget.numerator === null) details[`${widgetId}__num`] = slotValidation(`${widgetId}__num`, values[`${widgetId}__num`], expectedNum);
|
|
19329
|
+
if (widget.denominator === null) details[`${widgetId}__den`] = slotValidation(`${widgetId}__den`, values[`${widgetId}__den`], expectedDen);
|
|
19285
19330
|
}
|
|
19286
|
-
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
|
|
19331
|
+
}
|
|
19332
|
+
setValidationDetails(details);
|
|
19333
|
+
onSubmit?.(values);
|
|
19334
|
+
lastResultRef.current = result;
|
|
19335
|
+
return result;
|
|
19336
|
+
};
|
|
19337
|
+
React.useImperativeHandle(ref, () => ({
|
|
19338
|
+
submit: runSubmit,
|
|
19290
19339
|
getValues: () => values,
|
|
19291
19340
|
getCompletion: () => {
|
|
19292
19341
|
const answerable = getAnswerableInputs(json);
|
|
@@ -19329,14 +19378,20 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
19329
19378
|
setResetKey((k) => k + 1);
|
|
19330
19379
|
},
|
|
19331
19380
|
focusFirstInput: () => {
|
|
19332
|
-
const container =
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
|
|
19381
|
+
const container = rootRef.current;
|
|
19382
|
+
if (!container) return;
|
|
19383
|
+
const candidates = container.querySelectorAll(
|
|
19384
|
+
'input[type="text"], input[type="number"], textarea, select'
|
|
19385
|
+
);
|
|
19386
|
+
for (const el of Array.from(candidates)) {
|
|
19387
|
+
if (el.readOnly || el.disabled) continue;
|
|
19388
|
+
el.focus();
|
|
19389
|
+
return;
|
|
19336
19390
|
}
|
|
19337
19391
|
}
|
|
19338
|
-
}), [json, values, onSubmit, playSound]);
|
|
19392
|
+
}), [json, values, onSubmit, playSound, locked]);
|
|
19339
19393
|
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
19394
|
+
if (locked) return;
|
|
19340
19395
|
setRevealed(false);
|
|
19341
19396
|
const next = { ...valuesRef.current, [widgetId]: value };
|
|
19342
19397
|
valuesRef.current = next;
|
|
@@ -19353,7 +19408,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
19353
19408
|
onChange?.(next);
|
|
19354
19409
|
};
|
|
19355
19410
|
const handleSubmit = () => {
|
|
19356
|
-
|
|
19411
|
+
runSubmit();
|
|
19357
19412
|
};
|
|
19358
19413
|
const lineGroups = React.useMemo(() => {
|
|
19359
19414
|
const isRTLInlineWidget = (item) => {
|
|
@@ -19468,7 +19523,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
19468
19523
|
if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
|
|
19469
19524
|
return { tools: list, toolsMinHeight: maxH };
|
|
19470
19525
|
}, [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: [
|
|
19526
|
+
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
19527
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
19473
19528
|
"div",
|
|
19474
19529
|
{
|
|
@@ -19564,8 +19619,8 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
19564
19619
|
}
|
|
19565
19620
|
}
|
|
19566
19621
|
) }),
|
|
19567
|
-
showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "exercise-submit-btn", onClick: handleSubmit, children: "Check Answer" })
|
|
19568
|
-
] }) }) });
|
|
19622
|
+
showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" })
|
|
19623
|
+
] }) }) }) });
|
|
19569
19624
|
});
|
|
19570
19625
|
ExerciseRenderer.displayName = "ExerciseRenderer";
|
|
19571
19626
|
function renderExercise(options) {
|
|
@@ -19621,6 +19676,7 @@ exports.DragOptionsToolSchema = DragOptionsToolSchema;
|
|
|
19621
19676
|
exports.EngineError = EngineError;
|
|
19622
19677
|
exports.ExerciseDocumentSchema = ExerciseDocumentSchema;
|
|
19623
19678
|
exports.ExerciseJSONSchema = ExerciseJSONSchema;
|
|
19679
|
+
exports.ExerciseLockProvider = ExerciseLockProvider;
|
|
19624
19680
|
exports.ExerciseMetaSchema = ExerciseMetaSchema;
|
|
19625
19681
|
exports.ExerciseRenderer = ExerciseRenderer;
|
|
19626
19682
|
exports.ExpressionValidationSchema = ExpressionValidationSchema;
|
|
@@ -19843,6 +19899,7 @@ exports.toAST = toAST;
|
|
|
19843
19899
|
exports.toJSON = toJSON;
|
|
19844
19900
|
exports.tokenize = tokenize;
|
|
19845
19901
|
exports.tokenizeSafe = tokenizeSafe;
|
|
19902
|
+
exports.useExerciseLocked = useExerciseLocked;
|
|
19846
19903
|
exports.useSound = useSound;
|
|
19847
19904
|
exports.validate = validate;
|
|
19848
19905
|
exports.validateBooleanExpression = validateBooleanExpression;
|