Teren-app/resources/js/Components/Dropdown.vue
Simon Pocrnjič 63e0958b66 Dev branch
2025-11-02 12:31:01 +01:00

133 lines
3.3 KiB
Vue

<script setup>
import { computed, ref, watch, onMounted, onUnmounted } from 'vue';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/Components/ui/dropdown-menu';
const props = defineProps({
align: {
type: String,
default: 'right',
},
width: {
type: String,
default: '48',
},
contentClasses: {
type: Array,
default: () => ['py-1', 'bg-white'],
},
closeOnContentClick: {
type: Boolean,
default: true,
},
});
const open = ref(false);
// Expose close method for parent components
const close = () => {
open.value = false;
};
defineExpose({
close,
open,
});
// Close dropdown when dialog opens
const handleDialogOpen = () => {
if (open.value) {
open.value = false;
}
};
// Watch for dialog opens using MutationObserver
let observer = null;
onMounted(() => {
// Listen for custom dialog open events
window.addEventListener('dialog:open', handleDialogOpen);
// Watch for dialog state changes in the DOM
observer = new MutationObserver((mutations) => {
// Check if any dialog has data-state="open"
const openDialogs = document.querySelectorAll('[data-state="open"]');
const hasOpenDialog = Array.from(openDialogs).some((dialog) => {
// Check if it's a dialog element (has role="dialog" or is DialogContent)
const role = dialog.getAttribute('role');
const isDialogContent = dialog.classList?.contains('DialogContent') ||
dialog.querySelector('[role="dialog"]') ||
dialog.closest('[role="dialog"]');
return role === 'dialog' || isDialogContent;
});
if (hasOpenDialog && open.value) {
handleDialogOpen();
}
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ['data-state'],
subtree: true,
childList: true,
});
});
onUnmounted(() => {
window.removeEventListener('dialog:open', handleDialogOpen);
if (observer) {
observer.disconnect();
}
});
const widthClass = computed(() => {
const map = {
'48': 'w-48', // 12rem
'64': 'w-64', // 16rem
'72': 'w-72', // 18rem
'80': 'w-80', // 20rem
'96': 'w-96', // 24rem
'wide': 'w-[34rem] max-w-[90vw]',
'auto': '',
};
return map[props.width.toString()] || '';
});
// Map align prop to shadcn-vue's align prop
// 'left' -> 'start', 'right' -> 'end'
const alignProp = computed(() => {
if (props.align === 'left') return 'start';
if (props.align === 'right') return 'end';
return 'start';
});
const combinedContentClasses = computed(() => {
// Merge width class with custom content classes
// Note: shadcn-vue already provides base styling, so we append custom classes
const classes = [widthClass.value];
if (props.contentClasses && props.contentClasses.length) {
classes.push(...props.contentClasses);
}
return classes.filter(Boolean).join(' ');
});
</script>
<template>
<DropdownMenu v-model:open="open">
<DropdownMenuTrigger as-child>
<slot name="trigger" />
</DropdownMenuTrigger>
<DropdownMenuContent
:align="alignProp"
:class="combinedContentClasses"
>
<slot name="content" />
</DropdownMenuContent>
</DropdownMenu>
</template>