/** * Initializes MultiSelect.js instances from select[multiple] elements. */ import { warnOnce, resolveHost } from '../core/app-dom.js'; let multiSelectLoader = null; const resolveScriptUrl = (script) => { if (script) { return script; } const root = document.documentElement; const assetBase = String(root?.dataset?.assetBase || '').trim() || document.baseURI; return new URL('vendor/multi-select/MultiSelect.js', assetBase).toString(); }; const ensureMultiSelect = async (script) => { if (typeof window.MultiSelect !== 'undefined') { return window.MultiSelect; } if (!multiSelectLoader) { multiSelectLoader = new Promise((resolve, reject) => { const tag = document.createElement('script'); tag.src = resolveScriptUrl(script); tag.async = true; tag.onload = () => resolve(window.MultiSelect); tag.onerror = (event) => reject(event); document.head.appendChild(tag); }); } return multiSelectLoader; }; const resolveElements = (target, host) => { if (typeof target === 'string') { return Array.from(host.querySelectorAll(target)); } if (target instanceof Element) { return [target]; } if (target && typeof target.length === 'number') { return Array.from(target); } return []; }; const destroyElementInstance = (element, instance = null) => { if (!(element instanceof HTMLElement)) { return; } const cleanup = element._multiSelectCleanup; if (typeof cleanup === 'function') { cleanup(); } delete element._multiSelectCleanup; delete element.dataset.multiSelectInit; const resolvedInstance = instance || element._multiSelectInstance || null; if (resolvedInstance && typeof resolvedInstance.destroy === 'function') { resolvedInstance.destroy(); } delete element._multiSelectInstance; }; export const initMultiSelect = async (target = '[data-multi-select]', root = document) => { const host = resolveHost(root); const elements = resolveElements(target, host); if (!elements.length) { return []; } const MultiSelect = await ensureMultiSelect(); const instances = []; elements.forEach((element) => { if (!(element instanceof HTMLElement) || element.dataset.multiSelectInit === '1') { return; } const cleanupFns = []; const syncTarget = element.dataset.syncTarget; const syncInput = syncTarget ? document.querySelector(syncTarget) : null; const options = {}; const placeholder = element.dataset.placeholder; if (placeholder) { options.placeholder = placeholder; } if (element.dataset.search !== undefined) { options.search = element.dataset.search === 'true'; } if (element.dataset.selectAll !== undefined) { options.selectAll = element.dataset.selectAll === 'true'; } if (element.dataset.disabled !== undefined) { options.disabled = element.dataset.disabled === 'true'; } if (element.disabled) { options.disabled = true; } if (element.dataset.listAll !== undefined) { options.listAll = element.dataset.listAll === 'true'; } if (element.dataset.selectedLabel) { options.selectedLabel = element.dataset.selectedLabel; } if (element.dataset.searchPlaceholder) { options.searchPlaceholder = element.dataset.searchPlaceholder; } if (element.dataset.selectAllLabel) { options.selectAllLabel = element.dataset.selectAllLabel; } let isSyncing = false; let instance; try { instance = new MultiSelect(element, options); } catch (error) { warnOnce('UI_INIT_FAIL', 'multi-select init failed', { module: 'multi-select', element, error }); return; } if (element.dataset.disabled === 'true' || element.disabled) { instance.disable(); } element.dataset.multiSelectInit = '1'; element._multiSelectInstance = instance; instances.push(instance); if (syncInput instanceof HTMLInputElement) { syncInput._multiSelectInstance = instance; const syncValues = () => { if (isSyncing) { return; } isSyncing = true; const values = instance.selectedValues || []; syncInput.value = Array.isArray(values) ? values.join(',') : ''; syncInput.dispatchEvent(new Event('change', { bubbles: true })); isSyncing = false; }; let boundElement = null; const bindSyncListener = () => { if (!instance?.element || instance.element === boundElement) { return; } instance.element.addEventListener('multi-select:change', syncValues); cleanupFns.push(() => instance.element?.removeEventListener('multi-select:change', syncValues)); boundElement = instance.element; }; bindSyncListener(); if (typeof instance.refresh === 'function' && instance._syncRefreshWrapped !== true) { const originalRefresh = instance.refresh.bind(instance); instance.refresh = (...args) => { const result = originalRefresh(...args); bindSyncListener(); return result; }; instance._syncRefreshWrapped = true; } const values = instance.selectedValues || []; syncInput.value = Array.isArray(values) ? values.join(',') : ''; const onSyncInputChange = () => { if (isSyncing) { return; } const nextValues = syncInput.value ? syncInput.value.split(',').map((item) => item.trim()).filter(Boolean) : []; instance.setValues(nextValues); }; syncInput.addEventListener('change', onSyncInputChange); cleanupFns.push(() => syncInput.removeEventListener('change', onSyncInputChange)); } element._multiSelectCleanup = () => { cleanupFns.forEach((cleanup) => cleanup()); cleanupFns.length = 0; if (syncInput instanceof HTMLInputElement && syncInput._multiSelectInstance === instance) { delete syncInput._multiSelectInstance; } }; }); return instances; }; export const getMultiSelectInstance = (target, root = document) => { const host = resolveHost(root); const element = typeof target === 'string' ? host.querySelector(target) : target; if (!element) { return null; } if (element._multiSelectInstance) { return element._multiSelectInstance; } const syncTarget = element.dataset?.syncTarget; if (syncTarget) { const syncElement = document.querySelector(syncTarget); if (syncElement && syncElement._multiSelectInstance) { return syncElement._multiSelectInstance; } } return null; }; export function initMultiSelectComponent(root = document, options = {}) { const selector = String(options.selector || '[data-multi-select]').trim() || '[data-multi-select]'; const host = resolveHost(root); let destroyed = false; let instances = []; initMultiSelect(selector, host) .then((initialized) => { if (destroyed) { initialized.forEach((instance) => { const element = instance?.element; if (element instanceof HTMLElement) { destroyElementInstance(element, instance); } }); return; } instances = initialized; }) .catch((error) => { warnOnce('UI_INIT_FAIL', 'multi-select runtime init failed', { module: 'multi-select', error }); }); return { destroy: () => { destroyed = true; instances.forEach((instance) => { const element = instance?.element; if (element instanceof HTMLElement) { destroyElementInstance(element, instance); } }); instances = []; }, }; }