slim-select 1.27.0 → 2.0.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.
Files changed (155) hide show
  1. package/.nvmrc +1 -0
  2. package/.prettierrc +6 -0
  3. package/.vscode/extensions.json +3 -0
  4. package/.vscode/settings.json +15 -5
  5. package/README.md +2 -1
  6. package/dist/helper.d.ts +1 -4
  7. package/dist/index.d.ts +38 -58
  8. package/dist/render.d.ts +109 -0
  9. package/dist/select.d.ts +31 -20
  10. package/dist/settings.d.ts +33 -0
  11. package/dist/slimselect.cjs.js +2 -0
  12. package/dist/slimselect.cjs.js.map +1 -0
  13. package/dist/slimselect.css +1 -2
  14. package/dist/slimselect.css.map +1 -0
  15. package/dist/slimselect.es.js +2 -0
  16. package/dist/slimselect.es.js.map +1 -0
  17. package/dist/slimselect.global.js +2 -0
  18. package/dist/slimselect.global.js.map +1 -0
  19. package/dist/slimselect.js +2 -1956
  20. package/dist/slimselect.js.map +1 -0
  21. package/dist/slimselect.umd.js +2 -0
  22. package/dist/slimselect.umd.js.map +1 -0
  23. package/dist/store.d.ts +71 -0
  24. package/docs/assets/check-mark.svg +6 -0
  25. package/docs/assets/data.js +59 -0
  26. package/docs/assets/home.css +1 -0
  27. package/docs/assets/home.js +1 -0
  28. package/docs/assets/index.css +1 -0
  29. package/docs/assets/index.js +16 -0
  30. package/docs/assets/index2.js +274 -0
  31. package/docs/assets/index3.js +126 -0
  32. package/docs/assets/index4.js +60 -0
  33. package/docs/assets/install.js +34 -0
  34. package/docs/assets/selects.js +55 -0
  35. package/docs/index.html +18 -5
  36. package/docs.go +3 -1
  37. package/index.html +16 -0
  38. package/jest.config.js +7 -0
  39. package/package.json +39 -39
  40. package/src/docs/app.vue +185 -76
  41. package/src/docs/assets/scss/_variables.scss +26 -10
  42. package/src/docs/assets/scss/alert.scss +12 -0
  43. package/src/docs/assets/scss/animations.scss +5 -3
  44. package/src/docs/assets/scss/button.scss +21 -0
  45. package/src/docs/assets/scss/index.scss +8 -6
  46. package/src/docs/assets/scss/input.scss +2 -2
  47. package/src/docs/assets/scss/layout.scss +195 -65
  48. package/src/docs/assets/scss/misc.scss +144 -34
  49. package/src/docs/assets/scss/prism.scss +155 -153
  50. package/src/docs/env.d.ts +12 -0
  51. package/src/docs/images/check-mark.svg +6 -0
  52. package/src/docs/images/chrome.svg +22 -0
  53. package/src/docs/images/firefox.svg +58 -0
  54. package/src/docs/images/github.png +0 -0
  55. package/src/docs/images/ie.svg +11 -0
  56. package/src/docs/images/npm.png +0 -0
  57. package/src/docs/images/opera.svg +15 -0
  58. package/src/docs/images/safari.svg +22 -0
  59. package/src/docs/main.ts +27 -0
  60. package/src/docs/pages/data.vue +130 -0
  61. package/src/docs/pages/events/addable.vue +79 -0
  62. package/src/docs/pages/events/before_after_change.vue +140 -0
  63. package/src/docs/pages/events/before_after_open_close.vue +122 -0
  64. package/src/docs/pages/events/error.vue +59 -0
  65. package/src/docs/pages/events/index.vue +33 -0
  66. package/src/docs/pages/events/search.vue +154 -0
  67. package/src/docs/pages/events/search_filter.vue +45 -0
  68. package/src/docs/pages/home.vue +96 -116
  69. package/src/docs/pages/install.vue +47 -74
  70. package/src/docs/pages/methods/destroy.vue +66 -0
  71. package/src/docs/pages/methods/enable_disable.vue +70 -0
  72. package/src/docs/pages/methods/get_data.vue +81 -0
  73. package/src/docs/pages/methods/get_selected.vue +78 -0
  74. package/src/docs/pages/methods/index.vue +36 -0
  75. package/src/docs/pages/methods/open_close.vue +63 -0
  76. package/src/docs/pages/methods/search.vue +62 -0
  77. package/src/docs/pages/methods/set_data.vue +106 -0
  78. package/src/docs/pages/methods/set_selected.vue +70 -0
  79. package/src/docs/pages/selects.vue +108 -114
  80. package/src/docs/pages/settings/always_open.vue +88 -0
  81. package/src/docs/pages/settings/close_on_select.vue +69 -0
  82. package/src/docs/pages/settings/content_location.vue +64 -0
  83. package/src/docs/pages/settings/content_position.vue +86 -0
  84. package/src/docs/pages/settings/css.vue +73 -0
  85. package/src/docs/pages/settings/data_attributes.vue +39 -0
  86. package/src/docs/pages/settings/deselect.vue +69 -0
  87. package/src/docs/pages/settings/display.vue +88 -0
  88. package/src/docs/pages/settings/hide_selected.vue +73 -0
  89. package/src/docs/pages/settings/html.vue +86 -0
  90. package/src/docs/pages/settings/index.vue +73 -0
  91. package/src/docs/pages/settings/mandatory.vue +91 -0
  92. package/src/docs/pages/settings/min_max.vue +61 -0
  93. package/src/docs/pages/settings/open_position.vue +60 -0
  94. package/src/docs/pages/settings/placeholder.vue +72 -0
  95. package/src/docs/pages/settings/search.vue +140 -0
  96. package/src/docs/pages/settings/select.vue +32 -0
  97. package/src/docs/pages/settings/select_by_group.vue +46 -0
  98. package/src/docs/pages/settings/show_tooltip.vue +44 -0
  99. package/src/docs/pages/settings/styles.vue +51 -0
  100. package/src/docs/router.ts +41 -13
  101. package/src/slim-select/helper.ts +25 -86
  102. package/src/slim-select/index.ts +318 -413
  103. package/src/slim-select/render.test.ts +39 -0
  104. package/src/slim-select/render.ts +1129 -0
  105. package/src/slim-select/rollup.config.mjs +39 -0
  106. package/src/slim-select/select.test.ts +155 -0
  107. package/src/slim-select/select.ts +280 -107
  108. package/src/slim-select/settings.ts +68 -0
  109. package/src/slim-select/slimselect.scss +263 -278
  110. package/src/slim-select/store.test.ts +220 -0
  111. package/src/slim-select/store.ts +320 -0
  112. package/src/slim-select/tsconfig.json +11 -5
  113. package/src/vue/slimselect.vue +61 -0
  114. package/tsconfig.json +10 -29
  115. package/vite.config.ts +18 -0
  116. package/.browserslistrc +0 -4
  117. package/.postcssrc.js +0 -5
  118. package/.travis.yml +0 -22
  119. package/babel.config.js +0 -5
  120. package/dist/config.d.ts +0 -83
  121. package/dist/data.d.ts +0 -50
  122. package/dist/slim.d.ts +0 -50
  123. package/dist/slimselect.min.css +0 -1
  124. package/dist/slimselect.min.js +0 -1
  125. package/dist/slimselect.min.mjs +0 -1
  126. package/docs/css/app.d5840d21.css +0 -1
  127. package/docs/css/chunk-vendors.4db6930d.css +0 -1
  128. package/docs/css/home.57020178.css +0 -1
  129. package/docs/css/install.baa5d3fb.css +0 -1
  130. package/docs/css/methods.728c742a.css +0 -1
  131. package/docs/css/options.b28a32ef.css +0 -1
  132. package/docs/css/selects.1a7e1e6a.css +0 -1
  133. package/docs/js/app.db13c724.js +0 -2
  134. package/docs/js/app.db13c724.js.map +0 -1
  135. package/docs/js/chunk-vendors.cc40aa28.js +0 -28
  136. package/docs/js/chunk-vendors.cc40aa28.js.map +0 -1
  137. package/docs/js/home.8bcb44fd.js +0 -2
  138. package/docs/js/home.8bcb44fd.js.map +0 -1
  139. package/docs/js/install.02f31028.js +0 -2
  140. package/docs/js/install.02f31028.js.map +0 -1
  141. package/docs/js/methods.1b31e988.js +0 -11
  142. package/docs/js/methods.1b31e988.js.map +0 -1
  143. package/docs/js/options.39fe1005.js +0 -2
  144. package/docs/js/options.39fe1005.js.map +0 -1
  145. package/docs/js/selects.f4d0b859.js +0 -2
  146. package/docs/js/selects.f4d0b859.js.map +0 -1
  147. package/src/docs/pages/methods.vue +0 -427
  148. package/src/docs/pages/options.vue +0 -1480
  149. package/src/main.ts +0 -29
  150. package/src/shims-vue.d.ts +0 -8
  151. package/src/slim-select/config.ts +0 -118
  152. package/src/slim-select/data.ts +0 -377
  153. package/src/slim-select/slim.ts +0 -770
  154. package/tslint.json +0 -26
  155. package/vue.config.js +0 -23
@@ -0,0 +1,1129 @@
1
+ import { debounce } from './helper'
2
+ import Settings from './settings'
3
+ import Store, { DataArray, Optgroup, Option, OptionOptional } from './store'
4
+
5
+ export interface Callbacks {
6
+ open: () => void
7
+ close: () => void
8
+ addable?: (value: string) => OptionOptional | string
9
+ setSelected: (value: string[]) => void
10
+ addOption: (option: Option) => void
11
+ search: (search: string) => void
12
+ beforeChange?: (newVal: Option[], oldVal: Option[]) => boolean | void
13
+ afterChange?: (newVal: Option[]) => void
14
+ }
15
+
16
+ export interface Main {
17
+ main: HTMLDivElement
18
+ values: HTMLDivElement
19
+ deselect: {
20
+ main: HTMLDivElement
21
+ svg: SVGSVGElement
22
+ path: SVGPathElement
23
+ }
24
+ arrow: {
25
+ main: SVGSVGElement
26
+ path: SVGPathElement
27
+ }
28
+ }
29
+
30
+ export interface Content {
31
+ main: HTMLDivElement
32
+ search: Search
33
+ list: HTMLDivElement
34
+ }
35
+
36
+ export interface Search {
37
+ main: HTMLDivElement
38
+ input: HTMLInputElement
39
+ addable?: {
40
+ main: HTMLDivElement
41
+ svg: SVGSVGElement
42
+ path: SVGPathElement
43
+ }
44
+ }
45
+
46
+ export default class Render {
47
+ public settings: Settings
48
+ public store: Store
49
+ public callbacks: Callbacks
50
+
51
+ // Elements
52
+ public main: Main
53
+ public content: Content
54
+
55
+ // Classes
56
+ public classes = {
57
+ // Main
58
+ main: 'ss-main',
59
+
60
+ // Placeholder
61
+ placeholder: 'ss-placeholder',
62
+
63
+ // Values
64
+ values: 'ss-values',
65
+ single: 'ss-single',
66
+ value: 'ss-value',
67
+ valueText: 'ss-value-text',
68
+ valueDelete: 'ss-value-delete',
69
+ valueOut: 'ss-value-out',
70
+
71
+ // Deselect
72
+ deselect: 'ss-deselect',
73
+ deselectPath: 'M10,10 L90,90 M10,90 L90,10', // Not a class but whatever
74
+
75
+ // Arrow
76
+ arrow: 'ss-arrow',
77
+ arrowClose: 'M10,30 L50,70 L90,30', // Not a class but whatever
78
+ arrowOpen: 'M10,70 L50,30 L90,70', // Not a class but whatever
79
+
80
+ // Content
81
+ content: 'ss-content',
82
+ openAbove: 'ss-open-above',
83
+ openBelow: 'ss-open-below',
84
+
85
+ // Search
86
+ search: 'ss-search',
87
+ searchHighlighter: 'ss-search-highlight',
88
+ searching: 'ss-searching',
89
+ addable: 'ss-addable',
90
+ addablePath: 'M50,10 L50,90 M10,50 L90,50', // Not a class but whatever
91
+
92
+ // List options
93
+ list: 'ss-list',
94
+ optgroup: 'ss-optgroup',
95
+ optgroupLabel: 'ss-optgroup-label',
96
+ optgroupSelectable: 'ss-optgroup-selectable',
97
+ option: 'ss-option',
98
+ optionSelected: 'ss-option-selected',
99
+ optionDelete: 'M10,10 L90,90 M10,90 L90,10', // Not a class but whatever
100
+ highlighted: 'ss-highlighted',
101
+
102
+ // Misc
103
+ error: 'ss-error',
104
+ disabled: 'ss-disabled',
105
+ hide: 'ss-hide',
106
+ }
107
+
108
+ constructor(settings: Required<Settings>, store: Store, callbacks: Callbacks) {
109
+ this.store = store
110
+ this.settings = settings
111
+ this.callbacks = callbacks
112
+
113
+ this.main = this.mainDiv()
114
+ this.content = this.contentDiv()
115
+
116
+ // Render the values
117
+ this.renderValues()
118
+
119
+ // Render the options
120
+ this.renderOptions(this.store.getData())
121
+
122
+ // Add content to the content location settings
123
+ this.settings.contentLocation.appendChild(this.content.main)
124
+ }
125
+
126
+ // Remove disabled classes
127
+ public enable(): void {
128
+ // Remove disabled class
129
+ this.main.main.classList.remove(this.classes.disabled)
130
+
131
+ // Set search input to "enabled"
132
+ this.content.search.input.disabled = false
133
+ }
134
+
135
+ // Set disabled classes
136
+ public disable(): void {
137
+ // Add disabled class
138
+ this.main.main.classList.add(this.classes.disabled)
139
+
140
+ // Set search input to disabled
141
+ this.content.search.input.disabled = true
142
+ }
143
+
144
+ public open(): void {
145
+ this.main.arrow.path.setAttribute('d', this.classes.arrowOpen)
146
+
147
+ // Add class to main container
148
+ this.main.main.classList.add(this.settings.openPosition === 'up' ? this.classes.openAbove : this.classes.openBelow)
149
+
150
+ // move the content in to the right location
151
+ this.moveContent()
152
+
153
+ // Render the options
154
+ this.renderOptions(this.store.getData())
155
+
156
+ // Check showContent to see if they want to specifically show in a certain direction
157
+ if (this.settings.contentPosition === 'relative') {
158
+ this.moveContentBelow()
159
+ } else if (this.settings.openPosition.toLowerCase() === 'up') {
160
+ this.moveContentAbove()
161
+ } else if (this.settings.openPosition.toLowerCase() === 'down') {
162
+ this.moveContentBelow()
163
+ } else {
164
+ // Auto identify where to put it
165
+ if (this.putContent(this.content.main, this.settings.isOpen) === 'up') {
166
+ this.moveContentAbove()
167
+ } else {
168
+ this.moveContentBelow()
169
+ }
170
+ }
171
+
172
+ // Move to last selected option
173
+ const selectedOptions = this.store.getSelectedOptions()
174
+ if (selectedOptions.length) {
175
+ const selectedId = selectedOptions[selectedOptions.length - 1].id
176
+ const selectedOption = this.content.list.querySelector('[data-id="' + selectedId + '"]') as HTMLElement
177
+ if (selectedOption) {
178
+ this.ensureElementInView(this.content.list, selectedOption)
179
+ }
180
+ }
181
+ }
182
+
183
+ public close(): void {
184
+ this.main.main.classList.remove(this.classes.openAbove)
185
+ this.main.main.classList.remove(this.classes.openBelow)
186
+ this.content.main.classList.remove(this.classes.openAbove)
187
+ this.content.main.classList.remove(this.classes.openBelow)
188
+ this.main.arrow.path.setAttribute('d', this.classes.arrowClose)
189
+ }
190
+
191
+ public mainDiv(): Main {
192
+ // Create main container
193
+ const main = document.createElement('div')
194
+
195
+ // Set tabable to allow tabbing to the element
196
+ main.tabIndex = 0
197
+
198
+ // Add style and classes
199
+ main.style.cssText = this.settings.style !== '' ? this.settings.style : ''
200
+
201
+ // Clear out classlist
202
+ main.className = ''
203
+
204
+ // Loop through config class and add
205
+ main.classList.add(this.settings.id)
206
+ main.classList.add(this.classes.main)
207
+ if (this.settings.class) {
208
+ for (const c of this.settings.class) {
209
+ if (c.trim() !== '') {
210
+ main.classList.add(c.trim())
211
+ }
212
+ }
213
+ }
214
+
215
+ // If main gets focus, open the content
216
+ main.onfocus = () => {
217
+ if (this.settings.triggerFocus) {
218
+ this.callbacks.open()
219
+ }
220
+ }
221
+
222
+ // Deal with keyboard events on the main div
223
+ // This is to allow for normal selecting
224
+ // when you may not have a search bar
225
+ main.onkeydown = (e: KeyboardEvent) => {
226
+ // Convert above if else statemets to switch
227
+ switch (e.key) {
228
+ case 'ArrowUp':
229
+ case 'ArrowDown':
230
+ this.callbacks.open()
231
+ e.key === 'ArrowDown' ? this.highlight('down') : this.highlight('up')
232
+ return false
233
+ case 'Tab':
234
+ this.callbacks.close()
235
+ return true // Continue doing normal tabbing
236
+ case 'Enter':
237
+ const highlighted = this.content.list.querySelector('.' + this.classes.highlighted) as HTMLDivElement
238
+ if (highlighted) {
239
+ highlighted.click()
240
+ }
241
+ return false
242
+ case 'Escape':
243
+ this.callbacks.close()
244
+ return false
245
+ }
246
+ }
247
+
248
+ // Add onclick for main div
249
+ main.onclick = (e: Event) => {
250
+ // Dont do anything if disabled
251
+ if (!this.settings.isEnabled) {
252
+ return
253
+ }
254
+
255
+ this.settings.isOpen ? this.callbacks.close() : this.callbacks.open()
256
+ }
257
+
258
+ // Add values
259
+ const values = document.createElement('div')
260
+ values.classList.add(this.classes.values)
261
+ main.appendChild(values)
262
+
263
+ // Add deselect
264
+ const deselect = document.createElement('div')
265
+ deselect.classList.add(this.classes.deselect)
266
+ if (!this.settings.allowDeselect || this.settings.isMultiple) {
267
+ deselect.classList.add(this.classes.hide)
268
+ }
269
+ deselect.onclick = (e: Event) => {
270
+ e.stopPropagation()
271
+
272
+ // Dont do anything if disabled
273
+ if (!this.settings.isEnabled) {
274
+ return
275
+ }
276
+
277
+ this.callbacks.setSelected([''])
278
+ }
279
+
280
+ // Add deselect svg
281
+ const deselectSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
282
+ deselectSvg.setAttribute('viewBox', '0 0 100 100')
283
+ const deselectPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
284
+ deselectPath.setAttribute('d', this.classes.deselectPath)
285
+ deselectSvg.appendChild(deselectPath)
286
+ deselect.appendChild(deselectSvg)
287
+ main.appendChild(deselect)
288
+
289
+ // Add arrow
290
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
291
+ arrow.classList.add(this.classes.arrow)
292
+ arrow.setAttribute('viewBox', '0 0 100 100')
293
+ const arrowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
294
+ arrowPath.setAttribute('d', this.classes.arrowClose)
295
+ if (this.settings.alwaysOpen) {
296
+ arrow.classList.add(this.classes.hide)
297
+ }
298
+ arrow.appendChild(arrowPath)
299
+ main.appendChild(arrow)
300
+
301
+ return {
302
+ main: main,
303
+ values: values,
304
+ deselect: {
305
+ main: deselect,
306
+ svg: deselectSvg,
307
+ path: deselectPath,
308
+ },
309
+ arrow: {
310
+ main: arrow,
311
+ path: arrowPath,
312
+ },
313
+ }
314
+ }
315
+
316
+ public mainFocus(trigger: boolean): void {
317
+ if (!trigger) {
318
+ this.settings.triggerFocus = false
319
+ }
320
+ // Trigger focus but dont scroll to it
321
+ this.main.main.focus({ preventScroll: true })
322
+ this.settings.triggerFocus = true
323
+ }
324
+
325
+ public placeholder(): HTMLDivElement {
326
+ // Figure out if there is a placeholder option
327
+ const placeholderOption = this.store.filter((o) => o.placeholder, false) as Option[]
328
+
329
+ // If there is a placeholder option use that
330
+ // If placeholder has an html value, use that
331
+ // If placeholder has a text, use that
332
+ // If nothing is set, use the placeholder text
333
+ let placeholderText = this.settings.placeholderText
334
+ if (placeholderOption.length) {
335
+ if (placeholderOption[0].html !== '') {
336
+ placeholderText = placeholderOption[0].html
337
+ } else if (placeholderOption[0].text !== '') {
338
+ placeholderText = placeholderOption[0].text
339
+ }
340
+ }
341
+
342
+ // Create placeholder div
343
+ const placeholder = document.createElement('div')
344
+ placeholder.classList.add(this.classes.placeholder)
345
+ placeholder.innerHTML = placeholderText
346
+ return placeholder
347
+ }
348
+
349
+ // Get selected values and append to multiSelected values container
350
+ // and remove those who shouldnt exist
351
+ public renderValues(): void {
352
+ // If single select set placeholder or selected value
353
+ if (!this.settings.isMultiple) {
354
+ this.renderSingleValue()
355
+ return
356
+ }
357
+
358
+ this.renderMultipleValues()
359
+ }
360
+
361
+ private renderSingleValue(): void {
362
+ const selected = this.store.filter((o: Option): boolean => {
363
+ return o.selected && !o.placeholder
364
+ }, false) as Option[]
365
+ const selectedSingle = selected.length > 0 ? selected[0] : null
366
+
367
+ // If nothing is seleected use settings placeholder text
368
+ if (!selectedSingle) {
369
+ this.main.values.innerHTML = this.placeholder().outerHTML
370
+ } else {
371
+ // Create single value container
372
+ const singleValue = document.createElement('div')
373
+ singleValue.classList.add(this.classes.single)
374
+ singleValue.innerHTML = selectedSingle.html ? selectedSingle.html : selectedSingle.text
375
+
376
+ // If there is a selected value, set a single div
377
+ this.main.values.innerHTML = singleValue.outerHTML
378
+ }
379
+
380
+ // If allowDeselect is false or selected value is empty just hide deslect
381
+ if (!this.settings.allowDeselect || !selected.length) {
382
+ this.main.deselect.main.classList.add(this.classes.hide)
383
+ } else {
384
+ this.main.deselect.main.classList.remove(this.classes.hide)
385
+ }
386
+ }
387
+
388
+ private renderMultipleValues(): void {
389
+ // Get various peices of data
390
+ let currentNodes = this.main.values.childNodes as NodeListOf<HTMLDivElement>
391
+ let selectedOptions = this.store.filter((opt: Option) => {
392
+ // Only grab options that are selected and display is true
393
+ return opt.selected && opt.display
394
+ }, false) as Option[]
395
+
396
+ // If selectedOptions is empty set placeholder
397
+ if (selectedOptions.length === 0) {
398
+ this.main.values.innerHTML = this.placeholder().outerHTML
399
+ return
400
+ } else {
401
+ // If there is a placeholder, remove it
402
+ const placeholder = this.main.values.querySelector('.' + this.classes.placeholder)
403
+ if (placeholder) {
404
+ placeholder.remove()
405
+ }
406
+ }
407
+
408
+ // Loop through currentNodes and only include ones that are not in selectedIDs
409
+ let removeNodes: HTMLDivElement[] = []
410
+ for (let i = 0; i < currentNodes.length; i++) {
411
+ const node = currentNodes[i]
412
+ const id = node.getAttribute('data-id')
413
+ if (id) {
414
+ // Check if id is in selectedOptions
415
+ const found = selectedOptions.filter((opt: Option) => {
416
+ return opt.id === id
417
+ }, false)
418
+
419
+ // If not found, add to removeNodes
420
+ if (!found.length) {
421
+ removeNodes.push(node)
422
+ }
423
+ }
424
+ }
425
+
426
+ // Loop through and remove
427
+ for (const n of removeNodes) {
428
+ n.classList.add(this.classes.valueOut)
429
+ setTimeout(() => {
430
+ this.main.values.removeChild(n)
431
+ }, 100)
432
+ }
433
+
434
+ // Add values that dont currently exist
435
+ currentNodes = this.main.values.childNodes as NodeListOf<HTMLDivElement>
436
+ for (let d = 0; d < selectedOptions.length; d++) {
437
+ let shouldAdd = true
438
+ for (let i = 0; i < currentNodes.length; i++) {
439
+ if (selectedOptions[d].id === String(currentNodes[i].dataset.id)) {
440
+ shouldAdd = false
441
+ }
442
+ }
443
+
444
+ // If shouldAdd, insertAdjacentElement it to the values container in the order of the selectedOptions
445
+ if (shouldAdd) {
446
+ if (currentNodes.length === 0) {
447
+ this.main.values.appendChild(this.multipleValue(selectedOptions[d]))
448
+ } else if (d === 0) {
449
+ this.main.values.insertBefore(this.multipleValue(selectedOptions[d]), currentNodes[d])
450
+ } else {
451
+ currentNodes[d - 1].insertAdjacentElement('afterend', this.multipleValue(selectedOptions[d]))
452
+ }
453
+ }
454
+ }
455
+ }
456
+
457
+ public multipleValue(option: Option): HTMLDivElement {
458
+ const value = document.createElement('div')
459
+ value.classList.add(this.classes.value)
460
+ value.dataset.id = option.id
461
+
462
+ const text = document.createElement('div')
463
+ text.classList.add(this.classes.valueText)
464
+ text.innerHTML = option.text // For multiple values always use text
465
+ value.appendChild(text)
466
+
467
+ // Only add deletion if the option is not mandatory
468
+ if (!option.mandatory) {
469
+ const deleteDiv = document.createElement('div')
470
+ deleteDiv.classList.add(this.classes.valueDelete)
471
+ deleteDiv.onclick = (e: Event) => {
472
+ e.preventDefault()
473
+ e.stopPropagation()
474
+
475
+ // By Default we will delete
476
+ let shouldDelete = true
477
+ const before = this.store.getSelectedOptions()
478
+ const after = before.filter((o) => {
479
+ return o.selected && o.id !== option.id
480
+ }, true)
481
+
482
+ // Check if minSelected is set and if after length so, return
483
+ if (this.settings.minSelected && after.length < this.settings.minSelected) {
484
+ return
485
+ }
486
+
487
+ // If there is a beforeDeselect function run it
488
+ if (this.callbacks.beforeChange) {
489
+ shouldDelete = this.callbacks.beforeChange(after, before) === true
490
+ }
491
+
492
+ if (shouldDelete) {
493
+ // Loop through after and append values to a variable called selected
494
+ let selectedValues: string[] = []
495
+ for (const o of after) {
496
+ if (o instanceof Optgroup) {
497
+ for (const c of o.options) {
498
+ selectedValues.push(c.value)
499
+ }
500
+ }
501
+
502
+ if (o instanceof Option) {
503
+ selectedValues.push(o.value)
504
+ }
505
+ }
506
+ this.callbacks.setSelected(selectedValues)
507
+
508
+ // Check if we need to close the dropdown
509
+ if (this.settings.closeOnSelect) {
510
+ this.callbacks.close()
511
+ }
512
+
513
+ // Run afterChange callback
514
+ if (this.callbacks.afterChange) {
515
+ this.callbacks.afterChange(after)
516
+ }
517
+ }
518
+ }
519
+
520
+ // Add delete svg
521
+ const deleteSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
522
+ deleteSvg.setAttribute('viewBox', '0 0 100 100')
523
+ const deletePath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
524
+ deletePath.setAttribute('d', this.classes.optionDelete)
525
+ deleteSvg.appendChild(deletePath)
526
+ deleteDiv.appendChild(deleteSvg)
527
+
528
+ value.appendChild(deleteDiv)
529
+ }
530
+
531
+ return value
532
+ }
533
+
534
+ public contentDiv(): Content {
535
+ const main = document.createElement('div')
536
+ main.classList.add(this.classes.content)
537
+
538
+ // Add id to data-id
539
+ main.dataset.id = this.settings.id
540
+
541
+ // Add styles
542
+ if (this.settings.style !== '') {
543
+ main.style.cssText = this.settings.style
544
+ }
545
+
546
+ // Add classes
547
+ if (this.settings.contentPosition === 'relative') {
548
+ main.classList.add('ss-' + this.settings.contentPosition)
549
+ }
550
+ if (this.settings.class.length) {
551
+ for (const c of this.settings.class) {
552
+ if (c.trim() !== '') {
553
+ main.classList.add(c.trim())
554
+ }
555
+ }
556
+ }
557
+
558
+ // Add search
559
+ const search = this.searchDiv()
560
+ main.appendChild(search.main)
561
+
562
+ // Add list
563
+ const list = this.listDiv()
564
+ main.appendChild(list)
565
+
566
+ return {
567
+ main: main,
568
+ search: search,
569
+ list: list,
570
+ }
571
+ }
572
+
573
+ public moveContent(): void {
574
+ if (this.settings.contentPosition === 'relative') {
575
+ return
576
+ }
577
+
578
+ const containerRect = this.main.main.getBoundingClientRect()
579
+ this.content.main.style.top = containerRect.top + containerRect.height + window.scrollY + 'px'
580
+ this.content.main.style.left = containerRect.left + window.scrollX + 'px'
581
+ this.content.main.style.width = containerRect.width + 'px'
582
+ }
583
+
584
+ public searchDiv(): Search {
585
+ const main = document.createElement('div')
586
+ const input = document.createElement('input')
587
+ const addable = document.createElement('div')
588
+ main.classList.add(this.classes.search)
589
+
590
+ // Setup search return object
591
+ const searchReturn: Search = {
592
+ main,
593
+ input,
594
+ }
595
+
596
+ // We still want the search to be tabable but not shown
597
+ if (!this.settings.showSearch) {
598
+ main.classList.add(this.classes.hide)
599
+ input.readOnly = true
600
+ }
601
+
602
+ input.type = 'search'
603
+ input.placeholder = this.settings.searchPlaceholder
604
+ input.tabIndex = -1
605
+ input.setAttribute('aria-label', this.settings.searchPlaceholder)
606
+ input.setAttribute('autocapitalize', 'off')
607
+ input.setAttribute('autocomplete', 'off')
608
+ input.setAttribute('autocorrect', 'off')
609
+
610
+ input.oninput = debounce((e: Event) => {
611
+ this.callbacks.search((e.target as HTMLInputElement).value)
612
+ }, 100)
613
+
614
+ // Deal with keyboard events on search input field
615
+ input.onkeydown = (e: KeyboardEvent) => {
616
+ // Convert above if else statemets to switch
617
+ switch (e.key) {
618
+ case 'ArrowUp':
619
+ case 'ArrowDown':
620
+ this.callbacks.open()
621
+ e.key === 'ArrowDown' ? this.highlight('down') : this.highlight('up')
622
+ return false
623
+ case 'Tab':
624
+ // When tabbing close the dropdown
625
+ // which will also focus on main div
626
+ // and then continuing normal tabbing
627
+ this.callbacks.close()
628
+
629
+ return true // Continue doing normal tabbing
630
+ case 'Escape':
631
+ this.callbacks.close()
632
+ return false
633
+ case 'Enter':
634
+ if (this.callbacks.addable && e.ctrlKey) {
635
+ addable.click()
636
+ } else {
637
+ const highlighted = this.content.list.querySelector('.' + this.classes.highlighted) as HTMLDivElement
638
+ if (highlighted) {
639
+ highlighted.click()
640
+ }
641
+ }
642
+ return false
643
+ }
644
+ }
645
+
646
+ // If focus is on the search input, open the dropdown
647
+ input.onfocus = () => {
648
+ // If we are already open, do nothing
649
+ if (this.settings.isOpen) {
650
+ return
651
+ }
652
+
653
+ this.callbacks.open()
654
+ }
655
+ main.appendChild(input)
656
+
657
+ // If addable is enabled, add the addable div
658
+ if (this.callbacks.addable) {
659
+ addable.classList.add(this.classes.addable)
660
+ const plus = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
661
+ plus.setAttribute('viewBox', '0 0 100 100')
662
+ const plusPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
663
+ plusPath.setAttribute('d', this.classes.addablePath)
664
+ plus.appendChild(plusPath)
665
+ addable.appendChild(plus)
666
+ addable.onclick = (e: Event) => {
667
+ e.preventDefault()
668
+ e.stopPropagation()
669
+
670
+ // Do nothing if addable is not set
671
+ if (!this.callbacks.addable) {
672
+ return
673
+ }
674
+
675
+ // Grab input value
676
+ const inputValue = this.content.search.input.value.trim()
677
+ if (inputValue === '') {
678
+ this.content.search.input.focus()
679
+ return
680
+ }
681
+
682
+ // Call addable callback
683
+ const addableValue = this.callbacks.addable(inputValue)
684
+
685
+ // If the addableValue is a string, we will add it as a new option
686
+ // Otherwise we will assume it is an option object
687
+ if (typeof addableValue === 'string') {
688
+ this.callbacks.addOption(
689
+ new Option({
690
+ text: addableValue,
691
+ value: addableValue,
692
+ }),
693
+ )
694
+ } else {
695
+ this.callbacks.addOption(new Option(addableValue))
696
+ }
697
+
698
+ // Add option to selected
699
+ this.callbacks.setSelected([inputValue])
700
+
701
+ // Clear search
702
+ this.callbacks.search('')
703
+
704
+ // Close it only if closeOnSelect = true
705
+ if (this.settings.closeOnSelect) {
706
+ setTimeout(() => {
707
+ // Give it a little padding for a better looking animation
708
+ this.callbacks.close()
709
+ }, 100)
710
+ }
711
+ }
712
+ main.appendChild(addable)
713
+
714
+ // Add the addable to the search return
715
+ searchReturn.addable = {
716
+ main: addable,
717
+ svg: plus,
718
+ path: plusPath,
719
+ }
720
+ }
721
+
722
+ return searchReturn
723
+ }
724
+
725
+ public searchFocus(trigger: boolean): void {
726
+ if (!trigger) {
727
+ this.settings.triggerFocus = false
728
+ }
729
+ this.content.search.input.focus()
730
+ this.settings.triggerFocus = true
731
+ }
732
+
733
+ public getOptions(notPlaceholder = false, notDisabled = false, notHidden = false): HTMLDivElement[] {
734
+ // Put together query string
735
+ let query = '.' + this.classes.option
736
+ if (notPlaceholder) {
737
+ query += ':not(.' + this.classes.placeholder + ')'
738
+ }
739
+ if (notDisabled) {
740
+ query += ':not(.' + this.classes.disabled + ')'
741
+ }
742
+ if (notHidden) {
743
+ query += ':not(.' + this.classes.hide + ')'
744
+ }
745
+
746
+ return Array.from(this.content.list.querySelectorAll(query))
747
+ }
748
+
749
+ // highlightUp is used to highlight the previous option in the list
750
+ public highlight(dir: 'up' | 'down'): void {
751
+ // Get full list of options in list
752
+ const options = this.getOptions(true, true, true)
753
+
754
+ // If there are no options, do nothing
755
+ if (options.length === 0) {
756
+ return
757
+ }
758
+
759
+ // If length is 1, highlight it
760
+ if (options.length === 1) {
761
+ // Check if option doesnt already has highlighted class
762
+ if (!options[0].classList.contains(this.classes.highlighted)) {
763
+ options[0].classList.add(this.classes.highlighted)
764
+ return
765
+ }
766
+ }
767
+
768
+ // Loop through options and find the highlighted one
769
+ for (let i = 0; i < options.length; i++) {
770
+ if (options[i].classList.contains(this.classes.highlighted)) {
771
+ // Remove highlighted class from current one
772
+ options[i].classList.remove(this.classes.highlighted)
773
+
774
+ // Highlight the next one
775
+ if (dir === 'down') {
776
+ if (i + 1 < options.length) {
777
+ options[i + 1].classList.add(this.classes.highlighted)
778
+ this.ensureElementInView(this.content.list, options[i + 1])
779
+ } else {
780
+ options[0].classList.add(this.classes.highlighted)
781
+ this.ensureElementInView(this.content.list, options[0])
782
+ }
783
+ } else {
784
+ if (i - 1 >= 0) {
785
+ options[i - 1].classList.add(this.classes.highlighted)
786
+ this.ensureElementInView(this.content.list, options[i - 1])
787
+ } else {
788
+ options[options.length - 1].classList.add(this.classes.highlighted)
789
+ this.ensureElementInView(this.content.list, options[options.length - 1])
790
+ }
791
+ }
792
+
793
+ return
794
+ }
795
+ }
796
+
797
+ // If we get here, there is no highlighted option
798
+ // So we will highlight the first or last based upon direction
799
+ options[dir === 'down' ? 0 : options.length - 1].classList.add(this.classes.highlighted)
800
+
801
+ // Scroll to highlighted one
802
+ this.ensureElementInView(this.content.list, options[dir === 'down' ? 0 : options.length - 1])
803
+ }
804
+
805
+ // Create main container that options will reside
806
+ public listDiv(): HTMLDivElement {
807
+ const options = document.createElement('div')
808
+ options.classList.add(this.classes.list)
809
+ options.setAttribute('role', 'listbox')
810
+
811
+ return options
812
+ }
813
+
814
+ public renderError(error: string) {
815
+ // Clear out innerHtml
816
+ this.content.list.innerHTML = ''
817
+
818
+ const errorDiv = document.createElement('div')
819
+ errorDiv.classList.add(this.classes.error)
820
+ errorDiv.textContent = error
821
+ this.content.list.appendChild(errorDiv)
822
+ }
823
+
824
+ public renderSearching() {
825
+ // Clear out innerHtml
826
+ this.content.list.innerHTML = ''
827
+
828
+ const searchingDiv = document.createElement('div')
829
+ searchingDiv.classList.add(this.classes.searching)
830
+ searchingDiv.textContent = this.settings.searchingText
831
+ this.content.list.appendChild(searchingDiv)
832
+ }
833
+
834
+ // Take in data and add options to
835
+ public renderOptions(data: DataArray): void {
836
+ // Clear out innerHtml
837
+ this.content.list.innerHTML = ''
838
+
839
+ // If no results show no results text
840
+ if (data.length === 0) {
841
+ const noResults = document.createElement('div')
842
+ noResults.classList.add(this.classes.option)
843
+ noResults.classList.add(this.classes.disabled)
844
+ noResults.innerHTML = this.settings.searchText
845
+ this.content.list.appendChild(noResults)
846
+ return
847
+ }
848
+
849
+ // Append individual options to div container
850
+ for (const d of data) {
851
+ // Create optgroup
852
+ if (d instanceof Optgroup) {
853
+ // Create optgroup
854
+ const optgroupEl = document.createElement('div')
855
+ optgroupEl.classList.add(this.classes.optgroup)
856
+
857
+ // Create label
858
+ const optgroupLabel = document.createElement('div')
859
+ optgroupLabel.classList.add(this.classes.optgroupLabel)
860
+ optgroupLabel.innerHTML = d.label
861
+
862
+ // If selectByGroup is true and isMultiple then add click event to label
863
+ if (this.settings.selectByGroup && this.settings.isMultiple) {
864
+ optgroupLabel.classList.add(this.classes.optgroupSelectable)
865
+ optgroupLabel.addEventListener('click', (e: MouseEvent) => {
866
+ e.preventDefault()
867
+ e.stopPropagation()
868
+
869
+ for (const childEl of optgroupEl.children as any as HTMLDivElement[]) {
870
+ if (childEl.className.indexOf(this.classes.option) !== -1) {
871
+ childEl.click()
872
+ }
873
+ }
874
+ })
875
+ }
876
+
877
+ // Add optgroup label
878
+ optgroupEl.appendChild(optgroupLabel)
879
+
880
+ // Loop through options
881
+ for (const o of d.options) {
882
+ optgroupEl.appendChild(this.option(o))
883
+ }
884
+
885
+ // Add optgroup to list
886
+ this.content.list.appendChild(optgroupEl)
887
+ }
888
+
889
+ // Create option
890
+ if (d instanceof Option) {
891
+ this.content.list.appendChild(this.option(d as Option))
892
+ }
893
+ }
894
+ }
895
+
896
+ // Create option div element
897
+ public option(option: Option): HTMLDivElement {
898
+ // Add hidden placeholder
899
+ if (option.placeholder) {
900
+ const placeholder = document.createElement('div')
901
+ placeholder.classList.add(this.classes.option)
902
+ placeholder.classList.add(this.classes.hide)
903
+ return placeholder
904
+ }
905
+
906
+ // Create option
907
+ const optionEl = document.createElement('div')
908
+ optionEl.dataset.id = option.id // Dataset id for identifying an option
909
+ optionEl.classList.add(this.classes.option)
910
+ optionEl.setAttribute('role', 'option') // WCAG attribute
911
+ if (option.class) {
912
+ option.class.split(' ').forEach((dataClass: string) => {
913
+ optionEl.classList.add(dataClass)
914
+ })
915
+ }
916
+ if (option.style) {
917
+ optionEl.style.cssText = option.style
918
+ }
919
+
920
+ // Set option content
921
+ if (this.settings.searchHighlight && this.content.search.input.value.trim() !== '') {
922
+ const textOrHtml = option.html !== '' ? option.html : option.text
923
+ optionEl.innerHTML = this.highlightText(
924
+ textOrHtml,
925
+ this.content.search.input.value,
926
+ this.classes.searchHighlighter,
927
+ )
928
+ } else if (option.html !== '') {
929
+ optionEl.innerHTML = option.html
930
+ } else {
931
+ optionEl.textContent = option.text
932
+ }
933
+
934
+ // Set title attribute
935
+ if (this.settings.showOptionTooltips && optionEl.textContent) {
936
+ optionEl.setAttribute('title', optionEl.textContent)
937
+ }
938
+
939
+ // If allowed to deselect, null onclick and add disabled
940
+ if ((option.selected && !this.settings.allowDeselect) || (option.disabled && !this.settings.allowDeselect)) {
941
+ optionEl.classList.add(this.classes.disabled)
942
+ }
943
+
944
+ // If option is selected and hideSelectedOption is true, hide it
945
+ if (option.selected && this.settings.hideSelected) {
946
+ optionEl.classList.add(this.classes.hide)
947
+ }
948
+
949
+ // If option is selected
950
+ if (option.selected) {
951
+ optionEl.classList.add(this.classes.optionSelected)
952
+ } else {
953
+ optionEl.classList.remove(this.classes.optionSelected)
954
+ }
955
+
956
+ // Add click event listener
957
+ optionEl.addEventListener('click', (e: MouseEvent) => {
958
+ e.preventDefault()
959
+ e.stopPropagation()
960
+
961
+ // Setup variables
962
+ const selectedOptions = this.store.getSelected()
963
+ const element = e.currentTarget as HTMLDivElement
964
+ const elementID = String(element.dataset.id)
965
+
966
+ // If the option is disabled or selected and the user isnt allowed to deselect
967
+ if (option.disabled || (option.selected && !this.settings.allowDeselect)) {
968
+ return
969
+ }
970
+
971
+ // Check limit and do nothing if limit is reached
972
+ if (
973
+ this.settings.isMultiple &&
974
+ Array.isArray(selectedOptions) &&
975
+ this.settings.maxSelected <= selectedOptions.length
976
+ ) {
977
+ return
978
+ }
979
+
980
+ // Setup variables
981
+ let shouldUpdate = false
982
+ const before = this.store.getSelectedOptions()
983
+ let after = [] as Option[]
984
+
985
+ // If multiple
986
+ if (this.settings.isMultiple) {
987
+ if (option.selected) {
988
+ // If selected after would remove
989
+ after = before.filter((o: Option) => o.id !== elementID)
990
+ } else {
991
+ // If not selected after would add
992
+ after = before.concat(option)
993
+ }
994
+ }
995
+
996
+ // If single
997
+ if (!this.settings.isMultiple) {
998
+ if (option.selected) {
999
+ // If selected after would remove
1000
+ after = []
1001
+ } else {
1002
+ // If not selected after would add
1003
+ after = [option]
1004
+ }
1005
+ }
1006
+
1007
+ // If no beforeOnChange is set automatically update at end
1008
+ if (!this.callbacks.beforeChange) {
1009
+ shouldUpdate = true
1010
+ }
1011
+
1012
+ if (this.callbacks.beforeChange) {
1013
+ // Check if beforeChange returns false
1014
+ if (this.callbacks.beforeChange(after, before) === false) {
1015
+ shouldUpdate = false
1016
+ } else {
1017
+ shouldUpdate = true
1018
+ }
1019
+ }
1020
+
1021
+ if (shouldUpdate) {
1022
+ // Check if the option exists in the store
1023
+ // if not run addOption callback
1024
+ if (!this.store.getOptionByID(elementID)) {
1025
+ this.callbacks.addOption(option)
1026
+ }
1027
+
1028
+ // Get values from after and set as selected
1029
+ this.callbacks.setSelected(after.map((o: Option) => o.value))
1030
+
1031
+ // If closeOnSelect is true
1032
+ if (this.settings.closeOnSelect) {
1033
+ this.callbacks.close()
1034
+ }
1035
+
1036
+ // callback that the value has changed
1037
+ if (this.callbacks.afterChange) {
1038
+ this.callbacks.afterChange(after)
1039
+ }
1040
+ }
1041
+ })
1042
+
1043
+ return optionEl
1044
+ }
1045
+
1046
+ public destroy(): void {
1047
+ // Remove main
1048
+ this.main.main.remove()
1049
+
1050
+ // Remove content
1051
+ this.content.main.remove()
1052
+ }
1053
+
1054
+ private highlightText(str: string, search: any, className: string) {
1055
+ // the completed string will be itself if already set, otherwise, the string that was passed in
1056
+ let completedString: any = str
1057
+ const regex = new RegExp('(' + search.trim() + ')(?![^<]*>[^<>]*</)', 'i')
1058
+
1059
+ // If the regex doesn't match the string just exit
1060
+ if (!str.match(regex)) {
1061
+ return str
1062
+ }
1063
+
1064
+ // Otherwise, get to highlighting
1065
+ const matchStartPosition = (str.match(regex) as any).index
1066
+ const matchEndPosition = matchStartPosition + (str.match(regex) as any)[0].toString().length
1067
+ const originalTextFoundByRegex = str.substring(matchStartPosition, matchEndPosition)
1068
+ completedString = completedString.replace(regex, `<mark class="${className}">${originalTextFoundByRegex}</mark>`)
1069
+ return completedString
1070
+ }
1071
+
1072
+ public moveContentAbove(): void {
1073
+ let mainHeight: number = this.main.main.offsetHeight
1074
+
1075
+ const contentHeight = this.content.main.offsetHeight
1076
+ const height = mainHeight + contentHeight - 1
1077
+ this.content.main.style.margin = '-' + height + 'px 0px 0px 0px'
1078
+ this.content.main.style.transformOrigin = 'center bottom'
1079
+
1080
+ this.main.main.classList.remove(this.classes.openBelow)
1081
+ this.main.main.classList.add(this.classes.openAbove)
1082
+ this.content.main.classList.remove(this.classes.openBelow)
1083
+ this.content.main.classList.add(this.classes.openAbove)
1084
+ }
1085
+
1086
+ public moveContentBelow(): void {
1087
+ this.content.main.style.margin = '-1px 0px 0px 0px'
1088
+ this.content.main.style.transformOrigin = 'center top'
1089
+
1090
+ this.main.main.classList.remove(this.classes.openAbove)
1091
+ this.main.main.classList.add(this.classes.openBelow)
1092
+ this.content.main.classList.remove(this.classes.openAbove)
1093
+ this.content.main.classList.add(this.classes.openBelow)
1094
+ }
1095
+
1096
+ public ensureElementInView(container: HTMLElement, element: HTMLElement): void {
1097
+ // Determine container top and bottom
1098
+ const cTop = container.scrollTop + container.offsetTop // Make sure to have offsetTop
1099
+ const cBottom = cTop + container.clientHeight
1100
+
1101
+ // Determine element top and bottom
1102
+ const eTop = element.offsetTop
1103
+ const eBottom = eTop + element.clientHeight
1104
+
1105
+ // Check if out of view
1106
+ if (eTop < cTop) {
1107
+ container.scrollTop -= cTop - eTop
1108
+ } else if (eBottom > cBottom) {
1109
+ container.scrollTop += eBottom - cBottom
1110
+ }
1111
+ }
1112
+
1113
+ public putContent(el: HTMLElement, isOpen: boolean): 'up' | 'down' {
1114
+ const height = el.offsetHeight
1115
+ const rect = el.getBoundingClientRect()
1116
+ const elemTop = isOpen ? rect.top : rect.top - height
1117
+ const elemBottom = isOpen ? rect.bottom : rect.bottom + height
1118
+
1119
+ if (elemTop <= 0) {
1120
+ return 'down'
1121
+ }
1122
+ if (elemBottom >= window.innerHeight) {
1123
+ return 'up'
1124
+ }
1125
+
1126
+ // default to current position if we cant determine a perfect one
1127
+ return 'down'
1128
+ }
1129
+ }