UNPKG

@coreui/vue

Version:

UI Components Library for Vue.js

383 lines (379 loc) 13.9 kB
'use strict'; var vue = require('vue'); var CChip = require('../chip/CChip.js'); const uniqueValues = (values) => [ ...new Set(values.map((value) => value.trim()).filter(Boolean)), ]; const resolveChipClassName = (chipClassName, value) => { if (!chipClassName) { return undefined; } if (typeof chipClassName === 'function') { const resolvedClassName = chipClassName(value); return typeof resolvedClassName === 'string' ? resolvedClassName : undefined; } return chipClassName; }; const CChipInput = vue.defineComponent({ name: 'CChipInput', props: { /** * Adds custom classes to chips rendered by the component. Accepts a static className or a resolver function based on chip value. */ chipClassName: { type: [String, Function], default: undefined, }, /** * Creates a new chip when the component loses focus with a pending value. */ createOnBlur: { type: Boolean, default: true, }, /** * Sets the initial uncontrolled values rendered by the component. */ defaultValue: { type: Array, default: () => [], }, /** * Toggle the disabled state for the component. */ disabled: Boolean, /** * Sets the `id` of the internal text input rendered by the component. */ id: String, /** * Renders an inline label inside the component container. */ label: [String, Object], /** * Sets the maximum number of chips that can be created in the component. */ maxChips: { type: Number, default: null, }, /** * The default name for a value passed using v-model. */ modelValue: { type: Array, default: undefined, }, /** * Sets the name of the hidden input used by the component for form submission. */ name: String, /** * Sets placeholder text for the internal input of the component. */ placeholder: { type: String, default: '', }, /** * Toggle the readonly state for the component. */ readOnly: Boolean, /** * Displays remove buttons on chips managed by the component. */ removable: { type: Boolean, default: true, }, /** * Enables chip selection behavior in the component. */ selectable: Boolean, /** * Sets the separator character used to create chips while typing or pasting in the component. */ separator: { type: String, default: ',', }, /** * Size the component small or large. * * @values 'sm', 'lg' */ size: { type: String, validator: (value) => { return ['sm', 'lg'].includes(value); }, }, }, emits: [ /** * Event occurs when the component adds a new chip. */ 'add', /** * Event occurs when the value list changes. */ 'change', /** * Event occurs when the internal text input value changes. */ 'input', /** * Event occurs when the component removes a chip. */ 'remove', /** * Event occurs when the selected chip values change. */ 'select', /** * Emit the new value whenever there's a change. */ 'update:modelValue', ], setup(props, { attrs, emit, expose }) { const internalValues = vue.ref(uniqueValues(props.defaultValue)); const inputValue = vue.ref(''); const selectedValues = vue.ref([]); const rootRef = vue.ref(); const inputRef = vue.ref(); const values = vue.computed(() => props.modelValue !== undefined ? uniqueValues(props.modelValue) : uniqueValues(internalValues.value)); vue.watch(values, (newValues) => { selectedValues.value = selectedValues.value.filter((item) => newValues.includes(item)); }); const emitValuesChange = (nextValues) => { if (props.modelValue === undefined) { internalValues.value = nextValues; } emit('update:modelValue', nextValues); emit('change', nextValues); }; const canAddMore = vue.computed(() => props.maxChips === null || values.value.length < props.maxChips); const add = (rawValue) => { if (props.disabled || props.readOnly) { return false; } const normalizedValue = String(rawValue).trim(); if (!normalizedValue || values.value.includes(normalizedValue) || !canAddMore.value) { return false; } const nextValues = [...values.value, normalizedValue]; emitValuesChange(nextValues); emit('add', normalizedValue); return true; }; const remove = (valueToRemove) => { if (props.disabled || props.readOnly) { return false; } if (!values.value.includes(valueToRemove)) { return false; } const nextValues = values.value.filter((item) => item !== valueToRemove); emitValuesChange(nextValues); selectedValues.value = selectedValues.value.filter((item) => { const wasSelected = item === valueToRemove; if (wasSelected && selectedValues.value.length !== nextValues.length) { emit('select', selectedValues.value.filter((v) => v !== valueToRemove)); } return item !== valueToRemove; }); emit('remove', valueToRemove); return true; }; const createFromInput = () => { if (add(inputValue.value)) { inputValue.value = ''; } }; const focusLastChip = () => { if (!rootRef.value) { return; } const focusableChips = [ ...rootRef.value.querySelectorAll('[data-coreui-chip-focusable="true"]:not(.disabled)'), ]; if (focusableChips.length === 0) { return; } focusableChips[focusableChips.length - 1].focus(); }; const handleInputKeydown = (event) => { switch (event.key) { case 'Enter': { event.preventDefault(); createFromInput(); break; } case 'Backspace': case 'Delete': { if (inputValue.value === '') { event.preventDefault(); focusLastChip(); } break; } case 'ArrowLeft': { const target = event.currentTarget; if (target.selectionStart === 0 && target.selectionEnd === 0) { event.preventDefault(); focusLastChip(); } break; } case 'Escape': { inputValue.value = ''; event.currentTarget.blur(); break; } // No default } }; const handleInputChange = (value) => { if (props.disabled || props.readOnly) { return; } if (props.separator && value.includes(props.separator)) { const parts = value.split(props.separator); const chipsToAdd = uniqueValues(parts.slice(0, -1)); const newChips = chipsToAdd.filter(chip => !values.value.includes(chip)); const availableSlots = props.maxChips !== null ? props.maxChips - values.value.length : Infinity; const chipsToEmit = newChips.slice(0, availableSlots); if (chipsToEmit.length > 0) { const nextValues = [...values.value, ...chipsToEmit]; chipsToEmit.forEach(chip => emit('add', chip)); emitValuesChange(nextValues); } const tail = parts[parts.length - 1] || ''; inputValue.value = tail; emit('input', tail); return; } inputValue.value = value; emit('input', value); }; const handlePaste = (event) => { if (props.disabled || props.readOnly || !props.separator) { return; } const pastedData = event.clipboardData?.getData('text'); if (!pastedData?.includes(props.separator)) { return; } event.preventDefault(); const chipsToAdd = uniqueValues(pastedData.split(props.separator)); const newChips = chipsToAdd.filter(chip => !values.value.includes(chip)); const availableSlots = props.maxChips !== null ? props.maxChips - values.value.length : Infinity; const chipsToEmit = newChips.slice(0, availableSlots); if (chipsToEmit.length > 0) { const nextValues = [...values.value, ...chipsToEmit]; chipsToEmit.forEach(chip => emit('add', chip)); emitValuesChange(nextValues); } inputValue.value = ''; emit('input', ''); }; const handleInputBlur = (event) => { if (!props.createOnBlur) { return; } if (event.relatedTarget?.closest('.chip')) { return; } createFromInput(); }; const handleContainerKeydown = (event) => { if (event.target === inputRef.value) { return; } if (event.key.length === 1) { inputRef.value?.focus(); } }; const handleContainerClick = (event) => { if (event.target === rootRef.value) { inputRef.value?.focus(); } }; const handleSelectedChange = (chipValue, selected) => { selectedValues.value = selected ? uniqueValues([...selectedValues.value, chipValue]) : selectedValues.value.filter((value) => value !== chipValue); emit('select', selectedValues.value); }; expose({ rootRef, inputRef }); return () => { const inputSize = Math.max(props.placeholder.length, inputValue.value.length, 1); const children = [ props.label && vue.h('label', { class: 'chip-input-label', for: props.id, }, props.label), ...values.value.map((chipValue) => vue.h(CChip.CChip, { ariaRemoveLabel: `Remove ${chipValue}`, class: resolveChipClassName(props.chipClassName, chipValue), disabled: props.disabled, key: chipValue, onRemove: () => remove(chipValue), onSelectedChange: (selected) => handleSelectedChange(chipValue, selected), removable: Boolean(props.removable && !props.disabled && !props.readOnly), selectable: props.selectable, selected: selectedValues.value.includes(chipValue), }, { default: () => chipValue, })), vue.h('input', { ref: inputRef, type: 'text', id: props.id, class: 'chip-input-field', disabled: props.disabled, readonly: Boolean(!props.disabled && props.readOnly), placeholder: props.placeholder, size: inputSize, value: inputValue.value, onBlur: handleInputBlur, onInput: (event) => handleInputChange(event.target.value), onKeydown: handleInputKeydown, onPaste: handlePaste, onFocus: () => { if (selectedValues.value.length > 0) { selectedValues.value = []; emit('select', []); } }, }), props.name && vue.h('input', { type: 'hidden', name: props.name, value: values.value.join(','), }), ].filter(Boolean); return vue.h('div', { ref: rootRef, class: [ 'chip-input', { [`chip-input-${props.size}`]: props.size, disabled: props.disabled, }, attrs.class, ], 'aria-disabled': props.disabled ? true : undefined, 'aria-readonly': props.readOnly ? true : undefined, onClick: handleContainerClick, onKeydown: handleContainerKeydown, }, children); }; }, }); exports.CChipInput = CChipInput; //# sourceMappingURL=CChipInput.js.map