2024-07-31 13:43:39 +02:00
|
|
|
/* <== 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 :
|
|
|
|
* <multi-select>
|
|
|
|
<optgroup label="Groupe A">
|
|
|
|
<option value="val1">Option 1</option>
|
|
|
|
<option value="val2">Option 2</option>
|
|
|
|
</optgroup>
|
|
|
|
<optgroup label="Groupe B">
|
|
|
|
<option value="valB1">Option B1</option>
|
|
|
|
<option value="valB2">Option B2</option>
|
|
|
|
</optgroup>
|
|
|
|
</multi-select>
|
|
|
|
|
|
|
|
<multi-select>.values() => ["val1",...]
|
|
|
|
<multi-select>.values(["val1",...]) => // sélectionne les options correspondantes (ne vérifie pas les options "single")
|
|
|
|
<multi-select>.on("change", (values) => {}) => // écoute le changement de valeur
|
|
|
|
*/
|
|
|
|
|
|
|
|
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 = `
|
|
|
|
<style>
|
|
|
|
*{
|
|
|
|
box-sizing: border-box;
|
|
|
|
}
|
|
|
|
|
|
|
|
.dropdown {
|
|
|
|
position: relative;
|
|
|
|
display: inline-block;
|
|
|
|
border-radius: 10px;
|
|
|
|
user-select: none;
|
|
|
|
}
|
|
|
|
.dropdown-button {
|
|
|
|
// padding: 10px;
|
|
|
|
// background-color: #f1f1f1;
|
|
|
|
// border: 1px solid #ccc;
|
|
|
|
cursor: pointer;
|
|
|
|
font-family: inherit;
|
|
|
|
font-size: inherit;
|
|
|
|
line-height: inherit;
|
|
|
|
}
|
|
|
|
.dropdown-content {
|
|
|
|
display: none;
|
|
|
|
position: absolute;
|
|
|
|
background-color: #fff;
|
|
|
|
min-width: 200px;
|
|
|
|
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
|
|
|
z-index: 1;
|
|
|
|
}
|
|
|
|
.dropdown-content .optgroup {
|
2024-07-31 16:08:21 +02:00
|
|
|
padding: 4px 8px;
|
2024-07-31 13:43:39 +02:00
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
.dropdown-content .optgroup div {
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
2024-07-31 16:08:21 +02:00
|
|
|
.dropdown-button::after{
|
|
|
|
content: "";
|
|
|
|
display: inline-block;
|
|
|
|
width: 0;
|
|
|
|
height: 0;
|
|
|
|
margin-left: 4px;
|
|
|
|
vertical-align: middle;
|
|
|
|
border-top: 4px dashed;
|
|
|
|
border-right: 4px solid transparent;
|
|
|
|
border-left: 4px solid transparent;
|
|
|
|
}
|
|
|
|
|
2024-07-31 13:43:39 +02:00
|
|
|
.dropdown-content .option {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
.dropdown-content .option input[type="checkbox"] {
|
|
|
|
margin-right: 0.5em;
|
|
|
|
}
|
|
|
|
label.selected{
|
|
|
|
background-color: #C2DBFB;
|
|
|
|
}
|
|
|
|
label{
|
|
|
|
cursor: pointer;
|
|
|
|
transition: all 0.3s;
|
|
|
|
}
|
|
|
|
label:hover{
|
|
|
|
background-color: #f1f1f1;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
<div class="dropdown">
|
|
|
|
<button class="dropdown-button">Select options</button>
|
|
|
|
<div class="dropdown-content multi-select-container"></div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
|
|
|
|
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 values = checkedBoxes.map((checkbox) => checkbox.value);
|
|
|
|
|
|
|
|
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"));
|
|
|
|
}
|
|
|
|
|
|
|
|
_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._internals.setFormValue(this._values());
|
2024-07-31 16:08:21 +02:00
|
|
|
this._updateSelect();
|
2024-07-31 13:43:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get value() {
|
|
|
|
return this._values();
|
|
|
|
}
|
|
|
|
|
|
|
|
set value(values) {
|
|
|
|
this._values(values);
|
|
|
|
}
|
|
|
|
|
|
|
|
on(callback) {
|
|
|
|
this.addEventListener("change", callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
format(callback) {
|
|
|
|
this.exportFormat = callback;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
customElements.define("multi-select", MultiSelect);
|