slim-select 3.2.0 → 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.
@@ -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.classList.remove(this.classes.disabled)
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.classList.add(this.classes.disabled)
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.classList.add(dirClass)
123
- this.content.main.classList.add(dirClass)
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.classList.add(this.classes.contentOpen)
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.classList.remove(this.classes.contentOpen)
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.main.main.classList.remove(this.classes.dirAbove, this.classes.dirBelow)
163
- this.content.main.classList.remove(this.classes.dirAbove, this.classes.dirBelow)
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.classList.add(this.classes.main)
193
- this.content.main.classList.add(this.classes.content)
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('.' + this.classes.highlighted) as HTMLDivElement
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
- values.classList.add(this.classes.values)
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
- deselect.classList.add(this.classes.deselect)
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
- deselect.classList.add(this.classes.hide)
347
+ this.addClasses(deselect, this.classes.hide)
311
348
  } else {
312
- deselect.classList.remove(this.classes.hide)
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
- arrow.classList.add(this.classes.arrow)
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
- arrow.classList.add(this.classes.hide)
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
- placeholder.classList.add(this.classes.placeholder)
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
- singleValue.classList.add(this.classes.single)
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.classList.add(this.classes.hide)
504
+ this.addClasses(this.main.deselect.main, this.classes.hide)
468
505
  } else {
469
- this.main.deselect.main.classList.remove(this.classes.hide)
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
- singleValue.classList.add(this.classes.max)
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
- n.classList.add(this.classes.valueOut)
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
- value.classList.add(this.classes.value)
612
+ this.addClasses(value, this.classes.value)
576
613
  value.dataset.id = option.id
577
614
 
578
615
  const text = document.createElement('div')
579
- text.classList.add(this.classes.valueText)
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
- deleteDiv.classList.add(this.classes.valueDelete)
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
- main.classList.add(this.classes.search)
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
- main.classList.add(this.classes.hide)
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('.' + this.classes.highlighted) as HTMLDivElement
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('.' + this.classes.highlighted) as HTMLDivElement
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
- addable.classList.add(this.classes.addable)
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].classList.add(this.classes.highlighted)
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
- o.classList.add(this.classes.highlighted)
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
- prevOption.classList.remove(this.classes.highlighted)
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
- selectOption.classList.add(this.classes.highlighted)
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('.' + this.classes.optgroupLabel) as HTMLDivElement
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
- firstHighlight.classList.add(this.classes.highlighted)
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
- options.classList.add(this.classes.list)
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
- errorDiv.classList.add(this.classes.error)
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
- searchingDiv.classList.add(this.classes.searching)
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
- noResults.classList.add(this.classes.search)
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
- optgroupEl.classList.add(this.classes.optgroup)
1133
+ this.addClasses(optgroupEl, this.classes.optgroup)
1091
1134
 
1092
1135
  // Create label
1093
1136
  const optgroupLabel = document.createElement('div')
1094
- optgroupLabel.classList.add(this.classes.optgroupLabel)
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
- optgroupLabelText.classList.add(this.classes.optgroupLabelText)
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
- optgroupActions.classList.add(this.classes.optgroupActions)
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
- selectAll.classList.add(this.classes.optgroupSelectAll)
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
- selectAll.classList.add(this.classes.selected)
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
- optgroupClosable.classList.add(this.classes.optgroupClosable)
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
- optgroupClosableSvg.classList.add(this.classes.arrow)
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
- optgroupClosable.classList.add(this.classes.mainOpen)
1255
+ this.addClasses(optgroupClosable, this.classes.mainOpen)
1213
1256
  optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
1214
1257
  } else if (d.closable === 'open') {
1215
- optgroupEl.classList.add(this.classes.mainOpen)
1258
+ this.addClasses(optgroupEl, this.classes.mainOpen)
1216
1259
  optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
1217
1260
  } else if (d.closable === 'close') {
1218
- optgroupEl.classList.add(this.classes.close)
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
- optgroupEl.classList.remove(this.classes.close)
1230
- optgroupEl.classList.add(this.classes.mainOpen)
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
- optgroupEl.classList.remove(this.classes.mainOpen)
1234
- optgroupEl.classList.add(this.classes.close)
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
- placeholder.classList.add(this.classes.option)
1269
- placeholder.classList.add(this.classes.hide)
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
- optionEl.classList.add(this.classes.option)
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
- optionEl.classList.add(this.classes.hide)
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
- optionEl.classList.add(this.classes.disabled)
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
- optionEl.classList.add(this.classes.hide)
1361
+ this.addClasses(optionEl, this.classes.hide)
1319
1362
  }
1320
1363
 
1321
1364
  // If option is selected
1322
1365
  if (option.selected) {
1323
- optionEl.classList.add(this.classes.selected)
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
- optionEl.classList.remove(this.classes.selected)
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,48 +1588,62 @@ export default class Render {
1539
1588
  return tempDiv.innerHTML
1540
1589
  }
1541
1590
 
1542
- public moveContentAbove(): void {
1543
- // Get main and content height
1544
- const mainHeight = this.main.main.offsetHeight
1545
- const contentHeight = this.content.main.offsetHeight
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.main.main.classList.remove(this.classes.dirBelow)
1549
- this.main.main.classList.add(this.classes.dirAbove)
1550
- this.content.main.classList.remove(this.classes.dirBelow)
1551
- this.content.main.classList.add(this.classes.dirAbove)
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
+ }
1552
1616
 
1553
- // Set the content position
1554
1617
  const containerRect = this.main.main.getBoundingClientRect()
1555
- this.content.main.style.margin = '-' + (mainHeight + contentHeight - 1) + 'px 0px 0px 0px'
1556
- this.content.main.style.top =
1557
- containerRect.top + containerRect.height + (this.settings.contentPosition === 'fixed' ? 0 : window.scrollY) + 'px'
1558
- this.content.main.style.left =
1559
- containerRect.left + (this.settings.contentPosition === 'fixed' ? 0 : window.scrollX) + 'px'
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'
1560
1636
  this.content.main.style.width = containerRect.width + 'px'
1561
1637
  }
1562
1638
 
1563
- public moveContentBelow(): void {
1564
- // Set direction classes on both main and content
1565
- this.main.main.classList.remove(this.classes.dirAbove)
1566
- this.main.main.classList.add(this.classes.dirBelow)
1567
- this.content.main.classList.remove(this.classes.dirAbove)
1568
- this.content.main.classList.add(this.classes.dirBelow)
1639
+ public moveContentAbove(): void {
1640
+ this.setContentDirection('above')
1641
+ this.setContentPosition()
1642
+ }
1569
1643
 
1570
- // Set the content position
1571
- const containerRect = this.main.main.getBoundingClientRect()
1572
- this.content.main.style.margin = '-1px 0px 0px 0px'
1573
- // Dont do anything if the content is relative
1574
- if (this.settings.contentPosition !== 'relative') {
1575
- this.content.main.style.top =
1576
- containerRect.top +
1577
- containerRect.height +
1578
- (this.settings.contentPosition === 'fixed' ? 0 : window.scrollY) +
1579
- 'px'
1580
- this.content.main.style.left =
1581
- containerRect.left + (this.settings.contentPosition === 'fixed' ? 0 : window.scrollX) + 'px'
1582
- this.content.main.style.width = containerRect.width + 'px'
1583
- }
1644
+ public moveContentBelow(): void {
1645
+ this.setContentDirection('below')
1646
+ this.setContentPosition()
1584
1647
  }
1585
1648
 
1586
1649
  public ensureElementInView(container: HTMLElement, element: HTMLElement): void {
@@ -1639,9 +1702,9 @@ export default class Render {
1639
1702
  const hideClass = this.classes.hide
1640
1703
 
1641
1704
  if (allowDeselect && !(isMultiple && !hasSelectedItems)) {
1642
- deselectButton.classList.remove(hideClass)
1705
+ this.removeClasses(deselectButton, hideClass)
1643
1706
  } else {
1644
- deselectButton.classList.add(hideClass)
1707
+ this.addClasses(deselectButton, hideClass)
1645
1708
  }
1646
1709
  }
1647
1710
  }