slim-select 3.0.6 → 3.1.1
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/README.md +4 -3
- package/dist/index.d.ts +8 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +415 -328
- package/dist/react/index.umd.js +4 -4
- package/dist/slimselect.cjs.js +1 -1
- package/dist/slimselect.cjs.js.map +1 -1
- package/dist/slimselect.es.js +108 -39
- package/dist/slimselect.es.js.map +1 -1
- package/dist/slimselect.iife.js +1 -1
- package/dist/slimselect.iife.js.map +1 -1
- package/dist/slimselect.js +1 -1
- package/dist/slimselect.js.map +1 -1
- package/dist/vue/index.d.ts +8 -0
- package/dist/vue/index.js +139 -54
- package/dist/vue/index.umd.js +1 -1
- package/package.json +2 -1
- package/src/slim-select/index.test.ts +356 -0
- package/src/slim-select/index.ts +17 -2
- package/src/slim-select/react/react.test.tsx +498 -0
- package/src/slim-select/react/react.tsx +102 -5
- package/src/slim-select/select.test.ts +234 -0
- package/src/slim-select/select.ts +196 -0
- package/src/slim-select/vue/vue.test.ts +259 -0
- package/src/slim-select/vue/vue.vue +85 -25
- package/vitest.config.ts +3 -2
|
@@ -439,4 +439,238 @@ describe('select module', () => {
|
|
|
439
439
|
expect(dataOption[0].options[0].text).toBe('New One')
|
|
440
440
|
})
|
|
441
441
|
})
|
|
442
|
+
|
|
443
|
+
describe('label handling', () => {
|
|
444
|
+
test('setupLabelHandlers finds label with for attribute and adds click handler', async () => {
|
|
445
|
+
document.body.innerHTML = `
|
|
446
|
+
<label for="test-select">Select Label</label>
|
|
447
|
+
<select id="test-select">
|
|
448
|
+
<option value="1">One</option>
|
|
449
|
+
<option value="2">Two</option>
|
|
450
|
+
</select>
|
|
451
|
+
`
|
|
452
|
+
|
|
453
|
+
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
454
|
+
const select = new Select(selectElement)
|
|
455
|
+
select.hideUI()
|
|
456
|
+
|
|
457
|
+
const onLabelClickMock = vi.fn()
|
|
458
|
+
select.onLabelClick = onLabelClickMock
|
|
459
|
+
|
|
460
|
+
select.setupLabelHandlers()
|
|
461
|
+
|
|
462
|
+
const label = document.querySelector('label[for="test-select"]') as HTMLLabelElement
|
|
463
|
+
expect(label).toBeTruthy()
|
|
464
|
+
|
|
465
|
+
// Click the label
|
|
466
|
+
label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
467
|
+
|
|
468
|
+
// Wait for setTimeout in the label handler
|
|
469
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
470
|
+
|
|
471
|
+
// Should have prevented default and called the callback
|
|
472
|
+
expect(onLabelClickMock).toHaveBeenCalled()
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('setupLabelHandlers finds wrapped label and adds click handler', async () => {
|
|
476
|
+
document.body.innerHTML = `
|
|
477
|
+
<label>
|
|
478
|
+
Select Label
|
|
479
|
+
<select id="test-select">
|
|
480
|
+
<option value="1">One</option>
|
|
481
|
+
<option value="2">Two</option>
|
|
482
|
+
</select>
|
|
483
|
+
</label>
|
|
484
|
+
`
|
|
485
|
+
|
|
486
|
+
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
487
|
+
const select = new Select(selectElement)
|
|
488
|
+
select.hideUI()
|
|
489
|
+
|
|
490
|
+
const onLabelClickMock = vi.fn()
|
|
491
|
+
select.onLabelClick = onLabelClickMock
|
|
492
|
+
|
|
493
|
+
select.setupLabelHandlers()
|
|
494
|
+
|
|
495
|
+
const label = selectElement.parentElement as HTMLLabelElement
|
|
496
|
+
expect(label.tagName).toBe('LABEL')
|
|
497
|
+
|
|
498
|
+
// Click the label
|
|
499
|
+
label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
500
|
+
|
|
501
|
+
// Wait for setTimeout in the label handler
|
|
502
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
503
|
+
|
|
504
|
+
// Should have called the callback
|
|
505
|
+
expect(onLabelClickMock).toHaveBeenCalled()
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
test('setupLabelHandlers handles multiple labels', async () => {
|
|
509
|
+
document.body.innerHTML = `
|
|
510
|
+
<label for="test-select">Label 1</label>
|
|
511
|
+
<label for="test-select">Label 2</label>
|
|
512
|
+
<select id="test-select">
|
|
513
|
+
<option value="1">One</option>
|
|
514
|
+
</select>
|
|
515
|
+
`
|
|
516
|
+
|
|
517
|
+
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
518
|
+
const select = new Select(selectElement)
|
|
519
|
+
select.hideUI()
|
|
520
|
+
|
|
521
|
+
const onLabelClickMock = vi.fn()
|
|
522
|
+
select.onLabelClick = onLabelClickMock
|
|
523
|
+
|
|
524
|
+
select.setupLabelHandlers()
|
|
525
|
+
|
|
526
|
+
const labels = document.querySelectorAll('label[for="test-select"]')
|
|
527
|
+
expect(labels).toHaveLength(2)
|
|
528
|
+
|
|
529
|
+
// Click first label
|
|
530
|
+
labels[0].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
531
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
532
|
+
expect(onLabelClickMock).toHaveBeenCalledTimes(1)
|
|
533
|
+
|
|
534
|
+
// Click second label
|
|
535
|
+
labels[1].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
536
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
537
|
+
expect(onLabelClickMock).toHaveBeenCalledTimes(2)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
test('removeLabelHandlers removes click handlers from labels', () => {
|
|
541
|
+
document.body.innerHTML = `
|
|
542
|
+
<label for="test-select">Select Label</label>
|
|
543
|
+
<select id="test-select">
|
|
544
|
+
<option value="1">One</option>
|
|
545
|
+
</select>
|
|
546
|
+
`
|
|
547
|
+
|
|
548
|
+
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
549
|
+
const select = new Select(selectElement)
|
|
550
|
+
select.hideUI()
|
|
551
|
+
|
|
552
|
+
const onLabelClickMock = vi.fn()
|
|
553
|
+
select.onLabelClick = onLabelClickMock
|
|
554
|
+
|
|
555
|
+
select.setupLabelHandlers()
|
|
556
|
+
select.removeLabelHandlers()
|
|
557
|
+
|
|
558
|
+
const label = document.querySelector('label[for="test-select"]') as HTMLLabelElement
|
|
559
|
+
label.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
560
|
+
|
|
561
|
+
// Should not have been called after removal
|
|
562
|
+
expect(onLabelClickMock).not.toHaveBeenCalled()
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('hideUI prevents native select from opening on click', () => {
|
|
566
|
+
document.body.innerHTML = '<select id="test"><option value="1">One</option></select>'
|
|
567
|
+
|
|
568
|
+
const selectElement = document.getElementById('test') as HTMLSelectElement
|
|
569
|
+
const select = new Select(selectElement)
|
|
570
|
+
select.hideUI()
|
|
571
|
+
|
|
572
|
+
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })
|
|
573
|
+
const prevented = !selectElement.dispatchEvent(clickEvent)
|
|
574
|
+
|
|
575
|
+
// The event should be prevented by our handler
|
|
576
|
+
expect(clickEvent.defaultPrevented).toBe(true)
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
test('hideUI prevents native select from opening on focus', () => {
|
|
580
|
+
document.body.innerHTML = '<select id="test"><option value="1">One</option></select>'
|
|
581
|
+
|
|
582
|
+
const selectElement = document.getElementById('test') as HTMLSelectElement
|
|
583
|
+
const select = new Select(selectElement)
|
|
584
|
+
select.hideUI()
|
|
585
|
+
|
|
586
|
+
const focusEvent = new FocusEvent('focus', { bubbles: true, cancelable: true })
|
|
587
|
+
selectElement.dispatchEvent(focusEvent)
|
|
588
|
+
|
|
589
|
+
// The event should be prevented by our handler
|
|
590
|
+
expect(focusEvent.defaultPrevented).toBe(true)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
test('hideUI prevents native select from opening on mousedown', () => {
|
|
594
|
+
document.body.innerHTML = '<select id="test"><option value="1">One</option></select>'
|
|
595
|
+
|
|
596
|
+
const selectElement = document.getElementById('test') as HTMLSelectElement
|
|
597
|
+
const select = new Select(selectElement)
|
|
598
|
+
select.hideUI()
|
|
599
|
+
|
|
600
|
+
const mousedownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true })
|
|
601
|
+
selectElement.dispatchEvent(mousedownEvent)
|
|
602
|
+
|
|
603
|
+
// The event should be prevented by our handler
|
|
604
|
+
expect(mousedownEvent.defaultPrevented).toBe(true)
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
test('showUI removes event handlers that prevent native select', () => {
|
|
608
|
+
document.body.innerHTML = '<select id="test"><option value="1">One</option></select>'
|
|
609
|
+
|
|
610
|
+
const selectElement = document.getElementById('test') as HTMLSelectElement
|
|
611
|
+
const select = new Select(selectElement)
|
|
612
|
+
select.hideUI()
|
|
613
|
+
select.showUI()
|
|
614
|
+
|
|
615
|
+
// After showUI, events should not be prevented
|
|
616
|
+
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })
|
|
617
|
+
selectElement.dispatchEvent(clickEvent)
|
|
618
|
+
|
|
619
|
+
// Note: The event may still propagate normally, but defaultPrevented should be false
|
|
620
|
+
// We test this by ensuring the select doesn't have our prevent handlers attached
|
|
621
|
+
// The actual behavior depends on browser implementation
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
test('destroy removes label handlers', async () => {
|
|
625
|
+
document.body.innerHTML = `
|
|
626
|
+
<label for="test-select">Select Label</label>
|
|
627
|
+
<select id="test-select">
|
|
628
|
+
<option value="1">One</option>
|
|
629
|
+
</select>
|
|
630
|
+
`
|
|
631
|
+
|
|
632
|
+
const selectElement = document.getElementById('test-select') as HTMLSelectElement
|
|
633
|
+
const select = new Select(selectElement)
|
|
634
|
+
select.hideUI()
|
|
635
|
+
|
|
636
|
+
const onLabelClickMock = vi.fn()
|
|
637
|
+
select.onLabelClick = onLabelClickMock
|
|
638
|
+
|
|
639
|
+
select.setupLabelHandlers()
|
|
640
|
+
|
|
641
|
+
// Verify handler works
|
|
642
|
+
const label = document.querySelector('label[for="test-select"]') as HTMLLabelElement
|
|
643
|
+
label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
644
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
645
|
+
expect(onLabelClickMock).toHaveBeenCalledTimes(1)
|
|
646
|
+
|
|
647
|
+
// Destroy and verify handler removed
|
|
648
|
+
select.destroy()
|
|
649
|
+
onLabelClickMock.mockClear()
|
|
650
|
+
|
|
651
|
+
label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
652
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
653
|
+
expect(onLabelClickMock).not.toHaveBeenCalled()
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
test('setupLabelHandlers does nothing if select has no id and no wrapping label', () => {
|
|
657
|
+
document.body.innerHTML = `
|
|
658
|
+
<div>
|
|
659
|
+
<select>
|
|
660
|
+
<option value="1">One</option>
|
|
661
|
+
</select>
|
|
662
|
+
</div>
|
|
663
|
+
`
|
|
664
|
+
|
|
665
|
+
const selectElement = document.querySelector('select') as HTMLSelectElement
|
|
666
|
+
const select = new Select(selectElement)
|
|
667
|
+
select.hideUI()
|
|
668
|
+
|
|
669
|
+
const onLabelClickMock = vi.fn()
|
|
670
|
+
select.onLabelClick = onLabelClickMock
|
|
671
|
+
|
|
672
|
+
// Should not throw or error
|
|
673
|
+
expect(() => select.setupLabelHandlers()).not.toThrow()
|
|
674
|
+
})
|
|
675
|
+
})
|
|
442
676
|
})
|
|
@@ -9,10 +9,18 @@ export default class Select {
|
|
|
9
9
|
public onClassChange?: (classes: string[]) => void
|
|
10
10
|
public onDisabledChange?: (disabled: boolean) => void
|
|
11
11
|
public onOptionsChange?: (data: (Option | Optgroup)[]) => void
|
|
12
|
+
public onLabelClick?: () => void
|
|
12
13
|
|
|
13
14
|
// Change observers
|
|
14
15
|
private listen: boolean = false
|
|
15
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
|
|
16
24
|
|
|
17
25
|
constructor(select: HTMLSelectElement) {
|
|
18
26
|
this.select = select
|
|
@@ -56,6 +64,35 @@ export default class Select {
|
|
|
56
64
|
// Clip to completely hide the 1px
|
|
57
65
|
this.select.style.clip = 'rect(0 0 0 0)'
|
|
58
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
|
+
}
|
|
59
96
|
}
|
|
60
97
|
|
|
61
98
|
public showUI(): void {
|
|
@@ -72,6 +109,20 @@ export default class Select {
|
|
|
72
109
|
this.select.style.borderWidth = ''
|
|
73
110
|
this.select.style.clip = ''
|
|
74
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
|
+
}
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
public changeListen(listen: boolean) {
|
|
@@ -165,6 +216,20 @@ export default class Select {
|
|
|
165
216
|
|
|
166
217
|
// If optgroup or option has changed then call the select change function
|
|
167
218
|
if (optgroupOptionChanged && this.onOptionsChange) {
|
|
219
|
+
// If we're currently updating, queue this change to process after update completes
|
|
220
|
+
if (this.isUpdating) {
|
|
221
|
+
// Only queue if the select element actually has options (not empty from updateOptions clearing)
|
|
222
|
+
// Check the select element directly, not getData() which might return stale data
|
|
223
|
+
if (this.select.options.length > 0) {
|
|
224
|
+
const currentData = this.getData()
|
|
225
|
+
// Only queue if we have actual data
|
|
226
|
+
if (currentData.length > 0) {
|
|
227
|
+
this.pendingOptionsChange = currentData
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
168
233
|
this.changeListen(false)
|
|
169
234
|
this.onOptionsChange(this.getData())
|
|
170
235
|
this.changeListen(true)
|
|
@@ -353,6 +418,16 @@ export default class Select {
|
|
|
353
418
|
}
|
|
354
419
|
|
|
355
420
|
public updateOptions(data: (Option | Optgroup)[]): void {
|
|
421
|
+
// Don't update if data is empty (prevents clearing select when no data)
|
|
422
|
+
if (!data || data.length === 0) {
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Set updating flag FIRST to prevent mutation observer from processing during update
|
|
427
|
+
// This must be set before stopping listening so any queued mutations see the flag
|
|
428
|
+
this.isUpdating = true
|
|
429
|
+
this.pendingOptionsChange = null
|
|
430
|
+
|
|
356
431
|
// Stop listening to changes
|
|
357
432
|
this.changeListen(false)
|
|
358
433
|
|
|
@@ -374,6 +449,25 @@ export default class Select {
|
|
|
374
449
|
|
|
375
450
|
// Start listening to changes
|
|
376
451
|
this.changeListen(true)
|
|
452
|
+
|
|
453
|
+
// Clear updating flag
|
|
454
|
+
this.isUpdating = false
|
|
455
|
+
|
|
456
|
+
// Process any pending options change that was queued during the update
|
|
457
|
+
// Only process if it has data (don't process empty data from innerHTML clearing)
|
|
458
|
+
if (this.pendingOptionsChange !== null) {
|
|
459
|
+
const pending: (Option | Optgroup)[] = this.pendingOptionsChange
|
|
460
|
+
if (pending.length > 0 && this.onOptionsChange) {
|
|
461
|
+
this.pendingOptionsChange = null
|
|
462
|
+
// Process the queued change
|
|
463
|
+
this.changeListen(false)
|
|
464
|
+
this.onOptionsChange(pending)
|
|
465
|
+
this.changeListen(true)
|
|
466
|
+
} else {
|
|
467
|
+
// Clear pending if it was empty
|
|
468
|
+
this.pendingOptionsChange = null
|
|
469
|
+
}
|
|
470
|
+
}
|
|
377
471
|
}
|
|
378
472
|
|
|
379
473
|
public createOptgroup(optgroup: Optgroup): HTMLOptGroupElement {
|
|
@@ -430,18 +524,120 @@ export default class Select {
|
|
|
430
524
|
return optionEl
|
|
431
525
|
}
|
|
432
526
|
|
|
527
|
+
// Find and handle labels associated with this select
|
|
528
|
+
public setupLabelHandlers(): void {
|
|
529
|
+
const labels: HTMLLabelElement[] = []
|
|
530
|
+
|
|
531
|
+
// Find labels that point to this select via 'for' attribute
|
|
532
|
+
const selectId = this.select.id
|
|
533
|
+
if (selectId) {
|
|
534
|
+
const labelsByFor = document.querySelectorAll<HTMLLabelElement>(`label[for="${selectId}"]`)
|
|
535
|
+
labelsByFor.forEach((label) => labels.push(label))
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Find labels that wrap this select (label contains select)
|
|
539
|
+
let parent = this.select.parentElement
|
|
540
|
+
while (parent && parent !== document.body) {
|
|
541
|
+
if (parent.tagName === 'LABEL') {
|
|
542
|
+
labels.push(parent as HTMLLabelElement)
|
|
543
|
+
break
|
|
544
|
+
}
|
|
545
|
+
parent = parent.parentElement
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Remove duplicates
|
|
549
|
+
const uniqueLabels = Array.from(new Set(labels))
|
|
550
|
+
|
|
551
|
+
uniqueLabels.forEach((label) => {
|
|
552
|
+
// Skip if already handled
|
|
553
|
+
if ((label as any).__slimSelectLabelHandler) {
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Add click handler to label
|
|
558
|
+
const labelClickHandler = (e: MouseEvent) => {
|
|
559
|
+
// Prevent default label behavior (focusing the select)
|
|
560
|
+
e.preventDefault()
|
|
561
|
+
e.stopPropagation()
|
|
562
|
+
|
|
563
|
+
// Trigger the callback to open SlimSelect
|
|
564
|
+
if (this.onLabelClick) {
|
|
565
|
+
// Use setTimeout to ensure this happens after any browser-initiated events
|
|
566
|
+
// are prevented by our prevent handlers
|
|
567
|
+
setTimeout(() => {
|
|
568
|
+
this.onLabelClick!()
|
|
569
|
+
}, 0)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Store the handler on the label for cleanup later
|
|
574
|
+
;(label as any).__slimSelectLabelHandler = labelClickHandler
|
|
575
|
+
label.addEventListener('click', labelClickHandler, { capture: true, passive: false })
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Remove label handlers
|
|
580
|
+
public removeLabelHandlers(): void {
|
|
581
|
+
const labels: HTMLLabelElement[] = []
|
|
582
|
+
|
|
583
|
+
// Find labels that point to this select via 'for' attribute
|
|
584
|
+
const selectId = this.select.id
|
|
585
|
+
if (selectId) {
|
|
586
|
+
const labelsByFor = document.querySelectorAll<HTMLLabelElement>(`label[for="${selectId}"]`)
|
|
587
|
+
labelsByFor.forEach((label) => labels.push(label))
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Find labels that wrap this select
|
|
591
|
+
let parent = this.select.parentElement
|
|
592
|
+
while (parent && parent !== document.body) {
|
|
593
|
+
if (parent.tagName === 'LABEL') {
|
|
594
|
+
labels.push(parent as HTMLLabelElement)
|
|
595
|
+
break
|
|
596
|
+
}
|
|
597
|
+
parent = parent.parentElement
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Remove duplicates
|
|
601
|
+
const uniqueLabels = Array.from(new Set(labels))
|
|
602
|
+
|
|
603
|
+
uniqueLabels.forEach((label) => {
|
|
604
|
+
const handler = (label as any).__slimSelectLabelHandler
|
|
605
|
+
if (handler) {
|
|
606
|
+
label.removeEventListener('click', handler, { capture: true })
|
|
607
|
+
delete (label as any).__slimSelectLabelHandler
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
|
|
433
612
|
public destroy() {
|
|
434
613
|
this.changeListen(false)
|
|
435
614
|
|
|
436
615
|
// Remove event change listener
|
|
437
616
|
this.select.removeEventListener('change', this.valueChange)
|
|
438
617
|
|
|
618
|
+
// Remove event listeners that prevent native select behavior
|
|
619
|
+
if (this.preventNativeSelect) {
|
|
620
|
+
this.select.removeEventListener('click', this.preventNativeSelect, { capture: true })
|
|
621
|
+
this.preventNativeSelect = null
|
|
622
|
+
}
|
|
623
|
+
if (this.preventNativeSelectMousedown) {
|
|
624
|
+
this.select.removeEventListener('mousedown', this.preventNativeSelectMousedown, { capture: true })
|
|
625
|
+
this.preventNativeSelectMousedown = null
|
|
626
|
+
}
|
|
627
|
+
if (this.preventNativeSelectFocus) {
|
|
628
|
+
this.select.removeEventListener('focus', this.preventNativeSelectFocus, { capture: true })
|
|
629
|
+
this.preventNativeSelectFocus = null
|
|
630
|
+
}
|
|
631
|
+
|
|
439
632
|
// Disconnect observer and null
|
|
440
633
|
if (this.observer) {
|
|
441
634
|
this.observer.disconnect()
|
|
442
635
|
this.observer = null
|
|
443
636
|
}
|
|
444
637
|
|
|
638
|
+
// Remove label handlers
|
|
639
|
+
this.removeLabelHandlers()
|
|
640
|
+
|
|
445
641
|
// Remove dataset id from original select
|
|
446
642
|
delete this.select.dataset.id
|
|
447
643
|
|