@mathscan/math-exercise-engine 1.0.10 → 1.0.12
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/dist/index.d.ts +1414 -3
- package/dist/index.js +726 -276
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +727 -277
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +294 -31
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createToken, Lexer, CstParser } from "chevrotain";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { create, all } from "mathjs";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
5
5
|
import React, { useLayoutEffect, useEffect, useRef, useMemo, useCallback, useState, createContext, memo, useReducer, useContext, forwardRef, cloneElement, useImperativeHandle } from "react";
|
|
6
6
|
import { unstable_batchedUpdates, createPortal } from "react-dom";
|
|
7
7
|
import { createRoot } from "react-dom/client";
|
|
@@ -38,6 +38,10 @@ const AST_TYPES = {
|
|
|
38
38
|
NUMBER_LINE_BLOCK: "NumberLineBlock",
|
|
39
39
|
/** A position marker on the number line */
|
|
40
40
|
NUMBER_LINE_POSITION: "NumberLinePosition",
|
|
41
|
+
/** A dynamic subdivision counter embedded in a number line */
|
|
42
|
+
NUMBER_LINE_COUNTER: "NumberLineCounter",
|
|
43
|
+
/** A highlighted range on the number line axis */
|
|
44
|
+
NUMBER_LINE_HIGHLIGHT: "NumberLineHighlight",
|
|
41
45
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
46
|
// Tools
|
|
43
47
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -94,6 +98,10 @@ const JSON_TYPES = {
|
|
|
94
98
|
NUMBER_LINE: "number-line",
|
|
95
99
|
/** Position marker within a number line */
|
|
96
100
|
NUMBER_LINE_POSITION: "number-line-position",
|
|
101
|
+
/** Dynamic subdivision counter embedded in a number line */
|
|
102
|
+
NUMBER_LINE_COUNTER: "number-line-counter",
|
|
103
|
+
/** Highlighted range within a number line */
|
|
104
|
+
NUMBER_LINE_HIGHLIGHT: "number-line-highlight",
|
|
97
105
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
98
106
|
// Widget Types
|
|
99
107
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -120,8 +128,12 @@ const INPUT_TYPES = {
|
|
|
120
128
|
NUMERIC: "numeric"
|
|
121
129
|
};
|
|
122
130
|
const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line"];
|
|
131
|
+
const INTERNAL_PRECISION = 9;
|
|
132
|
+
const DISPLAY_PRECISION = 2;
|
|
123
133
|
const SIDE_PAD = 7;
|
|
124
134
|
const CSS_VAR_SIDE_PAD = "--nl-side-pad";
|
|
135
|
+
const NUMBER_LINE_LABELS = ["all", "extremes", "min-only", "max-only", "none"];
|
|
136
|
+
const NUMBER_LINE_HIGHLIGHT_COLORS = ["blue", "red", "green", "orange", "purple", "yellow"];
|
|
125
137
|
const Newline = createToken({
|
|
126
138
|
name: "Newline",
|
|
127
139
|
pattern: /\r?\n/,
|
|
@@ -215,6 +227,14 @@ const CELL = createToken({
|
|
|
215
227
|
name: "CELL",
|
|
216
228
|
pattern: /CELL/
|
|
217
229
|
});
|
|
230
|
+
const COUNTER = createToken({
|
|
231
|
+
name: "COUNTER",
|
|
232
|
+
pattern: /COUNTER/
|
|
233
|
+
});
|
|
234
|
+
const HIGHLIGHT = createToken({
|
|
235
|
+
name: "HIGHLIGHT",
|
|
236
|
+
pattern: /HIGHLIGHT(?!_TEXT)/
|
|
237
|
+
});
|
|
218
238
|
const NumberLiteral = createToken({
|
|
219
239
|
name: "NumberLiteral",
|
|
220
240
|
pattern: /-?\d+(\.\d+)?/
|
|
@@ -322,6 +342,8 @@ const allTokens = [
|
|
|
322
342
|
TABLE,
|
|
323
343
|
ROW,
|
|
324
344
|
CELL,
|
|
345
|
+
COUNTER,
|
|
346
|
+
HIGHLIGHT,
|
|
325
347
|
// Literals
|
|
326
348
|
NumberLiteral,
|
|
327
349
|
StringLiteral,
|
|
@@ -429,16 +451,14 @@ class MathExerciseParser extends CstParser {
|
|
|
429
451
|
});
|
|
430
452
|
this.exerciseDocument = this.RULE("exerciseDocument", () => {
|
|
431
453
|
this.MANY(() => {
|
|
432
|
-
this.OR([
|
|
433
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
434
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
435
|
-
]);
|
|
454
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
436
455
|
});
|
|
437
456
|
this.OPTION(() => {
|
|
438
457
|
this.SUBRULE(this.question);
|
|
439
458
|
});
|
|
440
|
-
this.MANY2(
|
|
441
|
-
this.
|
|
459
|
+
this.MANY2({
|
|
460
|
+
GATE: this.BACKTRACK(this.contentBlock),
|
|
461
|
+
DEF: () => this.SUBRULE(this.contentBlock)
|
|
442
462
|
});
|
|
443
463
|
this.MANY3(() => {
|
|
444
464
|
this.SUBRULE(this.tool);
|
|
@@ -447,10 +467,7 @@ class MathExerciseParser extends CstParser {
|
|
|
447
467
|
this.SUBRULE(this.validation);
|
|
448
468
|
});
|
|
449
469
|
this.MANY4(() => {
|
|
450
|
-
this.OR2([
|
|
451
|
-
{ ALT: () => this.CONSUME2(Newline) },
|
|
452
|
-
{ ALT: () => this.CONSUME2(Space) }
|
|
453
|
-
]);
|
|
470
|
+
this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
|
|
454
471
|
});
|
|
455
472
|
});
|
|
456
473
|
this.question = this.RULE("question", () => {
|
|
@@ -459,10 +476,7 @@ class MathExerciseParser extends CstParser {
|
|
|
459
476
|
this.CONSUME(StringLiteral, { LABEL: "value" });
|
|
460
477
|
this.CONSUME(RBracket);
|
|
461
478
|
this.MANY(() => {
|
|
462
|
-
this.OR([
|
|
463
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
464
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
465
|
-
]);
|
|
479
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
466
480
|
});
|
|
467
481
|
});
|
|
468
482
|
this.contentBlock = this.RULE("contentBlock", () => {
|
|
@@ -484,77 +498,50 @@ class MathExerciseParser extends CstParser {
|
|
|
484
498
|
});
|
|
485
499
|
this.table = this.RULE("table", () => {
|
|
486
500
|
this.MANY(() => {
|
|
487
|
-
this.OR2([
|
|
488
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
489
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
490
|
-
]);
|
|
501
|
+
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
491
502
|
});
|
|
492
503
|
this.CONSUME(TABLE);
|
|
493
504
|
this.CONSUME(LBracket);
|
|
494
505
|
this.SUBRULE(this.parameterList);
|
|
495
506
|
this.CONSUME(RBracket);
|
|
496
507
|
this.MANY2(() => {
|
|
497
|
-
this.OR3([
|
|
498
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
499
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
500
|
-
]);
|
|
508
|
+
this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
501
509
|
});
|
|
502
510
|
this.CONSUME(LBrace);
|
|
503
511
|
this.MANY3(() => {
|
|
504
|
-
this.OR4([
|
|
505
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
506
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
507
|
-
]);
|
|
512
|
+
this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
508
513
|
});
|
|
509
514
|
this.AT_LEAST_ONE(() => {
|
|
510
515
|
this.SUBRULE(this.row);
|
|
511
516
|
});
|
|
512
517
|
this.CONSUME(RBrace);
|
|
513
518
|
this.MANY4(() => {
|
|
514
|
-
this.OR5([
|
|
515
|
-
{ ALT: () => this.CONSUME4(Newline) },
|
|
516
|
-
{ ALT: () => this.CONSUME4(Space) }
|
|
517
|
-
]);
|
|
519
|
+
this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
518
520
|
});
|
|
519
521
|
});
|
|
520
522
|
this.row = this.RULE("row", () => {
|
|
521
523
|
this.MANY(() => {
|
|
522
|
-
this.OR([
|
|
523
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
524
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
525
|
-
]);
|
|
524
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
526
525
|
});
|
|
527
526
|
this.CONSUME(ROW);
|
|
528
527
|
this.MANY2(() => {
|
|
529
|
-
this.OR2([
|
|
530
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
531
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
532
|
-
]);
|
|
528
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
533
529
|
});
|
|
534
530
|
this.CONSUME(LBrace);
|
|
535
531
|
this.MANY3(() => {
|
|
536
|
-
this.OR3([
|
|
537
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
538
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
539
|
-
]);
|
|
532
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
540
533
|
});
|
|
541
534
|
this.AT_LEAST_ONE(() => {
|
|
542
535
|
this.SUBRULE(this.cell);
|
|
543
536
|
});
|
|
544
537
|
this.CONSUME(RBrace);
|
|
545
538
|
this.MANY4(() => {
|
|
546
|
-
this.OR4([
|
|
547
|
-
{ ALT: () => this.CONSUME4(Space) },
|
|
548
|
-
{ ALT: () => this.CONSUME4(Newline) }
|
|
549
|
-
]);
|
|
539
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
550
540
|
});
|
|
551
541
|
});
|
|
552
542
|
this.cell = this.RULE("cell", () => {
|
|
553
543
|
this.MANY(() => {
|
|
554
|
-
this.OR([
|
|
555
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
556
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
557
|
-
]);
|
|
544
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
558
545
|
});
|
|
559
546
|
this.CONSUME(CELL);
|
|
560
547
|
this.OPTION(() => {
|
|
@@ -563,10 +550,7 @@ class MathExerciseParser extends CstParser {
|
|
|
563
550
|
this.CONSUME(RBracket);
|
|
564
551
|
});
|
|
565
552
|
this.MANY2(() => {
|
|
566
|
-
this.OR2([
|
|
567
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
568
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
569
|
-
]);
|
|
553
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
570
554
|
});
|
|
571
555
|
this.CONSUME(LBrace);
|
|
572
556
|
this.MANY3(() => {
|
|
@@ -577,70 +561,47 @@ class MathExerciseParser extends CstParser {
|
|
|
577
561
|
});
|
|
578
562
|
this.CONSUME(RBrace);
|
|
579
563
|
this.MANY4(() => {
|
|
580
|
-
this.OR4([
|
|
581
|
-
{ ALT: () => this.CONSUME4(Space) },
|
|
582
|
-
{ ALT: () => this.CONSUME4(Newline) }
|
|
583
|
-
]);
|
|
564
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
584
565
|
});
|
|
585
566
|
});
|
|
586
567
|
this.numberLine = this.RULE("numberLine", () => {
|
|
587
568
|
this.MANY(() => {
|
|
588
|
-
this.OR2([
|
|
589
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
590
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
591
|
-
]);
|
|
569
|
+
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
592
570
|
});
|
|
593
571
|
this.CONSUME(NUMBER_LINE);
|
|
594
572
|
this.CONSUME(LBracket);
|
|
595
573
|
this.SUBRULE(this.parameterList);
|
|
596
574
|
this.CONSUME(RBracket);
|
|
597
575
|
this.MANY2(() => {
|
|
598
|
-
this.OR3([
|
|
599
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
600
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
601
|
-
]);
|
|
576
|
+
this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
602
577
|
});
|
|
603
578
|
this.CONSUME(LBrace);
|
|
604
579
|
this.MANY3(() => {
|
|
605
|
-
this.OR4([
|
|
606
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
607
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
608
|
-
]);
|
|
580
|
+
this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
609
581
|
});
|
|
610
582
|
this.MANY4(() => {
|
|
611
|
-
this.
|
|
583
|
+
this.OR6([
|
|
584
|
+
{ ALT: () => this.SUBRULE(this.position) },
|
|
585
|
+
{ ALT: () => this.SUBRULE(this.counter) },
|
|
586
|
+
{ ALT: () => this.SUBRULE(this.highlight) }
|
|
587
|
+
]);
|
|
612
588
|
});
|
|
613
589
|
this.CONSUME(RBrace);
|
|
614
590
|
this.MANY5(() => {
|
|
615
|
-
this.OR5([
|
|
616
|
-
{ ALT: () => this.CONSUME4(Newline) },
|
|
617
|
-
{ ALT: () => this.CONSUME4(Space) }
|
|
618
|
-
]);
|
|
591
|
+
this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
619
592
|
});
|
|
620
593
|
});
|
|
621
594
|
this.position = this.RULE("position", () => {
|
|
622
|
-
this.MANY(() => {
|
|
623
|
-
this.OR([
|
|
624
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
625
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
626
|
-
]);
|
|
627
|
-
});
|
|
628
595
|
this.CONSUME(POSITION);
|
|
629
596
|
this.CONSUME(LBracket);
|
|
630
597
|
this.SUBRULE(this.parameterList);
|
|
631
598
|
this.CONSUME(RBracket);
|
|
632
599
|
this.MANY2(() => {
|
|
633
|
-
this.OR2([
|
|
634
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
635
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
636
|
-
]);
|
|
600
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
637
601
|
});
|
|
638
602
|
this.CONSUME(LBrace);
|
|
639
603
|
this.MANY3(() => {
|
|
640
|
-
this.OR3([
|
|
641
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
642
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
643
|
-
]);
|
|
604
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
644
605
|
});
|
|
645
606
|
this.MANY4(() => {
|
|
646
607
|
this.OR4([
|
|
@@ -650,10 +611,25 @@ class MathExerciseParser extends CstParser {
|
|
|
650
611
|
});
|
|
651
612
|
this.CONSUME(RBrace);
|
|
652
613
|
this.MANY5(() => {
|
|
653
|
-
this.OR5([
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
614
|
+
this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
this.counter = this.RULE("counter", () => {
|
|
618
|
+
this.CONSUME(COUNTER);
|
|
619
|
+
this.CONSUME(LBracket);
|
|
620
|
+
this.SUBRULE(this.parameterList);
|
|
621
|
+
this.CONSUME(RBracket);
|
|
622
|
+
this.MANY2(() => {
|
|
623
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
this.highlight = this.RULE("highlight", () => {
|
|
627
|
+
this.CONSUME(HIGHLIGHT);
|
|
628
|
+
this.CONSUME(LBracket);
|
|
629
|
+
this.SUBRULE(this.parameterList);
|
|
630
|
+
this.CONSUME(RBracket);
|
|
631
|
+
this.MANY2(() => {
|
|
632
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
657
633
|
});
|
|
658
634
|
});
|
|
659
635
|
this.line = this.RULE("line", () => {
|
|
@@ -666,7 +642,7 @@ class MathExerciseParser extends CstParser {
|
|
|
666
642
|
this.MANY2(() => {
|
|
667
643
|
this.CONSUME2(Space);
|
|
668
644
|
});
|
|
669
|
-
this.
|
|
645
|
+
this.MANY3(() => {
|
|
670
646
|
this.CONSUME(Newline);
|
|
671
647
|
});
|
|
672
648
|
});
|
|
@@ -750,28 +726,19 @@ class MathExerciseParser extends CstParser {
|
|
|
750
726
|
this.SUBRULE(this.stringList);
|
|
751
727
|
this.CONSUME(RBracket);
|
|
752
728
|
this.MANY(() => {
|
|
753
|
-
this.OR([
|
|
754
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
755
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
756
|
-
]);
|
|
729
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
757
730
|
});
|
|
758
731
|
});
|
|
759
732
|
this.radioButtons = this.RULE("radioButtons", () => {
|
|
760
733
|
this.MANY(() => {
|
|
761
|
-
this.OR([
|
|
762
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
763
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
764
|
-
]);
|
|
734
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
765
735
|
});
|
|
766
736
|
this.CONSUME(RADIO_BUTTONS_OPTIONS);
|
|
767
737
|
this.CONSUME(LBracket);
|
|
768
738
|
this.SUBRULE(this.parameterList);
|
|
769
739
|
this.CONSUME(RBracket);
|
|
770
740
|
this.MANY2(() => {
|
|
771
|
-
this.OR2([
|
|
772
|
-
{ ALT: () => this.CONSUME2(Newline) },
|
|
773
|
-
{ ALT: () => this.CONSUME2(Space) }
|
|
774
|
-
]);
|
|
741
|
+
this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
|
|
775
742
|
});
|
|
776
743
|
});
|
|
777
744
|
this.numericKeypad = this.RULE("numericKeypad", () => {
|
|
@@ -782,10 +749,7 @@ class MathExerciseParser extends CstParser {
|
|
|
782
749
|
});
|
|
783
750
|
this.CONSUME(RBracket);
|
|
784
751
|
this.MANY(() => {
|
|
785
|
-
this.OR([
|
|
786
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
787
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
788
|
-
]);
|
|
752
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
789
753
|
});
|
|
790
754
|
});
|
|
791
755
|
this.validation = this.RULE("validation", () => {
|
|
@@ -1020,7 +984,7 @@ function isLineBlock(value) {
|
|
|
1020
984
|
}
|
|
1021
985
|
const NumberLinePositionSchema = z.object({
|
|
1022
986
|
type: z.literal(AST_TYPES.NUMBER_LINE_POSITION),
|
|
1023
|
-
value: z.number(),
|
|
987
|
+
value: z.number().finite({ message: "POSITION: value must be a finite number" }),
|
|
1024
988
|
inlines: z.array(InlineNodeSchema).min(1)
|
|
1025
989
|
});
|
|
1026
990
|
function createNumberLinePosition(value, inlines) {
|
|
@@ -1040,10 +1004,50 @@ function createNumberLinePosition(value, inlines) {
|
|
|
1040
1004
|
inlines
|
|
1041
1005
|
});
|
|
1042
1006
|
}
|
|
1007
|
+
const NumberLineCounterSchema = z.object({
|
|
1008
|
+
type: z.literal(AST_TYPES.NUMBER_LINE_COUNTER),
|
|
1009
|
+
min: z.number().int().min(1, { message: "COUNTER: min must be >= 1" }),
|
|
1010
|
+
max: z.number().int().positive({ message: "COUNTER: max must be > 0" }),
|
|
1011
|
+
default: z.number().int().nonnegative().optional(),
|
|
1012
|
+
name: z.string().optional()
|
|
1013
|
+
}).refine((d) => d.min <= d.max, {
|
|
1014
|
+
message: "COUNTER: min must be <= max"
|
|
1015
|
+
});
|
|
1016
|
+
function createNumberLineCounter(max, min, defaultValue2, name) {
|
|
1017
|
+
return NumberLineCounterSchema.parse({
|
|
1018
|
+
type: AST_TYPES.NUMBER_LINE_COUNTER,
|
|
1019
|
+
min: min ?? 1,
|
|
1020
|
+
max,
|
|
1021
|
+
...defaultValue2 !== void 0 && { default: defaultValue2 },
|
|
1022
|
+
...name !== void 0 && { name }
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
const NumberLineHighlightSchema = z.object({
|
|
1026
|
+
type: z.literal(AST_TYPES.NUMBER_LINE_HIGHLIGHT),
|
|
1027
|
+
from: z.number().finite({ message: "HIGHLIGHT: from must be a finite number" }),
|
|
1028
|
+
to: z.number().finite({ message: "HIGHLIGHT: to must be a finite number" }),
|
|
1029
|
+
color: z.enum(NUMBER_LINE_HIGHLIGHT_COLORS, {
|
|
1030
|
+
errorMap: () => ({
|
|
1031
|
+
message: `HIGHLIGHT: color must be one of ${NUMBER_LINE_HIGHLIGHT_COLORS.join(", ")}`
|
|
1032
|
+
})
|
|
1033
|
+
}).optional(),
|
|
1034
|
+
label: z.string().optional()
|
|
1035
|
+
}).refine((d) => d.from < d.to, {
|
|
1036
|
+
message: "HIGHLIGHT: from must be less than to"
|
|
1037
|
+
});
|
|
1038
|
+
function createNumberLineHighlight(from, to, color, label) {
|
|
1039
|
+
return NumberLineHighlightSchema.parse({
|
|
1040
|
+
type: AST_TYPES.NUMBER_LINE_HIGHLIGHT,
|
|
1041
|
+
from,
|
|
1042
|
+
to,
|
|
1043
|
+
...color !== void 0 && { color },
|
|
1044
|
+
...label !== void 0 && { label }
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1043
1047
|
const NumberLineBlockSchema = z.object({
|
|
1044
1048
|
type: z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
|
|
1045
|
-
min: z.number(),
|
|
1046
|
-
max: z.number(),
|
|
1049
|
+
min: z.number().finite({ message: "NUMBER_LINE: min must be a finite number" }),
|
|
1050
|
+
max: z.number().finite({ message: "NUMBER_LINE: max must be a finite number" }),
|
|
1047
1051
|
step: z.number().positive({ message: "NUMBER_LINE: step must be greater than 0" }).default(1),
|
|
1048
1052
|
minorTicks: z.number().int().nonnegative({ message: "NUMBER_LINE: minorTicks must be >= 0" }).optional(),
|
|
1049
1053
|
style: z.enum(NUMBER_LINE_STYLES, {
|
|
@@ -1051,6 +1055,13 @@ const NumberLineBlockSchema = z.object({
|
|
|
1051
1055
|
message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
|
|
1052
1056
|
})
|
|
1053
1057
|
}).optional(),
|
|
1058
|
+
labelVisibility: z.enum(NUMBER_LINE_LABELS, {
|
|
1059
|
+
errorMap: () => ({
|
|
1060
|
+
message: `NUMBER_LINE: labelVisibility must be one of ${NUMBER_LINE_LABELS.join(", ")}`
|
|
1061
|
+
})
|
|
1062
|
+
}).optional(),
|
|
1063
|
+
counter: NumberLineCounterSchema.optional(),
|
|
1064
|
+
highlights: z.array(NumberLineHighlightSchema).optional(),
|
|
1054
1065
|
positions: z.array(NumberLinePositionSchema)
|
|
1055
1066
|
}).refine((data) => data.min < data.max, {
|
|
1056
1067
|
message: "NUMBER_LINE: min must be less than max"
|
|
@@ -1071,8 +1082,24 @@ const NumberLineBlockSchema = z.object({
|
|
|
1071
1082
|
}
|
|
1072
1083
|
seen.add(pos.value);
|
|
1073
1084
|
}
|
|
1085
|
+
for (const h of data.highlights ?? []) {
|
|
1086
|
+
if (h.from < data.min || h.to > data.max) {
|
|
1087
|
+
ctx.addIssue({
|
|
1088
|
+
code: z.ZodIssueCode.custom,
|
|
1089
|
+
message: `NUMBER_LINE: HIGHLIGHT range [${h.from}, ${h.to}] must be within [${data.min}, ${data.max}]`
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (data.counter?.default !== void 0) {
|
|
1094
|
+
if (data.counter.default < data.counter.min || data.counter.default > data.counter.max) {
|
|
1095
|
+
ctx.addIssue({
|
|
1096
|
+
code: z.ZodIssueCode.custom,
|
|
1097
|
+
message: `NUMBER_LINE: COUNTER default (${data.counter.default}) must be within [${data.counter.min}, ${data.counter.max}]`
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1074
1101
|
});
|
|
1075
|
-
function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
|
|
1102
|
+
function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights) {
|
|
1076
1103
|
return NumberLineBlockSchema.parse({
|
|
1077
1104
|
type: AST_TYPES.NUMBER_LINE_BLOCK,
|
|
1078
1105
|
min,
|
|
@@ -1080,6 +1107,9 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
|
|
|
1080
1107
|
step,
|
|
1081
1108
|
minorTicks,
|
|
1082
1109
|
style,
|
|
1110
|
+
labelVisibility,
|
|
1111
|
+
counter,
|
|
1112
|
+
highlights,
|
|
1083
1113
|
positions
|
|
1084
1114
|
});
|
|
1085
1115
|
}
|
|
@@ -1290,8 +1320,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1290
1320
|
const step = params.step !== void 0 ? Number(params.step) : void 0;
|
|
1291
1321
|
const minorTicks = params.minorTicks !== void 0 ? Number(params.minorTicks) : void 0;
|
|
1292
1322
|
const style = params.style;
|
|
1323
|
+
const labelVisibility = params.labelVisibility;
|
|
1293
1324
|
const positions = ctx.position ? ctx.position.map((p) => this.visit(p)) : [];
|
|
1294
|
-
|
|
1325
|
+
const rawCounters = ctx.counter;
|
|
1326
|
+
if (rawCounters && rawCounters.length > 1) {
|
|
1327
|
+
throw new Error("NUMBER_LINE: only one COUNTER block is allowed");
|
|
1328
|
+
}
|
|
1329
|
+
const counter = rawCounters ? this.visit(rawCounters) : void 0;
|
|
1330
|
+
const highlights = ctx.highlight ? ctx.highlight.map((h) => this.visit(h)) : [];
|
|
1331
|
+
return createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights);
|
|
1295
1332
|
}
|
|
1296
1333
|
position(ctx) {
|
|
1297
1334
|
const params = this.visit(ctx.parameterList);
|
|
@@ -1302,6 +1339,28 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1302
1339
|
const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
|
|
1303
1340
|
return createNumberLinePosition(value, inlines);
|
|
1304
1341
|
}
|
|
1342
|
+
counter(ctx) {
|
|
1343
|
+
const params = this.visit(ctx.parameterList);
|
|
1344
|
+
if (params.max === void 0) {
|
|
1345
|
+
throw new Error("COUNTER requires parameter: max");
|
|
1346
|
+
}
|
|
1347
|
+
const max = Number(params.max);
|
|
1348
|
+
const min = params.min !== void 0 ? Number(params.min) : void 0;
|
|
1349
|
+
const defaultValue2 = params.default !== void 0 ? Number(params.default) : void 0;
|
|
1350
|
+
const name = params.name;
|
|
1351
|
+
return createNumberLineCounter(max, min, defaultValue2, name);
|
|
1352
|
+
}
|
|
1353
|
+
highlight(ctx) {
|
|
1354
|
+
const params = this.visit(ctx.parameterList);
|
|
1355
|
+
if (params.from === void 0 || params.to === void 0) {
|
|
1356
|
+
throw new Error("HIGHLIGHT requires parameters: from and to");
|
|
1357
|
+
}
|
|
1358
|
+
const from = Number(params.from);
|
|
1359
|
+
const to = Number(params.to);
|
|
1360
|
+
const color = params.color;
|
|
1361
|
+
const label = params.label;
|
|
1362
|
+
return createNumberLineHighlight(from, to, color, label);
|
|
1363
|
+
}
|
|
1305
1364
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1306
1365
|
// Line Block
|
|
1307
1366
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1632,17 +1691,36 @@ const TableItemSchema = z.object({
|
|
|
1632
1691
|
const NumberLinePositionItemSchema = z.object({
|
|
1633
1692
|
id: z.string(),
|
|
1634
1693
|
type: z.literal(JSON_TYPES.NUMBER_LINE_POSITION),
|
|
1635
|
-
value: z.number(),
|
|
1636
|
-
content: z.array(
|
|
1694
|
+
value: z.number().finite(),
|
|
1695
|
+
content: z.array(LayoutItemSchema)
|
|
1696
|
+
});
|
|
1697
|
+
const NumberLineCounterItemSchema = z.object({
|
|
1698
|
+
id: z.string(),
|
|
1699
|
+
type: z.literal(JSON_TYPES.NUMBER_LINE_COUNTER),
|
|
1700
|
+
min: z.number().int().min(1),
|
|
1701
|
+
max: z.number().int().positive(),
|
|
1702
|
+
default: z.number().int().nonnegative().optional(),
|
|
1703
|
+
name: z.string().optional()
|
|
1704
|
+
});
|
|
1705
|
+
const NumberLineHighlightItemSchema = z.object({
|
|
1706
|
+
id: z.string(),
|
|
1707
|
+
type: z.literal(JSON_TYPES.NUMBER_LINE_HIGHLIGHT),
|
|
1708
|
+
from: z.number().finite(),
|
|
1709
|
+
to: z.number().finite(),
|
|
1710
|
+
color: z.enum(NUMBER_LINE_HIGHLIGHT_COLORS).optional(),
|
|
1711
|
+
label: z.string().optional()
|
|
1637
1712
|
});
|
|
1638
1713
|
const NumberLineItemSchema = z.object({
|
|
1639
1714
|
id: z.string(),
|
|
1640
1715
|
type: z.literal(JSON_TYPES.NUMBER_LINE),
|
|
1641
|
-
min: z.number(),
|
|
1642
|
-
max: z.number(),
|
|
1716
|
+
min: z.number().finite(),
|
|
1717
|
+
max: z.number().finite(),
|
|
1643
1718
|
step: z.number().positive(),
|
|
1644
1719
|
minorTicks: z.number().int().nonnegative().optional(),
|
|
1645
1720
|
style: z.enum(NUMBER_LINE_STYLES).optional(),
|
|
1721
|
+
labelVisibility: z.enum(NUMBER_LINE_LABELS).optional(),
|
|
1722
|
+
counter: NumberLineCounterItemSchema.optional(),
|
|
1723
|
+
highlights: z.array(NumberLineHighlightItemSchema).optional(),
|
|
1646
1724
|
positions: z.array(NumberLinePositionItemSchema)
|
|
1647
1725
|
});
|
|
1648
1726
|
const StringInputJSONSchema = z.object({
|
|
@@ -1716,7 +1794,7 @@ function isQuestionText(item) {
|
|
|
1716
1794
|
function isTextItem$1(item) {
|
|
1717
1795
|
return item.type === JSON_TYPES.TEXT;
|
|
1718
1796
|
}
|
|
1719
|
-
function isWidgetRef
|
|
1797
|
+
function isWidgetRef(item) {
|
|
1720
1798
|
return item.type === JSON_TYPES.WIDGET_REF;
|
|
1721
1799
|
}
|
|
1722
1800
|
function isHighlightedText(item) {
|
|
@@ -1889,16 +1967,42 @@ function transformNumberLine(numberLine, idGenerator) {
|
|
|
1889
1967
|
content
|
|
1890
1968
|
};
|
|
1891
1969
|
});
|
|
1970
|
+
let counterItem;
|
|
1971
|
+
if (numberLine.counter !== void 0) {
|
|
1972
|
+
const c = numberLine.counter;
|
|
1973
|
+
counterItem = {
|
|
1974
|
+
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_COUNTER),
|
|
1975
|
+
type: JSON_TYPES.NUMBER_LINE_COUNTER,
|
|
1976
|
+
min: c.min,
|
|
1977
|
+
max: c.max,
|
|
1978
|
+
...c.default !== void 0 && { default: c.default },
|
|
1979
|
+
...c.name !== void 0 && { name: c.name }
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
let highlights;
|
|
1983
|
+
if (numberLine.highlights !== void 0 && numberLine.highlights.length > 0) {
|
|
1984
|
+
highlights = numberLine.highlights.map((h) => ({
|
|
1985
|
+
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_HIGHLIGHT),
|
|
1986
|
+
type: JSON_TYPES.NUMBER_LINE_HIGHLIGHT,
|
|
1987
|
+
from: h.from,
|
|
1988
|
+
to: h.to,
|
|
1989
|
+
...h.color !== void 0 && { color: h.color },
|
|
1990
|
+
...h.label !== void 0 && { label: h.label }
|
|
1991
|
+
}));
|
|
1992
|
+
}
|
|
1892
1993
|
const item = {
|
|
1893
1994
|
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
|
|
1894
1995
|
type: JSON_TYPES.NUMBER_LINE,
|
|
1895
1996
|
min: numberLine.min,
|
|
1896
1997
|
max: numberLine.max,
|
|
1897
1998
|
step: numberLine.step,
|
|
1999
|
+
...numberLine.minorTicks !== void 0 && { minorTicks: numberLine.minorTicks },
|
|
2000
|
+
...numberLine.style !== void 0 && { style: numberLine.style },
|
|
2001
|
+
...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
|
|
2002
|
+
...counterItem !== void 0 && { counter: counterItem },
|
|
2003
|
+
...highlights !== void 0 && { highlights },
|
|
1898
2004
|
positions
|
|
1899
2005
|
};
|
|
1900
|
-
if (numberLine.minorTicks !== void 0) item.minorTicks = numberLine.minorTicks;
|
|
1901
|
-
if (numberLine.style !== void 0) item.style = numberLine.style;
|
|
1902
2006
|
return item;
|
|
1903
2007
|
}
|
|
1904
2008
|
function transformTable(table, idGenerator) {
|
|
@@ -2021,6 +2125,49 @@ function extractVariables$1(expression, validVariables) {
|
|
|
2021
2125
|
}
|
|
2022
2126
|
return uniqueVars;
|
|
2023
2127
|
}
|
|
2128
|
+
function normalize(value) {
|
|
2129
|
+
if (typeof value === "number") {
|
|
2130
|
+
return String(Math.round(value * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
2131
|
+
}
|
|
2132
|
+
const trimmed = String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
2133
|
+
const asNum = Number(trimmed);
|
|
2134
|
+
if (trimmed !== "" && !isNaN(asNum)) {
|
|
2135
|
+
return String(Math.round(asNum * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
2136
|
+
}
|
|
2137
|
+
return trimmed;
|
|
2138
|
+
}
|
|
2139
|
+
const NL_DRAG_PREFIX = "\0nl-drag\0";
|
|
2140
|
+
const isNLDragEncoded = (v) => v.startsWith(NL_DRAG_PREFIX);
|
|
2141
|
+
const encodeNLDragValue = (label, position) => `${NL_DRAG_PREFIX}${label}@${String(position)}`;
|
|
2142
|
+
function isNLDragCorrect(encoded, expectedLabel, expectedPosition) {
|
|
2143
|
+
const payload = encoded.slice(NL_DRAG_PREFIX.length);
|
|
2144
|
+
const atIdx = payload.lastIndexOf("@");
|
|
2145
|
+
const label = normalize(payload.slice(0, atIdx));
|
|
2146
|
+
const position = normalize(payload.slice(atIdx + 1));
|
|
2147
|
+
return label === normalize(expectedLabel) && position === normalize(expectedPosition);
|
|
2148
|
+
}
|
|
2149
|
+
function collectNumberLines(items) {
|
|
2150
|
+
const result = [];
|
|
2151
|
+
for (const item of items) {
|
|
2152
|
+
if (item.type === JSON_TYPES.NUMBER_LINE) result.push(item);
|
|
2153
|
+
else if (item.type === JSON_TYPES.GROUP) result.push(...collectNumberLines(item.items));
|
|
2154
|
+
}
|
|
2155
|
+
return result;
|
|
2156
|
+
}
|
|
2157
|
+
function buildPositionMap(exercise) {
|
|
2158
|
+
const map = {};
|
|
2159
|
+
for (const nl of collectNumberLines(exercise.layout)) {
|
|
2160
|
+
for (const pos of nl.positions) {
|
|
2161
|
+
for (const c of pos.content) {
|
|
2162
|
+
if (isWidgetRef(c)) {
|
|
2163
|
+
map[c.widgetId] = pos.value;
|
|
2164
|
+
break;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
return map;
|
|
2170
|
+
}
|
|
2024
2171
|
function validateSimple(exercise, userInputs) {
|
|
2025
2172
|
const details = {};
|
|
2026
2173
|
let allCorrect = true;
|
|
@@ -2034,10 +2181,11 @@ function validateSimple(exercise, userInputs) {
|
|
|
2034
2181
|
score: 0
|
|
2035
2182
|
};
|
|
2036
2183
|
}
|
|
2184
|
+
const positionMap = buildPositionMap(exercise);
|
|
2037
2185
|
for (const [widgetId, expected] of Object.entries(answers)) {
|
|
2038
2186
|
totalCount++;
|
|
2039
2187
|
const userValue = userInputs[widgetId];
|
|
2040
|
-
const isCorrect = areValuesEqual(userValue, expected);
|
|
2188
|
+
const isCorrect = areValuesEqual(userValue, expected, positionMap[widgetId]);
|
|
2041
2189
|
details[widgetId] = {
|
|
2042
2190
|
widgetId,
|
|
2043
2191
|
userValue,
|
|
@@ -2065,19 +2213,13 @@ function validateSimple(exercise, userInputs) {
|
|
|
2065
2213
|
score: totalCount > 0 ? correctCount / totalCount : 0
|
|
2066
2214
|
};
|
|
2067
2215
|
}
|
|
2068
|
-
function areValuesEqual(userValue, expected) {
|
|
2069
|
-
if (userValue === void 0 || userValue === null)
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
const normalizedExpected = normalize(expected);
|
|
2074
|
-
return normalizedUser === normalizedExpected;
|
|
2075
|
-
}
|
|
2076
|
-
function normalize(value) {
|
|
2077
|
-
if (typeof value === "number") {
|
|
2078
|
-
return String(Math.round(value * 1e10) / 1e10);
|
|
2216
|
+
function areValuesEqual(userValue, expected, expectedPosition) {
|
|
2217
|
+
if (userValue === void 0 || userValue === null) return false;
|
|
2218
|
+
if (typeof userValue === "string" && isNLDragEncoded(userValue)) {
|
|
2219
|
+
if (expectedPosition === void 0) return false;
|
|
2220
|
+
return isNLDragCorrect(userValue, expected, expectedPosition);
|
|
2079
2221
|
}
|
|
2080
|
-
return
|
|
2222
|
+
return normalize(userValue) === normalize(expected);
|
|
2081
2223
|
}
|
|
2082
2224
|
const math = create(all);
|
|
2083
2225
|
const BLOCKED_FUNCTIONS = [
|
|
@@ -6619,7 +6761,10 @@ const WidgetRenderer = ({
|
|
|
6619
6761
|
}
|
|
6620
6762
|
return null;
|
|
6621
6763
|
};
|
|
6622
|
-
const
|
|
6764
|
+
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
6765
|
+
const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
|
|
6766
|
+
const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
|
|
6767
|
+
const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
|
|
6623
6768
|
const toPercent = (value, min, max) => {
|
|
6624
6769
|
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
6625
6770
|
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
@@ -6635,19 +6780,24 @@ const getMajorTicks = (min, max, step) => {
|
|
|
6635
6780
|
const count = Math.floor((max - min) / step);
|
|
6636
6781
|
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
6637
6782
|
const lastTick = ticks[ticks.length - 1];
|
|
6638
|
-
if (
|
|
6783
|
+
if (!almostEqual(lastTick, max)) {
|
|
6784
|
+
ticks.push(max);
|
|
6785
|
+
} else {
|
|
6786
|
+
ticks[ticks.length - 1] = max;
|
|
6787
|
+
}
|
|
6639
6788
|
return ticks;
|
|
6640
6789
|
};
|
|
6641
6790
|
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
6642
|
-
if (minorTicks <= 0) return [];
|
|
6791
|
+
if (minorTicks <= 0 || majorTicks.length < 2) return [];
|
|
6643
6792
|
const result = [];
|
|
6644
|
-
|
|
6645
|
-
const
|
|
6646
|
-
const
|
|
6793
|
+
for (let i = 0; i < majorTicks.length - 1; i++) {
|
|
6794
|
+
const from = majorTicks[i];
|
|
6795
|
+
const to = majorTicks[i + 1];
|
|
6796
|
+
const interval = to - from;
|
|
6647
6797
|
for (let j = 1; j <= minorTicks; j++) {
|
|
6648
|
-
result.push(cleanFloat(
|
|
6798
|
+
result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
|
|
6649
6799
|
}
|
|
6650
|
-
}
|
|
6800
|
+
}
|
|
6651
6801
|
return result;
|
|
6652
6802
|
};
|
|
6653
6803
|
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
@@ -6656,19 +6806,41 @@ const getSnapTicks = (majorTicks, minorTicks) => [
|
|
|
6656
6806
|
];
|
|
6657
6807
|
const snapToNearest = (raw, ticks) => {
|
|
6658
6808
|
if (ticks.length === 0) return cleanFloat(raw);
|
|
6659
|
-
|
|
6660
|
-
(best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best
|
|
6661
|
-
);
|
|
6662
|
-
return cleanFloat(snapped);
|
|
6809
|
+
return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
|
|
6663
6810
|
};
|
|
6664
|
-
const isWidgetRef = (item) => item.type === JSON_TYPES.WIDGET_REF;
|
|
6665
6811
|
const getPositionWidgetId = (content) => {
|
|
6666
6812
|
for (const item of content) {
|
|
6667
6813
|
if (isWidgetRef(item)) return item.widgetId;
|
|
6668
6814
|
}
|
|
6669
6815
|
return null;
|
|
6670
6816
|
};
|
|
6671
|
-
const
|
|
6817
|
+
const toHighlightColor = (color) => `var(--nl-highlight-${color})`;
|
|
6818
|
+
const getBandGeometry = (h, min, max) => {
|
|
6819
|
+
const leftPct = toPercent(Math.min(h.from, h.to), min, max);
|
|
6820
|
+
const rightPct = toPercent(Math.max(h.from, h.to), min, max);
|
|
6821
|
+
return { leftPct, widthPct: rightPct - leftPct };
|
|
6822
|
+
};
|
|
6823
|
+
const HighlightBands = ({ highlights, min, max }) => {
|
|
6824
|
+
if (!highlights?.length) return null;
|
|
6825
|
+
return /* @__PURE__ */ jsx(Fragment, { children: highlights.map((h) => {
|
|
6826
|
+
const { leftPct, widthPct } = getBandGeometry(h, min, max);
|
|
6827
|
+
if (widthPct <= 0) return null;
|
|
6828
|
+
return /* @__PURE__ */ jsx(
|
|
6829
|
+
"div",
|
|
6830
|
+
{
|
|
6831
|
+
className: "number-line-highlight-band",
|
|
6832
|
+
"aria-hidden": "true",
|
|
6833
|
+
style: {
|
|
6834
|
+
left: `${leftPct}%`,
|
|
6835
|
+
width: `${widthPct}%`,
|
|
6836
|
+
...h.color && { "--nl-highlight-color": toHighlightColor(h.color) }
|
|
6837
|
+
}
|
|
6838
|
+
},
|
|
6839
|
+
h.id
|
|
6840
|
+
);
|
|
6841
|
+
}) });
|
|
6842
|
+
};
|
|
6843
|
+
const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
6672
6844
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
6673
6845
|
id: widgetId,
|
|
6674
6846
|
data: { type: "placed-chip", widgetId, label }
|
|
@@ -6676,23 +6848,49 @@ const PlacedChip = ({ widgetId, label, leftPercent, onRemove }) => {
|
|
|
6676
6848
|
return /* @__PURE__ */ jsx(
|
|
6677
6849
|
"div",
|
|
6678
6850
|
{
|
|
6679
|
-
className:
|
|
6680
|
-
style: { left: `${leftPercent}
|
|
6681
|
-
children: /* @__PURE__ */
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6851
|
+
className: `number-line-placed-chip${isLanding ? " nl-chip-landing" : ""}${isDragging ? " nl-chip-dragging" : ""}`,
|
|
6852
|
+
style: { left: `${leftPercent}%` },
|
|
6853
|
+
children: /* @__PURE__ */ jsxs("div", { className: "number-line-placed-chip-inner", children: [
|
|
6854
|
+
/* @__PURE__ */ jsx(
|
|
6855
|
+
"button",
|
|
6856
|
+
{
|
|
6857
|
+
ref: setNodeRef,
|
|
6858
|
+
className: "number-line-placed-chip-btn",
|
|
6859
|
+
"aria-label": `${label}, drag to move`,
|
|
6860
|
+
...listeners,
|
|
6861
|
+
...attributes,
|
|
6862
|
+
children: label
|
|
6863
|
+
}
|
|
6864
|
+
),
|
|
6865
|
+
/* @__PURE__ */ jsx(
|
|
6866
|
+
"button",
|
|
6867
|
+
{
|
|
6868
|
+
type: "button",
|
|
6869
|
+
className: "number-line-placed-chip-remove",
|
|
6870
|
+
"aria-label": "Remove",
|
|
6871
|
+
onClick: onRemove,
|
|
6872
|
+
children: "✕"
|
|
6873
|
+
}
|
|
6874
|
+
)
|
|
6875
|
+
] })
|
|
6693
6876
|
}
|
|
6694
6877
|
);
|
|
6695
6878
|
};
|
|
6879
|
+
function findNearestFreeWidgetSlot(positions, axisValue, values, placedChips, excludeWid) {
|
|
6880
|
+
let best;
|
|
6881
|
+
let bestDist = Infinity;
|
|
6882
|
+
for (const pos of positions) {
|
|
6883
|
+
const wid = getPositionWidgetId(pos.content);
|
|
6884
|
+
if (!wid) continue;
|
|
6885
|
+
if (wid !== excludeWid && (values[wid] || placedChips.has(wid))) continue;
|
|
6886
|
+
const dist = Math.abs(pos.value - axisValue);
|
|
6887
|
+
if (dist < bestDist) {
|
|
6888
|
+
bestDist = dist;
|
|
6889
|
+
best = pos;
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
return best;
|
|
6893
|
+
}
|
|
6696
6894
|
const FreeDragAxis = ({
|
|
6697
6895
|
axisDropId,
|
|
6698
6896
|
min,
|
|
@@ -6703,17 +6901,51 @@ const FreeDragAxis = ({
|
|
|
6703
6901
|
showArrowRight,
|
|
6704
6902
|
positions,
|
|
6705
6903
|
values,
|
|
6706
|
-
onChange
|
|
6904
|
+
onChange,
|
|
6905
|
+
highlights,
|
|
6906
|
+
snapKey
|
|
6707
6907
|
}) => {
|
|
6708
6908
|
const axisRef = useRef(null);
|
|
6709
6909
|
const snapTicks = useMemo(
|
|
6710
6910
|
() => getSnapTicks(majorTicks, minorTicks),
|
|
6711
6911
|
[majorTicks, minorTicks]
|
|
6712
6912
|
);
|
|
6713
|
-
const
|
|
6714
|
-
|
|
6913
|
+
const minorValues = useMemo(
|
|
6914
|
+
() => getMinorTickValues(majorTicks, minorTicks),
|
|
6915
|
+
[majorTicks, minorTicks]
|
|
6916
|
+
);
|
|
6917
|
+
const [placedChips, setPlacedChips] = useState(/* @__PURE__ */ new Map());
|
|
6918
|
+
const [hoverSnap, setHoverSnap] = useState(null);
|
|
6919
|
+
const [landingChips, setLandingChips] = useState(/* @__PURE__ */ new Set());
|
|
6920
|
+
const landingTimersRef = useRef(/* @__PURE__ */ new Map());
|
|
6921
|
+
useEffect(() => () => landingTimersRef.current.forEach(clearTimeout), []);
|
|
6922
|
+
const onChangeRef = useRef(onChange);
|
|
6923
|
+
const placedChipsRef = useRef(placedChips);
|
|
6924
|
+
useEffect(() => {
|
|
6925
|
+
onChangeRef.current = onChange;
|
|
6926
|
+
}, [onChange]);
|
|
6927
|
+
useEffect(() => {
|
|
6928
|
+
placedChipsRef.current = placedChips;
|
|
6929
|
+
}, [placedChips]);
|
|
6930
|
+
const isFirstRender = useRef(true);
|
|
6715
6931
|
useEffect(() => {
|
|
6716
|
-
|
|
6932
|
+
if (isFirstRender.current) {
|
|
6933
|
+
isFirstRender.current = false;
|
|
6934
|
+
return;
|
|
6935
|
+
}
|
|
6936
|
+
for (const [widgetId, chip] of placedChipsRef.current) {
|
|
6937
|
+
onChangeRef.current(widgetId, "", false, chip.sourceId);
|
|
6938
|
+
}
|
|
6939
|
+
landingTimersRef.current.forEach(clearTimeout);
|
|
6940
|
+
landingTimersRef.current.clear();
|
|
6941
|
+
const empty = /* @__PURE__ */ new Map();
|
|
6942
|
+
placedChipsRef.current = empty;
|
|
6943
|
+
setPlacedChips(empty);
|
|
6944
|
+
setHoverSnap(null);
|
|
6945
|
+
setLandingChips(/* @__PURE__ */ new Set());
|
|
6946
|
+
}, [snapKey]);
|
|
6947
|
+
useEffect(() => {
|
|
6948
|
+
setPlacedChips((prev) => {
|
|
6717
6949
|
let changed = false;
|
|
6718
6950
|
const next = new Map(prev);
|
|
6719
6951
|
for (const [wid] of next) {
|
|
@@ -6722,21 +6954,13 @@ const FreeDragAxis = ({
|
|
|
6722
6954
|
changed = true;
|
|
6723
6955
|
}
|
|
6724
6956
|
}
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
setDropSources((prev) => {
|
|
6728
|
-
let changed = false;
|
|
6729
|
-
const next = new Map(prev);
|
|
6730
|
-
for (const [wid] of next) {
|
|
6731
|
-
if (!values[wid]) {
|
|
6732
|
-
next.delete(wid);
|
|
6733
|
-
changed = true;
|
|
6734
|
-
}
|
|
6957
|
+
if (changed) {
|
|
6958
|
+
placedChipsRef.current = next;
|
|
6735
6959
|
}
|
|
6736
6960
|
return changed ? next : prev;
|
|
6737
6961
|
});
|
|
6738
6962
|
}, [values]);
|
|
6739
|
-
const { setNodeRef
|
|
6963
|
+
const { setNodeRef } = useDroppable({ id: axisDropId });
|
|
6740
6964
|
const mergedRef = useCallback((node) => {
|
|
6741
6965
|
axisRef.current = node;
|
|
6742
6966
|
setNodeRef(node);
|
|
@@ -6744,91 +6968,163 @@ const FreeDragAxis = ({
|
|
|
6744
6968
|
const snapFromEvent = useCallback((translated) => {
|
|
6745
6969
|
if (!axisRef.current) return null;
|
|
6746
6970
|
const axisRect = axisRef.current.getBoundingClientRect();
|
|
6971
|
+
if (axisRect.width === 0) return null;
|
|
6747
6972
|
const chipCenterX = translated.left + translated.width / 2;
|
|
6748
6973
|
const raw = pixelToValue(chipCenterX, axisRect.left, axisRect.width, min, max);
|
|
6749
6974
|
return snapToNearest(raw, snapTicks);
|
|
6750
6975
|
}, [min, max, snapTicks]);
|
|
6976
|
+
const isTickOccupied = useCallback(
|
|
6977
|
+
(tick, excludeWid) => [...placedChipsRef.current.entries()].some(
|
|
6978
|
+
([wid, c]) => wid !== excludeWid && almostEqual(c.position, tick)
|
|
6979
|
+
),
|
|
6980
|
+
[]
|
|
6981
|
+
// reads placedChipsRef (a ref — stable, no dep needed)
|
|
6982
|
+
);
|
|
6983
|
+
const findNearestFreeWidgetSlotCb = useCallback(
|
|
6984
|
+
(axisValue, excludeWid) => findNearestFreeWidgetSlot(positions, axisValue, values, placedChipsRef.current, excludeWid),
|
|
6985
|
+
[positions, values]
|
|
6986
|
+
);
|
|
6987
|
+
const triggerLanding = useCallback((widgetId) => {
|
|
6988
|
+
setLandingChips((prev) => new Set(prev).add(widgetId));
|
|
6989
|
+
clearTimeout(landingTimersRef.current.get(widgetId));
|
|
6990
|
+
landingTimersRef.current.set(widgetId, setTimeout(() => {
|
|
6991
|
+
setLandingChips((prev) => {
|
|
6992
|
+
const s = new Set(prev);
|
|
6993
|
+
s.delete(widgetId);
|
|
6994
|
+
return s;
|
|
6995
|
+
});
|
|
6996
|
+
landingTimersRef.current.delete(widgetId);
|
|
6997
|
+
}, 300));
|
|
6998
|
+
}, []);
|
|
6751
6999
|
useDndMonitor({
|
|
7000
|
+
onDragMove(event) {
|
|
7001
|
+
if (!event.over || event.over.id !== axisDropId) {
|
|
7002
|
+
setHoverSnap(null);
|
|
7003
|
+
return;
|
|
7004
|
+
}
|
|
7005
|
+
const translated = event.active.rect.current.translated;
|
|
7006
|
+
if (!translated) return;
|
|
7007
|
+
setHoverSnap(snapFromEvent(translated));
|
|
7008
|
+
},
|
|
7009
|
+
onDragCancel() {
|
|
7010
|
+
setHoverSnap(null);
|
|
7011
|
+
},
|
|
6752
7012
|
onDragEnd(event) {
|
|
7013
|
+
setHoverSnap(null);
|
|
6753
7014
|
if (!event.over || event.over.id !== axisDropId) return;
|
|
6754
7015
|
if (!axisRef.current) return;
|
|
6755
7016
|
const translated = event.active.rect.current.translated;
|
|
6756
7017
|
if (!translated) return;
|
|
6757
|
-
const
|
|
6758
|
-
if (
|
|
7018
|
+
const rawPlaced = snapFromEvent(translated);
|
|
7019
|
+
if (rawPlaced === null) {
|
|
7020
|
+
if (process.env.NODE_ENV !== "production") {
|
|
7021
|
+
console.warn("[FreeDragAxis] snap calculation failed — axis may not be laid out yet");
|
|
7022
|
+
}
|
|
7023
|
+
return;
|
|
7024
|
+
}
|
|
7025
|
+
const placed = cleanFloat(rawPlaced);
|
|
6759
7026
|
const data = event.active.data.current;
|
|
6760
|
-
if (data
|
|
6761
|
-
|
|
7027
|
+
if (!data) return;
|
|
7028
|
+
if (data.type === "placed-chip") {
|
|
7029
|
+
const oldWidgetId = data.widgetId;
|
|
6762
7030
|
const label = data.label;
|
|
6763
|
-
const
|
|
6764
|
-
|
|
6765
|
-
|
|
7031
|
+
const sourceId2 = placedChipsRef.current.get(oldWidgetId)?.sourceId ?? oldWidgetId;
|
|
7032
|
+
if (isTickOccupied(placed, oldWidgetId)) return;
|
|
7033
|
+
const targetPos2 = findNearestFreeWidgetSlotCb(placed, oldWidgetId);
|
|
7034
|
+
if (!targetPos2) return;
|
|
7035
|
+
const newWidgetId = getPositionWidgetId(targetPos2.content);
|
|
7036
|
+
if (!newWidgetId) return;
|
|
7037
|
+
if (newWidgetId !== oldWidgetId) {
|
|
7038
|
+
onChangeRef.current(oldWidgetId, "", false, sourceId2);
|
|
7039
|
+
}
|
|
7040
|
+
onChangeRef.current(newWidgetId, encodeNLDragValue(label, placed), true, sourceId2);
|
|
7041
|
+
setPlacedChips((prev) => {
|
|
7042
|
+
const next = new Map(prev);
|
|
7043
|
+
if (newWidgetId !== oldWidgetId) next.delete(oldWidgetId);
|
|
7044
|
+
next.set(newWidgetId, { label, position: placed, sourceId: sourceId2 });
|
|
7045
|
+
placedChipsRef.current = next;
|
|
7046
|
+
return next;
|
|
7047
|
+
});
|
|
7048
|
+
triggerLanding(newWidgetId);
|
|
6766
7049
|
return;
|
|
6767
7050
|
}
|
|
6768
|
-
|
|
6769
|
-
if (
|
|
6770
|
-
const targetPos =
|
|
6771
|
-
const wid = getPositionWidgetId(pos.content);
|
|
6772
|
-
return wid && !values[wid];
|
|
6773
|
-
});
|
|
7051
|
+
if (data.type !== "drag-option") return;
|
|
7052
|
+
if (isTickOccupied(placed)) return;
|
|
7053
|
+
const targetPos = findNearestFreeWidgetSlotCb(placed);
|
|
6774
7054
|
if (!targetPos) return;
|
|
6775
7055
|
const targetWidgetId = getPositionWidgetId(targetPos.content);
|
|
6776
7056
|
if (!targetWidgetId) return;
|
|
6777
|
-
const chipLabel =
|
|
6778
|
-
const
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
7057
|
+
const chipLabel = data.data.value;
|
|
7058
|
+
const sourceId = String(event.active.id);
|
|
7059
|
+
onChangeRef.current(targetWidgetId, encodeNLDragValue(chipLabel, placed), true, sourceId);
|
|
7060
|
+
setPlacedChips((prev) => {
|
|
7061
|
+
const next = new Map(prev).set(targetWidgetId, { label: chipLabel, position: placed, sourceId });
|
|
7062
|
+
placedChipsRef.current = next;
|
|
7063
|
+
return next;
|
|
7064
|
+
});
|
|
7065
|
+
triggerLanding(targetWidgetId);
|
|
6782
7066
|
}
|
|
6783
7067
|
});
|
|
6784
7068
|
return /* @__PURE__ */ jsxs(
|
|
6785
7069
|
"div",
|
|
6786
7070
|
{
|
|
6787
7071
|
ref: mergedRef,
|
|
6788
|
-
className:
|
|
7072
|
+
className: "number-line-axis-row nl-axis-drag-mode",
|
|
6789
7073
|
children: [
|
|
6790
7074
|
/* @__PURE__ */ jsx("div", { className: "number-line-track" }),
|
|
6791
|
-
showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left" }),
|
|
6792
|
-
showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right" }),
|
|
6793
|
-
|
|
7075
|
+
showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
7076
|
+
showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
7077
|
+
/* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
|
|
7078
|
+
minorValues.map((v) => /* @__PURE__ */ jsx(
|
|
6794
7079
|
"div",
|
|
6795
7080
|
{
|
|
6796
|
-
className:
|
|
7081
|
+
className: `number-line-minor-tick${hoverSnap === v ? " nl-tick-hover" : ""}`,
|
|
6797
7082
|
style: { left: `${toPercent(v, min, max)}%` }
|
|
6798
7083
|
},
|
|
6799
|
-
`minor-${v}`
|
|
7084
|
+
`minor-${cleanFloat(v)}`
|
|
6800
7085
|
)),
|
|
6801
7086
|
majorTicks.map((tick) => /* @__PURE__ */ jsx(
|
|
6802
7087
|
"div",
|
|
6803
7088
|
{
|
|
6804
|
-
className:
|
|
7089
|
+
className: `number-line-major-tick${hoverSnap === tick ? " nl-tick-hover" : ""}`,
|
|
6805
7090
|
style: { left: `${toPercent(tick, min, max)}%` }
|
|
6806
7091
|
},
|
|
6807
|
-
`major-${tick}`
|
|
7092
|
+
`major-${cleanFloat(tick)}`
|
|
7093
|
+
)),
|
|
7094
|
+
hoverSnap !== null && /* @__PURE__ */ jsx(
|
|
7095
|
+
"div",
|
|
7096
|
+
{
|
|
7097
|
+
className: "nl-snap-preview",
|
|
7098
|
+
style: { left: `${toPercent(hoverSnap, min, max)}%` },
|
|
7099
|
+
"aria-hidden": "true"
|
|
7100
|
+
}
|
|
7101
|
+
),
|
|
7102
|
+
positions.filter((pos) => getPositionWidgetId(pos.content) === null).map((pos) => /* @__PURE__ */ jsx(
|
|
7103
|
+
"div",
|
|
7104
|
+
{
|
|
7105
|
+
className: "number-line-position-dot",
|
|
7106
|
+
style: { left: `${toPercent(pos.value, min, max)}%` }
|
|
7107
|
+
},
|
|
7108
|
+
`dot-${pos.id}`
|
|
6808
7109
|
)),
|
|
6809
7110
|
positions.map((pos) => {
|
|
6810
7111
|
const wid = getPositionWidgetId(pos.content);
|
|
6811
|
-
const
|
|
6812
|
-
|
|
6813
|
-
if (!wid || isNaN(dropPos) || !label) return null;
|
|
7112
|
+
const chip = wid ? placedChips.get(wid) : void 0;
|
|
7113
|
+
if (!wid || !chip) return null;
|
|
6814
7114
|
return /* @__PURE__ */ jsx(
|
|
6815
7115
|
PlacedChip,
|
|
6816
7116
|
{
|
|
6817
7117
|
widgetId: wid,
|
|
6818
|
-
label,
|
|
6819
|
-
leftPercent: toPercent(
|
|
7118
|
+
label: chip.label,
|
|
7119
|
+
leftPercent: toPercent(chip.position, min, max),
|
|
7120
|
+
isLanding: landingChips.has(wid),
|
|
6820
7121
|
onRemove: () => {
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
return
|
|
6827
|
-
});
|
|
6828
|
-
setDropSources((prev) => {
|
|
6829
|
-
const m = new Map(prev);
|
|
6830
|
-
m.delete(wid);
|
|
6831
|
-
return m;
|
|
7122
|
+
onChangeRef.current(wid, "", false, chip.sourceId);
|
|
7123
|
+
setPlacedChips((prev) => {
|
|
7124
|
+
const next = new Map(prev);
|
|
7125
|
+
next.delete(wid);
|
|
7126
|
+
placedChipsRef.current = next;
|
|
7127
|
+
return next;
|
|
6832
7128
|
});
|
|
6833
7129
|
}
|
|
6834
7130
|
},
|
|
@@ -6839,6 +7135,132 @@ const FreeDragAxis = ({
|
|
|
6839
7135
|
}
|
|
6840
7136
|
);
|
|
6841
7137
|
};
|
|
7138
|
+
const AxisTicks = ({
|
|
7139
|
+
min,
|
|
7140
|
+
max,
|
|
7141
|
+
majorTicks,
|
|
7142
|
+
minorValues,
|
|
7143
|
+
showArrowLeft,
|
|
7144
|
+
showArrowRight,
|
|
7145
|
+
highlights,
|
|
7146
|
+
positions
|
|
7147
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
|
|
7148
|
+
/* @__PURE__ */ jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
|
|
7149
|
+
showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
7150
|
+
showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
7151
|
+
/* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
|
|
7152
|
+
minorValues.map((v) => /* @__PURE__ */ jsx(
|
|
7153
|
+
"div",
|
|
7154
|
+
{
|
|
7155
|
+
className: "number-line-minor-tick",
|
|
7156
|
+
"aria-hidden": "true",
|
|
7157
|
+
style: { left: `${toPercent(v, min, max)}%` }
|
|
7158
|
+
},
|
|
7159
|
+
`minor-${v}`
|
|
7160
|
+
)),
|
|
7161
|
+
majorTicks.map((tick) => /* @__PURE__ */ jsx(
|
|
7162
|
+
"div",
|
|
7163
|
+
{
|
|
7164
|
+
className: "number-line-major-tick",
|
|
7165
|
+
"aria-hidden": "true",
|
|
7166
|
+
style: { left: `${toPercent(tick, min, max)}%` }
|
|
7167
|
+
},
|
|
7168
|
+
`major-${tick}`
|
|
7169
|
+
)),
|
|
7170
|
+
positions.map((pos) => /* @__PURE__ */ jsx(
|
|
7171
|
+
"div",
|
|
7172
|
+
{
|
|
7173
|
+
className: "number-line-position-dot",
|
|
7174
|
+
"aria-hidden": "true",
|
|
7175
|
+
style: { left: `${toPercent(pos.value, min, max)}%` }
|
|
7176
|
+
},
|
|
7177
|
+
`dot-${pos.id}`
|
|
7178
|
+
))
|
|
7179
|
+
] });
|
|
7180
|
+
const isLabelVisible = (tick, min, max, labelVisibility) => {
|
|
7181
|
+
switch (labelVisibility) {
|
|
7182
|
+
case "none":
|
|
7183
|
+
return false;
|
|
7184
|
+
case "min-only":
|
|
7185
|
+
return tick === roundTick(min);
|
|
7186
|
+
case "max-only":
|
|
7187
|
+
return tick === roundTick(max);
|
|
7188
|
+
case "extremes":
|
|
7189
|
+
return tick === roundTick(min) || tick === roundTick(max);
|
|
7190
|
+
default:
|
|
7191
|
+
return true;
|
|
7192
|
+
}
|
|
7193
|
+
};
|
|
7194
|
+
const AxisLabels = ({
|
|
7195
|
+
majorTicks,
|
|
7196
|
+
min,
|
|
7197
|
+
max,
|
|
7198
|
+
labelVisibility,
|
|
7199
|
+
inputTickSet
|
|
7200
|
+
}) => /* @__PURE__ */ jsx("div", { className: "number-line-labels-row", "aria-hidden": "true", children: majorTicks.map((tick) => {
|
|
7201
|
+
const visible = !inputTickSet.has(tick) && isLabelVisible(tick, min, max, labelVisibility);
|
|
7202
|
+
return /* @__PURE__ */ jsx(
|
|
7203
|
+
"span",
|
|
7204
|
+
{
|
|
7205
|
+
className: "number-line-tick-label",
|
|
7206
|
+
style: {
|
|
7207
|
+
left: `${toPercent(tick, min, max)}%`,
|
|
7208
|
+
visibility: visible ? "visible" : "hidden"
|
|
7209
|
+
},
|
|
7210
|
+
children: tick
|
|
7211
|
+
},
|
|
7212
|
+
`lbl-${tick}`
|
|
7213
|
+
);
|
|
7214
|
+
}) });
|
|
7215
|
+
const HighlightLabelsRow = ({ highlights, min, max }) => {
|
|
7216
|
+
if (!highlights?.some((h) => h.label)) return null;
|
|
7217
|
+
return /* @__PURE__ */ jsx("div", { className: "number-line-highlight-labels-row", "aria-hidden": "true", children: highlights.map((h) => {
|
|
7218
|
+
if (!h.label) return null;
|
|
7219
|
+
const centerPct = (toPercent(h.from, min, max) + toPercent(h.to, min, max)) / 2;
|
|
7220
|
+
return /* @__PURE__ */ jsx(
|
|
7221
|
+
"span",
|
|
7222
|
+
{
|
|
7223
|
+
className: "number-line-highlight-label",
|
|
7224
|
+
style: { left: `${centerPct}%` },
|
|
7225
|
+
children: h.label
|
|
7226
|
+
},
|
|
7227
|
+
`hl-lbl-${h.id}`
|
|
7228
|
+
);
|
|
7229
|
+
}) });
|
|
7230
|
+
};
|
|
7231
|
+
const CounterControls = ({
|
|
7232
|
+
counter,
|
|
7233
|
+
count,
|
|
7234
|
+
onDecrement,
|
|
7235
|
+
onIncrement
|
|
7236
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "number-line-counter", "aria-label": counter.name ?? "Counter", children: [
|
|
7237
|
+
/* @__PURE__ */ jsxs("div", { className: "number-line-counter-controls", children: [
|
|
7238
|
+
/* @__PURE__ */ jsx(
|
|
7239
|
+
"button",
|
|
7240
|
+
{
|
|
7241
|
+
type: "button",
|
|
7242
|
+
className: "number-line-counter-btn",
|
|
7243
|
+
"aria-label": "Decrease",
|
|
7244
|
+
disabled: count <= counter.min,
|
|
7245
|
+
onClick: onDecrement,
|
|
7246
|
+
children: "−"
|
|
7247
|
+
}
|
|
7248
|
+
),
|
|
7249
|
+
/* @__PURE__ */ jsx("span", { className: "number-line-counter-value", "data-testid": "counter-value", children: count }),
|
|
7250
|
+
/* @__PURE__ */ jsx(
|
|
7251
|
+
"button",
|
|
7252
|
+
{
|
|
7253
|
+
type: "button",
|
|
7254
|
+
className: "number-line-counter-btn",
|
|
7255
|
+
"aria-label": "Increase",
|
|
7256
|
+
disabled: count >= counter.max,
|
|
7257
|
+
onClick: onIncrement,
|
|
7258
|
+
children: "+"
|
|
7259
|
+
}
|
|
7260
|
+
)
|
|
7261
|
+
] }),
|
|
7262
|
+
counter.name && /* @__PURE__ */ jsx("span", { className: "number-line-counter-label", children: counter.name })
|
|
7263
|
+
] });
|
|
6842
7264
|
const NumberLineRenderer = ({
|
|
6843
7265
|
item,
|
|
6844
7266
|
widgets,
|
|
@@ -6847,27 +7269,59 @@ const NumberLineRenderer = ({
|
|
|
6847
7269
|
onChange,
|
|
6848
7270
|
onInputFocus
|
|
6849
7271
|
}) => {
|
|
6850
|
-
const {
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
7272
|
+
const {
|
|
7273
|
+
min,
|
|
7274
|
+
max,
|
|
7275
|
+
step,
|
|
7276
|
+
minorTicks = 0,
|
|
7277
|
+
style: lineStyle = "arrow-right",
|
|
7278
|
+
labelVisibility,
|
|
7279
|
+
highlights,
|
|
7280
|
+
counter,
|
|
7281
|
+
positions
|
|
7282
|
+
} = item;
|
|
7283
|
+
const [counterCount, setCounterCount] = useState(
|
|
7284
|
+
counter !== void 0 ? counter.default ?? counter.min : 0
|
|
7285
|
+
);
|
|
7286
|
+
useEffect(() => {
|
|
7287
|
+
setCounterCount(counter !== void 0 ? counter.default ?? counter.min : 0);
|
|
7288
|
+
}, [counter]);
|
|
7289
|
+
const effectiveStep = useMemo(() => {
|
|
7290
|
+
if (counter !== void 0 && counterCount > 0) {
|
|
7291
|
+
return cleanFloat((max - min) / counterCount);
|
|
7292
|
+
}
|
|
7293
|
+
return step;
|
|
7294
|
+
}, [counter, counterCount, min, max, step]);
|
|
7295
|
+
const majorTicks = useMemo(
|
|
7296
|
+
() => getMajorTicks(min, max, effectiveStep).map(roundTick),
|
|
7297
|
+
[min, max, effectiveStep]
|
|
7298
|
+
);
|
|
7299
|
+
const minorValues = useMemo(
|
|
7300
|
+
() => getMinorTickValues(majorTicks, minorTicks),
|
|
7301
|
+
[majorTicks, minorTicks]
|
|
7302
|
+
);
|
|
7303
|
+
const showArrowRight = lineStyle === "arrow-right" || lineStyle === "arrow-both";
|
|
7304
|
+
const showArrowLeft = lineStyle === "arrow-left" || lineStyle === "arrow-both";
|
|
6855
7305
|
const isDragMode = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
|
|
6856
7306
|
const inputTickSet = useMemo(() => {
|
|
6857
7307
|
if (isDragMode) return /* @__PURE__ */ new Set();
|
|
6858
7308
|
return new Set(
|
|
6859
|
-
positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => pos.value)
|
|
7309
|
+
positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => roundTick(pos.value))
|
|
6860
7310
|
);
|
|
6861
7311
|
}, [isDragMode, positions]);
|
|
7312
|
+
const visiblePositions = useMemo(
|
|
7313
|
+
() => positions.filter((pos) => !isDragMode || getPositionWidgetId(pos.content) === null),
|
|
7314
|
+
[isDragMode, positions]
|
|
7315
|
+
);
|
|
6862
7316
|
return /* @__PURE__ */ jsxs(
|
|
6863
7317
|
"div",
|
|
6864
7318
|
{
|
|
6865
|
-
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}`,
|
|
7319
|
+
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
|
|
6866
7320
|
style: { [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%` },
|
|
6867
|
-
role: "
|
|
7321
|
+
role: "group",
|
|
6868
7322
|
"aria-label": `Number line from ${min} to ${max}`,
|
|
6869
7323
|
children: [
|
|
6870
|
-
|
|
7324
|
+
visiblePositions.length > 0 && /* @__PURE__ */ jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsx(
|
|
6871
7325
|
"div",
|
|
6872
7326
|
{
|
|
6873
7327
|
className: "number-line-position-content",
|
|
@@ -6899,50 +7353,43 @@ const NumberLineRenderer = ({
|
|
|
6899
7353
|
showArrowRight,
|
|
6900
7354
|
positions,
|
|
6901
7355
|
values,
|
|
6902
|
-
onChange
|
|
7356
|
+
onChange,
|
|
7357
|
+
highlights,
|
|
7358
|
+
snapKey: counterCount
|
|
6903
7359
|
}
|
|
6904
|
-
) : /* @__PURE__ */
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
"span",
|
|
6938
|
-
{
|
|
6939
|
-
className: "number-line-tick-label",
|
|
6940
|
-
style: { left: `${toPercent(tick, min, max)}%`, visibility: hasInputAtTick ? "hidden" : "visible" },
|
|
6941
|
-
children: tick
|
|
6942
|
-
},
|
|
6943
|
-
`lbl-${tick}`
|
|
6944
|
-
);
|
|
6945
|
-
}) })
|
|
7360
|
+
) : /* @__PURE__ */ jsx(
|
|
7361
|
+
AxisTicks,
|
|
7362
|
+
{
|
|
7363
|
+
min,
|
|
7364
|
+
max,
|
|
7365
|
+
majorTicks,
|
|
7366
|
+
minorValues,
|
|
7367
|
+
showArrowLeft,
|
|
7368
|
+
showArrowRight,
|
|
7369
|
+
highlights,
|
|
7370
|
+
positions
|
|
7371
|
+
}
|
|
7372
|
+
),
|
|
7373
|
+
/* @__PURE__ */ jsx(
|
|
7374
|
+
AxisLabels,
|
|
7375
|
+
{
|
|
7376
|
+
majorTicks,
|
|
7377
|
+
min,
|
|
7378
|
+
max,
|
|
7379
|
+
labelVisibility,
|
|
7380
|
+
inputTickSet
|
|
7381
|
+
}
|
|
7382
|
+
),
|
|
7383
|
+
/* @__PURE__ */ jsx(HighlightLabelsRow, { highlights, min, max }),
|
|
7384
|
+
counter !== void 0 && /* @__PURE__ */ jsx(
|
|
7385
|
+
CounterControls,
|
|
7386
|
+
{
|
|
7387
|
+
counter,
|
|
7388
|
+
count: counterCount,
|
|
7389
|
+
onDecrement: () => setCounterCount((c) => Math.max(counter.min, c - 1)),
|
|
7390
|
+
onIncrement: () => setCounterCount((c) => Math.min(counter.max, c + 1))
|
|
7391
|
+
}
|
|
7392
|
+
)
|
|
6946
7393
|
]
|
|
6947
7394
|
}
|
|
6948
7395
|
);
|
|
@@ -7170,8 +7617,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
7170
7617
|
}
|
|
7171
7618
|
}), [json, values, onSubmit]);
|
|
7172
7619
|
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
7173
|
-
|
|
7174
|
-
setValues(newValues);
|
|
7620
|
+
setValues((prev) => ({ ...prev, [widgetId]: value }));
|
|
7175
7621
|
setDroppedSources((prev) => {
|
|
7176
7622
|
const next = new Map(prev);
|
|
7177
7623
|
if (fromDrop && value !== "" && sourceOptionId) {
|
|
@@ -7181,7 +7627,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
7181
7627
|
}
|
|
7182
7628
|
return next;
|
|
7183
7629
|
});
|
|
7184
|
-
onChange?.(
|
|
7630
|
+
onChange?.({ ...values, [widgetId]: value });
|
|
7185
7631
|
};
|
|
7186
7632
|
const handleSubmit = () => {
|
|
7187
7633
|
onSubmit?.(values);
|
|
@@ -7405,6 +7851,10 @@ export {
|
|
|
7405
7851
|
LexerError,
|
|
7406
7852
|
LineBlockSchema,
|
|
7407
7853
|
LinebreakSchema,
|
|
7854
|
+
NumberLineCounterItemSchema,
|
|
7855
|
+
NumberLineHighlightItemSchema,
|
|
7856
|
+
NumberLineItemSchema,
|
|
7857
|
+
NumberLinePositionItemSchema,
|
|
7408
7858
|
NumericKeypadJSONSchema,
|
|
7409
7859
|
NumericKeypadToolSchema,
|
|
7410
7860
|
OperatorItemSchema,
|
|
@@ -7478,7 +7928,7 @@ export {
|
|
|
7478
7928
|
isToolNode,
|
|
7479
7929
|
isValidExpression,
|
|
7480
7930
|
isValidationNode,
|
|
7481
|
-
isWidgetRef
|
|
7931
|
+
isWidgetRef,
|
|
7482
7932
|
parse,
|
|
7483
7933
|
parseSafe,
|
|
7484
7934
|
renderExercise,
|