slim-select 2.13.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +184 -43
  2. package/dist/index.d.ts +304 -52
  3. package/dist/react/index.d.ts +304 -0
  4. package/dist/react/index.js +1549 -0
  5. package/dist/react/index.umd.js +22 -0
  6. package/dist/slimselect.cjs.js +1 -2030
  7. package/dist/slimselect.css +1 -1
  8. package/dist/slimselect.css.map +1 -1
  9. package/dist/slimselect.es.js +1224 -2012
  10. package/dist/slimselect.umd.js +1 -2036
  11. package/dist/vue/index.d.ts +304 -0
  12. package/dist/vue/index.js +1341 -0
  13. package/dist/vue/index.umd.js +1 -0
  14. package/index.html +1 -0
  15. package/package.json +59 -37
  16. package/release.js +0 -7
  17. package/src/slim-select/accessibility.test.ts +782 -0
  18. package/src/slim-select/classes.test.ts +7 -6
  19. package/src/slim-select/classes.ts +8 -6
  20. package/src/slim-select/helpers.test.ts +10 -8
  21. package/src/slim-select/index.test.ts +509 -14
  22. package/src/slim-select/index.ts +58 -31
  23. package/src/slim-select/react/index.tsx +9 -0
  24. package/src/slim-select/react/react.tsx +112 -0
  25. package/src/slim-select/render.test.ts +479 -119
  26. package/src/slim-select/render.ts +213 -91
  27. package/src/slim-select/select.test.ts +28 -10
  28. package/src/slim-select/select.ts +35 -12
  29. package/src/slim-select/settings.test.ts +19 -8
  30. package/src/slim-select/settings.ts +2 -4
  31. package/src/slim-select/slimselect.scss +30 -15
  32. package/src/slim-select/store.test.ts +13 -16
  33. package/src/slim-select/store.ts +97 -86
  34. package/src/slim-select/vue/index.ts +9 -0
  35. package/src/slim-select/vue/vue.test.ts +832 -0
  36. package/src/slim-select/{vue.vue → vue/vue.vue} +45 -23
  37. package/tsconfig.json +6 -4
  38. package/{vite.config.mts → vite.config.docs.mts} +10 -0
  39. package/vite.config.lib.mts +42 -0
  40. package/vite.config.react.mts +40 -0
  41. package/vite.config.vue.mts +39 -0
  42. package/vitest.config.ts +29 -0
  43. package/dist/classes.d.ts +0 -43
  44. package/dist/helpers.d.ts +0 -5
  45. package/dist/render.d.ts +0 -81
  46. package/dist/select.d.ts +0 -30
  47. package/dist/settings.d.ts +0 -35
  48. package/dist/slimselect.global.js +0 -2033
  49. package/dist/slimselect.js +0 -2036
  50. package/dist/slimselect.min.js +0 -1
  51. package/dist/slimselect.umd.min.js +0 -1
  52. package/dist/store.d.ts +0 -84
  53. package/jest.config.js +0 -14
  54. package/src/slim-select/rollup.config.mjs +0 -127
  55. package/src/slim-select/tsconfig.json +0 -14
@@ -3,26 +3,28 @@ import { debounce, hasClassInTree, isEqual } from './helpers'
3
3
  import Render from './render'
4
4
  import Select from './select'
5
5
  import Settings from './settings'
6
- import type { SettingsPartial } from './settings'
7
- import Store from './store'
8
- import type { DataArray, DataArrayPartial, Option, OptionOptional } from './store'
6
+ import Store, { Option, Optgroup } from './store'
9
7
 
10
- export { DataArray, DataArrayPartial, Option, OptionOptional, SettingsPartial }
8
+ // Export classes
9
+ export { Select, Settings, Render, Store, Option, Optgroup }
11
10
 
12
11
  export interface Config {
13
12
  select: string | Element
14
- data?: DataArrayPartial
15
- settings?: SettingsPartial
13
+ data?: (Partial<Option> | Partial<Optgroup>)[]
14
+ settings?: Partial<Settings>
16
15
  cssClasses?: Partial<CssClasses>
17
16
  events?: Events
18
17
  }
19
18
 
20
19
  export interface Events {
21
- search?: (searchValue: string, currentData: DataArray) => Promise<DataArrayPartial> | DataArrayPartial
20
+ search?: (
21
+ searchValue: string,
22
+ currentData: (Option | Optgroup)[]
23
+ ) => Promise<(Partial<Option> | Partial<Optgroup>)[]> | (Partial<Option> | Partial<Optgroup>)[]
22
24
  searchFilter?: (option: Option, search: string) => boolean
23
25
  addable?: (
24
26
  value: string
25
- ) => Promise<OptionOptional | string> | OptionOptional | string | false | null | undefined | Error
27
+ ) => Promise<Partial<Option> | string> | Partial<Option> | string | false | null | undefined | Error
26
28
  beforeChange?: (newVal: Option[], oldVal: Option[]) => boolean | void
27
29
  afterChange?: (newVal: Option[]) => void
28
30
  beforeOpen?: () => void
@@ -42,6 +44,10 @@ export default class SlimSelect {
42
44
  public store!: Store
43
45
  public render!: Render
44
46
 
47
+ // Timeout tracking for cleanup
48
+ private openTimeout: ReturnType<typeof setTimeout> | null = null
49
+ private closeTimeout: ReturnType<typeof setTimeout> | null = null
50
+
45
51
  // Events
46
52
  public events = {
47
53
  search: undefined,
@@ -87,7 +93,7 @@ export default class SlimSelect {
87
93
  this.cssClasses = new CssClasses(config.cssClasses)
88
94
 
89
95
  // Set events
90
- const debounceEvents = ['afterChange', 'beforeOpen', 'afterOpen', 'beforeClose', 'afterClose']
96
+ const debounceEvents = ['beforeOpen', 'afterOpen', 'beforeClose', 'afterClose']
91
97
  for (const key in config.events) {
92
98
  // Check if key exists in events
93
99
  if (!config.events.hasOwnProperty(key)) {
@@ -132,16 +138,14 @@ export default class SlimSelect {
132
138
  this.enable()
133
139
  }
134
140
  }
135
- this.select.onOptionsChange = (data: DataArrayPartial) => {
141
+ this.select.onOptionsChange = (data: (Option | Optgroup)[]) => {
136
142
  // Run set data from the values given
137
143
  this.setData(data)
138
144
  }
139
145
 
140
146
  // Set store class
141
- this.store = new Store(
142
- this.settings.isMultiple ? 'multiple' : 'single',
143
- config.data ? config.data : this.select.getData()
144
- )
147
+ const data = config.data ? config.data : this.select.getData()
148
+ this.store = new Store(this.settings.isMultiple ? 'multiple' : 'single', data)
145
149
 
146
150
  // If data is passed update the original select element
147
151
  if (config.data) {
@@ -222,11 +226,11 @@ export default class SlimSelect {
222
226
  this.render.disable()
223
227
  }
224
228
 
225
- public getData(): DataArray {
229
+ public getData(): Option[] | Optgroup[] {
226
230
  return this.store.getData()
227
231
  }
228
232
 
229
- public setData(data: DataArrayPartial): void {
233
+ public setData(data: (Partial<Option> | Partial<Optgroup>)[]): void {
230
234
  // Get original selected values
231
235
  const selected = this.store.getSelected()
232
236
 
@@ -309,7 +313,7 @@ export default class SlimSelect {
309
313
  }
310
314
  }
311
315
 
312
- public addOption(option: OptionOptional): void {
316
+ public addOption(option: Partial<Option>): void {
313
317
  // Get original selected values
314
318
  const selected = this.store.getSelected()
315
319
 
@@ -354,7 +358,7 @@ export default class SlimSelect {
354
358
 
355
359
  this.settings.isOpen = true
356
360
  // setTimeout is for animation completion
357
- setTimeout(() => {
361
+ this.openTimeout = setTimeout(() => {
358
362
  // Run afterOpen callback
359
363
  if (this.events.afterOpen) {
360
364
  this.events.afterOpen()
@@ -396,10 +400,8 @@ export default class SlimSelect {
396
400
  // Tell render to close
397
401
  this.render.close()
398
402
 
399
- // Clear search only if not empty
400
- if (this.render.content.search.input.value !== '') {
401
- this.search('') // Clear search
402
- }
403
+ // Clear search input visually (but don't trigger search event)
404
+ this.render.clearSearch()
403
405
 
404
406
  // If we arent tabbing focus back on the main element
405
407
  this.render.mainFocus(eventType)
@@ -409,7 +411,7 @@ export default class SlimSelect {
409
411
  this.settings.isFullOpen = false
410
412
 
411
413
  // Reset the content below
412
- setTimeout(() => {
414
+ this.closeTimeout = setTimeout(() => {
413
415
  // Run afterClose callback
414
416
  if (this.events.afterClose) {
415
417
  this.events.afterClose()
@@ -435,9 +437,8 @@ export default class SlimSelect {
435
437
  // If no search event run regular search
436
438
  if (!this.events.search) {
437
439
  // If value is empty then render all options
438
- this.render.renderOptions(
439
- value === '' ? this.store.getData() : this.store.search(value, this.events.searchFilter!)
440
- )
440
+ const searchResults = value === '' ? this.store.getData() : this.store.search(value, this.events.searchFilter!)
441
+ this.render.renderOptions(searchResults)
441
442
  return
442
443
  }
443
444
 
@@ -450,9 +451,15 @@ export default class SlimSelect {
450
451
  // If the search event returns a promise
451
452
  if (searchResp instanceof Promise) {
452
453
  searchResp
453
- .then((data: DataArrayPartial) => {
454
- // Update the render with the new data
455
- this.render.renderOptions(this.store.partialToFullData(data))
454
+ .then((data: (Partial<Option> | Partial<Optgroup>)[]) => {
455
+ // Update store data with search results, preserving selected options
456
+ this.store.setData(data, true)
457
+
458
+ // Update original select element
459
+ this.select.updateOptions(this.store.getData())
460
+
461
+ // Render the updated data
462
+ this.render.renderOptions(this.store.getData())
456
463
  })
457
464
  .catch((err: Error | string) => {
458
465
  // Update the render with error
@@ -461,8 +468,14 @@ export default class SlimSelect {
461
468
 
462
469
  return
463
470
  } else if (Array.isArray(searchResp)) {
464
- // Update the render options
465
- this.render.renderOptions(this.store.partialToFullData(searchResp))
471
+ // Update store data with search results, preserving selected options
472
+ this.store.setData(searchResp, true)
473
+
474
+ // Update original select element
475
+ this.select.updateOptions(this.store.getData())
476
+
477
+ // Render the updated data
478
+ this.render.renderOptions(this.store.getData())
466
479
  } else {
467
480
  // Update the render with error
468
481
  this.render.renderError('Search event must return a promise or an array of data')
@@ -470,6 +483,20 @@ export default class SlimSelect {
470
483
  }
471
484
 
472
485
  public destroy(): void {
486
+ // Clear any pending timeouts
487
+ if (this.openTimeout) {
488
+ clearTimeout(this.openTimeout)
489
+ this.openTimeout = null
490
+ }
491
+ if (this.closeTimeout) {
492
+ clearTimeout(this.closeTimeout)
493
+ this.closeTimeout = null
494
+ }
495
+ if (this.settings.intervalMove) {
496
+ clearInterval(this.settings.intervalMove)
497
+ this.settings.intervalMove = null
498
+ }
499
+
473
500
  // Remove all event listeners
474
501
  document.removeEventListener('click', this.documentClick)
475
502
  window.removeEventListener('resize', this.windowResize, false)
@@ -0,0 +1,9 @@
1
+ import SlimSelect from './react'
2
+
3
+ export default SlimSelect
4
+
5
+ // Re-export classes
6
+ export { Settings, Option, Optgroup } from '../index'
7
+
8
+ // Re-export types
9
+ export type { Config, Events } from '../index'
@@ -0,0 +1,112 @@
1
+ import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
2
+ import SlimSelectCore from '../index'
3
+ import type { Config, Option, Optgroup } from '../index'
4
+
5
+ export interface SlimSelectProps {
6
+ data?: (Partial<Option> | Partial<Optgroup>)[]
7
+ settings?: Config['settings']
8
+ events?: Config['events']
9
+ cssClasses?: Config['cssClasses']
10
+ value?: string | string[]
11
+ onChange?: (value: string | string[]) => void
12
+ children?: React.ReactNode
13
+ multiple?: boolean
14
+ }
15
+
16
+ export interface SlimSelectRef {
17
+ slimSelect: SlimSelectCore | null
18
+ }
19
+
20
+ const SlimSelect = forwardRef<SlimSelectRef, SlimSelectProps>(
21
+ ({ data, settings, events, cssClasses, value, onChange, children, multiple }, ref) => {
22
+ const selectRef = useRef<HTMLSelectElement>(null)
23
+ const slimSelectRef = useRef<SlimSelectCore | null>(null)
24
+ const isInitialMount = useRef(true)
25
+
26
+ // Expose SlimSelect instance to parent via ref
27
+ useImperativeHandle(ref, () => ({
28
+ slimSelect: slimSelectRef.current
29
+ }))
30
+
31
+ // Initialize SlimSelect
32
+ useEffect(() => {
33
+ if (!selectRef.current) return
34
+
35
+ const config: Config = {
36
+ select: selectRef.current
37
+ }
38
+
39
+ if (data) {
40
+ config.data = data
41
+ }
42
+
43
+ if (settings) {
44
+ config.settings = settings
45
+ }
46
+
47
+ if (cssClasses) {
48
+ config.cssClasses = cssClasses
49
+ }
50
+
51
+ // Wrap onChange in afterChange event
52
+ const ogAfterChange = events?.afterChange
53
+ config.events = {
54
+ ...events,
55
+ afterChange: (newVal) => {
56
+ const newValue = multiple ? newVal.map((option) => option.value) : (newVal[0]?.value ?? '')
57
+
58
+ if (onChange) {
59
+ onChange(newValue)
60
+ }
61
+
62
+ if (ogAfterChange) {
63
+ ogAfterChange(newVal)
64
+ }
65
+ }
66
+ }
67
+
68
+ slimSelectRef.current = new SlimSelectCore(config)
69
+
70
+ // Set initial value if provided
71
+ if (value !== undefined) {
72
+ slimSelectRef.current.setSelected(value, false)
73
+ }
74
+
75
+ return () => {
76
+ if (slimSelectRef.current) {
77
+ slimSelectRef.current.destroy()
78
+ slimSelectRef.current = null
79
+ }
80
+ }
81
+ }, []) // Only run on mount/unmount
82
+
83
+ // Handle value changes from parent
84
+ useEffect(() => {
85
+ if (isInitialMount.current) {
86
+ isInitialMount.current = false
87
+ return
88
+ }
89
+
90
+ if (slimSelectRef.current && value !== undefined) {
91
+ slimSelectRef.current.setSelected(value, false)
92
+ }
93
+ }, [value])
94
+
95
+ // Handle data changes
96
+ useEffect(() => {
97
+ if (slimSelectRef.current && data && !isInitialMount.current) {
98
+ slimSelectRef.current.setData(data)
99
+ }
100
+ }, [data])
101
+
102
+ return (
103
+ <select ref={selectRef} multiple={multiple}>
104
+ {children}
105
+ </select>
106
+ )
107
+ }
108
+ )
109
+
110
+ SlimSelect.displayName = 'SlimSelect'
111
+
112
+ export default SlimSelect