209 lines
6.8 KiB
JavaScript
209 lines
6.8 KiB
JavaScript
const ToastConfig = {
|
|
containerId: 'toast-container',
|
|
positionClasses: 'toast-container position-fixed bottom-0 end-0 p-3',
|
|
defaultType: 'info',
|
|
defaultTimeout: 3000
|
|
};
|
|
|
|
function updateToastConfig(overrides = {}) {
|
|
Object.assign(ToastConfig, overrides);
|
|
}
|
|
|
|
function renderToast({ message, type = ToastConfig.defaultType, timeout = ToastConfig.defaultTimeout }) {
|
|
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(ToastConfig.containerId);
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = ToastConfig.containerId;
|
|
container.className = ToastConfig.positionClasses;
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
const toastId = `toast-${Date.now()}`;
|
|
const wrapper = document.createElement('div');
|
|
wrapper.innerHTML = `
|
|
<div id="${toastId}" class="toast align-items-center text-bg-${type}" role="alert" aria-live="assertive" aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
${message}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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
|
|
};
|
|
})();
|