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
@@ -1,523 +1,428 @@
1
- import { Config } from './config'
2
- import { Select } from './select'
3
- import { Slim } from './slim'
4
- import { Data, dataArray, Option, validateData } from './data'
5
- import { hasClassInTree, putContent, debounce, ensureElementInView } from './helper'
6
-
7
- interface Constructor {
1
+ import { debounce, hasClassInTree } from './helper'
2
+ import Render from './render'
3
+ import Select from './select'
4
+ import Settings, { SettingsPartial } from './settings'
5
+ import Store, { DataArray, DataArrayPartial, Option, OptionOptional } from './store'
6
+
7
+ // Export everything except the "export default"
8
+ export * from './helper'
9
+ export * from './settings'
10
+ export * from './select'
11
+ export * from './store'
12
+ export * from './render'
13
+
14
+ // Export all "export defaults"
15
+ export { Settings, Select, Store, Render }
16
+
17
+ export interface Config {
8
18
  select: string | Element
9
- data?: dataArray
10
- showSearch?: boolean
11
- searchPlaceholder?: string
12
- searchText?: string
13
- searchingText?: string
14
- searchFocus?: boolean
15
- searchHighlight?: boolean
16
- searchFilter?: (opt: Option, search: string) => boolean
17
- closeOnSelect?: boolean
18
- showContent?: string
19
- placeholder?: string
20
- allowDeselect?: boolean
21
- allowDeselectOption?: boolean
22
- hideSelectedOption?: boolean
23
- deselectLabel?: string
24
- isEnabled?: boolean
25
- valuesUseText?: boolean // Use text value when showing selected value
26
- showOptionTooltips?: boolean
27
- selectByGroup?: boolean
28
- limit?: number
29
- timeoutDelay?: number
30
- addToBody?: boolean
19
+ data?: DataArrayPartial
20
+ settings?: SettingsPartial
21
+ events?: Events
22
+ }
31
23
 
32
- // Events
33
- ajax?: (value: string, func: (info: any) => void) => void
34
- addable?: (value: string) => Option | string
35
- beforeOnChange?: (info: Option | Option[]) => void | boolean
36
- onChange?: (info: Option | Option[]) => void
24
+ export interface Events {
25
+ search?: (searchValue: string, currentData: DataArray) => Promise<DataArrayPartial> | DataArrayPartial
26
+ searchFilter?: (option: Option, search: string) => boolean
27
+ addable?: (value: string) => OptionOptional | string
28
+ beforeChange?: (newVal: Option[], oldVal: Option[]) => boolean | void
29
+ afterChange?: (newVal: Option[]) => void
37
30
  beforeOpen?: () => void
38
31
  afterOpen?: () => void
39
32
  beforeClose?: () => void
40
33
  afterClose?: () => void
34
+ error?: (err: Error) => void
41
35
  }
42
36
 
43
37
  export default class SlimSelect {
44
- public config: Config
45
- public select: Select
46
- public data: Data
47
- public slim: Slim
48
- public ajax: ((value: string, func: (info: any) => void) => void) | null = null
49
- public addable: ((value: string) => Option | string) | null = null
50
- public beforeOnChange: ((info: Option) => void | boolean) | null = null
51
- public onChange: ((info: Option) => void) | null = null
52
- public beforeOpen: (() => void) | null = null
53
- public afterOpen: (() => void) | null = null
54
- public beforeClose: (() => void) | null = null
55
- public afterClose: (() => void) | null = null
56
-
57
- private windowScroll: (e: Event) => void = debounce((e: Event) => {
58
- if (this.data.contentOpen) {
59
- if (putContent(this.slim.content, this.data.contentPosition, this.data.contentOpen) === 'above') {
60
- this.moveContentAbove()
61
- } else {
62
- this.moveContentBelow()
38
+ public selectEl: HTMLSelectElement
39
+
40
+ // Classes
41
+ public settings!: Settings
42
+ public select!: Select
43
+ public store!: Store
44
+ public render!: Render
45
+
46
+ // Events
47
+ public events = {
48
+ search: undefined,
49
+ searchFilter: (opt: Option, search: string) => {
50
+ return opt.text.toLowerCase().indexOf(search.toLowerCase()) !== -1
51
+ },
52
+ addable: undefined,
53
+ beforeChange: undefined,
54
+ afterChange: undefined,
55
+ beforeOpen: undefined,
56
+ afterOpen: undefined,
57
+ beforeClose: undefined,
58
+ afterClose: undefined,
59
+ } as Events
60
+
61
+ constructor(config: Config) {
62
+ // Make sure you get the right element
63
+ this.selectEl = (
64
+ typeof config.select === 'string' ? document.querySelector(config.select) : config.select
65
+ ) as HTMLSelectElement
66
+ if (!this.selectEl) {
67
+ if (config.events && config.events.error) {
68
+ config.events.error(new Error('Could not find select element'))
63
69
  }
70
+ return
71
+ }
72
+ if (this.selectEl.tagName !== 'SELECT') {
73
+ if (config.events && config.events.error) {
74
+ config.events.error(new Error('Element isnt of type select'))
75
+ }
76
+ return
64
77
  }
65
- })
66
-
67
- constructor(info: Constructor) {
68
- const selectElement = this.validate(info)
69
78
 
70
79
  // If select already has a slim select id on it lets destroy it first
71
- if (selectElement.dataset.ssid) { this.destroy(selectElement.dataset.ssid) }
72
-
73
- // Set ajax function if passed in
74
- if (info.ajax) { this.ajax = info.ajax }
75
-
76
- // Add addable if option is passed in
77
- if (info.addable) { this.addable = info.addable }
78
-
79
- this.config = new Config({
80
- select: selectElement,
81
- isAjax: (info.ajax ? true : false),
82
- showSearch: info.showSearch,
83
- searchPlaceholder: info.searchPlaceholder,
84
- searchText: info.searchText,
85
- searchingText: info.searchingText,
86
- searchFocus: info.searchFocus,
87
- searchHighlight: info.searchHighlight,
88
- searchFilter: info.searchFilter,
89
- closeOnSelect: info.closeOnSelect,
90
- showContent: info.showContent,
91
- placeholderText: info.placeholder,
92
- allowDeselect: info.allowDeselect,
93
- allowDeselectOption: info.allowDeselectOption,
94
- hideSelectedOption: info.hideSelectedOption,
95
- deselectLabel: info.deselectLabel,
96
- isEnabled: info.isEnabled,
97
- valuesUseText: info.valuesUseText,
98
- showOptionTooltips: info.showOptionTooltips,
99
- selectByGroup: info.selectByGroup,
100
- limit: info.limit,
101
- timeoutDelay: info.timeoutDelay,
102
- addToBody: info.addToBody
103
- })
80
+ if (this.selectEl.dataset.ssid) {
81
+ this.destroy()
82
+ }
83
+
84
+ // Set settings
85
+ this.settings = new Settings(config.settings)
104
86
 
105
- this.select = new Select({
106
- select: selectElement,
107
- main: this
87
+ // Set events
88
+ for (const key in config.events) {
89
+ if (config.events.hasOwnProperty(key)) {
90
+ ;(this.events as { [key: string]: any })[key] = (config.events as { [key: string]: any })[key]
91
+ }
92
+ }
93
+
94
+ // Upate settings with type, style and classname
95
+ this.settings.isMultiple = this.selectEl.multiple
96
+ this.settings.style = this.selectEl.style.cssText
97
+ this.settings.class = this.selectEl.className.split(' ')
98
+
99
+ // Set select class
100
+ this.select = new Select(this.selectEl)
101
+ this.select.updateSelect(this.settings.id, this.settings.style, this.settings.class)
102
+ this.select.hideUI() // Hide the original select element
103
+
104
+ // Add select listeners
105
+ this.select.addSelectChangeListener((data: DataArray) => {
106
+ // Run set data from the values given
107
+ this.setData(data)
108
+ })
109
+ this.select.addValueChangeListener((values: string[]) => {
110
+ // Run set selected from the values given
111
+ this.setSelected(values)
108
112
  })
109
113
 
110
- this.data = new Data({ main: this })
111
- this.slim = new Slim({ main: this })
114
+ // Set store class
115
+ this.store = new Store(
116
+ this.settings.isMultiple ? 'multiple' : 'single',
117
+ config.data ? config.data : this.select.getData(),
118
+ )
112
119
 
113
- // Add after original select element
114
- if (this.select.element.parentNode) {
115
- this.select.element.parentNode.insertBefore(this.slim.container, this.select.element.nextSibling)
120
+ // If data is passed update the original select element
121
+ if (config.data) {
122
+ this.select.updateOptions(this.store.getData())
116
123
  }
117
124
 
118
- // If data is passed in lets set it
119
- // and thus will start the render
120
- if (info.data) {
121
- this.setData(info.data)
122
- } else {
123
- // Do an initial render on startup
124
- this.render()
125
+ // Set render callbacks
126
+ const callbacks = {
127
+ open: this.open.bind(this),
128
+ close: this.close.bind(this),
129
+ addable: this.events.addable ? this.events.addable : undefined,
130
+ setSelected: this.setSelected.bind(this),
131
+ addOption: this.addOption.bind(this),
132
+ search: this.search.bind(this),
133
+ beforeChange: this.events.beforeChange,
134
+ afterChange: this.events.afterChange,
135
+ }
136
+
137
+ // Setup render class
138
+ this.render = new Render(this.settings, this.store, callbacks)
139
+
140
+ // Add render after original select element
141
+ if (this.selectEl.parentNode) {
142
+ this.selectEl.parentNode.insertBefore(this.render.main.main, this.selectEl.nextSibling)
125
143
  }
126
144
 
127
145
  // Add onclick listener to document to closeContent if clicked outside
128
146
  document.addEventListener('click', this.documentClick)
129
147
 
148
+ // Add window resize listener to moveContent if window size changes
149
+ window.addEventListener('resize', this.windowResize, false)
150
+
130
151
  // If the user wants to show the content forcibly on a specific side,
131
152
  // there is no need to listen for scroll events
132
- if (this.config.showContent === 'auto') {
153
+ if (this.settings.openPosition === 'auto') {
133
154
  window.addEventListener('scroll', this.windowScroll, false)
134
155
  }
135
156
 
136
- // Add event callbacks after everthing has been created
137
- if (info.beforeOnChange) { this.beforeOnChange = info.beforeOnChange }
138
- if (info.onChange) { this.onChange = info.onChange }
139
- if (info.beforeOpen) { this.beforeOpen = info.beforeOpen }
140
- if (info.afterOpen) { this.afterOpen = info.afterOpen }
141
- if (info.beforeClose) { this.beforeClose = info.beforeClose }
142
- if (info.afterClose) { this.afterClose = info.afterClose }
143
-
144
157
  // If disabled lets call it
145
- if (!this.config.isEnabled) { this.disable() }
158
+ if (!this.settings.isEnabled) {
159
+ this.disable()
160
+ }
161
+
162
+ // If alwaysOpnen then open it
163
+ if (this.settings.alwaysOpen) {
164
+ this.open()
165
+ }
166
+
167
+ // Add SlimSelect to select element
168
+ ;(this.selectEl as any).slim = this
146
169
  }
147
170
 
148
- public validate(info: Constructor) {
149
- const select = (typeof info.select === 'string' ? document.querySelector(info.select) : info.select) as HTMLSelectElement
150
- if (!select) { throw new Error('Could not find select element') }
151
- if (select.tagName !== 'SELECT') { throw new Error('Element isnt of type select') }
152
- return select
171
+ // Set to enabled and remove disabled classes
172
+ public enable(): void {
173
+ this.settings.isEnabled = true
174
+
175
+ this.select.enable()
176
+ this.render.enable()
153
177
  }
154
178
 
155
- public selected(): string | string[] {
156
- if (this.config.isMultiple) {
157
- const selected = this.data.getSelected() as Option[]
158
- const outputSelected: string[] = []
159
- for (const s of selected) {
160
- outputSelected.push(s.value as string)
161
- }
162
- return outputSelected
163
- } else {
164
- const selected = this.data.getSelected() as Option
165
- return (selected ? selected.value as string : '')
166
- }
179
+ // Set to disabled and add disabled classes
180
+ public disable(): void {
181
+ this.settings.isEnabled = false
182
+
183
+ this.select.disable()
184
+ this.render.disable()
167
185
  }
168
186
 
169
- // Sets value of the select, adds it to data and original select
170
- public set(value: string | string[], type: string = 'value', close: boolean = true, render: boolean = true) {
171
- if (this.config.isMultiple && !Array.isArray(value)) {
172
- this.data.addToSelected(value, type)
173
- } else {
174
- this.data.setSelected(value, type)
187
+ public getData(): DataArray {
188
+ return this.store.getData()
189
+ }
190
+
191
+ public setData(data: DataArrayPartial): void {
192
+ // Validate data
193
+ const err = this.store.validateDataArray(data)
194
+ if (err) {
195
+ if (this.events.error) {
196
+ this.events.error(err)
197
+ }
198
+ return
175
199
  }
176
- this.select.setValue()
177
- this.data.onDataChange() // Trigger on change callback
178
- this.render()
179
200
 
180
- if (close) { this.close() }
201
+ // Update the store
202
+ this.store.setData(data)
203
+ const dataClean = this.store.getData()
204
+
205
+ // Update original select element
206
+ this.select.updateOptions(dataClean)
207
+
208
+ // Update the render
209
+ this.render.renderValues()
210
+ this.render.renderOptions(dataClean)
181
211
  }
182
212
 
183
- // setSelected is just mapped to the set method
184
- public setSelected(value: string | string[], type: string = 'value', close: boolean = true, render: boolean = true) {
185
- this.set(value, type, close, render)
213
+ public getSelected(): string[] {
214
+ return this.store.getSelected()
186
215
  }
187
216
 
188
- public setData(data: dataArray) {
189
- // Validate data if passed in
190
- const isValid = validateData(data)
191
- if (!isValid) { console.error('Validation problem on: #' + this.select.element.id); return } // If data passed in is not valid DO NOT parse, set and render
217
+ public setSelected(value: string | string[]): void {
218
+ // Update the store
219
+ this.store.setSelectedBy('value', Array.isArray(value) ? value : [value])
220
+ const data = this.store.getData()
192
221
 
193
- const newData = JSON.parse(JSON.stringify(data))
194
- const selected = this.data.getSelected()
222
+ // Update the select element
223
+ this.select.updateOptions(data)
195
224
 
196
- // Check newData to make sure value is set
197
- // If not set from text
198
- for (let i = 0; i < newData.length; i++) {
199
- if (!newData[i].value && !newData[i].placeholder) {
200
- newData[i].value = newData[i].text
201
- }
202
- }
225
+ // Update the render
226
+ this.render.renderValues()
227
+ this.render.renderOptions(data)
228
+ }
203
229
 
204
- // If its an ajax type keep selected values
205
- if (this.config.isAjax && selected) {
206
- if (this.config.isMultiple) {
207
- const reverseSelected = (selected as Option[]).reverse()
208
- for (const r of reverseSelected) {
209
- newData.unshift(r)
210
- }
211
- } else {
212
- newData.unshift(selected)
213
-
214
- // Look for duplicate selected if so remove it
215
- for (let i = 0; i < newData.length; i++) {
216
- if (!newData[i].placeholder && newData[i].value === (selected as Option).value && newData[i].text === (selected as Option).text) {
217
- delete newData[i]
218
- }
219
- }
220
-
221
- // Add placeholder if it doesnt already have one
222
- let hasPlaceholder = false
223
- for (let i = 0; i < newData.length; i++) {
224
- if (newData[i].placeholder) {
225
- hasPlaceholder = true
226
- }
227
- }
228
- if (!hasPlaceholder) {
229
- newData.unshift({ text: '', placeholder: true })
230
- }
231
- }
232
- }
230
+ public addOption(option: OptionOptional): void {
231
+ // Add option to store
232
+ this.store.addOption(option)
233
+ const data = this.store.getData()
233
234
 
234
- this.select.create(newData)
235
- this.data.parseSelectData()
236
- this.data.setSelectedFromSelect()
237
- }
235
+ // Update the select element
236
+ this.select.updateOptions(data)
238
237
 
239
- // addData will append to the current data set
240
- public addData(data: Option) {
241
- // Validate data if passed in
242
- const isValid = validateData([data])
243
- if (!isValid) { console.error('Validation problem on: #' + this.select.element.id); return } // If data passed in is not valid DO NOT parse, set and render
244
-
245
- this.data.add(this.data.newOption(data))
246
- this.select.create(this.data.data)
247
- this.data.parseSelectData()
248
- this.data.setSelectedFromSelect()
249
- this.render()
238
+ // Update the render
239
+ this.render.renderValues()
240
+ this.render.renderOptions(data)
250
241
  }
251
242
 
252
- // Open content section
253
243
  public open(): void {
254
244
  // Dont open if disabled
255
- if (!this.config.isEnabled) { return }
256
-
257
245
  // Dont do anything if the content is already open
258
- if (this.data.contentOpen) { return }
246
+ if (!this.settings.isEnabled || this.settings.isOpen) {
247
+ return
248
+ }
259
249
 
260
250
  // Run beforeOpen callback
261
- if (this.beforeOpen) { this.beforeOpen() }
262
-
263
- if (this.config.isMultiple && this.slim.multiSelected) {
264
- this.slim.multiSelected.plus.classList.add('ss-cross')
265
- } else if (this.slim.singleSelected) {
266
- this.slim.singleSelected.arrowIcon.arrow.classList.remove('arrow-down')
267
- this.slim.singleSelected.arrowIcon.arrow.classList.add('arrow-up')
251
+ if (this.events.beforeOpen) {
252
+ this.events.beforeOpen()
268
253
  }
269
- (this.slim as any)[(this.config.isMultiple ? 'multiSelected' : 'singleSelected')].container.classList.add((this.data.contentPosition === 'above' ? this.config.openAbove : this.config.openBelow))
270
-
271
- if (this.config.addToBody) {
272
- // move the content in to the right location
273
- const containerRect = this.slim.container.getBoundingClientRect()
274
- this.slim.content.style.top = (containerRect.top + containerRect.height + window.scrollY) + 'px'
275
- this.slim.content.style.left = (containerRect.left + window.scrollX) + 'px'
276
- this.slim.content.style.width = containerRect.width + 'px'
277
- }
278
- this.slim.content.classList.add(this.config.open)
279
254
 
280
- // Check showContent to see if they want to specifically show in a certain direction
281
- if (this.config.showContent.toLowerCase() === 'up') {
282
- this.moveContentAbove()
283
- } else if (this.config.showContent.toLowerCase() === 'down') {
284
- this.moveContentBelow()
285
- } else {
286
- // Auto identify where to put it
287
- if (putContent(this.slim.content, this.data.contentPosition, this.data.contentOpen) === 'above') {
288
- this.moveContentAbove()
289
- } else {
290
- this.moveContentBelow()
291
- }
292
- }
255
+ // Tell render to open
256
+ this.render.open()
293
257
 
294
- // Move to selected option for single option
295
- if (!this.config.isMultiple) {
296
- const selected = this.data.getSelected() as Option
297
- if (selected) {
298
- const selectedId = selected.id
299
- const selectedOption = this.slim.list.querySelector('[data-id="' + selectedId + '"]') as HTMLElement
300
- if (selectedOption) {
301
- ensureElementInView(this.slim.list, selectedOption)
302
- }
303
- }
258
+ // Focus on input field only if search is enabled
259
+ if (this.settings.showSearch) {
260
+ this.render.searchFocus(false)
304
261
  }
305
262
 
306
263
  // setTimeout is for animation completion
307
264
  setTimeout(() => {
308
- this.data.contentOpen = true
309
-
310
- // Focus on input field
311
- if (this.config.searchFocus) {
312
- this.slim.search.input.focus()
313
- }
314
-
315
265
  // Run afterOpen callback
316
- if (this.afterOpen) {
317
- this.afterOpen()
266
+ if (this.events.afterOpen) {
267
+ this.events.afterOpen()
318
268
  }
319
- }, this.config.timeoutDelay)
269
+
270
+ // Update settings
271
+ this.settings.isOpen = true
272
+ }, this.settings.timeoutDelay)
273
+
274
+ // Start an interval to check if main has moved
275
+ // in order to keep content close to main
276
+ if (this.settings.intervalMove) {
277
+ clearInterval(this.settings.intervalMove)
278
+ }
279
+ this.settings.intervalMove = setInterval(this.render.moveContent.bind(this.render), 500)
320
280
  }
321
281
 
322
- // Close content section
323
282
  public close(): void {
324
283
  // Dont do anything if the content is already closed
325
- if (!this.data.contentOpen) { return }
284
+ // Dont do anything if alwaysOpen is true
285
+ if (!this.settings.isOpen || this.settings.alwaysOpen) {
286
+ return
287
+ }
326
288
 
327
289
  // Run beforeClose calback
328
- if (this.beforeClose) { this.beforeClose() }
329
-
330
- // this.slim.search.input.blur() // Removed due to safari quirk
331
- if (this.config.isMultiple && this.slim.multiSelected) {
332
- this.slim.multiSelected.container.classList.remove(this.config.openAbove)
333
- this.slim.multiSelected.container.classList.remove(this.config.openBelow)
334
- this.slim.multiSelected.plus.classList.remove('ss-cross')
335
- } else if (this.slim.singleSelected) {
336
- this.slim.singleSelected.container.classList.remove(this.config.openAbove)
337
- this.slim.singleSelected.container.classList.remove(this.config.openBelow)
338
- this.slim.singleSelected.arrowIcon.arrow.classList.add('arrow-down')
339
- this.slim.singleSelected.arrowIcon.arrow.classList.remove('arrow-up')
290
+ if (this.events.beforeClose) {
291
+ this.events.beforeClose()
340
292
  }
341
- this.slim.content.classList.remove(this.config.open)
342
- this.data.contentOpen = false
343
293
 
294
+ // Tell render to close
295
+ this.render.close()
296
+
297
+ // Clear search
344
298
  this.search('') // Clear search
345
299
 
300
+ // If we arent tabbing focus back on the main element
301
+ this.render.mainFocus(false)
302
+
346
303
  // Reset the content below
347
304
  setTimeout(() => {
348
- this.slim.content.removeAttribute('style')
349
- this.data.contentPosition = 'below'
350
-
351
- if (this.config.isMultiple && this.slim.multiSelected) {
352
- this.slim.multiSelected.container.classList.remove(this.config.openAbove)
353
- this.slim.multiSelected.container.classList.remove(this.config.openBelow)
354
- } else if (this.slim.singleSelected) {
355
- this.slim.singleSelected.container.classList.remove(this.config.openAbove)
356
- this.slim.singleSelected.container.classList.remove(this.config.openBelow)
305
+ // Run afterClose callback
306
+ if (this.events.afterClose) {
307
+ this.events.afterClose()
357
308
  }
358
309
 
359
- // After content is closed lets blur on the input field
360
- this.slim.search.input.blur()
310
+ // Update settings
311
+ this.settings.isOpen = false
312
+ }, this.settings.timeoutDelay)
361
313
 
362
- // Run afterClose callback
363
- if (this.afterClose) { this.afterClose() }
364
- }, this.config.timeoutDelay)
365
- }
366
-
367
- public moveContentAbove(): void {
368
- let selectHeight: number = 0
369
- if (this.config.isMultiple && this.slim.multiSelected) {
370
- selectHeight = this.slim.multiSelected.container.offsetHeight
371
- } else if (this.slim.singleSelected) {
372
- selectHeight = this.slim.singleSelected.container.offsetHeight
373
- }
374
- const contentHeight = this.slim.content.offsetHeight
375
- const height = selectHeight + contentHeight - 1
376
- this.slim.content.style.margin = '-' + height + 'px 0 0 0'
377
- this.slim.content.style.height = (height - selectHeight + 1) + 'px'
378
- this.slim.content.style.transformOrigin = 'center bottom'
379
- this.data.contentPosition = 'above'
380
-
381
- if (this.config.isMultiple && this.slim.multiSelected) {
382
- this.slim.multiSelected.container.classList.remove(this.config.openBelow)
383
- this.slim.multiSelected.container.classList.add(this.config.openAbove)
384
- } else if (this.slim.singleSelected) {
385
- this.slim.singleSelected.container.classList.remove(this.config.openBelow)
386
- this.slim.singleSelected.container.classList.add(this.config.openAbove)
314
+ if (this.settings.intervalMove) {
315
+ clearInterval(this.settings.intervalMove)
387
316
  }
388
317
  }
389
318
 
390
- public moveContentBelow(): void {
391
- this.data.contentPosition = 'below'
392
-
393
- if (this.config.isMultiple && this.slim.multiSelected) {
394
- this.slim.multiSelected.container.classList.remove(this.config.openAbove)
395
- this.slim.multiSelected.container.classList.add(this.config.openBelow)
396
- } else if (this.slim.singleSelected) {
397
- this.slim.singleSelected.container.classList.remove(this.config.openAbove)
398
- this.slim.singleSelected.container.classList.add(this.config.openBelow)
319
+ // Take in string value and search current options
320
+ public search(value: string): void {
321
+ // If the passed in value is not the same as the search input value
322
+ // then lets update the search input value
323
+ if (this.render.content.search.input.value !== value) {
324
+ this.render.content.search.input.value = value
399
325
  }
400
- }
401
326
 
402
- // Set to enabled, remove disabled classes and removed disabled from original select
403
- public enable(): void {
404
- this.config.isEnabled = true
405
- if (this.config.isMultiple && this.slim.multiSelected) {
406
- this.slim.multiSelected.container.classList.remove(this.config.disabled)
407
- } else if (this.slim.singleSelected) {
408
- this.slim.singleSelected.container.classList.remove(this.config.disabled)
327
+ // If no search event run regular search
328
+ if (!this.events.search) {
329
+ // If value is empty then render all options
330
+ this.render.renderOptions(
331
+ value === '' ? this.store.getData() : this.store.search(value, this.events.searchFilter!),
332
+ )
333
+ return
409
334
  }
410
335
 
411
- // Disable original select but dont trigger observer
412
- this.select.triggerMutationObserver = false
413
- this.select.element.disabled = false
414
- this.slim.search.input.disabled = false
415
- this.select.triggerMutationObserver = true
416
- }
336
+ // Search event exists so lets render the searching text
337
+ this.render.renderSearching()
417
338
 
418
- // Set to disabled, add disabled classes and add disabled to original select
419
- public disable(): void {
420
- this.config.isEnabled = false
421
- if (this.config.isMultiple && this.slim.multiSelected) {
422
- this.slim.multiSelected.container.classList.add(this.config.disabled)
423
- } else if (this.slim.singleSelected) {
424
- this.slim.singleSelected.container.classList.add(this.config.disabled)
425
- }
426
-
427
- // Enable original select but dont trigger observer
428
- this.select.triggerMutationObserver = false
429
- this.select.element.disabled = true
430
- this.slim.search.input.disabled = true
431
- this.select.triggerMutationObserver = true
432
- }
339
+ // Based upon the search event deal with the response
340
+ const searchResp = this.events.search(value, this.store.getSelectedOptions())
433
341
 
434
- // Take in string value and search current options
435
- public search(value: string): void {
436
- // Only filter data and rerender if value has changed
437
- if (this.data.searchValue === value) { return }
438
-
439
- this.slim.search.input.value = value
440
- if (this.config.isAjax) {
441
- const master = this
442
- this.config.isSearching = true
443
- this.render()
444
-
445
- // If ajax call it
446
- if (this.ajax) {
447
- this.ajax(value, (info: any) => {
448
- // Only process if return callback is not false
449
- master.config.isSearching = false
450
- if (Array.isArray(info)) {
451
- info.unshift({ text: '', placeholder: true })
452
- master.setData(info)
453
- master.data.search(value)
454
- master.render()
455
- } else if (typeof info === 'string') {
456
- master.slim.options(info)
457
- } else {
458
- master.render()
459
- }
342
+ // If the search event returns a promise
343
+ if (searchResp instanceof Promise) {
344
+ searchResp
345
+ .then((data: DataArrayPartial) => {
346
+ // Update the render with the new data
347
+ this.render.renderOptions(this.store.partialToFullData(data))
460
348
  })
461
- }
349
+ .catch((err: Error | string) => {
350
+ // Update the render with error
351
+ this.render.renderError(typeof err === 'string' ? err : err.message)
352
+ })
353
+
354
+ return
355
+ } else if (Array.isArray(searchResp)) {
356
+ // Update the render options
357
+ this.render.renderOptions(this.store.partialToFullData(searchResp))
462
358
  } else {
463
- this.data.search(value)
464
- this.render()
359
+ // Update the render with error
360
+ this.render.renderError('Search event must return a promise or an array of data')
465
361
  }
466
362
  }
467
363
 
468
- public setSearchText(text: string): void {
469
- this.config.searchText = text
470
- }
471
-
472
- public render(): void {
473
- if (this.config.isMultiple) {
474
- this.slim.values()
475
- } else {
476
- this.slim.placeholder()
477
- this.slim.deselect()
364
+ public destroy(): void {
365
+ // Remove all event listeners
366
+ document.removeEventListener('click', this.documentClick)
367
+ window.removeEventListener('resize', this.windowResize, false)
368
+ if (this.settings.openPosition === 'auto') {
369
+ window.removeEventListener('scroll', this.windowScroll, false)
478
370
  }
479
- this.slim.options()
480
- }
481
371
 
482
- // Display original select again and remove slim
483
- public destroy(id: string | null = null): void {
484
- const slim = (id ? document.querySelector('.' + id + '.ss-main') : this.slim.container)
485
- const select = (id ? document.querySelector(`[data-ssid=${id}]`) as HTMLSelectElement : this.select.element)
486
- // If there is no slim dont do anything
487
- if (!slim || !select) { return }
372
+ // Delete the store data
373
+ this.store.setData([])
488
374
 
375
+ // Remove the render
376
+ this.render.destroy()
489
377
 
490
- document.removeEventListener('click', this.documentClick)
378
+ // Show the original select element
379
+ this.select.destroy()
380
+ }
491
381
 
492
- if (this.config.showContent === 'auto') {
493
- window.removeEventListener('scroll', this.windowScroll, false)
382
+ private windowResize: (e: Event) => void = debounce(() => {
383
+ if (!this.settings.isOpen) {
384
+ return
494
385
  }
495
386
 
496
- // Show original select
497
- select.style.display = ''
498
- delete select.dataset.ssid
387
+ this.render.moveContent()
388
+ })
499
389
 
500
- // Remove slim from original select dropdown
501
- const el = select as any
502
- el.slim = null
390
+ // Event listener for window scrolling
391
+ private windowScroll: (e: Event) => void = debounce(() => {
392
+ // If the content is not open, there is no need to move it
393
+ if (!this.settings.isOpen) {
394
+ return
395
+ }
503
396
 
504
- // Remove slim select
505
- if (slim.parentElement) {
506
- slim.parentElement.removeChild(slim)
397
+ // If openContent is not auto set content
398
+ if (this.settings.openPosition === 'down') {
399
+ this.render.moveContentBelow()
400
+ return
401
+ } else if (this.settings.openPosition === 'up') {
402
+ this.render.moveContentAbove()
403
+ return
507
404
  }
508
405
 
509
- // remove the content if it was added to the document body
510
- if (this.config.addToBody) {
511
- const slimContent = (id ? document.querySelector('.' + id + '.ss-content') : this.slim.content)
512
- if (!slimContent) { return }
513
- document.body.removeChild(slimContent)
406
+ // Determine where to put the content
407
+ if (this.settings.contentPosition === 'relative') {
408
+ this.render.moveContentBelow()
409
+ } else if (this.render.putContent(this.render.content.main, this.settings.isOpen) === 'up') {
410
+ this.render.moveContentAbove()
411
+ } else {
412
+ this.render.moveContentBelow()
514
413
  }
515
- }
414
+ })
516
415
 
416
+ // Event listener for document click
517
417
  private documentClick: (e: Event) => void = (e: Event) => {
518
- if (e.target && !hasClassInTree(e.target as HTMLElement, this.config.id)) {
418
+ // If the content is not open, there is no need to close it
419
+ if (!this.settings.isOpen) {
420
+ return
421
+ }
422
+
423
+ // Check if the click was on the content by looking at the parents
424
+ if (e.target && !hasClassInTree(e.target as HTMLElement, this.settings.id)) {
519
425
  this.close()
520
426
  }
521
427
  }
522
-
523
428
  }