diff --git a/src/components/forms/ChipsInput.vue b/src/components/forms/ChipsInput.vue index eb19f5f..10d5687 100644 --- a/src/components/forms/ChipsInput.vue +++ b/src/components/forms/ChipsInput.vue @@ -1,10 +1,10 @@ <template> - <div class="flex flex-wrap items-center input-field gap-1"> - <div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2"> + <div class="flex flex-wrap items-center input-field gap-1" @click="focusInput"> + <div v-for="(chip, i) in internalValue" :key="i" class="flex gap-2.5 items-center bg-cyan rounded py-1 px-2" role="listitem"> <span class="text-xs text-white">{{ chip }}</span> - <button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click="deleteChip(i)" aria-label="Remove chip">×</button> + <button type="button" class="text-xs cursor-pointer text-white font-light font-default not-italic hover:text-gray-50" @click.stop="deleteChip(i)" aria-label="Remove tag">×</button> </div> - <input class="outline-none border-none p-1 text-gray-300" placeholder="Tag name" v-model="currentInput" @keypress.enter.prevent="addChip" @keydown.backspace="handleBackspace" /> + <input ref="inputRef" class="outline-none border-none p-1 text-gray-300 min-w-[60px] flex-grow" :placeholder="placeholder" v-model.trim="currentInput" @keydown="handleKeydown" @paste="handlePaste" :maxlength="maxChipLength" aria-label="Add new tag" /> </div> </template> @@ -14,20 +14,29 @@ import type { Ref } from 'vue' interface Props { modelValue?: string[] + maxChips?: number + maxChipLength?: number + placeholder?: string + allowDuplicates?: boolean } const props = withDefaults(defineProps<Props>(), { - modelValue: () => [] + modelValue: () => [], + maxChips: 10, + maxChipLength: 20, + placeholder: 'Add tag', + allowDuplicates: false }) const emit = defineEmits<{ (e: 'update:modelValue', value: string[]): void + (e: 'error', message: string): void }>() const currentInput: Ref<string> = ref('') const internalValue = ref<string[]>([]) +const inputRef = ref<HTMLInputElement | null>(null) -// Initialize internalValue with props.modelValue watch( () => props.modelValue, (newValue) => { @@ -36,9 +45,27 @@ watch( { immediate: true } ) +const validateChip = (chip: string): boolean => { + if (!chip) { + return false + } + + if (!props.allowDuplicates && internalValue.value.includes(chip)) { + emit('error', 'Duplicate tags are not allowed') + return false + } + + if (internalValue.value.length >= props.maxChips) { + emit('error', `Maximum ${props.maxChips} tags allowed`) + return false + } + + return true +} + const addChip = () => { const trimmedInput = currentInput.value.trim() - if (trimmedInput && !internalValue.value.includes(trimmedInput)) { + if (validateChip(trimmedInput)) { internalValue.value.push(trimmedInput) emit('update:modelValue', internalValue.value) currentInput.value = '' @@ -50,10 +77,36 @@ const deleteChip = (index: number) => { emit('update:modelValue', internalValue.value) } -const handleBackspace = (event: KeyboardEvent) => { - if (event.key === 'Backspace' && currentInput.value === '' && internalValue.value.length > 0) { - internalValue.value.pop() - emit('update:modelValue', internalValue.value) +const handleKeydown = (event: KeyboardEvent) => { + switch (event.key) { + case 'Enter': + event.preventDefault() + addChip() + break + case 'Backspace': + if (currentInput.value === '' && internalValue.value.length > 0) { + deleteChip(internalValue.value.length - 1) + } + break } } + +const handlePaste = (event: ClipboardEvent) => { + event.preventDefault() + const pastedText = event.clipboardData?.getData('text') + if (pastedText) { + const chips = pastedText + .split(/[,\n]/) + .map((chip) => chip.trim()) + .filter(Boolean) + chips.forEach((chip) => { + currentInput.value = chip + addChip() + }) + } +} + +const focusInput = () => { + inputRef.value?.focus() +} </script>