503 lines
19 KiB
JavaScript
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
|
|
}; |