slim-select 1.27.1 → 2.0.1

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 (157) hide show
  1. package/.github/ISSUE_TEMPLATE/issue-report.md +3 -3
  2. package/.nvmrc +1 -0
  3. package/.prettierrc +6 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/.vscode/settings.json +15 -5
  6. package/README.md +17 -19
  7. package/dist/helper.d.ts +1 -4
  8. package/dist/index.d.ts +38 -58
  9. package/dist/render.d.ts +109 -0
  10. package/dist/select.d.ts +31 -20
  11. package/dist/settings.d.ts +33 -0
  12. package/dist/slimselect.cjs.js +2 -0
  13. package/dist/slimselect.cjs.js.map +1 -0
  14. package/dist/slimselect.css +1 -2
  15. package/dist/slimselect.css.map +1 -0
  16. package/dist/slimselect.es.js +2 -0
  17. package/dist/slimselect.es.js.map +1 -0
  18. package/dist/slimselect.global.js +2 -0
  19. package/dist/slimselect.global.js.map +1 -0
  20. package/dist/slimselect.js +2 -1970
  21. package/dist/slimselect.js.map +1 -0
  22. package/dist/slimselect.umd.js +2 -0
  23. package/dist/slimselect.umd.js.map +1 -0
  24. package/dist/store.d.ts +71 -0
  25. package/docs/assets/check-mark.svg +6 -0
  26. package/docs/assets/data.js +59 -0
  27. package/docs/assets/home.css +1 -0
  28. package/docs/assets/home.js +1 -0
  29. package/docs/assets/index.css +1 -0
  30. package/docs/assets/index.js +16 -0
  31. package/docs/assets/index2.js +274 -0
  32. package/docs/assets/index3.js +126 -0
  33. package/docs/assets/index4.js +60 -0
  34. package/docs/assets/install.js +34 -0
  35. package/docs/assets/selects.js +55 -0
  36. package/docs/index.html +18 -5
  37. package/docs.go +3 -1
  38. package/index.html +16 -0
  39. package/jest.config.js +7 -0
  40. package/package.json +39 -35
  41. package/src/docs/app.vue +196 -73
  42. package/src/docs/assets/scss/_variables.scss +26 -10
  43. package/src/docs/assets/scss/alert.scss +12 -0
  44. package/src/docs/assets/scss/animations.scss +5 -3
  45. package/src/docs/assets/scss/button.scss +21 -0
  46. package/src/docs/assets/scss/index.scss +8 -6
  47. package/src/docs/assets/scss/input.scss +2 -2
  48. package/src/docs/assets/scss/layout.scss +195 -65
  49. package/src/docs/assets/scss/misc.scss +144 -34
  50. package/src/docs/assets/scss/prism.scss +155 -153
  51. package/src/docs/env.d.ts +12 -0
  52. package/src/docs/images/check-mark.svg +6 -0
  53. package/src/docs/images/chrome.svg +22 -0
  54. package/src/docs/images/firefox.svg +58 -0
  55. package/src/docs/images/github.png +0 -0
  56. package/src/docs/images/ie.svg +11 -0
  57. package/src/docs/images/npm.png +0 -0
  58. package/src/docs/images/opera.svg +15 -0
  59. package/src/docs/images/safari.svg +22 -0
  60. package/src/docs/main.ts +27 -0
  61. package/src/docs/pages/data.vue +130 -0
  62. package/src/docs/pages/events/addable.vue +79 -0
  63. package/src/docs/pages/events/before_after_change.vue +140 -0
  64. package/src/docs/pages/events/before_after_open_close.vue +122 -0
  65. package/src/docs/pages/events/error.vue +59 -0
  66. package/src/docs/pages/events/index.vue +33 -0
  67. package/src/docs/pages/events/search.vue +154 -0
  68. package/src/docs/pages/events/search_filter.vue +45 -0
  69. package/src/docs/pages/home.vue +131 -117
  70. package/src/docs/pages/install.vue +51 -74
  71. package/src/docs/pages/methods/destroy.vue +66 -0
  72. package/src/docs/pages/methods/enable_disable.vue +70 -0
  73. package/src/docs/pages/methods/get_data.vue +81 -0
  74. package/src/docs/pages/methods/get_selected.vue +78 -0
  75. package/src/docs/pages/methods/index.vue +36 -0
  76. package/src/docs/pages/methods/open_close.vue +63 -0
  77. package/src/docs/pages/methods/search.vue +62 -0
  78. package/src/docs/pages/methods/set_data.vue +106 -0
  79. package/src/docs/pages/methods/set_selected.vue +70 -0
  80. package/src/docs/pages/selects.vue +108 -114
  81. package/src/docs/pages/settings/always_open.vue +88 -0
  82. package/src/docs/pages/settings/close_on_select.vue +69 -0
  83. package/src/docs/pages/settings/content_location.vue +64 -0
  84. package/src/docs/pages/settings/content_position.vue +86 -0
  85. package/src/docs/pages/settings/css.vue +73 -0
  86. package/src/docs/pages/settings/data_attributes.vue +39 -0
  87. package/src/docs/pages/settings/deselect.vue +69 -0
  88. package/src/docs/pages/settings/display.vue +88 -0
  89. package/src/docs/pages/settings/hide_selected.vue +73 -0
  90. package/src/docs/pages/settings/html.vue +86 -0
  91. package/src/docs/pages/settings/index.vue +73 -0
  92. package/src/docs/pages/settings/mandatory.vue +91 -0
  93. package/src/docs/pages/settings/min_max.vue +61 -0
  94. package/src/docs/pages/settings/open_position.vue +60 -0
  95. package/src/docs/pages/settings/placeholder.vue +72 -0
  96. package/src/docs/pages/settings/search.vue +140 -0
  97. package/src/docs/pages/settings/select.vue +32 -0
  98. package/src/docs/pages/settings/select_by_group.vue +46 -0
  99. package/src/docs/pages/settings/show_tooltip.vue +44 -0
  100. package/src/docs/pages/settings/styles.vue +51 -0
  101. package/src/docs/router.ts +41 -13
  102. package/src/slim-select/helper.ts +25 -86
  103. package/src/slim-select/index.ts +317 -420
  104. package/src/slim-select/render.test.ts +39 -0
  105. package/src/slim-select/render.ts +1129 -0
  106. package/src/slim-select/rollup.config.mjs +40 -0
  107. package/src/slim-select/select.test.ts +155 -0
  108. package/src/slim-select/select.ts +280 -108
  109. package/src/slim-select/settings.ts +68 -0
  110. package/src/slim-select/slimselect.scss +263 -278
  111. package/src/slim-select/store.test.ts +220 -0
  112. package/src/slim-select/store.ts +320 -0
  113. package/src/slim-select/tsconfig.json +11 -5
  114. package/src/vue/slimselect.vue +61 -0
  115. package/tsconfig.json +10 -29
  116. package/vite.config.ts +18 -0
  117. package/.browserslistrc +0 -4
  118. package/.postcssrc.js +0 -5
  119. package/.travis.yml +0 -22
  120. package/babel.config.js +0 -5
  121. package/dist/config.d.ts +0 -83
  122. package/dist/data.d.ts +0 -50
  123. package/dist/slim.d.ts +0 -50
  124. package/dist/slimselect.min.css +0 -1
  125. package/dist/slimselect.min.js +0 -1
  126. package/dist/slimselect.min.mjs +0 -1
  127. package/docs/css/app.d5840d21.css +0 -1
  128. package/docs/css/chunk-vendors.fa8199af.css +0 -1
  129. package/docs/css/home.57020178.css +0 -1
  130. package/docs/css/install.baa5d3fb.css +0 -1
  131. package/docs/css/methods.728c742a.css +0 -1
  132. package/docs/css/options.b28a32ef.css +0 -1
  133. package/docs/css/selects.1a7e1e6a.css +0 -1
  134. package/docs/js/app.238ee382.js +0 -2
  135. package/docs/js/app.238ee382.js.map +0 -1
  136. package/docs/js/chunk-vendors.5125e3d8.js +0 -21
  137. package/docs/js/chunk-vendors.5125e3d8.js.map +0 -1
  138. package/docs/js/home.e7b28581.js +0 -2
  139. package/docs/js/home.e7b28581.js.map +0 -1
  140. package/docs/js/install.d4300783.js +0 -2
  141. package/docs/js/install.d4300783.js.map +0 -1
  142. package/docs/js/methods.fa1608f1.js +0 -11
  143. package/docs/js/methods.fa1608f1.js.map +0 -1
  144. package/docs/js/options.2c4ceb87.js +0 -2
  145. package/docs/js/options.2c4ceb87.js.map +0 -1
  146. package/docs/js/selects.1eeba693.js +0 -2
  147. package/docs/js/selects.1eeba693.js.map +0 -1
  148. package/src/docs/pages/methods.vue +0 -427
  149. package/src/docs/pages/options.vue +0 -1480
  150. package/src/main.ts +0 -29
  151. package/src/shims-vue.d.ts +0 -8
  152. package/src/slim-select/config.ts +0 -118
  153. package/src/slim-select/data.ts +0 -377
  154. package/src/slim-select/slim.ts +0 -775
  155. package/src/slim-select/webpack.config.js +0 -73
  156. package/tslint.json +0 -26
  157. package/vue.config.js +0 -23
@@ -1,531 +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)
175
- }
176
- this.select.setValue()
177
- this.data.onDataChange() // Trigger on change callback
178
- this.render()
187
+ public getData(): DataArray {
188
+ return this.store.getData()
189
+ }
179
190
 
180
- // Close when all options are selected and hidden
181
- if (this.config.hideSelectedOption && this.config.isMultiple && (this.data.getSelected() as Option[]).length === this.data.data.length) {
182
- close = true
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
183
199
  }
184
200
 
185
- 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)
186
211
  }
187
212
 
188
- // setSelected is just mapped to the set method
189
- public setSelected(value: string | string[], type: string = 'value', close: boolean = true, render: boolean = true) {
190
- this.set(value, type, close, render)
213
+ public getSelected(): string[] {
214
+ return this.store.getSelected()
191
215
  }
192
216
 
193
- public setData(data: dataArray) {
194
- // Validate data if passed in
195
- const isValid = validateData(data)
196
- 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()
197
221
 
198
- const newData = JSON.parse(JSON.stringify(data))
199
- const selected = this.data.getSelected()
222
+ // Update the select element
223
+ this.select.updateOptions(data)
200
224
 
201
- // Check newData to make sure value is set
202
- // If not set from text
203
- for (let i = 0; i < newData.length; i++) {
204
- if (!newData[i].value && !newData[i].placeholder) {
205
- newData[i].value = newData[i].text
206
- }
207
- }
225
+ // Update the render
226
+ this.render.renderValues()
227
+ this.render.renderOptions(data)
228
+ }
208
229
 
209
- // If its an ajax type keep selected values
210
- if (this.config.isAjax && selected) {
211
- if (this.config.isMultiple) {
212
- const reverseSelected = (selected as Option[]).reverse()
213
- for (const r of reverseSelected) {
214
- newData.unshift(r)
215
- }
216
- } else {
217
- newData.unshift(selected)
218
-
219
- // Look for duplicate selected if so remove it
220
- for (let i = 0; i < newData.length; i++) {
221
- if (!newData[i].placeholder && newData[i].value === (selected as Option).value && newData[i].text === (selected as Option).text) {
222
- newData.splice(i, 1)
223
- }
224
- }
225
-
226
- // Add placeholder if it doesnt already have one
227
- let hasPlaceholder = false
228
- for (let i = 0; i < newData.length; i++) {
229
- if (newData[i].placeholder) {
230
- hasPlaceholder = true
231
- }
232
- }
233
- if (!hasPlaceholder) {
234
- newData.unshift({ text: '', placeholder: true })
235
- }
236
- }
237
- }
230
+ public addOption(option: OptionOptional): void {
231
+ // Add option to store
232
+ this.store.addOption(option)
233
+ const data = this.store.getData()
238
234
 
239
- this.select.create(newData)
240
- this.data.parseSelectData()
241
- this.data.setSelectedFromSelect()
242
- }
235
+ // Update the select element
236
+ this.select.updateOptions(data)
243
237
 
244
- // addData will append to the current data set
245
- public addData(data: Option) {
246
- // Validate data if passed in
247
- const isValid = validateData([data])
248
- 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
249
-
250
- this.data.add(this.data.newOption(data))
251
- this.select.create(this.data.data)
252
- this.data.parseSelectData()
253
- this.data.setSelectedFromSelect()
254
- this.render()
238
+ // Update the render
239
+ this.render.renderValues()
240
+ this.render.renderOptions(data)
255
241
  }
256
242
 
257
- // Open content section
258
243
  public open(): void {
259
244
  // Dont open if disabled
260
- if (!this.config.isEnabled) { return }
261
-
262
245
  // Dont do anything if the content is already open
263
- if (this.data.contentOpen) { return }
264
-
265
- // Dont open when all options are selected and hidden
266
- if (this.config.hideSelectedOption && this.config.isMultiple && (this.data.getSelected() as Option[]).length === this.data.data.length) { return }
246
+ if (!this.settings.isEnabled || this.settings.isOpen) {
247
+ return
248
+ }
267
249
 
268
250
  // Run beforeOpen callback
269
- if (this.beforeOpen) { this.beforeOpen() }
270
-
271
- if (this.config.isMultiple && this.slim.multiSelected) {
272
- this.slim.multiSelected.plus.classList.add('ss-cross')
273
- } else if (this.slim.singleSelected) {
274
- this.slim.singleSelected.arrowIcon.arrow.classList.remove('arrow-down')
275
- this.slim.singleSelected.arrowIcon.arrow.classList.add('arrow-up')
251
+ if (this.events.beforeOpen) {
252
+ this.events.beforeOpen()
276
253
  }
277
- (this.slim as any)[(this.config.isMultiple ? 'multiSelected' : 'singleSelected')].container.classList.add((this.data.contentPosition === 'above' ? this.config.openAbove : this.config.openBelow))
278
-
279
- if (this.config.addToBody) {
280
- // move the content in to the right location
281
- const containerRect = this.slim.container.getBoundingClientRect()
282
- this.slim.content.style.top = (containerRect.top + containerRect.height + window.scrollY) + 'px'
283
- this.slim.content.style.left = (containerRect.left + window.scrollX) + 'px'
284
- this.slim.content.style.width = containerRect.width + 'px'
285
- }
286
- this.slim.content.classList.add(this.config.open)
287
254
 
288
- // Check showContent to see if they want to specifically show in a certain direction
289
- if (this.config.showContent.toLowerCase() === 'up') {
290
- this.moveContentAbove()
291
- } else if (this.config.showContent.toLowerCase() === 'down') {
292
- this.moveContentBelow()
293
- } else {
294
- // Auto identify where to put it
295
- if (putContent(this.slim.content, this.data.contentPosition, this.data.contentOpen) === 'above') {
296
- this.moveContentAbove()
297
- } else {
298
- this.moveContentBelow()
299
- }
300
- }
255
+ // Tell render to open
256
+ this.render.open()
301
257
 
302
- // Move to selected option for single option
303
- if (!this.config.isMultiple) {
304
- const selected = this.data.getSelected() as Option
305
- if (selected) {
306
- const selectedId = selected.id
307
- const selectedOption = this.slim.list.querySelector('[data-id="' + selectedId + '"]') as HTMLElement
308
- if (selectedOption) {
309
- ensureElementInView(this.slim.list, selectedOption)
310
- }
311
- }
258
+ // Focus on input field only if search is enabled
259
+ if (this.settings.showSearch) {
260
+ this.render.searchFocus(false)
312
261
  }
313
262
 
314
263
  // setTimeout is for animation completion
315
264
  setTimeout(() => {
316
- this.data.contentOpen = true
317
-
318
- // Focus on input field
319
- if (this.config.searchFocus) {
320
- this.slim.search.input.focus()
321
- }
322
-
323
265
  // Run afterOpen callback
324
- if (this.afterOpen) {
325
- this.afterOpen()
266
+ if (this.events.afterOpen) {
267
+ this.events.afterOpen()
326
268
  }
327
- }, 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)
328
280
  }
329
281
 
330
- // Close content section
331
282
  public close(): void {
332
283
  // Dont do anything if the content is already closed
333
- if (!this.data.contentOpen) { return }
284
+ // Dont do anything if alwaysOpen is true
285
+ if (!this.settings.isOpen || this.settings.alwaysOpen) {
286
+ return
287
+ }
334
288
 
335
289
  // Run beforeClose calback
336
- if (this.beforeClose) { this.beforeClose() }
337
-
338
- // this.slim.search.input.blur() // Removed due to safari quirk
339
- if (this.config.isMultiple && this.slim.multiSelected) {
340
- this.slim.multiSelected.container.classList.remove(this.config.openAbove)
341
- this.slim.multiSelected.container.classList.remove(this.config.openBelow)
342
- this.slim.multiSelected.plus.classList.remove('ss-cross')
343
- } else if (this.slim.singleSelected) {
344
- this.slim.singleSelected.container.classList.remove(this.config.openAbove)
345
- this.slim.singleSelected.container.classList.remove(this.config.openBelow)
346
- this.slim.singleSelected.arrowIcon.arrow.classList.add('arrow-down')
347
- this.slim.singleSelected.arrowIcon.arrow.classList.remove('arrow-up')
290
+ if (this.events.beforeClose) {
291
+ this.events.beforeClose()
348
292
  }
349
- this.slim.content.classList.remove(this.config.open)
350
- this.data.contentOpen = false
351
293
 
294
+ // Tell render to close
295
+ this.render.close()
296
+
297
+ // Clear search
352
298
  this.search('') // Clear search
353
299
 
300
+ // If we arent tabbing focus back on the main element
301
+ this.render.mainFocus(false)
302
+
354
303
  // Reset the content below
355
304
  setTimeout(() => {
356
- this.slim.content.removeAttribute('style')
357
- this.data.contentPosition = 'below'
358
-
359
- if (this.config.isMultiple && this.slim.multiSelected) {
360
- this.slim.multiSelected.container.classList.remove(this.config.openAbove)
361
- this.slim.multiSelected.container.classList.remove(this.config.openBelow)
362
- } else if (this.slim.singleSelected) {
363
- this.slim.singleSelected.container.classList.remove(this.config.openAbove)
364
- this.slim.singleSelected.container.classList.remove(this.config.openBelow)
305
+ // Run afterClose callback
306
+ if (this.events.afterClose) {
307
+ this.events.afterClose()
365
308
  }
366
309
 
367
- // After content is closed lets blur on the input field
368
- this.slim.search.input.blur()
310
+ // Update settings
311
+ this.settings.isOpen = false
312
+ }, this.settings.timeoutDelay)
369
313
 
370
- // Run afterClose callback
371
- if (this.afterClose) { this.afterClose() }
372
- }, this.config.timeoutDelay)
373
- }
374
-
375
- public moveContentAbove(): void {
376
- let selectHeight: number = 0
377
- if (this.config.isMultiple && this.slim.multiSelected) {
378
- selectHeight = this.slim.multiSelected.container.offsetHeight
379
- } else if (this.slim.singleSelected) {
380
- selectHeight = this.slim.singleSelected.container.offsetHeight
381
- }
382
- const contentHeight = this.slim.content.offsetHeight
383
- const height = selectHeight + contentHeight - 1
384
- this.slim.content.style.margin = '-' + height + 'px 0 0 0'
385
- this.slim.content.style.height = (height - selectHeight + 1) + 'px'
386
- this.slim.content.style.transformOrigin = 'center bottom'
387
- this.data.contentPosition = 'above'
388
-
389
- if (this.config.isMultiple && this.slim.multiSelected) {
390
- this.slim.multiSelected.container.classList.remove(this.config.openBelow)
391
- this.slim.multiSelected.container.classList.add(this.config.openAbove)
392
- } else if (this.slim.singleSelected) {
393
- this.slim.singleSelected.container.classList.remove(this.config.openBelow)
394
- this.slim.singleSelected.container.classList.add(this.config.openAbove)
314
+ if (this.settings.intervalMove) {
315
+ clearInterval(this.settings.intervalMove)
395
316
  }
396
317
  }
397
318
 
398
- public moveContentBelow(): void {
399
- this.data.contentPosition = 'below'
400
-
401
- if (this.config.isMultiple && this.slim.multiSelected) {
402
- this.slim.multiSelected.container.classList.remove(this.config.openAbove)
403
- this.slim.multiSelected.container.classList.add(this.config.openBelow)
404
- } else if (this.slim.singleSelected) {
405
- this.slim.singleSelected.container.classList.remove(this.config.openAbove)
406
- 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
407
325
  }
408
- }
409
326
 
410
- // Set to enabled, remove disabled classes and removed disabled from original select
411
- public enable(): void {
412
- this.config.isEnabled = true
413
- if (this.config.isMultiple && this.slim.multiSelected) {
414
- this.slim.multiSelected.container.classList.remove(this.config.disabled)
415
- } else if (this.slim.singleSelected) {
416
- 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
417
334
  }
418
335
 
419
- // Disable original select but dont trigger observer
420
- this.select.triggerMutationObserver = false
421
- this.select.element.disabled = false
422
- this.slim.search.input.disabled = false
423
- this.select.triggerMutationObserver = true
424
- }
336
+ // Search event exists so lets render the searching text
337
+ this.render.renderSearching()
425
338
 
426
- // Set to disabled, add disabled classes and add disabled to original select
427
- public disable(): void {
428
- this.config.isEnabled = false
429
- if (this.config.isMultiple && this.slim.multiSelected) {
430
- this.slim.multiSelected.container.classList.add(this.config.disabled)
431
- } else if (this.slim.singleSelected) {
432
- this.slim.singleSelected.container.classList.add(this.config.disabled)
433
- }
434
-
435
- // Enable original select but dont trigger observer
436
- this.select.triggerMutationObserver = false
437
- this.select.element.disabled = true
438
- this.slim.search.input.disabled = true
439
- this.select.triggerMutationObserver = true
440
- }
339
+ // Based upon the search event deal with the response
340
+ const searchResp = this.events.search(value, this.store.getSelectedOptions())
441
341
 
442
- // Take in string value and search current options
443
- public search(value: string): void {
444
- // Only filter data and rerender if value has changed
445
- if (this.data.searchValue === value) { return }
446
-
447
- this.slim.search.input.value = value
448
- if (this.config.isAjax) {
449
- const master = this
450
- this.config.isSearching = true
451
- this.render()
452
-
453
- // If ajax call it
454
- if (this.ajax) {
455
- this.ajax(value, (info: any) => {
456
- // Only process if return callback is not false
457
- master.config.isSearching = false
458
- if (Array.isArray(info)) {
459
- info.unshift({ text: '', placeholder: true })
460
- master.setData(info)
461
- master.data.search(value)
462
- master.render()
463
- } else if (typeof info === 'string') {
464
- master.slim.options(info)
465
- } else {
466
- master.render()
467
- }
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))
468
348
  })
469
- }
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))
470
358
  } else {
471
- this.data.search(value)
472
- this.render()
359
+ // Update the render with error
360
+ this.render.renderError('Search event must return a promise or an array of data')
473
361
  }
474
362
  }
475
363
 
476
- public setSearchText(text: string): void {
477
- this.config.searchText = text
478
- }
479
-
480
- public render(): void {
481
- if (this.config.isMultiple) {
482
- this.slim.values()
483
- } else {
484
- this.slim.placeholder()
485
- 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)
486
370
  }
487
- this.slim.options()
488
- }
489
371
 
490
- // Display original select again and remove slim
491
- public destroy(id: string | null = null): void {
492
- const slim = (id ? document.querySelector('.' + id + '.ss-main') : this.slim.container)
493
- const select = (id ? document.querySelector(`[data-ssid=${id}]`) as HTMLSelectElement : this.select.element)
494
- // If there is no slim dont do anything
495
- if (!slim || !select) { return }
372
+ // Delete the store data
373
+ this.store.setData([])
496
374
 
375
+ // Remove the render
376
+ this.render.destroy()
497
377
 
498
- document.removeEventListener('click', this.documentClick)
378
+ // Show the original select element
379
+ this.select.destroy()
380
+ }
499
381
 
500
- if (this.config.showContent === 'auto') {
501
- window.removeEventListener('scroll', this.windowScroll, false)
382
+ private windowResize: (e: Event) => void = debounce(() => {
383
+ if (!this.settings.isOpen) {
384
+ return
502
385
  }
503
386
 
504
- // Show original select
505
- select.style.display = ''
506
- delete select.dataset.ssid
387
+ this.render.moveContent()
388
+ })
507
389
 
508
- // Remove slim from original select dropdown
509
- const el = select as any
510
- 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
+ }
511
396
 
512
- // Remove slim select
513
- if (slim.parentElement) {
514
- 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
515
404
  }
516
405
 
517
- // remove the content if it was added to the document body
518
- if (this.config.addToBody) {
519
- const slimContent = (id ? document.querySelector('.' + id + '.ss-content') : this.slim.content)
520
- if (!slimContent) { return }
521
- 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()
522
413
  }
523
- }
414
+ })
524
415
 
416
+ // Event listener for document click
525
417
  private documentClick: (e: Event) => void = (e: Event) => {
526
- 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)) {
527
425
  this.close()
528
426
  }
529
427
  }
530
-
531
428
  }