mobilizon/js/src/components/Map/VueBottomSheet.vue

371 lines
7.7 KiB
Vue

<template>
<div
:class="[
'bottom-sheet',
{
opened: opened,
closed: opened === false,
moving: moving,
},
]"
v-on="handlers"
ref="bottomSheet"
:style="{
'pointer-events':
backgroundClickable && clickToClose === false ? 'none' : 'all',
}"
>
<div
v-if="overlay"
class="bottom-sheet__backdrop"
:style="{ background: overlayColor }"
/>
<div
:style="[
{ bottom: cardP + 'px', maxWidth: maxWidth, maxHeight: maxHeight },
{ height: isFullScreen ? '100%' : 'auto' },
{ 'pointer-events': 'all' },
]"
:class="[
'bottom-sheet__card bg-white dark:bg-gray-800',
{ stripe: stripe, square: !rounded },
effect,
]"
ref="bottomSheetCard"
>
<div class="bottom-sheet__pan" ref="pan">
<div class="bottom-sheet__bar bg-gray-700 dark:bg-gray-400" />
</div>
<div
:style="{ height: contentH }"
ref="bottomSheetCardContent"
class="bottom-sheet__content"
>
<slot />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import Hammer from "hammerjs";
import { onBeforeUnmount, reactive, ref } from "vue";
const inited = ref(false);
const opened = ref(false);
const contentH = ref("auto");
const hammer = reactive<{
pan: any;
content: any;
}>({
pan: null,
content: null,
});
const contentScroll = ref(0);
const cardP = ref<number>(0);
const cardH = ref<number>(0);
const moving = ref(false);
const stripe = ref(0);
const props = withDefaults(
defineProps<{
overlay?: boolean;
maxWidth?: string;
maxHeight?: string;
clickToClose?: boolean;
effect?: string;
rounded?: boolean;
swipeAble?: boolean;
isFullScreen?: boolean;
overlayColor?: string;
backgroundScrollable?: boolean;
backgroundClickable?: boolean;
}>(),
{
overlay: true,
maxWidth: "640px",
maxHeight: "95%",
clickToClose: true,
effect: "fx-default",
rounded: true,
swipeAble: true,
isFullScreen: false,
overlayColor: "#0000004D",
backgroundScrollable: false,
backgroundClickable: false,
}
);
const emit = defineEmits(["closed", "opened"]);
const bottomSheetCardContent = ref();
const bottomSheetCard = ref();
const pan = ref();
const isIphone = () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const iPhone = /iPhone/.test(navigator.userAgent) && !window.MSStream;
const aspect = window.screen.width / window.screen.height;
return iPhone && aspect.toFixed(3) === "0.462";
};
const move = (event: any, type: any) => {
if (props.swipeAble) {
const delta = -event.deltaY;
if (
(type === "content" && event.type === "panup") ||
(type === "content" &&
event.type === "pandown" &&
contentScroll.value > 0)
) {
bottomSheetCardContent.value.scrollTop = contentScroll.value + delta;
} else if (event.type === "panup" || event.type === "pandown") {
moving.value = true;
if (event.deltaY > 0) {
cardP.value = delta;
}
}
if (event.isFinal) {
contentScroll.value = bottomSheetCardContent.value.scrollTop;
moving.value = false;
if (cardP.value < -30) {
opened.value = false;
cardP.value = (-cardH.value ?? 0) - stripe.value;
document.body.style.overflow = "";
emit("closed");
} else {
cardP.value = 0;
}
}
}
};
const init = () => {
return new Promise((resolve) => {
contentH.value = "auto";
stripe.value = isIphone() ? 20 : 0;
cardH.value = bottomSheetCard.value.clientHeight;
contentH.value = `${cardH.value - pan.value.clientHeight}px`;
bottomSheetCard.value.style.maxHeight = props.maxHeight;
cardP.value =
props.effect === "fx-slide-from-right" ||
props.effect === "fx-slide-from-left"
? 0
: -cardH.value - stripe.value;
if (!inited.value) {
inited.value = true;
const options = {
recognizers: [[Hammer.Pan, { direction: Hammer.DIRECTION_VERTICAL }]],
};
hammer.pan = new Hammer(pan.value, options as any);
hammer.pan?.on("panstart panup pandown panend", (e: any) => {
move(e, "pan");
});
hammer.content = new Hammer(bottomSheetCardContent.value, options as any);
hammer.content?.on("panstart panup pandown panend", (e: any) => {
move(e, "content");
});
}
setTimeout(() => {
resolve(undefined);
}, 100);
});
};
const open = async () => {
console.debug("open vue bottom sheet");
await init();
opened.value = true;
cardP.value = 0;
if (!props.backgroundScrollable) {
document.body.style.overflow = "hidden";
}
emit("opened");
};
const close = () => {
opened.value = false;
cardP.value =
props.effect === "fx-slide-from-right" ||
props.effect === "fx-slide-from-left"
? 0
: -cardH.value - stripe.value;
document.body.style.overflow = "";
emit("closed");
};
const clickOnBottomSheet = (event: any) => {
if (props.clickToClose) {
if (
event.target.classList.contains("bottom-sheet__backdrop") ||
event.target.classList.contains("bottom-sheet")
) {
close();
}
}
};
onBeforeUnmount(() => {
hammer?.pan?.destroy();
hammer?.content?.destroy();
});
const handlers = {
mousedown: clickOnBottomSheet,
touchstart: clickOnBottomSheet,
};
defineExpose({ open, close });
</script>
<style lang="scss" scoped>
.bottom-sheet {
z-index: 99999;
transition: all 0.4s ease;
position: relative;
* {
box-sizing: border-box;
}
&__content {
overflow-y: scroll;
}
&__backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
opacity: 0;
visibility: hidden;
}
&__card {
width: 100%;
position: fixed;
border-radius: 14px 14px 0 0;
left: 50%;
z-index: 9999;
margin: 0 auto;
&.square {
border-radius: 0;
}
&.stripe {
padding-bottom: 20px;
}
&.fx-default {
transform: translate(-50%, 0);
transition: bottom 0.3s ease;
}
&.fx-fadein-scale {
transform: translate(-50%, 0) scale(0.7);
opacity: 0;
transition: all 0.3s;
}
&.fx-slide-from-right {
transform: translate(100%, 0);
opacity: 0;
transition: all 0.3s cubic-bezier(0.25, 0.5, 0.5, 0.9);
}
&.fx-slide-from-left {
transform: translate(-100%, 0);
opacity: 0;
transition: all 0.3s cubic-bezier(0.25, 0.5, 0.5, 0.9);
}
}
&__pan {
padding-bottom: 20px;
padding-top: 15px;
height: 38px;
}
&__bar {
display: block;
width: 50px;
height: 3px;
border-radius: 14px;
margin: 0 auto;
cursor: pointer;
}
&.closed {
opacity: 0;
visibility: hidden;
.bottom-sheet__backdrop {
animation: hide 0.3s ease;
}
}
&.moving {
.bottom-sheet__card {
transition: none;
}
}
&.opened {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
.bottom-sheet__backdrop {
animation: show 0.3s ease;
opacity: 1;
visibility: visible;
}
.bottom-sheet__card {
&.fx-fadein-scale {
transform: translate(-50%, 0) scale(1);
opacity: 1;
}
&.fx-slide-from-right {
transform: translate(-50%, 0);
opacity: 1;
}
&.fx-slide-from-left {
transform: translate(-50%, 0);
opacity: 1;
}
}
}
}
@keyframes show {
0% {
opacity: 0;
visibility: hidden;
}
100% {
opacity: 1;
visibility: visible;
}
}
@keyframes hide {
0% {
opacity: 1;
visibility: visible;
}
100% {
opacity: 0;
visibility: hidden;
}
}
</style>