(function () { 'use strict'; let guildData = null; let guildDataPromise = null; let nextDropdownId = 0; async function fetchGuildData() { if (guildData) return guildData; if (guildDataPromise) return guildDataPromise; guildDataPromise = fetch('/api/discord/guild') .then(r => r.json()) .then(data => { guildData = data; return data; }) .catch(() => ({ channels: [], roles: [], members: [], categories: [] })); return guildDataPromise; } async function renderChannelSelect(el, currentValue, filter) { const data = await fetchGuildData(); const channels = filter ? data.channels.filter(filter) : data.channels; renderSmartSelect(el, channels.map(c => ({ id: c.id, label: `#${c.name}`, sub: c.parentId ? (data.categories.find(cat => cat.id === c.parentId)?.name || null) : null })), currentValue); } async function renderCategorySelect(el, currentValue) { const data = await fetchGuildData(); renderSmartSelect(el, data.categories.map(c => ({ id: c.id, label: c.name })), currentValue); } async function renderRoleSelect(el, currentValue) { const data = await fetchGuildData(); renderSmartSelect(el, data.roles.map(r => ({ id: r.id, label: `@${r.name}`, color: r.color })), currentValue); } async function renderMemberSelect(el, currentValue) { const data = await fetchGuildData(); renderSmartSelect(el, data.members.map(m => ({ id: m.id, label: m.displayName, sub: `@${m.username}`, avatar: m.avatar })), currentValue); } async function renderMultiMemberSelect(el, currentValue) { const data = await fetchGuildData(); const currentIds = (currentValue || '').split(',').map(s => s.trim()).filter(Boolean); renderMultiSelect(el, data.members.map(m => ({ id: m.id, label: m.displayName, sub: `@${m.username}`, avatar: m.avatar })), currentIds); } function buildOptionRow(opt, { selected = false } = {}) { const item = document.createElement('div'); item.className = 'ss-option' + (selected ? ' selected' : ''); if (opt.avatar) { const img = document.createElement('img'); img.className = 'ss-avatar'; img.src = opt.avatar; img.alt = ''; item.appendChild(img); } if (opt.color && opt.color !== '#000000') { const dot = document.createElement('span'); dot.className = 'ss-dot'; dot.style.background = opt.color; item.appendChild(dot); } const label = document.createElement('span'); label.className = 'ss-label'; label.textContent = opt.label; item.appendChild(label); if (opt.sub) { const sub = document.createElement('span'); sub.className = 'ss-sub'; sub.textContent = opt.sub; item.appendChild(sub); } return item; } function setDisplayValue(displayEl, opt) { displayEl.replaceChildren(); const labelSpan = document.createElement('span'); labelSpan.className = 'ss-label'; labelSpan.textContent = opt.label; const idSpan = document.createElement('span'); idSpan.className = 'ss-id'; idSpan.textContent = opt.id; displayEl.appendChild(labelSpan); displayEl.appendChild(idSpan); } function setDisplayPlaceholder(displayEl, text) { displayEl.replaceChildren(); const placeholder = document.createElement('span'); placeholder.className = 'ss-placeholder'; placeholder.textContent = text; displayEl.appendChild(placeholder); } function getFieldLabelText(inputEl) { const field = inputEl.closest ? inputEl.closest('.field') : null; const label = field ? field.querySelector('label') : null; const text = label && label.textContent ? label.textContent.trim() : ''; return text || (inputEl.dataset?.key || 'value'); } function createDropdown(options, opts) { const { multi = false, getCurrentId = () => null, isExcluded = () => false, onChoose, onClear } = opts; const listId = `ss-listbox-${++nextDropdownId}`; const dropdown = document.createElement('div'); dropdown.className = 'smart-select-dropdown hidden'; const search = document.createElement('input'); search.type = 'text'; search.placeholder = 'Search...'; search.className = 'ss-search'; search.setAttribute('aria-label', 'Search options'); search.setAttribute('aria-controls', listId); search.setAttribute('autocomplete', 'off'); const list = document.createElement('div'); list.className = 'ss-list'; list.id = listId; list.setAttribute('role', 'listbox'); if (multi) list.setAttribute('aria-multiselectable', 'true'); function getListOptions() { return Array.from(list.querySelectorAll('[role="option"]')); } function focusOption(index) { const opts = getListOptions(); if (opts.length === 0) return; const i = ((index % opts.length) + opts.length) % opts.length; opts[i].focus(); } list.addEventListener('keydown', (e) => { const opts = getListOptions(); if (opts.length === 0) return; const current = opts.indexOf(document.activeElement); if (e.key === 'ArrowDown') { e.preventDefault(); focusOption(current < 0 ? 0 : current + 1); } else if (e.key === 'ArrowUp') { e.preventDefault(); focusOption(current <= 0 ? opts.length - 1 : current - 1); } else if (e.key === 'Home') { e.preventDefault(); focusOption(0); } else if (e.key === 'End') { e.preventDefault(); focusOption(opts.length - 1); } else if (e.key === 'Enter' || e.key === ' ') { if (current >= 0) { e.preventDefault(); opts[current].click(); } } }); search.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); focusOption(0); } }); function renderOptions(filter = '') { list.replaceChildren(); if (!multi && onClear) { const clearOpt = document.createElement('div'); clearOpt.className = 'ss-option ss-clear'; clearOpt.setAttribute('role', 'option'); clearOpt.setAttribute('aria-selected', 'false'); clearOpt.setAttribute('tabindex', '-1'); clearOpt.textContent = 'Clear (not set)'; clearOpt.addEventListener('click', onClear); list.appendChild(clearOpt); } const lq = filter.toLowerCase(); const filtered = options.filter(o => { if (isExcluded(o.id)) return false; if (!filter) return true; if (multi) { return o.label.toLowerCase().includes(lq) || o.id.includes(filter); } return o.label.toLowerCase().includes(lq) || (o.sub || '').toLowerCase().includes(lq) || o.id.includes(filter); }); const currentId = getCurrentId(); for (const opt of filtered.slice(0, 50)) { const isCurrent = !multi && opt.id === currentId; const item = buildOptionRow(opt, { selected: isCurrent }); item.setAttribute('role', 'option'); item.setAttribute('aria-selected', String(isCurrent)); item.setAttribute('tabindex', '-1'); item.addEventListener('click', () => onChoose(opt)); list.appendChild(item); } } search.addEventListener('input', () => renderOptions(search.value)); dropdown.appendChild(search); dropdown.appendChild(list); renderOptions(); return { dropdown, search, list, renderOptions, listId }; } function renderSmartSelect(inputEl, options, currentValue) { const wrapper = document.createElement('div'); wrapper.className = 'smart-select'; const display = document.createElement('div'); display.className = 'smart-select-display'; display.setAttribute('role', 'combobox'); display.setAttribute('tabindex', '0'); display.setAttribute('aria-haspopup', 'listbox'); display.setAttribute('aria-expanded', 'false'); display.setAttribute('aria-label', getFieldLabelText(inputEl)); const current = options.find(o => o.id === currentValue); if (current) setDisplayValue(display, current); else setDisplayPlaceholder(display, 'Not set'); const ddApi = createDropdown(options, { multi: false, getCurrentId: () => inputEl.value, onChoose: (opt) => { inputEl.value = opt.id; setDisplayValue(display, opt); closeDropdown(true); inputEl.dispatchEvent(new Event('change')); }, onClear: () => { inputEl.value = ''; setDisplayPlaceholder(display, 'Not set'); closeDropdown(true); inputEl.dispatchEvent(new Event('change')); } }); display.setAttribute('aria-controls', ddApi.listId); function isOpen() { return !ddApi.dropdown.classList.contains('hidden'); } function openDropdown() { ddApi.dropdown.classList.remove('hidden'); display.setAttribute('aria-expanded', 'true'); ddApi.search.focus(); } function closeDropdown(focusTrigger = false) { ddApi.dropdown.classList.add('hidden'); display.setAttribute('aria-expanded', 'false'); if (focusTrigger) display.focus(); } display.addEventListener('click', () => { if (isOpen()) closeDropdown(); else openDropdown(); }); display.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { e.preventDefault(); if (isOpen()) ddApi.search.focus(); else openDropdown(); } }); ddApi.dropdown.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); closeDropdown(true); } else if (e.key === 'Tab') { // Don't trap — close so the dropdown doesn't linger while focus moves on. closeDropdown(); } }); document.addEventListener('click', (e) => { if (!wrapper.contains(e.target) && isOpen()) closeDropdown(); }); wrapper.appendChild(display); wrapper.appendChild(ddApi.dropdown); inputEl.style.display = 'none'; inputEl.parentNode.insertBefore(wrapper, inputEl.nextSibling); } function renderMultiSelect(inputEl, options, currentIds) { const wrapper = document.createElement('div'); wrapper.className = 'smart-select'; const selected = new Set(currentIds); const fieldLabel = getFieldLabelText(inputEl); function updateInput() { inputEl.value = [...selected].join(','); inputEl.dispatchEvent(new Event('change')); } const chipsEl = document.createElement('div'); chipsEl.className = 'ss-chips'; chipsEl.setAttribute('role', 'list'); chipsEl.setAttribute('aria-label', `${fieldLabel} (selected)`); function renderChips() { chipsEl.replaceChildren(); for (const id of selected) { const opt = options.find(o => o.id === id); const label = opt ? opt.label : id; const chip = document.createElement('button'); chip.type = 'button'; chip.className = 'ss-option ss-chip selected'; chip.textContent = label; chip.setAttribute('aria-label', `Remove ${label}`); chip.title = 'Click to remove'; chip.addEventListener('click', () => { selected.delete(id); renderChips(); updateInput(); }); chipsEl.appendChild(chip); } } const addBtn = document.createElement('div'); addBtn.className = 'smart-select-display'; addBtn.setAttribute('role', 'combobox'); addBtn.setAttribute('tabindex', '0'); addBtn.setAttribute('aria-haspopup', 'listbox'); addBtn.setAttribute('aria-expanded', 'false'); addBtn.setAttribute('aria-label', `Add ${fieldLabel}`); setDisplayPlaceholder(addBtn, '+ Add'); const ddApi = createDropdown(options, { multi: true, isExcluded: (id) => selected.has(id), onChoose: (opt) => { selected.add(opt.id); renderChips(); ddApi.renderOptions(ddApi.search.value); updateInput(); // Keep dropdown open so the user can add more; refocus list for keyboard flow. const first = ddApi.list.querySelector('[role="option"]'); if (first) first.focus(); } }); addBtn.setAttribute('aria-controls', ddApi.listId); function isOpen() { return !ddApi.dropdown.classList.contains('hidden'); } function openDropdown() { ddApi.dropdown.classList.remove('hidden'); addBtn.setAttribute('aria-expanded', 'true'); ddApi.search.focus(); } function closeDropdown(focusTrigger = false) { ddApi.dropdown.classList.add('hidden'); addBtn.setAttribute('aria-expanded', 'false'); if (focusTrigger) addBtn.focus(); } addBtn.addEventListener('click', () => { if (isOpen()) closeDropdown(); else openDropdown(); }); addBtn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { e.preventDefault(); if (isOpen()) ddApi.search.focus(); else openDropdown(); } }); ddApi.dropdown.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); closeDropdown(true); } else if (e.key === 'Tab') { closeDropdown(); } }); renderChips(); document.addEventListener('click', (e) => { if (!wrapper.contains(e.target) && isOpen()) closeDropdown(); }); wrapper.appendChild(chipsEl); wrapper.appendChild(addBtn); wrapper.appendChild(ddApi.dropdown); inputEl.style.display = 'none'; inputEl.parentNode.insertBefore(wrapper, inputEl.nextSibling); } window.DiscordFields = { fetchGuildData, renderChannelSelect, renderCategorySelect, renderRoleSelect, renderMemberSelect, renderMultiMemberSelect }; })();