151 lines
4.0 KiB
Vue
151 lines
4.0 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, inject, provide, shallowReactive, watch, onScopeDispose } from 'vue'
|
|
import { whenever, useElementBounding, onClickOutside } from '@vueuse/core'
|
|
|
|
import { isMobileView, useScreenSize } from '~/composables/screen'
|
|
import { POPOVER_INJECTION_KEY, POPOVER_CONTEXT_INJECTION_KEY } from '~/injection-keys'
|
|
import { type ColorProps, type DefaultProps, propsToColor } from '~/composables/colors';
|
|
|
|
const open = defineModel('open', { default: false })
|
|
|
|
const { positioning = 'vertical', ...colorProps } = defineProps<{
|
|
positioning?:'horizontal' | 'vertical'
|
|
} & (ColorProps | DefaultProps)>()
|
|
|
|
// Template refs
|
|
const popover = ref()
|
|
const slot = ref()
|
|
|
|
// Click outside
|
|
const mobileClickOutside = (event: MouseEvent) => {
|
|
const inPopover = !!(event.target as HTMLElement).closest('.funkwhale.popover')
|
|
if (isMobile.value && !inPopover) {
|
|
open.value = false
|
|
}
|
|
}
|
|
onClickOutside(popover, async (event) => {
|
|
const inPopover = !!(event.target as HTMLElement).closest('.funkwhale.popover')
|
|
if (!isMobile.value && !inPopover) {
|
|
open.value = false
|
|
}
|
|
}, { ignore: [slot] })
|
|
|
|
// Auto positioning
|
|
const isMobile = isMobileView()
|
|
const { width, height, left, top, update } = useElementBounding(() => slot.value?.children[0])
|
|
const { width: popoverWidth, height: popoverHeight } = useElementBounding(popover, {
|
|
windowScroll: false
|
|
})
|
|
|
|
whenever(open, update, { immediate: true })
|
|
|
|
const { width: screenWidth, height: screenHeight } = useScreenSize()
|
|
|
|
// TODO (basic functionality):
|
|
// - I can't operate the popup with a keyboard. Remove barrier for people not using a mouse (A11y)
|
|
// - Switching to submenus is error-prone. When moving cursor into freshly opened submenu, it should not close if the cursor crosses another menu item
|
|
// - Large menus disappear. When menus get big, they need to scroll.
|
|
|
|
const position = computed(() => {
|
|
if (positioning === 'vertical' || isMobile.value) {
|
|
let offsetTop = top.value + height.value
|
|
if (offsetTop + popoverHeight.value > screenHeight.value) {
|
|
offsetTop -= popoverHeight.value + height.value
|
|
}
|
|
|
|
let offsetLeft = left.value
|
|
if (offsetLeft + popoverWidth.value > screenWidth.value) {
|
|
offsetLeft -= popoverWidth.value - width.value
|
|
}
|
|
|
|
return {
|
|
left: offsetLeft + 'px',
|
|
top: offsetTop + 'px'
|
|
}
|
|
}
|
|
|
|
let offsetTop = top.value
|
|
if (offsetTop + popoverHeight.value > screenHeight.value) {
|
|
offsetTop -= popoverHeight.value - height.value
|
|
}
|
|
|
|
let offsetLeft = left.value + width.value
|
|
if (offsetLeft + popoverWidth.value > screenWidth.value) {
|
|
offsetLeft -= popoverWidth.value + width.value
|
|
}
|
|
|
|
return {
|
|
left: offsetLeft + 'px',
|
|
top: offsetTop + 'px'
|
|
}
|
|
})
|
|
|
|
// Popover close stack
|
|
let stack = inject(POPOVER_INJECTION_KEY, [ref(false)])
|
|
if (!stack) {
|
|
provide(POPOVER_INJECTION_KEY, stack = shallowReactive([]))
|
|
}
|
|
|
|
stack.push(open)
|
|
onScopeDispose(() => {
|
|
stack?.splice(stack.indexOf(open), 1)
|
|
})
|
|
|
|
// Provide context for child items
|
|
const hoveredItem = ref(-2)
|
|
provide(POPOVER_CONTEXT_INJECTION_KEY, {
|
|
items: ref(0),
|
|
hoveredItem
|
|
})
|
|
|
|
// Closing
|
|
const closeChild = () => {
|
|
const ref = stack?.[stack.indexOf(open) + 1]
|
|
if (!ref) return
|
|
|
|
ref.value = false
|
|
}
|
|
|
|
// Recursively close popover tree
|
|
watch(open, (isOpen) => {
|
|
if (isOpen) return
|
|
closeChild()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="slot"
|
|
class="funkwhale popover-container"
|
|
>
|
|
<slot
|
|
:isOpen="open"
|
|
:toggleOpen="() => open = !open"
|
|
:open="() => open = true"
|
|
:close="() => open = false"
|
|
/>
|
|
</div>
|
|
|
|
<teleport v-if="open" to="body">
|
|
<div
|
|
:class="{ 'is-mobile': isMobile }"
|
|
class="funkwhale popover-outer"
|
|
@click.stop="mobileClickOutside"
|
|
>
|
|
<div
|
|
ref="popover"
|
|
:style="position"
|
|
:class="{ 'is-mobile': isMobile }"
|
|
class="funkwhale popover secondary"
|
|
v-bind="propsToColor(colorProps)"
|
|
>
|
|
<slot name="items" />
|
|
</div>
|
|
</div>
|
|
</teleport>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@import './popover.scss'
|
|
</style>
|