@rolldate/mcp 1.0.0 → 1.1.0

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.
@@ -6,9 +6,6 @@
6
6
  var RollDate = (function () {
7
7
  'use strict';
8
8
 
9
- function getTranslation(text) {
10
- return text
11
- }
12
9
  function getDecade(year) {
13
10
  return Math.floor(year / 10) * 10
14
11
  }
@@ -453,6 +450,9 @@ var RollDate = (function () {
453
450
  this.startWeekFromMonday = options.startWeekFromMonday;
454
451
  this.monthsNames = options.monthsNames;
455
452
  this.monthsShortNames = options.monthsShortNames;
453
+ this.weekDays = Array.isArray(options.weekDays) && options.weekDays.length === 7
454
+ ? options.weekDays
455
+ : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
456
456
  this.enableTime = Boolean(options.enableTime);
457
457
  this.hasFooter = Boolean(options.enableTime || (options.footerButtons && options.footerButtons.length));
458
458
 
@@ -509,48 +509,77 @@ var RollDate = (function () {
509
509
  }
510
510
 
511
511
  #dayHeaders() {
512
- const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
512
+ const weekDays = this.weekDays;
513
513
  let header = '';
514
514
  const startIndex = Number(this.startWeekFromMonday);
515
515
  for (let i = startIndex; i < 7 + startIndex; i++) {
516
- header += `<div class="RollDate__calendar__header__weekday">${getTranslation(weekDays[i % 7])}</div>`;
516
+ header += `<div class="RollDate__calendar__header__weekday">${weekDays[i % 7]}</div>`;
517
517
  }
518
518
  return header
519
519
  }
520
520
 
521
- dates(array, selectedDates = [], selectType = 'single') {
521
+ #highlightDots(colors = [], maxVisible = 5) {
522
+ if (!colors.length) return ''
523
+
524
+ const visible = colors.slice(0, maxVisible);
525
+ const extra = colors.length - maxVisible;
526
+ const many = visible.length > 3;
527
+ const dotClass = many
528
+ ? 'RollDate__calendar__day-dot RollDate__calendar__day-dot--compact'
529
+ : 'RollDate__calendar__day-dot';
530
+
531
+ const dots = visible.map(color => {
532
+ if (color) {
533
+ return `<span class="${dotClass} RollDate__calendar__day-dot--custom" style="--rd-highlight-dot:${color}"></span>`
534
+ }
535
+ return `<span class="${dotClass}"></span>`
536
+ }).join('');
537
+
538
+ const more = extra > 0
539
+ ? `<span class="RollDate__calendar__day-dot RollDate__calendar__day-dot--more">+${extra}</span>`
540
+ : '';
541
+
542
+ return `<span class="RollDate__calendar__day-dots" aria-hidden="true">${dots}${more}</span>`
543
+ }
544
+
545
+ dates(array, selectedDates = [], selectType = 'single', getHighlight = () => null) {
522
546
  let datesHtml = '';
523
547
  const today = new Date();
524
548
 
525
549
  for (let i = 0; i < array.length; i++) {
526
550
  const date = array[i].date;
527
551
  const disabledClass = array[i].disabled ? 'RollDate__calendar__day--disabled' : '';
552
+ const highlight = getHighlight(date);
553
+ const highlightDots = highlight ? this.#highlightDots(highlight.colors) : '';
528
554
  const isToday = date.toDateString() === today.toDateString();
529
555
 
530
556
  // Resolve selection class for current date cell.
531
557
  let selectionClass = '';
532
558
  if (selectType === 'single' || selectType === 'multi') {
559
+ const dayStamp = date.getTime();
533
560
  const isSelected = selectedDates.some(
534
- d => d.getTime() === date.getTime()
561
+ d => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() === dayStamp
535
562
  );
536
563
  if (isSelected) selectionClass = 'RollDate__calendar__day--selected';
537
564
  } else if (selectType === 'range' && selectedDates.length === 2) {
538
565
  const [start, end] = selectedDates;
539
566
  const time = date.getTime();
540
- if (time === start.getTime()) {
567
+ const startStamp = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime();
568
+ const endStamp = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime();
569
+ if (time === startStamp) {
541
570
  selectionClass = 'RollDate__calendar__day--range-first';
542
- } else if (time === end.getTime()) {
571
+ } else if (time === endStamp) {
543
572
  selectionClass = 'RollDate__calendar__day--range-last';
544
- } else if (time > start.getTime() && time < end.getTime()) {
573
+ } else if (time > startStamp && time < endStamp) {
545
574
  selectionClass = 'RollDate__calendar__day--range-selected';
546
575
  }
547
576
  }
548
577
 
549
- datesHtml += `<div class="RollDate__calendar__day ${disabledClass} ${isToday ? 'RollDate__calendar__day--today' : ''} ${selectionClass}"
578
+ datesHtml += `<div class="RollDate__calendar__day ${disabledClass} ${isToday ? 'RollDate__calendar__day--today' : ''} ${selectionClass}"
550
579
  data-bind='["year", "month"]'
551
580
  data-year="${date.getFullYear()}"
552
581
  data-month="${date.getMonth()}"
553
- data-day="${date.getDate()}">${date.getDate()}</div>`;
582
+ data-day="${date.getDate()}">${date.getDate()}${highlightDots}</div>`;
554
583
  }
555
584
 
556
585
  return datesHtml
@@ -610,495 +639,431 @@ var RollDate = (function () {
610
639
  }
611
640
  }
612
641
 
613
- class Scroll {
614
- #minScroll = null
615
- #EDGE_TOP = 25
616
- #EDGE_BOTTOM = 75
617
- #offset
618
- #baseOffset = 0
619
- #blocked
620
- #boundWheelHandler
621
- #boundTouchStartHandler
622
- #boundTouchMoveHandler
623
- #boundTouchEndHandler
624
- #isWheelAnimating = false
625
- #edgeTriggeredInCurrentWheel = false
626
- #lastEdgeTriggerAt = 0
627
- #isTouchDragging = false
628
- #touchLastY = 0
629
- #touchLastTime = 0
630
- #touchVelocityY = 0
631
- #touchMomentumId = null
632
-
633
- constructor(body, methods = {
634
- dominant: () => console.error('Function "dominant" is not found'),
635
- updatePeriod: () => console.error('Function "updatePeriod" is not found')
636
- }) {
637
-
638
- this.$body = body;
639
- this.$scroll_block = this.$body.querySelector('.RollDate__calendar__scrollblock');
640
- this.dominant = methods.dominant;
641
- this.updatePeriod = methods.updatePeriod;
642
- this.offset = 0;
643
- this.init();
644
- }
645
-
646
- init() {
647
- this.#boundWheelHandler = this.wheelHandler.bind(this);
648
- this.$body.addEventListener('wheel', this.#boundWheelHandler, { passive: false });
649
- this.#boundTouchStartHandler = this.touchStartHandler.bind(this);
650
- this.#boundTouchMoveHandler = this.touchMoveHandler.bind(this);
651
- this.#boundTouchEndHandler = this.touchEndHandler.bind(this);
652
- this.$body.addEventListener('touchstart', this.#boundTouchStartHandler, { passive: true });
653
- this.$body.addEventListener('touchmove', this.#boundTouchMoveHandler, { passive: false });
654
- this.$body.addEventListener('touchend', this.#boundTouchEndHandler, { passive: true });
655
- this.$body.addEventListener('touchcancel', this.#boundTouchEndHandler, { passive: true });
656
- }
657
-
658
- wheelHandler(e) {
659
- e.preventDefault();
660
- e.stopPropagation();
661
-
662
- const rawDelta = e.deltaY * 0.85;
663
- const steps = Math.abs(rawDelta) > 50 ? 20 : 1;
664
- const stepSize = rawDelta / steps;
665
- const currentWheelDirection = rawDelta > 0 ? 'down' : 'up';
666
- this.#isWheelAnimating = true;
667
- this.#edgeTriggeredInCurrentWheel = false;
668
-
669
- let i = 0;
670
- const animate = () => {
671
- if (i >= steps) {
672
- this.#isWheelAnimating = false;
673
- this.#edgeTriggeredInCurrentWheel = false;
674
- return
675
- }
676
-
677
- this.offset -= stepSize;
678
-
679
- requestAnimationFrame(() => {
680
- this.dominant();
681
- this.checkEdge(currentWheelDirection);
682
- i++;
683
- animate();
684
- });
685
- };
686
-
687
- animate();
688
- }
689
-
690
- touchStartHandler(e) {
691
- if (!e.touches || e.touches.length !== 1) return
692
- this.#cancelTouchMomentum();
693
- this.#isTouchDragging = true;
694
- this.#touchLastY = e.touches[0].clientY;
695
- this.#touchLastTime = performance.now();
696
- this.#touchVelocityY = 0;
697
- this.#edgeTriggeredInCurrentWheel = false;
698
- }
699
-
700
- touchMoveHandler(e) {
701
- if (!this.#isTouchDragging || !e.touches || e.touches.length !== 1) return
702
- e.preventDefault();
703
-
704
- const currentY = e.touches[0].clientY;
705
- const deltaY = currentY - this.#touchLastY;
706
- const now = performance.now();
707
- const dt = now - this.#touchLastTime;
708
-
709
- if (dt > 0 && dt < 120) {
710
- const instantVelocity = deltaY / dt;
711
- this.#touchVelocityY = this.#touchVelocityY * 0.65 + instantVelocity * 0.35;
712
- }
713
-
714
- this.#touchLastY = currentY;
715
- this.#touchLastTime = now;
716
-
717
- if (Math.abs(deltaY) < 1) return
718
-
719
- this.offset += deltaY;
720
- this.dominant();
721
-
722
- const direction = deltaY < 0 ? 'down' : 'up';
723
- this.checkEdge(direction);
724
- }
725
-
726
- touchEndHandler() {
727
- this.#isTouchDragging = false;
728
- this.#edgeTriggeredInCurrentWheel = false;
729
-
730
- const velocityPerFrame = this.#touchVelocityY * 16;
731
- if (Math.abs(velocityPerFrame) >= 0.4) {
732
- this.#runTouchMomentum(velocityPerFrame);
733
- }
734
- }
735
-
736
- #cancelTouchMomentum() {
737
- if (this.#touchMomentumId !== null) {
738
- cancelAnimationFrame(this.#touchMomentumId);
739
- this.#touchMomentumId = null;
740
- }
741
- }
742
-
743
- #runTouchMomentum(velocity) {
744
- this.#cancelTouchMomentum();
745
-
746
- const step = () => {
747
- if (Math.abs(velocity) < 0.35 || this.blocked) {
748
- this.#touchMomentumId = null;
749
- this.#edgeTriggeredInCurrentWheel = false;
750
- return
751
- }
752
-
753
- this.offset += velocity;
754
- velocity *= 0.92;
755
- this.dominant();
756
-
757
- const direction = velocity < 0 ? 'down' : 'up';
758
- this.checkEdge(direction);
759
-
760
- if (this.blocked) {
761
- this.#touchMomentumId = null;
762
- this.#edgeTriggeredInCurrentWheel = false;
763
- return
764
- }
765
-
766
- this.#touchMomentumId = requestAnimationFrame(step);
767
- };
768
-
769
- this.#touchMomentumId = requestAnimationFrame(step);
770
- }
771
-
772
- apply() {
773
- this.$scroll_block.style.transform = `translateY(${this.offset + this.#baseOffset}px)`;
774
- }
775
-
776
- get offset() { return this.#offset }
777
- set offset(value) {
778
-
779
- this.#offset = value;
780
-
781
- if ( this.#minScroll !== null ) {
782
- if (value >= 0) this.#offset = 0;
783
- else if (value <= this.#minScroll) this.#offset = this.#minScroll;
784
- }
785
- this.apply();
786
- }
787
-
788
- get blocked() { return this.#blocked }
789
- set blocked(value) { this.#blocked = value; }
790
- get minScroll() {
791
- if (this.#minScroll === null) this.checkMinScroll();
792
- return this.#minScroll ?? 0
793
- }
794
-
795
- checkEdge(direction) {
796
- if (this.#minScroll === null) {
797
- this.checkMinScroll();
798
- }
799
-
800
- if (this.#minScroll === 0 || this.blocked) return
801
- if (this.#isWheelAnimating && this.#edgeTriggeredInCurrentWheel) return
802
-
803
- const percent = Math.abs(this.offset / this.#minScroll) * 100;
804
- const now = Date.now();
805
- const EDGE_COOLDOWN_MS = 60;
806
-
807
- if (now - this.#lastEdgeTriggerAt < EDGE_COOLDOWN_MS) return
808
-
809
- if (percent < this.#EDGE_TOP) {
810
- if (direction !== 'up') return
811
- this.#lastEdgeTriggerAt = now;
812
- this.#edgeTriggeredInCurrentWheel = true;
813
- this.blocked = true;
814
- this.updatePeriod('up');
815
- } else if (percent > this.#EDGE_BOTTOM) {
816
- if (direction !== 'down') return
817
- this.#lastEdgeTriggerAt = now;
818
- this.#edgeTriggeredInCurrentWheel = true;
819
- this.blocked = true;
820
- this.updatePeriod('down');
821
- }
822
- }
823
-
824
- checkMinScroll() {
825
- const scrollHeight = this.$scroll_block.clientHeight;
826
- const bodyHeight = this.$body.clientHeight;
827
- this.#minScroll = scrollHeight > bodyHeight ? -(scrollHeight - bodyHeight) : 0;
828
- }
829
-
830
- setBaseOffset(value = 0) {
831
- this.#baseOffset = value;
832
- this.apply();
833
- }
834
-
835
- resetMinScroll() {
836
- this.#minScroll = null;
837
- }
838
-
839
- destroy() {
840
- this.#cancelTouchMomentum();
841
- if (this.#boundWheelHandler) {
842
- this.$body.removeEventListener('wheel', this.#boundWheelHandler);
843
- }
844
- if (this.#boundTouchStartHandler) {
845
- this.$body.removeEventListener('touchstart', this.#boundTouchStartHandler);
846
- }
847
- if (this.#boundTouchMoveHandler) {
848
- this.$body.removeEventListener('touchmove', this.#boundTouchMoveHandler);
849
- }
850
- if (this.#boundTouchEndHandler) {
851
- this.$body.removeEventListener('touchend', this.#boundTouchEndHandler);
852
- this.$body.removeEventListener('touchcancel', this.#boundTouchEndHandler);
853
- }
854
- }
642
+ class Scroll {
643
+ #minScroll = null
644
+ #EDGE_TOP = 25
645
+ #EDGE_BOTTOM = 75
646
+ #offset
647
+ #baseOffset = 0
648
+ #blocked
649
+ #boundWheelHandler
650
+ #boundTouchStartHandler
651
+ #boundTouchMoveHandler
652
+ #boundTouchEndHandler
653
+ #isWheelAnimating = false
654
+ #edgeTriggeredInCurrentWheel = false
655
+ #lastEdgeTriggerAt = 0
656
+ #isTouchDragging = false
657
+ #touchLastY = 0
658
+
659
+ constructor(body, methods = {
660
+ dominant: () => console.error('Function "dominant" is not found'),
661
+ updatePeriod: () => console.error('Function "updatePeriod" is not found')
662
+ }) {
663
+
664
+ this.$body = body;
665
+ this.$scroll_block = this.$body.querySelector('.RollDate__calendar__scrollblock');
666
+ this.dominant = methods.dominant;
667
+ this.updatePeriod = methods.updatePeriod;
668
+ this.offset = 0;
669
+ this.init();
670
+ }
671
+
672
+ init() {
673
+ this.#boundWheelHandler = this.wheelHandler.bind(this);
674
+ this.$body.addEventListener('wheel', this.#boundWheelHandler, { passive: false });
675
+ this.#boundTouchStartHandler = this.touchStartHandler.bind(this);
676
+ this.#boundTouchMoveHandler = this.touchMoveHandler.bind(this);
677
+ this.#boundTouchEndHandler = this.touchEndHandler.bind(this);
678
+ this.$body.addEventListener('touchstart', this.#boundTouchStartHandler, { passive: true });
679
+ this.$body.addEventListener('touchmove', this.#boundTouchMoveHandler, { passive: false });
680
+ this.$body.addEventListener('touchend', this.#boundTouchEndHandler, { passive: true });
681
+ this.$body.addEventListener('touchcancel', this.#boundTouchEndHandler, { passive: true });
682
+ }
683
+
684
+ wheelHandler(e) {
685
+ e.preventDefault();
686
+ e.stopPropagation();
687
+
688
+ const rawDelta = e.deltaY * 0.85;
689
+ const steps = Math.abs(rawDelta) > 50 ? 20 : 1;
690
+ const stepSize = rawDelta / steps;
691
+ const currentWheelDirection = rawDelta > 0 ? 'down' : 'up';
692
+ this.#isWheelAnimating = true;
693
+ this.#edgeTriggeredInCurrentWheel = false;
694
+
695
+ let i = 0;
696
+ const animate = () => {
697
+ if (i >= steps) {
698
+ this.#isWheelAnimating = false;
699
+ this.#edgeTriggeredInCurrentWheel = false;
700
+ return
701
+ }
702
+
703
+ this.offset -= stepSize;
704
+
705
+ requestAnimationFrame(() => {
706
+ this.dominant();
707
+ this.checkEdge(currentWheelDirection);
708
+ i++;
709
+ animate();
710
+ });
711
+ };
712
+
713
+ animate();
714
+ }
715
+
716
+ touchStartHandler(e) {
717
+ if (!e.touches || e.touches.length !== 1) return
718
+ this.#isTouchDragging = true;
719
+ this.#touchLastY = e.touches[0].clientY;
720
+ this.#edgeTriggeredInCurrentWheel = false;
721
+ }
722
+
723
+ touchMoveHandler(e) {
724
+ if (!this.#isTouchDragging || !e.touches || e.touches.length !== 1) return
725
+ e.preventDefault();
726
+
727
+ const currentY = e.touches[0].clientY;
728
+ const deltaY = currentY - this.#touchLastY;
729
+ this.#touchLastY = currentY;
730
+
731
+ if (Math.abs(deltaY) < 1) return
732
+
733
+ this.offset += deltaY;
734
+ this.dominant();
735
+
736
+ const direction = deltaY < 0 ? 'down' : 'up';
737
+ this.checkEdge(direction);
738
+ }
739
+
740
+ touchEndHandler() {
741
+ this.#isTouchDragging = false;
742
+ this.#edgeTriggeredInCurrentWheel = false;
743
+ }
744
+
745
+ apply() {
746
+ this.$scroll_block.style.transform = `translateY(${this.offset + this.#baseOffset}px)`;
747
+ }
748
+
749
+ get offset() { return this.#offset }
750
+ set offset(value) {
751
+
752
+ this.#offset = value;
753
+
754
+ if ( this.#minScroll !== null ) {
755
+ if (value >= 0) this.#offset = 0;
756
+ else if (value <= this.#minScroll) this.#offset = this.#minScroll;
757
+ }
758
+ this.apply();
759
+ }
760
+
761
+ get blocked() { return this.#blocked }
762
+ set blocked(value) { this.#blocked = value; }
763
+ get minScroll() {
764
+ if (this.#minScroll === null) this.checkMinScroll();
765
+ return this.#minScroll ?? 0
766
+ }
767
+
768
+ checkEdge(direction) {
769
+ if (this.#minScroll === null) {
770
+ this.checkMinScroll();
771
+ }
772
+
773
+ if (this.#minScroll === 0 || this.blocked) return
774
+ if (this.#isWheelAnimating && this.#edgeTriggeredInCurrentWheel) return
775
+
776
+ const percent = Math.abs(this.offset / this.#minScroll) * 100;
777
+ const now = Date.now();
778
+ const EDGE_COOLDOWN_MS = 60;
779
+
780
+ if (now - this.#lastEdgeTriggerAt < EDGE_COOLDOWN_MS) return
781
+
782
+ if (percent < this.#EDGE_TOP) {
783
+ if (direction !== 'up') return
784
+ this.#lastEdgeTriggerAt = now;
785
+ this.#edgeTriggeredInCurrentWheel = true;
786
+ this.blocked = true;
787
+ this.updatePeriod('up');
788
+ } else if (percent > this.#EDGE_BOTTOM) {
789
+ if (direction !== 'down') return
790
+ this.#lastEdgeTriggerAt = now;
791
+ this.#edgeTriggeredInCurrentWheel = true;
792
+ this.blocked = true;
793
+ this.updatePeriod('down');
794
+ }
795
+ }
796
+
797
+ checkMinScroll() {
798
+ const scrollHeight = this.$scroll_block.clientHeight;
799
+ const bodyHeight = this.$body.clientHeight;
800
+ this.#minScroll = scrollHeight > bodyHeight ? -(scrollHeight - bodyHeight) : 0;
801
+ }
802
+
803
+ setBaseOffset(value = 0) {
804
+ this.#baseOffset = value;
805
+ this.apply();
806
+ }
807
+
808
+ resetMinScroll() {
809
+ this.#minScroll = null;
810
+ }
811
+
812
+ destroy() {
813
+ if (this.#boundWheelHandler) {
814
+ this.$body.removeEventListener('wheel', this.#boundWheelHandler);
815
+ }
816
+ if (this.#boundTouchStartHandler) {
817
+ this.$body.removeEventListener('touchstart', this.#boundTouchStartHandler);
818
+ }
819
+ if (this.#boundTouchMoveHandler) {
820
+ this.$body.removeEventListener('touchmove', this.#boundTouchMoveHandler);
821
+ }
822
+ if (this.#boundTouchEndHandler) {
823
+ this.$body.removeEventListener('touchend', this.#boundTouchEndHandler);
824
+ this.$body.removeEventListener('touchcancel', this.#boundTouchEndHandler);
825
+ }
826
+ }
855
827
  }
856
828
 
857
- class Virtualizer {
858
- constructor(context) {
859
- this.ctx = context;
860
- }
861
-
862
- #isDayDisabled(date) {
863
- const d = date instanceof Date ? date : new Date(date);
864
- if (this.ctx.options.minDate && d < this.ctx.options.minDate) return true
865
- if (this.ctx.options.maxDate && d > this.ctx.options.maxDate) return true
866
- return typeof this.ctx.isDateDisabled === 'function' && this.ctx.isDateDisabled(d)
867
- }
868
-
869
- update(direction) {
870
- const period = this.ctx.period;
871
- const isUp = direction === 'up';
872
-
873
- // Strict boundary checks to stop loading beyond min/max limits.
874
- if (isUp) {
875
- if (period === 'year') {
876
- if (this.ctx.data.up_date.getFullYear() <= this.ctx.options.minDate.getFullYear()) {
877
- this.ctx.scroll.blocked = false;
878
- return
879
- }
880
- } else {
881
- if (this.ctx.data.up_date <= this.ctx.options.minDate) {
882
- this.ctx.scroll.blocked = false;
883
- return
884
- }
885
- }
886
- } else {
887
- if (period === 'year') {
888
- if (this.ctx.data.down_date.getFullYear() >= this.ctx.options.maxDate.getFullYear()) {
889
- this.ctx.scroll.blocked = false;
890
- return
891
- }
892
- } else {
893
- if (this.ctx.data.down_date >= this.ctx.options.maxDate) {
894
- this.ctx.scroll.blocked = false;
895
- return
896
- }
897
- }
898
- }
899
-
900
- const config = {
901
- day: {
902
- count: 35,
903
- getNewItems: (upDate, downDate) => {
904
- const newDates = [];
905
- if (isUp) {
906
- let newFirst = new Date(upDate);
907
- newFirst.setDate(upDate.getDate() - 35);
908
- if (newFirst < this.ctx.options.minDate) {
909
- const minDate = this.ctx.options.minDate;
910
- newFirst = adjustToWeekStart(
911
- new Date(minDate.getFullYear(), minDate.getMonth(), 1),
912
- this.ctx.options.startWeekFromMonday
913
- );
914
- }
915
- for (let d = new Date(newFirst); d < upDate; d.setDate(d.getDate() + 1)) {
916
- newDates.push({
917
- date: new Date(d),
918
- disabled: this.#isDayDisabled(d)
919
- });
920
- }
921
- return { items: newDates, newBound: newFirst }
922
- } else {
923
- let newLast = new Date(downDate);
924
- newLast.setDate(downDate.getDate() + 35);
925
- const firstNew = new Date(downDate);
926
- firstNew.setDate(downDate.getDate() + 1);
927
-
928
- if (newLast > this.ctx.options.maxDate) {
929
- const maxDate = this.ctx.options.maxDate;
930
- newLast = adjustToWeekEnd(
931
- new Date(maxDate.getFullYear(), maxDate.getMonth() + 1, 0),
932
- this.ctx.options.startWeekFromMonday
933
- );
934
- }
935
-
936
- for (let d = firstNew; d <= newLast; d.setDate(d.getDate() + 1)) {
937
- newDates.push({
938
- date: new Date(d),
939
- disabled: this.#isDayDisabled(d)
940
- });
941
- }
942
- return { items: newDates, newBound: newLast }
943
- }
944
- },
945
- render: dates => this.ctx.render.dates(
946
- dates,
947
- this.ctx.selectedDates,
948
- this.ctx.options.selectType
949
- ),
950
- block: this.ctx.dom.$days_block
951
- },
952
- month: {
953
- count: 36,
954
- ctx: this.ctx,
955
- getNewItems: function (upDate, downDate) {
956
- const newDates = [];
957
- const isMonthDisabled = (year, month) => {
958
- const date = new Date(year, month, 1);
959
- const monthEnd = new Date(year, month + 1, 0);
960
- if (this.ctx.options.minDate && monthEnd < this.ctx.options.minDate) return true
961
- if (this.ctx.options.maxDate && date > this.ctx.options.maxDate) return true
962
- return false
963
- };
964
-
965
- if (isUp) {
966
- // Generate full-year chunks (multiple of 12) so Jan always starts grid rows.
967
- let curr = new Date(upDate);
968
- for (let i = 0; i < this.count; i++) {
969
- curr.setMonth(curr.getMonth() - 1);
970
- newDates.unshift({
971
- date: new Date(curr.getFullYear(), curr.getMonth(), 1),
972
- disabled: isMonthDisabled(curr.getFullYear(), curr.getMonth())
973
- });
974
- }
975
- const newBound = newDates.length
976
- ? newDates[0].date
977
- : upDate;
978
- return { items: newDates, newBound }
979
- } else {
980
- // Generate full-year chunks to keep row structure stable while scrolling.
981
- let curr = new Date(downDate);
982
- curr.setMonth(curr.getMonth() + 1);
983
- for (let i = 0; i < this.count; i++) {
984
- newDates.push({
985
- date: new Date(curr.getFullYear(), curr.getMonth(), 1),
986
- disabled: isMonthDisabled(curr.getFullYear(), curr.getMonth())
987
- });
988
- curr.setMonth(curr.getMonth() + 1);
989
- }
990
- const newBound = newDates.length
991
- ? newDates[newDates.length - 1].date
992
- : downDate;
993
- return { items: newDates, newBound }
994
- }
995
- },
996
- render: months => this.ctx.render.months(months),
997
- block: this.ctx.dom.$months_block
998
- },
999
- year: {
1000
- count: 16,
1001
- ctx: this.ctx,
1002
- getNewItems: function (upDate, downDate) {
1003
- const newDates = [];
1004
- if (isUp) {
1005
- // Generate strictly before upDate year.
1006
- const startYear = upDate.getFullYear() - this.count;
1007
- for (let y = startYear; y < upDate.getFullYear(); y++) {
1008
- if (y < this.ctx.options.minDate.getFullYear()) continue
1009
- newDates.push({
1010
- date: new Date(y, 0, 1),
1011
- disabled: this.ctx.options.minDate.getFullYear() > y
1012
- });
1013
- }
1014
- const newBound = newDates.length
1015
- ? newDates[0].date
1016
- : upDate;
1017
- return { items: newDates, newBound }
1018
- } else {
1019
- // Generate strictly after downDate year.
1020
- const startYear = downDate.getFullYear() + 1;
1021
- for (let y = startYear; y < startYear + this.count; y++) {
1022
- if (y > this.ctx.options.maxDate.getFullYear()) break
1023
- newDates.push({
1024
- date: new Date(y, 0, 1),
1025
- disabled: this.ctx.options.maxDate.getFullYear() < y
1026
- });
1027
- }
1028
- const newBound = newDates.length
1029
- ? newDates[newDates.length - 1].date
1030
- : downDate;
1031
- return { items: newDates, newBound }
1032
- }
1033
- },
1034
- render: years => this.ctx.render.years(years),
1035
- block: this.ctx.dom.$years_block
1036
- }
1037
- }[period];
1038
-
1039
- if (!config) return
1040
-
1041
- const { items, newBound } = config.getNewItems(this.ctx.data.up_date, this.ctx.data.down_date);
1042
-
1043
- if (items.length) {
1044
- config.block.insertAdjacentHTML(
1045
- isUp ? 'afterbegin' : 'beforeend',
1046
- config.render(items)
1047
- );
1048
- }
1049
-
1050
- this.ctx.data[`${direction}_date`] = newBound;
1051
- this.ctx.observe.un();
1052
- const trimSize = period === 'year'
1053
- ? Math.min(4, items.length)
1054
- : items.length;
1055
- this.trim(direction, trimSize);
1056
- this.ctx.observe.on(period);
1057
- this.ctx.scroll.blocked = false;
1058
- }
1059
-
1060
- trim(direction, remove) {
1061
- const period = this.ctx.period;
1062
- const block = this.ctx.dom[`$${period}s_block`];
1063
- const isUp = direction === 'up';
1064
- const minItemsInDom = period === 'year' ? 28 : 0;
1065
-
1066
- const oldHeight = block.scrollHeight;
1067
- const items = Array.from(block.querySelectorAll(`.RollDate__calendar__${period}`));
1068
-
1069
- if (!remove || items.length <= remove * 2) return
1070
- if (minItemsInDom && (items.length - remove) < minItemsInDom) return
1071
-
1072
- if (isUp) {
1073
- items.slice(-remove).forEach(item => item.remove());
1074
- } else {
1075
- items.slice(0, remove).forEach(item => item.remove());
1076
- }
1077
-
1078
- // Recalculate bounds from current DOM after trim to avoid flicker and endless refill loops.
1079
- const currentItems = Array.from(block.querySelectorAll(`.RollDate__calendar__${period}`));
1080
- const firstItem = currentItems[0];
1081
- const lastItem = currentItems[currentItems.length - 1];
1082
- const toDate = (el) => {
1083
- if (!el) return null
1084
- const year = Number(el.dataset.year);
1085
- const month = Number(el.dataset.month) || 0;
1086
- const day = period === 'day' ? Number(el.dataset.day) : 1;
1087
- return new Date(year, month, day)
1088
- };
1089
-
1090
- const newUpDate = toDate(firstItem);
1091
- const newDownDate = toDate(lastItem);
1092
- if (newUpDate) this.ctx.data.up_date = newUpDate;
1093
- if (newDownDate) this.ctx.data.down_date = newDownDate;
1094
-
1095
- const newHeight = block.scrollHeight;
1096
- const heightDelta = oldHeight - newHeight;
1097
- this.ctx.scroll.checkMinScroll();
1098
-
1099
- if (isUp) this.ctx.scroll.offset -= heightDelta;
1100
- else this.ctx.scroll.offset += heightDelta;
1101
- }
829
+ class Virtualizer {
830
+ constructor(context) {
831
+ this.ctx = context;
832
+ }
833
+
834
+ update(direction) {
835
+ const period = this.ctx.period;
836
+ const isUp = direction === 'up';
837
+
838
+ // 🔑 Точна перевірка меж
839
+ if (isUp) {
840
+ if (period === 'year') {
841
+ if (this.ctx.data.up_date.getFullYear() <= this.ctx.options.minDate.getFullYear()) {
842
+ this.ctx.scroll.blocked = false;
843
+ return
844
+ }
845
+ } else {
846
+ if (this.ctx.data.up_date <= this.ctx.options.minDate) {
847
+ this.ctx.scroll.blocked = false;
848
+ return
849
+ }
850
+ }
851
+ } else {
852
+ if (period === 'year') {
853
+ if (this.ctx.data.down_date.getFullYear() >= this.ctx.options.maxDate.getFullYear()) {
854
+ this.ctx.scroll.blocked = false;
855
+ return
856
+ }
857
+ } else {
858
+ if (this.ctx.data.down_date >= this.ctx.options.maxDate) {
859
+ this.ctx.scroll.blocked = false;
860
+ return
861
+ }
862
+ }
863
+ }
864
+
865
+ const config = {
866
+ day: {
867
+ count: 35,
868
+ getNewItems: (upDate, downDate) => {
869
+ const newDates = [];
870
+ if (isUp) {
871
+ let newFirst = new Date(upDate);
872
+ newFirst.setDate(upDate.getDate() - 35);
873
+ if (newFirst < this.ctx.options.minDate) {
874
+ const minDate = this.ctx.options.minDate;
875
+ newFirst = adjustToWeekStart(
876
+ new Date(minDate.getFullYear(), minDate.getMonth(), 1),
877
+ this.ctx.options.startWeekFromMonday
878
+ );
879
+ }
880
+ for (let d = new Date(newFirst); d < upDate; d.setDate(d.getDate() + 1)) {
881
+ newDates.push({
882
+ date: new Date(d),
883
+ disabled: this.ctx?.options?.minDate > new Date(d) || false
884
+ });
885
+ }
886
+ return { items: newDates, newBound: newFirst }
887
+ } else {
888
+ let newLast = new Date(downDate);
889
+ newLast.setDate(downDate.getDate() + 35);
890
+ const firstNew = new Date(downDate);
891
+ firstNew.setDate(downDate.getDate() + 1);
892
+
893
+ if (newLast > this.ctx.options.maxDate) {
894
+ const maxDate = this.ctx.options.maxDate;
895
+ newLast = adjustToWeekEnd(
896
+ new Date(maxDate.getFullYear(), maxDate.getMonth() + 1, 0),
897
+ this.ctx.options.startWeekFromMonday
898
+ );
899
+ }
900
+
901
+ for (let d = firstNew; d <= newLast; d.setDate(d.getDate() + 1)) {
902
+ newDates.push({
903
+ date: new Date(d),
904
+ disabled: this.ctx?.options?.maxDate < new Date(d) || false
905
+ });
906
+ }
907
+ return { items: newDates, newBound: newLast }
908
+ }
909
+ },
910
+ render: dates => this.ctx.render.dates(
911
+ dates,
912
+ this.ctx.selectedDates,
913
+ this.ctx.options.selectType
914
+ ),
915
+ block: this.ctx.dom.$days_block
916
+ },
917
+ month: {
918
+ count: 36,
919
+ ctx: this.ctx,
920
+ getNewItems: function (upDate, downDate) {
921
+ const newDates = [];
922
+ const isMonthDisabled = (year, month) => {
923
+ const date = new Date(year, month, 1);
924
+ const monthEnd = new Date(year, month + 1, 0);
925
+ if (this.ctx.options.minDate && monthEnd < this.ctx.options.minDate) return true
926
+ if (this.ctx.options.maxDate && date > this.ctx.options.maxDate) return true
927
+ return false
928
+ };
929
+
930
+ if (isUp) {
931
+ // Генеруємо повними роками (кратно 12), щоб Jan завжди був ліворуч у сітці.
932
+ let curr = new Date(upDate);
933
+ for (let i = 0; i < this.count; i++) {
934
+ curr.setMonth(curr.getMonth() - 1);
935
+ newDates.unshift({
936
+ date: new Date(curr.getFullYear(), curr.getMonth(), 1),
937
+ disabled: isMonthDisabled(curr.getFullYear(), curr.getMonth())
938
+ });
939
+ }
940
+ const newBound = newDates.length
941
+ ? newDates[0].date
942
+ : upDate;
943
+ return { items: newDates, newBound }
944
+ } else {
945
+ // Генеруємо повними роками (кратно 12), щоб структура рядків не "плавала".
946
+ let curr = new Date(downDate);
947
+ curr.setMonth(curr.getMonth() + 1);
948
+ for (let i = 0; i < this.count; i++) {
949
+ newDates.push({
950
+ date: new Date(curr.getFullYear(), curr.getMonth(), 1),
951
+ disabled: isMonthDisabled(curr.getFullYear(), curr.getMonth())
952
+ });
953
+ curr.setMonth(curr.getMonth() + 1);
954
+ }
955
+ const newBound = newDates.length
956
+ ? newDates[newDates.length - 1].date
957
+ : downDate;
958
+ return { items: newDates, newBound }
959
+ }
960
+ },
961
+ render: months => this.ctx.render.months(months),
962
+ block: this.ctx.dom.$months_block
963
+ },
964
+ year: {
965
+ count: 16,
966
+ ctx: this.ctx,
967
+ getNewItems: function (upDate, downDate) {
968
+ const newDates = [];
969
+ if (isUp) {
970
+ // 🔑 Генерація СТРОГО перед upDate.getFullYear()
971
+ const startYear = upDate.getFullYear() - this.count;
972
+ for (let y = startYear; y < upDate.getFullYear(); y++) {
973
+ if (y < this.ctx.options.minDate.getFullYear()) continue
974
+ newDates.push({
975
+ date: new Date(y, 0, 1),
976
+ disabled: this.ctx.options.minDate.getFullYear() > y
977
+ });
978
+ }
979
+ const newBound = newDates.length
980
+ ? newDates[0].date
981
+ : upDate;
982
+ return { items: newDates, newBound }
983
+ } else {
984
+ // 🔑 Генерація СТРОГО після downDate.getFullYear()
985
+ const startYear = downDate.getFullYear() + 1;
986
+ for (let y = startYear; y < startYear + this.count; y++) {
987
+ if (y > this.ctx.options.maxDate.getFullYear()) break
988
+ newDates.push({
989
+ date: new Date(y, 0, 1),
990
+ disabled: this.ctx.options.maxDate.getFullYear() < y
991
+ });
992
+ }
993
+ const newBound = newDates.length
994
+ ? newDates[newDates.length - 1].date
995
+ : downDate;
996
+ return { items: newDates, newBound }
997
+ }
998
+ },
999
+ render: years => this.ctx.render.years(years),
1000
+ block: this.ctx.dom.$years_block
1001
+ }
1002
+ }[period];
1003
+
1004
+ if (!config) return
1005
+
1006
+ const { items, newBound } = config.getNewItems(this.ctx.data.up_date, this.ctx.data.down_date);
1007
+
1008
+ if (items.length) {
1009
+ config.block.insertAdjacentHTML(
1010
+ isUp ? 'afterbegin' : 'beforeend',
1011
+ config.render(items)
1012
+ );
1013
+ }
1014
+
1015
+ this.ctx.data[`${direction}_date`] = newBound;
1016
+ this.ctx.observe.un();
1017
+ const trimSize = period === 'year'
1018
+ ? Math.min(4, items.length)
1019
+ : items.length;
1020
+ this.trim(direction, trimSize);
1021
+ this.ctx.observe.on(period);
1022
+ this.ctx.scroll.blocked = false;
1023
+ }
1024
+
1025
+ trim(direction, remove) {
1026
+ const period = this.ctx.period;
1027
+ const block = this.ctx.dom[`$${period}s_block`];
1028
+ const isUp = direction === 'up';
1029
+ const minItemsInDom = period === 'year' ? 28 : 0;
1030
+
1031
+ const oldHeight = block.scrollHeight;
1032
+ const items = Array.from(block.querySelectorAll(`.RollDate__calendar__${period}`));
1033
+
1034
+ if (!remove || items.length <= remove * 2) return
1035
+ if (minItemsInDom && (items.length - remove) < minItemsInDom) return
1036
+
1037
+ if (isUp) {
1038
+ items.slice(-remove).forEach(item => item.remove());
1039
+ } else {
1040
+ items.slice(0, remove).forEach(item => item.remove());
1041
+ }
1042
+
1043
+ // Перераховуємо межі тільки з поточного DOM після trim, щоб уникнути "миготіння" і зациклення дозагрузки.
1044
+ const currentItems = Array.from(block.querySelectorAll(`.RollDate__calendar__${period}`));
1045
+ const firstItem = currentItems[0];
1046
+ const lastItem = currentItems[currentItems.length - 1];
1047
+ const toDate = (el) => {
1048
+ if (!el) return null
1049
+ const year = Number(el.dataset.year);
1050
+ const month = Number(el.dataset.month) || 0;
1051
+ const day = period === 'day' ? Number(el.dataset.day) : 1;
1052
+ return new Date(year, month, day)
1053
+ };
1054
+
1055
+ const newUpDate = toDate(firstItem);
1056
+ const newDownDate = toDate(lastItem);
1057
+ if (newUpDate) this.ctx.data.up_date = newUpDate;
1058
+ if (newDownDate) this.ctx.data.down_date = newDownDate;
1059
+
1060
+ const newHeight = block.scrollHeight;
1061
+ const heightDelta = oldHeight - newHeight;
1062
+ this.ctx.scroll.checkMinScroll();
1063
+
1064
+ if (isUp) this.ctx.scroll.offset -= heightDelta;
1065
+ else this.ctx.scroll.offset += heightDelta;
1066
+ }
1102
1067
  }
1103
1068
 
1104
1069
  const ITEM_HEIGHT = 28;
@@ -1222,6 +1187,12 @@ var RollDate = (function () {
1222
1187
  }
1223
1188
  }
1224
1189
 
1190
+ #commitTime() {
1191
+ this.#readColumns();
1192
+ this.#updateDisplay();
1193
+ this.onChange(this.getTime());
1194
+ }
1195
+
1225
1196
  #createColumn(unit, values, initial) {
1226
1197
  const field = this.root.querySelector(`[data-unit="${unit}"]`);
1227
1198
  const viewport = field.querySelector('.RollDate__time__viewport');
@@ -1250,6 +1221,7 @@ var RollDate = (function () {
1250
1221
  if (column._lastIndex !== index) {
1251
1222
  if (column._lastIndex !== undefined) {
1252
1223
  hapticTick(this.hapticFeedback);
1224
+ this.#commitTime();
1253
1225
  }
1254
1226
  column._lastIndex = index;
1255
1227
  }
@@ -1274,10 +1246,9 @@ var RollDate = (function () {
1274
1246
  column.snap = () => {
1275
1247
  const index = column.indexFromOffset();
1276
1248
  column.offset = -index * ITEM_HEIGHT;
1249
+ column._lastIndex = index;
1277
1250
  column.apply(true);
1278
- this.#readColumns();
1279
- this.#updateDisplay();
1280
- this.onChange(this.getTime());
1251
+ this.#commitTime();
1281
1252
  };
1282
1253
 
1283
1254
  column.scrollToValue = (value, animate = true) => {
@@ -1291,9 +1262,6 @@ var RollDate = (function () {
1291
1262
  const index = Math.min(values.length - 1, Math.max(0, column.indexFromOffset() + delta));
1292
1263
  column.offset = -index * ITEM_HEIGHT;
1293
1264
  column.apply(true);
1294
- this.#readColumns();
1295
- this.#updateDisplay();
1296
- this.onChange(this.getTime());
1297
1265
  };
1298
1266
 
1299
1267
  column.scrollToValue(initial, false);
@@ -1407,6 +1375,7 @@ var RollDate = (function () {
1407
1375
  #selectedDates = []
1408
1376
  #firstOpen = true
1409
1377
  #disabledDateStamps = new Set()
1378
+ #highlightDateMap = new Map()
1410
1379
  #docClickHandler = null
1411
1380
  #openTriggers = []
1412
1381
 
@@ -1422,12 +1391,36 @@ var RollDate = (function () {
1422
1391
  }
1423
1392
 
1424
1393
  #normalizeDateInput(dateLike) {
1394
+ if (dateLike instanceof Date && !Number.isNaN(dateLike.getTime())) {
1395
+ return new Date(dateLike.getFullYear(), dateLike.getMonth(), dateLike.getDate())
1396
+ }
1397
+
1398
+ if (typeof dateLike === 'string') {
1399
+ const clean = dateLike.trim();
1400
+ if (!clean) return null
1401
+
1402
+ const valueSep = clean.match(/[-/.]/)?.[0];
1403
+ const formatSep = this.options.dateFormat.match(/[-/.]/)?.[0];
1404
+ const format = valueSep && formatSep && valueSep !== formatSep
1405
+ ? 'auto'
1406
+ : this.options.dateFormat;
1407
+
1408
+ const date = parseDate(clean, format);
1409
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
1410
+ const fallback = parseDate(clean, 'auto');
1411
+ if (!(fallback instanceof Date) || Number.isNaN(fallback.getTime())) return null
1412
+ return new Date(fallback.getFullYear(), fallback.getMonth(), fallback.getDate())
1413
+ }
1414
+
1415
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate())
1416
+ }
1417
+
1425
1418
  const date = checkDateFormat(dateLike, this.options.dateFormat);
1426
1419
  if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null
1427
1420
  return new Date(date.getFullYear(), date.getMonth(), date.getDate())
1428
1421
  }
1429
1422
 
1430
- #buildDisabledDateSet(dates = []) {
1423
+ #buildDateStampSet(dates = []) {
1431
1424
  const set = new Set();
1432
1425
  dates.forEach(dateLike => {
1433
1426
  const normalized = this.#normalizeDateInput(dateLike);
@@ -1436,6 +1429,85 @@ var RollDate = (function () {
1436
1429
  return set
1437
1430
  }
1438
1431
 
1432
+ #buildDisabledDateSet(dates = []) {
1433
+ return this.#buildDateStampSet(dates)
1434
+ }
1435
+
1436
+ #buildHighlightDateMap(dates = []) {
1437
+ const map = new Map();
1438
+ if (!Array.isArray(dates)) return map
1439
+
1440
+ dates.forEach(entry => {
1441
+ const item = this.#normalizeHighlightEntry(entry);
1442
+ if (!item) return
1443
+
1444
+ const existing = map.get(item.stamp) || [];
1445
+ map.set(item.stamp, existing.concat(item.colors));
1446
+ });
1447
+
1448
+ return map
1449
+ }
1450
+
1451
+ #normalizeHighlightEntry(entry) {
1452
+ if (entry == null) return null
1453
+
1454
+ if (typeof entry === 'string' || entry instanceof Date) {
1455
+ const normalized = this.#normalizeDateInput(entry);
1456
+ if (!normalized) return null
1457
+ return {
1458
+ stamp: this.#toDateStamp(normalized),
1459
+ colors: [null]
1460
+ }
1461
+ }
1462
+
1463
+ if (typeof entry === 'object' && entry.date != null) {
1464
+ const normalized = this.#normalizeDateInput(entry.date);
1465
+ if (!normalized) return null
1466
+ const stamp = this.#toDateStamp(normalized);
1467
+
1468
+ if (Array.isArray(entry.colors)) {
1469
+ const colors = entry.colors.map(color => {
1470
+ if (color == null || color === '') return null
1471
+ return this.#sanitizeHighlightColor(color)
1472
+ }).filter(color => color !== undefined);
1473
+
1474
+ if (colors.length) return { stamp, colors }
1475
+ }
1476
+
1477
+ if (entry.color != null && entry.color !== '') {
1478
+ const color = this.#sanitizeHighlightColor(entry.color);
1479
+ if (color) return { stamp, colors: [color] }
1480
+ }
1481
+
1482
+ return { stamp, colors: [null] }
1483
+ }
1484
+
1485
+ return null
1486
+ }
1487
+
1488
+ #sanitizeHighlightColor(color) {
1489
+ if (typeof color !== 'string') return null
1490
+ const value = color.trim();
1491
+ if (!value) return null
1492
+
1493
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)) return value
1494
+ if (/^(rgb|rgba|hsl|hsla)\([^)]+\)$/i.test(value)) return value
1495
+ if (/^var\(--[\w-]+\)$/.test(value)) return value
1496
+
1497
+ return null
1498
+ }
1499
+
1500
+ #highlightMapToOptionsArray() {
1501
+ return [...this.#highlightDateMap.entries()].map(([stamp, colors]) => {
1502
+ const date = new Date(stamp);
1503
+
1504
+ if (colors.length === 1 && colors[0] === null) return date
1505
+ if (colors.length === 1 && colors[0]) return { date, color: colors[0] }
1506
+
1507
+ return { date, colors }
1508
+ })
1509
+ }
1510
+
1439
1511
  #disableInputAssist(input) {
1440
1512
  if (!input || input.tagName !== 'INPUT') return
1441
1513
  input.setAttribute('autocomplete', 'off');
@@ -1448,6 +1520,16 @@ var RollDate = (function () {
1448
1520
  return this.#disabledDateStamps.has(this.#toDateStamp(date))
1449
1521
  }
1450
1522
 
1523
+ #isDateHighlighted(date) {
1524
+ return this.#highlightDateMap.has(this.#toDateStamp(date))
1525
+ }
1526
+
1527
+ #getHighlightMeta(date) {
1528
+ const colors = this.#highlightDateMap.get(this.#toDateStamp(date));
1529
+ if (!colors?.length) return null
1530
+ return { colors }
1531
+ }
1532
+
1451
1533
  #notifySelectionChange() {
1452
1534
  if (this.options.selectType === 'single') {
1453
1535
  this.options.selectDate(this.#selectedDates[0] || null);
@@ -1523,6 +1605,69 @@ var RollDate = (function () {
1523
1605
  }
1524
1606
  }
1525
1607
 
1608
+ #mountRangePresets() {
1609
+ const presets = Array.isArray(this.options.rangePresets)
1610
+ ? this.options.rangePresets
1611
+ : [];
1612
+
1613
+ if (this.options.selectType !== 'range' || !presets.length) return
1614
+
1615
+ let bar = this.$container.querySelector('.RollDate__presets');
1616
+ if (!bar) {
1617
+ bar = document.createElement('div');
1618
+ bar.className = 'RollDate__presets';
1619
+ const footer = this.$container.querySelector('.RollDate__footer');
1620
+ const calendar = this.$container.querySelector('.RollDate__calendar');
1621
+ if (footer) footer.before(bar);
1622
+ else if (calendar) calendar.after(bar);
1623
+ }
1624
+
1625
+ bar.innerHTML = '';
1626
+ presets.forEach(preset => {
1627
+ if (!preset?.label || typeof preset.getRange !== 'function') return
1628
+
1629
+ const btn = document.createElement('button');
1630
+ btn.type = 'button';
1631
+ btn.className = 'RollDate__presets__button';
1632
+ btn.textContent = preset.label;
1633
+ btn.addEventListener('click', (e) => {
1634
+ e.preventDefault();
1635
+ e.stopPropagation();
1636
+ this.#applyRangePreset(preset);
1637
+ });
1638
+ bar.append(btn);
1639
+ });
1640
+
1641
+ if (bar.childElementCount) {
1642
+ this.$container.classList.add('RollDate__has-presets');
1643
+ }
1644
+ }
1645
+
1646
+ #applyRangePreset(preset) {
1647
+ const result = preset.getRange(this);
1648
+ if (!Array.isArray(result) || result.length < 2) return
1649
+
1650
+ const normalized = result
1651
+ .slice(0, 2)
1652
+ .map(dateLike => this.#normalizeDateInput(dateLike))
1653
+ .filter(Boolean)
1654
+ .map(date => this.#clampDateToRange(date, this.options.minDate, this.options.maxDate))
1655
+ .filter(date => !this.#isDateDisabled(date));
1656
+
1657
+ if (normalized.length < 2) return
1658
+
1659
+ let [start, end] = normalized;
1660
+ if (start > end) [start, end] = [end, start];
1661
+
1662
+ this.#selectedDates = [
1663
+ this.#applyTimeToDate(start),
1664
+ this.#applyTimeToDate(end)
1665
+ ];
1666
+ this.#notifySelectionChange();
1667
+ this.#updateInputValue();
1668
+ this.#paintRangeSelection();
1669
+ }
1670
+
1526
1671
  #initTimePicker() {
1527
1672
  const start = this.options.startDate;
1528
1673
  this.timePicker = new TimePicker(this.dom.$time, {
@@ -1579,11 +1724,13 @@ var RollDate = (function () {
1579
1724
  onViewChange: () => {},
1580
1725
  onHoverDate: () => {},
1581
1726
  disabledDates: [],
1727
+ highlightDates: [],
1582
1728
  closeOnSelect: true,
1583
1729
  enableTime: false,
1584
1730
  use12Hour: false,
1585
1731
  timeStep: 1,
1586
1732
  footerButtons: [],
1733
+ rangePresets: [],
1587
1734
  hapticFeedback: true,
1588
1735
  ...options
1589
1736
  };
@@ -1626,6 +1773,7 @@ var RollDate = (function () {
1626
1773
  this.options.maxDate
1627
1774
  );
1628
1775
  this.#disabledDateStamps = this.#buildDisabledDateSet(this.options.disabledDates);
1776
+ this.#highlightDateMap = this.#buildHighlightDateMap(this.options.highlightDates);
1629
1777
  if (this.#isDateDisabled(this.options.startDate)) {
1630
1778
  this.options.startDate = new Date(this.options.minDate);
1631
1779
  }
@@ -1698,6 +1846,7 @@ var RollDate = (function () {
1698
1846
  };
1699
1847
 
1700
1848
  this.#initFooterButtons();
1849
+ this.#mountRangePresets();
1701
1850
  if (this.options.enableTime && this.dom.$time) {
1702
1851
  this.#initTimePicker();
1703
1852
  }
@@ -1758,7 +1907,23 @@ var RollDate = (function () {
1758
1907
  this.$container.style.zIndex = '10000';
1759
1908
  }
1760
1909
 
1761
- #updateView() {
1910
+ #updateView(viewNumberOrOptions, maybeOptions) {
1911
+ let options = {};
1912
+
1913
+ if (typeof viewNumberOrOptions === 'object' && viewNumberOrOptions !== null) {
1914
+ options = viewNumberOrOptions;
1915
+ } else {
1916
+ if (typeof viewNumberOrOptions === 'number') {
1917
+ this.#viewNumber = viewNumberOrOptions;
1918
+ }
1919
+ if (typeof maybeOptions === 'object' && maybeOptions !== null) {
1920
+ options = maybeOptions;
1921
+ }
1922
+ }
1923
+
1924
+ const preserveScroll = options.preserveScroll === true;
1925
+ const savedOffset = preserveScroll ? this.scroll.offset : null;
1926
+
1762
1927
  this.#updateHeader();
1763
1928
  this.scroll.blocked = false;
1764
1929
  this.observe.un(this.period);
@@ -1770,7 +1935,8 @@ var RollDate = (function () {
1770
1935
  this.dom.$days_block.innerHTML = this.render.dates(
1771
1936
  days,
1772
1937
  this.#selectedDates,
1773
- this.options.selectType
1938
+ this.options.selectType,
1939
+ date => this.#getHighlightMeta(date)
1774
1940
  );
1775
1941
  break
1776
1942
 
@@ -1779,41 +1945,49 @@ var RollDate = (function () {
1779
1945
  this.dom[`$${this.period}s_block`].innerHTML = this.render[`${this.period}s`](dates);
1780
1946
  }
1781
1947
 
1782
- const selectors = {
1783
- day: `.RollDate__calendar__day[data-year="${this.data.current_year}"][data-month="${this.data.current_month}"]`,
1784
- month: `.RollDate__calendar__month[data-year="${this.data.current_year}"][data-month="0"]`,
1785
- year: `.RollDate__calendar__year[data-decade="${this.data.current_decade}"][data-year="${this.data.current_year}"]`
1786
- };
1787
- let $firstEl = this.$container.querySelector(selectors[this.period]);
1788
- if (!$firstEl) {
1789
- const fallbacks = {
1790
- day: `.RollDate__calendar__day[data-year="${this.data.current_year}"]`,
1791
- month: `.RollDate__calendar__month[data-year="${this.data.current_year}"]`,
1792
- year: `.RollDate__calendar__year[data-decade="${this.data.current_decade}"]`
1948
+ const applyScroll = () => {
1949
+ const $scrollBlock = this.dom.$body.querySelector('.RollDate__calendar__scrollblock');
1950
+ const bodyHeight = this.dom.$body.clientHeight;
1951
+ const blockHeight = $scrollBlock.clientHeight;
1952
+
1953
+ if (blockHeight <= bodyHeight) {
1954
+ this.scroll.setBaseOffset(bodyHeight - blockHeight);
1955
+ this.scroll.offset = 0;
1956
+ return
1957
+ }
1958
+
1959
+ this.scroll.setBaseOffset(0);
1960
+ this.scroll.resetMinScroll();
1961
+
1962
+ if (preserveScroll && savedOffset != null) {
1963
+ this.scroll.offset = Math.max(savedOffset, this.scroll.minScroll);
1964
+ return
1965
+ }
1966
+
1967
+ const selectors = {
1968
+ day: `.RollDate__calendar__day[data-year="${this.data.current_year}"][data-month="${this.data.current_month}"]`,
1969
+ month: `.RollDate__calendar__month[data-year="${this.data.current_year}"][data-month="0"]`,
1970
+ year: `.RollDate__calendar__year[data-decade="${this.data.current_decade}"][data-year="${this.data.current_year}"]`
1793
1971
  };
1794
- $firstEl = this.$container.querySelector(fallbacks[this.period]);
1795
- }
1972
+ let $firstEl = this.$container.querySelector(selectors[this.period]);
1973
+ if (!$firstEl) {
1974
+ const fallbacks = {
1975
+ day: `.RollDate__calendar__day[data-year="${this.data.current_year}"]`,
1976
+ month: `.RollDate__calendar__month[data-year="${this.data.current_year}"]`,
1977
+ year: `.RollDate__calendar__year[data-decade="${this.data.current_decade}"]`
1978
+ };
1979
+ $firstEl = this.$container.querySelector(fallbacks[this.period]);
1980
+ }
1796
1981
 
1797
- if ($firstEl) {
1798
- setTimeout(() => {
1799
- const $scrollBlock = this.dom.$body.querySelector('.RollDate__calendar__scrollblock');
1800
- const bodyHeight = this.dom.$body.clientHeight;
1801
- const blockHeight = $scrollBlock.clientHeight;
1982
+ if (!$firstEl) return
1802
1983
 
1803
- if (blockHeight <= bodyHeight) {
1804
- this.scroll.setBaseOffset(bodyHeight - blockHeight);
1805
- this.scroll.offset = 0;
1806
- return
1807
- }
1984
+ const containerRect = $scrollBlock.getBoundingClientRect();
1985
+ const firstRect = $firstEl.getBoundingClientRect();
1986
+ const firstRectOffset = -(firstRect.top - containerRect.top);
1987
+ this.scroll.offset = Math.max(firstRectOffset, this.scroll.minScroll);
1988
+ };
1808
1989
 
1809
- this.scroll.setBaseOffset(0);
1810
- const containerRect = $scrollBlock.getBoundingClientRect();
1811
- const firstRect = $firstEl.getBoundingClientRect();
1812
- const firstRectOffset = -(firstRect.top - containerRect.top);
1813
- const minScroll = this.scroll.minScroll;
1814
- this.scroll.offset = Math.max(firstRectOffset, minScroll);
1815
- }, 0);
1816
- }
1990
+ setTimeout(applyScroll, 0);
1817
1991
 
1818
1992
  this.observe.on(this.period, item => {
1819
1993
  const cond = JSON.parse(item.dataset.bind).every(data => Number(item.dataset[data]) === this.data[`current_${data}`]);
@@ -1967,9 +2141,10 @@ var RollDate = (function () {
1967
2141
  }
1968
2142
 
1969
2143
  if (this.options.selectType === 'range') {
2144
+ const clickStamp = this.#toDateStamp(date);
1970
2145
  const isClickInRange = this.#selectedDates.length === 2 &&
1971
- date.getTime() >= this.#selectedDates[0].getTime() &&
1972
- date.getTime() <= this.#selectedDates[1].getTime();
2146
+ clickStamp >= this.#toDateStamp(this.#selectedDates[0]) &&
2147
+ clickStamp <= this.#toDateStamp(this.#selectedDates[1]);
1973
2148
 
1974
2149
  if (this.#selectedDates.length === 2 && !isClickInRange) {
1975
2150
  this.#clearRangeSelection();
@@ -1980,7 +2155,8 @@ var RollDate = (function () {
1980
2155
  $day.classList.add('RollDate__calendar__day--range-first');
1981
2156
  } else {
1982
2157
  const firstDate = this.#selectedDates[0];
1983
- if (date.getTime() > firstDate.getTime()) {
2158
+ const firstStamp = this.#toDateStamp(firstDate);
2159
+ if (clickStamp > firstStamp) {
1984
2160
  this.#selectedDates = [firstDate, date];
1985
2161
 
1986
2162
  this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
@@ -1989,17 +2165,17 @@ var RollDate = (function () {
1989
2165
  Number(dayEl.dataset.month),
1990
2166
  Number(dayEl.dataset.day)
1991
2167
  );
1992
- const time = dayDate.getTime();
2168
+ const stamp = this.#toDateStamp(dayDate);
1993
2169
 
1994
- if (time === firstDate.getTime()) {
2170
+ if (stamp === firstStamp) {
1995
2171
  dayEl.classList.add('RollDate__calendar__day--range-first');
1996
- } else if (time === date.getTime()) {
2172
+ } else if (stamp === clickStamp) {
1997
2173
  dayEl.classList.add('RollDate__calendar__day--range-last');
1998
- } else if (time > firstDate.getTime() && time < date.getTime()) {
2174
+ } else if (stamp > firstStamp && stamp < clickStamp) {
1999
2175
  dayEl.classList.add('RollDate__calendar__day--range-selected');
2000
2176
  }
2001
2177
  });
2002
- } else if (date.getTime() < firstDate.getTime()) {
2178
+ } else if (clickStamp < firstStamp) {
2003
2179
  this.#selectedDates = [date];
2004
2180
  $day.classList.add('RollDate__calendar__day--range-first');
2005
2181
 
@@ -2017,12 +2193,13 @@ var RollDate = (function () {
2017
2193
  }
2018
2194
 
2019
2195
  if (this.options.selectType === 'multi') {
2196
+ const clickStamp = this.#toDateStamp(date);
2020
2197
  const isSelected = this.#selectedDates.some(
2021
- d => d.getTime() === date.getTime()
2198
+ d => this.#toDateStamp(d) === clickStamp
2022
2199
  );
2023
2200
  if (isSelected) {
2024
2201
  this.#selectedDates = this.#selectedDates.filter(
2025
- d => d.getTime() !== date.getTime()
2202
+ d => this.#toDateStamp(d) !== clickStamp
2026
2203
  );
2027
2204
  $day.classList.remove('RollDate__calendar__day--selected');
2028
2205
  } else {
@@ -2172,6 +2349,20 @@ var RollDate = (function () {
2172
2349
  return this.#selectedDates
2173
2350
  }
2174
2351
 
2352
+ /** Visible calendar month (updates while scrolling). Month is 0–11. */
2353
+ getViewMonth() {
2354
+ return {
2355
+ year: this.data.current_year,
2356
+ month: this.data.current_month
2357
+ }
2358
+ }
2359
+
2360
+ /** First day of the month currently shown in the calendar. */
2361
+ getViewDate() {
2362
+ const { year, month } = this.getViewMonth();
2363
+ return new Date(year, month, 1)
2364
+ }
2365
+
2175
2366
  setDisabledDates(dates = []) {
2176
2367
  this.options.disabledDates = Array.isArray(dates) ? dates : [];
2177
2368
  this.#disabledDateStamps = this.#buildDisabledDateSet(this.options.disabledDates);
@@ -2201,6 +2392,251 @@ var RollDate = (function () {
2201
2392
  return normalized ? this.#isDateDisabled(normalized) : false
2202
2393
  }
2203
2394
 
2395
+ setHighlightDates(dates = []) {
2396
+ this.options.highlightDates = Array.isArray(dates) ? dates : [];
2397
+ this.#highlightDateMap = this.#buildHighlightDateMap(this.options.highlightDates);
2398
+ this.#updateView(this.#viewNumber);
2399
+ }
2400
+
2401
+ highlightDate(dateLike, color) {
2402
+ const normalized = this.#normalizeDateInput(dateLike);
2403
+ if (!normalized) return
2404
+
2405
+ const stamp = this.#toDateStamp(normalized);
2406
+ const existing = this.#highlightDateMap.get(stamp) || [];
2407
+ const nextColor = color == null || color === ''
2408
+ ? null
2409
+ : this.#sanitizeHighlightColor(color);
2410
+
2411
+ if (color != null && color !== '' && nextColor === undefined) return
2412
+
2413
+ existing.push(nextColor ?? null);
2414
+ this.#highlightDateMap.set(stamp, existing);
2415
+ this.options.highlightDates = this.#highlightMapToOptionsArray();
2416
+ this.#updateView(this.#viewNumber);
2417
+ }
2418
+
2419
+ unhighlightDate(dateLike, color) {
2420
+ const normalized = this.#normalizeDateInput(dateLike);
2421
+ if (!normalized) return
2422
+
2423
+ const stamp = this.#toDateStamp(normalized);
2424
+ const existing = this.#highlightDateMap.get(stamp);
2425
+ if (!existing?.length) return
2426
+
2427
+ if (color === undefined) {
2428
+ this.#highlightDateMap.delete(stamp);
2429
+ } else {
2430
+ const target = color == null || color === ''
2431
+ ? null
2432
+ : this.#sanitizeHighlightColor(color);
2433
+
2434
+ const index = existing.findIndex(item => item === target);
2435
+ if (index === -1) return
2436
+
2437
+ existing.splice(index, 1);
2438
+ if (existing.length) {
2439
+ this.#highlightDateMap.set(stamp, existing);
2440
+ } else {
2441
+ this.#highlightDateMap.delete(stamp);
2442
+ }
2443
+ }
2444
+
2445
+ this.options.highlightDates = this.#highlightMapToOptionsArray();
2446
+ this.#updateView(this.#viewNumber);
2447
+ }
2448
+
2449
+ isDateHighlighted(dateLike) {
2450
+ const normalized = this.#normalizeDateInput(dateLike);
2451
+ return normalized ? this.#isDateHighlighted(normalized) : false
2452
+ }
2453
+
2454
+ getHighlightColors(dateLike) {
2455
+ const normalized = this.#normalizeDateInput(dateLike);
2456
+ if (!normalized) return []
2457
+ return [...(this.#highlightDateMap.get(this.#toDateStamp(normalized)) || [])]
2458
+ }
2459
+
2460
+ getHighlightColor(dateLike) {
2461
+ return this.getHighlightColors(dateLike).find(color => color != null) ?? null
2462
+ }
2463
+
2464
+ goToDate(dateLike) {
2465
+ const normalized = this.#normalizeDateInput(dateLike);
2466
+ if (!normalized) return false
2467
+
2468
+ const clamped = this.#clampDateToRange(
2469
+ normalized,
2470
+ this.options.minDate,
2471
+ this.options.maxDate
2472
+ );
2473
+
2474
+ this.data.current_year = clamped.getFullYear();
2475
+ this.data.current_month = clamped.getMonth();
2476
+ this.data.current_decade = getDecade(clamped.getFullYear());
2477
+
2478
+ if (this.#viewNumber !== 0) {
2479
+ this.#viewNumber = 0;
2480
+ for (const period of this.#viewPeriodNames) {
2481
+ this.$container.classList.remove('RollDate__calendar__type--' + period + 's');
2482
+ }
2483
+ this.$container.classList.add('RollDate__calendar__type--days');
2484
+ }
2485
+
2486
+ this.#updateView(0);
2487
+ return true
2488
+ }
2489
+
2490
+ getValue() {
2491
+ if (this.options.selectType === 'single') {
2492
+ const date = this.#selectedDates[0];
2493
+ return date ? new Date(date.getTime()) : null
2494
+ }
2495
+
2496
+ return this.#selectedDates.map(date => new Date(date.getTime()))
2497
+ }
2498
+
2499
+ setValue(value) {
2500
+ if (value == null || value === '') {
2501
+ this.clearSelection();
2502
+ return true
2503
+ }
2504
+
2505
+ if (this.options.selectType === 'single') {
2506
+ const normalized = this.#normalizeDateInput(value);
2507
+ if (!normalized || this.#isDateDisabled(normalized)) return false
2508
+ this.#selectedDates = [this.#applyTimeToDate(normalized)];
2509
+ } else if (this.options.selectType === 'range') {
2510
+ const raw = Array.isArray(value) ? value : [value];
2511
+ const parsed = raw
2512
+ .map(item => this.#normalizeDateInput(item))
2513
+ .filter(Boolean)
2514
+ .map(date => this.#clampDateToRange(date, this.options.minDate, this.options.maxDate))
2515
+ .filter(date => !this.#isDateDisabled(date));
2516
+
2517
+ if (!parsed.length) return false
2518
+
2519
+ let start = parsed[0];
2520
+ let end = parsed.length > 1 ? parsed[1] : parsed[0];
2521
+ if (start > end) [start, end] = [end, start];
2522
+
2523
+ this.#selectedDates = parsed.length > 1
2524
+ ? [this.#applyTimeToDate(start), this.#applyTimeToDate(end)]
2525
+ : [this.#applyTimeToDate(start)];
2526
+ } else {
2527
+ const raw = Array.isArray(value) ? value : [value];
2528
+ const parsed = raw
2529
+ .map(item => this.#normalizeDateInput(item))
2530
+ .filter(Boolean)
2531
+ .map(date => this.#clampDateToRange(date, this.options.minDate, this.options.maxDate))
2532
+ .filter(date => !this.#isDateDisabled(date))
2533
+ .map(date => this.#applyTimeToDate(date));
2534
+
2535
+ if (!parsed.length) return false
2536
+
2537
+ const unique = [];
2538
+ const seen = new Set();
2539
+ parsed.forEach(date => {
2540
+ const stamp = this.#toDateStamp(date);
2541
+ if (seen.has(stamp)) return
2542
+ seen.add(stamp);
2543
+ unique.push(date);
2544
+ });
2545
+ this.#selectedDates = unique;
2546
+ }
2547
+
2548
+ this.#paintSelection();
2549
+ this.#notifySelectionChange();
2550
+ this.#updateInputValue();
2551
+ return true
2552
+ }
2553
+
2554
+ #dayElementStamp(dayEl) {
2555
+ return this.#toDateStamp(new Date(
2556
+ Number(dayEl.dataset.year),
2557
+ Number(dayEl.dataset.month),
2558
+ Number(dayEl.dataset.day)
2559
+ ))
2560
+ }
2561
+
2562
+ #findDayElement(date) {
2563
+ if (!(date instanceof Date)) return null
2564
+ return this.$container.querySelector(
2565
+ `.RollDate__calendar__day[data-year="${date.getFullYear()}"][data-month="${date.getMonth()}"][data-day="${date.getDate()}"]`
2566
+ )
2567
+ }
2568
+
2569
+ #paintRangeSelection() {
2570
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
2571
+ dayEl.classList.remove(
2572
+ 'RollDate__calendar__day--range-first',
2573
+ 'RollDate__calendar__day--range-last',
2574
+ 'RollDate__calendar__day--range-selected'
2575
+ );
2576
+ });
2577
+
2578
+ if (!this.#selectedDates.length) return
2579
+
2580
+ const firstStamp = this.#toDateStamp(this.#selectedDates[0]);
2581
+
2582
+ if (this.#selectedDates.length === 1) {
2583
+ this.#findDayElement(this.#selectedDates[0])
2584
+ ?.classList.add('RollDate__calendar__day--range-first');
2585
+ return
2586
+ }
2587
+
2588
+ const endStamp = this.#toDateStamp(this.#selectedDates[1]);
2589
+
2590
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
2591
+ const stamp = this.#dayElementStamp(dayEl);
2592
+
2593
+ if (stamp === firstStamp) {
2594
+ dayEl.classList.add('RollDate__calendar__day--range-first');
2595
+ } else if (stamp === endStamp) {
2596
+ dayEl.classList.add('RollDate__calendar__day--range-last');
2597
+ } else if (stamp > firstStamp && stamp < endStamp) {
2598
+ dayEl.classList.add('RollDate__calendar__day--range-selected');
2599
+ }
2600
+ });
2601
+ }
2602
+
2603
+ #paintSingleSelection() {
2604
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
2605
+ dayEl.classList.remove('RollDate__calendar__day--selected');
2606
+ });
2607
+
2608
+ const selected = this.#selectedDates[0];
2609
+ if (selected) {
2610
+ this.#findDayElement(selected)?.classList.add('RollDate__calendar__day--selected');
2611
+ }
2612
+ }
2613
+
2614
+ #paintMultiSelection() {
2615
+ const selectedStamps = new Set(this.#selectedDates.map(date => this.#toDateStamp(date)));
2616
+
2617
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
2618
+ if (selectedStamps.has(this.#dayElementStamp(dayEl))) {
2619
+ dayEl.classList.add('RollDate__calendar__day--selected');
2620
+ } else {
2621
+ dayEl.classList.remove('RollDate__calendar__day--selected');
2622
+ }
2623
+ });
2624
+ }
2625
+
2626
+ #paintSelection() {
2627
+ if (this.options.selectType === 'range') {
2628
+ this.#paintRangeSelection();
2629
+ return
2630
+ }
2631
+
2632
+ if (this.options.selectType === 'multi') {
2633
+ this.#paintMultiSelection();
2634
+ return
2635
+ }
2636
+
2637
+ this.#paintSingleSelection();
2638
+ }
2639
+
2204
2640
  #clearRangeSelection() {
2205
2641
  this.#selectedDates = [];
2206
2642
  this.$container.querySelectorAll('[data-day]').forEach(item => {
@@ -2378,7 +2814,8 @@ var RollDate = (function () {
2378
2814
  } else if (this.options.selectType === 'range') {
2379
2815
  this.#selectedDates = [selected];
2380
2816
  } else if (this.options.selectType === 'multi') {
2381
- const exists = this.#selectedDates.some(d => d.getTime() === selected.getTime());
2817
+ const selectedStamp = this.#toDateStamp(selected);
2818
+ const exists = this.#selectedDates.some(d => this.#toDateStamp(d) === selectedStamp);
2382
2819
  if (!exists) this.#selectedDates.push(selected);
2383
2820
  }
2384
2821
 
@@ -2398,9 +2835,10 @@ var RollDate = (function () {
2398
2835
  this.#clearMultiSelection();
2399
2836
  } else {
2400
2837
  this.#selectedDates = [];
2838
+ this.$container.querySelectorAll('[data-day].RollDate__calendar__day--selected')
2839
+ .forEach(el => el.classList.remove('RollDate__calendar__day--selected'));
2401
2840
  }
2402
2841
 
2403
- this.#updateView(this.#viewNumber);
2404
2842
  this.#notifySelectionChange();
2405
2843
  this.#updateInputValue();
2406
2844
  }