update
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="support-picker-backdrop">
|
||||
<div
|
||||
v-if="open"
|
||||
class="support-messenger-picker__backdrop"
|
||||
:class="{ 'is-active': backdropActive }"
|
||||
@click="onBackdropClick"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<div
|
||||
v-show="open || closing"
|
||||
class="support-messenger-picker__stack"
|
||||
:class="[
|
||||
{ 'is-open': open },
|
||||
expandUp ? 'support-messenger-picker__stack--up' : 'support-messenger-picker__stack--down',
|
||||
]"
|
||||
:style="stackStyle"
|
||||
role="menu"
|
||||
aria-label="انتخاب پیامرسان"
|
||||
>
|
||||
<button
|
||||
v-for="(entry, index) in entries"
|
||||
:key="`${entry.messenger}-${entry.id}`"
|
||||
type="button"
|
||||
class="support-messenger-picker__btn"
|
||||
:style="{ '--i': index, '--total': entries.length }"
|
||||
:title="entry.label"
|
||||
:aria-label="entry.label"
|
||||
@click.stop="select(entry)"
|
||||
>
|
||||
<SupportMessengerBrandIcon :messenger="entry.messenger" />
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import SupportMessengerBrandIcon from '@/components/SupportMessengerBrandIcon.vue'
|
||||
import type { ResolvedSupportMessengerEntry } from '@/utils/supportMessenger'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
entries: ResolvedSupportMessengerEntry[]
|
||||
anchorEl?: HTMLElement | null
|
||||
}>(),
|
||||
{
|
||||
anchorEl: null,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
select: [entry: ResolvedSupportMessengerEntry]
|
||||
}>()
|
||||
|
||||
const BTN_SIZE = 46
|
||||
const GAP = 12
|
||||
const STACK_GAP = 10
|
||||
const CLOSE_MS = 220
|
||||
const BACKDROP_GUARD_MS = 380
|
||||
|
||||
const stackStyle = ref<Record<string, string>>({})
|
||||
const closing = ref(false)
|
||||
const expandUp = ref(true)
|
||||
const backdropActive = ref(false)
|
||||
let closeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let backdropTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function close() {
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function select(entry: ResolvedSupportMessengerEntry) {
|
||||
emit('select', entry)
|
||||
close()
|
||||
}
|
||||
|
||||
function clearTimers() {
|
||||
if (closeTimer) {
|
||||
clearTimeout(closeTimer)
|
||||
closeTimer = null
|
||||
}
|
||||
if (backdropTimer) {
|
||||
clearTimeout(backdropTimer)
|
||||
backdropTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function onBackdropClick() {
|
||||
if (!backdropActive.value) return
|
||||
close()
|
||||
}
|
||||
|
||||
function armBackdropGuard() {
|
||||
backdropActive.value = false
|
||||
if (backdropTimer) clearTimeout(backdropTimer)
|
||||
backdropTimer = setTimeout(() => {
|
||||
backdropActive.value = true
|
||||
backdropTimer = null
|
||||
}, BACKDROP_GUARD_MS)
|
||||
}
|
||||
|
||||
async function reposition() {
|
||||
await nextTick()
|
||||
|
||||
const anchor = props.anchorEl
|
||||
const count = props.entries.length
|
||||
if (!anchor || count === 0) {
|
||||
expandUp.value = true
|
||||
stackStyle.value = {
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
'--stack-rise': '56px',
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const stackHeight = count * BTN_SIZE + Math.max(0, count - 1) * GAP
|
||||
const anchorCenterX = rect.left + rect.width / 2
|
||||
const anchorCenterY = rect.top + rect.height / 2
|
||||
const right = Math.max(12, window.innerWidth - anchorCenterX - BTN_SIZE / 2)
|
||||
|
||||
expandUp.value = anchorCenterY > window.innerHeight / 2
|
||||
|
||||
if (expandUp.value) {
|
||||
const bottom = Math.max(12, window.innerHeight - rect.top + STACK_GAP)
|
||||
stackStyle.value = {
|
||||
right: `${right}px`,
|
||||
bottom: `${bottom}px`,
|
||||
left: 'auto',
|
||||
top: 'auto',
|
||||
transform: 'none',
|
||||
'--stack-rise': `${Math.min(stackHeight + 6, 220)}px`,
|
||||
}
|
||||
} else {
|
||||
const top = Math.max(12, rect.bottom + STACK_GAP)
|
||||
stackStyle.value = {
|
||||
right: `${right}px`,
|
||||
top: `${top}px`,
|
||||
left: 'auto',
|
||||
bottom: 'auto',
|
||||
transform: 'none',
|
||||
'--stack-rise': `${Math.min(stackHeight + 6, 220)}px`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(isOpen) => {
|
||||
clearTimers()
|
||||
if (isOpen) {
|
||||
closing.value = false
|
||||
armBackdropGuard()
|
||||
void reposition()
|
||||
return
|
||||
}
|
||||
|
||||
backdropActive.value = false
|
||||
closing.value = true
|
||||
const delay = CLOSE_MS + Math.max(0, props.entries.length - 1) * 28
|
||||
closeTimer = setTimeout(() => {
|
||||
closing.value = false
|
||||
closeTimer = null
|
||||
}, delay)
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.entries.length, props.anchorEl] as const,
|
||||
() => {
|
||||
if (props.open) void reposition()
|
||||
},
|
||||
)
|
||||
|
||||
function onViewportChange() {
|
||||
if (props.open || closing.value) void reposition()
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', onViewportChange, { passive: true })
|
||||
window.addEventListener('scroll', onViewportChange, { passive: true, capture: true })
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimers()
|
||||
if (typeof window === 'undefined') return
|
||||
window.removeEventListener('resize', onViewportChange, { capture: true })
|
||||
window.removeEventListener('scroll', onViewportChange, { capture: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.support-messenger-picker__backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5195;
|
||||
background: rgba(15, 23, 42, 0.2);
|
||||
pointer-events: none;
|
||||
|
||||
&.is-active {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack {
|
||||
position: fixed;
|
||||
z-index: 5205;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack--up {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack--down {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.support-messenger-picker__btn {
|
||||
--i: 0;
|
||||
--total: 1;
|
||||
--stack-rise: 56px;
|
||||
pointer-events: auto;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.2);
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
transition:
|
||||
transform 0.22s cubic-bezier(0.2, 1, 0.3, 1),
|
||||
opacity 0.16s ease,
|
||||
box-shadow 0.16s ease;
|
||||
transition-delay: calc(var(--i) * 38ms);
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack--up .support-messenger-picker__btn {
|
||||
transform-origin: center bottom;
|
||||
transform: translateY(calc(var(--stack-rise) * 0.55)) scale(0.2);
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack--down .support-messenger-picker__btn {
|
||||
transform-origin: center top;
|
||||
transform: translateY(calc(var(--stack-rise) * -0.55)) scale(0.2);
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack.is-open .support-messenger-picker__btn {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack:not(.is-open) .support-messenger-picker__btn {
|
||||
transition-delay: calc((var(--total) - 1 - var(--i)) * 24ms);
|
||||
transition-duration: 0.18s, 0.14s, 0.14s;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack--up:not(.is-open) .support-messenger-picker__btn {
|
||||
transform: translateY(calc(var(--stack-rise) * 0.4)) scale(0.15);
|
||||
}
|
||||
|
||||
.support-messenger-picker__stack--down:not(.is-open) .support-messenger-picker__btn {
|
||||
transform: translateY(calc(var(--stack-rise) * -0.4)) scale(0.15);
|
||||
}
|
||||
|
||||
.support-picker-backdrop-enter-active,
|
||||
.support-picker-backdrop-leave-active {
|
||||
transition: opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.support-picker-backdrop-enter-from,
|
||||
.support-picker-backdrop-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user