@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.
@@ -1,4 +1,2 @@
1
- import getNextSibling from './getNextSibling';
2
- import getPreviousSibling from './getPreviousSibling';
3
1
  import isInViewport from './isInViewport';
4
- export { getNextSibling, getPreviousSibling, isInViewport };
2
+ export { isInViewport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreui/vue-pro",
3
- "version": "4.8.0-next.0",
3
+ "version": "4.8.0-next.1",
4
4
  "description": "UI Components Library for Vue.js",
5
5
  "keywords": [
6
6
  "vue",
@@ -43,4 +43,5 @@ export * from './tabs'
43
43
  export * from './time-picker'
44
44
  export * from './toast'
45
45
  export * from './tooltip'
46
+ export * from './virtual-scroller'
46
47
  export * from './widgets'
@@ -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
- export interface SelectedOption {
20
- disabled?: boolean
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
- required: false,
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
- const selectOptions = (options: Option[], deselected?: Option[]) => {
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
- _selected && selectOptions(_selected, deselected)
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
- selectOptions(flattenArray(options.value).filter((option: Option) => !option.disabled))
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
- h('div', {}, [
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,5 +1,6 @@
1
1
  import { defineComponent, h, inject, PropType, VNode } from 'vue'
2
- import { Option } from './CMultiSelect'
2
+
3
+ import type { Option } from './types'
3
4
 
4
5
  const CMultiSelectNativeSelect = defineComponent({
5
6
  name: 'CMultiSelectGroupOption',
@@ -1,7 +1,9 @@
1
1
  import { defineComponent, h, PropType, VNode } from 'vue'
2
- import { Option } from './CMultiSelect'
3
2
 
4
- import { getNextSibling, getPreviousSibling } from './../../utils'
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
- required: false,
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
- h(
119
- 'div',
120
- {
121
- class: 'form-multi-select-options',
122
- ...(props.optionsMaxHeight !== 'auto' && {
123
- style: { maxHeight: props.optionsMaxHeight, overflow: 'scroll' },
124
- }),
125
- },
126
- createOptions(props.options),
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
 
@@ -1,5 +1,6 @@
1
1
  import { defineComponent, h, PropType } from 'vue'
2
- import { Option } from './CMultiSelect'
2
+
3
+ import type { Option } from './types'
3
4
 
4
5
  const CMultiSelectSelection = defineComponent({
5
6
  name: 'CMultiSelectSelection',
@@ -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 }
@@ -1,5 +1,3 @@
1
- import getNextSibling from './getNextSibling'
2
- import getPreviousSibling from './getPreviousSibling'
3
1
  import isInViewport from './isInViewport'
4
2
 
5
- export { getNextSibling, getPreviousSibling, isInViewport }
3
+ export { isInViewport }
@@ -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