Визуальное выделение диапазона

Механизм визуального выделения диапазона в календаре 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');
        }
    });
}

CSS-логика визуального выделения

Визуальная часть диапазона полностью определяется стилями. Основная задача — создать непрерывную полосу между двумя точками и визуально выделить границы.

Типичная схема классов:

  • 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-полями. После каждого изменения состояния выполняется синхронизация:

  • форматирование даты через toString
  • запись значения в скрытые или видимые поля
  • триггер пользовательских событий
if (startDate) {
    inputStart.value = formatDate(startDate);
}

if (endDate) {
    inputEnd.value = formatDate(endDate);
}

Поведение при сбросе диапазона

Сброс диапазона обычно происходит при:

  • выборе новой даты после завершённого диапазона
  • ручной очистке
  • повторном клике на уже выбранный диапазон

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

startDate = null;
endDate = null;
picker.draw();

Согласованность визуального состояния

Ключевая сложность реализации заключается в предотвращении рассинхронизации между:

  • внутренним состоянием дат
  • отображением календарной сетки
  • внешними input-полями

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