// Copyright 2026 Moko Consulting. All rights reserved. // SPDX-License-Identifier: MIT import {fomanticQuery} from '../modules/fomantic/base.ts'; import {addDelegatedEventListener, queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts'; /** * Generic multiselect combo list component. * Works with the "shared/combolist" template to provide a reusable * dropdown with search, checkable items, and a selected-items display list. * * Usage: add class="combo-multiselect" to the container element. * The component is self-contained — no backend calls, just updates the hidden input. */ class ComboMultiselect { container: HTMLElement; elDropdown: HTMLElement; elList: HTMLElement; elComboValue: HTMLInputElement; constructor(container: HTMLElement) { this.container = container; this.elDropdown = container.querySelector(':scope > .ui.dropdown')!; this.elList = container.querySelector(':scope > .combo-multiselect-list')!; this.elComboValue = container.querySelector(':scope > .combo-value')!; } collectCheckedValues(): string[] { return Array.from( this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value')!, ); } updateUiList() { const checkedValues = this.collectCheckedValues(); const elEmptyTip = this.elList.querySelector(':scope > .item.empty-list')!; queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove()); for (const value of checkedValues) { const el = this.elDropdown.querySelector(`.menu > .item[data-value="${CSS.escape(value)}"]`); if (!el) continue; const labelText = el.querySelector('.item-label')?.textContent || value; const listItem = document.createElement('span'); listItem.classList.add('item'); listItem.textContent = labelText; this.elList.append(listItem); } toggleElem(elEmptyTip, checkedValues.length === 0); this.elComboValue.value = checkedValues.join(','); this.elComboValue.dispatchEvent(new Event('change', {bubbles: true})); } onItemClick(elItem: HTMLElement, e: Event) { e.preventDefault(); if (elItem.matches('.clear-selection')) { queryElems(this.elDropdown, '.menu > .item', (el) => el.classList.remove('checked')); this.updateUiList(); return; } elItem.classList.toggle('checked'); this.updateUiList(); } init() { // Restore checked state from initial value const initialValues = this.elComboValue.value ? this.elComboValue.value.split(',') : []; for (const value of initialValues) { const elItem = this.elDropdown.querySelector(`.menu > .item[data-value="${CSS.escape(value)}"]`); elItem?.classList.add('checked'); } this.updateUiList(); addDelegatedEventListener(this.elDropdown, 'click', '.item', (el, e) => this.onItemClick(el, e)); fomanticQuery(this.elDropdown).dropdown('setting', { action: 'nothing', fullTextSearch: 'exact', hideDividers: 'empty', }); } } export function initComboMultiselect() { queryElems(document, '.combo-multiselect', (el) => { if (el.hasAttribute('data-combo-inited')) return; el.setAttribute('data-combo-inited', 'true'); new ComboMultiselect(el).init(); }); }