102 lines
3.4 KiB
Vue
102 lines
3.4 KiB
Vue
<script setup lang="ts">
|
|
import type { DropdownItem } from '~/types/dropdown';
|
|
|
|
interface Props {
|
|
items: DropdownItem[];
|
|
modelValue?: boolean;
|
|
placement?: 'right' | 'left' | 'center';
|
|
verticality?: 'asscending' | 'descending';
|
|
width?: string;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
modelValue: false,
|
|
placement: 'right',
|
|
verticality: 'descending',
|
|
width: 'auto'
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: boolean): void;
|
|
(e: 'select', item: DropdownItem): void;
|
|
}>()
|
|
|
|
const triggerRef = ref<HTMLElement | null>(null)
|
|
const isOpen = defineModel<boolean>({ required: true })
|
|
|
|
const toggle = () => {
|
|
isOpen.value = !isOpen.value
|
|
}
|
|
|
|
const select = (item: DropdownItem) => {
|
|
if (item.disabled || item.divider) return
|
|
emit('select', item)
|
|
isOpen.value = false
|
|
}
|
|
|
|
const placementClasses = computed(() => {
|
|
let classes = ''
|
|
|
|
switch (props.placement) {
|
|
case 'right':
|
|
classes += 'right-0 '
|
|
break
|
|
case 'left':
|
|
classes += 'left-0 '
|
|
break
|
|
case 'center':
|
|
classes += 'left-1/2 -translate-x-1/2 '
|
|
break
|
|
default:
|
|
classes += 'left-0 '
|
|
break
|
|
}
|
|
|
|
switch (props.verticality) {
|
|
case 'asscending':
|
|
classes += 'bottom-full mb-1.5'
|
|
break
|
|
case 'descending':
|
|
classes += 'top-full mt-1.5'
|
|
break
|
|
}
|
|
|
|
return classes
|
|
})
|
|
|
|
useClickOutside(triggerRef, () => {
|
|
isOpen.value = false
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="triggerRef" :class="$attrs.class">
|
|
<slot name="trigger" :toggle="toggle" :is-open="isOpen"></slot>
|
|
|
|
<Transition enter-active-class="transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
|
enter-from-class="opacity-0 scale-95" enter-to-class="opacity-100 scale-100"
|
|
leave-active-class="transition-all duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
|
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
|
<div v-if="isOpen" :class="[
|
|
'absolute z-50 bg-[var(--color-neutral)] border border-[var(--color-highlight)] rounded-xl p-1.5 flex flex-col gap-1',
|
|
placementClasses
|
|
]" :style="{ width: width !== 'auto' ? width : undefined }">
|
|
<template v-for="(item, index) in items" :key="index">
|
|
<div v-if="item.divider" class="h-px bg-[var(--color-highlight)] my-1" />
|
|
<button @click="select(item); item.onClick && item.onClick()" :disabled="item.disabled"
|
|
class="bg-transparent w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-colors text-left"
|
|
:class="[
|
|
item.disabled
|
|
? 'opacity-50 cursor-not-allowed'
|
|
: 'hover:bg-[var(--color-highlight)] cursor-pointer'
|
|
]">
|
|
<Icon v-if="item.icon" :name="item.icon" class="w-4 h-4 flex-shrink-0" />
|
|
<span class="text-sm whitespace-nowrap">{{ item.label }}</span>
|
|
<slot name="item-after" :item="item"></slot>
|
|
</button>
|
|
</template>
|
|
<slot name="content"></slot>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</template> |