├── .gitignore ├── README.md ├── _config.yml ├── index.html ├── multi-select-dropdown.js └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/babel.config.js 3 | **/.prettierrc 4 | **/.eslintrc.js 5 | **/package.json 6 | **/yarn.lock 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

JS multiselect dropdown

3 |

Simple & customizable multi-select dropdown picker, written in vanilla JS

4 |
5 | 6 | ## 🚧 Demo 7 | Check out [kiosion.github.io/js-multiselect-dropdown/](https://kiosion.github.io/js-multiselect-dropdown/) for a live demo, or [index.html](index.html) for example usage. 8 | 9 | ## 📃 Credit 10 | Based on [admirhodzic/multiselect-dropdown](https://github.com/admirhodzic/multiselect-dropdown) 11 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Multiselect Dropdown Demo 9 | 10 | 11 |
12 |
13 |

Multiselect Dropdown Demo

14 |
15 |
16 | 19 |
20 | 31 |
32 |
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /multi-select-dropdown.js: -------------------------------------------------------------------------------- 1 | const MultiSelectDropdown = (params) => { 2 | let config = { 3 | search: true, 4 | hideX: false, 5 | useStyles: true, 6 | placeholder: 'Select...', 7 | txtSelected: 'Selected', 8 | txtAll: 'All', 9 | txtRemove: 'Remove', 10 | txtSearch: 'Search...', 11 | minWidth: '160px', 12 | maxWidth: '360px', 13 | maxHeight: '180px', 14 | borderRadius: 6, 15 | ...params 16 | }; 17 | 18 | const newElement = (tag, params) => { 19 | let element = document.createElement(tag); 20 | if (params) { 21 | Object.keys(params).forEach((key) => { 22 | if (key === 'class') { 23 | Array.isArray(params[key]) 24 | ? params[key].forEach((o) => (o !== '' ? element.classList.add(o) : 0)) 25 | : params[key] !== '' 26 | ? element.classList.add(params[key]) 27 | : 0; 28 | } else if (key === 'style') { 29 | Object.keys(params[key]).forEach((value) => { 30 | element.style[value] = params[key][value]; 31 | }); 32 | } else if (key === 'text') { 33 | params[key] === '' ? (element.innerHTML = ' ') : (element.innerText = params[key]); 34 | } else { 35 | element[key] = params[key]; 36 | } 37 | }); 38 | } 39 | return element; 40 | }; 41 | 42 | document.querySelectorAll('select[multiple]').forEach((multiSelect) => { 43 | let div = newElement('div', { class: 'multiselect-dropdown' }); 44 | multiSelect.style.display = 'none'; 45 | multiSelect.parentNode.insertBefore(div, multiSelect.nextSibling); 46 | let dropdownListWrapper = newElement('div', { class: 'multiselect-dropdown-list-wrapper' }); 47 | let dropdownList = newElement('div', { class: 'multiselect-dropdown-list' }); 48 | let search = newElement('input', { 49 | class: ['multiselect-dropdown-search'].concat([config.searchInput?.class ?? 'form-control']), 50 | style: { 51 | width: '100%', 52 | display: config.search ? 'block' : multiSelect.attributes.search?.value === 'true' ? 'block' : 'none' 53 | }, 54 | placeholder: config.txtSearch 55 | }); 56 | dropdownListWrapper.appendChild(search); 57 | div.appendChild(dropdownListWrapper); 58 | dropdownListWrapper.appendChild(dropdownList); 59 | 60 | multiSelect.loadOptions = () => { 61 | dropdownList.innerHTML = ''; 62 | 63 | if (config.selectAll || multiSelect.attributes['select-all']?.value === 'true') { 64 | let optionElementAll = newElement('div', { class: 'multiselect-dropdown-all-selector' }); 65 | let optionCheckbox = newElement('input', { type: 'checkbox' }); 66 | optionElementAll.appendChild(optionCheckbox); 67 | optionElementAll.appendChild(newElement('label', { text: config.txtAll })); 68 | 69 | optionElementAll.addEventListener('click', () => { 70 | optionElementAll.classList.toggle('checked'); 71 | optionElementAll.querySelector('input').checked = !optionElementAll.querySelector('input').checked; 72 | 73 | let ch = optionElementAll.querySelector('input').checked; 74 | dropdownList.querySelectorAll(':scope > div:not(.multiselect-dropdown-all-selector)').forEach((i) => { 75 | if (i.style.display !== 'none') { 76 | i.querySelector('input').checked = ch; 77 | i.optEl.selected = ch; 78 | } 79 | }); 80 | 81 | multiSelect.dispatchEvent(new Event('change')); 82 | }); 83 | optionCheckbox.addEventListener('click', () => { 84 | optionCheckbox.checked = !optionCheckbox.checked; 85 | }); 86 | 87 | dropdownList.appendChild(optionElementAll); 88 | } 89 | 90 | Array.from(multiSelect.options).map((option) => { 91 | let optionElement = newElement('div', { class: option.selected ? 'checked' : '', srcElement: option }); 92 | let optionCheckbox = newElement('input', { type: 'checkbox', checked: option.selected }); 93 | optionElement.appendChild(optionCheckbox); 94 | optionElement.appendChild(newElement('label', { text: option.text })); 95 | 96 | optionElement.addEventListener('click', () => { 97 | optionElement.classList.toggle('checked'); 98 | optionElement.querySelector('input').checked = !optionElement.querySelector('input').checked; 99 | optionElement.srcElement.selected = !optionElement.srcElement.selected; 100 | multiSelect.dispatchEvent(new Event('change')); 101 | }); 102 | optionCheckbox.addEventListener('click', () => { 103 | optionCheckbox.checked = !optionCheckbox.checked; 104 | }); 105 | option.optionElement = optionElement; 106 | dropdownList.appendChild(optionElement); 107 | }); 108 | div.dropdownListWrapper = dropdownListWrapper; 109 | 110 | div.refresh = () => { 111 | // For demo purposes, remove 112 | let tempSelectedList = document.getElementById('dropdownSelected'); 113 | 114 | div.querySelectorAll('span.optext, span.placeholder').forEach((placeholder) => div.removeChild(placeholder)); 115 | let selected = Array.from(multiSelect.selectedOptions); 116 | if (selected.length > (multiSelect.attributes['max-items']?.value ?? 5)) { 117 | div.appendChild( 118 | newElement('span', { 119 | class: ['optext', 'maxselected'], 120 | text: selected.length + ' ' + config.txtSelected 121 | }) 122 | ); 123 | // For demo purposes, remove 124 | tempSelectedList 125 | .querySelectorAll('span') 126 | .forEach((span, index) => index !== 0 && tempSelectedList.removeChild(span)); 127 | selected.map((option) => tempSelectedList.appendChild(newElement('span', { text: option.text }))); 128 | } else { 129 | // For demo purposes, remove 130 | tempSelectedList 131 | .querySelectorAll('span') 132 | .forEach((span, index) => index !== 0 && tempSelectedList.removeChild(span)); 133 | 134 | selected.map((option) => { 135 | let span = newElement('span', { 136 | class: 'optext', 137 | text: option.text, 138 | srcElement: option 139 | }); 140 | if (!config.hideX) { 141 | span.appendChild( 142 | newElement('span', { 143 | class: 'optdel', 144 | text: '🗙', 145 | title: config.txtRemove, 146 | onclick: (e) => { 147 | span.srcElement.optionElement.dispatchEvent(new Event('click')); 148 | div.refresh(); 149 | e.stopPropagation(); 150 | } 151 | }) 152 | ); 153 | } 154 | div.appendChild(span); 155 | // For demo purposes, remove 156 | tempSelectedList.appendChild(newElement('span', { text: option.text })); 157 | }); 158 | } 159 | if (multiSelect.selectedOptions?.length === 0) { 160 | div.appendChild( 161 | newElement('span', { 162 | class: 'placeholder', 163 | text: multiSelect.attributes?.placeholder?.value ?? config.placeholder 164 | }) 165 | ); 166 | // For demo purposes, remove 167 | tempSelectedList.appendChild(newElement('span', { text: 'n/a' })); 168 | } 169 | }; 170 | div.refresh(); 171 | }; 172 | multiSelect.loadOptions(); 173 | 174 | search.addEventListener('input', () => { 175 | dropdownList.querySelectorAll(':scope div:not(.multiselect-dropdown-all-selector)').forEach((div) => { 176 | let innerText = div.querySelector('label').innerText.toLowerCase(); 177 | div.style.display = innerText.includes(search.value.toLowerCase()) ? 'flex' : 'none'; 178 | }); 179 | }); 180 | 181 | div.addEventListener('click', () => { 182 | div.dropdownListWrapper.style.display = 'block'; 183 | search.focus(); 184 | search.select(); 185 | }); 186 | 187 | document.addEventListener('click', (e) => { 188 | if (!div.contains(e.target)) { 189 | dropdownListWrapper.style.display = 'none'; 190 | div.refresh(); 191 | } 192 | }); 193 | }); 194 | 195 | const createStyles = () => { 196 | let styles = { 197 | ':root': { 198 | '--color-background': '#ffffff', 199 | '--color-border': '#ced4da', 200 | '--color-background--option': '#d6dde6', 201 | '--color-background--option--hover': '#cbd5e0a1', 202 | '--color-text--normal': '#0c0c0c', 203 | '--color-text--grey': '#24262c', 204 | '--color-text--red': '#cc6666', 205 | '--color-text--placeholder': '#ced4da', 206 | '--border-radius--base': `${parseInt(config.borderRadius)}px` || '6px', 207 | '--border-radius--small': `${parseInt(config.borderRadius) * 0.75}px` || '4px' 208 | }, 209 | '.multiselect-dropdown': { 210 | position: 'relative', 211 | display: 'inline-flex', 212 | 'flex-wrap': 'wrap', 213 | padding: '6px 36px 6px 6px', 214 | gap: '6px', 215 | 'border-radius': 'var(--border-radius--base)', 216 | border: 'solid 1px var(--color-border)', 217 | background: 'var(--color-background)', 218 | 'background-image': 219 | "url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\")", 220 | 'background-repeat': 'no-repeat', 221 | 'background-position': 'right 6px center', 222 | 'background-size': '16px 12px', 223 | 'min-width': `${config.minWidth}` ?? '140px', 224 | 'max-width': `${config.maxWidth}` ?? '360px', 225 | cursor: 'pointer' 226 | }, 227 | 'span.optext, span.placeholder': { 228 | display: 'inline-flex', 229 | 'justify-content': 'center', 230 | 'align-items': 'center', 231 | 'font-size': '16px', 232 | 'border-radius': 'var(--border-radius--small)' 233 | }, 234 | 'span.optext': { 235 | 'background-color': 'var(--color-background--option)', 236 | padding: '0 12px 2px 6px', 237 | cursor: 'default', 238 | '-webkit-user-select': 'none', 239 | '-moz-user-select': 'none', 240 | '-ms-user-select': 'none', 241 | 'user-select': 'none' 242 | }, 243 | 'span.optext .optdel': { 244 | float: 'right', 245 | margin: '0 -6px 1px 6px', 246 | 'font-size': '12px', 247 | cursor: 'pointer', 248 | color: 'var(--color-text--grey)' 249 | }, 250 | 'span.optext .optdel:hover': { 251 | color: 'var(--color-text--red)' 252 | }, 253 | 'span.placeholder': { 254 | color: 'var(--color-border)' 255 | }, 256 | '.multiselect-dropdown-list-wrapper': { 257 | 'z-index': 100, 258 | 'border-radius': 'var(--border-radius--base)', 259 | border: 'solid 1px var(--color-border)', 260 | display: 'none', 261 | margin: '-1px', 262 | position: 'absolute', 263 | top: 0, 264 | left: 0, 265 | right: 0, 266 | background: 'var(--color-background)' 267 | }, 268 | '.multiselect-dropdown-search': { 269 | padding: '5px 6px 6px 5px', 270 | 'border-top-left-radius': 'var(--border-radius--base)', 271 | 'border-top-right-radius': 'var(--border-radius--base)', 272 | border: 'solid 1px transparent', 273 | 'border-bottom': 'solid 1px var(--color-border)', 274 | 'font-size': 'inherit' 275 | }, 276 | '.multiselect-dropdown-search::placeholder': { 277 | color: 'var(--color-text--placeholder)', 278 | 'font-size': '16px' 279 | }, 280 | '.multiselect-dropdown-search:focus, .multiselect-dropdown-search:focus-visible': { 281 | outline: 'none' 282 | }, 283 | '.multiselect-dropdown-list': { 284 | 'overflow-y': 'auto', 285 | 'overflow-x': 'hidden', 286 | height: '100%', 287 | 'max-height': `${config.maxHeight}` ?? '160px' 288 | }, 289 | '.multiselect-dropdown-list::-webkit-scrollbar': { 290 | width: '4px' 291 | }, 292 | '.multiselect-dropdown-list::-webkit-scrollbar-thumb': { 293 | 'background-color': 'var(--color-background--option)', 294 | 'border-radius': '1000px' 295 | }, 296 | '.multiselect-dropdown-list div, .multiselect-dropdown-list div > input, .multiselect-dropdown-list div > label': 297 | { 298 | cursor: 'pointer', 299 | 'border-radius': 'var(--border-radius--base)' 300 | }, 301 | '.multiselect-dropdown-list div': { 302 | display: 'flex', 303 | 'align-items': 'center', 304 | 'justify-content': 'flex-start', 305 | 'column-gap': '6px', 306 | padding: '6px', 307 | margin: '6px 8px 6px 6px', 308 | transition: '100ms cubic-bezier(0.455, 0.03, 0.515, 0.955)' 309 | }, 310 | '.multiselect-dropdown-list div:hover': { 311 | 'background-color': 'var(--color-background--option--hover)' 312 | }, 313 | '.multiselect-dropdown-list-input': { 314 | height: '14px', 315 | width: '14px', 316 | border: 'solid 1px var(--color-text--grey)', 317 | margin: 0 318 | }, 319 | '.multiselect-dropdown span.maxselected': { 320 | width: '100%' 321 | }, 322 | '.multiselect-dropdown-all-selector': { 323 | 'border-bottom': 'solid 1px var(--color-border)' 324 | } 325 | }; 326 | const style = document.createElement('style'); 327 | style.setAttribute('type', 'text/css'); 328 | style.innerHTML = `${Object.keys(styles) 329 | .map( 330 | (selector) => 331 | `${selector} { ${Object.keys(styles[selector]) 332 | .map((property) => `${property}: ${styles[selector][property]}`) 333 | .join('; ')} }` 334 | ) 335 | .join('\n')}`; 336 | document.head.appendChild(style); 337 | }; 338 | 339 | config.useStyles && createStyles(); 340 | }; 341 | 342 | window.addEventListener('load', () => { 343 | MultiSelectDropdown(window.MultiSelectDropdownOptions); 344 | }); 345 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Generic styles for demo page */ 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body, div { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | body { 13 | height: 100vh; 14 | background-color: #fafafa; 15 | font-size: 16px; 16 | } 17 | 18 | body, div, span, input, select, p { 19 | font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 20 | } 21 | 22 | form { 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | justify-content: center; 27 | gap: 14px; 28 | } 29 | 30 | .wrapper { 31 | display: flex; 32 | flex-direction: row; 33 | justify-content: center; 34 | } 35 | 36 | body > .wrapper { 37 | flex-direction: column; 38 | } 39 | 40 | .container { 41 | width: 50%; 42 | max-width: 420px; 43 | display: flex; 44 | flex-direction: column; 45 | align-items: center; 46 | justify-content: flex-start; 47 | margin-top: 14px; 48 | } 49 | 50 | body > .wrapper > .container { 51 | width: 100%; 52 | max-width: unset; 53 | margin-bottom: 80px; 54 | } 55 | 56 | #dropdownSelected > span:first-of-type { 57 | font-weight: bold; 58 | font-size: 18px; 59 | margin-bottom: 4px; 60 | } 61 | --------------------------------------------------------------------------------