2022-04-15 14:24:30 +00:00
|
|
|
<template>
|
2024-05-08 09:11:32 +00:00
|
|
|
<nav
|
|
|
|
v-if="shown"
|
|
|
|
ref="el"
|
|
|
|
v-koel-focus
|
|
|
|
:class="extraClass"
|
2024-09-15 14:58:17 +00:00
|
|
|
:style="{ top, left, bottom, right }"
|
2024-05-19 05:49:42 +00:00
|
|
|
class="menu context-menu select-none shadow"
|
2024-05-08 09:11:32 +00:00
|
|
|
tabindex="0"
|
|
|
|
@contextmenu.prevent
|
|
|
|
@keydown.esc="close"
|
|
|
|
>
|
|
|
|
<ul>
|
|
|
|
<slot>Menu items go here.</slot>
|
|
|
|
</ul>
|
|
|
|
</nav>
|
2022-04-15 14:24:30 +00:00
|
|
|
</template>
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
<script lang="ts" setup>
|
|
|
|
import { nextTick, ref, toRefs } from 'vue'
|
2024-09-15 14:58:17 +00:00
|
|
|
import { onClickOutside } from '@vueuse/core'
|
2022-07-20 08:00:02 +00:00
|
|
|
import { eventBus, logger } from '@/utils'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-05-03 16:51:59 +00:00
|
|
|
const props = defineProps<{ extraClass?: string }>()
|
2022-04-15 17:00:08 +00:00
|
|
|
const { extraClass } = toRefs(props)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
const el = ref<HTMLElement>()
|
|
|
|
const shown = ref(false)
|
2024-04-23 20:36:46 +00:00
|
|
|
const top = ref('0')
|
|
|
|
const left = ref('0')
|
2024-09-15 14:58:17 +00:00
|
|
|
const bottom = ref('auto')
|
|
|
|
const right = ref('auto')
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
const preventOffScreen = async (element: HTMLElement, isSubmenu = false) => {
|
|
|
|
const { bottom, right } = element.getBoundingClientRect()
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
if (bottom > window.innerHeight) {
|
|
|
|
element.style.top = 'auto'
|
|
|
|
element.style.bottom = '0'
|
|
|
|
} else {
|
|
|
|
element.style.bottom = 'auto'
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
if (right > window.innerWidth) {
|
|
|
|
element.style.right = isSubmenu ? `${el.value?.getBoundingClientRect().width}px` : '0'
|
|
|
|
element.style.left = 'auto'
|
|
|
|
} else {
|
|
|
|
element.style.right = 'auto'
|
|
|
|
}
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2024-03-17 17:37:58 +00:00
|
|
|
const safeAreaHeight = ref('0px')
|
|
|
|
const safeAreaWidth = ref('0px')
|
|
|
|
const safeAreaClipPath = ref('0 0, 0 0, 0 0, 0 0')
|
|
|
|
|
|
|
|
type MenuItem = HTMLElement & {
|
|
|
|
eventsRegistered?: boolean
|
|
|
|
}
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
const initSubmenus = () => {
|
2024-03-17 17:37:58 +00:00
|
|
|
el.value?.querySelectorAll<HTMLElement>('.has-sub').forEach((item: MenuItem) => {
|
2022-04-15 17:00:08 +00:00
|
|
|
const submenu = item.querySelector<HTMLElement>('.submenu')
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2024-03-17 17:37:58 +00:00
|
|
|
if (!submenu || item.eventsRegistered) {
|
2022-04-15 17:00:08 +00:00
|
|
|
return
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
item.addEventListener('mouseenter', async () => {
|
2024-09-15 14:58:17 +00:00
|
|
|
submenu.style.top = '0'
|
|
|
|
submenu.style.left = '100%'
|
|
|
|
submenu.style.bottom = 'auto'
|
|
|
|
submenu.style.right = 'auto'
|
2022-04-15 17:00:08 +00:00
|
|
|
submenu.style.display = 'block'
|
2024-09-15 14:58:17 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
await nextTick()
|
|
|
|
await preventOffScreen(submenu, true)
|
|
|
|
})
|
|
|
|
|
2024-03-17 17:37:58 +00:00
|
|
|
item.addEventListener('mousemove', async (e: MouseEvent) => {
|
|
|
|
await nextTick()
|
|
|
|
const rect = submenu.getBoundingClientRect()
|
|
|
|
safeAreaHeight.value = rect.height + 'px'
|
|
|
|
safeAreaWidth.value = rect.x - e.clientX + 'px'
|
|
|
|
safeAreaClipPath.value = `polygon(100% 0, 0 ${e.clientY - rect.top}px, 100% 100%)`
|
|
|
|
})
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
item.addEventListener('mouseleave', () => {
|
|
|
|
submenu.style.top = '0'
|
|
|
|
submenu.style.bottom = 'auto'
|
|
|
|
submenu.style.display = 'none'
|
|
|
|
})
|
2024-03-17 17:37:58 +00:00
|
|
|
|
|
|
|
item.eventsRegistered = true
|
2022-04-15 17:00:08 +00:00
|
|
|
})
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2024-03-17 17:37:58 +00:00
|
|
|
const open = async (t = 0, l = 0) => {
|
2024-04-23 20:36:46 +00:00
|
|
|
top.value = `${t}px`
|
|
|
|
left.value = `${l}px`
|
2024-09-15 14:58:17 +00:00
|
|
|
bottom.value = 'auto'
|
|
|
|
right.value = 'auto'
|
2022-04-15 17:00:08 +00:00
|
|
|
shown.value = true
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
await nextTick()
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
try {
|
|
|
|
await preventOffScreen(el.value!)
|
2022-12-02 16:17:37 +00:00
|
|
|
initSubmenus()
|
2024-04-23 11:24:29 +00:00
|
|
|
} catch (error: unknown) {
|
|
|
|
logger.error(error)
|
2022-04-15 17:00:08 +00:00
|
|
|
// in a non-browser environment (e.g., unit testing), these two functions are broken due to calls to
|
2022-07-19 08:19:57 +00:00
|
|
|
// getBoundingClientRect() and querySelectorAll()
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
2022-04-30 11:55:54 +00:00
|
|
|
|
2022-12-02 16:17:37 +00:00
|
|
|
eventBus.emit('CONTEXT_MENU_OPENED', el.value!)
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
2022-04-15 17:00:08 +00:00
|
|
|
|
2022-11-15 15:52:38 +00:00
|
|
|
const close = () => (shown.value = false)
|
2022-04-15 17:00:08 +00:00
|
|
|
|
2024-09-15 14:58:17 +00:00
|
|
|
onClickOutside(el, close)
|
|
|
|
|
2022-05-08 18:18:27 +00:00
|
|
|
// ensure there's only one context menu at any time
|
2022-12-02 16:17:37 +00:00
|
|
|
eventBus.on('CONTEXT_MENU_OPENED', target => target === el.value || close())
|
2022-04-30 11:55:54 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
defineExpose({ open, close, shown })
|
|
|
|
</script>
|
2022-10-13 15:18:47 +00:00
|
|
|
|
2024-04-04 20:13:35 +00:00
|
|
|
<style lang="postcss" scoped>
|
2022-10-13 15:18:47 +00:00
|
|
|
nav {
|
2024-04-04 22:20:42 +00:00
|
|
|
:deep(.has-sub) {
|
|
|
|
@apply after:absolute after:right-0 after:top-0 after:z-[2] after:opacity-0;
|
|
|
|
}
|
2024-03-17 17:37:58 +00:00
|
|
|
|
|
|
|
:deep(.has-sub)::after {
|
|
|
|
width: v-bind(safeAreaWidth);
|
|
|
|
height: v-bind(safeAreaHeight);
|
|
|
|
clip-path: v-bind(safeAreaClipPath);
|
|
|
|
}
|
2022-10-13 15:18:47 +00:00
|
|
|
}
|
|
|
|
</style>
|