ScoDoc-Lille/app/static/js/multi-select.js

310 lines
9.3 KiB
JavaScript

/* <== 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 {
padding: 4px 8px;
width: 100%;
}
.dropdown-content .optgroup div {
font-weight: bold;
}
.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;
}
.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());
this._updateSelect();
}
}
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);