@coreui/vue-pro
Version:
UI Components Library for Vue.js
259 lines (229 loc) • 8.71 kB
text/typescript
import { defineComponent, h, PropType } from 'vue'
import { CElementCover } from '../element-cover'
import { CVirtualScroller } from '../virtual-scroller'
import { getNextSibling, getPreviousSibling } from '../../utils'
import type { Option, OptionsGroup } from './types'
const CMultiSelectOptions = defineComponent({
name: 'CMultiSelectOptions',
props: {
loading: Boolean,
options: {
type: Array as PropType<(Option | OptionsGroup)[]>,
default: () => [],
},
optionsGroupsSelectable: Boolean,
optionsGroupsStyle: {
type: String,
default: 'checkbox',
validator: (value: string) => {
return ['checkbox', 'text'].includes(value)
},
},
optionsMaxHeight: {
type: [Number, String],
default: 'auto',
},
optionsStyle: {
type: String,
default: 'checkbox',
validator: (value: string) => {
return ['checkbox', 'text'].includes(value)
},
},
scopedSlots: Object,
searchNoResultsLabel: {
type: String,
default: 'no items',
},
selected: {
type: Array as PropType<Option[]>,
default: () => [],
},
virtualScroller: Boolean,
visibleItems: {
type: Number,
default: 10,
},
},
emits: ['groupClick', 'optionClick'],
setup(props, { emit }) {
const handleHomeEndKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Home' && event.key !== 'End') {
return
}
event.preventDefault()
const target = event.target as HTMLElement
const container = target.closest('.form-multi-select-options') as HTMLElement | null
if (!container) {
return
}
const first = event.key === 'Home'
const focusEdgeOption = () => {
const options = container.querySelectorAll<HTMLElement>(
'.form-multi-select-option:not(.disabled)'
)
if (options.length > 0) {
options[first ? 0 : options.length - 1].focus()
}
}
if (props.virtualScroller) {
// The virtualized viewport only mounts a window of options — scroll it to
// the edge so the true first/last options render, then focus after the
// window has re-rendered (scroll event → state update → commit).
container.scrollTop = first ? 0 : container.scrollHeight
requestAnimationFrame(() => requestAnimationFrame(focusEdgeOption))
return
}
focusEdgeOption()
}
const handleKeyDown = (event: KeyboardEvent, option: Option) => {
if (event.code === 'Space' || event.key === 'Enter') {
event.preventDefault()
handleOptionClick && handleOptionClick(option)
return
}
if (event.key === 'Down' || event.key === 'ArrowDown') {
event.preventDefault()
const target = event.target as HTMLElement
const next = getNextSibling(target, '.form-multi-select-option')
next && (next as HTMLElement).focus()
}
if (event.key === 'Up' || event.key === 'ArrowUp') {
event.preventDefault()
const target = event.target as HTMLElement
const prev = getPreviousSibling(target, '.form-multi-select-option')
prev && (prev as HTMLElement).focus()
}
handleHomeEndKeyDown(event)
}
const handleGroupKeyDown = (event: KeyboardEvent, group: OptionsGroup) => {
if (event.code === 'Space' || event.key === 'Enter') {
event.preventDefault()
emit('groupClick', group)
return
}
if (event.key === 'Down' || event.key === 'ArrowDown') {
event.preventDefault()
const next = getNextSibling(event.target as HTMLElement, '.form-multi-select-option')
next && (next as HTMLElement).focus()
}
if (event.key === 'Up' || event.key === 'ArrowUp') {
event.preventDefault()
const prev = getPreviousSibling(event.target as HTMLElement, '.form-multi-select-option')
prev && (prev as HTMLElement).focus()
}
handleHomeEndKeyDown(event)
}
const getGroupCheckboxState = (group: OptionsGroup) => {
const groupOptions = (group.options ?? []).filter((option) => !option.disabled)
const selectedCount = groupOptions.filter((option) =>
props.selected.some((_option) => _option.value === option.value)
).length
if (groupOptions.length > 0 && selectedCount >= groupOptions.length) {
return 'all'
}
return selectedCount === 0 ? 'none' : 'indeterminate'
}
const handleOptionClick = (option: Option) => {
emit('optionClick', option as Option)
}
const handleGroupClick = (group: OptionsGroup) => {
emit('groupClick', group as OptionsGroup)
}
// TODO: find solution how to remove any
const createOptions = (options: (Option | OptionsGroup)[]) =>
options.length > 0
? options.map((option: Option | OptionsGroup) =>
'value' in option
? h(
'div',
{
class: [
'form-multi-select-option',
{
'form-multi-select-option-with-checkbox': props.optionsStyle === 'checkbox',
'form-multi-selected': props.selected.some(
(_option) => _option.value === option.value
),
disabled: option.disabled,
},
],
onClick: () => handleOptionClick(option as Option),
onKeydown: (event: KeyboardEvent) => handleKeyDown(event, option as Option),
tabindex: 0,
role: 'option',
'aria-selected': props.selected.some(
(_option) => _option.value === option.value
),
},
props.scopedSlots && props.scopedSlots['options']
? h(props.scopedSlots['options'], { option: option })
: option.label
)
: [
h(
'div',
{
class: [
'form-multi-select-optgroup-label',
{
'form-multi-select-optgroup-label-with-checkbox':
props.optionsGroupsSelectable &&
props.optionsGroupsStyle === 'checkbox',
'form-multi-selected':
props.optionsGroupsSelectable &&
props.optionsGroupsStyle === 'checkbox' &&
getGroupCheckboxState(option) === 'all',
'form-multi-select-indeterminate':
props.optionsGroupsSelectable &&
props.optionsGroupsStyle === 'checkbox' &&
getGroupCheckboxState(option) === 'indeterminate',
},
],
...(props.optionsGroupsSelectable &&
props.optionsGroupsStyle === 'checkbox' && {
onClick: () => handleGroupClick(option as OptionsGroup),
onKeydown: (event: KeyboardEvent) =>
handleGroupKeyDown(event, option as OptionsGroup),
tabindex: 0,
role: 'button',
}),
},
props.scopedSlots && props.scopedSlots['options-groups']
? h(props.scopedSlots['options-groups'], { option: option })
: option.label
),
]
)
: h(
'div',
{ class: 'form-multi-select-options-empty', role: 'status' },
props.searchNoResultsLabel
)
return () => [
props.virtualScroller
? h(
CVirtualScroller,
{
class: 'form-multi-select-options',
visibleItems: props.visibleItems,
},
{
default: () => createOptions(props.options),
}
)
: h(
'div',
{
class: 'form-multi-select-options',
...(props.optionsMaxHeight !== 'auto' && {
style: { maxHeight: props.optionsMaxHeight, overflow: 'scroll' },
}),
},
createOptions(props.options)
),
props.loading && h(CElementCover),
]
},
})
export { CMultiSelectOptions }