multi-select-dropdown-js 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 David Adams
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,264 @@
1
+ :root {
2
+ --spacing-smaller: 0.1875rem;
3
+ --spacing-small: 0.3125rem;
4
+ --spacing-medium: 0.4375rem;
5
+ --spacing-large: 0.75rem;
6
+ --font-size: 0.75rem;
7
+ --font-size-large: 0.875rem;
8
+ --font-size-larger: 1rem;
9
+ --line-height: 1rem;
10
+ --line-height-larger: 1.25rem;
11
+ --primary-color: #40c979;
12
+ --ms-bg: #ffffff;
13
+ --text-color-dark: #212529;
14
+ --text-color: #585858;
15
+ --text-color-light: #65727e;
16
+ --border-color: #bebebe;
17
+ --border-color-light: #f1f3f5;
18
+ --input-placeholder: #65727e;
19
+ --input-background: #e9e9ed;
20
+ --input-border: #dee2e6;
21
+ --input-border-active: #c1c9d0;
22
+ --input-border-invalid: #e44e4e;
23
+ --input-outline-invalid: rgba(219, 138, 138, 0.5);
24
+ --input-color: #e9e9ed;
25
+ --input-disabled: #f7f7f7;
26
+ --option-background: #f3f4f7;
27
+ --checkbox-border: #ced4da;
28
+ --checkbox-background: #ffffff;
29
+ --checkbox-active: #ffffff;
30
+ --input-min-height: 2.8125rem;
31
+ --options-height: 40dvh;
32
+ --border-radius: 0.3125rem;
33
+ --icon-size: 0.75rem;
34
+ --icon-space: 1.875rem;
35
+ --checkbox-size: 1rem;
36
+ --checkbox-radius: 0.25rem;
37
+ --checkbox-thickness: 0.125rem;
38
+ }
39
+ .multi-select[data-theme="dark"] {
40
+ --ms-bg: #2b2b2b;
41
+ --text-color-dark: #f8f9fa;
42
+ --text-color: #adb5bd;
43
+ --text-color-light: #adb5bd;
44
+ --border-color: #495057;
45
+ --border-color-light: #495057;
46
+ --input-background: #343a40;
47
+ --input-border: #495057;
48
+ --input-border-active: #6c757d;
49
+ --option-background: #343a40;
50
+ --checkbox-background: #2b2b2b;
51
+ --checkbox-border: #6c757d;
52
+ --checkbox-active: #ffffff;
53
+ --input-disabled: #1e1e1e;
54
+ }
55
+ @media (prefers-color-scheme: dark) {
56
+ .multi-select[data-theme="auto"] {
57
+ --ms-bg: #2b2b2b;
58
+ --text-color-dark: #f8f9fa;
59
+ --text-color: #adb5bd;
60
+ --text-color-light: #adb5bd;
61
+ --border-color: #495057;
62
+ --border-color-light: #495057;
63
+ --input-background: #343a40;
64
+ --input-border: #495057;
65
+ --input-border-active: #6c757d;
66
+ --option-background: #343a40;
67
+ --checkbox-background: #2b2b2b;
68
+ --checkbox-border: #6c757d;
69
+ --checkbox-active: #ffffff;
70
+ --input-disabled: #1e1e1e;
71
+ }
72
+ }
73
+ .multi-select {
74
+ display: flex;
75
+ box-sizing: border-box;
76
+ flex-direction: column;
77
+ position: relative;
78
+ width: 100%;
79
+ user-select: none;
80
+ }
81
+ .multi-select .multi-select-header {
82
+ background-color: var(--ms-bg);
83
+ border: 1px solid var(--input-border);
84
+ border-radius: var(--border-radius);
85
+ padding: var(--spacing-medium) var(--spacing-large);
86
+ padding-right: var(--icon-space);
87
+ overflow: hidden;
88
+ gap: var(--spacing-medium);
89
+ min-height: var(--input-min-height);
90
+ }
91
+ .multi-select .multi-select-header::after {
92
+ content: "";
93
+ display: block;
94
+ position: absolute;
95
+ top: 50%;
96
+ right: 1rem;
97
+ transform: translateY(-50%);
98
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23949ba3' viewBox='0 0 16 16'%3E%3Cpath d='M8 13.1l-8-8 2.1-2.2 5.9 5.9 5.9-5.9 2.1 2.2z'/%3E%3C/svg%3E");
99
+ height: var(--icon-size);
100
+ width: var(--icon-size);
101
+ }
102
+ .multi-select .multi-select-header.multi-select-header-active {
103
+ border-color: var(--input-border-active);
104
+ }
105
+ .multi-select .multi-select-header.multi-select-header-active::after {
106
+ transform: translateY(-50%) rotate(180deg);
107
+ }
108
+ .multi-select .multi-select-header.multi-select-header-active + .multi-select-options {
109
+ display: flex;
110
+ }
111
+ .multi-select .multi-select-header .multi-select-header-placeholder {
112
+ color: var(--text-color-light);
113
+ }
114
+ .multi-select .multi-select-header .multi-select-header-option {
115
+ display: inline-flex;
116
+ align-items: center;
117
+ background-color: var(--option-background);
118
+ color: var(--text-color-dark);
119
+ font-size: var(--font-size-large);
120
+ padding: var(--spacing-smaller) var(--spacing-small);
121
+ border-radius: var(--border-radius);
122
+ }
123
+ .multi-select .multi-select-header .multi-select-header-max {
124
+ font-size: var(--font-size-large);
125
+ color: var(--text-color-light);
126
+ }
127
+ .multi-select .multi-select-options {
128
+ display: none;
129
+ box-sizing: border-box;
130
+ flex-flow: wrap;
131
+ position: absolute;
132
+ top: 100%;
133
+ left: 0;
134
+ right: 0;
135
+ z-index: 999;
136
+ margin-top: var(--spacing-small);
137
+ padding: var(--spacing-small);
138
+ background-color: var(--ms-bg);
139
+ border-radius: var(--border-radius);
140
+ box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.15);
141
+ max-height: var(--options-height);
142
+ overflow-y: auto;
143
+ overflow-x: hidden;
144
+ }
145
+ .multi-select .multi-select-options::-webkit-scrollbar {
146
+ width: 0.3125rem;
147
+ }
148
+ .multi-select .multi-select-options::-webkit-scrollbar-track {
149
+ background: var(--input-background);
150
+ }
151
+ .multi-select .multi-select-options::-webkit-scrollbar-thumb {
152
+ background: var(--border-color);
153
+ }
154
+ .multi-select .multi-select-options::-webkit-scrollbar-thumb:hover {
155
+ background: var(--input-placeholder);
156
+ }
157
+ .multi-select .multi-select-options .multi-select-option,
158
+ .multi-select .multi-select-options .multi-select-all,
159
+ .multi-select .multi-select-options .multi-select-group {
160
+ padding: 0.5rem var(--spacing-large);
161
+ }
162
+ .multi-select .multi-select-options .multi-select-option[data-group]:not([data-group=""]) {
163
+ padding-left: 2.5rem;
164
+ }
165
+ .multi-select .multi-select-options .multi-select-option .multi-select-option-radio,
166
+ .multi-select .multi-select-options .multi-select-all .multi-select-option-radio,
167
+ .multi-select .multi-select-options .multi-select-group .multi-select-option-radio {
168
+ background: var(--checkbox-background);
169
+ margin-right: var(--spacing-large);
170
+ height: var(--checkbox-size);
171
+ width: var(--checkbox-size);
172
+ border: 1px solid var(--checkbox-border);
173
+ border-radius: var(--checkbox-radius);
174
+ }
175
+ .multi-select .multi-select-options .multi-select-option .multi-select-option-text,
176
+ .multi-select .multi-select-options .multi-select-all .multi-select-option-text,
177
+ .multi-select .multi-select-options .multi-select-group .multi-select-option-text {
178
+ box-sizing: border-box;
179
+ flex: 1;
180
+ overflow: hidden;
181
+ text-overflow: ellipsis;
182
+ white-space: nowrap;
183
+ color: inherit;
184
+ font-size: var(--font-size-larger);
185
+ line-height: var(--line-height-larger);
186
+ padding-bottom: 1px;
187
+ }
188
+ .multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-radio,
189
+ .multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-radio,
190
+ .multi-select .multi-select-options .multi-select-group.multi-select-selected .multi-select-option-radio {
191
+ border-color: var(--primary-color);
192
+ background-color: var(--primary-color);
193
+ }
194
+ .multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-radio::after,
195
+ .multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-radio::after,
196
+ .multi-select .multi-select-options .multi-select-group.multi-select-selected .multi-select-option-radio::after {
197
+ content: "";
198
+ display: block;
199
+ width: calc(var(--checkbox-size) / 4);
200
+ height: calc(var(--checkbox-size) / 2);
201
+ border: solid var(--checkbox-active);
202
+ border-width: 0 var(--checkbox-thickness) var(--checkbox-thickness) 0;
203
+ transform: rotate(45deg) translate(50%, -25%);
204
+ }
205
+ .multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-text,
206
+ .multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-text,
207
+ .multi-select .multi-select-options .multi-select-group.multi-select-selected .multi-select-option-text {
208
+ color: var(--text-color-dark);
209
+ }
210
+ .multi-select .multi-select-options .multi-select-option:hover,
211
+ .multi-select .multi-select-options .multi-select-option:active,
212
+ .multi-select .multi-select-options .multi-select-all:hover,
213
+ .multi-select .multi-select-options .multi-select-all:active,
214
+ .multi-select .multi-select-options .multi-select-group:hover,
215
+ .multi-select .multi-select-options .multi-select-group:active {
216
+ background-color: var(--option-background);
217
+ }
218
+ .multi-select .multi-select-options .multi-select-all {
219
+ border-bottom: 1px solid var(--border-color-light);
220
+ border-radius: 0;
221
+ }
222
+ .multi-select .multi-select-options .multi-select-group {
223
+ font-weight: bold;
224
+ border-radius: 0;
225
+ }
226
+ .multi-select .multi-select-options .multi-select-search {
227
+ padding: var(--spacing-medium) var(--spacing-large);
228
+ border: 1px solid var(--input-border);
229
+ border-radius: var(--border-radius);
230
+ margin: 0.625rem;
231
+ width: calc(100% - 1.25rem);
232
+ outline: none;
233
+ font-size: var(--font-size-larger);
234
+ background-color: var(--ms-bg);
235
+ color: var(--text-color-dark);
236
+ }
237
+ .multi-select .multi-select-options .multi-select-search::placeholder {
238
+ color: var(--text-color-light);
239
+ }
240
+ .multi-select .multi-select-header,
241
+ .multi-select .multi-select-option,
242
+ .multi-select .multi-select-all,
243
+ .multi-select .multi-select-group {
244
+ display: flex;
245
+ flex-wrap: wrap;
246
+ box-sizing: border-box;
247
+ align-items: center;
248
+ border-radius: var(--border-radius);
249
+ cursor: pointer;
250
+ display: flex;
251
+ align-items: center;
252
+ width: 100%;
253
+ font-size: var(--font-size-larger);
254
+ color: var(--text-color-dark);
255
+ }
256
+ .multi-select.disabled {
257
+ opacity: 0.6;
258
+ pointer-events: none;
259
+ background-color: var(--input-disabled);
260
+ }
261
+ .multi-select.multi-select-invalid .multi-select-header {
262
+ border-color: var(--input-border-invalid);
263
+ outline: var(--input-outline-invalid) solid 1px;
264
+ }
package/MultiSelect.js ADDED
@@ -0,0 +1,674 @@
1
+ /*
2
+ * Created by David Adams
3
+ * https://codeshack.io/multi-select-dropdown-html-javascript/
4
+ *
5
+ * Released under the MIT license
6
+ */
7
+ class MultiSelect {
8
+ constructor(element, options = {}) {
9
+ let defaults = {
10
+ placeholder: 'Select item(s)',
11
+ max: null,
12
+ min: null,
13
+ disabled: false,
14
+ search: true,
15
+ selectAll: true,
16
+ listAll: true,
17
+ closeListOnItemSelect: false,
18
+ name: '',
19
+ width: '',
20
+ height: '',
21
+ dropdownWidth: '',
22
+ dropdownHeight: '',
23
+ theme: 'auto',
24
+ required: false,
25
+ data:[],
26
+ onChange: function() {},
27
+ onSelect: function() {},
28
+ onUnselect: function() {},
29
+ onMaxReached: function() {}
30
+ };
31
+ this.options = Object.assign(defaults, options);
32
+ this.selectElement = typeof element === 'string' ? document.querySelector(element) : element;
33
+ if (this.selectElement._multiSelect) {
34
+ this.selectElement._multiSelect.destroy();
35
+ }
36
+ this.selectElement._multiSelect = this;
37
+ this.originalStyle = this.selectElement.getAttribute('style') || '';
38
+ this.originalTabIndex = this.selectElement.getAttribute('tabindex');
39
+ this._isBatching = false;
40
+ for(const prop in this.selectElement.dataset) {
41
+ if (this.options[prop] !== undefined) {
42
+ if (typeof this.options[prop] === 'boolean' || this.selectElement.dataset[prop] === 'true' || this.selectElement.dataset[prop] === 'false') {
43
+ this.options[prop] = this.selectElement.dataset[prop] === 'true';
44
+ } else {
45
+ this.options[prop] = this.selectElement.dataset[prop];
46
+ }
47
+ }
48
+ }
49
+ if (this.selectElement.hasAttribute('required')) this.options.required = true;
50
+ if (this.selectElement.hasAttribute('disabled')) this.options.disabled = true;
51
+ this.name = this.selectElement.getAttribute('name') ? this.selectElement.getAttribute('name') : 'multi-select-' + Math.floor(Math.random() * 1000000);
52
+ if (!this.options.data.length) {
53
+ let options = this.selectElement.querySelectorAll('option');
54
+ for (let i = 0; i < options.length; i++) {
55
+ let parent = options[i].parentElement;
56
+ let group = parent.tagName.toLowerCase() === 'optgroup' ? parent.getAttribute('label') : '';
57
+ this.options.data.push({
58
+ value: options[i].value,
59
+ text: options[i].textContent,
60
+ selected: options[i].selected,
61
+ disabled: options[i].disabled,
62
+ html: options[i].getAttribute('data-html'),
63
+ group: group
64
+ });
65
+ }
66
+ }
67
+ this.originalData = JSON.parse(JSON.stringify(this.options.data));
68
+ this.element = this._template();
69
+ this.selectElement.insertAdjacentElement('beforebegin', this.element);
70
+ this.element.appendChild(this.selectElement);
71
+ this.selectElement.multiple = true;
72
+ this.selectElement.setAttribute('tabindex', '-1');
73
+ this.selectElement.style.position = 'absolute';
74
+ this.selectElement.style.left = '0';
75
+ this.selectElement.style.top = '0';
76
+ this.selectElement.style.width = '100%';
77
+ this.selectElement.style.height = '100%';
78
+ this.selectElement.style.opacity = '0';
79
+ this.selectElement.style.zIndex = '-1';
80
+ this.selectElement.style.pointerEvents = 'none';
81
+ this.outsideClickHandler = this._outsideClick.bind(this);
82
+ this._buildOriginalSelect();
83
+ this._updateSelected();
84
+ this._eventHandlers();
85
+ if (this.options.disabled) this.disable();
86
+ if (this.selectElement.form) {
87
+ this.formResetHandler = () => setTimeout(() => this.reset(), 0);
88
+ this.selectElement.form.addEventListener('reset', this.formResetHandler);
89
+ }
90
+ }
91
+
92
+ _escapeHTML(str) {
93
+ return str !== undefined && str !== null ? str.toString().replace(/[&<>'"]/g, tag => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[tag] || tag)) : '';
94
+ }
95
+
96
+ _template() {
97
+ let optionsHTML = '';
98
+ let groupedData = {};
99
+ this.data.forEach(item => {
100
+ let g = item.group || '';
101
+ if(!groupedData[g]) groupedData[g] =[];
102
+ groupedData[g].push(item);
103
+ });
104
+ for (const[groupName, items] of Object.entries(groupedData)) {
105
+ if (groupName) {
106
+ let enabledItems = items.filter(i => !i.disabled);
107
+ let allGroupSelected = enabledItems.length > 0 && enabledItems.every(i => i.selected);
108
+ optionsHTML += `
109
+ <div class="multi-select-group${allGroupSelected ? ' multi-select-selected' : ''}" data-group="${this._escapeHTML(groupName)}" role="option" tabindex="-1">
110
+ <span class="multi-select-option-radio"></span>
111
+ <span class="multi-select-option-text">${this._escapeHTML(groupName)}</span>
112
+ </div>
113
+ `;
114
+ }
115
+ items.forEach(item => {
116
+ const isSelected = item.selected;
117
+ const isDisabled = item.disabled;
118
+ optionsHTML += `
119
+ <div class="multi-select-option${isSelected ? ' multi-select-selected' : ''}" data-value="${this._escapeHTML(item.value)}" data-group="${this._escapeHTML(groupName)}" role="option" aria-selected="${isSelected}" tabindex="-1" ${isDisabled ? 'style="opacity: 0.5; pointer-events: none;"' : ''}>
120
+ <span class="multi-select-option-radio"></span>
121
+ <span class="multi-select-option-text">${item.html ? item.html : this._escapeHTML(item.text)}</span>
122
+ </div>
123
+ `;
124
+ });
125
+ }
126
+ let selectAllHTML = '';
127
+ if (this.options.selectAll) {
128
+ let enabledData = this.data.filter(d => !d.disabled);
129
+ let allSelected = enabledData.length > 0 && enabledData.every(d => d.selected);
130
+ selectAllHTML = `<div class="multi-select-all${allSelected ? ' multi-select-selected' : ''}" role="option" tabindex="-1">
131
+ <span class="multi-select-option-radio"></span>
132
+ <span class="multi-select-option-text">Select all</span>
133
+ </div>`;
134
+ }
135
+ let template = `
136
+ <div class="multi-select ${this.name}"${this.selectElement.id ? ' id="ms-' + this._escapeHTML(this.selectElement.id) + '"' : ''} style="${this.width ? 'width:' + this.width + ';' : ''}${this.height ? 'height:' + this.height + ';' : ''}" role="combobox" aria-haspopup="listbox" aria-expanded="false" data-theme="${this.options.theme}">
137
+ <div class="multi-select-header" style="${this.width ? 'width:' + this.width + ';' : ''}${this.height ? 'height:' + this.height + ';' : ''}" tabindex="0">
138
+ <span class="multi-select-header-max">${this.options.max ? this.selectedValues.length + '/' + this.options.max : ''}</span>
139
+ <span class="multi-select-header-placeholder">${this._escapeHTML(this.placeholder)}</span>
140
+ </div>
141
+ <div class="multi-select-options" style="${this.options.dropdownWidth ? 'width:' + this.options.dropdownWidth + ';' : ''}${this.options.dropdownHeight ? 'height:' + this.options.dropdownHeight + ';' : ''}" role="listbox">
142
+ ${this.options.search ? '<input type="text" class="multi-select-search" placeholder="Search..." role="searchbox">' : ''}
143
+ ${selectAllHTML}
144
+ ${optionsHTML}
145
+ </div>
146
+ </div>
147
+ `;
148
+ let element = document.createElement('div');
149
+ element.innerHTML = template;
150
+ return element.firstElementChild;
151
+ }
152
+
153
+ _eventHandlers() {
154
+ let headerElement = this.element.querySelector('.multi-select-header');
155
+ const toggleDropdown = (forceClose = false) => {
156
+ if (this.element.classList.contains('disabled')) return;
157
+ if (forceClose || headerElement.classList.contains('multi-select-header-active')) {
158
+ headerElement.classList.remove('multi-select-header-active');
159
+ this.element.setAttribute('aria-expanded', 'false');
160
+ } else {
161
+ headerElement.classList.add('multi-select-header-active');
162
+ this.element.setAttribute('aria-expanded', 'true');
163
+ }
164
+ };
165
+ this.element.querySelectorAll('.multi-select-option').forEach(option => {
166
+ option.onclick = (e) => {
167
+ e.stopPropagation();
168
+ if (this.element.classList.contains('disabled')) return;
169
+ let dataItem = this.data.find(d => String(d.value) === String(option.dataset.value));
170
+ if (!dataItem || dataItem.disabled) return;
171
+ let selected = true;
172
+ if (!option.classList.contains('multi-select-selected')) {
173
+ if (this.options.max && this.selectedValues.length >= this.options.max) {
174
+ if (!this._isBatching) this.options.onMaxReached(this.options.max);
175
+ return;
176
+ }
177
+ option.classList.add('multi-select-selected');
178
+ option.setAttribute('aria-selected', 'true');
179
+ dataItem.selected = true;
180
+ } else {
181
+ option.classList.remove('multi-select-selected');
182
+ option.setAttribute('aria-selected', 'false');
183
+ dataItem.selected = false;
184
+ selected = false;
185
+ }
186
+ if (!this._isBatching) {
187
+ this._updateSelected();
188
+ this._syncOriginalSelect();
189
+ if (this.options.closeListOnItemSelect) {
190
+ if (this.options.search) {
191
+ this.element.querySelector('.multi-select-search').value = '';
192
+ this.element.querySelectorAll('.multi-select-option, .multi-select-group').forEach(opt => opt.style.display = 'flex');
193
+ }
194
+ toggleDropdown(true);
195
+ }
196
+ }
197
+ this.options.onChange(option.dataset.value, option.querySelector('.multi-select-option-text').innerHTML, option);
198
+ if (selected) {
199
+ this.options.onSelect(option.dataset.value, option.querySelector('.multi-select-option-text').innerHTML, option);
200
+ } else {
201
+ this.options.onUnselect(option.dataset.value, option.querySelector('.multi-select-option-text').innerHTML, option);
202
+ }
203
+ };
204
+ });
205
+
206
+ this.element.querySelectorAll('.multi-select-group').forEach(groupEl => {
207
+ groupEl.onclick = (e) => {
208
+ e.stopPropagation();
209
+ if (this.element.classList.contains('disabled')) return;
210
+ let groupName = groupEl.dataset.group;
211
+ let isSelected = groupEl.classList.contains('multi-select-selected');
212
+ let hitMax = false;
213
+ this._isBatching = true;
214
+ this.element.querySelectorAll('.multi-select-option').forEach(option => {
215
+ if (option.dataset.group === groupName && option.style.display !== 'none') {
216
+ let dataItem = this.data.find(d => String(d.value) === String(option.dataset.value));
217
+ if (dataItem && !dataItem.disabled && ((!isSelected && !dataItem.selected) || (isSelected && dataItem.selected))) {
218
+ if (this.options.max && this.selectedValues.length >= this.options.max && !isSelected) {
219
+ hitMax = true;
220
+ return;
221
+ }
222
+ option.click();
223
+ }
224
+ }
225
+ });
226
+ this._isBatching = false;
227
+ this._updateSelected();
228
+ this._syncOriginalSelect();
229
+ if (this.options.closeListOnItemSelect) {
230
+ if (this.options.search) {
231
+ this.element.querySelector('.multi-select-search').value = '';
232
+ this.element.querySelectorAll('.multi-select-option, .multi-select-group').forEach(opt => opt.style.display = 'flex');
233
+ }
234
+ toggleDropdown(true);
235
+ }
236
+ if (hitMax) this.options.onMaxReached(this.options.max);
237
+ };
238
+ });
239
+ headerElement.onclick = () => toggleDropdown();
240
+ if (this.options.search) {
241
+ let search = this.element.querySelector('.multi-select-search');
242
+ search.oninput = () => {
243
+ let searchValue = search.value.toLowerCase();
244
+ let visibleGroups = new Set();
245
+ this.element.querySelectorAll('.multi-select-option').forEach(option => {
246
+ const text = option.querySelector('.multi-select-option-text').textContent.toLowerCase();
247
+ if (text.includes(searchValue)) {
248
+ option.style.display = 'flex';
249
+ if (option.dataset.group) visibleGroups.add(option.dataset.group);
250
+ } else {
251
+ option.style.display = 'none';
252
+ }
253
+ });
254
+ this.element.querySelectorAll('.multi-select-group').forEach(group => {
255
+ if (visibleGroups.has(group.dataset.group) || group.querySelector('.multi-select-option-text').textContent.toLowerCase().includes(searchValue)) {
256
+ group.style.display = 'flex';
257
+ if (group.querySelector('.multi-select-option-text').textContent.toLowerCase().includes(searchValue)) {
258
+ this.element.querySelectorAll('.multi-select-option').forEach(opt => {
259
+ if (opt.dataset.group === group.dataset.group) opt.style.display = 'flex';
260
+ });
261
+ }
262
+ } else {
263
+ group.style.display = 'none';
264
+ }
265
+ });
266
+ };
267
+ }
268
+ if (this.options.selectAll) {
269
+ let selectAllButton = this.element.querySelector('.multi-select-all');
270
+ selectAllButton.onclick = (e) => {
271
+ e.stopPropagation();
272
+ if (this.element.classList.contains('disabled')) return;
273
+ let isSelected = selectAllButton.classList.contains('multi-select-selected');
274
+ let hitMax = false;
275
+ this._isBatching = true;
276
+ this.element.querySelectorAll('.multi-select-option').forEach(option => {
277
+ if (option.style.display !== 'none') {
278
+ let dataItem = this.data.find(d => String(d.value) === String(option.dataset.value));
279
+ if (dataItem && !dataItem.disabled && ((!isSelected && !dataItem.selected) || (isSelected && dataItem.selected))) {
280
+ if (this.options.max && this.selectedValues.length >= this.options.max && !isSelected) {
281
+ hitMax = true;
282
+ return;
283
+ }
284
+ option.click();
285
+ }
286
+ }
287
+ });
288
+ this._isBatching = false;
289
+ this._updateSelected();
290
+ this._syncOriginalSelect();
291
+ if (this.options.closeListOnItemSelect) {
292
+ if (this.options.search) {
293
+ this.element.querySelector('.multi-select-search').value = '';
294
+ this.element.querySelectorAll('.multi-select-option, .multi-select-group').forEach(opt => opt.style.display = 'flex');
295
+ }
296
+ toggleDropdown(true);
297
+ }
298
+ if (hitMax) this.options.onMaxReached(this.options.max);
299
+ };
300
+ }
301
+ if (this.selectElement.id) {
302
+ if (this.labelClickHandler) {
303
+ document.querySelectorAll(`label[for="${CSS.escape(this.selectElement.id)}"]`).forEach(label => {
304
+ label.removeEventListener('click', this.labelClickHandler);
305
+ });
306
+ }
307
+ this.labelClickHandler = (e) => {
308
+ e.preventDefault();
309
+ toggleDropdown();
310
+ headerElement.focus();
311
+ };
312
+ document.querySelectorAll(`label[for="${CSS.escape(this.selectElement.id)}"]`).forEach(label => {
313
+ label.addEventListener('click', this.labelClickHandler);
314
+ });
315
+ }
316
+ this.element.addEventListener('focusout', (e) => {
317
+ if (!this.element.contains(e.relatedTarget)) {
318
+ if (headerElement.classList.contains('multi-select-header-active')) {
319
+ headerElement.classList.remove('multi-select-header-active');
320
+ this.element.setAttribute('aria-expanded', 'false');
321
+ }
322
+ }
323
+ });
324
+ document.addEventListener('click', this.outsideClickHandler);
325
+ headerElement.addEventListener('keydown', (e) => {
326
+ if (['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(e.key)) {
327
+ e.preventDefault();
328
+ toggleDropdown();
329
+ const firstElement = this.element.querySelector('[role="searchbox"]') || this.element.querySelector('[role="option"]');
330
+ if (firstElement) firstElement.focus();
331
+ }
332
+ });
333
+ this.element.addEventListener('keydown', (e) => {
334
+ if (e.key === 'Escape') {
335
+ toggleDropdown(true);
336
+ headerElement.focus();
337
+ }
338
+ });
339
+ const optionsContainer = this.element.querySelector('.multi-select-options');
340
+ optionsContainer.addEventListener('keydown', (e) => {
341
+ const currentFocused = document.activeElement;
342
+ if (currentFocused.closest('.multi-select-options')) {
343
+ if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
344
+ e.preventDefault();
345
+ const direction = e.key === 'ArrowDown' ? 'nextElementSibling' : 'previousElementSibling';
346
+ let nextElement = currentFocused[direction];
347
+
348
+ while (nextElement && (nextElement.style.display === 'none' || nextElement.style.pointerEvents === 'none' || !nextElement.matches('[role="option"],[role="searchbox"]'))) {
349
+ nextElement = nextElement[direction];
350
+ }
351
+ if (nextElement) nextElement.focus();
352
+ } else if (e.key === 'Enter') {
353
+ if (currentFocused.matches('[role="searchbox"]')) {
354
+ e.preventDefault();
355
+ } else if (currentFocused.matches('[role="option"]')) {
356
+ e.preventDefault();
357
+ currentFocused.click();
358
+ }
359
+ } else if (e.key === ' ' && currentFocused.matches('[role="option"]')) {
360
+ e.preventDefault();
361
+ currentFocused.click();
362
+ }
363
+ }
364
+ });
365
+ }
366
+
367
+ _updateHeader() {
368
+ this.element.querySelectorAll('.multi-select-header-option, .multi-select-header-placeholder').forEach(el => el.remove());
369
+ if (this.selectedValues.length > 0) {
370
+ if (this.options.listAll) {
371
+ this.selectedItems.forEach(item => {
372
+ const el = document.createElement('span');
373
+ el.className = 'multi-select-header-option';
374
+ el.dataset.value = item.value;
375
+ el.innerHTML = item.html ? item.html : this._escapeHTML(item.text);
376
+ this.element.querySelector('.multi-select-header').prepend(el);
377
+ });
378
+ } else {
379
+ this.element.querySelector('.multi-select-header').insertAdjacentHTML('afterbegin', `<span class="multi-select-header-option">${this.selectedValues.length} selected</span>`);
380
+ }
381
+ } else {
382
+ this.element.querySelector('.multi-select-header').insertAdjacentHTML('beforeend', `<span class="multi-select-header-placeholder">${this._escapeHTML(this.placeholder)}</span>`);
383
+ }
384
+ if (this.options.max) {
385
+ this.element.querySelector('.multi-select-header-max').innerHTML = this.selectedValues.length + '/' + this.options.max;
386
+ }
387
+ }
388
+
389
+ _updateSelectAll() {
390
+ if (!this.options.selectAll) return;
391
+ const selectAllBtn = this.element.querySelector('.multi-select-all');
392
+ if (selectAllBtn) {
393
+ const enabledData = this.data.filter(d => !d.disabled);
394
+ const allSelected = enabledData.length > 0 && enabledData.every(d => d.selected);
395
+ if (allSelected) selectAllBtn.classList.add('multi-select-selected');
396
+ else selectAllBtn.classList.remove('multi-select-selected');
397
+ }
398
+ }
399
+
400
+ _updateGroups() {
401
+ this.element.querySelectorAll('.multi-select-group').forEach(groupEl => {
402
+ const groupName = groupEl.dataset.group;
403
+ const enabledItems = this.data.filter(d => d.group === groupName && !d.disabled);
404
+ if (enabledItems.length > 0 && enabledItems.every(d => d.selected)) {
405
+ groupEl.classList.add('multi-select-selected');
406
+ } else {
407
+ groupEl.classList.remove('multi-select-selected');
408
+ }
409
+ });
410
+ }
411
+
412
+ _updateSelected() {
413
+ this._updateHeader();
414
+ this._updateSelectAll();
415
+ this._updateGroups();
416
+ this._validate();
417
+ }
418
+
419
+ _validate() {
420
+ let isValid = true;
421
+ if (this.options.required) isValid = this.selectedValues.length > 0;
422
+ if (this.options.min && this.selectedValues.length < this.options.min) isValid = false;
423
+ if (!isValid) {
424
+ this.element.classList.add('multi-select-invalid');
425
+ if (this.selectElement && this.options.required) this.selectElement.setCustomValidity('Please fill out this field.');
426
+ } else {
427
+ this.element.classList.remove('multi-select-invalid');
428
+ if (this.selectElement) this.selectElement.setCustomValidity('');
429
+ }
430
+ }
431
+
432
+ _buildOriginalSelect() {
433
+ if (!this.selectElement) return;
434
+ this.selectElement.innerHTML = '';
435
+ let groupedData = {};
436
+ this.data.forEach(item => {
437
+ let g = item.group || '';
438
+ if(!groupedData[g]) groupedData[g] =[];
439
+ groupedData[g].push(item);
440
+ });
441
+ for (const[groupName, items] of Object.entries(groupedData)) {
442
+ let parent = this.selectElement;
443
+ if (groupName) {
444
+ let optgroup = document.createElement('optgroup');
445
+ optgroup.label = groupName;
446
+ this.selectElement.appendChild(optgroup);
447
+ parent = optgroup;
448
+ }
449
+ items.forEach(item => {
450
+ let opt = document.createElement('option');
451
+ opt.value = item.value;
452
+ opt.textContent = item.text !== undefined && item.text !== null ? item.text : (item.html ? item.html.replace(/<[^>]*>?/gm, '') : '');
453
+ opt.selected = item.selected;
454
+ opt.disabled = item.disabled || false;
455
+ if(item.html) opt.setAttribute('data-html', item.html);
456
+ parent.appendChild(opt);
457
+ });
458
+ }
459
+ }
460
+
461
+ _syncOriginalSelect() {
462
+ if (!this.selectElement) return;
463
+ let changed = false;
464
+ for (let option of this.selectElement.options) {
465
+ let dataItem = this.data.find(d => String(d.value) === String(option.value));
466
+ if (dataItem && option.selected !== dataItem.selected) {
467
+ option.selected = dataItem.selected;
468
+ changed = true;
469
+ }
470
+ }
471
+ if (changed) {
472
+ this.selectElement.dispatchEvent(new Event('change', { bubbles: true }));
473
+ }
474
+ }
475
+
476
+ _outsideClick(event) {
477
+ if (!this.selectElement.isConnected) {
478
+ document.removeEventListener('click', this.outsideClickHandler);
479
+ return;
480
+ }
481
+ const labelSelector = this.selectElement.id ? `label[for="${CSS.escape(this.selectElement.id)}"]` : null;
482
+ const clickedOnLabel = labelSelector ? event.target.closest(labelSelector) : false;
483
+ if (!this.element.contains(event.target) && !clickedOnLabel) {
484
+ let headerElement = this.element.querySelector('.multi-select-header');
485
+ if (headerElement.classList.contains('multi-select-header-active')) {
486
+ headerElement.classList.remove('multi-select-header-active');
487
+ this.element.setAttribute('aria-expanded', 'false');
488
+ }
489
+ }
490
+ }
491
+
492
+ select(value) {
493
+ const option = Array.from(this.element.querySelectorAll('.multi-select-option')).find(el => String(el.dataset.value) === String(value));
494
+ if (option && !option.classList.contains('multi-select-selected')) option.click();
495
+ }
496
+
497
+ unselect(value) {
498
+ const option = Array.from(this.element.querySelectorAll('.multi-select-option')).find(el => String(el.dataset.value) === String(value));
499
+ if (option && option.classList.contains('multi-select-selected')) option.click();
500
+ }
501
+
502
+ setValues(values) {
503
+ const valArray = Array.isArray(values) ? values :[values];
504
+ const stringValues = valArray.map(String);
505
+ let changed = false;
506
+ this.data.forEach(item => {
507
+ const isSelected = stringValues.includes(String(item.value));
508
+ if (item.selected !== isSelected && !item.disabled) {
509
+ item.selected = isSelected;
510
+ changed = true;
511
+ }
512
+ });
513
+ if (changed) {
514
+ this.refresh();
515
+ this.selectElement.dispatchEvent(new Event('change', { bubbles: true }));
516
+ }
517
+ }
518
+
519
+ disable() {
520
+ this.options.disabled = true;
521
+ this.element.classList.add('disabled');
522
+ this.element.querySelector('.multi-select-header').removeAttribute('tabindex');
523
+ const searchInput = this.element.querySelector('.multi-select-search');
524
+ if (searchInput) searchInput.disabled = true;
525
+ if (this.selectElement) this.selectElement.disabled = true;
526
+ let headerElement = this.element.querySelector('.multi-select-header');
527
+ if (headerElement && headerElement.classList.contains('multi-select-header-active')) {
528
+ headerElement.classList.remove('multi-select-header-active');
529
+ this.element.setAttribute('aria-expanded', 'false');
530
+ }
531
+ }
532
+
533
+ enable() {
534
+ this.options.disabled = false;
535
+ this.element.classList.remove('disabled');
536
+ this.element.querySelector('.multi-select-header').setAttribute('tabindex', '0');
537
+ const searchInput = this.element.querySelector('.multi-select-search');
538
+ if (searchInput) searchInput.disabled = false;
539
+ if (this.selectElement) this.selectElement.disabled = false;
540
+ }
541
+
542
+ destroy() {
543
+ this.element.insertAdjacentElement('beforebegin', this.selectElement);
544
+ this.element.remove();
545
+ if (this.originalStyle) {
546
+ this.selectElement.setAttribute('style', this.originalStyle);
547
+ } else {
548
+ this.selectElement.removeAttribute('style');
549
+ }
550
+ if (this.originalTabIndex !== null) {
551
+ this.selectElement.setAttribute('tabindex', this.originalTabIndex);
552
+ } else {
553
+ this.selectElement.removeAttribute('tabindex');
554
+ }
555
+ if (this.selectElement.form && this.formResetHandler) {
556
+ this.selectElement.form.removeEventListener('reset', this.formResetHandler);
557
+ }
558
+ if (this.selectElement.id && this.labelClickHandler) {
559
+ document.querySelectorAll(`label[for="${CSS.escape(this.selectElement.id)}"]`).forEach(label => {
560
+ label.removeEventListener('click', this.labelClickHandler);
561
+ });
562
+ }
563
+ document.removeEventListener('click', this.outsideClickHandler);
564
+ delete this.selectElement._multiSelect;
565
+ }
566
+
567
+ refresh() {
568
+ this.element.insertAdjacentElement('beforebegin', this.selectElement);
569
+ const newElement = this._template();
570
+ this.element.replaceWith(newElement);
571
+ this.element = newElement;
572
+ this.element.appendChild(this.selectElement);
573
+ this._buildOriginalSelect();
574
+ this._updateSelected();
575
+ this._eventHandlers();
576
+ }
577
+
578
+ addItem(item) {
579
+ this.options.data.push(item);
580
+ this.originalData.push(JSON.parse(JSON.stringify(item)));
581
+ this.refresh();
582
+ }
583
+
584
+ addItems(items) {
585
+ this.options.data.push(...items);
586
+ this.originalData.push(...JSON.parse(JSON.stringify(items)));
587
+ this.refresh();
588
+ }
589
+
590
+ async fetch(url, options = {}) {
591
+ try {
592
+ const response = await window.fetch(url, options);
593
+ if (!response.ok) throw new Error('Network response was not ok');
594
+ const data = await response.json();
595
+ this.addItems(data);
596
+ if (this.options.onload) {
597
+ this.options.onload(data, this.options);
598
+ }
599
+ } catch (error) {
600
+ console.error('MultiSelect Fetch Error:', error);
601
+ }
602
+ }
603
+
604
+ removeItem(value) {
605
+ this.options.data = this.options.data.filter(item => String(item.value) !== String(value));
606
+ this.originalData = this.originalData.filter(item => String(item.value) !== String(value));
607
+ this.refresh();
608
+ }
609
+
610
+ clear() {
611
+ this.options.data =[];
612
+ this.refresh();
613
+ this.selectElement.dispatchEvent(new Event('change', { bubbles: true }));
614
+ }
615
+
616
+ deselectAll() {
617
+ let changed = false;
618
+ this.data.forEach(item => {
619
+ if (item.selected && !item.disabled) {
620
+ item.selected = false;
621
+ changed = true;
622
+ }
623
+ });
624
+ if (changed) {
625
+ this.refresh();
626
+ this.selectElement.dispatchEvent(new Event('change', { bubbles: true }));
627
+ }
628
+ }
629
+
630
+ reset() {
631
+ this.data = JSON.parse(JSON.stringify(this.originalData));
632
+ this.refresh();
633
+ this.selectElement.dispatchEvent(new Event('change', { bubbles: true }));
634
+ }
635
+
636
+ selectAll() {
637
+ let changed = false;
638
+ this.data.forEach(item => {
639
+ if (!item.selected && !item.disabled) {
640
+ item.selected = true;
641
+ changed = true;
642
+ }
643
+ });
644
+ if (changed) {
645
+ this.refresh();
646
+ this.selectElement.dispatchEvent(new Event('change', { bubbles: true }));
647
+ }
648
+ }
649
+
650
+ get selectedValues() { return this.data.filter(d => d.selected).map(d => d.value); }
651
+ get selectedItems() { return this.data.filter(d => d.selected); }
652
+ get data() { return this.options.data; }
653
+ set data(value) { this.options.data = value; }
654
+
655
+ set selectElement(value) { this.options.selectElement = value; }
656
+ get selectElement() { return this.options.selectElement; }
657
+
658
+ set element(value) { this.options.element = value; }
659
+ get element() { return this.options.element; }
660
+
661
+ set placeholder(value) { this.options.placeholder = value; }
662
+ get placeholder() { return this.options.placeholder; }
663
+
664
+ set name(value) { this.options.name = value; }
665
+ get name() { return this.options.name; }
666
+
667
+ set width(value) { this.options.width = value; }
668
+ get width() { return this.options.width; }
669
+
670
+ set height(value) { this.options.height = value; }
671
+ get height() { return this.options.height; }
672
+ }
673
+
674
+ document.querySelectorAll('[data-multi-select]').forEach(select => new MultiSelect(select));
@@ -0,0 +1 @@
1
+ :root{--spacing-smaller:0.1875rem;--spacing-small:0.3125rem;--spacing-medium:0.4375rem;--spacing-large:0.75rem;--font-size:0.75rem;--font-size-large:0.875rem;--font-size-larger:1rem;--line-height:1rem;--line-height-larger:1.25rem;--primary-color:#40c979;--ms-bg:#ffffff;--text-color-dark:#212529;--text-color:#585858;--text-color-light:#65727e;--border-color:#bebebe;--border-color-light:#f1f3f5;--input-placeholder:#65727e;--input-background:#e9e9ed;--input-border:#dee2e6;--input-border-active:#c1c9d0;--input-border-invalid:#e44e4e;--input-outline-invalid:rgba(219, 138, 138, 0.5);--input-color:#e9e9ed;--input-disabled:#f7f7f7;--option-background:#f3f4f7;--checkbox-border:#ced4da;--checkbox-background:#ffffff;--checkbox-active:#ffffff;--input-min-height:2.8125rem;--options-height:40dvh;--border-radius:0.3125rem;--icon-size:0.75rem;--icon-space:1.875rem;--checkbox-size:1rem;--checkbox-radius:0.25rem;--checkbox-thickness:0.125rem}.multi-select[data-theme=dark]{--ms-bg:#2b2b2b;--text-color-dark:#f8f9fa;--text-color:#adb5bd;--text-color-light:#adb5bd;--border-color:#495057;--border-color-light:#495057;--input-background:#343a40;--input-border:#495057;--input-border-active:#6c757d;--option-background:#343a40;--checkbox-background:#2b2b2b;--checkbox-border:#6c757d;--checkbox-active:#ffffff;--input-disabled:#1e1e1e}@media (prefers-color-scheme:dark){.multi-select[data-theme=auto]{--ms-bg:#2b2b2b;--text-color-dark:#f8f9fa;--text-color:#adb5bd;--text-color-light:#adb5bd;--border-color:#495057;--border-color-light:#495057;--input-background:#343a40;--input-border:#495057;--input-border-active:#6c757d;--option-background:#343a40;--checkbox-background:#2b2b2b;--checkbox-border:#6c757d;--checkbox-active:#ffffff;--input-disabled:#1e1e1e}}.multi-select{display:flex;box-sizing:border-box;flex-direction:column;position:relative;width:100%;user-select:none}.multi-select .multi-select-header{background-color:var(--ms-bg);border:1px solid var(--input-border);border-radius:var(--border-radius);padding:var(--spacing-medium) var(--spacing-large);padding-right:var(--icon-space);overflow:hidden;gap:var(--spacing-medium);min-height:var(--input-min-height)}.multi-select .multi-select-header::after{content:"";display:block;position:absolute;top:50%;right:1rem;transform:translateY(-50%);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23949ba3' viewBox='0 0 16 16'%3E%3Cpath d='M8 13.1l-8-8 2.1-2.2 5.9 5.9 5.9-5.9 2.1 2.2z'/%3E%3C/svg%3E");height:var(--icon-size);width:var(--icon-size)}.multi-select .multi-select-header.multi-select-header-active{border-color:var(--input-border-active)}.multi-select .multi-select-header.multi-select-header-active::after{transform:translateY(-50%) rotate(180deg)}.multi-select .multi-select-header.multi-select-header-active+.multi-select-options{display:flex}.multi-select .multi-select-header .multi-select-header-placeholder{color:var(--text-color-light)}.multi-select .multi-select-header .multi-select-header-option{display:inline-flex;align-items:center;background-color:var(--option-background);color:var(--text-color-dark);font-size:var(--font-size-large);padding:var(--spacing-smaller) var(--spacing-small);border-radius:var(--border-radius)}.multi-select .multi-select-header .multi-select-header-max{font-size:var(--font-size-large);color:var(--text-color-light)}.multi-select .multi-select-options{display:none;box-sizing:border-box;flex-flow:wrap;position:absolute;top:100%;left:0;right:0;z-index:999;margin-top:var(--spacing-small);padding:var(--spacing-small);background-color:var(--ms-bg);border-radius:var(--border-radius);box-shadow:0 .25rem .5rem rgba(0,0,0,.15);max-height:var(--options-height);overflow-y:auto;overflow-x:hidden}.multi-select .multi-select-options::-webkit-scrollbar{width:.3125rem}.multi-select .multi-select-options::-webkit-scrollbar-track{background:var(--input-background)}.multi-select .multi-select-options::-webkit-scrollbar-thumb{background:var(--border-color)}.multi-select .multi-select-options::-webkit-scrollbar-thumb:hover{background:var(--input-placeholder)}.multi-select .multi-select-options .multi-select-all,.multi-select .multi-select-options .multi-select-group,.multi-select .multi-select-options .multi-select-option{padding:.5rem var(--spacing-large)}.multi-select .multi-select-options .multi-select-option[data-group]:not([data-group=""]){padding-left:2.5rem}.multi-select .multi-select-options .multi-select-all .multi-select-option-radio,.multi-select .multi-select-options .multi-select-group .multi-select-option-radio,.multi-select .multi-select-options .multi-select-option .multi-select-option-radio{background:var(--checkbox-background);margin-right:var(--spacing-large);height:var(--checkbox-size);width:var(--checkbox-size);border:1px solid var(--checkbox-border);border-radius:var(--checkbox-radius)}.multi-select .multi-select-options .multi-select-all .multi-select-option-text,.multi-select .multi-select-options .multi-select-group .multi-select-option-text,.multi-select .multi-select-options .multi-select-option .multi-select-option-text{box-sizing:border-box;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:inherit;font-size:var(--font-size-larger);line-height:var(--line-height-larger);padding-bottom:1px}.multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-radio,.multi-select .multi-select-options .multi-select-group.multi-select-selected .multi-select-option-radio,.multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-radio{border-color:var(--primary-color);background-color:var(--primary-color)}.multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-radio::after,.multi-select .multi-select-options .multi-select-group.multi-select-selected .multi-select-option-radio::after,.multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-radio::after{content:"";display:block;width:calc(var(--checkbox-size)/ 4);height:calc(var(--checkbox-size)/ 2);border:solid var(--checkbox-active);border-width:0 var(--checkbox-thickness) var(--checkbox-thickness) 0;transform:rotate(45deg) translate(50%,-25%)}.multi-select .multi-select-options .multi-select-all.multi-select-selected .multi-select-option-text,.multi-select .multi-select-options .multi-select-group.multi-select-selected .multi-select-option-text,.multi-select .multi-select-options .multi-select-option.multi-select-selected .multi-select-option-text{color:var(--text-color-dark)}.multi-select .multi-select-options .multi-select-all:active,.multi-select .multi-select-options .multi-select-all:hover,.multi-select .multi-select-options .multi-select-group:active,.multi-select .multi-select-options .multi-select-group:hover,.multi-select .multi-select-options .multi-select-option:active,.multi-select .multi-select-options .multi-select-option:hover{background-color:var(--option-background)}.multi-select .multi-select-options .multi-select-all{border-bottom:1px solid var(--border-color-light);border-radius:0}.multi-select .multi-select-options .multi-select-group{font-weight:700;border-radius:0}.multi-select .multi-select-options .multi-select-search{padding:var(--spacing-medium) var(--spacing-large);border:1px solid var(--input-border);border-radius:var(--border-radius);margin:.625rem;width:calc(100% - 1.25rem);outline:0;font-size:var(--font-size-larger);background-color:var(--ms-bg);color:var(--text-color-dark)}.multi-select .multi-select-options .multi-select-search::placeholder{color:var(--text-color-light)}.multi-select .multi-select-all,.multi-select .multi-select-group,.multi-select .multi-select-header,.multi-select .multi-select-option{display:flex;flex-wrap:wrap;box-sizing:border-box;align-items:center;border-radius:var(--border-radius);cursor:pointer;display:flex;align-items:center;width:100%;font-size:var(--font-size-larger);color:var(--text-color-dark)}.multi-select.disabled{opacity:.6;pointer-events:none;background-color:var(--input-disabled)}.multi-select.multi-select-invalid .multi-select-header{border-color:var(--input-border-invalid);outline:var(--input-outline-invalid) solid 1px}
@@ -0,0 +1 @@
1
+ class MultiSelect{constructor(element,options={}){let defaults={placeholder:"Select item(s)",max:null,min:null,disabled:!1,search:!0,selectAll:!0,listAll:!0,closeListOnItemSelect:!1,name:"",width:"",height:"",dropdownWidth:"",dropdownHeight:"",theme:"auto",required:!1,data:[],onChange:function(){},onSelect:function(){},onUnselect:function(){},onMaxReached:function(){}};this.options=Object.assign(defaults,options),this.selectElement="string"==typeof element?document.querySelector(element):element,this.selectElement._multiSelect&&this.selectElement._multiSelect.destroy(),this.selectElement._multiSelect=this,this.originalStyle=this.selectElement.getAttribute("style")||"",this.originalTabIndex=this.selectElement.getAttribute("tabindex"),this._isBatching=!1;for(const prop in this.selectElement.dataset)void 0!==this.options[prop]&&("boolean"==typeof this.options[prop]||"true"===this.selectElement.dataset[prop]||"false"===this.selectElement.dataset[prop]?this.options[prop]="true"===this.selectElement.dataset[prop]:this.options[prop]=this.selectElement.dataset[prop]);if(this.selectElement.hasAttribute("required")&&(this.options.required=!0),this.selectElement.hasAttribute("disabled")&&(this.options.disabled=!0),this.name=this.selectElement.getAttribute("name")?this.selectElement.getAttribute("name"):"multi-select-"+Math.floor(1e6*Math.random()),!this.options.data.length){let options=this.selectElement.querySelectorAll("option");for(let i=0;i<options.length;i++){let parent=options[i].parentElement,group="optgroup"===parent.tagName.toLowerCase()?parent.getAttribute("label"):"";this.options.data.push({value:options[i].value,text:options[i].textContent,selected:options[i].selected,disabled:options[i].disabled,html:options[i].getAttribute("data-html"),group:group})}}this.originalData=JSON.parse(JSON.stringify(this.options.data)),this.element=this._template(),this.selectElement.insertAdjacentElement("beforebegin",this.element),this.element.appendChild(this.selectElement),this.selectElement.multiple=!0,this.selectElement.setAttribute("tabindex","-1"),this.selectElement.style.position="absolute",this.selectElement.style.left="0",this.selectElement.style.top="0",this.selectElement.style.width="100%",this.selectElement.style.height="100%",this.selectElement.style.opacity="0",this.selectElement.style.zIndex="-1",this.selectElement.style.pointerEvents="none",this.outsideClickHandler=this._outsideClick.bind(this),this._buildOriginalSelect(),this._updateSelected(),this._eventHandlers(),this.options.disabled&&this.disable(),this.selectElement.form&&(this.formResetHandler=()=>setTimeout(()=>this.reset(),0),this.selectElement.form.addEventListener("reset",this.formResetHandler))}_escapeHTML(str){return null!=str?str.toString().replace(/[&<>'"]/g,tag=>({"&":"&amp;","<":"&lt;",">":"&gt;","'":"&#39;",'"':"&quot;"}[tag]||tag)):""}_template(){let optionsHTML="",groupedData={};this.data.forEach(item=>{let g=item.group||"";groupedData[g]||(groupedData[g]=[]),groupedData[g].push(item)});for(const[groupName,items]of Object.entries(groupedData)){if(groupName){let enabledItems=items.filter(i=>!i.disabled),allGroupSelected=enabledItems.length>0&&enabledItems.every(i=>i.selected);optionsHTML+=`\n <div class="multi-select-group${allGroupSelected?" multi-select-selected":""}" data-group="${this._escapeHTML(groupName)}" role="option" tabindex="-1">\n <span class="multi-select-option-radio"></span>\n <span class="multi-select-option-text">${this._escapeHTML(groupName)}</span>\n </div>\n `}items.forEach(item=>{const isSelected=item.selected,isDisabled=item.disabled;optionsHTML+=`\n <div class="multi-select-option${isSelected?" multi-select-selected":""}" data-value="${this._escapeHTML(item.value)}" data-group="${this._escapeHTML(groupName)}" role="option" aria-selected="${isSelected}" tabindex="-1" ${isDisabled?'style="opacity: 0.5; pointer-events: none;"':""}>\n <span class="multi-select-option-radio"></span>\n <span class="multi-select-option-text">${item.html?item.html:this._escapeHTML(item.text)}</span>\n </div>\n `})}let selectAllHTML="";if(this.options.selectAll){let enabledData=this.data.filter(d=>!d.disabled),allSelected;selectAllHTML=`<div class="multi-select-all${enabledData.length>0&&enabledData.every(d=>d.selected)?" multi-select-selected":""}" role="option" tabindex="-1">\n <span class="multi-select-option-radio"></span>\n <span class="multi-select-option-text">Select all</span>\n </div>`}let template=`\n <div class="multi-select ${this.name}"${this.selectElement.id?' id="ms-'+this._escapeHTML(this.selectElement.id)+'"':""} style="${this.width?"width:"+this.width+";":""}${this.height?"height:"+this.height+";":""}" role="combobox" aria-haspopup="listbox" aria-expanded="false" data-theme="${this.options.theme}">\n <div class="multi-select-header" style="${this.width?"width:"+this.width+";":""}${this.height?"height:"+this.height+";":""}" tabindex="0">\n <span class="multi-select-header-max">${this.options.max?this.selectedValues.length+"/"+this.options.max:""}</span>\n <span class="multi-select-header-placeholder">${this._escapeHTML(this.placeholder)}</span>\n </div>\n <div class="multi-select-options" style="${this.options.dropdownWidth?"width:"+this.options.dropdownWidth+";":""}${this.options.dropdownHeight?"height:"+this.options.dropdownHeight+";":""}" role="listbox">\n ${this.options.search?'<input type="text" class="multi-select-search" placeholder="Search..." role="searchbox">':""}\n ${selectAllHTML}\n ${optionsHTML}\n </div>\n </div>\n `,element=document.createElement("div");return element.innerHTML=template,element.firstElementChild}_eventHandlers(){let headerElement=this.element.querySelector(".multi-select-header");const toggleDropdown=(forceClose=!1)=>{this.element.classList.contains("disabled")||(forceClose||headerElement.classList.contains("multi-select-header-active")?(headerElement.classList.remove("multi-select-header-active"),this.element.setAttribute("aria-expanded","false")):(headerElement.classList.add("multi-select-header-active"),this.element.setAttribute("aria-expanded","true")))};if(this.element.querySelectorAll(".multi-select-option").forEach(option=>{option.onclick=e=>{if(e.stopPropagation(),this.element.classList.contains("disabled"))return;let dataItem=this.data.find(d=>String(d.value)===String(option.dataset.value));if(!dataItem||dataItem.disabled)return;let selected=!0;if(option.classList.contains("multi-select-selected"))option.classList.remove("multi-select-selected"),option.setAttribute("aria-selected","false"),dataItem.selected=!1,selected=!1;else{if(this.options.max&&this.selectedValues.length>=this.options.max)return void(this._isBatching||this.options.onMaxReached(this.options.max));option.classList.add("multi-select-selected"),option.setAttribute("aria-selected","true"),dataItem.selected=!0}this._isBatching||(this._updateSelected(),this._syncOriginalSelect(),this.options.closeListOnItemSelect&&(this.options.search&&(this.element.querySelector(".multi-select-search").value="",this.element.querySelectorAll(".multi-select-option, .multi-select-group").forEach(opt=>opt.style.display="flex")),toggleDropdown(!0))),this.options.onChange(option.dataset.value,option.querySelector(".multi-select-option-text").innerHTML,option),selected?this.options.onSelect(option.dataset.value,option.querySelector(".multi-select-option-text").innerHTML,option):this.options.onUnselect(option.dataset.value,option.querySelector(".multi-select-option-text").innerHTML,option)}}),this.element.querySelectorAll(".multi-select-group").forEach(groupEl=>{groupEl.onclick=e=>{if(e.stopPropagation(),this.element.classList.contains("disabled"))return;let groupName=groupEl.dataset.group,isSelected=groupEl.classList.contains("multi-select-selected"),hitMax=!1;this._isBatching=!0,this.element.querySelectorAll(".multi-select-option").forEach(option=>{if(option.dataset.group===groupName&&"none"!==option.style.display){let dataItem=this.data.find(d=>String(d.value)===String(option.dataset.value));if(dataItem&&!dataItem.disabled&&(!isSelected&&!dataItem.selected||isSelected&&dataItem.selected)){if(this.options.max&&this.selectedValues.length>=this.options.max&&!isSelected)return void(hitMax=!0);option.click()}}}),this._isBatching=!1,this._updateSelected(),this._syncOriginalSelect(),this.options.closeListOnItemSelect&&(this.options.search&&(this.element.querySelector(".multi-select-search").value="",this.element.querySelectorAll(".multi-select-option, .multi-select-group").forEach(opt=>opt.style.display="flex")),toggleDropdown(!0)),hitMax&&this.options.onMaxReached(this.options.max)}}),headerElement.onclick=()=>toggleDropdown(),this.options.search){let search=this.element.querySelector(".multi-select-search");search.oninput=()=>{let searchValue=search.value.toLowerCase(),visibleGroups=new Set;this.element.querySelectorAll(".multi-select-option").forEach(option=>{const text=option.querySelector(".multi-select-option-text").textContent.toLowerCase();text.includes(searchValue)?(option.style.display="flex",option.dataset.group&&visibleGroups.add(option.dataset.group)):option.style.display="none"}),this.element.querySelectorAll(".multi-select-group").forEach(group=>{visibleGroups.has(group.dataset.group)||group.querySelector(".multi-select-option-text").textContent.toLowerCase().includes(searchValue)?(group.style.display="flex",group.querySelector(".multi-select-option-text").textContent.toLowerCase().includes(searchValue)&&this.element.querySelectorAll(".multi-select-option").forEach(opt=>{opt.dataset.group===group.dataset.group&&(opt.style.display="flex")})):group.style.display="none"})}}if(this.options.selectAll){let selectAllButton=this.element.querySelector(".multi-select-all");selectAllButton.onclick=e=>{if(e.stopPropagation(),this.element.classList.contains("disabled"))return;let isSelected=selectAllButton.classList.contains("multi-select-selected"),hitMax=!1;this._isBatching=!0,this.element.querySelectorAll(".multi-select-option").forEach(option=>{if("none"!==option.style.display){let dataItem=this.data.find(d=>String(d.value)===String(option.dataset.value));if(dataItem&&!dataItem.disabled&&(!isSelected&&!dataItem.selected||isSelected&&dataItem.selected)){if(this.options.max&&this.selectedValues.length>=this.options.max&&!isSelected)return void(hitMax=!0);option.click()}}}),this._isBatching=!1,this._updateSelected(),this._syncOriginalSelect(),this.options.closeListOnItemSelect&&(this.options.search&&(this.element.querySelector(".multi-select-search").value="",this.element.querySelectorAll(".multi-select-option, .multi-select-group").forEach(opt=>opt.style.display="flex")),toggleDropdown(!0)),hitMax&&this.options.onMaxReached(this.options.max)}}this.selectElement.id&&(this.labelClickHandler&&document.querySelectorAll(`label[for="${CSS.escape(this.selectElement.id)}"]`).forEach(label=>{label.removeEventListener("click",this.labelClickHandler)}),this.labelClickHandler=e=>{e.preventDefault(),toggleDropdown(),headerElement.focus()},document.querySelectorAll(`label[for="${CSS.escape(this.selectElement.id)}"]`).forEach(label=>{label.addEventListener("click",this.labelClickHandler)})),this.element.addEventListener("focusout",e=>{this.element.contains(e.relatedTarget)||headerElement.classList.contains("multi-select-header-active")&&(headerElement.classList.remove("multi-select-header-active"),this.element.setAttribute("aria-expanded","false"))}),document.addEventListener("click",this.outsideClickHandler),headerElement.addEventListener("keydown",e=>{if(["Enter"," ","ArrowDown","ArrowUp"].includes(e.key)){e.preventDefault(),toggleDropdown();const firstElement=this.element.querySelector('[role="searchbox"]')||this.element.querySelector('[role="option"]');firstElement&&firstElement.focus()}}),this.element.addEventListener("keydown",e=>{"Escape"===e.key&&(toggleDropdown(!0),headerElement.focus())});const optionsContainer=this.element.querySelector(".multi-select-options");optionsContainer.addEventListener("keydown",e=>{const currentFocused=document.activeElement;if(currentFocused.closest(".multi-select-options"))if(["ArrowDown","ArrowUp"].includes(e.key)){e.preventDefault();const direction="ArrowDown"===e.key?"nextElementSibling":"previousElementSibling";let nextElement=currentFocused[direction];for(;nextElement&&("none"===nextElement.style.display||"none"===nextElement.style.pointerEvents||!nextElement.matches('[role="option"],[role="searchbox"]'));)nextElement=nextElement[direction];nextElement&&nextElement.focus()}else"Enter"===e.key?currentFocused.matches('[role="searchbox"]')?e.preventDefault():currentFocused.matches('[role="option"]')&&(e.preventDefault(),currentFocused.click()):" "===e.key&&currentFocused.matches('[role="option"]')&&(e.preventDefault(),currentFocused.click())})}_updateHeader(){this.element.querySelectorAll(".multi-select-header-option, .multi-select-header-placeholder").forEach(el=>el.remove()),this.selectedValues.length>0?this.options.listAll?this.selectedItems.forEach(item=>{const el=document.createElement("span");el.className="multi-select-header-option",el.dataset.value=item.value,el.innerHTML=item.html?item.html:this._escapeHTML(item.text),this.element.querySelector(".multi-select-header").prepend(el)}):this.element.querySelector(".multi-select-header").insertAdjacentHTML("afterbegin",`<span class="multi-select-header-option">${this.selectedValues.length} selected</span>`):this.element.querySelector(".multi-select-header").insertAdjacentHTML("beforeend",`<span class="multi-select-header-placeholder">${this._escapeHTML(this.placeholder)}</span>`),this.options.max&&(this.element.querySelector(".multi-select-header-max").innerHTML=this.selectedValues.length+"/"+this.options.max)}_updateSelectAll(){if(!this.options.selectAll)return;const selectAllBtn=this.element.querySelector(".multi-select-all");if(selectAllBtn){const enabledData=this.data.filter(d=>!d.disabled),allSelected=enabledData.length>0&&enabledData.every(d=>d.selected);allSelected?selectAllBtn.classList.add("multi-select-selected"):selectAllBtn.classList.remove("multi-select-selected")}}_updateGroups(){this.element.querySelectorAll(".multi-select-group").forEach(groupEl=>{const groupName=groupEl.dataset.group,enabledItems=this.data.filter(d=>d.group===groupName&&!d.disabled);enabledItems.length>0&&enabledItems.every(d=>d.selected)?groupEl.classList.add("multi-select-selected"):groupEl.classList.remove("multi-select-selected")})}_updateSelected(){this._updateHeader(),this._updateSelectAll(),this._updateGroups(),this._validate()}_validate(){let isValid=!0;this.options.required&&(isValid=this.selectedValues.length>0),this.options.min&&this.selectedValues.length<this.options.min&&(isValid=!1),isValid?(this.element.classList.remove("multi-select-invalid"),this.selectElement&&this.selectElement.setCustomValidity("")):(this.element.classList.add("multi-select-invalid"),this.selectElement&&this.options.required&&this.selectElement.setCustomValidity("Please fill out this field."))}_buildOriginalSelect(){if(!this.selectElement)return;this.selectElement.innerHTML="";let groupedData={};this.data.forEach(item=>{let g=item.group||"";groupedData[g]||(groupedData[g]=[]),groupedData[g].push(item)});for(const[groupName,items]of Object.entries(groupedData)){let parent=this.selectElement;if(groupName){let optgroup=document.createElement("optgroup");optgroup.label=groupName,this.selectElement.appendChild(optgroup),parent=optgroup}items.forEach(item=>{let opt=document.createElement("option");opt.value=item.value,opt.textContent=void 0!==item.text&&null!==item.text?item.text:item.html?item.html.replace(/<[^>]*>?/gm,""):"",opt.selected=item.selected,opt.disabled=item.disabled||!1,item.html&&opt.setAttribute("data-html",item.html),parent.appendChild(opt)})}}_syncOriginalSelect(){if(!this.selectElement)return;let changed=!1;for(let option of this.selectElement.options){let dataItem=this.data.find(d=>String(d.value)===String(option.value));dataItem&&option.selected!==dataItem.selected&&(option.selected=dataItem.selected,changed=!0)}changed&&this.selectElement.dispatchEvent(new Event("change",{bubbles:!0}))}_outsideClick(event){if(!this.selectElement.isConnected)return void document.removeEventListener("click",this.outsideClickHandler);const labelSelector=this.selectElement.id?`label[for="${CSS.escape(this.selectElement.id)}"]`:null,clickedOnLabel=!!labelSelector&&event.target.closest(labelSelector);if(!this.element.contains(event.target)&&!clickedOnLabel){let headerElement=this.element.querySelector(".multi-select-header");headerElement.classList.contains("multi-select-header-active")&&(headerElement.classList.remove("multi-select-header-active"),this.element.setAttribute("aria-expanded","false"))}}select(value){const option=Array.from(this.element.querySelectorAll(".multi-select-option")).find(el=>String(el.dataset.value)===String(value));option&&!option.classList.contains("multi-select-selected")&&option.click()}unselect(value){const option=Array.from(this.element.querySelectorAll(".multi-select-option")).find(el=>String(el.dataset.value)===String(value));option&&option.classList.contains("multi-select-selected")&&option.click()}setValues(values){const valArray=Array.isArray(values)?values:[values],stringValues=valArray.map(String);let changed=!1;this.data.forEach(item=>{const isSelected=stringValues.includes(String(item.value));item.selected===isSelected||item.disabled||(item.selected=isSelected,changed=!0)}),changed&&(this.refresh(),this.selectElement.dispatchEvent(new Event("change",{bubbles:!0})))}disable(){this.options.disabled=!0,this.element.classList.add("disabled"),this.element.querySelector(".multi-select-header").removeAttribute("tabindex");const searchInput=this.element.querySelector(".multi-select-search");searchInput&&(searchInput.disabled=!0),this.selectElement&&(this.selectElement.disabled=!0);let headerElement=this.element.querySelector(".multi-select-header");headerElement&&headerElement.classList.contains("multi-select-header-active")&&(headerElement.classList.remove("multi-select-header-active"),this.element.setAttribute("aria-expanded","false"))}enable(){this.options.disabled=!1,this.element.classList.remove("disabled"),this.element.querySelector(".multi-select-header").setAttribute("tabindex","0");const searchInput=this.element.querySelector(".multi-select-search");searchInput&&(searchInput.disabled=!1),this.selectElement&&(this.selectElement.disabled=!1)}destroy(){this.element.insertAdjacentElement("beforebegin",this.selectElement),this.element.remove(),this.originalStyle?this.selectElement.setAttribute("style",this.originalStyle):this.selectElement.removeAttribute("style"),null!==this.originalTabIndex?this.selectElement.setAttribute("tabindex",this.originalTabIndex):this.selectElement.removeAttribute("tabindex"),this.selectElement.form&&this.formResetHandler&&this.selectElement.form.removeEventListener("reset",this.formResetHandler),this.selectElement.id&&this.labelClickHandler&&document.querySelectorAll(`label[for="${CSS.escape(this.selectElement.id)}"]`).forEach(label=>{label.removeEventListener("click",this.labelClickHandler)}),document.removeEventListener("click",this.outsideClickHandler),delete this.selectElement._multiSelect}refresh(){this.element.insertAdjacentElement("beforebegin",this.selectElement);const newElement=this._template();this.element.replaceWith(newElement),this.element=newElement,this.element.appendChild(this.selectElement),this._buildOriginalSelect(),this._updateSelected(),this._eventHandlers()}addItem(item){this.options.data.push(item),this.originalData.push(JSON.parse(JSON.stringify(item))),this.refresh()}addItems(items){this.options.data.push(...items),this.originalData.push(...JSON.parse(JSON.stringify(items))),this.refresh()}async fetch(url,options={}){try{const response=await window.fetch(url,options);if(!response.ok)throw new Error("Network response was not ok");const data=await response.json();this.addItems(data),this.options.onload&&this.options.onload(data,this.options)}catch(error){console.error("MultiSelect Fetch Error:",error)}}removeItem(value){this.options.data=this.options.data.filter(item=>String(item.value)!==String(value)),this.originalData=this.originalData.filter(item=>String(item.value)!==String(value)),this.refresh()}clear(){this.options.data=[],this.refresh(),this.selectElement.dispatchEvent(new Event("change",{bubbles:!0}))}deselectAll(){let changed=!1;this.data.forEach(item=>{item.selected&&!item.disabled&&(item.selected=!1,changed=!0)}),changed&&(this.refresh(),this.selectElement.dispatchEvent(new Event("change",{bubbles:!0})))}reset(){this.data=JSON.parse(JSON.stringify(this.originalData)),this.refresh(),this.selectElement.dispatchEvent(new Event("change",{bubbles:!0}))}selectAll(){let changed=!1;this.data.forEach(item=>{item.selected||item.disabled||(item.selected=!0,changed=!0)}),changed&&(this.refresh(),this.selectElement.dispatchEvent(new Event("change",{bubbles:!0})))}get selectedValues(){return this.data.filter(d=>d.selected).map(d=>d.value)}get selectedItems(){return this.data.filter(d=>d.selected)}get data(){return this.options.data}set data(value){this.options.data=value}set selectElement(value){this.options.selectElement=value}get selectElement(){return this.options.selectElement}set element(value){this.options.element=value}get element(){return this.options.element}set placeholder(value){this.options.placeholder=value}get placeholder(){return this.options.placeholder}set name(value){this.options.name=value}get name(){return this.options.name}set width(value){this.options.width=value}get width(){return this.options.width}set height(value){this.options.height=value}get height(){return this.options.height}}document.querySelectorAll("[data-multi-select]").forEach(select=>new MultiSelect(select));
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Multi Select Dropdown JS
2
+
3
+ Create powerful user interfaces with our **Multi Select Dropdown**! This tool enhances native select elements, allowing for multiple selections, dynamic content generation, integrated search functionality, and customizable UI without any dependencies. No jQuery or other library is required!
4
+
5
+ The complete guide and reference is available here: [https://codeshack.io/multi-select-dropdown-html-javascript/](https://codeshack.io/multi-select-dropdown-html-javascript/)
6
+
7
+ Demo: [https://codeshack.io/multi-select-dropdown-js/](https://codeshack.io/multi-select-dropdown-js/)
8
+
9
+ ## Features
10
+ - **Multiple Selections**: Users can select more than one option in the dropdown.
11
+ - **OptGroups & Sublists**: Natively parses HTML `<optgroup>` tags and automatically generates master toggle switches for groups.
12
+ - **Native Form Validation**: Fully supports the HTML5 `required` attribute with perfectly positioned browser tooltips.
13
+ - **Dark Mode & Themes**: Built-in support for `auto` (follows OS preference), `light`, and `dark` themes.
14
+ - **Async Data Fetching**: Easily load external JSON arrays from APIs using the built-in `.fetch()` method.
15
+ - **Search Functionality**: Includes a built-in search to find options quickly.
16
+ - **Dynamic & Reactive**: Update options dynamically via JS setters. The UI instantly reacts to data changes.
17
+ - **Secure & Accessible**: Built-in XSS protection, memory-leak proof, framework-agnostic, and fully keyboard navigable.
18
+ - **Lightweight**: Lightweight in size and does not depend on other libraries.
19
+
20
+ ## Screenshot
21
+
22
+ ![Screenshot of Multi Select Dropdown](assets/screenshot.png)
23
+
24
+ ## Quick Start
25
+ Clone the repository and include the CSS and JavaScript files in your project.
26
+
27
+ Add the following to the **head** tag:
28
+ ```html
29
+ <link rel="stylesheet" href="path/to/multi-select-dropdown.css">
30
+ ```
31
+ Append the following to the **body** tag:
32
+ ```html
33
+ <script src="path/to/multi-select-dropdown.js"></script>
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ Here's a simple example to add the multi-select dropdown to your project.
39
+
40
+ *(Tip: Always append `[]` to your `name` attribute if you are submitting data to a backend like PHP!)*
41
+
42
+ ```html
43
+ <select id="example-multi-select" name="options[]" data-placeholder="Select options" multiple data-multi-select>
44
+ <option value="option1">Option 1</option>
45
+ <option value="option2">Option 2</option>
46
+ <!-- more options -->
47
+ </select>
48
+ ```
49
+ Or initialize it manually via JavaScript:
50
+ ```html
51
+ <select id="example-multi-select" name="options[]" multiple>
52
+ <option value="option1">Option 1</option>
53
+ <option value="option2">Option 2</option>
54
+ <!-- more options -->
55
+ </select>
56
+
57
+ <script src="path/to/multi-select-dropdown.js"></script>
58
+ <script>
59
+ new MultiSelect(document.getElementById('example-multi-select'), {
60
+ placeholder: 'Select options',
61
+ theme: 'auto'
62
+ });
63
+ </script>
64
+ ```
65
+
66
+ ## Configuration
67
+
68
+ You can pass a settings object to customize the dropdown behavior and styling:
69
+
70
+ ```javascript
71
+ new MultiSelect('#example-multi-select', {
72
+ placeholder: 'Select options',
73
+ theme: 'auto', // 'auto', 'light', or 'dark'
74
+ min: 1, // Minimum number of items required
75
+ max: 5, // Maximum number of items that can be selected
76
+ search: true, // Enable the search box
77
+ selectAll: true, // Add a "Select All" toggle
78
+ listAll: true, // If false, shows "X selected" instead of listing tags
79
+ closeListOnItemSelect: false, // Auto-close the dropdown after selection
80
+ required: true, // Enforce native HTML5 form validation
81
+
82
+ onChange: function(value, text, element) {
83
+ console.log('Change:', value, text, element);
84
+ },
85
+ onSelect: function(value, text, element) {
86
+ console.log('Selected:', value, text, element);
87
+ },
88
+ onUnselect: function(value, text, element) {
89
+ console.log('Unselected:', value, text, element);
90
+ },
91
+ onMaxReached: function(max) {
92
+ console.log('Maximum selections reached:', max);
93
+ }
94
+ });
95
+ ```
96
+
97
+ ### Dynamically Adding Options & Groups
98
+
99
+ You can dynamically populate the dropdown with grouped data, disabled options, and safe custom HTML injection:
100
+
101
+ ```javascript
102
+ const mySelect = new MultiSelect('#example-multi-select', {
103
+ data:[
104
+ {
105
+ value: 'opt1',
106
+ text: 'Option 1',
107
+ group: 'Basic Settings'
108
+ },
109
+ {
110
+ value: 'opt2',
111
+ html: '<strong style="color: red;">Option 2 with HTML!</strong>',
112
+ group: 'Basic Settings'
113
+ },
114
+ {
115
+ value: 'opt3',
116
+ text: 'Option 3',
117
+ selected: true,
118
+ group: 'Advanced Settings'
119
+ },
120
+ {
121
+ value: 'opt4',
122
+ text: 'Locked Option',
123
+ disabled: true // Prevents user interaction
124
+ }
125
+ ],
126
+ placeholder: 'Select options',
127
+ search: true,
128
+ selectAll: true
129
+ });
130
+ ```
131
+
132
+ ### Asynchronous Data Fetching
133
+
134
+ Loading data from a remote endpoint is handled natively:
135
+
136
+ ```javascript
137
+ const asyncSelect = new MultiSelect('#example-multi-select', {
138
+ placeholder: 'Loading remote data...'
139
+ });
140
+
141
+ // Fetches a JSON array formatted like the data object above
142
+ asyncSelect.fetch('https://api.yoursite.com/endpoint');
143
+ ```
144
+
145
+ ## License
146
+
147
+ Distributed under the MIT License. See `LICENSE` for more information.
148
+
149
+ ## Contact
150
+
151
+ David Adams - [info@codeshack.io](mailto:info@codeshack.io)
152
+
153
+ GitHub: [https://github.com/codeshackio/multi-select-dropdown-js](https://github.com/codeshackio/multi-select-dropdown-js)
154
+
155
+ X (Twitter): [https://twitter.com/codeshackio](https://twitter.com/codeshackio)
156
+
157
+ Feel free to open an issue or submit pull requests.
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "multi-select-dropdown-js",
3
+ "version": "1.0.3",
4
+ "description": "A lightweight, dependency-free Multi Select Dropdown in Vanilla JavaScript.",
5
+ "main": "MultiSelect.js",
6
+ "unpkg": "MultiSelect.min.js",
7
+ "jsdelivr": "MultiSelect.min.js",
8
+ "style": "MultiSelect.min.css",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/codeshackio/multi-select-dropdown-js.git"
12
+ },
13
+ "keywords":[
14
+ "select",
15
+ "multi-select",
16
+ "dropdown",
17
+ "vanilla-js",
18
+ "no-jquery",
19
+ "optgroup",
20
+ "form-validation"
21
+ ],
22
+ "author": "David Adams <info@codeshack.io> (https://codeshack.io)",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/codeshackio/multi-select-dropdown-js/issues"
26
+ },
27
+ "homepage": "https://codeshack.io/multi-select-dropdown-html-javascript/"
28
+ }