Pikaday удобно использовать как базовый календарный компонент, однако в реальных интерфейсах почти всегда требуется не один календарь, а связанная пара: выбор диапазона дат, где первый календарь отвечает за начальную дату, а второй — за конечную. Синхронизация таких календарей предполагает управление состоянием между двумя независимыми экземплярами, динамическое обновление ограничений и предотвращение неконсистентных выборов.
Типичный сценарий состоит из двух полей ввода:
startDateendDateОба календаря создаются как независимые экземпляры Pikaday, но связываются через общую бизнес-логику.
Ключевой принцип заключается в том, что:
Таким образом предотвращаются ситуации, когда конечная дата оказывается раньше начальной.
Базовая структура создаёт два объекта и связывает их через callbacks:
const startPicker = new Pikaday({
field: document.getElementById('start'),
onSelect: function(date) {
endPicker.setMinDate(date);
}
});
const endPicker = new Pikaday({
field: document.getElementById('end'),
onSelect: function(date) {
startPicker.setMaxDate(date);
}
});
В этой модели синхронизация строится на двух методах:
setMinDate(date) — ограничивает минимальную дату
выбораsetMaxDate(date) — ограничивает максимальную дату
выбораОни позволяют динамически перестраивать допустимый диапазон без пересоздания календаря.
При выборе начальной даты важно учитывать уже установленное значение конечной даты. Если пользователь сначала выбрал диапазон, а затем изменил startDate так, что оно становится больше endDate, необходимо корректировать второе значение.
Расширенная логика:
const startPicker = new Pikaday({
field: document.getElementById('start'),
onSelect: function(date) {
if (endPicker.getDate() && date > endPicker.getDate()) {
endPicker.setDate(null);
}
endPicker.setMinDate(date);
}
});
Здесь вводится дополнительная проверка:
Аналогичный принцип применяется для второго календаря:
const endPicker = new Pikaday({
field: document.getElementById('end'),
onSelect: function(date) {
if (startPicker.getDate() && date < startPicker.getDate()) {
startPicker.setDate(null);
}
startPicker.setMaxDate(date);
}
});
Таким образом достигается двусторонняя связь, при которой оба календаря поддерживают валидный диапазон без конфликтов.
В сложных интерфейсах ограничения часто задаются не только выбором пользователя, но и бизнес-правилами:
Эти ограничения комбинируются с синхронизацией:
const minAllowed = new Date();
const maxAllowed = new Date();
maxAllowed.setMonth(maxAllowed.getMonth() + 6);
const startPicker = new Pikaday({
field: document.getElementById('start'),
minDate: minAllowed,
maxDate: maxAllowed,
onSelect: function(date) {
endPicker.setMinDate(date);
enforceRangeLimit(date);
}
});
Функция контроля диапазона:
function enforceRangeLimit(startDate) {
const maxRangeDate = new Date(startDate);
maxRangeDate.setDate(maxRangeDate.getDate() + 30);
endPicker.setMaxDate(maxRangeDate);
}
Одним из ключевых преимуществ Pikaday является возможность изменять поведение календаря без пересоздания экземпляра. Это особенно важно при синхронизации, так как пересоздание двух календарей приводит к потере состояния.
Используются методы:
setDate(date, triggerChange)gotoDate(date)setMinDate(date)setMaxDate(date)Пример централизованного обновления:
function syncCalendars(from, to, selectedDate, type) {
if (type === 'start') {
to.setMinDate(selectedDate);
if (to.getDate() && to.getDate() < selectedDate) {
to.setDate(null);
}
}
if (type === 'end') {
from.setMaxDate(selectedDate);
if (from.getDate() && from.getDate() > selectedDate) {
from.setDate(null);
}
}
}
При масштабировании интерфейса до сложных форм удобно вводить единое состояние:
const state = {
startDate: null,
endDate: null
};
Каждый календарь только обновляет state, а синхронизация происходит централизованно:
const startPicker = new Pikaday({
field: document.getElementById('start'),
onSelect: function(date) {
state.startDate = date;
state.endDatePicker.setMinDate(date);
if (state.endDate && state.endDate < date) {
state.endDatePicker.setDate(null);
state.endDate = null;
}
}
});
Такой подход уменьшает дублирование логики и делает поведение предсказуемым.
При частом изменении дат возможна ситуация, когда оба календаря
одновременно вызывают onSelect, создавая циклические
обновления. Для предотвращения используются флаги блокировки:
let isSyncing = false;
const startPicker = new Pikaday({
field: document.getElementById('start'),
onSelect: function(date) {
if (isSyncing) return;
isSyncing = true;
endPicker.setMinDate(date);
if (endPicker.getDate() && endPicker.getDate() < date) {
endPicker.setDate(null);
}
isSyncing = false;
}
});
Это предотвращает рекурсивные вызовы синхронизации.
В некоторых сценариях диапазон зависит от внешних факторов: смены пользователя, типа бронирования, выбранного тарифа. Тогда синхронизация календарей становится частью общей реактивной системы.
function updateAllowedRange(min, max) {
startPicker.setMinDate(min);
startPicker.setMaxDate(max);
endPicker.setMinDate(min);
endPicker.setMaxDate(max);
if (startPicker.getDate() < min) startPicker.setDate(null);
if (endPicker.getDate() > max) endPicker.setDate(null);
}
Пользователь может вводить дату вручную в input, минуя календарь. В этом случае необходимо синхронизировать состояние и при изменении текста:
document.getElementById('start').addEventListener('change', function(e) {
const date = new Date(e.target.value);
startPicker.setDate(date);
endPicker.setMinDate(date);
});
Такой подход обеспечивает единое поведение независимо от способа ввода.
Синхронизация касается не только логики, но и визуального отображения:
Для этого используется метод:
startPicker.draw();
endPicker.draw();
Он принудительно перерисовывает календарь после изменения ограничений.
Особое внимание требуется при:
В таких случаях логика должна сохранять непротиворечивость:
if (startDate && endDate && startDate.getTime() === endDate.getTime()) {
endPicker.setDate(null);
}
Согласованность состояния важнее сохранения каждого пользовательского действия без изменений.