UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

601 lines (597 loc) 23.1 kB
'use strict'; 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