slim-select 3.1.4 → 3.3.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/README.md +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +290 -226
- package/dist/react/index.umd.js +2 -2
- package/dist/slimselect.cjs.js +1 -1
- 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 +310 -246
- 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 +346 -282
- package/dist/vue/index.umd.js +1 -1
- package/package.json +21 -20
- package/src/slim-select/classes.test.ts +3 -3
- package/src/slim-select/classes.ts +40 -34
- package/src/slim-select/index.test.ts +108 -1
- package/src/slim-select/index.ts +14 -2
- package/src/slim-select/render.ts +198 -117
- package/src/slim-select/select.ts +18 -11
- package/src/slim-select/settings.test.ts +1 -0
- package/src/slim-select/settings.ts +2 -0
- package/src/slim-select/slimselect.scss +5 -0
|
@@ -54,6 +54,9 @@ export default class Render {
|
|
|
54
54
|
// Used to compute the range selection
|
|
55
55
|
private lastSelectedOption: Option | null
|
|
56
56
|
|
|
57
|
+
// Timeout tracking for cleanup
|
|
58
|
+
private closeAnimationTimeout: ReturnType<typeof setTimeout> | null = null
|
|
59
|
+
|
|
57
60
|
// Elements
|
|
58
61
|
public main: Main
|
|
59
62
|
public content: Content
|
|
@@ -92,10 +95,33 @@ export default class Render {
|
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
// Helper method to add classes that may contain spaces
|
|
99
|
+
// Splits by spaces and adds each class individually to avoid DOMException
|
|
100
|
+
private addClasses(element: HTMLElement | SVGElement, classValue: string): void {
|
|
101
|
+
if (!classValue || classValue.trim() === '') {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const classes = classValue.split(' ').filter((c) => c.trim() !== '')
|
|
105
|
+
for (const cls of classes) {
|
|
106
|
+
element.classList.add(cls.trim())
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Helper method to remove classes that may contain spaces
|
|
111
|
+
private removeClasses(element: HTMLElement | SVGElement, classValue: string): void {
|
|
112
|
+
if (!classValue || classValue.trim() === '') {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const classes = classValue.split(' ').filter((c) => c.trim() !== '')
|
|
116
|
+
for (const cls of classes) {
|
|
117
|
+
element.classList.remove(cls.trim())
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
// Remove disabled classes
|
|
96
122
|
public enable(): void {
|
|
97
123
|
// Remove disabled class
|
|
98
|
-
this.main.main
|
|
124
|
+
this.removeClasses(this.main.main, this.classes.disabled)
|
|
99
125
|
this.main.main.setAttribute('aria-disabled', 'false')
|
|
100
126
|
|
|
101
127
|
// Set search input to "enabled"
|
|
@@ -105,7 +131,7 @@ export default class Render {
|
|
|
105
131
|
// Set disabled classes
|
|
106
132
|
public disable(): void {
|
|
107
133
|
// Add disabled class
|
|
108
|
-
this.main.main
|
|
134
|
+
this.addClasses(this.main.main, this.classes.disabled)
|
|
109
135
|
this.main.main.setAttribute('aria-disabled', 'true')
|
|
110
136
|
|
|
111
137
|
// Set search input to disabled
|
|
@@ -116,14 +142,20 @@ export default class Render {
|
|
|
116
142
|
this.main.arrow.path.setAttribute('d', this.classes.arrowOpen)
|
|
117
143
|
this.main.main.setAttribute('aria-expanded', 'true')
|
|
118
144
|
|
|
145
|
+
// Clear any pending close animation timeout to prevent race conditions
|
|
146
|
+
if (this.closeAnimationTimeout) {
|
|
147
|
+
clearTimeout(this.closeAnimationTimeout)
|
|
148
|
+
this.closeAnimationTimeout = null
|
|
149
|
+
}
|
|
150
|
+
|
|
119
151
|
// Set direction class on both main and content (persists, never removed)
|
|
120
152
|
const isAbove = this.settings.openPosition === 'up'
|
|
121
153
|
const dirClass = isAbove ? this.classes.dirAbove : this.classes.dirBelow
|
|
122
|
-
this.main.main
|
|
123
|
-
this.content.main
|
|
154
|
+
this.addClasses(this.main.main, dirClass)
|
|
155
|
+
this.addClasses(this.content.main, dirClass)
|
|
124
156
|
|
|
125
157
|
// Add open class to content to trigger open animation
|
|
126
|
-
this.content.main
|
|
158
|
+
this.addClasses(this.content.main, this.classes.contentOpen)
|
|
127
159
|
|
|
128
160
|
// Make search visible to screen readers when opened
|
|
129
161
|
this.content.search.input.removeAttribute('aria-hidden')
|
|
@@ -148,7 +180,7 @@ export default class Render {
|
|
|
148
180
|
|
|
149
181
|
// Remove open class from content to trigger close animation
|
|
150
182
|
// Direction class (dirAbove/dirBelow) persists to maintain correct transform-origin
|
|
151
|
-
this.content.main
|
|
183
|
+
this.removeClasses(this.content.main, this.classes.contentOpen)
|
|
152
184
|
|
|
153
185
|
// Hide search from screen readers when closed
|
|
154
186
|
this.content.search.input.setAttribute('aria-hidden', 'true')
|
|
@@ -157,10 +189,31 @@ export default class Render {
|
|
|
157
189
|
this.main.main.removeAttribute('aria-activedescendant')
|
|
158
190
|
|
|
159
191
|
// Remove direction class from main and content after animation is complete
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this.
|
|
163
|
-
|
|
192
|
+
const animationTiming = this.getAnimationTiming()
|
|
193
|
+
this.closeAnimationTimeout = setTimeout(() => {
|
|
194
|
+
this.removeClasses(this.main.main, this.classes.dirAbove)
|
|
195
|
+
this.removeClasses(this.main.main, this.classes.dirBelow)
|
|
196
|
+
this.removeClasses(this.content.main, this.classes.dirAbove)
|
|
197
|
+
this.removeClasses(this.content.main, this.classes.dirBelow)
|
|
198
|
+
this.closeAnimationTimeout = null
|
|
199
|
+
}, animationTiming)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private getAnimationTiming(): number {
|
|
203
|
+
const computedStyle = getComputedStyle(this.content.main)
|
|
204
|
+
const cssValue = computedStyle.getPropertyValue('--ss-animation-timing').trim()
|
|
205
|
+
|
|
206
|
+
if (cssValue) {
|
|
207
|
+
// Parse CSS time value (e.g., "0.2s" or "200ms")
|
|
208
|
+
if (cssValue.endsWith('ms')) {
|
|
209
|
+
return parseFloat(cssValue)
|
|
210
|
+
} else if (cssValue.endsWith('s')) {
|
|
211
|
+
return parseFloat(cssValue) * 1000
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Fall back to default 200ms
|
|
216
|
+
return 200
|
|
164
217
|
}
|
|
165
218
|
|
|
166
219
|
public updateClassStyles(): void {
|
|
@@ -171,8 +224,8 @@ export default class Render {
|
|
|
171
224
|
this.content.main.removeAttribute('style')
|
|
172
225
|
|
|
173
226
|
// Make sure main/content has its base class
|
|
174
|
-
this.main.main
|
|
175
|
-
this.content.main
|
|
227
|
+
this.addClasses(this.main.main, this.classes.main)
|
|
228
|
+
this.addClasses(this.content.main, this.classes.content)
|
|
176
229
|
|
|
177
230
|
// Add styles
|
|
178
231
|
if (this.settings.style !== '') {
|
|
@@ -249,7 +302,9 @@ export default class Render {
|
|
|
249
302
|
case 'Enter':
|
|
250
303
|
case ' ':
|
|
251
304
|
this.callbacks.open()
|
|
252
|
-
const highlighted = this.content.list.querySelector(
|
|
305
|
+
const highlighted = this.content.list.querySelector(
|
|
306
|
+
'.' + this.classes.getFirst('highlighted')
|
|
307
|
+
) as HTMLDivElement
|
|
253
308
|
if (highlighted) {
|
|
254
309
|
highlighted.click()
|
|
255
310
|
}
|
|
@@ -279,19 +334,19 @@ export default class Render {
|
|
|
279
334
|
|
|
280
335
|
// Add values
|
|
281
336
|
const values = document.createElement('div')
|
|
282
|
-
|
|
337
|
+
this.addClasses(values, this.classes.values)
|
|
283
338
|
main.appendChild(values)
|
|
284
339
|
|
|
285
340
|
// Add deselect
|
|
286
341
|
const deselect = document.createElement('div')
|
|
287
|
-
|
|
342
|
+
this.addClasses(deselect, this.classes.deselect)
|
|
288
343
|
|
|
289
344
|
// Check if deselect is to be shown or not
|
|
290
345
|
const selectedOptions = this.store?.getSelectedOptions()
|
|
291
346
|
if (!this.settings.allowDeselect || (this.settings.isMultiple && selectedOptions && selectedOptions.length <= 0)) {
|
|
292
|
-
|
|
347
|
+
this.addClasses(deselect, this.classes.hide)
|
|
293
348
|
} else {
|
|
294
|
-
|
|
349
|
+
this.removeClasses(deselect, this.classes.hide)
|
|
295
350
|
}
|
|
296
351
|
|
|
297
352
|
// Add deselect onclick event
|
|
@@ -348,12 +403,12 @@ export default class Render {
|
|
|
348
403
|
|
|
349
404
|
// Add arrow
|
|
350
405
|
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
351
|
-
|
|
406
|
+
this.addClasses(arrow, this.classes.arrow)
|
|
352
407
|
arrow.setAttribute('viewBox', '0 0 100 100')
|
|
353
408
|
const arrowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
354
409
|
arrowPath.setAttribute('d', this.classes.arrowClose)
|
|
355
410
|
if (this.settings.alwaysOpen) {
|
|
356
|
-
|
|
411
|
+
this.addClasses(arrow, this.classes.hide)
|
|
357
412
|
}
|
|
358
413
|
arrow.appendChild(arrowPath)
|
|
359
414
|
main.appendChild(arrow)
|
|
@@ -402,7 +457,7 @@ export default class Render {
|
|
|
402
457
|
|
|
403
458
|
// Create placeholder div
|
|
404
459
|
const placeholder = document.createElement('div')
|
|
405
|
-
|
|
460
|
+
this.addClasses(placeholder, this.classes.placeholder)
|
|
406
461
|
placeholder.innerHTML = placeholderText
|
|
407
462
|
return placeholder
|
|
408
463
|
}
|
|
@@ -433,7 +488,7 @@ export default class Render {
|
|
|
433
488
|
} else {
|
|
434
489
|
// Create single value container
|
|
435
490
|
const singleValue = document.createElement('div')
|
|
436
|
-
|
|
491
|
+
this.addClasses(singleValue, this.classes.single)
|
|
437
492
|
if (selectedSingle.html) {
|
|
438
493
|
singleValue.innerHTML = selectedSingle.html
|
|
439
494
|
} else {
|
|
@@ -446,9 +501,9 @@ export default class Render {
|
|
|
446
501
|
|
|
447
502
|
// If allowDeselect is false or selected value is empty just hide deselect
|
|
448
503
|
if (!this.settings.allowDeselect || !selected.length) {
|
|
449
|
-
this.main.deselect.main
|
|
504
|
+
this.addClasses(this.main.deselect.main, this.classes.hide)
|
|
450
505
|
} else {
|
|
451
|
-
this.main.deselect.main
|
|
506
|
+
this.removeClasses(this.main.deselect.main, this.classes.hide)
|
|
452
507
|
}
|
|
453
508
|
}
|
|
454
509
|
|
|
@@ -466,7 +521,7 @@ export default class Render {
|
|
|
466
521
|
return
|
|
467
522
|
} else {
|
|
468
523
|
// If there is a placeholder, remove it
|
|
469
|
-
const placeholder = this.main.values.querySelector('.' + this.classes.placeholder)
|
|
524
|
+
const placeholder = this.main.values.querySelector('.' + this.classes.getFirst('placeholder'))
|
|
470
525
|
if (placeholder) {
|
|
471
526
|
placeholder.remove()
|
|
472
527
|
}
|
|
@@ -476,7 +531,7 @@ export default class Render {
|
|
|
476
531
|
if (selectedOptions.length > this.settings.maxValuesShown) {
|
|
477
532
|
// Creating the element that shows the number of selected items
|
|
478
533
|
const singleValue = document.createElement('div')
|
|
479
|
-
|
|
534
|
+
this.addClasses(singleValue, this.classes.max)
|
|
480
535
|
singleValue.textContent = this.settings.maxValuesMessage.replace('{number}', selectedOptions.length.toString())
|
|
481
536
|
|
|
482
537
|
// If there is a selected value, set a single div
|
|
@@ -484,7 +539,7 @@ export default class Render {
|
|
|
484
539
|
return
|
|
485
540
|
} else {
|
|
486
541
|
// If there is a message, remove it
|
|
487
|
-
const maxValuesMessage = this.main.values.querySelector('.' + this.classes.max)
|
|
542
|
+
const maxValuesMessage = this.main.values.querySelector('.' + this.classes.getFirst('max'))
|
|
488
543
|
if (maxValuesMessage) {
|
|
489
544
|
maxValuesMessage.remove()
|
|
490
545
|
}
|
|
@@ -515,7 +570,7 @@ export default class Render {
|
|
|
515
570
|
|
|
516
571
|
// Loop through and remove
|
|
517
572
|
for (const n of removeNodes) {
|
|
518
|
-
|
|
573
|
+
this.addClasses(n, this.classes.valueOut)
|
|
519
574
|
setTimeout(() => {
|
|
520
575
|
if (this.main.values.hasChildNodes() && this.main.values.contains(n)) {
|
|
521
576
|
this.main.values.removeChild(n)
|
|
@@ -554,11 +609,11 @@ export default class Render {
|
|
|
554
609
|
|
|
555
610
|
public multipleValue(option: Option): HTMLDivElement {
|
|
556
611
|
const value = document.createElement('div')
|
|
557
|
-
|
|
612
|
+
this.addClasses(value, this.classes.value)
|
|
558
613
|
value.dataset.id = option.id
|
|
559
614
|
|
|
560
615
|
const text = document.createElement('div')
|
|
561
|
-
|
|
616
|
+
this.addClasses(text, this.classes.valueText)
|
|
562
617
|
text.textContent = option.text // For multiple values always use text
|
|
563
618
|
value.appendChild(text)
|
|
564
619
|
|
|
@@ -566,7 +621,7 @@ export default class Render {
|
|
|
566
621
|
if (!option.mandatory) {
|
|
567
622
|
// Create delete div element
|
|
568
623
|
const deleteDiv = document.createElement('div')
|
|
569
|
-
|
|
624
|
+
this.addClasses(deleteDiv, this.classes.valueDelete)
|
|
570
625
|
deleteDiv.setAttribute('tabindex', '0') // Make the div focusable for tab navigation
|
|
571
626
|
|
|
572
627
|
// Add delete onclick event
|
|
@@ -699,7 +754,7 @@ export default class Render {
|
|
|
699
754
|
const main = document.createElement('div')
|
|
700
755
|
const input = document.createElement('input')
|
|
701
756
|
const addable = document.createElement('div')
|
|
702
|
-
|
|
757
|
+
this.addClasses(main, this.classes.search)
|
|
703
758
|
|
|
704
759
|
// Setup search return object
|
|
705
760
|
const searchReturn: Search = {
|
|
@@ -709,7 +764,7 @@ export default class Render {
|
|
|
709
764
|
|
|
710
765
|
// We still want the search to be tabable but not shown
|
|
711
766
|
if (!this.settings.showSearch) {
|
|
712
|
-
|
|
767
|
+
this.addClasses(main, this.classes.hide)
|
|
713
768
|
input.readOnly = true
|
|
714
769
|
}
|
|
715
770
|
|
|
@@ -746,7 +801,9 @@ export default class Render {
|
|
|
746
801
|
this.callbacks.close()
|
|
747
802
|
return false
|
|
748
803
|
case ' ':
|
|
749
|
-
const highlighted = this.content.list.querySelector(
|
|
804
|
+
const highlighted = this.content.list.querySelector(
|
|
805
|
+
'.' + this.classes.getFirst('highlighted')
|
|
806
|
+
) as HTMLDivElement
|
|
750
807
|
if (highlighted) {
|
|
751
808
|
highlighted.click()
|
|
752
809
|
return false
|
|
@@ -754,7 +811,9 @@ export default class Render {
|
|
|
754
811
|
return true
|
|
755
812
|
case 'Enter':
|
|
756
813
|
// Check if there's a highlighted option first
|
|
757
|
-
const highlightedEnter = this.content.list.querySelector(
|
|
814
|
+
const highlightedEnter = this.content.list.querySelector(
|
|
815
|
+
'.' + this.classes.getFirst('highlighted')
|
|
816
|
+
) as HTMLDivElement
|
|
758
817
|
if (highlightedEnter) {
|
|
759
818
|
// If an option is highlighted, select it (even if addable is enabled)
|
|
760
819
|
highlightedEnter.click()
|
|
@@ -775,7 +834,7 @@ export default class Render {
|
|
|
775
834
|
// If addable is enabled, add the addable div
|
|
776
835
|
if (this.callbacks.addable) {
|
|
777
836
|
// Add main class
|
|
778
|
-
|
|
837
|
+
this.addClasses(addable, this.classes.addable)
|
|
779
838
|
|
|
780
839
|
// Add svg icon
|
|
781
840
|
const plus = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
@@ -881,20 +940,20 @@ export default class Render {
|
|
|
881
940
|
}
|
|
882
941
|
|
|
883
942
|
public searchFocus(): void {
|
|
884
|
-
this.content.search.input.focus()
|
|
943
|
+
this.content.search.input.focus({ preventScroll: true })
|
|
885
944
|
}
|
|
886
945
|
|
|
887
946
|
public getOptions(notPlaceholder = false, notDisabled = false, notHidden = false): HTMLDivElement[] {
|
|
888
947
|
// Put together query string
|
|
889
|
-
let query = '.' + this.classes.option
|
|
948
|
+
let query = '.' + this.classes.getFirst('option')
|
|
890
949
|
if (notPlaceholder) {
|
|
891
|
-
query += ':not(.' + this.classes.placeholder + ')'
|
|
950
|
+
query += ':not(.' + this.classes.getFirst('placeholder') + ')'
|
|
892
951
|
}
|
|
893
952
|
if (notDisabled) {
|
|
894
|
-
query += ':not(.' + this.classes.disabled + ')'
|
|
953
|
+
query += ':not(.' + this.classes.getFirst('disabled') + ')'
|
|
895
954
|
}
|
|
896
955
|
if (notHidden) {
|
|
897
|
-
query += ':not(.' + this.classes.hide + ')'
|
|
956
|
+
query += ':not(.' + this.classes.getFirst('hide') + ')'
|
|
898
957
|
}
|
|
899
958
|
|
|
900
959
|
return Array.from(this.content.list.querySelectorAll(query))
|
|
@@ -913,8 +972,8 @@ export default class Render {
|
|
|
913
972
|
// If length is 1, highlight it
|
|
914
973
|
if (options.length === 1) {
|
|
915
974
|
// Check if option doesnt already have highlighted class
|
|
916
|
-
if (!options[0].classList.contains(this.classes.highlighted)) {
|
|
917
|
-
options[0]
|
|
975
|
+
if (!options[0].classList.contains(this.classes.getFirst('highlighted'))) {
|
|
976
|
+
this.addClasses(options[0], this.classes.highlighted)
|
|
918
977
|
return
|
|
919
978
|
}
|
|
920
979
|
}
|
|
@@ -922,7 +981,7 @@ export default class Render {
|
|
|
922
981
|
// Loop through options and see if there are no highlighted ones
|
|
923
982
|
let highlighted = false
|
|
924
983
|
for (const o of options) {
|
|
925
|
-
if (o.classList.contains(this.classes.highlighted)) {
|
|
984
|
+
if (o.classList.contains(this.classes.getFirst('highlighted'))) {
|
|
926
985
|
highlighted = true
|
|
927
986
|
}
|
|
928
987
|
}
|
|
@@ -930,8 +989,8 @@ export default class Render {
|
|
|
930
989
|
// If no highlighted, see if any are selected and if so highlight selected first one
|
|
931
990
|
if (!highlighted) {
|
|
932
991
|
for (const o of options) {
|
|
933
|
-
if (o.classList.contains(this.classes.selected)) {
|
|
934
|
-
|
|
992
|
+
if (o.classList.contains(this.classes.getFirst('selected'))) {
|
|
993
|
+
this.addClasses(o, this.classes.highlighted)
|
|
935
994
|
break
|
|
936
995
|
}
|
|
937
996
|
}
|
|
@@ -940,15 +999,15 @@ export default class Render {
|
|
|
940
999
|
// Loop through options and find the highlighted one
|
|
941
1000
|
for (let i = 0; i < options.length; i++) {
|
|
942
1001
|
// Found highlighted option
|
|
943
|
-
if (options[i].classList.contains(this.classes.highlighted)) {
|
|
1002
|
+
if (options[i].classList.contains(this.classes.getFirst('highlighted'))) {
|
|
944
1003
|
const prevOption = options[i]
|
|
945
1004
|
// Remove highlighted class from current one
|
|
946
|
-
|
|
1005
|
+
this.removeClasses(prevOption, this.classes.highlighted)
|
|
947
1006
|
|
|
948
1007
|
// If previous option has parent classes ss-optgroup with ss-open then click it
|
|
949
1008
|
const prevParent = prevOption.parentElement
|
|
950
|
-
if (prevParent && prevParent.classList.contains(this.classes.mainOpen)) {
|
|
951
|
-
const optgroupLabel = prevParent.querySelector('.' + this.classes.optgroupLabel) as HTMLDivElement
|
|
1009
|
+
if (prevParent && prevParent.classList.contains(this.classes.getFirst('mainOpen'))) {
|
|
1010
|
+
const optgroupLabel = prevParent.querySelector('.' + this.classes.getFirst('optgroupLabel')) as HTMLDivElement
|
|
952
1011
|
if (optgroupLabel) {
|
|
953
1012
|
optgroupLabel.click()
|
|
954
1013
|
}
|
|
@@ -957,7 +1016,7 @@ export default class Render {
|
|
|
957
1016
|
// Highlight the next one
|
|
958
1017
|
let selectOption =
|
|
959
1018
|
options[dir === 'down' ? (i + 1 < options.length ? i + 1 : 0) : i - 1 >= 0 ? i - 1 : options.length - 1]
|
|
960
|
-
|
|
1019
|
+
this.addClasses(selectOption, this.classes.highlighted)
|
|
961
1020
|
this.ensureElementInView(this.content.list, selectOption)
|
|
962
1021
|
|
|
963
1022
|
// Update aria-activedescendant for screen readers
|
|
@@ -967,8 +1026,10 @@ export default class Render {
|
|
|
967
1026
|
|
|
968
1027
|
// If selected option has parent classes ss-optgroup with ss-close then click it
|
|
969
1028
|
const selectParent = selectOption.parentElement
|
|
970
|
-
if (selectParent && selectParent.classList.contains(this.classes.close)) {
|
|
971
|
-
const optgroupLabel = selectParent.querySelector(
|
|
1029
|
+
if (selectParent && selectParent.classList.contains(this.classes.getFirst('close'))) {
|
|
1030
|
+
const optgroupLabel = selectParent.querySelector(
|
|
1031
|
+
'.' + this.classes.getFirst('optgroupLabel')
|
|
1032
|
+
) as HTMLDivElement
|
|
972
1033
|
if (optgroupLabel) {
|
|
973
1034
|
optgroupLabel.click()
|
|
974
1035
|
}
|
|
@@ -981,7 +1042,7 @@ export default class Render {
|
|
|
981
1042
|
// If we get here, there is no highlighted option
|
|
982
1043
|
// So we will highlight the first or last based upon direction
|
|
983
1044
|
const firstHighlight = options[dir === 'down' ? 0 : options.length - 1]
|
|
984
|
-
|
|
1045
|
+
this.addClasses(firstHighlight, this.classes.highlighted)
|
|
985
1046
|
|
|
986
1047
|
// Update aria-activedescendant for screen readers
|
|
987
1048
|
if (firstHighlight.id) {
|
|
@@ -995,7 +1056,7 @@ export default class Render {
|
|
|
995
1056
|
// Create main container that options will reside
|
|
996
1057
|
public listDiv(): HTMLDivElement {
|
|
997
1058
|
const options = document.createElement('div')
|
|
998
|
-
|
|
1059
|
+
this.addClasses(options, this.classes.list)
|
|
999
1060
|
|
|
1000
1061
|
// Add id for ARIA controls reference
|
|
1001
1062
|
const listId = this.settings.id + '-list'
|
|
@@ -1010,7 +1071,7 @@ export default class Render {
|
|
|
1010
1071
|
this.content.list.innerHTML = ''
|
|
1011
1072
|
|
|
1012
1073
|
const errorDiv = document.createElement('div')
|
|
1013
|
-
|
|
1074
|
+
this.addClasses(errorDiv, this.classes.error)
|
|
1014
1075
|
errorDiv.textContent = error
|
|
1015
1076
|
this.content.list.appendChild(errorDiv)
|
|
1016
1077
|
}
|
|
@@ -1020,7 +1081,7 @@ export default class Render {
|
|
|
1020
1081
|
this.content.list.innerHTML = ''
|
|
1021
1082
|
|
|
1022
1083
|
const searchingDiv = document.createElement('div')
|
|
1023
|
-
|
|
1084
|
+
this.addClasses(searchingDiv, this.classes.searching)
|
|
1024
1085
|
searchingDiv.textContent = this.settings.searchingText
|
|
1025
1086
|
this.content.list.appendChild(searchingDiv)
|
|
1026
1087
|
}
|
|
@@ -1033,7 +1094,7 @@ export default class Render {
|
|
|
1033
1094
|
// If no results show no results text
|
|
1034
1095
|
if (data.length === 0) {
|
|
1035
1096
|
const noResults = document.createElement('div')
|
|
1036
|
-
|
|
1097
|
+
this.addClasses(noResults, this.classes.search)
|
|
1037
1098
|
|
|
1038
1099
|
//
|
|
1039
1100
|
if (this.callbacks.addable) {
|
|
@@ -1069,29 +1130,29 @@ export default class Render {
|
|
|
1069
1130
|
if (d instanceof Optgroup) {
|
|
1070
1131
|
// Create optgroup
|
|
1071
1132
|
const optgroupEl = document.createElement('div')
|
|
1072
|
-
|
|
1133
|
+
this.addClasses(optgroupEl, this.classes.optgroup)
|
|
1073
1134
|
|
|
1074
1135
|
// Create label
|
|
1075
1136
|
const optgroupLabel = document.createElement('div')
|
|
1076
|
-
|
|
1137
|
+
this.addClasses(optgroupLabel, this.classes.optgroupLabel)
|
|
1077
1138
|
optgroupEl.appendChild(optgroupLabel)
|
|
1078
1139
|
|
|
1079
1140
|
// Create label text div element
|
|
1080
1141
|
const optgroupLabelText = document.createElement('div')
|
|
1081
|
-
|
|
1142
|
+
this.addClasses(optgroupLabelText, this.classes.optgroupLabelText)
|
|
1082
1143
|
optgroupLabelText.textContent = d.label
|
|
1083
1144
|
optgroupLabel.appendChild(optgroupLabelText)
|
|
1084
1145
|
|
|
1085
1146
|
// Create options container
|
|
1086
1147
|
const optgroupActions = document.createElement('div')
|
|
1087
|
-
|
|
1148
|
+
this.addClasses(optgroupActions, this.classes.optgroupActions)
|
|
1088
1149
|
optgroupLabel.appendChild(optgroupActions)
|
|
1089
1150
|
|
|
1090
1151
|
// If selectByGroup is true and isMultiple then add click event to label
|
|
1091
1152
|
if (this.settings.isMultiple && d.selectAll) {
|
|
1092
1153
|
// Create new div to hold a checkbox svg
|
|
1093
1154
|
const selectAll = document.createElement('div')
|
|
1094
|
-
|
|
1155
|
+
this.addClasses(selectAll, this.classes.optgroupSelectAll)
|
|
1095
1156
|
|
|
1096
1157
|
// Check options and if all are selected, if so add class selected
|
|
1097
1158
|
let allSelected = true
|
|
@@ -1104,7 +1165,7 @@ export default class Render {
|
|
|
1104
1165
|
|
|
1105
1166
|
// Add class if all selected
|
|
1106
1167
|
if (allSelected) {
|
|
1107
|
-
|
|
1168
|
+
this.addClasses(selectAll, this.classes.selected)
|
|
1108
1169
|
}
|
|
1109
1170
|
|
|
1110
1171
|
// Add select all text span
|
|
@@ -1177,12 +1238,12 @@ export default class Render {
|
|
|
1177
1238
|
if (d.closable !== 'off') {
|
|
1178
1239
|
// Create new div to hold a checkbox svg
|
|
1179
1240
|
const optgroupClosable = document.createElement('div')
|
|
1180
|
-
|
|
1241
|
+
this.addClasses(optgroupClosable, this.classes.optgroupClosable)
|
|
1181
1242
|
|
|
1182
1243
|
// Create svg arrow
|
|
1183
1244
|
const optgroupClosableSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
1184
1245
|
optgroupClosableSvg.setAttribute('viewBox', '0 0 100 100')
|
|
1185
|
-
|
|
1246
|
+
this.addClasses(optgroupClosableSvg, this.classes.arrow)
|
|
1186
1247
|
optgroupClosable.appendChild(optgroupClosableSvg)
|
|
1187
1248
|
|
|
1188
1249
|
// Create new path for arrow
|
|
@@ -1191,13 +1252,13 @@ export default class Render {
|
|
|
1191
1252
|
|
|
1192
1253
|
// If any options are selected or someone is searching, set optgroup to open
|
|
1193
1254
|
if (d.options.some((o) => o.selected) || this.content.search.input.value.trim() !== '') {
|
|
1194
|
-
|
|
1255
|
+
this.addClasses(optgroupClosable, this.classes.mainOpen)
|
|
1195
1256
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
|
|
1196
1257
|
} else if (d.closable === 'open') {
|
|
1197
|
-
|
|
1258
|
+
this.addClasses(optgroupEl, this.classes.mainOpen)
|
|
1198
1259
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
|
|
1199
1260
|
} else if (d.closable === 'close') {
|
|
1200
|
-
|
|
1261
|
+
this.addClasses(optgroupEl, this.classes.close)
|
|
1201
1262
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowClose)
|
|
1202
1263
|
}
|
|
1203
1264
|
|
|
@@ -1207,13 +1268,13 @@ export default class Render {
|
|
|
1207
1268
|
e.stopPropagation()
|
|
1208
1269
|
|
|
1209
1270
|
// If optgroup is closed, open it
|
|
1210
|
-
if (optgroupEl.classList.contains(this.classes.close)) {
|
|
1211
|
-
|
|
1212
|
-
|
|
1271
|
+
if (optgroupEl.classList.contains(this.classes.getFirst('close'))) {
|
|
1272
|
+
this.removeClasses(optgroupEl, this.classes.close)
|
|
1273
|
+
this.addClasses(optgroupEl, this.classes.mainOpen)
|
|
1213
1274
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
|
|
1214
1275
|
} else {
|
|
1215
|
-
|
|
1216
|
-
|
|
1276
|
+
this.removeClasses(optgroupEl, this.classes.mainOpen)
|
|
1277
|
+
this.addClasses(optgroupEl, this.classes.close)
|
|
1217
1278
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowClose)
|
|
1218
1279
|
}
|
|
1219
1280
|
})
|
|
@@ -1247,8 +1308,8 @@ export default class Render {
|
|
|
1247
1308
|
// Add hidden placeholder
|
|
1248
1309
|
if (option.placeholder) {
|
|
1249
1310
|
const placeholder = document.createElement('div')
|
|
1250
|
-
|
|
1251
|
-
|
|
1311
|
+
this.addClasses(placeholder, this.classes.option)
|
|
1312
|
+
this.addClasses(placeholder, this.classes.hide)
|
|
1252
1313
|
return placeholder
|
|
1253
1314
|
}
|
|
1254
1315
|
|
|
@@ -1256,7 +1317,7 @@ export default class Render {
|
|
|
1256
1317
|
const optionEl = document.createElement('div')
|
|
1257
1318
|
optionEl.dataset.id = option.id // Dataset id for identifying an option
|
|
1258
1319
|
optionEl.id = this.settings.id + '-' + option.id // Unique ID for ARIA references
|
|
1259
|
-
|
|
1320
|
+
this.addClasses(optionEl, this.classes.option)
|
|
1260
1321
|
optionEl.setAttribute('role', 'option') // WCAG attribute
|
|
1261
1322
|
if (option.class) {
|
|
1262
1323
|
option.class.split(' ').forEach((dataClass: string) => {
|
|
@@ -1287,26 +1348,26 @@ export default class Render {
|
|
|
1287
1348
|
|
|
1288
1349
|
// If option is disabled
|
|
1289
1350
|
if (!option.display) {
|
|
1290
|
-
|
|
1351
|
+
this.addClasses(optionEl, this.classes.hide)
|
|
1291
1352
|
}
|
|
1292
1353
|
|
|
1293
1354
|
// If allowed to deselect, null onclick and add disabled
|
|
1294
1355
|
if (option.disabled) {
|
|
1295
|
-
|
|
1356
|
+
this.addClasses(optionEl, this.classes.disabled)
|
|
1296
1357
|
}
|
|
1297
1358
|
|
|
1298
1359
|
// If option is selected and hideSelectedOption is true, hide it
|
|
1299
1360
|
if (option.selected && this.settings.hideSelected) {
|
|
1300
|
-
|
|
1361
|
+
this.addClasses(optionEl, this.classes.hide)
|
|
1301
1362
|
}
|
|
1302
1363
|
|
|
1303
1364
|
// If option is selected
|
|
1304
1365
|
if (option.selected) {
|
|
1305
|
-
|
|
1366
|
+
this.addClasses(optionEl, this.classes.selected)
|
|
1306
1367
|
optionEl.setAttribute('aria-selected', 'true')
|
|
1307
1368
|
this.main.main.setAttribute('aria-activedescendant', optionEl.id)
|
|
1308
1369
|
} else {
|
|
1309
|
-
|
|
1370
|
+
this.removeClasses(optionEl, this.classes.selected)
|
|
1310
1371
|
optionEl.setAttribute('aria-selected', 'false')
|
|
1311
1372
|
}
|
|
1312
1373
|
|
|
@@ -1462,6 +1523,12 @@ export default class Render {
|
|
|
1462
1523
|
}
|
|
1463
1524
|
|
|
1464
1525
|
public destroy(): void {
|
|
1526
|
+
// Clear any pending timeouts
|
|
1527
|
+
if (this.closeAnimationTimeout) {
|
|
1528
|
+
clearTimeout(this.closeAnimationTimeout)
|
|
1529
|
+
this.closeAnimationTimeout = null
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1465
1532
|
// Remove main
|
|
1466
1533
|
this.main.main.remove()
|
|
1467
1534
|
|
|
@@ -1521,48 +1588,62 @@ export default class Render {
|
|
|
1521
1588
|
return tempDiv.innerHTML
|
|
1522
1589
|
}
|
|
1523
1590
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const
|
|
1527
|
-
const
|
|
1591
|
+
private setContentDirection(direction: 'above' | 'below'): void {
|
|
1592
|
+
const isAbove = direction === 'above'
|
|
1593
|
+
const addClass = isAbove ? this.classes.dirAbove : this.classes.dirBelow
|
|
1594
|
+
const removeClass = isAbove ? this.classes.dirBelow : this.classes.dirAbove
|
|
1528
1595
|
|
|
1529
1596
|
// Set direction classes on both main and content
|
|
1530
|
-
this.
|
|
1531
|
-
this.
|
|
1532
|
-
this.
|
|
1533
|
-
this.
|
|
1597
|
+
this.removeClasses(this.main.main, removeClass)
|
|
1598
|
+
this.addClasses(this.main.main, addClass)
|
|
1599
|
+
this.removeClasses(this.content.main, removeClass)
|
|
1600
|
+
this.addClasses(this.content.main, addClass)
|
|
1601
|
+
|
|
1602
|
+
// Set margin to position content
|
|
1603
|
+
if (isAbove) {
|
|
1604
|
+
const mainHeight = this.main.main.offsetHeight
|
|
1605
|
+
const contentHeight = this.content.main.offsetHeight
|
|
1606
|
+
this.content.main.style.margin = '-' + (mainHeight + contentHeight - 1) + 'px 0px 0px 0px'
|
|
1607
|
+
} else {
|
|
1608
|
+
this.content.main.style.margin = '-1px 0px 0px 0px'
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
private setContentPosition(): void {
|
|
1613
|
+
if (this.settings.contentPosition === 'relative') {
|
|
1614
|
+
return
|
|
1615
|
+
}
|
|
1534
1616
|
|
|
1535
|
-
// Set the content position
|
|
1536
1617
|
const containerRect = this.main.main.getBoundingClientRect()
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
this.
|
|
1541
|
-
|
|
1618
|
+
let top: number
|
|
1619
|
+
let left: number
|
|
1620
|
+
|
|
1621
|
+
if (this.settings.contentPosition === 'fixed') {
|
|
1622
|
+
// Fixed positioning - use viewport coordinates directly
|
|
1623
|
+
top = containerRect.top + containerRect.height
|
|
1624
|
+
left = containerRect.left
|
|
1625
|
+
} else {
|
|
1626
|
+
// Absolute positioning - calculate relative to offsetParent
|
|
1627
|
+
const offsetParent = this.content.main.offsetParent as HTMLElement
|
|
1628
|
+
const offsetParentRect = offsetParent ? offsetParent.getBoundingClientRect() : { top: 0, left: 0 }
|
|
1629
|
+
|
|
1630
|
+
top = containerRect.top - offsetParentRect.top + containerRect.height - (offsetParent?.clientTop || 0)
|
|
1631
|
+
left = containerRect.left - offsetParentRect.left - (offsetParent?.clientLeft || 0)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
this.content.main.style.top = top + 'px'
|
|
1635
|
+
this.content.main.style.left = left + 'px'
|
|
1542
1636
|
this.content.main.style.width = containerRect.width + 'px'
|
|
1543
1637
|
}
|
|
1544
1638
|
|
|
1545
|
-
public
|
|
1546
|
-
|
|
1547
|
-
this.
|
|
1548
|
-
|
|
1549
|
-
this.content.main.classList.remove(this.classes.dirAbove)
|
|
1550
|
-
this.content.main.classList.add(this.classes.dirBelow)
|
|
1639
|
+
public moveContentAbove(): void {
|
|
1640
|
+
this.setContentDirection('above')
|
|
1641
|
+
this.setContentPosition()
|
|
1642
|
+
}
|
|
1551
1643
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
this.
|
|
1555
|
-
// Dont do anything if the content is relative
|
|
1556
|
-
if (this.settings.contentPosition !== 'relative') {
|
|
1557
|
-
this.content.main.style.top =
|
|
1558
|
-
containerRect.top +
|
|
1559
|
-
containerRect.height +
|
|
1560
|
-
(this.settings.contentPosition === 'fixed' ? 0 : window.scrollY) +
|
|
1561
|
-
'px'
|
|
1562
|
-
this.content.main.style.left =
|
|
1563
|
-
containerRect.left + (this.settings.contentPosition === 'fixed' ? 0 : window.scrollX) + 'px'
|
|
1564
|
-
this.content.main.style.width = containerRect.width + 'px'
|
|
1565
|
-
}
|
|
1644
|
+
public moveContentBelow(): void {
|
|
1645
|
+
this.setContentDirection('below')
|
|
1646
|
+
this.setContentPosition()
|
|
1566
1647
|
}
|
|
1567
1648
|
|
|
1568
1649
|
public ensureElementInView(container: HTMLElement, element: HTMLElement): void {
|
|
@@ -1621,9 +1702,9 @@ export default class Render {
|
|
|
1621
1702
|
const hideClass = this.classes.hide
|
|
1622
1703
|
|
|
1623
1704
|
if (allowDeselect && !(isMultiple && !hasSelectedItems)) {
|
|
1624
|
-
|
|
1705
|
+
this.removeClasses(deselectButton, hideClass)
|
|
1625
1706
|
} else {
|
|
1626
|
-
|
|
1707
|
+
this.addClasses(deselectButton, hideClass)
|
|
1627
1708
|
}
|
|
1628
1709
|
}
|
|
1629
1710
|
}
|