147 lines
4.1 KiB
Vue
147 lines
4.1 KiB
Vue
<template>
|
|
<q-dialog
|
|
v-model="modelValue"
|
|
transition-show="slide-up"
|
|
transition-hide="slide-down"
|
|
position="bottom"
|
|
:persistent="loading"
|
|
>
|
|
<q-card
|
|
ref="cardRef"
|
|
class="fix-card-width column no-scroll no-wrap q-px-none overflow-hidden relative-position"
|
|
style="
|
|
border-top-left-radius: var(--top-radius) !important;
|
|
border-top-right-radius: var(--top-radius) !important;
|
|
max-height: calc(100vh - 48px - var(--tg-content-safe-area-inset-top) - var(--tg-safe-area-inset-top));
|
|
"
|
|
>
|
|
<div
|
|
ref="cardHeaderRef"
|
|
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
|
>
|
|
<q-resize-observer @resize="onHeaderResize" />
|
|
<div class="column q-mx-xs">
|
|
<span class="text-bold">
|
|
{{ $t(title) }}
|
|
</span>
|
|
<span v-if="caption" class="text-grey text-caption">
|
|
{{ $t(caption) }}
|
|
</span>
|
|
</div>
|
|
<q-space/>
|
|
<div
|
|
v-if="!loading"
|
|
class="flex items-center justify-between no-wrap"
|
|
>
|
|
<slot name="btn-slot">
|
|
<q-btn
|
|
icon="mdi-close"
|
|
dense flat round
|
|
v-close-popup
|
|
/>
|
|
</slot>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="q-px-none q-ma-none">
|
|
<pn-shadow-scroll
|
|
:height="bodyHeight"
|
|
:bottomOffset
|
|
:topOffset
|
|
>
|
|
<div class="q-pa-none q-ma-none">
|
|
<q-resize-observer @resize="onInnerBodyResize" />
|
|
<slot />
|
|
</div>
|
|
</pn-shadow-scroll>
|
|
</div>
|
|
|
|
<div v-if="hasFooter" class="absolute-bottom">
|
|
<q-resize-observer @resize="onFooterResize" />
|
|
<div class="q-pa-md">
|
|
<slot name="footer" />
|
|
</div>
|
|
<div :style="{ height: tgInsetBottom + 'px' }" />
|
|
</div>
|
|
</q-card>
|
|
</q-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted, useSlots } from 'vue'
|
|
|
|
const modelValue = defineModel<boolean>({ required: true })
|
|
const props = withDefaults(defineProps<{
|
|
title: string
|
|
caption?: string
|
|
maximized?: boolean
|
|
loading?: boolean
|
|
}>(), {
|
|
maximized: true,
|
|
loading: false
|
|
})
|
|
|
|
const slots = useSlots()
|
|
const cardRef = ref()
|
|
|
|
const headerHeight = ref(0)
|
|
const footerHeight = ref(0)
|
|
const bodyInnerHeight = ref(0)
|
|
const tgInsetBottom = ref(0)
|
|
const topOffset = 16
|
|
|
|
const windowHeight = ref(window.innerHeight)
|
|
|
|
const hasFooter = computed(() => !!slots['footer'])
|
|
const bottomOffset = computed(() => hasFooter.value ? footerHeight.value : tgInsetBottom.value)
|
|
|
|
const bodyHeight = computed(() => {
|
|
// hack for update computed params on window resize
|
|
const _trigger = windowHeight.value //eslint-disable-line
|
|
const el = cardRef.value?.$el
|
|
if (!el) return 0
|
|
|
|
const maxH = parseFloat(window.getComputedStyle(el).maxHeight) || 0
|
|
|
|
return props.maximized
|
|
? maxH - headerHeight.value
|
|
: Math.min(
|
|
bodyInnerHeight.value + topOffset + (hasFooter.value ? footerHeight.value : 0),
|
|
maxH - headerHeight.value
|
|
)
|
|
})
|
|
|
|
interface SizeParams { height: number; width: number }
|
|
const onHeaderResize = (size: SizeParams) => headerHeight.value = size.height
|
|
const onFooterResize = (size: SizeParams) => footerHeight.value = hasFooter.value ? size.height : 0
|
|
const onInnerBodyResize = (size: SizeParams) => bodyInnerHeight.value = size.height + 32
|
|
|
|
const handleWindowResize = () => {
|
|
windowHeight.value = window.innerHeight
|
|
}
|
|
|
|
const getTgInsetBottom = () => {
|
|
const value = getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--tg-safe-area-inset-bottom')
|
|
.trim()
|
|
|
|
tgInsetBottom.value = parseFloat(value) || 0
|
|
}
|
|
|
|
onMounted(() => getTgInsetBottom())
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('resize', handleWindowResize)
|
|
getTgInsetBottom()
|
|
})
|
|
|
|
onUnmounted(() => window.removeEventListener('resize', handleWindowResize))
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fix-card-width {
|
|
width: var(--body-width) !important;
|
|
}
|
|
</style>
|