@mathscan/math-exercise-engine 1.1.16 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -11
- package/dist/index.d.ts +613 -194
- package/dist/index.js +724 -163
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +724 -163
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +135 -0
- package/package.json +19 -19
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const chevrotain = require("chevrotain");
|
|
4
3
|
const zod = require("zod");
|
|
4
|
+
const chevrotain = require("chevrotain");
|
|
5
5
|
const mathjs = require("mathjs");
|
|
6
6
|
const jsxRuntime = require("react/jsx-runtime");
|
|
7
7
|
const React = require("react");
|
|
@@ -56,6 +56,8 @@ const AST_TYPES = {
|
|
|
56
56
|
VOP_CARRY_ROW: "VopCarryRow",
|
|
57
57
|
/** Number line layout block */
|
|
58
58
|
NUMBER_LINE_BLOCK: "NumberLineBlock",
|
|
59
|
+
/** Symmetry exercise on a square grid */
|
|
60
|
+
GRID_SYMMETRY_BLOCK: "GridSymmetryBlock",
|
|
59
61
|
/** A position marker on the number line */
|
|
60
62
|
NUMBER_LINE_POSITION: "NumberLinePosition",
|
|
61
63
|
/** A draggable cursor on the number line */
|
|
@@ -187,6 +189,8 @@ const JSON_TYPES = {
|
|
|
187
189
|
FRACTION: "fraction",
|
|
188
190
|
/** Visual fraction widget */
|
|
189
191
|
VISUAL_FRACTION: "visual-fraction",
|
|
192
|
+
/** Interactive symmetry exercise on a grid */
|
|
193
|
+
GRID_SYMMETRY: "grid-symmetry",
|
|
190
194
|
/** Ruler geometry tool widget */
|
|
191
195
|
RULER: "ruler",
|
|
192
196
|
/** Protractor geometry tool widget */
|
|
@@ -201,9 +205,11 @@ const SET_SQUARE_WIDGET_ID = "_set_square";
|
|
|
201
205
|
const STATEFUL_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
202
206
|
JSON_TYPES.MATCHER,
|
|
203
207
|
JSON_TYPES.PAIR_MATCHER,
|
|
204
|
-
JSON_TYPES.VISUAL_FRACTION
|
|
208
|
+
JSON_TYPES.VISUAL_FRACTION,
|
|
209
|
+
JSON_TYPES.GRID_SYMMETRY
|
|
205
210
|
]);
|
|
206
211
|
const INLINE_DRAG_OPT_PREFIX = "_inline_drag_opt_";
|
|
212
|
+
const GRID_SYMMETRY_WIDGET_PREFIX = "_grid_symmetry_";
|
|
207
213
|
const RTL_INLINE_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
208
214
|
JSON_TYPES.INLINE_INPUT,
|
|
209
215
|
JSON_TYPES.INLINE_DRAG_OPTION,
|
|
@@ -246,6 +252,11 @@ function parseFractionKey(key2) {
|
|
|
246
252
|
if (!match) return null;
|
|
247
253
|
return { widgetId: match[1], part: match[2] };
|
|
248
254
|
}
|
|
255
|
+
const GridCellSchema = zod.z.tuple([
|
|
256
|
+
zod.z.number().int().positive(),
|
|
257
|
+
zod.z.number().int().positive()
|
|
258
|
+
]);
|
|
259
|
+
const gridCellKey = ([row, col]) => `${row},${col}`;
|
|
249
260
|
const Newline = chevrotain.createToken({
|
|
250
261
|
name: "Newline",
|
|
251
262
|
pattern: /\r?\n/,
|
|
@@ -327,6 +338,10 @@ const NUMBER_LINE = chevrotain.createToken({
|
|
|
327
338
|
name: "NUMBER_LINE",
|
|
328
339
|
pattern: /NUMBER_LINE/
|
|
329
340
|
});
|
|
341
|
+
const GRID_SYMMETRY = chevrotain.createToken({
|
|
342
|
+
name: "GRID_SYMMETRY",
|
|
343
|
+
pattern: /GRID_SYMMETRY/
|
|
344
|
+
});
|
|
330
345
|
const POSITION = chevrotain.createToken({
|
|
331
346
|
name: "POSITION",
|
|
332
347
|
pattern: /POSITION/
|
|
@@ -535,6 +550,7 @@ const allTokens = [
|
|
|
535
550
|
W_N_I,
|
|
536
551
|
NUMERIC_INPUT,
|
|
537
552
|
NUMBER_LINE,
|
|
553
|
+
GRID_SYMMETRY,
|
|
538
554
|
POSITION,
|
|
539
555
|
CURSOR,
|
|
540
556
|
ARROW,
|
|
@@ -710,6 +726,10 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
710
726
|
GATE: this.BACKTRACK(this.numberLine),
|
|
711
727
|
ALT: () => this.SUBRULE(this.numberLine)
|
|
712
728
|
},
|
|
729
|
+
{
|
|
730
|
+
GATE: this.BACKTRACK(this.gridSymmetry),
|
|
731
|
+
ALT: () => this.SUBRULE(this.gridSymmetry)
|
|
732
|
+
},
|
|
713
733
|
{
|
|
714
734
|
GATE: this.BACKTRACK(this.pairMatcher),
|
|
715
735
|
ALT: () => this.SUBRULE(this.pairMatcher)
|
|
@@ -725,6 +745,18 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
725
745
|
{ ALT: () => this.SUBRULE(this.line) }
|
|
726
746
|
]);
|
|
727
747
|
});
|
|
748
|
+
this.gridSymmetry = this.RULE("gridSymmetry", () => {
|
|
749
|
+
this.MANY(() => {
|
|
750
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
751
|
+
});
|
|
752
|
+
this.CONSUME(GRID_SYMMETRY);
|
|
753
|
+
this.CONSUME(LBracket);
|
|
754
|
+
this.SUBRULE(this.parameterList);
|
|
755
|
+
this.CONSUME(RBracket);
|
|
756
|
+
this.MANY2(() => {
|
|
757
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
728
760
|
this.table = this.RULE("table", () => {
|
|
729
761
|
this.MANY(() => {
|
|
730
762
|
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
@@ -1438,27 +1470,51 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1438
1470
|
});
|
|
1439
1471
|
this.parameterList = this.RULE("parameterList", () => {
|
|
1440
1472
|
this.MANY(() => {
|
|
1441
|
-
this.
|
|
1473
|
+
this.OR([
|
|
1474
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
1475
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
1476
|
+
]);
|
|
1442
1477
|
});
|
|
1443
1478
|
this.SUBRULE(this.parameter);
|
|
1444
1479
|
this.MANY2(() => {
|
|
1445
1480
|
this.MANY3(() => {
|
|
1446
|
-
this.
|
|
1481
|
+
this.OR2([
|
|
1482
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
1483
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
1484
|
+
]);
|
|
1447
1485
|
});
|
|
1448
1486
|
this.CONSUME(Comma);
|
|
1449
1487
|
this.MANY4(() => {
|
|
1450
|
-
this.
|
|
1488
|
+
this.OR3([
|
|
1489
|
+
{ ALT: () => this.CONSUME3(Space) },
|
|
1490
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1491
|
+
]);
|
|
1451
1492
|
});
|
|
1452
1493
|
this.SUBRULE2(this.parameter);
|
|
1453
1494
|
});
|
|
1454
1495
|
this.MANY5(() => {
|
|
1455
|
-
this.
|
|
1496
|
+
this.OR4([
|
|
1497
|
+
{ ALT: () => this.CONSUME4(Space) },
|
|
1498
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
1499
|
+
]);
|
|
1456
1500
|
});
|
|
1457
1501
|
});
|
|
1458
1502
|
this.parameter = this.RULE("parameter", () => {
|
|
1459
1503
|
this.CONSUME(Identifier, { LABEL: "key" });
|
|
1504
|
+
this.MANY(() => {
|
|
1505
|
+
this.OR([
|
|
1506
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
1507
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
1508
|
+
]);
|
|
1509
|
+
});
|
|
1460
1510
|
this.CONSUME(Equals);
|
|
1461
|
-
this.
|
|
1511
|
+
this.MANY2(() => {
|
|
1512
|
+
this.OR2([
|
|
1513
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
1514
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
1515
|
+
]);
|
|
1516
|
+
});
|
|
1517
|
+
this.OR3([
|
|
1462
1518
|
// String value
|
|
1463
1519
|
{ ALT: () => this.CONSUME(StringLiteral, { LABEL: "value" }) },
|
|
1464
1520
|
// Number value
|
|
@@ -1467,6 +1523,11 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1467
1523
|
{ ALT: () => this.CONSUME2(Identifier, { LABEL: "value" }) },
|
|
1468
1524
|
// Tuple value: (start, end)
|
|
1469
1525
|
{ ALT: () => this.SUBRULE(this.tuple, { LABEL: "value" }) },
|
|
1526
|
+
// Number-pair list enclosed in braces: {[1, 2], [3, 4]}
|
|
1527
|
+
{
|
|
1528
|
+
GATE: () => this.isBracedNumberPairListAhead(),
|
|
1529
|
+
ALT: () => this.SUBRULE(this.bracedNumberPairList, { LABEL: "value" })
|
|
1530
|
+
},
|
|
1470
1531
|
// Curly-brace string list: {"a", "b", "c"}
|
|
1471
1532
|
{ ALT: () => this.SUBRULE(this.curlyStringList, { LABEL: "value" }) },
|
|
1472
1533
|
// Square-bracket number array: [1, 2, 3] — used by VISUAL_FRACTION filled/answer
|
|
@@ -1522,8 +1583,52 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1522
1583
|
this.SUBRULE(this.stringList);
|
|
1523
1584
|
this.CONSUME(RBrace);
|
|
1524
1585
|
});
|
|
1586
|
+
this.bracedNumberPairList = this.RULE("bracedNumberPairList", () => {
|
|
1587
|
+
this.CONSUME(LBrace);
|
|
1588
|
+
this.MANY(() => {
|
|
1589
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1590
|
+
});
|
|
1591
|
+
this.OPTION(() => {
|
|
1592
|
+
this.SUBRULE(this.numberPair);
|
|
1593
|
+
this.MANY2(() => {
|
|
1594
|
+
this.MANY3(() => {
|
|
1595
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1596
|
+
});
|
|
1597
|
+
this.CONSUME(Comma);
|
|
1598
|
+
this.MANY4(() => {
|
|
1599
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1600
|
+
});
|
|
1601
|
+
this.SUBRULE2(this.numberPair);
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
this.MANY5(() => {
|
|
1605
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1606
|
+
});
|
|
1607
|
+
this.CONSUME(RBrace);
|
|
1608
|
+
});
|
|
1609
|
+
this.numberPair = this.RULE("numberPair", () => {
|
|
1610
|
+
this.CONSUME(LBracket);
|
|
1611
|
+
this.MANY(() => this.CONSUME(Space));
|
|
1612
|
+
this.CONSUME(NumberLiteral, { LABEL: "row" });
|
|
1613
|
+
this.MANY2(() => this.CONSUME2(Space));
|
|
1614
|
+
this.CONSUME(Comma);
|
|
1615
|
+
this.MANY3(() => this.CONSUME3(Space));
|
|
1616
|
+
this.CONSUME2(NumberLiteral, { LABEL: "col" });
|
|
1617
|
+
this.MANY4(() => this.CONSUME4(Space));
|
|
1618
|
+
this.CONSUME(RBracket);
|
|
1619
|
+
});
|
|
1525
1620
|
this.performSelfAnalysis();
|
|
1526
1621
|
}
|
|
1622
|
+
/** Distinguish `{[number, number], ...}` and the valid empty form `{}` from string lists. */
|
|
1623
|
+
isBracedNumberPairListAhead() {
|
|
1624
|
+
if (this.LA(1).tokenType !== LBrace) return false;
|
|
1625
|
+
let offset = 2;
|
|
1626
|
+
while (this.LA(offset).tokenType === Space || this.LA(offset).tokenType === Newline) {
|
|
1627
|
+
offset++;
|
|
1628
|
+
}
|
|
1629
|
+
const firstContentToken = this.LA(offset).tokenType;
|
|
1630
|
+
return firstContentToken === LBracket || firstContentToken === RBrace;
|
|
1631
|
+
}
|
|
1527
1632
|
}
|
|
1528
1633
|
const parser = new MathExerciseParser();
|
|
1529
1634
|
function parseOrThrow(schema, data, label) {
|
|
@@ -2068,6 +2173,135 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style, lab
|
|
|
2068
2173
|
positions
|
|
2069
2174
|
});
|
|
2070
2175
|
}
|
|
2176
|
+
const GRID_SYMMETRY_AXES = ["vertical", "horizontal"];
|
|
2177
|
+
const GRID_SYMMETRY_INTERACTIVE_AREAS = ["left", "right", "top", "bottom"];
|
|
2178
|
+
const GRID_SYMMETRY_MAX_ROWS = 20;
|
|
2179
|
+
const GRID_SYMMETRY_MAX_COLS = 20;
|
|
2180
|
+
const GRID_SYMMETRY_MIN_CELL_SIZE = 0.25;
|
|
2181
|
+
const GRID_SYMMETRY_MAX_CELL_SIZE = 2;
|
|
2182
|
+
function isCellInInteractiveArea(cell, rows, cols, area) {
|
|
2183
|
+
const [row, col] = cell;
|
|
2184
|
+
switch (area) {
|
|
2185
|
+
case "left":
|
|
2186
|
+
return col <= Math.floor(cols / 2);
|
|
2187
|
+
case "right":
|
|
2188
|
+
return col > Math.ceil(cols / 2);
|
|
2189
|
+
case "top":
|
|
2190
|
+
return row <= Math.floor(rows / 2);
|
|
2191
|
+
case "bottom":
|
|
2192
|
+
return row > Math.ceil(rows / 2);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
function reflectGridSymmetryCell(cell, rows, cols, axis) {
|
|
2196
|
+
const [row, col] = cell;
|
|
2197
|
+
return axis === "vertical" ? [row, cols + 1 - col] : [rows + 1 - row, col];
|
|
2198
|
+
}
|
|
2199
|
+
function computeGridSymmetryExpectedCells(data) {
|
|
2200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2201
|
+
const expected = [];
|
|
2202
|
+
for (const cell of data.cells) {
|
|
2203
|
+
if (isCellInInteractiveArea(cell, data.rows, data.cols, data.interactiveArea)) continue;
|
|
2204
|
+
const reflected = reflectGridSymmetryCell(cell, data.rows, data.cols, data.axis);
|
|
2205
|
+
if (!isCellInInteractiveArea(reflected, data.rows, data.cols, data.interactiveArea)) continue;
|
|
2206
|
+
const key2 = gridCellKey(reflected);
|
|
2207
|
+
if (!seen.has(key2)) {
|
|
2208
|
+
seen.add(key2);
|
|
2209
|
+
expected.push(reflected);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return expected.sort(([ar, ac], [br, bc]) => ar - br || ac - bc);
|
|
2213
|
+
}
|
|
2214
|
+
function computeGridSymmetryCellsToFill(data) {
|
|
2215
|
+
const initial = new Set(data.cells.map(gridCellKey));
|
|
2216
|
+
return computeGridSymmetryExpectedCells(data).filter((cell) => !initial.has(gridCellKey(cell)));
|
|
2217
|
+
}
|
|
2218
|
+
const GridSymmetryBlockSchema = zod.z.object({
|
|
2219
|
+
type: zod.z.literal(AST_TYPES.GRID_SYMMETRY_BLOCK),
|
|
2220
|
+
rows: zod.z.number().int("The number of rows must be a whole number.").positive("The number of rows must be greater than 0.").max(GRID_SYMMETRY_MAX_ROWS, {
|
|
2221
|
+
message: `The number of rows cannot exceed ${GRID_SYMMETRY_MAX_ROWS}.`
|
|
2222
|
+
}),
|
|
2223
|
+
cols: zod.z.number().int("The number of columns must be a whole number.").positive("The number of columns must be greater than 0.").max(GRID_SYMMETRY_MAX_COLS, {
|
|
2224
|
+
message: `The number of columns cannot exceed ${GRID_SYMMETRY_MAX_COLS}.`
|
|
2225
|
+
}),
|
|
2226
|
+
axis: zod.z.enum(GRID_SYMMETRY_AXES),
|
|
2227
|
+
interactiveArea: zod.z.enum(GRID_SYMMETRY_INTERACTIVE_AREAS),
|
|
2228
|
+
cellSize: zod.z.number().finite().min(
|
|
2229
|
+
GRID_SYMMETRY_MIN_CELL_SIZE,
|
|
2230
|
+
`Cell size must be between ${GRID_SYMMETRY_MIN_CELL_SIZE} and ${GRID_SYMMETRY_MAX_CELL_SIZE}.`
|
|
2231
|
+
).max(
|
|
2232
|
+
GRID_SYMMETRY_MAX_CELL_SIZE,
|
|
2233
|
+
`Cell size must be between ${GRID_SYMMETRY_MIN_CELL_SIZE} and ${GRID_SYMMETRY_MAX_CELL_SIZE}.`
|
|
2234
|
+
).default(1),
|
|
2235
|
+
cells: zod.z.array(GridCellSchema)
|
|
2236
|
+
}).superRefine((data, ctx) => {
|
|
2237
|
+
const compatible = data.axis === "vertical" ? data.interactiveArea === "left" || data.interactiveArea === "right" : data.interactiveArea === "top" || data.interactiveArea === "bottom";
|
|
2238
|
+
if (!compatible) {
|
|
2239
|
+
ctx.addIssue({
|
|
2240
|
+
code: zod.z.ZodIssueCode.custom,
|
|
2241
|
+
path: ["interactiveArea"],
|
|
2242
|
+
message: data.axis === "vertical" ? 'For a vertical axis, the interactive area must be "left" or "right".' : 'For a horizontal axis, the interactive area must be "top" or "bottom".'
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
if (data.axis === "vertical" && data.cols % 2 !== 0) {
|
|
2246
|
+
ctx.addIssue({
|
|
2247
|
+
code: zod.z.ZodIssueCode.custom,
|
|
2248
|
+
path: ["cols"],
|
|
2249
|
+
message: `For a vertical axis, the number of columns must be even. Received: ${data.cols}.`
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
if (data.axis === "horizontal" && data.rows % 2 !== 0) {
|
|
2253
|
+
ctx.addIssue({
|
|
2254
|
+
code: zod.z.ZodIssueCode.custom,
|
|
2255
|
+
path: ["rows"],
|
|
2256
|
+
message: `For a horizontal axis, the number of rows must be even. Received: ${data.rows}.`
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2260
|
+
data.cells.forEach((cell, index) => {
|
|
2261
|
+
const [row, col] = cell;
|
|
2262
|
+
if (row > data.rows || col > data.cols) {
|
|
2263
|
+
ctx.addIssue({
|
|
2264
|
+
code: zod.z.ZodIssueCode.custom,
|
|
2265
|
+
path: ["cells", index],
|
|
2266
|
+
message: `Cell [${row},${col}] is outside the ${data.rows} × ${data.cols} grid.`
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
const key2 = gridCellKey(cell);
|
|
2270
|
+
if (seen.has(key2)) {
|
|
2271
|
+
ctx.addIssue({
|
|
2272
|
+
code: zod.z.ZodIssueCode.custom,
|
|
2273
|
+
path: ["cells", index],
|
|
2274
|
+
message: `Cell [${row},${col}] is listed more than once.`
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
seen.add(key2);
|
|
2278
|
+
});
|
|
2279
|
+
if (!compatible) return;
|
|
2280
|
+
const expected = new Set(computeGridSymmetryExpectedCells(data).map(gridCellKey));
|
|
2281
|
+
data.cells.forEach((cell, index) => {
|
|
2282
|
+
if (isCellInInteractiveArea(cell, data.rows, data.cols, data.interactiveArea) && !expected.has(gridCellKey(cell))) {
|
|
2283
|
+
ctx.addIssue({
|
|
2284
|
+
code: zod.z.ZodIssueCode.custom,
|
|
2285
|
+
path: ["cells", index],
|
|
2286
|
+
message: `Prefilled cell [${cell[0]},${cell[1]}] does not match the expected symmetry.`
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
function createGridSymmetryBlock(rows, cols, axis, interactiveArea, cells, cellSize = 1) {
|
|
2292
|
+
return GridSymmetryBlockSchema.parse({
|
|
2293
|
+
type: AST_TYPES.GRID_SYMMETRY_BLOCK,
|
|
2294
|
+
rows,
|
|
2295
|
+
cols,
|
|
2296
|
+
axis,
|
|
2297
|
+
interactiveArea,
|
|
2298
|
+
cellSize,
|
|
2299
|
+
cells
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
function isGridSymmetryBlock(value) {
|
|
2303
|
+
return GridSymmetryBlockSchema.safeParse(value).success;
|
|
2304
|
+
}
|
|
2071
2305
|
const TableCellSchema = zod.z.object({
|
|
2072
2306
|
type: zod.z.literal(AST_TYPES.TABLE_CELL),
|
|
2073
2307
|
colspan: zod.z.number().int().positive().optional(),
|
|
@@ -2567,7 +2801,7 @@ function isMixedValidation(value) {
|
|
|
2567
2801
|
function isValidationNode(value) {
|
|
2568
2802
|
return ValidationNodeSchema.safeParse(value).success;
|
|
2569
2803
|
}
|
|
2570
|
-
const ContentBlockSchema = zod.z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema]);
|
|
2804
|
+
const ContentBlockSchema = zod.z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema, GridSymmetryBlockSchema]);
|
|
2571
2805
|
const ExerciseDocumentSchema = zod.z.object({
|
|
2572
2806
|
type: zod.z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
2573
2807
|
question: TextBlockSchema.optional(),
|
|
@@ -2669,12 +2903,24 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2669
2903
|
if (ctx.table) return this.visit(ctx.table);
|
|
2670
2904
|
if (ctx.radioButtons) return this.visit(ctx.radioButtons);
|
|
2671
2905
|
if (ctx.numberLine) return this.visit(ctx.numberLine);
|
|
2906
|
+
if (ctx.gridSymmetry) return this.visit(ctx.gridSymmetry);
|
|
2672
2907
|
if (ctx.pairMatcher) return this.visit(ctx.pairMatcher);
|
|
2673
2908
|
if (ctx.matcher) return this.visit(ctx.matcher);
|
|
2674
2909
|
if (ctx.verticalOperation) return this.visit(ctx.verticalOperation);
|
|
2675
2910
|
if (ctx.line) return this.visit(ctx.line);
|
|
2676
2911
|
throw new Error("Unknown content block");
|
|
2677
2912
|
}
|
|
2913
|
+
gridSymmetry(ctx) {
|
|
2914
|
+
const params = this.visit(ctx.parameterList);
|
|
2915
|
+
return createGridSymmetryBlock(
|
|
2916
|
+
Number(params.rows),
|
|
2917
|
+
Number(params.cols),
|
|
2918
|
+
params.axis,
|
|
2919
|
+
params.interactiveArea,
|
|
2920
|
+
params.cells,
|
|
2921
|
+
params.cellSize === void 0 ? 1 : Number(params.cellSize)
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2678
2924
|
radioButtons(ctx) {
|
|
2679
2925
|
const params = this.visit(ctx.parameterList);
|
|
2680
2926
|
const name = params.name;
|
|
@@ -3263,6 +3509,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
3263
3509
|
curlyStringList(ctx) {
|
|
3264
3510
|
return this.visit(ctx.stringList);
|
|
3265
3511
|
}
|
|
3512
|
+
bracedNumberPairList(ctx) {
|
|
3513
|
+
return ctx.numberPair ? ctx.numberPair.map((pair) => this.visit(pair)) : [];
|
|
3514
|
+
}
|
|
3515
|
+
numberPair(ctx) {
|
|
3516
|
+
return [
|
|
3517
|
+
parseFloat(ctx.row[0].image),
|
|
3518
|
+
parseFloat(ctx.col[0].image)
|
|
3519
|
+
];
|
|
3520
|
+
}
|
|
3266
3521
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3267
3522
|
// Helpers
|
|
3268
3523
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -3639,6 +3894,34 @@ const VisualFractionJSONSchema = zod.z.object({
|
|
|
3639
3894
|
*/
|
|
3640
3895
|
size: zod.z.number().positive().optional()
|
|
3641
3896
|
}).superRefine(validateVisualFractionCapacity);
|
|
3897
|
+
const GridSymmetryJSONSchema = zod.z.object({
|
|
3898
|
+
type: zod.z.literal(JSON_TYPES.GRID_SYMMETRY),
|
|
3899
|
+
rows: zod.z.number().int().positive(),
|
|
3900
|
+
cols: zod.z.number().int().positive(),
|
|
3901
|
+
axis: zod.z.enum(["vertical", "horizontal"]),
|
|
3902
|
+
interactiveArea: zod.z.enum(["left", "right", "top", "bottom"]),
|
|
3903
|
+
cellSize: zod.z.number().finite().positive().default(1),
|
|
3904
|
+
cells: zod.z.array(GridCellSchema)
|
|
3905
|
+
}).superRefine((data, ctx) => {
|
|
3906
|
+
const result = GridSymmetryBlockSchema.safeParse({
|
|
3907
|
+
type: AST_TYPES.GRID_SYMMETRY_BLOCK,
|
|
3908
|
+
rows: data.rows,
|
|
3909
|
+
cols: data.cols,
|
|
3910
|
+
axis: data.axis,
|
|
3911
|
+
interactiveArea: data.interactiveArea,
|
|
3912
|
+
cellSize: data.cellSize,
|
|
3913
|
+
cells: data.cells
|
|
3914
|
+
});
|
|
3915
|
+
if (!result.success) {
|
|
3916
|
+
for (const issue of result.error.issues) {
|
|
3917
|
+
ctx.addIssue({
|
|
3918
|
+
code: zod.z.ZodIssueCode.custom,
|
|
3919
|
+
path: issue.path,
|
|
3920
|
+
message: issue.message
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
});
|
|
3642
3925
|
const RulerWidgetSchema = zod.z.object({
|
|
3643
3926
|
type: zod.z.literal(JSON_TYPES.RULER),
|
|
3644
3927
|
size: zod.z.number().optional()
|
|
@@ -3661,6 +3944,7 @@ const WidgetJSONSchema = zod.z.union([
|
|
|
3661
3944
|
MatcherWidgetJSONSchema,
|
|
3662
3945
|
PairMatcherWidgetJSONSchema,
|
|
3663
3946
|
VisualFractionJSONSchema,
|
|
3947
|
+
GridSymmetryJSONSchema,
|
|
3664
3948
|
RulerWidgetSchema,
|
|
3665
3949
|
ProtractorWidgetSchema,
|
|
3666
3950
|
SetSquareWidgetSchema
|
|
@@ -3676,7 +3960,8 @@ const ValidationJSONSchema = zod.z.object({
|
|
|
3676
3960
|
zod.z.string(),
|
|
3677
3961
|
zod.z.number(),
|
|
3678
3962
|
zod.z.array(zod.z.array(zod.z.string().min(1)).min(1)).min(1),
|
|
3679
|
-
zod.z.array(zod.z.number().int().nonnegative())
|
|
3963
|
+
zod.z.array(zod.z.number().int().nonnegative()),
|
|
3964
|
+
zod.z.array(GridCellSchema)
|
|
3680
3965
|
])
|
|
3681
3966
|
).optional(),
|
|
3682
3967
|
expression: zod.z.string().optional(),
|
|
@@ -3774,6 +4059,9 @@ function isNumericKeypadJSON(widget) {
|
|
|
3774
4059
|
function isFractionJSON(widget) {
|
|
3775
4060
|
return widget.type === JSON_TYPES.FRACTION;
|
|
3776
4061
|
}
|
|
4062
|
+
function isGridSymmetryJSON(widget) {
|
|
4063
|
+
return widget.type === JSON_TYPES.GRID_SYMMETRY;
|
|
4064
|
+
}
|
|
3777
4065
|
function isRulerWidget(widget) {
|
|
3778
4066
|
return widget.type === JSON_TYPES.RULER;
|
|
3779
4067
|
}
|
|
@@ -9281,6 +9569,7 @@ function toJSON(ast) {
|
|
|
9281
9569
|
const idGenerator = new LayoutIdGenerator();
|
|
9282
9570
|
const widgets = {};
|
|
9283
9571
|
const answers = {};
|
|
9572
|
+
let gridSymmetryCounter = 0;
|
|
9284
9573
|
const allInlines = collectAllInlines(ast.lines);
|
|
9285
9574
|
const inlineDragIds = /* @__PURE__ */ new Map();
|
|
9286
9575
|
{
|
|
@@ -9335,6 +9624,34 @@ function toJSON(ast) {
|
|
|
9335
9624
|
});
|
|
9336
9625
|
} else if (block.type === AST_TYPES.VERTICAL_OPERATION_BLOCK) {
|
|
9337
9626
|
layout.push(transformVerticalOperation(block, idGenerator));
|
|
9627
|
+
} else if (block.type === AST_TYPES.GRID_SYMMETRY_BLOCK) {
|
|
9628
|
+
const grid = block;
|
|
9629
|
+
const widgetId = `${GRID_SYMMETRY_WIDGET_PREFIX}${gridSymmetryCounter++}`;
|
|
9630
|
+
widgets[widgetId] = {
|
|
9631
|
+
type: JSON_TYPES.GRID_SYMMETRY,
|
|
9632
|
+
rows: grid.rows,
|
|
9633
|
+
cols: grid.cols,
|
|
9634
|
+
axis: grid.axis,
|
|
9635
|
+
interactiveArea: grid.interactiveArea,
|
|
9636
|
+
cellSize: grid.cellSize,
|
|
9637
|
+
cells: grid.cells
|
|
9638
|
+
};
|
|
9639
|
+
const prefilled = grid.cells.filter((cell) => isCellInInteractiveArea(
|
|
9640
|
+
cell,
|
|
9641
|
+
grid.rows,
|
|
9642
|
+
grid.cols,
|
|
9643
|
+
grid.interactiveArea
|
|
9644
|
+
));
|
|
9645
|
+
answers[widgetId] = [...prefilled, ...computeGridSymmetryCellsToFill(grid)].sort(([ar, ac], [br, bc]) => ar - br || ac - bc);
|
|
9646
|
+
layout.push({
|
|
9647
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
9648
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
9649
|
+
widgetId
|
|
9650
|
+
});
|
|
9651
|
+
layout.push({
|
|
9652
|
+
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
9653
|
+
type: JSON_TYPES.LINEBREAK
|
|
9654
|
+
});
|
|
9338
9655
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
9339
9656
|
const radioBlock = block;
|
|
9340
9657
|
layout.push({
|
|
@@ -9932,6 +10249,18 @@ function areVisualFractionCountsEqual(expected, result) {
|
|
|
9932
10249
|
if (expected.length !== result.length) return false;
|
|
9933
10250
|
return lodashExports.isEqual(lodashExports.sortBy(expected), lodashExports.sortBy(result));
|
|
9934
10251
|
}
|
|
10252
|
+
function isValidGridSymmetryCell(value, rows, cols) {
|
|
10253
|
+
if (!Array.isArray(value) || value.length !== 2) return false;
|
|
10254
|
+
const [row, col] = value;
|
|
10255
|
+
return Number.isInteger(row) && Number.isInteger(col) && row >= 1 && row <= rows && col >= 1 && col <= cols;
|
|
10256
|
+
}
|
|
10257
|
+
function areGridSymmetryCellsEqual(expected, actual, rows, cols) {
|
|
10258
|
+
if (!Array.isArray(actual) || actual.length !== expected.length) return false;
|
|
10259
|
+
if (!actual.every((cell) => isValidGridSymmetryCell(cell, rows, cols))) return false;
|
|
10260
|
+
const actualKeys = new Set(actual.map(gridCellKey));
|
|
10261
|
+
if (actualKeys.size !== actual.length) return false;
|
|
10262
|
+
return expected.every((cell) => actualKeys.has(gridCellKey(cell)));
|
|
10263
|
+
}
|
|
9935
10264
|
const math$1 = mathjs.create(mathjs.all);
|
|
9936
10265
|
function collectNumberLines(items) {
|
|
9937
10266
|
const result = [];
|
|
@@ -10005,6 +10334,20 @@ function validateSimple(exercise, userInputs) {
|
|
|
10005
10334
|
else allCorrect = false;
|
|
10006
10335
|
continue;
|
|
10007
10336
|
}
|
|
10337
|
+
if (widget?.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
10338
|
+
const userValue2 = userInputs[widgetId];
|
|
10339
|
+
const isCorrect2 = areGridSymmetryCellsEqual(
|
|
10340
|
+
expected,
|
|
10341
|
+
userValue2,
|
|
10342
|
+
widget.rows,
|
|
10343
|
+
widget.cols
|
|
10344
|
+
);
|
|
10345
|
+
details[widgetId] = { widgetId, userValue: userValue2, isCorrect: isCorrect2 };
|
|
10346
|
+
totalCount++;
|
|
10347
|
+
if (isCorrect2) correctCount++;
|
|
10348
|
+
else allCorrect = false;
|
|
10349
|
+
continue;
|
|
10350
|
+
}
|
|
10008
10351
|
totalCount++;
|
|
10009
10352
|
const userValue = userInputs[widgetId];
|
|
10010
10353
|
const isFractionLenient = widget?.type === JSON_TYPES.FRACTION && widget.simplified === false;
|
|
@@ -10435,9 +10778,19 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
10435
10778
|
}
|
|
10436
10779
|
const inputs = preprocessFractionValues(exercise, userInputs);
|
|
10437
10780
|
const isEmpty = (v) => v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
|
|
10781
|
+
const isMissingAnswer = (widgetId) => {
|
|
10782
|
+
const value = inputs[widgetId];
|
|
10783
|
+
const widget = exercise.widgets[widgetId];
|
|
10784
|
+
if (widget?.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
10785
|
+
const expected = exercise.validation?.answers?.[widgetId];
|
|
10786
|
+
const expectedCount = Array.isArray(expected) ? expected.length : 0;
|
|
10787
|
+
return !Array.isArray(value) || value.length < expectedCount;
|
|
10788
|
+
}
|
|
10789
|
+
return !(widgetId in inputs) || isEmpty(value);
|
|
10790
|
+
};
|
|
10438
10791
|
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
10439
10792
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
10440
|
-
if (
|
|
10793
|
+
if (isMissingAnswer(widgetId)) {
|
|
10441
10794
|
missing.push(widgetId);
|
|
10442
10795
|
}
|
|
10443
10796
|
}
|
|
@@ -10453,7 +10806,7 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
10453
10806
|
...exercise.validation.variables ?? []
|
|
10454
10807
|
]);
|
|
10455
10808
|
for (const k of keys) {
|
|
10456
|
-
if (
|
|
10809
|
+
if (isMissingAnswer(k)) missing.push(k);
|
|
10457
10810
|
}
|
|
10458
10811
|
}
|
|
10459
10812
|
return missing;
|
|
@@ -15098,13 +15451,18 @@ function applyDropToInput(targetId, item, onChange) {
|
|
|
15098
15451
|
function isUndoDrop(event, dragId) {
|
|
15099
15452
|
return event.active.id === dragId && !event.over;
|
|
15100
15453
|
}
|
|
15454
|
+
const LockContext = React.createContext(false);
|
|
15455
|
+
const ExerciseLockProvider = ({ locked, children }) => /* @__PURE__ */ jsxRuntime.jsx(LockContext.Provider, { value: locked, children });
|
|
15456
|
+
const useExerciseLocked = () => React.useContext(LockContext);
|
|
15101
15457
|
const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
|
|
15102
15458
|
const { droppedSources } = useExerciseDnd();
|
|
15459
|
+
const locked = useExerciseLocked();
|
|
15103
15460
|
const sourceOptionId = droppedSources.get(widgetId);
|
|
15104
15461
|
const dragId = `${widgetId}__chip`;
|
|
15105
15462
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
15106
15463
|
id: dragId,
|
|
15107
|
-
data: { type: "placed-chip", widgetId, label: value, sourceOptionId }
|
|
15464
|
+
data: { type: "placed-chip", widgetId, label: value, sourceOptionId },
|
|
15465
|
+
disabled: locked
|
|
15108
15466
|
});
|
|
15109
15467
|
useDndMonitor({
|
|
15110
15468
|
onDragEnd(event) {
|
|
@@ -15117,10 +15475,10 @@ const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
|
|
|
15117
15475
|
ref: setNodeRef,
|
|
15118
15476
|
className: `inline-input-chip ${className}${isDragging ? " dragging" : ""}`.trim(),
|
|
15119
15477
|
onClick: (e) => {
|
|
15120
|
-
if (e.detail !== 0) onRemove();
|
|
15478
|
+
if (!locked && e.detail !== 0) onRemove();
|
|
15121
15479
|
},
|
|
15122
|
-
title: "Drag to move, or click to remove",
|
|
15123
|
-
style: { touchAction: "none", cursor: "grab" },
|
|
15480
|
+
title: locked ? void 0 : "Drag to move, or click to remove",
|
|
15481
|
+
style: { touchAction: "none", cursor: locked ? "default" : "grab" },
|
|
15124
15482
|
...listeners,
|
|
15125
15483
|
...attributes,
|
|
15126
15484
|
children: value
|
|
@@ -15140,6 +15498,7 @@ const InlineInput = ({
|
|
|
15140
15498
|
onFocus
|
|
15141
15499
|
}) => {
|
|
15142
15500
|
const playSound = useSound();
|
|
15501
|
+
const locked = useExerciseLocked();
|
|
15143
15502
|
const handleChange = React.useCallback((newValue, fromDrop = false, sourceOptionId) => {
|
|
15144
15503
|
onChange(widgetId, newValue, fromDrop, sourceOptionId);
|
|
15145
15504
|
}, [onChange, widgetId]);
|
|
@@ -15155,6 +15514,7 @@ const InlineInput = ({
|
|
|
15155
15514
|
id: widgetId,
|
|
15156
15515
|
accepts: ["drag-option", "placed-chip"],
|
|
15157
15516
|
onDrop: handleDrop,
|
|
15517
|
+
disabled: locked,
|
|
15158
15518
|
className: "inline-widget inline-input-droppable",
|
|
15159
15519
|
children: isDropped && value !== "" ? (
|
|
15160
15520
|
// Show as chip only when value came from drag-drop
|
|
@@ -15177,8 +15537,11 @@ const InlineInput = ({
|
|
|
15177
15537
|
className: `inline-input${statusClass}`,
|
|
15178
15538
|
value,
|
|
15179
15539
|
onChange: (e) => handleChange(e.target.value, false),
|
|
15180
|
-
onMouseDown: () =>
|
|
15540
|
+
onMouseDown: () => {
|
|
15541
|
+
if (!locked) playSound("input-select");
|
|
15542
|
+
},
|
|
15181
15543
|
onFocus: () => onFocus?.(widgetId),
|
|
15544
|
+
readOnly: locked,
|
|
15182
15545
|
maxLength: expectedLength,
|
|
15183
15546
|
style: { width: dynamicWidth },
|
|
15184
15547
|
placeholder
|
|
@@ -15195,6 +15558,7 @@ const SingleDragOption = ({
|
|
|
15195
15558
|
className = ""
|
|
15196
15559
|
}) => {
|
|
15197
15560
|
const { usedOptionIds } = useExerciseDnd();
|
|
15561
|
+
const locked = useExerciseLocked();
|
|
15198
15562
|
const isUsed = usedOptionIds.has(optionId);
|
|
15199
15563
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
15200
15564
|
Draggable,
|
|
@@ -15202,12 +15566,16 @@ const SingleDragOption = ({
|
|
|
15202
15566
|
id: optionId,
|
|
15203
15567
|
type: "drag-option",
|
|
15204
15568
|
data: { value, label: value },
|
|
15569
|
+
disabled: locked,
|
|
15205
15570
|
className: "drag-option-wrapper",
|
|
15206
15571
|
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
15207
15572
|
"button",
|
|
15208
15573
|
{
|
|
15209
15574
|
className: "drag-option",
|
|
15210
|
-
onClick: () =>
|
|
15575
|
+
onClick: () => {
|
|
15576
|
+
if (!locked) onOptionClick?.(value);
|
|
15577
|
+
},
|
|
15578
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
15211
15579
|
children: value
|
|
15212
15580
|
}
|
|
15213
15581
|
)
|
|
@@ -15321,31 +15689,34 @@ const NumericKeypad = ({
|
|
|
15321
15689
|
onChange
|
|
15322
15690
|
}) => {
|
|
15323
15691
|
const playSound = useSound();
|
|
15692
|
+
const locked = useExerciseLocked();
|
|
15324
15693
|
const handleDigit = (digit) => {
|
|
15694
|
+
if (locked) return;
|
|
15325
15695
|
playSound("keypad-press");
|
|
15326
15696
|
onChange(value + digit);
|
|
15327
15697
|
};
|
|
15328
15698
|
const handleBackspace = () => {
|
|
15699
|
+
if (locked) return;
|
|
15329
15700
|
playSound("keypad-press");
|
|
15330
15701
|
onChange(value.slice(0, -1));
|
|
15331
15702
|
};
|
|
15332
15703
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, children: [
|
|
15333
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
|
|
15334
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
|
|
15335
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
|
|
15336
|
-
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
|
|
15337
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
|
|
15338
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
|
|
15339
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
|
|
15340
|
-
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
|
|
15341
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
|
|
15342
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
|
|
15343
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
|
|
15344
|
-
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
|
|
15345
|
-
includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
|
|
15346
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
|
|
15347
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
|
|
15348
|
-
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
|
|
15704
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
|
|
15705
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
|
|
15706
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
|
|
15707
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
|
|
15708
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
|
|
15709
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
|
|
15710
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
|
|
15711
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
|
|
15712
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
|
|
15713
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
|
|
15714
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
|
|
15715
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
|
|
15716
|
+
includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
|
|
15717
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
|
|
15718
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
|
|
15719
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
|
|
15349
15720
|
] });
|
|
15350
15721
|
};
|
|
15351
15722
|
const FractionInput = ({
|
|
@@ -15362,6 +15733,7 @@ const FractionInput = ({
|
|
|
15362
15733
|
placeholder
|
|
15363
15734
|
}) => {
|
|
15364
15735
|
const playSound = useSound();
|
|
15736
|
+
const locked = useExerciseLocked();
|
|
15365
15737
|
const numKey = getFractionKey(widgetId, "num");
|
|
15366
15738
|
const denKey = getFractionKey(widgetId, "den");
|
|
15367
15739
|
const numValue = values[numKey] ?? "";
|
|
@@ -15382,6 +15754,7 @@ const FractionInput = ({
|
|
|
15382
15754
|
id: subKey,
|
|
15383
15755
|
accepts: ["drag-option", "placed-chip"],
|
|
15384
15756
|
onDrop: handleDrop(subKey),
|
|
15757
|
+
disabled: locked,
|
|
15385
15758
|
className: "fraction-slot-droppable",
|
|
15386
15759
|
children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
15387
15760
|
PlacedInputChip,
|
|
@@ -15399,8 +15772,11 @@ const FractionInput = ({
|
|
|
15399
15772
|
className: `inline-input fraction-input${slotClass}`,
|
|
15400
15773
|
value: currentValue,
|
|
15401
15774
|
onChange: (e) => onChange(subKey, e.target.value, false),
|
|
15402
|
-
onMouseDown: () =>
|
|
15775
|
+
onMouseDown: () => {
|
|
15776
|
+
if (!locked) playSound("input-select");
|
|
15777
|
+
},
|
|
15403
15778
|
onFocus: () => onFocus?.(subKey),
|
|
15779
|
+
readOnly: locked,
|
|
15404
15780
|
maxLength: placeholder ? Math.max(20, placeholder.length) : 4,
|
|
15405
15781
|
size: placeholder ? placeholder.length : void 0,
|
|
15406
15782
|
"aria-label": label,
|
|
@@ -15631,12 +16007,13 @@ function getEqualTrianglePartPath(index, divisions) {
|
|
|
15631
16007
|
function getGridColumnCount(divisions) {
|
|
15632
16008
|
return Math.ceil(Math.sqrt(divisions));
|
|
15633
16009
|
}
|
|
15634
|
-
function getInteractiveProps(part, onPartClick) {
|
|
16010
|
+
function getInteractiveProps(part, onPartClick, locked = false) {
|
|
15635
16011
|
if (!part.isInteractiveSlot) return {};
|
|
15636
16012
|
return {
|
|
15637
16013
|
onClick: () => onPartClick(part.index),
|
|
15638
16014
|
role: "button",
|
|
15639
16015
|
"aria-pressed": part.isSelected,
|
|
16016
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
15640
16017
|
"aria-label": getPartAriaLabel(part),
|
|
15641
16018
|
tabIndex: 0,
|
|
15642
16019
|
onKeyDown: (event) => {
|
|
@@ -15656,6 +16033,7 @@ const BarShape = ({
|
|
|
15656
16033
|
orientation,
|
|
15657
16034
|
onPartClick
|
|
15658
16035
|
}) => {
|
|
16036
|
+
const locked = useExerciseLocked();
|
|
15659
16037
|
const renderedParts = orientation === "vertical" ? [...parts].reverse() : parts;
|
|
15660
16038
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
15661
16039
|
"div",
|
|
@@ -15667,7 +16045,7 @@ const BarShape = ({
|
|
|
15667
16045
|
{
|
|
15668
16046
|
className: part.className,
|
|
15669
16047
|
style: part.style,
|
|
15670
|
-
...getInteractiveProps(part, onPartClick)
|
|
16048
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
15671
16049
|
},
|
|
15672
16050
|
part.index
|
|
15673
16051
|
))
|
|
@@ -15679,87 +16057,96 @@ const GridShape = ({
|
|
|
15679
16057
|
parts,
|
|
15680
16058
|
divisions,
|
|
15681
16059
|
onPartClick
|
|
15682
|
-
}) =>
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
|
|
15686
|
-
|
|
15687
|
-
"
|
|
15688
|
-
|
|
15689
|
-
|
|
15690
|
-
|
|
15691
|
-
|
|
15692
|
-
|
|
15693
|
-
"div",
|
|
15694
|
-
{
|
|
15695
|
-
className: part.className,
|
|
15696
|
-
style: part.style,
|
|
15697
|
-
...getInteractiveProps(part, onPartClick)
|
|
16060
|
+
}) => {
|
|
16061
|
+
const locked = useExerciseLocked();
|
|
16062
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16063
|
+
"div",
|
|
16064
|
+
{
|
|
16065
|
+
className: "visual-fraction-widget__parts visual-fraction-widget__parts--horizontal",
|
|
16066
|
+
style: {
|
|
16067
|
+
"--visual-fraction-divisions": divisions,
|
|
16068
|
+
"--visual-fraction-grid-cols": getGridColumnCount(divisions),
|
|
16069
|
+
// Safe integer division: zod schema enforces divisions = perfect square.
|
|
16070
|
+
"--visual-fraction-grid-rows": divisions / getGridColumnCount(divisions)
|
|
15698
16071
|
},
|
|
15699
|
-
part.
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
16072
|
+
children: parts.map((part) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
16073
|
+
"div",
|
|
16074
|
+
{
|
|
16075
|
+
className: part.className,
|
|
16076
|
+
style: part.style,
|
|
16077
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
16078
|
+
},
|
|
16079
|
+
part.index
|
|
16080
|
+
))
|
|
16081
|
+
}
|
|
16082
|
+
);
|
|
16083
|
+
};
|
|
15703
16084
|
GridShape.displayName = "GridShape";
|
|
15704
16085
|
const CircleShape = ({
|
|
15705
16086
|
parts,
|
|
15706
16087
|
divisions,
|
|
15707
16088
|
onPartClick
|
|
15708
|
-
}) =>
|
|
15709
|
-
|
|
15710
|
-
|
|
15711
|
-
|
|
15712
|
-
|
|
15713
|
-
|
|
15714
|
-
|
|
15715
|
-
|
|
15716
|
-
|
|
15717
|
-
|
|
15718
|
-
|
|
15719
|
-
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
|
|
15740
|
-
)
|
|
16089
|
+
}) => {
|
|
16090
|
+
const locked = useExerciseLocked();
|
|
16091
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16092
|
+
"svg",
|
|
16093
|
+
{
|
|
16094
|
+
className: "visual-fraction-widget__shape visual-fraction-widget__circle",
|
|
16095
|
+
viewBox: "0 0 100 100",
|
|
16096
|
+
children: parts.map((part) => {
|
|
16097
|
+
const sectorProps = getCircleSectorProps(part.index, divisions);
|
|
16098
|
+
const className = `${part.className} visual-fraction-widget__sector`;
|
|
16099
|
+
const interactiveProps = getInteractiveProps(part, onPartClick, locked);
|
|
16100
|
+
return sectorProps.kind === "circle" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16101
|
+
"circle",
|
|
16102
|
+
{
|
|
16103
|
+
className,
|
|
16104
|
+
style: part.style,
|
|
16105
|
+
cx: "50",
|
|
16106
|
+
cy: "50",
|
|
16107
|
+
r: "48",
|
|
16108
|
+
...interactiveProps
|
|
16109
|
+
},
|
|
16110
|
+
part.index
|
|
16111
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
16112
|
+
"path",
|
|
16113
|
+
{
|
|
16114
|
+
className,
|
|
16115
|
+
style: part.style,
|
|
16116
|
+
d: sectorProps.d,
|
|
16117
|
+
...interactiveProps
|
|
16118
|
+
},
|
|
16119
|
+
part.index
|
|
16120
|
+
);
|
|
16121
|
+
})
|
|
16122
|
+
}
|
|
16123
|
+
);
|
|
16124
|
+
};
|
|
15741
16125
|
CircleShape.displayName = "CircleShape";
|
|
15742
16126
|
const TriangleShape = ({
|
|
15743
16127
|
parts,
|
|
15744
16128
|
divisions,
|
|
15745
16129
|
onPartClick
|
|
15746
|
-
}) =>
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
|
|
15752
|
-
"
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
)
|
|
16130
|
+
}) => {
|
|
16131
|
+
const locked = useExerciseLocked();
|
|
16132
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16133
|
+
"svg",
|
|
16134
|
+
{
|
|
16135
|
+
className: "visual-fraction-widget__shape visual-fraction-widget__triangle",
|
|
16136
|
+
viewBox: "0 0 100 100",
|
|
16137
|
+
children: parts.map((part) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
16138
|
+
"path",
|
|
16139
|
+
{
|
|
16140
|
+
className: `${part.className} visual-fraction-widget__sector`,
|
|
16141
|
+
style: part.style,
|
|
16142
|
+
d: getEqualTrianglePartPath(part.index, divisions),
|
|
16143
|
+
...getInteractiveProps(part, onPartClick, locked)
|
|
16144
|
+
},
|
|
16145
|
+
part.index
|
|
16146
|
+
))
|
|
16147
|
+
}
|
|
16148
|
+
);
|
|
16149
|
+
};
|
|
15763
16150
|
TriangleShape.displayName = "TriangleShape";
|
|
15764
16151
|
const ShapeBar = ({ parts, divisions, orientation, onPartClick }) => /* @__PURE__ */ jsxRuntime.jsx(BarShape, { parts, divisions, orientation, onPartClick });
|
|
15765
16152
|
const ShapeCircle = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsxRuntime.jsx(CircleShape, { parts, divisions, onPartClick });
|
|
@@ -15803,12 +16190,14 @@ const VisualFraction = ({
|
|
|
15803
16190
|
onChange
|
|
15804
16191
|
});
|
|
15805
16192
|
const playSound = useSound();
|
|
16193
|
+
const locked = useExerciseLocked();
|
|
15806
16194
|
const handlePartClickWithSound = React.useCallback(
|
|
15807
16195
|
(index) => {
|
|
16196
|
+
if (locked) return;
|
|
15808
16197
|
if (answer.length > 0 && index >= filledCount) playSound("fraction-shade");
|
|
15809
16198
|
handlePartClick(index);
|
|
15810
16199
|
},
|
|
15811
|
-
[answer.length, filledCount, playSound, handlePartClick]
|
|
16200
|
+
[locked, answer.length, filledCount, playSound, handlePartClick]
|
|
15812
16201
|
);
|
|
15813
16202
|
const parts = React.useMemo(
|
|
15814
16203
|
() => Array.from({ length: divisions }, (_, i) => {
|
|
@@ -15876,6 +16265,97 @@ function getSizeStyle(size, _shape) {
|
|
|
15876
16265
|
"--visual-fraction-size": `${size * VISUAL_FRACTION_SVG_SIZE_SCALE}rem`
|
|
15877
16266
|
};
|
|
15878
16267
|
}
|
|
16268
|
+
const GridSymmetry = ({
|
|
16269
|
+
widgetId,
|
|
16270
|
+
widget,
|
|
16271
|
+
value,
|
|
16272
|
+
validationStatus = null,
|
|
16273
|
+
onChange
|
|
16274
|
+
}) => {
|
|
16275
|
+
const exerciseLocked = useExerciseLocked();
|
|
16276
|
+
const initialKeys = React.useMemo(() => new Set(widget.cells.map(gridCellKey)), [widget.cells]);
|
|
16277
|
+
const initialInteractiveCells = React.useMemo(
|
|
16278
|
+
() => widget.cells.filter(
|
|
16279
|
+
(cell) => isCellInInteractiveArea(cell, widget.rows, widget.cols, widget.interactiveArea)
|
|
16280
|
+
),
|
|
16281
|
+
[widget.cells, widget.rows, widget.cols, widget.interactiveArea]
|
|
16282
|
+
);
|
|
16283
|
+
const selectedCells = value ?? initialInteractiveCells;
|
|
16284
|
+
const selectedKeys = React.useMemo(() => new Set(selectedCells.map(gridCellKey)), [selectedCells]);
|
|
16285
|
+
const toggleCell = (cell) => {
|
|
16286
|
+
if (exerciseLocked) return;
|
|
16287
|
+
const key2 = gridCellKey(cell);
|
|
16288
|
+
if (initialKeys.has(key2)) return;
|
|
16289
|
+
const next = selectedKeys.has(key2) ? selectedCells.filter((candidate) => gridCellKey(candidate) !== key2) : [...selectedCells, cell];
|
|
16290
|
+
onChange(widgetId, next);
|
|
16291
|
+
};
|
|
16292
|
+
const statusClass = validationStatus ? ` grid-symmetry--${validationStatus}` : "";
|
|
16293
|
+
const cells = [];
|
|
16294
|
+
for (let row = 1; row <= widget.rows; row++) {
|
|
16295
|
+
for (let col = 1; col <= widget.cols; col++) {
|
|
16296
|
+
const cell = [row, col];
|
|
16297
|
+
const key2 = gridCellKey(cell);
|
|
16298
|
+
const interactive = isCellInInteractiveArea(
|
|
16299
|
+
cell,
|
|
16300
|
+
widget.rows,
|
|
16301
|
+
widget.cols,
|
|
16302
|
+
widget.interactiveArea
|
|
16303
|
+
);
|
|
16304
|
+
const initial = initialKeys.has(key2);
|
|
16305
|
+
const selected = selectedKeys.has(key2);
|
|
16306
|
+
const locked = initial || !interactive;
|
|
16307
|
+
const roleClass = initial ? interactive ? " grid-symmetry__cell--prefilled" : " grid-symmetry__cell--reference" : selected ? " grid-symmetry__cell--answer" : "";
|
|
16308
|
+
const className = `grid-symmetry__cell${interactive ? " grid-symmetry__cell--interactive" : ""}${locked ? "" : " grid-symmetry__cell--editable"}${selected || initial ? " grid-symmetry__cell--selected" : ""}${roleClass}`;
|
|
16309
|
+
const label = `Row ${row}, column ${col}`;
|
|
16310
|
+
cells.push(
|
|
16311
|
+
locked ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16312
|
+
"span",
|
|
16313
|
+
{
|
|
16314
|
+
className,
|
|
16315
|
+
role: initial ? "img" : void 0,
|
|
16316
|
+
"aria-label": initial ? `${label}, filled` : void 0,
|
|
16317
|
+
"aria-hidden": initial ? void 0 : true
|
|
16318
|
+
},
|
|
16319
|
+
key2
|
|
16320
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
16321
|
+
"button",
|
|
16322
|
+
{
|
|
16323
|
+
type: "button",
|
|
16324
|
+
className,
|
|
16325
|
+
"aria-label": label,
|
|
16326
|
+
"aria-pressed": selected,
|
|
16327
|
+
"aria-disabled": exerciseLocked ? "true" : void 0,
|
|
16328
|
+
onClick: () => toggleCell(cell)
|
|
16329
|
+
},
|
|
16330
|
+
key2
|
|
16331
|
+
)
|
|
16332
|
+
);
|
|
16333
|
+
}
|
|
16334
|
+
}
|
|
16335
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `grid-symmetry${statusClass}`, "data-testid": "grid-symmetry", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
16336
|
+
"div",
|
|
16337
|
+
{
|
|
16338
|
+
className: "grid-symmetry__grid",
|
|
16339
|
+
style: {
|
|
16340
|
+
"--grid-symmetry-cell-size": `calc(48px * ${widget.cellSize})`,
|
|
16341
|
+
gridTemplateColumns: `repeat(${widget.cols}, var(--grid-symmetry-cell-size))`,
|
|
16342
|
+
gridTemplateRows: `repeat(${widget.rows}, var(--grid-symmetry-cell-size))`
|
|
16343
|
+
},
|
|
16344
|
+
role: "group",
|
|
16345
|
+
"aria-label": `Symmetry grid, ${widget.rows} rows by ${widget.cols} columns`,
|
|
16346
|
+
children: [
|
|
16347
|
+
cells,
|
|
16348
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16349
|
+
"span",
|
|
16350
|
+
{
|
|
16351
|
+
className: `grid-symmetry__axis grid-symmetry__axis--${widget.axis}`,
|
|
16352
|
+
"aria-hidden": "true"
|
|
16353
|
+
}
|
|
16354
|
+
)
|
|
16355
|
+
]
|
|
16356
|
+
}
|
|
16357
|
+
) });
|
|
16358
|
+
};
|
|
15879
16359
|
const RadioOptions = ({
|
|
15880
16360
|
name,
|
|
15881
16361
|
options,
|
|
@@ -15884,6 +16364,7 @@ const RadioOptions = ({
|
|
|
15884
16364
|
onChange
|
|
15885
16365
|
}) => {
|
|
15886
16366
|
const playSound = useSound();
|
|
16367
|
+
const locked = useExerciseLocked();
|
|
15887
16368
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "radio-options", children: options.map((option, idx) => {
|
|
15888
16369
|
const isSelected = value === option;
|
|
15889
16370
|
const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
|
|
@@ -15892,9 +16373,11 @@ const RadioOptions = ({
|
|
|
15892
16373
|
{
|
|
15893
16374
|
className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
|
|
15894
16375
|
onClick: () => {
|
|
16376
|
+
if (locked) return;
|
|
15895
16377
|
playSound("radio-select");
|
|
15896
16378
|
onChange(name, option);
|
|
15897
16379
|
},
|
|
16380
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
15898
16381
|
type: "button",
|
|
15899
16382
|
children: option
|
|
15900
16383
|
},
|
|
@@ -15906,7 +16389,10 @@ const Btn = ({
|
|
|
15906
16389
|
label,
|
|
15907
16390
|
extraClass = "keypad-btn-digit",
|
|
15908
16391
|
onClick
|
|
15909
|
-
}) =>
|
|
16392
|
+
}) => {
|
|
16393
|
+
const locked = useExerciseLocked();
|
|
16394
|
+
return /* @__PURE__ */ jsxRuntime.jsx("button", { className: `keypad-btn ${extraClass}`, disabled: locked, onClick, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }) });
|
|
16395
|
+
};
|
|
15910
16396
|
const DiamondKeypad = ({ value, onChange }) => {
|
|
15911
16397
|
const playSound = useSound();
|
|
15912
16398
|
const append = (ch) => {
|
|
@@ -15993,8 +16479,10 @@ const MatcherWidget = ({
|
|
|
15993
16479
|
() => defaultValue2 && defaultValue2.length > 0 ? assignmentsFromValue(defaultValue2, itemIds, groupCount) : initialAssignments(itemIds)
|
|
15994
16480
|
);
|
|
15995
16481
|
const playSound = useSound();
|
|
16482
|
+
const locked = useExerciseLocked();
|
|
15996
16483
|
const handleCardClick = React.useCallback(
|
|
15997
16484
|
(itemId) => {
|
|
16485
|
+
if (locked) return;
|
|
15998
16486
|
playSound("matcher-select");
|
|
15999
16487
|
setAssignments((prev) => {
|
|
16000
16488
|
const current = prev[itemId] ?? null;
|
|
@@ -16002,7 +16490,7 @@ const MatcherWidget = ({
|
|
|
16002
16490
|
return { ...prev, [itemId]: next };
|
|
16003
16491
|
});
|
|
16004
16492
|
},
|
|
16005
|
-
[groupCount, playSound]
|
|
16493
|
+
[groupCount, playSound, locked]
|
|
16006
16494
|
);
|
|
16007
16495
|
const onChangeRef = React.useRef(onChange);
|
|
16008
16496
|
onChangeRef.current = onChange;
|
|
@@ -16039,6 +16527,7 @@ const MatcherWidget = ({
|
|
|
16039
16527
|
"--matcher-card-text-color": `var(--matcher-group-${colorIdx + 1}-text-color, ${textColors[colorIdx]})`
|
|
16040
16528
|
} : void 0,
|
|
16041
16529
|
onClick: () => handleCardClick(item.itemId),
|
|
16530
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
16042
16531
|
"aria-pressed": isSelected,
|
|
16043
16532
|
"aria-label": isSelected ? `Assigned to group ${colorIdx + 1}` : "Not assigned",
|
|
16044
16533
|
children: item.content.map((layoutItem) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -16252,6 +16741,7 @@ const PairMatcherWidget = ({
|
|
|
16252
16741
|
shuffleItems = true
|
|
16253
16742
|
}) => {
|
|
16254
16743
|
const { topItems, bottomItems } = widget;
|
|
16744
|
+
const exerciseLocked = useExerciseLocked();
|
|
16255
16745
|
const [shuffledTop] = React.useState(
|
|
16256
16746
|
() => shuffleItems ? lodashExports.shuffle([...topItems]) : [...topItems]
|
|
16257
16747
|
);
|
|
@@ -16295,8 +16785,9 @@ const PairMatcherWidget = ({
|
|
|
16295
16785
|
}
|
|
16296
16786
|
soundPrevRef.current = { count, pendingId, flashId };
|
|
16297
16787
|
}, [state.pairs, state.pending, state.flashTopId, playSound]);
|
|
16788
|
+
const interactionLocked = validationStatus !== null || exerciseLocked;
|
|
16298
16789
|
const handleTopClick = React.useCallback((topId) => {
|
|
16299
|
-
if (
|
|
16790
|
+
if (interactionLocked) return;
|
|
16300
16791
|
if (flashTimerRef.current) {
|
|
16301
16792
|
clearTimeout(flashTimerRef.current);
|
|
16302
16793
|
flashTimerRef.current = null;
|
|
@@ -16338,9 +16829,9 @@ const PairMatcherWidget = ({
|
|
|
16338
16829
|
flashBottomId: null
|
|
16339
16830
|
};
|
|
16340
16831
|
});
|
|
16341
|
-
}, [
|
|
16832
|
+
}, [interactionLocked]);
|
|
16342
16833
|
const handleBottomClick = React.useCallback((bottomId) => {
|
|
16343
|
-
if (
|
|
16834
|
+
if (interactionLocked) return;
|
|
16344
16835
|
if (flashTimerRef.current) {
|
|
16345
16836
|
clearTimeout(flashTimerRef.current);
|
|
16346
16837
|
flashTimerRef.current = null;
|
|
@@ -16383,9 +16874,13 @@ const PairMatcherWidget = ({
|
|
|
16383
16874
|
flashBottomId: null
|
|
16384
16875
|
};
|
|
16385
16876
|
});
|
|
16386
|
-
}, [
|
|
16877
|
+
}, [interactionLocked]);
|
|
16387
16878
|
React.useEffect(() => {
|
|
16388
|
-
if (
|
|
16879
|
+
if (state.flashTopId === null || state.flashBottomId === null) return;
|
|
16880
|
+
if (interactionLocked) {
|
|
16881
|
+
setState((current) => ({ ...current, flashTopId: null, flashBottomId: null }));
|
|
16882
|
+
return;
|
|
16883
|
+
}
|
|
16389
16884
|
flashTimerRef.current = setTimeout(() => {
|
|
16390
16885
|
flashTimerRef.current = null;
|
|
16391
16886
|
setState((current) => {
|
|
@@ -16399,7 +16894,7 @@ const PairMatcherWidget = ({
|
|
|
16399
16894
|
flashTimerRef.current = null;
|
|
16400
16895
|
}
|
|
16401
16896
|
};
|
|
16402
|
-
}, [state.flashTopId, state.flashBottomId,
|
|
16897
|
+
}, [state.flashTopId, state.flashBottomId, interactionLocked]);
|
|
16403
16898
|
const onChangeRef = React.useRef(onChange);
|
|
16404
16899
|
onChangeRef.current = onChange;
|
|
16405
16900
|
const isMounted = React.useRef(false);
|
|
@@ -16459,7 +16954,7 @@ const PairMatcherWidget = ({
|
|
|
16459
16954
|
dir: isRtl ? "rtl" : void 0,
|
|
16460
16955
|
onClick: () => isTop ? handleTopClick(item.itemId) : handleBottomClick(item.itemId),
|
|
16461
16956
|
"aria-pressed": status !== "idle",
|
|
16462
|
-
"aria-disabled":
|
|
16957
|
+
"aria-disabled": interactionLocked ? "true" : void 0,
|
|
16463
16958
|
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pair-matcher-card-content", children: item.content.map((li) => renderPairMatcherItemContent(li, widgetsMap)) })
|
|
16464
16959
|
}
|
|
16465
16960
|
),
|
|
@@ -16524,6 +17019,9 @@ function isPairMatcherWidget(widget) {
|
|
|
16524
17019
|
function isVisualFractionWidget(widget) {
|
|
16525
17020
|
return widget.type === "visual-fraction";
|
|
16526
17021
|
}
|
|
17022
|
+
function isGridSymmetryWidget(widget) {
|
|
17023
|
+
return widget.type === "grid-symmetry";
|
|
17024
|
+
}
|
|
16527
17025
|
const WidgetRenderer = ({
|
|
16528
17026
|
widget,
|
|
16529
17027
|
widgetId,
|
|
@@ -16668,6 +17166,21 @@ const WidgetRenderer = ({
|
|
|
16668
17166
|
}
|
|
16669
17167
|
);
|
|
16670
17168
|
}
|
|
17169
|
+
if (isGridSymmetryWidget(widget)) {
|
|
17170
|
+
const cells = Array.isArray(value) && value.every(
|
|
17171
|
+
(cell) => Array.isArray(cell) && cell.length === 2 && cell.every(Number.isInteger)
|
|
17172
|
+
) ? value : void 0;
|
|
17173
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
17174
|
+
GridSymmetry,
|
|
17175
|
+
{
|
|
17176
|
+
widgetId,
|
|
17177
|
+
widget,
|
|
17178
|
+
value: cells,
|
|
17179
|
+
validationStatus,
|
|
17180
|
+
onChange: (id, nextValue) => onChange(id, nextValue)
|
|
17181
|
+
}
|
|
17182
|
+
);
|
|
17183
|
+
}
|
|
16671
17184
|
return null;
|
|
16672
17185
|
};
|
|
16673
17186
|
const ExpandDiamondIcon = () => {
|
|
@@ -16980,6 +17493,7 @@ const DraggableCursor = ({
|
|
|
16980
17493
|
currentValue,
|
|
16981
17494
|
validationStatus
|
|
16982
17495
|
}) => {
|
|
17496
|
+
const locked = useExerciseLocked();
|
|
16983
17497
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
16984
17498
|
const isDraggingRef = React.useRef(false);
|
|
16985
17499
|
const currentValueRef = React.useRef(currentValue);
|
|
@@ -17001,13 +17515,14 @@ const DraggableCursor = ({
|
|
|
17001
17515
|
return snapTicks.length > 0 ? snapToNearest(raw, snapTicks) : cleanFloat(raw);
|
|
17002
17516
|
}, [axisRowRef, min, max, snapTicks]);
|
|
17003
17517
|
const handlePointerDown = React.useCallback((e) => {
|
|
17518
|
+
if (locked) return;
|
|
17004
17519
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
17005
17520
|
e.stopPropagation();
|
|
17006
17521
|
isDraggingRef.current = true;
|
|
17007
17522
|
dragStartValueRef.current = currentValue;
|
|
17008
17523
|
lastSoundValueRef.current = currentValue;
|
|
17009
17524
|
setIsDragging(true);
|
|
17010
|
-
}, [currentValue]);
|
|
17525
|
+
}, [currentValue, locked]);
|
|
17011
17526
|
const updateCursorValue = React.useCallback((clientX) => {
|
|
17012
17527
|
const next = snap(clientX);
|
|
17013
17528
|
if (!almostEqual(next, lastSoundValueRef.current)) {
|
|
@@ -17035,7 +17550,7 @@ const DraggableCursor = ({
|
|
|
17035
17550
|
}
|
|
17036
17551
|
}, [updateCursorValue, playSound]);
|
|
17037
17552
|
const handleKeyDown = React.useCallback((e) => {
|
|
17038
|
-
if (!snapTicks.length) return;
|
|
17553
|
+
if (locked || !snapTicks.length) return;
|
|
17039
17554
|
const sorted = [...snapTicks].sort((a, b) => a - b);
|
|
17040
17555
|
const idx = sorted.findIndex((t) => t >= currentValue - 1e-9);
|
|
17041
17556
|
const first = sorted[0] ?? currentValue;
|
|
@@ -17057,7 +17572,7 @@ const DraggableCursor = ({
|
|
|
17057
17572
|
}
|
|
17058
17573
|
onChange(cursor.id, String(next));
|
|
17059
17574
|
}
|
|
17060
|
-
}, [snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
|
|
17575
|
+
}, [locked, snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
|
|
17061
17576
|
const ballClass = [
|
|
17062
17577
|
"number-line-cursor-ball",
|
|
17063
17578
|
isDragging ? "number-line-cursor-ball--dragging" : "",
|
|
@@ -17106,6 +17621,7 @@ const DraggableCursor = ({
|
|
|
17106
17621
|
"aria-valuemax": max,
|
|
17107
17622
|
"aria-valuenow": currentValue,
|
|
17108
17623
|
"aria-label": sliderAriaLabel,
|
|
17624
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
17109
17625
|
tabIndex: 0
|
|
17110
17626
|
}
|
|
17111
17627
|
)
|
|
@@ -17146,9 +17662,11 @@ const CursorBalls = ({
|
|
|
17146
17662
|
}) });
|
|
17147
17663
|
};
|
|
17148
17664
|
const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
17665
|
+
const locked = useExerciseLocked();
|
|
17149
17666
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
17150
17667
|
id: widgetId,
|
|
17151
|
-
data: { type: "placed-chip", widgetId, label }
|
|
17668
|
+
data: { type: "placed-chip", widgetId, label },
|
|
17669
|
+
disabled: locked
|
|
17152
17670
|
});
|
|
17153
17671
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
17154
17672
|
"div",
|
|
@@ -17173,6 +17691,7 @@ const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
|
17173
17691
|
type: "button",
|
|
17174
17692
|
className: "number-line-placed-chip-remove",
|
|
17175
17693
|
"aria-label": "Remove",
|
|
17694
|
+
disabled: locked,
|
|
17176
17695
|
onClick: onRemove,
|
|
17177
17696
|
children: "✕"
|
|
17178
17697
|
}
|
|
@@ -17212,6 +17731,7 @@ const FreeDragAxis = ({
|
|
|
17212
17731
|
cursorValidationStatuses,
|
|
17213
17732
|
snapKey
|
|
17214
17733
|
}) => {
|
|
17734
|
+
const axisLocked = useExerciseLocked();
|
|
17215
17735
|
const axisRef = React.useRef(null);
|
|
17216
17736
|
const snapTicks = React.useMemo(
|
|
17217
17737
|
() => getSnapTicks(majorTicks, minorTicks),
|
|
@@ -17267,7 +17787,7 @@ const FreeDragAxis = ({
|
|
|
17267
17787
|
return changed ? next : prev;
|
|
17268
17788
|
});
|
|
17269
17789
|
}, [values]);
|
|
17270
|
-
const { setNodeRef } = useDroppable({ id: axisDropId });
|
|
17790
|
+
const { setNodeRef } = useDroppable({ id: axisDropId, disabled: axisLocked });
|
|
17271
17791
|
const mergedRef = React.useCallback((node) => {
|
|
17272
17792
|
axisRef.current = node;
|
|
17273
17793
|
setNodeRef(node);
|
|
@@ -17581,6 +18101,7 @@ const CounterControls = ({
|
|
|
17581
18101
|
onIncrement
|
|
17582
18102
|
}) => {
|
|
17583
18103
|
const playSound = useSound();
|
|
18104
|
+
const locked = useExerciseLocked();
|
|
17584
18105
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-counter", "aria-label": counter.name ?? "Counter", children: [
|
|
17585
18106
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-counter-controls", children: [
|
|
17586
18107
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -17589,7 +18110,7 @@ const CounterControls = ({
|
|
|
17589
18110
|
type: "button",
|
|
17590
18111
|
className: "number-line-counter-btn",
|
|
17591
18112
|
"aria-label": "Decrease",
|
|
17592
|
-
disabled: count <= counter.min,
|
|
18113
|
+
disabled: locked || count <= counter.min,
|
|
17593
18114
|
onClick: () => {
|
|
17594
18115
|
playSound("counter-step");
|
|
17595
18116
|
onDecrement();
|
|
@@ -17604,7 +18125,7 @@ const CounterControls = ({
|
|
|
17604
18125
|
type: "button",
|
|
17605
18126
|
className: "number-line-counter-btn",
|
|
17606
18127
|
"aria-label": "Increase",
|
|
17607
|
-
disabled: count >= counter.max,
|
|
18128
|
+
disabled: locked || count >= counter.max,
|
|
17608
18129
|
onClick: () => {
|
|
17609
18130
|
playSound("counter-step");
|
|
17610
18131
|
onIncrement();
|
|
@@ -18188,6 +18709,7 @@ const ClockRenderer = ({
|
|
|
18188
18709
|
validationStatus,
|
|
18189
18710
|
onChange
|
|
18190
18711
|
}) => {
|
|
18712
|
+
const locked = useExerciseLocked();
|
|
18191
18713
|
const format = item.format ?? "24h";
|
|
18192
18714
|
const interactive = item.mode === "interactive";
|
|
18193
18715
|
const snap = interactive && "snap" in item ? item.snap ?? 1 : 1;
|
|
@@ -18233,18 +18755,18 @@ const ClockRenderer = ({
|
|
|
18233
18755
|
}
|
|
18234
18756
|
}, [snap, hours, minutes, item.mode, item.name, onChange, playSound]);
|
|
18235
18757
|
const onPointerDown = (hand) => (e) => {
|
|
18236
|
-
if (!interactive) return;
|
|
18758
|
+
if (!interactive || locked) return;
|
|
18237
18759
|
e.preventDefault();
|
|
18238
18760
|
e.target.setPointerCapture?.(e.pointerId);
|
|
18239
18761
|
draggingRef.current = hand;
|
|
18240
18762
|
updateFromPointer(e.clientX, e.clientY);
|
|
18241
18763
|
};
|
|
18242
18764
|
const onPointerMove = (e) => {
|
|
18243
|
-
if (!interactive || !draggingRef.current) return;
|
|
18765
|
+
if (!interactive || locked || !draggingRef.current) return;
|
|
18244
18766
|
updateFromPointer(e.clientX, e.clientY);
|
|
18245
18767
|
};
|
|
18246
18768
|
const onPointerUp = (e) => {
|
|
18247
|
-
if (!interactive) return;
|
|
18769
|
+
if (!interactive || locked) return;
|
|
18248
18770
|
e.target.releasePointerCapture?.(e.pointerId);
|
|
18249
18771
|
draggingRef.current = null;
|
|
18250
18772
|
};
|
|
@@ -18732,6 +19254,14 @@ function buildInitialValues(json) {
|
|
|
18732
19254
|
const initial = {};
|
|
18733
19255
|
for (const [id, widget] of Object.entries(json.widgets)) {
|
|
18734
19256
|
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) initial[id] = [];
|
|
19257
|
+
if (widget.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
19258
|
+
initial[id] = widget.cells.filter((cell) => isCellInInteractiveArea(
|
|
19259
|
+
cell,
|
|
19260
|
+
widget.rows,
|
|
19261
|
+
widget.cols,
|
|
19262
|
+
widget.interactiveArea
|
|
19263
|
+
));
|
|
19264
|
+
}
|
|
18735
19265
|
}
|
|
18736
19266
|
return initial;
|
|
18737
19267
|
}
|
|
@@ -18788,38 +19318,45 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
18788
19318
|
const [droppedSources, setDroppedSources] = React.useState(/* @__PURE__ */ new Map());
|
|
18789
19319
|
const [focusedInputId, setFocusedInputId] = React.useState(null);
|
|
18790
19320
|
const [validationDetails, setValidationDetails] = React.useState(null);
|
|
19321
|
+
const rootRef = React.useRef(null);
|
|
18791
19322
|
const [resetKey, setResetKey] = React.useState(0);
|
|
18792
19323
|
const [revealed, setRevealed] = React.useState(false);
|
|
18793
19324
|
const playSound = useSoundPlayer(soundEnabled, sounds);
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
|
|
18804
|
-
|
|
18805
|
-
|
|
18806
|
-
|
|
18807
|
-
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18811
|
-
|
|
18812
|
-
|
|
18813
|
-
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
|
|
18817
|
-
|
|
19325
|
+
const externallyLocked = useExerciseLocked();
|
|
19326
|
+
const locked = validationDetails !== null || externallyLocked;
|
|
19327
|
+
const lastResultRef = React.useRef(null);
|
|
19328
|
+
const runSubmit = () => {
|
|
19329
|
+
if (locked) return lastResultRef.current ?? validate(json, values);
|
|
19330
|
+
const result = validate(json, values);
|
|
19331
|
+
playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
|
|
19332
|
+
const details = { ...result.details };
|
|
19333
|
+
const slotValidation = (id, raw, expected) => ({
|
|
19334
|
+
widgetId: id,
|
|
19335
|
+
userValue: typeof raw === "string" ? raw : void 0,
|
|
19336
|
+
isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
|
|
19337
|
+
});
|
|
19338
|
+
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
19339
|
+
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
19340
|
+
if (!(widgetId in details)) continue;
|
|
19341
|
+
const answer = json.validation?.answers?.[widgetId];
|
|
19342
|
+
if (!answer) continue;
|
|
19343
|
+
const [expectedNum, expectedDen] = answer.split("/", 2);
|
|
19344
|
+
if (!expectedNum || !expectedDen) continue;
|
|
19345
|
+
if (details[widgetId]?.isCorrect) {
|
|
19346
|
+
if (widget.numerator === null) details[`${widgetId}__num`] = { widgetId: `${widgetId}__num`, userValue: void 0, isCorrect: true };
|
|
19347
|
+
if (widget.denominator === null) details[`${widgetId}__den`] = { widgetId: `${widgetId}__den`, userValue: void 0, isCorrect: true };
|
|
19348
|
+
} else {
|
|
19349
|
+
if (widget.numerator === null) details[`${widgetId}__num`] = slotValidation(`${widgetId}__num`, values[`${widgetId}__num`], expectedNum);
|
|
19350
|
+
if (widget.denominator === null) details[`${widgetId}__den`] = slotValidation(`${widgetId}__den`, values[`${widgetId}__den`], expectedDen);
|
|
18818
19351
|
}
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
19352
|
+
}
|
|
19353
|
+
setValidationDetails(details);
|
|
19354
|
+
onSubmit?.(values);
|
|
19355
|
+
lastResultRef.current = result;
|
|
19356
|
+
return result;
|
|
19357
|
+
};
|
|
19358
|
+
React.useImperativeHandle(ref, () => ({
|
|
19359
|
+
submit: runSubmit,
|
|
18823
19360
|
getValues: () => values,
|
|
18824
19361
|
getCompletion: () => {
|
|
18825
19362
|
const answerable = getAnswerableInputs(json);
|
|
@@ -18862,14 +19399,20 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
18862
19399
|
setResetKey((k) => k + 1);
|
|
18863
19400
|
},
|
|
18864
19401
|
focusFirstInput: () => {
|
|
18865
|
-
const container =
|
|
18866
|
-
|
|
18867
|
-
|
|
18868
|
-
|
|
19402
|
+
const container = rootRef.current;
|
|
19403
|
+
if (!container) return;
|
|
19404
|
+
const candidates = container.querySelectorAll(
|
|
19405
|
+
'input[type="text"], input[type="number"], textarea, select'
|
|
19406
|
+
);
|
|
19407
|
+
for (const el of Array.from(candidates)) {
|
|
19408
|
+
if (el.readOnly || el.disabled) continue;
|
|
19409
|
+
el.focus();
|
|
19410
|
+
return;
|
|
18869
19411
|
}
|
|
18870
19412
|
}
|
|
18871
|
-
}), [json, values, onSubmit, playSound]);
|
|
19413
|
+
}), [json, values, onSubmit, playSound, locked]);
|
|
18872
19414
|
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
19415
|
+
if (locked) return;
|
|
18873
19416
|
setRevealed(false);
|
|
18874
19417
|
const next = { ...valuesRef.current, [widgetId]: value };
|
|
18875
19418
|
valuesRef.current = next;
|
|
@@ -18886,7 +19429,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
18886
19429
|
onChange?.(next);
|
|
18887
19430
|
};
|
|
18888
19431
|
const handleSubmit = () => {
|
|
18889
|
-
|
|
19432
|
+
runSubmit();
|
|
18890
19433
|
};
|
|
18891
19434
|
const lineGroups = React.useMemo(() => {
|
|
18892
19435
|
const isRTLInlineWidget = (item) => {
|
|
@@ -19001,7 +19544,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
19001
19544
|
if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
|
|
19002
19545
|
return { tools: list, toolsMinHeight: maxH };
|
|
19003
19546
|
}, [rulerWidget, protractorWidget, setSquareWidget]);
|
|
19004
|
-
return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${className}`, children: [
|
|
19547
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, children: [
|
|
19005
19548
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
19006
19549
|
"div",
|
|
19007
19550
|
{
|
|
@@ -19097,8 +19640,8 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
19097
19640
|
}
|
|
19098
19641
|
}
|
|
19099
19642
|
) }),
|
|
19100
|
-
showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "exercise-submit-btn", onClick: handleSubmit, children: "Check Answer" })
|
|
19101
|
-
] }) }) });
|
|
19643
|
+
showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" })
|
|
19644
|
+
] }) }) }) });
|
|
19102
19645
|
});
|
|
19103
19646
|
ExerciseRenderer.displayName = "ExerciseRenderer";
|
|
19104
19647
|
function renderExercise(options) {
|
|
@@ -19154,12 +19697,22 @@ exports.DragOptionsToolSchema = DragOptionsToolSchema;
|
|
|
19154
19697
|
exports.EngineError = EngineError;
|
|
19155
19698
|
exports.ExerciseDocumentSchema = ExerciseDocumentSchema;
|
|
19156
19699
|
exports.ExerciseJSONSchema = ExerciseJSONSchema;
|
|
19700
|
+
exports.ExerciseLockProvider = ExerciseLockProvider;
|
|
19157
19701
|
exports.ExerciseMetaSchema = ExerciseMetaSchema;
|
|
19158
19702
|
exports.ExerciseRenderer = ExerciseRenderer;
|
|
19159
19703
|
exports.ExpressionValidationSchema = ExpressionValidationSchema;
|
|
19160
19704
|
exports.FractionInlineSchema = FractionInlineSchema;
|
|
19161
19705
|
exports.FractionJSONSchema = FractionJSONSchema;
|
|
19162
19706
|
exports.FreeTextInlineSchema = FreeTextInlineSchema;
|
|
19707
|
+
exports.GRID_SYMMETRY_AXES = GRID_SYMMETRY_AXES;
|
|
19708
|
+
exports.GRID_SYMMETRY_INTERACTIVE_AREAS = GRID_SYMMETRY_INTERACTIVE_AREAS;
|
|
19709
|
+
exports.GRID_SYMMETRY_MAX_CELL_SIZE = GRID_SYMMETRY_MAX_CELL_SIZE;
|
|
19710
|
+
exports.GRID_SYMMETRY_MAX_COLS = GRID_SYMMETRY_MAX_COLS;
|
|
19711
|
+
exports.GRID_SYMMETRY_MAX_ROWS = GRID_SYMMETRY_MAX_ROWS;
|
|
19712
|
+
exports.GRID_SYMMETRY_MIN_CELL_SIZE = GRID_SYMMETRY_MIN_CELL_SIZE;
|
|
19713
|
+
exports.GridCellSchema = GridCellSchema;
|
|
19714
|
+
exports.GridSymmetryBlockSchema = GridSymmetryBlockSchema;
|
|
19715
|
+
exports.GridSymmetryJSONSchema = GridSymmetryJSONSchema;
|
|
19163
19716
|
exports.GroupItemSchema = GroupItemSchema;
|
|
19164
19717
|
exports.HighlightedTextInlineSchema = HighlightedTextInlineSchema;
|
|
19165
19718
|
exports.HighlightedTextSchema = HighlightedTextSchema;
|
|
@@ -19246,6 +19799,8 @@ exports.WidgetRefSchema = WidgetRefSchema;
|
|
|
19246
19799
|
exports.WidgetRenderer = WidgetRenderer;
|
|
19247
19800
|
exports.compile = compile;
|
|
19248
19801
|
exports.compileWithAST = compileWithAST;
|
|
19802
|
+
exports.computeGridSymmetryCellsToFill = computeGridSymmetryCellsToFill;
|
|
19803
|
+
exports.computeGridSymmetryExpectedCells = computeGridSymmetryExpectedCells;
|
|
19249
19804
|
exports.createBoldText = createBoldText;
|
|
19250
19805
|
exports.createColorText = createColorText;
|
|
19251
19806
|
exports.createDisplayClock = createDisplayClock;
|
|
@@ -19254,6 +19809,7 @@ exports.createExerciseDocument = createExerciseDocument;
|
|
|
19254
19809
|
exports.createExpressionValidation = createExpressionValidation;
|
|
19255
19810
|
exports.createFraction = createFraction;
|
|
19256
19811
|
exports.createFreeText = createFreeText;
|
|
19812
|
+
exports.createGridSymmetryBlock = createGridSymmetryBlock;
|
|
19257
19813
|
exports.createHighlightedText = createHighlightedText;
|
|
19258
19814
|
exports.createImage = createImage;
|
|
19259
19815
|
exports.createInlineDragOption = createInlineDragOption;
|
|
@@ -19292,6 +19848,7 @@ exports.getAnswerableInputs = getAnswerableInputs;
|
|
|
19292
19848
|
exports.getFractionKey = getFractionKey;
|
|
19293
19849
|
exports.getMissingInputs = getMissingInputs;
|
|
19294
19850
|
exports.isBoldText = isBoldText;
|
|
19851
|
+
exports.isCellInInteractiveArea = isCellInInteractiveArea;
|
|
19295
19852
|
exports.isClock = isClock;
|
|
19296
19853
|
exports.isClockItem = isClockItem;
|
|
19297
19854
|
exports.isColorText = isColorText;
|
|
@@ -19303,6 +19860,8 @@ exports.isFraction = isFraction;
|
|
|
19303
19860
|
exports.isFractionJSON = isFractionJSON;
|
|
19304
19861
|
exports.isFractionKey = isFractionKey;
|
|
19305
19862
|
exports.isFreeText = isFreeText;
|
|
19863
|
+
exports.isGridSymmetryBlock = isGridSymmetryBlock;
|
|
19864
|
+
exports.isGridSymmetryJSON = isGridSymmetryJSON;
|
|
19306
19865
|
exports.isGroupItem = isGroupItem;
|
|
19307
19866
|
exports.isHighlightedText = isHighlightedText;
|
|
19308
19867
|
exports.isImage = isImage;
|
|
@@ -19355,11 +19914,13 @@ exports.normalizeVopOperator = normalizeVopOperator;
|
|
|
19355
19914
|
exports.parse = parse;
|
|
19356
19915
|
exports.parseFractionKey = parseFractionKey;
|
|
19357
19916
|
exports.parseSafe = parseSafe;
|
|
19917
|
+
exports.reflectGridSymmetryCell = reflectGridSymmetryCell;
|
|
19358
19918
|
exports.renderExercise = renderExercise;
|
|
19359
19919
|
exports.toAST = toAST;
|
|
19360
19920
|
exports.toJSON = toJSON;
|
|
19361
19921
|
exports.tokenize = tokenize;
|
|
19362
19922
|
exports.tokenizeSafe = tokenizeSafe;
|
|
19923
|
+
exports.useExerciseLocked = useExerciseLocked;
|
|
19363
19924
|
exports.useSound = useSound;
|
|
19364
19925
|
exports.validate = validate;
|
|
19365
19926
|
exports.validateBooleanExpression = validateBooleanExpression;
|