mirror of https://github.com/nocodb/nocodb
DarkPhoenix2704
2 months ago
6 changed files with 324 additions and 143 deletions
@ -0,0 +1,183 @@
|
||||
<script setup> |
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' |
||||
|
||||
const props = defineProps({ |
||||
modelValue: { |
||||
type: Boolean, |
||||
default: false, |
||||
}, |
||||
placement: { |
||||
type: String, |
||||
default: 'center', |
||||
validator: (value) => ['top', 'bottom', 'left', 'right', 'center'].includes(value), |
||||
}, |
||||
offset: { |
||||
type: Array, |
||||
default: () => [0, 10], |
||||
}, |
||||
width: { |
||||
type: String, |
||||
default: 'auto', |
||||
}, |
||||
zIndex: { |
||||
type: Number, |
||||
default: 1000, |
||||
}, |
||||
closeOnClickOutside: { |
||||
type: Boolean, |
||||
default: true, |
||||
}, |
||||
closeOnEsc: { |
||||
type: Boolean, |
||||
default: true, |
||||
}, |
||||
transition: { |
||||
type: String, |
||||
default: 'fade', |
||||
}, |
||||
}) |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const triggerRef = ref(null) |
||||
const popoverRef = ref(null) |
||||
const isOpen = ref(props.modelValue) |
||||
|
||||
const popoverStyle = computed(() => ({ |
||||
position: 'fixed', |
||||
zIndex: props.zIndex, |
||||
width: props.width, |
||||
})) |
||||
|
||||
const updatePopoverPosition = () => { |
||||
if (!triggerRef.value || !popoverRef.value) return |
||||
|
||||
const triggerRect = triggerRef.value.getBoundingClientRect() |
||||
const popoverRect = popoverRef.value.getBoundingClientRect() |
||||
|
||||
let top, left |
||||
|
||||
switch (props.placement) { |
||||
case 'top': |
||||
top = triggerRect.top - popoverRect.height - props.offset[1] |
||||
left = triggerRect.left + (triggerRect.width - popoverRect.width) / 2 + props.offset[0] |
||||
break |
||||
case 'bottom': |
||||
top = triggerRect.bottom + props.offset[1] |
||||
left = triggerRect.left + (triggerRect.width - popoverRect.width) / 2 + props.offset[0] |
||||
break |
||||
case 'left': |
||||
top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2 + props.offset[1] |
||||
left = triggerRect.left - popoverRect.width - props.offset[0] |
||||
break |
||||
case 'right': |
||||
top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2 + props.offset[1] |
||||
left = triggerRect.right + props.offset[0] |
||||
break |
||||
case 'center': |
||||
top = triggerRect.top + (triggerRect.height - popoverRect.height) / 2 + props.offset[1] |
||||
left = triggerRect.left + (triggerRect.width - popoverRect.width) / 2 + props.offset[0] |
||||
break |
||||
} |
||||
|
||||
// Ensure the popover stays within the viewport |
||||
const viewportWidth = window.innerWidth |
||||
const viewportHeight = window.innerHeight |
||||
|
||||
top = Math.max(0, Math.min(top, viewportHeight - popoverRect.height)) |
||||
left = Math.max(0, Math.min(left, viewportWidth - popoverRect.width)) |
||||
|
||||
Object.assign(popoverRef.value.style, { |
||||
top: `${top}px`, |
||||
left: `${left}px`, |
||||
}) |
||||
} |
||||
|
||||
const openPopover = () => { |
||||
isOpen.value = true |
||||
emit('update:modelValue', true) |
||||
nextTick(updatePopoverPosition) |
||||
} |
||||
|
||||
const closePopover = () => { |
||||
isOpen.value = false |
||||
emit('update:modelValue', false) |
||||
} |
||||
|
||||
const handleClickOutside = (event) => { |
||||
if ( |
||||
props.closeOnClickOutside && |
||||
popoverRef.value && |
||||
!popoverRef.value.contains(event.target) && |
||||
!triggerRef.value.contains(event.target) |
||||
) { |
||||
closePopover() |
||||
} |
||||
} |
||||
|
||||
const handleKeyDown = (event) => { |
||||
if (props.closeOnEsc && event.key === 'Escape') { |
||||
closePopover() |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
if (props.closeOnClickOutside) document.addEventListener('mousedown', handleClickOutside) |
||||
if (props.closeOnEsc) document.addEventListener('keydown', handleKeyDown) |
||||
window.addEventListener('resize', updatePopoverPosition) |
||||
window.addEventListener('scroll', updatePopoverPosition) |
||||
}) |
||||
|
||||
onUnmounted(() => { |
||||
if (props.closeOnClickOutside) document.removeEventListener('mousedown', handleClickOutside) |
||||
if (props.closeOnEsc) document.removeEventListener('keydown', handleKeyDown) |
||||
window.removeEventListener('resize', updatePopoverPosition) |
||||
window.removeEventListener('scroll', updatePopoverPosition) |
||||
}) |
||||
|
||||
watch( |
||||
() => props.modelValue, |
||||
(newValue) => { |
||||
isOpen.value = newValue |
||||
if (newValue) nextTick(updatePopoverPosition) |
||||
}, |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<div ref="triggerRef" @click="openPopover"> |
||||
<slot name="trigger" :open="openPopover" :close="closePopover" :is-open="isOpen"> |
||||
<button>Open Popover</button> |
||||
</slot> |
||||
</div> |
||||
|
||||
<Teleport to="body"> |
||||
<Transition :name="transition"> |
||||
<div v-if="isOpen" ref="popoverRef" :style="popoverStyle" class="popover-content"> |
||||
<slot name="content" :close="closePopover"> |
||||
<div class="p-4"> |
||||
<p>Default popover content</p> |
||||
<button @click="closePopover">Close</button> |
||||
</div> |
||||
</slot> |
||||
</div> |
||||
</Transition> |
||||
</Teleport> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.popover-content { |
||||
} |
||||
|
||||
.fade-enter-active, |
||||
.fade-leave-active { |
||||
transition: opacity 0.3s ease; |
||||
} |
||||
|
||||
.fade-enter-from, |
||||
.fade-leave-to { |
||||
opacity: 0; |
||||
} |
||||
</style> |
Loading…
Reference in new issue