/* <== définition Multi-Select ==> */ /** * Permet d'afficher un sélecteur multiple d'options. * Pour chaque option cela affichera un checkbox. * Les options peuvent être regroupées dans des optgroup. * * * Utilisation : * .values() => ["val1",...] .values(["val1",...]) => // sélectionne les options correspondantes (ne vérifie pas les options "single") .on((values) => {}) => // écoute le changement de valeur .format((values)=>{}) // modifie les valeurs avant d'être envoyées / récupérées. values est un tableau des valeurs des options sélectionnées */ class MultiSelect extends HTMLElement { static formAssociated = true; get form() { return this._internals.form; } get name() { return this.getAttribute("name"); } get label() { return this.getAttribute("label"); } set label(value) { this.setAttribute("label", value); } get type() { return this.localName; } constructor() { super(); this.attachShadow({ mode: "open" }); // HTML/CSS du composant this.shadowRoot.innerHTML = ` `; this.exportFormat = null; this.observer = new MutationObserver(() => this.render()); this.toggleDropdown = this.toggleDropdown.bind(this); this.handleDocumentClick = this.handleDocumentClick.bind(this); this._internals = this.attachInternals(); this._internals.setFormValue([]); } connectedCallback() { this.render(); this.observer.observe(this, { childList: true, subtree: true }); const btn = this.shadowRoot.querySelector(".dropdown-button"); btn.addEventListener("click", this.toggleDropdown); document.addEventListener("click", this.handleDocumentClick); this._updateSelect(); } disconnectedCallback() { this.observer.disconnect(); document.removeEventListener("click", this.handleDocumentClick); } toggleDropdown(event) { event.stopPropagation(); const dropdownContent = this.shadowRoot.querySelector(".dropdown-content"); dropdownContent.style.display = dropdownContent.style.display === "block" ? "none" : "block"; } handleDocumentClick(event) { if (!this.contains(event.target)) { this.shadowRoot.querySelector(".dropdown-content").style.display = "none"; } } render() { const container = this.shadowRoot.querySelector(".multi-select-container"); container.innerHTML = ""; const optgroups = this.querySelectorAll("optgroup"); optgroups.forEach((optgroup) => { const groupDiv = document.createElement("div"); groupDiv.className = "optgroup"; const groupLabel = document.createElement("div"); groupLabel.textContent = optgroup.label; groupDiv.appendChild(groupLabel); const options = optgroup.querySelectorAll("option"); options.forEach((option) => { const optionDiv = document.createElement("label"); optionDiv.className = "option"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = option.value; checkbox.name = this.getAttribute("name"); if (option.hasAttribute("selected")) { checkbox.checked = true; optionDiv.classList.add("selected"); } checkbox.addEventListener("change", () => { this.handleCheckboxChange(checkbox); }); optionDiv.appendChild(checkbox); optionDiv.appendChild(document.createTextNode(option.textContent)); groupDiv.appendChild(optionDiv); }); container.appendChild(groupDiv); }); this._updateSelect(); } handleCheckboxChange(checkbox) { const opt = this.querySelector(`option[value="${checkbox.value}"]`); const isSingle = opt.hasAttribute("single"); if (!checkbox.checked) { checkbox.parentElement.classList.remove("selected"); } else { checkbox.parentElement.classList.add("selected"); // Gestion de l'option "single" if (isSingle) { // Uncheck all other checkboxes const checkboxes = this.shadowRoot.querySelectorAll( 'input[type="checkbox"]' ); checkboxes.forEach((cb) => { if (cb !== checkbox) { cb.checked = false; cb.parentElement.classList.remove("selected"); } }); } else { // Uncheck the single checkbox if present const singleCheckbox = Array.from( this.shadowRoot.querySelectorAll('input[type="checkbox"]') ).find((cb) => this.querySelector(`option[value="${cb.value}"]`).hasAttribute( "single" ) ); if (singleCheckbox) { singleCheckbox.checked = false; singleCheckbox.parentElement.classList.remove("selected"); } } } this._updateSelect(); } _updateSelect() { const checkboxes = this.shadowRoot.querySelectorAll( 'input[type="checkbox"]' ); const checkedBoxes = Array.from(checkboxes).filter( (checkbox) => checkbox.checked ); const opts = checkedBoxes.map((checkbox) => { return this.querySelector(`option[value="${checkbox.value}"]`); }); const btn = this.shadowRoot.querySelector(".dropdown-button"); if (checkedBoxes.length === 0) { btn.textContent = this.label || "Select options"; } else if (checkedBoxes.length < 4) { btn.textContent = opts.map((opt) => opt.textContent).join(", ") + ""; } else { btn.textContent = `${checkedBoxes.length} sélections`; } this.dispatchEvent(new Event("change")); // update the form values this._internals.setFormValue(this._values()); } _values(newValues = null) { const checkboxes = this.shadowRoot.querySelectorAll( 'input[type="checkbox"]' ); if (newValues === null) { // Get selected values const values = Array.from(checkboxes) .filter((checkbox) => checkbox.checked) .map((checkbox) => checkbox.value); if (this.exportFormat) { return this.exportFormat(values); } return values; } else { // Set selected values checkboxes.forEach((checkbox) => { checkbox.checked = newValues.includes(checkbox.value); }); this._updateSelect(); } } get value() { return this._values(); } set value(values) { this._values(values); } on(callback) { this.addEventListener("change", () => { callback(this._values()); }); } format(callback) { this.exportFormat = callback; } } customElements.define("multi-select", MultiSelect);