slim-select 3.4.2 → 3.5.0
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/dist/classes.d.ts +46 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/index.d.ts +53 -318
- package/dist/index.umd.d.ts +2 -0
- package/dist/react/index.d.ts +4 -318
- package/dist/react/index.js +1459 -1696
- package/dist/react/index.umd.js +2 -2
- package/dist/react/react.d.ts +17 -0
- package/dist/react/react.test.d.ts +1 -0
- package/dist/react/slim-select/react/index.d.ts +4 -0
- package/dist/render.d.ts +88 -0
- package/dist/select.d.ts +38 -0
- package/dist/settings.d.ts +39 -0
- package/dist/slimselect.cjs.js +2 -2
- package/dist/slimselect.cjs.js.map +1 -1
- package/dist/slimselect.css +1 -1
- package/dist/slimselect.css.map +1 -1
- package/dist/slimselect.es.js +1233 -1384
- package/dist/slimselect.es.js.map +1 -1
- package/dist/slimselect.iife.js +2 -2
- package/dist/slimselect.iife.js.map +1 -1
- package/dist/slimselect.js +2 -2
- package/dist/slimselect.js.map +1 -1
- package/dist/src/docs/main.d.ts +0 -0
- package/dist/src/docs/router.d.ts +2 -0
- package/dist/src/docs/store.d.ts +12 -0
- package/dist/store.d.ts +53 -0
- package/dist/vue/index.d.ts +4 -318
- package/dist/vue/index.js +1335 -1503
- package/dist/vue/index.umd.js +1 -1
- package/dist/vue/slim-select/vue/index.d.ts +4 -0
- package/package.json +30 -36
- package/src/slim-select/slimselect.scss +34 -5
- package/.nvmrc +0 -1
- package/.prettierignore +0 -1
- package/.prettierrc +0 -7
- package/CODE_OF_CONDUCT.md +0 -46
- package/CONTRIBUTING.md +0 -1
- package/cdn.html +0 -21
- package/docs.go +0 -12
- package/example.html +0 -25
- package/index.html +0 -76
- package/release.js +0 -191
- package/sitemap.js +0 -108
- package/src/slim-select/accessibility.test.ts +0 -782
- package/src/slim-select/classes.test.ts +0 -73
- package/src/slim-select/classes.ts +0 -117
- package/src/slim-select/helpers.test.ts +0 -162
- package/src/slim-select/helpers.ts +0 -61
- package/src/slim-select/index.test.ts +0 -1356
- package/src/slim-select/index.ts +0 -594
- package/src/slim-select/index.umd.ts +0 -8
- package/src/slim-select/react/index.tsx +0 -9
- package/src/slim-select/react/react.test.tsx +0 -498
- package/src/slim-select/react/react.tsx +0 -209
- package/src/slim-select/render.test.ts +0 -1877
- package/src/slim-select/render.ts +0 -1765
- package/src/slim-select/select.test.ts +0 -683
- package/src/slim-select/select.ts +0 -659
- package/src/slim-select/settings.test.ts +0 -285
- package/src/slim-select/settings.ts +0 -78
- package/src/slim-select/store.test.ts +0 -1007
- package/src/slim-select/store.ts +0 -433
- package/src/slim-select/vue/index.ts +0 -9
- package/src/slim-select/vue/vue.test.ts +0 -1091
- package/src/slim-select/vue/vue.vue +0 -224
- package/tsconfig.json +0 -26
- package/vite.config.docs.mts +0 -29
- package/vite.config.lib.es-cjs.ts +0 -40
- package/vite.config.lib.umd-iife.ts +0 -34
- package/vite.config.react.mts +0 -41
- package/vite.config.vue.mts +0 -40
- package/vitest.config.ts +0 -30
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
import { kebabCase, hasClassInTree } from './helpers'
|
|
2
|
-
import { Optgroup, Option } from './store'
|
|
3
|
-
|
|
4
|
-
export default class Select {
|
|
5
|
-
public select: HTMLSelectElement
|
|
6
|
-
|
|
7
|
-
// Mutation observer fields
|
|
8
|
-
public onValueChange?: (value: Option[]) => void
|
|
9
|
-
public onClassChange?: (classes: string[]) => void
|
|
10
|
-
public onDisabledChange?: (disabled: boolean) => void
|
|
11
|
-
public onOptionsChange?: (data: (Option | Optgroup)[]) => void
|
|
12
|
-
public onLabelClick?: () => void
|
|
13
|
-
|
|
14
|
-
// Change observers
|
|
15
|
-
private listen: boolean = false
|
|
16
|
-
private observer: MutationObserver | null = null
|
|
17
|
-
private isUpdating: boolean = false
|
|
18
|
-
private pendingOptionsChange: (Option | Optgroup)[] | null = null
|
|
19
|
-
|
|
20
|
-
// Event handlers for preventing native select behavior (especially on iOS Safari)
|
|
21
|
-
private preventNativeSelect: ((e: Event) => void) | null = null
|
|
22
|
-
private preventNativeSelectMousedown: ((e: Event) => void) | null = null
|
|
23
|
-
private preventNativeSelectFocus: ((e: Event) => void) | null = null
|
|
24
|
-
|
|
25
|
-
constructor(select: HTMLSelectElement) {
|
|
26
|
-
this.select = select
|
|
27
|
-
|
|
28
|
-
this.valueChange = this.valueChange.bind(this)
|
|
29
|
-
// Add change event listener
|
|
30
|
-
this.select.addEventListener('change', this.valueChange, {
|
|
31
|
-
// allow bubbling of event
|
|
32
|
-
passive: true
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
// Initiate mutation observer
|
|
36
|
-
this.observer = new MutationObserver(this.observeCall.bind(this))
|
|
37
|
-
|
|
38
|
-
// Start listening for changes
|
|
39
|
-
this.changeListen(true)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public enable(): void {
|
|
43
|
-
this.select.disabled = false
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
public disable(): void {
|
|
47
|
-
this.select.disabled = true
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
public hideUI(): void {
|
|
51
|
-
this.select.tabIndex = -1
|
|
52
|
-
// Visually hide but keep focusable for form validation
|
|
53
|
-
// Use 1px dimensions so browser validation popup can display
|
|
54
|
-
this.select.style.position = 'absolute'
|
|
55
|
-
this.select.style.width = '1px'
|
|
56
|
-
this.select.style.height = '1px'
|
|
57
|
-
this.select.style.opacity = '0'
|
|
58
|
-
this.select.style.overflow = 'hidden'
|
|
59
|
-
this.select.style.pointerEvents = 'none'
|
|
60
|
-
// Remove any spacing or borders that could affect layout
|
|
61
|
-
this.select.style.margin = '0'
|
|
62
|
-
this.select.style.padding = '0'
|
|
63
|
-
this.select.style.borderWidth = '0'
|
|
64
|
-
// Clip to completely hide the 1px
|
|
65
|
-
this.select.style.clip = 'rect(0 0 0 0)'
|
|
66
|
-
this.select.setAttribute('aria-hidden', 'true')
|
|
67
|
-
|
|
68
|
-
// Prevent native select from opening when label is clicked (especially on iOS Safari)
|
|
69
|
-
// iOS Safari programmatically focuses/clicks the select when label is clicked,
|
|
70
|
-
// bypassing pointer-events: none, so we need to prevent these events
|
|
71
|
-
// Only add listeners if they haven't been added yet
|
|
72
|
-
if (!this.preventNativeSelect) {
|
|
73
|
-
this.preventNativeSelect = (e: Event) => {
|
|
74
|
-
e.preventDefault()
|
|
75
|
-
e.stopPropagation()
|
|
76
|
-
e.stopImmediatePropagation()
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.preventNativeSelectMousedown = (e: Event) => {
|
|
80
|
-
e.preventDefault()
|
|
81
|
-
e.stopPropagation()
|
|
82
|
-
e.stopImmediatePropagation()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
this.preventNativeSelectFocus = (e: Event) => {
|
|
86
|
-
e.preventDefault()
|
|
87
|
-
e.stopPropagation()
|
|
88
|
-
e.stopImmediatePropagation()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Add event listeners with capture phase to catch events before they reach the select
|
|
92
|
-
this.select.addEventListener('click', this.preventNativeSelect, { capture: true, passive: false })
|
|
93
|
-
this.select.addEventListener('mousedown', this.preventNativeSelectMousedown, { capture: true, passive: false })
|
|
94
|
-
this.select.addEventListener('focus', this.preventNativeSelectFocus, { capture: true, passive: false })
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
public showUI(): void {
|
|
99
|
-
this.select.removeAttribute('tabindex')
|
|
100
|
-
// Reset visual hiding styles
|
|
101
|
-
this.select.style.position = ''
|
|
102
|
-
this.select.style.width = ''
|
|
103
|
-
this.select.style.height = ''
|
|
104
|
-
this.select.style.opacity = ''
|
|
105
|
-
this.select.style.overflow = ''
|
|
106
|
-
this.select.style.pointerEvents = ''
|
|
107
|
-
this.select.style.margin = ''
|
|
108
|
-
this.select.style.padding = ''
|
|
109
|
-
this.select.style.borderWidth = ''
|
|
110
|
-
this.select.style.clip = ''
|
|
111
|
-
this.select.removeAttribute('aria-hidden')
|
|
112
|
-
|
|
113
|
-
// Remove event listeners that prevent native select behavior
|
|
114
|
-
if (this.preventNativeSelect) {
|
|
115
|
-
this.select.removeEventListener('click', this.preventNativeSelect, { capture: true })
|
|
116
|
-
this.preventNativeSelect = null
|
|
117
|
-
}
|
|
118
|
-
if (this.preventNativeSelectMousedown) {
|
|
119
|
-
this.select.removeEventListener('mousedown', this.preventNativeSelectMousedown, { capture: true })
|
|
120
|
-
this.preventNativeSelectMousedown = null
|
|
121
|
-
}
|
|
122
|
-
if (this.preventNativeSelectFocus) {
|
|
123
|
-
this.select.removeEventListener('focus', this.preventNativeSelectFocus, { capture: true })
|
|
124
|
-
this.preventNativeSelectFocus = null
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
public changeListen(listen: boolean) {
|
|
129
|
-
this.listen = listen
|
|
130
|
-
|
|
131
|
-
// Start listening for changes
|
|
132
|
-
if (listen) {
|
|
133
|
-
if (this.observer) {
|
|
134
|
-
this.observer.observe(this.select, {
|
|
135
|
-
subtree: true, // subtree for optgroups options
|
|
136
|
-
childList: true, // children changes
|
|
137
|
-
attributes: true // attributes changes
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Stop listening for changes
|
|
143
|
-
if (!listen) {
|
|
144
|
-
if (this.observer) {
|
|
145
|
-
this.observer.disconnect()
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// This function get triggers when the select value changes
|
|
151
|
-
// and will call the onValueChange function if it exists
|
|
152
|
-
public valueChange(ev: Event): boolean {
|
|
153
|
-
if (this.listen && this.onValueChange) {
|
|
154
|
-
this.onValueChange(this.getSelectedOptions())
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Allow bubbling back to other change event listeners
|
|
158
|
-
return true
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private observeCall(mutations: MutationRecord[]): void {
|
|
162
|
-
// If we are not listening, do nothing.
|
|
163
|
-
if (!this.listen) {
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let classChanged = false
|
|
168
|
-
let disabledChanged = false
|
|
169
|
-
let optgroupOptionChanged = false
|
|
170
|
-
let selectionChanged = false
|
|
171
|
-
|
|
172
|
-
// Loop through mutations and check various things
|
|
173
|
-
for (const m of mutations) {
|
|
174
|
-
// Check if its the select
|
|
175
|
-
if (m.target === this.select) {
|
|
176
|
-
// Check if disabled has changed
|
|
177
|
-
if (m.attributeName === 'disabled') {
|
|
178
|
-
disabledChanged = true
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Check if class has changed
|
|
182
|
-
if (m.attributeName === 'class') {
|
|
183
|
-
classChanged = true
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (m.type === 'childList') {
|
|
187
|
-
for (const n of Array.from(m.addedNodes)) {
|
|
188
|
-
if (n.nodeName === 'OPTION' && (<HTMLOptionElement>n).value === this.select.value) {
|
|
189
|
-
// we added a new option that's now the select value
|
|
190
|
-
selectionChanged = true
|
|
191
|
-
break
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// options changed, so we need the optionsChange event to fire
|
|
196
|
-
optgroupOptionChanged = true
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Check if its an optgroup or option
|
|
201
|
-
if (m.target.nodeName === 'OPTGROUP' || m.target.nodeName === 'OPTION') {
|
|
202
|
-
optgroupOptionChanged = true
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// If class has changed then call the class change function
|
|
207
|
-
if (classChanged && this.onClassChange) {
|
|
208
|
-
this.onClassChange(this.select.className.split(' '))
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// If disabled has changed then call the disabled change function
|
|
212
|
-
if (disabledChanged && this.onDisabledChange) {
|
|
213
|
-
this.changeListen(false)
|
|
214
|
-
this.onDisabledChange(this.select.disabled)
|
|
215
|
-
this.changeListen(true)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// If optgroup or option has changed then call the select change function
|
|
219
|
-
if (optgroupOptionChanged && this.onOptionsChange) {
|
|
220
|
-
// If we're currently updating, queue this change to process after update completes
|
|
221
|
-
if (this.isUpdating) {
|
|
222
|
-
// Only queue if the select element actually has options (not empty from updateOptions clearing)
|
|
223
|
-
// Check the select element directly, not getData() which might return stale data
|
|
224
|
-
if (this.select.options.length > 0) {
|
|
225
|
-
const currentData = this.getData()
|
|
226
|
-
// Only queue if we have actual data
|
|
227
|
-
if (currentData.length > 0) {
|
|
228
|
-
this.pendingOptionsChange = currentData
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (selectionChanged) {
|
|
232
|
-
this.select.dispatchEvent(new Event('change'))
|
|
233
|
-
}
|
|
234
|
-
return
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
this.changeListen(false)
|
|
238
|
-
this.onOptionsChange(this.getData())
|
|
239
|
-
this.changeListen(true)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (selectionChanged) {
|
|
243
|
-
this.select.dispatchEvent(new Event('change'))
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// From the select element pull optgroup and options into data
|
|
248
|
-
public getData(): (Option | Optgroup)[] {
|
|
249
|
-
let data = []
|
|
250
|
-
|
|
251
|
-
// Loop through nodes and get data
|
|
252
|
-
const nodes = this.select.childNodes as any as HTMLOptGroupElement[] | HTMLOptionElement[]
|
|
253
|
-
for (const n of nodes) {
|
|
254
|
-
// Optgroup
|
|
255
|
-
if (n.nodeName === 'OPTGROUP') {
|
|
256
|
-
data.push(this.getDataFromOptgroup(n as HTMLOptGroupElement))
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Option
|
|
260
|
-
if (n.nodeName === 'OPTION') {
|
|
261
|
-
data.push(this.getDataFromOption(n as HTMLOptionElement))
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return data
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
public getDataFromOptgroup(optgroup: HTMLOptGroupElement): Optgroup {
|
|
269
|
-
let data = {
|
|
270
|
-
id: optgroup.id,
|
|
271
|
-
label: optgroup.label,
|
|
272
|
-
selectAll: optgroup.dataset ? optgroup.dataset.selectall === 'true' : false,
|
|
273
|
-
selectAllText: optgroup.dataset ? optgroup.dataset.selectalltext : 'Select all',
|
|
274
|
-
closable: optgroup.dataset ? optgroup.dataset.closable : 'off',
|
|
275
|
-
options: []
|
|
276
|
-
} as Optgroup
|
|
277
|
-
|
|
278
|
-
const options = optgroup.childNodes as any as HTMLOptionElement[]
|
|
279
|
-
for (const o of options) {
|
|
280
|
-
if (o.nodeName === 'OPTION') {
|
|
281
|
-
data.options!.push(this.getDataFromOption(o as HTMLOptionElement))
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return data
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// From passed in option pull pieces of usable information
|
|
289
|
-
public getDataFromOption(option: HTMLOptionElement): Option {
|
|
290
|
-
return {
|
|
291
|
-
id: option.id,
|
|
292
|
-
value: option.value,
|
|
293
|
-
text: option.text,
|
|
294
|
-
html: option.dataset && option.dataset.html ? option.dataset.html : '',
|
|
295
|
-
defaultSelected: option.defaultSelected,
|
|
296
|
-
selected: option.selected,
|
|
297
|
-
display: option.style.display !== 'none',
|
|
298
|
-
disabled: option.disabled,
|
|
299
|
-
mandatory: option.dataset ? option.dataset.mandatory === 'true' : false,
|
|
300
|
-
placeholder: option.dataset.placeholder === 'true',
|
|
301
|
-
class: option.className,
|
|
302
|
-
style: option.style.cssText,
|
|
303
|
-
data: option.dataset
|
|
304
|
-
} as Option
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
public getSelectedOptions(): Option[] {
|
|
308
|
-
let options = []
|
|
309
|
-
|
|
310
|
-
// Loop through options and set selected
|
|
311
|
-
const opts = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[]
|
|
312
|
-
for (const o of opts) {
|
|
313
|
-
if (o.nodeName === 'OPTGROUP') {
|
|
314
|
-
const optgroupOptions = o.childNodes as any as HTMLOptionElement[]
|
|
315
|
-
for (const oo of optgroupOptions) {
|
|
316
|
-
if (oo.nodeName === 'OPTION') {
|
|
317
|
-
const option = oo as HTMLOptionElement
|
|
318
|
-
if (option.selected) {
|
|
319
|
-
options.push(this.getDataFromOption(option))
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (o.nodeName === 'OPTION') {
|
|
326
|
-
const option = o as HTMLOptionElement
|
|
327
|
-
if (option.selected) {
|
|
328
|
-
options.push(this.getDataFromOption(option))
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return options
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
public getSelectedValues(): string[] {
|
|
337
|
-
return this.getSelectedOptions().map((option) => option.value)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
public setSelected(ids: string[]): void {
|
|
341
|
-
// Stop listening to changes
|
|
342
|
-
this.changeListen(false)
|
|
343
|
-
|
|
344
|
-
// Loop through options and set selected
|
|
345
|
-
const options = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[]
|
|
346
|
-
for (const o of options) {
|
|
347
|
-
if (o.nodeName === 'OPTGROUP') {
|
|
348
|
-
const optgroup = o as HTMLOptGroupElement
|
|
349
|
-
const optgroupOptions = optgroup.childNodes as any as HTMLOptionElement[]
|
|
350
|
-
for (const oo of optgroupOptions) {
|
|
351
|
-
if (oo.nodeName === 'OPTION') {
|
|
352
|
-
const option = oo as HTMLOptionElement
|
|
353
|
-
option.selected = ids.includes(option.id)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (o.nodeName === 'OPTION') {
|
|
359
|
-
const option = o as HTMLOptionElement
|
|
360
|
-
option.selected = ids.includes(option.id)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Stop listening to changes
|
|
365
|
-
this.changeListen(true)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Set selected options by value instead of id
|
|
369
|
-
// This is useful when the id is not known
|
|
370
|
-
// and only the value is known
|
|
371
|
-
// but the value is not unique and can be duplicated
|
|
372
|
-
public setSelectedByValue(values: string[]): void {
|
|
373
|
-
// Stop listening to changes
|
|
374
|
-
this.changeListen(false)
|
|
375
|
-
|
|
376
|
-
// Loop through options and set selected
|
|
377
|
-
const options = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[]
|
|
378
|
-
for (const o of options) {
|
|
379
|
-
if (o.nodeName === 'OPTGROUP') {
|
|
380
|
-
const optgroup = o as HTMLOptGroupElement
|
|
381
|
-
const optgroupOptions = optgroup.childNodes as any as HTMLOptionElement[]
|
|
382
|
-
for (const oo of optgroupOptions) {
|
|
383
|
-
if (oo.nodeName === 'OPTION') {
|
|
384
|
-
const option = oo as HTMLOptionElement
|
|
385
|
-
option.selected = values.includes(option.value)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (o.nodeName === 'OPTION') {
|
|
391
|
-
const option = o as HTMLOptionElement
|
|
392
|
-
option.selected = values.includes(option.value)
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Stop listening to changes
|
|
397
|
-
this.changeListen(true)
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
public updateSelect(id?: string, style?: string, classes?: string[]): void {
|
|
401
|
-
// Stop listening to changes
|
|
402
|
-
this.changeListen(false)
|
|
403
|
-
|
|
404
|
-
// Update id, only if the id isnt already set
|
|
405
|
-
if (id) {
|
|
406
|
-
this.select.dataset.id = id
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Update style
|
|
410
|
-
if (style) {
|
|
411
|
-
this.select.style.cssText = style
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Update classes
|
|
415
|
-
if (classes) {
|
|
416
|
-
this.select.className = ''
|
|
417
|
-
classes.forEach((c) => {
|
|
418
|
-
if (c.trim() !== '') {
|
|
419
|
-
this.select.classList.add(c.trim())
|
|
420
|
-
}
|
|
421
|
-
})
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Start listening to changes
|
|
425
|
-
this.changeListen(true)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
public updateOptions(data: (Option | Optgroup)[]): void {
|
|
429
|
-
// Don't update if data is empty (prevents clearing select when no data)
|
|
430
|
-
if (!data || data.length === 0) {
|
|
431
|
-
return
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Set updating flag FIRST to prevent mutation observer from processing during update
|
|
435
|
-
// This must be set before stopping listening so any queued mutations see the flag
|
|
436
|
-
this.isUpdating = true
|
|
437
|
-
this.pendingOptionsChange = null
|
|
438
|
-
|
|
439
|
-
// Stop listening to changes
|
|
440
|
-
this.changeListen(false)
|
|
441
|
-
|
|
442
|
-
// Clear out select
|
|
443
|
-
this.select.innerHTML = ''
|
|
444
|
-
|
|
445
|
-
for (const d of data) {
|
|
446
|
-
if (d instanceof Optgroup) {
|
|
447
|
-
this.select.appendChild(this.createOptgroup(d))
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (d instanceof Option) {
|
|
451
|
-
this.select.appendChild(this.createOption(d))
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Trigger change event on original select
|
|
456
|
-
this.select.dispatchEvent(new Event('change', { bubbles: true }))
|
|
457
|
-
|
|
458
|
-
// Start listening to changes
|
|
459
|
-
this.changeListen(true)
|
|
460
|
-
|
|
461
|
-
// Clear updating flag
|
|
462
|
-
this.isUpdating = false
|
|
463
|
-
|
|
464
|
-
// Process any pending options change that was queued during the update
|
|
465
|
-
// Only process if it has data (don't process empty data from innerHTML clearing)
|
|
466
|
-
if (this.pendingOptionsChange !== null) {
|
|
467
|
-
const pending: (Option | Optgroup)[] = this.pendingOptionsChange
|
|
468
|
-
if (pending.length > 0 && this.onOptionsChange) {
|
|
469
|
-
this.pendingOptionsChange = null
|
|
470
|
-
// Process the queued change
|
|
471
|
-
this.changeListen(false)
|
|
472
|
-
this.onOptionsChange(pending)
|
|
473
|
-
this.changeListen(true)
|
|
474
|
-
} else {
|
|
475
|
-
// Clear pending if it was empty
|
|
476
|
-
this.pendingOptionsChange = null
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
public createOptgroup(optgroup: Optgroup): HTMLOptGroupElement {
|
|
482
|
-
const optgroupEl = document.createElement('optgroup')
|
|
483
|
-
optgroupEl.id = optgroup.id
|
|
484
|
-
optgroupEl.label = optgroup.label
|
|
485
|
-
if (optgroup.selectAll) {
|
|
486
|
-
optgroupEl.dataset.selectAll = 'true'
|
|
487
|
-
}
|
|
488
|
-
if (optgroup.closable !== 'off') {
|
|
489
|
-
optgroupEl.dataset.closable = optgroup.closable
|
|
490
|
-
}
|
|
491
|
-
if (optgroup.options) {
|
|
492
|
-
for (const option of optgroup.options) {
|
|
493
|
-
optgroupEl.appendChild(this.createOption(option as Option))
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
return optgroupEl
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
public createOption(info: Option): HTMLOptionElement {
|
|
500
|
-
const optionEl = document.createElement('option')
|
|
501
|
-
optionEl.id = info.id
|
|
502
|
-
optionEl.value = info.value
|
|
503
|
-
optionEl.textContent = info.text
|
|
504
|
-
if (info.html !== '') {
|
|
505
|
-
optionEl.setAttribute('data-html', info.html)
|
|
506
|
-
}
|
|
507
|
-
optionEl.defaultSelected = info.defaultSelected
|
|
508
|
-
optionEl.selected = info.selected // Explicitly set selected because defaultSelected: true sets selected to true
|
|
509
|
-
if (info.disabled) {
|
|
510
|
-
optionEl.disabled = true
|
|
511
|
-
}
|
|
512
|
-
if (!info.display) {
|
|
513
|
-
optionEl.style.display = 'none'
|
|
514
|
-
}
|
|
515
|
-
if (info.placeholder) {
|
|
516
|
-
optionEl.setAttribute('data-placeholder', 'true')
|
|
517
|
-
}
|
|
518
|
-
if (info.mandatory) {
|
|
519
|
-
optionEl.setAttribute('data-mandatory', 'true')
|
|
520
|
-
}
|
|
521
|
-
if (info.class) {
|
|
522
|
-
info.class.split(' ').forEach((optionClass: string) => {
|
|
523
|
-
optionEl.classList.add(optionClass)
|
|
524
|
-
})
|
|
525
|
-
}
|
|
526
|
-
if (info.data && typeof info.data === 'object') {
|
|
527
|
-
Object.keys(info.data).forEach((key) => {
|
|
528
|
-
optionEl.setAttribute('data-' + kebabCase(key), info.data[key])
|
|
529
|
-
})
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return optionEl
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Find and handle labels associated with this select
|
|
536
|
-
public setupLabelHandlers(): void {
|
|
537
|
-
const labels: HTMLLabelElement[] = []
|
|
538
|
-
|
|
539
|
-
// Find labels that point to this select via 'for' attribute
|
|
540
|
-
const selectId = this.select.id
|
|
541
|
-
if (selectId) {
|
|
542
|
-
const labelsByFor = document.querySelectorAll<HTMLLabelElement>(`label[for="${selectId}"]`)
|
|
543
|
-
labelsByFor.forEach((label) => labels.push(label))
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Find labels that wrap this select (label contains select)
|
|
547
|
-
let parent = this.select.parentElement
|
|
548
|
-
while (parent && parent !== document.body) {
|
|
549
|
-
if (parent.tagName === 'LABEL') {
|
|
550
|
-
labels.push(parent as HTMLLabelElement)
|
|
551
|
-
break
|
|
552
|
-
}
|
|
553
|
-
parent = parent.parentElement
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Remove duplicates
|
|
557
|
-
const uniqueLabels = Array.from(new Set(labels))
|
|
558
|
-
|
|
559
|
-
uniqueLabels.forEach((label) => {
|
|
560
|
-
// Skip if already handled
|
|
561
|
-
if ((label as any).__slimSelectLabelHandler) {
|
|
562
|
-
return
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Add click handler to label
|
|
566
|
-
const labelClickHandler = (e: MouseEvent) => {
|
|
567
|
-
const target = e.target as HTMLElement
|
|
568
|
-
|
|
569
|
-
// Check if click is on SlimSelect UI elements (main div or content)
|
|
570
|
-
const isSlimSelectElement = hasClassInTree(target, this.select.dataset.id!)
|
|
571
|
-
|
|
572
|
-
// Prevent default label behavior (focusing the select)
|
|
573
|
-
// This needs to happen for all clicks on the label or its children
|
|
574
|
-
// to prevent the browser from focusing the hidden select
|
|
575
|
-
e.preventDefault()
|
|
576
|
-
// e.stopPropagation() // dont stop propagation
|
|
577
|
-
|
|
578
|
-
// Only trigger the toggle if the click is NOT on SlimSelect elements
|
|
579
|
-
// This allows clicking label text/children to toggle, while wrapped SlimSelect handles its own clicks
|
|
580
|
-
if (!isSlimSelectElement && this.onLabelClick) {
|
|
581
|
-
this.onLabelClick()
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Store the handler on the label for cleanup later
|
|
586
|
-
;(label as any).__slimSelectLabelHandler = labelClickHandler
|
|
587
|
-
label.addEventListener('click', labelClickHandler, { capture: true, passive: false })
|
|
588
|
-
})
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Remove label handlers
|
|
592
|
-
public removeLabelHandlers(): void {
|
|
593
|
-
const labels: HTMLLabelElement[] = []
|
|
594
|
-
|
|
595
|
-
// Find labels that point to this select via 'for' attribute
|
|
596
|
-
const selectId = this.select.id
|
|
597
|
-
if (selectId) {
|
|
598
|
-
const labelsByFor = document.querySelectorAll<HTMLLabelElement>(`label[for="${selectId}"]`)
|
|
599
|
-
labelsByFor.forEach((label) => labels.push(label))
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Find labels that wrap this select
|
|
603
|
-
let parent = this.select.parentElement
|
|
604
|
-
while (parent && parent !== document.body) {
|
|
605
|
-
if (parent.tagName === 'LABEL') {
|
|
606
|
-
labels.push(parent as HTMLLabelElement)
|
|
607
|
-
break
|
|
608
|
-
}
|
|
609
|
-
parent = parent.parentElement
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Remove duplicates
|
|
613
|
-
const uniqueLabels = Array.from(new Set(labels))
|
|
614
|
-
|
|
615
|
-
uniqueLabels.forEach((label) => {
|
|
616
|
-
const handler = (label as any).__slimSelectLabelHandler
|
|
617
|
-
if (handler) {
|
|
618
|
-
label.removeEventListener('click', handler, { capture: true })
|
|
619
|
-
delete (label as any).__slimSelectLabelHandler
|
|
620
|
-
}
|
|
621
|
-
})
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
public destroy() {
|
|
625
|
-
this.changeListen(false)
|
|
626
|
-
|
|
627
|
-
// Remove event change listener
|
|
628
|
-
this.select.removeEventListener('change', this.valueChange)
|
|
629
|
-
|
|
630
|
-
// Remove event listeners that prevent native select behavior
|
|
631
|
-
if (this.preventNativeSelect) {
|
|
632
|
-
this.select.removeEventListener('click', this.preventNativeSelect, { capture: true })
|
|
633
|
-
this.preventNativeSelect = null
|
|
634
|
-
}
|
|
635
|
-
if (this.preventNativeSelectMousedown) {
|
|
636
|
-
this.select.removeEventListener('mousedown', this.preventNativeSelectMousedown, { capture: true })
|
|
637
|
-
this.preventNativeSelectMousedown = null
|
|
638
|
-
}
|
|
639
|
-
if (this.preventNativeSelectFocus) {
|
|
640
|
-
this.select.removeEventListener('focus', this.preventNativeSelectFocus, { capture: true })
|
|
641
|
-
this.preventNativeSelectFocus = null
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Disconnect observer and null
|
|
645
|
-
if (this.observer) {
|
|
646
|
-
this.observer.disconnect()
|
|
647
|
-
this.observer = null
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Remove label handlers
|
|
651
|
-
this.removeLabelHandlers()
|
|
652
|
-
|
|
653
|
-
// Remove dataset id from original select
|
|
654
|
-
delete this.select.dataset.id
|
|
655
|
-
|
|
656
|
-
// Show the original select
|
|
657
|
-
this.showUI()
|
|
658
|
-
}
|
|
659
|
-
}
|