Стилизация диапазона

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

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

При реализации диапазона дат используется два состояния:

  • начальная дата (start date)
  • конечная дата (end date)

Дополнительно вычисляется промежуточное состояние:

  • дата внутри диапазона (in range)

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

let startDate = null;
let endDate = null;

const picker = new Pikaday({
  field: document.getElementById('datepicker'),
  onSelect: function(date) {
    if (!startDate || (startDate && endDate)) {
      startDate = date;
      endDate = null;
    } else {
      if (date < startDate) {
        endDate = startDate;
        startDate = date;
      } else {
        endDate = date;
      }
    }

    updateUI();
  }
});

С точки зрения стилизации ключевым моментом становится не сама логика, а корректная передача состояния в DOM.

Подготовка классов состояния

Pikaday позволяет использовать кастомизацию через метод onDraw, который вызывается при каждом рендере календаря. Именно здесь происходит маркировка ячеек дат.

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

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

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

    if (startDate && date.getTime() === startDate.getTime()) {
      cell.classList.add('is-range-start');
    }

    if (endDate && date.getTime() === endDate.getTime()) {
      cell.classList.add('is-range-end');
    }

    if (startDate && endDate && date > startDate && date < endDate) {
      cell.classList.add('is-in-range');
    }
  });
}

Ключевой принцип заключается в том, что библиотека не хранит диапазон как сущность — он полностью формируется внешним кодом.

Базовая визуализация диапазона

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

.is-in-range {
  background-color: #e6f2ff;
  color: #1a1a1a;
  border-radius: 0;
}

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

.is-range-start,
.is-range-end {
  background-color: #1a73e8;
  color: #ffffff;
  font-weight: 600;
}

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

.is-in-range {
  border-radius: 0;
}

Формирование непрерывной линии диапазона

Визуальная целостность диапазона достигается за счёт псевдо-логики соединения ячеек. В Pikaday каждая дата — это отдельная кнопка, поэтому эффект “полосы” создаётся исключительно стилями.

Дополнительно используется разделение состояний:

  • левая граница диапазона
  • правая граница диапазона
  • середина диапазона
.is-range-start {
  border-top-left-radius: 6px;
  border-bottom-left-radius: 6px;
}

.is-range-end {
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}

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

if (startDate && endDate && startDate.getTime() === endDate.getTime()) {
  cell.classList.add('is-range-start', 'is-range-end');
}

Работа с hover-эффектами диапазона

Hover-состояния играют ключевую роль при выборе конечной даты. Часто используется временная визуализация “предпросмотра диапазона”.

Добавляется переменная hoverDate, которая обновляется при наведении:

let hoverDate = null;

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

И далее учитывается в onDraw:

if (startDate && !endDate && hoverDate) {
  const min = startDate < hoverDate ? startDate : hoverDate;
  const max = startDate < hoverDate ? hoverDate : startDate;

  if (date > min && date < max) {
    cell.classList.add('is-in-range-preview');
  }
}

CSS для предпросмотра отличается меньшей интенсивностью:

.is-in-range-preview {
  background-color: rgba(26, 115, 232, 0.15);
}

Управление конфликтами классов

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

  • началом диапазона
  • единственной выбранной датой
  • частью hover-предпросмотра

Для предотвращения визуальных артефактов применяется приоритетность:

  1. is-range-start / is-range-end
  2. is-in-range
  3. is-in-range-preview

Это достигается либо порядком добавления классов, либо более специфичными CSS-селекторами:

.is-range-start.is-in-range-preview {
  background-color: #1a73e8;
}

Стилизация отключённых дат в контексте диапазона

При работе с диапазонами важно учитывать недоступные даты (disabled). Они должны визуально отличаться и не участвовать в логике подсветки.

.is-disabled {
  color: #bdbdbd;
  pointer-events: none;
  background-color: transparent;
}

Если отключённая дата попадает в диапазон логически (например, из-за ошибок вычисления), её состояние должно принудительно переопределяться:

if (cell.classList.contains('is-disabled')) {
  cell.classList.remove('is-in-range', 'is-range-start', 'is-range-end');
}

Адаптация под тёмную тему

Стилизация диапазона должна учитывать контекст темы интерфейса. В тёмных интерфейсах важнее контраст и прозрачность, чем насыщенность цвета.

.dark-theme .is-in-range {
  background-color: rgba(66, 133, 244, 0.25);
  color: #ffffff;
}

.dark-theme .is-range-start,
.dark-theme .is-range-end {
  background-color: #4285f4;
}

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

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

Для устранения визуальных разрывов применяется устранение зазоров:

.pika-table td {
  padding: 0;
}

.pika-button {
  width: 100%;
  height: 100%;
}

Дополнительно используется компенсация границ:

.is-in-range + .is-in-range {
  border-left: none;
}

Сложные диапазоны и множественные выделения

При расширении логики до нескольких диапазонов добавляется массив интервалов:

const ranges = [
  { start: new Date(2026, 0, 1), end: new Date(2026, 0, 5) },
  { start: new Date(2026, 0, 10), end: new Date(2026, 0, 15) }
];

Стилизация в этом случае требует проверки попадания даты в любой из диапазонов:

function isInAnyRange(date) {
  return ranges.some(range => date >= range.start && date <= range.end);
}

CSS остаётся прежним, но логика добавления классов усложняется за счёт проверки всех интервалов.

Поведенческая согласованность интерфейса

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

Поэтому критически важно:

  • синхронизировать onSelect и onDraw
  • избегать прямого DOM-манипулирования без пересчёта состояния
  • пересчитывать диапазон при любом изменении даты

Так формируется предсказуемое поведение календаря даже при сложной кастомизации внешнего вида.