function renderToast({ message, type = 'info', timeout = 3000 }) { if (!message) { console.warn('renderToast was called without a message.'); return; } // Auto-create the toast container if it doesn't exist let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; document.body.appendChild(container); } const toastId = `toast-${Date.now()}`; const wrapper = document.createElement('div'); wrapper.innerHTML = ` `; const toastElement = wrapper.firstElementChild; container.appendChild(toastElement); const toast = new bootstrap.Toast(toastElement, { delay: timeout }); toast.show(); toastElement.addEventListener('hidden.bs.toast', () => { toastElement.remove(); // Clean up container if empty if (container.children.length === 0) { container.remove(); } }); } const ComboBoxWidget = (() => { let tempIdCounter = 1; function createTempId(prefix = "temp") { return `${prefix}-${tempIdCounter++}`; } function createOption(text, value = null) { const option = document.createElement('option'); option.textContent = text; option.value = value ?? createTempId(); return option; } function sortOptions(selectElement) { const sorted = Array.from(selectElement.options) .sort((a, b) => a.text.localeCompare(b.text)); selectElement.innerHTML = ''; sorted.forEach(option => selectElement.appendChild(option)); } function handleComboAdd(inputId, listId, stateArray, label = 'entry') { const input = document.getElementById(inputId); const value = input.value.trim(); if (!value) { alert(`Please enter a ${label}.`); return; } const select = document.getElementById(listId); const exists = Array.from(select.options).some(opt => opt.textContent === value); if (exists) { alert(`${label.charAt(0).toUpperCase() + label.slice(1)} "${value}" already exists.`); return; } const option = createOption(value); // Already built to handle temp IDs select.add(option); formState[stateArray].push(value); input.value = ''; } function initComboBox(ns, config = {}) { const input = document.querySelector(`#${ns}-input`); const list = document.querySelector(`#${ns}-list`); const addBtn = document.querySelector(`#${ns}-add`); const removeBtn = document.querySelector(`#${ns}-remove`); let currentlyEditing = null; if (!input || !list || !addBtn || !removeBtn) { console.warn(`ComboBoxWidget: Missing elements for namespace '${ns}'`); return; } function updateAddButtonIcon() { const iconEl = addBtn.querySelector('.icon-state'); const iconClass = currentlyEditing ? 'bi-pencil' : 'bi-plus-lg'; iconEl.classList.forEach(cls => { if (cls.startsWith('bi-') && cls !== 'icon-state') { iconEl.classList.remove(cls); } }); iconEl.classList.add(iconClass); } input.addEventListener('input', () => { addBtn.disabled = input.value.trim() === ''; updateAddButtonIcon(); }); list.addEventListener('change', () => { const selected = list.selectedOptions; removeBtn.disabled = selected.length === 0; if (selected.length === 1) { input.value = selected[0].textContent.trim(); currentlyEditing = selected[0]; addBtn.disabled = input.value.trim() === ''; } else { input.value = ''; currentlyEditing = null; addBtn.disabled = true; } updateAddButtonIcon(); }); addBtn.addEventListener('click', () => { const newItem = input.value.trim(); if (!newItem) return; if (currentlyEditing) { if (config.onEdit) { config.onEdit(currentlyEditing, newItem); } else { currentlyEditing.textContent = newItem; } currentlyEditing = null; } else { if (config.onAdd) { config.onAdd(newItem, list, createOption); return; // Skip the default logic! } const exists = Array.from(list.options).some(opt => opt.textContent === newItem); if (exists) { alert(`"${newItem}" already exists.`); return; } const option = createOption(newItem); list.appendChild(option); const key = config.stateArray ?? `${ns}s`; // fallback to pluralization if (Array.isArray(formState?.[key])) { formState[key].push({ name: newItem }); } if (config.sort !== false) { sortOptions(list); } } input.value = ''; addBtn.disabled = true; removeBtn.disabled = true; updateAddButtonIcon(); }); removeBtn.addEventListener('click', () => { Array.from(list.selectedOptions).forEach(option => { if (config.onRemove) { config.onRemove(option); } option.remove(); }); currentlyEditing = null; input.value = ''; addBtn.disabled = true; removeBtn.disabled = true; updateAddButtonIcon(); }); } return { initComboBox, createOption, sortOptions, handleComboAdd, createTempId }; })();