// js/components/timepicker.js // Initialize the visual time picker function initializeVisualTimePicker(dateStr, availableTimes) { // Remove any existing time picker const existingTimePicker = document.getElementById('visualTimePicker'); if (existingTimePicker) { existingTimePicker.remove(); } // Create the timepicker modal from the template - moved to a separate function for clarity document.body.insertAdjacentHTML('beforeend', createTimepickerTemplate()); // Store the dateStr for later use const currentWrapper = document.querySelector('.timepicker-current-wrapper'); if (currentWrapper) { currentWrapper.setAttribute('data-date', dateStr); } // Initialize timepicker functionality and store the reference to the controller const controller = initializeTimepickerFunctionality(dateStr, availableTimes); // Store a reference to the controller in the global scope for access from event handlers window.timepickerController = controller; return controller; } // Helper function to show the timepicker function showTimepicker(timepickerController) { const timepicker = document.getElementById('visualTimePicker'); if (!timepicker) return; // First make the timepicker visible but with opacity 0 timepicker.style.display = 'block'; timepicker.style.opacity = '0'; // Force a reflow to ensure the clock dimensions are calculated setTimeout(() => { // Get the controller from the DOM element if not provided if (!timepickerController || !timepickerController.switchToHoursView) { // Find the controller in the global scope const clockElements = document.querySelector('.timepicker-clock'); if (clockElements) { // Manually render the clock face if needed const hourTips = clockElements.querySelectorAll('.timepicker-time-tips-hours'); if (hourTips.length === 0) { // Re-initialize the timepicker if no hour tips are found const dateStr = document.querySelector('.timepicker-current-wrapper') ? document.querySelector('.timepicker-current-wrapper').getAttribute('data-date') : ''; const availableTimes = []; initializeTimepickerFunctionality(dateStr, availableTimes); } } } else { timepickerController.switchToHoursView(); } // Make the timepicker visible timepicker.style.opacity = '1'; document.body.style.overflow = 'hidden'; document.body.style.paddingRight = '0px'; }, 10); } // Create the timepicker HTML template function createTimepickerTemplate() { return ` `; } // Initialize the timepicker functionality function initializeTimepickerFunctionality(dateStr, availableTimes) { // DOM elements - group related elements together const timeElements = { hour: document.querySelector('.timepicker-hour'), minute: document.querySelector('.timepicker-minute'), am: document.querySelector('.timepicker-am'), pm: document.querySelector('.timepicker-pm'), dot: document.querySelector('.timepicker-dot') }; const clockElements = { wrapper: document.querySelector('.timepicker-clock-wrapper'), clock: document.querySelector('.timepicker-clock'), hand: document.querySelector('.timepicker-hand-pointer') }; const actionButtons = { clear: document.querySelector('.timepicker-clear'), cancel: document.querySelector('.timepicker-cancel'), submit: document.querySelector('.timepicker-submit') }; // State variables const state = { currentView: 'hours', selectedHour: 12, selectedMinute: 0, isDragging: false, isPM: timeElements.pm.classList.contains('active') }; // Create a global variable to track if we're dragging from inside the timepicker window.timepickerDragging = false; // Initialize setupEventListeners(); renderClockFace(state.currentView); function setupEventListeners() { // Header hour/minute buttons timeElements.hour.addEventListener('click', (e) => { switchView('hours'); e.stopPropagation(); }); timeElements.minute.addEventListener('click', (e) => { switchView('minutes'); e.stopPropagation(); }); // Clock face events clockElements.clock.addEventListener('mousedown', handleClockMouseDown); document.addEventListener('mousemove', handleClockMouseMove); document.addEventListener('mouseup', handleClockMouseUp); // Prevent text selection on the clock clockElements.clock.style.userSelect = 'none'; clockElements.clock.style.webkitUserSelect = 'none'; clockElements.clock.style.msUserSelect = 'none'; // AM/PM buttons timeElements.am.addEventListener('click', (e) => { setAmPm('AM'); e.stopPropagation(); }); timeElements.pm.addEventListener('click', (e) => { setAmPm('PM'); e.stopPropagation(); }); // Action buttons actionButtons.clear.addEventListener('click', (e) => { clearTime(); e.stopPropagation(); }); actionButtons.cancel.addEventListener('click', (e) => { closeTimepicker(); e.stopPropagation(); }); actionButtons.submit.addEventListener('click', (e) => { submitTime(); e.stopPropagation(); }); } function switchView(view) { state.currentView = view; // Update active state in header if (view === 'hours') { timeElements.hour.classList.add('active'); timeElements.minute.classList.remove('active'); timeElements.hour.style.pointerEvents = 'none'; timeElements.minute.style.pointerEvents = 'auto'; } else { timeElements.hour.classList.remove('active'); timeElements.minute.classList.add('active'); timeElements.hour.style.pointerEvents = 'auto'; timeElements.minute.style.pointerEvents = 'none'; } renderClockFace(view); } function renderClockFace(view) { // Clear existing time tips const existingTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours, .timepicker-time-tips-minutes'); existingTips.forEach(tip => tip.remove()); if (view === 'hours') { renderHoursFace(); updateHandPosition(state.selectedHour, 'hours'); } else { renderMinutesFace(); updateHandPosition(state.selectedMinute, 'minutes'); } } function renderHoursFace() { const clockRadius = clockElements.clock.offsetWidth / 2; const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius) for (let hour = 1; hour <= 12; hour++) { const tip = createClockTip(hour, clockRadius, tipRadius, 'hours'); if (hour === state.selectedHour) { tip.classList.add('active'); } clockElements.clock.appendChild(tip); } } function renderMinutesFace() { const clockRadius = clockElements.clock.offsetWidth / 2; const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius) for (let minute = 0; minute < 60; minute += 5) { const tip = createClockTip(minute, clockRadius, tipRadius, 'minutes'); if (minute === state.selectedMinute) { tip.classList.add('active'); } clockElements.clock.appendChild(tip); } } // Helper function to create clock face tips (numbers) function createClockTip(value, clockRadius, tipRadius, type) { // Calculate angle and position let angle, displayValue; if (type === 'hours') { angle = ((value * 30) - 90) * (Math.PI / 180); displayValue = value; } else { angle = ((value * 6) - 90) * (Math.PI / 180); displayValue = value.toString().padStart(2, '0'); } // Calculate position const left = clockRadius + tipRadius * Math.cos(angle); const top = clockRadius + tipRadius * Math.sin(angle); // Create the tip element const tip = document.createElement('span'); tip.className = `timepicker-time-tips-${type}`; tip.style.left = `${left}px`; tip.style.top = `${top}px`; tip.style.position = 'absolute'; tip.style.transform = 'translate(-50%, -50%)'; tip.style.userSelect = 'none'; tip.style.webkitUserSelect = 'none'; tip.style.msUserSelect = 'none'; const tipElement = document.createElement('span'); tipElement.className = 'timepicker-tips-element'; tipElement.textContent = displayValue; tip.appendChild(tipElement); return tip; } function updateHandPosition(value, view) { let angle; if (view === 'hours') { // For hours, convert 12 to 0 for calculation purposes const hour = value === 12 ? 0 : value; angle = (hour / 12) * 360; } else { angle = (value / 60) * 360; } clockElements.hand.style.transform = `rotateZ(${angle}deg)`; // Update the active circle on the hand const circle = clockElements.hand.querySelector('.timepicker-circle'); if (circle) { circle.classList.add('active'); } } function handleClockMouseDown(event) { state.isDragging = true; // Set the global flag to indicate we're dragging from inside the timepicker window.timepickerDragging = true; updateTimeFromClockPosition(event); // Stop event propagation to prevent the click outside handler from firing event.stopPropagation(); } function handleClockMouseMove(event) { if (state.isDragging) { updateTimeFromClockPosition(event); } } function handleClockMouseUp() { if (state.isDragging) { state.isDragging = false; // Reset the global dragging flag after a short delay // This allows the click event to process first setTimeout(() => { window.timepickerDragging = false; }, 10); // If we just finished selecting an hour, switch to minutes if (state.currentView === 'hours') { switchView('minutes'); } } } function updateTimeFromClockPosition(event) { const clockRect = clockElements.clock.getBoundingClientRect(); const centerX = clockRect.left + clockRect.width / 2; const centerY = clockRect.top + clockRect.height / 2; // Calculate angle from center to mouse position const x = event.clientX - centerX; const y = event.clientY - centerY; // Calculate angle in degrees, starting from 12 o'clock position let angle = Math.atan2(y, x) * (180 / Math.PI) + 90; // Normalize angle to 0-360 if (angle < 0) { angle += 360; } if (state.currentView === 'hours') { updateHourFromAngle(angle); } else { updateMinuteFromAngle(angle); } } function updateHourFromAngle(angle) { // Convert angle to hour (1-12) let hour = Math.round(angle / 30); // Handle edge cases for full circle if (hour === 0 || hour > 12) hour = 12; state.selectedHour = hour; timeElements.hour.textContent = hour; updateHandPosition(hour, 'hours'); // Update active class on hour tips const hourTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours'); hourTips.forEach(tip => { const hourValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent); if (hourValue === state.selectedHour) { tip.classList.add('active'); } else { tip.classList.remove('active'); } }); } function updateMinuteFromAngle(angle) { // Convert angle to minute (0-55, step 5) let minute = Math.floor(angle / 6); // Round to nearest 5 minute = Math.round(minute / 5) * 5; // Handle edge case for full circle if (minute >= 60) minute = 0; state.selectedMinute = minute; timeElements.minute.textContent = minute.toString().padStart(2, '0'); updateHandPosition(minute, 'minutes'); // Update active class on minute tips const minuteTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-minutes'); minuteTips.forEach(tip => { const minuteValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent); if (minuteValue === state.selectedMinute) { tip.classList.add('active'); } else { tip.classList.remove('active'); } }); } function setAmPm(period) { if (period === 'AM') { timeElements.am.classList.add('active'); timeElements.pm.classList.remove('active'); state.isPM = false; } else { timeElements.am.classList.remove('active'); timeElements.pm.classList.add('active'); state.isPM = true; } } function clearTime() { state.selectedHour = 12; state.selectedMinute = 0; timeElements.hour.textContent = '12'; timeElements.minute.textContent = '00'; setAmPm('PM'); switchView('hours'); } function closeTimepicker() { const timepicker = document.getElementById('visualTimePicker'); if (timepicker) { timepicker.style.display = 'none'; document.body.style.overflow = ''; document.body.style.paddingRight = ''; } // Call the onClose callback if provided through the controller if (window.activeTimepickerController && window.activeTimepickerController.onClose) { window.activeTimepickerController.onClose(); } } function submitTime() { const formattedHour = state.selectedHour; const formattedMinute = state.selectedMinute.toString().padStart(2, '0'); const period = state.isPM ? 'pm' : 'am'; const timeValue = `${formattedHour}:${formattedMinute}${period}`; console.log('Submitting time value:', timeValue); // Get the date string from the data attribute const currentWrapper = document.querySelector('.timepicker-current-wrapper'); const dateStr = currentWrapper ? currentWrapper.getAttribute('data-date') : ''; // Check if time already exists in available times if (availableTimes && availableTimes.includes(timeValue)) { // Silently handle duplicate time slots - no error message console.log('Time slot already exists, not adding duplicate'); } else { // Call the onSubmit callback if provided through the controller if (window.activeTimepickerController && window.activeTimepickerController.onSubmit) { console.log('Calling onSubmit with time:', timeValue); window.activeTimepickerController.onSubmit(timeValue); } else { console.error('No active timepicker controller found'); } } closeTimepicker(); } // Return controller object with public methods return { renderClockFace, closeTimepicker, switchToHoursView: () => switchView('hours') }; } // Export functions for use in other files export { initializeVisualTimePicker, showTimepicker, createTimepickerTemplate };