111 lines
2.2 KiB
Vue
111 lines
2.2 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
export interface MenuItem {
|
|
label: string
|
|
action: string
|
|
disabled?: boolean
|
|
separator?: boolean
|
|
}
|
|
|
|
defineProps<{
|
|
x: number
|
|
y: number
|
|
items: MenuItem[]
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
select: [action: string]
|
|
close: []
|
|
}>()
|
|
|
|
const menuRef = ref<HTMLElement | null>(null)
|
|
|
|
function handleClick(item: MenuItem) {
|
|
if (item.disabled) return
|
|
emit('select', item.action)
|
|
}
|
|
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (menuRef.value && !menuRef.value.contains(event.target as Node)) {
|
|
emit('close')
|
|
}
|
|
}
|
|
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
if (event.key === 'Escape') {
|
|
emit('close')
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('mousedown', handleClickOutside)
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('mousedown', handleClickOutside)
|
|
document.removeEventListener('keydown', handleKeyDown)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="menuRef"
|
|
class="context-menu"
|
|
:style="{ left: `${x}px`, top: `${y}px` }"
|
|
>
|
|
<template v-for="(item, index) in items" :key="index">
|
|
<div v-if="item.separator" class="separator" />
|
|
<button
|
|
v-else
|
|
class="menu-item"
|
|
:class="{ disabled: item.disabled }"
|
|
:disabled="item.disabled"
|
|
@click="handleClick(item)"
|
|
>
|
|
{{ item.label }}
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.context-menu {
|
|
position: fixed;
|
|
z-index: 1000;
|
|
min-width: 150px;
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
padding: var(--space-xs) 0;
|
|
}
|
|
|
|
.menu-item {
|
|
display: block;
|
|
width: 100%;
|
|
padding: var(--space-sm) var(--space-md);
|
|
text-align: left;
|
|
background: none;
|
|
border: none;
|
|
color: var(--color-text-primary);
|
|
font-size: var(--font-size-sm);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.menu-item:hover:not(.disabled) {
|
|
background: var(--color-bg-tertiary);
|
|
}
|
|
|
|
.menu-item.disabled {
|
|
color: var(--color-text-muted);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.separator {
|
|
height: 1px;
|
|
background: var(--color-border);
|
|
margin: var(--space-xs) 0;
|
|
}
|
|
</style>
|