Files
tgCrewAdmin/src/components/pnBottomSheetDialog.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>