@mathscan/math-exercise-engine 1.2.14 → 1.2.15
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 +6 -4
- package/dist/{GeometryWidget-D6_DlxhZ.js → GeometryWidget-CTTLw871.js} +2 -2
- package/dist/{GeometryWidget-D6_DlxhZ.js.map → GeometryWidget-CTTLw871.js.map} +1 -1
- package/dist/{GeometryWidget-B-EQ53jz.mjs → GeometryWidget-DZWM4LcD.mjs} +2 -2
- package/dist/{GeometryWidget-B-EQ53jz.mjs.map → GeometryWidget-DZWM4LcD.mjs.map} +1 -1
- package/dist/{index-Daj5xH0X.mjs → index-BLrnKegW.mjs} +1733 -911
- package/dist/index-BLrnKegW.mjs.map +1 -0
- package/dist/{index-gCvC2wRR.js → index-C9OHpy2h.js} +1468 -646
- package/dist/index-C9OHpy2h.js.map +1 -0
- package/dist/index.d.ts +8070 -3034
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +282 -266
- package/dist/style.css +178 -12
- package/package.json +1 -1
- package/dist/index-Daj5xH0X.mjs.map +0 -1
- package/dist/index-gCvC2wRR.js.map +0 -1
|
@@ -25,6 +25,8 @@ const AST_TYPES = {
|
|
|
25
25
|
BOLD_TEXT: "BoldText",
|
|
26
26
|
/** Inline input widget (string or numeric) */
|
|
27
27
|
INLINE_INPUT: "InlineInput",
|
|
28
|
+
/** Drop-only inline input; stores the selected option name */
|
|
29
|
+
DROP_INPUT: "DropInput",
|
|
28
30
|
/** Single inline draggable option (appears in-flow with text) */
|
|
29
31
|
INLINE_DRAG_OPTION: "InlineDragOption",
|
|
30
32
|
/** Inline fraction widget (numerator/denominator with optional inputs) */
|
|
@@ -112,6 +114,8 @@ const AST_TYPES = {
|
|
|
112
114
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
113
115
|
/** Drag-and-drop options tool */
|
|
114
116
|
DRAG_OPTIONS: "DragOptions",
|
|
117
|
+
/** Rich draggable options tool */
|
|
118
|
+
DRAGGABLE_OPTIONS: "DraggableOptions",
|
|
115
119
|
/** Radio button options tool */
|
|
116
120
|
RADIO_OPTIONS: "RadioOptions",
|
|
117
121
|
/** Numeric keypad input tool */
|
|
@@ -217,8 +221,12 @@ const JSON_TYPES = {
|
|
|
217
221
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
222
|
/** Inline input widget (string or numeric) */
|
|
219
223
|
INLINE_INPUT: "inline-input",
|
|
224
|
+
/** Drop-only inline input; stores the selected option name */
|
|
225
|
+
DROP_INPUT: "drop-input",
|
|
220
226
|
/** Drag-and-drop options widget */
|
|
221
227
|
DRAG_OPTIONS: "drag-options",
|
|
228
|
+
/** Rich draggable options widget */
|
|
229
|
+
DRAGGABLE_OPTIONS: "draggable-options",
|
|
222
230
|
/** Single inline draggable option (appears in-flow with text) */
|
|
223
231
|
INLINE_DRAG_OPTION: "inline-drag-option",
|
|
224
232
|
/** Radio button options widget */
|
|
@@ -241,6 +249,7 @@ const JSON_TYPES = {
|
|
|
241
249
|
GEOMETRY: "geometry"
|
|
242
250
|
};
|
|
243
251
|
const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
|
|
252
|
+
const DRAGGABLE_OPTIONS_WIDGET_ID = "_draggable_options";
|
|
244
253
|
const NUMERIC_KEYPAD_WIDGET_ID = "_numeric_keypad";
|
|
245
254
|
const RULER_WIDGET_ID = "_ruler";
|
|
246
255
|
const PROTRACTOR_WIDGET_ID = "_protractor";
|
|
@@ -256,6 +265,7 @@ const GRID_SYMMETRY_WIDGET_PREFIX = "_grid_symmetry_";
|
|
|
256
265
|
const GEOMETRY_WIDGET_PREFIX = "_geometry_";
|
|
257
266
|
const RTL_INLINE_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
258
267
|
JSON_TYPES.INLINE_INPUT,
|
|
268
|
+
JSON_TYPES.DROP_INPUT,
|
|
259
269
|
JSON_TYPES.INLINE_DRAG_OPTION,
|
|
260
270
|
JSON_TYPES.FRACTION,
|
|
261
271
|
JSON_TYPES.VISUAL_FRACTION
|
|
@@ -355,6 +365,10 @@ const STRING_INPUT = chevrotain.createToken({
|
|
|
355
365
|
name: "STRING_INPUT",
|
|
356
366
|
pattern: /STRING_INPUT/
|
|
357
367
|
});
|
|
368
|
+
const DROP_INPUT = chevrotain.createToken({
|
|
369
|
+
name: "DROP_INPUT",
|
|
370
|
+
pattern: /DROP_INPUT/
|
|
371
|
+
});
|
|
358
372
|
const INLINE_DRAG_OPTION = chevrotain.createToken({
|
|
359
373
|
name: "INLINE_DRAG_OPTION",
|
|
360
374
|
pattern: /INLINE_DRAG_OPTION/
|
|
@@ -363,6 +377,18 @@ const DRAGABLE_BUTTONS_OPTIONS = chevrotain.createToken({
|
|
|
363
377
|
name: "DRAGABLE_BUTTONS_OPTIONS",
|
|
364
378
|
pattern: /DRAGABLE_BUTTONS_OPTIONS/
|
|
365
379
|
});
|
|
380
|
+
const DRAGGABLE_OPTIONS = chevrotain.createToken({
|
|
381
|
+
name: "DRAGGABLE_OPTIONS",
|
|
382
|
+
pattern: /DRAGGABLE_OPTIONS/
|
|
383
|
+
});
|
|
384
|
+
const OPTION = chevrotain.createToken({
|
|
385
|
+
name: "OPTION",
|
|
386
|
+
// Only a keyword when immediately followed by `[` (which the grammar always
|
|
387
|
+
// requires: `OPTION[name=...]`). Without the lookahead, the bare common word
|
|
388
|
+
// "OPTION"/"OPTIONS" in exercise prose would tokenize as a keyword and break
|
|
389
|
+
// parsing.
|
|
390
|
+
pattern: /OPTION(?=\[)/
|
|
391
|
+
});
|
|
366
392
|
const RADIO_BUTTONS_OPTIONS = chevrotain.createToken({
|
|
367
393
|
name: "RADIO_BUTTONS_OPTIONS",
|
|
368
394
|
pattern: /RADIO_BUTTONS_OPTIONS/
|
|
@@ -673,8 +699,11 @@ const allTokens = [
|
|
|
673
699
|
HIGHLIGHT_TEXT,
|
|
674
700
|
W_S_I,
|
|
675
701
|
STRING_INPUT,
|
|
702
|
+
DROP_INPUT,
|
|
676
703
|
INLINE_DRAG_OPTION,
|
|
704
|
+
DRAGGABLE_OPTIONS,
|
|
677
705
|
DRAGABLE_BUTTONS_OPTIONS,
|
|
706
|
+
OPTION,
|
|
678
707
|
RADIO_BUTTONS_OPTIONS,
|
|
679
708
|
NUMERIC_KEYPAD,
|
|
680
709
|
WIDGET_ANSWER,
|
|
@@ -1446,6 +1475,8 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1446
1475
|
{ ALT: () => this.SUBRULE(this.visualFraction) },
|
|
1447
1476
|
// W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
|
|
1448
1477
|
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) },
|
|
1478
|
+
// DROP_INPUT (drop-only inline answer widget) — excluded in display-only contexts
|
|
1479
|
+
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.dropInput) },
|
|
1449
1480
|
// INLINE_DRAG_OPTION (single draggable option inline with text) — excluded in display-only
|
|
1450
1481
|
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) },
|
|
1451
1482
|
// CLOCK (inline clock widget)
|
|
@@ -1537,6 +1568,12 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1537
1568
|
this.SUBRULE(this.parameterList);
|
|
1538
1569
|
this.CONSUME(RBracket);
|
|
1539
1570
|
});
|
|
1571
|
+
this.dropInput = this.RULE("dropInput", () => {
|
|
1572
|
+
this.CONSUME(DROP_INPUT);
|
|
1573
|
+
this.CONSUME(LBracket);
|
|
1574
|
+
this.SUBRULE(this.parameterList);
|
|
1575
|
+
this.CONSUME(RBracket);
|
|
1576
|
+
});
|
|
1540
1577
|
this.inlineDragOption = this.RULE("inlineDragOption", () => {
|
|
1541
1578
|
this.CONSUME(INLINE_DRAG_OPTION);
|
|
1542
1579
|
this.CONSUME(LBracket);
|
|
@@ -1545,6 +1582,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1545
1582
|
});
|
|
1546
1583
|
this.tool = this.RULE("tool", () => {
|
|
1547
1584
|
this.OR([
|
|
1585
|
+
{ ALT: () => this.SUBRULE(this.draggableOptions) },
|
|
1548
1586
|
{ ALT: () => this.SUBRULE(this.dragableButtons) },
|
|
1549
1587
|
{ ALT: () => this.SUBRULE(this.numericKeypad) },
|
|
1550
1588
|
{ ALT: () => this.SUBRULE(this.rulerTool) },
|
|
@@ -1552,6 +1590,44 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1552
1590
|
{ ALT: () => this.SUBRULE(this.setSquareTool) }
|
|
1553
1591
|
]);
|
|
1554
1592
|
});
|
|
1593
|
+
this.draggableOptions = this.RULE("draggableOptions", () => {
|
|
1594
|
+
this.CONSUME(DRAGGABLE_OPTIONS);
|
|
1595
|
+
this.CONSUME(LBracket);
|
|
1596
|
+
this.OPTION(() => this.SUBRULE(this.parameterList));
|
|
1597
|
+
this.CONSUME(RBracket);
|
|
1598
|
+
this.MANY(() => {
|
|
1599
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1600
|
+
});
|
|
1601
|
+
this.CONSUME(LBrace);
|
|
1602
|
+
this.AT_LEAST_ONE(() => this.SUBRULE(this.draggableOption));
|
|
1603
|
+
this.CONSUME(RBrace);
|
|
1604
|
+
this.MANY2(() => {
|
|
1605
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1606
|
+
});
|
|
1607
|
+
});
|
|
1608
|
+
this.draggableOption = this.RULE("draggableOption", () => {
|
|
1609
|
+
this.MANY(() => {
|
|
1610
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1611
|
+
});
|
|
1612
|
+
this.CONSUME(OPTION);
|
|
1613
|
+
this.CONSUME(LBracket);
|
|
1614
|
+
this.SUBRULE(this.parameterList);
|
|
1615
|
+
this.CONSUME(RBracket);
|
|
1616
|
+
this.MANY2(() => {
|
|
1617
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1618
|
+
});
|
|
1619
|
+
this.CONSUME(LBrace);
|
|
1620
|
+
this.MANY3(() => {
|
|
1621
|
+
this.OR3([
|
|
1622
|
+
{ ALT: () => this.SUBRULE(this.inlineElement, { ARGS: [true] }) },
|
|
1623
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1624
|
+
]);
|
|
1625
|
+
});
|
|
1626
|
+
this.CONSUME(RBrace);
|
|
1627
|
+
this.MANY4(() => {
|
|
1628
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1629
|
+
});
|
|
1630
|
+
});
|
|
1555
1631
|
this.dragableButtons = this.RULE("dragableButtons", () => {
|
|
1556
1632
|
this.CONSUME(DRAGABLE_BUTTONS_OPTIONS);
|
|
1557
1633
|
this.CONSUME(LBracket);
|
|
@@ -2109,6 +2185,14 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
|
|
|
2109
2185
|
answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
|
|
2110
2186
|
})
|
|
2111
2187
|
]);
|
|
2188
|
+
const DropInputSchema = zod.z.object({
|
|
2189
|
+
type: zod.z.literal(AST_TYPES.DROP_INPUT),
|
|
2190
|
+
name: zod.z.string().min(1),
|
|
2191
|
+
answer: zod.z.string().min(1),
|
|
2192
|
+
/** Box width in characters: the answer is an option name, so nothing else implies a size. */
|
|
2193
|
+
width: InlineInputWidthSchema,
|
|
2194
|
+
placeholder: zod.z.string().max(32).optional()
|
|
2195
|
+
});
|
|
2112
2196
|
const FractionInlineSchema = zod.z.object({
|
|
2113
2197
|
type: zod.z.literal(AST_TYPES.FRACTION),
|
|
2114
2198
|
name: zod.z.string(),
|
|
@@ -2222,6 +2306,7 @@ const InlineNodeSchema = zod.z.union([
|
|
|
2222
2306
|
ColorTextInlineSchema,
|
|
2223
2307
|
BoldTextInlineSchema,
|
|
2224
2308
|
InlineInputWidgetSchema,
|
|
2309
|
+
DropInputSchema,
|
|
2225
2310
|
FractionInlineSchema,
|
|
2226
2311
|
ImageInlineSchema,
|
|
2227
2312
|
VisualFractionInlineSchema,
|
|
@@ -2297,6 +2382,19 @@ function createNumericInput(name, digits, answer, placeholder, disabled, width,
|
|
|
2297
2382
|
"NUMERIC_INPUT"
|
|
2298
2383
|
);
|
|
2299
2384
|
}
|
|
2385
|
+
function createDropInput(name, answer, placeholder, width) {
|
|
2386
|
+
return parseOrThrow(
|
|
2387
|
+
DropInputSchema,
|
|
2388
|
+
{
|
|
2389
|
+
type: AST_TYPES.DROP_INPUT,
|
|
2390
|
+
name,
|
|
2391
|
+
answer,
|
|
2392
|
+
placeholder,
|
|
2393
|
+
width
|
|
2394
|
+
},
|
|
2395
|
+
"DROP_INPUT"
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2300
2398
|
function createImage(name, src, alt, options = {}) {
|
|
2301
2399
|
return ImageInlineSchema.parse({
|
|
2302
2400
|
type: AST_TYPES.IMAGE,
|
|
@@ -2351,6 +2449,9 @@ function isSpace(value) {
|
|
|
2351
2449
|
function isInlineInput(value) {
|
|
2352
2450
|
return InlineInputWidgetSchema.safeParse(value).success;
|
|
2353
2451
|
}
|
|
2452
|
+
function isDropInput(value) {
|
|
2453
|
+
return DropInputSchema.safeParse(value).success;
|
|
2454
|
+
}
|
|
2354
2455
|
function isFraction(value) {
|
|
2355
2456
|
return FractionInlineSchema.safeParse(value).success;
|
|
2356
2457
|
}
|
|
@@ -2405,7 +2506,7 @@ function createNumberLinePosition(value, inlines) {
|
|
|
2405
2506
|
if (!hasRealContent) {
|
|
2406
2507
|
throw new Error(`POSITION[value=${value}]: content cannot be empty or whitespace only`);
|
|
2407
2508
|
}
|
|
2408
|
-
const inputs = inlines.filter((i) => i.type === AST_TYPES.INLINE_INPUT);
|
|
2509
|
+
const inputs = inlines.filter((i) => i.type === AST_TYPES.INLINE_INPUT || i.type === AST_TYPES.DROP_INPUT);
|
|
2409
2510
|
if (inputs.length > 1) {
|
|
2410
2511
|
throw new Error(
|
|
2411
2512
|
`POSITION[value=${value}]: only one input widget is allowed, got ${inputs.length}`
|
|
@@ -3118,6 +3219,45 @@ const DragOptionsToolSchema = zod.z.object({
|
|
|
3118
3219
|
type: zod.z.literal(AST_TYPES.DRAG_OPTIONS),
|
|
3119
3220
|
options: zod.z.array(zod.z.string()).min(1)
|
|
3120
3221
|
});
|
|
3222
|
+
function hasOptionContent(content, spaceType = AST_TYPES.SPACE) {
|
|
3223
|
+
return content.some((node) => node.type !== spaceType && !(typeof node.value === "string" && node.value.trim() === ""));
|
|
3224
|
+
}
|
|
3225
|
+
function isAnsweredFraction(node) {
|
|
3226
|
+
return node.type === AST_TYPES.FRACTION && node.answer !== void 0;
|
|
3227
|
+
}
|
|
3228
|
+
function isOptionContentNode(node) {
|
|
3229
|
+
const parsed = DisplayInlineNodeSchema.safeParse(node);
|
|
3230
|
+
return parsed.success && !isAnsweredFraction(parsed.data);
|
|
3231
|
+
}
|
|
3232
|
+
const DraggableOptionSchema = zod.z.object({
|
|
3233
|
+
name: zod.z.string().min(1),
|
|
3234
|
+
content: zod.z.array(DisplayInlineNodeSchema).min(1).refine((content) => hasOptionContent(content), { message: "OPTION content cannot be empty or whitespace only" }).refine(
|
|
3235
|
+
(content) => !content.some(isAnsweredFraction),
|
|
3236
|
+
{ message: "FRACTION inside OPTION must be display-only (no answer)" }
|
|
3237
|
+
)
|
|
3238
|
+
});
|
|
3239
|
+
function addDuplicateOptionNameIssues(options, ctx, pathPrefix = []) {
|
|
3240
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3241
|
+
options.forEach((option, index) => {
|
|
3242
|
+
const key2 = normalize(option.name);
|
|
3243
|
+
if (seen.has(key2)) {
|
|
3244
|
+
ctx.addIssue({
|
|
3245
|
+
code: zod.z.ZodIssueCode.custom,
|
|
3246
|
+
message: `Duplicate option name "${option.name}" (names are compared ignoring case and number format)`,
|
|
3247
|
+
path: [...pathPrefix, "options", index, "name"]
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
seen.add(key2);
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
const DraggableOptionsToolObjectSchema = zod.z.object({
|
|
3254
|
+
type: zod.z.literal(AST_TYPES.DRAGGABLE_OPTIONS),
|
|
3255
|
+
reusable: zod.z.boolean().default(false),
|
|
3256
|
+
options: zod.z.array(DraggableOptionSchema).min(1)
|
|
3257
|
+
});
|
|
3258
|
+
const DraggableOptionsToolSchema = DraggableOptionsToolObjectSchema.superRefine((tool, ctx) => {
|
|
3259
|
+
addDuplicateOptionNameIssues(tool.options, ctx);
|
|
3260
|
+
});
|
|
3121
3261
|
const NumericKeypadToolSchema = zod.z.object({
|
|
3122
3262
|
type: zod.z.literal(AST_TYPES.NUMERIC_KEYPAD),
|
|
3123
3263
|
includeOperators: zod.z.boolean().optional(),
|
|
@@ -3138,6 +3278,7 @@ const SetSquareToolSchema = zod.z.object({
|
|
|
3138
3278
|
});
|
|
3139
3279
|
const ToolNodeSchema = zod.z.discriminatedUnion("type", [
|
|
3140
3280
|
DragOptionsToolSchema,
|
|
3281
|
+
DraggableOptionsToolObjectSchema,
|
|
3141
3282
|
NumericKeypadToolSchema,
|
|
3142
3283
|
RulerToolSchema,
|
|
3143
3284
|
ProtractorToolSchema,
|
|
@@ -3149,6 +3290,19 @@ function createDragOptions(options) {
|
|
|
3149
3290
|
options
|
|
3150
3291
|
});
|
|
3151
3292
|
}
|
|
3293
|
+
function createDraggableOptions(options, reusable = false) {
|
|
3294
|
+
const result = DraggableOptionsToolSchema.safeParse({
|
|
3295
|
+
type: AST_TYPES.DRAGGABLE_OPTIONS,
|
|
3296
|
+
reusable,
|
|
3297
|
+
options
|
|
3298
|
+
});
|
|
3299
|
+
if (result.success) return result.data;
|
|
3300
|
+
const issue = result.error.issues[0];
|
|
3301
|
+
throw new Error(`DRAGGABLE_OPTIONS: ${issue ? issue.message : "validation failed"}`);
|
|
3302
|
+
}
|
|
3303
|
+
function createDraggableOption(name, content) {
|
|
3304
|
+
return DraggableOptionSchema.parse({ name, content });
|
|
3305
|
+
}
|
|
3152
3306
|
function createNumericKeypad(includeOperators, includeDecimal, style) {
|
|
3153
3307
|
return NumericKeypadToolSchema.parse({
|
|
3154
3308
|
type: AST_TYPES.NUMERIC_KEYPAD,
|
|
@@ -3169,6 +3323,9 @@ function createSetSquareTool(size) {
|
|
|
3169
3323
|
function isDragOptions(value) {
|
|
3170
3324
|
return DragOptionsToolSchema.safeParse(value).success;
|
|
3171
3325
|
}
|
|
3326
|
+
function isDraggableOptions(value) {
|
|
3327
|
+
return DraggableOptionsToolSchema.safeParse(value).success;
|
|
3328
|
+
}
|
|
3172
3329
|
function isNumericKeypad(value) {
|
|
3173
3330
|
return NumericKeypadToolSchema.safeParse(value).success;
|
|
3174
3331
|
}
|
|
@@ -3727,9 +3884,13 @@ const ExerciseDocumentSchema = zod.z.object({
|
|
|
3727
3884
|
seen.add(name);
|
|
3728
3885
|
}
|
|
3729
3886
|
};
|
|
3887
|
+
const dropInputs = [];
|
|
3730
3888
|
const visitInlines = (inlines, basePath) => {
|
|
3731
3889
|
inlines.forEach((inline, i) => {
|
|
3732
|
-
if (inline.type === AST_TYPES.
|
|
3890
|
+
if (inline.type === AST_TYPES.DROP_INPUT) {
|
|
3891
|
+
dropInputs.push({ name: inline.name, answer: inline.answer, path: [...basePath, i] });
|
|
3892
|
+
}
|
|
3893
|
+
if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.DROP_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE || inline.type === AST_TYPES.VISUAL_FRACTION) {
|
|
3733
3894
|
record(inline.name, [...basePath, i, "name"]);
|
|
3734
3895
|
} else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
|
|
3735
3896
|
record(inline.name, [...basePath, i, "name"]);
|
|
@@ -3746,9 +3907,13 @@ const ExerciseDocumentSchema = zod.z.object({
|
|
|
3746
3907
|
});
|
|
3747
3908
|
});
|
|
3748
3909
|
} else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
|
|
3749
|
-
|
|
3910
|
+
const numberLine = block;
|
|
3911
|
+
numberLine.positions.forEach((pos, pi) => {
|
|
3750
3912
|
visitInlines(pos.inlines, ["lines", bi, "positions", pi, "inlines"]);
|
|
3751
3913
|
});
|
|
3914
|
+
numberLine.arrows?.forEach((arrow, ai) => {
|
|
3915
|
+
visitInlines(arrow.inlines, ["lines", bi, "arrows", ai, "inlines"]);
|
|
3916
|
+
});
|
|
3752
3917
|
} else if (block.type === AST_TYPES.MATCHER_BLOCK) {
|
|
3753
3918
|
const m = block;
|
|
3754
3919
|
record(m.name, ["lines", bi, "name"]);
|
|
@@ -3791,15 +3956,78 @@ const ExerciseDocumentSchema = zod.z.object({
|
|
|
3791
3956
|
record(block.name, ["lines", bi, "name"]);
|
|
3792
3957
|
}
|
|
3793
3958
|
});
|
|
3959
|
+
const richBanks = (doc.tools ?? []).map((tool, toolIndex) => ({ tool, toolIndex })).filter((entry) => entry.tool.type === AST_TYPES.DRAGGABLE_OPTIONS);
|
|
3960
|
+
richBanks.forEach(({ tool: bank, toolIndex }) => {
|
|
3961
|
+
if (bank.type !== AST_TYPES.DRAGGABLE_OPTIONS) return;
|
|
3962
|
+
bank.options.forEach((option, optionIndex) => {
|
|
3963
|
+
visitInlines(option.content, ["tools", toolIndex, "options", optionIndex, "content"]);
|
|
3964
|
+
});
|
|
3965
|
+
addDuplicateOptionNameIssues(bank.options, ctx, ["tools", toolIndex]);
|
|
3966
|
+
if (dropInputs.length === 0) {
|
|
3967
|
+
ctx.addIssue({
|
|
3968
|
+
code: zod.z.ZodIssueCode.custom,
|
|
3969
|
+
message: "DRAGGABLE_OPTIONS needs at least one DROP_INPUT: its cards can only be dropped there",
|
|
3970
|
+
path: ["tools", toolIndex]
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
if (richBanks.length > 1) {
|
|
3975
|
+
ctx.addIssue({
|
|
3976
|
+
code: zod.z.ZodIssueCode.custom,
|
|
3977
|
+
message: "Only one DRAGGABLE_OPTIONS tool is allowed",
|
|
3978
|
+
path: ["tools"]
|
|
3979
|
+
});
|
|
3980
|
+
}
|
|
3981
|
+
const optionNames = new Set(
|
|
3982
|
+
richBanks.flatMap(({ tool }) => (tool.type === AST_TYPES.DRAGGABLE_OPTIONS ? tool.options : []).map((o) => o.name))
|
|
3983
|
+
);
|
|
3984
|
+
dropInputs.forEach(({ name, answer, path }) => {
|
|
3985
|
+
if (richBanks.length === 0) {
|
|
3986
|
+
ctx.addIssue({
|
|
3987
|
+
code: zod.z.ZodIssueCode.custom,
|
|
3988
|
+
message: `DROP_INPUT "${name}" needs a DRAGGABLE_OPTIONS bank to draw from`,
|
|
3989
|
+
path
|
|
3990
|
+
});
|
|
3991
|
+
} else if (!optionNames.has(answer)) {
|
|
3992
|
+
ctx.addIssue({
|
|
3993
|
+
code: zod.z.ZodIssueCode.custom,
|
|
3994
|
+
message: `DROP_INPUT "${name}": answer "${answer}" does not match any OPTION name`,
|
|
3995
|
+
path: [...path, "answer"]
|
|
3996
|
+
});
|
|
3997
|
+
}
|
|
3998
|
+
});
|
|
3999
|
+
const isSingleUseBank = richBanks.some(({ tool }) => tool.type === AST_TYPES.DRAGGABLE_OPTIONS && !tool.reusable);
|
|
4000
|
+
if (isSingleUseBank) {
|
|
4001
|
+
const firstInputByAnswer = /* @__PURE__ */ new Map();
|
|
4002
|
+
dropInputs.forEach(({ name, answer, path }) => {
|
|
4003
|
+
if (!optionNames.has(answer)) return;
|
|
4004
|
+
const firstInput = firstInputByAnswer.get(answer);
|
|
4005
|
+
if (firstInput === void 0) {
|
|
4006
|
+
firstInputByAnswer.set(answer, name);
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
ctx.addIssue({
|
|
4010
|
+
code: zod.z.ZodIssueCode.custom,
|
|
4011
|
+
message: `DROP_INPUT "${name}": answer "${answer}" is also the answer of "${firstInput}", but its card can only be placed once. Add a second option or use DRAGGABLE_OPTIONS[reusable=true]`,
|
|
4012
|
+
path: [...path, "answer"]
|
|
4013
|
+
});
|
|
4014
|
+
});
|
|
4015
|
+
}
|
|
3794
4016
|
});
|
|
3795
4017
|
function createExerciseDocument(question, lines, validation2, tools) {
|
|
3796
|
-
|
|
4018
|
+
const result = ExerciseDocumentSchema.safeParse({
|
|
3797
4019
|
type: AST_TYPES.EXERCISE_DOCUMENT,
|
|
3798
4020
|
question,
|
|
3799
4021
|
lines,
|
|
3800
4022
|
tools,
|
|
3801
4023
|
validation: validation2
|
|
3802
4024
|
});
|
|
4025
|
+
if (result.success) return result.data;
|
|
4026
|
+
const { issues } = result.error;
|
|
4027
|
+
if (issues.every((issue) => issue.code === zod.z.ZodIssueCode.custom)) {
|
|
4028
|
+
throw new Error(issues.map((issue) => issue.message).join("\n"));
|
|
4029
|
+
}
|
|
4030
|
+
throw result.error;
|
|
3803
4031
|
}
|
|
3804
4032
|
function isExerciseDocument(value) {
|
|
3805
4033
|
return ExerciseDocumentSchema.safeParse(value).success;
|
|
@@ -4199,6 +4427,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
4199
4427
|
if (ctx.inlineInput) {
|
|
4200
4428
|
return this.visit(ctx.inlineInput);
|
|
4201
4429
|
}
|
|
4430
|
+
if (ctx.dropInput) {
|
|
4431
|
+
return this.visit(ctx.dropInput);
|
|
4432
|
+
}
|
|
4202
4433
|
if (ctx.inlineDragOption) {
|
|
4203
4434
|
return this.visit(ctx.inlineDragOption);
|
|
4204
4435
|
}
|
|
@@ -4353,6 +4584,18 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
4353
4584
|
const digits = params.digits;
|
|
4354
4585
|
return createNumericInput(name, digits, answer, placeholder, disabled, width, prefill);
|
|
4355
4586
|
}
|
|
4587
|
+
dropInput(ctx) {
|
|
4588
|
+
const params = this.visit(ctx.parameterList);
|
|
4589
|
+
if (typeof params.name === "string" && params.name.startsWith("_")) {
|
|
4590
|
+
throw new Error(`DROP_INPUT: name "${params.name}" is reserved (names cannot start with "_")`);
|
|
4591
|
+
}
|
|
4592
|
+
return createDropInput(
|
|
4593
|
+
params.name,
|
|
4594
|
+
params.answer,
|
|
4595
|
+
params.placeholder,
|
|
4596
|
+
params.width
|
|
4597
|
+
);
|
|
4598
|
+
}
|
|
4356
4599
|
inlineDragOption(ctx) {
|
|
4357
4600
|
const valueToken = ctx.StringLiteral[0];
|
|
4358
4601
|
const value = this.extractString(valueToken.image);
|
|
@@ -4491,6 +4734,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
4491
4734
|
// Tools
|
|
4492
4735
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
4493
4736
|
tool(ctx) {
|
|
4737
|
+
if (ctx.draggableOptions) {
|
|
4738
|
+
return this.visit(ctx.draggableOptions);
|
|
4739
|
+
}
|
|
4494
4740
|
if (ctx.dragableButtons) {
|
|
4495
4741
|
return this.visit(ctx.dragableButtons);
|
|
4496
4742
|
}
|
|
@@ -4508,6 +4754,33 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
4508
4754
|
}
|
|
4509
4755
|
throw new Error("Unknown tool");
|
|
4510
4756
|
}
|
|
4757
|
+
draggableOptions(ctx) {
|
|
4758
|
+
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
4759
|
+
if (params.reusable !== void 0 && !["true", "false"].includes(String(params.reusable))) {
|
|
4760
|
+
throw new Error("DRAGGABLE_OPTIONS: reusable must be true or false");
|
|
4761
|
+
}
|
|
4762
|
+
const reusable = params.reusable === true || params.reusable === "true";
|
|
4763
|
+
const options = ctx.draggableOption.map((option) => this.visit(option));
|
|
4764
|
+
return createDraggableOptions(options, reusable);
|
|
4765
|
+
}
|
|
4766
|
+
draggableOption(ctx) {
|
|
4767
|
+
const params = this.visit(ctx.parameterList);
|
|
4768
|
+
const rawContent = ctx.inlineElement ? ctx.inlineElement.map((element) => this.visit(element)) : [];
|
|
4769
|
+
const name = params.name;
|
|
4770
|
+
if (typeof name !== "string" || name === "") {
|
|
4771
|
+
throw new Error("DRAGGABLE_OPTIONS: OPTION requires a non-empty string name");
|
|
4772
|
+
}
|
|
4773
|
+
if (!hasOptionContent(rawContent)) {
|
|
4774
|
+
throw new Error(`DRAGGABLE_OPTIONS: OPTION "${name}" content cannot be empty or whitespace only`);
|
|
4775
|
+
}
|
|
4776
|
+
const invalid = rawContent.find((node) => !isOptionContentNode(node));
|
|
4777
|
+
if (invalid) {
|
|
4778
|
+
throw new Error(
|
|
4779
|
+
`DRAGGABLE_OPTIONS: OPTION "${name}" cannot contain ${invalid.type} here. Content must be display-only: text, styled text, images, and fractions or visual fractions without an answer`
|
|
4780
|
+
);
|
|
4781
|
+
}
|
|
4782
|
+
return createDraggableOption(name, rawContent);
|
|
4783
|
+
}
|
|
4511
4784
|
dragableButtons(ctx) {
|
|
4512
4785
|
const strings = this.visit(ctx.stringList);
|
|
4513
4786
|
return createDragOptions(strings);
|
|
@@ -5115,10 +5388,30 @@ const InlineInputJSONSchema = zod.z.discriminatedUnion("inputType", [
|
|
|
5115
5388
|
StringInputJSONSchema,
|
|
5116
5389
|
NumericInputJSONSchema
|
|
5117
5390
|
]);
|
|
5391
|
+
const DropInputJSONSchema = zod.z.object({
|
|
5392
|
+
type: zod.z.literal(JSON_TYPES.DROP_INPUT),
|
|
5393
|
+
/** Box width in characters; absent means the default input size. */
|
|
5394
|
+
width: InlineInputWidthSchema,
|
|
5395
|
+
placeholder: zod.z.string().max(32).optional()
|
|
5396
|
+
});
|
|
5118
5397
|
const DragOptionsJSONSchema = zod.z.object({
|
|
5119
5398
|
type: zod.z.literal(JSON_TYPES.DRAG_OPTIONS),
|
|
5120
5399
|
options: zod.z.array(zod.z.string())
|
|
5121
5400
|
});
|
|
5401
|
+
const DraggableOptionJSONSchema = zod.z.object({
|
|
5402
|
+
name: zod.z.string().min(1),
|
|
5403
|
+
content: zod.z.array(LayoutItemSchema).min(1).refine(
|
|
5404
|
+
(content) => hasOptionContent(content, JSON_TYPES.SPACE),
|
|
5405
|
+
{ message: "Option content cannot be empty or whitespace only" }
|
|
5406
|
+
)
|
|
5407
|
+
});
|
|
5408
|
+
const DraggableOptionsJSONSchema = zod.z.object({
|
|
5409
|
+
type: zod.z.literal(JSON_TYPES.DRAGGABLE_OPTIONS),
|
|
5410
|
+
reusable: zod.z.boolean(),
|
|
5411
|
+
options: zod.z.array(DraggableOptionJSONSchema).min(1)
|
|
5412
|
+
}).superRefine((widget, ctx) => {
|
|
5413
|
+
addDuplicateOptionNameIssues(widget.options, ctx);
|
|
5414
|
+
});
|
|
5122
5415
|
const InlineDragOptionJSONSchema = zod.z.object({
|
|
5123
5416
|
type: zod.z.literal(JSON_TYPES.INLINE_DRAG_OPTION),
|
|
5124
5417
|
value: zod.z.string().min(1)
|
|
@@ -5228,7 +5521,9 @@ const GeometryJSONSchema = zod.z.object({
|
|
|
5228
5521
|
});
|
|
5229
5522
|
const WidgetJSONSchema = zod.z.union([
|
|
5230
5523
|
InlineInputJSONSchema,
|
|
5524
|
+
DropInputJSONSchema,
|
|
5231
5525
|
DragOptionsJSONSchema,
|
|
5526
|
+
DraggableOptionsJSONSchema,
|
|
5232
5527
|
InlineDragOptionJSONSchema,
|
|
5233
5528
|
RadioOptionsJSONSchema,
|
|
5234
5529
|
NumericKeypadJSONSchema,
|
|
@@ -5345,9 +5640,15 @@ function isInlineInputJSON(widget) {
|
|
|
5345
5640
|
function isDisabledInputJSON(widget) {
|
|
5346
5641
|
return isInlineInputJSON(widget) && widget.disabled === true;
|
|
5347
5642
|
}
|
|
5643
|
+
function isDropInputJSON(widget) {
|
|
5644
|
+
return widget.type === JSON_TYPES.DROP_INPUT;
|
|
5645
|
+
}
|
|
5348
5646
|
function isDragOptionsJSON(widget) {
|
|
5349
5647
|
return widget.type === JSON_TYPES.DRAG_OPTIONS;
|
|
5350
5648
|
}
|
|
5649
|
+
function isDraggableOptionsJSON(widget) {
|
|
5650
|
+
return widget.type === JSON_TYPES.DRAGGABLE_OPTIONS;
|
|
5651
|
+
}
|
|
5351
5652
|
function isInlineDragOptionJSON(widget) {
|
|
5352
5653
|
return widget.type === JSON_TYPES.INLINE_DRAG_OPTION;
|
|
5353
5654
|
}
|
|
@@ -10881,7 +11182,16 @@ function toJSON(ast) {
|
|
|
10881
11182
|
const answers = {};
|
|
10882
11183
|
let gridSymmetryCounter = 0;
|
|
10883
11184
|
let geometryCounter = 0;
|
|
10884
|
-
const
|
|
11185
|
+
const richOptionsTool = ast.tools?.find(
|
|
11186
|
+
(tool) => tool.type === AST_TYPES.DRAGGABLE_OPTIONS
|
|
11187
|
+
);
|
|
11188
|
+
const allInlines = [
|
|
11189
|
+
...collectAllInlines(ast.lines),
|
|
11190
|
+
...richOptionsTool?.options.flatMap((option) => option.content) ?? []
|
|
11191
|
+
];
|
|
11192
|
+
const matcherCardInlines = new Set(collectAllInlines(ast.lines.filter(
|
|
11193
|
+
(block) => block.type === AST_TYPES.MATCHER_BLOCK || block.type === AST_TYPES.PAIR_MATCHER_BLOCK
|
|
11194
|
+
)));
|
|
10885
11195
|
const inlineDragIds = /* @__PURE__ */ new Map();
|
|
10886
11196
|
{
|
|
10887
11197
|
let counter = 0;
|
|
@@ -11007,7 +11317,7 @@ function toJSON(ast) {
|
|
|
11007
11317
|
const hasDragBank = ast.tools?.some((tool) => tool.type === AST_TYPES.DRAG_OPTIONS) ?? false;
|
|
11008
11318
|
for (const inline of allInlines) {
|
|
11009
11319
|
if (inline.type === AST_TYPES.INLINE_INPUT) {
|
|
11010
|
-
const disabled = inline.disabled || hasDragBank && inline.prefill
|
|
11320
|
+
const disabled = inline.disabled || hasDragBank && !!inline.prefill;
|
|
11011
11321
|
if (inline.inputType === INPUT_TYPES.STRING) {
|
|
11012
11322
|
widgets[inline.name] = {
|
|
11013
11323
|
type: JSON_TYPES.INLINE_INPUT,
|
|
@@ -11032,6 +11342,13 @@ function toJSON(ast) {
|
|
|
11032
11342
|
if (inline.answer !== void 0 && !disabled) {
|
|
11033
11343
|
answers[inline.name] = inline.answer;
|
|
11034
11344
|
}
|
|
11345
|
+
} else if (inline.type === AST_TYPES.DROP_INPUT) {
|
|
11346
|
+
widgets[inline.name] = {
|
|
11347
|
+
type: JSON_TYPES.DROP_INPUT,
|
|
11348
|
+
...inline.width !== void 0 && { width: inline.width },
|
|
11349
|
+
placeholder: inline.placeholder
|
|
11350
|
+
};
|
|
11351
|
+
answers[inline.name] = inline.answer;
|
|
11035
11352
|
} else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
|
|
11036
11353
|
answers[inline.name] = inline.answer;
|
|
11037
11354
|
}
|
|
@@ -11043,7 +11360,7 @@ function toJSON(ast) {
|
|
|
11043
11360
|
simplified: inline.simplified ?? true,
|
|
11044
11361
|
placeholder: inline.placeholder
|
|
11045
11362
|
};
|
|
11046
|
-
if (inline.answer !== void 0) {
|
|
11363
|
+
if (inline.answer !== void 0 && !matcherCardInlines.has(inline)) {
|
|
11047
11364
|
answers[inline.name] = inline.answer;
|
|
11048
11365
|
}
|
|
11049
11366
|
}
|
|
@@ -11119,6 +11436,15 @@ function toJSON(ast) {
|
|
|
11119
11436
|
// Randomise chip order per compile so the answer position isn't fixed.
|
|
11120
11437
|
options: lodashExports.shuffle(tool.options)
|
|
11121
11438
|
};
|
|
11439
|
+
} else if (tool.type === AST_TYPES.DRAGGABLE_OPTIONS) {
|
|
11440
|
+
widgets[DRAGGABLE_OPTIONS_WIDGET_ID] = {
|
|
11441
|
+
type: JSON_TYPES.DRAGGABLE_OPTIONS,
|
|
11442
|
+
reusable: tool.reusable,
|
|
11443
|
+
options: tool.options.map((option) => ({
|
|
11444
|
+
name: option.name,
|
|
11445
|
+
content: option.content.map((inline) => transformInline(inline, idGenerator, inlineDragIds))
|
|
11446
|
+
}))
|
|
11447
|
+
};
|
|
11122
11448
|
} else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
|
|
11123
11449
|
widgets[NUMERIC_KEYPAD_WIDGET_ID] = {
|
|
11124
11450
|
type: JSON_TYPES.NUMERIC_KEYPAD,
|
|
@@ -11584,6 +11910,12 @@ function transformInline(inline, idGenerator, inlineDragIds) {
|
|
|
11584
11910
|
type: JSON_TYPES.WIDGET_REF,
|
|
11585
11911
|
widgetId: inline.name
|
|
11586
11912
|
};
|
|
11913
|
+
case AST_TYPES.DROP_INPUT:
|
|
11914
|
+
return {
|
|
11915
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
11916
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
11917
|
+
widgetId: inline.name
|
|
11918
|
+
};
|
|
11587
11919
|
case AST_TYPES.FRACTION:
|
|
11588
11920
|
return {
|
|
11589
11921
|
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
@@ -11640,6 +11972,7 @@ function transformValidation(validation2, answers, widgets) {
|
|
|
11640
11972
|
};
|
|
11641
11973
|
} else if (validation2.type === AST_TYPES.EXPRESSION_VALIDATION) {
|
|
11642
11974
|
assertNoDisabledInExpression(validation2.expression, widgets);
|
|
11975
|
+
assertNoDropInputs(Object.keys(widgets), widgets);
|
|
11643
11976
|
const validVariableNames = getEnabledWidgetNames(widgets);
|
|
11644
11977
|
const variables = extractVariables$1(validation2.expression, validVariableNames);
|
|
11645
11978
|
return {
|
|
@@ -11649,6 +11982,7 @@ function transformValidation(validation2, answers, widgets) {
|
|
|
11649
11982
|
};
|
|
11650
11983
|
} else if (validation2.type === AST_TYPES.MIXED_VALIDATION) {
|
|
11651
11984
|
assertNoDisabledInExpression(validation2.expression, widgets);
|
|
11985
|
+
assertNoDropInputs(extractVariables$1(validation2.expression, Object.keys(widgets)), widgets);
|
|
11652
11986
|
const validVariableNames = getEnabledWidgetNames(widgets);
|
|
11653
11987
|
const variables = extractVariables$1(validation2.expression, validVariableNames);
|
|
11654
11988
|
const inExpression = new Set(variables);
|
|
@@ -11687,6 +12021,14 @@ function assertNoDisabledInExpression(expression, widgets) {
|
|
|
11687
12021
|
);
|
|
11688
12022
|
}
|
|
11689
12023
|
}
|
|
12024
|
+
function assertNoDropInputs(names, widgets) {
|
|
12025
|
+
const dropInputs = names.filter((name) => widgets[name]?.type === JSON_TYPES.DROP_INPUT);
|
|
12026
|
+
if (dropInputs.length > 0) {
|
|
12027
|
+
throw new Error(
|
|
12028
|
+
`WIDGET_ANSWER: DROP_INPUT ${dropInputs.map((n) => `"${n}"`).join(", ")} cannot be used in expression validation — its value is an option name, not a number. Use type="simple"`
|
|
12029
|
+
);
|
|
12030
|
+
}
|
|
12031
|
+
}
|
|
11690
12032
|
function getEnabledWidgetNames(widgets) {
|
|
11691
12033
|
return Object.entries(widgets).filter(([, widget]) => !isDisabledInputJSON(widget)).map(([name]) => name);
|
|
11692
12034
|
}
|
|
@@ -12260,7 +12602,7 @@ function withDisabledPrefilledInputs(exercise) {
|
|
|
12260
12602
|
const widgets = { ...exercise.widgets };
|
|
12261
12603
|
let changed = false;
|
|
12262
12604
|
for (const [id, widget] of Object.entries(widgets)) {
|
|
12263
|
-
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.prefill
|
|
12605
|
+
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.prefill && !widget.disabled) {
|
|
12264
12606
|
widgets[id] = { ...widget, disabled: true };
|
|
12265
12607
|
changed = true;
|
|
12266
12608
|
}
|
|
@@ -12433,11 +12775,13 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
12433
12775
|
}
|
|
12434
12776
|
function validateInputFormats(exercise, userInputs) {
|
|
12435
12777
|
const errors = {};
|
|
12436
|
-
|
|
12778
|
+
const { widgets } = withDisabledPrefilledInputs(exercise);
|
|
12779
|
+
for (const [widgetId, widget] of Object.entries(widgets)) {
|
|
12437
12780
|
const value = userInputs[widgetId];
|
|
12438
12781
|
if (value === void 0 || value === "") {
|
|
12439
12782
|
continue;
|
|
12440
12783
|
}
|
|
12784
|
+
if (isDisabledInputJSON(widget)) continue;
|
|
12441
12785
|
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.inputType === INPUT_TYPES.NUMERIC) {
|
|
12442
12786
|
const numValue = typeof value === "number" ? value : parseFloat(String(value));
|
|
12443
12787
|
if (!Number.isFinite(numValue)) {
|
|
@@ -12455,6 +12799,8 @@ function validateInputFormats(exercise, userInputs) {
|
|
|
12455
12799
|
const DragItemTypeSchema = zod.z.enum([
|
|
12456
12800
|
"drag-option",
|
|
12457
12801
|
// Draggable answer options
|
|
12802
|
+
"rich-drag-option",
|
|
12803
|
+
// Rich option from DRAGGABLE_OPTIONS or DROP_INPUT
|
|
12458
12804
|
"placed-chip",
|
|
12459
12805
|
// Chip already placed on a number-line axis (re-drag)
|
|
12460
12806
|
"ruler-handle",
|
|
@@ -12468,6 +12814,16 @@ const DragOptionDataSchema = zod.z.object({
|
|
|
12468
12814
|
value: zod.z.string(),
|
|
12469
12815
|
label: zod.z.string()
|
|
12470
12816
|
});
|
|
12817
|
+
const RichDragOptionDataSchema = zod.z.object({
|
|
12818
|
+
optionName: zod.z.string().min(1),
|
|
12819
|
+
origin: zod.z.discriminatedUnion("kind", [
|
|
12820
|
+
zod.z.object({ kind: zod.z.literal("bank") }),
|
|
12821
|
+
zod.z.object({
|
|
12822
|
+
kind: zod.z.literal("drop-input"),
|
|
12823
|
+
inputName: zod.z.string().min(1)
|
|
12824
|
+
})
|
|
12825
|
+
])
|
|
12826
|
+
});
|
|
12471
12827
|
const DragItemSchema = (dataSchema) => zod.z.object({
|
|
12472
12828
|
id: zod.z.union([zod.z.string(), zod.z.number()]),
|
|
12473
12829
|
// UniqueIdentifier from dnd-kit
|
|
@@ -12475,6 +12831,7 @@ const DragItemSchema = (dataSchema) => zod.z.object({
|
|
|
12475
12831
|
data: dataSchema
|
|
12476
12832
|
});
|
|
12477
12833
|
DragItemSchema(DragOptionDataSchema);
|
|
12834
|
+
DragItemSchema(RichDragOptionDataSchema);
|
|
12478
12835
|
zod.z.object({
|
|
12479
12836
|
targetId: zod.z.string(),
|
|
12480
12837
|
item: zod.z.discriminatedUnion("type", [
|
|
@@ -16255,12 +16612,16 @@ function useExerciseDnd() {
|
|
|
16255
16612
|
}
|
|
16256
16613
|
return ctx;
|
|
16257
16614
|
}
|
|
16615
|
+
function useExerciseDndOptional() {
|
|
16616
|
+
return React.useContext(ExerciseDndContext);
|
|
16617
|
+
}
|
|
16258
16618
|
const NO_EXERCISE = Symbol("no-exercise");
|
|
16259
16619
|
const ExerciseDndProvider = ({
|
|
16260
16620
|
children,
|
|
16261
16621
|
droppedSources,
|
|
16262
16622
|
onDragEnd,
|
|
16263
|
-
exerciseKey
|
|
16623
|
+
exerciseKey,
|
|
16624
|
+
renderOverlay
|
|
16264
16625
|
}) => {
|
|
16265
16626
|
const [activeItem, setActiveItem] = React.useState(null);
|
|
16266
16627
|
const [draggedExerciseKey, setDraggedExerciseKey] = React.useState(NO_EXERCISE);
|
|
@@ -16296,7 +16657,9 @@ const ExerciseDndProvider = ({
|
|
|
16296
16657
|
const raw = active.data.current;
|
|
16297
16658
|
const item = raw?.type === "placed-chip" ? { type: "placed-chip", widgetId: raw.widgetId, label: raw.label, sourceOptionId: raw.sourceOptionId } : raw;
|
|
16298
16659
|
setActiveItem(item);
|
|
16299
|
-
if (item.type === "drag-option")
|
|
16660
|
+
if (item.type === "drag-option" || item.type === "rich-drag-option") {
|
|
16661
|
+
setDraggedExerciseKey(exerciseKey);
|
|
16662
|
+
}
|
|
16300
16663
|
playSound("drag-start");
|
|
16301
16664
|
};
|
|
16302
16665
|
const handleDragEnd = (event) => {
|
|
@@ -16339,14 +16702,16 @@ const ExerciseDndProvider = ({
|
|
|
16339
16702
|
onDragCancel: handleDragCancel,
|
|
16340
16703
|
children: [
|
|
16341
16704
|
children,
|
|
16342
|
-
/* @__PURE__ */ jsxRuntime.jsx(DragOverlay, { dropAnimation: null, children: activeItem && /* @__PURE__ */ jsxRuntime.jsx(DragOverlayContent, { item: activeItem }) })
|
|
16705
|
+
/* @__PURE__ */ jsxRuntime.jsx(DragOverlay, { dropAnimation: null, children: activeItem && /* @__PURE__ */ jsxRuntime.jsx(DragOverlayContent, { item: activeItem, renderOverlay }) })
|
|
16343
16706
|
]
|
|
16344
16707
|
}
|
|
16345
16708
|
)
|
|
16346
16709
|
}
|
|
16347
16710
|
);
|
|
16348
16711
|
};
|
|
16349
|
-
const DragOverlayContent = ({ item }) => {
|
|
16712
|
+
const DragOverlayContent = ({ item, renderOverlay }) => {
|
|
16713
|
+
const customOverlay = renderOverlay?.(item);
|
|
16714
|
+
if (customOverlay != null) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: customOverlay });
|
|
16350
16715
|
switch (item.type) {
|
|
16351
16716
|
case "drag-option": {
|
|
16352
16717
|
const data = item.data;
|
|
@@ -16365,7 +16730,8 @@ const Draggable = ({
|
|
|
16365
16730
|
data,
|
|
16366
16731
|
disabled = false,
|
|
16367
16732
|
children,
|
|
16368
|
-
className = ""
|
|
16733
|
+
className = "",
|
|
16734
|
+
behavior = "move"
|
|
16369
16735
|
}) => {
|
|
16370
16736
|
const dragItem = { id, type, data };
|
|
16371
16737
|
const {
|
|
@@ -16378,6 +16744,7 @@ const Draggable = ({
|
|
|
16378
16744
|
data: dragItem,
|
|
16379
16745
|
disabled
|
|
16380
16746
|
});
|
|
16747
|
+
const isCopy = behavior === "copy";
|
|
16381
16748
|
const style = {
|
|
16382
16749
|
cursor: disabled ? "default" : "grab",
|
|
16383
16750
|
touchAction: "none"
|
|
@@ -16387,7 +16754,7 @@ const Draggable = ({
|
|
|
16387
16754
|
{
|
|
16388
16755
|
ref: setNodeRef,
|
|
16389
16756
|
style,
|
|
16390
|
-
className: `draggable ${isDragging ? "dragging" : ""} ${className}`,
|
|
16757
|
+
className: `draggable ${isDragging && !isCopy ? "dragging" : ""} ${className}`,
|
|
16391
16758
|
...listeners,
|
|
16392
16759
|
...attributes,
|
|
16393
16760
|
children
|
|
@@ -17179,6 +17546,9 @@ const PlacedInputChip = ({
|
|
|
17179
17546
|
}
|
|
17180
17547
|
);
|
|
17181
17548
|
};
|
|
17549
|
+
function getInputBoxWidth(chars) {
|
|
17550
|
+
return `${chars * 0.8 + 1}em`;
|
|
17551
|
+
}
|
|
17182
17552
|
function getMaxInputLength(exercise, inputKey) {
|
|
17183
17553
|
const widget = exercise.widgets[inputKey];
|
|
17184
17554
|
if (widget?.type !== JSON_TYPES.INLINE_INPUT) return void 0;
|
|
@@ -17235,7 +17605,7 @@ const InlineInput = ({
|
|
|
17235
17605
|
return applyDropToInput(widgetId, item, onChange);
|
|
17236
17606
|
}, [onChange, widgetId]);
|
|
17237
17607
|
const expectedLength = inputType === INPUT_TYPES.NUMERIC ? digits : length;
|
|
17238
|
-
const dynamicWidth =
|
|
17608
|
+
const dynamicWidth = getInputBoxWidth(width ?? expectedLength ?? 5);
|
|
17239
17609
|
const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
|
|
17240
17610
|
const disabledClass = disabled ? " inline-input--disabled" : "";
|
|
17241
17611
|
const isFixedChip = hasDragBank && disabled && value !== "";
|
|
@@ -17439,42 +17809,418 @@ const InlineDragOption = ({
|
|
|
17439
17809
|
}
|
|
17440
17810
|
) });
|
|
17441
17811
|
};
|
|
17442
|
-
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
|
|
17447
|
-
}
|
|
17448
|
-
|
|
17449
|
-
|
|
17450
|
-
|
|
17451
|
-
|
|
17452
|
-
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
const
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17812
|
+
function isArabic(text) {
|
|
17813
|
+
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
17814
|
+
}
|
|
17815
|
+
function hasLatinChars(text) {
|
|
17816
|
+
return /[A-Za-z]/.test(text);
|
|
17817
|
+
}
|
|
17818
|
+
function getTextValue(item) {
|
|
17819
|
+
if ((item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.BOLD_TEXT || item.type === JSON_TYPES.COLOR_TEXT) && "value" in item) {
|
|
17820
|
+
return item.value;
|
|
17821
|
+
}
|
|
17822
|
+
return void 0;
|
|
17823
|
+
}
|
|
17824
|
+
function itemContainsArabic(item) {
|
|
17825
|
+
const text = getTextValue(item);
|
|
17826
|
+
if (text !== void 0) {
|
|
17827
|
+
return isArabic(text);
|
|
17828
|
+
}
|
|
17829
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
17830
|
+
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
17831
|
+
}
|
|
17832
|
+
return false;
|
|
17833
|
+
}
|
|
17834
|
+
function itemContainsLatin(item) {
|
|
17835
|
+
const text = getTextValue(item);
|
|
17836
|
+
if (text !== void 0) {
|
|
17837
|
+
return hasLatinChars(text);
|
|
17838
|
+
}
|
|
17839
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
17840
|
+
return item.items.some((subItem) => itemContainsLatin(subItem));
|
|
17841
|
+
}
|
|
17842
|
+
return false;
|
|
17843
|
+
}
|
|
17844
|
+
function shouldItemsUseRTL(items) {
|
|
17845
|
+
const hasArabic = items.some(itemContainsArabic);
|
|
17846
|
+
const hasLatin = items.some(itemContainsLatin);
|
|
17847
|
+
return hasArabic && !hasLatin;
|
|
17848
|
+
}
|
|
17849
|
+
function isTextItem(item) {
|
|
17850
|
+
return item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.BOLD_TEXT || item.type === JSON_TYPES.COLOR_TEXT || item.type === JSON_TYPES.OPERATOR || item.type === JSON_TYPES.SPACE || item.type === JSON_TYPES.GROUP;
|
|
17851
|
+
}
|
|
17852
|
+
function groupLineItems(items) {
|
|
17853
|
+
const groups = [];
|
|
17854
|
+
let currentTextGroup = [];
|
|
17855
|
+
for (const item of items) {
|
|
17856
|
+
if (isTextItem(item)) {
|
|
17857
|
+
currentTextGroup.push(item);
|
|
17858
|
+
} else {
|
|
17859
|
+
if (currentTextGroup.length > 0) {
|
|
17860
|
+
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
17861
|
+
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
17862
|
+
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
17863
|
+
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
17864
|
+
currentTextGroup = [];
|
|
17865
|
+
}
|
|
17866
|
+
groups.push({ items: [item], isTextGroup: false, hasArabic: false, hasLatin: false, shouldApplyRTL: false });
|
|
17867
|
+
}
|
|
17868
|
+
}
|
|
17869
|
+
if (currentTextGroup.length > 0) {
|
|
17870
|
+
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
17871
|
+
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
17872
|
+
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
17873
|
+
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
17874
|
+
}
|
|
17875
|
+
return groups;
|
|
17876
|
+
}
|
|
17877
|
+
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
17878
|
+
const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
|
|
17879
|
+
const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
|
|
17880
|
+
const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
|
|
17881
|
+
const NUMBER_LINE_AXIS_ENDS = {
|
|
17882
|
+
"arrow-right": { left: "tick", right: "arrow" },
|
|
17883
|
+
"arrow-left": { left: "arrow", right: "tick" },
|
|
17884
|
+
"arrow-both": { left: "arrow", right: "arrow" },
|
|
17885
|
+
"line": { left: "line", right: "line" },
|
|
17886
|
+
"line-left": { left: "line", right: "arrow" }
|
|
17887
|
+
};
|
|
17888
|
+
const toPercent = (value, min, max) => {
|
|
17889
|
+
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
17890
|
+
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
17891
|
+
};
|
|
17892
|
+
const pixelToValue = (chipCenterX, axisLeft, axisWidth, min, max) => {
|
|
17893
|
+
const relX = (chipCenterX - axisLeft) / axisWidth;
|
|
17894
|
+
const fraction = (relX - SIDE_PAD / 100) / (1 - 2 * SIDE_PAD / 100);
|
|
17895
|
+
const clamped = Math.max(0, Math.min(1, fraction));
|
|
17896
|
+
return min + clamped * (max - min);
|
|
17897
|
+
};
|
|
17898
|
+
const getMajorTicks = (min, max, step) => {
|
|
17899
|
+
if (step <= 0 || max <= min) return [min, max];
|
|
17900
|
+
const count = Math.floor((max - min) / step);
|
|
17901
|
+
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
17902
|
+
const lastTick = ticks[ticks.length - 1];
|
|
17903
|
+
if (roundTick(lastTick) !== roundTick(max)) {
|
|
17904
|
+
ticks.push(max);
|
|
17905
|
+
} else {
|
|
17906
|
+
ticks[ticks.length - 1] = max;
|
|
17907
|
+
}
|
|
17908
|
+
return ticks;
|
|
17909
|
+
};
|
|
17910
|
+
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
17911
|
+
if (minorTicks <= 0 || majorTicks.length < 2) return [];
|
|
17912
|
+
const result = [];
|
|
17913
|
+
for (let i = 0; i < majorTicks.length - 1; i++) {
|
|
17914
|
+
const from = majorTicks[i];
|
|
17915
|
+
const to = majorTicks[i + 1];
|
|
17916
|
+
const interval = to - from;
|
|
17917
|
+
for (let j = 1; j <= minorTicks; j++) {
|
|
17918
|
+
result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
|
|
17919
|
+
}
|
|
17920
|
+
}
|
|
17921
|
+
return result;
|
|
17922
|
+
};
|
|
17923
|
+
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
17924
|
+
...majorTicks,
|
|
17925
|
+
...getMinorTickValues(majorTicks, minorTicks)
|
|
17926
|
+
];
|
|
17927
|
+
const snapToNearest = (raw, ticks) => {
|
|
17928
|
+
if (ticks.length === 0) return cleanFloat(raw);
|
|
17929
|
+
return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
|
|
17930
|
+
};
|
|
17931
|
+
const getPositionWidgetId = (content) => {
|
|
17932
|
+
for (const item of content) {
|
|
17933
|
+
if (isWidgetRef(item)) return item.widgetId;
|
|
17934
|
+
}
|
|
17935
|
+
return null;
|
|
17936
|
+
};
|
|
17937
|
+
const toHighlightColor = (color) => color.startsWith("#") ? color : `var(--nl-highlight-${color})`;
|
|
17938
|
+
function placeTableCells(rows, cols) {
|
|
17939
|
+
const placements = /* @__PURE__ */ new Map();
|
|
17940
|
+
const occupied = rows.map(() => []);
|
|
17941
|
+
const totalRows = rows.length;
|
|
17942
|
+
rows.forEach((row, rowIndex) => {
|
|
17943
|
+
const taken = occupied[rowIndex] ?? [];
|
|
17944
|
+
let col = 0;
|
|
17945
|
+
for (const cell of row.cells) {
|
|
17946
|
+
while (taken[col]) col++;
|
|
17947
|
+
const colspan = cell.colspan || 1;
|
|
17948
|
+
const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
|
|
17949
|
+
for (let r = rowIndex; r < rowIndex + rowspan; r++) {
|
|
17950
|
+
const line = occupied[r];
|
|
17951
|
+
if (!line) continue;
|
|
17952
|
+
for (let c = col; c < col + colspan; c++) line[c] = true;
|
|
17953
|
+
}
|
|
17954
|
+
placements.set(cell.id, {
|
|
17955
|
+
column: col + 1,
|
|
17956
|
+
row: rowIndex + 1,
|
|
17957
|
+
colspan,
|
|
17958
|
+
rowspan,
|
|
17959
|
+
isLastColumn: col + colspan >= cols,
|
|
17960
|
+
isLastRow: rowIndex + rowspan >= totalRows
|
|
17961
|
+
});
|
|
17962
|
+
col += colspan;
|
|
17963
|
+
}
|
|
17964
|
+
});
|
|
17965
|
+
return placements;
|
|
17966
|
+
}
|
|
17967
|
+
const EMPTY_VALUES$2 = {};
|
|
17968
|
+
const EMPTY_DROPPED_INPUTS = /* @__PURE__ */ new Set();
|
|
17969
|
+
const ignoreChange = () => {
|
|
17970
|
+
};
|
|
17971
|
+
const PLAIN_TEXT_TYPES = /* @__PURE__ */ new Set([JSON_TYPES.TEXT, JSON_TYPES.SPACE, JSON_TYPES.BOLD_TEXT]);
|
|
17972
|
+
function isPlainTextContent(content) {
|
|
17973
|
+
return content.every((item) => PLAIN_TEXT_TYPES.has(item.type));
|
|
17974
|
+
}
|
|
17975
|
+
const RichOptionContent = ({ content, widgets }) => {
|
|
17976
|
+
const renderItem = (item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
17977
|
+
LayoutRenderer,
|
|
17978
|
+
{
|
|
17979
|
+
item: item.type === JSON_TYPES.IMAGE ? { ...item, fullscreen: false } : item,
|
|
17980
|
+
widgets,
|
|
17981
|
+
values: EMPTY_VALUES$2,
|
|
17982
|
+
droppedInputs: EMPTY_DROPPED_INPUTS,
|
|
17983
|
+
onChange: ignoreChange
|
|
17984
|
+
},
|
|
17985
|
+
item.id
|
|
17986
|
+
);
|
|
17987
|
+
const hasRTLInlineWidget = content.some((item) => {
|
|
17988
|
+
if (item.type !== JSON_TYPES.WIDGET_REF) return false;
|
|
17989
|
+
const widget = widgets[item.widgetId];
|
|
17990
|
+
return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
|
|
17991
|
+
});
|
|
17992
|
+
const isRTL = hasRTLInlineWidget && content.some(itemContainsArabic);
|
|
17993
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rich-option-content", dir: isRTL ? "rtl" : "ltr", children: groupLineItems(content).map((group, index) => group.isTextGroup ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-group", dir: group.shouldApplyRTL ? "rtl" : void 0, children: group.items.map(renderItem) }, index) : group.items.map(renderItem)) });
|
|
17994
|
+
};
|
|
17995
|
+
const DraggableOptions = ({
|
|
17996
|
+
widget,
|
|
17997
|
+
widgets,
|
|
17998
|
+
usedOptionNames
|
|
17999
|
+
}) => {
|
|
18000
|
+
const { exerciseKey } = useExerciseDnd();
|
|
18001
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PillBackground, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-options draggable-options", children: widget.options.map((option) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
18002
|
+
RichDragOption,
|
|
18003
|
+
{
|
|
18004
|
+
option,
|
|
18005
|
+
widgets,
|
|
18006
|
+
reusable: widget.reusable,
|
|
18007
|
+
used: !widget.reusable && usedOptionNames.has(option.name)
|
|
18008
|
+
},
|
|
18009
|
+
`${exerciseKey ?? ""}:${option.name}`
|
|
18010
|
+
)) }) });
|
|
18011
|
+
};
|
|
18012
|
+
const RichDragOption = ({ option, widgets, reusable, used }) => {
|
|
18013
|
+
const { activeItem, hasDraggedOption, playHintSound } = useExerciseDnd();
|
|
18014
|
+
const locked = useExerciseLocked();
|
|
18015
|
+
const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption, {
|
|
18016
|
+
ignoreBubbledAnimationEnd: true
|
|
18017
|
+
});
|
|
18018
|
+
const optionId = `rich-drag-option:${option.name}`;
|
|
18019
|
+
const data = { optionName: option.name, origin: { kind: "bank" } };
|
|
18020
|
+
const isPickedUp = !reusable && activeItem?.type === "rich-drag-option" && activeItem.id === optionId;
|
|
18021
|
+
const surfaceClass = isPlainTextContent(option.content) ? "drag-option" : "drag-option drag-option--rich";
|
|
18022
|
+
const emptySlot = /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-option-empty", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: surfaceClass, disabled: true, children: /* @__PURE__ */ jsxRuntime.jsx(RichOptionContent, { content: option.content, widgets }) }) });
|
|
18023
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-option-slot rich-drag-option-slot", children: used ? emptySlot : /* @__PURE__ */ jsxRuntime.jsx(
|
|
18024
|
+
Draggable,
|
|
18025
|
+
{
|
|
18026
|
+
id: optionId,
|
|
18027
|
+
type: "rich-drag-option",
|
|
18028
|
+
data,
|
|
18029
|
+
disabled: locked,
|
|
18030
|
+
behavior: reusable ? "copy" : "move",
|
|
18031
|
+
className: "drag-option-wrapper rich-drag-option-wrapper",
|
|
18032
|
+
children: isPickedUp ? emptySlot : /* @__PURE__ */ jsxRuntime.jsx(
|
|
18033
|
+
"button",
|
|
18034
|
+
{
|
|
18035
|
+
type: "button",
|
|
18036
|
+
className: `${surfaceClass} rich-drag-option${hinting && !locked ? " drag-option--hint" : ""}`,
|
|
18037
|
+
onAnimationStart: (event) => {
|
|
18038
|
+
if (hinting && event.target === event.currentTarget) playHintSound();
|
|
18039
|
+
},
|
|
18040
|
+
onAnimationEnd: onHintEnd,
|
|
18041
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
18042
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(RichOptionContent, { content: option.content, widgets })
|
|
18043
|
+
}
|
|
18044
|
+
)
|
|
18045
|
+
}
|
|
18046
|
+
) });
|
|
18047
|
+
};
|
|
18048
|
+
const PlacedRichOption = ({
|
|
18049
|
+
inputName,
|
|
18050
|
+
option,
|
|
18051
|
+
widgets,
|
|
18052
|
+
onRemove,
|
|
18053
|
+
className = ""
|
|
18054
|
+
}) => {
|
|
18055
|
+
const locked = useExerciseLocked();
|
|
18056
|
+
const dragId = `${inputName}__rich-option`;
|
|
18057
|
+
const data = {
|
|
18058
|
+
optionName: option.name,
|
|
18059
|
+
origin: { kind: "drop-input", inputName }
|
|
18060
|
+
};
|
|
18061
|
+
const dragItem = { id: dragId, type: "rich-drag-option", data };
|
|
18062
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
18063
|
+
id: dragId,
|
|
18064
|
+
data: dragItem,
|
|
18065
|
+
disabled: locked
|
|
18066
|
+
});
|
|
18067
|
+
useDndMonitor({
|
|
18068
|
+
onDragEnd(event) {
|
|
18069
|
+
if (isUndoDrop(event, dragId)) onRemove();
|
|
18070
|
+
}
|
|
18071
|
+
});
|
|
18072
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
18073
|
+
"button",
|
|
18074
|
+
{
|
|
18075
|
+
ref: setNodeRef,
|
|
18076
|
+
type: "button",
|
|
18077
|
+
className: `inline-input-chip placed-rich-option ${className}${isDragging ? " dragging" : ""}`.trim(),
|
|
18078
|
+
onClick: (event) => {
|
|
18079
|
+
if (!locked && event.detail !== 0) onRemove();
|
|
18080
|
+
},
|
|
18081
|
+
title: locked ? void 0 : "Drag to move, or click to remove",
|
|
18082
|
+
style: {
|
|
18083
|
+
width: "100%",
|
|
18084
|
+
height: "100%",
|
|
18085
|
+
minWidth: 0,
|
|
18086
|
+
maxWidth: "none",
|
|
18087
|
+
margin: 0,
|
|
18088
|
+
boxSizing: "border-box",
|
|
18089
|
+
touchAction: "none",
|
|
18090
|
+
cursor: locked ? "default" : "grab"
|
|
18091
|
+
},
|
|
18092
|
+
...listeners,
|
|
18093
|
+
...attributes,
|
|
18094
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(RichOptionContent, { content: option.content, widgets })
|
|
18095
|
+
}
|
|
18096
|
+
);
|
|
18097
|
+
};
|
|
18098
|
+
const dropSourceId = (optionName) => `rich-drag-option:${optionName}`;
|
|
18099
|
+
function applyRichDropToInput(targetInputName, item, onChange) {
|
|
18100
|
+
if (item.type !== "rich-drag-option") return false;
|
|
18101
|
+
const data = item.data;
|
|
18102
|
+
if (data.origin.kind === "drop-input") {
|
|
18103
|
+
if (data.origin.inputName === targetInputName) return false;
|
|
18104
|
+
onChange(data.origin.inputName, "", false);
|
|
18105
|
+
}
|
|
18106
|
+
onChange(targetInputName, data.optionName, true, dropSourceId(data.optionName));
|
|
18107
|
+
return true;
|
|
18108
|
+
}
|
|
18109
|
+
const ACCEPTED_DRAG_TYPES = ["rich-drag-option"];
|
|
18110
|
+
const DropInput = ({
|
|
18111
|
+
widgetId,
|
|
18112
|
+
value,
|
|
18113
|
+
width,
|
|
18114
|
+
placeholder,
|
|
18115
|
+
validationStatus = null,
|
|
18116
|
+
option,
|
|
18117
|
+
widgets = {},
|
|
18118
|
+
onChange = () => {
|
|
18119
|
+
}
|
|
18120
|
+
}) => {
|
|
18121
|
+
const locked = useExerciseLocked();
|
|
18122
|
+
const dnd = useExerciseDndOptional();
|
|
18123
|
+
const isEmpty = value === "";
|
|
18124
|
+
const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
|
|
18125
|
+
const hasChip = !isEmpty && Boolean(option) && Boolean(dnd);
|
|
18126
|
+
const boxStyle = width !== void 0 ? { width: getInputBoxWidth(width) } : void 0;
|
|
18127
|
+
const handleDrop = React.useCallback(
|
|
18128
|
+
(item) => applyRichDropToInput(widgetId, item, onChange),
|
|
18129
|
+
[widgetId, onChange]
|
|
18130
|
+
);
|
|
18131
|
+
const handleRemove = React.useCallback(
|
|
18132
|
+
() => onChange(widgetId, "", false),
|
|
18133
|
+
[widgetId, onChange]
|
|
18134
|
+
);
|
|
18135
|
+
const placeholderText = placeholder ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "drop-input-placeholder", children: placeholder }) : null;
|
|
18136
|
+
const input = /* @__PURE__ */ jsxRuntime.jsx(
|
|
18137
|
+
"div",
|
|
18138
|
+
{
|
|
18139
|
+
className: `inline-widget drop-input${hasChip ? " drop-input--filled" : ` inline-input${statusClass}`}${isEmpty ? " drop-input--empty" : ""}${locked ? " drop-input--locked" : ""}`,
|
|
18140
|
+
style: hasChip ? void 0 : boxStyle,
|
|
18141
|
+
role: "group",
|
|
18142
|
+
"aria-label": placeholder ?? widgetId,
|
|
18143
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
18144
|
+
"data-widget-id": widgetId,
|
|
18145
|
+
children: isEmpty ? placeholderText : option ? dnd ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
18146
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-input drop-input-sizer", style: boxStyle, "aria-hidden": "true", children: placeholderText }),
|
|
18147
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
18148
|
+
PlacedRichOption,
|
|
18149
|
+
{
|
|
18150
|
+
inputName: widgetId,
|
|
18151
|
+
option,
|
|
18152
|
+
widgets,
|
|
18153
|
+
onRemove: handleRemove,
|
|
18154
|
+
className: statusClass.trim()
|
|
18155
|
+
}
|
|
18156
|
+
)
|
|
18157
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(RichOptionContent, { content: option.content, widgets }) : dnd && !locked ? (
|
|
18158
|
+
// Value that resolves to no bank option (author answer/option-name
|
|
18159
|
+
// mismatch, legacy JSON). Keep it clearable so the field is not stuck.
|
|
18160
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
18161
|
+
"button",
|
|
18162
|
+
{
|
|
18163
|
+
type: "button",
|
|
18164
|
+
className: "drop-input-unresolved",
|
|
18165
|
+
onClick: (event) => {
|
|
18166
|
+
if (event.detail !== 0) handleRemove();
|
|
18167
|
+
},
|
|
18168
|
+
title: "Click to clear",
|
|
18169
|
+
children: value
|
|
18170
|
+
}
|
|
18171
|
+
)
|
|
18172
|
+
) : value
|
|
18173
|
+
}
|
|
18174
|
+
);
|
|
18175
|
+
if (!dnd) return input;
|
|
18176
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
18177
|
+
Droppable,
|
|
18178
|
+
{
|
|
18179
|
+
id: widgetId,
|
|
18180
|
+
accepts: ACCEPTED_DRAG_TYPES,
|
|
18181
|
+
onDrop: handleDrop,
|
|
18182
|
+
disabled: locked,
|
|
18183
|
+
className: "drop-input-droppable",
|
|
18184
|
+
children: input
|
|
18185
|
+
}
|
|
18186
|
+
);
|
|
18187
|
+
};
|
|
18188
|
+
const NumericKeypad = ({
|
|
18189
|
+
value,
|
|
18190
|
+
includeOperators = false,
|
|
18191
|
+
includeDecimal = false,
|
|
18192
|
+
onChange
|
|
18193
|
+
}) => {
|
|
18194
|
+
const playSound = useSound();
|
|
18195
|
+
const locked = useExerciseLocked();
|
|
18196
|
+
const handleDigit = (digit) => {
|
|
18197
|
+
if (locked) return;
|
|
18198
|
+
playSound("keypad-press");
|
|
18199
|
+
onChange(value + digit);
|
|
18200
|
+
};
|
|
18201
|
+
const handleBackspace = () => {
|
|
18202
|
+
if (locked) return;
|
|
18203
|
+
playSound("keypad-press");
|
|
18204
|
+
onChange(value.slice(0, -1));
|
|
18205
|
+
};
|
|
18206
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, "data-tutorial": "keypad", children: [
|
|
18207
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "1", onClick: () => handleDigit("1"), children: "1" }),
|
|
18208
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "2", onClick: () => handleDigit("2"), children: "2" }),
|
|
18209
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "3", onClick: () => handleDigit("3"), children: "3" }),
|
|
18210
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "+", onClick: () => handleDigit("+"), children: "+" }),
|
|
18211
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "4", onClick: () => handleDigit("4"), children: "4" }),
|
|
18212
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "5", onClick: () => handleDigit("5"), children: "5" }),
|
|
18213
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "6", onClick: () => handleDigit("6"), children: "6" }),
|
|
18214
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "-", onClick: () => handleDigit("-"), children: "-" }),
|
|
18215
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "7", onClick: () => handleDigit("7"), children: "7" }),
|
|
18216
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "8", onClick: () => handleDigit("8"), children: "8" }),
|
|
18217
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "9", onClick: () => handleDigit("9"), children: "9" }),
|
|
18218
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "×", onClick: () => handleDigit("×"), children: "×" }),
|
|
18219
|
+
includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", "data-key": ",", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
|
|
18220
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "0", onClick: () => handleDigit("0"), children: "0" }),
|
|
18221
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", "data-key": "backspace", onClick: handleBackspace, children: "⌫" }),
|
|
18222
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "÷", onClick: () => handleDigit("÷"), children: "÷" })
|
|
18223
|
+
] });
|
|
17478
18224
|
};
|
|
17479
18225
|
const FractionInput = ({
|
|
17480
18226
|
widgetId,
|
|
@@ -19277,41 +20023,6 @@ const MatcherWidget = ({
|
|
|
19277
20023
|
);
|
|
19278
20024
|
};
|
|
19279
20025
|
MatcherWidget.displayName = "MatcherWidget";
|
|
19280
|
-
function isArabic(text) {
|
|
19281
|
-
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
19282
|
-
}
|
|
19283
|
-
function hasLatinChars(text) {
|
|
19284
|
-
return /[A-Za-z]/.test(text);
|
|
19285
|
-
}
|
|
19286
|
-
function itemContainsArabic(item) {
|
|
19287
|
-
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
19288
|
-
return isArabic(item.value);
|
|
19289
|
-
}
|
|
19290
|
-
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
19291
|
-
return isArabic(item.value);
|
|
19292
|
-
}
|
|
19293
|
-
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
19294
|
-
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
19295
|
-
}
|
|
19296
|
-
return false;
|
|
19297
|
-
}
|
|
19298
|
-
function itemContainsLatin(item) {
|
|
19299
|
-
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
19300
|
-
return hasLatinChars(item.value);
|
|
19301
|
-
}
|
|
19302
|
-
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
19303
|
-
return hasLatinChars(item.value);
|
|
19304
|
-
}
|
|
19305
|
-
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
19306
|
-
return item.items.some((subItem) => itemContainsLatin(subItem));
|
|
19307
|
-
}
|
|
19308
|
-
return false;
|
|
19309
|
-
}
|
|
19310
|
-
function shouldItemsUseRTL(items) {
|
|
19311
|
-
const hasArabic = items.some(itemContainsArabic);
|
|
19312
|
-
const hasLatin = items.some(itemContainsLatin);
|
|
19313
|
-
return hasArabic && !hasLatin;
|
|
19314
|
-
}
|
|
19315
20026
|
function usePairLines({
|
|
19316
20027
|
containerRef,
|
|
19317
20028
|
pairs,
|
|
@@ -19725,9 +20436,15 @@ PairMatcherWidget.displayName = "PairMatcherWidget";
|
|
|
19725
20436
|
function isInlineInputWidget(widget) {
|
|
19726
20437
|
return widget.type === "inline-input";
|
|
19727
20438
|
}
|
|
20439
|
+
function isDropInputWidget(widget) {
|
|
20440
|
+
return widget.type === "drop-input";
|
|
20441
|
+
}
|
|
19728
20442
|
function isDragOptionsWidget(widget) {
|
|
19729
20443
|
return widget.type === "drag-options";
|
|
19730
20444
|
}
|
|
20445
|
+
function isDraggableOptionsWidget(widget) {
|
|
20446
|
+
return widget.type === "draggable-options";
|
|
20447
|
+
}
|
|
19731
20448
|
function isInlineDragOptionWidget(widget) {
|
|
19732
20449
|
return widget.type === "inline-drag-option";
|
|
19733
20450
|
}
|
|
@@ -19756,7 +20473,7 @@ function isGeometryWidget(widget) {
|
|
|
19756
20473
|
return widget.type === JSON_TYPES.GEOMETRY;
|
|
19757
20474
|
}
|
|
19758
20475
|
const GeometryWidget = React.lazy(async () => ({
|
|
19759
|
-
default: (await Promise.resolve().then(() => require("./GeometryWidget-
|
|
20476
|
+
default: (await Promise.resolve().then(() => require("./GeometryWidget-CTTLw871.js"))).GeometryWidget
|
|
19760
20477
|
}));
|
|
19761
20478
|
const WidgetRenderer = ({
|
|
19762
20479
|
widget,
|
|
@@ -19794,6 +20511,23 @@ const WidgetRenderer = ({
|
|
|
19794
20511
|
}
|
|
19795
20512
|
);
|
|
19796
20513
|
}
|
|
20514
|
+
if (isDropInputWidget(widget)) {
|
|
20515
|
+
const optionsWidget = widgets?.[DRAGGABLE_OPTIONS_WIDGET_ID];
|
|
20516
|
+
const option = optionsWidget && isDraggableOptionsWidget(optionsWidget) ? optionsWidget.options.find((candidate) => candidate.name === strValue) : void 0;
|
|
20517
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
20518
|
+
DropInput,
|
|
20519
|
+
{
|
|
20520
|
+
widgetId,
|
|
20521
|
+
value: strValue,
|
|
20522
|
+
width: widget.width,
|
|
20523
|
+
placeholder: widget.placeholder,
|
|
20524
|
+
validationStatus,
|
|
20525
|
+
option,
|
|
20526
|
+
widgets,
|
|
20527
|
+
onChange
|
|
20528
|
+
}
|
|
20529
|
+
);
|
|
20530
|
+
}
|
|
19797
20531
|
if (isDragOptionsWidget(widget)) {
|
|
19798
20532
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
19799
20533
|
DragOptions,
|
|
@@ -19804,6 +20538,25 @@ const WidgetRenderer = ({
|
|
|
19804
20538
|
}
|
|
19805
20539
|
);
|
|
19806
20540
|
}
|
|
20541
|
+
if (isDraggableOptionsWidget(widget)) {
|
|
20542
|
+
const usedOptionNames = /* @__PURE__ */ new Set();
|
|
20543
|
+
if (!widget.reusable) {
|
|
20544
|
+
for (const [id, candidate] of Object.entries(widgets ?? {})) {
|
|
20545
|
+
const candidateValue = values[id];
|
|
20546
|
+
if (isDropInputWidget(candidate) && typeof candidateValue === "string" && candidateValue !== "") {
|
|
20547
|
+
usedOptionNames.add(candidateValue);
|
|
20548
|
+
}
|
|
20549
|
+
}
|
|
20550
|
+
}
|
|
20551
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
20552
|
+
DraggableOptions,
|
|
20553
|
+
{
|
|
20554
|
+
widget,
|
|
20555
|
+
widgets: widgets ?? { [widgetId]: widget },
|
|
20556
|
+
usedOptionNames
|
|
20557
|
+
}
|
|
20558
|
+
);
|
|
20559
|
+
}
|
|
19807
20560
|
if (isInlineDragOptionWidget(widget)) {
|
|
19808
20561
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
19809
20562
|
InlineDragOption,
|
|
@@ -20145,75 +20898,14 @@ const ImageItem = ({
|
|
|
20145
20898
|
ref: triggerRef,
|
|
20146
20899
|
type: "button",
|
|
20147
20900
|
className: "image-zoom-btn",
|
|
20148
|
-
"aria-label": alt ? `View "${alt}" fullscreen` : "View image fullscreen",
|
|
20149
|
-
onClick: open,
|
|
20150
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(ExpandDiamondIcon, {})
|
|
20151
|
-
}
|
|
20152
|
-
),
|
|
20153
|
-
overlay
|
|
20154
|
-
] });
|
|
20155
|
-
};
|
|
20156
|
-
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
20157
|
-
const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
|
|
20158
|
-
const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
|
|
20159
|
-
const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
|
|
20160
|
-
const NUMBER_LINE_AXIS_ENDS = {
|
|
20161
|
-
"arrow-right": { left: "tick", right: "arrow" },
|
|
20162
|
-
"arrow-left": { left: "arrow", right: "tick" },
|
|
20163
|
-
"arrow-both": { left: "arrow", right: "arrow" },
|
|
20164
|
-
"line": { left: "line", right: "line" },
|
|
20165
|
-
"line-left": { left: "line", right: "arrow" }
|
|
20166
|
-
};
|
|
20167
|
-
const toPercent = (value, min, max) => {
|
|
20168
|
-
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
20169
|
-
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
20170
|
-
};
|
|
20171
|
-
const pixelToValue = (chipCenterX, axisLeft, axisWidth, min, max) => {
|
|
20172
|
-
const relX = (chipCenterX - axisLeft) / axisWidth;
|
|
20173
|
-
const fraction = (relX - SIDE_PAD / 100) / (1 - 2 * SIDE_PAD / 100);
|
|
20174
|
-
const clamped = Math.max(0, Math.min(1, fraction));
|
|
20175
|
-
return min + clamped * (max - min);
|
|
20176
|
-
};
|
|
20177
|
-
const getMajorTicks = (min, max, step) => {
|
|
20178
|
-
if (step <= 0 || max <= min) return [min, max];
|
|
20179
|
-
const count = Math.floor((max - min) / step);
|
|
20180
|
-
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
20181
|
-
const lastTick = ticks[ticks.length - 1];
|
|
20182
|
-
if (roundTick(lastTick) !== roundTick(max)) {
|
|
20183
|
-
ticks.push(max);
|
|
20184
|
-
} else {
|
|
20185
|
-
ticks[ticks.length - 1] = max;
|
|
20186
|
-
}
|
|
20187
|
-
return ticks;
|
|
20188
|
-
};
|
|
20189
|
-
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
20190
|
-
if (minorTicks <= 0 || majorTicks.length < 2) return [];
|
|
20191
|
-
const result = [];
|
|
20192
|
-
for (let i = 0; i < majorTicks.length - 1; i++) {
|
|
20193
|
-
const from = majorTicks[i];
|
|
20194
|
-
const to = majorTicks[i + 1];
|
|
20195
|
-
const interval = to - from;
|
|
20196
|
-
for (let j = 1; j <= minorTicks; j++) {
|
|
20197
|
-
result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
|
|
20198
|
-
}
|
|
20199
|
-
}
|
|
20200
|
-
return result;
|
|
20201
|
-
};
|
|
20202
|
-
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
20203
|
-
...majorTicks,
|
|
20204
|
-
...getMinorTickValues(majorTicks, minorTicks)
|
|
20205
|
-
];
|
|
20206
|
-
const snapToNearest = (raw, ticks) => {
|
|
20207
|
-
if (ticks.length === 0) return cleanFloat(raw);
|
|
20208
|
-
return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
|
|
20209
|
-
};
|
|
20210
|
-
const getPositionWidgetId = (content) => {
|
|
20211
|
-
for (const item of content) {
|
|
20212
|
-
if (isWidgetRef(item)) return item.widgetId;
|
|
20213
|
-
}
|
|
20214
|
-
return null;
|
|
20901
|
+
"aria-label": alt ? `View "${alt}" fullscreen` : "View image fullscreen",
|
|
20902
|
+
onClick: open,
|
|
20903
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ExpandDiamondIcon, {})
|
|
20904
|
+
}
|
|
20905
|
+
),
|
|
20906
|
+
overlay
|
|
20907
|
+
] });
|
|
20215
20908
|
};
|
|
20216
|
-
const toHighlightColor = (color) => color.startsWith("#") ? color : `var(--nl-highlight-${color})`;
|
|
20217
20909
|
const getBandGeometry = (h, min, max) => {
|
|
20218
20910
|
const leftPct = toPercent(Math.min(h.from, h.to), min, max);
|
|
20219
20911
|
const rightPct = toPercent(Math.max(h.from, h.to), min, max);
|
|
@@ -21670,63 +22362,6 @@ const ClockRenderer = ({
|
|
|
21670
22362
|
format === "12h" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "clock-period-badge", children: hours >= 12 ? "PM" : "AM" })
|
|
21671
22363
|
] });
|
|
21672
22364
|
};
|
|
21673
|
-
function isTextItem(item) {
|
|
21674
|
-
return item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.BOLD_TEXT || item.type === JSON_TYPES.COLOR_TEXT || item.type === JSON_TYPES.OPERATOR || item.type === JSON_TYPES.SPACE || item.type === JSON_TYPES.GROUP;
|
|
21675
|
-
}
|
|
21676
|
-
function groupLineItems(items) {
|
|
21677
|
-
const groups = [];
|
|
21678
|
-
let currentTextGroup = [];
|
|
21679
|
-
for (const item of items) {
|
|
21680
|
-
if (isTextItem(item)) {
|
|
21681
|
-
currentTextGroup.push(item);
|
|
21682
|
-
} else {
|
|
21683
|
-
if (currentTextGroup.length > 0) {
|
|
21684
|
-
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
21685
|
-
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
21686
|
-
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
21687
|
-
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
21688
|
-
currentTextGroup = [];
|
|
21689
|
-
}
|
|
21690
|
-
groups.push({ items: [item], isTextGroup: false, hasArabic: false, hasLatin: false, shouldApplyRTL: false });
|
|
21691
|
-
}
|
|
21692
|
-
}
|
|
21693
|
-
if (currentTextGroup.length > 0) {
|
|
21694
|
-
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
21695
|
-
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
21696
|
-
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
21697
|
-
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
21698
|
-
}
|
|
21699
|
-
return groups;
|
|
21700
|
-
}
|
|
21701
|
-
function placeTableCells(rows, cols) {
|
|
21702
|
-
const placements = /* @__PURE__ */ new Map();
|
|
21703
|
-
const occupied = rows.map(() => []);
|
|
21704
|
-
const totalRows = rows.length;
|
|
21705
|
-
rows.forEach((row, rowIndex) => {
|
|
21706
|
-
const taken = occupied[rowIndex] ?? [];
|
|
21707
|
-
let col = 0;
|
|
21708
|
-
for (const cell of row.cells) {
|
|
21709
|
-
while (taken[col]) col++;
|
|
21710
|
-
const colspan = cell.colspan || 1;
|
|
21711
|
-
const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
|
|
21712
|
-
for (let r = rowIndex; r < rowIndex + rowspan; r++) {
|
|
21713
|
-
const line = occupied[r];
|
|
21714
|
-
if (!line) continue;
|
|
21715
|
-
for (let c = col; c < col + colspan; c++) line[c] = true;
|
|
21716
|
-
}
|
|
21717
|
-
placements.set(cell.id, {
|
|
21718
|
-
column: col + 1,
|
|
21719
|
-
row: rowIndex + 1,
|
|
21720
|
-
colspan,
|
|
21721
|
-
rowspan,
|
|
21722
|
-
isLastColumn: col + colspan >= cols,
|
|
21723
|
-
isLastRow: rowIndex + rowspan >= totalRows
|
|
21724
|
-
});
|
|
21725
|
-
col += colspan;
|
|
21726
|
-
}
|
|
21727
|
-
});
|
|
21728
|
-
return placements;
|
|
21729
|
-
}
|
|
21730
22365
|
const toValidationStatus = (v) => v === true ? "correct" : v === false ? "incorrect" : null;
|
|
21731
22366
|
const isNumericText = (value) => /^[\d.,\s]+$/.test(value);
|
|
21732
22367
|
const numberClass = (value) => isNumericText(value) ? "text-item-number" : "";
|
|
@@ -22174,9 +22809,11 @@ function ChameleonHand({ className }) {
|
|
|
22174
22809
|
);
|
|
22175
22810
|
}
|
|
22176
22811
|
const TUTORIAL_HAND_TIP = { x: 56 / 75, y: 22 / 78 };
|
|
22812
|
+
const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
22177
22813
|
const CYCLE_MS = 1400;
|
|
22178
22814
|
const PRESS_AT_MS = 650;
|
|
22179
22815
|
const PRESS_HOLD_MS = 220;
|
|
22816
|
+
const MAX_SPEECH_MS = 3e4;
|
|
22180
22817
|
const HOLE_PAD = 10;
|
|
22181
22818
|
const HOLE_RADIUS = 18;
|
|
22182
22819
|
const HAND_W = 72;
|
|
@@ -22194,7 +22831,6 @@ function toRect(t) {
|
|
|
22194
22831
|
if (r.width <= 0 || r.height <= 0) return null;
|
|
22195
22832
|
return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) };
|
|
22196
22833
|
}
|
|
22197
|
-
const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
22198
22834
|
const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
|
|
22199
22835
|
const { steps } = tutorial;
|
|
22200
22836
|
const [stepIndex, setStepIndex] = React.useState(
|
|
@@ -22270,7 +22906,9 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
|
|
|
22270
22906
|
const [completedStep, setCompletedStep] = React.useState(null);
|
|
22271
22907
|
React.useEffect(() => {
|
|
22272
22908
|
optionsRef.current.onStep?.(stepIndex, step.id);
|
|
22273
|
-
const speech = Promise
|
|
22909
|
+
const speech = new Promise((resolve2) => {
|
|
22910
|
+
resolve2(optionsRef.current.onSpeak?.(step.id, textRef.current));
|
|
22911
|
+
}).catch(() => {
|
|
22274
22912
|
});
|
|
22275
22913
|
if (!autoAdvance) return;
|
|
22276
22914
|
const duration = Math.max(2, step.taps(ctxRef.current).length) * CYCLE_MS;
|
|
@@ -22279,7 +22917,11 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
|
|
|
22279
22917
|
const animation = new Promise((resolve2) => {
|
|
22280
22918
|
timer = setTimeout(resolve2, duration);
|
|
22281
22919
|
});
|
|
22282
|
-
|
|
22920
|
+
let speechTimer;
|
|
22921
|
+
const speechLimit = new Promise((resolve2) => {
|
|
22922
|
+
speechTimer = setTimeout(resolve2, MAX_SPEECH_MS);
|
|
22923
|
+
});
|
|
22924
|
+
void Promise.all([Promise.race([speech, speechLimit]), animation]).then(() => {
|
|
22283
22925
|
if (cancelled) return;
|
|
22284
22926
|
if (isLast) {
|
|
22285
22927
|
if (showBubble) setCompletedStep(stepIndex);
|
|
@@ -22289,6 +22931,7 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
|
|
|
22289
22931
|
return () => {
|
|
22290
22932
|
cancelled = true;
|
|
22291
22933
|
clearTimeout(timer);
|
|
22934
|
+
clearTimeout(speechTimer);
|
|
22292
22935
|
};
|
|
22293
22936
|
}, [stepIndex, step, autoAdvance, isLast, showBubble]);
|
|
22294
22937
|
const showNext = !autoAdvance || isLast && completedStep === stepIndex;
|
|
@@ -22444,7 +23087,7 @@ function key(ctx, k) {
|
|
|
22444
23087
|
}
|
|
22445
23088
|
function answerBox(ctx) {
|
|
22446
23089
|
const inputs = getTextInputs(ctx.root);
|
|
22447
|
-
return inputs.find(canTakeInput) ?? inputs
|
|
23090
|
+
return inputs.find(canTakeInput) ?? inputs.find(canAdvanceTo) ?? null;
|
|
22448
23091
|
}
|
|
22449
23092
|
function answerDigits(ctx) {
|
|
22450
23093
|
const id = answerBox(ctx)?.id.replace(/-input$/, "");
|
|
@@ -22481,7 +23124,6 @@ const StandInFill = ({ box, slots, filled, digits }) => /* @__PURE__ */ jsxRunti
|
|
|
22481
23124
|
children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? digits?.[i] ?? "●" : "_" }, i))
|
|
22482
23125
|
}
|
|
22483
23126
|
);
|
|
22484
|
-
const center = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
22485
23127
|
const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
|
|
22486
23128
|
const box = spotlight[1];
|
|
22487
23129
|
if (!box) return null;
|
|
@@ -22491,8 +23133,8 @@ const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
|
|
|
22491
23133
|
const from = tapCount > 0 ? taps[(tapCount - 1) % taps.length] : null;
|
|
22492
23134
|
let arc = null;
|
|
22493
23135
|
if (from) {
|
|
22494
|
-
const a =
|
|
22495
|
-
const b =
|
|
23136
|
+
const a = centerOf(from);
|
|
23137
|
+
const b = centerOf(box);
|
|
22496
23138
|
const lift = Math.max(40, Math.hypot(b.x - a.x, b.y - a.y) * 0.35);
|
|
22497
23139
|
arc = `M ${a.x} ${a.y} Q ${(a.x + b.x) / 2} ${Math.min(a.y, b.y) - lift} ${b.x} ${b.y}`;
|
|
22498
23140
|
}
|
|
@@ -22555,29 +23197,72 @@ const WIDGET_TUTORIALS = [keypadTutorial];
|
|
|
22555
23197
|
function findWidgetTutorial(json) {
|
|
22556
23198
|
return WIDGET_TUTORIALS.find((t) => t.appliesTo(json)) ?? null;
|
|
22557
23199
|
}
|
|
23200
|
+
function hasMatchingLength(value, expected) {
|
|
23201
|
+
return (typeof expected === "string" || typeof expected === "number") && (typeof value === "string" || typeof value === "number") && String(value).length > 0 && String(value).length === String(expected).length;
|
|
23202
|
+
}
|
|
23203
|
+
function isSelectionGroups(value) {
|
|
23204
|
+
return Array.isArray(value) && value.every(
|
|
23205
|
+
(group) => Array.isArray(group) && group.length > 0 && group.every((id) => typeof id === "string")
|
|
23206
|
+
);
|
|
23207
|
+
}
|
|
23208
|
+
function hasMatchingSelectionCount(value, expected, itemIds, pairsOnly) {
|
|
23209
|
+
if (!isSelectionGroups(value) || !isSelectionGroups(expected)) return false;
|
|
23210
|
+
if (pairsOnly && (value.some((group) => group.length !== 2) || expected.some((group) => group.length !== 2)))
|
|
23211
|
+
return false;
|
|
23212
|
+
const selected = value.flat();
|
|
23213
|
+
const requiredCount = expected.flat().length;
|
|
23214
|
+
const declared = new Set(itemIds);
|
|
23215
|
+
return requiredCount > 0 && selected.length === requiredCount && new Set(selected).size === selected.length && selected.every((id) => declared.has(id));
|
|
23216
|
+
}
|
|
22558
23217
|
function getAnswerLengthCompletion(json, answerable, values, touched) {
|
|
22559
23218
|
const fields = [];
|
|
23219
|
+
const addTypedField = (id, widgetId, expected) => {
|
|
23220
|
+
const value = values[id];
|
|
23221
|
+
const hasKnownLength = typeof expected === "string" || typeof expected === "number";
|
|
23222
|
+
const hasScalarValue = typeof value === "string" || typeof value === "number";
|
|
23223
|
+
fields.push({
|
|
23224
|
+
id,
|
|
23225
|
+
widgetId,
|
|
23226
|
+
matches: hasMatchingLength(value, expected),
|
|
23227
|
+
// Unknown answer lengths use normal completion; known ones use grading's normalization.
|
|
23228
|
+
guideMatches: !hasKnownLength || touched.has(widgetId) && hasScalarValue && hasMatchingLength(normalize(value), normalize(expected))
|
|
23229
|
+
});
|
|
23230
|
+
};
|
|
22560
23231
|
for (const widgetId of answerable) {
|
|
22561
23232
|
const widget = json.widgets[widgetId];
|
|
22562
23233
|
const expected = json.validation?.answers?.[widgetId];
|
|
22563
23234
|
if (widget?.type === JSON_TYPES.INLINE_INPUT && !widget.disabled) {
|
|
22564
|
-
|
|
23235
|
+
addTypedField(widgetId, widgetId, expected);
|
|
22565
23236
|
} else if (widget?.type === JSON_TYPES.FRACTION) {
|
|
22566
23237
|
const parts = typeof expected === "string" ? expected.split("/") : [];
|
|
22567
23238
|
const validParts = parts.length === 2;
|
|
22568
23239
|
if (widget.numerator === null) {
|
|
22569
|
-
|
|
23240
|
+
const id = getFractionKey(widgetId, "num");
|
|
23241
|
+
addTypedField(id, widgetId, validParts ? parts[0] : void 0);
|
|
22570
23242
|
}
|
|
22571
23243
|
if (widget.denominator === null) {
|
|
22572
|
-
|
|
23244
|
+
const id = getFractionKey(widgetId, "den");
|
|
23245
|
+
addTypedField(id, widgetId, validParts ? parts[1] : void 0);
|
|
22573
23246
|
}
|
|
23247
|
+
} else if (widget?.type === JSON_TYPES.MATCHER || widget?.type === JSON_TYPES.PAIR_MATCHER) {
|
|
23248
|
+
const itemIds = widget.type === JSON_TYPES.MATCHER ? widget.rows.flatMap((row) => row.items.map((item) => item.itemId)) : widget.groups.flatMap((group) => group.map((item) => item.itemId));
|
|
23249
|
+
const matches = hasMatchingSelectionCount(
|
|
23250
|
+
values[widgetId],
|
|
23251
|
+
expected,
|
|
23252
|
+
itemIds,
|
|
23253
|
+
widget.type === JSON_TYPES.PAIR_MATCHER
|
|
23254
|
+
);
|
|
23255
|
+
fields.push({
|
|
23256
|
+
id: widgetId,
|
|
23257
|
+
widgetId,
|
|
23258
|
+
matches,
|
|
23259
|
+
guideMatches: expected == null || touched.has(widgetId) && matches
|
|
23260
|
+
});
|
|
22574
23261
|
}
|
|
22575
23262
|
}
|
|
22576
|
-
const answerLengthMatchedIds = fields.filter(({
|
|
22577
|
-
const value = values[id];
|
|
22578
|
-
return touched.has(widgetId) && (typeof expected === "string" || typeof expected === "number") && (typeof value === "string" || typeof value === "number") && String(value).length > 0 && String(value).length === String(expected).length;
|
|
22579
|
-
}).map(({ id }) => id);
|
|
23263
|
+
const answerLengthMatchedIds = fields.filter(({ widgetId, matches }) => touched.has(widgetId) && matches).map(({ id }) => id);
|
|
22580
23264
|
return {
|
|
23265
|
+
areSubmitGuideCountsMatched: fields.every(({ guideMatches }) => guideMatches),
|
|
22581
23266
|
isAnswerLengthMatched: fields.length > 0 && answerLengthMatchedIds.length === fields.length,
|
|
22582
23267
|
answerLengthMatchedIds
|
|
22583
23268
|
};
|
|
@@ -22603,12 +23288,9 @@ function buildInitialValues(json) {
|
|
|
22603
23288
|
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) {
|
|
22604
23289
|
initial[id] = [];
|
|
22605
23290
|
} else if (widget.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
22606
|
-
initial[id] = widget.cells.filter(
|
|
22607
|
-
cell,
|
|
22608
|
-
|
|
22609
|
-
widget.cols,
|
|
22610
|
-
widget.interactiveArea
|
|
22611
|
-
));
|
|
23291
|
+
initial[id] = widget.cells.filter(
|
|
23292
|
+
(cell) => isCellInInteractiveArea(cell, widget.rows, widget.cols, widget.interactiveArea)
|
|
23293
|
+
);
|
|
22612
23294
|
} else if (widget.type === JSON_TYPES.CHART_BAR || widget.type === JSON_TYPES.CHART_PICTO) {
|
|
22613
23295
|
initial[id] = String(widget.value);
|
|
22614
23296
|
} else if (widget.type === JSON_TYPES.GEOMETRY) {
|
|
@@ -22647,8 +23329,10 @@ function buildRevealedValues(json) {
|
|
|
22647
23329
|
const widget = json.widgets[widgetId];
|
|
22648
23330
|
if (widget?.type === JSON_TYPES.FRACTION && typeof expected === "string") {
|
|
22649
23331
|
const [num, den] = expected.split("/", 2);
|
|
22650
|
-
if (widget.numerator === null && num !== void 0)
|
|
22651
|
-
|
|
23332
|
+
if (widget.numerator === null && num !== void 0)
|
|
23333
|
+
revealed[getFractionKey(widgetId, "num")] = num;
|
|
23334
|
+
if (widget.denominator === null && den !== void 0)
|
|
23335
|
+
revealed[getFractionKey(widgetId, "den")] = den;
|
|
22652
23336
|
continue;
|
|
22653
23337
|
}
|
|
22654
23338
|
revealed[widgetId] = typeof expected === "number" ? String(expected) : expected;
|
|
@@ -22680,449 +23364,571 @@ function buildRevealedDropSources(json, revealedValues) {
|
|
|
22680
23364
|
const pool = [];
|
|
22681
23365
|
const bank = json.widgets[DRAG_OPTIONS_WIDGET_ID];
|
|
22682
23366
|
if (bank?.type === JSON_TYPES.DRAG_OPTIONS) {
|
|
22683
|
-
bank.options.forEach(
|
|
23367
|
+
bank.options.forEach(
|
|
23368
|
+
(value, idx) => pool.push({ optionId: `drag-option-${idx}`, value: String(value) })
|
|
23369
|
+
);
|
|
22684
23370
|
}
|
|
22685
23371
|
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
22686
|
-
if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION)
|
|
23372
|
+
if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION)
|
|
23373
|
+
pool.push({ optionId: widgetId, value: widget.value });
|
|
22687
23374
|
}
|
|
22688
23375
|
const sources = /* @__PURE__ */ new Map();
|
|
22689
23376
|
if (pool.length === 0) return sources;
|
|
22690
23377
|
const used = /* @__PURE__ */ new Set();
|
|
22691
23378
|
for (const [inputId, value] of Object.entries(revealedValues)) {
|
|
22692
23379
|
if (typeof value !== "string" || value === "") continue;
|
|
22693
|
-
|
|
23380
|
+
if (json.widgets[inputId]?.type !== JSON_TYPES.INLINE_INPUT && !isFractionKey(inputId))
|
|
23381
|
+
continue;
|
|
23382
|
+
const match = pool.find(
|
|
23383
|
+
(o) => !used.has(o.optionId) && normalize(o.value) === normalize(value)
|
|
23384
|
+
);
|
|
22694
23385
|
if (!match) continue;
|
|
22695
23386
|
used.add(match.optionId);
|
|
22696
23387
|
sources.set(inputId, match.optionId);
|
|
22697
23388
|
}
|
|
22698
23389
|
return sources;
|
|
22699
23390
|
}
|
|
22700
|
-
const ExerciseRenderer = React.forwardRef(
|
|
22701
|
-
|
|
22702
|
-
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
|
|
22706
|
-
|
|
22707
|
-
|
|
22708
|
-
|
|
22709
|
-
|
|
22710
|
-
|
|
22711
|
-
|
|
22712
|
-
|
|
22713
|
-
|
|
22714
|
-
|
|
22715
|
-
|
|
22716
|
-
|
|
22717
|
-
|
|
22718
|
-
|
|
22719
|
-
|
|
22720
|
-
|
|
22721
|
-
|
|
22722
|
-
|
|
22723
|
-
|
|
22724
|
-
|
|
22725
|
-
|
|
22726
|
-
const
|
|
22727
|
-
|
|
22728
|
-
|
|
22729
|
-
|
|
22730
|
-
|
|
22731
|
-
|
|
22732
|
-
|
|
22733
|
-
|
|
22734
|
-
|
|
22735
|
-
|
|
22736
|
-
|
|
22737
|
-
|
|
22738
|
-
|
|
22739
|
-
|
|
22740
|
-
|
|
22741
|
-
|
|
22742
|
-
setResetKey((k) => k + 1);
|
|
22743
|
-
setLoadedValuesSignature(initialValuesSignature);
|
|
22744
|
-
touchedRef.current = /* @__PURE__ */ new Set();
|
|
22745
|
-
setTutorialRun(null);
|
|
22746
|
-
}, [json, initialValuesSignature, loadedValuesSignature]);
|
|
22747
|
-
const locked = validationDetails !== null || externallyLocked;
|
|
22748
|
-
const lastResultRef = React.useRef(null);
|
|
22749
|
-
const runSubmit = () => {
|
|
22750
|
-
if (locked) return lastResultRef.current ?? validate(json, values);
|
|
22751
|
-
const result = validate(json, values);
|
|
22752
|
-
playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
|
|
22753
|
-
const details = { ...result.details };
|
|
22754
|
-
const slotValidation = (id, raw, expected) => ({
|
|
22755
|
-
widgetId: id,
|
|
22756
|
-
userValue: typeof raw === "string" ? raw : void 0,
|
|
22757
|
-
isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
|
|
22758
|
-
});
|
|
22759
|
-
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
22760
|
-
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
22761
|
-
if (!(widgetId in details)) continue;
|
|
22762
|
-
const answer = json.validation?.answers?.[widgetId];
|
|
22763
|
-
if (!answer) continue;
|
|
22764
|
-
const [expectedNum, expectedDen] = answer.split("/", 2);
|
|
22765
|
-
if (!expectedNum || !expectedDen) continue;
|
|
22766
|
-
if (details[widgetId]?.isCorrect) {
|
|
22767
|
-
if (widget.numerator === null) details[`${widgetId}__num`] = { widgetId: `${widgetId}__num`, userValue: void 0, isCorrect: true };
|
|
22768
|
-
if (widget.denominator === null) details[`${widgetId}__den`] = { widgetId: `${widgetId}__den`, userValue: void 0, isCorrect: true };
|
|
22769
|
-
} else {
|
|
22770
|
-
if (widget.numerator === null) details[`${widgetId}__num`] = slotValidation(`${widgetId}__num`, values[`${widgetId}__num`], expectedNum);
|
|
22771
|
-
if (widget.denominator === null) details[`${widgetId}__den`] = slotValidation(`${widgetId}__den`, values[`${widgetId}__den`], expectedDen);
|
|
22772
|
-
}
|
|
22773
|
-
}
|
|
22774
|
-
setValidationDetails(details);
|
|
22775
|
-
onSubmit?.(values);
|
|
22776
|
-
lastResultRef.current = result;
|
|
22777
|
-
return result;
|
|
22778
|
-
};
|
|
22779
|
-
React.useImperativeHandle(ref, () => ({
|
|
22780
|
-
submit: runSubmit,
|
|
22781
|
-
getValues: () => values,
|
|
22782
|
-
getCompletion: () => {
|
|
22783
|
-
const answerable = getAnswerableInputs(json);
|
|
22784
|
-
const missingSet = new Set(getMissingInputs(json, valuesRef.current));
|
|
22785
|
-
for (const id of answerable) {
|
|
22786
|
-
if (!touchedRef.current.has(id)) missingSet.add(id);
|
|
22787
|
-
}
|
|
22788
|
-
const missing = [...missingSet];
|
|
22789
|
-
const filledIds = answerable.filter((id) => !missingSet.has(id));
|
|
22790
|
-
return {
|
|
22791
|
-
total: answerable.length,
|
|
22792
|
-
filled: filledIds.length,
|
|
22793
|
-
missing,
|
|
22794
|
-
filledIds,
|
|
22795
|
-
isComplete: answerable.length > 0 && missing.length === 0,
|
|
22796
|
-
isAnyFilled: filledIds.length > 0,
|
|
22797
|
-
...getAnswerLengthCompletion(json, answerable, valuesRef.current, touchedRef.current)
|
|
22798
|
-
};
|
|
22799
|
-
},
|
|
22800
|
-
reset: () => {
|
|
22801
|
-
const initial = buildInitialValues(json);
|
|
22802
|
-
valuesRef.current = initial;
|
|
22803
|
-
setValues(initial);
|
|
23391
|
+
const ExerciseRenderer = React.forwardRef(
|
|
23392
|
+
({
|
|
23393
|
+
json: suppliedJson,
|
|
23394
|
+
onSubmit,
|
|
23395
|
+
onChange,
|
|
23396
|
+
showSubmitButton = false,
|
|
23397
|
+
className = "",
|
|
23398
|
+
soundEnabled = true,
|
|
23399
|
+
sounds,
|
|
23400
|
+
theme
|
|
23401
|
+
}, ref) => {
|
|
23402
|
+
const json = React.useMemo(() => withDisabledPrefilledInputs(suppliedJson), [suppliedJson]);
|
|
23403
|
+
const initialValuesSignature = React.useMemo(() => buildInitialValuesSignature(json), [json]);
|
|
23404
|
+
const [values, setValues] = React.useState(
|
|
23405
|
+
() => buildInitialValues(json)
|
|
23406
|
+
);
|
|
23407
|
+
const valuesRef = React.useRef(values);
|
|
23408
|
+
valuesRef.current = values;
|
|
23409
|
+
const touchedRef = React.useRef(/* @__PURE__ */ new Set());
|
|
23410
|
+
const [loadedValuesSignature, setLoadedValuesSignature] = React.useState(initialValuesSignature);
|
|
23411
|
+
const [droppedSources, setDroppedSources] = React.useState(/* @__PURE__ */ new Map());
|
|
23412
|
+
const [focusedInputId, setFocusedInputId] = React.useState(null);
|
|
23413
|
+
const [validationDetails, setValidationDetails] = React.useState(null);
|
|
23414
|
+
const rootRef = React.useRef(null);
|
|
23415
|
+
const [resetKey, setResetKey] = React.useState(0);
|
|
23416
|
+
const [revealed, setRevealed] = React.useState(false);
|
|
23417
|
+
const tutorial = React.useMemo(() => findWidgetTutorial(json), [json]);
|
|
23418
|
+
const [tutorialRun, setTutorialRun] = React.useState(null);
|
|
23419
|
+
const themeStyle = React.useMemo(() => {
|
|
23420
|
+
const style = {};
|
|
23421
|
+
if (theme?.correct) style["--exercise-correct"] = theme.correct;
|
|
23422
|
+
if (theme?.incorrect) style["--exercise-incorrect"] = theme.incorrect;
|
|
23423
|
+
return Object.keys(style).length > 0 ? style : void 0;
|
|
23424
|
+
}, [theme?.correct, theme?.incorrect]);
|
|
23425
|
+
React.useEffect(() => {
|
|
23426
|
+
warnOnInvalidThemeColors({ correct: theme?.correct, incorrect: theme?.incorrect });
|
|
23427
|
+
}, [theme?.correct, theme?.incorrect]);
|
|
23428
|
+
const playSound = useSoundPlayer(soundEnabled, sounds);
|
|
23429
|
+
const externallyLocked = useExerciseLocked();
|
|
23430
|
+
React.useLayoutEffect(() => {
|
|
23431
|
+
if (loadedValuesSignature === initialValuesSignature) return;
|
|
23432
|
+
setValues(buildInitialValues(json));
|
|
22804
23433
|
setDroppedSources(/* @__PURE__ */ new Map());
|
|
22805
23434
|
setFocusedInputId(null);
|
|
22806
23435
|
setValidationDetails(null);
|
|
22807
23436
|
setResetKey((k) => k + 1);
|
|
22808
|
-
setRevealed(false);
|
|
22809
23437
|
setLoadedValuesSignature(initialValuesSignature);
|
|
22810
23438
|
touchedRef.current = /* @__PURE__ */ new Set();
|
|
22811
|
-
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
|
|
22817
|
-
|
|
22818
|
-
|
|
22819
|
-
|
|
22820
|
-
|
|
22821
|
-
|
|
23439
|
+
setTutorialRun(null);
|
|
23440
|
+
}, [json, initialValuesSignature, loadedValuesSignature]);
|
|
23441
|
+
const locked = validationDetails !== null || externallyLocked;
|
|
23442
|
+
const lastResultRef = React.useRef(null);
|
|
23443
|
+
const runSubmit = () => {
|
|
23444
|
+
if (locked) return lastResultRef.current ?? validate(json, values);
|
|
23445
|
+
const result = validate(json, values);
|
|
23446
|
+
playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
|
|
23447
|
+
const details = { ...result.details };
|
|
23448
|
+
const slotValidation = (id, raw, expected) => ({
|
|
23449
|
+
widgetId: id,
|
|
23450
|
+
userValue: typeof raw === "string" ? raw : void 0,
|
|
23451
|
+
isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
|
|
23452
|
+
});
|
|
23453
|
+
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
23454
|
+
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
23455
|
+
if (!(widgetId in details)) continue;
|
|
23456
|
+
const answer = json.validation?.answers?.[widgetId];
|
|
23457
|
+
if (!answer) continue;
|
|
23458
|
+
const [expectedNum, expectedDen] = answer.split("/", 2);
|
|
23459
|
+
if (!expectedNum || !expectedDen) continue;
|
|
23460
|
+
if (details[widgetId]?.isCorrect) {
|
|
23461
|
+
if (widget.numerator === null)
|
|
23462
|
+
details[`${widgetId}__num`] = {
|
|
23463
|
+
widgetId: `${widgetId}__num`,
|
|
23464
|
+
userValue: void 0,
|
|
23465
|
+
isCorrect: true
|
|
23466
|
+
};
|
|
23467
|
+
if (widget.denominator === null)
|
|
23468
|
+
details[`${widgetId}__den`] = {
|
|
23469
|
+
widgetId: `${widgetId}__den`,
|
|
23470
|
+
userValue: void 0,
|
|
23471
|
+
isCorrect: true
|
|
23472
|
+
};
|
|
22822
23473
|
} else {
|
|
22823
|
-
|
|
23474
|
+
if (widget.numerator === null)
|
|
23475
|
+
details[`${widgetId}__num`] = slotValidation(
|
|
23476
|
+
`${widgetId}__num`,
|
|
23477
|
+
values[`${widgetId}__num`],
|
|
23478
|
+
expectedNum
|
|
23479
|
+
);
|
|
23480
|
+
if (widget.denominator === null)
|
|
23481
|
+
details[`${widgetId}__den`] = slotValidation(
|
|
23482
|
+
`${widgetId}__den`,
|
|
23483
|
+
values[`${widgetId}__den`],
|
|
23484
|
+
expectedDen
|
|
23485
|
+
);
|
|
22824
23486
|
}
|
|
23487
|
+
}
|
|
23488
|
+
setValidationDetails(details);
|
|
23489
|
+
onSubmit?.(values);
|
|
23490
|
+
lastResultRef.current = result;
|
|
23491
|
+
return result;
|
|
23492
|
+
};
|
|
23493
|
+
React.useImperativeHandle(
|
|
23494
|
+
ref,
|
|
23495
|
+
() => ({
|
|
23496
|
+
submit: runSubmit,
|
|
23497
|
+
getValues: () => values,
|
|
23498
|
+
getCompletion: () => {
|
|
23499
|
+
const answerable = getAnswerableInputs(json);
|
|
23500
|
+
const missingSet = new Set(getMissingInputs(json, valuesRef.current));
|
|
23501
|
+
for (const id of answerable) {
|
|
23502
|
+
if (!touchedRef.current.has(id)) missingSet.add(id);
|
|
23503
|
+
}
|
|
23504
|
+
const missing = [...missingSet];
|
|
23505
|
+
const filledIds = answerable.filter((id) => !missingSet.has(id));
|
|
23506
|
+
const isComplete = answerable.length > 0 && missing.length === 0;
|
|
23507
|
+
const { areSubmitGuideCountsMatched, ...answerLengthCompletion } = getAnswerLengthCompletion(json, answerable, valuesRef.current, touchedRef.current);
|
|
23508
|
+
return {
|
|
23509
|
+
total: answerable.length,
|
|
23510
|
+
filled: filledIds.length,
|
|
23511
|
+
missing,
|
|
23512
|
+
filledIds,
|
|
23513
|
+
isComplete,
|
|
23514
|
+
isSubmitGuideReady: isComplete && areSubmitGuideCountsMatched,
|
|
23515
|
+
isAnyFilled: filledIds.length > 0,
|
|
23516
|
+
...answerLengthCompletion
|
|
23517
|
+
};
|
|
23518
|
+
},
|
|
23519
|
+
reset: () => {
|
|
23520
|
+
const initial = buildInitialValues(json);
|
|
23521
|
+
valuesRef.current = initial;
|
|
23522
|
+
setValues(initial);
|
|
23523
|
+
setDroppedSources(/* @__PURE__ */ new Map());
|
|
23524
|
+
setFocusedInputId(null);
|
|
23525
|
+
setValidationDetails(null);
|
|
23526
|
+
setResetKey((k) => k + 1);
|
|
23527
|
+
setRevealed(false);
|
|
23528
|
+
setLoadedValuesSignature(initialValuesSignature);
|
|
23529
|
+
touchedRef.current = /* @__PURE__ */ new Set();
|
|
23530
|
+
},
|
|
23531
|
+
revealAnswers: () => {
|
|
23532
|
+
const revealedValues = buildRevealedValues(json);
|
|
23533
|
+
if (Object.keys(revealedValues).length === 0) return;
|
|
23534
|
+
playSound("answer-reveal");
|
|
23535
|
+
const details = {};
|
|
23536
|
+
for (const [widgetId, userValue] of Object.entries(revealedValues)) {
|
|
23537
|
+
const widget = json.widgets[widgetId];
|
|
23538
|
+
if (widget?.type === JSON_TYPES.GEOMETRY) {
|
|
23539
|
+
const { isCorrect, result } = checkGeometryWidget(widget.json, userValue, widgetId);
|
|
23540
|
+
details[widgetId] = { widgetId, userValue, isCorrect, geometryFeedback: result };
|
|
23541
|
+
} else {
|
|
23542
|
+
details[widgetId] = { widgetId, userValue, isCorrect: true };
|
|
23543
|
+
}
|
|
23544
|
+
touchedRef.current.add(toBaseId(widgetId));
|
|
23545
|
+
}
|
|
23546
|
+
const next = { ...valuesRef.current, ...revealedValues };
|
|
23547
|
+
valuesRef.current = next;
|
|
23548
|
+
setValues(next);
|
|
23549
|
+
setDroppedSources(buildRevealedDropSources(json, revealedValues));
|
|
23550
|
+
setValidationDetails(details);
|
|
23551
|
+
setRevealed(true);
|
|
23552
|
+
setResetKey((k) => k + 1);
|
|
23553
|
+
},
|
|
23554
|
+
focusFirstInput: () => {
|
|
23555
|
+
const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
|
|
23556
|
+
if (!container) return;
|
|
23557
|
+
const candidates = getTextInputs(container);
|
|
23558
|
+
const target = candidates.find(canTakeInput) ?? // A locked exercise is read-only on purpose — nothing to focus until
|
|
23559
|
+
// reset(). Unlocked, a read-only field is not the lock, so refusing to
|
|
23560
|
+
// focus it would strand the student with no caret; take the first one
|
|
23561
|
+
// that isn't disabled or a chip's invisible SlotSizer.
|
|
23562
|
+
(locked ? void 0 : candidates.find(canAdvanceTo));
|
|
23563
|
+
target?.focus();
|
|
23564
|
+
},
|
|
23565
|
+
hasTutorial: () => tutorial !== null,
|
|
23566
|
+
tutorialId: () => tutorial?.id ?? null,
|
|
23567
|
+
startTutorial: (options = {}) => {
|
|
23568
|
+
if (!tutorial || !rootRef.current) return false;
|
|
23569
|
+
setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
|
|
23570
|
+
return true;
|
|
23571
|
+
},
|
|
23572
|
+
stopTutorial: () => setTutorialRun(null)
|
|
23573
|
+
}),
|
|
23574
|
+
[json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]
|
|
23575
|
+
);
|
|
23576
|
+
const focusNextInput = (currentKey) => {
|
|
23577
|
+
if (!rootRef.current) return;
|
|
23578
|
+
findNextInput(rootRef.current, currentKey)?.focus();
|
|
23579
|
+
};
|
|
23580
|
+
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
|
|
23581
|
+
if (locked) return;
|
|
23582
|
+
if (!silent) {
|
|
23583
|
+
setRevealed(false);
|
|
22825
23584
|
touchedRef.current.add(toBaseId(widgetId));
|
|
22826
23585
|
}
|
|
22827
|
-
const
|
|
23586
|
+
const previous = valuesRef.current[widgetId];
|
|
23587
|
+
const next = { ...valuesRef.current, [widgetId]: value };
|
|
22828
23588
|
valuesRef.current = next;
|
|
22829
23589
|
setValues(next);
|
|
22830
|
-
setDroppedSources(
|
|
22831
|
-
|
|
22832
|
-
|
|
22833
|
-
|
|
22834
|
-
|
|
22835
|
-
|
|
22836
|
-
|
|
22837
|
-
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
|
|
22841
|
-
|
|
22842
|
-
hasTutorial: () => tutorial !== null,
|
|
22843
|
-
tutorialId: () => tutorial?.id ?? null,
|
|
22844
|
-
startTutorial: (options = {}) => {
|
|
22845
|
-
if (!tutorial || !rootRef.current) return false;
|
|
22846
|
-
setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
|
|
22847
|
-
return true;
|
|
22848
|
-
},
|
|
22849
|
-
stopTutorial: () => setTutorialRun(null)
|
|
22850
|
-
}), [json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]);
|
|
22851
|
-
const focusNextInput = (currentKey) => {
|
|
22852
|
-
if (!rootRef.current) return;
|
|
22853
|
-
findNextInput(rootRef.current, currentKey)?.focus();
|
|
22854
|
-
};
|
|
22855
|
-
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
|
|
22856
|
-
if (locked) return;
|
|
22857
|
-
if (!silent) {
|
|
22858
|
-
setRevealed(false);
|
|
22859
|
-
touchedRef.current.add(toBaseId(widgetId));
|
|
22860
|
-
}
|
|
22861
|
-
const previous = valuesRef.current[widgetId];
|
|
22862
|
-
const next = { ...valuesRef.current, [widgetId]: value };
|
|
22863
|
-
valuesRef.current = next;
|
|
22864
|
-
setValues(next);
|
|
22865
|
-
setDroppedSources((prev) => {
|
|
22866
|
-
const next2 = new Map(prev);
|
|
22867
|
-
if (fromDrop && typeof value === "string" && value !== "" && sourceOptionId) {
|
|
22868
|
-
next2.set(widgetId, sourceOptionId);
|
|
22869
|
-
} else {
|
|
22870
|
-
next2.delete(widgetId);
|
|
22871
|
-
}
|
|
22872
|
-
return next2;
|
|
22873
|
-
});
|
|
22874
|
-
onChange?.(next);
|
|
22875
|
-
if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
|
|
22876
|
-
focusNextInput(widgetId);
|
|
22877
|
-
}
|
|
22878
|
-
};
|
|
22879
|
-
const handleSubmit = () => {
|
|
22880
|
-
runSubmit();
|
|
22881
|
-
};
|
|
22882
|
-
const lineGroups = React.useMemo(() => {
|
|
22883
|
-
const isRTLInlineWidget = (item) => {
|
|
22884
|
-
if (item.type === JSON_TYPES.WIDGET_REF) {
|
|
22885
|
-
const widget = json.widgets[item.widgetId];
|
|
22886
|
-
return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
|
|
23590
|
+
setDroppedSources((prev) => {
|
|
23591
|
+
const next2 = new Map(prev);
|
|
23592
|
+
if (fromDrop && typeof value === "string" && value !== "" && sourceOptionId) {
|
|
23593
|
+
next2.set(widgetId, sourceOptionId);
|
|
23594
|
+
} else {
|
|
23595
|
+
next2.delete(widgetId);
|
|
23596
|
+
}
|
|
23597
|
+
return next2;
|
|
23598
|
+
});
|
|
23599
|
+
onChange?.(next);
|
|
23600
|
+
if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
|
|
23601
|
+
focusNextInput(widgetId);
|
|
22887
23602
|
}
|
|
22888
|
-
return RTL_INLINE_WIDGET_TYPES.has(item.type);
|
|
22889
23603
|
};
|
|
22890
|
-
const
|
|
22891
|
-
|
|
22892
|
-
for (const item of json.layout) {
|
|
22893
|
-
if (item.type === JSON_TYPES.QUESTION_TEXT) {
|
|
22894
|
-
groups.push({
|
|
22895
|
-
items: [item],
|
|
22896
|
-
hasArabic: itemContainsArabic(item),
|
|
22897
|
-
hasRTLInlineWidget: false,
|
|
22898
|
-
subGroups: []
|
|
22899
|
-
// Question text doesn't need sub-grouping
|
|
22900
|
-
});
|
|
22901
|
-
continue;
|
|
22902
|
-
}
|
|
22903
|
-
if (item.type === JSON_TYPES.TABLE) {
|
|
22904
|
-
if (currentGroup.length > 0) {
|
|
22905
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22906
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22907
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22908
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22909
|
-
currentGroup = [];
|
|
22910
|
-
}
|
|
22911
|
-
groups.push({
|
|
22912
|
-
items: [item],
|
|
22913
|
-
hasArabic: false,
|
|
22914
|
-
hasRTLInlineWidget: false,
|
|
22915
|
-
subGroups: []
|
|
22916
|
-
});
|
|
22917
|
-
continue;
|
|
22918
|
-
}
|
|
22919
|
-
if (item.type === JSON_TYPES.NUMBER_LINE) {
|
|
22920
|
-
if (currentGroup.length > 0) {
|
|
22921
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22922
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22923
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22924
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22925
|
-
currentGroup = [];
|
|
22926
|
-
}
|
|
22927
|
-
groups.push({
|
|
22928
|
-
items: [item],
|
|
22929
|
-
hasArabic: false,
|
|
22930
|
-
hasRTLInlineWidget: false,
|
|
22931
|
-
subGroups: []
|
|
22932
|
-
});
|
|
22933
|
-
continue;
|
|
22934
|
-
}
|
|
22935
|
-
if (item.type === JSON_TYPES.VERTICAL_OPERATION) {
|
|
22936
|
-
if (currentGroup.length > 0) {
|
|
22937
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22938
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22939
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22940
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22941
|
-
currentGroup = [];
|
|
22942
|
-
}
|
|
22943
|
-
groups.push({
|
|
22944
|
-
items: [item],
|
|
22945
|
-
hasArabic: false,
|
|
22946
|
-
hasRTLInlineWidget: false,
|
|
22947
|
-
subGroups: []
|
|
22948
|
-
});
|
|
22949
|
-
continue;
|
|
22950
|
-
}
|
|
22951
|
-
if (item.type === JSON_TYPES.CHART) {
|
|
22952
|
-
if (currentGroup.length > 0) {
|
|
22953
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22954
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22955
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22956
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22957
|
-
currentGroup = [];
|
|
22958
|
-
}
|
|
22959
|
-
groups.push({
|
|
22960
|
-
items: [item],
|
|
22961
|
-
hasArabic: false,
|
|
22962
|
-
hasRTLInlineWidget: false,
|
|
22963
|
-
subGroups: []
|
|
22964
|
-
});
|
|
22965
|
-
continue;
|
|
22966
|
-
}
|
|
22967
|
-
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
22968
|
-
if (currentGroup.length > 0) {
|
|
22969
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22970
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22971
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22972
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22973
|
-
currentGroup = [];
|
|
22974
|
-
}
|
|
22975
|
-
} else {
|
|
22976
|
-
currentGroup.push(item);
|
|
22977
|
-
}
|
|
22978
|
-
}
|
|
22979
|
-
if (currentGroup.length > 0) {
|
|
22980
|
-
const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
|
|
22981
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22982
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22983
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22984
|
-
}
|
|
22985
|
-
return groups;
|
|
22986
|
-
}, [json.layout, json.widgets]);
|
|
22987
|
-
const droppedInputs = React.useMemo(() => new Set(droppedSources.keys()), [droppedSources]);
|
|
22988
|
-
const dragOptionsWidget = json.widgets["_drag_options"];
|
|
22989
|
-
const rulerWidget = json.widgets[RULER_WIDGET_ID];
|
|
22990
|
-
const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
|
|
22991
|
-
const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
|
|
22992
|
-
const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
|
|
22993
|
-
const { tools, toolsMinHeight } = React.useMemo(() => {
|
|
22994
|
-
const list = [];
|
|
22995
|
-
const PAD = 12;
|
|
22996
|
-
const STAGGER = 24;
|
|
22997
|
-
let maxH = 0;
|
|
22998
|
-
const getSize = (w) => typeof w === "object" && w !== null && "size" in w && typeof w.size === "number" ? w.size : 1;
|
|
22999
|
-
const add2 = (id, type, width) => {
|
|
23000
|
-
const offset = list.length * STAGGER;
|
|
23001
|
-
list.push({ id, type, width, initialPosition: { x: PAD + offset, y: PAD + offset } });
|
|
23002
|
-
const extra = type === "set-square" ? FLIP_BTN_EXTRA : 0;
|
|
23003
|
-
const h = PAD + Math.ceil(width * (TOOL_ASPECT_RATIO[type] ?? 0)) + extra + PAD;
|
|
23004
|
-
if (h > maxH) maxH = h;
|
|
23604
|
+
const handleSubmit = () => {
|
|
23605
|
+
runSubmit();
|
|
23005
23606
|
};
|
|
23006
|
-
|
|
23007
|
-
|
|
23008
|
-
|
|
23009
|
-
|
|
23010
|
-
|
|
23011
|
-
|
|
23012
|
-
|
|
23013
|
-
|
|
23607
|
+
const lineGroups = React.useMemo(() => {
|
|
23608
|
+
const isRTLInlineWidget = (item) => {
|
|
23609
|
+
if (item.type === JSON_TYPES.WIDGET_REF) {
|
|
23610
|
+
const widget = json.widgets[item.widgetId];
|
|
23611
|
+
return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
|
|
23612
|
+
}
|
|
23613
|
+
return RTL_INLINE_WIDGET_TYPES.has(item.type);
|
|
23614
|
+
};
|
|
23615
|
+
const groups = [];
|
|
23616
|
+
let currentGroup = [];
|
|
23617
|
+
for (const item of json.layout) {
|
|
23618
|
+
if (item.type === JSON_TYPES.QUESTION_TEXT) {
|
|
23619
|
+
groups.push({
|
|
23620
|
+
items: [item],
|
|
23621
|
+
hasArabic: itemContainsArabic(item),
|
|
23622
|
+
hasRTLInlineWidget: false,
|
|
23623
|
+
subGroups: []
|
|
23624
|
+
// Question text doesn't need sub-grouping
|
|
23625
|
+
});
|
|
23626
|
+
continue;
|
|
23627
|
+
}
|
|
23628
|
+
if (item.type === JSON_TYPES.TABLE) {
|
|
23629
|
+
if (currentGroup.length > 0) {
|
|
23630
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23631
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23632
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23633
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23634
|
+
currentGroup = [];
|
|
23635
|
+
}
|
|
23636
|
+
groups.push({
|
|
23637
|
+
items: [item],
|
|
23638
|
+
hasArabic: false,
|
|
23639
|
+
hasRTLInlineWidget: false,
|
|
23640
|
+
subGroups: []
|
|
23641
|
+
});
|
|
23642
|
+
continue;
|
|
23643
|
+
}
|
|
23644
|
+
if (item.type === JSON_TYPES.NUMBER_LINE) {
|
|
23645
|
+
if (currentGroup.length > 0) {
|
|
23646
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23647
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23648
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23649
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23650
|
+
currentGroup = [];
|
|
23651
|
+
}
|
|
23652
|
+
groups.push({
|
|
23653
|
+
items: [item],
|
|
23654
|
+
hasArabic: false,
|
|
23655
|
+
hasRTLInlineWidget: false,
|
|
23656
|
+
subGroups: []
|
|
23657
|
+
});
|
|
23658
|
+
continue;
|
|
23659
|
+
}
|
|
23660
|
+
if (item.type === JSON_TYPES.VERTICAL_OPERATION) {
|
|
23661
|
+
if (currentGroup.length > 0) {
|
|
23662
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23663
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23664
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23665
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23666
|
+
currentGroup = [];
|
|
23667
|
+
}
|
|
23668
|
+
groups.push({
|
|
23669
|
+
items: [item],
|
|
23670
|
+
hasArabic: false,
|
|
23671
|
+
hasRTLInlineWidget: false,
|
|
23672
|
+
subGroups: []
|
|
23673
|
+
});
|
|
23674
|
+
continue;
|
|
23675
|
+
}
|
|
23676
|
+
if (item.type === JSON_TYPES.CHART) {
|
|
23677
|
+
if (currentGroup.length > 0) {
|
|
23678
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23679
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23680
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23681
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23682
|
+
currentGroup = [];
|
|
23683
|
+
}
|
|
23684
|
+
groups.push({
|
|
23685
|
+
items: [item],
|
|
23686
|
+
hasArabic: false,
|
|
23687
|
+
hasRTLInlineWidget: false,
|
|
23688
|
+
subGroups: []
|
|
23689
|
+
});
|
|
23690
|
+
continue;
|
|
23691
|
+
}
|
|
23692
|
+
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
23693
|
+
if (currentGroup.length > 0) {
|
|
23694
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23695
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23696
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23697
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23698
|
+
currentGroup = [];
|
|
23699
|
+
}
|
|
23700
|
+
} else {
|
|
23701
|
+
currentGroup.push(item);
|
|
23702
|
+
}
|
|
23703
|
+
}
|
|
23704
|
+
if (currentGroup.length > 0) {
|
|
23705
|
+
const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
|
|
23706
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23707
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23708
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23709
|
+
}
|
|
23710
|
+
return groups;
|
|
23711
|
+
}, [json.layout, json.widgets]);
|
|
23712
|
+
const droppedInputs = React.useMemo(() => new Set(droppedSources.keys()), [droppedSources]);
|
|
23713
|
+
const dragOptionsWidget = json.widgets["_drag_options"];
|
|
23714
|
+
const draggableOptionsWidget = json.widgets[DRAGGABLE_OPTIONS_WIDGET_ID];
|
|
23715
|
+
const rulerWidget = json.widgets[RULER_WIDGET_ID];
|
|
23716
|
+
const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
|
|
23717
|
+
const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
|
|
23718
|
+
const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
|
|
23719
|
+
const { tools, toolsMinHeight } = React.useMemo(() => {
|
|
23720
|
+
const list = [];
|
|
23721
|
+
const PAD = 12;
|
|
23722
|
+
const STAGGER = 24;
|
|
23723
|
+
let maxH = 0;
|
|
23724
|
+
const getSize = (w) => typeof w === "object" && w !== null && "size" in w && typeof w.size === "number" ? w.size : 1;
|
|
23725
|
+
const add2 = (id, type, width) => {
|
|
23726
|
+
const offset = list.length * STAGGER;
|
|
23727
|
+
list.push({ id, type, width, initialPosition: { x: PAD + offset, y: PAD + offset } });
|
|
23728
|
+
const extra = type === "set-square" ? FLIP_BTN_EXTRA : 0;
|
|
23729
|
+
const h = PAD + Math.ceil(width * (TOOL_ASPECT_RATIO[type] ?? 0)) + extra + PAD;
|
|
23730
|
+
if (h > maxH) maxH = h;
|
|
23731
|
+
};
|
|
23732
|
+
if (rulerWidget) add2("ruler-1", "ruler", Math.round(getSize(rulerWidget) * 240));
|
|
23733
|
+
if (protractorWidget)
|
|
23734
|
+
add2("protractor-1", "protractor", Math.round(getSize(protractorWidget) * 180));
|
|
23735
|
+
if (setSquareWidget)
|
|
23736
|
+
add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
|
|
23737
|
+
return { tools: list, toolsMinHeight: maxH };
|
|
23738
|
+
}, [rulerWidget, protractorWidget, setSquareWidget]);
|
|
23739
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
23740
|
+
ExerciseDndProvider,
|
|
23014
23741
|
{
|
|
23015
|
-
|
|
23016
|
-
|
|
23017
|
-
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
|
|
23022
|
-
|
|
23023
|
-
|
|
23024
|
-
|
|
23025
|
-
|
|
23026
|
-
|
|
23027
|
-
|
|
23028
|
-
|
|
23029
|
-
|
|
23030
|
-
|
|
23031
|
-
|
|
23032
|
-
|
|
23033
|
-
|
|
23034
|
-
|
|
23035
|
-
|
|
23036
|
-
onInputFocus: setFocusedInputId,
|
|
23037
|
-
resetKey
|
|
23038
|
-
},
|
|
23039
|
-
item.id
|
|
23040
|
-
)) }, subIdx)
|
|
23041
|
-
) : (
|
|
23042
|
-
// Widget group - no dir wrapper, render directly
|
|
23043
|
-
subGroup.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
23044
|
-
LayoutRenderer,
|
|
23045
|
-
{
|
|
23046
|
-
item,
|
|
23047
|
-
widgets: json.widgets,
|
|
23048
|
-
values,
|
|
23049
|
-
droppedInputs,
|
|
23050
|
-
validationDetails,
|
|
23051
|
-
onChange: handleChange,
|
|
23052
|
-
onInputFocus: setFocusedInputId,
|
|
23053
|
-
resetKey
|
|
23054
|
-
},
|
|
23055
|
-
item.id
|
|
23056
|
-
))
|
|
23057
|
-
))
|
|
23058
|
-
) : (
|
|
23059
|
-
// For QUESTION_TEXT items (no sub-groups)
|
|
23060
|
-
group.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
23061
|
-
LayoutRenderer,
|
|
23742
|
+
droppedSources,
|
|
23743
|
+
exerciseKey: loadedValuesSignature,
|
|
23744
|
+
renderOverlay: (item) => {
|
|
23745
|
+
if (item.type !== "rich-drag-option" || draggableOptionsWidget?.type !== JSON_TYPES.DRAGGABLE_OPTIONS) {
|
|
23746
|
+
return null;
|
|
23747
|
+
}
|
|
23748
|
+
const data = item.data;
|
|
23749
|
+
const option = draggableOptionsWidget.options.find(
|
|
23750
|
+
(candidate) => candidate.name === data.optionName
|
|
23751
|
+
);
|
|
23752
|
+
return option ? (
|
|
23753
|
+
// The overlay renders outside the root below, so it needs the root's class
|
|
23754
|
+
// and theme style for nested widgets to resolve the --exercise-* variables.
|
|
23755
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
23756
|
+
"div",
|
|
23757
|
+
{
|
|
23758
|
+
className: `math-exercise-renderer rich-drag-overlay-root ${className}`,
|
|
23759
|
+
style: themeStyle,
|
|
23760
|
+
"aria-hidden": "true",
|
|
23761
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
23762
|
+
"div",
|
|
23062
23763
|
{
|
|
23063
|
-
|
|
23064
|
-
widgets: json.widgets
|
|
23065
|
-
|
|
23066
|
-
|
|
23067
|
-
validationDetails,
|
|
23068
|
-
onChange: handleChange,
|
|
23069
|
-
onInputFocus: setFocusedInputId,
|
|
23070
|
-
resetKey
|
|
23071
|
-
},
|
|
23072
|
-
item.id
|
|
23073
|
-
))
|
|
23074
|
-
)
|
|
23075
|
-
},
|
|
23076
|
-
groupIdx
|
|
23077
|
-
)) }),
|
|
23078
|
-
numericKeypadWidget && numericKeypadWidget.type === "numeric-keypad" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "numeric-keypad-container", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
23079
|
-
WidgetRenderer,
|
|
23080
|
-
{
|
|
23081
|
-
widget: numericKeypadWidget,
|
|
23082
|
-
widgetId: "_numeric_keypad",
|
|
23083
|
-
value: focusedInputId ? values[focusedInputId] || "" : "",
|
|
23084
|
-
onChange: () => {
|
|
23085
|
-
},
|
|
23086
|
-
onKeypadChange: (newValue) => {
|
|
23087
|
-
if (!focusedInputId) return;
|
|
23088
|
-
const maxLength = getMaxInputLength(json, focusedInputId);
|
|
23089
|
-
if (maxLength !== void 0 && newValue.length > maxLength) return;
|
|
23090
|
-
handleChange(focusedInputId, newValue, false);
|
|
23764
|
+
className: `drag-overlay-option rich-drag-overlay-option${isPlainTextContent(option.content) ? "" : " drag-overlay-option--rich"}`,
|
|
23765
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(RichOptionContent, { content: option.content, widgets: json.widgets })
|
|
23766
|
+
}
|
|
23767
|
+
)
|
|
23091
23768
|
}
|
|
23092
|
-
|
|
23093
|
-
)
|
|
23094
|
-
|
|
23095
|
-
]
|
|
23096
|
-
|
|
23097
|
-
|
|
23098
|
-
|
|
23099
|
-
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
|
|
23104
|
-
|
|
23105
|
-
|
|
23769
|
+
)
|
|
23770
|
+
) : null;
|
|
23771
|
+
},
|
|
23772
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(KeypadContext.Provider, { value: Boolean(numericKeypadWidget), children: /* @__PURE__ */ jsxRuntime.jsx(DragBankContext.Provider, { value: json.widgets[DRAG_OPTIONS_WIDGET_ID] !== void 0, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
23773
|
+
"div",
|
|
23774
|
+
{
|
|
23775
|
+
ref: rootRef,
|
|
23776
|
+
className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`,
|
|
23777
|
+
style: themeStyle,
|
|
23778
|
+
children: [
|
|
23779
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
23780
|
+
"div",
|
|
23781
|
+
{
|
|
23782
|
+
className: `exercise-layout-container ${numericKeypadWidget ? "has-keypad" : ""}`,
|
|
23783
|
+
style: toolsMinHeight > 0 ? { minHeight: toolsMinHeight } : void 0,
|
|
23784
|
+
children: [
|
|
23785
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-layout", children: lineGroups.map((group, groupIdx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
23786
|
+
"div",
|
|
23787
|
+
{
|
|
23788
|
+
className: "exercise-line",
|
|
23789
|
+
dir: group.hasArabic && group.hasRTLInlineWidget ? "rtl" : void 0,
|
|
23790
|
+
children: group.subGroups.length > 0 ? (
|
|
23791
|
+
// Render sub-groups for regular lines
|
|
23792
|
+
group.subGroups.map(
|
|
23793
|
+
(subGroup, subIdx) => subGroup.isTextGroup ? (
|
|
23794
|
+
// Text group - only apply RTL for pure Arabic (no mixed languages)
|
|
23795
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
23796
|
+
"span",
|
|
23797
|
+
{
|
|
23798
|
+
className: "text-group",
|
|
23799
|
+
dir: subGroup.shouldApplyRTL ? "rtl" : void 0,
|
|
23800
|
+
children: subGroup.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
23801
|
+
LayoutRenderer,
|
|
23802
|
+
{
|
|
23803
|
+
item,
|
|
23804
|
+
widgets: json.widgets,
|
|
23805
|
+
values,
|
|
23806
|
+
droppedInputs,
|
|
23807
|
+
validationDetails,
|
|
23808
|
+
onChange: handleChange,
|
|
23809
|
+
onInputFocus: setFocusedInputId,
|
|
23810
|
+
resetKey
|
|
23811
|
+
},
|
|
23812
|
+
item.id
|
|
23813
|
+
))
|
|
23814
|
+
},
|
|
23815
|
+
subIdx
|
|
23816
|
+
)
|
|
23817
|
+
) : (
|
|
23818
|
+
// Widget group - no dir wrapper, render directly
|
|
23819
|
+
subGroup.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
23820
|
+
LayoutRenderer,
|
|
23821
|
+
{
|
|
23822
|
+
item,
|
|
23823
|
+
widgets: json.widgets,
|
|
23824
|
+
values,
|
|
23825
|
+
droppedInputs,
|
|
23826
|
+
validationDetails,
|
|
23827
|
+
onChange: handleChange,
|
|
23828
|
+
onInputFocus: setFocusedInputId,
|
|
23829
|
+
resetKey
|
|
23830
|
+
},
|
|
23831
|
+
item.id
|
|
23832
|
+
))
|
|
23833
|
+
)
|
|
23834
|
+
)
|
|
23835
|
+
) : (
|
|
23836
|
+
// For QUESTION_TEXT items (no sub-groups)
|
|
23837
|
+
group.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
23838
|
+
LayoutRenderer,
|
|
23839
|
+
{
|
|
23840
|
+
item,
|
|
23841
|
+
widgets: json.widgets,
|
|
23842
|
+
values,
|
|
23843
|
+
droppedInputs,
|
|
23844
|
+
validationDetails,
|
|
23845
|
+
onChange: handleChange,
|
|
23846
|
+
onInputFocus: setFocusedInputId,
|
|
23847
|
+
resetKey
|
|
23848
|
+
},
|
|
23849
|
+
item.id
|
|
23850
|
+
))
|
|
23851
|
+
)
|
|
23852
|
+
},
|
|
23853
|
+
groupIdx
|
|
23854
|
+
)) }),
|
|
23855
|
+
numericKeypadWidget && numericKeypadWidget.type === "numeric-keypad" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "numeric-keypad-container", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
23856
|
+
WidgetRenderer,
|
|
23857
|
+
{
|
|
23858
|
+
widget: numericKeypadWidget,
|
|
23859
|
+
widgetId: "_numeric_keypad",
|
|
23860
|
+
value: focusedInputId ? values[focusedInputId] || "" : "",
|
|
23861
|
+
onChange: () => {
|
|
23862
|
+
},
|
|
23863
|
+
onKeypadChange: (newValue) => {
|
|
23864
|
+
if (!focusedInputId) return;
|
|
23865
|
+
const maxLength = getMaxInputLength(json, focusedInputId);
|
|
23866
|
+
const currentLength = String(values[focusedInputId] ?? "").length;
|
|
23867
|
+
if (maxLength !== void 0 && newValue.length > maxLength && newValue.length > currentLength)
|
|
23868
|
+
return;
|
|
23869
|
+
handleChange(focusedInputId, newValue, false);
|
|
23870
|
+
}
|
|
23871
|
+
}
|
|
23872
|
+
) }),
|
|
23873
|
+
tools.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ToolsOverlay, { tools, resetKey })
|
|
23874
|
+
]
|
|
23875
|
+
}
|
|
23876
|
+
),
|
|
23877
|
+
dragOptionsWidget && dragOptionsWidget.type === "drag-options" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-options-container", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
23878
|
+
WidgetRenderer,
|
|
23879
|
+
{
|
|
23880
|
+
widget: dragOptionsWidget,
|
|
23881
|
+
widgetId: "_drag_options",
|
|
23882
|
+
value: "",
|
|
23883
|
+
onChange: () => {
|
|
23884
|
+
}
|
|
23885
|
+
}
|
|
23886
|
+
) }),
|
|
23887
|
+
draggableOptionsWidget && draggableOptionsWidget.type === JSON_TYPES.DRAGGABLE_OPTIONS && // Keeps the legacy class too: host CSS uses :has(.drag-options-container) to
|
|
23888
|
+
// tell drag exercises from typed ones.
|
|
23889
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-options-container draggable-options-container", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
23890
|
+
WidgetRenderer,
|
|
23891
|
+
{
|
|
23892
|
+
widget: draggableOptionsWidget,
|
|
23893
|
+
widgetId: DRAGGABLE_OPTIONS_WIDGET_ID,
|
|
23894
|
+
value: "",
|
|
23895
|
+
values,
|
|
23896
|
+
widgets: json.widgets,
|
|
23897
|
+
onChange: () => {
|
|
23898
|
+
}
|
|
23899
|
+
}
|
|
23900
|
+
) }),
|
|
23901
|
+
showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx(
|
|
23902
|
+
"button",
|
|
23903
|
+
{
|
|
23904
|
+
className: "exercise-submit-btn",
|
|
23905
|
+
disabled: locked,
|
|
23906
|
+
onClick: handleSubmit,
|
|
23907
|
+
children: "Check Answer"
|
|
23908
|
+
}
|
|
23909
|
+
),
|
|
23910
|
+
tutorial && tutorialRun && rootRef.current && /* @__PURE__ */ jsxRuntime.jsx(
|
|
23911
|
+
TutorialOverlay,
|
|
23912
|
+
{
|
|
23913
|
+
tutorial,
|
|
23914
|
+
root: rootRef.current,
|
|
23915
|
+
json,
|
|
23916
|
+
options: tutorialRun.options,
|
|
23917
|
+
onClose: (finished) => {
|
|
23918
|
+
setTutorialRun(null);
|
|
23919
|
+
tutorialRun.options.onClose?.();
|
|
23920
|
+
if (finished) tutorialRun.options.onDone?.();
|
|
23921
|
+
}
|
|
23922
|
+
},
|
|
23923
|
+
tutorialRun.run
|
|
23924
|
+
)
|
|
23925
|
+
]
|
|
23926
|
+
}
|
|
23927
|
+
) }) })
|
|
23106
23928
|
}
|
|
23107
|
-
) })
|
|
23108
|
-
|
|
23109
|
-
|
|
23110
|
-
TutorialOverlay,
|
|
23111
|
-
{
|
|
23112
|
-
tutorial,
|
|
23113
|
-
root: rootRef.current,
|
|
23114
|
-
json,
|
|
23115
|
-
options: tutorialRun.options,
|
|
23116
|
-
onClose: (finished) => {
|
|
23117
|
-
setTutorialRun(null);
|
|
23118
|
-
tutorialRun.options.onClose?.();
|
|
23119
|
-
if (finished) tutorialRun.options.onDone?.();
|
|
23120
|
-
}
|
|
23121
|
-
},
|
|
23122
|
-
tutorialRun.run
|
|
23123
|
-
)
|
|
23124
|
-
] }) }) }) }) }) });
|
|
23125
|
-
});
|
|
23929
|
+
) }) });
|
|
23930
|
+
}
|
|
23931
|
+
);
|
|
23126
23932
|
ExerciseRenderer.displayName = "ExerciseRenderer";
|
|
23127
23933
|
function renderExercise(options) {
|
|
23128
23934
|
const container = typeof options.container === "string" ? document.querySelector(options.container) : options.container;
|
|
@@ -23195,6 +24001,12 @@ exports.DEFAULT_TABLE_BORDER_COLOR = DEFAULT_TABLE_BORDER_COLOR;
|
|
|
23195
24001
|
exports.DisplayInlineNodeSchema = DisplayInlineNodeSchema;
|
|
23196
24002
|
exports.DragOptionsJSONSchema = DragOptionsJSONSchema;
|
|
23197
24003
|
exports.DragOptionsToolSchema = DragOptionsToolSchema;
|
|
24004
|
+
exports.DraggableOptionJSONSchema = DraggableOptionJSONSchema;
|
|
24005
|
+
exports.DraggableOptionSchema = DraggableOptionSchema;
|
|
24006
|
+
exports.DraggableOptionsJSONSchema = DraggableOptionsJSONSchema;
|
|
24007
|
+
exports.DraggableOptionsToolSchema = DraggableOptionsToolSchema;
|
|
24008
|
+
exports.DropInputJSONSchema = DropInputJSONSchema;
|
|
24009
|
+
exports.DropInputSchema = DropInputSchema;
|
|
23198
24010
|
exports.EngineError = EngineError;
|
|
23199
24011
|
exports.ExerciseDocumentSchema = ExerciseDocumentSchema;
|
|
23200
24012
|
exports.ExerciseJSONSchema = ExerciseJSONSchema;
|
|
@@ -23303,6 +24115,7 @@ exports.WIDGET_TUTORIALS = WIDGET_TUTORIALS;
|
|
|
23303
24115
|
exports.WidgetJSONSchema = WidgetJSONSchema;
|
|
23304
24116
|
exports.WidgetRefSchema = WidgetRefSchema;
|
|
23305
24117
|
exports.WidgetRenderer = WidgetRenderer;
|
|
24118
|
+
exports.addDuplicateOptionNameIssues = addDuplicateOptionNameIssues;
|
|
23306
24119
|
exports.alignGroupBarsToSeries = alignGroupBarsToSeries;
|
|
23307
24120
|
exports.compile = compile;
|
|
23308
24121
|
exports.compileWithAST = compileWithAST;
|
|
@@ -23319,6 +24132,9 @@ exports.createChartSeriesBar = createChartSeriesBar;
|
|
|
23319
24132
|
exports.createColorText = createColorText;
|
|
23320
24133
|
exports.createDisplayClock = createDisplayClock;
|
|
23321
24134
|
exports.createDragOptions = createDragOptions;
|
|
24135
|
+
exports.createDraggableOption = createDraggableOption;
|
|
24136
|
+
exports.createDraggableOptions = createDraggableOptions;
|
|
24137
|
+
exports.createDropInput = createDropInput;
|
|
23322
24138
|
exports.createExerciseDocument = createExerciseDocument;
|
|
23323
24139
|
exports.createExpressionValidation = createExpressionValidation;
|
|
23324
24140
|
exports.createFraction = createFraction;
|
|
@@ -23363,6 +24179,7 @@ exports.findWidgetTutorial = findWidgetTutorial;
|
|
|
23363
24179
|
exports.getAnswerableInputs = getAnswerableInputs;
|
|
23364
24180
|
exports.getFractionKey = getFractionKey;
|
|
23365
24181
|
exports.getMissingInputs = getMissingInputs;
|
|
24182
|
+
exports.hasOptionContent = hasOptionContent;
|
|
23366
24183
|
exports.isBoldText = isBoldText;
|
|
23367
24184
|
exports.isCellInInteractiveArea = isCellInInteractiveArea;
|
|
23368
24185
|
exports.isChartAxis = isChartAxis;
|
|
@@ -23380,6 +24197,10 @@ exports.isClockItem = isClockItem;
|
|
|
23380
24197
|
exports.isColorText = isColorText;
|
|
23381
24198
|
exports.isDragOptions = isDragOptions;
|
|
23382
24199
|
exports.isDragOptionsJSON = isDragOptionsJSON;
|
|
24200
|
+
exports.isDraggableOptions = isDraggableOptions;
|
|
24201
|
+
exports.isDraggableOptionsJSON = isDraggableOptionsJSON;
|
|
24202
|
+
exports.isDropInput = isDropInput;
|
|
24203
|
+
exports.isDropInputJSON = isDropInputJSON;
|
|
23383
24204
|
exports.isExerciseDocument = isExerciseDocument;
|
|
23384
24205
|
exports.isExpressionValidation = isExpressionValidation;
|
|
23385
24206
|
exports.isFraction = isFraction;
|
|
@@ -23409,6 +24230,7 @@ exports.isMixedValidation = isMixedValidation;
|
|
|
23409
24230
|
exports.isNumericKeypad = isNumericKeypad;
|
|
23410
24231
|
exports.isNumericKeypadJSON = isNumericKeypadJSON;
|
|
23411
24232
|
exports.isOperatorItem = isOperatorItem;
|
|
24233
|
+
exports.isOptionContentNode = isOptionContentNode;
|
|
23412
24234
|
exports.isPairMatcherBlock = isPairMatcherBlock;
|
|
23413
24235
|
exports.isPairMatcherWidget = isPairMatcherWidget;
|
|
23414
24236
|
exports.isProtractorTool = isProtractorTool;
|
|
@@ -23465,4 +24287,4 @@ exports.validateUniqueIds = validateUniqueIds;
|
|
|
23465
24287
|
exports.validateVisualFractionCapacity = validateVisualFractionCapacity;
|
|
23466
24288
|
exports.validateWidgetJSON = validateWidgetJSON;
|
|
23467
24289
|
exports.validateWidgetJSONSafe = validateWidgetJSONSafe;
|
|
23468
|
-
//# sourceMappingURL=index-
|
|
24290
|
+
//# sourceMappingURL=index-C9OHpy2h.js.map
|