slim-select 2.8.2 → 2.9.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 (69) hide show
  1. package/.prettierignore +1 -0
  2. package/.prettierrc +4 -3
  3. package/README.md +5 -2
  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 +31 -40
  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 +78 -97
  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 +70 -21
  34. package/src/slim-select/settings.test.ts +272 -0
  35. package/src/slim-select/settings.ts +5 -1
  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/{vite.config.ts → vite.config.mts} +11 -4
  42. package/src/react/.eslintignore +0 -2
  43. package/src/react/.eslintrc.cjs +0 -24
  44. package/src/react/index.html +0 -14
  45. package/src/react/jestconfig.json +0 -11
  46. package/src/react/package.json +0 -32
  47. package/src/react/src/frameworkpage.tsx +0 -133
  48. package/src/react/src/index.ts +0 -3
  49. package/src/react/src/slimselect.tsx +0 -148
  50. package/src/react/tests/slimselect.test.tsx +0 -52
  51. package/src/react/tsconfig.json +0 -37
  52. package/src/react/tsconfig.node.json +0 -10
  53. package/src/react/vite.config.ts +0 -55
  54. package/src/vue/README.md +0 -48
  55. package/src/vue/dist/slim-select/helpers.d.ts +0 -5
  56. package/src/vue/dist/slim-select/index.d.ts +0 -46
  57. package/src/vue/dist/slim-select/render.d.ts +0 -120
  58. package/src/vue/dist/slim-select/select.d.ts +0 -28
  59. package/src/vue/dist/slim-select/settings.d.ts +0 -34
  60. package/src/vue/dist/slim-select/store.d.ts +0 -80
  61. package/src/vue/dist/slimselectvue.es.js +0 -1978
  62. package/src/vue/dist/slimselectvue.global.js +0 -1981
  63. package/src/vue/dist/slimselectvue.ssr.js +0 -1980
  64. package/src/vue/dist/vue/hello.d.ts +0 -2
  65. package/src/vue/dist/vue/slimselect.vue.d.ts +0 -326
  66. package/src/vue/hello.ts +0 -5
  67. package/src/vue/package.json +0 -28
  68. package/src/vue/rollup.config.mjs +0 -44
  69. package/src/vue/tsconfig.json +0 -14
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ 'use strict'
6
+
7
+ import { describe, expect, test } from '@jest/globals'
8
+ import { hasClassInTree, debounce, isEqual, kebabCase } from './helpers'
9
+
10
+ describe('helpers module', () => {
11
+ describe('hasClassInTree', () => {
12
+ test('single element does not have class', () => {
13
+ const element = document.createElement('div')
14
+ element.className = 'invalid-class'
15
+
16
+ expect(hasClassInTree(element, 'test-class')).toBeNull()
17
+ })
18
+
19
+ test('single element has class', () => {
20
+ const element = document.createElement('div')
21
+ element.className = 'test-class'
22
+
23
+ expect(hasClassInTree(element, 'test-class')).toBe(element)
24
+
25
+ element.classList.add('class-2', 'class-3')
26
+ expect(hasClassInTree(element, 'test-class')).toBe(element)
27
+ })
28
+
29
+ test('single element has class as data id', () => {
30
+ const element = document.createElement('div')
31
+ element.dataset.id = 'test-class'
32
+
33
+ expect(hasClassInTree(element, 'test-class')).toBe(element)
34
+
35
+ element.classList.add('class-2', 'class-3')
36
+ expect(hasClassInTree(element, 'test-class')).toBe(element)
37
+ })
38
+
39
+ test('parent element does not have class', () => {
40
+ const element = document.createElement('div')
41
+ element.className = 'invalid-class'
42
+ const child = document.createElement('div')
43
+ element.appendChild(child)
44
+
45
+ expect(hasClassInTree(child, 'test-class')).toBeNull()
46
+ })
47
+
48
+ test('parent element has class', () => {
49
+ const element = document.createElement('div')
50
+ element.className = 'test-class'
51
+ const child = document.createElement('div')
52
+ element.appendChild(child)
53
+
54
+ expect(hasClassInTree(child, 'test-class')).toBe(element)
55
+
56
+ element.classList.add('class-2', 'class-3')
57
+ expect(hasClassInTree(child, 'test-class')).toBe(element)
58
+ })
59
+
60
+ test('parent element has class as data id', () => {
61
+ const element = document.createElement('div')
62
+ element.dataset.id = 'test-class'
63
+
64
+ const child = document.createElement('div')
65
+ element.appendChild(child)
66
+
67
+ expect(hasClassInTree(child, 'test-class')).toBe(element)
68
+
69
+ element.classList.add('class-2', 'class-3')
70
+ expect(hasClassInTree(child, 'test-class')).toBe(element)
71
+ })
72
+ })
73
+
74
+ describe('debounce', () => {
75
+ test('debounce calls function after default timeout', async () => {
76
+ const callback = jest.fn()
77
+ const debounced_function = debounce(callback)
78
+ debounced_function()
79
+
80
+ await new Promise((r) => setTimeout(r, 100))
81
+
82
+ expect(callback).toHaveBeenCalled()
83
+ expect(callback).toHaveBeenCalledTimes(1)
84
+ })
85
+
86
+ test('debounce calls function after higher timeout', async () => {
87
+ const callback = jest.fn()
88
+ const debounced_function = debounce(callback, 100)
89
+ debounced_function()
90
+
91
+ await new Promise((r) => setTimeout(r, 50))
92
+ expect(callback).not.toHaveBeenCalled()
93
+
94
+ await new Promise((r) => setTimeout(r, 50))
95
+ expect(callback).toHaveBeenCalled()
96
+ expect(callback).toHaveBeenCalledTimes(1)
97
+ })
98
+
99
+ test('debounce calls function after lower timeout', async () => {
100
+ const callback = jest.fn()
101
+ const debounced_function = debounce(callback, 10)
102
+ debounced_function()
103
+
104
+ await new Promise((r) => setTimeout(r, 5))
105
+ expect(callback).not.toHaveBeenCalled()
106
+
107
+ await new Promise((r) => setTimeout(r, 5))
108
+ expect(callback).toHaveBeenCalled()
109
+ expect(callback).toHaveBeenCalledTimes(1)
110
+ })
111
+
112
+ test('debounce respects inmediate setting', () => {
113
+ const callback = jest.fn()
114
+ const debounced_function = debounce(callback, 1000, true)
115
+ debounced_function()
116
+
117
+ expect(callback).toHaveBeenCalled()
118
+ expect(callback).toHaveBeenCalledTimes(1)
119
+ })
120
+
121
+ test('debounce respects order of calls', async () => {
122
+ const callback = jest.fn(() => {})
123
+ const debounced_function: (a: number) => void = debounce(callback)
124
+ debounced_function(0)
125
+ debounced_function(1)
126
+
127
+ await new Promise((r) => setTimeout(r, 50))
128
+ expect(callback).toHaveBeenCalledTimes(1)
129
+ expect(callback.mock.calls[0]).toStrictEqual([1])
130
+ })
131
+ })
132
+
133
+ describe('isEqual', () => {
134
+ test('different objects are not equal', () => {
135
+ expect(isEqual({ a: 1 }, { b: 1 })).toBe(false)
136
+ })
137
+
138
+ test('equal objects are equal', () => {
139
+ expect(isEqual({ a: 1 }, { a: 1 })).toBe(true)
140
+ })
141
+
142
+ test('more complex objects are equal', () => {
143
+ expect(isEqual({ a: 1, b: { c: 'asdf' } }, { a: 1, b: { c: 'asdf' } })).toBe(true)
144
+ })
145
+ })
146
+
147
+ describe('kebabCase', () => {
148
+ test('kebab-case string', () => {
149
+ expect(kebabCase('kebab-case')).toBe('kebab-case')
150
+ })
151
+
152
+ test('camelCase string', () => {
153
+ expect(kebabCase('camelCase')).toBe('camel-case')
154
+ })
155
+
156
+ test('string with initial uppercase', () => {
157
+ expect(kebabCase('UpperCase')).toBe('upper-case')
158
+ })
159
+ })
160
+ })
@@ -3,7 +3,7 @@ export function generateID(): string {
3
3
  return Math.random().toString(36).substring(2, 10)
4
4
  }
5
5
 
6
- export function hasClassInTree(element: HTMLElement, className: string) {
6
+ export function hasClassInTree(element: HTMLElement, className: string): HTMLElement | null {
7
7
  function hasClass(e: HTMLElement, c: string) {
8
8
  // If the element has the class return element
9
9
  if (c && e && e.classList && e.classList.contains(c)) {
@@ -51,20 +51,11 @@ export function debounce<T extends (...args: any[]) => void>(func: T, wait = 50,
51
51
  }
52
52
  }
53
53
 
54
- // reverseDebounce will call the function on the first call and then debounce
55
- function reverseDebounce<T extends (...args: any[]) => void>(func: T, timeout: number): T {
56
- let timer: NodeJS.Timeout | null = null
57
- return function (...args: any[]): void {
58
- if (!timer) func(...args)
59
- timer = setTimeout(() => (timer = null), timeout)
60
- } as T
61
- }
62
-
63
- export function isEqual(a: any, b: any) {
54
+ export function isEqual(a: any, b: any): boolean {
64
55
  return JSON.stringify(a) === JSON.stringify(b)
65
56
  }
66
57
 
67
- export function kebabCase(str: string) {
58
+ export function kebabCase(str: string): string {
68
59
  const result = str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => '-' + match.toLowerCase())
69
60
  return str[0] === str[0].toUpperCase() ? result.substring(1) : result
70
61
  }
@@ -3,19 +3,112 @@
3
3
  */
4
4
 
5
5
  'use strict'
6
+
6
7
  import { describe, expect, test } from '@jest/globals'
7
8
  import SlimSelect from './'
8
9
  import { OptionOptional } from './store'
9
10
  import { Config } from './index'
10
11
 
11
12
  describe('SlimSelect Module', () => {
12
- test('constructor', () => {
13
+ let slim: SlimSelect
14
+
15
+ beforeEach(() => {
13
16
  document.body.innerHTML = '<select id="test"></select>'
14
17
 
15
- let slimSelect = new SlimSelect({
16
- select: '#test',
18
+ slim = new SlimSelect({
19
+ select: '#test'
17
20
  })
18
- expect(slimSelect).toBeInstanceOf(SlimSelect)
21
+ })
22
+
23
+ describe('constructor', () => {
24
+ test('missing select element throws error', () => {
25
+ const errorMock = jest.fn()
26
+ const slim = new SlimSelect({ select: '#invalid', events: { error: errorMock } })
27
+
28
+ expect(slim.select).toBeUndefined()
29
+ expect(slim.store).toBeUndefined()
30
+ expect(slim.render).toBeUndefined()
31
+ expect(errorMock).toHaveBeenCalled()
32
+ expect(errorMock.mock.calls[0][0].message).toBe('Could not find select element')
33
+ })
34
+
35
+ test('invalid element throws error', () => {
36
+ document.body.innerHTML = '<div id="invalid"></div>'
37
+
38
+ const errorMock = jest.fn()
39
+ const slim = new SlimSelect({ select: '#invalid', events: { error: errorMock } })
40
+
41
+ expect(slim.select).toBeUndefined()
42
+ expect(slim.store).toBeUndefined()
43
+ expect(slim.render).toBeUndefined()
44
+ expect(errorMock).toHaveBeenCalled()
45
+ expect(errorMock.mock.calls[0][0].message).toBe('Element isnt of type select')
46
+ })
47
+
48
+ test('valid minimal constructor with query string', () => {
49
+ document.body.innerHTML = '<select id="test"></select>'
50
+
51
+ const slimSelect = new SlimSelect({
52
+ select: '#test'
53
+ })
54
+
55
+ expect(slimSelect).toBeInstanceOf(SlimSelect)
56
+ })
57
+
58
+ test('valid minimal constructor with HTML element', () => {
59
+ document.body.innerHTML = '<select id="test"></select>'
60
+
61
+ const slimSelect = new SlimSelect({
62
+ select: document.getElementById('test') as Element
63
+ })
64
+
65
+ expect(slimSelect).toBeInstanceOf(SlimSelect)
66
+ })
67
+
68
+ test('disabled gets applied correctly', () => {
69
+ document.body.innerHTML = '<select id="test"></select>'
70
+
71
+ const slim = new SlimSelect({
72
+ select: document.getElementById('test') as Element,
73
+ settings: {
74
+ disabled: true
75
+ }
76
+ })
77
+
78
+ expect(slim.settings.disabled).toBe(true)
79
+ })
80
+ })
81
+
82
+ test('enable', () => {
83
+ slim.settings.disabled = true
84
+
85
+ const selectEnableMock = jest.fn()
86
+ const renderEnableMock = jest.fn()
87
+
88
+ slim.select.enable = selectEnableMock
89
+ slim.render.enable = renderEnableMock
90
+
91
+ slim.enable()
92
+
93
+ expect(slim.settings.disabled).toBe(false)
94
+ expect(selectEnableMock).toHaveBeenCalled()
95
+ expect(renderEnableMock).toHaveBeenCalled()
96
+ })
97
+
98
+ test('disable', () => {
99
+ slim.settings.disabled = false
100
+
101
+ const selectDisableMock = jest.fn()
102
+ const renderDisableMock = jest.fn()
103
+
104
+ slim.select.disable = selectDisableMock
105
+ slim.render.disable = renderDisableMock
106
+
107
+ slim.disable()
108
+
109
+ expect(slim.settings.disabled).toBe(true)
110
+ expect(selectDisableMock).toHaveBeenCalled()
111
+ expect(renderDisableMock).toHaveBeenCalled()
19
112
  })
20
113
 
21
114
  test('multiple - do not render deselect all with no selected options', () => {
@@ -30,25 +123,25 @@ describe('SlimSelect Module', () => {
30
123
  {
31
124
  id: '1',
32
125
  value: '1',
33
- text: 'One',
126
+ text: 'One'
34
127
  },
35
128
  {
36
129
  id: '2',
37
130
  value: '2',
38
- text: 'Two',
131
+ text: 'Two'
39
132
  },
40
133
  {
41
134
  id: '3',
42
135
  value: '3',
43
- text: 'Three',
44
- },
136
+ text: 'Three'
137
+ }
45
138
  ]
46
139
  const config: Config = {
47
140
  select: '#test',
48
141
  settings: {
49
- allowDeselect: true,
142
+ allowDeselect: true
50
143
  },
51
- data: options,
144
+ data: options
52
145
  }
53
146
 
54
147
  let slimSelect = new SlimSelect(config)
@@ -69,26 +162,26 @@ describe('SlimSelect Module', () => {
69
162
  id: '1',
70
163
  value: '1',
71
164
  text: 'One',
72
- selected: true,
165
+ selected: true
73
166
  },
74
167
  {
75
168
  id: '2',
76
169
  value: '2',
77
170
  text: 'Two',
78
- selected: true,
171
+ selected: true
79
172
  },
80
173
  {
81
174
  id: '3',
82
175
  value: '3',
83
- text: 'Three',
84
- },
176
+ text: 'Three'
177
+ }
85
178
  ]
86
179
  const config: Config = {
87
180
  select: '#test',
88
181
  settings: {
89
- allowDeselect: true,
182
+ allowDeselect: true
90
183
  },
91
- data: options,
184
+ data: options
92
185
  }
93
186
 
94
187
  let slimSelect = new SlimSelect(config)
@@ -1,3 +1,4 @@
1
+ import CssClasses from './classes'
1
2
  import { debounce, hasClassInTree, isEqual } from './helpers'
2
3
  import Render from './render'
3
4
  import Select from './select'
@@ -8,13 +9,16 @@ export interface Config {
8
9
  select: string | Element
9
10
  data?: DataArrayPartial
10
11
  settings?: SettingsPartial
12
+ cssClasses?: Partial<CssClasses>
11
13
  events?: Events
12
14
  }
13
15
 
14
16
  export interface Events {
15
17
  search?: (searchValue: string, currentData: DataArray) => Promise<DataArrayPartial> | DataArrayPartial
16
18
  searchFilter?: (option: Option, search: string) => boolean
17
- addable?: (value: string) => Promise<OptionOptional | string> | OptionOptional | string | false | null | undefined
19
+ addable?: (
20
+ value: string
21
+ ) => Promise<OptionOptional | string> | OptionOptional | string | false | null | undefined | Error
18
22
  beforeChange?: (newVal: Option[], oldVal: Option[]) => boolean | void
19
23
  afterChange?: (newVal: Option[]) => void
20
24
  beforeOpen?: () => void
@@ -29,6 +33,7 @@ export default class SlimSelect {
29
33
 
30
34
  // Classes
31
35
  public settings!: Settings
36
+ public cssClasses!: CssClasses
32
37
  public select!: Select
33
38
  public store!: Store
34
39
  public render!: Render
@@ -45,7 +50,7 @@ export default class SlimSelect {
45
50
  beforeOpen: undefined,
46
51
  afterOpen: undefined,
47
52
  beforeClose: undefined,
48
- afterClose: undefined,
53
+ afterClose: undefined
49
54
  } as Events
50
55
 
51
56
  constructor(config: Config) {
@@ -74,6 +79,9 @@ export default class SlimSelect {
74
79
  // Set settings
75
80
  this.settings = new Settings(config.settings)
76
81
 
82
+ // Set CSS classes
83
+ this.cssClasses = new CssClasses(config.cssClasses)
84
+
77
85
  // Set events
78
86
  const debounceEvents = ['afterChange', 'beforeOpen', 'afterOpen', 'beforeClose', 'afterClose']
79
87
  for (const key in config.events) {
@@ -102,9 +110,9 @@ export default class SlimSelect {
102
110
  this.select.hideUI() // Hide the original select element
103
111
 
104
112
  // Add select listeners
105
- this.select.onValueChange = (values: string[]) => {
113
+ this.select.onValueChange = (options: Option[]) => {
106
114
  // Run set selected from the values given
107
- this.setSelected(values)
115
+ this.setSelected(options.map((option) => option.id))
108
116
  }
109
117
  this.select.onClassChange = (classes: string[]) => {
110
118
  // Update settings with new class
@@ -128,7 +136,7 @@ export default class SlimSelect {
128
136
  // Set store class
129
137
  this.store = new Store(
130
138
  this.settings.isMultiple ? 'multiple' : 'single',
131
- config.data ? config.data : this.select.getData(),
139
+ config.data ? config.data : this.select.getData()
132
140
  )
133
141
 
134
142
  // If data is passed update the original select element
@@ -145,11 +153,11 @@ export default class SlimSelect {
145
153
  addOption: this.addOption.bind(this),
146
154
  search: this.search.bind(this),
147
155
  beforeChange: this.events.beforeChange,
148
- afterChange: this.events.afterChange,
156
+ afterChange: this.events.afterChange
149
157
  }
150
158
 
151
159
  // Setup render class
152
- this.render = new Render(this.settings, this.store, renderCallbacks)
160
+ this.render = new Render(this.settings, this.cssClasses, this.store, renderCallbacks)
153
161
  this.render.renderValues()
154
162
  this.render.renderOptions(this.store.getData())
155
163
 
@@ -245,15 +253,36 @@ export default class SlimSelect {
245
253
  }
246
254
 
247
255
  public getSelected(): string[] {
248
- return this.store.getSelected()
256
+ let options = this.store.getSelectedOptions()
257
+ if (this.settings.keepOrder) {
258
+ options = this.store.selectedOrderOptions(options)
259
+ }
260
+ return options.map((option) => option.value)
249
261
  }
250
262
 
251
- public setSelected(value: string | string[], runAfterChange = true): void {
263
+ // Will take in a string or array of strings and set the selected by either the id or value
264
+ public setSelected(values: string | string[], runAfterChange = true): void {
252
265
  // Get original selected values
253
266
  const selected = this.store.getSelected()
267
+ const options = this.store.getDataOptions()
268
+ values = Array.isArray(values) ? values : [values]
269
+ const ids = []
270
+
271
+ // for back-compatibility support both, set by id and set by value
272
+ for (const value of values) {
273
+ if (options.find((option) => option.id == value)) {
274
+ ids.push(value)
275
+ continue
276
+ }
277
+
278
+ // if option with given id is not found try to search by value
279
+ for (const option of options.filter((option) => option.value == value)) {
280
+ ids.push(option.id)
281
+ }
282
+ }
254
283
 
255
284
  // Update the store
256
- this.store.setSelectedBy('value', Array.isArray(value) ? value : [value])
285
+ this.store.setSelectedBy('id', ids)
257
286
  const data = this.store.getData()
258
287
 
259
288
  // Update the select element
@@ -315,7 +344,7 @@ export default class SlimSelect {
315
344
  this.render.open()
316
345
 
317
346
  // Focus on input field only if search is enabled
318
- if (this.settings.showSearch) {
347
+ if (this.settings.showSearch && this.settings.focusSearch) {
319
348
  this.render.searchFocus()
320
349
  }
321
350
 
@@ -402,7 +431,7 @@ export default class SlimSelect {
402
431
  if (!this.events.search) {
403
432
  // If value is empty then render all options
404
433
  this.render.renderOptions(
405
- value === '' ? this.store.getData() : this.store.search(value, this.events.searchFilter!),
434
+ value === '' ? this.store.getData() : this.store.search(value, this.events.searchFilter!)
406
435
  )
407
436
  return
408
437
  }