// ── Voice Picker ────────────────────────────────────────────────────────── // Replaces
`; root.appendChild(drop); const searchInp = drop.querySelector('.vp-search'); const list = drop.querySelector('.vp-list'); function _renderList(filter) { const opts = Array.from(sel.options).filter(o => o.value); const q = (filter || '').toLowerCase(); const filtered = q ? opts.filter(o => o.value.toLowerCase().includes(q) || o.textContent.toLowerCase().includes(q)) : opts; if (!filtered.length) { list.innerHTML = '
No voices found
'; return; } list.innerHTML = filtered.map(o => _buildItem(o.value, o.textContent)).join(''); // highlight current list.querySelectorAll('.vp-item').forEach(el => { el.classList.toggle('selected', el.dataset.value === sel.value); }); } function _open() { drop.hidden = false; drop.style.display = ''; trigger.classList.add('open'); searchInp.value = ''; _renderList(''); searchInp.focus(); // scroll selected into view requestAnimationFrame(() => { const sel2 = list.querySelector('.vp-item.selected'); if (sel2) sel2.scrollIntoView({ block: 'nearest' }); }); } function _close() { drop.hidden = true; drop.style.display = 'none'; trigger.classList.remove('open'); searchInp.blur(); } function _select(value) { sel.value = value; sel.dispatchEvent(new Event('change', { bubbles: true })); _syncTrigger(); _close(); trigger.focus(); } function _syncTrigger() { const v = sel.value; const content = trigger.querySelector('.vp-trigger-content'); if (!v) { content.innerHTML = '— select after fetch —'; } else { const label = v; const meta = _voiceMetaLabel(v); content.innerHTML = `${_avatarHtml(v, 24)}${_esc(label)}${meta ? `${_esc(meta)}` : ''}`; } } trigger.addEventListener('click', () => drop.hidden ? _open() : _close()); trigger.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _open(); } }); searchInp.addEventListener('input', () => _renderList(searchInp.value)); searchInp.addEventListener('keydown', e => { if (e.key === 'Escape') { e.stopPropagation(); _close(); trigger.focus(); } if (e.key === 'ArrowDown') { e.preventDefault(); list.querySelector('.vp-item')?.focus(); } }); list.addEventListener('pointerdown', e => { const item = e.target.closest('.vp-item'); if (!item) return; e.preventDefault(); _select(item.dataset.value); }); list.addEventListener('click', e => { const item = e.target.closest('.vp-item'); if (item) _select(item.dataset.value); }); list.addEventListener('keydown', e => { const item = e.target.closest('.vp-item'); if (!item) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _select(item.dataset.value); } if (e.key === 'ArrowDown') { e.preventDefault(); (item.nextElementSibling || item).focus(); } if (e.key === 'ArrowUp') { e.preventDefault(); (item.previousElementSibling || item)?.focus() || searchInp.focus(); } if (e.key === 'Escape') { _close(); trigger.focus(); } }); // Close on outside click document.addEventListener('click', e => { if (!root.contains(e.target)) _close(); }, true); // Any real select change means the user picked something; close the popup. sel.addEventListener('change', () => { _syncTrigger(); _close(); }); // Watch for programmatic changes to the underlying select const mo = new MutationObserver(() => { _syncTrigger(); }); mo.observe(sel, { childList: true, attributes: true, subtree: true }); _pickers[selectId] = { root, trigger, drop, searchInp, list, sel, renderList: _renderList, syncTrigger: _syncTrigger }; } function populate(selectId, voices) { const p = _pickers[selectId]; if (!p) return; const { sel, renderList, syncTrigger } = p; const prev = sel.value; sel.innerHTML = ''; voices.forEach(id => { const opt = document.createElement('option'); opt.value = id; opt.textContent = _voiceOptionLabel(id); sel.appendChild(opt); }); if (prev && voices.includes(prev)) sel.value = prev; renderList(''); syncTrigger(); } function getValue(selectId) { return document.getElementById(selectId)?.value || ''; } function setValue(selectId, value) { const sel = document.getElementById(selectId); if (!sel) return; sel.value = value; _pickers[selectId]?.syncTrigger(); } window.VoicePicker = { upgrade, populate, getValue, setValue }; })();