@coreui/vue-pro
Version:
UI Components Library for Vue.js
601 lines (597 loc) • 23.1 kB
JavaScript
var vue = require('vue');
var core = require('@popperjs/core');
var CFormControlWrapper = require('../form/CFormControlWrapper.js');
var CMultiSelectNativeSelect = require('./CMultiSelectNativeSelect.js');
var CMultiSelectOptions = require('./CMultiSelectOptions.js');
var CMultiSelectSelection = require('./CMultiSelectSelection.js');
var isRTL = require('../../utils/isRTL.js');
var utils = require('./utils.js');
const CMultiSelect = vue.defineComponent({
name: 'CMultiSelect',
props: {
/**
* Allow users to create options if they are not in the list of options.
*
* @since 4.9.0
*/
allowCreateOptions: Boolean,
/**
* Enables selection cleaner element.
*
* @default true
*/
cleaner: {
type: Boolean,
default: true,
},
/**
* Clear current search on selecting an item.
*
* @since 4.9.0
*/
clearSearchOnSelect: Boolean,
/**
* Toggle the disabled state for the component.
*/
disabled: Boolean,
/**
* Provide valuable, actionable feedback.
*
* @since 4.6.0
*/
feedback: String,
/**
* Provide valuable, actionable feedback.
*
* @since 4.6.0
*/
feedbackInvalid: String,
/**
* Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
*
* @since 4.6.0
*/
feedbackValid: String,
/**
* Set the id attribute for the native select element.
*
* **[Deprecated since v5.3.0]** The name attribute for the native select element is generated based on the `id` property:
* - `<select name="\{id\}-multi-select" />`
*/
id: String,
/**
* Set component validation state to invalid.
*
* @since 4.6.0
*/
invalid: Boolean,
/**
* Add a caption for a component.
*
* @since 4.6.0
*/
label: String,
/**
* When set, the options list will have a loading style: loading spinner and reduced opacity.
*
* @since 4.9.0
*/
loading: Boolean,
/**
* It specifies that multiple options can be selected at once.
*
* @default true
*/
multiple: {
type: Boolean,
default: true,
},
/**
* The name attribute for the select element.
*
* @since 5.3.0
*/
name: String,
/**
* List of option elements.
*/
options: {
type: Array,
default: () => [],
},
/**
* Sets maxHeight of options list.
*
* @default 'auto'
*/
optionsMaxHeight: {
type: [Number, String],
default: 'auto',
},
/**
* Sets option style.
*
* @values 'checkbox', 'text'
* @default 'checkbox'
*/
optionsStyle: {
type: String,
default: 'checkbox',
validator: (value) => {
return ['checkbox', 'text'].includes(value);
},
},
/**
* Specifies a short hint that is visible in the search input.
*
* @default 'Select...''
*/
placeholder: {
type: String,
default: 'Select...',
},
/**
* When it is present, it indicates that the user must choose a value before submitting the form.
*/
required: Boolean,
/**
* Determines whether the selected options should be cleared when the options list is updated. When set to true, any previously selected options will be reset whenever the options list undergoes a change. This ensures that outdated selections are not retained when new options are provided.
*
* @since 5.3.0
*/
resetSelectionOnOptionsChange: Boolean,
/**
* Enables search input element.
*/
search: {
type: [Boolean, String],
default: true,
validator: (value) => {
if (typeof value == 'string') {
return ['external'].includes(value);
}
if (typeof value == 'boolean') {
return true;
}
return false;
},
},
/**
* Sets the label for no results when filtering.
*/
searchNoResultsLabel: {
type: String,
default: 'no items',
},
/**
* Enables select all button.
*
* @default true
*/
selectAll: {
type: Boolean,
default: true,
},
/**
* Sets the select all button label.
*
* @default 'Select all options'
*/
selectAllLabel: {
type: String,
default: 'Select all options',
},
/**
* Sets the selection style.
*
* @values 'counter', 'tags', 'text'
* @default 'tags'
*/
selectionType: {
type: String,
default: 'tags',
validator: (value) => {
return ['counter', 'tags', 'text'].includes(value);
},
},
/**
* Sets the counter selection label.
*
* @default 'item(s) selected'
*/
selectionTypeCounterText: {
type: String,
default: 'item(s) selected',
},
/**
* Size the component small or large.
*
* @values 'sm', 'lg'
*/
size: {
type: String,
validator: (value) => {
return ['sm', 'lg'].includes(value);
},
},
/**
* Add helper text to the component.
*
* @since 4.6.0
*/
text: String,
/**
* Display validation feedback in a styled tooltip.
*
* @since 4.6.0
*/
tooltipFeedback: Boolean,
/**
* Set component validation state to valid.
*
* @since 4.6.0
*/
valid: Boolean,
/**
* Enable virtual scroller for the options list.
*
* @since 4.8.0
*/
virtualScroller: Boolean,
/**
* Toggle the visibility of multi select dropdown.
*
* @default false
*/
visible: Boolean,
/**
*
* Amount of visible items when virtualScroller is set to `true`.
*
* @since 4.8.0
*/
visibleItems: {
type: Number,
default: 10,
},
},
emits: [
/**
* Execute a function when a user changes the selected option. [docs]
*/
'change',
/**
* Execute a function when the filter value changed.
*
* @since 4.7.0
*/
'filterChange',
/**
* The callback is fired when the Multi Select component requests to be hidden.
*/
'hide',
/**
* The callback is fired when the Multi Select component requests to be shown.
*/
'show',
],
setup(props, { attrs, emit, slots }) {
const multiSelectRef = vue.ref();
const dropdownRef = vue.ref();
const nativeSelectRef = vue.ref();
const togglerRef = vue.ref();
const searchRef = vue.ref();
const options = vue.ref(props.options);
const popper = vue.ref();
const searchValue = vue.ref('');
const selected = vue.ref([]);
const userOptions = vue.ref([]);
const visible = vue.ref(props.visible);
vue.provide('nativeSelectRef', nativeSelectRef);
const filteredOptions = vue.computed(() => utils.flattenOptionsArray(props.search === 'external'
? [...options.value, ...utils.filterOptionsList(searchValue.value, userOptions.value)]
: utils.filterOptionsList(searchValue.value, [...options.value, ...userOptions.value]), true));
const flattenedOptions = vue.computed(() => utils.flattenOptionsArray(props.options));
const userOption = vue.computed(() => {
if (props.allowCreateOptions &&
filteredOptions.value.some((option) => option.label && option.label.toLowerCase() === searchValue.value.toLowerCase())) {
return false;
}
return searchRef.value && utils.createOption(String(searchValue.value), flattenedOptions.value);
});
vue.watch(() => props.options, (newValue, oldValue) => {
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
options.value = newValue;
if (props.resetSelectionOnOptionsChange) {
selected.value = [];
return;
}
const _selected = flattenedOptions.value.filter((option) => option.selected === true);
const deselected = flattenedOptions.value.filter((option) => option.selected === false);
if (_selected) {
selected.value = utils.selectOptions(_selected, selected.value, deselected);
}
}
}, { immediate: true });
vue.watch(selected, () => {
nativeSelectRef.value &&
nativeSelectRef.value.dispatchEvent(new Event('change', { bubbles: true }));
if (popper.value) {
popper.value.update();
}
});
vue.watch(visible, () => {
if (visible.value) {
emit('show');
window.addEventListener('mouseup', handleMouseUp);
window.addEventListener('keyup', handleKeyUp);
initPopper();
// TODO: find better solution
setTimeout(() => {
searchRef.value && searchRef.value.focus();
}, 100);
return;
}
emit('hide');
searchValue.value = '';
if (searchRef.value) {
searchRef.value.value = '';
}
window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('keyup', handleKeyUp);
destroyPopper();
});
vue.onBeforeUnmount(() => {
window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('keyup', handleKeyUp);
});
const initPopper = () => {
if (togglerRef.value && dropdownRef.value) {
popper.value = core.createPopper(togglerRef.value, dropdownRef.value, {
placement: isRTL.default() ? 'bottom-end' : 'bottom-start',
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'clippingParents',
},
},
{
name: 'offset',
options: {
offset: [0, 2],
},
},
],
});
}
};
const destroyPopper = () => {
if (popper.value) {
popper.value.destroy();
}
popper.value = undefined;
};
const handleKeyUp = (event) => {
if (event.key === 'Escape') {
visible.value = false;
}
};
const handleMouseUp = (event) => {
if (multiSelectRef.value && multiSelectRef.value.contains(event.target)) {
return;
}
visible.value = false;
};
const handleSearchChange = (event) => {
const target = event.target;
searchValue.value = target.value.toLowerCase();
emit('filterChange', target.value);
};
const handleSearchKeyDown = (event) => {
if (event.key === 'Enter' && searchValue.value && props.allowCreateOptions) {
event.preventDefault();
if (!userOption.value) {
selected.value = [
...selected.value,
filteredOptions.value.find((option) => String(option.label).toLowerCase() === searchValue.value.toLowerCase()),
];
}
if (userOption.value) {
selected.value = [...selected.value, ...userOption.value];
userOptions.value = [...userOptions.value, ...userOption.value];
}
searchValue.value = '';
if (searchRef.value) {
searchRef.value.value = '';
}
return;
}
if (searchValue.value.length > 0) {
return;
}
if (event.key === 'Backspace' || event.key === 'Delete') {
const last = selected.value.filter((option) => !option.disabled).pop();
if (last) {
selected.value = selected.value.filter((option) => option.value !== last.value);
}
}
};
const handleOptionClick = (option) => {
if (!props.multiple) {
selected.value = [option];
visible.value = false;
if (searchRef.value) {
searchRef.value.value = '';
}
return;
}
if (option.custom && !userOptions.value.some((_option) => _option.value === option.value)) {
userOptions.value = [...userOptions.value, option];
}
if (props.clearSearchOnSelect || option.custom) {
searchValue.value = '';
if (searchRef.value) {
searchRef.value.value = '';
searchRef.value.focus();
}
}
if (selected.value.some((_option) => _option.value === option.value)) {
selected.value = selected.value.filter((_option) => _option.value !== option.value);
}
else {
selected.value = [...selected.value, option];
}
};
const handleSelectAll = () => {
selected.value = utils.selectOptions([
...flattenedOptions.value.filter((option) => !option.disabled),
...userOptions.value,
], selected.value);
};
const handleDeselectAll = () => {
selected.value = selected.value.filter((option) => option.disabled);
};
return () => [
vue.h(CMultiSelectNativeSelect.CMultiSelectNativeSelect, {
id: props.id,
multiple: props.multiple,
name: props.name,
options: selected.value,
required: props.required,
value: props.multiple
? selected.value.map((option) => option.value.toString())
: selected.value.map((option) => option.value)[0],
onChange: () => emit('change', selected.value),
}),
vue.h(CFormControlWrapper.CFormControlWrapper, {
...(typeof attrs['aria-describedby'] === 'string' && {
describedby: attrs['aria-describedby'],
}),
feedback: props.feedback,
feedbackInvalid: props.feedbackInvalid,
feedbackValid: props.feedbackValid,
id: props.id,
invalid: props.invalid,
label: props.label,
text: props.text,
tooltipFeedback: props.tooltipFeedback,
valid: props.valid,
}, {
default: () => vue.h('div', {
class: [
'form-multi-select',
{
disabled: props.disabled,
[`form-multi-select-${props.size}`]: props.size,
'is-invalid': props.invalid,
'is-valid': props.valid,
show: visible.value,
},
],
'aria-expanded': visible.value,
ref: multiSelectRef,
}, {
default: () => [
vue.h('div', {
class: 'form-multi-select-input-group',
onClick: () => {
visible.value = true;
},
ref: togglerRef,
}, {
default: () => [
vue.h(CMultiSelectSelection.CMultiSelectSelection, {
multiple: props.multiple,
placeholder: props.placeholder,
onRemove: (option) => !props.disabled && handleOptionClick(option),
search: props.search,
selected: selected.value,
selectionType: props.selectionType,
selectionTypeCounterText: props.selectionTypeCounterText,
}, {
default: () => props.search &&
vue.h('input', {
type: 'text',
class: 'form-multi-select-search',
disabled: props.disabled,
autocomplete: 'off',
onInput: (event) => handleSearchChange(event),
onKeydown: (event) => handleSearchKeyDown(event),
...(selected.value.length === 0 && {
placeholder: props.placeholder,
}),
...(selected.value.length > 0 &&
props.selectionType === 'counter' && {
placeholder: `${selected.value.length} ${props.selectionTypeCounterText}`,
}),
...(selected.value.length > 0 &&
!props.multiple && {
placeholder: selected.value.map((option) => option.label)[0],
}),
...(props.multiple &&
selected.value.length > 0 &&
props.selectionType !== 'counter' && {
size: searchValue.value.length + 2,
}),
ref: searchRef,
}),
}),
vue.h('div', { class: 'form-multi-select-buttons' }, {
default: () => [
vue.h('button', {
class: 'form-multi-select-cleaner',
onClick: () => handleDeselectAll(),
type: 'button',
}),
vue.h('button', {
class: 'form-multi-select-indicator',
onClick: (event) => {
event.preventDefault();
event.stopPropagation();
visible.value = !visible.value;
},
type: 'button',
}),
],
}),
],
}),
vue.h('div', {
class: 'form-multi-select-dropdown',
role: 'menu',
ref: dropdownRef,
}, {
default: () => [
props.multiple &&
props.selectAll &&
vue.h('button', {
class: 'form-multi-select-all',
onClick: () => handleSelectAll(),
type: 'button',
}, props.selectAllLabel),
vue.h(CMultiSelectOptions.CMultiSelectOptions, {
loading: props.loading,
onOptionClick: (option) => handleOptionClick(option),
options: filteredOptions.value.length === 0 && props.allowCreateOptions
? userOption.value || []
: filteredOptions.value,
optionsMaxHeight: props.optionsMaxHeight,
optionsStyle: props.optionsStyle,
scopedSlots: slots,
searchNoResultsLabel: props.searchNoResultsLabel,
selected: selected.value,
virtualScroller: props.virtualScroller,
visibleItems: props.visibleItems,
}),
],
}),
],
}),
}),
];
},
});
exports.CMultiSelect = CMultiSelect;
//# sourceMappingURL=CMultiSelect.js.map
;