Временные диапазоны

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

Модель временного диапазона чаще всего сводится к двум точкам: началу и завершению интервала. В пользовательском интерфейсе это можно упаковать в единый элемент списка:

  • “09:00 — 12:00”
  • “12:00 — 15:00”
  • “15:00 — 18:00”

Каждый такой элемент фактически является атомарным значением, хотя логически включает два времени. Tom Select работает с этим без дополнительных адаптаций, если заранее подготовить список options.

Базовая инициализация с фиксированными интервалами выглядит следующим образом:

new TomSelect("
    options: [
        { value: "09:00-12:00", text: "09:00 — 12:00" },
        { value: "12:00-15:00", text: "12:00 — 15:00" },
        { value: "15:00-18:00", text: "15:00 — 18:00" },
        { value: "18:00-21:00", text: "18:00 — 21:00" }
    ],
    maxItems: 1,
    placeholder: "Выбор интервала"
});

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


Формирование интервалов на лету

Гораздо более гибкая модель — генерация временных интервалов программно. Это позволяет менять шаг (например, 15, 30 или 60 минут) без изменения разметки.

function buildTimeRanges(stepMinutes = 60) {
    const result = [];

    for (let minutes = 0; minutes < 24 * 60; minutes += stepMinutes) {
        const start = formatTime(minutes);
        const end = formatTime(minutes + stepMinutes);

        result.push({
            value: `${start}-${end}`,
            text: `${start} — ${end}`
        });
    }

    return result;
}

function formatTime(totalMinutes) {
    const h = String(Math.floor(totalMinutes / 60)).padStart(2, "0");
    const m = String(totalMinutes % 60).padStart(2, "0");
    return `${h}:${m}`;
}

Инициализация:

new TomSelect("#time-ranges", {
    options: buildTimeRanges(30),
    maxItems: null,
    placeholder: "Интервалы времени"
});

Такой подход делает интерфейс независимым от заранее заданного набора данных и позволяет легко адаптировать расписание под разные бизнес-логики.


Разделение начала и конца как составная модель

В более сложных сценариях один селект недостаточен. Например, при создании бронирования важно контролировать валидность диапазона: конец не может быть раньше начала, а пересечения интервалов должны исключаться.

В этом случае используется связка из двух Tom Select компонентов.

<select id="time-start"></select>
<select id="time-end"></select>

Инициализация:

const times = buildTimeOptions(15);

const startSelect = new TomSelect("#time-start", {
    options: times,
    placeholder: "Начало"
});

const endSelect = new TomSelect("#time-end", {
    options: times,
    placeholder: "Окончание"
});

Генерация списка времени с шагом:

function buildTimeOptions(step = 15) {
    const list = [];

    for (let i = 0; i < 24 * 60; i += step) {
        const t = formatTime(i);
        list.push({ value: t, text: t });
    }

    return list;
}

Синхронизация логики диапазона

После разделения полей возникает необходимость в контроле корректности выбора. Tom Select предоставляет события change, которые удобно использовать для динамической фильтрации доступных значений.

startSelect.on("change", (value) => {
    const startMinutes = toMinutes(value);

    endSelect.clearOptions();

    const filtered = times.filter(t => toMinutes(t.value) > startMinutes);

    endSelect.addOptions(filtered);
    endSelect.refreshOptions(false);
});

Преобразование времени в минуты:

function toMinutes(time) {
    const [h, m] = time.split(":").map(Number);
    return h * 60 + m;
}

Такая схема гарантирует, что конечное время всегда больше начального, а список автоматически перестраивается при изменении первого селекта.


Кастомная отрисовка интервалов

Tom Select поддерживает переопределение шаблонов отображения. Это позволяет визуально выделять диапазоны, добавлять подсказки или форматирование.

new TomSelect("#time-ranges", {
    options: buildTimeRanges(60),
    render: {
        option: function(data, escape) {
            return `
                <div class="option-range">
                    <strong>${escape(data.text)}</strong>
                </div>
            `;
        },
        item: function(data, escape) {
            return `<div class="selected-range">${escape(data.text)}</div>`;
        }
    }
});

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


Расширенные структуры данных

При переходе от простых строк к объектной модели появляется возможность хранить дополнительные параметры:

{
    value: "09:00-12:00",
    start: "09:00",
    end: "12:00",
    capacity: 5,
    disabled: false
}

Tom Select игнорирует неизвестные поля, но они доступны в логике приложения:

new TomSelect("#time-ranges", {
    options: ranges,
    render: {
        option: function(data, escape) {
            const status = data.capacity === 0 ? "нет мест" : `мест: ${data.capacity}`;

            return `
                <div>
                    <span>${escape(data.start)} — ${escape(data.end)}</span>
                    <small>${status}</small>
                </div>
            `;
        }
    }
});

Динамическое исключение пересекающихся интервалов

При работе с расписаниями часто требуется исключать уже занятые периоды. Это достигается фильтрацией массива options перед обновлением Tom Select.

function excludeBusyRanges(ranges, busy) {
    return ranges.filter(r => {
        const start = toMinutes(r.start);
        const end = toMinutes(r.end);

        return !busy.some(b => {
            const bStart = toMinutes(b.start);
            const bEnd = toMinutes(b.end);

            return start < bEnd && end > bStart;
        });
    });
}

Применение:

const busy = [
    { start: "10:00", end: "11:00" },
    { start: "14:00", end: "15:30" }
];

const available = excludeBusyRanges(ranges, busy);

select.clearOptions();
select.addOptions(available);
select.refreshOptions();

Множественные диапазоны и составные выборы

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

new TomSelect("#time-ranges", {
    options: buildTimeRanges(60),
    maxItems: null,
    plugins: ["remove_button"]
});

Каждый выбранный элемент остаётся независимым, что удобно для графиков смен, бронирований или планирования ресурсов.

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

select.on("change", (values) => {
    const parsed = values.map(v => {
        const [start, end] = v.split("-");
        return { start, end };
    });

    // проверка пересечений
});

Интеграция с серверной моделью

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

function serializeRanges(values) {
    return values.map(v => {
        const [start, end] = v.split("-");
        return { start, end };
    });
}

И обратная операция при загрузке:

function deserializeRanges(data) {
    return data.map(r => ({
        value: `${r.start}-${r.end}`,
        text: `${r.start} — ${r.end}`
    }));
}

Поведение при больших наборах данных

Если шаг интервалов уменьшается до 5–10 минут, количество options резко возрастает. В таких условиях важна оптимизация:

  • минимизация перерисовок через refreshOptions(false)
  • предварительная генерация массива
  • отключение лишних рендеров через упрощённые templates
select.addOptions(bigDataset);
select.refreshOptions(false);

Состояния и отключение элементов

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

{
    value: "13:00-14:00",
    text: "13:00 — 14:00",
    disabled: true
}

Tom Select автоматически исключает такие элементы из выбора, но оставляет их видимыми, что важно для отображения занятости времени.