@rolldate/mcp 1.0.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.
@@ -0,0 +1,2429 @@
1
+ /*!
2
+ * RollDate
3
+ * Human-readable build for review and customization.
4
+ */
5
+
6
+ var RollDate = (function () {
7
+ 'use strict';
8
+
9
+ function getTranslation(text) {
10
+ return text
11
+ }
12
+ function getDecade(year) {
13
+ return Math.floor(year / 10) * 10
14
+ }
15
+
16
+ let lastHapticAt = 0;
17
+ let hapticAudioCtx = null;
18
+
19
+ function playSoftClick() {
20
+ try {
21
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
22
+ if (!AudioCtx) return
23
+ if (!hapticAudioCtx) hapticAudioCtx = new AudioCtx();
24
+ if (hapticAudioCtx.state === 'suspended') {
25
+ hapticAudioCtx.resume().catch(() => {});
26
+ }
27
+ const t = hapticAudioCtx.currentTime;
28
+ const osc = hapticAudioCtx.createOscillator();
29
+ const gain = hapticAudioCtx.createGain();
30
+ osc.type = 'triangle';
31
+ osc.frequency.value = 180;
32
+ gain.gain.setValueAtTime(0.0001, t);
33
+ gain.gain.exponentialRampToValueAtTime(0.045, t + 0.008);
34
+ gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.04);
35
+ osc.connect(gain);
36
+ gain.connect(hapticAudioCtx.destination);
37
+ osc.start(t);
38
+ osc.stop(t + 0.045);
39
+ } catch {
40
+ // Ignore audio unlock / autoplay failures.
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Light tick feedback for scroll snaps (month/year/time).
46
+ * Uses Vibration API when available; otherwise a soft click (helps on iOS).
47
+ */
48
+ function hapticTick(enabled = true) {
49
+ if (!enabled || typeof window === 'undefined') return
50
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
51
+ if (now - lastHapticAt < 28) return
52
+ lastHapticAt = now;
53
+
54
+ try {
55
+ if (typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function') {
56
+ navigator.vibrate(10);
57
+ return
58
+ }
59
+ } catch {
60
+ // Fall through to audio click.
61
+ }
62
+
63
+ playSoftClick();
64
+ }
65
+
66
+ function parseDate(str, format = 'auto') {
67
+ if (!str) return null
68
+
69
+ const clean = str.trim();
70
+ if (!clean) return null
71
+
72
+ if (str instanceof Date) return str
73
+
74
+ if (/^\d{4}-\d{2}-\d{2}$/.test(clean)) {
75
+ return new Date(clean)
76
+ }
77
+
78
+ if (format === 'auto') {
79
+
80
+ const separator = clean.match(/[-/.]/)?.[0] || '.';
81
+ const parts = clean.split(separator).map(Number);
82
+
83
+ if (parts.length !== 3) return null
84
+
85
+ const [a, b, c] = parts;
86
+ let year, month, day;
87
+
88
+ if (a >= 1000) {
89
+ [year, month, day] = [a, b, c];
90
+ }
91
+ else if (c >= 1000) {
92
+ [day, month, year] = [a, b, c];
93
+ }
94
+ else {
95
+ [day, month, year] = [a, b, c];
96
+ }
97
+
98
+ return new Date(year, month - 1, day)
99
+ }
100
+
101
+ const patterns = {
102
+ 'YYYY-MM-DD': /^(\d{4})[-/\.](\d{1,2})[-/\.](\d{1,2})$/,
103
+ 'DD/MM/YYYY': /^(\d{1,2})[-/\.](\d{1,2})[-/\.](\d{4})$/,
104
+ 'MM/DD/YYYY': /^(\d{1,2})[-/\.](\d{1,2})[-/\.](\d{4})$/,
105
+ 'DD.MM.YYYY': /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/,
106
+ 'MM.DD.YYYY': /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/
107
+ };
108
+
109
+ const pattern = patterns[format];
110
+ if (!pattern) {
111
+ console.warn(`RollDate: unknown format "${format}". Using auto-detect.`);
112
+ return parseDate(str, 'auto')
113
+ }
114
+
115
+ const match = clean.match(pattern);
116
+ if (!match) return null
117
+
118
+ let year, month, day;
119
+ switch (format) {
120
+ case 'YYYY-MM-DD':
121
+ [, year, month, day] = match;
122
+ break
123
+ case 'DD/MM/YYYY':
124
+ case 'DD.MM.YYYY':
125
+ [, day, month, year] = match;
126
+ break
127
+ case 'MM/DD/YYYY':
128
+ case 'MM.DD.YYYY':
129
+ [, month, day, year] = match;
130
+ break
131
+ }
132
+
133
+ return new Date(Number(year), Number(month) - 1, Number(day))
134
+ }
135
+
136
+ function formatDate(date, format = 'YYYY-MM-DD') {
137
+ if (!date || !(date instanceof Date) || isNaN(date)) return ''
138
+
139
+ const y = date.getFullYear();
140
+ const m = String(date.getMonth() + 1).padStart(2, '0');
141
+ const d = String(date.getDate()).padStart(2, '0');
142
+
143
+ const tokens = {
144
+ 'YYYY': y,
145
+ 'YY': String(y).slice(-2),
146
+ 'MM': m,
147
+ 'M': date.getMonth() + 1,
148
+ 'DD': d,
149
+ 'D': date.getDate()
150
+ };
151
+
152
+ return format.replace(/YYYY|YY|MM|M|DD|D/g, match => tokens[match] || match)
153
+ }
154
+
155
+ function getLocaleInputFormat(locale) {
156
+ const resolvedLocale = locale ||
157
+ (typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'en-US');
158
+ try {
159
+ const dtf = new Intl.DateTimeFormat(resolvedLocale, {
160
+ year: 'numeric',
161
+ month: '2-digit',
162
+ day: '2-digit'
163
+ });
164
+ const parts = dtf.formatToParts(new Date(2023, 5, 15)); // 15.06.2023
165
+ return parts.map(p => {
166
+ if (p.type === 'year') return 'YYYY'
167
+ if (p.type === 'month') return 'MM'
168
+ if (p.type === 'day') return 'DD'
169
+ return p.value
170
+ }).join('')
171
+ } catch {
172
+ return 'MM/DD/YYYY'
173
+ }
174
+ }
175
+
176
+ function checkDateFormat(date, format) {
177
+ return typeof date === 'string' ? parseDate(date, format) : date
178
+ }
179
+
180
+ function adjustToWeekStart(date, startMonday) {
181
+ const day = date.getDay();
182
+ const diff = (day - Number(startMonday) + 7) % 7;
183
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() - diff)
184
+ }
185
+
186
+ function adjustToWeekEnd(date, startMonday){
187
+ const day = date.getDay();
188
+ const diff = (6 - day + Number(startMonday) + 7) % 7;
189
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + diff)
190
+ }
191
+
192
+ class Observe {
193
+ constructor(container) {
194
+ this.$container = container;
195
+ this.intersecting = new Map();
196
+ this.init();
197
+ }
198
+ init() {
199
+ return this.observe = new IntersectionObserver(entries => {
200
+ entries.forEach(entry => {
201
+ const year = entry.target.dataset.year;
202
+ const month = entry.target.dataset?.month ? '_' + entry.target.dataset.month : '';
203
+ const day = entry.target.dataset?.day ? '_' + entry.target.dataset.day : '';
204
+
205
+ const key = `${year}${month}${day}`;
206
+ if (entry.isIntersecting) {
207
+ this.intersecting.set(key, entry);
208
+ } else {
209
+ this.intersecting.delete(key);
210
+ }
211
+ });
212
+ }, { threshold: 0.5 })
213
+ }
214
+ on(type, callback) {
215
+ this.$container.querySelectorAll(`.RollDate__calendar__${type}`).forEach(item => {
216
+ if (callback) callback(item);
217
+ this.observe.observe(item);
218
+ });
219
+ }
220
+ un() {
221
+ this.intersecting.clear();
222
+ this.$container.querySelectorAll(`.RollDate__calendar__day, .RollDate__calendar__month, .RollDate__calendar__year`).forEach(period => {
223
+ this.observe.unobserve(period);
224
+ });
225
+
226
+ }
227
+
228
+ dominant() {
229
+ if (this.intersecting.size === 0) return null
230
+
231
+ const counts = {};
232
+ let periods = [];
233
+ this.intersecting.forEach(entry => {
234
+ const keys = {};
235
+ periods = JSON.parse(entry.target.dataset.bind);
236
+ for (const key of periods) {
237
+ keys[key] = entry.target.dataset[key];
238
+ }
239
+
240
+ const key = Object.values(keys).join('-');
241
+ counts[key] = (counts[key] || 0) + 1;
242
+ });
243
+
244
+ const dominantKey = Object.keys(counts).reduce((a, b) =>
245
+ counts[a] > counts[b] ? a : b
246
+ );
247
+
248
+ const values = dominantKey.split('-').map(Number);
249
+
250
+ return Object.fromEntries(periods.map((k, i) => [k, values[i]]))
251
+ }
252
+
253
+ disconnect() {
254
+ this.observe?.disconnect();
255
+ }
256
+ }
257
+
258
+ class Data {
259
+ up_date = null
260
+ down_date = null
261
+ current_year
262
+ current_month
263
+ current_decade
264
+
265
+ constructor(options) {
266
+ this.options = options;
267
+ this.current_year = this.options.startDate.getFullYear();
268
+ this.current_month = this.options.startDate.getMonth();
269
+ this.current_decade = getDecade(this.current_year);
270
+ }
271
+
272
+ get currentPeriod() {
273
+ return {
274
+ year: this.current_year,
275
+ month: this.current_month,
276
+ decade: this.current_decade
277
+ }
278
+ }
279
+
280
+ setPeriod(period) {
281
+ if ('year' in period) this.current_year = period.year;
282
+ if ('month' in period) this.current_month = period.month;
283
+ if ('decade' in period) this.current_decade = period.decade;
284
+ }
285
+
286
+ getDates() {
287
+ let startDate = new Date(this.current_year, this.current_month - 3, 1);
288
+ let endDate = new Date(this.current_year, this.current_month + 3, 0);
289
+
290
+ if (this.options.minDate) {
291
+ if (this.options.minDate > startDate) {
292
+ startDate = new Date(
293
+ this.options.minDate.getFullYear(),
294
+ this.options.minDate.getMonth(),
295
+ 1
296
+ );
297
+ }
298
+ }
299
+
300
+ if (this.options.maxDate) {
301
+ if (this.options.maxDate < endDate) {
302
+ endDate = new Date(
303
+ this.options.maxDate.getFullYear(),
304
+ this.options.maxDate.getMonth() + 1,
305
+ 0
306
+ );
307
+ }
308
+ }
309
+
310
+ if (
311
+ this.options.minDate &&
312
+ this.options.maxDate &&
313
+ this.options.minDate.getFullYear() === this.options.maxDate.getFullYear() &&
314
+ this.options.minDate.getMonth() === this.options.maxDate.getMonth()
315
+ ) {
316
+ const y = this.options.minDate.getFullYear();
317
+ const m = this.options.minDate.getMonth();
318
+ startDate = new Date(y, m, 1);
319
+ endDate = new Date(y, m + 1, 1);
320
+ }
321
+
322
+ startDate = adjustToWeekStart(startDate, this.options.startWeekFromMonday);
323
+ endDate = adjustToWeekEnd(endDate, this.options.startWeekFromMonday);
324
+
325
+ const daysBetween = Math.floor((endDate - startDate) / (24 * 60 * 60 * 1000)) + 1;
326
+ if (daysBetween < 42) {
327
+
328
+ const missing = 42 - daysBetween;
329
+ const addStart = Math.ceil(missing / 2);
330
+ const addEnd = Math.floor(missing / 2);
331
+
332
+ startDate.setDate(startDate.getDate() - addStart);
333
+ endDate.setDate(endDate.getDate() + addEnd);
334
+
335
+ startDate = adjustToWeekStart(startDate, this.options.startWeekFromMonday);
336
+ endDate = adjustToWeekEnd(endDate, this.options.startWeekFromMonday);
337
+ }
338
+
339
+ this.up_date = new Date(startDate);
340
+ this.down_date = new Date(endDate);
341
+
342
+ const dates = [];
343
+ for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
344
+ const isDisabledByCustomRule = this.options.isDateDisabled ? this.options.isDateDisabled(d) : false;
345
+ const isDisabled = (
346
+ (this.options.minDate && d < this.options.minDate) ||
347
+ (this.options.maxDate && d > this.options.maxDate) ||
348
+ isDisabledByCustomRule
349
+ );
350
+ dates.push({
351
+ date: new Date(d),
352
+ disabled: isDisabled
353
+ });
354
+ }
355
+
356
+ return dates
357
+ }
358
+
359
+ getMonthsOrYears(period) {
360
+
361
+ let upYear = this.current_year - (period === 'month' ? 5 : 18);
362
+ let downYear = this.current_year + (period === 'month' ? 5 : 18);
363
+
364
+ if (this.options.minDate) {
365
+ const minYear = this.options.minDate.getFullYear();
366
+ if (minYear > upYear) upYear = minYear;
367
+ }
368
+ if (this.options.maxDate) {
369
+ const maxYear = this.options.maxDate.getFullYear();
370
+ if (maxYear < downYear) downYear = maxYear;
371
+ }
372
+
373
+ if (period === 'month') {
374
+
375
+ if (
376
+ this.options.minDate &&
377
+ this.options.maxDate &&
378
+ this.options.minDate.getFullYear() === this.options.maxDate.getFullYear()
379
+ ) {
380
+ const y = this.options.minDate.getFullYear();
381
+ upYear = y - 2;
382
+ downYear = y + 2;
383
+ }
384
+
385
+ }
386
+
387
+ // For month/year views bounds must match the currently generated window.
388
+ // Otherwise, after switching view type (e.g. month -> year near edges),
389
+ // stale bounds can cause endless add/remove virtualization loops.
390
+ this.up_date = new Date(upYear, 0, 1);
391
+ this.down_date = new Date(downYear + 1, 0, 0);
392
+
393
+ const items = [];
394
+
395
+ for (let y = upYear; y <= downYear; y++) {
396
+ if (period === 'month') {
397
+ for (let m = 0; m < 12; m++) {
398
+ const date = new Date(y, m, 1);
399
+ const monthEnd = new Date(y, m + 1, 0);
400
+
401
+ let disabled = false;
402
+
403
+ if (this.options.minDate && monthEnd < this.options.minDate) {
404
+ disabled = true;
405
+ } else if (this.options.maxDate && date > this.options.maxDate) {
406
+ disabled = true;
407
+ }
408
+
409
+ items.push({ date, disabled });
410
+ }
411
+ } else { // 'year'
412
+ const date = new Date(y, 0, 1);
413
+ let disabled = false;
414
+
415
+ if (this.options.minDate && y < this.options.minDate.getFullYear()) {
416
+ disabled = true;
417
+ } else if (this.options.maxDate && y > this.options.maxDate.getFullYear()) {
418
+ disabled = true;
419
+ }
420
+
421
+ items.push({ date, disabled });
422
+ }
423
+ }
424
+
425
+ if (items.length < 16) {
426
+ let extraYear = downYear + 1;
427
+ while (items.length < 16) {
428
+ if (period === 'month') {
429
+ for (let m = 0; m < 12 && items.length < 16; m++) {
430
+ items.push({
431
+ date: new Date(extraYear, m, 1),
432
+ disabled: true
433
+ });
434
+ }
435
+ } else {
436
+ items.push({
437
+ date: new Date(extraYear, 0, 1),
438
+ disabled: true
439
+ });
440
+ }
441
+ extraYear++;
442
+ }
443
+ }
444
+
445
+ return items;
446
+ }
447
+ }
448
+
449
+ class Render {
450
+ constructor(options) {
451
+ this.$container = options.container;
452
+ this.$trigger = options.trigger;
453
+ this.startWeekFromMonday = options.startWeekFromMonday;
454
+ this.monthsNames = options.monthsNames;
455
+ this.monthsShortNames = options.monthsShortNames;
456
+ this.enableTime = Boolean(options.enableTime);
457
+ this.hasFooter = Boolean(options.enableTime || (options.footerButtons && options.footerButtons.length));
458
+
459
+ this.init();
460
+ }
461
+
462
+ init() {
463
+ const footerHtml = this.hasFooter ? `
464
+ <div class="RollDate__footer">
465
+ ${this.enableTime ? '<div class="RollDate__time"></div>' : ''}
466
+ <div class="RollDate__footer__buttons"></div>
467
+ </div>
468
+ ` : '';
469
+
470
+ 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>
475
+ </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>
487
+ </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>
498
+ </div>
499
+ </div>
500
+ </div>
501
+ ${footerHtml}
502
+ `;
503
+
504
+ if ( this.$trigger.nodeName === 'DIV' || this.$trigger.nodeName === 'SPAN' || this.$trigger.nodeName === 'SECTION' ) {
505
+ this.$trigger.append(this.$container);
506
+ } else {
507
+ document.body.append(this.$container);
508
+ }
509
+ }
510
+
511
+ #dayHeaders() {
512
+ const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
513
+ let header = '';
514
+ const startIndex = Number(this.startWeekFromMonday);
515
+ for (let i = startIndex; i < 7 + startIndex; i++) {
516
+ header += `<div class="RollDate__calendar__header__weekday">${getTranslation(weekDays[i % 7])}</div>`;
517
+ }
518
+ return header
519
+ }
520
+
521
+ dates(array, selectedDates = [], selectType = 'single') {
522
+ let datesHtml = '';
523
+ const today = new Date();
524
+
525
+ for (let i = 0; i < array.length; i++) {
526
+ const date = array[i].date;
527
+ const disabledClass = array[i].disabled ? 'RollDate__calendar__day--disabled' : '';
528
+ const isToday = date.toDateString() === today.toDateString();
529
+
530
+ // Resolve selection class for current date cell.
531
+ let selectionClass = '';
532
+ if (selectType === 'single' || selectType === 'multi') {
533
+ const isSelected = selectedDates.some(
534
+ d => d.getTime() === date.getTime()
535
+ );
536
+ if (isSelected) selectionClass = 'RollDate__calendar__day--selected';
537
+ } else if (selectType === 'range' && selectedDates.length === 2) {
538
+ const [start, end] = selectedDates;
539
+ const time = date.getTime();
540
+ if (time === start.getTime()) {
541
+ selectionClass = 'RollDate__calendar__day--range-first';
542
+ } else if (time === end.getTime()) {
543
+ selectionClass = 'RollDate__calendar__day--range-last';
544
+ } else if (time > start.getTime() && time < end.getTime()) {
545
+ selectionClass = 'RollDate__calendar__day--range-selected';
546
+ }
547
+ }
548
+
549
+ datesHtml += `<div class="RollDate__calendar__day ${disabledClass} ${isToday ? 'RollDate__calendar__day--today' : ''} ${selectionClass}"
550
+ data-bind='["year", "month"]'
551
+ data-year="${date.getFullYear()}"
552
+ data-month="${date.getMonth()}"
553
+ data-day="${date.getDate()}">${date.getDate()}</div>`;
554
+ }
555
+
556
+ return datesHtml
557
+ }
558
+ months(array) {
559
+ let monthsHtml = '';
560
+ for (let i = 0; i < array.length; i++) {
561
+ const date = new Date(array[i].date);
562
+ const isDisabled = array[i].disabled;
563
+ const month = date.getMonth();
564
+
565
+ let currentClass = '';
566
+ if (date.getFullYear() === new Date().getFullYear() && month === new Date().getMonth()) {
567
+ currentClass = 'RollDate__calendar__month--current';
568
+ }
569
+
570
+ monthsHtml += `<div class="RollDate__calendar__month${isDisabled ? ' RollDate__calendar__month--disabled' : ''} ${currentClass}"
571
+ data-bind='["year"]'
572
+ data-month="${month}"
573
+ data-year="${date.getFullYear()}"
574
+ data-click="day">
575
+ ${this.monthsShortNames[month]}
576
+ </div>`;
577
+ }
578
+
579
+ return monthsHtml
580
+ }
581
+
582
+ years(array) {
583
+ let yearsHtml = '';
584
+
585
+ for (let i = 0; i < array.length; i++) {
586
+ const date = new Date(array[i].date);
587
+ const isDisabled = array[i].disabled;
588
+ const year = date.getFullYear();
589
+
590
+ let currentClass = '';
591
+ if (year === new Date().getFullYear()) {
592
+ currentClass = 'RollDate__calendar__year--current';
593
+ }
594
+
595
+ yearsHtml += `<div class="RollDate__calendar__year${isDisabled ? ' RollDate__calendar__year--disabled' : ''} ${currentClass}"
596
+ data-bind='["decade"]'
597
+ data-decade="${Math.floor(year / 10) * 10}"
598
+ data-month="0"
599
+ data-year="${year}"
600
+ data-click="month">
601
+ ${year}
602
+ </div>`;
603
+ }
604
+
605
+ return yearsHtml
606
+ }
607
+
608
+ clear(block) {
609
+ block.innerHTML = '';
610
+ }
611
+ }
612
+
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
+ }
855
+ }
856
+
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
+ }
1102
+ }
1103
+
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>`;
1109
+
1110
+ class TimePicker {
1111
+ #hours = 0
1112
+ #minutes = 0
1113
+ #use12Hour = false
1114
+ #period = 'AM'
1115
+ #minuteStep = 1
1116
+ #columns = []
1117
+ #displayEl = null
1118
+
1119
+ constructor(root, options = {}) {
1120
+ this.root = root;
1121
+ this.#use12Hour = Boolean(options.use12Hour);
1122
+ this.#minuteStep = Math.max(1, Number(options.minuteStep) || 1);
1123
+ this.#hours = this.#clamp24Hour(options.hours ?? 0);
1124
+ this.#minutes = this.#normalizeMinute(options.minutes ?? 0);
1125
+ if (this.#use12Hour) {
1126
+ this.#period = this.#hours >= 12 ? 'PM' : 'AM';
1127
+ }
1128
+ this.onChange = options.onChange || (() => {});
1129
+ this.hapticFeedback = options.hapticFeedback !== false;
1130
+ this.#build();
1131
+ }
1132
+
1133
+ #clamp24Hour(value) {
1134
+ const h = Number(value);
1135
+ if (Number.isNaN(h)) return 0
1136
+ return Math.min(23, Math.max(0, h))
1137
+ }
1138
+
1139
+ #displayHour() {
1140
+ if (!this.#use12Hour) return this.#hours
1141
+ const h = this.#hours % 12;
1142
+ return h === 0 ? 12 : h
1143
+ }
1144
+
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))
1149
+ }
1150
+
1151
+ #build() {
1152
+ const hourValues = this.#use12Hour
1153
+ ? Array.from({ length: 12 }, (_, i) => i + 1)
1154
+ : Array.from({ length: 24 }, (_, i) => i);
1155
+
1156
+ const minuteValues = [];
1157
+ for (let m = 0; m < 60; m += this.#minuteStep) {
1158
+ minuteValues.push(m);
1159
+ }
1160
+
1161
+ this.root.innerHTML = `
1162
+ <div class="RollDate__time__picker">
1163
+ <div class="RollDate__time__field" data-unit="hour">
1164
+ <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
+ <div class="RollDate__time__viewport">
1167
+ <div class="RollDate__time__list"></div>
1168
+ </div>
1169
+ <button type="button" class="RollDate__time__arrow RollDate__time__arrow--next" data-dir="next" aria-label="Later hour">${ARROW_SVG}</button>
1170
+ </div>
1171
+ </div>
1172
+ <span class="RollDate__time__sep">:</span>
1173
+ <div class="RollDate__time__field" data-unit="minute">
1174
+ <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
+ <div class="RollDate__time__viewport">
1187
+ <div class="RollDate__time__list"></div>
1188
+ </div>
1189
+ <button type="button" class="RollDate__time__arrow RollDate__time__arrow--next" data-dir="next" aria-label="Later period">${ARROW_SVG}</button>
1190
+ </div>
1191
+ </div>` : ''}
1192
+ </div>
1193
+ <div class="RollDate__time__display" aria-live="polite"></div>
1194
+ `;
1195
+
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()));
1202
+ 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
+ }
1209
+
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}`
1217
+ }
1218
+
1219
+ #updateDisplay() {
1220
+ if (this.#displayEl) {
1221
+ this.#displayEl.textContent = this.#formatDisplay();
1222
+ }
1223
+ }
1224
+
1225
+ #createColumn(unit, values, initial) {
1226
+ const field = this.root.querySelector(`[data-unit="${unit}"]`);
1227
+ const viewport = field.querySelector('.RollDate__time__viewport');
1228
+ const list = field.querySelector('.RollDate__time__list');
1229
+
1230
+ list.innerHTML = values.map((value) => {
1231
+ const label = unit === 'minute'
1232
+ ? String(value).padStart(2, '0')
1233
+ : String(value);
1234
+ return `<div class="RollDate__time__item" data-value="${value}">${label}</div>`
1235
+ }).join('');
1236
+
1237
+ list.style.paddingTop = `${ITEM_HEIGHT}px`;
1238
+ list.style.paddingBottom = `${ITEM_HEIGHT}px`;
1239
+
1240
+ const column = {
1241
+ unit,
1242
+ viewport,
1243
+ list,
1244
+ values,
1245
+ offset: 0
1246
+ };
1247
+
1248
+ column.updateActive = () => {
1249
+ const index = column.indexFromOffset();
1250
+ if (column._lastIndex !== index) {
1251
+ if (column._lastIndex !== undefined) {
1252
+ hapticTick(this.hapticFeedback);
1253
+ }
1254
+ column._lastIndex = index;
1255
+ }
1256
+ list.querySelectorAll('.RollDate__time__item').forEach((el, i) => {
1257
+ el.classList.toggle('RollDate__time__item--active', i === index);
1258
+ });
1259
+ };
1260
+
1261
+ column.indexFromOffset = () => {
1262
+ const raw = Math.round(-column.offset / ITEM_HEIGHT);
1263
+ return Math.min(values.length - 1, Math.max(0, raw))
1264
+ };
1265
+
1266
+ column.getValue = () => values[column.indexFromOffset()];
1267
+
1268
+ column.apply = (animate) => {
1269
+ list.style.transition = animate ? 'transform 0.2s ease' : 'none';
1270
+ list.style.transform = `translateY(${column.offset}px)`;
1271
+ column.updateActive();
1272
+ };
1273
+
1274
+ column.snap = () => {
1275
+ const index = column.indexFromOffset();
1276
+ column.offset = -index * ITEM_HEIGHT;
1277
+ column.apply(true);
1278
+ this.#readColumns();
1279
+ this.#updateDisplay();
1280
+ this.onChange(this.getTime());
1281
+ };
1282
+
1283
+ column.scrollToValue = (value, animate = true) => {
1284
+ const index = values.indexOf(value);
1285
+ if (index < 0) return
1286
+ column.offset = -index * ITEM_HEIGHT;
1287
+ column.apply(animate);
1288
+ };
1289
+
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());
1297
+ };
1298
+
1299
+ column.scrollToValue(initial, false);
1300
+ this.#bindWheel(column);
1301
+ this.#bindTouch(column);
1302
+ this.#bindArrows(field, column);
1303
+
1304
+ return column
1305
+ }
1306
+
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);
1315
+ });
1316
+ }
1317
+
1318
+ #bindWheel(column) {
1319
+ column.viewport.addEventListener('wheel', (e) => {
1320
+ e.preventDefault();
1321
+ e.stopPropagation();
1322
+ column.list.style.transition = 'none';
1323
+ column.offset -= e.deltaY * 0.35;
1324
+ const min = -(column.values.length - 1) * ITEM_HEIGHT;
1325
+ column.offset = Math.max(min, Math.min(0, column.offset));
1326
+ column.list.style.transform = `translateY(${column.offset}px)`;
1327
+ column.updateActive();
1328
+ clearTimeout(column._snapTimer);
1329
+ column._snapTimer = setTimeout(() => column.snap(), 90);
1330
+ }, { passive: false });
1331
+ }
1332
+
1333
+ #bindTouch(column) {
1334
+ let startY = 0;
1335
+ let startOffset = 0;
1336
+
1337
+ column.viewport.addEventListener('touchstart', (e) => {
1338
+ if (!e.touches || e.touches.length !== 1) return
1339
+ startY = e.touches[0].clientY;
1340
+ startOffset = column.offset;
1341
+ column.list.style.transition = 'none';
1342
+ }, { passive: true });
1343
+
1344
+ column.viewport.addEventListener('touchmove', (e) => {
1345
+ if (!e.touches || e.touches.length !== 1) return
1346
+ e.preventDefault();
1347
+ column.offset = startOffset + (e.touches[0].clientY - startY);
1348
+ const min = -(column.values.length - 1) * ITEM_HEIGHT;
1349
+ column.offset = Math.max(min, Math.min(0, column.offset));
1350
+ column.list.style.transform = `translateY(${column.offset}px)`;
1351
+ column.updateActive();
1352
+ }, { passive: false });
1353
+
1354
+ column.viewport.addEventListener('touchend', () => column.snap());
1355
+ }
1356
+
1357
+ #readColumns() {
1358
+ const hourCol = this.#columns.find(c => c.unit === 'hour');
1359
+ const minuteCol = this.#columns.find(c => c.unit === 'minute');
1360
+ const periodCol = this.#columns.find(c => c.unit === 'period');
1361
+
1362
+ const hourValue = hourCol.getValue();
1363
+ this.#minutes = minuteCol.getValue();
1364
+
1365
+ if (this.#use12Hour && periodCol) {
1366
+ this.#period = periodCol.getValue();
1367
+ if (this.#period === 'AM') {
1368
+ this.#hours = hourValue === 12 ? 0 : hourValue;
1369
+ } else {
1370
+ this.#hours = hourValue === 12 ? 12 : hourValue + 12;
1371
+ }
1372
+ } else {
1373
+ this.#hours = hourValue;
1374
+ }
1375
+ }
1376
+
1377
+ getTime() {
1378
+ return { hours: this.#hours, minutes: this.#minutes }
1379
+ }
1380
+
1381
+ setTime(hours, minutes) {
1382
+ this.#hours = this.#clamp24Hour(hours);
1383
+ this.#minutes = this.#normalizeMinute(minutes);
1384
+ if (this.#use12Hour) {
1385
+ this.#period = this.#hours >= 12 ? 'PM' : 'AM';
1386
+ this.#columns.find(c => c.unit === 'hour')?.scrollToValue(this.#displayHour());
1387
+ this.#columns.find(c => c.unit === 'period')?.scrollToValue(this.#period);
1388
+ } else {
1389
+ this.#columns.find(c => c.unit === 'hour')?.scrollToValue(this.#hours);
1390
+ }
1391
+ this.#columns.find(c => c.unit === 'minute')?.scrollToValue(this.#minutes);
1392
+ this.#updateDisplay();
1393
+ }
1394
+
1395
+ destroy() {
1396
+ this.#columns = [];
1397
+ this.root.innerHTML = '';
1398
+ }
1399
+ }
1400
+
1401
+ class RollDate {
1402
+ static #instances = new Set()
1403
+
1404
+ #wheelHandler
1405
+ #viewNumber = 0
1406
+ #viewPeriodNames = ['day', 'month', 'year']
1407
+ #selectedDates = []
1408
+ #firstOpen = true
1409
+ #disabledDateStamps = new Set()
1410
+ #docClickHandler = null
1411
+ #openTriggers = []
1412
+
1413
+ #clampDateToRange(date, minDate, maxDate) {
1414
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return minDate
1415
+ if (date < minDate) return new Date(minDate)
1416
+ if (date > maxDate) return new Date(maxDate)
1417
+ return date
1418
+ }
1419
+
1420
+ #toDateStamp(date) {
1421
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
1422
+ }
1423
+
1424
+ #normalizeDateInput(dateLike) {
1425
+ const date = checkDateFormat(dateLike, this.options.dateFormat);
1426
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null
1427
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate())
1428
+ }
1429
+
1430
+ #buildDisabledDateSet(dates = []) {
1431
+ const set = new Set();
1432
+ dates.forEach(dateLike => {
1433
+ const normalized = this.#normalizeDateInput(dateLike);
1434
+ if (normalized) set.add(this.#toDateStamp(normalized));
1435
+ });
1436
+ return set
1437
+ }
1438
+
1439
+ #disableInputAssist(input) {
1440
+ if (!input || input.tagName !== 'INPUT') return
1441
+ input.setAttribute('autocomplete', 'off');
1442
+ input.setAttribute('autocorrect', 'off');
1443
+ input.setAttribute('autocapitalize', 'off');
1444
+ input.setAttribute('spellcheck', 'false');
1445
+ }
1446
+
1447
+ #isDateDisabled(date) {
1448
+ return this.#disabledDateStamps.has(this.#toDateStamp(date))
1449
+ }
1450
+
1451
+ #notifySelectionChange() {
1452
+ if (this.options.selectType === 'single') {
1453
+ this.options.selectDate(this.#selectedDates[0] || null);
1454
+ return
1455
+ }
1456
+ this.options.selectDate([...this.#selectedDates]);
1457
+ }
1458
+
1459
+ #syncSelectedWithDisabledDates() {
1460
+ const prevLength = this.#selectedDates.length;
1461
+ this.#selectedDates = this.#selectedDates.filter(date => !this.#isDateDisabled(date));
1462
+ if (prevLength !== this.#selectedDates.length) {
1463
+ this.#notifySelectionChange();
1464
+ this.#updateInputValue();
1465
+ }
1466
+ }
1467
+
1468
+ #applyTimeToDate(date) {
1469
+ const d = new Date(date);
1470
+ if (!this.options.enableTime) return d
1471
+ const time = this.timePicker?.getTime() || {
1472
+ hours: this.options.startDate.getHours(),
1473
+ minutes: this.options.startDate.getMinutes()
1474
+ };
1475
+ d.setHours(time.hours, time.minutes, 0, 0);
1476
+ return d
1477
+ }
1478
+
1479
+ #formatDateTime(date) {
1480
+ const datePart = formatDate(date, this.options.dateFormat);
1481
+
1482
+ if (!this.options.enableTime) return datePart
1483
+
1484
+ const h = String(date.getHours()).padStart(2, '0');
1485
+ const m = String(date.getMinutes()).padStart(2, '0');
1486
+ return `${datePart} ${h}:${m}`
1487
+ }
1488
+
1489
+ #initFooterButtons() {
1490
+ if (!this.dom.$footer_buttons) return
1491
+
1492
+ const buttons = Array.isArray(this.options.footerButtons)
1493
+ ? this.options.footerButtons
1494
+ : [];
1495
+
1496
+ this.dom.$footer_buttons.innerHTML = '';
1497
+ buttons.forEach((cfg) => {
1498
+ if (!cfg?.text) return
1499
+ const btn = document.createElement('button');
1500
+ btn.type = 'button';
1501
+ btn.className = 'RollDate__footer__button';
1502
+ btn.textContent = cfg.text;
1503
+ btn.addEventListener('click', (e) => {
1504
+ e.preventDefault();
1505
+ e.stopPropagation();
1506
+ this.#handleFooterButton(cfg);
1507
+ });
1508
+ this.dom.$footer_buttons.append(btn);
1509
+ });
1510
+ }
1511
+
1512
+ #handleFooterButton(cfg) {
1513
+ if (cfg.action === 'today') {
1514
+ this.selectToday();
1515
+ return
1516
+ }
1517
+ if (cfg.action === 'clear') {
1518
+ this.clearSelection();
1519
+ return
1520
+ }
1521
+ if (typeof cfg.onClick === 'function') {
1522
+ cfg.onClick(this);
1523
+ }
1524
+ }
1525
+
1526
+ #initTimePicker() {
1527
+ const start = this.options.startDate;
1528
+ this.timePicker = new TimePicker(this.dom.$time, {
1529
+ hours: start.getHours(),
1530
+ minutes: start.getMinutes(),
1531
+ use12Hour: this.options.use12Hour,
1532
+ minuteStep: this.options.timeStep,
1533
+ hapticFeedback: this.options.hapticFeedback !== false,
1534
+ onChange: () => {
1535
+ if (!this.#selectedDates.length) return
1536
+ this.#selectedDates = this.#selectedDates.map(date => this.#applyTimeToDate(date));
1537
+ this.#notifySelectionChange();
1538
+ this.#updateInputValue();
1539
+ }
1540
+ });
1541
+ }
1542
+
1543
+ constructor(selector, options = {}) {
1544
+ if (options === null || options === undefined) options = {};
1545
+
1546
+ this.triggerSelector = options.triggerSelector;
1547
+
1548
+ if (Array.isArray(selector)) {
1549
+ this.$startInput = document.querySelector(selector[0]);
1550
+ this.$endInput = document.querySelector(selector[1]);
1551
+ this.$trigger = this.$startInput;
1552
+ this.mode = 'popup';
1553
+ } else {
1554
+ this.$trigger = document.querySelector(selector);
1555
+
1556
+ if (this.triggerSelector) {
1557
+ this.$openTrigger = document.querySelector(this.triggerSelector);
1558
+ this.mode = 'popup';
1559
+ } else if (this.$trigger.tagName === 'INPUT') {
1560
+ this.mode = 'popup';
1561
+ } else {
1562
+ this.mode = 'inline';
1563
+ }
1564
+ }
1565
+
1566
+ const today = new Date();
1567
+
1568
+ const baseOptions = {
1569
+ mode: 'auto',
1570
+ theme: 'dark',
1571
+ startWeekFromMonday: true,
1572
+ selectType: 'single',
1573
+ monthsNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
1574
+ monthsShortNames: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1575
+ weekDaysNames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
1576
+ selectDate: dates => console.log(dates),
1577
+ onOpen: () => {},
1578
+ onClose: () => {},
1579
+ onViewChange: () => {},
1580
+ onHoverDate: () => {},
1581
+ disabledDates: [],
1582
+ closeOnSelect: true,
1583
+ enableTime: false,
1584
+ use12Hour: false,
1585
+ timeStep: 1,
1586
+ footerButtons: [],
1587
+ hapticFeedback: true,
1588
+ ...options
1589
+ };
1590
+
1591
+ if (baseOptions.theme === 'default') {
1592
+ baseOptions.theme = 'dark';
1593
+ }
1594
+
1595
+ const resolvedLocale = baseOptions.locale ||
1596
+ (typeof navigator !== 'undefined' ? navigator.language : undefined);
1597
+ baseOptions.dateFormat = baseOptions.dateFormat || getLocaleInputFormat(resolvedLocale);
1598
+
1599
+ const parsedDates = {
1600
+ startDate: checkDateFormat(
1601
+ options.startDate !== undefined ? options.startDate : today,
1602
+ baseOptions.dateFormat
1603
+ ),
1604
+ minDate: checkDateFormat(
1605
+ options.minDate !== undefined ? options.minDate : new Date(today.getFullYear() - 100, today.getMonth(), 1),
1606
+ baseOptions.dateFormat
1607
+ ),
1608
+ maxDate: checkDateFormat(
1609
+ options.maxDate !== undefined ? options.maxDate : new Date(today.getFullYear() + 100, today.getMonth() + 1, 0),
1610
+ baseOptions.dateFormat
1611
+ )
1612
+ };
1613
+
1614
+ this.options = {
1615
+ ...baseOptions,
1616
+ ...parsedDates
1617
+ };
1618
+
1619
+ if (this.options.minDate > this.options.maxDate) {
1620
+ console.error('Date Error: maxDate is less than minDate!');
1621
+ }
1622
+
1623
+ this.options.startDate = this.#clampDateToRange(
1624
+ this.options.startDate,
1625
+ this.options.minDate,
1626
+ this.options.maxDate
1627
+ );
1628
+ this.#disabledDateStamps = this.#buildDisabledDateSet(this.options.disabledDates);
1629
+ if (this.#isDateDisabled(this.options.startDate)) {
1630
+ this.options.startDate = new Date(this.options.minDate);
1631
+ }
1632
+
1633
+ this.#init();
1634
+ RollDate.#instances.add(this);
1635
+ }
1636
+
1637
+ #closeOtherPopups() {
1638
+ for (const instance of RollDate.#instances) {
1639
+ if (instance !== this && instance.mode === 'popup') {
1640
+ instance.close();
1641
+ }
1642
+ }
1643
+ }
1644
+
1645
+ #init() {
1646
+ if (typeof this.options !== 'object' || this.options === null) {
1647
+ console.error('RollDate: options is not an object!');
1648
+ return
1649
+ }
1650
+
1651
+ this.$container = document.createElement('div');
1652
+ this.$container.className = `RollDate__container RollDate__calendar__type--days RollDate__theme_${this.options.theme}`;
1653
+
1654
+ this.#disableInputAssist(this.$trigger);
1655
+ this.#disableInputAssist(this.$startInput);
1656
+ this.#disableInputAssist(this.$endInput);
1657
+
1658
+ if (this.mode === 'popup') {
1659
+ document.body.appendChild(this.$container);
1660
+ this.$container.style.display = 'none';
1661
+ } else {
1662
+ if (this.$trigger.nodeName === 'DIV' || this.$trigger.nodeName === 'SPAN' || this.$trigger.nodeName === 'SECTION') {
1663
+ this.$trigger.append(this.$container);
1664
+ } else {
1665
+ document.body.append(this.$container);
1666
+ }
1667
+ }
1668
+
1669
+ if (this.options.enableTime) {
1670
+ this.$container.classList.add('RollDate__has-time');
1671
+ }
1672
+ if (this.options.footerButtons?.length || this.options.enableTime) {
1673
+ this.$container.classList.add('RollDate__has-footer');
1674
+ }
1675
+
1676
+ this.render = new Render({
1677
+ container: this.$container,
1678
+ trigger: this.$trigger,
1679
+ startWeekFromMonday: this.options.startWeekFromMonday,
1680
+ monthsNames: this.options.monthsNames,
1681
+ monthsShortNames: this.options.monthsShortNames,
1682
+ weekDays: this.options.weekDaysNames,
1683
+ enableTime: this.options.enableTime,
1684
+ footerButtons: this.options.footerButtons
1685
+ });
1686
+
1687
+ this.dom ={
1688
+ $year: this.$container.querySelector('.RollDate__header__year'),
1689
+ $month: this.$container.querySelector('.RollDate__header__month'),
1690
+ $years_block: this.$container.querySelector('.RollDate__calendar__years'),
1691
+ $months_block: this.$container.querySelector('.RollDate__calendar__months'),
1692
+ $days_block: this.$container.querySelector('.RollDate__calendar__days'),
1693
+ $body: this.$container.querySelector('.RollDate__calendar__body'),
1694
+ $type_switcher: this.$container.querySelector('.RollDate__calendar__switcher'),
1695
+ $page_switcher: this.$container.querySelectorAll('.RollDate__calendar__button'),
1696
+ $footer_buttons: this.$container.querySelector('.RollDate__footer__buttons'),
1697
+ $time: this.$container.querySelector('.RollDate__time'),
1698
+ };
1699
+
1700
+ this.#initFooterButtons();
1701
+ if (this.options.enableTime && this.dom.$time) {
1702
+ this.#initTimePicker();
1703
+ }
1704
+
1705
+ this.virtualizer = new Virtualizer(this);
1706
+
1707
+ this.#attachEvents();
1708
+
1709
+ this.observe = new Observe(this.$container);
1710
+
1711
+ this.data = new Data({
1712
+ startDate: this.options.startDate,
1713
+ minDate: this.options.minDate,
1714
+ maxDate: this.options.maxDate,
1715
+ startWeekFromMonday: this.options.startWeekFromMonday,
1716
+ isDateDisabled: date => this.#isDateDisabled(date)
1717
+ });
1718
+
1719
+ this.#updateView(this.#viewNumber);
1720
+ }
1721
+
1722
+ #positionCalendar() {
1723
+ if (this.mode !== 'popup') return
1724
+
1725
+ if (window.innerWidth <= 380) {
1726
+ this.$container.style.position = 'fixed';
1727
+ this.$container.style.left = '8px';
1728
+ this.$container.style.right = '8px';
1729
+ this.$container.style.top = 'auto';
1730
+ this.$container.style.bottom = '8px';
1731
+ this.$container.style.width = 'auto';
1732
+ this.$container.style.maxHeight = 'min(80vh, 560px)';
1733
+ this.$container.style.zIndex = '10000';
1734
+ return
1735
+ }
1736
+
1737
+ // Use the primary trigger element for popup positioning.
1738
+ const positionTrigger = this.$endInput ? this.$startInput :
1739
+ (this.$openTrigger ? this.$trigger : this.$trigger);
1740
+
1741
+ const rect = positionTrigger.getBoundingClientRect();
1742
+
1743
+ const viewportHeight = window.innerHeight;
1744
+ const spaceBelow = viewportHeight - rect.bottom;
1745
+
1746
+ if (spaceBelow < 300) {
1747
+ this.$container.style.top = `${rect.top + window.scrollY - this.$container.offsetHeight}px`;
1748
+ } else {
1749
+ this.$container.style.top = `${rect.bottom + window.scrollY}px`;
1750
+ }
1751
+
1752
+ this.$container.style.left = `${rect.left + window.scrollX}px`;
1753
+ this.$container.style.right = 'auto';
1754
+ this.$container.style.bottom = 'auto';
1755
+ this.$container.style.width = '';
1756
+ this.$container.style.maxHeight = '';
1757
+ this.$container.style.position = 'absolute';
1758
+ this.$container.style.zIndex = '10000';
1759
+ }
1760
+
1761
+ #updateView() {
1762
+ this.#updateHeader();
1763
+ this.scroll.blocked = false;
1764
+ this.observe.un(this.period);
1765
+ this.#clearContent(this.period);
1766
+
1767
+ switch (this.period) {
1768
+ case 'day':
1769
+ const days = this.data.getDates();
1770
+ this.dom.$days_block.innerHTML = this.render.dates(
1771
+ days,
1772
+ this.#selectedDates,
1773
+ this.options.selectType
1774
+ );
1775
+ break
1776
+
1777
+ default:
1778
+ const dates = this.data.getMonthsOrYears(this.period);
1779
+ this.dom[`$${this.period}s_block`].innerHTML = this.render[`${this.period}s`](dates);
1780
+ }
1781
+
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}"]`
1793
+ };
1794
+ $firstEl = this.$container.querySelector(fallbacks[this.period]);
1795
+ }
1796
+
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;
1802
+
1803
+ if (blockHeight <= bodyHeight) {
1804
+ this.scroll.setBaseOffset(bodyHeight - blockHeight);
1805
+ this.scroll.offset = 0;
1806
+ return
1807
+ }
1808
+
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
+ }
1817
+
1818
+ this.observe.on(this.period, item => {
1819
+ const cond = JSON.parse(item.dataset.bind).every(data => Number(item.dataset[data]) === this.data[`current_${data}`]);
1820
+ if (cond) item.classList.add(`RollDate__calendar__${this.period}--active`);
1821
+ });
1822
+ }
1823
+
1824
+ #updateHeader() {
1825
+ if (this.period !== 'year') {
1826
+ this.dom.$year.innerText = this.data.current_year;
1827
+ this.dom.$month.innerText = this.options.monthsNames[this.data.current_month];
1828
+ } else {
1829
+ this.dom.$year.innerText = Number(this.data.current_decade) + '-' + (Number(this.data.current_decade) + 9);
1830
+ }
1831
+
1832
+ const items = this.$container.querySelectorAll(`.RollDate__calendar__${this.period}`);
1833
+
1834
+ items.forEach(item => {
1835
+ for (const period of JSON.parse(item.dataset.bind)) {
1836
+ const condition = JSON.parse(item.dataset.bind).every(data => Number(item.dataset[data]) === this.data[`current_${data}`]);
1837
+ if (condition) {
1838
+ item.classList.add(`RollDate__calendar__${this.period}--active`);
1839
+ } else {
1840
+ item.classList.remove(`RollDate__calendar__${this.period}--active`);
1841
+ }
1842
+ }
1843
+ });
1844
+ }
1845
+
1846
+ #attachEvents() {
1847
+ if (this.mode === 'popup') {
1848
+ const openTriggers = [];
1849
+
1850
+ // Resolve elements that are allowed to open the popup.
1851
+ if (this.triggerSelector) {
1852
+ // Custom trigger provided by selector.
1853
+ this.$openTrigger = document.querySelector(this.triggerSelector);
1854
+ openTriggers.push(this.$openTrigger);
1855
+ } else if (this.$endInput) {
1856
+ // Range mode with two inputs and no separate icon trigger.
1857
+ openTriggers.push(this.$startInput, this.$endInput);
1858
+ } else {
1859
+ // Single input mode.
1860
+ openTriggers.push(this.$trigger);
1861
+ }
1862
+
1863
+ this.#openTriggers = openTriggers.filter(Boolean);
1864
+
1865
+ this.#openTriggers.forEach(trigger => {
1866
+ trigger.addEventListener('click', (e) => {
1867
+ e.stopPropagation();
1868
+ this.open();
1869
+ });
1870
+
1871
+ if (trigger.tagName === 'INPUT') {
1872
+ trigger.addEventListener('focus', () => this.open());
1873
+ }
1874
+ });
1875
+
1876
+ // Close when clicking outside picker and trigger elements.
1877
+ this.#docClickHandler = (e) => {
1878
+ if (!this.$container.contains(e.target) &&
1879
+ !this.#openTriggers.some(t => t.contains(e.target))) {
1880
+ this.close();
1881
+ }
1882
+ };
1883
+ document.addEventListener('click', this.#docClickHandler);
1884
+
1885
+ // Parse manual text input for popup inputs.
1886
+ const inputs = this.$endInput ? [this.$startInput, this.$endInput] : [this.$trigger];
1887
+ inputs.forEach(input => {
1888
+ if (input?.tagName === 'INPUT') {
1889
+ input.addEventListener('input', (e) => {
1890
+ this.#parseInputValue(e.target.value);
1891
+ });
1892
+ }
1893
+ });
1894
+ }
1895
+
1896
+ this.scroll = new Scroll(this.dom.$body, {
1897
+ dominant: () => {
1898
+ const dominant = this.observe.dominant();
1899
+ if (dominant) {
1900
+ for (const period of Object.keys(dominant)) {
1901
+ if (this.data[`current_${period}`] !== dominant[period]) {
1902
+ if (dominant.hasOwnProperty('year') || dominant.hasOwnProperty('month')) {
1903
+ this.data.current_year = dominant.year;
1904
+ this.data.current_month = dominant.month;
1905
+ this.data.current_decade = getDecade(dominant.year);
1906
+ }
1907
+ if (dominant.hasOwnProperty('decade'))
1908
+ this.data.current_decade = dominant.decade;
1909
+
1910
+ hapticTick(this.options.hapticFeedback !== false);
1911
+ this.#updateHeader();
1912
+ break
1913
+ }
1914
+ }
1915
+ }
1916
+ },
1917
+ updatePeriod: (direction) => this.virtualizer.update(direction)
1918
+ });
1919
+
1920
+ this.dom.$type_switcher.addEventListener('click', e => {
1921
+ e.preventDefault();
1922
+ e.stopPropagation();
1923
+ this.#switchViewType(++this.#viewNumber);
1924
+ });
1925
+
1926
+ this.dom.$body.addEventListener('click', e => {
1927
+ e.preventDefault();
1928
+ e.stopPropagation();
1929
+
1930
+ const $view_clicker = e.target.closest('[data-click]');
1931
+ const $day = e.target.closest('[data-day]');
1932
+
1933
+ if ($view_clicker) {
1934
+ if (
1935
+ $view_clicker.classList.contains('RollDate__calendar__month--disabled') ||
1936
+ $view_clicker.classList.contains('RollDate__calendar__year--disabled')
1937
+ ) {
1938
+ return
1939
+ }
1940
+ this.data.current_month = Number($view_clicker.dataset.month);
1941
+ this.data.current_year = Number($view_clicker.dataset.year);
1942
+ this.data.current_decade = getDecade(this.data.current_year);
1943
+ const view = $view_clicker.dataset.click;
1944
+ this.#switchViewType(this.#viewPeriodNames.indexOf(view));
1945
+ }
1946
+
1947
+ if ($day) {
1948
+ if ($day.classList.contains('RollDate__calendar__day--disabled')) {
1949
+ return
1950
+ }
1951
+ const date = this.#applyTimeToDate(new Date(
1952
+ Number($day.dataset.year),
1953
+ Number($day.dataset.month),
1954
+ Number($day.dataset.day)
1955
+ ));
1956
+
1957
+ if (this.options.selectType === 'single') {
1958
+ this.$container.querySelectorAll('[data-day]').forEach(item => {
1959
+ item.classList.remove('RollDate__calendar__day--selected');
1960
+ });
1961
+ $day.classList.add('RollDate__calendar__day--selected');
1962
+
1963
+ this.#selectedDates = [date];
1964
+ this.options.selectDate(date);
1965
+ this.#updateInputValue();
1966
+ if (this.mode === 'popup' && this.options.closeOnSelect) this.close();
1967
+ }
1968
+
1969
+ if (this.options.selectType === 'range') {
1970
+ const isClickInRange = this.#selectedDates.length === 2 &&
1971
+ date.getTime() >= this.#selectedDates[0].getTime() &&
1972
+ date.getTime() <= this.#selectedDates[1].getTime();
1973
+
1974
+ if (this.#selectedDates.length === 2 && !isClickInRange) {
1975
+ this.#clearRangeSelection();
1976
+ }
1977
+
1978
+ if (this.#selectedDates.length === 0) {
1979
+ this.#selectedDates = [date];
1980
+ $day.classList.add('RollDate__calendar__day--range-first');
1981
+ } else {
1982
+ const firstDate = this.#selectedDates[0];
1983
+ if (date.getTime() > firstDate.getTime()) {
1984
+ this.#selectedDates = [firstDate, date];
1985
+
1986
+ this.$container.querySelectorAll('[data-day]').forEach(dayEl => {
1987
+ const dayDate = new Date(
1988
+ Number(dayEl.dataset.year),
1989
+ Number(dayEl.dataset.month),
1990
+ Number(dayEl.dataset.day)
1991
+ );
1992
+ const time = dayDate.getTime();
1993
+
1994
+ if (time === firstDate.getTime()) {
1995
+ dayEl.classList.add('RollDate__calendar__day--range-first');
1996
+ } else if (time === date.getTime()) {
1997
+ dayEl.classList.add('RollDate__calendar__day--range-last');
1998
+ } else if (time > firstDate.getTime() && time < date.getTime()) {
1999
+ dayEl.classList.add('RollDate__calendar__day--range-selected');
2000
+ }
2001
+ });
2002
+ } else if (date.getTime() < firstDate.getTime()) {
2003
+ this.#selectedDates = [date];
2004
+ $day.classList.add('RollDate__calendar__day--range-first');
2005
+
2006
+ this.$container.querySelector('.RollDate__calendar__day--range-first:not([data-day="' + date.getDate() + '"])')
2007
+ ?.classList.remove('RollDate__calendar__day--range-first');
2008
+ }
2009
+ }
2010
+
2011
+ this.options.selectDate([...this.#selectedDates]);
2012
+ this.#updateInputValue();
2013
+
2014
+ if (this.mode === 'popup' && this.#selectedDates.length === 2 && this.options.closeOnSelect) {
2015
+ this.close();
2016
+ }
2017
+ }
2018
+
2019
+ if (this.options.selectType === 'multi') {
2020
+ const isSelected = this.#selectedDates.some(
2021
+ d => d.getTime() === date.getTime()
2022
+ );
2023
+ if (isSelected) {
2024
+ this.#selectedDates = this.#selectedDates.filter(
2025
+ d => d.getTime() !== date.getTime()
2026
+ );
2027
+ $day.classList.remove('RollDate__calendar__day--selected');
2028
+ } else {
2029
+ this.#selectedDates.push(date);
2030
+ $day.classList.add('RollDate__calendar__day--selected');
2031
+ }
2032
+ this.options.selectDate(this.#selectedDates);
2033
+ this.#updateInputValue();
2034
+ if (this.mode === 'popup' && this.options.closeOnSelect) {
2035
+ this.close();
2036
+ }
2037
+ }
2038
+ }
2039
+
2040
+ this.scroll.resetMinScroll();
2041
+ });
2042
+
2043
+ this.dom.$body.addEventListener('mousemove', e => {
2044
+ const $day = e.target.closest('[data-day]');
2045
+ if (!$day) return
2046
+ if ($day.classList.contains('RollDate__calendar__day--disabled')) return
2047
+
2048
+ const hoveredDate = new Date(
2049
+ Number($day.dataset.year),
2050
+ Number($day.dataset.month),
2051
+ Number($day.dataset.day)
2052
+ );
2053
+ this.options.onHoverDate(hoveredDate, {
2054
+ period: this.period,
2055
+ selectedDates: [...this.#selectedDates]
2056
+ });
2057
+ });
2058
+
2059
+ this.dom.$body.addEventListener('mouseleave', () => {
2060
+ this.options.onHoverDate(null, {
2061
+ period: this.period,
2062
+ selectedDates: [...this.#selectedDates]
2063
+ });
2064
+ });
2065
+
2066
+ this.dom.$page_switcher.forEach($button => {
2067
+ $button.addEventListener('click', e => {
2068
+ e.preventDefault();
2069
+ e.stopPropagation();
2070
+
2071
+ const direction = e.currentTarget?.dataset?.direction;
2072
+ if (direction !== 'prev' && direction !== 'next') return
2073
+ const shift = direction === 'prev' ? -1 : 1;
2074
+ const minYear = this.options.minDate.getFullYear();
2075
+ const maxYear = this.options.maxDate.getFullYear();
2076
+
2077
+ if (this.period === 'day') {
2078
+ const current = new Date(this.data.current_year, this.data.current_month, 1);
2079
+ const target = new Date(this.data.current_year, this.data.current_month + shift, 1);
2080
+ const minMonth = new Date(this.options.minDate.getFullYear(), this.options.minDate.getMonth(), 1);
2081
+ const maxMonth = new Date(this.options.maxDate.getFullYear(), this.options.maxDate.getMonth(), 1);
2082
+
2083
+ if (target < minMonth || target > maxMonth) return
2084
+ if (target.getTime() === current.getTime()) return
2085
+
2086
+ this.data.current_year = target.getFullYear();
2087
+ this.data.current_month = target.getMonth();
2088
+ this.data.current_decade = getDecade(this.data.current_year);
2089
+ } else if (this.period === 'month') {
2090
+ const targetYear = this.data.current_year + shift;
2091
+ if (targetYear < minYear || targetYear > maxYear) return
2092
+
2093
+ this.data.current_year = targetYear;
2094
+ this.data.current_decade = getDecade(targetYear);
2095
+ } else if (this.period === 'year') {
2096
+ const currentDecade = getDecade(this.data.current_year);
2097
+ const targetDecade = currentDecade + shift * 10;
2098
+ const minDecade = getDecade(minYear);
2099
+ const maxDecade = getDecade(maxYear);
2100
+
2101
+ if (targetDecade < minDecade || targetDecade > maxDecade) return
2102
+
2103
+ this.data.current_year = targetDecade;
2104
+ this.data.current_decade = targetDecade;
2105
+ }
2106
+
2107
+ hapticTick(this.options.hapticFeedback !== false);
2108
+ this.#updateView(this.#viewNumber);
2109
+ });
2110
+ });
2111
+ }
2112
+
2113
+ #switchViewType(type) {
2114
+ const prevPeriod = this.period;
2115
+ if (type > 2) {
2116
+ type = this.period = 2;
2117
+ return
2118
+ }
2119
+
2120
+ this.scroll.resetMinScroll();
2121
+ this.period = type;
2122
+ this.#updateView(this.period);
2123
+
2124
+ for (const period of this.#viewPeriodNames)
2125
+ this.$container.classList.remove('RollDate__calendar__type--' + period + 's');
2126
+
2127
+ this.$container.classList.add('RollDate__calendar__type--' + this.#viewPeriodNames[type] + 's');
2128
+
2129
+ this.options.onViewChange({
2130
+ from: prevPeriod,
2131
+ to: this.period,
2132
+ current: {
2133
+ year: this.data.current_year,
2134
+ month: this.data.current_month,
2135
+ decade: this.data.current_decade
2136
+ }
2137
+ });
2138
+ }
2139
+
2140
+ #clearContent(type) {
2141
+ if (type !== 2) this.dom.$years_block.innerHTML = '';
2142
+ if (type !== 1) this.dom.$months_block.innerHTML = '';
2143
+ if (type !== 0) this.dom.$days_block.innerHTML = '';
2144
+ }
2145
+
2146
+ #scrollToStartDate() {
2147
+ requestAnimationFrame(() => {
2148
+ this.scroll.checkMinScroll();
2149
+
2150
+ const targetDate = this.options.startDate;
2151
+ const $targetEl = this.$container.querySelector(
2152
+ `[data-year="${targetDate.getFullYear()}"][data-month="${targetDate.getMonth()}"]`
2153
+ );
2154
+
2155
+ if ($targetEl) {
2156
+ const containerRect = this.dom.$body.querySelector('.RollDate__calendar__scrollblock').getBoundingClientRect();
2157
+ const targetRect = $targetEl.getBoundingClientRect();
2158
+ const offset = -(targetRect.top - containerRect.top);
2159
+
2160
+ this.scroll.offset = offset;
2161
+ }
2162
+ });
2163
+ }
2164
+
2165
+ get period() {
2166
+ return this.#viewPeriodNames[this.#viewNumber]
2167
+ }
2168
+ set period(number) {
2169
+ this.#viewNumber = number;
2170
+ }
2171
+ get selectedDates() {
2172
+ return this.#selectedDates
2173
+ }
2174
+
2175
+ setDisabledDates(dates = []) {
2176
+ this.options.disabledDates = Array.isArray(dates) ? dates : [];
2177
+ this.#disabledDateStamps = this.#buildDisabledDateSet(this.options.disabledDates);
2178
+ this.#syncSelectedWithDisabledDates();
2179
+ this.#updateView(this.#viewNumber);
2180
+ }
2181
+
2182
+ disableDate(dateLike) {
2183
+ const normalized = this.#normalizeDateInput(dateLike);
2184
+ if (!normalized) return
2185
+ this.#disabledDateStamps.add(this.#toDateStamp(normalized));
2186
+ this.options.disabledDates = [...this.#disabledDateStamps].map(stamp => new Date(stamp));
2187
+ this.#syncSelectedWithDisabledDates();
2188
+ this.#updateView(this.#viewNumber);
2189
+ }
2190
+
2191
+ enableDate(dateLike) {
2192
+ const normalized = this.#normalizeDateInput(dateLike);
2193
+ if (!normalized) return
2194
+ this.#disabledDateStamps.delete(this.#toDateStamp(normalized));
2195
+ this.options.disabledDates = [...this.#disabledDateStamps].map(stamp => new Date(stamp));
2196
+ this.#updateView(this.#viewNumber);
2197
+ }
2198
+
2199
+ isDateDisabled(dateLike) {
2200
+ const normalized = this.#normalizeDateInput(dateLike);
2201
+ return normalized ? this.#isDateDisabled(normalized) : false
2202
+ }
2203
+
2204
+ #clearRangeSelection() {
2205
+ this.#selectedDates = [];
2206
+ this.$container.querySelectorAll('[data-day]').forEach(item => {
2207
+ item.classList.remove(
2208
+ 'RollDate__calendar__day--range-first',
2209
+ 'RollDate__calendar__day--range-last',
2210
+ 'RollDate__calendar__day--range-selected'
2211
+ );
2212
+ });
2213
+ }
2214
+
2215
+ #clearMultiSelection() {
2216
+ this.#selectedDates = [];
2217
+ this.$container.querySelectorAll('[data-day].RollDate__calendar__day--selected')
2218
+ .forEach(el => el.classList.remove('RollDate__calendar__day--selected'));
2219
+ }
2220
+
2221
+ #updateInputValue() {
2222
+ if (this.mode !== 'popup') return
2223
+
2224
+ const format = (date) => this.#formatDateTime(date);
2225
+
2226
+ if (this.$endInput) {
2227
+ if (this.#selectedDates.length >= 1) {
2228
+ this.$startInput.value = format(this.#selectedDates[0]);
2229
+ } else {
2230
+ this.$startInput.value = '';
2231
+ }
2232
+
2233
+ if (this.#selectedDates.length === 2) {
2234
+ this.$endInput.value = format(this.#selectedDates[1]);
2235
+ } else {
2236
+ this.$endInput.value = '';
2237
+ }
2238
+ } else {
2239
+ const target = this.$trigger;
2240
+ if (target.tagName === 'INPUT') {
2241
+ if (this.options.selectType === 'single' && this.#selectedDates.length > 0) {
2242
+ target.value = format(this.#selectedDates[0]);
2243
+ } else if (this.options.selectType === 'range') {
2244
+ if (this.#selectedDates.length === 1) {
2245
+ target.value = `${format(this.#selectedDates[0])} - `;
2246
+ } else if (this.#selectedDates.length === 2) {
2247
+ target.value = `${format(this.#selectedDates[0])} - ${format(this.#selectedDates[1])}`;
2248
+ } else {
2249
+ target.value = '';
2250
+ }
2251
+ } else if (this.options.selectType === 'multi' && this.#selectedDates.length > 0) {
2252
+ target.value = this.#selectedDates.map(format).join(', ');
2253
+ } else {
2254
+ target.value = '';
2255
+ }
2256
+ }
2257
+ }
2258
+ }
2259
+
2260
+ #parseInputValue(value) {
2261
+ if (!value) {
2262
+ this.#selectedDates = [];
2263
+ this.#updateView(this.#viewNumber);
2264
+ return
2265
+ }
2266
+
2267
+ try {
2268
+ if (this.options.selectType === 'single') {
2269
+ const date = parseDate(value, this.options.dateFormat);
2270
+ if (date && !this.#isDateDisabled(date)) {
2271
+ this.#selectedDates = [date];
2272
+ this.data.current_year = date.getFullYear();
2273
+ this.data.current_month = date.getMonth();
2274
+ this.#updateView(this.#viewNumber);
2275
+ }
2276
+ } else if (this.options.selectType === 'range') {
2277
+ const dates = value.split(/\s+-\s+/).map(part => part.trim());
2278
+ const parsedDates = dates
2279
+ .filter(d => d)
2280
+ .map(d => parseDate(d, this.options.dateFormat))
2281
+ .filter(d => !this.#isDateDisabled(d))
2282
+ .filter(d => d);
2283
+
2284
+ if (parsedDates.length > 0) {
2285
+ this.#selectedDates = parsedDates;
2286
+ if (parsedDates.length >= 1) {
2287
+ this.data.current_year = parsedDates[0].getFullYear();
2288
+ this.data.current_month = parsedDates[0].getMonth();
2289
+ }
2290
+ this.#updateView(this.#viewNumber);
2291
+ }
2292
+ } else if (this.options.selectType === 'multi') {
2293
+ const dates = value.split(',').map(part => part.trim());
2294
+ const parsedDates = dates
2295
+ .filter(d => d)
2296
+ .map(d => parseDate(d, this.options.dateFormat))
2297
+ .filter(d => !this.#isDateDisabled(d))
2298
+ .filter(d => d);
2299
+
2300
+ if (parsedDates.length > 0) {
2301
+ this.#selectedDates = parsedDates;
2302
+ this.data.current_year = parsedDates[0].getFullYear();
2303
+ this.data.current_month = parsedDates[0].getMonth();
2304
+ this.#updateView(this.#viewNumber);
2305
+ }
2306
+ }
2307
+ } catch (e) {
2308
+ console.warn('Invalid date format:', value);
2309
+ }
2310
+ }
2311
+
2312
+ open() {
2313
+ if (this.mode === 'popup') {
2314
+ this.#closeOtherPopups();
2315
+ }
2316
+
2317
+ const wasOpen = this.$container.style.display !== 'none';
2318
+ this.$container.style.display = 'block';
2319
+
2320
+ if (this.mode === 'popup') {
2321
+ this.#positionCalendar();
2322
+
2323
+ if (this.#firstOpen) {
2324
+ this.#scrollToStartDate();
2325
+ this.#firstOpen = false;
2326
+ } else {
2327
+ this.#updateView(this.#viewNumber);
2328
+ }
2329
+ } else {
2330
+ if (!this.dom?.$body) {
2331
+ this.#updateView(this.#viewNumber);
2332
+ }
2333
+ }
2334
+
2335
+ if (!wasOpen) {
2336
+ this.options.onOpen({
2337
+ period: this.period,
2338
+ selectedDates: [...this.#selectedDates]
2339
+ });
2340
+ }
2341
+ }
2342
+
2343
+ close() {
2344
+ const wasOpen = this.$container.style.display !== 'none';
2345
+ this.$container.style.display = 'none';
2346
+ if (this.mode === 'popup') {
2347
+ const triggers = this.$endInput ? [this.$startInput, this.$endInput] :
2348
+ this.$openTrigger ? [this.$openTrigger] : [this.$trigger];
2349
+
2350
+ triggers.forEach(trigger => {
2351
+ if (trigger && typeof trigger.blur === 'function') {
2352
+ trigger.blur();
2353
+ }
2354
+ });
2355
+ }
2356
+
2357
+ if (wasOpen) {
2358
+ this.options.onClose({
2359
+ period: this.period,
2360
+ selectedDates: [...this.#selectedDates]
2361
+ });
2362
+ }
2363
+ }
2364
+
2365
+ selectToday() {
2366
+ const now = new Date();
2367
+ const dayOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
2368
+ if (this.#isDateDisabled(dayOnly)) return
2369
+
2370
+ this.data.current_year = now.getFullYear();
2371
+ this.data.current_month = now.getMonth();
2372
+ this.data.current_decade = getDecade(now.getFullYear());
2373
+
2374
+ const selected = this.#applyTimeToDate(now);
2375
+
2376
+ if (this.options.selectType === 'single') {
2377
+ this.#selectedDates = [selected];
2378
+ } else if (this.options.selectType === 'range') {
2379
+ this.#selectedDates = [selected];
2380
+ } else if (this.options.selectType === 'multi') {
2381
+ const exists = this.#selectedDates.some(d => d.getTime() === selected.getTime());
2382
+ if (!exists) this.#selectedDates.push(selected);
2383
+ }
2384
+
2385
+ this.#updateView(this.#viewNumber);
2386
+ this.#notifySelectionChange();
2387
+ this.#updateInputValue();
2388
+
2389
+ if (this.mode === 'popup' && this.options.closeOnSelect && this.options.selectType === 'single') {
2390
+ this.close();
2391
+ }
2392
+ }
2393
+
2394
+ clearSelection() {
2395
+ if (this.options.selectType === 'range') {
2396
+ this.#clearRangeSelection();
2397
+ } else if (this.options.selectType === 'multi') {
2398
+ this.#clearMultiSelection();
2399
+ } else {
2400
+ this.#selectedDates = [];
2401
+ }
2402
+
2403
+ this.#updateView(this.#viewNumber);
2404
+ this.#notifySelectionChange();
2405
+ this.#updateInputValue();
2406
+ }
2407
+
2408
+ destroy() {
2409
+ if (this.#docClickHandler) {
2410
+ document.removeEventListener('click', this.#docClickHandler);
2411
+ this.#docClickHandler = null;
2412
+ }
2413
+ RollDate.#instances.delete(this);
2414
+ this.observe.disconnect();
2415
+ this.scroll?.destroy();
2416
+ this.timePicker?.destroy();
2417
+ if (this.$container.parentNode) {
2418
+ this.$container.parentNode.removeChild(this.$container);
2419
+ }
2420
+ }
2421
+ }
2422
+
2423
+ if (typeof window !== 'undefined') {
2424
+ window.RollDate = RollDate;
2425
+ }
2426
+
2427
+ return RollDate;
2428
+
2429
+ })();