slim-select 3.2.0 → 3.4.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 +7 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +291 -246
- 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 +308 -263
- 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 +7 -0
- package/dist/vue/index.js +214 -169
- package/dist/vue/index.umd.js +1 -1
- package/package.json +24 -23
- package/sitemap.js +21 -2
- package/src/slim-select/classes.test.ts +3 -3
- package/src/slim-select/classes.ts +40 -34
- package/src/slim-select/index.test.ts +26 -5
- package/src/slim-select/index.ts +12 -0
- package/src/slim-select/render.test.ts +216 -0
- package/src/slim-select/render.ts +204 -115
- package/src/slim-select/select.ts +11 -7
- package/src/slim-select/settings.test.ts +2 -1
- 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')
|
|
@@ -158,9 +190,12 @@ export default class Render {
|
|
|
158
190
|
|
|
159
191
|
// Remove direction class from main and content after animation is complete
|
|
160
192
|
const animationTiming = this.getAnimationTiming()
|
|
161
|
-
setTimeout(() => {
|
|
162
|
-
this.
|
|
163
|
-
this.
|
|
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
|
|
164
199
|
}, animationTiming)
|
|
165
200
|
}
|
|
166
201
|
|
|
@@ -189,8 +224,8 @@ export default class Render {
|
|
|
189
224
|
this.content.main.removeAttribute('style')
|
|
190
225
|
|
|
191
226
|
// Make sure main/content has its base class
|
|
192
|
-
this.main.main
|
|
193
|
-
this.content.main
|
|
227
|
+
this.addClasses(this.main.main, this.classes.main)
|
|
228
|
+
this.addClasses(this.content.main, this.classes.content)
|
|
194
229
|
|
|
195
230
|
// Add styles
|
|
196
231
|
if (this.settings.style !== '') {
|
|
@@ -267,7 +302,9 @@ export default class Render {
|
|
|
267
302
|
case 'Enter':
|
|
268
303
|
case ' ':
|
|
269
304
|
this.callbacks.open()
|
|
270
|
-
const highlighted = this.content.list.querySelector(
|
|
305
|
+
const highlighted = this.content.list.querySelector(
|
|
306
|
+
'.' + this.classes.getFirst('highlighted')
|
|
307
|
+
) as HTMLDivElement
|
|
271
308
|
if (highlighted) {
|
|
272
309
|
highlighted.click()
|
|
273
310
|
}
|
|
@@ -297,19 +334,19 @@ export default class Render {
|
|
|
297
334
|
|
|
298
335
|
// Add values
|
|
299
336
|
const values = document.createElement('div')
|
|
300
|
-
|
|
337
|
+
this.addClasses(values, this.classes.values)
|
|
301
338
|
main.appendChild(values)
|
|
302
339
|
|
|
303
340
|
// Add deselect
|
|
304
341
|
const deselect = document.createElement('div')
|
|
305
|
-
|
|
342
|
+
this.addClasses(deselect, this.classes.deselect)
|
|
306
343
|
|
|
307
344
|
// Check if deselect is to be shown or not
|
|
308
345
|
const selectedOptions = this.store?.getSelectedOptions()
|
|
309
346
|
if (!this.settings.allowDeselect || (this.settings.isMultiple && selectedOptions && selectedOptions.length <= 0)) {
|
|
310
|
-
|
|
347
|
+
this.addClasses(deselect, this.classes.hide)
|
|
311
348
|
} else {
|
|
312
|
-
|
|
349
|
+
this.removeClasses(deselect, this.classes.hide)
|
|
313
350
|
}
|
|
314
351
|
|
|
315
352
|
// Add deselect onclick event
|
|
@@ -366,12 +403,12 @@ export default class Render {
|
|
|
366
403
|
|
|
367
404
|
// Add arrow
|
|
368
405
|
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
369
|
-
|
|
406
|
+
this.addClasses(arrow, this.classes.arrow)
|
|
370
407
|
arrow.setAttribute('viewBox', '0 0 100 100')
|
|
371
408
|
const arrowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
372
409
|
arrowPath.setAttribute('d', this.classes.arrowClose)
|
|
373
410
|
if (this.settings.alwaysOpen) {
|
|
374
|
-
|
|
411
|
+
this.addClasses(arrow, this.classes.hide)
|
|
375
412
|
}
|
|
376
413
|
arrow.appendChild(arrowPath)
|
|
377
414
|
main.appendChild(arrow)
|
|
@@ -420,7 +457,7 @@ export default class Render {
|
|
|
420
457
|
|
|
421
458
|
// Create placeholder div
|
|
422
459
|
const placeholder = document.createElement('div')
|
|
423
|
-
|
|
460
|
+
this.addClasses(placeholder, this.classes.placeholder)
|
|
424
461
|
placeholder.innerHTML = placeholderText
|
|
425
462
|
return placeholder
|
|
426
463
|
}
|
|
@@ -451,7 +488,7 @@ export default class Render {
|
|
|
451
488
|
} else {
|
|
452
489
|
// Create single value container
|
|
453
490
|
const singleValue = document.createElement('div')
|
|
454
|
-
|
|
491
|
+
this.addClasses(singleValue, this.classes.single)
|
|
455
492
|
if (selectedSingle.html) {
|
|
456
493
|
singleValue.innerHTML = selectedSingle.html
|
|
457
494
|
} else {
|
|
@@ -464,9 +501,9 @@ export default class Render {
|
|
|
464
501
|
|
|
465
502
|
// If allowDeselect is false or selected value is empty just hide deselect
|
|
466
503
|
if (!this.settings.allowDeselect || !selected.length) {
|
|
467
|
-
this.main.deselect.main
|
|
504
|
+
this.addClasses(this.main.deselect.main, this.classes.hide)
|
|
468
505
|
} else {
|
|
469
|
-
this.main.deselect.main
|
|
506
|
+
this.removeClasses(this.main.deselect.main, this.classes.hide)
|
|
470
507
|
}
|
|
471
508
|
}
|
|
472
509
|
|
|
@@ -484,7 +521,7 @@ export default class Render {
|
|
|
484
521
|
return
|
|
485
522
|
} else {
|
|
486
523
|
// If there is a placeholder, remove it
|
|
487
|
-
const placeholder = this.main.values.querySelector('.' + this.classes.placeholder)
|
|
524
|
+
const placeholder = this.main.values.querySelector('.' + this.classes.getFirst('placeholder'))
|
|
488
525
|
if (placeholder) {
|
|
489
526
|
placeholder.remove()
|
|
490
527
|
}
|
|
@@ -494,7 +531,7 @@ export default class Render {
|
|
|
494
531
|
if (selectedOptions.length > this.settings.maxValuesShown) {
|
|
495
532
|
// Creating the element that shows the number of selected items
|
|
496
533
|
const singleValue = document.createElement('div')
|
|
497
|
-
|
|
534
|
+
this.addClasses(singleValue, this.classes.max)
|
|
498
535
|
singleValue.textContent = this.settings.maxValuesMessage.replace('{number}', selectedOptions.length.toString())
|
|
499
536
|
|
|
500
537
|
// If there is a selected value, set a single div
|
|
@@ -502,7 +539,7 @@ export default class Render {
|
|
|
502
539
|
return
|
|
503
540
|
} else {
|
|
504
541
|
// If there is a message, remove it
|
|
505
|
-
const maxValuesMessage = this.main.values.querySelector('.' + this.classes.max)
|
|
542
|
+
const maxValuesMessage = this.main.values.querySelector('.' + this.classes.getFirst('max'))
|
|
506
543
|
if (maxValuesMessage) {
|
|
507
544
|
maxValuesMessage.remove()
|
|
508
545
|
}
|
|
@@ -533,7 +570,7 @@ export default class Render {
|
|
|
533
570
|
|
|
534
571
|
// Loop through and remove
|
|
535
572
|
for (const n of removeNodes) {
|
|
536
|
-
|
|
573
|
+
this.addClasses(n, this.classes.valueOut)
|
|
537
574
|
setTimeout(() => {
|
|
538
575
|
if (this.main.values.hasChildNodes() && this.main.values.contains(n)) {
|
|
539
576
|
this.main.values.removeChild(n)
|
|
@@ -572,11 +609,11 @@ export default class Render {
|
|
|
572
609
|
|
|
573
610
|
public multipleValue(option: Option): HTMLDivElement {
|
|
574
611
|
const value = document.createElement('div')
|
|
575
|
-
|
|
612
|
+
this.addClasses(value, this.classes.value)
|
|
576
613
|
value.dataset.id = option.id
|
|
577
614
|
|
|
578
615
|
const text = document.createElement('div')
|
|
579
|
-
|
|
616
|
+
this.addClasses(text, this.classes.valueText)
|
|
580
617
|
text.textContent = option.text // For multiple values always use text
|
|
581
618
|
value.appendChild(text)
|
|
582
619
|
|
|
@@ -584,7 +621,7 @@ export default class Render {
|
|
|
584
621
|
if (!option.mandatory) {
|
|
585
622
|
// Create delete div element
|
|
586
623
|
const deleteDiv = document.createElement('div')
|
|
587
|
-
|
|
624
|
+
this.addClasses(deleteDiv, this.classes.valueDelete)
|
|
588
625
|
deleteDiv.setAttribute('tabindex', '0') // Make the div focusable for tab navigation
|
|
589
626
|
|
|
590
627
|
// Add delete onclick event
|
|
@@ -717,7 +754,7 @@ export default class Render {
|
|
|
717
754
|
const main = document.createElement('div')
|
|
718
755
|
const input = document.createElement('input')
|
|
719
756
|
const addable = document.createElement('div')
|
|
720
|
-
|
|
757
|
+
this.addClasses(main, this.classes.search)
|
|
721
758
|
|
|
722
759
|
// Setup search return object
|
|
723
760
|
const searchReturn: Search = {
|
|
@@ -727,7 +764,7 @@ export default class Render {
|
|
|
727
764
|
|
|
728
765
|
// We still want the search to be tabable but not shown
|
|
729
766
|
if (!this.settings.showSearch) {
|
|
730
|
-
|
|
767
|
+
this.addClasses(main, this.classes.hide)
|
|
731
768
|
input.readOnly = true
|
|
732
769
|
}
|
|
733
770
|
|
|
@@ -764,7 +801,9 @@ export default class Render {
|
|
|
764
801
|
this.callbacks.close()
|
|
765
802
|
return false
|
|
766
803
|
case ' ':
|
|
767
|
-
const highlighted = this.content.list.querySelector(
|
|
804
|
+
const highlighted = this.content.list.querySelector(
|
|
805
|
+
'.' + this.classes.getFirst('highlighted')
|
|
806
|
+
) as HTMLDivElement
|
|
768
807
|
if (highlighted) {
|
|
769
808
|
highlighted.click()
|
|
770
809
|
return false
|
|
@@ -772,7 +811,9 @@ export default class Render {
|
|
|
772
811
|
return true
|
|
773
812
|
case 'Enter':
|
|
774
813
|
// Check if there's a highlighted option first
|
|
775
|
-
const highlightedEnter = this.content.list.querySelector(
|
|
814
|
+
const highlightedEnter = this.content.list.querySelector(
|
|
815
|
+
'.' + this.classes.getFirst('highlighted')
|
|
816
|
+
) as HTMLDivElement
|
|
776
817
|
if (highlightedEnter) {
|
|
777
818
|
// If an option is highlighted, select it (even if addable is enabled)
|
|
778
819
|
highlightedEnter.click()
|
|
@@ -793,7 +834,7 @@ export default class Render {
|
|
|
793
834
|
// If addable is enabled, add the addable div
|
|
794
835
|
if (this.callbacks.addable) {
|
|
795
836
|
// Add main class
|
|
796
|
-
|
|
837
|
+
this.addClasses(addable, this.classes.addable)
|
|
797
838
|
|
|
798
839
|
// Add svg icon
|
|
799
840
|
const plus = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
@@ -904,15 +945,15 @@ export default class Render {
|
|
|
904
945
|
|
|
905
946
|
public getOptions(notPlaceholder = false, notDisabled = false, notHidden = false): HTMLDivElement[] {
|
|
906
947
|
// Put together query string
|
|
907
|
-
let query = '.' + this.classes.option
|
|
948
|
+
let query = '.' + this.classes.getFirst('option')
|
|
908
949
|
if (notPlaceholder) {
|
|
909
|
-
query += ':not(.' + this.classes.placeholder + ')'
|
|
950
|
+
query += ':not(.' + this.classes.getFirst('placeholder') + ')'
|
|
910
951
|
}
|
|
911
952
|
if (notDisabled) {
|
|
912
|
-
query += ':not(.' + this.classes.disabled + ')'
|
|
953
|
+
query += ':not(.' + this.classes.getFirst('disabled') + ')'
|
|
913
954
|
}
|
|
914
955
|
if (notHidden) {
|
|
915
|
-
query += ':not(.' + this.classes.hide + ')'
|
|
956
|
+
query += ':not(.' + this.classes.getFirst('hide') + ')'
|
|
916
957
|
}
|
|
917
958
|
|
|
918
959
|
return Array.from(this.content.list.querySelectorAll(query))
|
|
@@ -931,8 +972,8 @@ export default class Render {
|
|
|
931
972
|
// If length is 1, highlight it
|
|
932
973
|
if (options.length === 1) {
|
|
933
974
|
// Check if option doesnt already have highlighted class
|
|
934
|
-
if (!options[0].classList.contains(this.classes.highlighted)) {
|
|
935
|
-
options[0]
|
|
975
|
+
if (!options[0].classList.contains(this.classes.getFirst('highlighted'))) {
|
|
976
|
+
this.addClasses(options[0], this.classes.highlighted)
|
|
936
977
|
return
|
|
937
978
|
}
|
|
938
979
|
}
|
|
@@ -940,7 +981,7 @@ export default class Render {
|
|
|
940
981
|
// Loop through options and see if there are no highlighted ones
|
|
941
982
|
let highlighted = false
|
|
942
983
|
for (const o of options) {
|
|
943
|
-
if (o.classList.contains(this.classes.highlighted)) {
|
|
984
|
+
if (o.classList.contains(this.classes.getFirst('highlighted'))) {
|
|
944
985
|
highlighted = true
|
|
945
986
|
}
|
|
946
987
|
}
|
|
@@ -948,8 +989,8 @@ export default class Render {
|
|
|
948
989
|
// If no highlighted, see if any are selected and if so highlight selected first one
|
|
949
990
|
if (!highlighted) {
|
|
950
991
|
for (const o of options) {
|
|
951
|
-
if (o.classList.contains(this.classes.selected)) {
|
|
952
|
-
|
|
992
|
+
if (o.classList.contains(this.classes.getFirst('selected'))) {
|
|
993
|
+
this.addClasses(o, this.classes.highlighted)
|
|
953
994
|
break
|
|
954
995
|
}
|
|
955
996
|
}
|
|
@@ -958,15 +999,15 @@ export default class Render {
|
|
|
958
999
|
// Loop through options and find the highlighted one
|
|
959
1000
|
for (let i = 0; i < options.length; i++) {
|
|
960
1001
|
// Found highlighted option
|
|
961
|
-
if (options[i].classList.contains(this.classes.highlighted)) {
|
|
1002
|
+
if (options[i].classList.contains(this.classes.getFirst('highlighted'))) {
|
|
962
1003
|
const prevOption = options[i]
|
|
963
1004
|
// Remove highlighted class from current one
|
|
964
|
-
|
|
1005
|
+
this.removeClasses(prevOption, this.classes.highlighted)
|
|
965
1006
|
|
|
966
1007
|
// If previous option has parent classes ss-optgroup with ss-open then click it
|
|
967
1008
|
const prevParent = prevOption.parentElement
|
|
968
|
-
if (prevParent && prevParent.classList.contains(this.classes.mainOpen)) {
|
|
969
|
-
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
|
|
970
1011
|
if (optgroupLabel) {
|
|
971
1012
|
optgroupLabel.click()
|
|
972
1013
|
}
|
|
@@ -975,7 +1016,7 @@ export default class Render {
|
|
|
975
1016
|
// Highlight the next one
|
|
976
1017
|
let selectOption =
|
|
977
1018
|
options[dir === 'down' ? (i + 1 < options.length ? i + 1 : 0) : i - 1 >= 0 ? i - 1 : options.length - 1]
|
|
978
|
-
|
|
1019
|
+
this.addClasses(selectOption, this.classes.highlighted)
|
|
979
1020
|
this.ensureElementInView(this.content.list, selectOption)
|
|
980
1021
|
|
|
981
1022
|
// Update aria-activedescendant for screen readers
|
|
@@ -985,8 +1026,10 @@ export default class Render {
|
|
|
985
1026
|
|
|
986
1027
|
// If selected option has parent classes ss-optgroup with ss-close then click it
|
|
987
1028
|
const selectParent = selectOption.parentElement
|
|
988
|
-
if (selectParent && selectParent.classList.contains(this.classes.close)) {
|
|
989
|
-
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
|
|
990
1033
|
if (optgroupLabel) {
|
|
991
1034
|
optgroupLabel.click()
|
|
992
1035
|
}
|
|
@@ -999,7 +1042,7 @@ export default class Render {
|
|
|
999
1042
|
// If we get here, there is no highlighted option
|
|
1000
1043
|
// So we will highlight the first or last based upon direction
|
|
1001
1044
|
const firstHighlight = options[dir === 'down' ? 0 : options.length - 1]
|
|
1002
|
-
|
|
1045
|
+
this.addClasses(firstHighlight, this.classes.highlighted)
|
|
1003
1046
|
|
|
1004
1047
|
// Update aria-activedescendant for screen readers
|
|
1005
1048
|
if (firstHighlight.id) {
|
|
@@ -1013,7 +1056,7 @@ export default class Render {
|
|
|
1013
1056
|
// Create main container that options will reside
|
|
1014
1057
|
public listDiv(): HTMLDivElement {
|
|
1015
1058
|
const options = document.createElement('div')
|
|
1016
|
-
|
|
1059
|
+
this.addClasses(options, this.classes.list)
|
|
1017
1060
|
|
|
1018
1061
|
// Add id for ARIA controls reference
|
|
1019
1062
|
const listId = this.settings.id + '-list'
|
|
@@ -1028,7 +1071,7 @@ export default class Render {
|
|
|
1028
1071
|
this.content.list.innerHTML = ''
|
|
1029
1072
|
|
|
1030
1073
|
const errorDiv = document.createElement('div')
|
|
1031
|
-
|
|
1074
|
+
this.addClasses(errorDiv, this.classes.error)
|
|
1032
1075
|
errorDiv.textContent = error
|
|
1033
1076
|
this.content.list.appendChild(errorDiv)
|
|
1034
1077
|
}
|
|
@@ -1038,7 +1081,7 @@ export default class Render {
|
|
|
1038
1081
|
this.content.list.innerHTML = ''
|
|
1039
1082
|
|
|
1040
1083
|
const searchingDiv = document.createElement('div')
|
|
1041
|
-
|
|
1084
|
+
this.addClasses(searchingDiv, this.classes.searching)
|
|
1042
1085
|
searchingDiv.textContent = this.settings.searchingText
|
|
1043
1086
|
this.content.list.appendChild(searchingDiv)
|
|
1044
1087
|
}
|
|
@@ -1051,7 +1094,7 @@ export default class Render {
|
|
|
1051
1094
|
// If no results show no results text
|
|
1052
1095
|
if (data.length === 0) {
|
|
1053
1096
|
const noResults = document.createElement('div')
|
|
1054
|
-
|
|
1097
|
+
this.addClasses(noResults, this.classes.search)
|
|
1055
1098
|
|
|
1056
1099
|
//
|
|
1057
1100
|
if (this.callbacks.addable) {
|
|
@@ -1087,29 +1130,29 @@ export default class Render {
|
|
|
1087
1130
|
if (d instanceof Optgroup) {
|
|
1088
1131
|
// Create optgroup
|
|
1089
1132
|
const optgroupEl = document.createElement('div')
|
|
1090
|
-
|
|
1133
|
+
this.addClasses(optgroupEl, this.classes.optgroup)
|
|
1091
1134
|
|
|
1092
1135
|
// Create label
|
|
1093
1136
|
const optgroupLabel = document.createElement('div')
|
|
1094
|
-
|
|
1137
|
+
this.addClasses(optgroupLabel, this.classes.optgroupLabel)
|
|
1095
1138
|
optgroupEl.appendChild(optgroupLabel)
|
|
1096
1139
|
|
|
1097
1140
|
// Create label text div element
|
|
1098
1141
|
const optgroupLabelText = document.createElement('div')
|
|
1099
|
-
|
|
1142
|
+
this.addClasses(optgroupLabelText, this.classes.optgroupLabelText)
|
|
1100
1143
|
optgroupLabelText.textContent = d.label
|
|
1101
1144
|
optgroupLabel.appendChild(optgroupLabelText)
|
|
1102
1145
|
|
|
1103
1146
|
// Create options container
|
|
1104
1147
|
const optgroupActions = document.createElement('div')
|
|
1105
|
-
|
|
1148
|
+
this.addClasses(optgroupActions, this.classes.optgroupActions)
|
|
1106
1149
|
optgroupLabel.appendChild(optgroupActions)
|
|
1107
1150
|
|
|
1108
1151
|
// If selectByGroup is true and isMultiple then add click event to label
|
|
1109
1152
|
if (this.settings.isMultiple && d.selectAll) {
|
|
1110
1153
|
// Create new div to hold a checkbox svg
|
|
1111
1154
|
const selectAll = document.createElement('div')
|
|
1112
|
-
|
|
1155
|
+
this.addClasses(selectAll, this.classes.optgroupSelectAll)
|
|
1113
1156
|
|
|
1114
1157
|
// Check options and if all are selected, if so add class selected
|
|
1115
1158
|
let allSelected = true
|
|
@@ -1122,7 +1165,7 @@ export default class Render {
|
|
|
1122
1165
|
|
|
1123
1166
|
// Add class if all selected
|
|
1124
1167
|
if (allSelected) {
|
|
1125
|
-
|
|
1168
|
+
this.addClasses(selectAll, this.classes.selected)
|
|
1126
1169
|
}
|
|
1127
1170
|
|
|
1128
1171
|
// Add select all text span
|
|
@@ -1195,12 +1238,12 @@ export default class Render {
|
|
|
1195
1238
|
if (d.closable !== 'off') {
|
|
1196
1239
|
// Create new div to hold a checkbox svg
|
|
1197
1240
|
const optgroupClosable = document.createElement('div')
|
|
1198
|
-
|
|
1241
|
+
this.addClasses(optgroupClosable, this.classes.optgroupClosable)
|
|
1199
1242
|
|
|
1200
1243
|
// Create svg arrow
|
|
1201
1244
|
const optgroupClosableSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
1202
1245
|
optgroupClosableSvg.setAttribute('viewBox', '0 0 100 100')
|
|
1203
|
-
|
|
1246
|
+
this.addClasses(optgroupClosableSvg, this.classes.arrow)
|
|
1204
1247
|
optgroupClosable.appendChild(optgroupClosableSvg)
|
|
1205
1248
|
|
|
1206
1249
|
// Create new path for arrow
|
|
@@ -1209,13 +1252,13 @@ export default class Render {
|
|
|
1209
1252
|
|
|
1210
1253
|
// If any options are selected or someone is searching, set optgroup to open
|
|
1211
1254
|
if (d.options.some((o) => o.selected) || this.content.search.input.value.trim() !== '') {
|
|
1212
|
-
|
|
1255
|
+
this.addClasses(optgroupClosable, this.classes.mainOpen)
|
|
1213
1256
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
|
|
1214
1257
|
} else if (d.closable === 'open') {
|
|
1215
|
-
|
|
1258
|
+
this.addClasses(optgroupEl, this.classes.mainOpen)
|
|
1216
1259
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
|
|
1217
1260
|
} else if (d.closable === 'close') {
|
|
1218
|
-
|
|
1261
|
+
this.addClasses(optgroupEl, this.classes.close)
|
|
1219
1262
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowClose)
|
|
1220
1263
|
}
|
|
1221
1264
|
|
|
@@ -1225,13 +1268,13 @@ export default class Render {
|
|
|
1225
1268
|
e.stopPropagation()
|
|
1226
1269
|
|
|
1227
1270
|
// If optgroup is closed, open it
|
|
1228
|
-
if (optgroupEl.classList.contains(this.classes.close)) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1271
|
+
if (optgroupEl.classList.contains(this.classes.getFirst('close'))) {
|
|
1272
|
+
this.removeClasses(optgroupEl, this.classes.close)
|
|
1273
|
+
this.addClasses(optgroupEl, this.classes.mainOpen)
|
|
1231
1274
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
|
|
1232
1275
|
} else {
|
|
1233
|
-
|
|
1234
|
-
|
|
1276
|
+
this.removeClasses(optgroupEl, this.classes.mainOpen)
|
|
1277
|
+
this.addClasses(optgroupEl, this.classes.close)
|
|
1235
1278
|
optgroupClosableArrow.setAttribute('d', this.classes.arrowClose)
|
|
1236
1279
|
}
|
|
1237
1280
|
})
|
|
@@ -1265,8 +1308,8 @@ export default class Render {
|
|
|
1265
1308
|
// Add hidden placeholder
|
|
1266
1309
|
if (option.placeholder) {
|
|
1267
1310
|
const placeholder = document.createElement('div')
|
|
1268
|
-
|
|
1269
|
-
|
|
1311
|
+
this.addClasses(placeholder, this.classes.option)
|
|
1312
|
+
this.addClasses(placeholder, this.classes.hide)
|
|
1270
1313
|
return placeholder
|
|
1271
1314
|
}
|
|
1272
1315
|
|
|
@@ -1274,7 +1317,7 @@ export default class Render {
|
|
|
1274
1317
|
const optionEl = document.createElement('div')
|
|
1275
1318
|
optionEl.dataset.id = option.id // Dataset id for identifying an option
|
|
1276
1319
|
optionEl.id = this.settings.id + '-' + option.id // Unique ID for ARIA references
|
|
1277
|
-
|
|
1320
|
+
this.addClasses(optionEl, this.classes.option)
|
|
1278
1321
|
optionEl.setAttribute('role', 'option') // WCAG attribute
|
|
1279
1322
|
if (option.class) {
|
|
1280
1323
|
option.class.split(' ').forEach((dataClass: string) => {
|
|
@@ -1305,26 +1348,26 @@ export default class Render {
|
|
|
1305
1348
|
|
|
1306
1349
|
// If option is disabled
|
|
1307
1350
|
if (!option.display) {
|
|
1308
|
-
|
|
1351
|
+
this.addClasses(optionEl, this.classes.hide)
|
|
1309
1352
|
}
|
|
1310
1353
|
|
|
1311
1354
|
// If allowed to deselect, null onclick and add disabled
|
|
1312
1355
|
if (option.disabled) {
|
|
1313
|
-
|
|
1356
|
+
this.addClasses(optionEl, this.classes.disabled)
|
|
1314
1357
|
}
|
|
1315
1358
|
|
|
1316
1359
|
// If option is selected and hideSelectedOption is true, hide it
|
|
1317
1360
|
if (option.selected && this.settings.hideSelected) {
|
|
1318
|
-
|
|
1361
|
+
this.addClasses(optionEl, this.classes.hide)
|
|
1319
1362
|
}
|
|
1320
1363
|
|
|
1321
1364
|
// If option is selected
|
|
1322
1365
|
if (option.selected) {
|
|
1323
|
-
|
|
1366
|
+
this.addClasses(optionEl, this.classes.selected)
|
|
1324
1367
|
optionEl.setAttribute('aria-selected', 'true')
|
|
1325
1368
|
this.main.main.setAttribute('aria-activedescendant', optionEl.id)
|
|
1326
1369
|
} else {
|
|
1327
|
-
|
|
1370
|
+
this.removeClasses(optionEl, this.classes.selected)
|
|
1328
1371
|
optionEl.setAttribute('aria-selected', 'false')
|
|
1329
1372
|
}
|
|
1330
1373
|
|
|
@@ -1480,6 +1523,12 @@ export default class Render {
|
|
|
1480
1523
|
}
|
|
1481
1524
|
|
|
1482
1525
|
public destroy(): void {
|
|
1526
|
+
// Clear any pending timeouts
|
|
1527
|
+
if (this.closeAnimationTimeout) {
|
|
1528
|
+
clearTimeout(this.closeAnimationTimeout)
|
|
1529
|
+
this.closeAnimationTimeout = null
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1483
1532
|
// Remove main
|
|
1484
1533
|
this.main.main.remove()
|
|
1485
1534
|
|
|
@@ -1539,50 +1588,90 @@ export default class Render {
|
|
|
1539
1588
|
return tempDiv.innerHTML
|
|
1540
1589
|
}
|
|
1541
1590
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
const
|
|
1545
|
-
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
|
|
1546
1595
|
|
|
1547
1596
|
// Set direction classes on both main and content
|
|
1548
|
-
this.
|
|
1549
|
-
this.
|
|
1550
|
-
this.
|
|
1551
|
-
this.
|
|
1552
|
-
|
|
1553
|
-
// Set
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
+
}
|
|
1561
1610
|
}
|
|
1562
1611
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
this.content.main.classList.remove(this.classes.dirAbove)
|
|
1568
|
-
this.content.main.classList.add(this.classes.dirBelow)
|
|
1612
|
+
private setContentPosition(): void {
|
|
1613
|
+
if (this.settings.contentPosition === 'relative') {
|
|
1614
|
+
return
|
|
1615
|
+
}
|
|
1569
1616
|
|
|
1570
|
-
//
|
|
1617
|
+
// getBoundingClientRect() returns viewport-relative coordinates.
|
|
1571
1618
|
const containerRect = this.main.main.getBoundingClientRect()
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1619
|
+
let top: number
|
|
1620
|
+
let left: number
|
|
1621
|
+
|
|
1622
|
+
if (this.settings.contentPosition === 'fixed') {
|
|
1623
|
+
// position:fixed is relative to the viewport, so viewport coords work directly.
|
|
1624
|
+
top = containerRect.top + containerRect.height
|
|
1625
|
+
left = containerRect.left
|
|
1626
|
+
} else {
|
|
1627
|
+
// position:absolute is relative to the containing block. By default the content
|
|
1628
|
+
// is appended to document.body (static), so the containing block is the initial
|
|
1629
|
+
// containing block (document origin). We must convert viewport coords to document
|
|
1630
|
+
// coords by adding scroll offsets. Using offsetParent subtraction was unreliable
|
|
1631
|
+
// because offsetParent for body children varies across browsers (body vs html)
|
|
1632
|
+
// and body margin caused misalignment.
|
|
1633
|
+
top = containerRect.top + window.scrollY + containerRect.height
|
|
1634
|
+
left = containerRect.left + window.scrollX
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
this.content.main.style.top = top + 'px'
|
|
1638
|
+
this.content.main.style.left = left + 'px'
|
|
1639
|
+
|
|
1640
|
+
// Apply content width based on contentWidth setting
|
|
1641
|
+
// "" → width matches trigger (default/current behavior)
|
|
1642
|
+
// "500px" → exact width of 500px
|
|
1643
|
+
// ">500px" → min-width of 500px, width auto (content can grow)
|
|
1644
|
+
// "<500px" → max-width of 500px, width auto (content can shrink)
|
|
1645
|
+
const cw = this.settings.contentWidth
|
|
1646
|
+
this.content.main.style.width = ''
|
|
1647
|
+
this.content.main.style.minWidth = ''
|
|
1648
|
+
this.content.main.style.maxWidth = ''
|
|
1649
|
+
|
|
1650
|
+
if (!cw) {
|
|
1651
|
+
// Default: match trigger width exactly
|
|
1582
1652
|
this.content.main.style.width = containerRect.width + 'px'
|
|
1653
|
+
} else if (cw.startsWith('>')) {
|
|
1654
|
+
// Min-width mode: at least this wide, can grow to fit content
|
|
1655
|
+
this.content.main.style.minWidth = cw.slice(1)
|
|
1656
|
+
} else if (cw.startsWith('<')) {
|
|
1657
|
+
// Max-width mode: no wider than this, content wraps if longer
|
|
1658
|
+
this.content.main.style.maxWidth = cw.slice(1)
|
|
1659
|
+
} else {
|
|
1660
|
+
// Exact width
|
|
1661
|
+
this.content.main.style.width = cw
|
|
1583
1662
|
}
|
|
1584
1663
|
}
|
|
1585
1664
|
|
|
1665
|
+
public moveContentAbove(): void {
|
|
1666
|
+
this.setContentDirection('above')
|
|
1667
|
+
this.setContentPosition()
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
public moveContentBelow(): void {
|
|
1671
|
+
this.setContentDirection('below')
|
|
1672
|
+
this.setContentPosition()
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1586
1675
|
public ensureElementInView(container: HTMLElement, element: HTMLElement): void {
|
|
1587
1676
|
// Determine container top and bottom
|
|
1588
1677
|
const cTop = container.scrollTop + container.offsetTop // Make sure to have offsetTop
|
|
@@ -1639,9 +1728,9 @@ export default class Render {
|
|
|
1639
1728
|
const hideClass = this.classes.hide
|
|
1640
1729
|
|
|
1641
1730
|
if (allowDeselect && !(isMultiple && !hasSelectedItems)) {
|
|
1642
|
-
|
|
1731
|
+
this.removeClasses(deselectButton, hideClass)
|
|
1643
1732
|
} else {
|
|
1644
|
-
|
|
1733
|
+
this.addClasses(deselectButton, hideClass)
|
|
1645
1734
|
}
|
|
1646
1735
|
}
|
|
1647
1736
|
}
|