@coreui/vue
Version:
UI Components Library for Vue.js
381 lines (378 loc) • 13.9 kB
JavaScript
import { defineComponent, ref, computed, watch, h } from 'vue';
import { CChip } from '../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 = 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 = ref(uniqueValues(props.defaultValue));
const inputValue = ref('');
const selectedValues = ref([]);
const rootRef = ref();
const inputRef = ref();
const values = computed(() => props.modelValue !== undefined
? uniqueValues(props.modelValue)
: uniqueValues(internalValues.value));
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 = 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 &&
h('label', {
class: 'chip-input-label',
for: props.id,
}, props.label),
...values.value.map((chipValue) => h(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,
})),
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 &&
h('input', {
type: 'hidden',
name: props.name,
value: values.value.join(','),
}),
].filter(Boolean);
return 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);
};
},
});
export { CChipInput };
//# sourceMappingURL=CChipInput.js.map