UNPKG

@uplink-protocol/calendar-controller

Version:

Flexible calendar API supporting both calendar and date-picker integrations for any JavaScript framework or library

1,019 lines (845 loc) 34.9 kB
// filepath: d:\Projects\Odyssey\components\date-picker\examples\date-picker\js\view.js // Date Picker View - Handles DOM manipulation and event binding import calendars from './controller.js'; import accessibility from './accessibility.js'; /** * Initializes the calendar view */ export function initCalendarView() { // Primary calendar for main view const calendar = calendars.primary; const secondaryCalendar = calendars.secondary; // Get DOM elements - Single Month View const calendarEl = document.getElementById('calendar'); const weekdaysEl = document.getElementById('weekdays'); const currentMonthYearEl = document.getElementById('currentMonthYear'); const selectedDateEl = document.getElementById('selectedDate'); const selectedRangeEl = document.getElementById('selectedRange'); const dateInputEl = document.getElementById('dateInput'); const calendarPopupEl = document.getElementById('calendarPopup'); const monthsListEl = document.getElementById('monthsList'); const yearsListEl = document.getElementById('yearsList'); // View mode elements const singleMonthViewEl = document.getElementById('singleMonthView'); const doubleMonthViewEl = document.getElementById('doubleMonthView'); const monthsViewEl = document.getElementById('monthsView'); const yearsViewEl = document.getElementById('yearsView'); // Double month view elements const firstMonthCalendarEl = document.getElementById('firstMonthCalendar'); const firstMonthWeekdaysEl = document.getElementById('firstMonthWeekdays'); const firstMonthTitleEl = document.getElementById('firstMonthTitle'); const secondMonthCalendarEl = document.getElementById('secondMonthCalendar'); const secondMonthWeekdaysEl = document.getElementById('secondMonthWeekdays'); const secondMonthTitleEl = document.getElementById('secondMonthTitle'); // Month and year view elements const monthsGridEl = document.getElementById('monthsGrid'); const yearsGridEl = document.getElementById('yearsGrid'); // Set up bindings from the controllers const bindings = calendar.bindings; const secondaryBindings = secondaryCalendar.bindings; // Initialize UI based on controller state renderWeekdays(weekdaysEl, bindings.weekdays.current); renderCurrentMonthYear(currentMonthYearEl, bindings.monthName.current, bindings.currentYear.current); renderCalendarDays(calendarEl, bindings.calendarDays.current, calendar); updateSelectedDateDisplay(selectedDateEl, bindings.selectedDate.current); updateSelectedRangeDisplay(selectedRangeEl, bindings.selectedDateRange.current); updateInputFieldValue(dateInputEl, bindings.selectedDate.current); // Initialize double month view if (firstMonthWeekdaysEl) renderWeekdays(firstMonthWeekdaysEl, bindings.weekdays.current); if (secondMonthWeekdaysEl) renderWeekdays(secondMonthWeekdaysEl, secondaryBindings.weekdays.current); if (firstMonthCalendarEl && firstMonthTitleEl) { renderCurrentMonthYear(firstMonthTitleEl, bindings.monthName.current, bindings.currentYear.current); renderCalendarDays(firstMonthCalendarEl, bindings.calendarDays.current, calendar); } if (secondMonthCalendarEl && secondMonthTitleEl) { renderCurrentMonthYear(secondMonthTitleEl, secondaryBindings.monthName.current, secondaryBindings.currentYear.current); renderCalendarDays(secondMonthCalendarEl, secondaryBindings.calendarDays.current, secondaryCalendar); } // Generate months and years view renderMonthsView(monthsGridEl); renderYearsView(yearsGridEl); // Generate months for the month selector dropdown renderMonthsList(monthsListEl); // Generate years for the year selector dropdown renderYearsList(yearsListEl); // Set up binding subscriptions for single month view bindings.calendarDays.subscribe(days => { renderCalendarDays(calendarEl, days, calendar); if (firstMonthCalendarEl && calendars.state.getViewMode() === 'doubleMonth') { renderCalendarDays(firstMonthCalendarEl, days, calendar); } }); bindings.monthName.subscribe(monthName => { renderCurrentMonthYear(currentMonthYearEl, monthName, bindings.currentYear.current); if (firstMonthTitleEl && calendars.state.getViewMode() === 'doubleMonth') { renderCurrentMonthYear(firstMonthTitleEl, monthName, bindings.currentYear.current); } }); bindings.currentYear.subscribe(year => { renderCurrentMonthYear(currentMonthYearEl, bindings.monthName.current, year); if (firstMonthTitleEl && calendars.state.getViewMode() === 'doubleMonth') { renderCurrentMonthYear(firstMonthTitleEl, bindings.monthName.current, year); } // Update years list and years view when current year changes if (!isYearInVisibleRange(year, yearsListEl)) { renderYearsList(yearsListEl, year); } if (yearsGridEl) { renderYearsView(yearsGridEl, year); } }); // Set up binding subscriptions for secondary calendar (double month view) secondaryBindings.calendarDays.subscribe(days => { if (secondMonthCalendarEl && calendars.state.getViewMode() === 'doubleMonth') { renderCalendarDays(secondMonthCalendarEl, days, secondaryCalendar); } }); secondaryBindings.monthName.subscribe(monthName => { if (secondMonthTitleEl && calendars.state.getViewMode() === 'doubleMonth') { renderCurrentMonthYear(secondMonthTitleEl, monthName, secondaryBindings.currentYear.current); } }); secondaryBindings.currentYear.subscribe(year => { if (secondMonthTitleEl && calendars.state.getViewMode() === 'doubleMonth') { renderCurrentMonthYear(secondMonthTitleEl, secondaryBindings.monthName.current, year); } }); bindings.selectedDate.subscribe(date => { updateSelectedDateDisplay(selectedDateEl, date); updateInputFieldValue(dateInputEl, date); // Update months view to highlight the selected month if applicable if (monthsGridEl && date) { renderMonthsView(monthsGridEl); } // Update sidebar selection highlighting updateSidebarSelection(); }); bindings.selectedDateRange.subscribe(range => { updateSelectedRangeDisplay(selectedRangeEl, range); updateInputFieldRangeValue(dateInputEl, range); // Update sidebar selection highlighting for ranges updateSidebarSelection(); }); bindings.isRangeSelection.subscribe(isRange => { if (isRange) { selectedDateEl.classList.add('hidden'); selectedDateEl.classList.remove('block'); selectedRangeEl.classList.add('block'); selectedRangeEl.classList.remove('hidden'); // Update input placeholder dateInputEl.placeholder = 'Select date range...'; } else { selectedDateEl.classList.add('block'); selectedDateEl.classList.remove('hidden'); selectedRangeEl.classList.add('hidden'); selectedRangeEl.classList.remove('block'); // Update input placeholder dateInputEl.placeholder = 'Select a date...'; } // Reset sidebar selection highlighting when changing modes updateSidebarSelection(); }); // Initial display mode if (bindings.isRangeSelection.current) { selectedDateEl.classList.add('hidden'); selectedRangeEl.classList.add('block'); dateInputEl.placeholder = 'Select date range...'; } else { selectedRangeEl.classList.add('hidden'); selectedDateEl.classList.add('block'); dateInputEl.placeholder = 'Select a date...'; } // Initialize view mode to single month by default setCalendarViewMode('singleMonth'); // Initialize accessibility features accessibility.initAccessibility(); } /** * Updates the input field with the selected date */ function updateInputFieldValue(inputEl, date) { if (!inputEl) return; if (date) { inputEl.value = accessibility.formatDate(date, 'MM/DD/YYYY'); accessibility.announce(`Date selected: ${date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}`); } else { inputEl.value = ''; } } /** * Updates the input field with the selected date range */ function updateInputFieldRangeValue(inputEl, range) { if (!inputEl) return; if (range.startDate && range.endDate) { const startFormatted = accessibility.formatDate(range.startDate, 'MM/DD/YYYY'); const endFormatted = accessibility.formatDate(range.endDate, 'MM/DD/YYYY'); inputEl.value = `${startFormatted} - ${endFormatted}`; // Announce to screen readers const startReadable = range.startDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); const endReadable = range.endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); accessibility.announce(`Date range selected: ${startReadable} to ${endReadable}`); } else if (range.startDate) { inputEl.value = `${accessibility.formatDate(range.startDate, 'MM/DD/YYYY')} - ...`; // Announce to screen readers const startReadable = range.startDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); accessibility.announce(`Range start date selected: ${startReadable}`); } else { inputEl.value = ''; } } /** * Renders the months list for the month selector dropdown */ function renderMonthsList(monthsListEl) { if (!monthsListEl) return; monthsListEl.innerHTML = ''; // Get primary calendar const calendar = calendars.primary; // Get all month names const months = []; for (let i = 0; i < 12; i++) { const date = new Date(2000, i, 1); months.push({ index: i, name: date.toLocaleString('default', { month: 'short' }) }); } // Create month buttons months.forEach(month => { const monthButton = document.createElement('button'); monthButton.className = 'py-2 px-3 text-sm rounded hover:bg-gray-100 w-full text-center transition-colors'; monthButton.textContent = month.name; monthButton.setAttribute('data-month', month.index); monthButton.setAttribute('aria-label', `Select ${month.name}`); // Set current month as active if (month.index === calendar.bindings.currentMonth.current) { monthButton.classList.add('bg-primary', 'text-white', 'font-medium'); } else if (month.index === new Date().getMonth() && calendar.bindings.currentYear.current === new Date().getFullYear()) { // Highlight current calendar month if we're in the current year monthButton.classList.add('font-medium'); } monthButton.addEventListener('click', () => { calendar.methods.goToMonth(month.index); // Also update secondary calendar for double month view if (month.index === 11) { calendars.secondary.methods.goToMonth(0); calendars.secondary.methods.goToYear(calendar.bindings.currentYear.current + 1); } else { calendars.secondary.methods.goToMonth(month.index + 1); calendars.secondary.methods.goToYear(calendar.bindings.currentYear.current); } hideMonthYearDropdown(); }); monthsListEl.appendChild(monthButton); }); } /** * Renders the months view (grid of all 12 months) */ function renderMonthsView(monthsGridEl) { if (!monthsGridEl) return; monthsGridEl.innerHTML = ''; // Get months data from calendar state const months = calendars.state.generateMonthsView(); // Create month buttons months.forEach(month => { const monthButton = document.createElement('button'); monthButton.className = 'py-4 px-2 rounded-lg hover:bg-gray-100 w-full text-center flex flex-col items-center justify-center transition-colors'; monthButton.setAttribute('data-month', month.index); monthButton.setAttribute('aria-label', `Go to ${month.fullName}`); // Month name const nameSpan = document.createElement('span'); nameSpan.className = 'text-lg font-medium'; nameSpan.textContent = month.fullName; monthButton.appendChild(nameSpan); // Year const yearSpan = document.createElement('span'); yearSpan.className = 'text-xs text-gray-500'; yearSpan.textContent = month.year; monthButton.appendChild(yearSpan); // Set current month as active if (month.isSelected) { monthButton.classList.add('bg-primary', 'text-white'); nameSpan.classList.add('text-white'); yearSpan.classList.add('text-white'); yearSpan.classList.remove('text-gray-500'); } if (month.isCurrent && !month.isSelected) { nameSpan.classList.add('font-semibold'); monthButton.classList.add('ring-1', 'ring-primary'); } monthButton.addEventListener('click', () => { const calendar = calendars.primary; // Go to this month and switch view mode back to calendar calendar.methods.goToMonth(month.index); calendar.methods.goToYear(month.year); // Update secondary calendar for double month view if (month.index === 11) { calendars.secondary.methods.goToMonth(0); calendars.secondary.methods.goToYear(month.year + 1); } else { calendars.secondary.methods.goToMonth(month.index + 1); calendars.secondary.methods.goToYear(month.year); } // Switch back to the appropriate view mode const currentViewMode = calendars.state.getViewMode(); if (currentViewMode === 'months' || currentViewMode === 'years') { setCalendarViewMode('singleMonth'); } }); monthsGridEl.appendChild(monthButton); }); } /** * Renders the years list for the year selector dropdown */ function renderYearsList(yearsListEl, centerYear = null) { if (!yearsListEl) return; yearsListEl.innerHTML = ''; // Get primary calendar const calendar = calendars.primary; // Default to current year if no center year provided const currentYear = centerYear || calendar.bindings.currentYear.current; const startYear = currentYear - 5; // Show a range of 12 years (5 before, current, 6 after) const endYear = currentYear + 6; // Set a data attribute to track the visible range yearsListEl.setAttribute('data-start-year', startYear); yearsListEl.setAttribute('data-end-year', endYear); // Create year buttons for (let year = startYear; year <= endYear; year++) { const yearButton = document.createElement('button'); yearButton.className = 'py-2 px-3 text-sm rounded hover:bg-gray-100 w-full text-center transition-colors'; yearButton.textContent = year; yearButton.setAttribute('data-year', year); yearButton.setAttribute('aria-label', `Select year ${year}`); // Set current year as active if (year === currentYear) { yearButton.classList.add('bg-primary', 'text-white', 'font-medium'); } else if (year === new Date().getFullYear()) { // Highlight current calendar year yearButton.classList.add('font-medium'); } yearButton.addEventListener('click', () => { calendar.methods.goToYear(year); // Also update secondary calendar for double month view calendars.secondary.methods.goToYear(year); hideMonthYearDropdown(); }); yearsListEl.appendChild(yearButton); } } /** * Renders the years view (grid of years) */ function renderYearsView(yearsGridEl, centerYear = null) { if (!yearsGridEl) return; yearsGridEl.innerHTML = ''; // Get years data from calendar state const years = calendars.state.generateYearsView(centerYear); // Create year buttons years.forEach(yearData => { const yearButton = document.createElement('button'); yearButton.className = 'py-4 px-4 rounded-lg hover:bg-gray-100 w-full text-center flex items-center justify-center transition-colors'; yearButton.setAttribute('data-year', yearData.year); yearButton.setAttribute('aria-label', `Go to ${yearData.year}`); // Year text const yearSpan = document.createElement('span'); yearSpan.className = 'text-xl'; yearSpan.textContent = yearData.year; yearButton.appendChild(yearSpan); // Set selected year as active if (yearData.isSelected) { yearButton.classList.add('bg-primary', 'text-white'); } if (yearData.isCurrent && !yearData.isSelected) { yearButton.classList.add('font-semibold', 'ring-1', 'ring-primary'); } yearButton.addEventListener('click', () => { const calendar = calendars.primary; // Go to this year and switch view mode back to months calendar.methods.goToYear(yearData.year); // Update secondary calendar for double month view calendars.secondary.methods.goToYear(yearData.year); // Switch to months view setCalendarViewMode('months'); }); yearsGridEl.appendChild(yearButton); }); } /** * Sets the calendar view mode * @param {string} mode - The view mode ('singleMonth', 'doubleMonth', 'months', 'years') */ function setCalendarViewMode(mode) { // Update state calendars.state.setViewMode(mode); // Get view elements const singleMonthViewEl = document.getElementById('singleMonthView'); const doubleMonthViewEl = document.getElementById('doubleMonthView'); const monthsViewEl = document.getElementById('monthsView'); const yearsViewEl = document.getElementById('yearsView'); const singleMonthModeBtn = document.getElementById('singleMonthMode'); const doubleMonthModeBtn = document.getElementById('doubleMonthMode'); // Hide all views first [singleMonthViewEl, doubleMonthViewEl, monthsViewEl, yearsViewEl].forEach(el => { if (el) { el.classList.add('hidden'); el.classList.remove('active'); } }); // Reset mode button styles [singleMonthModeBtn, doubleMonthModeBtn].forEach(btn => { if (btn) { btn.classList.remove('bg-primary', 'text-white'); btn.classList.add('hover:bg-gray-100'); } }); // Activate the appropriate view switch(mode) { case 'singleMonth': singleMonthViewEl?.classList.remove('hidden'); singleMonthViewEl?.classList.add('active'); singleMonthModeBtn?.classList.add('bg-primary', 'text-white'); singleMonthModeBtn?.classList.remove('hover:bg-gray-100'); break; case 'doubleMonth': doubleMonthViewEl?.classList.remove('hidden'); doubleMonthViewEl?.classList.add('active'); doubleMonthModeBtn?.classList.add('bg-primary', 'text-white'); doubleMonthModeBtn?.classList.remove('hover:bg-gray-100'); break; case 'months': monthsViewEl?.classList.remove('hidden'); monthsViewEl?.classList.add('active'); break; case 'years': yearsViewEl?.classList.remove('hidden'); yearsViewEl?.classList.add('active'); break; } // Announce view change to screen readers accessibility.announce(`Calendar view changed to ${mode}`); } /** * Check if a year is in the visible range of years list */ function isYearInVisibleRange(year, yearsListEl) { if (!yearsListEl) return false; const startYear = parseInt(yearsListEl.getAttribute('data-start-year') || 0); const endYear = parseInt(yearsListEl.getAttribute('data-end-year') || 0); return year >= startYear && year <= endYear; } /** * Shows the month/year dropdown */ function showMonthYearDropdown() { const dropdown = document.getElementById('monthYearDropdown'); if (dropdown) { dropdown.classList.remove('hidden'); } document.getElementById('monthYearSelector').setAttribute('aria-expanded', 'true'); } /** * Hides the month/year dropdown */ function hideMonthYearDropdown() { const dropdown = document.getElementById('monthYearDropdown'); if (dropdown) { dropdown.classList.add('hidden'); } document.getElementById('monthYearSelector').setAttribute('aria-expanded', 'false'); } /** * Shows the calendar popup */ function showCalendarPopup() { const popup = document.getElementById('calendarPopup'); const input = document.getElementById('dateInput'); if (popup && input) { // Position below the input const inputRect = input.getBoundingClientRect(); const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; // Determine screen size categories const isMobile = window.innerWidth <= 640; const isSmallMobile = window.innerWidth <= 380; const isTablet = window.innerWidth > 640 && window.innerWidth <= 1024; const isLandscape = window.innerWidth > window.innerHeight; popup.classList.remove('hidden'); if (isMobile) { // For mobile, position it at the bottom of the screen with full width popup.style.position = 'fixed'; popup.style.left = '0'; popup.style.right = '0'; popup.style.bottom = '0'; popup.style.top = 'auto'; popup.style.width = '100%'; popup.style.maxHeight = isLandscape ? '85vh' : '90vh'; // Slightly less height in landscape popup.style.transform = 'translateY(0)'; popup.style.borderRadius = '1.5rem 1.5rem 0 0'; // Add safe area inset padding for iOS devices popup.style.paddingBottom = 'calc(env(safe-area-inset-bottom, 1rem) + 1rem)'; // Add mobile-specific class popup.classList.add('mobile-popup'); // Add specific size classes for more targeted styling popup.classList.toggle('small-mobile', isSmallMobile); popup.classList.toggle('landscape', isLandscape); // Ensure the body doesn't scroll under the popup document.body.style.overflow = 'hidden'; } else { // Reset mobile-specific classes popup.classList.remove('mobile-popup', 'small-mobile', 'landscape'); // Calculate available space below and above const spaceBelow = windowHeight - inputRect.bottom; const spaceAbove = inputRect.top; // Position popup below or above based on available space if (spaceBelow >= 350 || spaceBelow > spaceAbove) { popup.style.top = `${inputRect.bottom + window.scrollY + 5}px`; popup.style.bottom = 'auto'; } else { popup.style.bottom = `${window.innerHeight - inputRect.top + window.scrollY - 5}px`; popup.style.top = 'auto'; } // Reset any mobile-specific styles popup.style.position = 'absolute'; popup.style.maxHeight = ''; popup.style.transform = ''; popup.style.borderRadius = ''; popup.style.paddingBottom = ''; document.body.style.overflow = ''; // Add tablet-specific class if needed popup.classList.toggle('tablet', isTablet); // Handle horizontal positioning to ensure visibility // Adjust popup width based on device category let popupWidth; if (isTablet) { popupWidth = Math.min(90, Math.max(70, inputRect.width)); popupWidth = `${popupWidth}vw`; // Use viewport units for tablets } else { popupWidth = Math.max(620, inputRect.width); // Desktop width } // For desktop, position it relative to the input let leftPos = inputRect.left + window.scrollX; // Check if popup would overflow to the right if (typeof popupWidth === 'number' && leftPos + popupWidth > windowWidth) { // Adjust to center the popup or align with right edge leftPos = Math.max(10, windowWidth - popupWidth - 10); } popup.style.left = `${leftPos}px`; popup.style.width = popupWidth; } // Set ARIA attributes document.getElementById('calendarTrigger').setAttribute('aria-expanded', 'true'); // Focus the first interactive element (for keyboard navigation) setTimeout(() => { // First try to focus on the current day const todayCell = popup.querySelector('.calendar-cell[aria-current="date"]'); if (todayCell) { todayCell.focus(); } else { // Otherwise focus on the first interactive element const firstFocusable = popup.querySelector('button:not([disabled]), [tabindex]:not([tabindex="-1"])'); if (firstFocusable) { firstFocusable.focus(); } } }, 100); // Announce to screen readers accessibility.announce('Calendar dialog opened'); } } /** * Hides the calendar popup */ function hideCalendarPopup() { const popup = document.getElementById('calendarPopup'); if (popup) { popup.classList.add('hidden'); // Remove mobile-specific styles and restore page scrolling if (popup.classList.contains('mobile-popup')) { document.body.style.overflow = ''; popup.classList.remove('mobile-popup'); } } document.getElementById('calendarTrigger').setAttribute('aria-expanded', 'false'); // Return focus to the input field document.getElementById('dateInput').focus(); // Announce to screen readers accessibility.announce('Calendar dialog closed'); } /** * Renders the weekdays */ function renderWeekdays(weekdaysEl, weekdays) { if (!weekdaysEl) return; weekdaysEl.innerHTML = ''; weekdays.forEach(dayName => { const dayEl = document.createElement('div'); dayEl.className = 'text-center font-semibold text-gray-600 text-sm'; dayEl.textContent = dayName; weekdaysEl.appendChild(dayEl); }); } /** * Renders the current month and year */ function renderCurrentMonthYear(element, month, year) { if (!element) return; element.textContent = `${month} ${year}`; } /** * Renders the calendar days */ function renderCalendarDays(calendarEl, days, calendarInstance) { if (!calendarEl) return; // Create a temporary container to build the new calendar const tempContainer = document.createElement('div'); tempContainer.className = calendarEl.className; // Add a slide-in animation effect for container calendarEl.classList.add('opacity-0'); // Check if we're on mobile for enhanced transitions const isMobile = window.innerWidth <= 640; if (isMobile) { calendarEl.classList.add('horizontal-swipe-animation'); } setTimeout(() => calendarEl.classList.remove('opacity-0'), 50); days.forEach(day => { const dayEl = document.createElement('div'); // Base classes with Tailwind let className = 'w-10 h-10 flex items-center justify-center rounded-full transition transform hover:scale-105 calendar-cell'; // Apply conditional classes if (!day.isCurrentMonth) { className += ' text-gray-400'; } // Add touch feedback for mobile if ('ontouchstart' in window) { dayEl.addEventListener('touchstart', () => { dayEl.classList.add('touching'); }, { passive: true }); dayEl.addEventListener('touchend', () => { dayEl.classList.remove('touching'); }, { passive: true }); dayEl.addEventListener('touchcancel', () => { dayEl.classList.remove('touching'); }, { passive: true }); } if (day.isToday) { className += ' ring-2 ring-primary'; } if (day.isSelected) { className += ' bg-primary text-white'; } if (day.isDisabled) { className += ' text-gray-300 cursor-not-allowed'; } else { className += ' cursor-pointer hover:bg-gray-100'; dayEl.addEventListener('click', () => { calendarInstance.methods.selectDate(day.year, day.month, day.day); }); } if (day.isInRange) { className += ' bg-primary-light'; } if (day.isRangeStart || day.isRangeEnd) { className += ' bg-primary text-white'; } dayEl.className = className; dayEl.textContent = day.day; // Add accessibility attributes dayEl.setAttribute('role', 'button'); dayEl.setAttribute('tabindex', day.isDisabled ? '-1' : '0'); dayEl.setAttribute('aria-label', getDateAriaLabel(day)); if (day.isSelected) { dayEl.setAttribute('aria-selected', 'true'); } if (day.isToday) { dayEl.setAttribute('aria-current', 'date'); } if (day.isDisabled) { dayEl.setAttribute('aria-disabled', 'true'); } // Add keyboard interaction dayEl.addEventListener('keydown', (e) => { // Handle Enter/Space if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!day.isDisabled) { calendarInstance.methods.selectDate(day.year, day.month, day.day); } } }); tempContainer.appendChild(dayEl); }); // Replace the calendar content with our new elements calendarEl.innerHTML = ''; Array.from(tempContainer.children).forEach(child => { calendarEl.appendChild(child); }); // Add a subtle fade-in effect requestAnimationFrame(() => { calendarEl.classList.add('transition-opacity', 'duration-200'); calendarEl.classList.remove('opacity-0'); }); } /** * Create an accessible label for a calendar date */ function getDateAriaLabel(day) { const date = new Date(day.year, day.month, day.day); const formattedDate = date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); let label = formattedDate; if (day.isToday) { label += ', Today'; } if (day.isSelected) { label += ', Selected'; } if (day.isDisabled) { label += ', Unavailable'; } if (day.isInRange) { label += ', In selected range'; } return label; } /** * Updates the selected date display */ function updateSelectedDateDisplay(element, date) { if (!element) return; element.innerHTML = ''; // Clear previous content if (date) { const formattedDate = date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); const labelSpan = document.createElement('span'); labelSpan.className = 'font-medium text-gray-700'; labelSpan.textContent = 'Selected Date: '; const dateSpan = document.createElement('span'); dateSpan.className = 'text-primary font-semibold'; dateSpan.textContent = formattedDate; element.appendChild(labelSpan); element.appendChild(dateSpan); } else { const noDateSpan = document.createElement('span'); noDateSpan.className = 'text-gray-500 italic'; noDateSpan.textContent = 'No date selected'; element.appendChild(noDateSpan); } } /** * Updates the selected date range display */ function updateSelectedRangeDisplay(element, range) { if (!element) return; element.innerHTML = ''; // Clear previous content if (range.startDate && range.endDate) { const start = range.startDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); const end = range.endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); const labelSpan = document.createElement('span'); labelSpan.className = 'font-medium text-gray-700'; labelSpan.textContent = 'Selected Range: '; const startSpan = document.createElement('span'); startSpan.className = 'text-primary font-semibold'; startSpan.textContent = start; const toSpan = document.createElement('span'); toSpan.className = 'mx-1'; toSpan.textContent = 'to'; const endSpan = document.createElement('span'); endSpan.className = 'text-primary font-semibold'; endSpan.textContent = end; element.appendChild(labelSpan); element.appendChild(startSpan); element.appendChild(toSpan); element.appendChild(endSpan); } else if (range.startDate) { const start = range.startDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); const labelSpan = document.createElement('span'); labelSpan.className = 'font-medium text-gray-700'; labelSpan.textContent = 'Start Date: '; const startSpan = document.createElement('span'); startSpan.className = 'text-primary font-semibold'; startSpan.textContent = start; element.appendChild(labelSpan); element.appendChild(startSpan); } else { const noRangeSpan = document.createElement('span'); noRangeSpan.className = 'text-gray-500 italic'; noRangeSpan.textContent = 'No range selected'; element.appendChild(noRangeSpan); } } /** * Handles updating the sidebar based on selected date or range */ function updateSidebarSelection() { // Get all sidebar quick selection buttons const quickSelectButtons = document.querySelectorAll('#datepickerSidebar button'); // Reset all button states quickSelectButtons.forEach(btn => { btn.classList.remove('bg-primary', 'text-white'); }); // Determine which button to highlight based on selection const primaryCalendar = calendars.primary; // We'd check which date or range matches our presets and highlight the appropriate button // For example: const today = new Date(); const selectedDate = primaryCalendar.bindings.selectedDate.current; if (selectedDate) { // Check if selected date is today if (isSameDay(selectedDate, today)) { document.getElementById('selectToday')?.classList.add('bg-primary', 'text-white'); } // Check other preset dates... } } /** * Helper function to check if two dates are the same day */ function isSameDay(date1, date2) { return date1.getDate() === date2.getDate() && date1.getMonth() === date2.getMonth() && date1.getFullYear() === date2.getFullYear(); } // Export initialization function and helper functions export default { initCalendarView, showCalendarPopup, hideCalendarPopup, showMonthYearDropdown, hideMonthYearDropdown, setCalendarViewMode };