PageSourceSearch

https://railsblocks.com/assets/controllers/date_picker_controller-…d868125b97bc24351fec1b787f90ce8ff2feb0c0.js

js railsblocks.com collected 2026-09-25 20:23:08 UTC 28,498 bytes, 795 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2import { computePosition, offset, flip, shift, arrow, autoUpdate } from "@floating-ui/dom";
3import AirDatepicker from "air-datepicker";
4import localeEn from "air-datepicker/locale/en";
5
6// AirDatepicker CSS is now imported directly.
7// Ensure air-datepicker is added to your package.json and your build system handles these imports.
8
9export default class extends Controller {
10  static targets = ["input", "inlineCalendar"];
11  static values = {
12    placement: { type: String, default: "bottom-start" }, // Placement of the datepicker
13    range: { type: Boolean, default: false }, // Whether to allow selecting a range of dates
14    disabledDates: { type: Array, default: [] }, // Expects array of 'YYYY-MM-DD' strings
15    timepicker: { type: Boolean, default: false },
16    timeOnly: { type: Boolean, default: false }, // New value for time-only selection
17    weekPicker: { type: Boolean, default: false }, // New value for week selection
18    timeFormat: { type: String, default: "" }, // Default empty, logic will apply 'hh:mm AA' if timepicker is true and this is empty
19    minHours: Number, // Undefined if not set, AirDatepicker uses its default
20    maxHours: Number, // Undefined if not set
21    minutesStep: Number, // Undefined if not set
22    showTodayButton: { type: Boolean, default: false },
23    showClearButton: { type: Boolean, default: false },
24    showThisMonthButton: { type: Boolean, default: false },
25    showThisYearButton: { type: Boolean, default: false },
26    dateFormat: { type: String, default: "" }, // e.g., 'MM/dd/yyyy', AirDatepicker default if empty
27    startView: { type: String, default: "days" }, // 'days', 'months', 'years'
28    minView: { type: String, default: "days" }, // 'days', 'months', 'years'
29    initialDate: { type: String, default: "" }, // 'YYYY-MM-DD' or JSON array of 'YYYY-MM-DD' for range
30    minDate: { type: String, default: "" }, // 'YYYY-MM-DD'
31    maxDate: { type: String, default: "" }, // 'YYYY-MM-DD'
32    inline: { type: Boolean, default: false }, // Makes the calendar permanently visible
33  };
34
35  // CSS classes for preset buttons
36  static PRESET_CLASSES = {
37    active: [
38      "bg-neutral-900",
39      "text-white",
40      "hover:!bg-neutral-700",
41      "dark:bg-neutral-100",
42      "dark:hover:!bg-neutral-200",
43      "dark:text-neutral-900",
44    ],
45    inactive: ["text-neutral-700", "hover:bg-neutral-100", "dark:text-neutral-300", "dark:hover:bg-neutral-700/50"],
46  };
47
48  connect() {
49    // Check if this element also has dropdown-popover controller
50    // If so, don't initialize the datepicker (it's just used for preset methods)
51    const hasDropdownController =
52      this.element.hasAttribute("data-controller") &&
53      this.element.getAttribute("data-controller").includes("dropdown-popover");
54
55    if (!hasDropdownController) {
56      this.initializeDatepicker();
57      this.inputTarget.addEventListener("keydown", this.handleKeydown.bind(this));
58    } else {
59      // Listen for input click to detect active preset when dropdown opens
60      this.inputTarget.addEventListener("click", () => {
61        // Small delay to ensure dropdown is fully rendered
62        setTimeout(() => {
63          this._detectActivePreset();
64        }, 50);
65      });
66    }
67  }
68
69  initializeDatepicker() {
70    if (this.datepickerInstance) {
71      this.datepickerInstance.destroy();
72      // Ensure autoUpdate cleanup is robustly handled if destroy didn't trigger position's cleanup
73      if (this.cleanupAutoUpdate) {
74        this.cleanupAutoUpdate();
75        this.cleanupAutoUpdate = null;
76      }
77      // Also clear the specific one for the current position call, if any
78      if (this.currentPositionCleanupAutoUpdate) {
79        this.currentPositionCleanupAutoUpdate();
80        this.currentPositionCleanupAutoUpdate = null;
81      }
82    }
83
84    const options = this._buildDatepickerOptions();
85
86    if (!this.inlineValue) {
87      options.position = this._createPositionFunction();
88    }
89
90    this.datepickerInstance = new AirDatepicker(this.inputTarget, options);
91
92    // Format initial value for week picker
93    if (this.weekPickerValue) {
94      setTimeout(() => this._updateWeekDisplay(), 0);
95    }
96
97    // Trigger change for inline calendars
98    if (this.inlineValue) {
99      setTimeout(() => this._triggerChangeEvent(), 0);
100    }
101  }
102
103  _buildDatepickerOptions() {
104    const options = {
105      locale: localeEn,
106      autoClose: !this.inlineValue,
107      inline: this.inlineValue,
108      container: this.inputTarget.closest("dialog") || undefined,
109    };
110
111    // Date/Time format
112    if (this.timeOnlyValue) {
113      options.dateFormat = "";
114      options.onSelect = this._createTimeOnlySelectHandler();
115    } else if (this.weekPickerValue) {
116      options.dateFormat = "";
117      options.onSelect = this._createWeekSelectHandler();
118    } else if (this.hasDateFormatValue && this.dateFormatValue) {
119      options.dateFormat = this.dateFormatValue;
120    }
121
122    // Views
123    if (this.hasStartViewValue) options.view = this.startViewValue;
124    if (this.hasMinViewValue) options.minView = this.minViewValue;
125
126    // Date constraints
127    this._setDateConstraint(options, "minDate", this.minDateValue);
128    this._setDateConstraint(options, "maxDate", this.maxDateValue);
129
130    // Initial dates
131    const initialDates = this._parseInitialDates();
132    if (initialDates.length > 0) options.selectedDates = initialDates;
133
134    // Range mode
135    if (this.rangeValue) {
136      options.range = true;
137      options.multipleDatesSeparator = " - ";
138    }
139
140    // Timepicker
141    if (this.timepickerValue || this.timeOnlyValue) {
142      Object.assign(options, {
143        timepicker: true,
144        timeFormat: this.timeFormatValue || "hh:mm AA",
145        ...(this.hasMinHoursValue && { minHours: this.minHoursValue }),
146        ...(this.hasMaxHoursValue && { maxHours: this.maxHoursValue }),
147        ...(this.hasMinutesStepValue && { minutesStep: this.minutesStepValue }),
148      });
149      if (this.timeOnlyValue) options.classes = "only-timepicker";
150    }
151
152    // Buttons
153    const buttons = this._buildButtons();
154    if (buttons.length > 0) options.buttons = buttons;
155
156    // Disabled dates handling
157    const disabledDates = this._parseDisabledDates();
158    if (disabledDates.length > 0) {
159      options.onRenderCell = this._createRenderCellHandler(disabledDates);
160    }
161
162    // General onSelect for inline calendars
163    if (!this.timeOnlyValue && !this.weekPickerValue && this.inlineValue) {
164      const originalOnSelect = options.onSelect;
165      options.onSelect = (params) => {
166        originalOnSelect?.(params);
167        setTimeout(() => this.syncToMainPicker(), 10);
168        this._triggerChangeEvent();
169      };
170    }
171
172    // Special handling for inline time-only picker
173    if (this.timeOnlyValue && this.inlineValue) {
174      const originalOnSelect = options.onSelect;
175      options.onSelect = ({ date, datepicker }) => {
176        if (originalOnSelect) {
177          originalOnSelect({ date, datepicker });
178        }
179        this._triggerChangeEvent();
180      };
181    }
182
183    return options;
184  }
185
186  _setDateConstraint(options, key, value) {
187    if (value) {
188      const date = this._parseDate(value);
189      if (date) options[key] = date;
190    }
191  }
192
193  _parseInitialDates() {
194    if (!this.hasInitialDateValue || !this.initialDateValue) return [];
195
196    try {
197      if (this.initialDateValue.startsWith("[") && this.initialDateValue.endsWith("]")) {
198        const dateStrings = JSON.parse(this.initialDateValue);
199        return dateStrings.map((str) => this._parseDate(str)).filter(Boolean);
200      }
201      const date = this._parseDate(this.initialDateValue);
202      return date ? [date] : [];
203    } catch (e) {
204      console.error("Error parsing initialDateValue:", e, "Value was:", this.initialDateValue);
205      return [];
206    }
207  }
208
209  _buildButtons() {
210    const buttons = [];
211    const buttonConfigs = [
212      { condition: this.showTodayButtonValue, button: this._createTodayButton() },
213      { condition: this.showThisMonthButtonValue, button: this._createMonthButton() },
214      { condition: this.showThisYearButtonValue, button: this._createYearButton() },
215      { condition: this.showClearButtonValue, button: "clear" },
216    ];
217
218    buttonConfigs.forEach(({ condition, button }) => {
219      if (condition && button) buttons.push(button);
220    });
221
222    return buttons;
223  }
224
225  _createTodayButton() {
226    const isTimepickerEnabled = this.timepickerValue || this.timeOnlyValue || this.weekPickerValue;
227    const buttonText = this.weekPickerValue ? "This week" : isTimepickerEnabled ? "Now" : "Today";
228
229    return {
230      content: buttonText,
231      onClick: (dp) => {
232        const currentDate = new Date();
233        const dates = this.rangeValue ? [currentDate, currentDate] : currentDate;
234
235        if (isTimepickerEnabled && !this.weekPickerValue) {
236          dp.clear();
237          setTimeout(() => dp.selectDate(dates, { updateTime: true }), 0);
238        } else {
239          dp.selectDate(dates);
240        }
241      },
242    };
243  }
244
245  _createMonthButton() {
246    return {
247      content: "This month",
248      onClick: (dp) => {
249        const currentDate = new Date();
250        dp.selectDate(new Date(currentDate.getFullYear(), currentDate.getMonth(), 1));
251      },
252    };
253  }
254
255  _createYearButton() {
256    return {
257      content: "This year",
258      onClick: (dp) => {
259        const currentDate = new Date();
260        dp.selectDate(new Date(currentDate.getFullYear(), 0, 1));
261      },
262    };
263  }
264
265  _parseDisabledDates() {
266    if (!this.disabledDatesValue?.length) return [];
267
268    return this.disabledDatesValue.map((str) => this._parseDate(str)).filter(Boolean);
269  }
270
271  _createRenderCellHandler(disabledDates) {
272    return ({ date, cellType }) => {
273      if (cellType !== "day") return {};
274
275      const cellDateUTC = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
276      const isDisabled = disabledDates.some((disabledDate) => disabledDate.getTime() === cellDateUTC.getTime());
277
278      return isDisabled ? { disabled: true } : {};
279    };
280  }
281
282  _createTimeOnlySelectHandler() {
283    return ({ date, datepicker }) => {
284      if (date) {
285        const timeFormat = this.timeFormatValue || "hh:mm AA";
286        this.inputTarget.value = datepicker.formatDate(date, timeFormat);
287        this._triggerChangeEvent();
288      }
289    };
290  }
291
292  _createWeekSelectHandler() {
293    return ({ date }) => {
294      if (date) {
295        const weekNumber = this._getWeekNumber(date);
296        const year = date.getFullYear();
297        this.inputTarget.value = `${year}-W${weekNumber.toString().padStart(2, "0")}`;
298        this._triggerChangeEvent();
299      }
300    };
301  }
302
303  _createPositionFunction() {
304    return ({ $datepicker, $target, $pointer, done }) => {
305      const middleware = [offset(8), flip(), shift({ padding: 8 })];
306
307      if ($pointer instanceof HTMLElement) {
308        middleware.push(arrow({ element: $pointer, padding: 5 }));
309      }
310
311      this._cleanupPositioning();
312
313      this.currentPositionCleanupAutoUpdate = autoUpdate(
314        $target,
315        $datepicker,
316        () => {
317          computePosition($target, $datepicker, {
318            placement: this.placementValue,
319            middleware: middleware,
320          }).then(({ x, y, middlewareData }) => {
321            Object.assign($datepicker.style, { left: `${x}px`, top: `${y}px` });
322
323            if ($pointer instanceof HTMLElement && middlewareData.arrow) {
324              const { x: arrowX, y: arrowY } = middlewareData.arrow;
325              Object.assign($pointer.style, {
326                left: arrowX != null ? `${arrowX}px` : "",
327                top: arrowY != null ? `${arrowY}px` : "",
328              });
329            }
330          });
331        },
332        { animationFrame: true },
333      );
334
335      this.cleanupAutoUpdate = this.currentPositionCleanupAutoUpdate;
336
337      return () => {
338        this._cleanupPositioning();
339        done();
340      };
341    };
342  }
343
344  _getWeekNumber(date) {
345    const tempDate = new Date(date.getTime());
346    const dayNumber = (tempDate.getDay() + 6) % 7;
347    tempDate.setDate(tempDate.getDate() - dayNumber + 3);
348
349    const firstThursday = new Date(tempDate.getFullYear(), 0, 4);
350    firstThursday.setDate(firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3);
351
352    return Math.round((tempDate.getTime() - firstThursday.getTime()) / 86400000 / 7) + 1;
353  }
354
355  _parseDate(dateString) {
356    if (!dateString || typeof dateString !== "string") return null;
357
358    // First check if it's a datetime string (YYYY-MM-DD HH:MM)
359    if (dateString.includes(" ")) {
360      const [datePart, timePart] = dateString.split(" ");
361      const dateParts = datePart.split("-");
362
363      if (dateParts.length === 3 && timePart) {
364        const [year, month, day] = dateParts.map(Number);
365        const [hours, minutes] = timePart.split(":").map(Number);
366
367        if (!isNaN(year) && !isNaN(month) && !isNaN(day) && !isNaN(hours) && !isNaN(minutes)) {
368          return new Date(year, month - 1, day, hours, minutes);
369        }
370      }
371    }
372
373    // Otherwise try to parse as date only (YYYY-MM-DD)
374    const parts = dateString.split("-");
375    if (parts.length === 3) {
376      const [year, month, day] = parts.map(Number);
377      if (!isNaN(year) && !isNaN(month) && !isNaN(day)) {
378        return new Date(Date.UTC(year, month - 1, day));
379      }
380    }
381
382    console.warn(`Invalid date string format: ${dateString}. Expected YYYY-MM-DD or YYYY-MM-DD HH:MM.`);
383    return null;
384  }
385
386  handleKeydown(event) {
387    if (event.key === "Delete" || event.key === "Backspace") {
388      this.datepickerInstance?.clear();
389      this.inputTarget.value = "";
390    }
391  }
392
393  disconnect() {
394    if (this.datepickerInstance) {
395      this.datepickerInstance.destroy(); // This should trigger the cleanup returned by position()
396      this.datepickerInstance = null;
397    }
398    // Fallback cleanup for autoUpdate, in case destroy() didn't clear it or it was managed outside position's return.
399    if (this.cleanupAutoUpdate) {
400      this.cleanupAutoUpdate();
401      this.cleanupAutoUpdate = null;
402    }
403    if (this.currentPositionCleanupAutoUpdate) {
404      // Also ensure any lingering position-specific cleanup is called
405      this.currentPositionCleanupAutoUpdate();
406      this.currentPositionCleanupAutoUpdate = null;
407    }
408
409    // Only remove event listener if it was added
410    if (this.inputTarget && !this.element.getAttribute("data-controller").includes("dropdown-popover")) {
411      this.inputTarget.removeEventListener("keydown", this.handleKeydown.bind(this));
412    }
413  }
414
415  // Preset time range methods
416  setToday(event) {
417    const today = new Date();
418    // Set hours to noon to avoid any timezone edge cases
419    today.setHours(12, 0, 0, 0);
420    this._applyPreset(event.currentTarget, today, today);
421  }
422
423  setYesterday(event) {
424    const yesterday = new Date();
425    yesterday.setDate(yesterday.getDate() - 1);
426    this._applyPreset(event.currentTarget, yesterday, yesterday);
427  }
428
429  setLastDays(event) {
430    const days = parseInt(event.currentTarget.dataset.days, 10);
431    if (isNaN(days) || days <= 0) {
432      console.warn("Invalid number of days specified in data-days attribute");
433      return;
434    }
435
436    const endDate = new Date();
437    const startDate = new Date();
438    startDate.setDate(startDate.getDate() - (days - 1)); // days - 1 because we include today
439    this._applyPreset(event.currentTarget, startDate, endDate);
440  }
441
442  setPreset(event) {
443    const presetType = event.currentTarget.dataset.presetType;
444
445    switch (presetType) {
446      case "this-month":
447        this._setThisMonth();
448        break;
449      case "last-month":
450        this._setLastMonth();
451        break;
452      case "this-year":
453        this._setThisYear();
454        break;
455      default:
456        console.warn(`Unknown preset type: ${presetType}`);
457    }
458    this._setActivePreset(event.currentTarget);
459  }
460
461  _setThisMonth() {
462    const now = new Date();
463    const startDate = new Date(now.getFullYear(), now.getMonth(), 1);
464    const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
465    this._applyPreset(event.currentTarget, startDate, endDate);
466  }
467
468  _setLastMonth() {
469    const now = new Date();
470    const startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
471    const endDate = new Date(now.getFullYear(), now.getMonth(), 0);
472    this._applyPreset(event.currentTarget, startDate, endDate);
473  }
474
475  _setThisYear() {
476    const now = new Date();
477    const startDate = new Date(now.getFullYear(), 0, 1);
478    const endDate = new Date(now.getFullYear(), 11, 31);
479    this._applyPreset(event.currentTarget, startDate, endDate);
480  }
481
482  clearSelection() {
483    if (this.datepickerInstance) {
484      this.datepickerInstance.clear();
485    }
486    this.inputTarget.value = "";
487    this._syncToInlineCalendar();
488    this._triggerChangeEvent();
489    this._clearActivePreset();
490  }
491
492  syncFromInlineCalendar(event) {
493    // Sync the main input from the inline calendar when it changes
494    const inlineInput = event.target;
495    if (inlineInput.value) {
496      this.inputTarget.value = inlineInput.value;
497      this._triggerChangeEvent();
498    }
499  }
500
501  syncToMainPicker(event = null) {
502    // This method is called from the inline calendar to sync to main picker
503    // Find the main date picker controller in the parent dropdown
504    const dropdownElement = this.element.closest(
505      '[data-controller*="dropdown-popover"][data-controller*="date-picker"]',
506    );
507    if (dropdownElement) {
508      const mainController = this.application.getControllerForElementAndIdentifier(dropdownElement, "date-picker");
509      if (mainController && mainController !== this) {
510        // Get the selected dates from the inline calendar
511        const selectedDates = this.datepickerInstance ? this.datepickerInstance.selectedDates : [];
512
513        if (selectedDates.length > 0) {
514          // Format the dates for the main input
515          const formattedValue = mainController._formatDateRange(
516            selectedDates[0],
517            selectedDates[selectedDates.length - 1],
518          );
519          mainController.inputTarget.value = formattedValue;
520
521          // If the main controller has a datepicker instance, sync the selected dates
522          if (mainController.datepickerInstance) {
523            // For range pickers, ensure we complete the range selection properly
524            if (mainController.rangeValue) {
525              if (selectedDates.length === 1) {
526                // Select the same date twice to complete the range selection
527                mainController.datepickerInstance.selectDate([selectedDates[0], selectedDates[0]]);
528              } else {
529                mainController.datepickerInstance.selectDate(selectedDates);
530              }
531            } else {
532              mainController.datepickerInstance.selectDate(selectedDates[0]);
533            }
534          }
535        } else {
536          // Clear the main input if no dates selected
537          mainController.inputTarget.value = "";
538          if (mainController.datepickerInstance) {
539            mainController.datepickerInstance.clear();
540          }
541        }
542
543        mainController._triggerChangeEvent();
544
545        // Clear active preset when dates are manually selected
546        mainController._clearActivePreset();
547
548        // Close the dropdown after selection if we have a complete range or single date
549        if (mainController.rangeValue ? selectedDates.length >= 2 : selectedDates.length >= 1) {
550          const dropdownController = this.application.getControllerForElementAndIdentifier(
551            dropdownElement,
552            "dropdown-popover",
553          );
554          if (dropdownController) {
555            dropdownController.close();
556          }
557        }
558      }
559    }
560  }
561
562  // Helper methods
563  _applyPreset(button, startDate, endDate) {
564    this._setDateRange(startDate, endDate);
565    this._setActivePreset(button);
566  }
567
568  _setDateRange(startDate, endDate) {
569    if (this.datepickerInstance) {
570      const dates = this.rangeValue ? [startDate, endDate] : [startDate];
571      this.datepickerInstance.selectDate(dates);
572    } else {
573      this.inputTarget.value = this._formatDateRange(startDate, endDate);
574    }
575
576    this._syncToInlineCalendar();
577    this._triggerChangeEvent();
578  }
579
580  _formatDateRange(startDate, endDate) {
581    const formatDate = (date) => {
582      const year = date.getFullYear();
583      const month = String(date.getMonth() + 1).padStart(2, "0");
584      const day = String(date.getDate()).padStart(2, "0");
585      return `${month}/${day}/${year}`;
586    };
587
588    return this.rangeValue ? `${formatDate(startDate)} - ${formatDate(endDate)}` : formatDate(startDate);
589  }
590
591  _syncToInlineCalendar() {
592    // Find inline calendar in the same dropdown and sync its value
593    const dropdownMenu = this.element.querySelector('[data-dropdown-popover-target="menu"]');
594    if (dropdownMenu) {
595      const inlineCalendar = dropdownMenu.querySelector('.inline-calendar[data-controller="date-picker"]');
596      if (inlineCalendar) {
597        const inlineController = this.application.getControllerForElementAndIdentifier(inlineCalendar, "date-picker");
598        if (inlineController && inlineController.datepickerInstance) {
599          // Get selected dates from main picker and apply to inline calendar
600          if (this.datepickerInstance && this.datepickerInstance.selectedDates.length > 0) {
601            // If it's a range picker and both dates are the same, only select one date
602            const datesToSelect =
603              this.rangeValue &&
604              this.datepickerInstance.selectedDates.length === 2 &&
605              this.datepickerInstance.selectedDates[0].getTime() === this.datepickerInstance.selectedDates[1].getTime()
606                ? [this.datepickerInstance.selectedDates[0]]
607                : this.datepickerInstance.selectedDates;
608            inlineController.datepickerInstance.selectDate(datesToSelect);
609          } else {
610            // If no main datepicker instance, parse the input value and set dates
611            if (this.inputTarget.value) {
612              const dates = this._parseDateRangeValue(this.inputTarget.value);
613              if (dates.length > 0) {
614                // Clear any existing selection first to ensure clean state
615                inlineController.datepickerInstance.clear();
616
617                // If it's a range picker and we have a single date or both parsed dates are the same
618                if (inlineController.rangeValue) {
619                  if (dates.length === 1) {
620                    // For single date in range mode, select it twice to complete the range
621                    inlineController.datepickerInstance.selectDate([dates[0], dates[0]]);
622                  } else if (dates.length === 2 && dates[0].getTime() === dates[1].getTime()) {
623                    // For identical start/end dates, select twice to complete the range
624                    inlineController.datepickerInstance.selectDate([dates[0], dates[0]]);
625                  } else {
626                    // For different dates, select normally
627                    inlineController.datepickerInstance.selectDate(dates);
628                  }
629                } else {
630                  // Non-range mode, select normally
631                  inlineController.datepickerInstance.selectDate(dates);
632                }
633              }
634            } else {
635              inlineController.datepickerInstance.clear();
636            }
637          }
638        }
639      }
640    }
641  }
642
643  _parseDateRangeValue(value) {
644    // Parse a date range string like "01/15/2025 - 01/20/2025" or "01/15/2025"
645    const dates = [];
646    if (value.includes(" - ")) {
647      const [startStr, endStr] = value.split(" - ");
648      const startDate = this._parseFormattedDate(startStr.trim());
649      const endDate = this._parseFormattedDate(endStr.trim());
650      if (startDate) dates.push(startDate);
651      if (endDate) dates.push(endDate);
652    } else {
653      const date = this._parseFormattedDate(value.trim());
654      if (date) dates.push(date);
655    }
656    return dates;
657  }
658
659  _parseFormattedDate(dateStr) {
660    // Parse MM/DD/YYYY format
661    const parts = dateStr.split("/");
662    if (parts.length === 3) {
663      const month = parseInt(parts[0], 10) - 1; // Month is 0-indexed
664      const day = parseInt(parts[1], 10);
665      const year = parseInt(parts[2], 10);
666      return new Date(year, month, day);
667    }
668    return null;
669  }
670
671  _triggerChangeEvent() {
672    this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }));
673  }
674
675  _setActivePreset(button) {
676    this._clearActivePreset();
677    if (button) {
678      button.classList.remove(...this.constructor.PRESET_CLASSES.inactive);
679      button.classList.add(...this.constructor.PRESET_CLASSES.active);
680    }
681  }
682
683  _clearActivePreset() {
684    const dropdownMenu = this.element.querySelector('[data-dropdown-popover-target="menu"]');
685    const presetButtons =
686      dropdownMenu?.querySelectorAll('[data-menu-target="item"]:not([data-action*="clearSelection"])') || [];
687
688    presetButtons.forEach((button) => {
689      button.classList.remove(...this.constructor.PRESET_CLASSES.active);
690      button.classList.add(...this.constructor.PRESET_CLASSES.inactive);
691    });
692  }
693
694  _detectActivePreset() {
695    const dateRangeValue = this.inputTarget.value;
696    if (!dateRangeValue) return;
697
698    const dates = this._parseDateRangeValue(dateRangeValue);
699    if (dates.length !== 2) return;
700
701    const [startDate, endDate] = dates;
702    const today = new Date();
703    [startDate, endDate, today].forEach((date) => date.setHours(0, 0, 0, 0));
704
705    const dropdownMenu = this.element.querySelector('[data-dropdown-popover-target="menu"]');
706    const presetButtons =
707      dropdownMenu?.querySelectorAll('[data-menu-target="item"]:not([data-action*="clearSelection"])') || [];
708
709    presetButtons.forEach((button) => {
710      const isMatch = this._checkPresetMatch(button, startDate, endDate, today);
711      if (isMatch) this._setActivePreset(button);
712    });
713  }
714
715  _checkPresetMatch(button, startDate, endDate, today) {
716    const action = button.dataset.action;
717
718    if (action?.includes("setToday")) {
719      return this._isSameDay(startDate, today) && this._isSameDay(endDate, today);
720    }
721
722    if (action?.includes("setYesterday")) {
723      const yesterday = new Date(today);
724      yesterday.setDate(yesterday.getDate() - 1);
725      return this._isSameDay(startDate, yesterday) && this._isSameDay(endDate, yesterday);
726    }
727
728    if (action?.includes("setLastDays")) {
729      const days = parseInt(button.dataset.days, 10);
730      const expectedStart = new Date(today);
731      expectedStart.setDate(expectedStart.getDate() - (days - 1));
732      return this._isSameDay(startDate, expectedStart) && this._isSameDay(endDate, today);
733    }
734
735    if (action?.includes("setPreset")) {
736      const presetType = button.dataset.presetType;
737      const presetRanges = {
738        "this-month": [
739          new Date(today.getFullYear(), today.getMonth(), 1),
740          new Date(today.getFullYear(), today.getMonth() + 1, 0),
741        ],
742        "last-month": [
743          new Date(today.getFullYear(), today.getMonth() - 1, 1),
744          new Date(today.getFullYear(), today.getMonth(), 0),
745        ],
746        "this-year": [new Date(today.getFullYear(), 0, 1), new Date(today.getFullYear(), 11, 31)],
747      };
748
749      const range = presetRanges[presetType];
750      return range && this._isSameDay(startDate, range[0]) && this._isSameDay(endDate, range[1]);
751    }
752
753    return false;
754  }
755
756  _isSameDay(date1, date2) {
757    return (
758      date1.getFullYear() === date2.getFullYear() &&
759      date1.getMonth() === date2.getMonth() &&
760      date1.getDate() === date2.getDate()
761    );
762  }
763
764  _updateWeekDisplay() {
765    if (!this.datepickerInstance) return;
766
767    const selectedDates = this.datepickerInstance.selectedDates;
768    if (selectedDates.length > 0) {
769      const initialDate = selectedDates[0];
770      const weekNumber = this._getWeekNumber(initialDate);
771      const year = initialDate.getFullYear();
772      this.inputTarget.value = `${year}-W${weekNumber.toString().padStart(2, "0")}`;
773      this._triggerChangeEvent();
774    } else {
775      this.inputTarget.value = "";
776      this._triggerChangeEvent();
777    }
778  }
779
780  _cleanup() {
781    if (this.datepickerInstance) {
782      this.datepickerInstance.destroy();
783      this.datepickerInstance = null;
784    }
785    this._cleanupPositioning();
786  }
787
788  _cleanupPositioning() {
789    [this.cleanupAutoUpdate, this.currentPositionCleanupAutoUpdate].forEach((cleanup) => {
790      if (cleanup) cleanup();
791    });
792    this.cleanupAutoUpdate = null;
793    this.currentPositionCleanupAutoUpdate = null;
794  }
795};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.