slim-select 2.10.0 → 2.12.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/.nvmrc +1 -1
- package/README.md +1 -0
- package/dist/render.d.ts +1 -0
- package/dist/slimselect.cjs.js +35 -6
- package/dist/slimselect.es.js +35 -6
- package/dist/slimselect.global.js +35 -6
- package/dist/slimselect.js +35 -6
- package/dist/slimselect.min.js +1 -1
- package/dist/slimselect.umd.js +35 -6
- package/dist/slimselect.umd.min.js +1 -1
- package/dist/store.d.ts +2 -0
- package/package.json +29 -29
- package/src/slim-select/render.test.ts +99 -1
- package/src/slim-select/render.ts +39 -5
- package/src/slim-select/select.ts +3 -3
- package/src/slim-select/store.ts +3 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slim-select",
|
|
3
3
|
"description": "Slim advanced select dropdown",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.12.0",
|
|
5
5
|
"author": "Brian Voelker <brian@webiswhatido.com> (http://webiswhatido.com)",
|
|
6
6
|
"homepage": "https://slimselectjs.com",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"sass": "src/slim-select/slimselect.scss",
|
|
29
29
|
"exports": {
|
|
30
30
|
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
31
32
|
"import": "./dist/slimselect.js",
|
|
32
|
-
"require": "./dist/slimselect.umd.js"
|
|
33
|
-
"types": "./dist/index.d.ts"
|
|
33
|
+
"require": "./dist/slimselect.umd.js"
|
|
34
34
|
},
|
|
35
35
|
"./*": {
|
|
36
36
|
"types": "./dist/*.d.ts"
|
|
@@ -51,45 +51,45 @@
|
|
|
51
51
|
"test:coverage": "jest --coverage"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@jest/globals": "^
|
|
54
|
+
"@jest/globals": "^30.0.2",
|
|
55
55
|
"@rollup/plugin-babel": "^6.0.4",
|
|
56
|
-
"@rollup/plugin-commonjs": "^28.0.
|
|
57
|
-
"@rollup/plugin-node-resolve": "^
|
|
58
|
-
"@rollup/plugin-replace": "^6.0.
|
|
56
|
+
"@rollup/plugin-commonjs": "^28.0.6",
|
|
57
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
58
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
59
59
|
"@rollup/plugin-terser": "^0.4.4",
|
|
60
|
-
"@rollup/plugin-typescript": "^12.1.
|
|
60
|
+
"@rollup/plugin-typescript": "^12.1.3",
|
|
61
61
|
"@testing-library/jest-dom": "^6.6.3",
|
|
62
|
-
"@testing-library/user-event": "^14.
|
|
62
|
+
"@testing-library/user-event": "^14.6.1",
|
|
63
63
|
"@types/downloadjs": "^1.4.6",
|
|
64
|
-
"@types/jest": "^
|
|
65
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
66
|
-
"@typescript-eslint/parser": "^8.
|
|
67
|
-
"@vitejs/plugin-vue": "^5.2.
|
|
64
|
+
"@types/jest": "^30.0.0",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
|
66
|
+
"@typescript-eslint/parser": "^8.34.1",
|
|
67
|
+
"@vitejs/plugin-vue": "^5.2.4",
|
|
68
68
|
"clipboard": "^2.0.11",
|
|
69
69
|
"downloadjs": "^1.4.7",
|
|
70
|
-
"eslint": "^9.
|
|
71
|
-
"eslint-config-prettier": "^
|
|
72
|
-
"eslint-plugin-prettier": "^5.
|
|
70
|
+
"eslint": "^9.29.0",
|
|
71
|
+
"eslint-config-prettier": "^10.1.5",
|
|
72
|
+
"eslint-plugin-prettier": "^5.5.0",
|
|
73
73
|
"identity-obj-proxy": "^3.0.0",
|
|
74
|
-
"jest": "^
|
|
74
|
+
"jest": "^30.0.2",
|
|
75
75
|
"jest-canvas-mock": "^2.5.0",
|
|
76
|
-
"jest-environment-jsdom": "^
|
|
77
|
-
"prettier": "^3.
|
|
78
|
-
"prismjs": "^1.
|
|
76
|
+
"jest-environment-jsdom": "^30.0.2",
|
|
77
|
+
"prettier": "^3.5.3",
|
|
78
|
+
"prismjs": "^1.30.0",
|
|
79
79
|
"rimraf": "^6.0.1",
|
|
80
|
-
"rollup": "^4.
|
|
80
|
+
"rollup": "^4.44.0",
|
|
81
81
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
82
82
|
"rollup-plugin-typescript2": "^0.36.0",
|
|
83
83
|
"rollup-plugin-vue": "^6.0.0",
|
|
84
|
-
"sass": "^1.
|
|
85
|
-
"ts-jest": "^29.
|
|
84
|
+
"sass": "^1.89.2",
|
|
85
|
+
"ts-jest": "^29.4.0",
|
|
86
86
|
"tslib": "^2.8.1",
|
|
87
|
-
"typescript": "^5.
|
|
88
|
-
"vite": "^
|
|
89
|
-
"vite-plugin-dts": "^4.
|
|
90
|
-
"vue": "^3.5.
|
|
91
|
-
"vue-router": "^4.
|
|
92
|
-
"vue-tsc": "^2.
|
|
87
|
+
"typescript": "^5.8.3",
|
|
88
|
+
"vite": "^6.3.5",
|
|
89
|
+
"vite-plugin-dts": "^4.5.4",
|
|
90
|
+
"vue": "^3.5.17",
|
|
91
|
+
"vue-router": "^4.5.1",
|
|
92
|
+
"vue-tsc": "^2.2.10",
|
|
93
93
|
"vuex": "^4.0.2"
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -200,7 +200,7 @@ describe('render module', () => {
|
|
|
200
200
|
|
|
201
201
|
expect(render.main.main.role).toBe('combobox')
|
|
202
202
|
expect(render.main.main.getAttribute('aria-haspopup')).toBe('listbox')
|
|
203
|
-
expect(render.main.main.getAttribute('aria-controls')).toBe(render.content.main.id)
|
|
203
|
+
expect(render.main.main.getAttribute('aria-controls')).toBe(render.content.main.dataset.id ?? '')
|
|
204
204
|
expect(render.main.main.getAttribute('aria-expanded')).toBe('false')
|
|
205
205
|
expect(render.content.main.getAttribute('role')).toBe('listbox')
|
|
206
206
|
})
|
|
@@ -1142,6 +1142,104 @@ describe('render module', () => {
|
|
|
1142
1142
|
})
|
|
1143
1143
|
})
|
|
1144
1144
|
|
|
1145
|
+
describe('range selection', () => {
|
|
1146
|
+
let render: Render
|
|
1147
|
+
let afterChangeMock: jest.Mock
|
|
1148
|
+
|
|
1149
|
+
beforeEach(() => {
|
|
1150
|
+
// create a new store with 3 options
|
|
1151
|
+
const store = new Store('multiple', [
|
|
1152
|
+
{ text: 'test1', value: 'test1' },
|
|
1153
|
+
{ text: 'test2', value: 'test2' },
|
|
1154
|
+
{ text: 'test3', value: 'test3' }
|
|
1155
|
+
])
|
|
1156
|
+
|
|
1157
|
+
const settings = new Settings()
|
|
1158
|
+
const classes = new CssClasses()
|
|
1159
|
+
|
|
1160
|
+
afterChangeMock = jest.fn(() => {})
|
|
1161
|
+
|
|
1162
|
+
const callbacks = {
|
|
1163
|
+
open: () => {},
|
|
1164
|
+
close: () => {},
|
|
1165
|
+
addSelected: () => {},
|
|
1166
|
+
setSelected: (value) => {
|
|
1167
|
+
store.setSelectedBy('id', typeof value === 'string' ? [value] : value)
|
|
1168
|
+
},
|
|
1169
|
+
addOption: () => {},
|
|
1170
|
+
search: () => {},
|
|
1171
|
+
afterChange: afterChangeMock
|
|
1172
|
+
} as Callbacks
|
|
1173
|
+
|
|
1174
|
+
render = new Render(settings, classes, store, callbacks)
|
|
1175
|
+
render.settings.isMultiple = true
|
|
1176
|
+
render.settings.closeOnSelect = false
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
test('click holding shift key selects range from last clicked to current', () => {
|
|
1180
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1181
|
+
|
|
1182
|
+
const opts = render.getOptions(false, false, true)
|
|
1183
|
+
expect(opts).toHaveLength(3)
|
|
1184
|
+
|
|
1185
|
+
// click on the first option
|
|
1186
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1187
|
+
expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1188
|
+
|
|
1189
|
+
// click on the last option holding the shift key
|
|
1190
|
+
opts[2].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
|
|
1191
|
+
|
|
1192
|
+
// check that also the middle value is selected
|
|
1193
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1194
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1195
|
+
expect.objectContaining({ value: 'test2' }),
|
|
1196
|
+
expect.objectContaining({ value: 'test3' })
|
|
1197
|
+
])
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
test('click holding shift key selects range from current to last clicked', () => {
|
|
1201
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1202
|
+
|
|
1203
|
+
const opts = render.getOptions(false, false, true)
|
|
1204
|
+
expect(opts).toHaveLength(3)
|
|
1205
|
+
|
|
1206
|
+
// click on the last option
|
|
1207
|
+
opts[2].dispatchEvent(new MouseEvent('click'))
|
|
1208
|
+
expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test3' })])
|
|
1209
|
+
|
|
1210
|
+
// click on the first option holding the shift key
|
|
1211
|
+
opts[0].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
|
|
1212
|
+
|
|
1213
|
+
// check that also the middle value is selected
|
|
1214
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1215
|
+
expect.objectContaining({ value: 'test3' }),
|
|
1216
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1217
|
+
expect.objectContaining({ value: 'test2' })
|
|
1218
|
+
])
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
test('range selection is ignored if range length is greater than maxSelected', () => {
|
|
1222
|
+
render.settings.maxSelected = 2
|
|
1223
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1224
|
+
|
|
1225
|
+
const opts = render.getOptions(false, false, true)
|
|
1226
|
+
expect(opts).toHaveLength(3)
|
|
1227
|
+
|
|
1228
|
+
// click on the first option
|
|
1229
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1230
|
+
expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1231
|
+
|
|
1232
|
+
// click on the last option holding the shift key
|
|
1233
|
+
opts[2].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
|
|
1234
|
+
|
|
1235
|
+
// check that the middle value is NOT selected
|
|
1236
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1237
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1238
|
+
expect.objectContaining({ value: 'test3' })
|
|
1239
|
+
])
|
|
1240
|
+
})
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1145
1243
|
describe('destroy', () => {
|
|
1146
1244
|
test('elements get removed', () => {
|
|
1147
1245
|
expect(render.main.main).toBeInstanceOf(HTMLDivElement)
|
|
@@ -50,6 +50,8 @@ export default class Render {
|
|
|
50
50
|
public settings: Settings
|
|
51
51
|
public store: Store
|
|
52
52
|
public callbacks: Callbacks
|
|
53
|
+
// Used to compute the range selection
|
|
54
|
+
private lastSelectedOption: Option | null
|
|
53
55
|
|
|
54
56
|
// Elements
|
|
55
57
|
public main: Main
|
|
@@ -63,6 +65,7 @@ export default class Render {
|
|
|
63
65
|
this.settings = settings
|
|
64
66
|
this.classes = classes
|
|
65
67
|
this.callbacks = callbacks
|
|
68
|
+
this.lastSelectedOption = null
|
|
66
69
|
|
|
67
70
|
this.main = this.mainDiv()
|
|
68
71
|
this.content = this.contentDiv()
|
|
@@ -162,7 +165,7 @@ export default class Render {
|
|
|
162
165
|
public updateAriaAttributes() {
|
|
163
166
|
this.main.main.role = 'combobox'
|
|
164
167
|
this.main.main.setAttribute('aria-haspopup', 'listbox')
|
|
165
|
-
this.main.main.setAttribute('aria-controls', this.content.main.id)
|
|
168
|
+
this.main.main.setAttribute('aria-controls', this.content.main.dataset.id ?? '')
|
|
166
169
|
this.main.main.setAttribute('aria-expanded', 'false')
|
|
167
170
|
this.content.main.setAttribute('role', 'listbox')
|
|
168
171
|
// do an aria-labelledby here maybe
|
|
@@ -517,6 +520,7 @@ export default class Render {
|
|
|
517
520
|
// Create delete div element
|
|
518
521
|
const deleteDiv = document.createElement('div')
|
|
519
522
|
deleteDiv.classList.add(this.classes.valueDelete)
|
|
523
|
+
deleteDiv.setAttribute('tabindex', '0') // Make the div focusable for tab navigation
|
|
520
524
|
|
|
521
525
|
// Add delete onclick event
|
|
522
526
|
deleteDiv.onclick = (e: Event) => {
|
|
@@ -584,6 +588,13 @@ export default class Render {
|
|
|
584
588
|
deleteDiv.appendChild(deleteSvg)
|
|
585
589
|
|
|
586
590
|
value.appendChild(deleteDiv)
|
|
591
|
+
|
|
592
|
+
// Add keydown event listener for keyboard navigation (Enter key)
|
|
593
|
+
deleteDiv.onkeydown = (e) => {
|
|
594
|
+
if (e.key === 'Enter') {
|
|
595
|
+
deleteDiv.click() // Trigger the click event when Enter is pressed
|
|
596
|
+
}
|
|
597
|
+
}
|
|
587
598
|
}
|
|
588
599
|
|
|
589
600
|
return value
|
|
@@ -983,6 +994,7 @@ export default class Render {
|
|
|
983
994
|
}
|
|
984
995
|
|
|
985
996
|
// Append individual options to div container
|
|
997
|
+
const fragment = document.createDocumentFragment()
|
|
986
998
|
for (const d of data) {
|
|
987
999
|
// Create optgroup
|
|
988
1000
|
if (d instanceof Optgroup) {
|
|
@@ -1146,17 +1158,18 @@ export default class Render {
|
|
|
1146
1158
|
// Loop through options
|
|
1147
1159
|
for (const o of d.options) {
|
|
1148
1160
|
optgroupEl.appendChild(this.option(o))
|
|
1161
|
+
fragment.appendChild(optgroupEl)
|
|
1149
1162
|
}
|
|
1150
|
-
|
|
1151
|
-
// Add optgroup to list
|
|
1152
|
-
this.content.list.appendChild(optgroupEl)
|
|
1153
1163
|
}
|
|
1154
1164
|
|
|
1155
1165
|
// Create option
|
|
1156
1166
|
if (d instanceof Option) {
|
|
1157
|
-
|
|
1167
|
+
fragment.appendChild(this.option(d as Option))
|
|
1158
1168
|
}
|
|
1159
1169
|
}
|
|
1170
|
+
|
|
1171
|
+
// Append fragment to list
|
|
1172
|
+
this.content.list.appendChild(fragment)
|
|
1160
1173
|
}
|
|
1161
1174
|
|
|
1162
1175
|
// Create option div element
|
|
@@ -1264,6 +1277,27 @@ export default class Render {
|
|
|
1264
1277
|
} else {
|
|
1265
1278
|
// If not selected after would add
|
|
1266
1279
|
after = before.concat(option)
|
|
1280
|
+
|
|
1281
|
+
// Handles range selection
|
|
1282
|
+
if (!this.settings.closeOnSelect) {
|
|
1283
|
+
if (e.shiftKey && this.lastSelectedOption) {
|
|
1284
|
+
const options = this.store.getDataOptions()
|
|
1285
|
+
let lastClickedOptionIndex = options.findIndex((o: Option) => o.id === this.lastSelectedOption!.id)
|
|
1286
|
+
let currentOptionIndex = options.findIndex((o: Option) => o.id === option.id)
|
|
1287
|
+
if (lastClickedOptionIndex >= 0 && currentOptionIndex >= 0) {
|
|
1288
|
+
// Select the range from the last clicked option to the current one, or vice versa.
|
|
1289
|
+
const startIndex = Math.min(lastClickedOptionIndex, currentOptionIndex)
|
|
1290
|
+
const endIndex = Math.max(lastClickedOptionIndex, currentOptionIndex)
|
|
1291
|
+
const afterRange = options.slice(startIndex, endIndex + 1)
|
|
1292
|
+
if (afterRange.length > 0 && afterRange.length < this.settings.maxSelected) {
|
|
1293
|
+
// Set the union between the current range and the previously selected values, removing duplicates
|
|
1294
|
+
after = before.concat(afterRange.filter((a) => !before.find((b) => b.id === a.id)))
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
} else if (!option.selected) {
|
|
1298
|
+
this.lastSelectedOption = option
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1267
1301
|
}
|
|
1268
1302
|
}
|
|
1269
1303
|
|
|
@@ -196,6 +196,7 @@ export default class Select {
|
|
|
196
196
|
value: option.value,
|
|
197
197
|
text: option.text,
|
|
198
198
|
html: option.dataset && option.dataset.html ? option.dataset.html : '',
|
|
199
|
+
defaultSelected: option.defaultSelected,
|
|
199
200
|
selected: option.selected,
|
|
200
201
|
display: option.style.display !== 'none',
|
|
201
202
|
disabled: option.disabled,
|
|
@@ -378,9 +379,8 @@ export default class Select {
|
|
|
378
379
|
if (info.html !== '') {
|
|
379
380
|
optionEl.setAttribute('data-html', info.html)
|
|
380
381
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
382
|
+
optionEl.defaultSelected = info.defaultSelected
|
|
383
|
+
optionEl.selected = info.selected // Explicitly set selected because defaultSelected: true sets selected to true
|
|
384
384
|
if (info.disabled) {
|
|
385
385
|
optionEl.disabled = true
|
|
386
386
|
}
|
package/src/slim-select/store.ts
CHANGED
|
@@ -49,6 +49,7 @@ export interface OptionOptional {
|
|
|
49
49
|
text: string // Required
|
|
50
50
|
html?: string
|
|
51
51
|
selected?: boolean
|
|
52
|
+
defaultSelected?: boolean
|
|
52
53
|
display?: boolean
|
|
53
54
|
disabled?: boolean
|
|
54
55
|
mandatory?: boolean
|
|
@@ -63,6 +64,7 @@ export class Option {
|
|
|
63
64
|
value: string
|
|
64
65
|
text: string
|
|
65
66
|
html: string
|
|
67
|
+
defaultSelected: boolean
|
|
66
68
|
selected: boolean
|
|
67
69
|
display: boolean
|
|
68
70
|
disabled: boolean
|
|
@@ -77,6 +79,7 @@ export class Option {
|
|
|
77
79
|
this.value = option.value === undefined ? option.text : option.value
|
|
78
80
|
this.text = option.text || ''
|
|
79
81
|
this.html = option.html || ''
|
|
82
|
+
this.defaultSelected = option.defaultSelected !== undefined ? option.defaultSelected : false
|
|
80
83
|
this.selected = option.selected !== undefined ? option.selected : false
|
|
81
84
|
this.display = option.display !== undefined ? option.display : true
|
|
82
85
|
this.disabled = option.disabled !== undefined ? option.disabled : false
|