/** * Одиночний select із пошуком по введенню тексту — заміна звичайного * * * * * * * Використання: initSearchableSelects(scopeEl) після вставки розмітки * в DOM (в т.ч. після HTMX-свапу фрагмента). */ (function () { function options(select) { return Array.prototype.slice.call(select.options).filter(function (o) { return o.value; }); } function currentLabel(select) { var opt = select.options[select.selectedIndex]; return opt && opt.value ? opt.textContent : ''; } /** * Латиниця ↔ кирилиця для кодів аптек: a/а, p/п, t/т, g/г, l/л. * Нормалізуємо і запит, і текст опції в один алфавіт (кирилиця). */ var LOOKALIKE_TO_CYR = { a: 'а', A: 'а', p: 'п', P: 'п', t: 'т', T: 'т', g: 'г', G: 'г', l: 'л', L: 'л', а: 'а', А: 'а', п: 'п', П: 'п', т: 'т', Т: 'т', г: 'г', Г: 'г', л: 'л', Л: 'л' }; function foldLookalikes(text) { return String(text || '').replace(/[aApPtTgGlLаАпПтТгГлЛ]/g, function (ch) { return LOOKALIKE_TO_CYR[ch] || ch; }).toLowerCase(); } function optionHaystack(opt) { var extra = opt.getAttribute('data-search') || ''; return foldLookalikes(opt.textContent + ' ' + extra); } function renderResults(widget, query) { var select = widget._select; var results = widget._results; var q = foldLookalikes(query.trim()); // також без пробілів: «а 3» → «а3» var qCompact = q.replace(/\s+/g, ''); // Без лимита: иначе при пустом запросе видны только первые ~50 опций // (аптеки «ниже по списку» пропадали). Скролл в dropdown справляется. var matches = options(select).filter(function (o) { if (!q) return true; var hay = optionHaystack(o); var hayCompact = hay.replace(/\s+/g, ''); return hay.indexOf(q) !== -1 || (qCompact && hayCompact.indexOf(qCompact) !== -1); }); results.innerHTML = ''; matches.forEach(function (opt) { var row = document.createElement('div'); row.className = 'people-picker-row'; if (opt.value === select.value) row.classList.add('is-selected'); var info = document.createElement('div'); info.className = 'people-picker-row-info'; var name = document.createElement('div'); name.className = 'people-picker-row-name'; name.textContent = opt.textContent; info.appendChild(name); row.appendChild(info); row.addEventListener('mousedown', function (e) { e.preventDefault(); select.value = opt.value; select.dispatchEvent(new Event('change')); widget._search.value = opt.textContent; widget._dropdown.style.display = 'none'; updateClearButton(widget); }); results.appendChild(row); }); if (!matches.length) { var empty = document.createElement('div'); empty.className = 'people-picker-row'; empty.style.cursor = 'default'; empty.style.color = 'var(--color-muted-foreground)'; empty.textContent = 'Нічого не знайдено'; results.appendChild(empty); } } function updateClearButton(widget) { widget._clear.style.display = widget._select.value ? 'flex' : 'none'; } function build(widget) { var select = widget.querySelector('select'); if (!select) return; widget._select = select; var searchWrap = document.createElement('div'); searchWrap.style.position = 'relative'; var search = document.createElement('input'); search.type = 'text'; search.className = 'input searchable-select-search'; search.placeholder = widget.dataset.placeholder || 'Пошук...'; search.value = currentLabel(select); search.autocomplete = 'off'; // required на hidden-select браузер ігнорує (barred from constraint // validation) — переносимо на видимий інпут, щоб підказка лишилась if (select.required) search.required = true; widget._search = search; var clearBtn = document.createElement('button'); clearBtn.type = 'button'; clearBtn.className = 'searchable-select-clear'; clearBtn.innerHTML = '×'; clearBtn.style.display = select.value ? 'flex' : 'none'; clearBtn.setAttribute('aria-label', 'Очистити'); clearBtn.addEventListener('mousedown', function (e) { e.preventDefault(); select.value = ''; select.dispatchEvent(new Event('change')); search.value = ''; clearBtn.style.display = 'none'; search.focus(); }); widget._clear = clearBtn; var dropdown = document.createElement('div'); dropdown.className = 'people-picker-dropdown'; dropdown.style.display = 'none'; widget._dropdown = dropdown; var results = document.createElement('div'); results.className = 'people-picker-results'; dropdown.appendChild(results); widget._results = results; function open() { dropdown.style.display = 'block'; renderResults(widget, search.value === currentLabel(select) ? '' : search.value); } search.addEventListener('focus', function () { search.select(); open(); }); search.addEventListener('input', function () { renderResults(widget, search.value); dropdown.style.display = 'block'; }); search.addEventListener('keydown', function (e) { if (e.key === 'Escape') { dropdown.style.display = 'none'; search.value = currentLabel(select); search.blur(); return; } // Enter у полі пошуку не повинен сабмітити форму — обираємо перший результат if (e.key === 'Enter') { e.preventDefault(); var first = results.querySelector('.people-picker-row:not([style*="cursor: default"])'); if (first) first.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); } }); search.addEventListener('blur', function () { // Якщо користувач не обрав опцію — повертаємо текст обраного значення setTimeout(function () { if (dropdown.style.display === 'none') search.value = currentLabel(select); }, 150); }); document.addEventListener('click', function (e) { if (!widget.contains(e.target)) { dropdown.style.display = 'none'; search.value = currentLabel(select); } }); searchWrap.appendChild(search); searchWrap.appendChild(clearBtn); searchWrap.appendChild(dropdown); widget.appendChild(searchWrap); } window.initSearchableSelects = function (scope) { (scope || document).querySelectorAll('.searchable-select:not([data-searchable-init])').forEach(function (widget) { widget.setAttribute('data-searchable-init', '1'); build(widget); }); }; })();