Механизм визуального выделения диапазона в календаре Pikaday строится поверх базовой модели выбора одной даты и требует расширения стандартного поведения через управление состоянием и кастомную отрисовку ячеек. Основная идея заключается в том, что календарь должен одновременно хранить две опорные точки — начало и конец интервала — и динамически подсвечивать все даты между ними.
В стандартной конфигурации Pikaday оперирует единственным значением выбранной даты. Для реализации диапазона вводится дополнительное состояние:
startDate — начало интервалаendDate — конец интервалаЛогика выбора обычно строится по принципу «первый клик задаёт начало, второй — конец». При этом возможны промежуточные состояния:
startDateКлючевым моментом является то, что сами даты не хранятся внутри календаря как диапазонный тип — они поддерживаются внешним состоянием приложения.
Поведение выбора строится на анализе текущего состояния при каждом клике:
если не задан startDate — выбранная дата становится
началом диапазона
если задан startDate, но не задан
endDate:
startDate она становится концом
диапазонаstartDate происходит сдвиг
начала диапазонаесли заданы обе даты — новый клик начинает новый диапазон
Такая модель исключает неоднозначность и позволяет пользователю интуитивно корректировать диапазон без дополнительных элементов управления.
В обработчике onSelect Pikaday фиксируется базовая точка
взаимодействия, после чего логика диапазона реализуется вручную:
onSelect: function(date) {
if (!startDate || (startDate && endDate)) {
startDate = date;
endDate = null;
} else {
if (date >= startDate) {
endDate = date;
} else {
endDate = startDate;
startDate = date;
}
}
picker.draw();
}
Визуальное выделение диапазона реализуется через переопределение
логики рендеринга ячеек. Pikaday предоставляет механизм
onDraw, который вызывается при каждой перерисовке
календаря.
Каждая дата в сетке проверяется на принадлежность к диапазону:
startDate → начало диапазонаendDate → конец диапазонаЭта проверка позволяет добавлять соответствующие CSS-классы.
onDraw: function() {
const cells = picker.el.querySelectorAll('.pika-button');
cells.forEach(cell => {
const time = cell.getAttribute('data-pika-year') + '-' +
cell.getAttribute('data-pika-month') + '-' +
cell.getAttribute('data-pika-day');
const cellDate = new Date(time);
cell.classList.remove('in-range', 'range-start', 'range-end');
if (startDate && isSameDate(cellDate, startDate)) {
cell.classList.add('range-start');
}
if (endDate && isSameDate(cellDate, endDate)) {
cell.classList.add('range-end');
}
if (startDate && endDate &&
cellDate > startDate && cellDate < endDate) {
cell.classList.add('in-range');
}
});
}
Визуальная часть диапазона полностью определяется стилями. Основная задача — создать непрерывную полосу между двумя точками и визуально выделить границы.
Типичная схема классов:
range-start — левая граница диапазонаrange-end — правая граница диапазонаin-range — промежуточные датыПример стилизации:
.pika-button.in-range {
background: rgba(66, 133, 244, 0.15);
border-radius: 0;
}
.pika-button.range-start,
.pika-button.range-end {
background: #4285f4;
color: #fff;
border-radius: 50%;
}
Особое внимание уделяется скруглениям: внутренние элементы должны
выглядеть как единая полоса, поэтому border-radius у
промежуточных элементов обычно убирается.
Поскольку Pikaday поддерживает многостраничную навигацию по месяцам, диапазон должен сохранять консистентность при перелистывании календаря.
При каждом вызове draw():
Это означает, что состояние диапазона никогда не зависит от DOM —
только от startDate и endDate.
При использовании numberOfMonths > 1 диапазон
визуально растягивается через несколько календарных сеток. Логика
остаётся неизменной, но усложняется визуальная непрерывность:
in-range без
границВажно, что каждая сетка обрабатывается отдельно, но на основе единого состояния.
Состояние, при котором задан только startDate, требует
отдельного визуального поведения. Обычно применяется «предпросмотр»
диапазона:
startDate и hover-дата подсвечиваются
временноЭто реализуется через событие onMouseEnter на ячейках
календаря:
cell.addEventListener('mouseenter', () => {
if (startDate && !endDate) {
previewEndDate = cellDate;
picker.draw();
}
});
И затем добавляется дополнительный класс:
in-range-preview — временное состояниеif (startDate && previewEndDate &&
cellDate > startDate && cellDate < previewEndDate) {
cell.classList.add('in-range-preview');
}
Диапазон часто связан с двумя input-полями. После каждого изменения состояния выполняется синхронизация:
toStringif (startDate) {
inputStart.value = formatDate(startDate);
}
if (endDate) {
inputEnd.value = formatDate(endDate);
}
Сброс диапазона обычно происходит при:
Логика сброса заключается в обнулении состояния и перерисовке календаря:
startDate = null;
endDate = null;
picker.draw();
Ключевая сложность реализации заключается в предотвращении рассинхронизации между:
Поскольку Pikaday не имеет встроенной поддержки диапазонов, вся консистентность достигается через единый источник истины — переменные состояния, на основе которых полностью перестраивается интерфейс при каждом изменении.