1213 lines
43 KiB
HTML
1213 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Date Picker Enhanced</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--primary: #2563eb;
|
||
--primary-hover: #1d4ed8;
|
||
--primary-light: #dbeafe;
|
||
--secondary: #64748b;
|
||
--text: #1e293b;
|
||
--text-light: #64748b;
|
||
--bg: #ffffff;
|
||
--bg-hover: #f8fafc;
|
||
--border: #e2e8f0;
|
||
--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||
--radius: 12px;
|
||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
color: var(--text);
|
||
}
|
||
|
||
main {
|
||
width: 100%;
|
||
max-width: 1200px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2.5rem;
|
||
font-weight: 800;
|
||
text-align: center;
|
||
margin-bottom: 3rem;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
.component-showcase {
|
||
background: var(--bg);
|
||
border-radius: 16px;
|
||
padding: 3rem;
|
||
box-shadow: var(--shadow);
|
||
position: relative;
|
||
}
|
||
|
||
.demo-container {
|
||
display: grid;
|
||
gap: 2rem;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
}
|
||
|
||
.demo-section {
|
||
background: var(--bg-hover);
|
||
border-radius: var(--radius);
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.demo-section h3 {
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
margin-bottom: 1rem;
|
||
color: var(--text);
|
||
}
|
||
|
||
/* Date Input Styling */
|
||
.date-input-wrapper {
|
||
position: relative;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.date-input {
|
||
width: 100%;
|
||
padding: 0.75rem 2.5rem 0.75rem 1rem;
|
||
border: 2px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
transition: var(--transition);
|
||
background: var(--bg);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.date-input:hover {
|
||
border-color: var(--primary-light);
|
||
}
|
||
|
||
.date-input:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.calendar-icon {
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 20px;
|
||
height: 20px;
|
||
pointer-events: none;
|
||
color: var(--text-light);
|
||
}
|
||
|
||
/* Date Picker Container */
|
||
.date-picker {
|
||
position: absolute;
|
||
top: calc(100% + 8px);
|
||
left: 0;
|
||
z-index: 1000;
|
||
background: var(--bg);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
padding: 1rem;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transform: translateY(-10px);
|
||
transition: var(--transition);
|
||
min-width: 320px;
|
||
}
|
||
|
||
.date-picker.active {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* Date Picker Header */
|
||
.picker-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 1rem;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.month-year-select {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.month-select, .year-select {
|
||
padding: 0.5rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
background: var(--bg);
|
||
font-size: 0.875rem;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.month-select:hover, .year-select:hover {
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.nav-buttons {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.nav-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: none;
|
||
background: var(--bg-hover);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: var(--transition);
|
||
color: var(--text);
|
||
}
|
||
|
||
.nav-btn:hover {
|
||
background: var(--primary-light);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.nav-btn:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
/* Calendar Grid */
|
||
.calendar-wrapper {
|
||
position: relative;
|
||
overflow: hidden;
|
||
height: 280px;
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 2px;
|
||
position: absolute;
|
||
width: 100%;
|
||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.calendar-grid.prev {
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.calendar-grid.next {
|
||
transform: translateX(100%);
|
||
}
|
||
|
||
.calendar-grid.animating-prev {
|
||
animation: slideInFromLeft 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.calendar-grid.animating-next {
|
||
animation: slideInFromRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
@keyframes slideInFromLeft {
|
||
from { transform: translateX(-100%); }
|
||
to { transform: translateX(0); }
|
||
}
|
||
|
||
@keyframes slideInFromRight {
|
||
from { transform: translateX(100%); }
|
||
to { transform: translateX(0); }
|
||
}
|
||
|
||
.day-header {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
padding: 0.5rem;
|
||
color: var(--text-light);
|
||
}
|
||
|
||
.day-cell {
|
||
aspect-ratio: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 0.875rem;
|
||
position: relative;
|
||
transition: var(--transition);
|
||
background: var(--bg);
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.day-cell:hover:not(.disabled):not(.other-month) {
|
||
background: var(--bg-hover);
|
||
border-color: var(--border);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.day-cell.today {
|
||
background: var(--primary-light);
|
||
color: var(--primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.day-cell.selected {
|
||
background: var(--primary);
|
||
color: white;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.day-cell.in-range {
|
||
background: var(--primary-light);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.day-cell.range-start {
|
||
border-top-right-radius: 0;
|
||
border-bottom-right-radius: 0;
|
||
}
|
||
|
||
.day-cell.range-end {
|
||
border-top-left-radius: 0;
|
||
border-bottom-left-radius: 0;
|
||
}
|
||
|
||
.day-cell.disabled {
|
||
color: var(--text-light);
|
||
opacity: 0.3;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.day-cell.other-month {
|
||
color: var(--text-light);
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.week-number {
|
||
font-size: 0.625rem;
|
||
color: var(--text-light);
|
||
position: absolute;
|
||
top: 2px;
|
||
right: 4px;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
/* Quick Presets */
|
||
.presets {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.preset-btn {
|
||
padding: 0.375rem 0.75rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
background: var(--bg);
|
||
font-size: 0.75rem;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.preset-btn:hover {
|
||
background: var(--primary-light);
|
||
border-color: var(--primary);
|
||
color: var(--primary);
|
||
}
|
||
|
||
/* Time Picker */
|
||
.time-picker {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 1rem;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.time-input {
|
||
width: 60px;
|
||
padding: 0.5rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.time-separator {
|
||
font-weight: 600;
|
||
color: var(--text-light);
|
||
}
|
||
|
||
/* Keyboard shortcuts hint */
|
||
.keyboard-hint {
|
||
position: absolute;
|
||
bottom: -40px;
|
||
left: 0;
|
||
right: 0;
|
||
text-align: center;
|
||
font-size: 0.75rem;
|
||
color: var(--text-light);
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
.date-picker:focus-within .keyboard-hint {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Multi-select styles */
|
||
.multi-select-badge {
|
||
position: absolute;
|
||
top: -6px;
|
||
right: -6px;
|
||
background: var(--primary);
|
||
color: white;
|
||
font-size: 0.625rem;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Mobile optimizations */
|
||
@media (max-width: 640px) {
|
||
.date-picker {
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: calc(100vw - 40px);
|
||
max-width: 360px;
|
||
}
|
||
|
||
.date-picker.active {
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 999;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.backdrop.active {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
}
|
||
|
||
/* Focus indicators */
|
||
.day-cell:focus {
|
||
outline: 2px solid var(--primary);
|
||
outline-offset: -1px;
|
||
}
|
||
|
||
/* Loading state */
|
||
.loading {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
.spinner {
|
||
width: 24px;
|
||
height: 24px;
|
||
border: 3px solid var(--border);
|
||
border-top-color: var(--primary);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>Date Picker - Enhanced</h1>
|
||
|
||
<div class="component-showcase">
|
||
<div class="demo-container">
|
||
<!-- Single Date Picker -->
|
||
<div class="demo-section">
|
||
<h3>Single Date Selection</h3>
|
||
<div class="date-input-wrapper">
|
||
<input type="text" class="date-input" id="singleDate" placeholder="Select a date" readonly>
|
||
<svg class="calendar-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||
</svg>
|
||
</div>
|
||
<p style="font-size: 0.875rem; color: var(--text-light); margin-top: 0.5rem;">
|
||
Use arrow keys to navigate, Enter to select
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Date Range Picker -->
|
||
<div class="demo-section">
|
||
<h3>Date Range Selection</h3>
|
||
<div class="date-input-wrapper">
|
||
<input type="text" class="date-input" id="dateRange" placeholder="Select date range" readonly>
|
||
<svg class="calendar-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||
</svg>
|
||
</div>
|
||
<p style="font-size: 0.875rem; color: var(--text-light); margin-top: 0.5rem;">
|
||
Click start date, then end date
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Date Time Picker -->
|
||
<div class="demo-section">
|
||
<h3>Date & Time Selection</h3>
|
||
<div class="date-input-wrapper">
|
||
<input type="text" class="date-input" id="dateTime" placeholder="Select date and time" readonly>
|
||
<svg class="calendar-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
</div>
|
||
<p style="font-size: 0.875rem; color: var(--text-light); margin-top: 0.5rem;">
|
||
Includes time picker integration
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Multiple Dates Picker -->
|
||
<div class="demo-section">
|
||
<h3>Multiple Date Selection</h3>
|
||
<div class="date-input-wrapper">
|
||
<input type="text" class="date-input" id="multiDate" placeholder="Select multiple dates" readonly>
|
||
<svg class="calendar-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||
</svg>
|
||
</div>
|
||
<p style="font-size: 0.875rem; color: var(--text-light); margin-top: 0.5rem;">
|
||
Ctrl/Cmd + click to select multiple
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="backdrop" id="backdrop"></div>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
class EnhancedDatePicker {
|
||
constructor(input, options = {}) {
|
||
this.input = input;
|
||
this.options = {
|
||
mode: 'single', // single, range, multi
|
||
showTime: false,
|
||
showWeekNumbers: false,
|
||
minDate: null,
|
||
maxDate: null,
|
||
disabledDates: [],
|
||
locale: 'en-US',
|
||
firstDayOfWeek: 0, // 0 = Sunday
|
||
...options
|
||
};
|
||
|
||
this.selectedDates = [];
|
||
this.rangeStart = null;
|
||
this.rangeEnd = null;
|
||
this.currentMonth = new Date().getMonth();
|
||
this.currentYear = new Date().getFullYear();
|
||
this.isAnimating = false;
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.createPicker();
|
||
this.attachEventListeners();
|
||
this.updateCalendar();
|
||
}
|
||
|
||
createPicker() {
|
||
// Create picker container
|
||
this.picker = document.createElement('div');
|
||
this.picker.className = 'date-picker';
|
||
this.picker.setAttribute('tabindex', '-1');
|
||
|
||
// Create header
|
||
const header = document.createElement('div');
|
||
header.className = 'picker-header';
|
||
|
||
// Month/Year selects
|
||
const monthYearSelect = document.createElement('div');
|
||
monthYearSelect.className = 'month-year-select';
|
||
|
||
this.monthSelect = document.createElement('select');
|
||
this.monthSelect.className = 'month-select';
|
||
const months = [];
|
||
for (let i = 0; i < 12; i++) {
|
||
const date = new Date(2000, i, 1);
|
||
months.push(date.toLocaleDateString(this.options.locale, { month: 'long' }));
|
||
}
|
||
months.forEach((month, index) => {
|
||
const option = document.createElement('option');
|
||
option.value = index;
|
||
option.textContent = month;
|
||
this.monthSelect.appendChild(option);
|
||
});
|
||
|
||
this.yearSelect = document.createElement('select');
|
||
this.yearSelect.className = 'year-select';
|
||
const currentYear = new Date().getFullYear();
|
||
for (let year = currentYear - 100; year <= currentYear + 100; year++) {
|
||
const option = document.createElement('option');
|
||
option.value = year;
|
||
option.textContent = year;
|
||
this.yearSelect.appendChild(option);
|
||
}
|
||
|
||
monthYearSelect.appendChild(this.monthSelect);
|
||
monthYearSelect.appendChild(this.yearSelect);
|
||
|
||
// Navigation buttons
|
||
const navButtons = document.createElement('div');
|
||
navButtons.className = 'nav-buttons';
|
||
|
||
this.prevBtn = document.createElement('button');
|
||
this.prevBtn.className = 'nav-btn';
|
||
this.prevBtn.innerHTML = '‹';
|
||
this.prevBtn.setAttribute('aria-label', 'Previous month');
|
||
|
||
this.nextBtn = document.createElement('button');
|
||
this.nextBtn.className = 'nav-btn';
|
||
this.nextBtn.innerHTML = '›';
|
||
this.nextBtn.setAttribute('aria-label', 'Next month');
|
||
|
||
navButtons.appendChild(this.prevBtn);
|
||
navButtons.appendChild(this.nextBtn);
|
||
|
||
header.appendChild(monthYearSelect);
|
||
header.appendChild(navButtons);
|
||
|
||
// Calendar wrapper
|
||
this.calendarWrapper = document.createElement('div');
|
||
this.calendarWrapper.className = 'calendar-wrapper';
|
||
|
||
// Presets
|
||
if (this.options.mode !== 'multi') {
|
||
this.presetsContainer = document.createElement('div');
|
||
this.presetsContainer.className = 'presets';
|
||
|
||
const presets = [
|
||
{ label: 'Today', value: () => [new Date()] },
|
||
{ label: 'Yesterday', value: () => [new Date(Date.now() - 86400000)] },
|
||
{ label: 'Last Week', value: () => {
|
||
const end = new Date();
|
||
const start = new Date(Date.now() - 7 * 86400000);
|
||
return [start, end];
|
||
}},
|
||
{ label: 'Last Month', value: () => {
|
||
const end = new Date();
|
||
const start = new Date(Date.now() - 30 * 86400000);
|
||
return [start, end];
|
||
}}
|
||
];
|
||
|
||
presets.forEach(preset => {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'preset-btn';
|
||
btn.textContent = preset.label;
|
||
btn.addEventListener('click', () => {
|
||
const dates = preset.value();
|
||
if (this.options.mode === 'range' && dates.length === 2) {
|
||
this.rangeStart = dates[0];
|
||
this.rangeEnd = dates[1];
|
||
this.updateInput();
|
||
this.close();
|
||
} else if (dates.length === 1) {
|
||
this.selectedDates = dates;
|
||
this.updateInput();
|
||
this.close();
|
||
}
|
||
});
|
||
this.presetsContainer.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
// Time picker
|
||
if (this.options.showTime) {
|
||
this.timePicker = document.createElement('div');
|
||
this.timePicker.className = 'time-picker';
|
||
|
||
this.hourInput = document.createElement('input');
|
||
this.hourInput.className = 'time-input';
|
||
this.hourInput.type = 'number';
|
||
this.hourInput.min = '0';
|
||
this.hourInput.max = '23';
|
||
this.hourInput.value = '12';
|
||
|
||
const separator = document.createElement('span');
|
||
separator.className = 'time-separator';
|
||
separator.textContent = ':';
|
||
|
||
this.minuteInput = document.createElement('input');
|
||
this.minuteInput.className = 'time-input';
|
||
this.minuteInput.type = 'number';
|
||
this.minuteInput.min = '0';
|
||
this.minuteInput.max = '59';
|
||
this.minuteInput.value = '00';
|
||
|
||
this.timePicker.appendChild(this.hourInput);
|
||
this.timePicker.appendChild(separator);
|
||
this.timePicker.appendChild(this.minuteInput);
|
||
}
|
||
|
||
// Keyboard hint
|
||
const keyboardHint = document.createElement('div');
|
||
keyboardHint.className = 'keyboard-hint';
|
||
keyboardHint.textContent = '↑↓←→ Navigate • Enter Select • PgUp/PgDn Month • Esc Close';
|
||
|
||
// Assemble picker
|
||
this.picker.appendChild(header);
|
||
this.picker.appendChild(this.calendarWrapper);
|
||
if (this.presetsContainer) {
|
||
this.picker.appendChild(this.presetsContainer);
|
||
}
|
||
if (this.timePicker) {
|
||
this.picker.appendChild(this.timePicker);
|
||
}
|
||
this.picker.appendChild(keyboardHint);
|
||
|
||
// Add to wrapper
|
||
this.input.parentElement.appendChild(this.picker);
|
||
}
|
||
|
||
updateCalendar() {
|
||
if (this.isAnimating) return;
|
||
|
||
this.monthSelect.value = this.currentMonth;
|
||
this.yearSelect.value = this.currentYear;
|
||
|
||
const newGrid = this.createCalendarGrid();
|
||
|
||
if (this.currentGrid) {
|
||
this.isAnimating = true;
|
||
this.calendarWrapper.appendChild(newGrid);
|
||
|
||
setTimeout(() => {
|
||
this.currentGrid.remove();
|
||
this.currentGrid = newGrid;
|
||
this.isAnimating = false;
|
||
}, 300);
|
||
} else {
|
||
this.calendarWrapper.appendChild(newGrid);
|
||
this.currentGrid = newGrid;
|
||
}
|
||
}
|
||
|
||
createCalendarGrid() {
|
||
const grid = document.createElement('div');
|
||
grid.className = 'calendar-grid';
|
||
|
||
// Day headers
|
||
const dayNames = [];
|
||
for (let i = 0; i < 7; i++) {
|
||
const date = new Date(2000, 0, 2 + i); // January 2, 2000 was a Sunday
|
||
dayNames.push(date.toLocaleDateString(this.options.locale, { weekday: 'short' }));
|
||
}
|
||
|
||
// Reorder based on firstDayOfWeek
|
||
const orderedDayNames = [
|
||
...dayNames.slice(this.options.firstDayOfWeek),
|
||
...dayNames.slice(0, this.options.firstDayOfWeek)
|
||
];
|
||
|
||
orderedDayNames.forEach(day => {
|
||
const header = document.createElement('div');
|
||
header.className = 'day-header';
|
||
header.textContent = day.substring(0, 2);
|
||
grid.appendChild(header);
|
||
});
|
||
|
||
// Calculate days
|
||
const firstDay = new Date(this.currentYear, this.currentMonth, 1);
|
||
const lastDay = new Date(this.currentYear, this.currentMonth + 1, 0);
|
||
const prevLastDay = new Date(this.currentYear, this.currentMonth, 0);
|
||
|
||
let startDay = firstDay.getDay() - this.options.firstDayOfWeek;
|
||
if (startDay < 0) startDay += 7;
|
||
|
||
// Previous month days
|
||
for (let i = startDay - 1; i >= 0; i--) {
|
||
const date = new Date(this.currentYear, this.currentMonth - 1, prevLastDay.getDate() - i);
|
||
grid.appendChild(this.createDayCell(date, true));
|
||
}
|
||
|
||
// Current month days
|
||
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||
const date = new Date(this.currentYear, this.currentMonth, day);
|
||
grid.appendChild(this.createDayCell(date, false));
|
||
}
|
||
|
||
// Next month days
|
||
const remainingCells = 42 - grid.children.length + 7; // +7 for headers
|
||
for (let day = 1; day <= remainingCells - 7; day++) {
|
||
const date = new Date(this.currentYear, this.currentMonth + 1, day);
|
||
grid.appendChild(this.createDayCell(date, true));
|
||
}
|
||
|
||
return grid;
|
||
}
|
||
|
||
createDayCell(date, isOtherMonth) {
|
||
const cell = document.createElement('button');
|
||
cell.className = 'day-cell';
|
||
cell.setAttribute('role', 'gridcell');
|
||
cell.setAttribute('aria-label', date.toLocaleDateString(this.options.locale));
|
||
|
||
if (isOtherMonth) {
|
||
cell.classList.add('other-month');
|
||
}
|
||
|
||
// Check if today
|
||
const today = new Date();
|
||
if (this.isSameDay(date, today)) {
|
||
cell.classList.add('today');
|
||
}
|
||
|
||
// Check if selected
|
||
if (this.options.mode === 'single' || this.options.mode === 'multi') {
|
||
this.selectedDates.forEach(selected => {
|
||
if (this.isSameDay(date, selected)) {
|
||
cell.classList.add('selected');
|
||
}
|
||
});
|
||
} else if (this.options.mode === 'range') {
|
||
if (this.rangeStart && this.isSameDay(date, this.rangeStart)) {
|
||
cell.classList.add('selected', 'range-start');
|
||
}
|
||
if (this.rangeEnd && this.isSameDay(date, this.rangeEnd)) {
|
||
cell.classList.add('selected', 'range-end');
|
||
}
|
||
if (this.rangeStart && this.rangeEnd &&
|
||
date > this.rangeStart && date < this.rangeEnd) {
|
||
cell.classList.add('in-range');
|
||
}
|
||
}
|
||
|
||
// Check if disabled
|
||
if (this.isDateDisabled(date)) {
|
||
cell.classList.add('disabled');
|
||
cell.disabled = true;
|
||
}
|
||
|
||
// Day number
|
||
const dayNumber = document.createElement('span');
|
||
dayNumber.textContent = date.getDate();
|
||
cell.appendChild(dayNumber);
|
||
|
||
// Week number
|
||
if (this.options.showWeekNumbers && date.getDay() === this.options.firstDayOfWeek) {
|
||
const weekNumber = document.createElement('span');
|
||
weekNumber.className = 'week-number';
|
||
weekNumber.textContent = this.getWeekNumber(date);
|
||
cell.appendChild(weekNumber);
|
||
}
|
||
|
||
// Multi-select badge
|
||
if (this.options.mode === 'multi') {
|
||
const count = this.selectedDates.filter(d => this.isSameDay(d, date)).length;
|
||
if (count > 1) {
|
||
const badge = document.createElement('span');
|
||
badge.className = 'multi-select-badge';
|
||
badge.textContent = count;
|
||
cell.appendChild(badge);
|
||
}
|
||
}
|
||
|
||
// Click handler
|
||
cell.addEventListener('click', () => this.selectDate(date));
|
||
|
||
return cell;
|
||
}
|
||
|
||
selectDate(date) {
|
||
if (this.isDateDisabled(date)) return;
|
||
|
||
if (this.options.mode === 'single') {
|
||
this.selectedDates = [date];
|
||
this.updateInput();
|
||
this.close();
|
||
} else if (this.options.mode === 'range') {
|
||
if (!this.rangeStart || (this.rangeStart && this.rangeEnd)) {
|
||
this.rangeStart = date;
|
||
this.rangeEnd = null;
|
||
} else {
|
||
if (date < this.rangeStart) {
|
||
this.rangeEnd = this.rangeStart;
|
||
this.rangeStart = date;
|
||
} else {
|
||
this.rangeEnd = date;
|
||
}
|
||
this.updateInput();
|
||
this.close();
|
||
}
|
||
} else if (this.options.mode === 'multi') {
|
||
const index = this.selectedDates.findIndex(d => this.isSameDay(d, date));
|
||
if (index > -1) {
|
||
this.selectedDates.splice(index, 1);
|
||
} else {
|
||
this.selectedDates.push(date);
|
||
}
|
||
this.updateInput();
|
||
}
|
||
|
||
this.updateCalendar();
|
||
}
|
||
|
||
attachEventListeners() {
|
||
// Input click
|
||
this.input.addEventListener('click', () => this.toggle());
|
||
|
||
// Month/Year change
|
||
this.monthSelect.addEventListener('change', (e) => {
|
||
this.currentMonth = parseInt(e.target.value);
|
||
this.updateCalendar();
|
||
});
|
||
|
||
this.yearSelect.addEventListener('change', (e) => {
|
||
this.currentYear = parseInt(e.target.value);
|
||
this.updateCalendar();
|
||
});
|
||
|
||
// Navigation buttons
|
||
this.prevBtn.addEventListener('click', () => this.navigate(-1));
|
||
this.nextBtn.addEventListener('click', () => this.navigate(1));
|
||
|
||
// Keyboard navigation
|
||
this.picker.addEventListener('keydown', (e) => this.handleKeyboard(e));
|
||
|
||
// Click outside
|
||
document.addEventListener('click', (e) => {
|
||
if (!this.picker.contains(e.target) && !this.input.contains(e.target)) {
|
||
this.close();
|
||
}
|
||
});
|
||
|
||
// Mobile backdrop
|
||
const backdrop = document.getElementById('backdrop');
|
||
if (backdrop) {
|
||
backdrop.addEventListener('click', () => this.close());
|
||
}
|
||
}
|
||
|
||
handleKeyboard(e) {
|
||
const focusedDate = this.getFocusedDate();
|
||
let newDate;
|
||
|
||
switch(e.key) {
|
||
case 'ArrowLeft':
|
||
e.preventDefault();
|
||
newDate = new Date(focusedDate);
|
||
newDate.setDate(newDate.getDate() - 1);
|
||
this.focusDate(newDate);
|
||
break;
|
||
case 'ArrowRight':
|
||
e.preventDefault();
|
||
newDate = new Date(focusedDate);
|
||
newDate.setDate(newDate.getDate() + 1);
|
||
this.focusDate(newDate);
|
||
break;
|
||
case 'ArrowUp':
|
||
e.preventDefault();
|
||
newDate = new Date(focusedDate);
|
||
newDate.setDate(newDate.getDate() - 7);
|
||
this.focusDate(newDate);
|
||
break;
|
||
case 'ArrowDown':
|
||
e.preventDefault();
|
||
newDate = new Date(focusedDate);
|
||
newDate.setDate(newDate.getDate() + 7);
|
||
this.focusDate(newDate);
|
||
break;
|
||
case 'PageUp':
|
||
e.preventDefault();
|
||
this.navigate(-1);
|
||
break;
|
||
case 'PageDown':
|
||
e.preventDefault();
|
||
this.navigate(1);
|
||
break;
|
||
case 'Enter':
|
||
e.preventDefault();
|
||
this.selectDate(focusedDate);
|
||
break;
|
||
case 'Escape':
|
||
e.preventDefault();
|
||
this.close();
|
||
break;
|
||
}
|
||
}
|
||
|
||
getFocusedDate() {
|
||
const focused = this.picker.querySelector('.day-cell:focus');
|
||
if (focused) {
|
||
// Extract date from focused cell
|
||
const day = parseInt(focused.textContent);
|
||
return new Date(this.currentYear, this.currentMonth, day);
|
||
}
|
||
return new Date();
|
||
}
|
||
|
||
focusDate(date) {
|
||
if (date.getMonth() !== this.currentMonth || date.getFullYear() !== this.currentYear) {
|
||
this.currentMonth = date.getMonth();
|
||
this.currentYear = date.getFullYear();
|
||
this.updateCalendar();
|
||
}
|
||
|
||
setTimeout(() => {
|
||
const cells = this.picker.querySelectorAll('.day-cell:not(.other-month)');
|
||
cells.forEach(cell => {
|
||
if (parseInt(cell.textContent) === date.getDate()) {
|
||
cell.focus();
|
||
}
|
||
});
|
||
}, 100);
|
||
}
|
||
|
||
navigate(direction) {
|
||
if (this.isAnimating) return;
|
||
|
||
const oldGrid = this.currentGrid;
|
||
|
||
this.currentMonth += direction;
|
||
if (this.currentMonth > 11) {
|
||
this.currentMonth = 0;
|
||
this.currentYear++;
|
||
} else if (this.currentMonth < 0) {
|
||
this.currentMonth = 11;
|
||
this.currentYear--;
|
||
}
|
||
|
||
this.monthSelect.value = this.currentMonth;
|
||
this.yearSelect.value = this.currentYear;
|
||
|
||
// Create new grid
|
||
const newGrid = this.createCalendarGrid();
|
||
|
||
// Animate transition
|
||
this.isAnimating = true;
|
||
|
||
if (direction > 0) {
|
||
newGrid.classList.add('next');
|
||
this.calendarWrapper.appendChild(newGrid);
|
||
|
||
setTimeout(() => {
|
||
oldGrid.classList.add('prev');
|
||
newGrid.classList.remove('next');
|
||
newGrid.classList.add('animating-next');
|
||
}, 10);
|
||
} else {
|
||
newGrid.classList.add('prev');
|
||
this.calendarWrapper.appendChild(newGrid);
|
||
|
||
setTimeout(() => {
|
||
oldGrid.classList.add('next');
|
||
newGrid.classList.remove('prev');
|
||
newGrid.classList.add('animating-prev');
|
||
}, 10);
|
||
}
|
||
|
||
setTimeout(() => {
|
||
oldGrid.remove();
|
||
newGrid.classList.remove('animating-prev', 'animating-next');
|
||
this.currentGrid = newGrid;
|
||
this.isAnimating = false;
|
||
}, 300);
|
||
}
|
||
|
||
updateInput() {
|
||
let value = '';
|
||
|
||
if (this.options.mode === 'single' && this.selectedDates.length > 0) {
|
||
value = this.formatDate(this.selectedDates[0]);
|
||
} else if (this.options.mode === 'range' && this.rangeStart && this.rangeEnd) {
|
||
value = `${this.formatDate(this.rangeStart)} - ${this.formatDate(this.rangeEnd)}`;
|
||
} else if (this.options.mode === 'multi' && this.selectedDates.length > 0) {
|
||
const sorted = [...this.selectedDates].sort((a, b) => a - b);
|
||
value = sorted.map(d => this.formatDate(d)).join(', ');
|
||
if (value.length > 30) {
|
||
value = `${this.selectedDates.length} dates selected`;
|
||
}
|
||
}
|
||
|
||
this.input.value = value;
|
||
}
|
||
|
||
formatDate(date) {
|
||
if (this.options.showTime) {
|
||
const hours = this.hourInput ? this.hourInput.value : date.getHours();
|
||
const minutes = this.minuteInput ? this.minuteInput.value : date.getMinutes();
|
||
date.setHours(hours, minutes);
|
||
return date.toLocaleString(this.options.locale, {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
return date.toLocaleDateString(this.options.locale, {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric'
|
||
});
|
||
}
|
||
|
||
isSameDay(date1, date2) {
|
||
return date1.getFullYear() === date2.getFullYear() &&
|
||
date1.getMonth() === date2.getMonth() &&
|
||
date1.getDate() === date2.getDate();
|
||
}
|
||
|
||
isDateDisabled(date) {
|
||
if (this.options.minDate && date < this.options.minDate) return true;
|
||
if (this.options.maxDate && date > this.options.maxDate) return true;
|
||
|
||
return this.options.disabledDates.some(disabled =>
|
||
this.isSameDay(date, disabled)
|
||
);
|
||
}
|
||
|
||
getWeekNumber(date) {
|
||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||
const dayNum = d.getUTCDay() || 7;
|
||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
|
||
return Math.ceil((((d - yearStart) / 86400000) + 1)/7);
|
||
}
|
||
|
||
toggle() {
|
||
if (this.picker.classList.contains('active')) {
|
||
this.close();
|
||
} else {
|
||
this.open();
|
||
}
|
||
}
|
||
|
||
open() {
|
||
this.picker.classList.add('active');
|
||
|
||
// Mobile backdrop
|
||
const backdrop = document.getElementById('backdrop');
|
||
if (backdrop && window.innerWidth <= 640) {
|
||
backdrop.classList.add('active');
|
||
}
|
||
|
||
// Position picker
|
||
this.positionPicker();
|
||
|
||
// Focus picker
|
||
setTimeout(() => this.picker.focus(), 100);
|
||
}
|
||
|
||
close() {
|
||
this.picker.classList.remove('active');
|
||
|
||
// Mobile backdrop
|
||
const backdrop = document.getElementById('backdrop');
|
||
if (backdrop) {
|
||
backdrop.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
positionPicker() {
|
||
if (window.innerWidth <= 640) return; // Fixed position on mobile
|
||
|
||
const inputRect = this.input.getBoundingClientRect();
|
||
const pickerRect = this.picker.getBoundingClientRect();
|
||
|
||
// Check if picker would go off-screen
|
||
if (inputRect.bottom + pickerRect.height > window.innerHeight) {
|
||
// Position above input
|
||
this.picker.style.top = 'auto';
|
||
this.picker.style.bottom = `calc(100% + 8px)`;
|
||
} else {
|
||
// Position below input
|
||
this.picker.style.top = `calc(100% + 8px)`;
|
||
this.picker.style.bottom = 'auto';
|
||
}
|
||
|
||
if (inputRect.left + pickerRect.width > window.innerWidth) {
|
||
// Align to right edge
|
||
this.picker.style.left = 'auto';
|
||
this.picker.style.right = '0';
|
||
} else {
|
||
// Align to left edge
|
||
this.picker.style.left = '0';
|
||
this.picker.style.right = 'auto';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize demo pickers
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Single date picker
|
||
new EnhancedDatePicker(document.getElementById('singleDate'), {
|
||
mode: 'single',
|
||
showWeekNumbers: true
|
||
});
|
||
|
||
// Date range picker
|
||
new EnhancedDatePicker(document.getElementById('dateRange'), {
|
||
mode: 'range'
|
||
});
|
||
|
||
// Date time picker
|
||
new EnhancedDatePicker(document.getElementById('dateTime'), {
|
||
mode: 'single',
|
||
showTime: true
|
||
});
|
||
|
||
// Multiple dates picker
|
||
new EnhancedDatePicker(document.getElementById('multiDate'), {
|
||
mode: 'multi',
|
||
showWeekNumbers: true
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |