VGSelect

Стилизованный HTML Select

API, события и оформление

Жизненный цикл, callbacks, DOM-события и публичные CSS-переменные.

VGSelect сохраняет исходный <select> для формы и строит интерфейс рядом с ним. Каждый пример запускается один раз после появления HTML. Параметры из data-* читаются при инициализации; библиотека сама не сканирует страницу в поисках полей. По умолчанию модуль использует position: 'auto': список выбирает сторону с большим свободным местом.

Методы, callbacks и DOM-события

Установите значение, откройте список, уничтожьте интерфейс и создайте его заново. После destroy остаётся обычный select. Журнал ограничен последними 12 сообщениями.


HTML
<div id="select-api-host" class="select-demo">
  <label for="select-api">Режим обслуживания</label>
  <select id="select-api" class="vg-select" data-select-demo>
    <option value="standard" selected>Стандартный</option>
    <option value="express">Экспресс</option>
    <option value="extended">Расширенный</option>
  </select>
  <div class="select-demo-actions">
    <button type="button" class="btn btn-primary btn-sm" data-select-command="choose">Выбрать «Экспресс»</button>
    <button type="button" class="btn btn-surface btn-sm" data-select-command="toggle">Открыть / закрыть</button>
    <button type="button" class="btn btn-surface btn-sm" data-select-command="destroy">Уничтожить</button>
    <button type="button" class="btn btn-surface btn-sm" data-select-command="init">Инициализировать</button>
  </div>
  <pre id="select-api-log" class="select-demo-log" aria-live="polite"></pre>
</div>
JavaScript
/**
 * Описание: рабочий пример VGSelect — api.
 * Возможности: инициализация и демонстрация публичного контракта на исходном select.
 */
import {VGSelect} from 'vgapp';

function initSelectDemo() {
    const host = document.querySelector('#select-api-host');
    if (!host || host.dataset.ready === 'true') return;
    host.dataset.ready = 'true';
    const select = host.querySelector('select');
    const log = host.querySelector('#select-api-log');
    const report = message => { log.textContent = (message + '\n' + log.textContent).split('\n').slice(0, 12).join('\n'); };
    ['init', 'show', 'shown', 'hide', 'hidden', 'select', 'change', 'error'].forEach(name => {
        host.addEventListener('vg.select.' + name, event => {
            report('DOM ' + name + ': ' + JSON.stringify(event.detail || {}));
        });
    });
    select.addEventListener('change', () => report('native change: ' + select.value));
    const init = () => {
        VGSelect.init(select, {
            onInit() { report('callback onInit'); },
            onSelect(wrapper, data) { report('callback onSelect: ' + data.value); }
        });
    };
    init();
    host.querySelectorAll('[data-select-command]').forEach(button => {
        button.addEventListener('click', () => {
            const command = button.dataset.selectCommand;
            if (command === 'init') init();
            if (command === 'destroy') {
                VGSelect.destroy(select);
                report('destroy: доступен нативный select');
            }
            if (command === 'choose') VGSelect.changeSelector(select, 'express');
            if (command === 'toggle') {
                const instance = VGSelect.getInstance(select.nextElementSibling);
                if (instance) instance.toggle();
                else report('Сначала нажмите «Инициализировать».');
            }
        });
    });
}

initSelectDemo();
CSS
.select-demo {
    display: grid;
    gap: 12px;
    max-width: 560px;
    min-width: 0;
}
.select-demo-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
.select-demo output {
    overflow-wrap: anywhere;
}
.select-demo-log {
    max-height: 240px;
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    padding: 12px;
    border: 1px solid var(--vg-border-default);
    border-radius: var(--vg-radius-md);
    background: var(--vg-surface-muted-bg);
    color: var(--vg-text-primary);
}
.select-demo-placement {
    padding-top: 180px;
}
.vg-select.select-demo-compact {
    --vg-select-current-font-size: 14px;
    --vg-select-current-padding-top: 8px;
    --vg-select-current-padding-bottom: 8px;
    --vg-select-current-border-radius: 10px;
    --vg-select-list-hover-background-color: var(--vg-surface-muted-bg);
}
.module-select-content {
    min-width: 0;
}
.module-select-reference {
    margin-top: 32px;
    overflow-wrap: anywhere;
}
.module-select-reference dt {
    margin-top: 16px;
    font-weight: 600;
}
.module-select-reference dd {
    margin: 6px 0 0;
}

Выбор по внешней ссылке

Нажмите «Оранжевый»: ссылка найдёт связанный select в document по data-select-target и выберет option по value. Ссылка не обязана находиться рядом с полем.

Быстрый выбор цвета: Оранжевый

Значение: green
HTML
<div class="select-demo">
  <p>Быстрый выбор цвета:
    <a href="#select-linked-color" data-select-target="#select-linked-color" data-select-value="orange">Оранжевый</a>
  </p>
  <div>
    <label for="select-linked-color">Цвет</label>
    <select id="select-linked-color" class="vg-select" name="color" data-select-demo>
      <option value="green" selected>Зелёный</option>
      <option value="orange">Оранжевый</option>
      <option value="blue">Синий</option>
    </select>
  </div>
  <output id="select-linked-value" aria-live="polite">Значение: green</output>
</div>
JavaScript
/**
 * Описание: выбор значения VGSelect по внешней ссылке.
 * Возможности: поиск связанного select в document по data-select-target и выбор по value.
 */
import {VGSelect} from 'vgapp';

function initSelectDemo() {
    const select = document.querySelector('#select-linked-color');
    if (!select || select.dataset.inited === 'true') return;
    VGSelect.init(select);
    select.addEventListener('change', () => {
        document.querySelector('#select-linked-value').textContent = 'Значение: ' + select.value;
    });
    document.querySelectorAll('a[data-select-target="#select-linked-color"]').forEach(link => {
        link.addEventListener('click', event => {
            event.preventDefault();
            // Ссылка и поле могут находиться в разных частях страницы.
            const target = document.querySelector(link.dataset.selectTarget);
            if (!(target instanceof HTMLSelectElement) || target.disabled) return;
            VGSelect.changeSelector(target, link.dataset.selectValue);
        });
    });
}

initSelectDemo();
CSS
.select-demo {
    display: grid;
    gap: 12px;
    max-width: 560px;
    min-width: 0;
}
.select-demo-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
.select-demo output {
    overflow-wrap: anywhere;
}
.select-demo-log {
    max-height: 240px;
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    padding: 12px;
    border: 1px solid var(--vg-border-default);
    border-radius: var(--vg-radius-md);
    background: var(--vg-surface-muted-bg);
    color: var(--vg-text-primary);
}
.select-demo-placement {
    padding-top: 180px;
}
.vg-select.select-demo-compact {
    --vg-select-current-font-size: 14px;
    --vg-select-current-padding-top: 8px;
    --vg-select-current-padding-bottom: 8px;
    --vg-select-current-border-radius: 10px;
    --vg-select-list-hover-background-color: var(--vg-surface-muted-bg);
}
.module-select-content {
    min-width: 0;
}
.module-select-reference {
    margin-top: 32px;
    overflow-wrap: anywhere;
}
.module-select-reference dt {
    margin-top: 16px;
    font-weight: 600;
}
.module-select-reference dd {
    margin: 6px 0 0;
}

Компактный размер и направление вверх

Размер задают публичные CSS-переменные, data-position="top" открывает список вверх. Проверьте переключатель темы в шапке: состояние темы принадлежит OKAUX.

HTML
<div class="select-demo select-demo-placement">
  <label for="select-theme">Компактный список, направление вверх</label>
  <select id="select-theme" class="vg-select select-demo-compact" data-select-demo data-position="top" data-lang="en" data-search-enabled="true">
    <option value="light" selected>Lightweight package</option>
    <option value="standard">Standard package</option>
    <option value="full">Full package</option>
  </select>
</div>
JavaScript
/**
 * Описание: рабочий пример VGSelect — theme.
 * Возможности: инициализация и демонстрация публичного контракта на исходном select.
 */
import {VGSelect} from 'vgapp';

function initSelectDemo() {
    const select = document.querySelector('#select-theme');
    if (!select || select.dataset.inited === 'true') return;
    VGSelect.init(select); // Цвета наследуются от общего адаптера vgapp/theme.
}

initSelectDemo();
CSS
.select-demo {
    display: grid;
    gap: 12px;
    max-width: 560px;
    min-width: 0;
}
.select-demo-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
.select-demo output {
    overflow-wrap: anywhere;
}
.select-demo-log {
    max-height: 240px;
    overflow: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    padding: 12px;
    border: 1px solid var(--vg-border-default);
    border-radius: var(--vg-radius-md);
    background: var(--vg-surface-muted-bg);
    color: var(--vg-text-primary);
}
.select-demo-placement {
    padding-top: 180px;
}
.vg-select.select-demo-compact {
    --vg-select-current-font-size: 14px;
    --vg-select-current-padding-top: 8px;
    --vg-select-current-padding-bottom: 8px;
    --vg-select-current-border-radius: 10px;
    --vg-select-list-hover-background-color: var(--vg-surface-muted-bg);
}
.module-select-content {
    min-width: 0;
}
.module-select-reference {
    margin-top: 32px;
    overflow-wrap: anywhere;
}
.module-select-reference dt {
    margin-top: 16px;
    font-weight: 600;
}
.module-select-reference dd {
    margin: 6px 0 0;
}

Параметры и методы

VGSelect.init(select, params = {}, isRebuild = false)
Строит интерфейс; возвращает undefined. Повторный init пересоздаёт интерфейс. Экземпляр: VGSelect.getInstance(select.nextElementSibling), не getInstance(select).
Data API
data-placeholder, data-placeholder-value (значения через запятую), data-autosearch, data-search-enabled, data-search-remote, data-search-route, data-search-minterm, data-search-perpage, data-search-pagination, data-close, data-tree, data-position, data-lang. Нужен явный init исходного select.
Поиск
autosearch: true — порог 7; число — свой порог; false — отключить автоматику. search.enabled / search.remote включают поиск независимо от порога. search.delay: 300, minterm: 1, perpage: 20. Для AJAX используйте GET.
Направление
position: auto (по умолчанию), none, top, bottom. auto выбирает сторону с большим свободным местом в ближайшем overflow-контейнере или viewport. none оставляет позиционирование CSS. Выпадающий список не переносится в body.
Выбор
VGSelect.changeSelector(select, value, data); для multiple передайте {selected: false}, чтобы снять выбор. changeSelectorByIndex(select, index, data) подходит для option без value. updateUI(select) синхронизирует интерфейс после прямой смены selected.
Данные
VGSelect.addOptions(select, arrayOrResults, {preserve: false}) заменяет обычные опции, сохраняя пустые и data-preserve. preserve: true добавляет данные без дедупликации. Поддерживаются id, text, selected, disabled, children; дополнительные поля становятся data-*.
Жизненный цикл
instance.show(), hide(), toggle(); VGSelect.destroy(select) освобождает экземпляр и восстанавливает нативное поле. Для новой конфигурации вызывайте init повторно. MutationObserver видит DOM-изменения, но не присваивание select.value.
Локализация
lang берётся из html[lang], по умолчанию ru. Например, lang: 'en' меняет подписи поиска и загрузки. Placeholder передавайте через data-placeholder на select.

События и callbacks

Исходный select
Нативный change и vg.select.change. Значения формы читайте через value / selectedOptions. Для программного выбора vg.select.change отправляется только при выборе ранее не выбранной опции.
Обёртка
vg.select.init, show, shown, open, hide, hidden, close, select, deselect, clear, search, rebuild, loadNext, error. События всплывают; данные находятся в event.detail. show / hide можно отменить через preventDefault().
Callbacks
onInit, onShow, onHide, onSelect, onDeselect, onClear, onSearch, onLoadNext получают (wrapper, data), this — экземпляр. onSearch вызывается при вводе и после ответа, payload различается. У onChange есть параметр по умолчанию, но вызова в текущем исходнике нет: используйте change. onClear относится к удалению последнего тега multiple.
Границы контракта
Не обещаем полный ARIA listbox: текущая реализация поддерживает открытие Enter / Space / ArrowDown, но не полноценную клавиатурную навигацию по опциям. У пустого AJAX-ответа нет встроенного текстового сообщения; его показывает журнал примера.

CSS-переменные

Текущее значение
--vg-select-current-background-color, color, border-width/style/color/radius, padding-left/right/top/bottom, font-size, line-height. Placeholder: --vg-select-current-placehoder-color (написание в API именно placehoder).
Выпадающий список
--vg-select-dropdown-background-color, color, border-*, box-shadow, z-index; --vg-select-list-max-height, scrollbar-width/bg/thumb. При position не равном none высотой списка управляет JS.
Опции и группы
--vg-select-list-background-color/color/padding-*, --vg-select-list-hover-background-color/color/border-bottom-color, --vg-select-optgroup-color/font-weight/padding-*.
Поиск и теги
--vg-select-search-background-color/color; --vg-select-tags-gap/min-height/padding; --vg-select-tag-background/color/padding/border-radius/font-size/remove-width/remove-height.
Тема
Демо использует установленный OKAUX 1.0.2. Общий адаптер vgapp/theme уже подключён к [data-theme="light"] / [data-theme="dark"]. Отдельное хранилище темы не создаётся.