// ============================================================ // IconSelect: visual icon-grid selector (replaces inline. Popup is absolutely // positioned inside a wrapper that sits next to the trigger. // Works inside — dialog must have // overflow: visible. // ============================================================ const POPUP_CLASS = 'icon-select-popup'; let _globalListenerAdded = false; export function closeAllIconSelects() { document.querySelectorAll(`.${POPUP_CLASS}.open`).forEach(p => { p.classList.remove('open'); }); } function _ensureGlobalListener() { if (_globalListenerAdded) return; _globalListenerAdded = true; document.addEventListener('click', (e) => { if (!e.target.closest(`.${POPUP_CLASS}`) && !e.target.closest('.icon-select-trigger')) { closeAllIconSelects(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeAllIconSelects(); }); } export class IconSelect { constructor({ target, items, onChange, columns = 2, placeholder = '', horizontal = false }) { _ensureGlobalListener(); this._select = target; this._items = items; this._onChange = onChange; this._columns = columns; this._placeholder = placeholder; this._horizontal = horizontal; // Hide native select this._select.style.display = 'none'; // Trigger button (replaces select visually) this._trigger = document.createElement('button'); this._trigger.type = 'button'; this._trigger.className = 'icon-select-trigger'; this._trigger.addEventListener('click', (e) => { e.stopPropagation(); this._toggle(); }); this._select.parentNode.insertBefore(this._trigger, this._select.nextSibling); // Popup — absolutely positioned, appended to dialog (overflow:visible) // or body, escaping any scrollable ancestors this._popup = document.createElement('div'); this._popup.className = POPUP_CLASS; this._popup.addEventListener('click', (e) => e.stopPropagation()); this._popup.innerHTML = this._buildGrid(); const portal = this._select.closest('dialog') || document.body; portal.appendChild(this._popup); this._bindCells(); this._syncTrigger(); } _buildGrid() { const cells = this._items.map(item => `
${item.icon} ${item.label} ${item.desc ? `${item.desc}` : ''}
` ).join(''); const cls = 'icon-select-grid' + (this._horizontal ? ' icon-select-grid--horizontal' : ''); return `
${cells}
`; } _bindCells() { this._popup.querySelectorAll('.icon-select-cell').forEach(cell => { cell.addEventListener('click', () => { this.setValue(cell.dataset.value, true); this._popup.classList.remove('open'); }); }); } _syncTrigger() { const val = this._select.value; const item = this._items.find(i => i.value === val); if (item) { this._trigger.innerHTML = `${item.icon}` + `${item.label}` + ``; } else if (this._placeholder) { this._trigger.innerHTML = `${this._placeholder}` + ``; } this._popup.querySelectorAll('.icon-select-cell').forEach(cell => { cell.classList.toggle('active', cell.dataset.value === val); }); } _positionPopup() { // Get trigger position relative to the popup's offset parent // (the dialog or body). Use getBoundingClientRect for both and // compute the offset. const triggerRect = this._trigger.getBoundingClientRect(); const parentRect = this._popup.offsetParent ? this._popup.offsetParent.getBoundingClientRect() : { left: 0, top: 0 }; const relTop = triggerRect.bottom - parentRect.top; const relLeft = triggerRect.left - parentRect.left; const popupW = Math.max(triggerRect.width, 200); this._popup.style.left = relLeft + 'px'; this._popup.style.top = (relTop + 4) + 'px'; this._popup.style.width = popupW + 'px'; } _toggle() { const wasOpen = this._popup.classList.contains('open'); closeAllIconSelects(); if (!wasOpen) { this._positionPopup(); this._popup.classList.add('open'); } } setValue(value, fireChange = false) { this._select.value = value; this._syncTrigger(); if (fireChange) { this._select.dispatchEvent(new Event('change', { bubbles: true })); if (this._onChange) this._onChange(value); } } updateItems(items) { this._items = items; this._popup.innerHTML = this._buildGrid(); this._bindCells(); this._syncTrigger(); } destroy() { this._trigger.remove(); this._popup.remove(); this._select.style.display = ''; } }