UNPKG

@coreui/vue

Version:

UI Components Library for Vue.js

425 lines (421 loc) 15.5 kB
'use strict'; var vue = require('vue'); var buildChips = require('../chip-set/buildChips.js'); var useChipSet = require('../chip-set/useChipSet.js'); var isRTL = require('../../utils/isRTL.js'); // Initial chip values can be supplied declaratively as CChip slot content // (parity with the vanilla ChipInput, which reads existing .chip elements). const slotText = (slot) => { if (typeof slot !== 'function') { return undefined; } const rendered = slot(); const first = Array.isArray(rendered) ? rendered[0] : rendered; if (typeof first === 'string') { return first; } return typeof first?.children === 'string' ? first.children : undefined; }; const valuesFromSlot = (nodes = []) => { const values = []; for (const node of nodes) { if (Array.isArray(node.children)) { values.push(...valuesFromSlot(node.children)); continue; } const value = node.props?.value ?? slotText(node.children?.default); if (value) { values.push(value); } } return values; }; 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, /** * Renders the chips as filter chips, each showing a leading check icon while selected. Implies `selectable`. */ filter: 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 how many chips can be selected at once. * * @values 'single', 'multiple' */ selectionMode: { type: String, default: 'multiple', }, /** * 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, slots }) { const internalValues = vue.ref(uniqueValues(props.defaultValue.length > 0 ? props.defaultValue : valuesFromSlot(slots.default?.()))); const inputValue = vue.ref(''); const inputRef = vue.ref(); const generatedName = vue.useId(); const values = vue.computed(() => props.modelValue === undefined ? uniqueValues(internalValues.value) : uniqueValues(props.modelValue)); // CChipInput builds on the same engine as CChipSet: useChipSet owns selection // coordination, roving focus, and chip prop forwarding (provided to the chips // below). CChipInput owns the chip list and adds the text-input layer. const config = vue.computed(() => ({ disabled: props.disabled, filter: props.filter, removable: Boolean(props.removable && !props.disabled && !props.readOnly), selectable: props.selectable, })); const { rootRef, clearSelection, getFocusableChips, handleKeydown } = useChipSet.useChipSet({ config, selectionMode: () => props.selectionMode, selected: () => undefined, restoreFocusOnRemove: false, onSelectionChange: (selected) => emit('select', selected), onRemove: (value) => remove(value), }); 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; } // Selection is cleaned up by useChipSet; here we just drop the value. emitValuesChange(values.value.filter((item) => item !== valueToRemove)); emit('remove', valueToRemove); inputRef.value?.focus(); return true; }; const createFromInput = () => { if (add(inputValue.value)) { inputValue.value = ''; } }; const focusLastChip = () => { const chips = getFocusableChips(); chips[chips.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': case 'ArrowRight': { // The arrow pointing toward the chips (left in LTR, right in RTL) jumps // to the last chip when the caret is at the start of the input. const towardChipsKey = isRTL.default(rootRef.value) ? 'ArrowRight' : 'ArrowLeft'; const target = event.currentTarget; if (event.key === towardChipsKey && 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 ? Infinity : props.maxChips - values.value.length; 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 ? Infinity : props.maxChips - values.value.length; 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; } // The arrow past the last chip moves focus into the text field (mirrored in RTL). if (event.key === (isRTL.default(rootRef.value) ? 'ArrowLeft' : 'ArrowRight')) { const chips = getFocusableChips(); const lastChip = chips[chips.length - 1]; if (lastChip?.contains(event.target)) { event.preventDefault(); inputRef.value?.focus(); return; } } if (handleKeydown(event)) { return; } if (event.key.length === 1) { inputRef.value?.focus(); } }; const handleContainerClick = (event) => { if (event.target === rootRef.value) { inputRef.value?.focus(); } }; 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), ...buildChips.chipsFromData(values.value.map((chipValue) => ({ value: chipValue, label: chipValue, ariaRemoveLabel: `Remove ${chipValue}`, class: resolveChipClassName(props.chipClassName, 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: clearSelection, }), vue.h('input', { type: 'hidden', name: props.name ?? generatedName, 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