slim-select 2.13.1 → 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.
- package/README.md +184 -43
- package/dist/index.d.ts +304 -52
- package/dist/react/index.d.ts +304 -0
- package/dist/react/index.js +1549 -0
- package/dist/react/index.umd.js +22 -0
- package/dist/slimselect.cjs.js +1 -2030
- package/dist/slimselect.css +1 -1
- package/dist/slimselect.css.map +1 -1
- package/dist/slimselect.es.js +1224 -2012
- package/dist/slimselect.umd.js +1 -2036
- package/dist/vue/index.d.ts +304 -0
- package/dist/vue/index.js +1341 -0
- package/dist/vue/index.umd.js +1 -0
- package/index.html +1 -0
- package/package.json +58 -36
- package/release.js +0 -7
- package/src/slim-select/accessibility.test.ts +782 -0
- package/src/slim-select/classes.test.ts +7 -6
- package/src/slim-select/classes.ts +8 -6
- package/src/slim-select/helpers.test.ts +10 -8
- package/src/slim-select/index.test.ts +509 -14
- package/src/slim-select/index.ts +58 -31
- package/src/slim-select/react/index.tsx +9 -0
- package/src/slim-select/react/react.tsx +112 -0
- package/src/slim-select/render.test.ts +479 -119
- package/src/slim-select/render.ts +213 -91
- package/src/slim-select/select.test.ts +28 -10
- package/src/slim-select/select.ts +35 -12
- package/src/slim-select/settings.test.ts +19 -8
- package/src/slim-select/settings.ts +2 -4
- package/src/slim-select/slimselect.scss +30 -15
- package/src/slim-select/store.test.ts +13 -16
- package/src/slim-select/store.ts +97 -86
- package/src/slim-select/vue/index.ts +9 -0
- package/src/slim-select/vue/vue.test.ts +832 -0
- package/src/slim-select/{vue.vue → vue/vue.vue} +45 -23
- package/tsconfig.json +6 -4
- package/{vite.config.mts → vite.config.docs.mts} +10 -0
- package/vite.config.lib.mts +42 -0
- package/vite.config.react.mts +40 -0
- package/vite.config.vue.mts +39 -0
- package/vitest.config.ts +29 -0
- package/dist/classes.d.ts +0 -43
- package/dist/helpers.d.ts +0 -5
- package/dist/render.d.ts +0 -81
- package/dist/select.d.ts +0 -30
- package/dist/settings.d.ts +0 -35
- package/dist/slimselect.global.js +0 -2033
- package/dist/slimselect.js +0 -2036
- package/dist/slimselect.min.js +0 -1
- package/dist/slimselect.umd.min.js +0 -1
- package/dist/store.d.ts +0 -84
- package/jest.config.js +0 -14
- package/src/slim-select/rollup.config.mjs +0 -127
- package/src/slim-select/tsconfig.json +0 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { kebabCase } from './helpers'
|
|
2
|
-
import {
|
|
2
|
+
import { Optgroup, Option } from './store'
|
|
3
3
|
|
|
4
4
|
export default class Select {
|
|
5
5
|
public select: HTMLSelectElement
|
|
@@ -8,7 +8,7 @@ export default class Select {
|
|
|
8
8
|
public onValueChange?: (value: Option[]) => void
|
|
9
9
|
public onClassChange?: (classes: string[]) => void
|
|
10
10
|
public onDisabledChange?: (disabled: boolean) => void
|
|
11
|
-
public onOptionsChange?: (data:
|
|
11
|
+
public onOptionsChange?: (data: (Option | Optgroup)[]) => void
|
|
12
12
|
|
|
13
13
|
// Change observers
|
|
14
14
|
public listen: boolean = false
|
|
@@ -41,13 +41,36 @@ export default class Select {
|
|
|
41
41
|
|
|
42
42
|
public hideUI(): void {
|
|
43
43
|
this.select.tabIndex = -1
|
|
44
|
-
|
|
44
|
+
// Visually hide but keep focusable for form validation
|
|
45
|
+
// Use 1px dimensions so browser validation popup can display
|
|
46
|
+
this.select.style.position = 'absolute'
|
|
47
|
+
this.select.style.width = '1px'
|
|
48
|
+
this.select.style.height = '1px'
|
|
49
|
+
this.select.style.opacity = '0'
|
|
50
|
+
this.select.style.overflow = 'hidden'
|
|
51
|
+
this.select.style.pointerEvents = 'none'
|
|
52
|
+
// Remove any spacing or borders that could affect layout
|
|
53
|
+
this.select.style.margin = '0'
|
|
54
|
+
this.select.style.padding = '0'
|
|
55
|
+
this.select.style.borderWidth = '0'
|
|
56
|
+
// Clip to completely hide the 1px
|
|
57
|
+
this.select.style.clip = 'rect(0 0 0 0)'
|
|
45
58
|
this.select.setAttribute('aria-hidden', 'true')
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
public showUI(): void {
|
|
49
62
|
this.select.removeAttribute('tabindex')
|
|
50
|
-
|
|
63
|
+
// Reset visual hiding styles
|
|
64
|
+
this.select.style.position = ''
|
|
65
|
+
this.select.style.width = ''
|
|
66
|
+
this.select.style.height = ''
|
|
67
|
+
this.select.style.opacity = ''
|
|
68
|
+
this.select.style.overflow = ''
|
|
69
|
+
this.select.style.pointerEvents = ''
|
|
70
|
+
this.select.style.margin = ''
|
|
71
|
+
this.select.style.padding = ''
|
|
72
|
+
this.select.style.borderWidth = ''
|
|
73
|
+
this.select.style.clip = ''
|
|
51
74
|
this.select.removeAttribute('aria-hidden')
|
|
52
75
|
}
|
|
53
76
|
|
|
@@ -109,7 +132,7 @@ export default class Select {
|
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
if (m.type === 'childList') {
|
|
112
|
-
for (const n of m.addedNodes) {
|
|
135
|
+
for (const n of Array.from(m.addedNodes)) {
|
|
113
136
|
if (n.nodeName === 'OPTION' && (<HTMLOptionElement>n).value === this.select.value) {
|
|
114
137
|
// we added a new option that's now the select value
|
|
115
138
|
this.select.dispatchEvent(new Event('change'))
|
|
@@ -149,8 +172,8 @@ export default class Select {
|
|
|
149
172
|
}
|
|
150
173
|
|
|
151
174
|
// From the select element pull optgroup and options into data
|
|
152
|
-
public getData():
|
|
153
|
-
let data = []
|
|
175
|
+
public getData(): (Option | Optgroup)[] {
|
|
176
|
+
let data = []
|
|
154
177
|
|
|
155
178
|
// Loop through nodes and get data
|
|
156
179
|
const nodes = this.select.childNodes as any as HTMLOptGroupElement[] | HTMLOptionElement[]
|
|
@@ -169,7 +192,7 @@ export default class Select {
|
|
|
169
192
|
return data
|
|
170
193
|
}
|
|
171
194
|
|
|
172
|
-
public getDataFromOptgroup(optgroup: HTMLOptGroupElement):
|
|
195
|
+
public getDataFromOptgroup(optgroup: HTMLOptGroupElement): Optgroup {
|
|
173
196
|
let data = {
|
|
174
197
|
id: optgroup.id,
|
|
175
198
|
label: optgroup.label,
|
|
@@ -177,7 +200,7 @@ export default class Select {
|
|
|
177
200
|
selectAllText: optgroup.dataset ? optgroup.dataset.selectalltext : 'Select all',
|
|
178
201
|
closable: optgroup.dataset ? optgroup.dataset.closable : 'off',
|
|
179
202
|
options: []
|
|
180
|
-
} as
|
|
203
|
+
} as Optgroup
|
|
181
204
|
|
|
182
205
|
const options = optgroup.childNodes as any as HTMLOptionElement[]
|
|
183
206
|
for (const o of options) {
|
|
@@ -329,7 +352,7 @@ export default class Select {
|
|
|
329
352
|
this.changeListen(true)
|
|
330
353
|
}
|
|
331
354
|
|
|
332
|
-
public updateOptions(data:
|
|
355
|
+
public updateOptions(data: (Option | Optgroup)[]): void {
|
|
333
356
|
// Stop listening to changes
|
|
334
357
|
this.changeListen(false)
|
|
335
358
|
|
|
@@ -364,8 +387,8 @@ export default class Select {
|
|
|
364
387
|
optgroupEl.dataset.closable = optgroup.closable
|
|
365
388
|
}
|
|
366
389
|
if (optgroup.options) {
|
|
367
|
-
for (const
|
|
368
|
-
optgroupEl.appendChild(this.createOption(
|
|
390
|
+
for (const option of optgroup.options) {
|
|
391
|
+
optgroupEl.appendChild(this.createOption(option as Option))
|
|
369
392
|
}
|
|
370
393
|
}
|
|
371
394
|
return optgroupEl
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
import { describe, expect, test } from '
|
|
3
|
+
import { describe, expect, test, beforeEach } from 'vitest'
|
|
4
4
|
import Settings from './settings'
|
|
5
5
|
import Select from './select'
|
|
6
6
|
import { Option, Optgroup } from './store'
|
|
@@ -109,12 +109,22 @@ describe('Settings module', () => {
|
|
|
109
109
|
test('showUI', () => {
|
|
110
110
|
select.select.setAttribute('tabindex', '1')
|
|
111
111
|
select.select.setAttribute('aria-hidden', 'true')
|
|
112
|
-
select.select.style.
|
|
112
|
+
select.select.style.position = 'absolute'
|
|
113
|
+
select.select.style.width = '0'
|
|
114
|
+
select.select.style.opacity = '0'
|
|
115
|
+
select.select.style.margin = '0'
|
|
116
|
+
select.select.style.padding = '0'
|
|
117
|
+
select.select.style.borderWidth = '0'
|
|
113
118
|
|
|
114
119
|
select.showUI()
|
|
115
120
|
expect(select.select.getAttribute('tabindex')).toBeFalsy()
|
|
116
121
|
expect(select.select.getAttribute('aria-hidden')).toBeFalsy()
|
|
117
|
-
expect(select.select.style.
|
|
122
|
+
expect(select.select.style.position).toBeFalsy()
|
|
123
|
+
expect(select.select.style.width).toBeFalsy()
|
|
124
|
+
expect(select.select.style.opacity).toBeFalsy()
|
|
125
|
+
expect(select.select.style.margin).toBeFalsy()
|
|
126
|
+
expect(select.select.style.padding).toBeFalsy()
|
|
127
|
+
// borderWidth gets reset
|
|
118
128
|
})
|
|
119
129
|
|
|
120
130
|
describe('createOptgroup', () => {
|
|
@@ -197,12 +207,13 @@ describe('Settings module', () => {
|
|
|
197
207
|
|
|
198
208
|
test('malicious text is inserted with innerText', () => {
|
|
199
209
|
// decoded text: <img src=x onerror=alert(1)></img>
|
|
200
|
-
const str =
|
|
210
|
+
const str =
|
|
211
|
+
'<img src=x onerror=alert(1)></img>'
|
|
201
212
|
// const str = 'opt'
|
|
202
|
-
const decode = (string: string|null) => {
|
|
203
|
-
if(string === null) return ''
|
|
204
|
-
const doc = new DOMParser().parseFromString(string,
|
|
205
|
-
return doc.documentElement.textContent
|
|
213
|
+
const decode = (string: string | null) => {
|
|
214
|
+
if (string === null) return ''
|
|
215
|
+
const doc = new DOMParser().parseFromString(string, 'text/html')
|
|
216
|
+
return doc.documentElement.textContent
|
|
206
217
|
}
|
|
207
218
|
const option = new Option({ text: str })
|
|
208
219
|
const optionElement = select.createOption(option)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { generateID } from './helpers'
|
|
2
2
|
|
|
3
|
-
export type SettingsPartial = Partial<Settings>
|
|
4
|
-
|
|
5
3
|
export default class Settings {
|
|
6
4
|
public id: string = '' // Primary ID for the select
|
|
7
5
|
public style: string = '' // Style attribute from the select element
|
|
@@ -11,7 +9,7 @@ export default class Settings {
|
|
|
11
9
|
public isMultiple: boolean = false
|
|
12
10
|
public isOpen: boolean = false
|
|
13
11
|
public isFullOpen: boolean = false
|
|
14
|
-
public intervalMove:
|
|
12
|
+
public intervalMove: ReturnType<typeof setInterval> | null = null
|
|
15
13
|
|
|
16
14
|
// Fields set from constructor
|
|
17
15
|
public disabled: boolean
|
|
@@ -39,7 +37,7 @@ export default class Settings {
|
|
|
39
37
|
public maxValuesMessage: string
|
|
40
38
|
public addableText: string
|
|
41
39
|
|
|
42
|
-
constructor(settings?:
|
|
40
|
+
constructor(settings?: Partial<Settings>) {
|
|
43
41
|
if (!settings) {
|
|
44
42
|
settings = {}
|
|
45
43
|
}
|
|
@@ -92,11 +92,11 @@
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
&.ss-
|
|
95
|
+
&.ss-dir-above {
|
|
96
96
|
border-top-left-radius: 0px;
|
|
97
97
|
border-top-right-radius: 0px;
|
|
98
98
|
}
|
|
99
|
-
&.ss-
|
|
99
|
+
&.ss-dir-below {
|
|
100
100
|
border-bottom-left-radius: 0px;
|
|
101
101
|
border-bottom-right-radius: 0px;
|
|
102
102
|
}
|
|
@@ -106,14 +106,17 @@
|
|
|
106
106
|
flex-wrap: wrap;
|
|
107
107
|
gap: var(--ss-spacing-m);
|
|
108
108
|
flex: 1 1 100%;
|
|
109
|
+
min-width: 0; // Allow container to shrink
|
|
110
|
+
max-width: 100%; // Ensure container doesn't exceed parent
|
|
111
|
+
overflow: hidden; // Prevent overflow
|
|
109
112
|
|
|
110
113
|
.ss-placeholder {
|
|
111
|
-
display:
|
|
114
|
+
display: block; // Use block for proper ellipsis
|
|
115
|
+
width: 100%;
|
|
116
|
+
min-width: 0; // Allow placeholder to shrink
|
|
112
117
|
padding: var(--ss-spacing-s) var(--ss-spacing-m) var(--ss-spacing-s) var(--ss-spacing-m);
|
|
113
118
|
margin: auto 0px auto 0px;
|
|
114
119
|
line-height: 1em;
|
|
115
|
-
align-items: center;
|
|
116
|
-
width: 100%;
|
|
117
120
|
color: var(--ss-font-placeholder-color);
|
|
118
121
|
overflow: hidden;
|
|
119
122
|
text-overflow: ellipsis;
|
|
@@ -136,8 +139,14 @@
|
|
|
136
139
|
|
|
137
140
|
// Used for single select selected value
|
|
138
141
|
.ss-single {
|
|
139
|
-
display:
|
|
142
|
+
display: block; // Use block for proper ellipsis
|
|
140
143
|
margin: auto 0px auto var(--ss-spacing-s);
|
|
144
|
+
min-width: 0; // Allow flex item to shrink below content size
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
text-overflow: ellipsis;
|
|
147
|
+
white-space: nowrap; // Prevent text wrapping
|
|
148
|
+
flex: 1 1 auto; // Allow it to grow but also shrink
|
|
149
|
+
max-width: calc(100% - 20px); // Leave space for arrow and padding
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
// Used for the selected values
|
|
@@ -146,6 +155,7 @@
|
|
|
146
155
|
user-select: none;
|
|
147
156
|
align-items: center;
|
|
148
157
|
width: fit-content;
|
|
158
|
+
max-width: 100%; // Ensure value doesn't exceed container width
|
|
149
159
|
background-color: var(--ss-primary-color);
|
|
150
160
|
border-radius: var(--ss-border-radius);
|
|
151
161
|
animation-name: ss-valueIn;
|
|
@@ -160,10 +170,15 @@
|
|
|
160
170
|
}
|
|
161
171
|
|
|
162
172
|
.ss-value-text {
|
|
173
|
+
flex: 1; // Take up available space in flex container
|
|
174
|
+
min-width: 0; // Allow text to shrink
|
|
163
175
|
font-size: 12px;
|
|
164
|
-
color: var(--ss-bg-color);
|
|
165
176
|
line-height: 1;
|
|
177
|
+
color: var(--ss-bg-color);
|
|
166
178
|
padding: var(--ss-spacing-s) var(--ss-spacing-m);
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
text-overflow: ellipsis;
|
|
181
|
+
white-space: nowrap;
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
.ss-value-delete {
|
|
@@ -252,7 +267,6 @@
|
|
|
252
267
|
opacity var(--ss-animation-timing);
|
|
253
268
|
opacity: 0;
|
|
254
269
|
transform: scaleY(0);
|
|
255
|
-
transform-origin: center top;
|
|
256
270
|
overflow: hidden;
|
|
257
271
|
z-index: 10000;
|
|
258
272
|
|
|
@@ -265,23 +279,24 @@
|
|
|
265
279
|
position: fixed;
|
|
266
280
|
}
|
|
267
281
|
|
|
268
|
-
&.ss-
|
|
269
|
-
flex-direction: column-reverse;
|
|
270
|
-
opacity: 1;
|
|
271
|
-
transform: scaleY(1);
|
|
282
|
+
&.ss-dir-above {
|
|
272
283
|
transform-origin: center bottom;
|
|
284
|
+
flex-direction: column-reverse;
|
|
273
285
|
border-top-left-radius: var(--ss-border-radius);
|
|
274
286
|
border-top-right-radius: var(--ss-border-radius);
|
|
275
287
|
}
|
|
276
288
|
|
|
277
|
-
&.ss-
|
|
278
|
-
opacity: 1;
|
|
279
|
-
transform: scaleY(1);
|
|
289
|
+
&.ss-dir-below {
|
|
280
290
|
transform-origin: center top;
|
|
281
291
|
border-bottom-left-radius: var(--ss-border-radius);
|
|
282
292
|
border-bottom-right-radius: var(--ss-border-radius);
|
|
283
293
|
}
|
|
284
294
|
|
|
295
|
+
&.ss-open {
|
|
296
|
+
opacity: 1;
|
|
297
|
+
transform: scaleY(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
285
300
|
.ss-search {
|
|
286
301
|
flex: 0 1 auto;
|
|
287
302
|
display: flex;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
import { describe, expect, test } from '
|
|
4
|
-
import Store, {
|
|
3
|
+
import { describe, expect, test, beforeAll, beforeEach } from 'vitest'
|
|
4
|
+
import Store, { Optgroup, Option } from './store'
|
|
5
5
|
|
|
6
6
|
describe('store module', () => {
|
|
7
7
|
describe('constructor', () => {
|
|
@@ -208,17 +208,17 @@ describe('store module', () => {
|
|
|
208
208
|
})
|
|
209
209
|
|
|
210
210
|
test('invalid data returns error', () => {
|
|
211
|
-
const invalidData = { test: true }
|
|
211
|
+
const invalidData = { test: true } as any
|
|
212
212
|
|
|
213
|
-
const res = store.validateDataArray(invalidData
|
|
213
|
+
const res = store.validateDataArray(invalidData)
|
|
214
214
|
expect(res).toBeInstanceOf(Error)
|
|
215
215
|
expect(res?.message).toBe('Data must be an array')
|
|
216
216
|
})
|
|
217
217
|
|
|
218
218
|
test('single invalid data entry returns error', () => {
|
|
219
|
-
const invalidData = [{ name: 'this is invalid' }]
|
|
219
|
+
const invalidData = [{ name: 'this is invalid' }] as any
|
|
220
220
|
|
|
221
|
-
const res = store.validateDataArray(invalidData
|
|
221
|
+
const res = store.validateDataArray(invalidData)
|
|
222
222
|
expect(res).toBeInstanceOf(Error)
|
|
223
223
|
expect(res?.message).toBe('Data object must be a valid optgroup or option')
|
|
224
224
|
})
|
|
@@ -231,9 +231,9 @@ describe('store module', () => {
|
|
|
231
231
|
{
|
|
232
232
|
text: 'valid option'
|
|
233
233
|
}
|
|
234
|
-
]
|
|
234
|
+
] as any
|
|
235
235
|
|
|
236
|
-
const res = store.validateDataArray(invalidData
|
|
236
|
+
const res = store.validateDataArray(invalidData)
|
|
237
237
|
expect(res).toBeInstanceOf(Error)
|
|
238
238
|
expect(res?.message).toBe('Data object must be a valid optgroup or option')
|
|
239
239
|
})
|
|
@@ -246,9 +246,9 @@ describe('store module', () => {
|
|
|
246
246
|
{
|
|
247
247
|
name: 'this is invalid'
|
|
248
248
|
}
|
|
249
|
-
]
|
|
249
|
+
] as any
|
|
250
250
|
|
|
251
|
-
const res = store.validateDataArray(invalidData
|
|
251
|
+
const res = store.validateDataArray(invalidData)
|
|
252
252
|
expect(res).toBeInstanceOf(Error)
|
|
253
253
|
expect(res?.message).toBe('Data object must be a valid optgroup or option')
|
|
254
254
|
})
|
|
@@ -263,9 +263,9 @@ describe('store module', () => {
|
|
|
263
263
|
}
|
|
264
264
|
]
|
|
265
265
|
}
|
|
266
|
-
]
|
|
266
|
+
] as any
|
|
267
267
|
|
|
268
|
-
const res = store.validateDataArray(invalidData
|
|
268
|
+
const res = store.validateDataArray(invalidData)
|
|
269
269
|
expect(res).toBeInstanceOf(Error)
|
|
270
270
|
expect(res?.message).toBe('Option must have a text')
|
|
271
271
|
})
|
|
@@ -389,10 +389,7 @@ describe('store module', () => {
|
|
|
389
389
|
})
|
|
390
390
|
|
|
391
391
|
test("invalid data get's ignored", () => {
|
|
392
|
-
const res = store.partialToFullData([
|
|
393
|
-
{ error: 'this is invalid' } as unknown as DataObjectPartial,
|
|
394
|
-
{ text: 'valid' }
|
|
395
|
-
])
|
|
392
|
+
const res = store.partialToFullData([{ error: 'this is invalid' } as any, { text: 'valid' }])
|
|
396
393
|
expect(res).toBeInstanceOf(Array)
|
|
397
394
|
expect(res).toHaveLength(1)
|
|
398
395
|
|