freesched/public/js/components/timepicker.js

503 lines
19 KiB
JavaScript

// 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 `
<div class="timepicker-modal" role="dialog" tabindex="-1" id="visualTimePicker" style="display: none;">
<div id="timepickerWrapper" class="timepicker-wrapper h-100 d-flex align-items-center justify-content-center flex-column position-fixed animation fade-in" style="animation-duration: 300ms;">
<div class="d-flex align-items-center justify-content-center flex-column timepicker-container">
<div class="d-flex flex-column timepicker-elements justify-content-around">
<div id="timepickerHead" class="timepicker-head d-flex flex-row align-items-center justify-content-center" style="padding-right:0px">
<div class="timepicker-head-content d-flex w-100 justify-content-evenly">
<div class="timepicker-current-wrapper">
<span class="position-relative h-100">
<button type="button" class="timepicker-current timepicker-hour active" tabindex="0" style="pointer-events: none;">12</button>
</span>
<button type="button" class="timepicker-dot" disabled="">:</button>
<span class="position-relative h-100">
<button type="button" class="timepicker-current timepicker-minute" tabindex="0">00</button>
</span>
</div>
<div class="d-flex flex-column justify-content-center timepicker-mode-wrapper">
<button type="button" class="timepicker-hour-mode timepicker-am" tabindex="0">AM</button>
<button type="button" class="timepicker-hour-mode timepicker-pm active" tabindex="0">PM</button>
</div>
</div>
</div>
<div id="timepickerClockWrapper" class="timepicker-clock-wrapper d-flex justify-content-center flex-column align-items-center">
<div class="timepicker-clock timepicker-clock-animation">
<span class="timepicker-middle-dot position-absolute"></span>
<div class="timepicker-hand-pointer position-absolute" style="transform: rotateZ(360deg);">
<div class="timepicker-circle position-absolute active"></div>
</div>
<!-- Clock face will be dynamically generated by JS -->
</div>
</div>
</div>
<div id="timepickerFooter" class="timepicker-footer">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="timepicker-button timepicker-clear" tabindex="0">Clear</button>
<button type="button" class="timepicker-button timepicker-cancel" tabindex="0">Cancel</button>
<button type="button" class="timepicker-button timepicker-submit" tabindex="0">Ok</button>
</div>
</div>
</div>
</div>
</div>`;
}
// 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
};