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