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.
- package/.github/ISSUE_TEMPLATE/issue-report.md +3 -3
- package/.nvmrc +1 -0
- package/.prettierrc +6 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/settings.json +15 -5
- package/README.md +17 -19
- package/dist/helper.d.ts +1 -4
- package/dist/index.d.ts +38 -58
- package/dist/render.d.ts +109 -0
- package/dist/select.d.ts +31 -20
- package/dist/settings.d.ts +33 -0
- package/dist/slimselect.cjs.js +2 -0
- package/dist/slimselect.cjs.js.map +1 -0
- package/dist/slimselect.css +1 -2
- package/dist/slimselect.css.map +1 -0
- package/dist/slimselect.es.js +2 -0
- package/dist/slimselect.es.js.map +1 -0
- package/dist/slimselect.global.js +2 -0
- package/dist/slimselect.global.js.map +1 -0
- package/dist/slimselect.js +2 -1970
- package/dist/slimselect.js.map +1 -0
- package/dist/slimselect.umd.js +2 -0
- package/dist/slimselect.umd.js.map +1 -0
- package/dist/store.d.ts +71 -0
- package/docs/assets/check-mark.svg +6 -0
- package/docs/assets/data.js +59 -0
- package/docs/assets/home.css +1 -0
- package/docs/assets/home.js +1 -0
- package/docs/assets/index.css +1 -0
- package/docs/assets/index.js +16 -0
- package/docs/assets/index2.js +274 -0
- package/docs/assets/index3.js +126 -0
- package/docs/assets/index4.js +60 -0
- package/docs/assets/install.js +34 -0
- package/docs/assets/selects.js +55 -0
- package/docs/index.html +18 -5
- package/docs.go +3 -1
- package/index.html +16 -0
- package/jest.config.js +7 -0
- package/package.json +39 -35
- package/src/docs/app.vue +196 -73
- package/src/docs/assets/scss/_variables.scss +26 -10
- package/src/docs/assets/scss/alert.scss +12 -0
- package/src/docs/assets/scss/animations.scss +5 -3
- package/src/docs/assets/scss/button.scss +21 -0
- package/src/docs/assets/scss/index.scss +8 -6
- package/src/docs/assets/scss/input.scss +2 -2
- package/src/docs/assets/scss/layout.scss +195 -65
- package/src/docs/assets/scss/misc.scss +144 -34
- package/src/docs/assets/scss/prism.scss +155 -153
- package/src/docs/env.d.ts +12 -0
- package/src/docs/images/check-mark.svg +6 -0
- package/src/docs/images/chrome.svg +22 -0
- package/src/docs/images/firefox.svg +58 -0
- package/src/docs/images/github.png +0 -0
- package/src/docs/images/ie.svg +11 -0
- package/src/docs/images/npm.png +0 -0
- package/src/docs/images/opera.svg +15 -0
- package/src/docs/images/safari.svg +22 -0
- package/src/docs/main.ts +27 -0
- package/src/docs/pages/data.vue +130 -0
- package/src/docs/pages/events/addable.vue +79 -0
- package/src/docs/pages/events/before_after_change.vue +140 -0
- package/src/docs/pages/events/before_after_open_close.vue +122 -0
- package/src/docs/pages/events/error.vue +59 -0
- package/src/docs/pages/events/index.vue +33 -0
- package/src/docs/pages/events/search.vue +154 -0
- package/src/docs/pages/events/search_filter.vue +45 -0
- package/src/docs/pages/home.vue +131 -117
- package/src/docs/pages/install.vue +51 -74
- package/src/docs/pages/methods/destroy.vue +66 -0
- package/src/docs/pages/methods/enable_disable.vue +70 -0
- package/src/docs/pages/methods/get_data.vue +81 -0
- package/src/docs/pages/methods/get_selected.vue +78 -0
- package/src/docs/pages/methods/index.vue +36 -0
- package/src/docs/pages/methods/open_close.vue +63 -0
- package/src/docs/pages/methods/search.vue +62 -0
- package/src/docs/pages/methods/set_data.vue +106 -0
- package/src/docs/pages/methods/set_selected.vue +70 -0
- package/src/docs/pages/selects.vue +108 -114
- package/src/docs/pages/settings/always_open.vue +88 -0
- package/src/docs/pages/settings/close_on_select.vue +69 -0
- package/src/docs/pages/settings/content_location.vue +64 -0
- package/src/docs/pages/settings/content_position.vue +86 -0
- package/src/docs/pages/settings/css.vue +73 -0
- package/src/docs/pages/settings/data_attributes.vue +39 -0
- package/src/docs/pages/settings/deselect.vue +69 -0
- package/src/docs/pages/settings/display.vue +88 -0
- package/src/docs/pages/settings/hide_selected.vue +73 -0
- package/src/docs/pages/settings/html.vue +86 -0
- package/src/docs/pages/settings/index.vue +73 -0
- package/src/docs/pages/settings/mandatory.vue +91 -0
- package/src/docs/pages/settings/min_max.vue +61 -0
- package/src/docs/pages/settings/open_position.vue +60 -0
- package/src/docs/pages/settings/placeholder.vue +72 -0
- package/src/docs/pages/settings/search.vue +140 -0
- package/src/docs/pages/settings/select.vue +32 -0
- package/src/docs/pages/settings/select_by_group.vue +46 -0
- package/src/docs/pages/settings/show_tooltip.vue +44 -0
- package/src/docs/pages/settings/styles.vue +51 -0
- package/src/docs/router.ts +41 -13
- package/src/slim-select/helper.ts +25 -86
- package/src/slim-select/index.ts +317 -420
- package/src/slim-select/render.test.ts +39 -0
- package/src/slim-select/render.ts +1129 -0
- package/src/slim-select/rollup.config.mjs +40 -0
- package/src/slim-select/select.test.ts +155 -0
- package/src/slim-select/select.ts +280 -108
- package/src/slim-select/settings.ts +68 -0
- package/src/slim-select/slimselect.scss +263 -278
- package/src/slim-select/store.test.ts +220 -0
- package/src/slim-select/store.ts +320 -0
- package/src/slim-select/tsconfig.json +11 -5
- package/src/vue/slimselect.vue +61 -0
- package/tsconfig.json +10 -29
- package/vite.config.ts +18 -0
- package/.browserslistrc +0 -4
- package/.postcssrc.js +0 -5
- package/.travis.yml +0 -22
- package/babel.config.js +0 -5
- package/dist/config.d.ts +0 -83
- package/dist/data.d.ts +0 -50
- package/dist/slim.d.ts +0 -50
- package/dist/slimselect.min.css +0 -1
- package/dist/slimselect.min.js +0 -1
- package/dist/slimselect.min.mjs +0 -1
- package/docs/css/app.d5840d21.css +0 -1
- package/docs/css/chunk-vendors.fa8199af.css +0 -1
- package/docs/css/home.57020178.css +0 -1
- package/docs/css/install.baa5d3fb.css +0 -1
- package/docs/css/methods.728c742a.css +0 -1
- package/docs/css/options.b28a32ef.css +0 -1
- package/docs/css/selects.1a7e1e6a.css +0 -1
- package/docs/js/app.238ee382.js +0 -2
- package/docs/js/app.238ee382.js.map +0 -1
- package/docs/js/chunk-vendors.5125e3d8.js +0 -21
- package/docs/js/chunk-vendors.5125e3d8.js.map +0 -1
- package/docs/js/home.e7b28581.js +0 -2
- package/docs/js/home.e7b28581.js.map +0 -1
- package/docs/js/install.d4300783.js +0 -2
- package/docs/js/install.d4300783.js.map +0 -1
- package/docs/js/methods.fa1608f1.js +0 -11
- package/docs/js/methods.fa1608f1.js.map +0 -1
- package/docs/js/options.2c4ceb87.js +0 -2
- package/docs/js/options.2c4ceb87.js.map +0 -1
- package/docs/js/selects.1eeba693.js +0 -2
- package/docs/js/selects.1eeba693.js.map +0 -1
- package/src/docs/pages/methods.vue +0 -427
- package/src/docs/pages/options.vue +0 -1480
- package/src/main.ts +0 -29
- package/src/shims-vue.d.ts +0 -8
- package/src/slim-select/config.ts +0 -118
- package/src/slim-select/data.ts +0 -377
- package/src/slim-select/slim.ts +0 -775
- package/src/slim-select/webpack.config.js +0 -73
- package/tslint.json +0 -26
- 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
|
+
}
|