1
0
Files
breadcrumb-the-shire/web/js/components/app-multiselect-cascade.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

102 lines
3.1 KiB
JavaScript

/**
* Cascading multi-select — filters child options based on parent selection.
*/
import { getMultiSelectInstance } from './app-multiselect-init.js';
import { warnOnce } from '../core/app-dom.js';
const parseValueList = (value) =>
String(value || '')
.split(',')
.map((item) => item.trim())
.filter(Boolean);
const setDisabledState = (option, disabled) => {
option.dataset.disabled = disabled ? '1' : '0';
option.classList.toggle('is-disabled', disabled);
option.setAttribute('aria-disabled', disabled ? 'true' : 'false');
option.style.pointerEvents = disabled ? 'none' : '';
if (!option._cascadeGuard) {
option.addEventListener(
'click',
(event) => {
if (option.dataset.disabled === '1') {
event.preventDefault();
event.stopImmediatePropagation();
}
},
true
);
option._cascadeGuard = true;
}
};
const toggleSelectAll = (instance, disabled) => {
const selectAll = instance?.element?.querySelector('.multi-select-all');
if (!selectAll) {return;}
selectAll.classList.toggle('is-disabled', disabled);
selectAll.setAttribute('aria-disabled', disabled ? 'true' : 'false');
selectAll.style.pointerEvents = disabled ? 'none' : '';
};
export const initMultiSelectCascade = ({
parent,
child,
map = {},
mode = 'disable',
clearInvalid = true,
}) => {
const parentInput = typeof parent === 'string' ? document.querySelector(parent) : parent;
const childInput = typeof child === 'string' ? document.querySelector(child) : child;
if (!parentInput || !childInput) {
if (!parentInput) {
warnOnce('UI_EL_MISSING', `Missing multiselect parent: ${parent}`, { module: 'multiselect-cascade' });
}
if (!childInput) {
warnOnce('UI_EL_MISSING', `Missing multiselect child: ${child}`, { module: 'multiselect-cascade' });
}
return;
}
const childInstance = getMultiSelectInstance(childInput);
if (!childInstance) {
warnOnce('UI_INIT_MISSING', 'Missing multiselect instance for child', { module: 'multiselect-cascade' });
return;
}
const apply = () => {
const selectedParents = parseValueList(parentInput.value);
const allowAll = selectedParents.length === 0;
const allowed = new Set();
if (!allowAll) {
selectedParents.forEach((id) => {
const entries = map[id] || [];
entries.forEach((entry) => allowed.add(String(entry)));
});
}
if (clearInvalid && !allowAll) {
const selected = childInstance.selectedValues || [];
selected.forEach((value) => {
if (!allowed.has(String(value))) {
childInstance.unselect(value);
}
});
}
const options = childInstance.element.querySelectorAll('.multi-select-option');
let disabledCount = 0;
options.forEach((option) => {
const isAllowed = allowAll || allowed.has(String(option.dataset.value || ''));
const disabled = mode === 'disable' ? !isAllowed : false;
if (disabled) {disabledCount += 1;}
setDisabledState(option, disabled);
});
toggleSelectAll(childInstance, disabledCount > 0);
};
parentInput.addEventListener('change', apply);
apply();
};