├── .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 |
17 | Selected
18 |
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 |
--------------------------------------------------------------------------------