Files
breadcrumb-the-shire/web/js/components/app-multiselect-init.js
fs d9f07dcd63 fix: web/ quick wins — CSS cleanup, ARIA tabs, and file header comments
CSS:
- Remove duplicate li::before block in app-search.css
- Fix typo: search-reuslt → search-result in app-search.css
- Remove commented-out CSS rules in app-flash.css
- Add descriptive header comment to all 27 CSS component files

JS:
- Complete WAI-ARIA Tabs pattern: generate IDs, add aria-controls on
  tab buttons and aria-labelledby on tab panels (app-tabs.js)
- Add JSDoc header comments to ~33 JS files (core + components)
- Add explanatory block comment to app-boot.js (why classic IIFE)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 22:21:37 +01:00

147 lines
5.1 KiB
JavaScript

/**
* Initializes MultiSelect.js instances from select[multiple] elements.
*/
import { warnOnce } from '../core/app-dom.js';
let multiSelectLoader = null;
const resolveScriptUrl = (script) => {
if (script) {return script;}
const assetBase = window.APP_ASSET_BASE || 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;
};
export const initMultiSelect = async (target = '[data-multi-select]') => {
const elements = typeof target === 'string' ? document.querySelectorAll(target) : [target];
if (!elements.length) {return [];}
const MultiSelect = await ensureMultiSelect();
const instances = [];
elements.forEach((element) => {
if (!element || element.dataset.multiSelectInit === '1') {return;}
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) {
syncInput._multiSelectInstance = instance;
const syncValues = () => {
if (!syncInput || 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);
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(',') : '';
syncInput.addEventListener('change', () => {
if (isSyncing) {return;}
const nextValues = syncInput.value
? syncInput.value.split(',').map((item) => item.trim()).filter(Boolean)
: [];
instance.setValues(nextValues);
});
}
});
return instances;
};
export const getMultiSelectInstance = (target) => {
const element = typeof target === 'string' ? document.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;
};
const autoInit = () => {
initMultiSelect().catch((error) => {
warnOnce('UI_INIT_FAIL', 'multi-select auto init failed', { module: 'multi-select', error });
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoInit);
} else {
autoInit();
}