Выбор начальной и конечной даты

Работа с диапазонами дат в календарях требует не только отображения двух точек — начальной и конечной — но и управления промежуточным состоянием выбора, визуальной подсветкой и корректной обработкой повторных кликов. В контексте Pikaday это обычно реализуется поверх базового функционала одиночного выбора даты через расширение логики обработки onSelect и хранения пользовательского состояния.

Базовая модель состояния диапазона

Для реализации выбора диапазона вводится два ключевых состояния:

  • startRange — начальная дата диапазона
  • endRange — конечная дата диапазона

Эти значения не являются встроенными в ядро Pikaday, но часто используются в пользовательских расширениях поверх экземпляра календаря.

let startRange = null;
let endRange = null;

Логика строится вокруг последовательного выбора:

  1. Первый клик устанавливает startRange
  2. Второй клик устанавливает endRange
  3. Следующий клик сбрасывает диапазон и начинает заново

Перехват выбора даты через onSelect

Pikaday предоставляет механизм onSelect, который вызывается при каждом выборе даты. Именно он используется для построения логики диапазона.

const picker = new Pikaday({
  field: document.getElementById('datepicker'),
  onSelect: function(date) {
    handleRangeSelection(date);
  }
});

Функция обработки становится центральным элементом управления состоянием.

function handleRangeSelection(date) {
  if (!startRange || (startRange && endRange)) {
    startRange = date;
    endRange = null;
    return;
  }

  if (date < startRange) {
    endRange = startRange;
    startRange = date;
  } else {
    endRange = date;
  }
}

Такой подход обеспечивает устойчивую логику даже при выборе дат в обратном порядке.

Нормализация порядка дат

Одной из ключевых проблем диапазонов является порядок выбора. Пользователь может сначала выбрать позднюю дату, а затем раннюю. Поэтому важно всегда нормализовать значения:

  • более ранняя дата становится startRange
  • более поздняя дата становится endRange
function normalizeRange(a, b) {
  return a <= b ? [a, b] : [b, a];
}

В контексте Pikaday это позволяет избежать дополнительных проверок при отрисовке интерфейса.

Визуальная подсветка диапазона

Pikaday не предоставляет встроенной подсветки диапазонов, поэтому она реализуется через onDraw, который вызывается при каждом рендере календаря.

onDraw: function() {
  highlightRange();
}

Алгоритм подсветки основан на сравнении каждой ячейки календаря с текущим диапазоном:

function highlightRange() {
  const cells = document.querySelectorAll('.pika-button');

  cells.forEach(cell => {
    const cellDate = cell.getAttribute('data-pika-day');
    const date = new Date(cellDate);

    cell.classList.remove('in-range', 'range-start', 'range-end');

    if (startRange && isSameDate(date, startRange)) {
      cell.classList.add('range-start');
    }

    if (endRange && isSameDate(date, endRange)) {
      cell.classList.add('range-end');
    }

    if (startRange && endRange && date > startRange && date < endRange) {
      cell.classList.add('in-range');
    }
  });
}

Функция сравнения дат обычно упрощается до сравнения timestamp без учёта времени:

function isSameDate(a, b) {
  return a.getFullYear() === b.getFullYear() &&
         a.getMonth() === b.getMonth() &&
         a.getDate() === b.getDate();
}

Сброс диапазона при повторном выборе

Поведение календаря часто требует сброса диапазона при третьем клике. Это позволяет пользователю быстро начать новый выбор без дополнительных действий.

function handleRangeSelection(date) {
  if (startRange && endRange) {
    startRange = date;
    endRange = null;
    return;
  }

  if (!startRange) {
    startRange = date;
    return;
  }

  if (date < startRange) {
    endRange = startRange;
    startRange = date;
  } else {
    endRange = date;
  }
}

Такой подход делает интерфейс предсказуемым и снижает количество ошибок при выборе.

Ограничение диапазона с помощью minDate и maxDate

Pikaday поддерживает встроенные ограничения:

  • minDate
  • maxDate

При работе с диапазонами они используются как внешние рамки допустимого выбора.

const picker = new Pikaday({
  field: document.getElementById('datepicker'),
  minDate: new Date(2025, 0, 1),
  maxDate: new Date(2026, 11, 31)
});

При наличии активного диапазона часто требуется динамическое обновление ограничений:

picker.setMinDate(startRange);
picker.setMaxDate(new Date(2026, 11, 31));

Это предотвращает выбор некорректных промежуточных значений в зависимости от бизнес-логики.

Подсветка «предпросмотра» диапазона при наведении

Расширенная реализация диапазонов включает состояние hover-preview — визуальную подсветку потенциального диапазона до фиксации второго клика.

let hoverDate = null;

document.addEventListener('mouseover', function(e) {
  if (e.target.classList.contains('pika-button')) {
    hoverDate = new Date(e.target.getAttribute('data-pika-day'));
    previewRange();
  }
});

Логика отображения временного диапазона:

function previewRange() {
  if (!startRange || endRange) return;

  const cells = document.querySelectorAll('.pika-button');

  cells.forEach(cell => {
    const date = new Date(cell.getAttribute('data-pika-day'));
    cell.classList.remove('in-preview');

    if (date > startRange && date < hoverDate) {
      cell.classList.add('in-preview');
    }
  });
}

Это создаёт более интерактивное поведение, приближенное к современным date-range пикерам.

Форматирование результата диапазона

После завершения выбора часто требуется преобразовать диапазон в строку или структуру данных для отправки на сервер.

function formatRange(start, end) {
  const format = d => d.toISOString().split('T')[0];
  return `${format(start)} - ${format(end)}`;
}

Либо в структурированном виде:

function getRangeObject() {
  return {
    start: startRange,
    end: endRange
  };
}

Синхронизация с внешним input-полем

Pikaday обычно привязан к текстовому полю, и диапазон необходимо отображать явно:

picker.setField(document.getElementById('datepicker'));

Обновление значения:

function updateInput() {
  if (startRange && endRange) {
    picker._o.field.value = formatRange(startRange, endRange);
  } else if (startRange) {
    picker._o.field.value = formatRange(startRange, startRange);
  }
}

Обработка повторного выбора той же даты

Особый случай возникает при повторном выборе одной и той же даты:

  • если startRange === date, диапазон может сбрасываться
  • либо фиксироваться как однодневный диапазон
if (startRange && isSameDate(startRange, date) && !endRange) {
  startRange = null;
  return;
}

Такое поведение зависит от логики интерфейса, но важно учитывать его во избежание «залипания» выбора.

Согласованность состояния и перерисовка

Любое изменение startRange или endRange должно сопровождаться принудительным обновлением календаря:

picker.draw();

Это гарантирует синхронизацию DOM с внутренним состоянием и корректную подсветку диапазона.


Реализация диапазонного выбора поверх Pikaday строится как комбинация управления состоянием, перехвата событий выбора и кастомной отрисовки. Основная сложность заключается не в выборе дат как таковом, а в поддержании согласованного визуального и логического состояния при каждом взаимодействии пользователя с календарём.