koel/resources/assets/js/components/ui/ContextMenuBase.vue

134 lines
3.2 KiB
Vue
Raw Normal View History

2022-04-15 14:24:30 +00:00
<template>
<nav
2022-04-24 08:29:14 +00:00
v-if="shown"
ref="el"
v-koel-clickaway="close"
v-koel-focus
2022-04-15 14:24:30 +00:00
:class="extraClass"
:style="{ top: `${top}px`, left: `${left}px` }"
2022-04-24 08:29:14 +00:00
class="menu context-menu"
2022-04-15 14:24:30 +00:00
tabindex="0"
2022-04-24 08:29:14 +00:00
@contextmenu.prevent
2022-04-15 14:24:30 +00:00
@keydown.esc="close"
>
<ul>
<slot>Menu items go here.</slot>
</ul>
</nav>
</template>
2022-04-15 17:00:08 +00:00
<script lang="ts" setup>
import { nextTick, ref, toRefs } from 'vue'
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)
const top = ref(0)
const left = ref(0)
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
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 = () => {
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
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 () => {
submenu.style.display = 'block'
await nextTick()
await preventOffScreen(submenu, true)
})
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'
})
item.eventsRegistered = true
2022-04-15 17:00:08 +00:00
})
}
2022-04-15 14:24:30 +00:00
const open = async (t = 0, l = 0) => {
top.value = t
left.value = l
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()
2022-04-15 17:00:08 +00:00
} catch (e) {
2022-07-20 08:00:02 +00:00
logger.error(e)
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
const close = () => (shown.value = false)
2022-04-15 17:00:08 +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
<style lang="scss" scoped>
nav {
user-select: none;
:deep(.has-sub)::after {
position: absolute;
content: '';
right: 0;
top: 0;
z-index: 2;
opacity: 0;
width: v-bind(safeAreaWidth);
height: v-bind(safeAreaHeight);
clip-path: v-bind(safeAreaClipPath);
}
2022-10-13 15:18:47 +00:00
}
</style>