forked from noxious/client
113 lines
3.0 KiB
Vue
113 lines
3.0 KiB
Vue
<template>
|
||
<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.stop="deleteChip(i)" aria-label="Remove tag">×</button>
|
||
</div>
|
||
<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>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, watch } from 'vue'
|
||
import type { Ref } from 'vue'
|
||
|
||
interface Props {
|
||
modelValue?: string[]
|
||
maxChips?: number
|
||
maxChipLength?: number
|
||
placeholder?: string
|
||
allowDuplicates?: boolean
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
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)
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
(newValue) => {
|
||
internalValue.value = newValue ? [...newValue] : []
|
||
},
|
||
{ 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 (validateChip(trimmedInput)) {
|
||
internalValue.value.push(trimmedInput)
|
||
emit('update:modelValue', internalValue.value)
|
||
currentInput.value = ''
|
||
}
|
||
}
|
||
|
||
const deleteChip = (index: number) => {
|
||
internalValue.value.splice(index, 1)
|
||
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>
|