@rolldate/mcp 1.0.0 → 1.3.1

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,50 +450,59 @@ 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
- this.hasFooter = Boolean(options.enableTime || (options.footerButtons && options.footerButtons.length));
457
+ this.hasFooter = Boolean(options.footerButtons && options.footerButtons.length);
458
458
 
459
459
  this.init();
460
460
  }
461
461
 
462
462
  init() {
463
+ const timePanelHtml = this.enableTime
464
+ ? '<div class="RollDate__time-separator" aria-hidden="true"></div><div class="RollDate__time-panel"><div class="RollDate__time"></div></div>'
465
+ : '';
466
+
463
467
  const footerHtml = this.hasFooter ? `
464
468
  <div class="RollDate__footer">
465
- ${this.enableTime ? '<div class="RollDate__time"></div>' : ''}
466
469
  <div class="RollDate__footer__buttons"></div>
467
470
  </div>
468
471
  ` : '';
469
472
 
470
473
  this.$container.innerHTML = `
471
- <div class="RollDate__header">
472
- <div class="RollDate__calendar__switcher">
473
- <div class="RollDate__header__year"></div>
474
- <div class="RollDate__header__month"></div>
474
+ <div class="RollDate__content">
475
+ <div class="RollDate__calendar">
476
+ <div class="RollDate__header">
477
+ <div class="RollDate__calendar__switcher">
478
+ <div class="RollDate__header__year"></div>
479
+ <div class="RollDate__header__month"></div>
480
+ </div>
481
+ <div class="RollDate__calendar__buttons">
482
+ <button class="RollDate__calendar__button" data-direction="prev">
483
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 55" width="55" height="50">
484
+ <path id="Форма 2" fill-rule="evenodd" class="s0" d="m52.76 54.67l-44.73 0.12c-6.16 0.02-10.02-6.64-6.96-11.98l22.26-38.79c3.07-5.35 10.76-5.37 13.86-0.04l22.47 38.67c3.09 5.33-0.74 12.01-6.9 12.02z"/>
485
+ </svg>
486
+ </button>
487
+ <button class="RollDate__calendar__button" data-direction="next">
488
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 55" width="55" height="50">
489
+ <path id="Форма 2" fill-rule="evenodd" class="s0" d="m52.76 54.67l-44.73 0.12c-6.16 0.02-10.02-6.64-6.96-11.98l22.26-38.79c3.07-5.35 10.76-5.37 13.86-0.04l22.47 38.67c3.09 5.33-0.74 12.01-6.9 12.02z" />
490
+ </svg>
491
+ </button>
492
+ </div>
475
493
  </div>
476
- <div class="RollDate__calendar__buttons">
477
- <button class="RollDate__calendar__button" data-direction="prev">
478
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 55" width="55" height="50">
479
- <path id="Форма 2" fill-rule="evenodd" class="s0" d="m52.76 54.67l-44.73 0.12c-6.16 0.02-10.02-6.64-6.96-11.98l22.26-38.79c3.07-5.35 10.76-5.37 13.86-0.04l22.47 38.67c3.09 5.33-0.74 12.01-6.9 12.02z"/>
480
- </svg>
481
- </button>
482
- <button class="RollDate__calendar__button" data-direction="next">
483
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 55" width="55" height="50">
484
- <path id="Форма 2" fill-rule="evenodd" class="s0" d="m52.76 54.67l-44.73 0.12c-6.16 0.02-10.02-6.64-6.96-11.98l22.26-38.79c3.07-5.35 10.76-5.37 13.86-0.04l22.47 38.67c3.09 5.33-0.74 12.01-6.9 12.02z" />
485
- </svg>
486
- </button>
494
+ <div class="RollDate__calendar__header">
495
+ ${this.#dayHeaders()}
487
496
  </div>
488
- </div>
489
- <div class="RollDate__calendar">
490
- <div class="RollDate__calendar__header">
491
- ${this.#dayHeaders()}
492
- </div>
493
- <div class="RollDate__calendar__body">
494
- <div class="RollDate__calendar__scrollblock">
495
- <div class="RollDate__calendar__days"></div>
496
- <div class="RollDate__calendar__months"></div>
497
- <div class="RollDate__calendar__years"></div>
497
+ <div class="RollDate__calendar__body">
498
+ <div class="RollDate__calendar__scrollblock">
499
+ <div class="RollDate__calendar__days"></div>
500
+ <div class="RollDate__calendar__months"></div>
501
+ <div class="RollDate__calendar__years"></div>
502
+ </div>
498
503
  </div>
499
504
  </div>
505
+ ${timePanelHtml}
500
506
  </div>
501
507
  ${footerHtml}
502
508
  `;
@@ -509,48 +515,77 @@ var RollDate = (function () {
509
515
  }
510
516
 
511
517
  #dayHeaders() {
512
- const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
518
+ const weekDays = this.weekDays;
513
519
  let header = '';
514
520
  const startIndex = Number(this.startWeekFromMonday);
515
521
  for (let i = startIndex; i < 7 + startIndex; i++) {
516
- header += `<div class="RollDate__calendar__header__weekday">${getTranslation(weekDays[i % 7])}</div>`;
522
+ header += `<div class="RollDate__calendar__header__weekday">${weekDays[i % 7]}</div>`;
517
523
  }
518
524
  return header
519
525
  }
520
526
 
521
- dates(array, selectedDates = [], selectType = 'single') {
527
+ #highlightDots(colors = [], maxVisible = 5) {
528
+ if (!colors.length) return ''
529
+
530
+ const visible = colors.slice(0, maxVisible);
531
+ const extra = colors.length - maxVisible;
532
+ const many = visible.length > 3;
533
+ const dotClass = many
534
+ ? 'RollDate__calendar__day-dot RollDate__calendar__day-dot--compact'
535
+ : 'RollDate__calendar__day-dot';
536
+
537
+ const dots = visible.map(color => {
538
+ if (color) {
539
+ return `<span class="${dotClass} RollDate__calendar__day-dot--custom" style="--rd-highlight-dot:${color}"></span>`
540
+ }
541
+ return `<span class="${dotClass}"></span>`
542
+ }).join('');
543
+
544
+ const more = extra > 0
545
+ ? `<span class="RollDate__calendar__day-dot RollDate__calendar__day-dot--more">+${extra}</span>`
546
+ : '';
547
+
548
+ return `<span class="RollDate__calendar__day-dots" aria-hidden="true">${dots}${more}</span>`
549
+ }
550
+
551
+ dates(array, selectedDates = [], selectType = 'single', getHighlight = () => null) {
522
552
  let datesHtml = '';
523
553
  const today = new Date();
524
554
 
525
555
  for (let i = 0; i < array.length; i++) {
526
556
  const date = array[i].date;
527
557
  const disabledClass = array[i].disabled ? 'RollDate__calendar__day--disabled' : '';
558
+ const highlight = getHighlight(date);
559
+ const highlightDots = highlight ? this.#highlightDots(highlight.colors) : '';
528
560
  const isToday = date.toDateString() === today.toDateString();
529
561
 
530
562
  // Resolve selection class for current date cell.
531
563
  let selectionClass = '';
532
564
  if (selectType === 'single' || selectType === 'multi') {
565
+ const dayStamp = date.getTime();
533
566
  const isSelected = selectedDates.some(
534
- d => d.getTime() === date.getTime()
567
+ d => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() === dayStamp
535
568
  );
536
569
  if (isSelected) selectionClass = 'RollDate__calendar__day--selected';
537
570
  } else if (selectType === 'range' && selectedDates.length === 2) {
538
571
  const [start, end] = selectedDates;
539
572
  const time = date.getTime();
540
- if (time === start.getTime()) {
573
+ const startStamp = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime();
574
+ const endStamp = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime();
575
+ if (time === startStamp) {
541
576
  selectionClass = 'RollDate__calendar__day--range-first';
542
- } else if (time === end.getTime()) {
577
+ } else if (time === endStamp) {
543
578
  selectionClass = 'RollDate__calendar__day--range-last';
544
- } else if (time > start.getTime() && time < end.getTime()) {
579
+ } else if (time > startStamp && time < endStamp) {
545
580
  selectionClass = 'RollDate__calendar__day--range-selected';
546
581
  }
547
582
  }
548
583
 
549
- datesHtml += `<div class="RollDate__calendar__day ${disabledClass} ${isToday ? 'RollDate__calendar__day--today' : ''} ${selectionClass}"
584
+ datesHtml += `<div class="RollDate__calendar__day ${disabledClass} ${isToday ? 'RollDate__calendar__day--today' : ''} ${selectionClass}"
550
585
  data-bind='["year", "month"]'
551
586
  data-year="${date.getFullYear()}"
552
587
  data-month="${date.getMonth()}"
553
- data-day="${date.getDate()}">${date.getDate()}</div>`;
588
+ data-day="${date.getDate()}">${date.getDate()}${highlightDots}</div>`;
554
589
  }
555
590
 
556
591
  return datesHtml
@@ -610,502 +645,531 @@ var RollDate = (function () {
610
645
  }
611
646
  }
612
647
 
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
- }
648
+ class Scroll {
649
+ #minScroll = null
650
+ #EDGE_TOP = 25
651
+ #EDGE_BOTTOM = 75
652
+ #offset
653
+ #baseOffset = 0
654
+ #blocked
655
+ #boundWheelHandler
656
+ #boundTouchStartHandler
657
+ #boundTouchMoveHandler
658
+ #boundTouchEndHandler
659
+ #isWheelAnimating = false
660
+ #edgeTriggeredInCurrentWheel = false
661
+ #lastEdgeTriggerAt = 0
662
+ #isTouchDragging = false
663
+ #touchLastY = 0
664
+ #touchLastTime = 0
665
+ #touchVelocity = 0
666
+ #momentumId = 0
667
+ #animId = 0
668
+
669
+ constructor(body, methods = {
670
+ dominant: () => console.error('Function "dominant" is not found'),
671
+ updatePeriod: () => console.error('Function "updatePeriod" is not found')
672
+ }) {
673
+
674
+ this.$body = body;
675
+ this.$scroll_block = this.$body.querySelector('.RollDate__calendar__scrollblock');
676
+ this.dominant = methods.dominant;
677
+ this.updatePeriod = methods.updatePeriod;
678
+ this.offset = 0;
679
+ this.init();
680
+ }
681
+
682
+ init() {
683
+ this.#boundWheelHandler = this.wheelHandler.bind(this);
684
+ this.$body.addEventListener('wheel', this.#boundWheelHandler, { passive: false });
685
+ this.#boundTouchStartHandler = this.touchStartHandler.bind(this);
686
+ this.#boundTouchMoveHandler = this.touchMoveHandler.bind(this);
687
+ this.#boundTouchEndHandler = this.touchEndHandler.bind(this);
688
+ this.$body.addEventListener('touchstart', this.#boundTouchStartHandler, { passive: true });
689
+ this.$body.addEventListener('touchmove', this.#boundTouchMoveHandler, { passive: false });
690
+ this.$body.addEventListener('touchend', this.#boundTouchEndHandler, { passive: true });
691
+ this.$body.addEventListener('touchcancel', this.#boundTouchEndHandler, { passive: true });
692
+ }
693
+
694
+ wheelHandler(e) {
695
+ e.preventDefault();
696
+ e.stopPropagation();
697
+ this.#cancelMomentum();
698
+
699
+ const rawDelta = e.deltaY * 0.85;
700
+ const steps = Math.abs(rawDelta) > 50 ? 20 : 1;
701
+ const stepSize = rawDelta / steps;
702
+ const currentWheelDirection = rawDelta > 0 ? 'down' : 'up';
703
+ this.#isWheelAnimating = true;
704
+ this.#edgeTriggeredInCurrentWheel = false;
705
+
706
+ let i = 0;
707
+ const animate = () => {
708
+ if (i >= steps) {
709
+ this.#isWheelAnimating = false;
710
+ this.#edgeTriggeredInCurrentWheel = false;
711
+ return
712
+ }
713
+
714
+ this.offset -= stepSize;
715
+
716
+ requestAnimationFrame(() => {
717
+ this.dominant();
718
+ this.checkEdge(currentWheelDirection);
719
+ i++;
720
+ animate();
721
+ });
722
+ };
723
+
724
+ animate();
725
+ }
726
+
727
+ touchStartHandler(e) {
728
+ if (!e.touches || e.touches.length !== 1) return
729
+ this.#cancelMomentum();
730
+ this.#isTouchDragging = true;
731
+ this.#touchLastY = e.touches[0].clientY;
732
+ this.#touchLastTime = performance.now();
733
+ this.#touchVelocity = 0;
734
+ this.#edgeTriggeredInCurrentWheel = false;
735
+ }
736
+
737
+ touchMoveHandler(e) {
738
+ if (!this.#isTouchDragging || !e.touches || e.touches.length !== 1) return
739
+ e.preventDefault();
740
+
741
+ const currentY = e.touches[0].clientY;
742
+ const deltaY = currentY - this.#touchLastY;
743
+ const now = performance.now();
744
+ const dt = now - this.#touchLastTime;
745
+ if (dt > 0 && dt < 120) {
746
+ this.#touchVelocity = this.#touchVelocity * 0.65 + (deltaY / dt) * 0.35;
747
+ }
748
+ this.#touchLastY = currentY;
749
+ this.#touchLastTime = now;
750
+
751
+ if (Math.abs(deltaY) < 1) return
752
+
753
+ this.offset += deltaY;
754
+ this.dominant();
755
+
756
+ const direction = deltaY < 0 ? 'down' : 'up';
757
+ this.checkEdge(direction);
758
+ }
759
+
760
+ touchEndHandler() {
761
+ this.#isTouchDragging = false;
762
+ this.#edgeTriggeredInCurrentWheel = false;
763
+
764
+ const v = this.#touchVelocity * 16;
765
+ if (Math.abs(v) < 0.4) return
766
+
767
+ let velocity = v;
768
+ const step = () => {
769
+ if (Math.abs(velocity) < 0.35) {
770
+ this.#momentumId = 0;
771
+ return
772
+ }
773
+
774
+ const prevOffset = this.#offset;
775
+ this.offset += velocity;
776
+ if (this.#offset === prevOffset) {
777
+ this.#momentumId = 0;
778
+ return
779
+ }
780
+
781
+ this.dominant();
782
+ const direction = velocity < 0 ? 'down' : 'up';
783
+ this.checkEdge(direction);
784
+
785
+ velocity *= 0.92;
786
+ this.#momentumId = requestAnimationFrame(step);
787
+ };
788
+ this.#momentumId = requestAnimationFrame(step);
789
+ }
790
+
791
+ #cancelMomentum() {
792
+ if (!this.#momentumId) return
793
+ cancelAnimationFrame(this.#momentumId);
794
+ this.#momentumId = 0;
795
+ }
796
+
797
+ #cancelAnimate() {
798
+ if (!this.#animId) return
799
+ cancelAnimationFrame(this.#animId);
800
+ this.#animId = 0;
801
+ this.blocked = false;
802
+ }
803
+
804
+ animateTo(targetOffset, opts = {}) {
805
+ this.#cancelMomentum();
806
+ this.#cancelAnimate();
807
+
808
+ const duration = opts.duration ?? 220;
809
+ const start = this.#offset;
810
+ const min = this.minScroll;
811
+ const target = Math.max(Math.min(targetOffset, 0), min);
812
+
813
+ if (Math.abs(target - start) < 1) {
814
+ this.offset = target;
815
+ this.dominant();
816
+ opts.onComplete?.();
817
+ return Promise.resolve()
818
+ }
819
+
820
+ this.blocked = true;
821
+ const startTime = performance.now();
822
+
823
+ return new Promise(resolve => {
824
+ const step = (now) => {
825
+ const t = Math.min(1, (now - startTime) / duration);
826
+ const eased = 1 - Math.pow(1 - t, 3);
827
+ this.offset = start + (target - start) * eased;
828
+
829
+ if (t < 1) {
830
+ this.#animId = requestAnimationFrame(step);
831
+ return
832
+ }
833
+
834
+ this.offset = target;
835
+ this.blocked = false;
836
+ this.dominant();
837
+ this.#animId = 0;
838
+ opts.onComplete?.();
839
+ resolve();
840
+ };
841
+ this.#animId = requestAnimationFrame(step);
842
+ })
843
+ }
844
+
845
+ apply() {
846
+ this.$scroll_block.style.transform = `translateY(${this.offset + this.#baseOffset}px)`;
847
+ }
848
+
849
+ get offset() { return this.#offset }
850
+ set offset(value) {
851
+
852
+ this.#offset = value;
853
+
854
+ if ( this.#minScroll !== null ) {
855
+ if (value >= 0) this.#offset = 0;
856
+ else if (value <= this.#minScroll) this.#offset = this.#minScroll;
857
+ }
858
+ this.apply();
859
+ }
860
+
861
+ get blocked() { return this.#blocked }
862
+ set blocked(value) { this.#blocked = value; }
863
+ get minScroll() {
864
+ if (this.#minScroll === null) this.checkMinScroll();
865
+ return this.#minScroll ?? 0
866
+ }
867
+
868
+ checkEdge(direction) {
869
+ if (this.#minScroll === null) {
870
+ this.checkMinScroll();
871
+ }
872
+
873
+ if (this.#minScroll === 0 || this.blocked) return
874
+ if (this.#isWheelAnimating && this.#edgeTriggeredInCurrentWheel) return
875
+
876
+ const percent = Math.abs(this.offset / this.#minScroll) * 100;
877
+ const now = Date.now();
878
+ const EDGE_COOLDOWN_MS = 60;
879
+
880
+ if (now - this.#lastEdgeTriggerAt < EDGE_COOLDOWN_MS) return
881
+
882
+ if (percent < this.#EDGE_TOP) {
883
+ if (direction !== 'up') return
884
+ this.#lastEdgeTriggerAt = now;
885
+ this.#edgeTriggeredInCurrentWheel = true;
886
+ this.blocked = true;
887
+ this.updatePeriod('up');
888
+ } else if (percent > this.#EDGE_BOTTOM) {
889
+ if (direction !== 'down') return
890
+ this.#lastEdgeTriggerAt = now;
891
+ this.#edgeTriggeredInCurrentWheel = true;
892
+ this.blocked = true;
893
+ this.updatePeriod('down');
894
+ }
895
+ }
896
+
897
+ checkMinScroll() {
898
+ const scrollHeight = this.$scroll_block.clientHeight;
899
+ const bodyHeight = this.$body.clientHeight;
900
+ this.#minScroll = scrollHeight > bodyHeight ? -(scrollHeight - bodyHeight) : 0;
901
+ }
902
+
903
+ setBaseOffset(value = 0) {
904
+ this.#baseOffset = value;
905
+ this.apply();
906
+ }
907
+
908
+ resetMinScroll() {
909
+ this.#minScroll = null;
910
+ }
911
+
912
+ destroy() {
913
+ this.#cancelMomentum();
914
+ this.#cancelAnimate();
915
+ if (this.#boundWheelHandler) {
916
+ this.$body.removeEventListener('wheel', this.#boundWheelHandler);
917
+ }
918
+ if (this.#boundTouchStartHandler) {
919
+ this.$body.removeEventListener('touchstart', this.#boundTouchStartHandler);
920
+ }
921
+ if (this.#boundTouchMoveHandler) {
922
+ this.$body.removeEventListener('touchmove', this.#boundTouchMoveHandler);
923
+ }
924
+ if (this.#boundTouchEndHandler) {
925
+ this.$body.removeEventListener('touchend', this.#boundTouchEndHandler);
926
+ this.$body.removeEventListener('touchcancel', this.#boundTouchEndHandler);
927
+ }
928
+ }
855
929
  }
856
930
 
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
- }
931
+ class Virtualizer {
932
+ constructor(context) {
933
+ this.ctx = context;
934
+ }
935
+
936
+ update(direction) {
937
+ const period = this.ctx.period;
938
+ const isUp = direction === 'up';
939
+
940
+ // 🔑 Точна перевірка меж
941
+ if (isUp) {
942
+ if (period === 'year') {
943
+ if (this.ctx.data.up_date.getFullYear() <= this.ctx.options.minDate.getFullYear()) {
944
+ this.ctx.scroll.blocked = false;
945
+ return
946
+ }
947
+ } else {
948
+ if (this.ctx.data.up_date <= this.ctx.options.minDate) {
949
+ this.ctx.scroll.blocked = false;
950
+ return
951
+ }
952
+ }
953
+ } else {
954
+ if (period === 'year') {
955
+ if (this.ctx.data.down_date.getFullYear() >= this.ctx.options.maxDate.getFullYear()) {
956
+ this.ctx.scroll.blocked = false;
957
+ return
958
+ }
959
+ } else {
960
+ if (this.ctx.data.down_date >= this.ctx.options.maxDate) {
961
+ this.ctx.scroll.blocked = false;
962
+ return
963
+ }
964
+ }
965
+ }
966
+
967
+ const config = {
968
+ day: {
969
+ count: 35,
970
+ getNewItems: (upDate, downDate) => {
971
+ const newDates = [];
972
+ if (isUp) {
973
+ let newFirst = new Date(upDate);
974
+ newFirst.setDate(upDate.getDate() - 35);
975
+ if (newFirst < this.ctx.options.minDate) {
976
+ const minDate = this.ctx.options.minDate;
977
+ newFirst = adjustToWeekStart(
978
+ new Date(minDate.getFullYear(), minDate.getMonth(), 1),
979
+ this.ctx.options.startWeekFromMonday
980
+ );
981
+ }
982
+ for (let d = new Date(newFirst); d < upDate; d.setDate(d.getDate() + 1)) {
983
+ newDates.push({
984
+ date: new Date(d),
985
+ disabled: this.ctx?.options?.minDate > new Date(d) || false
986
+ });
987
+ }
988
+ return { items: newDates, newBound: newFirst }
989
+ } else {
990
+ let newLast = new Date(downDate);
991
+ newLast.setDate(downDate.getDate() + 35);
992
+ const firstNew = new Date(downDate);
993
+ firstNew.setDate(downDate.getDate() + 1);
994
+
995
+ if (newLast > this.ctx.options.maxDate) {
996
+ const maxDate = this.ctx.options.maxDate;
997
+ newLast = adjustToWeekEnd(
998
+ new Date(maxDate.getFullYear(), maxDate.getMonth() + 1, 0),
999
+ this.ctx.options.startWeekFromMonday
1000
+ );
1001
+ }
1002
+
1003
+ for (let d = firstNew; d <= newLast; d.setDate(d.getDate() + 1)) {
1004
+ newDates.push({
1005
+ date: new Date(d),
1006
+ disabled: this.ctx?.options?.maxDate < new Date(d) || false
1007
+ });
1008
+ }
1009
+ return { items: newDates, newBound: newLast }
1010
+ }
1011
+ },
1012
+ render: dates => this.ctx.render.dates(
1013
+ dates,
1014
+ this.ctx.selectedDates,
1015
+ this.ctx.options.selectType
1016
+ ),
1017
+ block: this.ctx.dom.$days_block
1018
+ },
1019
+ month: {
1020
+ count: 36,
1021
+ ctx: this.ctx,
1022
+ getNewItems: function (upDate, downDate) {
1023
+ const newDates = [];
1024
+ const isMonthDisabled = (year, month) => {
1025
+ const date = new Date(year, month, 1);
1026
+ const monthEnd = new Date(year, month + 1, 0);
1027
+ if (this.ctx.options.minDate && monthEnd < this.ctx.options.minDate) return true
1028
+ if (this.ctx.options.maxDate && date > this.ctx.options.maxDate) return true
1029
+ return false
1030
+ };
1031
+
1032
+ if (isUp) {
1033
+ // Генеруємо повними роками (кратно 12), щоб Jan завжди був ліворуч у сітці.
1034
+ let curr = new Date(upDate);
1035
+ for (let i = 0; i < this.count; i++) {
1036
+ curr.setMonth(curr.getMonth() - 1);
1037
+ newDates.unshift({
1038
+ date: new Date(curr.getFullYear(), curr.getMonth(), 1),
1039
+ disabled: isMonthDisabled(curr.getFullYear(), curr.getMonth())
1040
+ });
1041
+ }
1042
+ const newBound = newDates.length
1043
+ ? newDates[0].date
1044
+ : upDate;
1045
+ return { items: newDates, newBound }
1046
+ } else {
1047
+ // Генеруємо повними роками (кратно 12), щоб структура рядків не "плавала".
1048
+ let curr = new Date(downDate);
1049
+ curr.setMonth(curr.getMonth() + 1);
1050
+ for (let i = 0; i < this.count; i++) {
1051
+ newDates.push({
1052
+ date: new Date(curr.getFullYear(), curr.getMonth(), 1),
1053
+ disabled: isMonthDisabled(curr.getFullYear(), curr.getMonth())
1054
+ });
1055
+ curr.setMonth(curr.getMonth() + 1);
1056
+ }
1057
+ const newBound = newDates.length
1058
+ ? newDates[newDates.length - 1].date
1059
+ : downDate;
1060
+ return { items: newDates, newBound }
1061
+ }
1062
+ },
1063
+ render: months => this.ctx.render.months(months),
1064
+ block: this.ctx.dom.$months_block
1065
+ },
1066
+ year: {
1067
+ count: 16,
1068
+ ctx: this.ctx,
1069
+ getNewItems: function (upDate, downDate) {
1070
+ const newDates = [];
1071
+ if (isUp) {
1072
+ // 🔑 Генерація СТРОГО перед upDate.getFullYear()
1073
+ const startYear = upDate.getFullYear() - this.count;
1074
+ for (let y = startYear; y < upDate.getFullYear(); y++) {
1075
+ if (y < this.ctx.options.minDate.getFullYear()) continue
1076
+ newDates.push({
1077
+ date: new Date(y, 0, 1),
1078
+ disabled: this.ctx.options.minDate.getFullYear() > y
1079
+ });
1080
+ }
1081
+ const newBound = newDates.length
1082
+ ? newDates[0].date
1083
+ : upDate;
1084
+ return { items: newDates, newBound }
1085
+ } else {
1086
+ // 🔑 Генерація СТРОГО після downDate.getFullYear()
1087
+ const startYear = downDate.getFullYear() + 1;
1088
+ for (let y = startYear; y < startYear + this.count; y++) {
1089
+ if (y > this.ctx.options.maxDate.getFullYear()) break
1090
+ newDates.push({
1091
+ date: new Date(y, 0, 1),
1092
+ disabled: this.ctx.options.maxDate.getFullYear() < y
1093
+ });
1094
+ }
1095
+ const newBound = newDates.length
1096
+ ? newDates[newDates.length - 1].date
1097
+ : downDate;
1098
+ return { items: newDates, newBound }
1099
+ }
1100
+ },
1101
+ render: years => this.ctx.render.years(years),
1102
+ block: this.ctx.dom.$years_block
1103
+ }
1104
+ }[period];
1105
+
1106
+ if (!config) return
1107
+
1108
+ const { items, newBound } = config.getNewItems(this.ctx.data.up_date, this.ctx.data.down_date);
1109
+
1110
+ if (items.length) {
1111
+ config.block.insertAdjacentHTML(
1112
+ isUp ? 'afterbegin' : 'beforeend',
1113
+ config.render(items)
1114
+ );
1115
+ }
1116
+
1117
+ this.ctx.data[`${direction}_date`] = newBound;
1118
+ this.ctx.observe.un();
1119
+ const trimSize = period === 'year'
1120
+ ? Math.min(4, items.length)
1121
+ : items.length;
1122
+ this.trim(direction, trimSize);
1123
+ this.ctx.observe.on(period);
1124
+ this.ctx.scroll.blocked = false;
1125
+ }
1126
+
1127
+ trim(direction, remove) {
1128
+ const period = this.ctx.period;
1129
+ const block = this.ctx.dom[`$${period}s_block`];
1130
+ const isUp = direction === 'up';
1131
+ const minItemsInDom = period === 'year' ? 28 : 0;
1132
+
1133
+ const oldHeight = block.scrollHeight;
1134
+ const items = Array.from(block.querySelectorAll(`.RollDate__calendar__${period}`));
1135
+
1136
+ if (!remove || items.length <= remove * 2) return
1137
+ if (minItemsInDom && (items.length - remove) < minItemsInDom) return
1138
+
1139
+ if (isUp) {
1140
+ items.slice(-remove).forEach(item => item.remove());
1141
+ } else {
1142
+ items.slice(0, remove).forEach(item => item.remove());
1143
+ }
1144
+
1145
+ // Перераховуємо межі тільки з поточного DOM після trim, щоб уникнути "миготіння" і зациклення дозагрузки.
1146
+ const currentItems = Array.from(block.querySelectorAll(`.RollDate__calendar__${period}`));
1147
+ const firstItem = currentItems[0];
1148
+ const lastItem = currentItems[currentItems.length - 1];
1149
+ const toDate = (el) => {
1150
+ if (!el) return null
1151
+ const year = Number(el.dataset.year);
1152
+ const month = Number(el.dataset.month) || 0;
1153
+ const day = period === 'day' ? Number(el.dataset.day) : 1;
1154
+ return new Date(year, month, day)
1155
+ };
1156
+
1157
+ const newUpDate = toDate(firstItem);
1158
+ const newDownDate = toDate(lastItem);
1159
+ if (newUpDate) this.ctx.data.up_date = newUpDate;
1160
+ if (newDownDate) this.ctx.data.down_date = newDownDate;
1161
+
1162
+ const newHeight = block.scrollHeight;
1163
+ const heightDelta = oldHeight - newHeight;
1164
+ this.ctx.scroll.checkMinScroll();
1165
+
1166
+ if (isUp) this.ctx.scroll.offset -= heightDelta;
1167
+ else this.ctx.scroll.offset += heightDelta;
1168
+ }
1102
1169
  }
1103
1170
 
1104
- const ITEM_HEIGHT = 28;
1105
-
1106
- const ARROW_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 61 55" width="55" height="50" aria-hidden="true">
1107
- <path fill-rule="evenodd" d="m52.76 54.67l-44.73 0.12c-6.16 0.02-10.02-6.64-6.96-11.98l22.26-38.79c3.07-5.35 10.76-5.37 13.86-0.04l22.47 38.67c3.09 5.33-0.74 12.01-6.9 12.02z"/>
1108
- </svg>`;
1171
+ const DEFAULT_ITEM_HEIGHT = 28;
1172
+ const DEFAULT_VISIBLE = 3;
1109
1173
 
1110
1174
  class TimePicker {
1111
1175
  #hours = 0
@@ -1114,7 +1178,6 @@ var RollDate = (function () {
1114
1178
  #period = 'AM'
1115
1179
  #minuteStep = 1
1116
1180
  #columns = []
1117
- #displayEl = null
1118
1181
 
1119
1182
  constructor(root, options = {}) {
1120
1183
  this.root = root;
@@ -1142,13 +1205,89 @@ var RollDate = (function () {
1142
1205
  return h === 0 ? 12 : h
1143
1206
  }
1144
1207
 
1145
- #normalizeMinute(value) {
1146
- const step = this.#minuteStep;
1147
- const m = Math.round(Number(value) / step) * step;
1148
- return Math.min(59, Math.max(0, Number.isNaN(m) ? 0 : m))
1208
+ #normalizeMinute(value) {
1209
+ const step = this.#minuteStep;
1210
+ const m = Math.round(Number(value) / step) * step;
1211
+ return Math.min(59, Math.max(0, Number.isNaN(m) ? 0 : m))
1212
+ }
1213
+
1214
+ #readMetrics(viewport) {
1215
+ const container = viewport.closest('.RollDate__container');
1216
+ const cs = container ? getComputedStyle(container) : getComputedStyle(viewport);
1217
+ const itemH = parseFloat(cs.getPropertyValue('--rd-time-item-h')) || DEFAULT_ITEM_HEIGHT;
1218
+ const visible = parseInt(cs.getPropertyValue('--rd-time-visible'), 10) || DEFAULT_VISIBLE;
1219
+ const pad = Math.floor((visible - 1) / 2) * itemH;
1220
+ return { itemH, visible, pad }
1221
+ }
1222
+
1223
+ #build() {
1224
+ this.root.innerHTML = `
1225
+ <div class="RollDate__time__picker"></div>
1226
+ <div class="RollDate__time__period">
1227
+ <div class="RollDate__time__segmented RollDate__time__segmented--period" data-role="period">
1228
+ <button type="button" class="RollDate__time__segment-btn" data-period="AM">AM</button>
1229
+ <button type="button" class="RollDate__time__segment-btn" data-period="PM">PM</button>
1230
+ </div>
1231
+ </div>
1232
+ `;
1233
+
1234
+ if (this.#use12Hour) {
1235
+ this.root.classList.add('RollDate__time--12h');
1236
+ }
1237
+ this.#bindToolbar();
1238
+ this.#buildPickerColumns();
1239
+ this.#syncToolbar();
1240
+ }
1241
+
1242
+ #bindToolbar() {
1243
+ this.root.querySelector('[data-role="period"]')?.addEventListener('click', (e) => {
1244
+ const btn = e.target.closest('[data-period]');
1245
+ if (!btn || !this.#use12Hour) return
1246
+ this.#setPeriod(btn.dataset.period);
1247
+ });
1248
+ }
1249
+
1250
+ #setPeriod(period) {
1251
+ if (period !== 'AM' && period !== 'PM') return
1252
+ if (this.#period === period) return
1253
+
1254
+ this.#readColumns();
1255
+ const was = this.#period;
1256
+ this.#period = period;
1257
+
1258
+ if (was === 'AM' && period === 'PM' && this.#hours < 12) {
1259
+ this.#hours += 12;
1260
+ } else if (was === 'PM' && period === 'AM' && this.#hours >= 12) {
1261
+ this.#hours -= 12;
1262
+ }
1263
+
1264
+ this.#columns.find(c => c.unit === 'hour')?.scrollToValue(this.#displayHour(), true);
1265
+ this.#syncToolbar();
1266
+ hapticTick(this.hapticFeedback);
1267
+ this.#commitTime();
1268
+ }
1269
+
1270
+ #syncToolbar() {
1271
+ this.root.classList.toggle('RollDate__time--12h', this.#use12Hour);
1272
+
1273
+ this.root.querySelectorAll('[data-period]').forEach((btn) => {
1274
+ const active = btn.dataset.period === this.#period;
1275
+ btn.classList.toggle('RollDate__time__segment-btn--active', active);
1276
+ btn.setAttribute('aria-pressed', active ? 'true' : 'false');
1277
+ });
1278
+ }
1279
+
1280
+ #destroyColumns() {
1281
+ this.#columns.forEach((column) => column._resizeObserver?.disconnect());
1282
+ this.#columns = [];
1149
1283
  }
1150
1284
 
1151
- #build() {
1285
+ #buildPickerColumns() {
1286
+ this.#destroyColumns();
1287
+
1288
+ const picker = this.root.querySelector('.RollDate__time__picker');
1289
+ if (!picker) return
1290
+
1152
1291
  const hourValues = this.#use12Hour
1153
1292
  ? Array.from({ length: 12 }, (_, i) => i + 1)
1154
1293
  : Array.from({ length: 24 }, (_, i) => i);
@@ -1158,68 +1297,36 @@ var RollDate = (function () {
1158
1297
  minuteValues.push(m);
1159
1298
  }
1160
1299
 
1161
- this.root.innerHTML = `
1162
- <div class="RollDate__time__picker">
1300
+ picker.innerHTML = `
1163
1301
  <div class="RollDate__time__field" data-unit="hour">
1164
1302
  <div class="RollDate__time__column">
1165
- <button type="button" class="RollDate__time__arrow RollDate__time__arrow--prev" data-dir="prev" aria-label="Earlier hour">${ARROW_SVG}</button>
1166
1303
  <div class="RollDate__time__viewport">
1167
1304
  <div class="RollDate__time__list"></div>
1168
1305
  </div>
1169
- <button type="button" class="RollDate__time__arrow RollDate__time__arrow--next" data-dir="next" aria-label="Later hour">${ARROW_SVG}</button>
1170
1306
  </div>
1171
1307
  </div>
1172
1308
  <span class="RollDate__time__sep">:</span>
1173
1309
  <div class="RollDate__time__field" data-unit="minute">
1174
1310
  <div class="RollDate__time__column">
1175
- <button type="button" class="RollDate__time__arrow RollDate__time__arrow--prev" data-dir="prev" aria-label="Earlier minute">${ARROW_SVG}</button>
1176
- <div class="RollDate__time__viewport">
1177
- <div class="RollDate__time__list"></div>
1178
- </div>
1179
- <button type="button" class="RollDate__time__arrow RollDate__time__arrow--next" data-dir="next" aria-label="Later minute">${ARROW_SVG}</button>
1180
- </div>
1181
- </div>
1182
- ${this.#use12Hour ? `
1183
- <div class="RollDate__time__field RollDate__time__field--period" data-unit="period">
1184
- <div class="RollDate__time__column">
1185
- <button type="button" class="RollDate__time__arrow RollDate__time__arrow--prev" data-dir="prev" aria-label="Earlier period">${ARROW_SVG}</button>
1186
1311
  <div class="RollDate__time__viewport">
1187
1312
  <div class="RollDate__time__list"></div>
1188
1313
  </div>
1189
- <button type="button" class="RollDate__time__arrow RollDate__time__arrow--next" data-dir="next" aria-label="Later period">${ARROW_SVG}</button>
1190
1314
  </div>
1191
- </div>` : ''}
1192
1315
  </div>
1193
- <div class="RollDate__time__display" aria-live="polite"></div>
1194
1316
  `;
1195
1317
 
1196
- this.#displayEl = this.root.querySelector('.RollDate__time__display');
1197
- if (this.#use12Hour) {
1198
- this.root.classList.add('RollDate__time--12h');
1199
- }
1200
-
1201
- this.#columns.push(this.#createColumn('hour', hourValues, this.#displayHour()));
1318
+ this.#columns.push(this.#createColumn('hour', hourValues, this.#use12Hour ? this.#displayHour() : this.#hours));
1202
1319
  this.#columns.push(this.#createColumn('minute', minuteValues, this.#minutes));
1203
- if (this.#use12Hour) {
1204
- this.#columns.push(this.#createColumn('period', ['AM', 'PM'], this.#period));
1205
- }
1206
-
1207
- this.#updateDisplay();
1208
1320
  }
1209
1321
 
1210
- #formatDisplay() {
1211
- const hours = String(this.#hours).padStart(2, '0');
1212
- const minutes = String(this.#minutes).padStart(2, '0');
1213
- if (this.#use12Hour) {
1214
- return `${String(this.#displayHour()).padStart(2, '0')}:${minutes} ${this.#period}`
1215
- }
1216
- return `${hours}:${minutes}`
1322
+ #commitTime() {
1323
+ this.#readColumns();
1324
+ this.onChange(this.getTime());
1217
1325
  }
1218
1326
 
1219
- #updateDisplay() {
1220
- if (this.#displayEl) {
1221
- this.#displayEl.textContent = this.#formatDisplay();
1222
- }
1327
+ #parseItemValue(unit, raw) {
1328
+ if (unit === 'period') return raw
1329
+ return Number(raw)
1223
1330
  }
1224
1331
 
1225
1332
  #createColumn(unit, values, initial) {
@@ -1234,22 +1341,28 @@ var RollDate = (function () {
1234
1341
  return `<div class="RollDate__time__item" data-value="${value}">${label}</div>`
1235
1342
  }).join('');
1236
1343
 
1237
- list.style.paddingTop = `${ITEM_HEIGHT}px`;
1238
- list.style.paddingBottom = `${ITEM_HEIGHT}px`;
1239
-
1240
1344
  const column = {
1241
1345
  unit,
1242
1346
  viewport,
1243
1347
  list,
1244
1348
  values,
1245
- offset: 0
1349
+ offset: 0,
1350
+ getMetrics: () => this.#readMetrics(viewport)
1246
1351
  };
1247
1352
 
1353
+ const applyPadding = () => {
1354
+ const { pad } = column.getMetrics();
1355
+ list.style.paddingTop = `${pad}px`;
1356
+ list.style.paddingBottom = `${pad}px`;
1357
+ };
1358
+ applyPadding();
1359
+
1248
1360
  column.updateActive = () => {
1249
1361
  const index = column.indexFromOffset();
1250
1362
  if (column._lastIndex !== index) {
1251
1363
  if (column._lastIndex !== undefined) {
1252
1364
  hapticTick(this.hapticFeedback);
1365
+ this.#commitTime();
1253
1366
  }
1254
1367
  column._lastIndex = index;
1255
1368
  }
@@ -1259,7 +1372,8 @@ var RollDate = (function () {
1259
1372
  };
1260
1373
 
1261
1374
  column.indexFromOffset = () => {
1262
- const raw = Math.round(-column.offset / ITEM_HEIGHT);
1375
+ const { itemH } = column.getMetrics();
1376
+ const raw = Math.round(-column.offset / itemH);
1263
1377
  return Math.min(values.length - 1, Math.max(0, raw))
1264
1378
  };
1265
1379
 
@@ -1272,46 +1386,51 @@ var RollDate = (function () {
1272
1386
  };
1273
1387
 
1274
1388
  column.snap = () => {
1389
+ const { itemH } = column.getMetrics();
1275
1390
  const index = column.indexFromOffset();
1276
- column.offset = -index * ITEM_HEIGHT;
1391
+ column.offset = -index * itemH;
1392
+ column._lastIndex = index;
1277
1393
  column.apply(true);
1278
- this.#readColumns();
1279
- this.#updateDisplay();
1280
- this.onChange(this.getTime());
1394
+ this.#commitTime();
1281
1395
  };
1282
1396
 
1283
1397
  column.scrollToValue = (value, animate = true) => {
1398
+ const { itemH } = column.getMetrics();
1284
1399
  const index = values.indexOf(value);
1285
1400
  if (index < 0) return
1286
- column.offset = -index * ITEM_HEIGHT;
1401
+ column.offset = -index * itemH;
1402
+ column._lastIndex = index;
1287
1403
  column.apply(animate);
1288
1404
  };
1289
1405
 
1290
- column.stepBy = (delta) => {
1291
- const index = Math.min(values.length - 1, Math.max(0, column.indexFromOffset() + delta));
1292
- column.offset = -index * ITEM_HEIGHT;
1293
- column.apply(true);
1294
- this.#readColumns();
1295
- this.#updateDisplay();
1296
- this.onChange(this.getTime());
1406
+ column.scrollToIndex = (index, animate = true) => {
1407
+ const { itemH } = column.getMetrics();
1408
+ const clamped = Math.min(values.length - 1, Math.max(0, index));
1409
+ column.offset = -clamped * itemH;
1410
+ column._lastIndex = clamped;
1411
+ column.apply(animate);
1297
1412
  };
1298
1413
 
1299
1414
  column.scrollToValue(initial, false);
1300
1415
  this.#bindWheel(column);
1301
1416
  this.#bindTouch(column);
1302
- this.#bindArrows(field, column);
1417
+ this.#bindItemClick(column);
1418
+ column._resizeObserver = new ResizeObserver(() => {
1419
+ applyPadding();
1420
+ column.scrollToIndex(column.indexFromOffset(), false);
1421
+ });
1422
+ column._resizeObserver.observe(viewport);
1303
1423
 
1304
1424
  return column
1305
1425
  }
1306
1426
 
1307
- #bindArrows(field, column) {
1308
- field.querySelector('[data-dir="prev"]')?.addEventListener('click', (e) => {
1309
- e.preventDefault();
1310
- column.stepBy(-1);
1311
- });
1312
- field.querySelector('[data-dir="next"]')?.addEventListener('click', (e) => {
1313
- e.preventDefault();
1314
- column.stepBy(1);
1427
+ #bindItemClick(column) {
1428
+ column.list.addEventListener('click', (e) => {
1429
+ const item = e.target.closest('.RollDate__time__item');
1430
+ if (!item) return
1431
+ const value = this.#parseItemValue(column.unit, item.dataset.value);
1432
+ column.scrollToValue(value, true);
1433
+ this.#commitTime();
1315
1434
  });
1316
1435
  }
1317
1436
 
@@ -1319,9 +1438,10 @@ var RollDate = (function () {
1319
1438
  column.viewport.addEventListener('wheel', (e) => {
1320
1439
  e.preventDefault();
1321
1440
  e.stopPropagation();
1441
+ const { itemH } = column.getMetrics();
1322
1442
  column.list.style.transition = 'none';
1323
1443
  column.offset -= e.deltaY * 0.35;
1324
- const min = -(column.values.length - 1) * ITEM_HEIGHT;
1444
+ const min = -(column.values.length - 1) * itemH;
1325
1445
  column.offset = Math.max(min, Math.min(0, column.offset));
1326
1446
  column.list.style.transform = `translateY(${column.offset}px)`;
1327
1447
  column.updateActive();
@@ -1344,8 +1464,9 @@ var RollDate = (function () {
1344
1464
  column.viewport.addEventListener('touchmove', (e) => {
1345
1465
  if (!e.touches || e.touches.length !== 1) return
1346
1466
  e.preventDefault();
1467
+ const { itemH } = column.getMetrics();
1347
1468
  column.offset = startOffset + (e.touches[0].clientY - startY);
1348
- const min = -(column.values.length - 1) * ITEM_HEIGHT;
1469
+ const min = -(column.values.length - 1) * itemH;
1349
1470
  column.offset = Math.max(min, Math.min(0, column.offset));
1350
1471
  column.list.style.transform = `translateY(${column.offset}px)`;
1351
1472
  column.updateActive();
@@ -1357,13 +1478,12 @@ var RollDate = (function () {
1357
1478
  #readColumns() {
1358
1479
  const hourCol = this.#columns.find(c => c.unit === 'hour');
1359
1480
  const minuteCol = this.#columns.find(c => c.unit === 'minute');
1360
- const periodCol = this.#columns.find(c => c.unit === 'period');
1481
+ if (!hourCol || !minuteCol) return
1361
1482
 
1362
1483
  const hourValue = hourCol.getValue();
1363
1484
  this.#minutes = minuteCol.getValue();
1364
1485
 
1365
- if (this.#use12Hour && periodCol) {
1366
- this.#period = periodCol.getValue();
1486
+ if (this.#use12Hour) {
1367
1487
  if (this.#period === 'AM') {
1368
1488
  this.#hours = hourValue === 12 ? 0 : hourValue;
1369
1489
  } else {
@@ -1384,16 +1504,15 @@ var RollDate = (function () {
1384
1504
  if (this.#use12Hour) {
1385
1505
  this.#period = this.#hours >= 12 ? 'PM' : 'AM';
1386
1506
  this.#columns.find(c => c.unit === 'hour')?.scrollToValue(this.#displayHour());
1387
- this.#columns.find(c => c.unit === 'period')?.scrollToValue(this.#period);
1388
1507
  } else {
1389
1508
  this.#columns.find(c => c.unit === 'hour')?.scrollToValue(this.#hours);
1390
1509
  }
1391
1510
  this.#columns.find(c => c.unit === 'minute')?.scrollToValue(this.#minutes);
1392
- this.#updateDisplay();
1511
+ this.#syncToolbar();
1393
1512
  }
1394
1513
 
1395
1514
  destroy() {
1396
- this.#columns = [];
1515
+ this.#destroyColumns();
1397
1516
  this.root.innerHTML = '';
1398
1517
  }
1399
1518
  }
@@ -1403,10 +1522,12 @@ var RollDate = (function () {
1403
1522
 
1404
1523
  #wheelHandler
1405
1524
  #viewNumber = 0
1525
+ #arrowAnimating = false
1406
1526
  #viewPeriodNames = ['day', 'month', 'year']
1407
1527
  #selectedDates = []
1408
1528
  #firstOpen = true
1409
1529
  #disabledDateStamps = new Set()
1530
+ #highlightDateMap = new Map()
1410
1531
  #docClickHandler = null
1411
1532
  #openTriggers = []
1412
1533
 
@@ -1422,12 +1543,36 @@ var RollDate = (function () {
1422
1543
  }
1423
1544
 
1424
1545
  #normalizeDateInput(dateLike) {
1546
+ if (dateLike instanceof Date && !Number.isNaN(dateLike.getTime())) {
1547
+ return new Date(dateLike.getFullYear(), dateLike.getMonth(), dateLike.getDate())
1548
+ }
1549
+
1550
+ if (typeof dateLike === 'string') {
1551
+ const clean = dateLike.trim();
1552
+ if (!clean) return null
1553
+
1554
+ const valueSep = clean.match(/[-/.]/)?.[0];
1555
+ const formatSep = this.options.dateFormat.match(/[-/.]/)?.[0];
1556
+ const format = valueSep && formatSep && valueSep !== formatSep
1557
+ ? 'auto'
1558
+ : this.options.dateFormat;
1559
+
1560
+ const date = parseDate(clean, format);
1561
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
1562
+ const fallback = parseDate(clean, 'auto');
1563
+ if (!(fallback instanceof Date) || Number.isNaN(fallback.getTime())) return null
1564
+ return new Date(fallback.getFullYear(), fallback.getMonth(), fallback.getDate())
1565
+ }
1566
+
1567
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate())
1568
+ }
1569
+
1425
1570
  const date = checkDateFormat(dateLike, this.options.dateFormat);
1426
1571
  if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null
1427
1572
  return new Date(date.getFullYear(), date.getMonth(), date.getDate())
1428
1573
  }
1429
1574
 
1430
- #buildDisabledDateSet(dates = []) {
1575
+ #buildDateStampSet(dates = []) {
1431
1576
  const set = new Set();
1432
1577
  dates.forEach(dateLike => {
1433
1578
  const normalized = this.#normalizeDateInput(dateLike);
@@ -1436,6 +1581,85 @@ var RollDate = (function () {
1436
1581
  return set
1437
1582
  }
1438
1583
 
1584
+ #buildDisabledDateSet(dates = []) {
1585
+ return this.#buildDateStampSet(dates)
1586
+ }
1587
+
1588
+ #buildHighlightDateMap(dates = []) {
1589
+ const map = new Map();
1590
+ if (!Array.isArray(dates)) return map
1591
+
1592
+ dates.forEach(entry => {
1593
+ const item = this.#normalizeHighlightEntry(entry);
1594
+ if (!item) return
1595
+
1596
+ const existing = map.get(item.stamp) || [];
1597
+ map.set(item.stamp, existing.concat(item.colors));
1598
+ });
1599
+
1600
+ return map
1601
+ }
1602
+
1603
+ #normalizeHighlightEntry(entry) {
1604
+ if (entry == null) return null
1605
+
1606
+ if (typeof entry === 'string' || entry instanceof Date) {
1607
+ const normalized = this.#normalizeDateInput(entry);
1608
+ if (!normalized) return null
1609
+ return {
1610
+ stamp: this.#toDateStamp(normalized),
1611
+ colors: [null]
1612
+ }
1613
+ }
1614
+
1615
+ if (typeof entry === 'object' && entry.date != null) {
1616
+ const normalized = this.#normalizeDateInput(entry.date);
1617
+ if (!normalized) return null
1618
+ const stamp = this.#toDateStamp(normalized);
1619
+
1620
+ if (Array.isArray(entry.colors)) {
1621
+ const colors = entry.colors.map(color => {
1622
+ if (color == null || color === '') return null
1623
+ return this.#sanitizeHighlightColor(color)
1624
+ }).filter(color => color !== undefined);
1625
+
1626
+ if (colors.length) return { stamp, colors }
1627
+ }
1628
+
1629
+ if (entry.color != null && entry.color !== '') {
1630
+ const color = this.#sanitizeHighlightColor(entry.color);
1631
+ if (color) return { stamp, colors: [color] }
1632
+ }
1633
+
1634
+ return { stamp, colors: [null] }
1635
+ }
1636
+
1637
+ return null
1638
+ }
1639
+
1640
+ #sanitizeHighlightColor(color) {
1641
+ if (typeof color !== 'string') return null
1642
+ const value = color.trim();
1643
+ if (!value) return null
1644
+
1645
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)) return value
1646
+ if (/^(rgb|rgba|hsl|hsla)\([^)]+\)$/i.test(value)) return value
1647
+ if (/^var\(--[\w-]+\)$/.test(value)) return value
1648
+
1649
+ return null
1650
+ }
1651
+
1652
+ #highlightMapToOptionsArray() {
1653
+ return [...this.#highlightDateMap.entries()].map(([stamp, colors]) => {
1654
+ const date = new Date(stamp);
1655
+
1656
+ if (colors.length === 1 && colors[0] === null) return date
1657
+ if (colors.length === 1 && colors[0]) return { date, color: colors[0] }
1658
+
1659
+ return { date, colors }
1660
+ })
1661
+ }
1662
+
1439
1663
  #disableInputAssist(input) {
1440
1664
  if (!input || input.tagName !== 'INPUT') return
1441
1665
  input.setAttribute('autocomplete', 'off');
@@ -1448,6 +1672,16 @@ var RollDate = (function () {
1448
1672
  return this.#disabledDateStamps.has(this.#toDateStamp(date))
1449
1673
  }
1450
1674
 
1675
+ #isDateHighlighted(date) {
1676
+ return this.#highlightDateMap.has(this.#toDateStamp(date))
1677
+ }
1678
+
1679
+ #getHighlightMeta(date) {
1680
+ const colors = this.#highlightDateMap.get(this.#toDateStamp(date));
1681
+ if (!colors?.length) return null
1682
+ return { colors }
1683
+ }
1684
+
1451
1685
  #notifySelectionChange() {
1452
1686
  if (this.options.selectType === 'single') {
1453
1687
  this.options.selectDate(this.#selectedDates[0] || null);
@@ -1499,6 +1733,11 @@ var RollDate = (function () {
1499
1733
  const btn = document.createElement('button');
1500
1734
  btn.type = 'button';
1501
1735
  btn.className = 'RollDate__footer__button';
1736
+ if (cfg.variant === 'primary') {
1737
+ btn.classList.add('RollDate__footer__button--primary');
1738
+ } else if (cfg.variant === 'secondary') {
1739
+ btn.classList.add('RollDate__footer__button--secondary');
1740
+ }
1502
1741
  btn.textContent = cfg.text;
1503
1742
  btn.addEventListener('click', (e) => {
1504
1743
  e.preventDefault();
@@ -1523,6 +1762,69 @@ var RollDate = (function () {
1523
1762
  }
1524
1763
  }
1525
1764
 
1765
+ #mountRangePresets() {
1766
+ const presets = Array.isArray(this.options.rangePresets)
1767
+ ? this.options.rangePresets
1768
+ : [];
1769
+
1770
+ if (this.options.selectType !== 'range' || !presets.length) return
1771
+
1772
+ let bar = this.$container.querySelector('.RollDate__presets');
1773
+ if (!bar) {
1774
+ bar = document.createElement('div');
1775
+ bar.className = 'RollDate__presets';
1776
+ const footer = this.$container.querySelector('.RollDate__footer');
1777
+ const calendar = this.$container.querySelector('.RollDate__calendar');
1778
+ if (footer) footer.before(bar);
1779
+ else if (calendar) calendar.after(bar);
1780
+ }
1781
+
1782
+ bar.innerHTML = '';
1783
+ presets.forEach(preset => {
1784
+ if (!preset?.label || typeof preset.getRange !== 'function') return
1785
+
1786
+ const btn = document.createElement('button');
1787
+ btn.type = 'button';
1788
+ btn.className = 'RollDate__presets__button';
1789
+ btn.textContent = preset.label;
1790
+ btn.addEventListener('click', (e) => {
1791
+ e.preventDefault();
1792
+ e.stopPropagation();
1793
+ this.#applyRangePreset(preset);
1794
+ });
1795
+ bar.append(btn);
1796
+ });
1797
+
1798
+ if (bar.childElementCount) {
1799
+ this.$container.classList.add('RollDate__has-presets');
1800
+ }
1801
+ }
1802
+
1803
+ #applyRangePreset(preset) {
1804
+ const result = preset.getRange(this);
1805
+ if (!Array.isArray(result) || result.length < 2) return
1806
+
1807
+ const normalized = result
1808
+ .slice(0, 2)
1809
+ .map(dateLike => this.#normalizeDateInput(dateLike))
1810
+ .filter(Boolean)
1811
+ .map(date => this.#clampDateToRange(date, this.options.minDate, this.options.maxDate))
1812
+ .filter(date => !this.#isDateDisabled(date));
1813
+
1814
+ if (normalized.length < 2) return
1815
+
1816
+ let [start, end] = normalized;
1817
+ if (start > end) [start, end] = [end, start];
1818
+
1819
+ this.#selectedDates = [
1820
+ this.#applyTimeToDate(start),
1821
+ this.#applyTimeToDate(end)
1822
+ ];
1823
+ this.#notifySelectionChange();
1824
+ this.#updateInputValue();
1825
+ this.#paintRangeSelection();
1826
+ }
1827
+
1526
1828
  #initTimePicker() {
1527
1829
  const start = this.options.startDate;
1528
1830
  this.timePicker = new TimePicker(this.dom.$time, {
@@ -1567,7 +1869,7 @@ var RollDate = (function () {
1567
1869
 
1568
1870
  const baseOptions = {
1569
1871
  mode: 'auto',
1570
- theme: 'dark',
1872
+ theme: 'main',
1571
1873
  startWeekFromMonday: true,
1572
1874
  selectType: 'single',
1573
1875
  monthsNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
@@ -1579,17 +1881,20 @@ var RollDate = (function () {
1579
1881
  onViewChange: () => {},
1580
1882
  onHoverDate: () => {},
1581
1883
  disabledDates: [],
1884
+ highlightDates: [],
1582
1885
  closeOnSelect: true,
1583
1886
  enableTime: false,
1584
1887
  use12Hour: false,
1585
1888
  timeStep: 1,
1889
+ timePosition: 'right',
1586
1890
  footerButtons: [],
1891
+ rangePresets: [],
1587
1892
  hapticFeedback: true,
1588
1893
  ...options
1589
1894
  };
1590
1895
 
1591
1896
  if (baseOptions.theme === 'default') {
1592
- baseOptions.theme = 'dark';
1897
+ baseOptions.theme = 'main';
1593
1898
  }
1594
1899
 
1595
1900
  const resolvedLocale = baseOptions.locale ||
@@ -1626,6 +1931,7 @@ var RollDate = (function () {
1626
1931
  this.options.maxDate
1627
1932
  );
1628
1933
  this.#disabledDateStamps = this.#buildDisabledDateSet(this.options.disabledDates);
1934
+ this.#highlightDateMap = this.#buildHighlightDateMap(this.options.highlightDates);
1629
1935
  if (this.#isDateDisabled(this.options.startDate)) {
1630
1936
  this.options.startDate = new Date(this.options.minDate);
1631
1937
  }
@@ -1668,8 +1974,10 @@ var RollDate = (function () {
1668
1974
 
1669
1975
  if (this.options.enableTime) {
1670
1976
  this.$container.classList.add('RollDate__has-time');
1977
+ const timePosition = this.options.timePosition === 'bottom' ? 'bottom' : 'right';
1978
+ this.$container.classList.add(`RollDate__time-position_${timePosition}`);
1671
1979
  }
1672
- if (this.options.footerButtons?.length || this.options.enableTime) {
1980
+ if (this.options.footerButtons?.length) {
1673
1981
  this.$container.classList.add('RollDate__has-footer');
1674
1982
  }
1675
1983
 
@@ -1698,6 +2006,7 @@ var RollDate = (function () {
1698
2006
  };
1699
2007
 
1700
2008
  this.#initFooterButtons();
2009
+ this.#mountRangePresets();
1701
2010
  if (this.options.enableTime && this.dom.$time) {
1702
2011
  this.#initTimePicker();
1703
2012
  }
@@ -1758,7 +2067,30 @@ var RollDate = (function () {
1758
2067
  this.$container.style.zIndex = '10000';
1759
2068
  }
1760
2069
 
1761
- #updateView() {
2070
+ #updateView(viewNumberOrOptions, maybeOptions) {
2071
+ let options = {};
2072
+
2073
+ if (typeof viewNumberOrOptions === 'object' && viewNumberOrOptions !== null) {
2074
+ options = viewNumberOrOptions;
2075
+ } else {
2076
+ if (typeof viewNumberOrOptions === 'number') {
2077
+ this.#viewNumber = viewNumberOrOptions;
2078
+ }
2079
+ if (typeof maybeOptions === 'object' && maybeOptions !== null) {
2080
+ options = maybeOptions;
2081
+ }
2082
+ }
2083
+
2084
+ const preserveScroll = options.preserveScroll === true;
2085
+ const savedOffset = preserveScroll ? this.scroll.offset : null;
2086
+ const animateScroll = options.animate === true && Number(options.shift);
2087
+ const onScrollComplete = typeof options.onScrollComplete === 'function'
2088
+ ? options.onScrollComplete
2089
+ : null;
2090
+ const finishScroll = () => {
2091
+ onScrollComplete?.();
2092
+ };
2093
+
1762
2094
  this.#updateHeader();
1763
2095
  this.scroll.blocked = false;
1764
2096
  this.observe.un(this.period);
@@ -1770,7 +2102,8 @@ var RollDate = (function () {
1770
2102
  this.dom.$days_block.innerHTML = this.render.dates(
1771
2103
  days,
1772
2104
  this.#selectedDates,
1773
- this.options.selectType
2105
+ this.options.selectType,
2106
+ date => this.#getHighlightMeta(date)
1774
2107
  );
1775
2108
  break
1776
2109
 
@@ -1779,41 +2112,68 @@ var RollDate = (function () {
1779
2112
  this.dom[`$${this.period}s_block`].innerHTML = this.render[`${this.period}s`](dates);
1780
2113
  }
1781
2114
 
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}"]`
2115
+ const applyScroll = () => {
2116
+ const $scrollBlock = this.dom.$body.querySelector('.RollDate__calendar__scrollblock');
2117
+ const bodyHeight = this.dom.$body.clientHeight;
2118
+ const blockHeight = $scrollBlock.clientHeight;
2119
+
2120
+ if (blockHeight <= bodyHeight) {
2121
+ this.scroll.setBaseOffset(bodyHeight - blockHeight);
2122
+ this.scroll.offset = 0;
2123
+ finishScroll();
2124
+ return
2125
+ }
2126
+
2127
+ this.scroll.setBaseOffset(0);
2128
+ this.scroll.resetMinScroll();
2129
+
2130
+ if (preserveScroll && savedOffset != null) {
2131
+ this.scroll.offset = Math.max(savedOffset, this.scroll.minScroll);
2132
+ finishScroll();
2133
+ return
2134
+ }
2135
+
2136
+ const selectors = {
2137
+ day: `.RollDate__calendar__day[data-year="${this.data.current_year}"][data-month="${this.data.current_month}"]`,
2138
+ month: `.RollDate__calendar__month[data-year="${this.data.current_year}"][data-month="0"]`,
2139
+ year: `.RollDate__calendar__year[data-decade="${this.data.current_decade}"][data-year="${this.data.current_year}"]`
1793
2140
  };
1794
- $firstEl = this.$container.querySelector(fallbacks[this.period]);
1795
- }
2141
+ let $firstEl = this.$container.querySelector(selectors[this.period]);
2142
+ if (!$firstEl) {
2143
+ const fallbacks = {
2144
+ day: `.RollDate__calendar__day[data-year="${this.data.current_year}"]`,
2145
+ month: `.RollDate__calendar__month[data-year="${this.data.current_year}"]`,
2146
+ year: `.RollDate__calendar__year[data-decade="${this.data.current_decade}"]`
2147
+ };
2148
+ $firstEl = this.$container.querySelector(fallbacks[this.period]);
2149
+ }
1796
2150
 
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;
2151
+ if (!$firstEl) {
2152
+ finishScroll();
2153
+ return
2154
+ }
1802
2155
 
1803
- if (blockHeight <= bodyHeight) {
1804
- this.scroll.setBaseOffset(bodyHeight - blockHeight);
1805
- this.scroll.offset = 0;
1806
- return
1807
- }
2156
+ const containerRect = $scrollBlock.getBoundingClientRect();
2157
+ const firstRect = $firstEl.getBoundingClientRect();
2158
+ const firstRectOffset = -(firstRect.top - containerRect.top);
2159
+ const targetOffset = Math.max(firstRectOffset, this.scroll.minScroll);
1808
2160
 
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
- }
2161
+ if (animateScroll) {
2162
+ const slide = Math.min(bodyHeight * 0.38, 140);
2163
+ const startOffset = Math.max(
2164
+ Math.min(targetOffset + (options.shift > 0 ? slide : -slide), 0),
2165
+ this.scroll.minScroll
2166
+ );
2167
+ this.scroll.offset = startOffset;
2168
+ this.scroll.animateTo(targetOffset, { onComplete: finishScroll });
2169
+ return
2170
+ }
2171
+
2172
+ this.scroll.offset = targetOffset;
2173
+ finishScroll();
2174
+ };
2175
+
2176
+ setTimeout(applyScroll, 0);
1817
2177
 
1818
2178
  this.observe.on(this.period, item => {
1819
2179
  const cond = JSON.parse(item.dataset.bind).every(data => Number(item.dataset[data]) === this.data[`current_${data}`]);
@@ -1967,9 +2327,10 @@ var RollDate = (function () {
1967
2327
  }
1968
2328
 
1969
2329
  if (this.options.selectType === 'range') {
2330
+ const clickStamp = this.#toDateStamp(date);
1970
2331
  const isClickInRange = this.#selectedDates.length === 2 &&
1971
- date.getTime() >= this.#selectedDates[0].getTime() &&
1972
- date.getTime() <= this.#selectedDates[1].getTime();
2332
+ clickStamp >= this.#toDateStamp(this.#selectedDates[0]) &&
2333
+ clickStamp <= this.#toDateStamp(this.#selectedDates[1]);
1973
2334
 
1974
2335
  if (this.#selectedDates.length === 2 && !isClickInRange) {
1975
2336
  this.#clearRangeSelection();
@@ -1980,7 +2341,8 @@ var RollDate = (function () {
1980
2341
  $day.classList.add('RollDate__calendar__day--range-first');
1981
2342
  } else {
1982
2343
  const firstDate = this.#selectedDates[0];
1983
- if (date.getTime() > firstDate.getTime()) {
2344
+ const firstStamp = this.#toDateStamp(firstDate);
2345
+ if (clickStamp > firstStamp) {
1984
2346
  this.#selectedDates = [firstDate, date];
1985
2347
 
1986
2348
  this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
@@ -1989,17 +2351,17 @@ var RollDate = (function () {
1989
2351
  Number(dayEl.dataset.month),
1990
2352
  Number(dayEl.dataset.day)
1991
2353
  );
1992
- const time = dayDate.getTime();
2354
+ const stamp = this.#toDateStamp(dayDate);
1993
2355
 
1994
- if (time === firstDate.getTime()) {
2356
+ if (stamp === firstStamp) {
1995
2357
  dayEl.classList.add('RollDate__calendar__day--range-first');
1996
- } else if (time === date.getTime()) {
2358
+ } else if (stamp === clickStamp) {
1997
2359
  dayEl.classList.add('RollDate__calendar__day--range-last');
1998
- } else if (time > firstDate.getTime() && time < date.getTime()) {
2360
+ } else if (stamp > firstStamp && stamp < clickStamp) {
1999
2361
  dayEl.classList.add('RollDate__calendar__day--range-selected');
2000
2362
  }
2001
2363
  });
2002
- } else if (date.getTime() < firstDate.getTime()) {
2364
+ } else if (clickStamp < firstStamp) {
2003
2365
  this.#selectedDates = [date];
2004
2366
  $day.classList.add('RollDate__calendar__day--range-first');
2005
2367
 
@@ -2017,12 +2379,13 @@ var RollDate = (function () {
2017
2379
  }
2018
2380
 
2019
2381
  if (this.options.selectType === 'multi') {
2382
+ const clickStamp = this.#toDateStamp(date);
2020
2383
  const isSelected = this.#selectedDates.some(
2021
- d => d.getTime() === date.getTime()
2384
+ d => this.#toDateStamp(d) === clickStamp
2022
2385
  );
2023
2386
  if (isSelected) {
2024
2387
  this.#selectedDates = this.#selectedDates.filter(
2025
- d => d.getTime() !== date.getTime()
2388
+ d => this.#toDateStamp(d) !== clickStamp
2026
2389
  );
2027
2390
  $day.classList.remove('RollDate__calendar__day--selected');
2028
2391
  } else {
@@ -2068,6 +2431,8 @@ var RollDate = (function () {
2068
2431
  e.preventDefault();
2069
2432
  e.stopPropagation();
2070
2433
 
2434
+ if (this.#arrowAnimating) return
2435
+
2071
2436
  const direction = e.currentTarget?.dataset?.direction;
2072
2437
  if (direction !== 'prev' && direction !== 'next') return
2073
2438
  const shift = direction === 'prev' ? -1 : 1;
@@ -2104,8 +2469,15 @@ var RollDate = (function () {
2104
2469
  this.data.current_decade = targetDecade;
2105
2470
  }
2106
2471
 
2472
+ this.#arrowAnimating = true;
2107
2473
  hapticTick(this.options.hapticFeedback !== false);
2108
- this.#updateView(this.#viewNumber);
2474
+ this.#updateView(this.#viewNumber, {
2475
+ animate: true,
2476
+ shift,
2477
+ onScrollComplete: () => {
2478
+ this.#arrowAnimating = false;
2479
+ }
2480
+ });
2109
2481
  });
2110
2482
  });
2111
2483
  }
@@ -2172,6 +2544,20 @@ var RollDate = (function () {
2172
2544
  return this.#selectedDates
2173
2545
  }
2174
2546
 
2547
+ /** Visible calendar month (updates while scrolling). Month is 0–11. */
2548
+ getViewMonth() {
2549
+ return {
2550
+ year: this.data.current_year,
2551
+ month: this.data.current_month
2552
+ }
2553
+ }
2554
+
2555
+ /** First day of the month currently shown in the calendar. */
2556
+ getViewDate() {
2557
+ const { year, month } = this.getViewMonth();
2558
+ return new Date(year, month, 1)
2559
+ }
2560
+
2175
2561
  setDisabledDates(dates = []) {
2176
2562
  this.options.disabledDates = Array.isArray(dates) ? dates : [];
2177
2563
  this.#disabledDateStamps = this.#buildDisabledDateSet(this.options.disabledDates);
@@ -2201,6 +2587,251 @@ var RollDate = (function () {
2201
2587
  return normalized ? this.#isDateDisabled(normalized) : false
2202
2588
  }
2203
2589
 
2590
+ setHighlightDates(dates = []) {
2591
+ this.options.highlightDates = Array.isArray(dates) ? dates : [];
2592
+ this.#highlightDateMap = this.#buildHighlightDateMap(this.options.highlightDates);
2593
+ this.#updateView(this.#viewNumber);
2594
+ }
2595
+
2596
+ highlightDate(dateLike, color) {
2597
+ const normalized = this.#normalizeDateInput(dateLike);
2598
+ if (!normalized) return
2599
+
2600
+ const stamp = this.#toDateStamp(normalized);
2601
+ const existing = this.#highlightDateMap.get(stamp) || [];
2602
+ const nextColor = color == null || color === ''
2603
+ ? null
2604
+ : this.#sanitizeHighlightColor(color);
2605
+
2606
+ if (color != null && color !== '' && nextColor === undefined) return
2607
+
2608
+ existing.push(nextColor ?? null);
2609
+ this.#highlightDateMap.set(stamp, existing);
2610
+ this.options.highlightDates = this.#highlightMapToOptionsArray();
2611
+ this.#updateView(this.#viewNumber);
2612
+ }
2613
+
2614
+ unhighlightDate(dateLike, color) {
2615
+ const normalized = this.#normalizeDateInput(dateLike);
2616
+ if (!normalized) return
2617
+
2618
+ const stamp = this.#toDateStamp(normalized);
2619
+ const existing = this.#highlightDateMap.get(stamp);
2620
+ if (!existing?.length) return
2621
+
2622
+ if (color === undefined) {
2623
+ this.#highlightDateMap.delete(stamp);
2624
+ } else {
2625
+ const target = color == null || color === ''
2626
+ ? null
2627
+ : this.#sanitizeHighlightColor(color);
2628
+
2629
+ const index = existing.findIndex(item => item === target);
2630
+ if (index === -1) return
2631
+
2632
+ existing.splice(index, 1);
2633
+ if (existing.length) {
2634
+ this.#highlightDateMap.set(stamp, existing);
2635
+ } else {
2636
+ this.#highlightDateMap.delete(stamp);
2637
+ }
2638
+ }
2639
+
2640
+ this.options.highlightDates = this.#highlightMapToOptionsArray();
2641
+ this.#updateView(this.#viewNumber);
2642
+ }
2643
+
2644
+ isDateHighlighted(dateLike) {
2645
+ const normalized = this.#normalizeDateInput(dateLike);
2646
+ return normalized ? this.#isDateHighlighted(normalized) : false
2647
+ }
2648
+
2649
+ getHighlightColors(dateLike) {
2650
+ const normalized = this.#normalizeDateInput(dateLike);
2651
+ if (!normalized) return []
2652
+ return [...(this.#highlightDateMap.get(this.#toDateStamp(normalized)) || [])]
2653
+ }
2654
+
2655
+ getHighlightColor(dateLike) {
2656
+ return this.getHighlightColors(dateLike).find(color => color != null) ?? null
2657
+ }
2658
+
2659
+ goToDate(dateLike) {
2660
+ const normalized = this.#normalizeDateInput(dateLike);
2661
+ if (!normalized) return false
2662
+
2663
+ const clamped = this.#clampDateToRange(
2664
+ normalized,
2665
+ this.options.minDate,
2666
+ this.options.maxDate
2667
+ );
2668
+
2669
+ this.data.current_year = clamped.getFullYear();
2670
+ this.data.current_month = clamped.getMonth();
2671
+ this.data.current_decade = getDecade(clamped.getFullYear());
2672
+
2673
+ if (this.#viewNumber !== 0) {
2674
+ this.#viewNumber = 0;
2675
+ for (const period of this.#viewPeriodNames) {
2676
+ this.$container.classList.remove('RollDate__calendar__type--' + period + 's');
2677
+ }
2678
+ this.$container.classList.add('RollDate__calendar__type--days');
2679
+ }
2680
+
2681
+ this.#updateView(0);
2682
+ return true
2683
+ }
2684
+
2685
+ getValue() {
2686
+ if (this.options.selectType === 'single') {
2687
+ const date = this.#selectedDates[0];
2688
+ return date ? new Date(date.getTime()) : null
2689
+ }
2690
+
2691
+ return this.#selectedDates.map(date => new Date(date.getTime()))
2692
+ }
2693
+
2694
+ setValue(value) {
2695
+ if (value == null || value === '') {
2696
+ this.clearSelection();
2697
+ return true
2698
+ }
2699
+
2700
+ if (this.options.selectType === 'single') {
2701
+ const normalized = this.#normalizeDateInput(value);
2702
+ if (!normalized || this.#isDateDisabled(normalized)) return false
2703
+ this.#selectedDates = [this.#applyTimeToDate(normalized)];
2704
+ } else if (this.options.selectType === 'range') {
2705
+ const raw = Array.isArray(value) ? value : [value];
2706
+ const parsed = raw
2707
+ .map(item => this.#normalizeDateInput(item))
2708
+ .filter(Boolean)
2709
+ .map(date => this.#clampDateToRange(date, this.options.minDate, this.options.maxDate))
2710
+ .filter(date => !this.#isDateDisabled(date));
2711
+
2712
+ if (!parsed.length) return false
2713
+
2714
+ let start = parsed[0];
2715
+ let end = parsed.length > 1 ? parsed[1] : parsed[0];
2716
+ if (start > end) [start, end] = [end, start];
2717
+
2718
+ this.#selectedDates = parsed.length > 1
2719
+ ? [this.#applyTimeToDate(start), this.#applyTimeToDate(end)]
2720
+ : [this.#applyTimeToDate(start)];
2721
+ } else {
2722
+ const raw = Array.isArray(value) ? value : [value];
2723
+ const parsed = raw
2724
+ .map(item => this.#normalizeDateInput(item))
2725
+ .filter(Boolean)
2726
+ .map(date => this.#clampDateToRange(date, this.options.minDate, this.options.maxDate))
2727
+ .filter(date => !this.#isDateDisabled(date))
2728
+ .map(date => this.#applyTimeToDate(date));
2729
+
2730
+ if (!parsed.length) return false
2731
+
2732
+ const unique = [];
2733
+ const seen = new Set();
2734
+ parsed.forEach(date => {
2735
+ const stamp = this.#toDateStamp(date);
2736
+ if (seen.has(stamp)) return
2737
+ seen.add(stamp);
2738
+ unique.push(date);
2739
+ });
2740
+ this.#selectedDates = unique;
2741
+ }
2742
+
2743
+ this.#paintSelection();
2744
+ this.#notifySelectionChange();
2745
+ this.#updateInputValue();
2746
+ return true
2747
+ }
2748
+
2749
+ #dayElementStamp(dayEl) {
2750
+ return this.#toDateStamp(new Date(
2751
+ Number(dayEl.dataset.year),
2752
+ Number(dayEl.dataset.month),
2753
+ Number(dayEl.dataset.day)
2754
+ ))
2755
+ }
2756
+
2757
+ #findDayElement(date) {
2758
+ if (!(date instanceof Date)) return null
2759
+ return this.$container.querySelector(
2760
+ `.RollDate__calendar__day[data-year="${date.getFullYear()}"][data-month="${date.getMonth()}"][data-day="${date.getDate()}"]`
2761
+ )
2762
+ }
2763
+
2764
+ #paintRangeSelection() {
2765
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
2766
+ dayEl.classList.remove(
2767
+ 'RollDate__calendar__day--range-first',
2768
+ 'RollDate__calendar__day--range-last',
2769
+ 'RollDate__calendar__day--range-selected'
2770
+ );
2771
+ });
2772
+
2773
+ if (!this.#selectedDates.length) return
2774
+
2775
+ const firstStamp = this.#toDateStamp(this.#selectedDates[0]);
2776
+
2777
+ if (this.#selectedDates.length === 1) {
2778
+ this.#findDayElement(this.#selectedDates[0])
2779
+ ?.classList.add('RollDate__calendar__day--range-first');
2780
+ return
2781
+ }
2782
+
2783
+ const endStamp = this.#toDateStamp(this.#selectedDates[1]);
2784
+
2785
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
2786
+ const stamp = this.#dayElementStamp(dayEl);
2787
+
2788
+ if (stamp === firstStamp) {
2789
+ dayEl.classList.add('RollDate__calendar__day--range-first');
2790
+ } else if (stamp === endStamp) {
2791
+ dayEl.classList.add('RollDate__calendar__day--range-last');
2792
+ } else if (stamp > firstStamp && stamp < endStamp) {
2793
+ dayEl.classList.add('RollDate__calendar__day--range-selected');
2794
+ }
2795
+ });
2796
+ }
2797
+
2798
+ #paintSingleSelection() {
2799
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
2800
+ dayEl.classList.remove('RollDate__calendar__day--selected');
2801
+ });
2802
+
2803
+ const selected = this.#selectedDates[0];
2804
+ if (selected) {
2805
+ this.#findDayElement(selected)?.classList.add('RollDate__calendar__day--selected');
2806
+ }
2807
+ }
2808
+
2809
+ #paintMultiSelection() {
2810
+ const selectedStamps = new Set(this.#selectedDates.map(date => this.#toDateStamp(date)));
2811
+
2812
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
2813
+ if (selectedStamps.has(this.#dayElementStamp(dayEl))) {
2814
+ dayEl.classList.add('RollDate__calendar__day--selected');
2815
+ } else {
2816
+ dayEl.classList.remove('RollDate__calendar__day--selected');
2817
+ }
2818
+ });
2819
+ }
2820
+
2821
+ #paintSelection() {
2822
+ if (this.options.selectType === 'range') {
2823
+ this.#paintRangeSelection();
2824
+ return
2825
+ }
2826
+
2827
+ if (this.options.selectType === 'multi') {
2828
+ this.#paintMultiSelection();
2829
+ return
2830
+ }
2831
+
2832
+ this.#paintSingleSelection();
2833
+ }
2834
+
2204
2835
  #clearRangeSelection() {
2205
2836
  this.#selectedDates = [];
2206
2837
  this.$container.querySelectorAll('[data-day]').forEach(item => {
@@ -2371,6 +3002,10 @@ var RollDate = (function () {
2371
3002
  this.data.current_month = now.getMonth();
2372
3003
  this.data.current_decade = getDecade(now.getFullYear());
2373
3004
 
3005
+ if (this.options.enableTime && this.timePicker) {
3006
+ this.timePicker.setTime(now.getHours(), now.getMinutes());
3007
+ }
3008
+
2374
3009
  const selected = this.#applyTimeToDate(now);
2375
3010
 
2376
3011
  if (this.options.selectType === 'single') {
@@ -2378,7 +3013,8 @@ var RollDate = (function () {
2378
3013
  } else if (this.options.selectType === 'range') {
2379
3014
  this.#selectedDates = [selected];
2380
3015
  } else if (this.options.selectType === 'multi') {
2381
- const exists = this.#selectedDates.some(d => d.getTime() === selected.getTime());
3016
+ const selectedStamp = this.#toDateStamp(selected);
3017
+ const exists = this.#selectedDates.some(d => this.#toDateStamp(d) === selectedStamp);
2382
3018
  if (!exists) this.#selectedDates.push(selected);
2383
3019
  }
2384
3020
 
@@ -2398,9 +3034,10 @@ var RollDate = (function () {
2398
3034
  this.#clearMultiSelection();
2399
3035
  } else {
2400
3036
  this.#selectedDates = [];
3037
+ this.$container.querySelectorAll('[data-day].RollDate__calendar__day--selected')
3038
+ .forEach(el => el.classList.remove('RollDate__calendar__day--selected'));
2401
3039
  }
2402
3040
 
2403
- this.#updateView(this.#viewNumber);
2404
3041
  this.#notifySelectionChange();
2405
3042
  this.#updateInputValue();
2406
3043
  }