slim-select 2.2.3 → 2.3.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.
@@ -1,36 +1,43 @@
1
- import { generateID, kebabCase } from './helpers'
1
+ import { kebabCase } from './helpers'
2
2
  import { DataArray, DataArrayPartial, Optgroup, OptgroupOptional, Option } from './store'
3
3
 
4
4
  export default class Select {
5
5
  public select: HTMLSelectElement
6
- public listen: boolean = false
7
6
 
8
7
  // Mutation observer fields
9
- public onSelectChange?: (data: DataArrayPartial) => void
10
8
  public onValueChange?: (value: string[]) => void
9
+ public onClassChange?: (classes: string[]) => void
10
+ public onDisabledChange?: (disabled: boolean) => void
11
+ public onOptionsChange?: (data: DataArrayPartial) => void
12
+
13
+ // Change observers
14
+ public listen: boolean = false
11
15
  private observer: MutationObserver | null = null
12
16
 
13
17
  constructor(select: HTMLSelectElement) {
14
18
  this.select = select
19
+
20
+ // Add change event listener
21
+ this.select.addEventListener('change', this.valueChange.bind(this), {
22
+ // allow bubbling of event
23
+ passive: true,
24
+ })
25
+
26
+ // Initiate mutation observer
27
+ this.observer = new MutationObserver(this.observeCall.bind(this))
28
+
29
+ // Start listening for changes
30
+ this.changeListen(true)
15
31
  }
16
32
 
17
- // Set to enabled
18
33
  public enable(): void {
19
- // Disable original select but dont trigger observer
20
- this.disconnectObserver()
21
34
  this.select.disabled = false
22
- this.connectObserver()
23
35
  }
24
36
 
25
- // Set to disabled
26
37
  public disable(): void {
27
- // Enable original select but dont trigger observer
28
- this.disconnectObserver()
29
38
  this.select.disabled = true
30
- this.connectObserver()
31
39
  }
32
40
 
33
- // Set misc attributes on the main select element
34
41
  public hideUI(): void {
35
42
  this.select.tabIndex = -1
36
43
  this.select.style.display = 'none'
@@ -43,44 +50,30 @@ export default class Select {
43
50
  this.select.removeAttribute('aria-hidden')
44
51
  }
45
52
 
46
- public changeListen(on: boolean) {
47
- this.listen = on
48
-
49
- // Deal with some observer situations
50
- if (this.listen) {
51
- this.connectObserver()
52
- } else {
53
- this.disconnectObserver()
53
+ public changeListen(listen: boolean) {
54
+ this.listen = listen
55
+
56
+ // Start listening for changes
57
+ if (listen) {
58
+ if (this.observer) {
59
+ this.observer.observe(this.select, {
60
+ subtree: true, // subtree for optgroups options
61
+ childList: true, // children changes
62
+ attributes: true, // attributes changes
63
+ })
64
+ }
54
65
  }
55
- }
56
-
57
- // Add change listener to original select
58
- public addSelectChangeListener(func: (data: DataArrayPartial) => void): void {
59
- this.onSelectChange = func
60
- this.addObserver()
61
- this.connectObserver()
62
- this.changeListen(true) // Last start listening
63
- }
64
66
 
65
- // remove change listener from original select
66
- public removeSelectChangeListener(): void {
67
- this.changeListen(false) // First stop listening
68
- this.onSelectChange = undefined
69
- }
70
-
71
- public addValueChangeListener(func: (value: string[]) => void): void {
72
- this.onValueChange = func
73
- this.select.addEventListener('change', this.valueChange.bind(this), {
74
- // allow bubbling of event
75
- passive: true,
76
- })
77
- }
78
-
79
- public removeValueChangeListener(): void {
80
- this.onValueChange = undefined
81
- this.select.removeEventListener('change', this.valueChange.bind(this))
67
+ // Stop listening for changes
68
+ if (!listen) {
69
+ if (this.observer) {
70
+ this.observer.disconnect()
71
+ }
72
+ }
82
73
  }
83
74
 
75
+ // This function get triggers when the select value changes
76
+ // and will call the onValueChange function if it exists
84
77
  public valueChange(ev: Event): boolean {
85
78
  if (this.listen && this.onValueChange) {
86
79
  this.onValueChange(this.getSelectedValues())
@@ -90,47 +83,54 @@ export default class Select {
90
83
  return true
91
84
  }
92
85
 
93
- private observeWrapper(mutations: MutationRecord[]): void {
94
- if (this.onSelectChange) {
95
- // Just in case this triggers a change in the select
96
- // we want to stop listening to it while we run onSelectChange
97
- this.changeListen(false)
98
- this.onSelectChange(this.getData())
99
- this.changeListen(true)
86
+ private observeCall(mutations: MutationRecord[]): void {
87
+ // If we are not listeing do nothing
88
+ if (!this.listen) {
89
+ return
100
90
  }
101
- }
102
91
 
103
- // Add MutationObserver to select
104
- private addObserver(): void {
105
- // If mutation observer already exists then disconnect and
106
- if (this.observer) {
107
- this.disconnectObserver()
108
- this.observer = null
92
+ let classChanged = false
93
+ let disabledChanged = false
94
+ let optgroupOptionChanged = false
95
+
96
+ // Loop through mutations and check various things
97
+ for (const m of mutations) {
98
+ // Check if its the select
99
+ if (m.target === this.select) {
100
+ // Check if disabled has changed
101
+ if (m.attributeName === 'disabled') {
102
+ disabledChanged = true
103
+ }
104
+
105
+ // Check if class has changed
106
+ if (m.attributeName === 'class') {
107
+ classChanged = true
108
+ }
109
+ }
110
+
111
+ // Check if its an optgroup or option
112
+ if (m.target.nodeName === 'OPTGROUP' || m.target.nodeName === 'OPTION') {
113
+ optgroupOptionChanged = true
114
+ }
109
115
  }
110
116
 
111
- // If anything changes in the select then update the data
112
- this.observer = new MutationObserver(this.observeWrapper.bind(this))
113
- }
117
+ // If class has changed then call the class change function
118
+ if (classChanged && this.onClassChange) {
119
+ this.onClassChange(this.select.className.split(' '))
120
+ }
114
121
 
115
- // Start observing the select
116
- private connectObserver(): void {
117
- if (this.observer) {
118
- this.observer.observe(this.select, {
119
- // For now we only care about if the children change
120
- childList: true,
121
-
122
- // We dont care about attributes for now
123
- // might need to add this later
124
- // attributes: true,
125
- // characterData: true,
126
- // subtree: true,
127
- })
122
+ // If disabled has changed then call the disabled change function
123
+ if (disabledChanged && this.onDisabledChange) {
124
+ this.changeListen(false)
125
+ this.onDisabledChange(this.select.disabled)
126
+ this.changeListen(true)
128
127
  }
129
- }
130
128
 
131
- private disconnectObserver(): void {
132
- if (this.observer) {
133
- this.observer.disconnect()
129
+ // If optgroup or option has changed then call the select change function
130
+ if (optgroupOptionChanged && this.onOptionsChange) {
131
+ this.changeListen(false)
132
+ this.onOptionsChange(this.getData())
133
+ this.changeListen(true)
134
134
  }
135
135
  }
136
136
 
@@ -157,7 +157,7 @@ export default class Select {
157
157
 
158
158
  public getDataFromOptgroup(optgroup: HTMLOptGroupElement): OptgroupOptional {
159
159
  let data = {
160
- id: (optgroup.dataset ? optgroup.dataset.id : false) || '',
160
+ id: optgroup.id,
161
161
  label: optgroup.label,
162
162
  selectAll: optgroup.dataset ? optgroup.dataset.selectall === 'true' : false,
163
163
  closable: optgroup.dataset ? optgroup.dataset.closable : 'off',
@@ -177,10 +177,10 @@ export default class Select {
177
177
  // From passed in option pull pieces of usable information
178
178
  public getDataFromOption(option: HTMLOptionElement): Option {
179
179
  return {
180
- id: (option.dataset ? option.dataset.id : false) || '',
180
+ id: option.id,
181
181
  value: option.value,
182
182
  text: option.text,
183
- html: option.innerHTML,
183
+ html: option.dataset && option.dataset.html ? option.dataset.html : '',
184
184
  selected: option.selected,
185
185
  display: option.style.display === 'none' ? false : true,
186
186
  disabled: option.disabled,
@@ -222,6 +222,9 @@ export default class Select {
222
222
  }
223
223
 
224
224
  public setSelected(value: string[]): void {
225
+ // Stop listening to changes
226
+ this.changeListen(false)
227
+
225
228
  // Loop through options and set selected
226
229
  const options = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[]
227
230
  for (const o of options) {
@@ -241,15 +244,18 @@ export default class Select {
241
244
  option.selected = value.includes(option.value)
242
245
  }
243
246
  }
247
+
248
+ // Stop listening to changes
249
+ this.changeListen(true)
244
250
  }
245
251
 
246
252
  public updateSelect(id?: string, style?: string, classes?: string[]): void {
247
253
  // Stop listening to changes
248
254
  this.changeListen(false)
249
255
 
250
- // Update id
256
+ // Update id, only if the id isnt already set
251
257
  if (id) {
252
- this.select.id = id
258
+ this.select.dataset.id = id
253
259
  }
254
260
 
255
261
  // Update style
@@ -315,8 +321,12 @@ export default class Select {
315
321
 
316
322
  public createOption(info: Option): HTMLOptionElement {
317
323
  const optionEl = document.createElement('option')
318
- optionEl.value = info.value !== '' ? info.value : info.text
319
- optionEl.innerHTML = info.html || info.text
324
+ optionEl.id = info.id
325
+ optionEl.value = info.value
326
+ optionEl.innerHTML = info.text
327
+ if (info.html !== '') {
328
+ optionEl.setAttribute('data-html', info.html)
329
+ }
320
330
  if (info.selected) {
321
331
  optionEl.selected = info.selected
322
332
  }
@@ -348,11 +358,20 @@ export default class Select {
348
358
 
349
359
  public destroy() {
350
360
  this.changeListen(false)
351
- this.disconnectObserver()
352
- this.removeSelectChangeListener()
353
- this.removeValueChangeListener()
354
361
 
355
- // show the original select
362
+ // Remove event change listener
363
+ this.select.removeEventListener('change', this.valueChange.bind(this))
364
+
365
+ // Disconnect observer and null
366
+ if (this.observer) {
367
+ this.observer.disconnect()
368
+ this.observer = null
369
+ }
370
+
371
+ // Remove dataset id from original select
372
+ delete this.select.dataset.id
373
+
374
+ // Show the original select
356
375
  this.showUI()
357
376
  }
358
377
  }
@@ -14,7 +14,7 @@ export default class Settings {
14
14
  public intervalMove: NodeJS.Timeout | null = null
15
15
 
16
16
  // Fields set from constructor
17
- public isEnabled: boolean
17
+ public disabled: boolean
18
18
  public alwaysOpen: boolean
19
19
  public showSearch: boolean
20
20
  public searchPlaceholder: string
@@ -32,6 +32,8 @@ export default class Settings {
32
32
  public minSelected: number
33
33
  public maxSelected: number
34
34
  public timeoutDelay: number
35
+ public maxValuesShown: number
36
+ public maxValuesMessage: string
35
37
 
36
38
  constructor(settings?: SettingsPartial) {
37
39
  if (!settings) {
@@ -42,7 +44,7 @@ export default class Settings {
42
44
  this.style = settings.style || ''
43
45
  this.class = settings.class || []
44
46
 
45
- this.isEnabled = settings.isEnabled !== undefined ? settings.isEnabled : true
47
+ this.disabled = settings.disabled !== undefined ? settings.disabled : false
46
48
  this.alwaysOpen = settings.alwaysOpen !== undefined ? settings.alwaysOpen : false
47
49
  this.showSearch = settings.showSearch !== undefined ? settings.showSearch : true
48
50
  this.searchPlaceholder = settings.searchPlaceholder || 'Search'
@@ -60,5 +62,7 @@ export default class Settings {
60
62
  this.minSelected = settings.minSelected || 0
61
63
  this.maxSelected = settings.maxSelected || 1000
62
64
  this.timeoutDelay = settings.timeoutDelay || 200
65
+ this.maxValuesShown = settings.maxValuesShown || 20
66
+ this.maxValuesMessage = settings.maxValuesMessage || '{number} selected'
63
67
  }
64
68
  }
@@ -4,7 +4,7 @@
4
4
  --ss-bg-color: #ffffff;
5
5
  --ss-font-color: #4d4d4d;
6
6
  --ss-font-placeholder-color: #8d8d8d;
7
- --ss-disabled-color: #8a8a8a;
7
+ --ss-disabled-color: #dcdee2;
8
8
  --ss-border-color: #dcdee2;
9
9
  --ss-highlight-color: #fffb8c;
10
10
  --ss-success-color: #00b755;
@@ -74,7 +74,7 @@
74
74
  }
75
75
 
76
76
  &.ss-disabled {
77
- background-color: var(--ss-border-color);
77
+ background-color: var(--ss-disabled-color);
78
78
  cursor: not-allowed;
79
79
 
80
80
  .ss-values {
@@ -118,11 +118,27 @@
118
118
  white-space: nowrap;
119
119
  }
120
120
 
121
+ // Max is used to display max number of values
122
+ .ss-max {
123
+ display: flex;
124
+ user-select: none;
125
+ align-items: center;
126
+ width: fit-content;
127
+ font-size: 12px;
128
+ color: var(--ss-bg-color);
129
+ line-height: 1;
130
+ padding: var(--ss-spacing-s) var(--ss-spacing-m);
131
+ background-color: var(--ss-primary-color);
132
+ border-radius: var(--ss-border-radius);
133
+ }
134
+
135
+ // Used for single select selected value
121
136
  .ss-single {
122
137
  display: flex;
123
138
  margin: auto 0px auto var(--ss-spacing-s);
124
139
  }
125
140
 
141
+ // Used for the selected values
126
142
  .ss-value {
127
143
  display: flex;
128
144
  user-select: none;
@@ -231,6 +247,7 @@
231
247
  transition: transform var(--ss-animation-timing), opacity var(--ss-animation-timing);
232
248
  opacity: 0;
233
249
  transform: scaleY(0);
250
+ transform-origin: center top;
234
251
  overflow: hidden;
235
252
  z-index: 10000;
236
253
 
@@ -240,6 +257,7 @@
240
257
  }
241
258
 
242
259
  &.ss-open-above {
260
+ flex-direction: column-reverse;
243
261
  opacity: 1;
244
262
  transform: scaleY(1);
245
263
  transform-origin: center bottom;
@@ -456,26 +474,31 @@
456
474
  .ss-option {
457
475
  display: flex;
458
476
  padding: var(--ss-spacing-m) var(--ss-spacing-l) var(--ss-spacing-m) var(--ss-spacing-l);
477
+ color: var(--ss-font-color);
459
478
  cursor: pointer;
460
479
  user-select: none;
461
480
 
462
- &:hover,
463
- &.ss-highlighted {
481
+ &:hover {
482
+ color: var(--ss-bg-color);
483
+ background-color: var(--ss-primary-color);
484
+ }
485
+
486
+ &.ss-highlighted,
487
+ &:not(.ss-disabled).ss-selected {
464
488
  color: var(--ss-bg-color);
465
489
  background-color: var(--ss-primary-color);
466
490
  }
467
491
 
468
492
  &.ss-disabled {
469
493
  cursor: not-allowed;
470
- color: var(--ss-disabled-color);
471
- background-color: var(--ss-bg-color);
472
- }
494
+ background-color: var(--ss-disabled-color);
473
495
 
474
- &:not(.ss-disabled).ss-selected {
475
- color: var(--ss-font-color);
476
- background-color: var(--ss-highlight-color);
496
+ &:hover {
497
+ color: var(--ss-font-color);
498
+ }
477
499
  }
478
500
 
501
+ // Sub div wrapper for highlighting text
479
502
  .ss-search-highlight {
480
503
  background-color: var(--ss-highlight-color);
481
504
  }
@@ -117,6 +117,28 @@ describe('store module', () => {
117
117
  expect((data[0] as Option).selected).toBe(true)
118
118
  })
119
119
 
120
+ test('set data and set selected to empty string', () => {
121
+ let store = new Store('single', [
122
+ {
123
+ text: 'all',
124
+ value: '',
125
+ },
126
+ {
127
+ text: 'Value 1',
128
+ value: '1',
129
+ selected: true,
130
+ },
131
+ ])
132
+ store.setSelectedBy('value', [''])
133
+
134
+ let data = store.getData()
135
+
136
+ // Make sure data has one item and that it has the correct text
137
+ expect(data.length).toBe(2)
138
+ expect((data[0] as Option).text).toBe('all')
139
+ expect((data[0] as Option).selected).toBe(true)
140
+ })
141
+
120
142
  test('set data and set selected by value multiple for single element', () => {
121
143
  let store = new Store('single', [
122
144
  {
@@ -33,7 +33,7 @@ export default class SlimSelect {
33
33
  getData(): DataArray;
34
34
  setData(data: DataArrayPartial): void;
35
35
  getSelected(): string[];
36
- setSelected(value: string | string[]): void;
36
+ setSelected(value: string | string[], runAfterChange?: boolean): void;
37
37
  addOption(option: OptionOptional): void;
38
38
  open(): void;
39
39
  close(): void;
@@ -4,7 +4,7 @@ export interface Callbacks {
4
4
  open: () => void;
5
5
  close: () => void;
6
6
  addable?: (value: string) => Promise<OptionOptional | string> | OptionOptional | string;
7
- setSelected: (value: string[]) => void;
7
+ setSelected: (value: string[], runAfterChange: boolean) => void;
8
8
  addOption: (option: Option) => void;
9
9
  search: (search: string) => void;
10
10
  beforeChange?: (newVal: Option[], oldVal: Option[]) => boolean | void;
@@ -48,6 +48,7 @@ export default class Render {
48
48
  placeholder: string;
49
49
  values: string;
50
50
  single: string;
51
+ max: string;
51
52
  value: string;
52
53
  valueText: string;
53
54
  valueDelete: string;
@@ -89,6 +90,7 @@ export default class Render {
89
90
  disable(): void;
90
91
  open(): void;
91
92
  close(): void;
93
+ updateClassStyles(): void;
92
94
  mainDiv(): Main;
93
95
  mainFocus(trigger: boolean): void;
94
96
  placeholder(): HTMLDivElement;
@@ -112,5 +114,5 @@ export default class Render {
112
114
  moveContentAbove(): void;
113
115
  moveContentBelow(): void;
114
116
  ensureElementInView(container: HTMLElement, element: HTMLElement): void;
115
- putContent(el: HTMLElement, isOpen: boolean): 'up' | 'down';
117
+ putContent(): 'up' | 'down';
116
118
  }
@@ -1,25 +1,20 @@
1
1
  import { DataArray, DataArrayPartial, Optgroup, OptgroupOptional, Option } from './store';
2
2
  export default class Select {
3
3
  select: HTMLSelectElement;
4
- listen: boolean;
5
- onSelectChange?: (data: DataArrayPartial) => void;
6
4
  onValueChange?: (value: string[]) => void;
5
+ onClassChange?: (classes: string[]) => void;
6
+ onDisabledChange?: (disabled: boolean) => void;
7
+ onOptionsChange?: (data: DataArrayPartial) => void;
8
+ listen: boolean;
7
9
  private observer;
8
10
  constructor(select: HTMLSelectElement);
9
11
  enable(): void;
10
12
  disable(): void;
11
13
  hideUI(): void;
12
14
  showUI(): void;
13
- changeListen(on: boolean): void;
14
- addSelectChangeListener(func: (data: DataArrayPartial) => void): void;
15
- removeSelectChangeListener(): void;
16
- addValueChangeListener(func: (value: string[]) => void): void;
17
- removeValueChangeListener(): void;
15
+ changeListen(listen: boolean): void;
18
16
  valueChange(ev: Event): boolean;
19
- private observeWrapper;
20
- private addObserver;
21
- private connectObserver;
22
- private disconnectObserver;
17
+ private observeCall;
23
18
  getData(): DataArrayPartial;
24
19
  getDataFromOptgroup(optgroup: HTMLOptGroupElement): OptgroupOptional;
25
20
  getDataFromOption(option: HTMLOptionElement): Option;
@@ -8,7 +8,7 @@ export default class Settings {
8
8
  isOpen: boolean;
9
9
  triggerFocus: boolean;
10
10
  intervalMove: NodeJS.Timeout | null;
11
- isEnabled: boolean;
11
+ disabled: boolean;
12
12
  alwaysOpen: boolean;
13
13
  showSearch: boolean;
14
14
  searchPlaceholder: string;
@@ -26,5 +26,7 @@ export default class Settings {
26
26
  minSelected: number;
27
27
  maxSelected: number;
28
28
  timeoutDelay: number;
29
+ maxValuesShown: number;
30
+ maxValuesMessage: string;
29
31
  constructor(settings?: SettingsPartial);
30
32
  }