/* <== 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 :
*
Option 1
Option 2
Option B1
Option B2
.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"));
// create a FormData object
const fd = new FormData();
const values = this._values();
// check if values is an array
if (Array.isArray(values)) {
values.forEach((value) => {
fd.append(this.name, value);
});
} else {
fd.append(this.name, values);
}
// update the form values
this._internals.setFormValue(fd);
}
_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);