@coreui/vue-pro 4.8.0-next.0 → 4.8.0-next.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.
- package/dist/components/index.d.ts +1 -0
- package/dist/components/multi-select/CMultiSelect.d.ts +35 -44
- package/dist/components/multi-select/CMultiSelectNativeSelect.d.ts +1 -1
- package/dist/components/multi-select/CMultiSelectOptions.d.ts +13 -11
- package/dist/components/multi-select/CMultiSelectSelection.d.ts +1 -1
- package/dist/components/multi-select/types.d.ts +14 -0
- package/dist/components/multi-select/utils.d.ts +6 -0
- package/dist/components/virtual-scroller/CVirtualScroller.d.ts +23 -0
- package/dist/components/virtual-scroller/index.d.ts +6 -0
- package/dist/index.es.js +203 -104
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +203 -102
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -3
- package/package.json +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/multi-select/CMultiSelect.ts +33 -89
- package/src/components/multi-select/CMultiSelectNativeSelect.ts +2 -1
- package/src/components/multi-select/CMultiSelectOptions.ts +31 -17
- package/src/components/multi-select/CMultiSelectSelection.ts +2 -1
- package/src/components/multi-select/types.ts +15 -0
- package/src/components/multi-select/utils.ts +92 -0
- package/src/components/virtual-scroller/CVirtualScroller.ts +109 -0
- package/src/components/virtual-scroller/index.ts +10 -0
- package/src/utils/index.ts +1 -3
- package/src/utils/getNextSibling.ts +0 -18
- package/src/utils/getPreviousSibling.ts +0 -18
package/dist/utils/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -6,27 +6,9 @@ import { CPicker } from '../picker'
|
|
|
6
6
|
import { CMultiSelectNativeSelect } from './CMultiSelectNativeSelect'
|
|
7
7
|
import { CMultiSelectOptions } from './CMultiSelectOptions'
|
|
8
8
|
import { CMultiSelectSelection } from './CMultiSelectSelection'
|
|
9
|
-
export interface Option {
|
|
10
|
-
disabled?: boolean
|
|
11
|
-
label?: string
|
|
12
|
-
options?: Option[]
|
|
13
|
-
order?: number
|
|
14
|
-
selected?: boolean
|
|
15
|
-
text: string
|
|
16
|
-
value: number | string
|
|
17
|
-
}
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
text: string
|
|
22
|
-
value: number | string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const flattenArray = (options: Option[]): Option[] => {
|
|
26
|
-
return options.reduce((acc: Option[], val: Option) => {
|
|
27
|
-
return acc.concat(Array.isArray(val.options) ? flattenArray(val.options) : val)
|
|
28
|
-
}, [])
|
|
29
|
-
}
|
|
10
|
+
import { filterOptionsList, flattenArray, selectOptions } from './utils'
|
|
11
|
+
import type { Option, SelectedOption } from './types'
|
|
30
12
|
|
|
31
13
|
const CMultiSelect = defineComponent({
|
|
32
14
|
name: 'CMultiSelect',
|
|
@@ -38,7 +20,6 @@ const CMultiSelect = defineComponent({
|
|
|
38
20
|
*/
|
|
39
21
|
cleaner: {
|
|
40
22
|
type: Boolean,
|
|
41
|
-
required: false,
|
|
42
23
|
default: true,
|
|
43
24
|
},
|
|
44
25
|
/**
|
|
@@ -46,7 +27,6 @@ const CMultiSelect = defineComponent({
|
|
|
46
27
|
*/
|
|
47
28
|
disabled: {
|
|
48
29
|
type: Boolean,
|
|
49
|
-
required: false,
|
|
50
30
|
default: false,
|
|
51
31
|
},
|
|
52
32
|
/**
|
|
@@ -101,7 +81,6 @@ const CMultiSelect = defineComponent({
|
|
|
101
81
|
multiple: {
|
|
102
82
|
type: Boolean,
|
|
103
83
|
default: true,
|
|
104
|
-
required: false,
|
|
105
84
|
},
|
|
106
85
|
/**
|
|
107
86
|
* List of option elements.
|
|
@@ -109,7 +88,6 @@ const CMultiSelect = defineComponent({
|
|
|
109
88
|
options: {
|
|
110
89
|
type: Array as PropType<Option[]>,
|
|
111
90
|
default: () => [],
|
|
112
|
-
required: false,
|
|
113
91
|
},
|
|
114
92
|
/**
|
|
115
93
|
* Sets maxHeight of options list.
|
|
@@ -119,7 +97,6 @@ const CMultiSelect = defineComponent({
|
|
|
119
97
|
optionsMaxHeight: {
|
|
120
98
|
type: [Number, String],
|
|
121
99
|
default: 'auto',
|
|
122
|
-
required: false,
|
|
123
100
|
},
|
|
124
101
|
/**
|
|
125
102
|
* Sets option style.
|
|
@@ -130,7 +107,6 @@ const CMultiSelect = defineComponent({
|
|
|
130
107
|
optionsStyle: {
|
|
131
108
|
type: String,
|
|
132
109
|
default: 'checkbox',
|
|
133
|
-
required: false,
|
|
134
110
|
validator: (value: string) => {
|
|
135
111
|
return ['checkbox', 'text'].includes(value)
|
|
136
112
|
},
|
|
@@ -143,7 +119,6 @@ const CMultiSelect = defineComponent({
|
|
|
143
119
|
placeholder: {
|
|
144
120
|
type: String,
|
|
145
121
|
default: 'Select...',
|
|
146
|
-
required: false,
|
|
147
122
|
},
|
|
148
123
|
/**
|
|
149
124
|
* Enables search input element.
|
|
@@ -151,7 +126,6 @@ const CMultiSelect = defineComponent({
|
|
|
151
126
|
search: {
|
|
152
127
|
type: [Boolean, String],
|
|
153
128
|
default: true,
|
|
154
|
-
required: false,
|
|
155
129
|
validator: (value: boolean | string) => {
|
|
156
130
|
if (typeof value == 'string') {
|
|
157
131
|
return ['external'].includes(value)
|
|
@@ -168,7 +142,6 @@ const CMultiSelect = defineComponent({
|
|
|
168
142
|
searchNoResultsLabel: {
|
|
169
143
|
type: String,
|
|
170
144
|
default: 'no items',
|
|
171
|
-
required: false,
|
|
172
145
|
},
|
|
173
146
|
/**
|
|
174
147
|
* Enables select all button.
|
|
@@ -177,7 +150,6 @@ const CMultiSelect = defineComponent({
|
|
|
177
150
|
*/
|
|
178
151
|
selectAll: {
|
|
179
152
|
type: Boolean,
|
|
180
|
-
required: false,
|
|
181
153
|
default: true,
|
|
182
154
|
},
|
|
183
155
|
/**
|
|
@@ -187,7 +159,6 @@ const CMultiSelect = defineComponent({
|
|
|
187
159
|
*/
|
|
188
160
|
selectAllLabel: {
|
|
189
161
|
type: String,
|
|
190
|
-
required: false,
|
|
191
162
|
default: 'Select all options',
|
|
192
163
|
},
|
|
193
164
|
/**
|
|
@@ -199,7 +170,6 @@ const CMultiSelect = defineComponent({
|
|
|
199
170
|
selectionType: {
|
|
200
171
|
type: String,
|
|
201
172
|
default: 'tags',
|
|
202
|
-
required: false,
|
|
203
173
|
validator: (value: string) => {
|
|
204
174
|
return ['counter', 'tags', 'text'].includes(value)
|
|
205
175
|
},
|
|
@@ -212,7 +182,6 @@ const CMultiSelect = defineComponent({
|
|
|
212
182
|
selectionTypeCounterText: {
|
|
213
183
|
type: String,
|
|
214
184
|
default: 'item(s) selected',
|
|
215
|
-
required: false,
|
|
216
185
|
},
|
|
217
186
|
/**
|
|
218
187
|
* Size the component small or large.
|
|
@@ -221,7 +190,6 @@ const CMultiSelect = defineComponent({
|
|
|
221
190
|
*/
|
|
222
191
|
size: {
|
|
223
192
|
type: String,
|
|
224
|
-
required: false,
|
|
225
193
|
validator: (value: string) => {
|
|
226
194
|
return ['sm', 'lg'].includes(value)
|
|
227
195
|
},
|
|
@@ -246,6 +214,12 @@ const CMultiSelect = defineComponent({
|
|
|
246
214
|
* @since 4.6.0
|
|
247
215
|
*/
|
|
248
216
|
valid: Boolean,
|
|
217
|
+
/**
|
|
218
|
+
* Enable virtual scroller for the options list.
|
|
219
|
+
*
|
|
220
|
+
* @since 4.8.0
|
|
221
|
+
*/
|
|
222
|
+
virtualScroller: Boolean,
|
|
249
223
|
/**
|
|
250
224
|
* Toggle the visibility of multi select dropdown.
|
|
251
225
|
*
|
|
@@ -254,7 +228,16 @@ const CMultiSelect = defineComponent({
|
|
|
254
228
|
visible: {
|
|
255
229
|
type: Boolean,
|
|
256
230
|
default: false,
|
|
257
|
-
|
|
231
|
+
},
|
|
232
|
+
/**
|
|
233
|
+
*
|
|
234
|
+
* Amount of visible items when virtualScroller is set to `true`.
|
|
235
|
+
*
|
|
236
|
+
* @since 4.8.0
|
|
237
|
+
*/
|
|
238
|
+
visibleItems: {
|
|
239
|
+
type: Number,
|
|
240
|
+
default: 10,
|
|
258
241
|
},
|
|
259
242
|
},
|
|
260
243
|
emits: [
|
|
@@ -271,37 +254,13 @@ const CMultiSelect = defineComponent({
|
|
|
271
254
|
],
|
|
272
255
|
setup(props, { attrs, emit }) {
|
|
273
256
|
const nativeSelectRef = ref<HTMLSelectElement>()
|
|
274
|
-
provide('nativeSelectRef', nativeSelectRef)
|
|
275
|
-
const searchRef = ref<HTMLInputElement>()
|
|
276
|
-
|
|
277
257
|
const options = ref<Option[]>(props.options)
|
|
278
258
|
const search = ref('')
|
|
259
|
+
const searchRef = ref<HTMLInputElement>()
|
|
279
260
|
const selected = ref<SelectedOption[]>([])
|
|
280
261
|
const visible = ref<Boolean>(props.visible)
|
|
281
262
|
|
|
282
|
-
|
|
283
|
-
let _selected = [...selected.value, ...options]
|
|
284
|
-
|
|
285
|
-
if (deselected) {
|
|
286
|
-
_selected = _selected.filter(
|
|
287
|
-
(selectedOption) =>
|
|
288
|
-
!deselected.some((deselectedOption) => deselectedOption.value === selectedOption.value),
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const deduplicated = _selected.reduce((unique: Option[], option) => {
|
|
293
|
-
if (!unique.some((obj) => obj.value === option.value)) {
|
|
294
|
-
unique.push({
|
|
295
|
-
value: option.value,
|
|
296
|
-
text: option.text,
|
|
297
|
-
...(option.disabled && { disabled: option.disabled }),
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
return unique
|
|
301
|
-
}, []) as SelectedOption[]
|
|
302
|
-
|
|
303
|
-
selected.value = deduplicated
|
|
304
|
-
}
|
|
263
|
+
provide('nativeSelectRef', nativeSelectRef)
|
|
305
264
|
|
|
306
265
|
watch(
|
|
307
266
|
() => props.options,
|
|
@@ -328,9 +287,12 @@ const CMultiSelect = defineComponent({
|
|
|
328
287
|
return
|
|
329
288
|
})
|
|
330
289
|
|
|
331
|
-
|
|
290
|
+
if (_selected) {
|
|
291
|
+
selected.value = selectOptions(_selected, selected.value, deselected)
|
|
292
|
+
}
|
|
332
293
|
}
|
|
333
294
|
},
|
|
295
|
+
{ immediate: true },
|
|
334
296
|
)
|
|
335
297
|
|
|
336
298
|
watch(selected, () => {
|
|
@@ -338,29 +300,6 @@ const CMultiSelect = defineComponent({
|
|
|
338
300
|
nativeSelectRef.value.dispatchEvent(new Event('change', { bubbles: true }))
|
|
339
301
|
})
|
|
340
302
|
|
|
341
|
-
const filterOptionsList = (search: string, _options: Option[]) => {
|
|
342
|
-
return search.length
|
|
343
|
-
? _options &&
|
|
344
|
-
_options.reduce((acc: Option[], val: Option) => {
|
|
345
|
-
const options =
|
|
346
|
-
val.options &&
|
|
347
|
-
val.options.filter(
|
|
348
|
-
(element) =>
|
|
349
|
-
element.text && element.text.toLowerCase().includes(search.toLowerCase()),
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
(val.text && val.text.toLowerCase().includes(search.toLowerCase())) ||
|
|
354
|
-
(options && options.length)
|
|
355
|
-
) {
|
|
356
|
-
acc.push(Object.assign({}, val, options && options.length && { options }))
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return acc
|
|
360
|
-
}, [])
|
|
361
|
-
: options.value
|
|
362
|
-
}
|
|
363
|
-
|
|
364
303
|
const handleSearchChange = (event: InputEvent) => {
|
|
365
304
|
const target = event.target as HTMLInputElement
|
|
366
305
|
search.value = target.value.toLowerCase()
|
|
@@ -404,7 +343,10 @@ const CMultiSelect = defineComponent({
|
|
|
404
343
|
}
|
|
405
344
|
|
|
406
345
|
const handleSelectAll = () => {
|
|
407
|
-
|
|
346
|
+
selected.value = selectOptions(
|
|
347
|
+
flattenArray(options.value).filter((option: Option) => !option.disabled),
|
|
348
|
+
selected.value,
|
|
349
|
+
)
|
|
408
350
|
}
|
|
409
351
|
|
|
410
352
|
const handleDeselectAll = () => {
|
|
@@ -507,7 +449,7 @@ const CMultiSelect = defineComponent({
|
|
|
507
449
|
}),
|
|
508
450
|
]),
|
|
509
451
|
default: () =>
|
|
510
|
-
|
|
452
|
+
visible.value && [
|
|
511
453
|
props.multiple &&
|
|
512
454
|
props.selectAll &&
|
|
513
455
|
h(
|
|
@@ -528,9 +470,11 @@ const CMultiSelect = defineComponent({
|
|
|
528
470
|
optionsMaxHeight: props.optionsMaxHeight,
|
|
529
471
|
optionsStyle: props.optionsStyle,
|
|
530
472
|
searchNoResultsLabel: props.searchNoResultsLabel,
|
|
531
|
-
selected: selected.value
|
|
473
|
+
selected: selected.value,
|
|
474
|
+
virtualScroller: props.virtualScroller,
|
|
475
|
+
visibleItems: props.visibleItems,
|
|
532
476
|
}),
|
|
533
|
-
]
|
|
477
|
+
],
|
|
534
478
|
},
|
|
535
479
|
),
|
|
536
480
|
},
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { defineComponent, h, PropType, VNode } from 'vue'
|
|
2
|
-
import { Option } from './CMultiSelect'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { CVirtualScroller } from '../virtual-scroller'
|
|
4
|
+
|
|
5
|
+
import { getNextSibling, getPreviousSibling } from './utils'
|
|
6
|
+
import type { Option } from './types'
|
|
5
7
|
|
|
6
8
|
const CMultiSelectOptions = defineComponent({
|
|
7
9
|
name: 'CMultiSelectOptions',
|
|
@@ -12,7 +14,6 @@ const CMultiSelectOptions = defineComponent({
|
|
|
12
14
|
options: {
|
|
13
15
|
type: Array as PropType<Option[]>,
|
|
14
16
|
default: () => [],
|
|
15
|
-
required: false,
|
|
16
17
|
},
|
|
17
18
|
/**
|
|
18
19
|
* Sets maxHeight of options list.
|
|
@@ -22,7 +23,6 @@ const CMultiSelectOptions = defineComponent({
|
|
|
22
23
|
optionsMaxHeight: {
|
|
23
24
|
type: [Number, String],
|
|
24
25
|
default: 'auto',
|
|
25
|
-
required: false,
|
|
26
26
|
},
|
|
27
27
|
/**
|
|
28
28
|
* Sets option style.
|
|
@@ -33,7 +33,6 @@ const CMultiSelectOptions = defineComponent({
|
|
|
33
33
|
optionsStyle: {
|
|
34
34
|
type: String,
|
|
35
35
|
default: 'checkbox',
|
|
36
|
-
required: false,
|
|
37
36
|
validator: (value: string) => {
|
|
38
37
|
return ['checkbox', 'text'].includes(value)
|
|
39
38
|
},
|
|
@@ -44,12 +43,15 @@ const CMultiSelectOptions = defineComponent({
|
|
|
44
43
|
searchNoResultsLabel: {
|
|
45
44
|
type: String,
|
|
46
45
|
default: 'no items',
|
|
47
|
-
required: false,
|
|
48
46
|
},
|
|
49
47
|
selected: {
|
|
50
48
|
type: Array as PropType<Option[]>,
|
|
51
49
|
default: () => [],
|
|
52
|
-
|
|
50
|
+
},
|
|
51
|
+
virtualScroller: Boolean,
|
|
52
|
+
visibleItems: {
|
|
53
|
+
type: Number,
|
|
54
|
+
default: 10,
|
|
53
55
|
},
|
|
54
56
|
},
|
|
55
57
|
emits: ['optionClick'],
|
|
@@ -114,17 +116,29 @@ const CMultiSelectOptions = defineComponent({
|
|
|
114
116
|
),
|
|
115
117
|
)
|
|
116
118
|
: h('div', { class: 'form-multi-select-options-empty' }, props.searchNoResultsLabel)
|
|
119
|
+
|
|
117
120
|
return () =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
props.virtualScroller
|
|
122
|
+
? h(
|
|
123
|
+
CVirtualScroller,
|
|
124
|
+
{
|
|
125
|
+
class: 'form-multi-select-options',
|
|
126
|
+
visibleItems: props.visibleItems,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
default: () => createOptions(props.options),
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
: h(
|
|
133
|
+
'div',
|
|
134
|
+
{
|
|
135
|
+
class: 'form-multi-select-options',
|
|
136
|
+
...(props.optionsMaxHeight !== 'auto' && {
|
|
137
|
+
style: { maxHeight: props.optionsMaxHeight, overflow: 'scroll' },
|
|
138
|
+
}),
|
|
139
|
+
},
|
|
140
|
+
createOptions(props.options),
|
|
141
|
+
)
|
|
128
142
|
},
|
|
129
143
|
})
|
|
130
144
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type Option = {
|
|
2
|
+
disabled?: boolean
|
|
3
|
+
label?: string
|
|
4
|
+
options?: Option[]
|
|
5
|
+
order?: number
|
|
6
|
+
selected?: boolean
|
|
7
|
+
text: string
|
|
8
|
+
value: number | string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SelectedOption = {
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
text: string
|
|
14
|
+
value: number | string
|
|
15
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Option, SelectedOption } from './types'
|
|
2
|
+
|
|
3
|
+
export const filterOptionsList = (search: string, _options: Option[]) => {
|
|
4
|
+
return search.length
|
|
5
|
+
? _options &&
|
|
6
|
+
_options.reduce((acc: Option[], val: Option) => {
|
|
7
|
+
const options =
|
|
8
|
+
val.options &&
|
|
9
|
+
val.options.filter(
|
|
10
|
+
(element) =>
|
|
11
|
+
element.text && element.text.toLowerCase().includes(search.toLowerCase()),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
(val.text && val.text.toLowerCase().includes(search.toLowerCase())) ||
|
|
16
|
+
(options && options.length)
|
|
17
|
+
) {
|
|
18
|
+
acc.push(Object.assign({}, val, options && options.length && { options }))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return acc
|
|
22
|
+
}, [])
|
|
23
|
+
: _options
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const flattenArray = (options: Option[]): Option[] => {
|
|
27
|
+
return options.reduce((acc: Option[], val: Option) => {
|
|
28
|
+
return acc.concat(Array.isArray(val.options) ? flattenArray(val.options) : val)
|
|
29
|
+
}, [])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const getNextSibling = (elem: HTMLElement, selector?: string) => {
|
|
33
|
+
// Get the next sibling element
|
|
34
|
+
let sibling = elem.nextElementSibling
|
|
35
|
+
|
|
36
|
+
// If there's no selector, return the first sibling
|
|
37
|
+
if (!selector) return sibling
|
|
38
|
+
|
|
39
|
+
// If the sibling matches our selector, use it
|
|
40
|
+
// If not, jump to the next sibling and continue the loop
|
|
41
|
+
while (sibling) {
|
|
42
|
+
if (sibling.matches(selector)) return sibling
|
|
43
|
+
sibling = sibling.nextElementSibling
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const getPreviousSibling = (elem: HTMLElement, selector?: string) => {
|
|
50
|
+
// Get the next sibling element
|
|
51
|
+
let sibling = elem.previousElementSibling
|
|
52
|
+
|
|
53
|
+
// If there's no selector, return the first sibling
|
|
54
|
+
if (!selector) return sibling
|
|
55
|
+
|
|
56
|
+
// If the sibling matches our selector, use it
|
|
57
|
+
// If not, jump to the next sibling and continue the loop
|
|
58
|
+
while (sibling) {
|
|
59
|
+
if (sibling.matches(selector)) return sibling
|
|
60
|
+
sibling = sibling.previousElementSibling
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const selectOptions = (
|
|
67
|
+
options: Option[],
|
|
68
|
+
selected: SelectedOption[],
|
|
69
|
+
deselected?: Option[],
|
|
70
|
+
) => {
|
|
71
|
+
let _selected = [...selected, ...options]
|
|
72
|
+
|
|
73
|
+
if (deselected) {
|
|
74
|
+
_selected = _selected.filter(
|
|
75
|
+
(selectedOption) =>
|
|
76
|
+
!deselected.some((deselectedOption) => deselectedOption.value === selectedOption.value),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const deduplicated = _selected.reduce((unique: Option[], option) => {
|
|
81
|
+
if (!unique.some((obj) => obj.value === option.value)) {
|
|
82
|
+
unique.push({
|
|
83
|
+
value: option.value,
|
|
84
|
+
text: option.text,
|
|
85
|
+
...(option.disabled && { disabled: option.disabled }),
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
return unique
|
|
89
|
+
}, []) as SelectedOption[]
|
|
90
|
+
|
|
91
|
+
return deduplicated
|
|
92
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { cloneVNode, computed, defineComponent, h, onMounted, ref, VNode } from 'vue'
|
|
2
|
+
|
|
3
|
+
const CVirtualScroller = defineComponent({
|
|
4
|
+
name: 'CVirtualScroller',
|
|
5
|
+
props: {
|
|
6
|
+
/**
|
|
7
|
+
* Amount of visible items
|
|
8
|
+
*/
|
|
9
|
+
visibleItems: {
|
|
10
|
+
type: Number,
|
|
11
|
+
default: 10,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
setup(props, { slots }) {
|
|
15
|
+
const virtualScrollRef = ref<HTMLDivElement>()
|
|
16
|
+
const virtualScrollContentRef = ref<HTMLDivElement>()
|
|
17
|
+
|
|
18
|
+
const currentItemIndex = ref(1)
|
|
19
|
+
const itemHeight = ref<number>(0)
|
|
20
|
+
const itemsNumber = ref<number>(0)
|
|
21
|
+
const viewportPadding = ref(0)
|
|
22
|
+
|
|
23
|
+
const buffer = computed(() => Math.floor(props.visibleItems / 2))
|
|
24
|
+
|
|
25
|
+
const maxHeight = computed(
|
|
26
|
+
() => itemsNumber.value * itemHeight.value + 2 * viewportPadding.value,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const viewportHeight = computed(
|
|
30
|
+
() => props.visibleItems * itemHeight.value + 2 * viewportPadding.value,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
onMounted(() => {
|
|
34
|
+
if (virtualScrollRef.value) {
|
|
35
|
+
viewportPadding.value = parseFloat(getComputedStyle(virtualScrollRef.value).paddingTop)
|
|
36
|
+
// It's necessary to calculate heights of items
|
|
37
|
+
virtualScrollRef.value.dispatchEvent(new CustomEvent('scroll'))
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const handleScroll = (scrollTop: number) => {
|
|
42
|
+
currentItemIndex.value =
|
|
43
|
+
itemHeight.value && Math.max(Math.ceil(scrollTop / itemHeight.value), 1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
const children: any = slots.default
|
|
48
|
+
? Array.isArray(slots.default()[0].children)
|
|
49
|
+
? slots.default()[0].children
|
|
50
|
+
: slots.default()
|
|
51
|
+
: []
|
|
52
|
+
itemsNumber.value = children && children.length ? children.length : 0
|
|
53
|
+
return h(
|
|
54
|
+
'div',
|
|
55
|
+
{
|
|
56
|
+
class: ['virtual-scroller'],
|
|
57
|
+
onScroll: (event: any) => handleScroll((event.target as HTMLDivElement).scrollTop),
|
|
58
|
+
style: {
|
|
59
|
+
height: `${
|
|
60
|
+
maxHeight.value > viewportHeight.value ? viewportHeight.value : maxHeight.value
|
|
61
|
+
}px`,
|
|
62
|
+
overflowY: 'auto',
|
|
63
|
+
},
|
|
64
|
+
ref: virtualScrollRef,
|
|
65
|
+
},
|
|
66
|
+
h(
|
|
67
|
+
'div',
|
|
68
|
+
{
|
|
69
|
+
class: 'virtual-scroller-content',
|
|
70
|
+
style: {
|
|
71
|
+
height: `${maxHeight.value}px`,
|
|
72
|
+
},
|
|
73
|
+
ref: virtualScrollContentRef,
|
|
74
|
+
},
|
|
75
|
+
children.map(
|
|
76
|
+
(slot: VNode, index: number) =>
|
|
77
|
+
index + 1 > Math.max(currentItemIndex.value - buffer.value, 0) &&
|
|
78
|
+
index + 1 <= currentItemIndex.value + props.visibleItems + buffer.value &&
|
|
79
|
+
cloneVNode(slot, {
|
|
80
|
+
class: [
|
|
81
|
+
{
|
|
82
|
+
'virtual-scroller-item-preload':
|
|
83
|
+
index + 1 > currentItemIndex.value + props.visibleItems ||
|
|
84
|
+
index + 1 < currentItemIndex.value,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
style: {
|
|
88
|
+
...(currentItemIndex.value > buffer.value && {
|
|
89
|
+
transform: `translateY(${
|
|
90
|
+
(currentItemIndex.value - buffer.value) * itemHeight.value
|
|
91
|
+
}px)`,
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
ref: (node) => {
|
|
95
|
+
if (node && (node as HTMLElement).offsetHeight) {
|
|
96
|
+
itemHeight.value =
|
|
97
|
+
(node as HTMLElement).offsetHeight +
|
|
98
|
+
parseFloat(getComputedStyle(node as HTMLElement).marginTop) +
|
|
99
|
+
parseFloat(getComputedStyle(node as HTMLElement).marginBottom)
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
export { CVirtualScroller }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { App } from 'vue'
|
|
2
|
+
import { CVirtualScroller } from './CVirtualScroller'
|
|
3
|
+
|
|
4
|
+
const CVirtualScrollerPlugin = {
|
|
5
|
+
install: (app: App): void => {
|
|
6
|
+
app.component(CVirtualScroller.name, CVirtualScroller)
|
|
7
|
+
},
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { CVirtualScroller, CVirtualScrollerPlugin }
|
package/src/utils/index.ts
CHANGED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
const getNextSibling = (elem: HTMLElement, selector?: string) => {
|
|
2
|
-
// Get the next sibling element
|
|
3
|
-
let sibling = elem.nextElementSibling
|
|
4
|
-
|
|
5
|
-
// If there's no selector, return the first sibling
|
|
6
|
-
if (!selector) return sibling
|
|
7
|
-
|
|
8
|
-
// If the sibling matches our selector, use it
|
|
9
|
-
// If not, jump to the next sibling and continue the loop
|
|
10
|
-
while (sibling) {
|
|
11
|
-
if (sibling.matches(selector)) return sibling
|
|
12
|
-
sibling = sibling.nextElementSibling
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default getNextSibling
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
const getPreviousSibling = (elem: HTMLElement, selector?: string) => {
|
|
2
|
-
// Get the next sibling element
|
|
3
|
-
let sibling = elem.previousElementSibling
|
|
4
|
-
|
|
5
|
-
// If there's no selector, return the first sibling
|
|
6
|
-
if (!selector) return sibling
|
|
7
|
-
|
|
8
|
-
// If the sibling matches our selector, use it
|
|
9
|
-
// If not, jump to the next sibling and continue the loop
|
|
10
|
-
while (sibling) {
|
|
11
|
-
if (sibling.matches(selector)) return sibling
|
|
12
|
-
sibling = sibling.previousElementSibling
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default getPreviousSibling
|