@mathscan/math-exercise-engine 1.0.11 → 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 +722 -273
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +723 -274
- 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,10 +453,7 @@ 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);
|
|
@@ -450,10 +469,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
450
469
|
this.SUBRULE(this.validation);
|
|
451
470
|
});
|
|
452
471
|
this.MANY4(() => {
|
|
453
|
-
this.OR2([
|
|
454
|
-
{ ALT: () => this.CONSUME2(Newline) },
|
|
455
|
-
{ ALT: () => this.CONSUME2(Space) }
|
|
456
|
-
]);
|
|
472
|
+
this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
|
|
457
473
|
});
|
|
458
474
|
});
|
|
459
475
|
this.question = this.RULE("question", () => {
|
|
@@ -462,10 +478,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
462
478
|
this.CONSUME(StringLiteral, { LABEL: "value" });
|
|
463
479
|
this.CONSUME(RBracket);
|
|
464
480
|
this.MANY(() => {
|
|
465
|
-
this.OR([
|
|
466
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
467
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
468
|
-
]);
|
|
481
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
469
482
|
});
|
|
470
483
|
});
|
|
471
484
|
this.contentBlock = this.RULE("contentBlock", () => {
|
|
@@ -487,77 +500,50 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
487
500
|
});
|
|
488
501
|
this.table = this.RULE("table", () => {
|
|
489
502
|
this.MANY(() => {
|
|
490
|
-
this.OR2([
|
|
491
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
492
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
493
|
-
]);
|
|
503
|
+
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
494
504
|
});
|
|
495
505
|
this.CONSUME(TABLE);
|
|
496
506
|
this.CONSUME(LBracket);
|
|
497
507
|
this.SUBRULE(this.parameterList);
|
|
498
508
|
this.CONSUME(RBracket);
|
|
499
509
|
this.MANY2(() => {
|
|
500
|
-
this.OR3([
|
|
501
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
502
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
503
|
-
]);
|
|
510
|
+
this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
504
511
|
});
|
|
505
512
|
this.CONSUME(LBrace);
|
|
506
513
|
this.MANY3(() => {
|
|
507
|
-
this.OR4([
|
|
508
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
509
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
510
|
-
]);
|
|
514
|
+
this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
511
515
|
});
|
|
512
516
|
this.AT_LEAST_ONE(() => {
|
|
513
517
|
this.SUBRULE(this.row);
|
|
514
518
|
});
|
|
515
519
|
this.CONSUME(RBrace);
|
|
516
520
|
this.MANY4(() => {
|
|
517
|
-
this.OR5([
|
|
518
|
-
{ ALT: () => this.CONSUME4(Newline) },
|
|
519
|
-
{ ALT: () => this.CONSUME4(Space) }
|
|
520
|
-
]);
|
|
521
|
+
this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
521
522
|
});
|
|
522
523
|
});
|
|
523
524
|
this.row = this.RULE("row", () => {
|
|
524
525
|
this.MANY(() => {
|
|
525
|
-
this.OR([
|
|
526
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
527
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
528
|
-
]);
|
|
526
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
529
527
|
});
|
|
530
528
|
this.CONSUME(ROW);
|
|
531
529
|
this.MANY2(() => {
|
|
532
|
-
this.OR2([
|
|
533
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
534
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
535
|
-
]);
|
|
530
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
536
531
|
});
|
|
537
532
|
this.CONSUME(LBrace);
|
|
538
533
|
this.MANY3(() => {
|
|
539
|
-
this.OR3([
|
|
540
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
541
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
542
|
-
]);
|
|
534
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
543
535
|
});
|
|
544
536
|
this.AT_LEAST_ONE(() => {
|
|
545
537
|
this.SUBRULE(this.cell);
|
|
546
538
|
});
|
|
547
539
|
this.CONSUME(RBrace);
|
|
548
540
|
this.MANY4(() => {
|
|
549
|
-
this.OR4([
|
|
550
|
-
{ ALT: () => this.CONSUME4(Space) },
|
|
551
|
-
{ ALT: () => this.CONSUME4(Newline) }
|
|
552
|
-
]);
|
|
541
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
553
542
|
});
|
|
554
543
|
});
|
|
555
544
|
this.cell = this.RULE("cell", () => {
|
|
556
545
|
this.MANY(() => {
|
|
557
|
-
this.OR([
|
|
558
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
559
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
560
|
-
]);
|
|
546
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
561
547
|
});
|
|
562
548
|
this.CONSUME(CELL);
|
|
563
549
|
this.OPTION(() => {
|
|
@@ -566,10 +552,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
566
552
|
this.CONSUME(RBracket);
|
|
567
553
|
});
|
|
568
554
|
this.MANY2(() => {
|
|
569
|
-
this.OR2([
|
|
570
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
571
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
572
|
-
]);
|
|
555
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
573
556
|
});
|
|
574
557
|
this.CONSUME(LBrace);
|
|
575
558
|
this.MANY3(() => {
|
|
@@ -580,70 +563,47 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
580
563
|
});
|
|
581
564
|
this.CONSUME(RBrace);
|
|
582
565
|
this.MANY4(() => {
|
|
583
|
-
this.OR4([
|
|
584
|
-
{ ALT: () => this.CONSUME4(Space) },
|
|
585
|
-
{ ALT: () => this.CONSUME4(Newline) }
|
|
586
|
-
]);
|
|
566
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
587
567
|
});
|
|
588
568
|
});
|
|
589
569
|
this.numberLine = this.RULE("numberLine", () => {
|
|
590
570
|
this.MANY(() => {
|
|
591
|
-
this.OR2([
|
|
592
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
593
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
594
|
-
]);
|
|
571
|
+
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
595
572
|
});
|
|
596
573
|
this.CONSUME(NUMBER_LINE);
|
|
597
574
|
this.CONSUME(LBracket);
|
|
598
575
|
this.SUBRULE(this.parameterList);
|
|
599
576
|
this.CONSUME(RBracket);
|
|
600
577
|
this.MANY2(() => {
|
|
601
|
-
this.OR3([
|
|
602
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
603
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
604
|
-
]);
|
|
578
|
+
this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
605
579
|
});
|
|
606
580
|
this.CONSUME(LBrace);
|
|
607
581
|
this.MANY3(() => {
|
|
608
|
-
this.OR4([
|
|
609
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
610
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
611
|
-
]);
|
|
582
|
+
this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
612
583
|
});
|
|
613
584
|
this.MANY4(() => {
|
|
614
|
-
this.
|
|
585
|
+
this.OR6([
|
|
586
|
+
{ ALT: () => this.SUBRULE(this.position) },
|
|
587
|
+
{ ALT: () => this.SUBRULE(this.counter) },
|
|
588
|
+
{ ALT: () => this.SUBRULE(this.highlight) }
|
|
589
|
+
]);
|
|
615
590
|
});
|
|
616
591
|
this.CONSUME(RBrace);
|
|
617
592
|
this.MANY5(() => {
|
|
618
|
-
this.OR5([
|
|
619
|
-
{ ALT: () => this.CONSUME4(Newline) },
|
|
620
|
-
{ ALT: () => this.CONSUME4(Space) }
|
|
621
|
-
]);
|
|
593
|
+
this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
622
594
|
});
|
|
623
595
|
});
|
|
624
596
|
this.position = this.RULE("position", () => {
|
|
625
|
-
this.MANY(() => {
|
|
626
|
-
this.OR([
|
|
627
|
-
{ ALT: () => this.CONSUME(Space) },
|
|
628
|
-
{ ALT: () => this.CONSUME(Newline) }
|
|
629
|
-
]);
|
|
630
|
-
});
|
|
631
597
|
this.CONSUME(POSITION);
|
|
632
598
|
this.CONSUME(LBracket);
|
|
633
599
|
this.SUBRULE(this.parameterList);
|
|
634
600
|
this.CONSUME(RBracket);
|
|
635
601
|
this.MANY2(() => {
|
|
636
|
-
this.OR2([
|
|
637
|
-
{ ALT: () => this.CONSUME2(Space) },
|
|
638
|
-
{ ALT: () => this.CONSUME2(Newline) }
|
|
639
|
-
]);
|
|
602
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
640
603
|
});
|
|
641
604
|
this.CONSUME(LBrace);
|
|
642
605
|
this.MANY3(() => {
|
|
643
|
-
this.OR3([
|
|
644
|
-
{ ALT: () => this.CONSUME3(Space) },
|
|
645
|
-
{ ALT: () => this.CONSUME3(Newline) }
|
|
646
|
-
]);
|
|
606
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
647
607
|
});
|
|
648
608
|
this.MANY4(() => {
|
|
649
609
|
this.OR4([
|
|
@@ -653,10 +613,25 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
653
613
|
});
|
|
654
614
|
this.CONSUME(RBrace);
|
|
655
615
|
this.MANY5(() => {
|
|
656
|
-
this.OR5([
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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) }]);
|
|
660
635
|
});
|
|
661
636
|
});
|
|
662
637
|
this.line = this.RULE("line", () => {
|
|
@@ -753,28 +728,19 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
753
728
|
this.SUBRULE(this.stringList);
|
|
754
729
|
this.CONSUME(RBracket);
|
|
755
730
|
this.MANY(() => {
|
|
756
|
-
this.OR([
|
|
757
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
758
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
759
|
-
]);
|
|
731
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
760
732
|
});
|
|
761
733
|
});
|
|
762
734
|
this.radioButtons = this.RULE("radioButtons", () => {
|
|
763
735
|
this.MANY(() => {
|
|
764
|
-
this.OR([
|
|
765
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
766
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
767
|
-
]);
|
|
736
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
768
737
|
});
|
|
769
738
|
this.CONSUME(RADIO_BUTTONS_OPTIONS);
|
|
770
739
|
this.CONSUME(LBracket);
|
|
771
740
|
this.SUBRULE(this.parameterList);
|
|
772
741
|
this.CONSUME(RBracket);
|
|
773
742
|
this.MANY2(() => {
|
|
774
|
-
this.OR2([
|
|
775
|
-
{ ALT: () => this.CONSUME2(Newline) },
|
|
776
|
-
{ ALT: () => this.CONSUME2(Space) }
|
|
777
|
-
]);
|
|
743
|
+
this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
|
|
778
744
|
});
|
|
779
745
|
});
|
|
780
746
|
this.numericKeypad = this.RULE("numericKeypad", () => {
|
|
@@ -785,10 +751,7 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
785
751
|
});
|
|
786
752
|
this.CONSUME(RBracket);
|
|
787
753
|
this.MANY(() => {
|
|
788
|
-
this.OR([
|
|
789
|
-
{ ALT: () => this.CONSUME(Newline) },
|
|
790
|
-
{ ALT: () => this.CONSUME(Space) }
|
|
791
|
-
]);
|
|
754
|
+
this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
|
|
792
755
|
});
|
|
793
756
|
});
|
|
794
757
|
this.validation = this.RULE("validation", () => {
|
|
@@ -1023,7 +986,7 @@ function isLineBlock(value) {
|
|
|
1023
986
|
}
|
|
1024
987
|
const NumberLinePositionSchema = zod.z.object({
|
|
1025
988
|
type: zod.z.literal(AST_TYPES.NUMBER_LINE_POSITION),
|
|
1026
|
-
value: zod.z.number(),
|
|
989
|
+
value: zod.z.number().finite({ message: "POSITION: value must be a finite number" }),
|
|
1027
990
|
inlines: zod.z.array(InlineNodeSchema).min(1)
|
|
1028
991
|
});
|
|
1029
992
|
function createNumberLinePosition(value, inlines) {
|
|
@@ -1043,10 +1006,50 @@ function createNumberLinePosition(value, inlines) {
|
|
|
1043
1006
|
inlines
|
|
1044
1007
|
});
|
|
1045
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
|
+
}
|
|
1046
1049
|
const NumberLineBlockSchema = zod.z.object({
|
|
1047
1050
|
type: zod.z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
|
|
1048
|
-
min: zod.z.number(),
|
|
1049
|
-
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" }),
|
|
1050
1053
|
step: zod.z.number().positive({ message: "NUMBER_LINE: step must be greater than 0" }).default(1),
|
|
1051
1054
|
minorTicks: zod.z.number().int().nonnegative({ message: "NUMBER_LINE: minorTicks must be >= 0" }).optional(),
|
|
1052
1055
|
style: zod.z.enum(NUMBER_LINE_STYLES, {
|
|
@@ -1054,6 +1057,13 @@ const NumberLineBlockSchema = zod.z.object({
|
|
|
1054
1057
|
message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
|
|
1055
1058
|
})
|
|
1056
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(),
|
|
1057
1067
|
positions: zod.z.array(NumberLinePositionSchema)
|
|
1058
1068
|
}).refine((data) => data.min < data.max, {
|
|
1059
1069
|
message: "NUMBER_LINE: min must be less than max"
|
|
@@ -1074,8 +1084,24 @@ const NumberLineBlockSchema = zod.z.object({
|
|
|
1074
1084
|
}
|
|
1075
1085
|
seen.add(pos.value);
|
|
1076
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
|
+
}
|
|
1077
1103
|
});
|
|
1078
|
-
function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
|
|
1104
|
+
function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights) {
|
|
1079
1105
|
return NumberLineBlockSchema.parse({
|
|
1080
1106
|
type: AST_TYPES.NUMBER_LINE_BLOCK,
|
|
1081
1107
|
min,
|
|
@@ -1083,6 +1109,9 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
|
|
|
1083
1109
|
step,
|
|
1084
1110
|
minorTicks,
|
|
1085
1111
|
style,
|
|
1112
|
+
labelVisibility,
|
|
1113
|
+
counter,
|
|
1114
|
+
highlights,
|
|
1086
1115
|
positions
|
|
1087
1116
|
});
|
|
1088
1117
|
}
|
|
@@ -1293,8 +1322,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1293
1322
|
const step = params.step !== void 0 ? Number(params.step) : void 0;
|
|
1294
1323
|
const minorTicks = params.minorTicks !== void 0 ? Number(params.minorTicks) : void 0;
|
|
1295
1324
|
const style = params.style;
|
|
1325
|
+
const labelVisibility = params.labelVisibility;
|
|
1296
1326
|
const positions = ctx.position ? ctx.position.map((p) => this.visit(p)) : [];
|
|
1297
|
-
|
|
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);
|
|
1298
1334
|
}
|
|
1299
1335
|
position(ctx) {
|
|
1300
1336
|
const params = this.visit(ctx.parameterList);
|
|
@@ -1305,6 +1341,28 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1305
1341
|
const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
|
|
1306
1342
|
return createNumberLinePosition(value, inlines);
|
|
1307
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
|
+
}
|
|
1308
1366
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1309
1367
|
// Line Block
|
|
1310
1368
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1635,17 +1693,36 @@ const TableItemSchema = zod.z.object({
|
|
|
1635
1693
|
const NumberLinePositionItemSchema = zod.z.object({
|
|
1636
1694
|
id: zod.z.string(),
|
|
1637
1695
|
type: zod.z.literal(JSON_TYPES.NUMBER_LINE_POSITION),
|
|
1638
|
-
value: zod.z.number(),
|
|
1639
|
-
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()
|
|
1640
1714
|
});
|
|
1641
1715
|
const NumberLineItemSchema = zod.z.object({
|
|
1642
1716
|
id: zod.z.string(),
|
|
1643
1717
|
type: zod.z.literal(JSON_TYPES.NUMBER_LINE),
|
|
1644
|
-
min: zod.z.number(),
|
|
1645
|
-
max: zod.z.number(),
|
|
1718
|
+
min: zod.z.number().finite(),
|
|
1719
|
+
max: zod.z.number().finite(),
|
|
1646
1720
|
step: zod.z.number().positive(),
|
|
1647
1721
|
minorTicks: zod.z.number().int().nonnegative().optional(),
|
|
1648
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(),
|
|
1649
1726
|
positions: zod.z.array(NumberLinePositionItemSchema)
|
|
1650
1727
|
});
|
|
1651
1728
|
const StringInputJSONSchema = zod.z.object({
|
|
@@ -1719,7 +1796,7 @@ function isQuestionText(item) {
|
|
|
1719
1796
|
function isTextItem$1(item) {
|
|
1720
1797
|
return item.type === JSON_TYPES.TEXT;
|
|
1721
1798
|
}
|
|
1722
|
-
function isWidgetRef
|
|
1799
|
+
function isWidgetRef(item) {
|
|
1723
1800
|
return item.type === JSON_TYPES.WIDGET_REF;
|
|
1724
1801
|
}
|
|
1725
1802
|
function isHighlightedText(item) {
|
|
@@ -1892,16 +1969,42 @@ function transformNumberLine(numberLine, idGenerator) {
|
|
|
1892
1969
|
content
|
|
1893
1970
|
};
|
|
1894
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
|
+
}
|
|
1895
1995
|
const item = {
|
|
1896
1996
|
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
|
|
1897
1997
|
type: JSON_TYPES.NUMBER_LINE,
|
|
1898
1998
|
min: numberLine.min,
|
|
1899
1999
|
max: numberLine.max,
|
|
1900
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 },
|
|
1901
2006
|
positions
|
|
1902
2007
|
};
|
|
1903
|
-
if (numberLine.minorTicks !== void 0) item.minorTicks = numberLine.minorTicks;
|
|
1904
|
-
if (numberLine.style !== void 0) item.style = numberLine.style;
|
|
1905
2008
|
return item;
|
|
1906
2009
|
}
|
|
1907
2010
|
function transformTable(table, idGenerator) {
|
|
@@ -2024,6 +2127,49 @@ function extractVariables$1(expression, validVariables) {
|
|
|
2024
2127
|
}
|
|
2025
2128
|
return uniqueVars;
|
|
2026
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
|
+
}
|
|
2027
2173
|
function validateSimple(exercise, userInputs) {
|
|
2028
2174
|
const details = {};
|
|
2029
2175
|
let allCorrect = true;
|
|
@@ -2037,10 +2183,11 @@ function validateSimple(exercise, userInputs) {
|
|
|
2037
2183
|
score: 0
|
|
2038
2184
|
};
|
|
2039
2185
|
}
|
|
2186
|
+
const positionMap = buildPositionMap(exercise);
|
|
2040
2187
|
for (const [widgetId, expected] of Object.entries(answers)) {
|
|
2041
2188
|
totalCount++;
|
|
2042
2189
|
const userValue = userInputs[widgetId];
|
|
2043
|
-
const isCorrect = areValuesEqual(userValue, expected);
|
|
2190
|
+
const isCorrect = areValuesEqual(userValue, expected, positionMap[widgetId]);
|
|
2044
2191
|
details[widgetId] = {
|
|
2045
2192
|
widgetId,
|
|
2046
2193
|
userValue,
|
|
@@ -2068,19 +2215,13 @@ function validateSimple(exercise, userInputs) {
|
|
|
2068
2215
|
score: totalCount > 0 ? correctCount / totalCount : 0
|
|
2069
2216
|
};
|
|
2070
2217
|
}
|
|
2071
|
-
function areValuesEqual(userValue, expected) {
|
|
2072
|
-
if (userValue === void 0 || userValue === null)
|
|
2073
|
-
|
|
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);
|
|
2074
2223
|
}
|
|
2075
|
-
|
|
2076
|
-
const normalizedExpected = normalize(expected);
|
|
2077
|
-
return normalizedUser === normalizedExpected;
|
|
2078
|
-
}
|
|
2079
|
-
function normalize(value) {
|
|
2080
|
-
if (typeof value === "number") {
|
|
2081
|
-
return String(Math.round(value * 1e10) / 1e10);
|
|
2082
|
-
}
|
|
2083
|
-
return String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
2224
|
+
return normalize(userValue) === normalize(expected);
|
|
2084
2225
|
}
|
|
2085
2226
|
const math = mathjs.create(mathjs.all);
|
|
2086
2227
|
const BLOCKED_FUNCTIONS = [
|
|
@@ -6622,7 +6763,10 @@ const WidgetRenderer = ({
|
|
|
6622
6763
|
}
|
|
6623
6764
|
return null;
|
|
6624
6765
|
};
|
|
6625
|
-
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;
|
|
6626
6770
|
const toPercent = (value, min, max) => {
|
|
6627
6771
|
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
6628
6772
|
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
@@ -6638,19 +6782,24 @@ const getMajorTicks = (min, max, step) => {
|
|
|
6638
6782
|
const count = Math.floor((max - min) / step);
|
|
6639
6783
|
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
6640
6784
|
const lastTick = ticks[ticks.length - 1];
|
|
6641
|
-
if (
|
|
6785
|
+
if (!almostEqual(lastTick, max)) {
|
|
6786
|
+
ticks.push(max);
|
|
6787
|
+
} else {
|
|
6788
|
+
ticks[ticks.length - 1] = max;
|
|
6789
|
+
}
|
|
6642
6790
|
return ticks;
|
|
6643
6791
|
};
|
|
6644
6792
|
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
6645
|
-
if (minorTicks <= 0) return [];
|
|
6793
|
+
if (minorTicks <= 0 || majorTicks.length < 2) return [];
|
|
6646
6794
|
const result = [];
|
|
6647
|
-
|
|
6648
|
-
const
|
|
6649
|
-
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;
|
|
6650
6799
|
for (let j = 1; j <= minorTicks; j++) {
|
|
6651
|
-
result.push(cleanFloat(
|
|
6800
|
+
result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
|
|
6652
6801
|
}
|
|
6653
|
-
}
|
|
6802
|
+
}
|
|
6654
6803
|
return result;
|
|
6655
6804
|
};
|
|
6656
6805
|
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
@@ -6659,19 +6808,41 @@ const getSnapTicks = (majorTicks, minorTicks) => [
|
|
|
6659
6808
|
];
|
|
6660
6809
|
const snapToNearest = (raw, ticks) => {
|
|
6661
6810
|
if (ticks.length === 0) return cleanFloat(raw);
|
|
6662
|
-
|
|
6663
|
-
(best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best
|
|
6664
|
-
);
|
|
6665
|
-
return cleanFloat(snapped);
|
|
6811
|
+
return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
|
|
6666
6812
|
};
|
|
6667
|
-
const isWidgetRef = (item) => item.type === JSON_TYPES.WIDGET_REF;
|
|
6668
6813
|
const getPositionWidgetId = (content) => {
|
|
6669
6814
|
for (const item of content) {
|
|
6670
6815
|
if (isWidgetRef(item)) return item.widgetId;
|
|
6671
6816
|
}
|
|
6672
6817
|
return null;
|
|
6673
6818
|
};
|
|
6674
|
-
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 }) => {
|
|
6675
6846
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
6676
6847
|
id: widgetId,
|
|
6677
6848
|
data: { type: "placed-chip", widgetId, label }
|
|
@@ -6679,23 +6850,49 @@ const PlacedChip = ({ widgetId, label, leftPercent, onRemove }) => {
|
|
|
6679
6850
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6680
6851
|
"div",
|
|
6681
6852
|
{
|
|
6682
|
-
className:
|
|
6683
|
-
style: { left: `${leftPercent}
|
|
6684
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
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
|
+
] })
|
|
6696
6878
|
}
|
|
6697
6879
|
);
|
|
6698
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
|
+
}
|
|
6699
6896
|
const FreeDragAxis = ({
|
|
6700
6897
|
axisDropId,
|
|
6701
6898
|
min,
|
|
@@ -6706,17 +6903,51 @@ const FreeDragAxis = ({
|
|
|
6706
6903
|
showArrowRight,
|
|
6707
6904
|
positions,
|
|
6708
6905
|
values,
|
|
6709
|
-
onChange
|
|
6906
|
+
onChange,
|
|
6907
|
+
highlights,
|
|
6908
|
+
snapKey
|
|
6710
6909
|
}) => {
|
|
6711
6910
|
const axisRef = React.useRef(null);
|
|
6712
6911
|
const snapTicks = React.useMemo(
|
|
6713
6912
|
() => getSnapTicks(majorTicks, minorTicks),
|
|
6714
6913
|
[majorTicks, minorTicks]
|
|
6715
6914
|
);
|
|
6716
|
-
const
|
|
6717
|
-
|
|
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]);
|
|
6718
6929
|
React.useEffect(() => {
|
|
6719
|
-
|
|
6930
|
+
placedChipsRef.current = placedChips;
|
|
6931
|
+
}, [placedChips]);
|
|
6932
|
+
const isFirstRender = React.useRef(true);
|
|
6933
|
+
React.useEffect(() => {
|
|
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) => {
|
|
6720
6951
|
let changed = false;
|
|
6721
6952
|
const next = new Map(prev);
|
|
6722
6953
|
for (const [wid] of next) {
|
|
@@ -6725,21 +6956,13 @@ const FreeDragAxis = ({
|
|
|
6725
6956
|
changed = true;
|
|
6726
6957
|
}
|
|
6727
6958
|
}
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
setDropSources((prev) => {
|
|
6731
|
-
let changed = false;
|
|
6732
|
-
const next = new Map(prev);
|
|
6733
|
-
for (const [wid] of next) {
|
|
6734
|
-
if (!values[wid]) {
|
|
6735
|
-
next.delete(wid);
|
|
6736
|
-
changed = true;
|
|
6737
|
-
}
|
|
6959
|
+
if (changed) {
|
|
6960
|
+
placedChipsRef.current = next;
|
|
6738
6961
|
}
|
|
6739
6962
|
return changed ? next : prev;
|
|
6740
6963
|
});
|
|
6741
6964
|
}, [values]);
|
|
6742
|
-
const { setNodeRef
|
|
6965
|
+
const { setNodeRef } = useDroppable({ id: axisDropId });
|
|
6743
6966
|
const mergedRef = React.useCallback((node) => {
|
|
6744
6967
|
axisRef.current = node;
|
|
6745
6968
|
setNodeRef(node);
|
|
@@ -6747,91 +6970,163 @@ const FreeDragAxis = ({
|
|
|
6747
6970
|
const snapFromEvent = React.useCallback((translated) => {
|
|
6748
6971
|
if (!axisRef.current) return null;
|
|
6749
6972
|
const axisRect = axisRef.current.getBoundingClientRect();
|
|
6973
|
+
if (axisRect.width === 0) return null;
|
|
6750
6974
|
const chipCenterX = translated.left + translated.width / 2;
|
|
6751
6975
|
const raw = pixelToValue(chipCenterX, axisRect.left, axisRect.width, min, max);
|
|
6752
6976
|
return snapToNearest(raw, snapTicks);
|
|
6753
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
|
+
}, []);
|
|
6754
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
|
+
},
|
|
6755
7014
|
onDragEnd(event) {
|
|
7015
|
+
setHoverSnap(null);
|
|
6756
7016
|
if (!event.over || event.over.id !== axisDropId) return;
|
|
6757
7017
|
if (!axisRef.current) return;
|
|
6758
7018
|
const translated = event.active.rect.current.translated;
|
|
6759
7019
|
if (!translated) return;
|
|
6760
|
-
const
|
|
6761
|
-
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);
|
|
6762
7028
|
const data = event.active.data.current;
|
|
6763
|
-
if (data
|
|
6764
|
-
|
|
7029
|
+
if (!data) return;
|
|
7030
|
+
if (data.type === "placed-chip") {
|
|
7031
|
+
const oldWidgetId = data.widgetId;
|
|
6765
7032
|
const label = data.label;
|
|
6766
|
-
const
|
|
6767
|
-
|
|
6768
|
-
|
|
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);
|
|
6769
7051
|
return;
|
|
6770
7052
|
}
|
|
6771
|
-
|
|
6772
|
-
if (
|
|
6773
|
-
const targetPos =
|
|
6774
|
-
const wid = getPositionWidgetId(pos.content);
|
|
6775
|
-
return wid && !values[wid];
|
|
6776
|
-
});
|
|
7053
|
+
if (data.type !== "drag-option") return;
|
|
7054
|
+
if (isTickOccupied(placed)) return;
|
|
7055
|
+
const targetPos = findNearestFreeWidgetSlotCb(placed);
|
|
6777
7056
|
if (!targetPos) return;
|
|
6778
7057
|
const targetWidgetId = getPositionWidgetId(targetPos.content);
|
|
6779
7058
|
if (!targetWidgetId) return;
|
|
6780
|
-
const chipLabel =
|
|
6781
|
-
const
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
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);
|
|
6785
7068
|
}
|
|
6786
7069
|
});
|
|
6787
7070
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6788
7071
|
"div",
|
|
6789
7072
|
{
|
|
6790
7073
|
ref: mergedRef,
|
|
6791
|
-
className:
|
|
7074
|
+
className: "number-line-axis-row nl-axis-drag-mode",
|
|
6792
7075
|
children: [
|
|
6793
7076
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track" }),
|
|
6794
|
-
showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left" }),
|
|
6795
|
-
showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right" }),
|
|
6796
|
-
|
|
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(
|
|
6797
7081
|
"div",
|
|
6798
7082
|
{
|
|
6799
|
-
className:
|
|
7083
|
+
className: `number-line-minor-tick${hoverSnap === v ? " nl-tick-hover" : ""}`,
|
|
6800
7084
|
style: { left: `${toPercent(v, min, max)}%` }
|
|
6801
7085
|
},
|
|
6802
|
-
`minor-${v}`
|
|
7086
|
+
`minor-${cleanFloat(v)}`
|
|
6803
7087
|
)),
|
|
6804
7088
|
majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6805
7089
|
"div",
|
|
6806
7090
|
{
|
|
6807
|
-
className:
|
|
7091
|
+
className: `number-line-major-tick${hoverSnap === tick ? " nl-tick-hover" : ""}`,
|
|
6808
7092
|
style: { left: `${toPercent(tick, min, max)}%` }
|
|
6809
7093
|
},
|
|
6810
|
-
`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}`
|
|
6811
7111
|
)),
|
|
6812
7112
|
positions.map((pos) => {
|
|
6813
7113
|
const wid = getPositionWidgetId(pos.content);
|
|
6814
|
-
const
|
|
6815
|
-
|
|
6816
|
-
if (!wid || isNaN(dropPos) || !label) return null;
|
|
7114
|
+
const chip = wid ? placedChips.get(wid) : void 0;
|
|
7115
|
+
if (!wid || !chip) return null;
|
|
6817
7116
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6818
7117
|
PlacedChip,
|
|
6819
7118
|
{
|
|
6820
7119
|
widgetId: wid,
|
|
6821
|
-
label,
|
|
6822
|
-
leftPercent: toPercent(
|
|
7120
|
+
label: chip.label,
|
|
7121
|
+
leftPercent: toPercent(chip.position, min, max),
|
|
7122
|
+
isLanding: landingChips.has(wid),
|
|
6823
7123
|
onRemove: () => {
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
return
|
|
6830
|
-
});
|
|
6831
|
-
setDropSources((prev) => {
|
|
6832
|
-
const m = new Map(prev);
|
|
6833
|
-
m.delete(wid);
|
|
6834
|
-
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;
|
|
6835
7130
|
});
|
|
6836
7131
|
}
|
|
6837
7132
|
},
|
|
@@ -6842,6 +7137,132 @@ const FreeDragAxis = ({
|
|
|
6842
7137
|
}
|
|
6843
7138
|
);
|
|
6844
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
|
+
] });
|
|
6845
7266
|
const NumberLineRenderer = ({
|
|
6846
7267
|
item,
|
|
6847
7268
|
widgets,
|
|
@@ -6850,27 +7271,59 @@ const NumberLineRenderer = ({
|
|
|
6850
7271
|
onChange,
|
|
6851
7272
|
onInputFocus
|
|
6852
7273
|
}) => {
|
|
6853
|
-
const {
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
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";
|
|
6858
7307
|
const isDragMode = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
|
|
6859
7308
|
const inputTickSet = React.useMemo(() => {
|
|
6860
7309
|
if (isDragMode) return /* @__PURE__ */ new Set();
|
|
6861
7310
|
return new Set(
|
|
6862
|
-
positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => pos.value)
|
|
7311
|
+
positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => roundTick(pos.value))
|
|
6863
7312
|
);
|
|
6864
7313
|
}, [isDragMode, positions]);
|
|
7314
|
+
const visiblePositions = React.useMemo(
|
|
7315
|
+
() => positions.filter((pos) => !isDragMode || getPositionWidgetId(pos.content) === null),
|
|
7316
|
+
[isDragMode, positions]
|
|
7317
|
+
);
|
|
6865
7318
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6866
7319
|
"div",
|
|
6867
7320
|
{
|
|
6868
|
-
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}`,
|
|
7321
|
+
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
|
|
6869
7322
|
style: { [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%` },
|
|
6870
|
-
role: "
|
|
7323
|
+
role: "group",
|
|
6871
7324
|
"aria-label": `Number line from ${min} to ${max}`,
|
|
6872
7325
|
children: [
|
|
6873
|
-
|
|
7326
|
+
visiblePositions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6874
7327
|
"div",
|
|
6875
7328
|
{
|
|
6876
7329
|
className: "number-line-position-content",
|
|
@@ -6902,50 +7355,43 @@ const NumberLineRenderer = ({
|
|
|
6902
7355
|
showArrowRight,
|
|
6903
7356
|
positions,
|
|
6904
7357
|
values,
|
|
6905
|
-
onChange
|
|
7358
|
+
onChange,
|
|
7359
|
+
highlights,
|
|
7360
|
+
snapKey: counterCount
|
|
6906
7361
|
}
|
|
6907
|
-
) : /* @__PURE__ */ jsxRuntime.
|
|
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
|
-
|
|
6940
|
-
"span",
|
|
6941
|
-
{
|
|
6942
|
-
className: "number-line-tick-label",
|
|
6943
|
-
style: { left: `${toPercent(tick, min, max)}%`, visibility: hasInputAtTick ? "hidden" : "visible" },
|
|
6944
|
-
children: tick
|
|
6945
|
-
},
|
|
6946
|
-
`lbl-${tick}`
|
|
6947
|
-
);
|
|
6948
|
-
}) })
|
|
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
|
+
)
|
|
6949
7395
|
]
|
|
6950
7396
|
}
|
|
6951
7397
|
);
|
|
@@ -7173,8 +7619,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7173
7619
|
}
|
|
7174
7620
|
}), [json, values, onSubmit]);
|
|
7175
7621
|
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
7176
|
-
|
|
7177
|
-
setValues(newValues);
|
|
7622
|
+
setValues((prev) => ({ ...prev, [widgetId]: value }));
|
|
7178
7623
|
setDroppedSources((prev) => {
|
|
7179
7624
|
const next = new Map(prev);
|
|
7180
7625
|
if (fromDrop && value !== "" && sourceOptionId) {
|
|
@@ -7184,7 +7629,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
7184
7629
|
}
|
|
7185
7630
|
return next;
|
|
7186
7631
|
});
|
|
7187
|
-
onChange?.(
|
|
7632
|
+
onChange?.({ ...values, [widgetId]: value });
|
|
7188
7633
|
};
|
|
7189
7634
|
const handleSubmit = () => {
|
|
7190
7635
|
onSubmit?.(values);
|
|
@@ -7407,6 +7852,10 @@ exports.LayoutRenderer = LayoutRenderer;
|
|
|
7407
7852
|
exports.LexerError = LexerError;
|
|
7408
7853
|
exports.LineBlockSchema = LineBlockSchema;
|
|
7409
7854
|
exports.LinebreakSchema = LinebreakSchema;
|
|
7855
|
+
exports.NumberLineCounterItemSchema = NumberLineCounterItemSchema;
|
|
7856
|
+
exports.NumberLineHighlightItemSchema = NumberLineHighlightItemSchema;
|
|
7857
|
+
exports.NumberLineItemSchema = NumberLineItemSchema;
|
|
7858
|
+
exports.NumberLinePositionItemSchema = NumberLinePositionItemSchema;
|
|
7410
7859
|
exports.NumericKeypadJSONSchema = NumericKeypadJSONSchema;
|
|
7411
7860
|
exports.NumericKeypadToolSchema = NumericKeypadToolSchema;
|
|
7412
7861
|
exports.OperatorItemSchema = OperatorItemSchema;
|
|
@@ -7480,7 +7929,7 @@ exports.isTextItem = isTextItem$1;
|
|
|
7480
7929
|
exports.isToolNode = isToolNode;
|
|
7481
7930
|
exports.isValidExpression = isValidExpression;
|
|
7482
7931
|
exports.isValidationNode = isValidationNode;
|
|
7483
|
-
exports.isWidgetRef = isWidgetRef
|
|
7932
|
+
exports.isWidgetRef = isWidgetRef;
|
|
7484
7933
|
exports.parse = parse;
|
|
7485
7934
|
exports.parseSafe = parseSafe;
|
|
7486
7935
|
exports.renderExercise = renderExercise;
|