slim-select 2.8.2 → 2.9.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 (69) hide show
  1. package/.prettierignore +1 -0
  2. package/.prettierrc +4 -3
  3. package/README.md +2 -0
  4. package/dist/classes.d.ts +43 -0
  5. package/dist/helpers.d.ts +1 -1
  6. package/dist/index.d.ts +5 -2
  7. package/dist/render.d.ts +4 -44
  8. package/dist/select.d.ts +4 -2
  9. package/dist/settings.d.ts +2 -1
  10. package/dist/slimselect.cjs.js +248 -120
  11. package/dist/slimselect.css +1 -1
  12. package/dist/slimselect.css.map +1 -1
  13. package/dist/slimselect.es.js +248 -120
  14. package/dist/slimselect.global.js +248 -120
  15. package/dist/slimselect.js +248 -120
  16. package/dist/slimselect.min.js +1 -1
  17. package/dist/slimselect.umd.js +248 -120
  18. package/dist/slimselect.umd.min.js +1 -1
  19. package/dist/store.d.ts +4 -2
  20. package/index.html +1 -4
  21. package/jest.config.js +6 -0
  22. package/package.json +30 -39
  23. package/src/slim-select/classes.test.ts +72 -0
  24. package/src/slim-select/classes.ts +109 -0
  25. package/src/slim-select/helpers.test.ts +160 -0
  26. package/src/slim-select/helpers.ts +3 -12
  27. package/src/slim-select/index.test.ts +109 -16
  28. package/src/slim-select/index.ts +41 -12
  29. package/src/slim-select/render.test.ts +1169 -18
  30. package/src/slim-select/render.ts +75 -96
  31. package/src/slim-select/rollup.config.mjs +28 -28
  32. package/src/slim-select/select.test.ts +306 -156
  33. package/src/slim-select/select.ts +69 -20
  34. package/src/slim-select/settings.test.ts +257 -0
  35. package/src/slim-select/settings.ts +4 -0
  36. package/src/slim-select/slimselect.scss +7 -3
  37. package/src/slim-select/store.test.ts +986 -235
  38. package/src/slim-select/store.ts +70 -28
  39. package/src/{vue/slimselect.vue → slim-select/vue.vue} +19 -17
  40. package/tsconfig.json +2 -3
  41. package/src/react/.eslintignore +0 -2
  42. package/src/react/.eslintrc.cjs +0 -24
  43. package/src/react/index.html +0 -14
  44. package/src/react/jestconfig.json +0 -11
  45. package/src/react/package.json +0 -32
  46. package/src/react/src/frameworkpage.tsx +0 -133
  47. package/src/react/src/index.ts +0 -3
  48. package/src/react/src/slimselect.tsx +0 -148
  49. package/src/react/tests/slimselect.test.tsx +0 -52
  50. package/src/react/tsconfig.json +0 -37
  51. package/src/react/tsconfig.node.json +0 -10
  52. package/src/react/vite.config.ts +0 -55
  53. package/src/vue/README.md +0 -48
  54. package/src/vue/dist/slim-select/helpers.d.ts +0 -5
  55. package/src/vue/dist/slim-select/index.d.ts +0 -46
  56. package/src/vue/dist/slim-select/render.d.ts +0 -120
  57. package/src/vue/dist/slim-select/select.d.ts +0 -28
  58. package/src/vue/dist/slim-select/settings.d.ts +0 -34
  59. package/src/vue/dist/slim-select/store.d.ts +0 -80
  60. package/src/vue/dist/slimselectvue.es.js +0 -1978
  61. package/src/vue/dist/slimselectvue.global.js +0 -1981
  62. package/src/vue/dist/slimselectvue.ssr.js +0 -1980
  63. package/src/vue/dist/vue/hello.d.ts +0 -2
  64. package/src/vue/dist/vue/slimselect.vue.d.ts +0 -326
  65. package/src/vue/hello.ts +0 -5
  66. package/src/vue/package.json +0 -28
  67. package/src/vue/rollup.config.mjs +0 -44
  68. package/src/vue/tsconfig.json +0 -14
  69. /package/{vite.config.ts → vite.config.mts} +0 -0
@@ -5,7 +5,7 @@ export default class Select {
5
5
  public select: HTMLSelectElement
6
6
 
7
7
  // Mutation observer fields
8
- public onValueChange?: (value: string[]) => void
8
+ public onValueChange?: (value: Option[]) => void
9
9
  public onClassChange?: (classes: string[]) => void
10
10
  public onDisabledChange?: (disabled: boolean) => void
11
11
  public onOptionsChange?: (data: DataArrayPartial) => void
@@ -21,7 +21,7 @@ export default class Select {
21
21
  // Add change event listener
22
22
  this.select.addEventListener('change', this.valueChange, {
23
23
  // allow bubbling of event
24
- passive: true,
24
+ passive: true
25
25
  })
26
26
 
27
27
  // Initiate mutation observer
@@ -60,7 +60,7 @@ export default class Select {
60
60
  this.observer.observe(this.select, {
61
61
  subtree: true, // subtree for optgroups options
62
62
  childList: true, // children changes
63
- attributes: true, // attributes changes
63
+ attributes: true // attributes changes
64
64
  })
65
65
  }
66
66
  }
@@ -77,7 +77,7 @@ export default class Select {
77
77
  // and will call the onValueChange function if it exists
78
78
  public valueChange(ev: Event): boolean {
79
79
  if (this.listen && this.onValueChange) {
80
- this.onValueChange(this.getSelectedValues())
80
+ this.onValueChange(this.getSelectedOptions())
81
81
  }
82
82
 
83
83
  // Allow bubbling back to other change event listeners
@@ -85,7 +85,7 @@ export default class Select {
85
85
  }
86
86
 
87
87
  private observeCall(mutations: MutationRecord[]): void {
88
- // If we are not listeing do nothing
88
+ // If we are not listening, do nothing.
89
89
  if (!this.listen) {
90
90
  return
91
91
  }
@@ -107,6 +107,19 @@ export default class Select {
107
107
  if (m.attributeName === 'class') {
108
108
  classChanged = true
109
109
  }
110
+
111
+ if (m.type === 'childList') {
112
+ for (const n of m.addedNodes) {
113
+ if (n.nodeName === 'OPTION' && (<HTMLOptionElement>n).value === this.select.value) {
114
+ // we added a new option that's now the select value
115
+ this.select.dispatchEvent(new Event('change'))
116
+ break
117
+ }
118
+ }
119
+
120
+ // options changed, so we need the optionsChange event to fire
121
+ optgroupOptionChanged = true
122
+ }
110
123
  }
111
124
 
112
125
  // Check if its an optgroup or option
@@ -163,7 +176,7 @@ export default class Select {
163
176
  selectAll: optgroup.dataset ? optgroup.dataset.selectall === 'true' : false,
164
177
  selectAllText: optgroup.dataset ? optgroup.dataset.selectalltext : 'Select all',
165
178
  closable: optgroup.dataset ? optgroup.dataset.closable : 'off',
166
- options: [],
179
+ options: []
167
180
  } as OptgroupOptional
168
181
 
169
182
  const options = optgroup.childNodes as any as HTMLOptionElement[]
@@ -184,29 +197,29 @@ export default class Select {
184
197
  text: option.text,
185
198
  html: option.dataset && option.dataset.html ? option.dataset.html : '',
186
199
  selected: option.selected,
187
- display: option.style.display === 'none' ? false : true,
200
+ display: option.style.display !== 'none',
188
201
  disabled: option.disabled,
189
202
  mandatory: option.dataset ? option.dataset.mandatory === 'true' : false,
190
203
  placeholder: option.dataset.placeholder === 'true',
191
204
  class: option.className,
192
205
  style: option.style.cssText,
193
- data: option.dataset,
206
+ data: option.dataset
194
207
  } as Option
195
208
  }
196
209
 
197
- public getSelectedValues(): string[] {
198
- let values = []
210
+ public getSelectedOptions(): Option[] {
211
+ let options = []
199
212
 
200
213
  // Loop through options and set selected
201
- const options = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[]
202
- for (const o of options) {
214
+ const opts = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[]
215
+ for (const o of opts) {
203
216
  if (o.nodeName === 'OPTGROUP') {
204
217
  const optgroupOptions = o.childNodes as any as HTMLOptionElement[]
205
218
  for (const oo of optgroupOptions) {
206
219
  if (oo.nodeName === 'OPTION') {
207
220
  const option = oo as HTMLOptionElement
208
221
  if (option.selected) {
209
- values.push(option.value)
222
+ options.push(this.getDataFromOption(option))
210
223
  }
211
224
  }
212
225
  }
@@ -215,15 +228,51 @@ export default class Select {
215
228
  if (o.nodeName === 'OPTION') {
216
229
  const option = o as HTMLOptionElement
217
230
  if (option.selected) {
218
- values.push(option.value)
231
+ options.push(this.getDataFromOption(option))
232
+ }
233
+ }
234
+ }
235
+
236
+ return options
237
+ }
238
+
239
+ public getSelectedValues(): string[] {
240
+ return this.getSelectedOptions().map((option) => option.value)
241
+ }
242
+
243
+ public setSelected(ids: string[]): void {
244
+ // Stop listening to changes
245
+ this.changeListen(false)
246
+
247
+ // Loop through options and set selected
248
+ const options = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[]
249
+ for (const o of options) {
250
+ if (o.nodeName === 'OPTGROUP') {
251
+ const optgroup = o as HTMLOptGroupElement
252
+ const optgroupOptions = optgroup.childNodes as any as HTMLOptionElement[]
253
+ for (const oo of optgroupOptions) {
254
+ if (oo.nodeName === 'OPTION') {
255
+ const option = oo as HTMLOptionElement
256
+ option.selected = ids.includes(option.id)
257
+ }
219
258
  }
220
259
  }
260
+
261
+ if (o.nodeName === 'OPTION') {
262
+ const option = o as HTMLOptionElement
263
+ option.selected = ids.includes(option.id)
264
+ }
221
265
  }
222
266
 
223
- return values
267
+ // Stop listening to changes
268
+ this.changeListen(true)
224
269
  }
225
270
 
226
- public setSelected(value: string[]): void {
271
+ // Set selected options by value instead of id
272
+ // This is useful when the id is not known
273
+ // and only the value is known
274
+ // but the value is not unique and can be duplicated
275
+ public setSelectedByValue(values: string[]): void {
227
276
  // Stop listening to changes
228
277
  this.changeListen(false)
229
278
 
@@ -236,14 +285,14 @@ export default class Select {
236
285
  for (const oo of optgroupOptions) {
237
286
  if (oo.nodeName === 'OPTION') {
238
287
  const option = oo as HTMLOptionElement
239
- option.selected = value.includes(option.value)
288
+ option.selected = values.includes(option.value)
240
289
  }
241
290
  }
242
291
  }
243
292
 
244
293
  if (o.nodeName === 'OPTION') {
245
294
  const option = o as HTMLOptionElement
246
- option.selected = value.includes(option.value)
295
+ option.selected = values.includes(option.value)
247
296
  }
248
297
  }
249
298
 
@@ -297,7 +346,7 @@ export default class Select {
297
346
  }
298
347
 
299
348
  // Trigger change event on original select
300
- this.select.dispatchEvent(new Event('change'))
349
+ this.select.dispatchEvent(new Event('change', { bubbles: true }))
301
350
 
302
351
  // Start listening to changes
303
352
  this.changeListen(true)
@@ -335,7 +384,7 @@ export default class Select {
335
384
  if (info.disabled) {
336
385
  optionEl.disabled = true
337
386
  }
338
- if (info.display === false) {
387
+ if (!info.display) {
339
388
  optionEl.style.display = 'none'
340
389
  }
341
390
  if (info.placeholder) {
@@ -0,0 +1,257 @@
1
+ 'use strict'
2
+
3
+ import { describe, expect, test } from '@jest/globals'
4
+ import Settings from './settings'
5
+ import Select from './select'
6
+ import { Option, Optgroup } from './store'
7
+
8
+ const defaultSettings: { [key: string]: any } = {
9
+ id: 'ss-qucyuytu',
10
+ style: '',
11
+ class: [],
12
+ isMultiple: false,
13
+ isOpen: false,
14
+ isFullOpen: false,
15
+ intervalMove: null,
16
+ disabled: false,
17
+ alwaysOpen: false,
18
+ showSearch: true,
19
+ focusSearch: true,
20
+ ariaLabel: 'Combobox',
21
+ searchPlaceholder: 'Search',
22
+ searchText: 'No Results',
23
+ searchingText: 'Searching...',
24
+ searchHighlight: false,
25
+ closeOnSelect: true,
26
+ contentLocation: HTMLBodyElement,
27
+ contentPosition: 'absolute',
28
+ openPosition: 'auto',
29
+ placeholderText: 'Select Value',
30
+ allowDeselect: false,
31
+ hideSelected: false,
32
+ keepOrder: false,
33
+ showOptionTooltips: false,
34
+ minSelected: 0,
35
+ maxSelected: 1000,
36
+ timeoutDelay: 200,
37
+ maxValuesShown: 20,
38
+ maxValuesMessage: '{number} selected'
39
+ }
40
+
41
+ describe('Settings module', () => {
42
+ let select: Select
43
+
44
+ beforeEach(() => {
45
+ const selectElement = document.createElement('select')
46
+ select = new Select(selectElement)
47
+ })
48
+
49
+ test('empty constructor returns default settings', () => {
50
+ // Convert to unknown and then to custom object to prevent TS from throwing errors
51
+ const settings = new Settings() as unknown as { [key: string]: string }
52
+ Object.keys(defaultSettings).forEach((key) => {
53
+ if (key === 'id') {
54
+ expect(settings[key].substring(0, 3)).toBe('ss-')
55
+ } else if (key === 'contentLocation') {
56
+ expect(settings[key]).toBeInstanceOf(defaultSettings[key])
57
+ } else {
58
+ expect(settings[key]).toStrictEqual(defaultSettings[key])
59
+ }
60
+ })
61
+ })
62
+
63
+ test('settings can be overwritten via the constructor', () => {
64
+ const customSettings = {
65
+ disabled: true,
66
+ alwaysOpen: true,
67
+ showSearch: false,
68
+ focusSearch: true,
69
+ searchHighlight: true,
70
+ closeOnSelect: false,
71
+ placeholderText: 'new placeholder',
72
+ hideSelected: true,
73
+ keepOrder: true,
74
+ showOptionTooltips: true
75
+ }
76
+
77
+ const settingsWithOverride = {
78
+ ...defaultSettings,
79
+ ...customSettings
80
+ } as unknown as { [key: string]: any }
81
+
82
+ // Convert to unknown and then to custom object to prevent TS from throwing errors
83
+ const settings = new Settings(customSettings) as unknown as { [key: string]: any }
84
+ Object.keys(settingsWithOverride).forEach((key) => {
85
+ if (key === 'id') {
86
+ expect(settings[key].substring(0, 3)).toBe('ss-')
87
+ } else if (key === 'contentLocation') {
88
+ expect(settings[key]).toBeInstanceOf(defaultSettings[key])
89
+ } else {
90
+ expect(settings[key]).toStrictEqual(settingsWithOverride[key])
91
+ }
92
+ })
93
+ })
94
+
95
+ test('enable', () => {
96
+ select.select.disabled = true
97
+
98
+ select.enable()
99
+ expect(select.select.disabled).toBe(false)
100
+ })
101
+
102
+ test('disable', () => {
103
+ select.select.disabled = false
104
+
105
+ select.disable()
106
+ expect(select.select.disabled).toBe(true)
107
+ })
108
+
109
+ test('showUI', () => {
110
+ select.select.setAttribute('tabindex', '1')
111
+ select.select.setAttribute('aria-hidden', 'true')
112
+ select.select.style.display = 'none'
113
+
114
+ select.showUI()
115
+ expect(select.select.getAttribute('tabindex')).toBeFalsy()
116
+ expect(select.select.getAttribute('aria-hidden')).toBeFalsy()
117
+ expect(select.select.style.display).toBeFalsy()
118
+ })
119
+
120
+ describe('createOptgroup', () => {
121
+ test('group gets created correctly', () => {
122
+ const optGroup = new Optgroup({
123
+ label: 'test',
124
+ options: [
125
+ {
126
+ text: 'opt 1'
127
+ },
128
+ {
129
+ text: 'opt 2'
130
+ }
131
+ ]
132
+ })
133
+
134
+ const optGroupElement = select.createOptgroup(optGroup)
135
+
136
+ expect(optGroupElement).toBeInstanceOf(HTMLOptGroupElement)
137
+ expect(optGroupElement.children).toHaveLength(2)
138
+ })
139
+
140
+ test('selectAll get set correctly', () => {
141
+ const optGroup = new Optgroup({
142
+ label: 'test',
143
+ selectAll: true,
144
+ options: [
145
+ {
146
+ text: 'opt 1'
147
+ },
148
+ {
149
+ text: 'opt 2'
150
+ }
151
+ ]
152
+ })
153
+
154
+ const optGroupElement = select.createOptgroup(optGroup)
155
+
156
+ expect(optGroupElement).toBeInstanceOf(HTMLOptGroupElement)
157
+ expect(optGroupElement.dataset.selectAll).toBe('true')
158
+ })
159
+
160
+ test('closable get set correctly', () => {
161
+ const optGroup = new Optgroup({
162
+ label: 'test',
163
+ closable: 'open',
164
+ options: [
165
+ {
166
+ text: 'opt 1'
167
+ },
168
+ {
169
+ text: 'opt 2'
170
+ }
171
+ ]
172
+ })
173
+
174
+ const optGroupElement = select.createOptgroup(optGroup)
175
+
176
+ expect(optGroupElement).toBeInstanceOf(HTMLOptGroupElement)
177
+ expect(optGroupElement.dataset.closable).toBe(optGroup.closable)
178
+ })
179
+ })
180
+
181
+ describe('createOption', () => {
182
+ test('correct content is set', () => {
183
+ const option = new Option({ text: 'opt' })
184
+ const optionElement = select.createOption(option)
185
+
186
+ expect(optionElement).toBeInstanceOf(HTMLOptionElement)
187
+ expect(optionElement.id).toBe(option.id)
188
+ expect(optionElement.textContent).toBe(option.text)
189
+ })
190
+
191
+ test('HTML is set as data attribute', () => {
192
+ const option = new Option({ text: 'opt', html: '<h1>opt</h1>' })
193
+ const optionElement = select.createOption(option)
194
+
195
+ expect(optionElement.dataset.html).toBe(option.html)
196
+ })
197
+
198
+ test('disabled sets disabled property correctly', () => {
199
+ const option = new Option({ text: 'opt', disabled: true })
200
+ const optionElement = select.createOption(option)
201
+
202
+ expect(optionElement.disabled).toBe(true)
203
+ })
204
+
205
+ test('display false sets inline style correctly', () => {
206
+ const option = new Option({ text: 'opt', display: false })
207
+ const optionElement = select.createOption(option)
208
+
209
+ expect(optionElement.style.display).toBe('none')
210
+ })
211
+
212
+ test('placeholder and mandatory set data attributes correctly', () => {
213
+ const option = new Option({
214
+ text: 'opt',
215
+ placeholder: true,
216
+ mandatory: true
217
+ })
218
+ const optionElement = select.createOption(option)
219
+
220
+ expect(optionElement.dataset.placeholder).toBeTruthy()
221
+ expect(optionElement.dataset.mandatory).toBeTruthy()
222
+ })
223
+
224
+ test('class sets CSS classes correctly', () => {
225
+ const option = new Option({
226
+ text: 'opt',
227
+ class: 'class0 class1 class2'
228
+ })
229
+ const optionElement = select.createOption(option)
230
+
231
+ expect(optionElement.classList.contains('class0')).toBe(true)
232
+ expect(optionElement.classList.contains('class1')).toBe(true)
233
+ expect(optionElement.classList.contains('class2')).toBe(true)
234
+ })
235
+
236
+ test('custom data attributes are set correctly', () => {
237
+ const option = new Option({
238
+ text: 'opt',
239
+ data: {
240
+ test0: 'a',
241
+ test1: 'b'
242
+ }
243
+ })
244
+ const optionElement = select.createOption(option)
245
+
246
+ expect(optionElement.dataset.test0).toBe('a')
247
+ expect(optionElement.dataset.test1).toBe('b')
248
+ })
249
+ })
250
+
251
+ test('destroy', () => {
252
+ select.destroy()
253
+
254
+ expect((<any>select).observer).toBeFalsy()
255
+ expect('id' in select.select.dataset).toBeFalsy()
256
+ })
257
+ })
@@ -17,6 +17,7 @@ export default class Settings {
17
17
  public disabled: boolean
18
18
  public alwaysOpen: boolean
19
19
  public showSearch: boolean
20
+ public focusSearch: boolean
20
21
  public ariaLabel: string
21
22
  public searchPlaceholder: string
22
23
  public searchText: string
@@ -36,6 +37,7 @@ export default class Settings {
36
37
  public timeoutDelay: number
37
38
  public maxValuesShown: number
38
39
  public maxValuesMessage: string
40
+ public addableText: string
39
41
 
40
42
  constructor(settings?: SettingsPartial) {
41
43
  if (!settings) {
@@ -49,6 +51,7 @@ export default class Settings {
49
51
  this.disabled = settings.disabled !== undefined ? settings.disabled : false
50
52
  this.alwaysOpen = settings.alwaysOpen !== undefined ? settings.alwaysOpen : false
51
53
  this.showSearch = settings.showSearch !== undefined ? settings.showSearch : true
54
+ this.focusSearch = settings.focusSearch !== undefined ? settings.focusSearch : true
52
55
  this.ariaLabel = settings.ariaLabel || 'Combobox'
53
56
  this.searchPlaceholder = settings.searchPlaceholder || 'Search'
54
57
  this.searchText = settings.searchText || 'No Results'
@@ -68,5 +71,6 @@ export default class Settings {
68
71
  this.timeoutDelay = settings.timeoutDelay || 200
69
72
  this.maxValuesShown = settings.maxValuesShown || 20
70
73
  this.maxValuesMessage = settings.maxValuesMessage || '{number} selected'
74
+ this.addableText = settings.addableText || 'Press "Enter" to add {value}'
71
75
  }
72
76
  }
@@ -199,7 +199,7 @@
199
199
  justify-content: center;
200
200
  width: fit-content;
201
201
  height: auto;
202
- margin: 0 var(--ss-spacing-m) 0 var(--ss-spacing-m);
202
+ padding: 0 var(--ss-spacing-m) 0 var(--ss-spacing-m);
203
203
 
204
204
  svg {
205
205
  width: 8px;
@@ -247,7 +247,9 @@
247
247
  box-sizing: border-box;
248
248
  border: solid 1px var(--ss-border-color);
249
249
  background-color: var(--ss-bg-color);
250
- transition: transform var(--ss-animation-timing), opacity var(--ss-animation-timing);
250
+ transition:
251
+ transform var(--ss-animation-timing),
252
+ opacity var(--ss-animation-timing);
251
253
  opacity: 0;
252
254
  transform: scaleY(0);
253
255
  transform-origin: center top;
@@ -469,8 +471,9 @@
469
471
  }
470
472
 
471
473
  .ss-option {
472
- display: flex;
474
+ display: block; // Important for text with mark element highlight
473
475
  padding: var(--ss-spacing-m) var(--ss-spacing-l) var(--ss-spacing-m) var(--ss-spacing-l);
476
+ white-space: normal;
474
477
  color: var(--ss-font-color);
475
478
  cursor: pointer;
476
479
  user-select: none;
@@ -497,6 +500,7 @@
497
500
 
498
501
  // Sub div wrapper for highlighting text
499
502
  .ss-search-highlight {
503
+ display: inline-block;
500
504
  background-color: var(--ss-highlight-color);
501
505
  }
502
506
  }