@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
|
@@ -24,6 +24,8 @@ const AST_TYPES = {
|
|
|
24
24
|
BOLD_TEXT: "BoldText",
|
|
25
25
|
/** Inline input widget (string or numeric) */
|
|
26
26
|
INLINE_INPUT: "InlineInput",
|
|
27
|
+
/** Drop-only inline input; stores the selected option name */
|
|
28
|
+
DROP_INPUT: "DropInput",
|
|
27
29
|
/** Single inline draggable option (appears in-flow with text) */
|
|
28
30
|
INLINE_DRAG_OPTION: "InlineDragOption",
|
|
29
31
|
/** Inline fraction widget (numerator/denominator with optional inputs) */
|
|
@@ -111,6 +113,8 @@ const AST_TYPES = {
|
|
|
111
113
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
112
114
|
/** Drag-and-drop options tool */
|
|
113
115
|
DRAG_OPTIONS: "DragOptions",
|
|
116
|
+
/** Rich draggable options tool */
|
|
117
|
+
DRAGGABLE_OPTIONS: "DraggableOptions",
|
|
114
118
|
/** Radio button options tool */
|
|
115
119
|
RADIO_OPTIONS: "RadioOptions",
|
|
116
120
|
/** Numeric keypad input tool */
|
|
@@ -216,8 +220,12 @@ const JSON_TYPES = {
|
|
|
216
220
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
217
221
|
/** Inline input widget (string or numeric) */
|
|
218
222
|
INLINE_INPUT: "inline-input",
|
|
223
|
+
/** Drop-only inline input; stores the selected option name */
|
|
224
|
+
DROP_INPUT: "drop-input",
|
|
219
225
|
/** Drag-and-drop options widget */
|
|
220
226
|
DRAG_OPTIONS: "drag-options",
|
|
227
|
+
/** Rich draggable options widget */
|
|
228
|
+
DRAGGABLE_OPTIONS: "draggable-options",
|
|
221
229
|
/** Single inline draggable option (appears in-flow with text) */
|
|
222
230
|
INLINE_DRAG_OPTION: "inline-drag-option",
|
|
223
231
|
/** Radio button options widget */
|
|
@@ -240,6 +248,7 @@ const JSON_TYPES = {
|
|
|
240
248
|
GEOMETRY: "geometry"
|
|
241
249
|
};
|
|
242
250
|
const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
|
|
251
|
+
const DRAGGABLE_OPTIONS_WIDGET_ID = "_draggable_options";
|
|
243
252
|
const NUMERIC_KEYPAD_WIDGET_ID = "_numeric_keypad";
|
|
244
253
|
const RULER_WIDGET_ID = "_ruler";
|
|
245
254
|
const PROTRACTOR_WIDGET_ID = "_protractor";
|
|
@@ -255,6 +264,7 @@ const GRID_SYMMETRY_WIDGET_PREFIX = "_grid_symmetry_";
|
|
|
255
264
|
const GEOMETRY_WIDGET_PREFIX = "_geometry_";
|
|
256
265
|
const RTL_INLINE_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
257
266
|
JSON_TYPES.INLINE_INPUT,
|
|
267
|
+
JSON_TYPES.DROP_INPUT,
|
|
258
268
|
JSON_TYPES.INLINE_DRAG_OPTION,
|
|
259
269
|
JSON_TYPES.FRACTION,
|
|
260
270
|
JSON_TYPES.VISUAL_FRACTION
|
|
@@ -354,6 +364,10 @@ const STRING_INPUT = createToken({
|
|
|
354
364
|
name: "STRING_INPUT",
|
|
355
365
|
pattern: /STRING_INPUT/
|
|
356
366
|
});
|
|
367
|
+
const DROP_INPUT = createToken({
|
|
368
|
+
name: "DROP_INPUT",
|
|
369
|
+
pattern: /DROP_INPUT/
|
|
370
|
+
});
|
|
357
371
|
const INLINE_DRAG_OPTION = createToken({
|
|
358
372
|
name: "INLINE_DRAG_OPTION",
|
|
359
373
|
pattern: /INLINE_DRAG_OPTION/
|
|
@@ -362,6 +376,18 @@ const DRAGABLE_BUTTONS_OPTIONS = createToken({
|
|
|
362
376
|
name: "DRAGABLE_BUTTONS_OPTIONS",
|
|
363
377
|
pattern: /DRAGABLE_BUTTONS_OPTIONS/
|
|
364
378
|
});
|
|
379
|
+
const DRAGGABLE_OPTIONS = createToken({
|
|
380
|
+
name: "DRAGGABLE_OPTIONS",
|
|
381
|
+
pattern: /DRAGGABLE_OPTIONS/
|
|
382
|
+
});
|
|
383
|
+
const OPTION = createToken({
|
|
384
|
+
name: "OPTION",
|
|
385
|
+
// Only a keyword when immediately followed by `[` (which the grammar always
|
|
386
|
+
// requires: `OPTION[name=...]`). Without the lookahead, the bare common word
|
|
387
|
+
// "OPTION"/"OPTIONS" in exercise prose would tokenize as a keyword and break
|
|
388
|
+
// parsing.
|
|
389
|
+
pattern: /OPTION(?=\[)/
|
|
390
|
+
});
|
|
365
391
|
const RADIO_BUTTONS_OPTIONS = createToken({
|
|
366
392
|
name: "RADIO_BUTTONS_OPTIONS",
|
|
367
393
|
pattern: /RADIO_BUTTONS_OPTIONS/
|
|
@@ -672,8 +698,11 @@ const allTokens = [
|
|
|
672
698
|
HIGHLIGHT_TEXT,
|
|
673
699
|
W_S_I,
|
|
674
700
|
STRING_INPUT,
|
|
701
|
+
DROP_INPUT,
|
|
675
702
|
INLINE_DRAG_OPTION,
|
|
703
|
+
DRAGGABLE_OPTIONS,
|
|
676
704
|
DRAGABLE_BUTTONS_OPTIONS,
|
|
705
|
+
OPTION,
|
|
677
706
|
RADIO_BUTTONS_OPTIONS,
|
|
678
707
|
NUMERIC_KEYPAD,
|
|
679
708
|
WIDGET_ANSWER,
|
|
@@ -1445,6 +1474,8 @@ class MathExerciseParser extends CstParser {
|
|
|
1445
1474
|
{ ALT: () => this.SUBRULE(this.visualFraction) },
|
|
1446
1475
|
// W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
|
|
1447
1476
|
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) },
|
|
1477
|
+
// DROP_INPUT (drop-only inline answer widget) — excluded in display-only contexts
|
|
1478
|
+
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.dropInput) },
|
|
1448
1479
|
// INLINE_DRAG_OPTION (single draggable option inline with text) — excluded in display-only
|
|
1449
1480
|
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) },
|
|
1450
1481
|
// CLOCK (inline clock widget)
|
|
@@ -1536,6 +1567,12 @@ class MathExerciseParser extends CstParser {
|
|
|
1536
1567
|
this.SUBRULE(this.parameterList);
|
|
1537
1568
|
this.CONSUME(RBracket);
|
|
1538
1569
|
});
|
|
1570
|
+
this.dropInput = this.RULE("dropInput", () => {
|
|
1571
|
+
this.CONSUME(DROP_INPUT);
|
|
1572
|
+
this.CONSUME(LBracket);
|
|
1573
|
+
this.SUBRULE(this.parameterList);
|
|
1574
|
+
this.CONSUME(RBracket);
|
|
1575
|
+
});
|
|
1539
1576
|
this.inlineDragOption = this.RULE("inlineDragOption", () => {
|
|
1540
1577
|
this.CONSUME(INLINE_DRAG_OPTION);
|
|
1541
1578
|
this.CONSUME(LBracket);
|
|
@@ -1544,6 +1581,7 @@ class MathExerciseParser extends CstParser {
|
|
|
1544
1581
|
});
|
|
1545
1582
|
this.tool = this.RULE("tool", () => {
|
|
1546
1583
|
this.OR([
|
|
1584
|
+
{ ALT: () => this.SUBRULE(this.draggableOptions) },
|
|
1547
1585
|
{ ALT: () => this.SUBRULE(this.dragableButtons) },
|
|
1548
1586
|
{ ALT: () => this.SUBRULE(this.numericKeypad) },
|
|
1549
1587
|
{ ALT: () => this.SUBRULE(this.rulerTool) },
|
|
@@ -1551,6 +1589,44 @@ class MathExerciseParser extends CstParser {
|
|
|
1551
1589
|
{ ALT: () => this.SUBRULE(this.setSquareTool) }
|
|
1552
1590
|
]);
|
|
1553
1591
|
});
|
|
1592
|
+
this.draggableOptions = this.RULE("draggableOptions", () => {
|
|
1593
|
+
this.CONSUME(DRAGGABLE_OPTIONS);
|
|
1594
|
+
this.CONSUME(LBracket);
|
|
1595
|
+
this.OPTION(() => this.SUBRULE(this.parameterList));
|
|
1596
|
+
this.CONSUME(RBracket);
|
|
1597
|
+
this.MANY(() => {
|
|
1598
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1599
|
+
});
|
|
1600
|
+
this.CONSUME(LBrace);
|
|
1601
|
+
this.AT_LEAST_ONE(() => this.SUBRULE(this.draggableOption));
|
|
1602
|
+
this.CONSUME(RBrace);
|
|
1603
|
+
this.MANY2(() => {
|
|
1604
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1605
|
+
});
|
|
1606
|
+
});
|
|
1607
|
+
this.draggableOption = this.RULE("draggableOption", () => {
|
|
1608
|
+
this.MANY(() => {
|
|
1609
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1610
|
+
});
|
|
1611
|
+
this.CONSUME(OPTION);
|
|
1612
|
+
this.CONSUME(LBracket);
|
|
1613
|
+
this.SUBRULE(this.parameterList);
|
|
1614
|
+
this.CONSUME(RBracket);
|
|
1615
|
+
this.MANY2(() => {
|
|
1616
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1617
|
+
});
|
|
1618
|
+
this.CONSUME(LBrace);
|
|
1619
|
+
this.MANY3(() => {
|
|
1620
|
+
this.OR3([
|
|
1621
|
+
{ ALT: () => this.SUBRULE(this.inlineElement, { ARGS: [true] }) },
|
|
1622
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1623
|
+
]);
|
|
1624
|
+
});
|
|
1625
|
+
this.CONSUME(RBrace);
|
|
1626
|
+
this.MANY4(() => {
|
|
1627
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1628
|
+
});
|
|
1629
|
+
});
|
|
1554
1630
|
this.dragableButtons = this.RULE("dragableButtons", () => {
|
|
1555
1631
|
this.CONSUME(DRAGABLE_BUTTONS_OPTIONS);
|
|
1556
1632
|
this.CONSUME(LBracket);
|
|
@@ -2108,6 +2184,14 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
|
|
|
2108
2184
|
answer: z.union([z.string(), z.number()]).optional()
|
|
2109
2185
|
})
|
|
2110
2186
|
]);
|
|
2187
|
+
const DropInputSchema = z.object({
|
|
2188
|
+
type: z.literal(AST_TYPES.DROP_INPUT),
|
|
2189
|
+
name: z.string().min(1),
|
|
2190
|
+
answer: z.string().min(1),
|
|
2191
|
+
/** Box width in characters: the answer is an option name, so nothing else implies a size. */
|
|
2192
|
+
width: InlineInputWidthSchema,
|
|
2193
|
+
placeholder: z.string().max(32).optional()
|
|
2194
|
+
});
|
|
2111
2195
|
const FractionInlineSchema = z.object({
|
|
2112
2196
|
type: z.literal(AST_TYPES.FRACTION),
|
|
2113
2197
|
name: z.string(),
|
|
@@ -2221,6 +2305,7 @@ const InlineNodeSchema = z.union([
|
|
|
2221
2305
|
ColorTextInlineSchema,
|
|
2222
2306
|
BoldTextInlineSchema,
|
|
2223
2307
|
InlineInputWidgetSchema,
|
|
2308
|
+
DropInputSchema,
|
|
2224
2309
|
FractionInlineSchema,
|
|
2225
2310
|
ImageInlineSchema,
|
|
2226
2311
|
VisualFractionInlineSchema,
|
|
@@ -2296,6 +2381,19 @@ function createNumericInput(name, digits, answer, placeholder, disabled, width,
|
|
|
2296
2381
|
"NUMERIC_INPUT"
|
|
2297
2382
|
);
|
|
2298
2383
|
}
|
|
2384
|
+
function createDropInput(name, answer, placeholder, width) {
|
|
2385
|
+
return parseOrThrow(
|
|
2386
|
+
DropInputSchema,
|
|
2387
|
+
{
|
|
2388
|
+
type: AST_TYPES.DROP_INPUT,
|
|
2389
|
+
name,
|
|
2390
|
+
answer,
|
|
2391
|
+
placeholder,
|
|
2392
|
+
width
|
|
2393
|
+
},
|
|
2394
|
+
"DROP_INPUT"
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2299
2397
|
function createImage(name, src, alt, options = {}) {
|
|
2300
2398
|
return ImageInlineSchema.parse({
|
|
2301
2399
|
type: AST_TYPES.IMAGE,
|
|
@@ -2350,6 +2448,9 @@ function isSpace(value) {
|
|
|
2350
2448
|
function isInlineInput(value) {
|
|
2351
2449
|
return InlineInputWidgetSchema.safeParse(value).success;
|
|
2352
2450
|
}
|
|
2451
|
+
function isDropInput(value) {
|
|
2452
|
+
return DropInputSchema.safeParse(value).success;
|
|
2453
|
+
}
|
|
2353
2454
|
function isFraction(value) {
|
|
2354
2455
|
return FractionInlineSchema.safeParse(value).success;
|
|
2355
2456
|
}
|
|
@@ -2404,7 +2505,7 @@ function createNumberLinePosition(value, inlines) {
|
|
|
2404
2505
|
if (!hasRealContent) {
|
|
2405
2506
|
throw new Error(`POSITION[value=${value}]: content cannot be empty or whitespace only`);
|
|
2406
2507
|
}
|
|
2407
|
-
const inputs = inlines.filter((i) => i.type === AST_TYPES.INLINE_INPUT);
|
|
2508
|
+
const inputs = inlines.filter((i) => i.type === AST_TYPES.INLINE_INPUT || i.type === AST_TYPES.DROP_INPUT);
|
|
2408
2509
|
if (inputs.length > 1) {
|
|
2409
2510
|
throw new Error(
|
|
2410
2511
|
`POSITION[value=${value}]: only one input widget is allowed, got ${inputs.length}`
|
|
@@ -3117,6 +3218,45 @@ const DragOptionsToolSchema = z.object({
|
|
|
3117
3218
|
type: z.literal(AST_TYPES.DRAG_OPTIONS),
|
|
3118
3219
|
options: z.array(z.string()).min(1)
|
|
3119
3220
|
});
|
|
3221
|
+
function hasOptionContent(content, spaceType = AST_TYPES.SPACE) {
|
|
3222
|
+
return content.some((node) => node.type !== spaceType && !(typeof node.value === "string" && node.value.trim() === ""));
|
|
3223
|
+
}
|
|
3224
|
+
function isAnsweredFraction(node) {
|
|
3225
|
+
return node.type === AST_TYPES.FRACTION && node.answer !== void 0;
|
|
3226
|
+
}
|
|
3227
|
+
function isOptionContentNode(node) {
|
|
3228
|
+
const parsed = DisplayInlineNodeSchema.safeParse(node);
|
|
3229
|
+
return parsed.success && !isAnsweredFraction(parsed.data);
|
|
3230
|
+
}
|
|
3231
|
+
const DraggableOptionSchema = z.object({
|
|
3232
|
+
name: z.string().min(1),
|
|
3233
|
+
content: z.array(DisplayInlineNodeSchema).min(1).refine((content) => hasOptionContent(content), { message: "OPTION content cannot be empty or whitespace only" }).refine(
|
|
3234
|
+
(content) => !content.some(isAnsweredFraction),
|
|
3235
|
+
{ message: "FRACTION inside OPTION must be display-only (no answer)" }
|
|
3236
|
+
)
|
|
3237
|
+
});
|
|
3238
|
+
function addDuplicateOptionNameIssues(options, ctx, pathPrefix = []) {
|
|
3239
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3240
|
+
options.forEach((option, index) => {
|
|
3241
|
+
const key2 = normalize(option.name);
|
|
3242
|
+
if (seen.has(key2)) {
|
|
3243
|
+
ctx.addIssue({
|
|
3244
|
+
code: z.ZodIssueCode.custom,
|
|
3245
|
+
message: `Duplicate option name "${option.name}" (names are compared ignoring case and number format)`,
|
|
3246
|
+
path: [...pathPrefix, "options", index, "name"]
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
seen.add(key2);
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
const DraggableOptionsToolObjectSchema = z.object({
|
|
3253
|
+
type: z.literal(AST_TYPES.DRAGGABLE_OPTIONS),
|
|
3254
|
+
reusable: z.boolean().default(false),
|
|
3255
|
+
options: z.array(DraggableOptionSchema).min(1)
|
|
3256
|
+
});
|
|
3257
|
+
const DraggableOptionsToolSchema = DraggableOptionsToolObjectSchema.superRefine((tool, ctx) => {
|
|
3258
|
+
addDuplicateOptionNameIssues(tool.options, ctx);
|
|
3259
|
+
});
|
|
3120
3260
|
const NumericKeypadToolSchema = z.object({
|
|
3121
3261
|
type: z.literal(AST_TYPES.NUMERIC_KEYPAD),
|
|
3122
3262
|
includeOperators: z.boolean().optional(),
|
|
@@ -3137,6 +3277,7 @@ const SetSquareToolSchema = z.object({
|
|
|
3137
3277
|
});
|
|
3138
3278
|
const ToolNodeSchema = z.discriminatedUnion("type", [
|
|
3139
3279
|
DragOptionsToolSchema,
|
|
3280
|
+
DraggableOptionsToolObjectSchema,
|
|
3140
3281
|
NumericKeypadToolSchema,
|
|
3141
3282
|
RulerToolSchema,
|
|
3142
3283
|
ProtractorToolSchema,
|
|
@@ -3148,6 +3289,19 @@ function createDragOptions(options) {
|
|
|
3148
3289
|
options
|
|
3149
3290
|
});
|
|
3150
3291
|
}
|
|
3292
|
+
function createDraggableOptions(options, reusable = false) {
|
|
3293
|
+
const result = DraggableOptionsToolSchema.safeParse({
|
|
3294
|
+
type: AST_TYPES.DRAGGABLE_OPTIONS,
|
|
3295
|
+
reusable,
|
|
3296
|
+
options
|
|
3297
|
+
});
|
|
3298
|
+
if (result.success) return result.data;
|
|
3299
|
+
const issue = result.error.issues[0];
|
|
3300
|
+
throw new Error(`DRAGGABLE_OPTIONS: ${issue ? issue.message : "validation failed"}`);
|
|
3301
|
+
}
|
|
3302
|
+
function createDraggableOption(name, content) {
|
|
3303
|
+
return DraggableOptionSchema.parse({ name, content });
|
|
3304
|
+
}
|
|
3151
3305
|
function createNumericKeypad(includeOperators, includeDecimal, style) {
|
|
3152
3306
|
return NumericKeypadToolSchema.parse({
|
|
3153
3307
|
type: AST_TYPES.NUMERIC_KEYPAD,
|
|
@@ -3168,6 +3322,9 @@ function createSetSquareTool(size) {
|
|
|
3168
3322
|
function isDragOptions(value) {
|
|
3169
3323
|
return DragOptionsToolSchema.safeParse(value).success;
|
|
3170
3324
|
}
|
|
3325
|
+
function isDraggableOptions(value) {
|
|
3326
|
+
return DraggableOptionsToolSchema.safeParse(value).success;
|
|
3327
|
+
}
|
|
3171
3328
|
function isNumericKeypad(value) {
|
|
3172
3329
|
return NumericKeypadToolSchema.safeParse(value).success;
|
|
3173
3330
|
}
|
|
@@ -3726,9 +3883,13 @@ const ExerciseDocumentSchema = z.object({
|
|
|
3726
3883
|
seen.add(name);
|
|
3727
3884
|
}
|
|
3728
3885
|
};
|
|
3886
|
+
const dropInputs = [];
|
|
3729
3887
|
const visitInlines = (inlines, basePath) => {
|
|
3730
3888
|
inlines.forEach((inline, i) => {
|
|
3731
|
-
if (inline.type === AST_TYPES.
|
|
3889
|
+
if (inline.type === AST_TYPES.DROP_INPUT) {
|
|
3890
|
+
dropInputs.push({ name: inline.name, answer: inline.answer, path: [...basePath, i] });
|
|
3891
|
+
}
|
|
3892
|
+
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) {
|
|
3732
3893
|
record(inline.name, [...basePath, i, "name"]);
|
|
3733
3894
|
} else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
|
|
3734
3895
|
record(inline.name, [...basePath, i, "name"]);
|
|
@@ -3745,9 +3906,13 @@ const ExerciseDocumentSchema = z.object({
|
|
|
3745
3906
|
});
|
|
3746
3907
|
});
|
|
3747
3908
|
} else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
|
|
3748
|
-
|
|
3909
|
+
const numberLine = block;
|
|
3910
|
+
numberLine.positions.forEach((pos, pi) => {
|
|
3749
3911
|
visitInlines(pos.inlines, ["lines", bi, "positions", pi, "inlines"]);
|
|
3750
3912
|
});
|
|
3913
|
+
numberLine.arrows?.forEach((arrow, ai) => {
|
|
3914
|
+
visitInlines(arrow.inlines, ["lines", bi, "arrows", ai, "inlines"]);
|
|
3915
|
+
});
|
|
3751
3916
|
} else if (block.type === AST_TYPES.MATCHER_BLOCK) {
|
|
3752
3917
|
const m = block;
|
|
3753
3918
|
record(m.name, ["lines", bi, "name"]);
|
|
@@ -3790,15 +3955,78 @@ const ExerciseDocumentSchema = z.object({
|
|
|
3790
3955
|
record(block.name, ["lines", bi, "name"]);
|
|
3791
3956
|
}
|
|
3792
3957
|
});
|
|
3958
|
+
const richBanks = (doc.tools ?? []).map((tool, toolIndex) => ({ tool, toolIndex })).filter((entry) => entry.tool.type === AST_TYPES.DRAGGABLE_OPTIONS);
|
|
3959
|
+
richBanks.forEach(({ tool: bank, toolIndex }) => {
|
|
3960
|
+
if (bank.type !== AST_TYPES.DRAGGABLE_OPTIONS) return;
|
|
3961
|
+
bank.options.forEach((option, optionIndex) => {
|
|
3962
|
+
visitInlines(option.content, ["tools", toolIndex, "options", optionIndex, "content"]);
|
|
3963
|
+
});
|
|
3964
|
+
addDuplicateOptionNameIssues(bank.options, ctx, ["tools", toolIndex]);
|
|
3965
|
+
if (dropInputs.length === 0) {
|
|
3966
|
+
ctx.addIssue({
|
|
3967
|
+
code: z.ZodIssueCode.custom,
|
|
3968
|
+
message: "DRAGGABLE_OPTIONS needs at least one DROP_INPUT: its cards can only be dropped there",
|
|
3969
|
+
path: ["tools", toolIndex]
|
|
3970
|
+
});
|
|
3971
|
+
}
|
|
3972
|
+
});
|
|
3973
|
+
if (richBanks.length > 1) {
|
|
3974
|
+
ctx.addIssue({
|
|
3975
|
+
code: z.ZodIssueCode.custom,
|
|
3976
|
+
message: "Only one DRAGGABLE_OPTIONS tool is allowed",
|
|
3977
|
+
path: ["tools"]
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
const optionNames = new Set(
|
|
3981
|
+
richBanks.flatMap(({ tool }) => (tool.type === AST_TYPES.DRAGGABLE_OPTIONS ? tool.options : []).map((o) => o.name))
|
|
3982
|
+
);
|
|
3983
|
+
dropInputs.forEach(({ name, answer, path }) => {
|
|
3984
|
+
if (richBanks.length === 0) {
|
|
3985
|
+
ctx.addIssue({
|
|
3986
|
+
code: z.ZodIssueCode.custom,
|
|
3987
|
+
message: `DROP_INPUT "${name}" needs a DRAGGABLE_OPTIONS bank to draw from`,
|
|
3988
|
+
path
|
|
3989
|
+
});
|
|
3990
|
+
} else if (!optionNames.has(answer)) {
|
|
3991
|
+
ctx.addIssue({
|
|
3992
|
+
code: z.ZodIssueCode.custom,
|
|
3993
|
+
message: `DROP_INPUT "${name}": answer "${answer}" does not match any OPTION name`,
|
|
3994
|
+
path: [...path, "answer"]
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
});
|
|
3998
|
+
const isSingleUseBank = richBanks.some(({ tool }) => tool.type === AST_TYPES.DRAGGABLE_OPTIONS && !tool.reusable);
|
|
3999
|
+
if (isSingleUseBank) {
|
|
4000
|
+
const firstInputByAnswer = /* @__PURE__ */ new Map();
|
|
4001
|
+
dropInputs.forEach(({ name, answer, path }) => {
|
|
4002
|
+
if (!optionNames.has(answer)) return;
|
|
4003
|
+
const firstInput = firstInputByAnswer.get(answer);
|
|
4004
|
+
if (firstInput === void 0) {
|
|
4005
|
+
firstInputByAnswer.set(answer, name);
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
ctx.addIssue({
|
|
4009
|
+
code: z.ZodIssueCode.custom,
|
|
4010
|
+
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]`,
|
|
4011
|
+
path: [...path, "answer"]
|
|
4012
|
+
});
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
3793
4015
|
});
|
|
3794
4016
|
function createExerciseDocument(question, lines, validation, tools) {
|
|
3795
|
-
|
|
4017
|
+
const result = ExerciseDocumentSchema.safeParse({
|
|
3796
4018
|
type: AST_TYPES.EXERCISE_DOCUMENT,
|
|
3797
4019
|
question,
|
|
3798
4020
|
lines,
|
|
3799
4021
|
tools,
|
|
3800
4022
|
validation
|
|
3801
4023
|
});
|
|
4024
|
+
if (result.success) return result.data;
|
|
4025
|
+
const { issues } = result.error;
|
|
4026
|
+
if (issues.every((issue) => issue.code === z.ZodIssueCode.custom)) {
|
|
4027
|
+
throw new Error(issues.map((issue) => issue.message).join("\n"));
|
|
4028
|
+
}
|
|
4029
|
+
throw result.error;
|
|
3802
4030
|
}
|
|
3803
4031
|
function isExerciseDocument(value) {
|
|
3804
4032
|
return ExerciseDocumentSchema.safeParse(value).success;
|
|
@@ -4198,6 +4426,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
4198
4426
|
if (ctx.inlineInput) {
|
|
4199
4427
|
return this.visit(ctx.inlineInput);
|
|
4200
4428
|
}
|
|
4429
|
+
if (ctx.dropInput) {
|
|
4430
|
+
return this.visit(ctx.dropInput);
|
|
4431
|
+
}
|
|
4201
4432
|
if (ctx.inlineDragOption) {
|
|
4202
4433
|
return this.visit(ctx.inlineDragOption);
|
|
4203
4434
|
}
|
|
@@ -4352,6 +4583,18 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
4352
4583
|
const digits = params.digits;
|
|
4353
4584
|
return createNumericInput(name, digits, answer, placeholder, disabled, width, prefill);
|
|
4354
4585
|
}
|
|
4586
|
+
dropInput(ctx) {
|
|
4587
|
+
const params = this.visit(ctx.parameterList);
|
|
4588
|
+
if (typeof params.name === "string" && params.name.startsWith("_")) {
|
|
4589
|
+
throw new Error(`DROP_INPUT: name "${params.name}" is reserved (names cannot start with "_")`);
|
|
4590
|
+
}
|
|
4591
|
+
return createDropInput(
|
|
4592
|
+
params.name,
|
|
4593
|
+
params.answer,
|
|
4594
|
+
params.placeholder,
|
|
4595
|
+
params.width
|
|
4596
|
+
);
|
|
4597
|
+
}
|
|
4355
4598
|
inlineDragOption(ctx) {
|
|
4356
4599
|
const valueToken = ctx.StringLiteral[0];
|
|
4357
4600
|
const value = this.extractString(valueToken.image);
|
|
@@ -4490,6 +4733,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
4490
4733
|
// Tools
|
|
4491
4734
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
4492
4735
|
tool(ctx) {
|
|
4736
|
+
if (ctx.draggableOptions) {
|
|
4737
|
+
return this.visit(ctx.draggableOptions);
|
|
4738
|
+
}
|
|
4493
4739
|
if (ctx.dragableButtons) {
|
|
4494
4740
|
return this.visit(ctx.dragableButtons);
|
|
4495
4741
|
}
|
|
@@ -4507,6 +4753,33 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
4507
4753
|
}
|
|
4508
4754
|
throw new Error("Unknown tool");
|
|
4509
4755
|
}
|
|
4756
|
+
draggableOptions(ctx) {
|
|
4757
|
+
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
4758
|
+
if (params.reusable !== void 0 && !["true", "false"].includes(String(params.reusable))) {
|
|
4759
|
+
throw new Error("DRAGGABLE_OPTIONS: reusable must be true or false");
|
|
4760
|
+
}
|
|
4761
|
+
const reusable = params.reusable === true || params.reusable === "true";
|
|
4762
|
+
const options = ctx.draggableOption.map((option) => this.visit(option));
|
|
4763
|
+
return createDraggableOptions(options, reusable);
|
|
4764
|
+
}
|
|
4765
|
+
draggableOption(ctx) {
|
|
4766
|
+
const params = this.visit(ctx.parameterList);
|
|
4767
|
+
const rawContent = ctx.inlineElement ? ctx.inlineElement.map((element) => this.visit(element)) : [];
|
|
4768
|
+
const name = params.name;
|
|
4769
|
+
if (typeof name !== "string" || name === "") {
|
|
4770
|
+
throw new Error("DRAGGABLE_OPTIONS: OPTION requires a non-empty string name");
|
|
4771
|
+
}
|
|
4772
|
+
if (!hasOptionContent(rawContent)) {
|
|
4773
|
+
throw new Error(`DRAGGABLE_OPTIONS: OPTION "${name}" content cannot be empty or whitespace only`);
|
|
4774
|
+
}
|
|
4775
|
+
const invalid = rawContent.find((node) => !isOptionContentNode(node));
|
|
4776
|
+
if (invalid) {
|
|
4777
|
+
throw new Error(
|
|
4778
|
+
`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`
|
|
4779
|
+
);
|
|
4780
|
+
}
|
|
4781
|
+
return createDraggableOption(name, rawContent);
|
|
4782
|
+
}
|
|
4510
4783
|
dragableButtons(ctx) {
|
|
4511
4784
|
const strings = this.visit(ctx.stringList);
|
|
4512
4785
|
return createDragOptions(strings);
|
|
@@ -5114,10 +5387,30 @@ const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
|
|
|
5114
5387
|
StringInputJSONSchema,
|
|
5115
5388
|
NumericInputJSONSchema
|
|
5116
5389
|
]);
|
|
5390
|
+
const DropInputJSONSchema = z.object({
|
|
5391
|
+
type: z.literal(JSON_TYPES.DROP_INPUT),
|
|
5392
|
+
/** Box width in characters; absent means the default input size. */
|
|
5393
|
+
width: InlineInputWidthSchema,
|
|
5394
|
+
placeholder: z.string().max(32).optional()
|
|
5395
|
+
});
|
|
5117
5396
|
const DragOptionsJSONSchema = z.object({
|
|
5118
5397
|
type: z.literal(JSON_TYPES.DRAG_OPTIONS),
|
|
5119
5398
|
options: z.array(z.string())
|
|
5120
5399
|
});
|
|
5400
|
+
const DraggableOptionJSONSchema = z.object({
|
|
5401
|
+
name: z.string().min(1),
|
|
5402
|
+
content: z.array(LayoutItemSchema).min(1).refine(
|
|
5403
|
+
(content) => hasOptionContent(content, JSON_TYPES.SPACE),
|
|
5404
|
+
{ message: "Option content cannot be empty or whitespace only" }
|
|
5405
|
+
)
|
|
5406
|
+
});
|
|
5407
|
+
const DraggableOptionsJSONSchema = z.object({
|
|
5408
|
+
type: z.literal(JSON_TYPES.DRAGGABLE_OPTIONS),
|
|
5409
|
+
reusable: z.boolean(),
|
|
5410
|
+
options: z.array(DraggableOptionJSONSchema).min(1)
|
|
5411
|
+
}).superRefine((widget, ctx) => {
|
|
5412
|
+
addDuplicateOptionNameIssues(widget.options, ctx);
|
|
5413
|
+
});
|
|
5121
5414
|
const InlineDragOptionJSONSchema = z.object({
|
|
5122
5415
|
type: z.literal(JSON_TYPES.INLINE_DRAG_OPTION),
|
|
5123
5416
|
value: z.string().min(1)
|
|
@@ -5227,7 +5520,9 @@ const GeometryJSONSchema = z.object({
|
|
|
5227
5520
|
});
|
|
5228
5521
|
const WidgetJSONSchema = z.union([
|
|
5229
5522
|
InlineInputJSONSchema,
|
|
5523
|
+
DropInputJSONSchema,
|
|
5230
5524
|
DragOptionsJSONSchema,
|
|
5525
|
+
DraggableOptionsJSONSchema,
|
|
5231
5526
|
InlineDragOptionJSONSchema,
|
|
5232
5527
|
RadioOptionsJSONSchema,
|
|
5233
5528
|
NumericKeypadJSONSchema,
|
|
@@ -5344,9 +5639,15 @@ function isInlineInputJSON(widget) {
|
|
|
5344
5639
|
function isDisabledInputJSON(widget) {
|
|
5345
5640
|
return isInlineInputJSON(widget) && widget.disabled === true;
|
|
5346
5641
|
}
|
|
5642
|
+
function isDropInputJSON(widget) {
|
|
5643
|
+
return widget.type === JSON_TYPES.DROP_INPUT;
|
|
5644
|
+
}
|
|
5347
5645
|
function isDragOptionsJSON(widget) {
|
|
5348
5646
|
return widget.type === JSON_TYPES.DRAG_OPTIONS;
|
|
5349
5647
|
}
|
|
5648
|
+
function isDraggableOptionsJSON(widget) {
|
|
5649
|
+
return widget.type === JSON_TYPES.DRAGGABLE_OPTIONS;
|
|
5650
|
+
}
|
|
5350
5651
|
function isInlineDragOptionJSON(widget) {
|
|
5351
5652
|
return widget.type === JSON_TYPES.INLINE_DRAG_OPTION;
|
|
5352
5653
|
}
|
|
@@ -10880,7 +11181,16 @@ function toJSON(ast) {
|
|
|
10880
11181
|
const answers = {};
|
|
10881
11182
|
let gridSymmetryCounter = 0;
|
|
10882
11183
|
let geometryCounter = 0;
|
|
10883
|
-
const
|
|
11184
|
+
const richOptionsTool = ast.tools?.find(
|
|
11185
|
+
(tool) => tool.type === AST_TYPES.DRAGGABLE_OPTIONS
|
|
11186
|
+
);
|
|
11187
|
+
const allInlines = [
|
|
11188
|
+
...collectAllInlines(ast.lines),
|
|
11189
|
+
...richOptionsTool?.options.flatMap((option) => option.content) ?? []
|
|
11190
|
+
];
|
|
11191
|
+
const matcherCardInlines = new Set(collectAllInlines(ast.lines.filter(
|
|
11192
|
+
(block) => block.type === AST_TYPES.MATCHER_BLOCK || block.type === AST_TYPES.PAIR_MATCHER_BLOCK
|
|
11193
|
+
)));
|
|
10884
11194
|
const inlineDragIds = /* @__PURE__ */ new Map();
|
|
10885
11195
|
{
|
|
10886
11196
|
let counter = 0;
|
|
@@ -11006,7 +11316,7 @@ function toJSON(ast) {
|
|
|
11006
11316
|
const hasDragBank = ast.tools?.some((tool) => tool.type === AST_TYPES.DRAG_OPTIONS) ?? false;
|
|
11007
11317
|
for (const inline of allInlines) {
|
|
11008
11318
|
if (inline.type === AST_TYPES.INLINE_INPUT) {
|
|
11009
|
-
const disabled = inline.disabled || hasDragBank && inline.prefill
|
|
11319
|
+
const disabled = inline.disabled || hasDragBank && !!inline.prefill;
|
|
11010
11320
|
if (inline.inputType === INPUT_TYPES.STRING) {
|
|
11011
11321
|
widgets[inline.name] = {
|
|
11012
11322
|
type: JSON_TYPES.INLINE_INPUT,
|
|
@@ -11031,6 +11341,13 @@ function toJSON(ast) {
|
|
|
11031
11341
|
if (inline.answer !== void 0 && !disabled) {
|
|
11032
11342
|
answers[inline.name] = inline.answer;
|
|
11033
11343
|
}
|
|
11344
|
+
} else if (inline.type === AST_TYPES.DROP_INPUT) {
|
|
11345
|
+
widgets[inline.name] = {
|
|
11346
|
+
type: JSON_TYPES.DROP_INPUT,
|
|
11347
|
+
...inline.width !== void 0 && { width: inline.width },
|
|
11348
|
+
placeholder: inline.placeholder
|
|
11349
|
+
};
|
|
11350
|
+
answers[inline.name] = inline.answer;
|
|
11034
11351
|
} else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
|
|
11035
11352
|
answers[inline.name] = inline.answer;
|
|
11036
11353
|
}
|
|
@@ -11042,7 +11359,7 @@ function toJSON(ast) {
|
|
|
11042
11359
|
simplified: inline.simplified ?? true,
|
|
11043
11360
|
placeholder: inline.placeholder
|
|
11044
11361
|
};
|
|
11045
|
-
if (inline.answer !== void 0) {
|
|
11362
|
+
if (inline.answer !== void 0 && !matcherCardInlines.has(inline)) {
|
|
11046
11363
|
answers[inline.name] = inline.answer;
|
|
11047
11364
|
}
|
|
11048
11365
|
}
|
|
@@ -11118,6 +11435,15 @@ function toJSON(ast) {
|
|
|
11118
11435
|
// Randomise chip order per compile so the answer position isn't fixed.
|
|
11119
11436
|
options: lodashExports.shuffle(tool.options)
|
|
11120
11437
|
};
|
|
11438
|
+
} else if (tool.type === AST_TYPES.DRAGGABLE_OPTIONS) {
|
|
11439
|
+
widgets[DRAGGABLE_OPTIONS_WIDGET_ID] = {
|
|
11440
|
+
type: JSON_TYPES.DRAGGABLE_OPTIONS,
|
|
11441
|
+
reusable: tool.reusable,
|
|
11442
|
+
options: tool.options.map((option) => ({
|
|
11443
|
+
name: option.name,
|
|
11444
|
+
content: option.content.map((inline) => transformInline(inline, idGenerator, inlineDragIds))
|
|
11445
|
+
}))
|
|
11446
|
+
};
|
|
11121
11447
|
} else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
|
|
11122
11448
|
widgets[NUMERIC_KEYPAD_WIDGET_ID] = {
|
|
11123
11449
|
type: JSON_TYPES.NUMERIC_KEYPAD,
|
|
@@ -11583,6 +11909,12 @@ function transformInline(inline, idGenerator, inlineDragIds) {
|
|
|
11583
11909
|
type: JSON_TYPES.WIDGET_REF,
|
|
11584
11910
|
widgetId: inline.name
|
|
11585
11911
|
};
|
|
11912
|
+
case AST_TYPES.DROP_INPUT:
|
|
11913
|
+
return {
|
|
11914
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
11915
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
11916
|
+
widgetId: inline.name
|
|
11917
|
+
};
|
|
11586
11918
|
case AST_TYPES.FRACTION:
|
|
11587
11919
|
return {
|
|
11588
11920
|
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
@@ -11639,6 +11971,7 @@ function transformValidation(validation, answers, widgets) {
|
|
|
11639
11971
|
};
|
|
11640
11972
|
} else if (validation.type === AST_TYPES.EXPRESSION_VALIDATION) {
|
|
11641
11973
|
assertNoDisabledInExpression(validation.expression, widgets);
|
|
11974
|
+
assertNoDropInputs(Object.keys(widgets), widgets);
|
|
11642
11975
|
const validVariableNames = getEnabledWidgetNames(widgets);
|
|
11643
11976
|
const variables = extractVariables$1(validation.expression, validVariableNames);
|
|
11644
11977
|
return {
|
|
@@ -11648,6 +11981,7 @@ function transformValidation(validation, answers, widgets) {
|
|
|
11648
11981
|
};
|
|
11649
11982
|
} else if (validation.type === AST_TYPES.MIXED_VALIDATION) {
|
|
11650
11983
|
assertNoDisabledInExpression(validation.expression, widgets);
|
|
11984
|
+
assertNoDropInputs(extractVariables$1(validation.expression, Object.keys(widgets)), widgets);
|
|
11651
11985
|
const validVariableNames = getEnabledWidgetNames(widgets);
|
|
11652
11986
|
const variables = extractVariables$1(validation.expression, validVariableNames);
|
|
11653
11987
|
const inExpression = new Set(variables);
|
|
@@ -11686,6 +12020,14 @@ function assertNoDisabledInExpression(expression, widgets) {
|
|
|
11686
12020
|
);
|
|
11687
12021
|
}
|
|
11688
12022
|
}
|
|
12023
|
+
function assertNoDropInputs(names, widgets) {
|
|
12024
|
+
const dropInputs = names.filter((name) => widgets[name]?.type === JSON_TYPES.DROP_INPUT);
|
|
12025
|
+
if (dropInputs.length > 0) {
|
|
12026
|
+
throw new Error(
|
|
12027
|
+
`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"`
|
|
12028
|
+
);
|
|
12029
|
+
}
|
|
12030
|
+
}
|
|
11689
12031
|
function getEnabledWidgetNames(widgets) {
|
|
11690
12032
|
return Object.entries(widgets).filter(([, widget]) => !isDisabledInputJSON(widget)).map(([name]) => name);
|
|
11691
12033
|
}
|
|
@@ -12259,7 +12601,7 @@ function withDisabledPrefilledInputs(exercise) {
|
|
|
12259
12601
|
const widgets = { ...exercise.widgets };
|
|
12260
12602
|
let changed = false;
|
|
12261
12603
|
for (const [id, widget] of Object.entries(widgets)) {
|
|
12262
|
-
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.prefill
|
|
12604
|
+
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.prefill && !widget.disabled) {
|
|
12263
12605
|
widgets[id] = { ...widget, disabled: true };
|
|
12264
12606
|
changed = true;
|
|
12265
12607
|
}
|
|
@@ -12432,11 +12774,13 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
12432
12774
|
}
|
|
12433
12775
|
function validateInputFormats(exercise, userInputs) {
|
|
12434
12776
|
const errors = {};
|
|
12435
|
-
|
|
12777
|
+
const { widgets } = withDisabledPrefilledInputs(exercise);
|
|
12778
|
+
for (const [widgetId, widget] of Object.entries(widgets)) {
|
|
12436
12779
|
const value = userInputs[widgetId];
|
|
12437
12780
|
if (value === void 0 || value === "") {
|
|
12438
12781
|
continue;
|
|
12439
12782
|
}
|
|
12783
|
+
if (isDisabledInputJSON(widget)) continue;
|
|
12440
12784
|
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.inputType === INPUT_TYPES.NUMERIC) {
|
|
12441
12785
|
const numValue = typeof value === "number" ? value : parseFloat(String(value));
|
|
12442
12786
|
if (!Number.isFinite(numValue)) {
|
|
@@ -12454,6 +12798,8 @@ function validateInputFormats(exercise, userInputs) {
|
|
|
12454
12798
|
const DragItemTypeSchema = z.enum([
|
|
12455
12799
|
"drag-option",
|
|
12456
12800
|
// Draggable answer options
|
|
12801
|
+
"rich-drag-option",
|
|
12802
|
+
// Rich option from DRAGGABLE_OPTIONS or DROP_INPUT
|
|
12457
12803
|
"placed-chip",
|
|
12458
12804
|
// Chip already placed on a number-line axis (re-drag)
|
|
12459
12805
|
"ruler-handle",
|
|
@@ -12467,6 +12813,16 @@ const DragOptionDataSchema = z.object({
|
|
|
12467
12813
|
value: z.string(),
|
|
12468
12814
|
label: z.string()
|
|
12469
12815
|
});
|
|
12816
|
+
const RichDragOptionDataSchema = z.object({
|
|
12817
|
+
optionName: z.string().min(1),
|
|
12818
|
+
origin: z.discriminatedUnion("kind", [
|
|
12819
|
+
z.object({ kind: z.literal("bank") }),
|
|
12820
|
+
z.object({
|
|
12821
|
+
kind: z.literal("drop-input"),
|
|
12822
|
+
inputName: z.string().min(1)
|
|
12823
|
+
})
|
|
12824
|
+
])
|
|
12825
|
+
});
|
|
12470
12826
|
const DragItemSchema = (dataSchema) => z.object({
|
|
12471
12827
|
id: z.union([z.string(), z.number()]),
|
|
12472
12828
|
// UniqueIdentifier from dnd-kit
|
|
@@ -12474,6 +12830,7 @@ const DragItemSchema = (dataSchema) => z.object({
|
|
|
12474
12830
|
data: dataSchema
|
|
12475
12831
|
});
|
|
12476
12832
|
DragItemSchema(DragOptionDataSchema);
|
|
12833
|
+
DragItemSchema(RichDragOptionDataSchema);
|
|
12477
12834
|
z.object({
|
|
12478
12835
|
targetId: z.string(),
|
|
12479
12836
|
item: z.discriminatedUnion("type", [
|
|
@@ -16254,12 +16611,16 @@ function useExerciseDnd() {
|
|
|
16254
16611
|
}
|
|
16255
16612
|
return ctx;
|
|
16256
16613
|
}
|
|
16614
|
+
function useExerciseDndOptional() {
|
|
16615
|
+
return useContext(ExerciseDndContext);
|
|
16616
|
+
}
|
|
16257
16617
|
const NO_EXERCISE = Symbol("no-exercise");
|
|
16258
16618
|
const ExerciseDndProvider = ({
|
|
16259
16619
|
children,
|
|
16260
16620
|
droppedSources,
|
|
16261
16621
|
onDragEnd,
|
|
16262
|
-
exerciseKey
|
|
16622
|
+
exerciseKey,
|
|
16623
|
+
renderOverlay
|
|
16263
16624
|
}) => {
|
|
16264
16625
|
const [activeItem, setActiveItem] = useState(null);
|
|
16265
16626
|
const [draggedExerciseKey, setDraggedExerciseKey] = useState(NO_EXERCISE);
|
|
@@ -16295,7 +16656,9 @@ const ExerciseDndProvider = ({
|
|
|
16295
16656
|
const raw = active.data.current;
|
|
16296
16657
|
const item = raw?.type === "placed-chip" ? { type: "placed-chip", widgetId: raw.widgetId, label: raw.label, sourceOptionId: raw.sourceOptionId } : raw;
|
|
16297
16658
|
setActiveItem(item);
|
|
16298
|
-
if (item.type === "drag-option")
|
|
16659
|
+
if (item.type === "drag-option" || item.type === "rich-drag-option") {
|
|
16660
|
+
setDraggedExerciseKey(exerciseKey);
|
|
16661
|
+
}
|
|
16299
16662
|
playSound("drag-start");
|
|
16300
16663
|
};
|
|
16301
16664
|
const handleDragEnd = (event) => {
|
|
@@ -16338,14 +16701,16 @@ const ExerciseDndProvider = ({
|
|
|
16338
16701
|
onDragCancel: handleDragCancel,
|
|
16339
16702
|
children: [
|
|
16340
16703
|
children,
|
|
16341
|
-
/* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: activeItem && /* @__PURE__ */ jsx(DragOverlayContent, { item: activeItem }) })
|
|
16704
|
+
/* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: activeItem && /* @__PURE__ */ jsx(DragOverlayContent, { item: activeItem, renderOverlay }) })
|
|
16342
16705
|
]
|
|
16343
16706
|
}
|
|
16344
16707
|
)
|
|
16345
16708
|
}
|
|
16346
16709
|
);
|
|
16347
16710
|
};
|
|
16348
|
-
const DragOverlayContent = ({ item }) => {
|
|
16711
|
+
const DragOverlayContent = ({ item, renderOverlay }) => {
|
|
16712
|
+
const customOverlay = renderOverlay?.(item);
|
|
16713
|
+
if (customOverlay != null) return /* @__PURE__ */ jsx(Fragment, { children: customOverlay });
|
|
16349
16714
|
switch (item.type) {
|
|
16350
16715
|
case "drag-option": {
|
|
16351
16716
|
const data = item.data;
|
|
@@ -16364,7 +16729,8 @@ const Draggable = ({
|
|
|
16364
16729
|
data,
|
|
16365
16730
|
disabled = false,
|
|
16366
16731
|
children,
|
|
16367
|
-
className = ""
|
|
16732
|
+
className = "",
|
|
16733
|
+
behavior = "move"
|
|
16368
16734
|
}) => {
|
|
16369
16735
|
const dragItem = { id, type, data };
|
|
16370
16736
|
const {
|
|
@@ -16377,6 +16743,7 @@ const Draggable = ({
|
|
|
16377
16743
|
data: dragItem,
|
|
16378
16744
|
disabled
|
|
16379
16745
|
});
|
|
16746
|
+
const isCopy = behavior === "copy";
|
|
16380
16747
|
const style = {
|
|
16381
16748
|
cursor: disabled ? "default" : "grab",
|
|
16382
16749
|
touchAction: "none"
|
|
@@ -16386,7 +16753,7 @@ const Draggable = ({
|
|
|
16386
16753
|
{
|
|
16387
16754
|
ref: setNodeRef,
|
|
16388
16755
|
style,
|
|
16389
|
-
className: `draggable ${isDragging ? "dragging" : ""} ${className}`,
|
|
16756
|
+
className: `draggable ${isDragging && !isCopy ? "dragging" : ""} ${className}`,
|
|
16390
16757
|
...listeners,
|
|
16391
16758
|
...attributes,
|
|
16392
16759
|
children
|
|
@@ -17178,6 +17545,9 @@ const PlacedInputChip = ({
|
|
|
17178
17545
|
}
|
|
17179
17546
|
);
|
|
17180
17547
|
};
|
|
17548
|
+
function getInputBoxWidth(chars) {
|
|
17549
|
+
return `${chars * 0.8 + 1}em`;
|
|
17550
|
+
}
|
|
17181
17551
|
function getMaxInputLength(exercise, inputKey) {
|
|
17182
17552
|
const widget = exercise.widgets[inputKey];
|
|
17183
17553
|
if (widget?.type !== JSON_TYPES.INLINE_INPUT) return void 0;
|
|
@@ -17234,7 +17604,7 @@ const InlineInput = ({
|
|
|
17234
17604
|
return applyDropToInput(widgetId, item, onChange);
|
|
17235
17605
|
}, [onChange, widgetId]);
|
|
17236
17606
|
const expectedLength = inputType === INPUT_TYPES.NUMERIC ? digits : length;
|
|
17237
|
-
const dynamicWidth =
|
|
17607
|
+
const dynamicWidth = getInputBoxWidth(width ?? expectedLength ?? 5);
|
|
17238
17608
|
const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
|
|
17239
17609
|
const disabledClass = disabled ? " inline-input--disabled" : "";
|
|
17240
17610
|
const isFixedChip = hasDragBank && disabled && value !== "";
|
|
@@ -17438,42 +17808,418 @@ const InlineDragOption = ({
|
|
|
17438
17808
|
}
|
|
17439
17809
|
) });
|
|
17440
17810
|
};
|
|
17441
|
-
|
|
17442
|
-
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
}
|
|
17447
|
-
|
|
17448
|
-
|
|
17449
|
-
|
|
17450
|
-
|
|
17451
|
-
|
|
17452
|
-
|
|
17453
|
-
|
|
17454
|
-
const
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17811
|
+
function isArabic(text) {
|
|
17812
|
+
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
17813
|
+
}
|
|
17814
|
+
function hasLatinChars(text) {
|
|
17815
|
+
return /[A-Za-z]/.test(text);
|
|
17816
|
+
}
|
|
17817
|
+
function getTextValue(item) {
|
|
17818
|
+
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) {
|
|
17819
|
+
return item.value;
|
|
17820
|
+
}
|
|
17821
|
+
return void 0;
|
|
17822
|
+
}
|
|
17823
|
+
function itemContainsArabic(item) {
|
|
17824
|
+
const text = getTextValue(item);
|
|
17825
|
+
if (text !== void 0) {
|
|
17826
|
+
return isArabic(text);
|
|
17827
|
+
}
|
|
17828
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
17829
|
+
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
17830
|
+
}
|
|
17831
|
+
return false;
|
|
17832
|
+
}
|
|
17833
|
+
function itemContainsLatin(item) {
|
|
17834
|
+
const text = getTextValue(item);
|
|
17835
|
+
if (text !== void 0) {
|
|
17836
|
+
return hasLatinChars(text);
|
|
17837
|
+
}
|
|
17838
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
17839
|
+
return item.items.some((subItem) => itemContainsLatin(subItem));
|
|
17840
|
+
}
|
|
17841
|
+
return false;
|
|
17842
|
+
}
|
|
17843
|
+
function shouldItemsUseRTL(items) {
|
|
17844
|
+
const hasArabic = items.some(itemContainsArabic);
|
|
17845
|
+
const hasLatin = items.some(itemContainsLatin);
|
|
17846
|
+
return hasArabic && !hasLatin;
|
|
17847
|
+
}
|
|
17848
|
+
function isTextItem(item) {
|
|
17849
|
+
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;
|
|
17850
|
+
}
|
|
17851
|
+
function groupLineItems(items) {
|
|
17852
|
+
const groups = [];
|
|
17853
|
+
let currentTextGroup = [];
|
|
17854
|
+
for (const item of items) {
|
|
17855
|
+
if (isTextItem(item)) {
|
|
17856
|
+
currentTextGroup.push(item);
|
|
17857
|
+
} else {
|
|
17858
|
+
if (currentTextGroup.length > 0) {
|
|
17859
|
+
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
17860
|
+
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
17861
|
+
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
17862
|
+
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
17863
|
+
currentTextGroup = [];
|
|
17864
|
+
}
|
|
17865
|
+
groups.push({ items: [item], isTextGroup: false, hasArabic: false, hasLatin: false, shouldApplyRTL: false });
|
|
17866
|
+
}
|
|
17867
|
+
}
|
|
17868
|
+
if (currentTextGroup.length > 0) {
|
|
17869
|
+
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
17870
|
+
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
17871
|
+
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
17872
|
+
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
17873
|
+
}
|
|
17874
|
+
return groups;
|
|
17875
|
+
}
|
|
17876
|
+
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
17877
|
+
const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
|
|
17878
|
+
const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
|
|
17879
|
+
const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
|
|
17880
|
+
const NUMBER_LINE_AXIS_ENDS = {
|
|
17881
|
+
"arrow-right": { left: "tick", right: "arrow" },
|
|
17882
|
+
"arrow-left": { left: "arrow", right: "tick" },
|
|
17883
|
+
"arrow-both": { left: "arrow", right: "arrow" },
|
|
17884
|
+
"line": { left: "line", right: "line" },
|
|
17885
|
+
"line-left": { left: "line", right: "arrow" }
|
|
17886
|
+
};
|
|
17887
|
+
const toPercent = (value, min, max) => {
|
|
17888
|
+
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
17889
|
+
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
17890
|
+
};
|
|
17891
|
+
const pixelToValue = (chipCenterX, axisLeft, axisWidth, min, max) => {
|
|
17892
|
+
const relX = (chipCenterX - axisLeft) / axisWidth;
|
|
17893
|
+
const fraction = (relX - SIDE_PAD / 100) / (1 - 2 * SIDE_PAD / 100);
|
|
17894
|
+
const clamped = Math.max(0, Math.min(1, fraction));
|
|
17895
|
+
return min + clamped * (max - min);
|
|
17896
|
+
};
|
|
17897
|
+
const getMajorTicks = (min, max, step) => {
|
|
17898
|
+
if (step <= 0 || max <= min) return [min, max];
|
|
17899
|
+
const count = Math.floor((max - min) / step);
|
|
17900
|
+
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
17901
|
+
const lastTick = ticks[ticks.length - 1];
|
|
17902
|
+
if (roundTick(lastTick) !== roundTick(max)) {
|
|
17903
|
+
ticks.push(max);
|
|
17904
|
+
} else {
|
|
17905
|
+
ticks[ticks.length - 1] = max;
|
|
17906
|
+
}
|
|
17907
|
+
return ticks;
|
|
17908
|
+
};
|
|
17909
|
+
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
17910
|
+
if (minorTicks <= 0 || majorTicks.length < 2) return [];
|
|
17911
|
+
const result = [];
|
|
17912
|
+
for (let i = 0; i < majorTicks.length - 1; i++) {
|
|
17913
|
+
const from = majorTicks[i];
|
|
17914
|
+
const to = majorTicks[i + 1];
|
|
17915
|
+
const interval = to - from;
|
|
17916
|
+
for (let j = 1; j <= minorTicks; j++) {
|
|
17917
|
+
result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
|
|
17918
|
+
}
|
|
17919
|
+
}
|
|
17920
|
+
return result;
|
|
17921
|
+
};
|
|
17922
|
+
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
17923
|
+
...majorTicks,
|
|
17924
|
+
...getMinorTickValues(majorTicks, minorTicks)
|
|
17925
|
+
];
|
|
17926
|
+
const snapToNearest = (raw, ticks) => {
|
|
17927
|
+
if (ticks.length === 0) return cleanFloat(raw);
|
|
17928
|
+
return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
|
|
17929
|
+
};
|
|
17930
|
+
const getPositionWidgetId = (content) => {
|
|
17931
|
+
for (const item of content) {
|
|
17932
|
+
if (isWidgetRef(item)) return item.widgetId;
|
|
17933
|
+
}
|
|
17934
|
+
return null;
|
|
17935
|
+
};
|
|
17936
|
+
const toHighlightColor = (color) => color.startsWith("#") ? color : `var(--nl-highlight-${color})`;
|
|
17937
|
+
function placeTableCells(rows, cols) {
|
|
17938
|
+
const placements = /* @__PURE__ */ new Map();
|
|
17939
|
+
const occupied = rows.map(() => []);
|
|
17940
|
+
const totalRows = rows.length;
|
|
17941
|
+
rows.forEach((row, rowIndex) => {
|
|
17942
|
+
const taken = occupied[rowIndex] ?? [];
|
|
17943
|
+
let col = 0;
|
|
17944
|
+
for (const cell of row.cells) {
|
|
17945
|
+
while (taken[col]) col++;
|
|
17946
|
+
const colspan = cell.colspan || 1;
|
|
17947
|
+
const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
|
|
17948
|
+
for (let r = rowIndex; r < rowIndex + rowspan; r++) {
|
|
17949
|
+
const line = occupied[r];
|
|
17950
|
+
if (!line) continue;
|
|
17951
|
+
for (let c = col; c < col + colspan; c++) line[c] = true;
|
|
17952
|
+
}
|
|
17953
|
+
placements.set(cell.id, {
|
|
17954
|
+
column: col + 1,
|
|
17955
|
+
row: rowIndex + 1,
|
|
17956
|
+
colspan,
|
|
17957
|
+
rowspan,
|
|
17958
|
+
isLastColumn: col + colspan >= cols,
|
|
17959
|
+
isLastRow: rowIndex + rowspan >= totalRows
|
|
17960
|
+
});
|
|
17961
|
+
col += colspan;
|
|
17962
|
+
}
|
|
17963
|
+
});
|
|
17964
|
+
return placements;
|
|
17965
|
+
}
|
|
17966
|
+
const EMPTY_VALUES$2 = {};
|
|
17967
|
+
const EMPTY_DROPPED_INPUTS = /* @__PURE__ */ new Set();
|
|
17968
|
+
const ignoreChange = () => {
|
|
17969
|
+
};
|
|
17970
|
+
const PLAIN_TEXT_TYPES = /* @__PURE__ */ new Set([JSON_TYPES.TEXT, JSON_TYPES.SPACE, JSON_TYPES.BOLD_TEXT]);
|
|
17971
|
+
function isPlainTextContent(content) {
|
|
17972
|
+
return content.every((item) => PLAIN_TEXT_TYPES.has(item.type));
|
|
17973
|
+
}
|
|
17974
|
+
const RichOptionContent = ({ content, widgets }) => {
|
|
17975
|
+
const renderItem = (item) => /* @__PURE__ */ jsx(
|
|
17976
|
+
LayoutRenderer,
|
|
17977
|
+
{
|
|
17978
|
+
item: item.type === JSON_TYPES.IMAGE ? { ...item, fullscreen: false } : item,
|
|
17979
|
+
widgets,
|
|
17980
|
+
values: EMPTY_VALUES$2,
|
|
17981
|
+
droppedInputs: EMPTY_DROPPED_INPUTS,
|
|
17982
|
+
onChange: ignoreChange
|
|
17983
|
+
},
|
|
17984
|
+
item.id
|
|
17985
|
+
);
|
|
17986
|
+
const hasRTLInlineWidget = content.some((item) => {
|
|
17987
|
+
if (item.type !== JSON_TYPES.WIDGET_REF) return false;
|
|
17988
|
+
const widget = widgets[item.widgetId];
|
|
17989
|
+
return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
|
|
17990
|
+
});
|
|
17991
|
+
const isRTL = hasRTLInlineWidget && content.some(itemContainsArabic);
|
|
17992
|
+
return /* @__PURE__ */ jsx("span", { className: "rich-option-content", dir: isRTL ? "rtl" : "ltr", children: groupLineItems(content).map((group, index) => group.isTextGroup ? /* @__PURE__ */ jsx("span", { className: "text-group", dir: group.shouldApplyRTL ? "rtl" : void 0, children: group.items.map(renderItem) }, index) : group.items.map(renderItem)) });
|
|
17993
|
+
};
|
|
17994
|
+
const DraggableOptions = ({
|
|
17995
|
+
widget,
|
|
17996
|
+
widgets,
|
|
17997
|
+
usedOptionNames
|
|
17998
|
+
}) => {
|
|
17999
|
+
const { exerciseKey } = useExerciseDnd();
|
|
18000
|
+
return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options draggable-options", children: widget.options.map((option) => /* @__PURE__ */ jsx(
|
|
18001
|
+
RichDragOption,
|
|
18002
|
+
{
|
|
18003
|
+
option,
|
|
18004
|
+
widgets,
|
|
18005
|
+
reusable: widget.reusable,
|
|
18006
|
+
used: !widget.reusable && usedOptionNames.has(option.name)
|
|
18007
|
+
},
|
|
18008
|
+
`${exerciseKey ?? ""}:${option.name}`
|
|
18009
|
+
)) }) });
|
|
18010
|
+
};
|
|
18011
|
+
const RichDragOption = ({ option, widgets, reusable, used }) => {
|
|
18012
|
+
const { activeItem, hasDraggedOption, playHintSound } = useExerciseDnd();
|
|
18013
|
+
const locked = useExerciseLocked();
|
|
18014
|
+
const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption, {
|
|
18015
|
+
ignoreBubbledAnimationEnd: true
|
|
18016
|
+
});
|
|
18017
|
+
const optionId = `rich-drag-option:${option.name}`;
|
|
18018
|
+
const data = { optionName: option.name, origin: { kind: "bank" } };
|
|
18019
|
+
const isPickedUp = !reusable && activeItem?.type === "rich-drag-option" && activeItem.id === optionId;
|
|
18020
|
+
const surfaceClass = isPlainTextContent(option.content) ? "drag-option" : "drag-option drag-option--rich";
|
|
18021
|
+
const emptySlot = /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true", children: /* @__PURE__ */ jsx("button", { type: "button", className: surfaceClass, disabled: true, children: /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets }) }) });
|
|
18022
|
+
return /* @__PURE__ */ jsx("div", { className: "drag-option-slot rich-drag-option-slot", children: used ? emptySlot : /* @__PURE__ */ jsx(
|
|
18023
|
+
Draggable,
|
|
18024
|
+
{
|
|
18025
|
+
id: optionId,
|
|
18026
|
+
type: "rich-drag-option",
|
|
18027
|
+
data,
|
|
18028
|
+
disabled: locked,
|
|
18029
|
+
behavior: reusable ? "copy" : "move",
|
|
18030
|
+
className: "drag-option-wrapper rich-drag-option-wrapper",
|
|
18031
|
+
children: isPickedUp ? emptySlot : /* @__PURE__ */ jsx(
|
|
18032
|
+
"button",
|
|
18033
|
+
{
|
|
18034
|
+
type: "button",
|
|
18035
|
+
className: `${surfaceClass} rich-drag-option${hinting && !locked ? " drag-option--hint" : ""}`,
|
|
18036
|
+
onAnimationStart: (event) => {
|
|
18037
|
+
if (hinting && event.target === event.currentTarget) playHintSound();
|
|
18038
|
+
},
|
|
18039
|
+
onAnimationEnd: onHintEnd,
|
|
18040
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
18041
|
+
children: /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets })
|
|
18042
|
+
}
|
|
18043
|
+
)
|
|
18044
|
+
}
|
|
18045
|
+
) });
|
|
18046
|
+
};
|
|
18047
|
+
const PlacedRichOption = ({
|
|
18048
|
+
inputName,
|
|
18049
|
+
option,
|
|
18050
|
+
widgets,
|
|
18051
|
+
onRemove,
|
|
18052
|
+
className = ""
|
|
18053
|
+
}) => {
|
|
18054
|
+
const locked = useExerciseLocked();
|
|
18055
|
+
const dragId = `${inputName}__rich-option`;
|
|
18056
|
+
const data = {
|
|
18057
|
+
optionName: option.name,
|
|
18058
|
+
origin: { kind: "drop-input", inputName }
|
|
18059
|
+
};
|
|
18060
|
+
const dragItem = { id: dragId, type: "rich-drag-option", data };
|
|
18061
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
18062
|
+
id: dragId,
|
|
18063
|
+
data: dragItem,
|
|
18064
|
+
disabled: locked
|
|
18065
|
+
});
|
|
18066
|
+
useDndMonitor({
|
|
18067
|
+
onDragEnd(event) {
|
|
18068
|
+
if (isUndoDrop(event, dragId)) onRemove();
|
|
18069
|
+
}
|
|
18070
|
+
});
|
|
18071
|
+
return /* @__PURE__ */ jsx(
|
|
18072
|
+
"button",
|
|
18073
|
+
{
|
|
18074
|
+
ref: setNodeRef,
|
|
18075
|
+
type: "button",
|
|
18076
|
+
className: `inline-input-chip placed-rich-option ${className}${isDragging ? " dragging" : ""}`.trim(),
|
|
18077
|
+
onClick: (event) => {
|
|
18078
|
+
if (!locked && event.detail !== 0) onRemove();
|
|
18079
|
+
},
|
|
18080
|
+
title: locked ? void 0 : "Drag to move, or click to remove",
|
|
18081
|
+
style: {
|
|
18082
|
+
width: "100%",
|
|
18083
|
+
height: "100%",
|
|
18084
|
+
minWidth: 0,
|
|
18085
|
+
maxWidth: "none",
|
|
18086
|
+
margin: 0,
|
|
18087
|
+
boxSizing: "border-box",
|
|
18088
|
+
touchAction: "none",
|
|
18089
|
+
cursor: locked ? "default" : "grab"
|
|
18090
|
+
},
|
|
18091
|
+
...listeners,
|
|
18092
|
+
...attributes,
|
|
18093
|
+
children: /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets })
|
|
18094
|
+
}
|
|
18095
|
+
);
|
|
18096
|
+
};
|
|
18097
|
+
const dropSourceId = (optionName) => `rich-drag-option:${optionName}`;
|
|
18098
|
+
function applyRichDropToInput(targetInputName, item, onChange) {
|
|
18099
|
+
if (item.type !== "rich-drag-option") return false;
|
|
18100
|
+
const data = item.data;
|
|
18101
|
+
if (data.origin.kind === "drop-input") {
|
|
18102
|
+
if (data.origin.inputName === targetInputName) return false;
|
|
18103
|
+
onChange(data.origin.inputName, "", false);
|
|
18104
|
+
}
|
|
18105
|
+
onChange(targetInputName, data.optionName, true, dropSourceId(data.optionName));
|
|
18106
|
+
return true;
|
|
18107
|
+
}
|
|
18108
|
+
const ACCEPTED_DRAG_TYPES = ["rich-drag-option"];
|
|
18109
|
+
const DropInput = ({
|
|
18110
|
+
widgetId,
|
|
18111
|
+
value,
|
|
18112
|
+
width,
|
|
18113
|
+
placeholder,
|
|
18114
|
+
validationStatus = null,
|
|
18115
|
+
option,
|
|
18116
|
+
widgets = {},
|
|
18117
|
+
onChange = () => {
|
|
18118
|
+
}
|
|
18119
|
+
}) => {
|
|
18120
|
+
const locked = useExerciseLocked();
|
|
18121
|
+
const dnd = useExerciseDndOptional();
|
|
18122
|
+
const isEmpty = value === "";
|
|
18123
|
+
const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
|
|
18124
|
+
const hasChip = !isEmpty && Boolean(option) && Boolean(dnd);
|
|
18125
|
+
const boxStyle = width !== void 0 ? { width: getInputBoxWidth(width) } : void 0;
|
|
18126
|
+
const handleDrop = useCallback(
|
|
18127
|
+
(item) => applyRichDropToInput(widgetId, item, onChange),
|
|
18128
|
+
[widgetId, onChange]
|
|
18129
|
+
);
|
|
18130
|
+
const handleRemove = useCallback(
|
|
18131
|
+
() => onChange(widgetId, "", false),
|
|
18132
|
+
[widgetId, onChange]
|
|
18133
|
+
);
|
|
18134
|
+
const placeholderText = placeholder ? /* @__PURE__ */ jsx("span", { className: "drop-input-placeholder", children: placeholder }) : null;
|
|
18135
|
+
const input = /* @__PURE__ */ jsx(
|
|
18136
|
+
"div",
|
|
18137
|
+
{
|
|
18138
|
+
className: `inline-widget drop-input${hasChip ? " drop-input--filled" : ` inline-input${statusClass}`}${isEmpty ? " drop-input--empty" : ""}${locked ? " drop-input--locked" : ""}`,
|
|
18139
|
+
style: hasChip ? void 0 : boxStyle,
|
|
18140
|
+
role: "group",
|
|
18141
|
+
"aria-label": placeholder ?? widgetId,
|
|
18142
|
+
"aria-disabled": locked ? "true" : void 0,
|
|
18143
|
+
"data-widget-id": widgetId,
|
|
18144
|
+
children: isEmpty ? placeholderText : option ? dnd ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18145
|
+
/* @__PURE__ */ jsx("span", { className: "inline-input drop-input-sizer", style: boxStyle, "aria-hidden": "true", children: placeholderText }),
|
|
18146
|
+
/* @__PURE__ */ jsx(
|
|
18147
|
+
PlacedRichOption,
|
|
18148
|
+
{
|
|
18149
|
+
inputName: widgetId,
|
|
18150
|
+
option,
|
|
18151
|
+
widgets,
|
|
18152
|
+
onRemove: handleRemove,
|
|
18153
|
+
className: statusClass.trim()
|
|
18154
|
+
}
|
|
18155
|
+
)
|
|
18156
|
+
] }) : /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets }) : dnd && !locked ? (
|
|
18157
|
+
// Value that resolves to no bank option (author answer/option-name
|
|
18158
|
+
// mismatch, legacy JSON). Keep it clearable so the field is not stuck.
|
|
18159
|
+
/* @__PURE__ */ jsx(
|
|
18160
|
+
"button",
|
|
18161
|
+
{
|
|
18162
|
+
type: "button",
|
|
18163
|
+
className: "drop-input-unresolved",
|
|
18164
|
+
onClick: (event) => {
|
|
18165
|
+
if (event.detail !== 0) handleRemove();
|
|
18166
|
+
},
|
|
18167
|
+
title: "Click to clear",
|
|
18168
|
+
children: value
|
|
18169
|
+
}
|
|
18170
|
+
)
|
|
18171
|
+
) : value
|
|
18172
|
+
}
|
|
18173
|
+
);
|
|
18174
|
+
if (!dnd) return input;
|
|
18175
|
+
return /* @__PURE__ */ jsx(
|
|
18176
|
+
Droppable,
|
|
18177
|
+
{
|
|
18178
|
+
id: widgetId,
|
|
18179
|
+
accepts: ACCEPTED_DRAG_TYPES,
|
|
18180
|
+
onDrop: handleDrop,
|
|
18181
|
+
disabled: locked,
|
|
18182
|
+
className: "drop-input-droppable",
|
|
18183
|
+
children: input
|
|
18184
|
+
}
|
|
18185
|
+
);
|
|
18186
|
+
};
|
|
18187
|
+
const NumericKeypad = ({
|
|
18188
|
+
value,
|
|
18189
|
+
includeOperators = false,
|
|
18190
|
+
includeDecimal = false,
|
|
18191
|
+
onChange
|
|
18192
|
+
}) => {
|
|
18193
|
+
const playSound = useSound();
|
|
18194
|
+
const locked = useExerciseLocked();
|
|
18195
|
+
const handleDigit = (digit) => {
|
|
18196
|
+
if (locked) return;
|
|
18197
|
+
playSound("keypad-press");
|
|
18198
|
+
onChange(value + digit);
|
|
18199
|
+
};
|
|
18200
|
+
const handleBackspace = () => {
|
|
18201
|
+
if (locked) return;
|
|
18202
|
+
playSound("keypad-press");
|
|
18203
|
+
onChange(value.slice(0, -1));
|
|
18204
|
+
};
|
|
18205
|
+
return /* @__PURE__ */ jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, "data-tutorial": "keypad", children: [
|
|
18206
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "1", onClick: () => handleDigit("1"), children: "1" }),
|
|
18207
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "2", onClick: () => handleDigit("2"), children: "2" }),
|
|
18208
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "3", onClick: () => handleDigit("3"), children: "3" }),
|
|
18209
|
+
includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "+", onClick: () => handleDigit("+"), children: "+" }),
|
|
18210
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "4", onClick: () => handleDigit("4"), children: "4" }),
|
|
18211
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "5", onClick: () => handleDigit("5"), children: "5" }),
|
|
18212
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "6", onClick: () => handleDigit("6"), children: "6" }),
|
|
18213
|
+
includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "-", onClick: () => handleDigit("-"), children: "-" }),
|
|
18214
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "7", onClick: () => handleDigit("7"), children: "7" }),
|
|
18215
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "8", onClick: () => handleDigit("8"), children: "8" }),
|
|
18216
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "9", onClick: () => handleDigit("9"), children: "9" }),
|
|
18217
|
+
includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "×", onClick: () => handleDigit("×"), children: "×" }),
|
|
18218
|
+
includeDecimal ? /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", "data-key": ",", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
|
|
18219
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "0", onClick: () => handleDigit("0"), children: "0" }),
|
|
18220
|
+
/* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", "data-key": "backspace", onClick: handleBackspace, children: "⌫" }),
|
|
18221
|
+
includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "÷", onClick: () => handleDigit("÷"), children: "÷" })
|
|
18222
|
+
] });
|
|
17477
18223
|
};
|
|
17478
18224
|
const FractionInput = ({
|
|
17479
18225
|
widgetId,
|
|
@@ -19276,41 +20022,6 @@ const MatcherWidget = ({
|
|
|
19276
20022
|
);
|
|
19277
20023
|
};
|
|
19278
20024
|
MatcherWidget.displayName = "MatcherWidget";
|
|
19279
|
-
function isArabic(text) {
|
|
19280
|
-
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
19281
|
-
}
|
|
19282
|
-
function hasLatinChars(text) {
|
|
19283
|
-
return /[A-Za-z]/.test(text);
|
|
19284
|
-
}
|
|
19285
|
-
function itemContainsArabic(item) {
|
|
19286
|
-
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
19287
|
-
return isArabic(item.value);
|
|
19288
|
-
}
|
|
19289
|
-
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
19290
|
-
return isArabic(item.value);
|
|
19291
|
-
}
|
|
19292
|
-
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
19293
|
-
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
19294
|
-
}
|
|
19295
|
-
return false;
|
|
19296
|
-
}
|
|
19297
|
-
function itemContainsLatin(item) {
|
|
19298
|
-
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
19299
|
-
return hasLatinChars(item.value);
|
|
19300
|
-
}
|
|
19301
|
-
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
19302
|
-
return hasLatinChars(item.value);
|
|
19303
|
-
}
|
|
19304
|
-
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
19305
|
-
return item.items.some((subItem) => itemContainsLatin(subItem));
|
|
19306
|
-
}
|
|
19307
|
-
return false;
|
|
19308
|
-
}
|
|
19309
|
-
function shouldItemsUseRTL(items) {
|
|
19310
|
-
const hasArabic = items.some(itemContainsArabic);
|
|
19311
|
-
const hasLatin = items.some(itemContainsLatin);
|
|
19312
|
-
return hasArabic && !hasLatin;
|
|
19313
|
-
}
|
|
19314
20025
|
function usePairLines({
|
|
19315
20026
|
containerRef,
|
|
19316
20027
|
pairs,
|
|
@@ -19724,9 +20435,15 @@ PairMatcherWidget.displayName = "PairMatcherWidget";
|
|
|
19724
20435
|
function isInlineInputWidget(widget) {
|
|
19725
20436
|
return widget.type === "inline-input";
|
|
19726
20437
|
}
|
|
20438
|
+
function isDropInputWidget(widget) {
|
|
20439
|
+
return widget.type === "drop-input";
|
|
20440
|
+
}
|
|
19727
20441
|
function isDragOptionsWidget(widget) {
|
|
19728
20442
|
return widget.type === "drag-options";
|
|
19729
20443
|
}
|
|
20444
|
+
function isDraggableOptionsWidget(widget) {
|
|
20445
|
+
return widget.type === "draggable-options";
|
|
20446
|
+
}
|
|
19730
20447
|
function isInlineDragOptionWidget(widget) {
|
|
19731
20448
|
return widget.type === "inline-drag-option";
|
|
19732
20449
|
}
|
|
@@ -19755,7 +20472,7 @@ function isGeometryWidget(widget) {
|
|
|
19755
20472
|
return widget.type === JSON_TYPES.GEOMETRY;
|
|
19756
20473
|
}
|
|
19757
20474
|
const GeometryWidget = React.lazy(async () => ({
|
|
19758
|
-
default: (await import("./GeometryWidget-
|
|
20475
|
+
default: (await import("./GeometryWidget-DZWM4LcD.mjs")).GeometryWidget
|
|
19759
20476
|
}));
|
|
19760
20477
|
const WidgetRenderer = ({
|
|
19761
20478
|
widget,
|
|
@@ -19793,6 +20510,23 @@ const WidgetRenderer = ({
|
|
|
19793
20510
|
}
|
|
19794
20511
|
);
|
|
19795
20512
|
}
|
|
20513
|
+
if (isDropInputWidget(widget)) {
|
|
20514
|
+
const optionsWidget = widgets?.[DRAGGABLE_OPTIONS_WIDGET_ID];
|
|
20515
|
+
const option = optionsWidget && isDraggableOptionsWidget(optionsWidget) ? optionsWidget.options.find((candidate) => candidate.name === strValue) : void 0;
|
|
20516
|
+
return /* @__PURE__ */ jsx(
|
|
20517
|
+
DropInput,
|
|
20518
|
+
{
|
|
20519
|
+
widgetId,
|
|
20520
|
+
value: strValue,
|
|
20521
|
+
width: widget.width,
|
|
20522
|
+
placeholder: widget.placeholder,
|
|
20523
|
+
validationStatus,
|
|
20524
|
+
option,
|
|
20525
|
+
widgets,
|
|
20526
|
+
onChange
|
|
20527
|
+
}
|
|
20528
|
+
);
|
|
20529
|
+
}
|
|
19796
20530
|
if (isDragOptionsWidget(widget)) {
|
|
19797
20531
|
return /* @__PURE__ */ jsx(
|
|
19798
20532
|
DragOptions,
|
|
@@ -19803,6 +20537,25 @@ const WidgetRenderer = ({
|
|
|
19803
20537
|
}
|
|
19804
20538
|
);
|
|
19805
20539
|
}
|
|
20540
|
+
if (isDraggableOptionsWidget(widget)) {
|
|
20541
|
+
const usedOptionNames = /* @__PURE__ */ new Set();
|
|
20542
|
+
if (!widget.reusable) {
|
|
20543
|
+
for (const [id, candidate] of Object.entries(widgets ?? {})) {
|
|
20544
|
+
const candidateValue = values[id];
|
|
20545
|
+
if (isDropInputWidget(candidate) && typeof candidateValue === "string" && candidateValue !== "") {
|
|
20546
|
+
usedOptionNames.add(candidateValue);
|
|
20547
|
+
}
|
|
20548
|
+
}
|
|
20549
|
+
}
|
|
20550
|
+
return /* @__PURE__ */ jsx(
|
|
20551
|
+
DraggableOptions,
|
|
20552
|
+
{
|
|
20553
|
+
widget,
|
|
20554
|
+
widgets: widgets ?? { [widgetId]: widget },
|
|
20555
|
+
usedOptionNames
|
|
20556
|
+
}
|
|
20557
|
+
);
|
|
20558
|
+
}
|
|
19806
20559
|
if (isInlineDragOptionWidget(widget)) {
|
|
19807
20560
|
return /* @__PURE__ */ jsx(
|
|
19808
20561
|
InlineDragOption,
|
|
@@ -20144,75 +20897,14 @@ const ImageItem = ({
|
|
|
20144
20897
|
ref: triggerRef,
|
|
20145
20898
|
type: "button",
|
|
20146
20899
|
className: "image-zoom-btn",
|
|
20147
|
-
"aria-label": alt ? `View "${alt}" fullscreen` : "View image fullscreen",
|
|
20148
|
-
onClick: open,
|
|
20149
|
-
children: /* @__PURE__ */ jsx(ExpandDiamondIcon, {})
|
|
20150
|
-
}
|
|
20151
|
-
),
|
|
20152
|
-
overlay
|
|
20153
|
-
] });
|
|
20154
|
-
};
|
|
20155
|
-
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
20156
|
-
const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
|
|
20157
|
-
const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
|
|
20158
|
-
const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
|
|
20159
|
-
const NUMBER_LINE_AXIS_ENDS = {
|
|
20160
|
-
"arrow-right": { left: "tick", right: "arrow" },
|
|
20161
|
-
"arrow-left": { left: "arrow", right: "tick" },
|
|
20162
|
-
"arrow-both": { left: "arrow", right: "arrow" },
|
|
20163
|
-
"line": { left: "line", right: "line" },
|
|
20164
|
-
"line-left": { left: "line", right: "arrow" }
|
|
20165
|
-
};
|
|
20166
|
-
const toPercent = (value, min, max) => {
|
|
20167
|
-
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
20168
|
-
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
20169
|
-
};
|
|
20170
|
-
const pixelToValue = (chipCenterX, axisLeft, axisWidth, min, max) => {
|
|
20171
|
-
const relX = (chipCenterX - axisLeft) / axisWidth;
|
|
20172
|
-
const fraction = (relX - SIDE_PAD / 100) / (1 - 2 * SIDE_PAD / 100);
|
|
20173
|
-
const clamped = Math.max(0, Math.min(1, fraction));
|
|
20174
|
-
return min + clamped * (max - min);
|
|
20175
|
-
};
|
|
20176
|
-
const getMajorTicks = (min, max, step) => {
|
|
20177
|
-
if (step <= 0 || max <= min) return [min, max];
|
|
20178
|
-
const count = Math.floor((max - min) / step);
|
|
20179
|
-
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
20180
|
-
const lastTick = ticks[ticks.length - 1];
|
|
20181
|
-
if (roundTick(lastTick) !== roundTick(max)) {
|
|
20182
|
-
ticks.push(max);
|
|
20183
|
-
} else {
|
|
20184
|
-
ticks[ticks.length - 1] = max;
|
|
20185
|
-
}
|
|
20186
|
-
return ticks;
|
|
20187
|
-
};
|
|
20188
|
-
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
20189
|
-
if (minorTicks <= 0 || majorTicks.length < 2) return [];
|
|
20190
|
-
const result = [];
|
|
20191
|
-
for (let i = 0; i < majorTicks.length - 1; i++) {
|
|
20192
|
-
const from = majorTicks[i];
|
|
20193
|
-
const to = majorTicks[i + 1];
|
|
20194
|
-
const interval = to - from;
|
|
20195
|
-
for (let j = 1; j <= minorTicks; j++) {
|
|
20196
|
-
result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
|
|
20197
|
-
}
|
|
20198
|
-
}
|
|
20199
|
-
return result;
|
|
20200
|
-
};
|
|
20201
|
-
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
20202
|
-
...majorTicks,
|
|
20203
|
-
...getMinorTickValues(majorTicks, minorTicks)
|
|
20204
|
-
];
|
|
20205
|
-
const snapToNearest = (raw, ticks) => {
|
|
20206
|
-
if (ticks.length === 0) return cleanFloat(raw);
|
|
20207
|
-
return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
|
|
20208
|
-
};
|
|
20209
|
-
const getPositionWidgetId = (content) => {
|
|
20210
|
-
for (const item of content) {
|
|
20211
|
-
if (isWidgetRef(item)) return item.widgetId;
|
|
20212
|
-
}
|
|
20213
|
-
return null;
|
|
20900
|
+
"aria-label": alt ? `View "${alt}" fullscreen` : "View image fullscreen",
|
|
20901
|
+
onClick: open,
|
|
20902
|
+
children: /* @__PURE__ */ jsx(ExpandDiamondIcon, {})
|
|
20903
|
+
}
|
|
20904
|
+
),
|
|
20905
|
+
overlay
|
|
20906
|
+
] });
|
|
20214
20907
|
};
|
|
20215
|
-
const toHighlightColor = (color) => color.startsWith("#") ? color : `var(--nl-highlight-${color})`;
|
|
20216
20908
|
const getBandGeometry = (h, min, max) => {
|
|
20217
20909
|
const leftPct = toPercent(Math.min(h.from, h.to), min, max);
|
|
20218
20910
|
const rightPct = toPercent(Math.max(h.from, h.to), min, max);
|
|
@@ -21669,63 +22361,6 @@ const ClockRenderer = ({
|
|
|
21669
22361
|
format === "12h" && /* @__PURE__ */ jsx("span", { className: "clock-period-badge", children: hours >= 12 ? "PM" : "AM" })
|
|
21670
22362
|
] });
|
|
21671
22363
|
};
|
|
21672
|
-
function isTextItem(item) {
|
|
21673
|
-
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;
|
|
21674
|
-
}
|
|
21675
|
-
function groupLineItems(items) {
|
|
21676
|
-
const groups = [];
|
|
21677
|
-
let currentTextGroup = [];
|
|
21678
|
-
for (const item of items) {
|
|
21679
|
-
if (isTextItem(item)) {
|
|
21680
|
-
currentTextGroup.push(item);
|
|
21681
|
-
} else {
|
|
21682
|
-
if (currentTextGroup.length > 0) {
|
|
21683
|
-
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
21684
|
-
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
21685
|
-
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
21686
|
-
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
21687
|
-
currentTextGroup = [];
|
|
21688
|
-
}
|
|
21689
|
-
groups.push({ items: [item], isTextGroup: false, hasArabic: false, hasLatin: false, shouldApplyRTL: false });
|
|
21690
|
-
}
|
|
21691
|
-
}
|
|
21692
|
-
if (currentTextGroup.length > 0) {
|
|
21693
|
-
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
21694
|
-
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
21695
|
-
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
21696
|
-
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
21697
|
-
}
|
|
21698
|
-
return groups;
|
|
21699
|
-
}
|
|
21700
|
-
function placeTableCells(rows, cols) {
|
|
21701
|
-
const placements = /* @__PURE__ */ new Map();
|
|
21702
|
-
const occupied = rows.map(() => []);
|
|
21703
|
-
const totalRows = rows.length;
|
|
21704
|
-
rows.forEach((row, rowIndex) => {
|
|
21705
|
-
const taken = occupied[rowIndex] ?? [];
|
|
21706
|
-
let col = 0;
|
|
21707
|
-
for (const cell of row.cells) {
|
|
21708
|
-
while (taken[col]) col++;
|
|
21709
|
-
const colspan = cell.colspan || 1;
|
|
21710
|
-
const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
|
|
21711
|
-
for (let r = rowIndex; r < rowIndex + rowspan; r++) {
|
|
21712
|
-
const line = occupied[r];
|
|
21713
|
-
if (!line) continue;
|
|
21714
|
-
for (let c = col; c < col + colspan; c++) line[c] = true;
|
|
21715
|
-
}
|
|
21716
|
-
placements.set(cell.id, {
|
|
21717
|
-
column: col + 1,
|
|
21718
|
-
row: rowIndex + 1,
|
|
21719
|
-
colspan,
|
|
21720
|
-
rowspan,
|
|
21721
|
-
isLastColumn: col + colspan >= cols,
|
|
21722
|
-
isLastRow: rowIndex + rowspan >= totalRows
|
|
21723
|
-
});
|
|
21724
|
-
col += colspan;
|
|
21725
|
-
}
|
|
21726
|
-
});
|
|
21727
|
-
return placements;
|
|
21728
|
-
}
|
|
21729
22364
|
const toValidationStatus = (v) => v === true ? "correct" : v === false ? "incorrect" : null;
|
|
21730
22365
|
const isNumericText = (value) => /^[\d.,\s]+$/.test(value);
|
|
21731
22366
|
const numberClass = (value) => isNumericText(value) ? "text-item-number" : "";
|
|
@@ -22173,9 +22808,11 @@ function ChameleonHand({ className }) {
|
|
|
22173
22808
|
);
|
|
22174
22809
|
}
|
|
22175
22810
|
const TUTORIAL_HAND_TIP = { x: 56 / 75, y: 22 / 78 };
|
|
22811
|
+
const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
22176
22812
|
const CYCLE_MS = 1400;
|
|
22177
22813
|
const PRESS_AT_MS = 650;
|
|
22178
22814
|
const PRESS_HOLD_MS = 220;
|
|
22815
|
+
const MAX_SPEECH_MS = 3e4;
|
|
22179
22816
|
const HOLE_PAD = 10;
|
|
22180
22817
|
const HOLE_RADIUS = 18;
|
|
22181
22818
|
const HAND_W = 72;
|
|
@@ -22193,7 +22830,6 @@ function toRect(t) {
|
|
|
22193
22830
|
if (r.width <= 0 || r.height <= 0) return null;
|
|
22194
22831
|
return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) };
|
|
22195
22832
|
}
|
|
22196
|
-
const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
22197
22833
|
const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
|
|
22198
22834
|
const { steps } = tutorial;
|
|
22199
22835
|
const [stepIndex, setStepIndex] = useState(
|
|
@@ -22269,7 +22905,9 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
|
|
|
22269
22905
|
const [completedStep, setCompletedStep] = useState(null);
|
|
22270
22906
|
useEffect(() => {
|
|
22271
22907
|
optionsRef.current.onStep?.(stepIndex, step.id);
|
|
22272
|
-
const speech = Promise
|
|
22908
|
+
const speech = new Promise((resolve2) => {
|
|
22909
|
+
resolve2(optionsRef.current.onSpeak?.(step.id, textRef.current));
|
|
22910
|
+
}).catch(() => {
|
|
22273
22911
|
});
|
|
22274
22912
|
if (!autoAdvance) return;
|
|
22275
22913
|
const duration = Math.max(2, step.taps(ctxRef.current).length) * CYCLE_MS;
|
|
@@ -22278,7 +22916,11 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
|
|
|
22278
22916
|
const animation = new Promise((resolve2) => {
|
|
22279
22917
|
timer = setTimeout(resolve2, duration);
|
|
22280
22918
|
});
|
|
22281
|
-
|
|
22919
|
+
let speechTimer;
|
|
22920
|
+
const speechLimit = new Promise((resolve2) => {
|
|
22921
|
+
speechTimer = setTimeout(resolve2, MAX_SPEECH_MS);
|
|
22922
|
+
});
|
|
22923
|
+
void Promise.all([Promise.race([speech, speechLimit]), animation]).then(() => {
|
|
22282
22924
|
if (cancelled) return;
|
|
22283
22925
|
if (isLast) {
|
|
22284
22926
|
if (showBubble) setCompletedStep(stepIndex);
|
|
@@ -22288,6 +22930,7 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
|
|
|
22288
22930
|
return () => {
|
|
22289
22931
|
cancelled = true;
|
|
22290
22932
|
clearTimeout(timer);
|
|
22933
|
+
clearTimeout(speechTimer);
|
|
22291
22934
|
};
|
|
22292
22935
|
}, [stepIndex, step, autoAdvance, isLast, showBubble]);
|
|
22293
22936
|
const showNext = !autoAdvance || isLast && completedStep === stepIndex;
|
|
@@ -22443,7 +23086,7 @@ function key(ctx, k) {
|
|
|
22443
23086
|
}
|
|
22444
23087
|
function answerBox(ctx) {
|
|
22445
23088
|
const inputs = getTextInputs(ctx.root);
|
|
22446
|
-
return inputs.find(canTakeInput) ?? inputs
|
|
23089
|
+
return inputs.find(canTakeInput) ?? inputs.find(canAdvanceTo) ?? null;
|
|
22447
23090
|
}
|
|
22448
23091
|
function answerDigits(ctx) {
|
|
22449
23092
|
const id = answerBox(ctx)?.id.replace(/-input$/, "");
|
|
@@ -22480,7 +23123,6 @@ const StandInFill = ({ box, slots, filled, digits }) => /* @__PURE__ */ jsx(
|
|
|
22480
23123
|
children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? digits?.[i] ?? "●" : "_" }, i))
|
|
22481
23124
|
}
|
|
22482
23125
|
);
|
|
22483
|
-
const center = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
22484
23126
|
const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
|
|
22485
23127
|
const box = spotlight[1];
|
|
22486
23128
|
if (!box) return null;
|
|
@@ -22490,8 +23132,8 @@ const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
|
|
|
22490
23132
|
const from = tapCount > 0 ? taps[(tapCount - 1) % taps.length] : null;
|
|
22491
23133
|
let arc = null;
|
|
22492
23134
|
if (from) {
|
|
22493
|
-
const a =
|
|
22494
|
-
const b =
|
|
23135
|
+
const a = centerOf(from);
|
|
23136
|
+
const b = centerOf(box);
|
|
22495
23137
|
const lift = Math.max(40, Math.hypot(b.x - a.x, b.y - a.y) * 0.35);
|
|
22496
23138
|
arc = `M ${a.x} ${a.y} Q ${(a.x + b.x) / 2} ${Math.min(a.y, b.y) - lift} ${b.x} ${b.y}`;
|
|
22497
23139
|
}
|
|
@@ -22554,29 +23196,72 @@ const WIDGET_TUTORIALS = [keypadTutorial];
|
|
|
22554
23196
|
function findWidgetTutorial(json) {
|
|
22555
23197
|
return WIDGET_TUTORIALS.find((t) => t.appliesTo(json)) ?? null;
|
|
22556
23198
|
}
|
|
23199
|
+
function hasMatchingLength(value, expected) {
|
|
23200
|
+
return (typeof expected === "string" || typeof expected === "number") && (typeof value === "string" || typeof value === "number") && String(value).length > 0 && String(value).length === String(expected).length;
|
|
23201
|
+
}
|
|
23202
|
+
function isSelectionGroups(value) {
|
|
23203
|
+
return Array.isArray(value) && value.every(
|
|
23204
|
+
(group) => Array.isArray(group) && group.length > 0 && group.every((id) => typeof id === "string")
|
|
23205
|
+
);
|
|
23206
|
+
}
|
|
23207
|
+
function hasMatchingSelectionCount(value, expected, itemIds, pairsOnly) {
|
|
23208
|
+
if (!isSelectionGroups(value) || !isSelectionGroups(expected)) return false;
|
|
23209
|
+
if (pairsOnly && (value.some((group) => group.length !== 2) || expected.some((group) => group.length !== 2)))
|
|
23210
|
+
return false;
|
|
23211
|
+
const selected = value.flat();
|
|
23212
|
+
const requiredCount = expected.flat().length;
|
|
23213
|
+
const declared = new Set(itemIds);
|
|
23214
|
+
return requiredCount > 0 && selected.length === requiredCount && new Set(selected).size === selected.length && selected.every((id) => declared.has(id));
|
|
23215
|
+
}
|
|
22557
23216
|
function getAnswerLengthCompletion(json, answerable, values, touched) {
|
|
22558
23217
|
const fields = [];
|
|
23218
|
+
const addTypedField = (id, widgetId, expected) => {
|
|
23219
|
+
const value = values[id];
|
|
23220
|
+
const hasKnownLength = typeof expected === "string" || typeof expected === "number";
|
|
23221
|
+
const hasScalarValue = typeof value === "string" || typeof value === "number";
|
|
23222
|
+
fields.push({
|
|
23223
|
+
id,
|
|
23224
|
+
widgetId,
|
|
23225
|
+
matches: hasMatchingLength(value, expected),
|
|
23226
|
+
// Unknown answer lengths use normal completion; known ones use grading's normalization.
|
|
23227
|
+
guideMatches: !hasKnownLength || touched.has(widgetId) && hasScalarValue && hasMatchingLength(normalize(value), normalize(expected))
|
|
23228
|
+
});
|
|
23229
|
+
};
|
|
22559
23230
|
for (const widgetId of answerable) {
|
|
22560
23231
|
const widget = json.widgets[widgetId];
|
|
22561
23232
|
const expected = json.validation?.answers?.[widgetId];
|
|
22562
23233
|
if (widget?.type === JSON_TYPES.INLINE_INPUT && !widget.disabled) {
|
|
22563
|
-
|
|
23234
|
+
addTypedField(widgetId, widgetId, expected);
|
|
22564
23235
|
} else if (widget?.type === JSON_TYPES.FRACTION) {
|
|
22565
23236
|
const parts = typeof expected === "string" ? expected.split("/") : [];
|
|
22566
23237
|
const validParts = parts.length === 2;
|
|
22567
23238
|
if (widget.numerator === null) {
|
|
22568
|
-
|
|
23239
|
+
const id = getFractionKey(widgetId, "num");
|
|
23240
|
+
addTypedField(id, widgetId, validParts ? parts[0] : void 0);
|
|
22569
23241
|
}
|
|
22570
23242
|
if (widget.denominator === null) {
|
|
22571
|
-
|
|
23243
|
+
const id = getFractionKey(widgetId, "den");
|
|
23244
|
+
addTypedField(id, widgetId, validParts ? parts[1] : void 0);
|
|
22572
23245
|
}
|
|
23246
|
+
} else if (widget?.type === JSON_TYPES.MATCHER || widget?.type === JSON_TYPES.PAIR_MATCHER) {
|
|
23247
|
+
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));
|
|
23248
|
+
const matches = hasMatchingSelectionCount(
|
|
23249
|
+
values[widgetId],
|
|
23250
|
+
expected,
|
|
23251
|
+
itemIds,
|
|
23252
|
+
widget.type === JSON_TYPES.PAIR_MATCHER
|
|
23253
|
+
);
|
|
23254
|
+
fields.push({
|
|
23255
|
+
id: widgetId,
|
|
23256
|
+
widgetId,
|
|
23257
|
+
matches,
|
|
23258
|
+
guideMatches: expected == null || touched.has(widgetId) && matches
|
|
23259
|
+
});
|
|
22573
23260
|
}
|
|
22574
23261
|
}
|
|
22575
|
-
const answerLengthMatchedIds = fields.filter(({
|
|
22576
|
-
const value = values[id];
|
|
22577
|
-
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;
|
|
22578
|
-
}).map(({ id }) => id);
|
|
23262
|
+
const answerLengthMatchedIds = fields.filter(({ widgetId, matches }) => touched.has(widgetId) && matches).map(({ id }) => id);
|
|
22579
23263
|
return {
|
|
23264
|
+
areSubmitGuideCountsMatched: fields.every(({ guideMatches }) => guideMatches),
|
|
22580
23265
|
isAnswerLengthMatched: fields.length > 0 && answerLengthMatchedIds.length === fields.length,
|
|
22581
23266
|
answerLengthMatchedIds
|
|
22582
23267
|
};
|
|
@@ -22602,12 +23287,9 @@ function buildInitialValues(json) {
|
|
|
22602
23287
|
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) {
|
|
22603
23288
|
initial[id] = [];
|
|
22604
23289
|
} else if (widget.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
22605
|
-
initial[id] = widget.cells.filter(
|
|
22606
|
-
cell,
|
|
22607
|
-
|
|
22608
|
-
widget.cols,
|
|
22609
|
-
widget.interactiveArea
|
|
22610
|
-
));
|
|
23290
|
+
initial[id] = widget.cells.filter(
|
|
23291
|
+
(cell) => isCellInInteractiveArea(cell, widget.rows, widget.cols, widget.interactiveArea)
|
|
23292
|
+
);
|
|
22611
23293
|
} else if (widget.type === JSON_TYPES.CHART_BAR || widget.type === JSON_TYPES.CHART_PICTO) {
|
|
22612
23294
|
initial[id] = String(widget.value);
|
|
22613
23295
|
} else if (widget.type === JSON_TYPES.GEOMETRY) {
|
|
@@ -22646,8 +23328,10 @@ function buildRevealedValues(json) {
|
|
|
22646
23328
|
const widget = json.widgets[widgetId];
|
|
22647
23329
|
if (widget?.type === JSON_TYPES.FRACTION && typeof expected === "string") {
|
|
22648
23330
|
const [num, den] = expected.split("/", 2);
|
|
22649
|
-
if (widget.numerator === null && num !== void 0)
|
|
22650
|
-
|
|
23331
|
+
if (widget.numerator === null && num !== void 0)
|
|
23332
|
+
revealed[getFractionKey(widgetId, "num")] = num;
|
|
23333
|
+
if (widget.denominator === null && den !== void 0)
|
|
23334
|
+
revealed[getFractionKey(widgetId, "den")] = den;
|
|
22651
23335
|
continue;
|
|
22652
23336
|
}
|
|
22653
23337
|
revealed[widgetId] = typeof expected === "number" ? String(expected) : expected;
|
|
@@ -22679,449 +23363,571 @@ function buildRevealedDropSources(json, revealedValues) {
|
|
|
22679
23363
|
const pool = [];
|
|
22680
23364
|
const bank = json.widgets[DRAG_OPTIONS_WIDGET_ID];
|
|
22681
23365
|
if (bank?.type === JSON_TYPES.DRAG_OPTIONS) {
|
|
22682
|
-
bank.options.forEach(
|
|
23366
|
+
bank.options.forEach(
|
|
23367
|
+
(value, idx) => pool.push({ optionId: `drag-option-${idx}`, value: String(value) })
|
|
23368
|
+
);
|
|
22683
23369
|
}
|
|
22684
23370
|
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
22685
|
-
if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION)
|
|
23371
|
+
if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION)
|
|
23372
|
+
pool.push({ optionId: widgetId, value: widget.value });
|
|
22686
23373
|
}
|
|
22687
23374
|
const sources = /* @__PURE__ */ new Map();
|
|
22688
23375
|
if (pool.length === 0) return sources;
|
|
22689
23376
|
const used = /* @__PURE__ */ new Set();
|
|
22690
23377
|
for (const [inputId, value] of Object.entries(revealedValues)) {
|
|
22691
23378
|
if (typeof value !== "string" || value === "") continue;
|
|
22692
|
-
|
|
23379
|
+
if (json.widgets[inputId]?.type !== JSON_TYPES.INLINE_INPUT && !isFractionKey(inputId))
|
|
23380
|
+
continue;
|
|
23381
|
+
const match = pool.find(
|
|
23382
|
+
(o) => !used.has(o.optionId) && normalize(o.value) === normalize(value)
|
|
23383
|
+
);
|
|
22693
23384
|
if (!match) continue;
|
|
22694
23385
|
used.add(match.optionId);
|
|
22695
23386
|
sources.set(inputId, match.optionId);
|
|
22696
23387
|
}
|
|
22697
23388
|
return sources;
|
|
22698
23389
|
}
|
|
22699
|
-
const ExerciseRenderer = forwardRef(
|
|
22700
|
-
|
|
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
|
-
const
|
|
22726
|
-
|
|
22727
|
-
|
|
22728
|
-
|
|
22729
|
-
|
|
22730
|
-
|
|
22731
|
-
|
|
22732
|
-
|
|
22733
|
-
|
|
22734
|
-
|
|
22735
|
-
|
|
22736
|
-
|
|
22737
|
-
|
|
22738
|
-
|
|
22739
|
-
|
|
22740
|
-
|
|
22741
|
-
setResetKey((k) => k + 1);
|
|
22742
|
-
setLoadedValuesSignature(initialValuesSignature);
|
|
22743
|
-
touchedRef.current = /* @__PURE__ */ new Set();
|
|
22744
|
-
setTutorialRun(null);
|
|
22745
|
-
}, [json, initialValuesSignature, loadedValuesSignature]);
|
|
22746
|
-
const locked = validationDetails !== null || externallyLocked;
|
|
22747
|
-
const lastResultRef = useRef(null);
|
|
22748
|
-
const runSubmit = () => {
|
|
22749
|
-
if (locked) return lastResultRef.current ?? validate(json, values);
|
|
22750
|
-
const result = validate(json, values);
|
|
22751
|
-
playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
|
|
22752
|
-
const details = { ...result.details };
|
|
22753
|
-
const slotValidation = (id, raw, expected) => ({
|
|
22754
|
-
widgetId: id,
|
|
22755
|
-
userValue: typeof raw === "string" ? raw : void 0,
|
|
22756
|
-
isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
|
|
22757
|
-
});
|
|
22758
|
-
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
22759
|
-
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
22760
|
-
if (!(widgetId in details)) continue;
|
|
22761
|
-
const answer = json.validation?.answers?.[widgetId];
|
|
22762
|
-
if (!answer) continue;
|
|
22763
|
-
const [expectedNum, expectedDen] = answer.split("/", 2);
|
|
22764
|
-
if (!expectedNum || !expectedDen) continue;
|
|
22765
|
-
if (details[widgetId]?.isCorrect) {
|
|
22766
|
-
if (widget.numerator === null) details[`${widgetId}__num`] = { widgetId: `${widgetId}__num`, userValue: void 0, isCorrect: true };
|
|
22767
|
-
if (widget.denominator === null) details[`${widgetId}__den`] = { widgetId: `${widgetId}__den`, userValue: void 0, isCorrect: true };
|
|
22768
|
-
} else {
|
|
22769
|
-
if (widget.numerator === null) details[`${widgetId}__num`] = slotValidation(`${widgetId}__num`, values[`${widgetId}__num`], expectedNum);
|
|
22770
|
-
if (widget.denominator === null) details[`${widgetId}__den`] = slotValidation(`${widgetId}__den`, values[`${widgetId}__den`], expectedDen);
|
|
22771
|
-
}
|
|
22772
|
-
}
|
|
22773
|
-
setValidationDetails(details);
|
|
22774
|
-
onSubmit?.(values);
|
|
22775
|
-
lastResultRef.current = result;
|
|
22776
|
-
return result;
|
|
22777
|
-
};
|
|
22778
|
-
useImperativeHandle(ref, () => ({
|
|
22779
|
-
submit: runSubmit,
|
|
22780
|
-
getValues: () => values,
|
|
22781
|
-
getCompletion: () => {
|
|
22782
|
-
const answerable = getAnswerableInputs(json);
|
|
22783
|
-
const missingSet = new Set(getMissingInputs(json, valuesRef.current));
|
|
22784
|
-
for (const id of answerable) {
|
|
22785
|
-
if (!touchedRef.current.has(id)) missingSet.add(id);
|
|
22786
|
-
}
|
|
22787
|
-
const missing = [...missingSet];
|
|
22788
|
-
const filledIds = answerable.filter((id) => !missingSet.has(id));
|
|
22789
|
-
return {
|
|
22790
|
-
total: answerable.length,
|
|
22791
|
-
filled: filledIds.length,
|
|
22792
|
-
missing,
|
|
22793
|
-
filledIds,
|
|
22794
|
-
isComplete: answerable.length > 0 && missing.length === 0,
|
|
22795
|
-
isAnyFilled: filledIds.length > 0,
|
|
22796
|
-
...getAnswerLengthCompletion(json, answerable, valuesRef.current, touchedRef.current)
|
|
22797
|
-
};
|
|
22798
|
-
},
|
|
22799
|
-
reset: () => {
|
|
22800
|
-
const initial = buildInitialValues(json);
|
|
22801
|
-
valuesRef.current = initial;
|
|
22802
|
-
setValues(initial);
|
|
23390
|
+
const ExerciseRenderer = forwardRef(
|
|
23391
|
+
({
|
|
23392
|
+
json: suppliedJson,
|
|
23393
|
+
onSubmit,
|
|
23394
|
+
onChange,
|
|
23395
|
+
showSubmitButton = false,
|
|
23396
|
+
className = "",
|
|
23397
|
+
soundEnabled = true,
|
|
23398
|
+
sounds,
|
|
23399
|
+
theme
|
|
23400
|
+
}, ref) => {
|
|
23401
|
+
const json = useMemo(() => withDisabledPrefilledInputs(suppliedJson), [suppliedJson]);
|
|
23402
|
+
const initialValuesSignature = useMemo(() => buildInitialValuesSignature(json), [json]);
|
|
23403
|
+
const [values, setValues] = useState(
|
|
23404
|
+
() => buildInitialValues(json)
|
|
23405
|
+
);
|
|
23406
|
+
const valuesRef = useRef(values);
|
|
23407
|
+
valuesRef.current = values;
|
|
23408
|
+
const touchedRef = useRef(/* @__PURE__ */ new Set());
|
|
23409
|
+
const [loadedValuesSignature, setLoadedValuesSignature] = useState(initialValuesSignature);
|
|
23410
|
+
const [droppedSources, setDroppedSources] = useState(/* @__PURE__ */ new Map());
|
|
23411
|
+
const [focusedInputId, setFocusedInputId] = useState(null);
|
|
23412
|
+
const [validationDetails, setValidationDetails] = useState(null);
|
|
23413
|
+
const rootRef = useRef(null);
|
|
23414
|
+
const [resetKey, setResetKey] = useState(0);
|
|
23415
|
+
const [revealed, setRevealed] = useState(false);
|
|
23416
|
+
const tutorial = useMemo(() => findWidgetTutorial(json), [json]);
|
|
23417
|
+
const [tutorialRun, setTutorialRun] = useState(null);
|
|
23418
|
+
const themeStyle = useMemo(() => {
|
|
23419
|
+
const style = {};
|
|
23420
|
+
if (theme?.correct) style["--exercise-correct"] = theme.correct;
|
|
23421
|
+
if (theme?.incorrect) style["--exercise-incorrect"] = theme.incorrect;
|
|
23422
|
+
return Object.keys(style).length > 0 ? style : void 0;
|
|
23423
|
+
}, [theme?.correct, theme?.incorrect]);
|
|
23424
|
+
useEffect(() => {
|
|
23425
|
+
warnOnInvalidThemeColors({ correct: theme?.correct, incorrect: theme?.incorrect });
|
|
23426
|
+
}, [theme?.correct, theme?.incorrect]);
|
|
23427
|
+
const playSound = useSoundPlayer(soundEnabled, sounds);
|
|
23428
|
+
const externallyLocked = useExerciseLocked();
|
|
23429
|
+
useLayoutEffect(() => {
|
|
23430
|
+
if (loadedValuesSignature === initialValuesSignature) return;
|
|
23431
|
+
setValues(buildInitialValues(json));
|
|
22803
23432
|
setDroppedSources(/* @__PURE__ */ new Map());
|
|
22804
23433
|
setFocusedInputId(null);
|
|
22805
23434
|
setValidationDetails(null);
|
|
22806
23435
|
setResetKey((k) => k + 1);
|
|
22807
|
-
setRevealed(false);
|
|
22808
23436
|
setLoadedValuesSignature(initialValuesSignature);
|
|
22809
23437
|
touchedRef.current = /* @__PURE__ */ new Set();
|
|
22810
|
-
|
|
22811
|
-
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
|
|
22817
|
-
|
|
22818
|
-
|
|
22819
|
-
|
|
22820
|
-
|
|
23438
|
+
setTutorialRun(null);
|
|
23439
|
+
}, [json, initialValuesSignature, loadedValuesSignature]);
|
|
23440
|
+
const locked = validationDetails !== null || externallyLocked;
|
|
23441
|
+
const lastResultRef = useRef(null);
|
|
23442
|
+
const runSubmit = () => {
|
|
23443
|
+
if (locked) return lastResultRef.current ?? validate(json, values);
|
|
23444
|
+
const result = validate(json, values);
|
|
23445
|
+
playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
|
|
23446
|
+
const details = { ...result.details };
|
|
23447
|
+
const slotValidation = (id, raw, expected) => ({
|
|
23448
|
+
widgetId: id,
|
|
23449
|
+
userValue: typeof raw === "string" ? raw : void 0,
|
|
23450
|
+
isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
|
|
23451
|
+
});
|
|
23452
|
+
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
23453
|
+
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
23454
|
+
if (!(widgetId in details)) continue;
|
|
23455
|
+
const answer = json.validation?.answers?.[widgetId];
|
|
23456
|
+
if (!answer) continue;
|
|
23457
|
+
const [expectedNum, expectedDen] = answer.split("/", 2);
|
|
23458
|
+
if (!expectedNum || !expectedDen) continue;
|
|
23459
|
+
if (details[widgetId]?.isCorrect) {
|
|
23460
|
+
if (widget.numerator === null)
|
|
23461
|
+
details[`${widgetId}__num`] = {
|
|
23462
|
+
widgetId: `${widgetId}__num`,
|
|
23463
|
+
userValue: void 0,
|
|
23464
|
+
isCorrect: true
|
|
23465
|
+
};
|
|
23466
|
+
if (widget.denominator === null)
|
|
23467
|
+
details[`${widgetId}__den`] = {
|
|
23468
|
+
widgetId: `${widgetId}__den`,
|
|
23469
|
+
userValue: void 0,
|
|
23470
|
+
isCorrect: true
|
|
23471
|
+
};
|
|
22821
23472
|
} else {
|
|
22822
|
-
|
|
23473
|
+
if (widget.numerator === null)
|
|
23474
|
+
details[`${widgetId}__num`] = slotValidation(
|
|
23475
|
+
`${widgetId}__num`,
|
|
23476
|
+
values[`${widgetId}__num`],
|
|
23477
|
+
expectedNum
|
|
23478
|
+
);
|
|
23479
|
+
if (widget.denominator === null)
|
|
23480
|
+
details[`${widgetId}__den`] = slotValidation(
|
|
23481
|
+
`${widgetId}__den`,
|
|
23482
|
+
values[`${widgetId}__den`],
|
|
23483
|
+
expectedDen
|
|
23484
|
+
);
|
|
22823
23485
|
}
|
|
23486
|
+
}
|
|
23487
|
+
setValidationDetails(details);
|
|
23488
|
+
onSubmit?.(values);
|
|
23489
|
+
lastResultRef.current = result;
|
|
23490
|
+
return result;
|
|
23491
|
+
};
|
|
23492
|
+
useImperativeHandle(
|
|
23493
|
+
ref,
|
|
23494
|
+
() => ({
|
|
23495
|
+
submit: runSubmit,
|
|
23496
|
+
getValues: () => values,
|
|
23497
|
+
getCompletion: () => {
|
|
23498
|
+
const answerable = getAnswerableInputs(json);
|
|
23499
|
+
const missingSet = new Set(getMissingInputs(json, valuesRef.current));
|
|
23500
|
+
for (const id of answerable) {
|
|
23501
|
+
if (!touchedRef.current.has(id)) missingSet.add(id);
|
|
23502
|
+
}
|
|
23503
|
+
const missing = [...missingSet];
|
|
23504
|
+
const filledIds = answerable.filter((id) => !missingSet.has(id));
|
|
23505
|
+
const isComplete = answerable.length > 0 && missing.length === 0;
|
|
23506
|
+
const { areSubmitGuideCountsMatched, ...answerLengthCompletion } = getAnswerLengthCompletion(json, answerable, valuesRef.current, touchedRef.current);
|
|
23507
|
+
return {
|
|
23508
|
+
total: answerable.length,
|
|
23509
|
+
filled: filledIds.length,
|
|
23510
|
+
missing,
|
|
23511
|
+
filledIds,
|
|
23512
|
+
isComplete,
|
|
23513
|
+
isSubmitGuideReady: isComplete && areSubmitGuideCountsMatched,
|
|
23514
|
+
isAnyFilled: filledIds.length > 0,
|
|
23515
|
+
...answerLengthCompletion
|
|
23516
|
+
};
|
|
23517
|
+
},
|
|
23518
|
+
reset: () => {
|
|
23519
|
+
const initial = buildInitialValues(json);
|
|
23520
|
+
valuesRef.current = initial;
|
|
23521
|
+
setValues(initial);
|
|
23522
|
+
setDroppedSources(/* @__PURE__ */ new Map());
|
|
23523
|
+
setFocusedInputId(null);
|
|
23524
|
+
setValidationDetails(null);
|
|
23525
|
+
setResetKey((k) => k + 1);
|
|
23526
|
+
setRevealed(false);
|
|
23527
|
+
setLoadedValuesSignature(initialValuesSignature);
|
|
23528
|
+
touchedRef.current = /* @__PURE__ */ new Set();
|
|
23529
|
+
},
|
|
23530
|
+
revealAnswers: () => {
|
|
23531
|
+
const revealedValues = buildRevealedValues(json);
|
|
23532
|
+
if (Object.keys(revealedValues).length === 0) return;
|
|
23533
|
+
playSound("answer-reveal");
|
|
23534
|
+
const details = {};
|
|
23535
|
+
for (const [widgetId, userValue] of Object.entries(revealedValues)) {
|
|
23536
|
+
const widget = json.widgets[widgetId];
|
|
23537
|
+
if (widget?.type === JSON_TYPES.GEOMETRY) {
|
|
23538
|
+
const { isCorrect, result } = checkGeometryWidget(widget.json, userValue, widgetId);
|
|
23539
|
+
details[widgetId] = { widgetId, userValue, isCorrect, geometryFeedback: result };
|
|
23540
|
+
} else {
|
|
23541
|
+
details[widgetId] = { widgetId, userValue, isCorrect: true };
|
|
23542
|
+
}
|
|
23543
|
+
touchedRef.current.add(toBaseId(widgetId));
|
|
23544
|
+
}
|
|
23545
|
+
const next = { ...valuesRef.current, ...revealedValues };
|
|
23546
|
+
valuesRef.current = next;
|
|
23547
|
+
setValues(next);
|
|
23548
|
+
setDroppedSources(buildRevealedDropSources(json, revealedValues));
|
|
23549
|
+
setValidationDetails(details);
|
|
23550
|
+
setRevealed(true);
|
|
23551
|
+
setResetKey((k) => k + 1);
|
|
23552
|
+
},
|
|
23553
|
+
focusFirstInput: () => {
|
|
23554
|
+
const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
|
|
23555
|
+
if (!container) return;
|
|
23556
|
+
const candidates = getTextInputs(container);
|
|
23557
|
+
const target = candidates.find(canTakeInput) ?? // A locked exercise is read-only on purpose — nothing to focus until
|
|
23558
|
+
// reset(). Unlocked, a read-only field is not the lock, so refusing to
|
|
23559
|
+
// focus it would strand the student with no caret; take the first one
|
|
23560
|
+
// that isn't disabled or a chip's invisible SlotSizer.
|
|
23561
|
+
(locked ? void 0 : candidates.find(canAdvanceTo));
|
|
23562
|
+
target?.focus();
|
|
23563
|
+
},
|
|
23564
|
+
hasTutorial: () => tutorial !== null,
|
|
23565
|
+
tutorialId: () => tutorial?.id ?? null,
|
|
23566
|
+
startTutorial: (options = {}) => {
|
|
23567
|
+
if (!tutorial || !rootRef.current) return false;
|
|
23568
|
+
setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
|
|
23569
|
+
return true;
|
|
23570
|
+
},
|
|
23571
|
+
stopTutorial: () => setTutorialRun(null)
|
|
23572
|
+
}),
|
|
23573
|
+
[json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]
|
|
23574
|
+
);
|
|
23575
|
+
const focusNextInput = (currentKey) => {
|
|
23576
|
+
if (!rootRef.current) return;
|
|
23577
|
+
findNextInput(rootRef.current, currentKey)?.focus();
|
|
23578
|
+
};
|
|
23579
|
+
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
|
|
23580
|
+
if (locked) return;
|
|
23581
|
+
if (!silent) {
|
|
23582
|
+
setRevealed(false);
|
|
22824
23583
|
touchedRef.current.add(toBaseId(widgetId));
|
|
22825
23584
|
}
|
|
22826
|
-
const
|
|
23585
|
+
const previous = valuesRef.current[widgetId];
|
|
23586
|
+
const next = { ...valuesRef.current, [widgetId]: value };
|
|
22827
23587
|
valuesRef.current = next;
|
|
22828
23588
|
setValues(next);
|
|
22829
|
-
setDroppedSources(
|
|
22830
|
-
|
|
22831
|
-
|
|
22832
|
-
|
|
22833
|
-
|
|
22834
|
-
|
|
22835
|
-
|
|
22836
|
-
|
|
22837
|
-
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
|
|
22841
|
-
hasTutorial: () => tutorial !== null,
|
|
22842
|
-
tutorialId: () => tutorial?.id ?? null,
|
|
22843
|
-
startTutorial: (options = {}) => {
|
|
22844
|
-
if (!tutorial || !rootRef.current) return false;
|
|
22845
|
-
setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
|
|
22846
|
-
return true;
|
|
22847
|
-
},
|
|
22848
|
-
stopTutorial: () => setTutorialRun(null)
|
|
22849
|
-
}), [json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]);
|
|
22850
|
-
const focusNextInput = (currentKey) => {
|
|
22851
|
-
if (!rootRef.current) return;
|
|
22852
|
-
findNextInput(rootRef.current, currentKey)?.focus();
|
|
22853
|
-
};
|
|
22854
|
-
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
|
|
22855
|
-
if (locked) return;
|
|
22856
|
-
if (!silent) {
|
|
22857
|
-
setRevealed(false);
|
|
22858
|
-
touchedRef.current.add(toBaseId(widgetId));
|
|
22859
|
-
}
|
|
22860
|
-
const previous = valuesRef.current[widgetId];
|
|
22861
|
-
const next = { ...valuesRef.current, [widgetId]: value };
|
|
22862
|
-
valuesRef.current = next;
|
|
22863
|
-
setValues(next);
|
|
22864
|
-
setDroppedSources((prev) => {
|
|
22865
|
-
const next2 = new Map(prev);
|
|
22866
|
-
if (fromDrop && typeof value === "string" && value !== "" && sourceOptionId) {
|
|
22867
|
-
next2.set(widgetId, sourceOptionId);
|
|
22868
|
-
} else {
|
|
22869
|
-
next2.delete(widgetId);
|
|
22870
|
-
}
|
|
22871
|
-
return next2;
|
|
22872
|
-
});
|
|
22873
|
-
onChange?.(next);
|
|
22874
|
-
if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
|
|
22875
|
-
focusNextInput(widgetId);
|
|
22876
|
-
}
|
|
22877
|
-
};
|
|
22878
|
-
const handleSubmit = () => {
|
|
22879
|
-
runSubmit();
|
|
22880
|
-
};
|
|
22881
|
-
const lineGroups = useMemo(() => {
|
|
22882
|
-
const isRTLInlineWidget = (item) => {
|
|
22883
|
-
if (item.type === JSON_TYPES.WIDGET_REF) {
|
|
22884
|
-
const widget = json.widgets[item.widgetId];
|
|
22885
|
-
return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
|
|
23589
|
+
setDroppedSources((prev) => {
|
|
23590
|
+
const next2 = new Map(prev);
|
|
23591
|
+
if (fromDrop && typeof value === "string" && value !== "" && sourceOptionId) {
|
|
23592
|
+
next2.set(widgetId, sourceOptionId);
|
|
23593
|
+
} else {
|
|
23594
|
+
next2.delete(widgetId);
|
|
23595
|
+
}
|
|
23596
|
+
return next2;
|
|
23597
|
+
});
|
|
23598
|
+
onChange?.(next);
|
|
23599
|
+
if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
|
|
23600
|
+
focusNextInput(widgetId);
|
|
22886
23601
|
}
|
|
22887
|
-
return RTL_INLINE_WIDGET_TYPES.has(item.type);
|
|
22888
23602
|
};
|
|
22889
|
-
const
|
|
22890
|
-
|
|
22891
|
-
for (const item of json.layout) {
|
|
22892
|
-
if (item.type === JSON_TYPES.QUESTION_TEXT) {
|
|
22893
|
-
groups.push({
|
|
22894
|
-
items: [item],
|
|
22895
|
-
hasArabic: itemContainsArabic(item),
|
|
22896
|
-
hasRTLInlineWidget: false,
|
|
22897
|
-
subGroups: []
|
|
22898
|
-
// Question text doesn't need sub-grouping
|
|
22899
|
-
});
|
|
22900
|
-
continue;
|
|
22901
|
-
}
|
|
22902
|
-
if (item.type === JSON_TYPES.TABLE) {
|
|
22903
|
-
if (currentGroup.length > 0) {
|
|
22904
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22905
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22906
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22907
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22908
|
-
currentGroup = [];
|
|
22909
|
-
}
|
|
22910
|
-
groups.push({
|
|
22911
|
-
items: [item],
|
|
22912
|
-
hasArabic: false,
|
|
22913
|
-
hasRTLInlineWidget: false,
|
|
22914
|
-
subGroups: []
|
|
22915
|
-
});
|
|
22916
|
-
continue;
|
|
22917
|
-
}
|
|
22918
|
-
if (item.type === JSON_TYPES.NUMBER_LINE) {
|
|
22919
|
-
if (currentGroup.length > 0) {
|
|
22920
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22921
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22922
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22923
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22924
|
-
currentGroup = [];
|
|
22925
|
-
}
|
|
22926
|
-
groups.push({
|
|
22927
|
-
items: [item],
|
|
22928
|
-
hasArabic: false,
|
|
22929
|
-
hasRTLInlineWidget: false,
|
|
22930
|
-
subGroups: []
|
|
22931
|
-
});
|
|
22932
|
-
continue;
|
|
22933
|
-
}
|
|
22934
|
-
if (item.type === JSON_TYPES.VERTICAL_OPERATION) {
|
|
22935
|
-
if (currentGroup.length > 0) {
|
|
22936
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22937
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22938
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22939
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22940
|
-
currentGroup = [];
|
|
22941
|
-
}
|
|
22942
|
-
groups.push({
|
|
22943
|
-
items: [item],
|
|
22944
|
-
hasArabic: false,
|
|
22945
|
-
hasRTLInlineWidget: false,
|
|
22946
|
-
subGroups: []
|
|
22947
|
-
});
|
|
22948
|
-
continue;
|
|
22949
|
-
}
|
|
22950
|
-
if (item.type === JSON_TYPES.CHART) {
|
|
22951
|
-
if (currentGroup.length > 0) {
|
|
22952
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22953
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22954
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22955
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22956
|
-
currentGroup = [];
|
|
22957
|
-
}
|
|
22958
|
-
groups.push({
|
|
22959
|
-
items: [item],
|
|
22960
|
-
hasArabic: false,
|
|
22961
|
-
hasRTLInlineWidget: false,
|
|
22962
|
-
subGroups: []
|
|
22963
|
-
});
|
|
22964
|
-
continue;
|
|
22965
|
-
}
|
|
22966
|
-
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
22967
|
-
if (currentGroup.length > 0) {
|
|
22968
|
-
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
22969
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22970
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22971
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22972
|
-
currentGroup = [];
|
|
22973
|
-
}
|
|
22974
|
-
} else {
|
|
22975
|
-
currentGroup.push(item);
|
|
22976
|
-
}
|
|
22977
|
-
}
|
|
22978
|
-
if (currentGroup.length > 0) {
|
|
22979
|
-
const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
|
|
22980
|
-
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
22981
|
-
const subGroups = groupLineItems(currentGroup);
|
|
22982
|
-
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
22983
|
-
}
|
|
22984
|
-
return groups;
|
|
22985
|
-
}, [json.layout, json.widgets]);
|
|
22986
|
-
const droppedInputs = useMemo(() => new Set(droppedSources.keys()), [droppedSources]);
|
|
22987
|
-
const dragOptionsWidget = json.widgets["_drag_options"];
|
|
22988
|
-
const rulerWidget = json.widgets[RULER_WIDGET_ID];
|
|
22989
|
-
const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
|
|
22990
|
-
const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
|
|
22991
|
-
const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
|
|
22992
|
-
const { tools, toolsMinHeight } = useMemo(() => {
|
|
22993
|
-
const list = [];
|
|
22994
|
-
const PAD = 12;
|
|
22995
|
-
const STAGGER = 24;
|
|
22996
|
-
let maxH = 0;
|
|
22997
|
-
const getSize = (w) => typeof w === "object" && w !== null && "size" in w && typeof w.size === "number" ? w.size : 1;
|
|
22998
|
-
const add2 = (id, type, width) => {
|
|
22999
|
-
const offset = list.length * STAGGER;
|
|
23000
|
-
list.push({ id, type, width, initialPosition: { x: PAD + offset, y: PAD + offset } });
|
|
23001
|
-
const extra = type === "set-square" ? FLIP_BTN_EXTRA : 0;
|
|
23002
|
-
const h = PAD + Math.ceil(width * (TOOL_ASPECT_RATIO[type] ?? 0)) + extra + PAD;
|
|
23003
|
-
if (h > maxH) maxH = h;
|
|
23603
|
+
const handleSubmit = () => {
|
|
23604
|
+
runSubmit();
|
|
23004
23605
|
};
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
|
|
23008
|
-
|
|
23009
|
-
|
|
23010
|
-
|
|
23011
|
-
|
|
23012
|
-
|
|
23606
|
+
const lineGroups = useMemo(() => {
|
|
23607
|
+
const isRTLInlineWidget = (item) => {
|
|
23608
|
+
if (item.type === JSON_TYPES.WIDGET_REF) {
|
|
23609
|
+
const widget = json.widgets[item.widgetId];
|
|
23610
|
+
return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
|
|
23611
|
+
}
|
|
23612
|
+
return RTL_INLINE_WIDGET_TYPES.has(item.type);
|
|
23613
|
+
};
|
|
23614
|
+
const groups = [];
|
|
23615
|
+
let currentGroup = [];
|
|
23616
|
+
for (const item of json.layout) {
|
|
23617
|
+
if (item.type === JSON_TYPES.QUESTION_TEXT) {
|
|
23618
|
+
groups.push({
|
|
23619
|
+
items: [item],
|
|
23620
|
+
hasArabic: itemContainsArabic(item),
|
|
23621
|
+
hasRTLInlineWidget: false,
|
|
23622
|
+
subGroups: []
|
|
23623
|
+
// Question text doesn't need sub-grouping
|
|
23624
|
+
});
|
|
23625
|
+
continue;
|
|
23626
|
+
}
|
|
23627
|
+
if (item.type === JSON_TYPES.TABLE) {
|
|
23628
|
+
if (currentGroup.length > 0) {
|
|
23629
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23630
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23631
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23632
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23633
|
+
currentGroup = [];
|
|
23634
|
+
}
|
|
23635
|
+
groups.push({
|
|
23636
|
+
items: [item],
|
|
23637
|
+
hasArabic: false,
|
|
23638
|
+
hasRTLInlineWidget: false,
|
|
23639
|
+
subGroups: []
|
|
23640
|
+
});
|
|
23641
|
+
continue;
|
|
23642
|
+
}
|
|
23643
|
+
if (item.type === JSON_TYPES.NUMBER_LINE) {
|
|
23644
|
+
if (currentGroup.length > 0) {
|
|
23645
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23646
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23647
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23648
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23649
|
+
currentGroup = [];
|
|
23650
|
+
}
|
|
23651
|
+
groups.push({
|
|
23652
|
+
items: [item],
|
|
23653
|
+
hasArabic: false,
|
|
23654
|
+
hasRTLInlineWidget: false,
|
|
23655
|
+
subGroups: []
|
|
23656
|
+
});
|
|
23657
|
+
continue;
|
|
23658
|
+
}
|
|
23659
|
+
if (item.type === JSON_TYPES.VERTICAL_OPERATION) {
|
|
23660
|
+
if (currentGroup.length > 0) {
|
|
23661
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23662
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23663
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23664
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23665
|
+
currentGroup = [];
|
|
23666
|
+
}
|
|
23667
|
+
groups.push({
|
|
23668
|
+
items: [item],
|
|
23669
|
+
hasArabic: false,
|
|
23670
|
+
hasRTLInlineWidget: false,
|
|
23671
|
+
subGroups: []
|
|
23672
|
+
});
|
|
23673
|
+
continue;
|
|
23674
|
+
}
|
|
23675
|
+
if (item.type === JSON_TYPES.CHART) {
|
|
23676
|
+
if (currentGroup.length > 0) {
|
|
23677
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23678
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23679
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23680
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23681
|
+
currentGroup = [];
|
|
23682
|
+
}
|
|
23683
|
+
groups.push({
|
|
23684
|
+
items: [item],
|
|
23685
|
+
hasArabic: false,
|
|
23686
|
+
hasRTLInlineWidget: false,
|
|
23687
|
+
subGroups: []
|
|
23688
|
+
});
|
|
23689
|
+
continue;
|
|
23690
|
+
}
|
|
23691
|
+
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
23692
|
+
if (currentGroup.length > 0) {
|
|
23693
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
23694
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23695
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23696
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23697
|
+
currentGroup = [];
|
|
23698
|
+
}
|
|
23699
|
+
} else {
|
|
23700
|
+
currentGroup.push(item);
|
|
23701
|
+
}
|
|
23702
|
+
}
|
|
23703
|
+
if (currentGroup.length > 0) {
|
|
23704
|
+
const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
|
|
23705
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
23706
|
+
const subGroups = groupLineItems(currentGroup);
|
|
23707
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
23708
|
+
}
|
|
23709
|
+
return groups;
|
|
23710
|
+
}, [json.layout, json.widgets]);
|
|
23711
|
+
const droppedInputs = useMemo(() => new Set(droppedSources.keys()), [droppedSources]);
|
|
23712
|
+
const dragOptionsWidget = json.widgets["_drag_options"];
|
|
23713
|
+
const draggableOptionsWidget = json.widgets[DRAGGABLE_OPTIONS_WIDGET_ID];
|
|
23714
|
+
const rulerWidget = json.widgets[RULER_WIDGET_ID];
|
|
23715
|
+
const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
|
|
23716
|
+
const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
|
|
23717
|
+
const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
|
|
23718
|
+
const { tools, toolsMinHeight } = useMemo(() => {
|
|
23719
|
+
const list = [];
|
|
23720
|
+
const PAD = 12;
|
|
23721
|
+
const STAGGER = 24;
|
|
23722
|
+
let maxH = 0;
|
|
23723
|
+
const getSize = (w) => typeof w === "object" && w !== null && "size" in w && typeof w.size === "number" ? w.size : 1;
|
|
23724
|
+
const add2 = (id, type, width) => {
|
|
23725
|
+
const offset = list.length * STAGGER;
|
|
23726
|
+
list.push({ id, type, width, initialPosition: { x: PAD + offset, y: PAD + offset } });
|
|
23727
|
+
const extra = type === "set-square" ? FLIP_BTN_EXTRA : 0;
|
|
23728
|
+
const h = PAD + Math.ceil(width * (TOOL_ASPECT_RATIO[type] ?? 0)) + extra + PAD;
|
|
23729
|
+
if (h > maxH) maxH = h;
|
|
23730
|
+
};
|
|
23731
|
+
if (rulerWidget) add2("ruler-1", "ruler", Math.round(getSize(rulerWidget) * 240));
|
|
23732
|
+
if (protractorWidget)
|
|
23733
|
+
add2("protractor-1", "protractor", Math.round(getSize(protractorWidget) * 180));
|
|
23734
|
+
if (setSquareWidget)
|
|
23735
|
+
add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
|
|
23736
|
+
return { tools: list, toolsMinHeight: maxH };
|
|
23737
|
+
}, [rulerWidget, protractorWidget, setSquareWidget]);
|
|
23738
|
+
return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(
|
|
23739
|
+
ExerciseDndProvider,
|
|
23013
23740
|
{
|
|
23014
|
-
|
|
23015
|
-
|
|
23016
|
-
|
|
23017
|
-
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
|
|
23022
|
-
|
|
23023
|
-
|
|
23024
|
-
|
|
23025
|
-
|
|
23026
|
-
|
|
23027
|
-
|
|
23028
|
-
|
|
23029
|
-
|
|
23030
|
-
|
|
23031
|
-
|
|
23032
|
-
|
|
23033
|
-
|
|
23034
|
-
|
|
23035
|
-
onInputFocus: setFocusedInputId,
|
|
23036
|
-
resetKey
|
|
23037
|
-
},
|
|
23038
|
-
item.id
|
|
23039
|
-
)) }, subIdx)
|
|
23040
|
-
) : (
|
|
23041
|
-
// Widget group - no dir wrapper, render directly
|
|
23042
|
-
subGroup.items.map((item) => /* @__PURE__ */ jsx(
|
|
23043
|
-
LayoutRenderer,
|
|
23044
|
-
{
|
|
23045
|
-
item,
|
|
23046
|
-
widgets: json.widgets,
|
|
23047
|
-
values,
|
|
23048
|
-
droppedInputs,
|
|
23049
|
-
validationDetails,
|
|
23050
|
-
onChange: handleChange,
|
|
23051
|
-
onInputFocus: setFocusedInputId,
|
|
23052
|
-
resetKey
|
|
23053
|
-
},
|
|
23054
|
-
item.id
|
|
23055
|
-
))
|
|
23056
|
-
))
|
|
23057
|
-
) : (
|
|
23058
|
-
// For QUESTION_TEXT items (no sub-groups)
|
|
23059
|
-
group.items.map((item) => /* @__PURE__ */ jsx(
|
|
23060
|
-
LayoutRenderer,
|
|
23741
|
+
droppedSources,
|
|
23742
|
+
exerciseKey: loadedValuesSignature,
|
|
23743
|
+
renderOverlay: (item) => {
|
|
23744
|
+
if (item.type !== "rich-drag-option" || draggableOptionsWidget?.type !== JSON_TYPES.DRAGGABLE_OPTIONS) {
|
|
23745
|
+
return null;
|
|
23746
|
+
}
|
|
23747
|
+
const data = item.data;
|
|
23748
|
+
const option = draggableOptionsWidget.options.find(
|
|
23749
|
+
(candidate) => candidate.name === data.optionName
|
|
23750
|
+
);
|
|
23751
|
+
return option ? (
|
|
23752
|
+
// The overlay renders outside the root below, so it needs the root's class
|
|
23753
|
+
// and theme style for nested widgets to resolve the --exercise-* variables.
|
|
23754
|
+
/* @__PURE__ */ jsx(
|
|
23755
|
+
"div",
|
|
23756
|
+
{
|
|
23757
|
+
className: `math-exercise-renderer rich-drag-overlay-root ${className}`,
|
|
23758
|
+
style: themeStyle,
|
|
23759
|
+
"aria-hidden": "true",
|
|
23760
|
+
children: /* @__PURE__ */ jsx(
|
|
23761
|
+
"div",
|
|
23061
23762
|
{
|
|
23062
|
-
|
|
23063
|
-
widgets: json.widgets
|
|
23064
|
-
|
|
23065
|
-
|
|
23066
|
-
validationDetails,
|
|
23067
|
-
onChange: handleChange,
|
|
23068
|
-
onInputFocus: setFocusedInputId,
|
|
23069
|
-
resetKey
|
|
23070
|
-
},
|
|
23071
|
-
item.id
|
|
23072
|
-
))
|
|
23073
|
-
)
|
|
23074
|
-
},
|
|
23075
|
-
groupIdx
|
|
23076
|
-
)) }),
|
|
23077
|
-
numericKeypadWidget && numericKeypadWidget.type === "numeric-keypad" && /* @__PURE__ */ jsx("div", { className: "numeric-keypad-container", children: /* @__PURE__ */ jsx(
|
|
23078
|
-
WidgetRenderer,
|
|
23079
|
-
{
|
|
23080
|
-
widget: numericKeypadWidget,
|
|
23081
|
-
widgetId: "_numeric_keypad",
|
|
23082
|
-
value: focusedInputId ? values[focusedInputId] || "" : "",
|
|
23083
|
-
onChange: () => {
|
|
23084
|
-
},
|
|
23085
|
-
onKeypadChange: (newValue) => {
|
|
23086
|
-
if (!focusedInputId) return;
|
|
23087
|
-
const maxLength = getMaxInputLength(json, focusedInputId);
|
|
23088
|
-
if (maxLength !== void 0 && newValue.length > maxLength) return;
|
|
23089
|
-
handleChange(focusedInputId, newValue, false);
|
|
23763
|
+
className: `drag-overlay-option rich-drag-overlay-option${isPlainTextContent(option.content) ? "" : " drag-overlay-option--rich"}`,
|
|
23764
|
+
children: /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets: json.widgets })
|
|
23765
|
+
}
|
|
23766
|
+
)
|
|
23090
23767
|
}
|
|
23091
|
-
|
|
23092
|
-
)
|
|
23093
|
-
|
|
23094
|
-
]
|
|
23095
|
-
|
|
23096
|
-
|
|
23097
|
-
|
|
23098
|
-
|
|
23099
|
-
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
|
|
23104
|
-
|
|
23768
|
+
)
|
|
23769
|
+
) : null;
|
|
23770
|
+
},
|
|
23771
|
+
children: /* @__PURE__ */ jsx(KeypadContext.Provider, { value: Boolean(numericKeypadWidget), children: /* @__PURE__ */ jsx(DragBankContext.Provider, { value: json.widgets[DRAG_OPTIONS_WIDGET_ID] !== void 0, children: /* @__PURE__ */ jsxs(
|
|
23772
|
+
"div",
|
|
23773
|
+
{
|
|
23774
|
+
ref: rootRef,
|
|
23775
|
+
className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`,
|
|
23776
|
+
style: themeStyle,
|
|
23777
|
+
children: [
|
|
23778
|
+
/* @__PURE__ */ jsxs(
|
|
23779
|
+
"div",
|
|
23780
|
+
{
|
|
23781
|
+
className: `exercise-layout-container ${numericKeypadWidget ? "has-keypad" : ""}`,
|
|
23782
|
+
style: toolsMinHeight > 0 ? { minHeight: toolsMinHeight } : void 0,
|
|
23783
|
+
children: [
|
|
23784
|
+
/* @__PURE__ */ jsx("div", { className: "exercise-layout", children: lineGroups.map((group, groupIdx) => /* @__PURE__ */ jsx(
|
|
23785
|
+
"div",
|
|
23786
|
+
{
|
|
23787
|
+
className: "exercise-line",
|
|
23788
|
+
dir: group.hasArabic && group.hasRTLInlineWidget ? "rtl" : void 0,
|
|
23789
|
+
children: group.subGroups.length > 0 ? (
|
|
23790
|
+
// Render sub-groups for regular lines
|
|
23791
|
+
group.subGroups.map(
|
|
23792
|
+
(subGroup, subIdx) => subGroup.isTextGroup ? (
|
|
23793
|
+
// Text group - only apply RTL for pure Arabic (no mixed languages)
|
|
23794
|
+
/* @__PURE__ */ jsx(
|
|
23795
|
+
"span",
|
|
23796
|
+
{
|
|
23797
|
+
className: "text-group",
|
|
23798
|
+
dir: subGroup.shouldApplyRTL ? "rtl" : void 0,
|
|
23799
|
+
children: subGroup.items.map((item) => /* @__PURE__ */ jsx(
|
|
23800
|
+
LayoutRenderer,
|
|
23801
|
+
{
|
|
23802
|
+
item,
|
|
23803
|
+
widgets: json.widgets,
|
|
23804
|
+
values,
|
|
23805
|
+
droppedInputs,
|
|
23806
|
+
validationDetails,
|
|
23807
|
+
onChange: handleChange,
|
|
23808
|
+
onInputFocus: setFocusedInputId,
|
|
23809
|
+
resetKey
|
|
23810
|
+
},
|
|
23811
|
+
item.id
|
|
23812
|
+
))
|
|
23813
|
+
},
|
|
23814
|
+
subIdx
|
|
23815
|
+
)
|
|
23816
|
+
) : (
|
|
23817
|
+
// Widget group - no dir wrapper, render directly
|
|
23818
|
+
subGroup.items.map((item) => /* @__PURE__ */ jsx(
|
|
23819
|
+
LayoutRenderer,
|
|
23820
|
+
{
|
|
23821
|
+
item,
|
|
23822
|
+
widgets: json.widgets,
|
|
23823
|
+
values,
|
|
23824
|
+
droppedInputs,
|
|
23825
|
+
validationDetails,
|
|
23826
|
+
onChange: handleChange,
|
|
23827
|
+
onInputFocus: setFocusedInputId,
|
|
23828
|
+
resetKey
|
|
23829
|
+
},
|
|
23830
|
+
item.id
|
|
23831
|
+
))
|
|
23832
|
+
)
|
|
23833
|
+
)
|
|
23834
|
+
) : (
|
|
23835
|
+
// For QUESTION_TEXT items (no sub-groups)
|
|
23836
|
+
group.items.map((item) => /* @__PURE__ */ jsx(
|
|
23837
|
+
LayoutRenderer,
|
|
23838
|
+
{
|
|
23839
|
+
item,
|
|
23840
|
+
widgets: json.widgets,
|
|
23841
|
+
values,
|
|
23842
|
+
droppedInputs,
|
|
23843
|
+
validationDetails,
|
|
23844
|
+
onChange: handleChange,
|
|
23845
|
+
onInputFocus: setFocusedInputId,
|
|
23846
|
+
resetKey
|
|
23847
|
+
},
|
|
23848
|
+
item.id
|
|
23849
|
+
))
|
|
23850
|
+
)
|
|
23851
|
+
},
|
|
23852
|
+
groupIdx
|
|
23853
|
+
)) }),
|
|
23854
|
+
numericKeypadWidget && numericKeypadWidget.type === "numeric-keypad" && /* @__PURE__ */ jsx("div", { className: "numeric-keypad-container", children: /* @__PURE__ */ jsx(
|
|
23855
|
+
WidgetRenderer,
|
|
23856
|
+
{
|
|
23857
|
+
widget: numericKeypadWidget,
|
|
23858
|
+
widgetId: "_numeric_keypad",
|
|
23859
|
+
value: focusedInputId ? values[focusedInputId] || "" : "",
|
|
23860
|
+
onChange: () => {
|
|
23861
|
+
},
|
|
23862
|
+
onKeypadChange: (newValue) => {
|
|
23863
|
+
if (!focusedInputId) return;
|
|
23864
|
+
const maxLength = getMaxInputLength(json, focusedInputId);
|
|
23865
|
+
const currentLength = String(values[focusedInputId] ?? "").length;
|
|
23866
|
+
if (maxLength !== void 0 && newValue.length > maxLength && newValue.length > currentLength)
|
|
23867
|
+
return;
|
|
23868
|
+
handleChange(focusedInputId, newValue, false);
|
|
23869
|
+
}
|
|
23870
|
+
}
|
|
23871
|
+
) }),
|
|
23872
|
+
tools.length > 0 && /* @__PURE__ */ jsx(ToolsOverlay, { tools, resetKey })
|
|
23873
|
+
]
|
|
23874
|
+
}
|
|
23875
|
+
),
|
|
23876
|
+
dragOptionsWidget && dragOptionsWidget.type === "drag-options" && /* @__PURE__ */ jsx("div", { className: "drag-options-container", children: /* @__PURE__ */ jsx(
|
|
23877
|
+
WidgetRenderer,
|
|
23878
|
+
{
|
|
23879
|
+
widget: dragOptionsWidget,
|
|
23880
|
+
widgetId: "_drag_options",
|
|
23881
|
+
value: "",
|
|
23882
|
+
onChange: () => {
|
|
23883
|
+
}
|
|
23884
|
+
}
|
|
23885
|
+
) }),
|
|
23886
|
+
draggableOptionsWidget && draggableOptionsWidget.type === JSON_TYPES.DRAGGABLE_OPTIONS && // Keeps the legacy class too: host CSS uses :has(.drag-options-container) to
|
|
23887
|
+
// tell drag exercises from typed ones.
|
|
23888
|
+
/* @__PURE__ */ jsx("div", { className: "drag-options-container draggable-options-container", children: /* @__PURE__ */ jsx(
|
|
23889
|
+
WidgetRenderer,
|
|
23890
|
+
{
|
|
23891
|
+
widget: draggableOptionsWidget,
|
|
23892
|
+
widgetId: DRAGGABLE_OPTIONS_WIDGET_ID,
|
|
23893
|
+
value: "",
|
|
23894
|
+
values,
|
|
23895
|
+
widgets: json.widgets,
|
|
23896
|
+
onChange: () => {
|
|
23897
|
+
}
|
|
23898
|
+
}
|
|
23899
|
+
) }),
|
|
23900
|
+
showSubmitButton && /* @__PURE__ */ jsx(
|
|
23901
|
+
"button",
|
|
23902
|
+
{
|
|
23903
|
+
className: "exercise-submit-btn",
|
|
23904
|
+
disabled: locked,
|
|
23905
|
+
onClick: handleSubmit,
|
|
23906
|
+
children: "Check Answer"
|
|
23907
|
+
}
|
|
23908
|
+
),
|
|
23909
|
+
tutorial && tutorialRun && rootRef.current && /* @__PURE__ */ jsx(
|
|
23910
|
+
TutorialOverlay,
|
|
23911
|
+
{
|
|
23912
|
+
tutorial,
|
|
23913
|
+
root: rootRef.current,
|
|
23914
|
+
json,
|
|
23915
|
+
options: tutorialRun.options,
|
|
23916
|
+
onClose: (finished) => {
|
|
23917
|
+
setTutorialRun(null);
|
|
23918
|
+
tutorialRun.options.onClose?.();
|
|
23919
|
+
if (finished) tutorialRun.options.onDone?.();
|
|
23920
|
+
}
|
|
23921
|
+
},
|
|
23922
|
+
tutorialRun.run
|
|
23923
|
+
)
|
|
23924
|
+
]
|
|
23925
|
+
}
|
|
23926
|
+
) }) })
|
|
23105
23927
|
}
|
|
23106
|
-
) })
|
|
23107
|
-
|
|
23108
|
-
|
|
23109
|
-
TutorialOverlay,
|
|
23110
|
-
{
|
|
23111
|
-
tutorial,
|
|
23112
|
-
root: rootRef.current,
|
|
23113
|
-
json,
|
|
23114
|
-
options: tutorialRun.options,
|
|
23115
|
-
onClose: (finished) => {
|
|
23116
|
-
setTutorialRun(null);
|
|
23117
|
-
tutorialRun.options.onClose?.();
|
|
23118
|
-
if (finished) tutorialRun.options.onDone?.();
|
|
23119
|
-
}
|
|
23120
|
-
},
|
|
23121
|
-
tutorialRun.run
|
|
23122
|
-
)
|
|
23123
|
-
] }) }) }) }) }) });
|
|
23124
|
-
});
|
|
23928
|
+
) }) });
|
|
23929
|
+
}
|
|
23930
|
+
);
|
|
23125
23931
|
ExerciseRenderer.displayName = "ExerciseRenderer";
|
|
23126
23932
|
function renderExercise(options) {
|
|
23127
23933
|
const container = typeof options.container === "string" ? document.querySelector(options.container) : options.container;
|
|
@@ -23159,7 +23965,7 @@ function compileWithAST(dsl) {
|
|
|
23159
23965
|
return { ast, json };
|
|
23160
23966
|
}
|
|
23161
23967
|
export {
|
|
23162
|
-
|
|
23968
|
+
isTableRowItem as $,
|
|
23163
23969
|
AST_TYPES as A,
|
|
23164
23970
|
isQuestionText as B,
|
|
23165
23971
|
isTextItem$1 as C,
|
|
@@ -23175,275 +23981,291 @@ export {
|
|
|
23175
23981
|
isGroupItem as M,
|
|
23176
23982
|
isOperatorItem as N,
|
|
23177
23983
|
isInlineInputJSON as O,
|
|
23178
|
-
|
|
23179
|
-
|
|
23180
|
-
|
|
23181
|
-
|
|
23182
|
-
|
|
23183
|
-
|
|
23184
|
-
|
|
23185
|
-
|
|
23186
|
-
|
|
23187
|
-
|
|
23188
|
-
|
|
23189
|
-
|
|
23984
|
+
isDropInputJSON as P,
|
|
23985
|
+
isDragOptionsJSON as Q,
|
|
23986
|
+
isDraggableOptionsJSON as R,
|
|
23987
|
+
isInlineDragOptionJSON as S,
|
|
23988
|
+
isRadioOptionsJSON as T,
|
|
23989
|
+
isNumericKeypadJSON as U,
|
|
23990
|
+
isFractionJSON as V,
|
|
23991
|
+
isGridSymmetryJSON as W,
|
|
23992
|
+
isChartItem as X,
|
|
23993
|
+
isChartBarWidgetJSON as Y,
|
|
23994
|
+
isChartPictoWidgetJSON as Z,
|
|
23995
|
+
isTableItem as _,
|
|
23190
23996
|
compileWithAST as a,
|
|
23191
|
-
|
|
23192
|
-
|
|
23193
|
-
|
|
23194
|
-
|
|
23195
|
-
|
|
23196
|
-
|
|
23197
|
-
|
|
23198
|
-
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
|
|
23203
|
-
|
|
23204
|
-
|
|
23205
|
-
|
|
23206
|
-
|
|
23207
|
-
|
|
23208
|
-
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
|
|
23212
|
-
|
|
23213
|
-
|
|
23214
|
-
|
|
23215
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
23219
|
-
|
|
23220
|
-
|
|
23221
|
-
|
|
23222
|
-
|
|
23223
|
-
|
|
23224
|
-
|
|
23225
|
-
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
|
|
23230
|
-
|
|
23231
|
-
|
|
23232
|
-
|
|
23233
|
-
|
|
23234
|
-
|
|
23235
|
-
|
|
23236
|
-
|
|
23237
|
-
|
|
23238
|
-
|
|
23239
|
-
|
|
23240
|
-
|
|
23241
|
-
|
|
23242
|
-
|
|
23243
|
-
|
|
23244
|
-
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
|
|
23253
|
-
|
|
23254
|
-
|
|
23997
|
+
RulerWidgetSchema as a$,
|
|
23998
|
+
isTableCellItem as a0,
|
|
23999
|
+
isImageItem as a1,
|
|
24000
|
+
isVerticalOperationItem as a2,
|
|
24001
|
+
isClockItem as a3,
|
|
24002
|
+
isRulerWidget as a4,
|
|
24003
|
+
isProtractorWidget as a5,
|
|
24004
|
+
isSetSquareWidget as a6,
|
|
24005
|
+
isGeometryJSON as a7,
|
|
24006
|
+
ExerciseJSONSchema as a8,
|
|
24007
|
+
ExerciseMetaSchema as a9,
|
|
24008
|
+
VopSeparatorItemSchema as aA,
|
|
24009
|
+
VopCellItemSchema as aB,
|
|
24010
|
+
VopColorCellItemSchema as aC,
|
|
24011
|
+
VopBoldCellItemSchema as aD,
|
|
24012
|
+
VopHighlightCellItemSchema as aE,
|
|
24013
|
+
ClockItemSchema as aF,
|
|
24014
|
+
ChartItemSchema as aG,
|
|
24015
|
+
ChartAxisJSONSchema as aH,
|
|
24016
|
+
ChartBarJSONSchema as aI,
|
|
24017
|
+
ChartPictoJSONSchema as aJ,
|
|
24018
|
+
ChartSeriesJSONSchema as aK,
|
|
24019
|
+
ChartSeriesBarJSONSchema as aL,
|
|
24020
|
+
ChartGroupJSONSchema as aM,
|
|
24021
|
+
WidgetJSONSchema as aN,
|
|
24022
|
+
InlineInputJSONSchema as aO,
|
|
24023
|
+
DropInputJSONSchema as aP,
|
|
24024
|
+
DragOptionsJSONSchema as aQ,
|
|
24025
|
+
DraggableOptionJSONSchema as aR,
|
|
24026
|
+
DraggableOptionsJSONSchema as aS,
|
|
24027
|
+
RadioOptionsJSONSchema as aT,
|
|
24028
|
+
NumericKeypadJSONSchema as aU,
|
|
24029
|
+
FractionJSONSchema as aV,
|
|
24030
|
+
MatcherWidgetJSONSchema as aW,
|
|
24031
|
+
PairMatcherWidgetJSONSchema as aX,
|
|
24032
|
+
GridSymmetryJSONSchema as aY,
|
|
24033
|
+
ChartBarWidgetJSONSchema as aZ,
|
|
24034
|
+
ChartPictoWidgetJSONSchema as a_,
|
|
24035
|
+
ValidationJSONSchema as aa,
|
|
24036
|
+
LayoutItemSchema as ab,
|
|
24037
|
+
QuestionTextSchema as ac,
|
|
24038
|
+
TextItemSchema as ad,
|
|
24039
|
+
WidgetRefSchema as ae,
|
|
24040
|
+
HighlightedTextSchema as af,
|
|
24041
|
+
ColorTextSchema as ag,
|
|
24042
|
+
BoldTextSchema as ah,
|
|
24043
|
+
ImageItemSchema as ai,
|
|
24044
|
+
SpaceItemSchema as aj,
|
|
24045
|
+
LinebreakSchema as ak,
|
|
24046
|
+
GroupItemSchema as al,
|
|
24047
|
+
OperatorItemSchema as am,
|
|
24048
|
+
OperatorSchema as an,
|
|
24049
|
+
TableItemSchema as ao,
|
|
24050
|
+
TableRowItemSchema as ap,
|
|
24051
|
+
TableCellItemSchema as aq,
|
|
24052
|
+
NumberLineItemSchema as ar,
|
|
24053
|
+
NumberLinePositionItemSchema as as,
|
|
24054
|
+
NumberLineCounterItemSchema as at,
|
|
24055
|
+
NumberLineHighlightItemSchema as au,
|
|
24056
|
+
NumberLineCursorItemSchema as av,
|
|
24057
|
+
NumberLineArrowItemSchema as aw,
|
|
24058
|
+
VerticalOperationItemSchema as ax,
|
|
24059
|
+
VopRowItemSchema as ay,
|
|
24060
|
+
VopCarryRowItemSchema as az,
|
|
23255
24061
|
tokenizeSafe as b,
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
|
|
23264
|
-
|
|
23265
|
-
|
|
23266
|
-
|
|
23267
|
-
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
23272
|
-
|
|
23273
|
-
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
|
|
23278
|
-
|
|
23279
|
-
|
|
23280
|
-
|
|
23281
|
-
|
|
23282
|
-
|
|
23283
|
-
|
|
23284
|
-
|
|
23285
|
-
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
|
|
23290
|
-
|
|
23291
|
-
|
|
23292
|
-
|
|
23293
|
-
|
|
23294
|
-
|
|
23295
|
-
|
|
23296
|
-
|
|
23297
|
-
|
|
23298
|
-
|
|
23299
|
-
|
|
23300
|
-
|
|
23301
|
-
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
|
|
23307
|
-
|
|
23308
|
-
|
|
23309
|
-
|
|
23310
|
-
|
|
23311
|
-
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
|
|
23318
|
-
|
|
23319
|
-
|
|
24062
|
+
isInlineInput as b$,
|
|
24063
|
+
ProtractorWidgetSchema as b0,
|
|
24064
|
+
SetSquareWidgetSchema as b1,
|
|
24065
|
+
GeometryJSONSchema as b2,
|
|
24066
|
+
validateExerciseJSON as b3,
|
|
24067
|
+
validateExerciseJSONSafe as b4,
|
|
24068
|
+
validateWidgetJSON as b5,
|
|
24069
|
+
validateWidgetJSONSafe as b6,
|
|
24070
|
+
EngineError as b7,
|
|
24071
|
+
LexerError as b8,
|
|
24072
|
+
ParseError as b9,
|
|
24073
|
+
FractionInlineSchema as bA,
|
|
24074
|
+
ImageInlineSchema as bB,
|
|
24075
|
+
VisualFractionInlineSchema as bC,
|
|
24076
|
+
validateVisualFractionCapacity as bD,
|
|
24077
|
+
InlineDragOptionSchema as bE,
|
|
24078
|
+
CLOCK_TYPES as bF,
|
|
24079
|
+
CLOCK_MODES as bG,
|
|
24080
|
+
CLOCK_FORMATS as bH,
|
|
24081
|
+
CLOCK_TIME_REGEX as bI,
|
|
24082
|
+
ClockInlineSchema as bJ,
|
|
24083
|
+
InlineNodeSchema as bK,
|
|
24084
|
+
createFreeText as bL,
|
|
24085
|
+
createSpace as bM,
|
|
24086
|
+
createHighlightedText as bN,
|
|
24087
|
+
createColorText as bO,
|
|
24088
|
+
createBoldText as bP,
|
|
24089
|
+
createStringInput as bQ,
|
|
24090
|
+
createNumericInput as bR,
|
|
24091
|
+
createDropInput as bS,
|
|
24092
|
+
createImage as bT,
|
|
24093
|
+
createFraction as bU,
|
|
24094
|
+
createVisualFraction as bV,
|
|
24095
|
+
createInlineDragOption as bW,
|
|
24096
|
+
createDisplayClock as bX,
|
|
24097
|
+
createInteractiveClock as bY,
|
|
24098
|
+
isFreeText as bZ,
|
|
24099
|
+
isSpace as b_,
|
|
24100
|
+
TransformError as ba,
|
|
24101
|
+
ValidationError as bb,
|
|
24102
|
+
ExerciseRenderer as bc,
|
|
24103
|
+
LayoutRenderer as bd,
|
|
24104
|
+
WidgetRenderer as be,
|
|
24105
|
+
renderExercise as bf,
|
|
24106
|
+
SoundProvider as bg,
|
|
24107
|
+
useSound as bh,
|
|
24108
|
+
ExerciseLockProvider as bi,
|
|
24109
|
+
WIDGET_TUTORIALS as bj,
|
|
24110
|
+
findWidgetTutorial as bk,
|
|
24111
|
+
isPairMatcherWidget as bl,
|
|
24112
|
+
isMatcherWidget as bm,
|
|
24113
|
+
VISUAL_FRACTION_SHAPES as bn,
|
|
24114
|
+
VISUAL_FRACTION_SHAPE as bo,
|
|
24115
|
+
VISUAL_FRACTION_DEFAULT_SIZE as bp,
|
|
24116
|
+
VISUAL_FRACTION_MAX_DIVISIONS as bq,
|
|
24117
|
+
INLINE_INPUT_MAX_WIDTH as br,
|
|
24118
|
+
InlineInputWidthSchema as bs,
|
|
24119
|
+
FreeTextInlineSchema as bt,
|
|
24120
|
+
SpaceInlineSchema as bu,
|
|
24121
|
+
HighlightedTextInlineSchema as bv,
|
|
24122
|
+
ColorTextInlineSchema as bw,
|
|
24123
|
+
BoldTextInlineSchema as bx,
|
|
24124
|
+
InlineInputWidgetSchema as by,
|
|
24125
|
+
DropInputSchema as bz,
|
|
23320
24126
|
compile as c,
|
|
23321
|
-
|
|
23322
|
-
|
|
23323
|
-
|
|
23324
|
-
|
|
23325
|
-
|
|
23326
|
-
|
|
23327
|
-
|
|
23328
|
-
|
|
23329
|
-
|
|
23330
|
-
|
|
23331
|
-
|
|
23332
|
-
|
|
23333
|
-
|
|
23334
|
-
|
|
23335
|
-
|
|
23336
|
-
|
|
23337
|
-
|
|
23338
|
-
|
|
23339
|
-
|
|
23340
|
-
|
|
23341
|
-
|
|
23342
|
-
|
|
23343
|
-
|
|
23344
|
-
|
|
23345
|
-
|
|
23346
|
-
|
|
23347
|
-
|
|
23348
|
-
|
|
23349
|
-
|
|
23350
|
-
|
|
23351
|
-
|
|
23352
|
-
|
|
23353
|
-
|
|
23354
|
-
|
|
23355
|
-
|
|
23356
|
-
|
|
23357
|
-
|
|
23358
|
-
|
|
23359
|
-
|
|
23360
|
-
|
|
23361
|
-
|
|
23362
|
-
|
|
23363
|
-
|
|
23364
|
-
|
|
23365
|
-
|
|
23366
|
-
|
|
23367
|
-
|
|
23368
|
-
|
|
23369
|
-
|
|
23370
|
-
|
|
23371
|
-
|
|
23372
|
-
|
|
23373
|
-
|
|
23374
|
-
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23379
|
-
|
|
23380
|
-
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
24127
|
+
ExerciseDocumentSchema as c$,
|
|
24128
|
+
isDropInput as c0,
|
|
24129
|
+
isFraction as c1,
|
|
24130
|
+
isImage as c2,
|
|
24131
|
+
isClock as c3,
|
|
24132
|
+
isInlineNode as c4,
|
|
24133
|
+
isVisualFractionInline as c5,
|
|
24134
|
+
isInlineDragOption as c6,
|
|
24135
|
+
TextBlockSchema as c7,
|
|
24136
|
+
LineBlockSchema as c8,
|
|
24137
|
+
createTextBlock as c9,
|
|
24138
|
+
ToolNodeSchema as cA,
|
|
24139
|
+
createDragOptions as cB,
|
|
24140
|
+
createDraggableOptions as cC,
|
|
24141
|
+
createDraggableOption as cD,
|
|
24142
|
+
createNumericKeypad as cE,
|
|
24143
|
+
createRulerTool as cF,
|
|
24144
|
+
createProtractorTool as cG,
|
|
24145
|
+
createSetSquareTool as cH,
|
|
24146
|
+
isDragOptions as cI,
|
|
24147
|
+
isDraggableOptions as cJ,
|
|
24148
|
+
isNumericKeypad as cK,
|
|
24149
|
+
isRulerTool as cL,
|
|
24150
|
+
isProtractorTool as cM,
|
|
24151
|
+
isSetSquareTool as cN,
|
|
24152
|
+
isToolNode as cO,
|
|
24153
|
+
SimpleValidationSchema as cP,
|
|
24154
|
+
ExpressionValidationSchema as cQ,
|
|
24155
|
+
MixedValidationSchema as cR,
|
|
24156
|
+
ValidationNodeSchema as cS,
|
|
24157
|
+
createSimpleValidation as cT,
|
|
24158
|
+
createExpressionValidation as cU,
|
|
24159
|
+
createMixedValidation as cV,
|
|
24160
|
+
isSimpleValidation as cW,
|
|
24161
|
+
isExpressionValidation as cX,
|
|
24162
|
+
isMixedValidation as cY,
|
|
24163
|
+
isValidationNode as cZ,
|
|
24164
|
+
ContentBlockSchema as c_,
|
|
24165
|
+
createLineBlock as ca,
|
|
24166
|
+
isTextBlock as cb,
|
|
24167
|
+
isLineBlock as cc,
|
|
24168
|
+
DisplayInlineNodeSchema as cd,
|
|
24169
|
+
MatcherItemSchema as ce,
|
|
24170
|
+
createMatcherItem as cf,
|
|
24171
|
+
isMatcherItem as cg,
|
|
24172
|
+
MatcherRowSchema as ch,
|
|
24173
|
+
createMatcherRow as ci,
|
|
24174
|
+
isMatcherRow as cj,
|
|
24175
|
+
MatcherBlockSchema as ck,
|
|
24176
|
+
createMatcherBlock as cl,
|
|
24177
|
+
isMatcherBlock as cm,
|
|
24178
|
+
PairMatcherBlockSchema as cn,
|
|
24179
|
+
createPairMatcherBlock as co,
|
|
24180
|
+
isPairMatcherBlock as cp,
|
|
24181
|
+
DragOptionsToolSchema as cq,
|
|
24182
|
+
hasOptionContent as cr,
|
|
24183
|
+
isOptionContentNode as cs,
|
|
24184
|
+
DraggableOptionSchema as ct,
|
|
24185
|
+
addDuplicateOptionNameIssues as cu,
|
|
24186
|
+
DraggableOptionsToolSchema as cv,
|
|
24187
|
+
NumericKeypadToolSchema as cw,
|
|
24188
|
+
RulerToolSchema as cx,
|
|
24189
|
+
ProtractorToolSchema as cy,
|
|
24190
|
+
SetSquareToolSchema as cz,
|
|
23385
24191
|
parse as d,
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
|
|
23407
|
-
|
|
23408
|
-
|
|
23409
|
-
|
|
23410
|
-
|
|
23411
|
-
|
|
23412
|
-
|
|
23413
|
-
|
|
23414
|
-
|
|
23415
|
-
|
|
23416
|
-
|
|
23417
|
-
|
|
23418
|
-
|
|
23419
|
-
|
|
23420
|
-
|
|
23421
|
-
|
|
23422
|
-
|
|
23423
|
-
|
|
23424
|
-
|
|
23425
|
-
|
|
23426
|
-
|
|
23427
|
-
|
|
23428
|
-
|
|
23429
|
-
|
|
23430
|
-
|
|
23431
|
-
|
|
23432
|
-
|
|
23433
|
-
|
|
23434
|
-
|
|
23435
|
-
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
|
|
24192
|
+
createChartSeriesBar as d$,
|
|
24193
|
+
createExerciseDocument as d0,
|
|
24194
|
+
isExerciseDocument as d1,
|
|
24195
|
+
VOP_OPERATORS as d2,
|
|
24196
|
+
normalizeVopOperator as d3,
|
|
24197
|
+
VopDigitCellSchema as d4,
|
|
24198
|
+
VopLabelCellSchema as d5,
|
|
24199
|
+
VopInputCellSchema as d6,
|
|
24200
|
+
VopEmptyCellSchema as d7,
|
|
24201
|
+
VopColorCellSchema as d8,
|
|
24202
|
+
VopBoldCellSchema as d9,
|
|
24203
|
+
GRID_SYMMETRY_MAX_COLS as dA,
|
|
24204
|
+
GRID_SYMMETRY_MIN_CELL_SIZE as dB,
|
|
24205
|
+
GRID_SYMMETRY_MAX_CELL_SIZE as dC,
|
|
24206
|
+
isCellInInteractiveArea as dD,
|
|
24207
|
+
reflectGridSymmetryCell as dE,
|
|
24208
|
+
computeGridSymmetryExpectedCells as dF,
|
|
24209
|
+
computeGridSymmetryCellsToFill as dG,
|
|
24210
|
+
GridSymmetryBlockSchema as dH,
|
|
24211
|
+
createGridSymmetryBlock as dI,
|
|
24212
|
+
isGridSymmetryBlock as dJ,
|
|
24213
|
+
CHART_TYPES as dK,
|
|
24214
|
+
CHART_AXIS_DEFAULTS as dL,
|
|
24215
|
+
CHART_MAX_AXIS_TICKS as dM,
|
|
24216
|
+
CHART_MAX_PICTO_SLOTS as dN,
|
|
24217
|
+
ChartAxisSchema as dO,
|
|
24218
|
+
createChartAxis as dP,
|
|
24219
|
+
isChartAxis as dQ,
|
|
24220
|
+
ChartPictoSchema as dR,
|
|
24221
|
+
createChartPicto as dS,
|
|
24222
|
+
isChartPicto as dT,
|
|
24223
|
+
ChartBarSchema as dU,
|
|
24224
|
+
createChartBar as dV,
|
|
24225
|
+
isChartBar as dW,
|
|
24226
|
+
ChartSeriesSchema as dX,
|
|
24227
|
+
createChartSeries as dY,
|
|
24228
|
+
isChartSeries as dZ,
|
|
24229
|
+
ChartSeriesBarSchema as d_,
|
|
24230
|
+
VopHighlightCellSchema as da,
|
|
24231
|
+
VopCellSchema as db,
|
|
24232
|
+
createVopDigitCell as dc,
|
|
24233
|
+
createVopInputCell as dd,
|
|
24234
|
+
createVopLabelCell as de,
|
|
24235
|
+
createVopEmptyCell as df,
|
|
24236
|
+
createVopColorCell as dg,
|
|
24237
|
+
createVopBoldCell as dh,
|
|
24238
|
+
createVopHighlightCell as di,
|
|
24239
|
+
VopRowSchema as dj,
|
|
24240
|
+
createVopRow as dk,
|
|
24241
|
+
VopCarryRowSchema as dl,
|
|
24242
|
+
createVopCarryRow as dm,
|
|
24243
|
+
VopSeparatorSchema as dn,
|
|
24244
|
+
createVopSeparator as dp,
|
|
24245
|
+
VerticalOperationBlockSchema as dq,
|
|
24246
|
+
createVerticalOperationBlock as dr,
|
|
24247
|
+
isVopCell as ds,
|
|
24248
|
+
isVopRow as dt,
|
|
24249
|
+
isVopCarryRow as du,
|
|
24250
|
+
isVopSeparator as dv,
|
|
24251
|
+
isVerticalOperationBlock as dw,
|
|
24252
|
+
GRID_SYMMETRY_AXES as dx,
|
|
24253
|
+
GRID_SYMMETRY_INTERACTIVE_AREAS as dy,
|
|
24254
|
+
GRID_SYMMETRY_MAX_ROWS as dz,
|
|
23446
24255
|
parseSafe as e,
|
|
24256
|
+
isChartSeriesBar as e0,
|
|
24257
|
+
ChartGroupSchema as e1,
|
|
24258
|
+
createChartGroup as e2,
|
|
24259
|
+
isChartGroup as e3,
|
|
24260
|
+
alignGroupBarsToSeries as e4,
|
|
24261
|
+
ChartBlockSchema as e5,
|
|
24262
|
+
resolveChartAxes as e6,
|
|
24263
|
+
validateUniqueIds as e7,
|
|
24264
|
+
createChartBlock as e8,
|
|
24265
|
+
isChartBlock as e9,
|
|
24266
|
+
GeometryBlockSchema as ea,
|
|
24267
|
+
createGeometryBlock as eb,
|
|
24268
|
+
isGeometryBlock as ec,
|
|
23447
24269
|
toAST as f,
|
|
23448
24270
|
getFractionKey as g,
|
|
23449
24271
|
toJSON as h,
|
|
@@ -23466,4 +24288,4 @@ export {
|
|
|
23466
24288
|
extractVariables as y,
|
|
23467
24289
|
validateBooleanExpression as z
|
|
23468
24290
|
};
|
|
23469
|
-
//# sourceMappingURL=index-
|
|
24291
|
+
//# sourceMappingURL=index-BLrnKegW.mjs.map
|