Update timepicker element
This commit is contained in:
parent
12c5698f57
commit
3e89d1f4b6
626
public/admin.js
626
public/admin.js
@ -309,217 +309,427 @@ function initializeVisualTimePicker(dateStr, availableTimes) {
|
||||
existingTimePicker.remove();
|
||||
}
|
||||
|
||||
// Create the visual time picker container
|
||||
const timePicker = document.createElement('div');
|
||||
timePicker.id = 'visualTimePicker';
|
||||
timePicker.classList.add('card', 'shadow');
|
||||
timePicker.style.display = 'none';
|
||||
timePicker.style.position = 'fixed';
|
||||
timePicker.style.zIndex = '1050';
|
||||
timePicker.style.width = '300px';
|
||||
timePicker.style.top = '50%';
|
||||
timePicker.style.left = '50%';
|
||||
timePicker.style.transform = 'translate(-50%, -50%)';
|
||||
// Create the timepicker modal from the template - moved to a separate function for clarity
|
||||
document.body.insertAdjacentHTML('beforeend', createTimepickerTemplate());
|
||||
|
||||
// Create the header
|
||||
const header = document.createElement('div');
|
||||
header.classList.add('card-header', 'bg-primary', 'text-white', 'text-center', 'p-0');
|
||||
// Initialize timepicker functionality and store the reference to the controller
|
||||
const timepickerController = initializeTimepickerFunctionality(dateStr, availableTimes);
|
||||
|
||||
// Create the time display in the header
|
||||
const timeDisplay = document.createElement('div');
|
||||
timeDisplay.classList.add('display-4', 'py-3', 'mb-0');
|
||||
timeDisplay.innerHTML = '<span id="timePickerHour">12</span>:<span id="timePickerMinute">00</span> <span id="timePickerAMPM">PM</span>';
|
||||
header.appendChild(timeDisplay);
|
||||
|
||||
timePicker.appendChild(header);
|
||||
|
||||
// Create the body
|
||||
const body = document.createElement('div');
|
||||
body.classList.add('card-body', 'bg-white');
|
||||
|
||||
// Create the clock face container
|
||||
const clockFace = document.createElement('div');
|
||||
clockFace.classList.add('time-picker-clock', 'mb-3');
|
||||
clockFace.style.position = 'relative';
|
||||
clockFace.style.width = '250px';
|
||||
clockFace.style.height = '250px';
|
||||
clockFace.style.margin = '0 auto';
|
||||
clockFace.style.backgroundColor = '#f8f9fa';
|
||||
clockFace.style.borderRadius = '50%';
|
||||
|
||||
// Add clock center dot
|
||||
const clockCenter = document.createElement('div');
|
||||
clockCenter.style.position = 'absolute';
|
||||
clockCenter.style.top = '50%';
|
||||
clockCenter.style.left = '50%';
|
||||
clockCenter.style.transform = 'translate(-50%, -50%)';
|
||||
clockCenter.style.width = '8px';
|
||||
clockCenter.style.height = '8px';
|
||||
clockCenter.style.backgroundColor = '#0d6efd';
|
||||
clockCenter.style.borderRadius = '50%';
|
||||
clockFace.appendChild(clockCenter);
|
||||
|
||||
// Add hour numbers to the clock face
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
const hourNumber = document.createElement('div');
|
||||
hourNumber.classList.add('hour-number');
|
||||
hourNumber.textContent = i;
|
||||
hourNumber.style.position = 'absolute';
|
||||
hourNumber.style.width = '40px';
|
||||
hourNumber.style.height = '40px';
|
||||
hourNumber.style.textAlign = 'center';
|
||||
hourNumber.style.lineHeight = '40px';
|
||||
hourNumber.style.fontWeight = 'bold';
|
||||
hourNumber.style.cursor = 'pointer';
|
||||
|
||||
// Calculate position (in a circle)
|
||||
const angle = (i * 30 - 90) * (Math.PI / 180); // 30 degrees per hour, starting at 12 o'clock
|
||||
const radius = 100; // Distance from center
|
||||
const left = 125 + radius * Math.cos(angle);
|
||||
const top = 125 + radius * Math.sin(angle);
|
||||
|
||||
hourNumber.style.left = `${left - 20}px`; // Adjust for element width
|
||||
hourNumber.style.top = `${top - 20}px`; // Adjust for element height
|
||||
|
||||
// Add click event to select hour
|
||||
hourNumber.addEventListener('click', () => {
|
||||
document.getElementById('timePickerHour').textContent = i;
|
||||
|
||||
// Highlight selected hour
|
||||
document.querySelectorAll('.hour-number').forEach(el => {
|
||||
el.style.backgroundColor = 'transparent';
|
||||
el.style.color = '#212529';
|
||||
});
|
||||
hourNumber.style.backgroundColor = '#0d6efd';
|
||||
hourNumber.style.color = 'white';
|
||||
|
||||
// Show minute selection
|
||||
showMinuteSelection();
|
||||
// Show time picker when button is clicked
|
||||
timeButton.addEventListener('click', (e) => {
|
||||
showTimepicker(timepickerController);
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
clockFace.appendChild(hourNumber);
|
||||
// Close time picker when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
const timepicker = document.getElementById('visualTimePicker');
|
||||
const timepickerContainer = document.querySelector('.timepicker-container');
|
||||
|
||||
// Only close if we're not in the middle of a drag operation
|
||||
if (timepicker &&
|
||||
timepicker.style.display === 'block' &&
|
||||
e.target !== timeButton &&
|
||||
(!timepickerContainer || !timepickerContainer.contains(e.target)) &&
|
||||
!window.timepickerDragging) { // Check the global dragging flag
|
||||
timepickerController.closeTimepicker();
|
||||
}
|
||||
|
||||
// Function to show minute selection
|
||||
function showMinuteSelection() {
|
||||
// Hide hour numbers
|
||||
document.querySelectorAll('.hour-number').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
|
||||
// Show minute numbers
|
||||
document.querySelectorAll('.minute-number').forEach(el => {
|
||||
el.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// Function to show hour selection
|
||||
function showHourSelection() {
|
||||
// Hide minute numbers
|
||||
document.querySelectorAll('.minute-number').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
// Helper function to show the timepicker
|
||||
function showTimepicker(timepickerController) {
|
||||
const timepicker = document.getElementById('visualTimePicker');
|
||||
if (!timepicker) return;
|
||||
|
||||
// Show hour numbers
|
||||
document.querySelectorAll('.hour-number').forEach(el => {
|
||||
el.style.display = 'block';
|
||||
// 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(() => {
|
||||
// Always start with hours view when opening the timepicker
|
||||
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', () => switchView('hours'));
|
||||
timeElements.minute.addEventListener('click', () => switchView('minutes'));
|
||||
|
||||
// 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', () => setAmPm('AM'));
|
||||
timeElements.pm.addEventListener('click', () => setAmPm('PM'));
|
||||
|
||||
// Action buttons
|
||||
actionButtons.clear.addEventListener('click', clearTime);
|
||||
actionButtons.cancel.addEventListener('click', closeTimepicker);
|
||||
actionButtons.submit.addEventListener('click', submitTime);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add minute numbers to the clock face (initially hidden)
|
||||
const minuteValues = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
||||
minuteValues.forEach((minute, index) => {
|
||||
const minuteNumber = document.createElement('div');
|
||||
minuteNumber.classList.add('minute-number');
|
||||
minuteNumber.textContent = minute.toString().padStart(2, '0');
|
||||
minuteNumber.style.position = 'absolute';
|
||||
minuteNumber.style.width = '40px';
|
||||
minuteNumber.style.height = '40px';
|
||||
minuteNumber.style.textAlign = 'center';
|
||||
minuteNumber.style.lineHeight = '40px';
|
||||
minuteNumber.style.fontWeight = 'bold';
|
||||
minuteNumber.style.cursor = 'pointer';
|
||||
minuteNumber.style.display = 'none'; // Initially hidden
|
||||
function updateMinuteFromAngle(angle) {
|
||||
// Convert angle to minute (0-55, step 5)
|
||||
let minute = Math.floor(angle / 6);
|
||||
|
||||
// Calculate position (in a circle)
|
||||
const angle = (index * 30 - 90) * (Math.PI / 180);
|
||||
const radius = 100; // Distance from center
|
||||
const left = 125 + radius * Math.cos(angle);
|
||||
const top = 125 + radius * Math.sin(angle);
|
||||
// Round to nearest 5
|
||||
minute = Math.round(minute / 5) * 5;
|
||||
|
||||
minuteNumber.style.left = `${left - 20}px`; // Adjust for element width
|
||||
minuteNumber.style.top = `${top - 20}px`; // Adjust for element height
|
||||
// Handle edge case for full circle
|
||||
if (minute >= 60) minute = 0;
|
||||
|
||||
// Add click event to select minute
|
||||
minuteNumber.addEventListener('click', () => {
|
||||
document.getElementById('timePickerMinute').textContent = minute.toString().padStart(2, '0');
|
||||
state.selectedMinute = minute;
|
||||
timeElements.minute.textContent = minute.toString().padStart(2, '0');
|
||||
updateHandPosition(minute, 'minutes');
|
||||
|
||||
// Highlight selected minute
|
||||
document.querySelectorAll('.minute-number').forEach(el => {
|
||||
el.style.backgroundColor = 'transparent';
|
||||
el.style.color = '#212529';
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
minuteNumber.style.backgroundColor = '#0d6efd';
|
||||
minuteNumber.style.color = 'white';
|
||||
}
|
||||
|
||||
// Show hour selection again
|
||||
showHourSelection();
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
clockFace.appendChild(minuteNumber);
|
||||
});
|
||||
function clearTime() {
|
||||
state.selectedHour = 12;
|
||||
state.selectedMinute = 0;
|
||||
timeElements.hour.textContent = '12';
|
||||
timeElements.minute.textContent = '00';
|
||||
setAmPm('PM');
|
||||
switchView('hours');
|
||||
}
|
||||
|
||||
body.appendChild(clockFace);
|
||||
function closeTimepicker() {
|
||||
const timepicker = document.getElementById('visualTimePicker');
|
||||
if (timepicker) {
|
||||
timepicker.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.paddingRight = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Create AM/PM toggle
|
||||
const ampmToggle = document.createElement('div');
|
||||
ampmToggle.classList.add('d-flex', 'justify-content-center', 'mb-3');
|
||||
function submitTime() {
|
||||
const formattedHour = state.selectedHour;
|
||||
const formattedMinute = state.selectedMinute.toString().padStart(2, '0');
|
||||
const period = state.isPM ? 'PM' : 'AM';
|
||||
|
||||
const amButton = document.createElement('button');
|
||||
amButton.classList.add('btn', 'btn-outline-primary', 'me-2');
|
||||
amButton.textContent = 'AM';
|
||||
amButton.addEventListener('click', () => {
|
||||
document.getElementById('timePickerAMPM').textContent = 'AM';
|
||||
amButton.classList.add('active');
|
||||
pmButton.classList.remove('active');
|
||||
});
|
||||
|
||||
const pmButton = document.createElement('button');
|
||||
pmButton.classList.add('btn', 'btn-outline-primary');
|
||||
pmButton.textContent = 'PM';
|
||||
pmButton.addEventListener('click', () => {
|
||||
document.getElementById('timePickerAMPM').textContent = 'PM';
|
||||
pmButton.classList.add('active');
|
||||
amButton.classList.remove('active');
|
||||
});
|
||||
pmButton.classList.add('active'); // Add active class to PM button
|
||||
|
||||
ampmToggle.appendChild(amButton);
|
||||
ampmToggle.appendChild(pmButton);
|
||||
|
||||
body.appendChild(ampmToggle);
|
||||
|
||||
// Create action buttons
|
||||
const actionButtons = document.createElement('div');
|
||||
actionButtons.classList.add('d-flex', 'justify-content-between');
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.classList.add('btn', 'btn-outline-secondary', 'flex-grow-1', 'me-2');
|
||||
closeButton.textContent = 'CANCEL';
|
||||
closeButton.addEventListener('click', () => {
|
||||
timePicker.style.display = 'none';
|
||||
});
|
||||
|
||||
const okButton = document.createElement('button');
|
||||
okButton.classList.add('btn', 'btn-primary', 'flex-grow-1');
|
||||
okButton.textContent = 'OK';
|
||||
okButton.addEventListener('click', () => {
|
||||
const hour = document.getElementById('timePickerHour').textContent;
|
||||
const minute = document.getElementById('timePickerMinute').textContent;
|
||||
const ampm = document.getElementById('timePickerAMPM').textContent.toLowerCase();
|
||||
|
||||
const timeValue = `${hour}:${minute}${ampm}`;
|
||||
document.getElementById('customTimeInput').value = timeValue;
|
||||
const timeValue = `${formattedHour}:${formattedMinute}${period.toLowerCase()}`;
|
||||
|
||||
// Check if time already exists in available times
|
||||
if (availableTimes.includes(timeValue)) {
|
||||
@ -530,37 +740,15 @@ function initializeVisualTimePicker(dateStr, availableTimes) {
|
||||
addSingleTime(dateStr, timeValue);
|
||||
}
|
||||
|
||||
// Hide the time picker
|
||||
timePicker.style.display = 'none';
|
||||
});
|
||||
|
||||
actionButtons.appendChild(closeButton);
|
||||
actionButtons.appendChild(okButton);
|
||||
|
||||
body.appendChild(actionButtons);
|
||||
|
||||
timePicker.appendChild(body);
|
||||
|
||||
// Add the time picker to the document
|
||||
document.body.appendChild(timePicker);
|
||||
|
||||
// Show time picker when button is clicked
|
||||
timeButton.addEventListener('click', (e) => {
|
||||
// Remove positioning relative to button
|
||||
timePicker.style.display = 'block';
|
||||
|
||||
// Reset to default view (hour selection)
|
||||
showHourSelection();
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Close time picker when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target !== timeButton && !timePicker.contains(e.target)) {
|
||||
timePicker.style.display = 'none';
|
||||
closeTimepicker();
|
||||
}
|
||||
});
|
||||
|
||||
// Return controller object with public methods
|
||||
return {
|
||||
renderClockFace,
|
||||
closeTimepicker,
|
||||
switchToHoursView: () => switchView('hours')
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch available dates from the API
|
||||
|
@ -1 +0,0 @@
|
||||
|
413
public/timepicker.css
Normal file
413
public/timepicker.css
Normal file
@ -0,0 +1,413 @@
|
||||
/**
|
||||
* Timepicker styling
|
||||
*/
|
||||
/* Core timepicker styling */
|
||||
.timepicker-wrapper {
|
||||
--fs-timepicker-wrapper-bg: rgba(0, 0, 0, 0.4);
|
||||
--fs-timepicker-elements-min-width: 310px;
|
||||
--fs-timepicker-elements-min-height: 325px;
|
||||
--fs-timepicker-elements-background: #fff;
|
||||
--fs-timepicker-elements-border-top-right-radius: 0.6rem;
|
||||
--fs-timepicker-elements-border-top-left-radius: 0.6rem;
|
||||
--fs-timepicker-head-bg: #3b71ca;
|
||||
--fs-timepicker-head-height: 100px;
|
||||
--fs-timepicker-head-border-top-right-radius: 0.5rem;
|
||||
--fs-timepicker-head-border-top-left-radius: 0.5rem;
|
||||
--fs-timepicker-head-padding-y: 10px;
|
||||
--fs-timepicker-head-padding-right: 24px;
|
||||
--fs-timepicker-head-padding-left: 50px;
|
||||
--fs-timepicker-button-font-size: 0.8rem;
|
||||
--fs-timepicker-button-min-width: 64px;
|
||||
--fs-timepicker-button-font-weight: 500;
|
||||
--fs-timepicker-button-line-height: 40px;
|
||||
--fs-timepicker-button-border-radius: 10px;
|
||||
--fs-timepicker-button-color: #4f4f4f;
|
||||
--fs-timepicker-button-hover-bg: rgba(0, 0, 0, 0.08);
|
||||
--fs-timepicker-button-focus-bg: rgba(0, 0, 0, 0.08);
|
||||
--fs-timepicker-button-padding-x: 10px;
|
||||
--fs-timepicker-button-height: 40px;
|
||||
--fs-timepicker-button-margin-bottom: 10px;
|
||||
--fs-timepicker-current-font-size: 3.75rem;
|
||||
--fs-timepicker-current-font-weight: 300;
|
||||
--fs-timepicker-current-line-height: 1.2;
|
||||
--fs-timepicker-current-color: #fff;
|
||||
--fs-timepicker-current-opacity: 0.54;
|
||||
--fs-timepicker-clock-wrapper-min-width: 310px;
|
||||
--fs-timepicker-clock-wrapper-max-width: 325px;
|
||||
--fs-timepicker-clock-wrapper-min-height: 305px;
|
||||
--fs-timepicker-clock-wrapper-text-color: #4f4f4f;
|
||||
--fs-timepicker-mode-wrapper-font-size: 18px;
|
||||
--fs-timepicker-mode-wrapper-color: rgba(255, 255, 255, 0.54);
|
||||
--fs-timepicker-clock-width: 260px;
|
||||
--fs-timepicker-clock-height: 260px;
|
||||
--fs-timepicker-clock-face-bg: #f0f0f0;
|
||||
--fs-timepicker-time-tips-inner-active-color: #fff;
|
||||
--fs-timepicker-time-tips-inner-active-bg: #3b71ca;
|
||||
--fs-timepicker-time-tips-inner-active-font-weight: 400;
|
||||
--fs-timepicker-dot-font-weight: 300;
|
||||
--fs-timepicker-dot-line-height: 1.2;
|
||||
--fs-timepicker-dot-color: #fff;
|
||||
--fs-timepicker-dot-font-size: 3.75rem;
|
||||
--fs-timepicker-dot-opacity: 0.54;
|
||||
--fs-timepicker-item-middle-dot-width: 6px;
|
||||
--fs-timepicker-item-middle-dot-height: 6px;
|
||||
--fs-timepicker-item-middle-dot-border-radius: 50%;
|
||||
--fs-timepicker-item-middle-dot-bg: #3b71ca;
|
||||
--fs-timepicker-hand-pointer-bg: #3b71ca;
|
||||
--fs-timepicker-hand-pointer-bottom: 50%;
|
||||
--fs-timepicker-hand-pointer-height: 40%;
|
||||
--fs-timepicker-hand-pointer-left: calc(50% - 1px);
|
||||
--fs-timepicker-hand-pointer-width: 2px;
|
||||
--fs-timepicker-circle-top: -21px;
|
||||
--fs-timepicker-circle-left: -15px;
|
||||
--fs-timepicker-circle-width: 4px;
|
||||
--fs-timepicker-circle-border-width: 14px;
|
||||
--fs-timepicker-circle-border-color: #3b71ca;
|
||||
--fs-timepicker-circle-height: 4px;
|
||||
--fs-timepicker-circle-active-background-color: #fff;
|
||||
--fs-timepicker-hour-mode-color: #fff;
|
||||
--fs-timepicker-hour-mode-opacity: 0.54;
|
||||
--fs-timepicker-hour-mode-hover-bg: rgba(0, 0, 0, 0.15);
|
||||
--fs-timepicker-hour-mode-active-color: #fff;
|
||||
--fs-timepicker-footer-border-bottom-left-radius: 0.5rem;
|
||||
--fs-timepicker-footer-border-bottom-right-radius: 0.5rem;
|
||||
--fs-timepicker-footer-height: 56px;
|
||||
--fs-timepicker-footer-padding-x: 12px;
|
||||
--fs-timepicker-footer-bg: #fff;
|
||||
--fs-timepicker-clock-animation: show-up-clock 350ms linear;
|
||||
--fs-timepicker-zindex: 1065;
|
||||
|
||||
touch-action: none;
|
||||
z-index: var(--fs-timepicker-zindex);
|
||||
opacity: 1;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--fs-timepicker-wrapper-bg);
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
.animation {
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-up-clock {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Timepicker components */
|
||||
.timepicker-modal {
|
||||
margin: 0;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
color: #4f4f4f;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
z-index: var(--fs-timepicker-zindex);
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Ensure all elements inside the timepicker use border-box */
|
||||
.timepicker-modal *,
|
||||
.timepicker-modal *::before,
|
||||
.timepicker-modal *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.timepicker-container {
|
||||
max-height: calc(100% - 64px);
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.timepicker-elements {
|
||||
min-width: var(--fs-timepicker-elements-min-width);
|
||||
min-height: var(--fs-timepicker-elements-min-height);
|
||||
background: var(--fs-timepicker-elements-background);
|
||||
border-top-right-radius: var(--fs-timepicker-elements-border-top-right-radius);
|
||||
border-top-left-radius: var(--fs-timepicker-elements-border-top-left-radius);
|
||||
}
|
||||
|
||||
.timepicker-head {
|
||||
background-color: var(--fs-timepicker-head-bg);
|
||||
height: var(--fs-timepicker-head-height);
|
||||
border-top-right-radius: var(--fs-timepicker-head-border-top-right-radius);
|
||||
border-top-left-radius: var(--fs-timepicker-head-border-top-left-radius);
|
||||
padding: var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-right) var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-left);
|
||||
}
|
||||
|
||||
.timepicker-current {
|
||||
font-size: var(--fs-timepicker-current-font-size);
|
||||
font-weight: var(--fs-timepicker-current-font-weight);
|
||||
line-height: var(--fs-timepicker-current-line-height);
|
||||
color: var(--fs-timepicker-current-color);
|
||||
opacity: var(--fs-timepicker-current-opacity);
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
vertical-align: unset;
|
||||
}
|
||||
|
||||
.timepicker-current.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.timepicker-dot {
|
||||
font-size: var(--fs-timepicker-dot-font-size);
|
||||
font-weight: var(--fs-timepicker-dot-font-weight);
|
||||
line-height: var(--fs-timepicker-dot-line-height);
|
||||
color: var(--fs-timepicker-dot-color);
|
||||
opacity: var(--fs-timepicker-dot-opacity);
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.timepicker-mode-wrapper {
|
||||
font-size: var(--fs-timepicker-mode-wrapper-font-size);
|
||||
color: var(--fs-timepicker-mode-wrapper-color);
|
||||
}
|
||||
|
||||
.timepicker-hour-mode {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--fs-timepicker-hour-mode-color);
|
||||
opacity: var(--fs-timepicker-hour-mode-opacity);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* These focus styles are overridden later in the file with !important */
|
||||
.timepicker-hour-mode:hover,
|
||||
.timepicker-hour-mode:focus,
|
||||
.timepicker-hour:hover,
|
||||
.timepicker-hour:focus,
|
||||
.timepicker-minute:hover,
|
||||
.timepicker-minute:focus {
|
||||
background-color: var(--fs-timepicker-hour-mode-hover-bg);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timepicker-hour-mode.active,
|
||||
.timepicker-hour.active,
|
||||
.timepicker-minute.active {
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.timepicker-clock-wrapper {
|
||||
min-width: var(--fs-timepicker-clock-wrapper-min-width);
|
||||
max-width: var(--fs-timepicker-clock-wrapper-max-width);
|
||||
min-height: var(--fs-timepicker-clock-wrapper-min-height);
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
color: var(--fs-timepicker-clock-wrapper-text-color);
|
||||
}
|
||||
|
||||
.timepicker-clock {
|
||||
position: relative;
|
||||
border-radius: 100%;
|
||||
width: var(--fs-timepicker-clock-width);
|
||||
height: var(--fs-timepicker-clock-height);
|
||||
cursor: default;
|
||||
margin: 0 auto;
|
||||
background-color: var(--fs-timepicker-clock-face-bg);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.timepicker-clock-animation {
|
||||
animation: var(--fs-timepicker-clock-animation);
|
||||
}
|
||||
|
||||
.timepicker-middle-dot {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: var(--fs-timepicker-item-middle-dot-width);
|
||||
height: var(--fs-timepicker-item-middle-dot-height);
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: var(--fs-timepicker-item-middle-dot-border-radius);
|
||||
background-color: var(--fs-timepicker-item-middle-dot-bg);
|
||||
}
|
||||
|
||||
.timepicker-hand-pointer {
|
||||
background-color: var(--fs-timepicker-hand-pointer-bg);
|
||||
bottom: var(--fs-timepicker-hand-pointer-bottom);
|
||||
height: var(--fs-timepicker-hand-pointer-height);
|
||||
left: var(--fs-timepicker-hand-pointer-left);
|
||||
transform-origin: center bottom 0;
|
||||
width: var(--fs-timepicker-hand-pointer-width);
|
||||
}
|
||||
|
||||
.timepicker-circle {
|
||||
top: var(--fs-timepicker-circle-top);
|
||||
left: var(--fs-timepicker-circle-left);
|
||||
width: var(--fs-timepicker-circle-width);
|
||||
border: var(--fs-timepicker-circle-border-width) solid var(--fs-timepicker-circle-border-color);
|
||||
height: var(--fs-timepicker-circle-height);
|
||||
box-sizing: content-box;
|
||||
border-radius: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.timepicker-circle.active {
|
||||
background-color: var(--fs-timepicker-circle-active-background-color);
|
||||
}
|
||||
|
||||
.timepicker-time-tips-minutes,
|
||||
.timepicker-time-tips-hours {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 300;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.timepicker-time-tips-minutes.active,
|
||||
.timepicker-time-tips-hours.active {
|
||||
color: var(--fs-timepicker-time-tips-inner-active-color);
|
||||
background-color: var(--fs-timepicker-time-tips-inner-active-bg);
|
||||
font-weight: var(--fs-timepicker-time-tips-inner-active-font-weight);
|
||||
}
|
||||
|
||||
.timepicker-footer {
|
||||
border-bottom-left-radius: var(--fs-timepicker-footer-border-bottom-left-radius);
|
||||
border-bottom-right-radius: var(--fs-timepicker-footer-border-bottom-right-radius);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--fs-timepicker-footer-height);
|
||||
padding-left: var(--fs-timepicker-footer-padding-x);
|
||||
padding-right: var(--fs-timepicker-footer-padding-x);
|
||||
background-color: var(--fs-timepicker-footer-bg);
|
||||
}
|
||||
|
||||
.timepicker-button {
|
||||
font-size: var(--fs-timepicker-button-font-size);
|
||||
min-width: var(--fs-timepicker-button-min-width);
|
||||
box-sizing: border-box;
|
||||
font-weight: var(--fs-timepicker-button-font-weight);
|
||||
line-height: var(--fs-timepicker-button-line-height);
|
||||
border-radius: var(--fs-timepicker-button-border-radius);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fs-timepicker-button-color);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
outline: none;
|
||||
padding: 0 var(--fs-timepicker-button-padding-x);
|
||||
height: var(--fs-timepicker-button-height);
|
||||
margin-bottom: var(--fs-timepicker-button-margin-bottom);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timepicker-button:hover {
|
||||
background-color: var(--fs-timepicker-button-hover-bg);
|
||||
}
|
||||
|
||||
.timepicker-button:focus {
|
||||
outline: none;
|
||||
background-color: var(--fs-timepicker-button-focus-bg);
|
||||
}
|
||||
|
||||
/* Add missing button styling */
|
||||
button {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/** Remove focus from buttons after clicked **/
|
||||
/* Prevent selection/focus styling on all buttons in the timepicker */
|
||||
.timepicker-modal button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Override focus styles completely */
|
||||
.timepicker-modal button:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Prevent text selection on all elements */
|
||||
.timepicker-modal * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* Add this to the body when the timepicker is active */
|
||||
body.timepicker-active {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Add !important to ensure these styles take precedence */
|
||||
.timepicker-current:focus,
|
||||
.timepicker-dot:focus,
|
||||
.timepicker-hour-mode:focus,
|
||||
.timepicker-button:focus {
|
||||
outline: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Only apply background color on hover, not on focus */
|
||||
.timepicker-hour-mode:hover,
|
||||
.timepicker-hour:hover,
|
||||
.timepicker-minute:hover,
|
||||
.timepicker-button:hover {
|
||||
background-color: var(--fs-timepicker-hour-mode-hover-bg);
|
||||
}
|
||||
|
||||
/* Ensure active states are properly styled */
|
||||
.timepicker-hour-mode.active,
|
||||
.timepicker-hour.active,
|
||||
.timepicker-minute.active {
|
||||
color: #fff !important;
|
||||
opacity: 1 !important;
|
||||
}
|
468
timepicker-modal/timepicker-modal.css
Normal file
468
timepicker-modal/timepicker-modal.css
Normal file
@ -0,0 +1,468 @@
|
||||
/* Core timepicker styling */
|
||||
.timepicker-wrapper {
|
||||
--fs-timepicker-wrapper-bg: rgba(0, 0, 0, 0.4);
|
||||
--fs-timepicker-elements-min-width: 310px;
|
||||
--fs-timepicker-elements-min-height: 325px;
|
||||
--fs-timepicker-elements-background: #fff;
|
||||
--fs-timepicker-elements-border-top-right-radius: 0.6rem;
|
||||
--fs-timepicker-elements-border-top-left-radius: 0.6rem;
|
||||
--fs-timepicker-head-bg: #3b71ca;
|
||||
--fs-timepicker-head-height: 100px;
|
||||
--fs-timepicker-head-border-top-right-radius: 0.5rem;
|
||||
--fs-timepicker-head-border-top-left-radius: 0.5rem;
|
||||
--fs-timepicker-head-padding-y: 10px;
|
||||
--fs-timepicker-head-padding-right: 24px;
|
||||
--fs-timepicker-head-padding-left: 50px;
|
||||
--fs-timepicker-button-font-size: 0.8rem;
|
||||
--fs-timepicker-button-min-width: 64px;
|
||||
--fs-timepicker-button-font-weight: 500;
|
||||
--fs-timepicker-button-line-height: 40px;
|
||||
--fs-timepicker-button-border-radius: 10px;
|
||||
--fs-timepicker-button-color: #4f4f4f;
|
||||
--fs-timepicker-button-hover-bg: rgba(0, 0, 0, 0.08);
|
||||
--fs-timepicker-button-focus-bg: rgba(0, 0, 0, 0.08);
|
||||
--fs-timepicker-button-padding-x: 10px;
|
||||
--fs-timepicker-button-height: 40px;
|
||||
--fs-timepicker-button-margin-bottom: 10px;
|
||||
--fs-timepicker-current-font-size: 3.75rem;
|
||||
--fs-timepicker-current-font-weight: 300;
|
||||
--fs-timepicker-current-line-height: 1.2;
|
||||
--fs-timepicker-current-color: #fff;
|
||||
--fs-timepicker-current-opacity: 0.54;
|
||||
--fs-timepicker-clock-wrapper-min-width: 310px;
|
||||
--fs-timepicker-clock-wrapper-max-width: 325px;
|
||||
--fs-timepicker-clock-wrapper-min-height: 305px;
|
||||
--fs-timepicker-clock-wrapper-text-color: #4f4f4f;
|
||||
--fs-timepicker-mode-wrapper-font-size: 18px;
|
||||
--fs-timepicker-mode-wrapper-color: rgba(255, 255, 255, 0.54);
|
||||
--fs-timepicker-clock-width: 260px;
|
||||
--fs-timepicker-clock-height: 260px;
|
||||
--fs-timepicker-clock-face-bg: #f0f0f0;
|
||||
--fs-timepicker-time-tips-inner-active-color: #fff;
|
||||
--fs-timepicker-time-tips-inner-active-bg: #3b71ca;
|
||||
--fs-timepicker-time-tips-inner-active-font-weight: 400;
|
||||
--fs-timepicker-dot-font-weight: 300;
|
||||
--fs-timepicker-dot-line-height: 1.2;
|
||||
--fs-timepicker-dot-color: #fff;
|
||||
--fs-timepicker-dot-font-size: 3.75rem;
|
||||
--fs-timepicker-dot-opacity: 0.54;
|
||||
--fs-timepicker-item-middle-dot-width: 6px;
|
||||
--fs-timepicker-item-middle-dot-height: 6px;
|
||||
--fs-timepicker-item-middle-dot-border-radius: 50%;
|
||||
--fs-timepicker-item-middle-dot-bg: #3b71ca;
|
||||
--fs-timepicker-hand-pointer-bg: #3b71ca;
|
||||
--fs-timepicker-hand-pointer-bottom: 50%;
|
||||
--fs-timepicker-hand-pointer-height: 40%;
|
||||
--fs-timepicker-hand-pointer-left: calc(50% - 1px);
|
||||
--fs-timepicker-hand-pointer-width: 2px;
|
||||
--fs-timepicker-circle-top: -21px;
|
||||
--fs-timepicker-circle-left: -15px;
|
||||
--fs-timepicker-circle-width: 4px;
|
||||
--fs-timepicker-circle-border-width: 14px;
|
||||
--fs-timepicker-circle-border-color: #3b71ca;
|
||||
--fs-timepicker-circle-height: 4px;
|
||||
--fs-timepicker-circle-active-background-color: #fff;
|
||||
--fs-timepicker-hour-mode-color: #fff;
|
||||
--fs-timepicker-hour-mode-opacity: 0.54;
|
||||
--fs-timepicker-hour-mode-hover-bg: rgba(0, 0, 0, 0.15);
|
||||
--fs-timepicker-hour-mode-active-color: #fff;
|
||||
--fs-timepicker-footer-border-bottom-left-radius: 0.5rem;
|
||||
--fs-timepicker-footer-border-bottom-right-radius: 0.5rem;
|
||||
--fs-timepicker-footer-height: 56px;
|
||||
--fs-timepicker-footer-padding-x: 12px;
|
||||
--fs-timepicker-footer-bg: #fff;
|
||||
--fs-timepicker-clock-animation: show-up-clock 350ms linear;
|
||||
--fs-timepicker-zindex: 1065;
|
||||
|
||||
touch-action: none;
|
||||
z-index: var(--fs-timepicker-zindex);
|
||||
opacity: 1;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--fs-timepicker-wrapper-bg);
|
||||
}
|
||||
|
||||
/* Layout classes */
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-content-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-content-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.justify-content-evenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.position-fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.position-absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
.animation {
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-up-clock {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Base typography and inherited properties */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
color: #4f4f4f;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* Timepicker components */
|
||||
.timepicker-modal {
|
||||
z-index: var(--fs-timepicker-zindex);
|
||||
font-family: "Roboto", sans-serif;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Ensure all elements inside the timepicker use border-box */
|
||||
.timepicker-modal *,
|
||||
.timepicker-modal *::before,
|
||||
.timepicker-modal *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.timepicker-container {
|
||||
max-height: calc(100% - 64px);
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.timepicker-elements {
|
||||
min-width: var(--fs-timepicker-elements-min-width);
|
||||
min-height: var(--fs-timepicker-elements-min-height);
|
||||
background: var(--fs-timepicker-elements-background);
|
||||
border-top-right-radius: var(--fs-timepicker-elements-border-top-right-radius);
|
||||
border-top-left-radius: var(--fs-timepicker-elements-border-top-left-radius);
|
||||
}
|
||||
|
||||
.timepicker-head {
|
||||
background-color: var(--fs-timepicker-head-bg);
|
||||
height: var(--fs-timepicker-head-height);
|
||||
border-top-right-radius: var(--fs-timepicker-head-border-top-right-radius);
|
||||
border-top-left-radius: var(--fs-timepicker-head-border-top-left-radius);
|
||||
padding: var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-right) var(--fs-timepicker-head-padding-y) var(--fs-timepicker-head-padding-left);
|
||||
}
|
||||
|
||||
.timepicker-current {
|
||||
font-size: var(--fs-timepicker-current-font-size);
|
||||
font-weight: var(--fs-timepicker-current-font-weight);
|
||||
line-height: var(--fs-timepicker-current-line-height);
|
||||
color: var(--fs-timepicker-current-color);
|
||||
opacity: var(--fs-timepicker-current-opacity);
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
vertical-align: unset;
|
||||
}
|
||||
|
||||
.timepicker-current.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.timepicker-dot {
|
||||
font-size: var(--fs-timepicker-dot-font-size);
|
||||
font-weight: var(--fs-timepicker-dot-font-weight);
|
||||
line-height: var(--fs-timepicker-dot-line-height);
|
||||
color: var(--fs-timepicker-dot-color);
|
||||
opacity: var(--fs-timepicker-dot-opacity);
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.timepicker-mode-wrapper {
|
||||
font-size: var(--fs-timepicker-mode-wrapper-font-size);
|
||||
color: var(--fs-timepicker-mode-wrapper-color);
|
||||
}
|
||||
|
||||
.timepicker-hour-mode {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--fs-timepicker-hour-mode-color);
|
||||
opacity: var(--fs-timepicker-hour-mode-opacity);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* These focus styles are overridden later in the file with !important */
|
||||
.timepicker-hour-mode:hover,
|
||||
.timepicker-hour-mode:focus,
|
||||
.timepicker-hour:hover,
|
||||
.timepicker-hour:focus,
|
||||
.timepicker-minute:hover,
|
||||
.timepicker-minute:focus {
|
||||
background-color: var(--fs-timepicker-hour-mode-hover-bg);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timepicker-hour-mode.active,
|
||||
.timepicker-hour.active,
|
||||
.timepicker-minute.active {
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.timepicker-clock-wrapper {
|
||||
min-width: var(--fs-timepicker-clock-wrapper-min-width);
|
||||
max-width: var(--fs-timepicker-clock-wrapper-max-width);
|
||||
min-height: var(--fs-timepicker-clock-wrapper-min-height);
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
color: var(--fs-timepicker-clock-wrapper-text-color);
|
||||
}
|
||||
|
||||
.timepicker-clock {
|
||||
position: relative;
|
||||
border-radius: 100%;
|
||||
width: var(--fs-timepicker-clock-width);
|
||||
height: var(--fs-timepicker-clock-height);
|
||||
cursor: default;
|
||||
margin: 0 auto;
|
||||
background-color: var(--fs-timepicker-clock-face-bg);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.timepicker-clock-animation {
|
||||
animation: var(--fs-timepicker-clock-animation);
|
||||
}
|
||||
|
||||
.timepicker-middle-dot {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: var(--fs-timepicker-item-middle-dot-width);
|
||||
height: var(--fs-timepicker-item-middle-dot-height);
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: var(--fs-timepicker-item-middle-dot-border-radius);
|
||||
background-color: var(--fs-timepicker-item-middle-dot-bg);
|
||||
}
|
||||
|
||||
.timepicker-hand-pointer {
|
||||
background-color: var(--fs-timepicker-hand-pointer-bg);
|
||||
bottom: var(--fs-timepicker-hand-pointer-bottom);
|
||||
height: var(--fs-timepicker-hand-pointer-height);
|
||||
left: var(--fs-timepicker-hand-pointer-left);
|
||||
transform-origin: center bottom 0;
|
||||
width: var(--fs-timepicker-hand-pointer-width);
|
||||
}
|
||||
|
||||
.timepicker-circle {
|
||||
top: var(--fs-timepicker-circle-top);
|
||||
left: var(--fs-timepicker-circle-left);
|
||||
width: var(--fs-timepicker-circle-width);
|
||||
border: var(--fs-timepicker-circle-border-width) solid var(--fs-timepicker-circle-border-color);
|
||||
height: var(--fs-timepicker-circle-height);
|
||||
box-sizing: content-box;
|
||||
border-radius: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.timepicker-circle.active {
|
||||
background-color: var(--fs-timepicker-circle-active-background-color);
|
||||
}
|
||||
|
||||
.timepicker-time-tips-minutes,
|
||||
.timepicker-time-tips-hours {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 300;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.timepicker-time-tips-minutes.active,
|
||||
.timepicker-time-tips-hours.active {
|
||||
color: var(--fs-timepicker-time-tips-inner-active-color);
|
||||
background-color: var(--fs-timepicker-time-tips-inner-active-bg);
|
||||
font-weight: var(--fs-timepicker-time-tips-inner-active-font-weight);
|
||||
}
|
||||
|
||||
.timepicker-footer {
|
||||
border-bottom-left-radius: var(--fs-timepicker-footer-border-bottom-left-radius);
|
||||
border-bottom-right-radius: var(--fs-timepicker-footer-border-bottom-right-radius);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--fs-timepicker-footer-height);
|
||||
padding-left: var(--fs-timepicker-footer-padding-x);
|
||||
padding-right: var(--fs-timepicker-footer-padding-x);
|
||||
background-color: var(--fs-timepicker-footer-bg);
|
||||
}
|
||||
|
||||
.timepicker-button {
|
||||
font-size: var(--fs-timepicker-button-font-size);
|
||||
min-width: var(--fs-timepicker-button-min-width);
|
||||
box-sizing: border-box;
|
||||
font-weight: var(--fs-timepicker-button-font-weight);
|
||||
line-height: var(--fs-timepicker-button-line-height);
|
||||
border-radius: var(--fs-timepicker-button-border-radius);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fs-timepicker-button-color);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
outline: none;
|
||||
padding: 0 var(--fs-timepicker-button-padding-x);
|
||||
height: var(--fs-timepicker-button-height);
|
||||
margin-bottom: var(--fs-timepicker-button-margin-bottom);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timepicker-button:hover {
|
||||
background-color: var(--fs-timepicker-button-hover-bg);
|
||||
}
|
||||
|
||||
.timepicker-button:focus {
|
||||
outline: none;
|
||||
background-color: var(--fs-timepicker-button-focus-bg);
|
||||
}
|
||||
|
||||
/* Add missing button styling */
|
||||
button {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/** Remove focus from buttons after clicked **/
|
||||
/* Prevent selection/focus styling on all buttons in the timepicker */
|
||||
.timepicker-modal button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Override focus styles completely */
|
||||
.timepicker-modal button:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Prevent text selection on all elements */
|
||||
.timepicker-modal * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* Add this to the body when the timepicker is active */
|
||||
body.timepicker-active {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Add !important to ensure these styles take precedence */
|
||||
.timepicker-current:focus,
|
||||
.timepicker-dot:focus,
|
||||
.timepicker-hour-mode:focus,
|
||||
.timepicker-button:focus {
|
||||
outline: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Only apply background color on hover, not on focus */
|
||||
.timepicker-hour-mode:hover,
|
||||
.timepicker-hour:hover,
|
||||
.timepicker-minute:hover,
|
||||
.timepicker-button:hover {
|
||||
background-color: var(--fs-timepicker-hour-mode-hover-bg);
|
||||
}
|
||||
|
||||
/* Ensure active states are properly styled */
|
||||
.timepicker-hour-mode.active,
|
||||
.timepicker-hour.active,
|
||||
.timepicker-minute.active {
|
||||
color: #fff !important;
|
||||
opacity: 1 !important;
|
||||
}
|
84
timepicker-modal/timepicker-modal.html
Normal file
84
timepicker-modal/timepicker-modal.html
Normal file
@ -0,0 +1,84 @@
|
||||
<link rel="stylesheet" href="timepicker.css">
|
||||
|
||||
<div class="timepicker-modal" role="dialog" tabindex="-1" id="">
|
||||
<div id="" 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="" 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="" 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>
|
||||
<span class="timepicker-time-tips-hours active" style="left: 114px; bottom: 224px;">
|
||||
<span class="timepicker-tips-element">12</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 169px; bottom: 209.263px;">
|
||||
<span class="timepicker-tips-element">1</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 209.263px; bottom: 169px;">
|
||||
<span class="timepicker-tips-element">2</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 224px; bottom: 114px;">
|
||||
<span class="timepicker-tips-element">3</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 209.263px; bottom: 59px;">
|
||||
<span class="timepicker-tips-element">4</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 169px; bottom: 18.7372px;">
|
||||
<span class="timepicker-tips-element">5</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 114px; bottom: 4px;">
|
||||
<span class="timepicker-tips-element">6</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 59px; bottom: 18.7372px;">
|
||||
<span class="timepicker-tips-element">7</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 18.7372px; bottom: 59px;">
|
||||
<span class="timepicker-tips-element">8</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 4px; bottom: 114px;">
|
||||
<span class="timepicker-tips-element">9</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 18.7372px; bottom: 169px;">
|
||||
<span class="timepicker-tips-element">10</span>
|
||||
</span>
|
||||
<span class="timepicker-time-tips-hours" style="left: 59px; bottom: 209.263px;">
|
||||
<span class="timepicker-tips-element">11</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="" 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>
|
||||
|
||||
<script src="timepicker.js"></script>
|
311
timepicker-modal/timepicker-modal.js
Normal file
311
timepicker-modal/timepicker-modal.js
Normal file
@ -0,0 +1,311 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// DOM elements
|
||||
const hourButton = document.querySelector('.timepicker-hour');
|
||||
const minuteButton = document.querySelector('.timepicker-minute');
|
||||
const clockWrapper = document.querySelector('.timepicker-clock-wrapper');
|
||||
const clock = document.querySelector('.timepicker-clock');
|
||||
const handPointer = document.querySelector('.timepicker-hand-pointer');
|
||||
const amButton = document.querySelector('.timepicker-am');
|
||||
const pmButton = document.querySelector('.timepicker-pm');
|
||||
const clearButton = document.querySelector('.timepicker-clear');
|
||||
const cancelButton = document.querySelector('.timepicker-cancel');
|
||||
const submitButton = document.querySelector('.timepicker-submit');
|
||||
|
||||
// State variables
|
||||
let currentView = 'hours'; // 'hours' or 'minutes'
|
||||
let selectedHour = 12;
|
||||
let selectedMinute = 0;
|
||||
let isDragging = false;
|
||||
let isPM = pmButton.classList.contains('active');
|
||||
|
||||
// Initialize
|
||||
setupEventListeners();
|
||||
renderClockFace(currentView);
|
||||
|
||||
function setupEventListeners() {
|
||||
// Header hour/minute buttons
|
||||
hourButton.addEventListener('click', () => switchView('hours'));
|
||||
minuteButton.addEventListener('click', () => switchView('minutes'));
|
||||
|
||||
// Clock face events
|
||||
clock.addEventListener('mousedown', handleClockMouseDown);
|
||||
document.addEventListener('mousemove', handleClockMouseMove);
|
||||
document.addEventListener('mouseup', handleClockMouseUp);
|
||||
|
||||
// Prevent text selection on the clock
|
||||
clock.style.userSelect = 'none';
|
||||
clock.style.webkitUserSelect = 'none';
|
||||
clock.style.msUserSelect = 'none';
|
||||
|
||||
// AM/PM buttons
|
||||
amButton.addEventListener('click', () => setAmPm('AM'));
|
||||
pmButton.addEventListener('click', () => setAmPm('PM'));
|
||||
|
||||
// Action buttons
|
||||
clearButton.addEventListener('click', clearTime);
|
||||
cancelButton.addEventListener('click', closeTimepicker);
|
||||
submitButton.addEventListener('click', submitTime);
|
||||
}
|
||||
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
|
||||
// Update active state in header
|
||||
if (view === 'hours') {
|
||||
hourButton.classList.add('active');
|
||||
minuteButton.classList.remove('active');
|
||||
hourButton.style.pointerEvents = 'none';
|
||||
minuteButton.style.pointerEvents = 'auto';
|
||||
} else {
|
||||
hourButton.classList.remove('active');
|
||||
minuteButton.classList.add('active');
|
||||
hourButton.style.pointerEvents = 'auto';
|
||||
minuteButton.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
renderClockFace(view);
|
||||
}
|
||||
|
||||
function renderClockFace(view) {
|
||||
// Clear existing time tips
|
||||
const existingTips = clock.querySelectorAll('.timepicker-time-tips-hours, .timepicker-time-tips-minutes');
|
||||
existingTips.forEach(tip => tip.remove());
|
||||
|
||||
if (view === 'hours') {
|
||||
renderHoursFace();
|
||||
updateHandPosition(selectedHour, 'hours');
|
||||
} else {
|
||||
renderMinutesFace();
|
||||
updateHandPosition(selectedMinute, 'minutes');
|
||||
}
|
||||
}
|
||||
|
||||
function renderHoursFace() {
|
||||
const clockRadius = clock.offsetWidth / 2;
|
||||
const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius)
|
||||
|
||||
for (let hour = 1; hour <= 12; hour++) {
|
||||
// Adjust angle calculation to position numbers correctly
|
||||
// Start at 12 o'clock position and go clockwise
|
||||
const angle = ((hour * 30) - 90) * (Math.PI / 180);
|
||||
|
||||
// Center the numbers on the clock face
|
||||
const left = clockRadius + tipRadius * Math.cos(angle);
|
||||
const top = clockRadius + tipRadius * Math.sin(angle);
|
||||
|
||||
const tip = document.createElement('span');
|
||||
tip.className = 'timepicker-time-tips-hours';
|
||||
// Adjust positioning to center the numbers
|
||||
tip.style.left = `${left}px`;
|
||||
tip.style.top = `${top}px`;
|
||||
tip.style.position = 'absolute';
|
||||
tip.style.transform = 'translate(-50%, -50%)'; // Center the element at its position
|
||||
|
||||
// Prevent text selection
|
||||
tip.style.userSelect = 'none';
|
||||
tip.style.webkitUserSelect = 'none';
|
||||
tip.style.msUserSelect = 'none';
|
||||
|
||||
const tipElement = document.createElement('span');
|
||||
tipElement.className = 'timepicker-tips-element';
|
||||
tipElement.textContent = hour;
|
||||
|
||||
tip.appendChild(tipElement);
|
||||
|
||||
if (hour === selectedHour) {
|
||||
tip.classList.add('active');
|
||||
}
|
||||
|
||||
clock.appendChild(tip);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMinutesFace() {
|
||||
const clockRadius = clock.offsetWidth / 2;
|
||||
const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius)
|
||||
|
||||
for (let minute = 0; minute < 60; minute += 5) {
|
||||
// Adjust angle calculation to position numbers correctly
|
||||
// Start at 12 o'clock position and go clockwise
|
||||
const angle = ((minute * 6) - 90) * (Math.PI / 180);
|
||||
|
||||
// Center the numbers on the clock face
|
||||
const left = clockRadius + tipRadius * Math.cos(angle);
|
||||
const top = clockRadius + tipRadius * Math.sin(angle);
|
||||
|
||||
const tip = document.createElement('span');
|
||||
tip.className = 'timepicker-time-tips-minutes';
|
||||
// Adjust positioning to center the numbers
|
||||
tip.style.left = `${left}px`;
|
||||
tip.style.top = `${top}px`;
|
||||
tip.style.position = 'absolute';
|
||||
tip.style.transform = 'translate(-50%, -50%)'; // Center the element at its position
|
||||
|
||||
// Prevent text selection
|
||||
tip.style.userSelect = 'none';
|
||||
tip.style.webkitUserSelect = 'none';
|
||||
tip.style.msUserSelect = 'none';
|
||||
|
||||
const tipElement = document.createElement('span');
|
||||
tipElement.className = 'timepicker-tips-element';
|
||||
tipElement.textContent = minute.toString().padStart(2, '0');
|
||||
|
||||
tip.appendChild(tipElement);
|
||||
|
||||
if (minute === selectedMinute) {
|
||||
tip.classList.add('active');
|
||||
}
|
||||
|
||||
clock.appendChild(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;
|
||||
}
|
||||
|
||||
handPointer.style.transform = `rotateZ(${angle}deg)`;
|
||||
|
||||
// Update the active circle on the hand
|
||||
const circle = handPointer.querySelector('.timepicker-circle');
|
||||
if (circle) {
|
||||
circle.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function handleClockMouseDown(event) {
|
||||
isDragging = true;
|
||||
updateTimeFromClockPosition(event);
|
||||
}
|
||||
|
||||
function handleClockMouseMove(event) {
|
||||
if (isDragging) {
|
||||
updateTimeFromClockPosition(event);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClockMouseUp() {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
|
||||
// If we just finished selecting an hour, switch to minutes
|
||||
if (currentView === 'hours') {
|
||||
switchView('minutes');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimeFromClockPosition(event) {
|
||||
const clockRect = 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
|
||||
// atan2 returns angle in radians from -π to π, with 0 at 3 o'clock position
|
||||
// We add 90 degrees to start from 12 o'clock instead of 3 o'clock
|
||||
let angle = Math.atan2(y, x) * (180 / Math.PI) + 90;
|
||||
|
||||
// Normalize angle to 0-360
|
||||
if (angle < 0) {
|
||||
angle += 360;
|
||||
}
|
||||
|
||||
if (currentView === 'hours') {
|
||||
// Convert angle to hour (1-12)
|
||||
// Each hour is 30 degrees (360/12)
|
||||
let hour = Math.round(angle / 30);
|
||||
|
||||
// Handle edge cases for full circle
|
||||
if (hour === 0 || hour > 12) hour = 12;
|
||||
|
||||
selectedHour = hour;
|
||||
hourButton.textContent = hour;
|
||||
updateHandPosition(hour, 'hours');
|
||||
|
||||
// Update active class on hour tips
|
||||
const hourTips = clock.querySelectorAll('.timepicker-time-tips-hours');
|
||||
hourTips.forEach(tip => {
|
||||
const hourValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent);
|
||||
if (hourValue === selectedHour) {
|
||||
tip.classList.add('active');
|
||||
} else {
|
||||
tip.classList.remove('active');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Convert angle to minute (0-55, step 5)
|
||||
// Each 5-minute increment is 30 degrees (360/12)
|
||||
// First, get the minute value from 0-59
|
||||
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;
|
||||
|
||||
selectedMinute = minute;
|
||||
minuteButton.textContent = minute.toString().padStart(2, '0');
|
||||
updateHandPosition(minute, 'minutes');
|
||||
|
||||
// Update active class on minute tips
|
||||
const minuteTips = clock.querySelectorAll('.timepicker-time-tips-minutes');
|
||||
minuteTips.forEach(tip => {
|
||||
const minuteValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent);
|
||||
if (minuteValue === selectedMinute) {
|
||||
tip.classList.add('active');
|
||||
} else {
|
||||
tip.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setAmPm(period) {
|
||||
if (period === 'AM') {
|
||||
amButton.classList.add('active');
|
||||
pmButton.classList.remove('active');
|
||||
isPM = false;
|
||||
} else {
|
||||
amButton.classList.remove('active');
|
||||
pmButton.classList.add('active');
|
||||
isPM = true;
|
||||
}
|
||||
}
|
||||
|
||||
function clearTime() {
|
||||
selectedHour = 12;
|
||||
selectedMinute = 0;
|
||||
hourButton.textContent = '12';
|
||||
minuteButton.textContent = '00';
|
||||
setAmPm('PM');
|
||||
switchView('hours');
|
||||
}
|
||||
|
||||
function closeTimepicker() {
|
||||
// You can implement closing the modal here
|
||||
console.log('Timepicker closed');
|
||||
}
|
||||
|
||||
function submitTime() {
|
||||
const formattedHour = selectedHour;
|
||||
const formattedMinute = selectedMinute.toString().padStart(2, '0');
|
||||
const period = isPM ? 'PM' : 'AM';
|
||||
|
||||
console.log(`Selected time: ${formattedHour}:${formattedMinute} ${period}`);
|
||||
// Here you can pass the selected time to your application
|
||||
|
||||
closeTimepicker();
|
||||
}
|
||||
});
|
@ -7,6 +7,7 @@
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
<link rel="stylesheet" href="/timepicker.css">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||
</head>
|
||||
|
Loading…
Reference in New Issue
Block a user