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 < / o p t i o n >
< option value = "val2" > Option 2 < / o p t i o n >
< / o p t g r o u p >
< optgroup label = "Groupe B" >
< option value = "valB1" > Option B1 < / o p t i o n >
< option value = "valB2" > Option B2 < / o p t i o n >
< / o p t g r o u p >
< / m u l t i - s e l e c t >
< multi - select > . values ( ) => [ "val1" , ... ]
< multi - select > . values ( [ "val1" , ... ] ) => // sélectionne les options correspondantes (ne vérifie pas les options "single")
2024-08-24 18:24:46 +02:00
< multi - select > . on ( ( values ) => { } ) => // écoute le changement de valeur
< multi - select > . 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
2024-07-31 13:43:39 +02:00
* /
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 : 10 px ;
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 : 200 px ;
box - shadow : 0 px 8 px 16 px 0 px rgba ( 0 , 0 , 0 , 0.2 ) ;
z - index : 1 ;
}
. dropdown - content . optgroup {
2024-07-31 16:08:21 +02:00
padding : 4 px 8 px ;
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 : 4 px ;
vertical - align : middle ;
border - top : 4 px dashed ;
border - right : 4 px solid transparent ;
border - left : 4 px 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.5 em ;
}
label . selected {
background - color : # C2DBFB ;
}
label {
cursor : pointer ;
transition : all 0.3 s ;
}
label : hover {
background - color : # f1f1f1 ;
}
< / s t y l e >
< div class = "dropdown" >
< button class = "dropdown-button" > Select options < / b u t t o n >
< div class = "dropdown-content multi-select-container" > < / d i v >
< / d i v >
` ;
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" ) ) ;
2024-08-24 18:24:46 +02:00
2024-08-25 08:05:40 +02:00
// 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 ) ;
}
2024-08-24 18:24:46 +02:00
// update the form values
2024-08-25 08:05:40 +02:00
this . _internals . setFormValue ( fd ) ;
2024-07-31 13:43:39 +02:00
}
_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 ) ;
} ) ;
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 ) {
2024-08-24 18:24:46 +02:00
this . addEventListener ( "change" , ( ) => {
callback ( this . _values ( ) ) ;
} ) ;
2024-07-31 13:43:39 +02:00
}
format ( callback ) {
this . exportFormat = callback ;
}
}
customElements . define ( "multi-select" , MultiSelect ) ;