UNPKG

@uplink-protocol/calendar-controller

Version:

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

659 lines (556 loc) 24.4 kB
// filepath: d:\Projects\Odyssey\components\date-picker\examples\date-picker\js\main.js // Main entry point for the date picker example import calendars from './controller.js'; import view from './view.js'; import accessibility from './accessibility.js'; // Initialize the calendar view document.addEventListener('DOMContentLoaded', () => { // Get controllers const primaryCalendar = calendars.primary; const secondaryCalendar = calendars.secondary; // Handle screen orientation changes and resize events function handleViewportChange() { const popup = document.getElementById('calendarPopup'); if (popup && !popup.classList.contains('hidden')) { // Re-position the popup view.hideCalendarPopup(); view.showCalendarPopup(); } // Adjust calendar cell sizes for current viewport const isMobile = window.innerWidth <= 640; const isSmallScreen = window.innerWidth <= 380; const isLandscape = window.innerWidth > window.innerHeight; // Apply landscape-specific styles if needed if (isMobile && isLandscape) { document.body.classList.add('landscape-mode'); // On small devices in landscape, we might want to switch to single month view // to ensure better visibility of calendar cells if (calendars.state.getViewMode() === 'doubleMonth') { view.setCalendarViewMode('singleMonth'); } } else { document.body.classList.remove('landscape-mode'); } } window.addEventListener('orientationchange', () => { // Wait for orientation change to complete setTimeout(handleViewportChange, 300); }); // Also handle regular resize events with debounce let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(handleViewportChange, 250); }); // Initialize the view view.initCalendarView(); // Set up navigation controls document.getElementById('prevMonth').addEventListener('click', () => { primaryCalendar.methods.prevMonth(); // If in double month view, also update secondary calendar if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.prevMonth(); } }); document.getElementById('nextMonth').addEventListener('click', () => { primaryCalendar.methods.nextMonth(); // If in double month view, also update secondary calendar if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.nextMonth(); } }); document.getElementById('prevYear').addEventListener('click', () => { primaryCalendar.methods.prevYear(); // If in double month view, also update secondary calendar if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.prevYear(); } }); document.getElementById('nextYear').addEventListener('click', () => { primaryCalendar.methods.nextYear(); // If in double month view, also update secondary calendar if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.nextYear(); } }); // Set up mode toggle button document.getElementById('toggleMode').addEventListener('click', () => { const isCurrentlyRangeMode = primaryCalendar.bindings.isRangeSelection.current; primaryCalendar.methods.setRangeSelectionMode(!isCurrentlyRangeMode); // Update icon and text for mode toggle updateToggleModeButton(!isCurrentlyRangeMode); }); // Set up clear button (both in the main controls and in the calendar footer) document.getElementById('clearSelection').addEventListener('click', () => { primaryCalendar.methods.clearSelection(); }); document.getElementById('clearSelectionBtn')?.addEventListener('click', () => { primaryCalendar.methods.clearSelection(); }); // Set up today button document.getElementById('todayButton').addEventListener('click', () => { primaryCalendar.methods.goToToday(); // If in double month view, set secondary calendar to next month if (calendars.state.getViewMode() === 'doubleMonth') { const today = new Date(); const nextMonth = new Date(today); nextMonth.setMonth(today.getMonth() + 1); secondaryCalendar.methods.goToDate(nextMonth); } // If in single date mode, also select today's date if (!primaryCalendar.bindings.isRangeSelection.current) { const today = new Date(); primaryCalendar.methods.selectDate( today.getFullYear(), today.getMonth(), today.getDate() ); } }); // Set up view mode toggle buttons document.getElementById('singleMonthMode')?.addEventListener('click', () => { view.setCalendarViewMode('singleMonth'); }); document.getElementById('doubleMonthMode')?.addEventListener('click', () => { view.setCalendarViewMode('doubleMonth'); }); // Month/Year view switching document.getElementById('currentMonthYear')?.addEventListener('click', () => { const currentViewMode = calendars.state.getViewMode(); if (currentViewMode === 'singleMonth' || currentViewMode === 'doubleMonth') { view.setCalendarViewMode('months'); } }); // Set up sidebar quick select options setupQuickSelectOptions(); // Set up close button document.getElementById('closeButton').addEventListener('click', () => { view.hideCalendarPopup(); }); // Set up calendar trigger button document.getElementById('calendarTrigger').addEventListener('click', () => { view.showCalendarPopup(); }); // Set up input field calendar trigger document.getElementById('dateInput').addEventListener('click', () => { view.showCalendarPopup(); }); // Set up month/year selector document.getElementById('monthYearSelector').addEventListener('click', () => { // Toggle the dropdown const dropdown = document.getElementById('monthYearDropdown'); if (dropdown.classList.contains('hidden')) { view.showMonthYearDropdown(); } else { view.hideMonthYearDropdown(); } }); // Set up view mode toggles document.getElementById('viewModeMonths').addEventListener('click', () => { switchMonthYearView('months'); }); document.getElementById('viewModeYears').addEventListener('click', () => { switchMonthYearView('years'); }); // Handle clicking outside to close dropdowns document.addEventListener('click', (event) => { const monthYearDropdown = document.getElementById('monthYearDropdown'); const monthYearSelector = document.getElementById('monthYearSelector'); const calendarPopup = document.getElementById('calendarPopup'); const dateInput = document.getElementById('dateInput'); const calendarTrigger = document.getElementById('calendarTrigger'); // Close month/year dropdown if clicking outside of it if (monthYearDropdown && !monthYearDropdown.classList.contains('hidden')) { if (!monthYearDropdown.contains(event.target) && event.target !== monthYearSelector) { view.hideMonthYearDropdown(); } } // Close calendar popup if clicking outside of it and input if (calendarPopup && !calendarPopup.classList.contains('hidden')) { if (!calendarPopup.contains(event.target) && event.target !== dateInput && event.target !== calendarTrigger) { view.hideCalendarPopup(); } } }); // Add touch swipe gesture for calendar navigation on mobile setupTouchGestures(); // Add passive touch handlers to sidebar for better scrolling const datepickerSidebar = document.getElementById('datepickerSidebar'); if (datepickerSidebar) { datepickerSidebar.addEventListener('touchstart', () => {}, { passive: true }); datepickerSidebar.addEventListener('touchmove', () => {}, { passive: true }); // Also add to the quick option groups const quickOptionGroups = datepickerSidebar.querySelectorAll('.space-y-2'); quickOptionGroups.forEach(group => { group.addEventListener('touchstart', () => {}, { passive: true }); group.addEventListener('touchmove', () => {}, { passive: true }); }); } // Add touch handlers to popup for better scrolling const calendarPopup = document.getElementById('calendarPopup'); if (calendarPopup) { calendarPopup.addEventListener('touchstart', () => {}, { passive: true }); calendarPopup.addEventListener('touchmove', (e) => { // Let popup scroll normally e.stopPropagation(); }, { passive: true }); } // Add keyboard navigation to the calendar popup document.addEventListener('keydown', (event) => { const calendarPopup = document.getElementById('calendarPopup'); // Handle escape key to close popups if (event.key === 'Escape') { view.hideMonthYearDropdown(); view.hideCalendarPopup(); } // Only process other keys if calendar is visible if (calendarPopup && !calendarPopup.classList.contains('hidden')) { // Handle arrow keys for navigation if (event.key === 'ArrowLeft' && event.altKey) { primaryCalendar.methods.prevMonth(); if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.prevMonth(); } } else if (event.key === 'ArrowRight' && event.altKey) { primaryCalendar.methods.nextMonth(); if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.nextMonth(); } } else if (event.key === 'ArrowUp' && event.altKey) { primaryCalendar.methods.prevYear(); if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.prevYear(); } } else if (event.key === 'ArrowDown' && event.altKey) { primaryCalendar.methods.nextYear(); if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.nextYear(); } } } }); // Handle input field blur document.getElementById('dateInput').addEventListener('blur', (event) => { // If not clicking within the calendar popup, try to parse the date setTimeout(() => { const activeElement = document.activeElement; const calendarPopup = document.getElementById('calendarPopup'); if (!calendarPopup.contains(activeElement)) { parseInputDate(event.target.value); } }, 100); }); // Handle input field keydown document.getElementById('dateInput').addEventListener('keydown', (event) => { if (event.key === 'Enter') { // Parse date on Enter parseInputDate(event.target.value); view.hideCalendarPopup(); } else if (event.key === 'ArrowDown' || event.key === 'Space') { // Open calendar on ArrowDown or Space event.preventDefault(); view.showCalendarPopup(); } else if (event.key === 'Escape') { // Close calendar on Escape view.hideCalendarPopup(); } }); // Add keyboard navigation in the calendar grid document.getElementById('calendar').addEventListener('keydown', (event) => { accessibility.handleCalendarKeyboardNavigation(event); }); if (document.getElementById('firstMonthCalendar')) { document.getElementById('firstMonthCalendar').addEventListener('keydown', (event) => { accessibility.handleCalendarKeyboardNavigation(event); }); } if (document.getElementById('secondMonthCalendar')) { document.getElementById('secondMonthCalendar').addEventListener('keydown', (event) => { accessibility.handleCalendarKeyboardNavigation(event); }); } // Parse a date string from the input field function parseInputDate(dateStr) { if (!dateStr.trim()) { primaryCalendar.methods.clearSelection(); return; } try { if (primaryCalendar.bindings.isRangeSelection.current) { // Try to parse a date range (format: "MM/DD/YYYY - MM/DD/YYYY") const rangeParts = dateStr.split('-').map(part => part.trim()); if (rangeParts.length === 2) { const startDate = accessibility.parseDate(rangeParts[0]); let endDate = null; // Only try to parse end date if it's not empty if (rangeParts[1] !== '...' && rangeParts[1].trim() !== '') { endDate = accessibility.parseDate(rangeParts[1]); } if (startDate) { // Select start date primaryCalendar.methods.selectDate( startDate.getFullYear(), startDate.getMonth(), startDate.getDate() ); // Navigate to start date month/year primaryCalendar.methods.goToDate(startDate); // If end date is valid, select it too if (endDate) { primaryCalendar.methods.selectDate( endDate.getFullYear(), endDate.getMonth(), endDate.getDate() ); // Update secondary calendar for double month view if needed if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.goToDate(endDate); } } } else { accessibility.announce('Invalid date format. Please enter dates in MM/DD/YYYY format.'); } } } else { // Try to parse a single date using our enhanced parser const date = accessibility.parseDate(dateStr); if (date) { // Navigate to the parsed date's month/year primaryCalendar.methods.goToDate(date); // Select the parsed date primaryCalendar.methods.selectDate( date.getFullYear(), date.getMonth(), date.getDate() ); } else { accessibility.announce('Invalid date format. Please enter dates in MM/DD/YYYY format.'); } } } catch (error) { console.error('Error parsing date:', error); accessibility.announce('Error parsing date. Please enter a valid date.'); } } // Set up sidebar quick select options function setupQuickSelectOptions() { // Single date options document.getElementById('selectToday')?.addEventListener('click', () => { const today = new Date(); selectSingleDate(today); }); document.getElementById('selectTomorrow')?.addEventListener('click', () => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); selectSingleDate(tomorrow); }); document.getElementById('selectNextWeek')?.addEventListener('click', () => { const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7); selectSingleDate(nextWeek); }); document.getElementById('selectNextMonth')?.addEventListener('click', () => { const nextMonth = new Date(); nextMonth.setMonth(nextMonth.getMonth() + 1); selectSingleDate(nextMonth); }); // Date range options document.getElementById('selectThisWeek')?.addEventListener('click', () => { const today = new Date(); const startOfWeek = new Date(today); const dayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc. startOfWeek.setDate(today.getDate() - dayOfWeek); const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(startOfWeek.getDate() + 6); selectDateRange(startOfWeek, endOfWeek); }); document.getElementById('selectThisMonth')?.addEventListener('click', () => { const today = new Date(); const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); selectDateRange(startOfMonth, endOfMonth); }); document.getElementById('selectNextWeekRange')?.addEventListener('click', () => { const today = new Date(); const endDate = new Date(today); endDate.setDate(today.getDate() + 6); selectDateRange(today, endDate); }); document.getElementById('selectNext30Days')?.addEventListener('click', () => { const today = new Date(); const endDate = new Date(today); endDate.setDate(today.getDate() + 29); // +29 to make it inclusive of 30 days selectDateRange(today, endDate); }); } // Helper functions for date selection function selectSingleDate(date) { // Make sure we're in single date mode if (primaryCalendar.bindings.isRangeSelection.current) { primaryCalendar.methods.setRangeSelectionMode(false); updateToggleModeButton(false); } // Navigate to the date and select it primaryCalendar.methods.goToDate(date); primaryCalendar.methods.selectDate( date.getFullYear(), date.getMonth(), date.getDate() ); } function selectDateRange(startDate, endDate) { // Make sure we're in range mode if (!primaryCalendar.bindings.isRangeSelection.current) { primaryCalendar.methods.setRangeSelectionMode(true); updateToggleModeButton(true); } // Switch to double month view view.setCalendarViewMode('doubleMonth'); // Navigate primary calendar to start date month primaryCalendar.methods.goToDate(startDate); // Navigate secondary calendar to end date month secondaryCalendar.methods.goToDate(endDate); // Select start date first, then end date primaryCalendar.methods.selectDate( startDate.getFullYear(), startDate.getMonth(), startDate.getDate() ); primaryCalendar.methods.selectDate( endDate.getFullYear(), endDate.getMonth(), endDate.getDate() ); } // Switch between month and year views in the dropdown function switchMonthYearView(viewType) { const monthsList = document.getElementById('monthsList'); const yearsList = document.getElementById('yearsList'); const monthsButton = document.getElementById('viewModeMonths'); const yearsButton = document.getElementById('viewModeYears'); if (viewType === 'months') { monthsList.classList.remove('hidden'); yearsList.classList.add('hidden'); monthsButton.classList.add('bg-primary', 'text-white'); monthsButton.classList.remove('bg-gray-100', 'text-gray-700'); yearsButton.classList.remove('bg-primary', 'text-white'); yearsButton.classList.add('bg-gray-100', 'hover:bg-gray-200'); monthsButton.setAttribute('aria-pressed', 'true'); yearsButton.setAttribute('aria-pressed', 'false'); } else { monthsList.classList.add('hidden'); yearsList.classList.remove('hidden'); monthsButton.classList.remove('bg-primary', 'text-white'); monthsButton.classList.add('bg-gray-100', 'hover:bg-gray-200'); yearsButton.classList.add('bg-primary', 'text-white'); yearsButton.classList.remove('bg-gray-100', 'text-gray-700'); monthsButton.setAttribute('aria-pressed', 'false'); yearsButton.setAttribute('aria-pressed', 'true'); } } // Set up touch gestures for mobile function setupTouchGestures() { const calendarViewContainer = document.getElementById('calendarViewContainer'); if (!calendarViewContainer) return; let touchStartX = 0; let touchEndX = 0; let touchStartY = 0; let touchEndY = 0; let isSwiping = false; calendarViewContainer.addEventListener('touchstart', (e) => { touchStartX = e.changedTouches[0].screenX; touchStartY = e.changedTouches[0].screenY; isSwiping = true; }, { passive: true }); calendarViewContainer.addEventListener('touchmove', (e) => { if (!isSwiping) return; // Add visual feedback during swipe const deltaX = e.changedTouches[0].screenX - touchStartX; if (Math.abs(deltaX) > 30) { calendarViewContainer.style.transform = `translateX(${deltaX * 0.3}px)`; calendarViewContainer.style.opacity = `${1 - Math.abs(deltaX) * 0.002}`; } }, { passive: true }); calendarViewContainer.addEventListener('touchend', (e) => { if (!isSwiping) return; touchEndX = e.changedTouches[0].screenX; touchEndY = e.changedTouches[0].screenY; // Reset the visual transformation calendarViewContainer.style.transform = ''; calendarViewContainer.style.opacity = ''; handleSwipe(); isSwiping = false; }, { passive: true }); calendarViewContainer.addEventListener('touchcancel', () => { // Reset the visual transformation if touch is canceled calendarViewContainer.style.transform = ''; calendarViewContainer.style.opacity = ''; isSwiping = false; }, { passive: true }); function handleSwipe() { const minSwipeDistance = 50; const verticalMovement = Math.abs(touchEndY - touchStartY); const horizontalMovement = Math.abs(touchEndX - touchStartX); // Only process as horizontal swipe if horizontal movement is greater than vertical if (horizontalMovement > verticalMovement && horizontalMovement > minSwipeDistance) { // Add swipe animation class calendarViewContainer.classList.add('swiping'); if (touchStartX - touchEndX > minSwipeDistance) { // Swipe left - next month primaryCalendar.methods.nextMonth(); if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.nextMonth(); } } if (touchEndX - touchStartX > minSwipeDistance) { // Swipe right - previous month primaryCalendar.methods.prevMonth(); if (calendars.state.getViewMode() === 'doubleMonth') { secondaryCalendar.methods.prevMonth(); } } // Remove animation class after transition completes setTimeout(() => { calendarViewContainer.classList.remove('swiping'); }, 300); } } } // Update the tooltips for all buttons const buttons = document.querySelectorAll('button:not([id="monthYearSelector"])'); buttons.forEach(button => { if (!button.hasAttribute('title') && button.textContent.trim()) { button.setAttribute('title', button.textContent.trim()); } }); // Helper function to update toggle button text and icon function updateToggleModeButton(isRangeMode) { const toggleButton = document.getElementById('toggleMode'); const iconElement = toggleButton.querySelector('i'); // Update the icon class if (isRangeMode) { iconElement.className = 'fa-solid fa-calendar mr-2'; // If in single month view, switch to double month view for range selection if (calendars.state.getViewMode() === 'singleMonth') { view.setCalendarViewMode('doubleMonth'); } } else { iconElement.className = 'fa-solid fa-calendar-days mr-2'; // If in double month view, switch to single month view for single selection if (calendars.state.getViewMode() === 'doubleMonth') { view.setCalendarViewMode('singleMonth'); } } // Update text content (without affecting the icon) const textNode = document.getElementById('toggleModeText'); if (textNode) { textNode.textContent = isRangeMode ? 'Toggle Single Selection' : 'Toggle Range Selection'; } else { toggleButton.appendChild(document.createTextNode( isRangeMode ? 'Toggle Single Selection' : 'Toggle Range Selection' )); } } });