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