Caret Position

В компоненте Tom Select управление кареткой (caret position) является ключевым механизмом, определяющим удобство ввода, редактирования и навигации внутри инпута. Каретка в данном контексте — это позиция текстового курсора внутри внутреннего input-элемента (control_input), который Tom Select использует для ввода поискового запроса или создания новых значений.

В отличие от обычного HTML input, где каретка управляется напрямую браузером, здесь добавляется слой логики: вставка тегов, фильтрация опций, управление выбранными элементами и динамическое изменение значения поля. Это делает позиционирование каретки зависимым от состояния компонента.


Внутренняя модель input и влияние на каретку

Tom Select использует скрытую структуру ввода:

  • control — контейнер всего компонента
  • control_input — реальный текстовый input
  • items — выбранные элементы (теги)
  • dropdown — список опций

Каретка существует только внутри control_input, но её поведение зависит от:

  • количества выбранных элементов
  • режима multiple или single
  • наличия placeholder
  • фильтрации результатов
  • состояния фокуса

Каждое изменение списка выбранных элементов может привести к пересозданию или перерасчёту input, что делает контроль позиции каретки нетривиальной задачей.


Базовое управление кареткой

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

select.setCaret(position);

Где position — индекс символа внутри строки input.

Пример установки каретки в конец строки

const select = new TomSelect("#select");

select.setTextboxValue("hello world");
select.setCaret(11);

После выполнения каретка окажется в конце строки.


Чтение и синхронизация позиции

Tom Select не всегда хранит явное состояние каретки, поэтому фактическое положение определяется через DOM-элемент:

const input = select.control_input;

const position = input.selectionStart;

Для диапазона выделения:

const start = input.selectionStart;
const end = input.selectionEnd;

Это важно при обработке пользовательского ввода, особенно при фильтрации или автодополнении.


Поведение при изменении значения input

При вызове:

select.setTextboxValue("abc");

происходит обновление DOM-значения, но каретка может:

  • остаться в прежней позиции
  • сместиться в конец строки
  • сброситься в 0 (в зависимости от браузера и состояния rerender)

Чтобы зафиксировать позицию, используется комбинация:

const pos = select.control_input.selectionStart;

select.setTextboxValue("abcdef");

select.setCaret(pos);

Однако это работает корректно только если новая строка длиннее или равна старой.


Влияние добавления и удаления items

В режиме multiple добавление элемента:

select.addItem("value");

приводит к:

  • пересозданию списка items
  • возможному изменению ширины input
  • перерасчёту позиции каретки

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

Решение — восстановление позиции:

const pos = select.control_input.selectionStart;

select.addItem("value");

select.setCaret(pos);

Каретка и backspace-логика

При удалении символов или элементов поведение каретки становится зависимым от контекста:

  • если input пуст — удаляется последний item
  • если курсор в начале строки — backspace может триггерить удаление тега
  • если курсор в середине строки — удаляется символ

Пример обработки:

select.on("keydown", (e) => {
    if (e.key === "Backspace") {
        const input = select.control_input;

        if (input.selectionStart === 0 && !input.value) {
            const lastItem = select.items[select.items.length - 1];
            select.removeItem(lastItem);
        }
    }
});

Потеря каретки при rerender

Некоторые действия приводят к полной пересборке input:

  • refreshOptions()
  • clearOptions()
  • изменение items
  • динамическое обновление render

В таких случаях DOM input может быть заменён, и каретка сбрасывается.

Типичный паттерн восстановления:

function preserveCaret(select, fn) {
    const input = select.control_input;
    const pos = input.selectionStart;

    fn();

    requestAnimationFrame(() => {
        select.setCaret(pos);
    });
}

Работа с IME и composition events

При вводе на языках с IME (например, китайский, японский, корейский) каретка может вести себя нестабильно из-за промежуточного состояния композиции.

Ключевые события:

  • compositionstart
  • compositionupdate
  • compositionend

Пример защиты:

let composing = false;

select.control_input.addEventListener("compositionstart", () => {
    composing = true;
});

select.control_input.addEventListener("compositionend", () => {
    composing = false;
});

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


Синхронизация каретки при фильтрации

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

Если пользователь вводит текст:

select.on("type", (query) => {
    const pos = select.control_input.selectionStart;

    select.refreshOptions(false);

    select.setCaret(pos);
});

Однако при быстром вводе возможна десинхронизация, поэтому часто применяется debounce:

let t;

select.on("type", () => {
    clearTimeout(t);

    t = setTimeout(() => {
        const pos = select.control_input.selectionStart;
        select.setCaret(pos);
    }, 50);
});

Особенности в single и multiple режимах

single

  • каретка всегда одна
  • input ведёт себя как обычный text field
  • смещение минимальное

multiple

  • каретка зависит от количества items
  • input может перемещаться между тегами
  • возможна «прыгающая» каретка при удалении элементов

В multiple-режиме важно учитывать ширину контейнера: при переполнении input переносится на новую строку, что визуально изменяет позицию каретки без изменения selectionStart.


Программное перемещение каретки

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

select.focus();

select.setTextboxValue("text");

select.setCaret(4);

Иногда требуется гарантировать фокус:

select.control_input.focus();
select.setCaret(select.control_input.value.length);

Взаимодействие с клавишами управления

Каретка тесно связана с обработкой клавиатуры:

  • ArrowLeft / ArrowRight — перемещение
  • Home / End — переход в границы строки
  • Enter — фиксация значения
  • Escape — сброс ввода

Пример кастомной логики:

select.on("keydown", (e) => {
    const input = select.control_input;

    if (e.key === "ArrowLeft" && input.selectionStart === 0) {
        select.setCaret(0);
    }

    if (e.key === "End") {
        select.setCaret(input.value.length);
    }
});

Влияние CSS и layout на визуальную позицию

Хотя каретка управляется логически через selectionStart, визуально она зависит от:

  • шрифта
  • line-height
  • padding input
  • flex-wrap контейнера
  • наличия tags (items)

Особенно критично в multi-line режимах: логическая позиция может не совпадать с визуальной.


Типичные ошибки при управлении кареткой

  • вызов setCaret до появления input в DOM
  • попытка восстановить позицию после полного rerender без задержки
  • игнорирование IME composition
  • отсутствие проверки selectionStart === null
  • управление кареткой без учета изменения длины строки

Надёжная стратегия контроля

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

  • чтение selectionStart до изменений
  • выполнение изменения состояния
  • восстановление через requestAnimationFrame
  • защита от composition
  • минимизация rerender
function safeUpdate(select, updateFn) {
    const input = select.control_input;
    const pos = input.selectionStart || 0;

    updateFn();

    requestAnimationFrame(() => {
        if (!document.activeElement === input) return;
        select.setCaret(pos);
    });
}