Skip to content

Instantly share code, notes, and snippets.

@AnoRebel
Created March 15, 2023 15:56
Show Gist options
  • Save AnoRebel/ba52a077b7b3bb0efe7748ab6e40c169 to your computer and use it in GitHub Desktop.
Save AnoRebel/ba52a077b7b3bb0efe7748ab6e40c169 to your computer and use it in GitHub Desktop.
Vue 3 Drawers
<template>
<section v-if="enabled">
<aside class="sidebar" :style="style" ref="element">
<slot></slot>
</aside>
<div class="overlay" ref="overlay"></div>
</section>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
const props = defineProps({
direction: {
type: String,
required: true,
},
exist: {
type: Boolean,
required: true,
},
});
const element = ref(null);
const overlay = ref(null);
const config = reactive({
"auto_speed" : '0.3s',
"manual_speed" : '0.03s',
"threshold" : 20,
"startTime" : null,
"startPos" : null,
"translate" : null,
"active" : false,
"visible" : true,
});
const enabled = computed(() => {
if(props.exist == true){
return true;
}
return false;
});
const style = computed(() => {
if(props.direction == 'right'){
return 'transform:translate3d(100%,0,0);right:0;';
}
return 'transform:translate3d(-100%,0,0);left:0;';
});
onMounted(() =>{
document.addEventListener('touchstart', e => handleStart(e));
document.addEventListener('touchmove', e => handleMove(e));
document.addEventListener('touchend', e => handleEnd(e));
document.addEventListener('touchcancel', e => handleEnd(e));
window.addEventListener('resize', e => setVisibality(e), true);
overlay.value.addEventListener('transitionend', e => handleZindex(e),false);
overlay.value.addEventListener('click', () => close(),false);
setVisibality();
});
const setVisibality = () => {
if(element.value.offsetWidth == 0){
config.visible = false;
} else {
config.visible = true;
}
};
const handleStart = e => {
config.startTime = new Date().getTime();
config.startPos = e.targetTouches[0].pageX;
element.value.style.transitionDuration = config.manual_speed;
};
const handleMove = e => {
let gesture = gesture(e);
let valid = validate(props.direction, gesture);
if (valid) {
let percent = percentage(props.direction, gesture);
if (props.direction == 'left'){
config.translate = (e.touches[0].pageX - element.value.offsetWidth);
if (config.translate < 0) {
element.value.style.transform = 'translate3d('+ config.translate +'px,0,0)';
} else {
open();
}
} else {
config.translate = -(screen.width - element.value.offsetWidth - e.touches[0].pageX);
if (config.translate > 0) {
element.value.style.transform = 'translate3d('+ config.translate +'px,0,0)';
} else {
open();
}
}
overlayOpacity(percent/100);
}
};
const handleEnd = e => {
let speed = speed(e);
let gesture = gesture(e);
let valid = validate(props.direction, gesture);
if (valid) {
if (speed > 0.6) {
if(!config.active){
open();
} else {
close();
}
} else {
if (element.value.offsetWidth/2 > Math.abs(config.translate)) {
open();
} else {
close();
}
}
}
};
const handleZindex = () => {
let opacity = window.getComputedStyle(overlay.value).getPropertyValue('opacity');
if (opacity <= 0) {
overlay.value.style.zIndex = -999;
}
};
const validate = (direction, gesture) => {
if (direction == 'left') {
if((config.active && gesture == 'swiperight') || (!config.active && (gesture == 'swipeleft' || config.startPos > config.threshold))){
return false;
}
} else {
if ((config.active && gesture == 'swipeleft') || (!config.active && (gesture == 'swiperight' || config.startPos < (screen.width - config.threshold)))){
return false;
}
}
if ((document.querySelector('.sidebar.active') && !config.active) || !config.visible) {
return false;
}
return true;
};
const overlayOpacity = (opacity) => {
overlay.value.style.opacity = opacity;
if (opacity > 0) {
overlay.value.style.zIndex = 999;
}
};
const gesture = e => {
let directions = [
'swipeleft',
'swiperight',
];
let currentPos = e.changedTouches[0].pageX;
if ((config.startPos - currentPos) < 0) {
return directions[1];
} else {
return directions[0];
}
};
const open = () => {
config.translate = 0;
element.value.style.transform = 'translate3d(' + config.translate + ', 0, 0)';
element.value.style.transitionDuration = config.auto_speed;
overlayOpacity(1);
lock(document.querySelector('html'));
lock(document.querySelector('body'));
element.value.classList.add('active');
config.active = true;
};
const close = () => {
if(props.direction=='left'){
config.translate = '-' + element.value.offsetWidth + 'px';
}else{
config.translate = element.value.offsetWidth + 'px';
}
element.value.style.transform = 'translate3d('+config.translate+',0,0)';
element.value.style.transitionDuration = config.auto_speed;
overlayOpacity(0);
unlock(document.querySelector('html'));
unlock(document.querySelector('body'));
element.value.classList.remove('active');
config.active = false;
};
const speed = e => {
let time = new Date().getTime() - config.startTime;
let startP = Math.abs(config.startPos);
let endP = Math.abs(e.changedTouches[0].pageX);
let distance = startP > endP ? (startP-endP) : (endP-startP);
return distance/time;
};
const percentage = (direction, gesture) => {
let percentage = 0;
let test = [];
if(direction == 'left'){
test = ['swipeleft','swiperight']
}else{
test = ['swiperight','swipeleft']
}
if(config.active && gesture == test[0]){
percentage = 100-Math.round(Math.abs(config.translate)/element.value.offsetWidth*100);
}
if(!config.active && gesture == test[1]){
percentage = Math.round(100-Math.abs(config.translate)/element.value.offsetWidth*100);
}
if(percentage>100){
percentage = 100;
}
if(percentage<0){
percentage = 0;
}
return percentage;
};
const lock = (element) => {
element.value.style.overflow = 'hidden';
element.value.style.touchAction = 'none';
};
const unlock = (element) => {
element.value.style.removeProperty('overflow');
element.value.style.removeProperty('touch-action');
};
onBeforeUnmount(() => {
document.removeEventListener('touchstart', e => handleStart(e));
document.removeEventListener('touchmove', e => handleMove(e));
document.removeEventListener('touchend', e => handleEnd(e));
document.removeEventListener('touchcancel', e => handleEnd(e));
window.removeEventListener('resize', e => setVisibality(e), true);
overlay.value.removeEventListener('transitionend', e => handleZindex(e),false);
overlay.value.removeEventListener('click', () => close(),false);
});
</script>
<style lang="scss" scoped>
div.overlay{
position:fixed;
z-index: -999;
left: 0px;
top:0px;
background:rgba(11, 10, 12, 0.35);
opacity: 0;
width: 100%;
height: 100%;
transition: opacity 0.3s ease;
}
aside.sidebar{
z-index: 9999;
position: fixed;
will-change: transform;
height: 100%;
top: 0px;
transition:transform 0.3s ease;
background:#fff;
width: 300px;
overflow-y: auto;
overflow-x: hidden;
word-wrap: break-word;
}
</style>
<template>
<div>
<transition name="fade" mode="out-in">
<div
:style="indexCls()"
@click="onMask"
v-if="$slots.default"
:class="{ mask }"
></div>
</transition>
<transition
:enter-active-class="alignInCls"
:leave-active-class="alignOutCls"
>
<div
key="content"
:class="{ closeable, [align.toLowerCase()]: true }"
v-if="$slots.default"
class="vue-simple-drawer cover"
:style="indexCls()"
>
<div @click.stop="close" v-if="closeable" class="close-btn">
<div class="leftright"></div>
<div class="rightleft"></div>
</div>
<slot></slot>
</div>
</transition>
</div>
</template>
<script setup>
import { computed, provide, inject } from "vue";
const props = defineProps({
align: {
type: String,
default: "right",
validator: function(value) {
// The value must match one of these strings
return ["left", "up", "right", "down"].indexOf(value) !== -1;
}
},
closeable: {
type: Boolean,
default: true
},
mask: {
type: Boolean,
default: true
},
maskClosable: {
type: Boolean,
default: false
},
zIndex: {
type: Number,
default() {
return simpleDrawerIndex;
}
}
});
const emit = defineEmits(["close"]);
provide("simpleDrawerIndex", computedIndex.value + 1);
const simpleDrawerIndex = inject("simpleDrawerIndex", 1000);
const close = () => emit("close");
const onMask = () => {
if (props.maskClosable) close();
};
const indexCls = (offset = 0) => {
return {
zIndex: computedIndex.value + offset
};
};
const alignInCls = computed(() => {
return `animated bounceIn${props.align.toLowerCase()}`;
});
const alignOutCls = computed(() => {
return `animated bounceOut${props.align.toLowerCase()}`;
});
const alighCloseCls = computed(() => {
return `close-${props.align.toLowerCase()}`;
});
const computedIndex = computed(() => {
return props.zIndex || simpleDrawerIndex;
});
</script>
<style lang="scss">
$--simple-drawer-softorange: #f4a259 !default;
$--simple-drawer-tomatored: #f25c66 !default;
$--simple-drawer-mediumblu: #1e272d !default;
$--simple-drawer-close-width: 28px !default;
$--simple-drawer-bg-color: #333333 !default;
$--simple-drawer-fg-color: white !default;
.vue-simple-drawer {
// default style
padding: 20px;
color: $--simple-drawer-fg-color;
background: $--simple-drawer-bg-color;
// common
position: fixed;
overflow: auto;
// top: 0;
// z-index: 99;
// bottom: 0;
&.closeable {
padding-top: 40px;
}
&.left {
left: 0;
top: 0;
bottom: 0;
}
&.right {
right: 0;
top: 0;
bottom: 0;
}
&.up {
top: 0;
left: 0;
right: 0;
}
&.down {
bottom: 0;
left: 0;
right: 0;
}
.close-btn {
// position: relative;
// margin: auto;
width: $--simple-drawer-close-width;
height: $--simple-drawer-close-width;
// margin-top: 100px;
// cursor: pointer;
position: absolute;
right: 0;
top: 20px;
// z-index: 100;
transform: translate(-50%, -50%);
color: currentColor;
font-size: 20px;
cursor: pointer;
user-select: none;
.leftright {
height: 4px;
width: $--simple-drawer-close-width;
position: absolute;
margin-top: calc($--simple-drawer-close-width/2);
background-color: $--simple-drawer-softorange;
border-radius: 2px;
transform: rotate(45deg);
transition: all 0.3s ease-in;
}
.rightleft {
height: 4px;
width: $--simple-drawer-close-width;
position: absolute;
margin-top: calc($--simple-drawer-close-width/2);
background-color: $--simple-drawer-softorange;
border-radius: 2px;
transform: rotate(-45deg);
transition: all 0.3s ease-in;
}
.close {
margin: 60px 0 0 5px;
position: absolute;
}
&:hover .leftright {
transform: rotate(-45deg);
background-color: $--simple-drawer-tomatored;
}
&:hover .rightleft {
transform: rotate(45deg);
background-color: $--simple-drawer-tomatored;
}
}
}
.mask {
position: fixed;
background: grey;
opacity: 0.5;
width: 100%;
left: 0;
top: 0;
height: 100%;
// &::before {
// content: "";
// position: absolute;
// background: grey;
// opacity: 0.5;
// width: 100%;
// left: 0;
// top: 0;
// height: 100%;
// // z-index: 98;
// }
}
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
// bounceInRight
@-webkit-keyframes bounceInRight {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
-webkit-transform: translate3d(3000px, 0, 0);
transform: translate3d(3000px, 0, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(-25px, 0, 0);
transform: translate3d(-25px, 0, 0);
}
75% {
-webkit-transform: translate3d(10px, 0, 0);
transform: translate3d(10px, 0, 0);
}
90% {
-webkit-transform: translate3d(-5px, 0, 0);
transform: translate3d(-5px, 0, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes bounceInRight {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
-webkit-transform: translate3d(3000px, 0, 0);
transform: translate3d(3000px, 0, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(-25px, 0, 0);
transform: translate3d(-25px, 0, 0);
}
75% {
-webkit-transform: translate3d(10px, 0, 0);
transform: translate3d(10px, 0, 0);
}
90% {
-webkit-transform: translate3d(-5px, 0, 0);
transform: translate3d(-5px, 0, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.bounceInright {
-webkit-animation-name: bounceInRight;
animation-name: bounceInRight;
}
// bounceOutLeft
@-webkit-keyframes bounceOutLeft {
20% {
opacity: 1;
-webkit-transform: translate3d(20px, 0, 0);
transform: translate3d(20px, 0, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(-2000px, 0, 0);
transform: translate3d(-2000px, 0, 0);
}
}
@keyframes bounceOutLeft {
20% {
opacity: 1;
-webkit-transform: translate3d(20px, 0, 0);
transform: translate3d(20px, 0, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(-2000px, 0, 0);
transform: translate3d(-2000px, 0, 0);
}
}
.bounceOutleft {
-webkit-animation-name: bounceOutLeft;
animation-name: bounceOutLeft;
}
// bounceInLeft
@-webkit-keyframes bounceInLeft {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
-webkit-transform: translate3d(-3000px, 0, 0);
transform: translate3d(-3000px, 0, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(25px, 0, 0);
transform: translate3d(25px, 0, 0);
}
75% {
-webkit-transform: translate3d(-10px, 0, 0);
transform: translate3d(-10px, 0, 0);
}
90% {
-webkit-transform: translate3d(5px, 0, 0);
transform: translate3d(5px, 0, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes bounceInLeft {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
-webkit-transform: translate3d(-3000px, 0, 0);
transform: translate3d(-3000px, 0, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(25px, 0, 0);
transform: translate3d(25px, 0, 0);
}
75% {
-webkit-transform: translate3d(-10px, 0, 0);
transform: translate3d(-10px, 0, 0);
}
90% {
-webkit-transform: translate3d(5px, 0, 0);
transform: translate3d(5px, 0, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.bounceInleft {
-webkit-animation-name: bounceInLeft;
animation-name: bounceInLeft;
}
// bounceOutRight
@-webkit-keyframes bounceOutRight {
20% {
opacity: 1;
-webkit-transform: translate3d(-20px, 0, 0);
transform: translate3d(-20px, 0, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(2000px, 0, 0);
transform: translate3d(2000px, 0, 0);
}
}
@keyframes bounceOutRight {
20% {
opacity: 1;
-webkit-transform: translate3d(-20px, 0, 0);
transform: translate3d(-20px, 0, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(2000px, 0, 0);
transform: translate3d(2000px, 0, 0);
}
}
.bounceOutright {
-webkit-animation-name: bounceOutRight;
animation-name: bounceOutRight;
}
@-webkit-keyframes bounceInDown {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
-webkit-transform: translate3d(0, -3000px, 0);
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(0, 25px, 0);
transform: translate3d(0, 25px, 0);
}
75% {
-webkit-transform: translate3d(0, -10px, 0);
transform: translate3d(0, -10px, 0);
}
90% {
-webkit-transform: translate3d(0, 5px, 0);
transform: translate3d(0, 5px, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes bounceInDown {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
-webkit-transform: translate3d(0, -3000px, 0);
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(0, 25px, 0);
transform: translate3d(0, 25px, 0);
}
75% {
-webkit-transform: translate3d(0, -10px, 0);
transform: translate3d(0, -10px, 0);
}
90% {
-webkit-transform: translate3d(0, 5px, 0);
transform: translate3d(0, 5px, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.bounceInup {
-webkit-animation-name: bounceInDown;
animation-name: bounceInDown;
}
@-webkit-keyframes bounceInUp {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
-webkit-transform: translate3d(0, 3000px, 0);
transform: translate3d(0, 3000px, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
}
75% {
-webkit-transform: translate3d(0, 10px, 0);
transform: translate3d(0, 10px, 0);
}
90% {
-webkit-transform: translate3d(0, -5px, 0);
transform: translate3d(0, -5px, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
@keyframes bounceInUp {
from,
60%,
75%,
90%,
to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
-webkit-transform: translate3d(0, 3000px, 0);
transform: translate3d(0, 3000px, 0);
}
60% {
opacity: 1;
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
}
75% {
-webkit-transform: translate3d(0, 10px, 0);
transform: translate3d(0, 10px, 0);
}
90% {
-webkit-transform: translate3d(0, -5px, 0);
transform: translate3d(0, -5px, 0);
}
to {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
}
.bounceIndown {
-webkit-animation-name: bounceInUp;
animation-name: bounceInUp;
}
@-webkit-keyframes bounceOutDown {
20% {
-webkit-transform: translate3d(0, 10px, 0);
transform: translate3d(0, 10px, 0);
}
40%,
45% {
opacity: 1;
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(0, 2000px, 0);
transform: translate3d(0, 2000px, 0);
}
}
@keyframes bounceOutDown {
20% {
-webkit-transform: translate3d(0, 10px, 0);
transform: translate3d(0, 10px, 0);
}
40%,
45% {
opacity: 1;
-webkit-transform: translate3d(0, -20px, 0);
transform: translate3d(0, -20px, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(0, 2000px, 0);
transform: translate3d(0, 2000px, 0);
}
}
.bounceOutdown {
-webkit-animation-name: bounceOutDown;
animation-name: bounceOutDown;
}
@-webkit-keyframes bounceOutUp {
20% {
-webkit-transform: translate3d(0, -10px, 0);
transform: translate3d(0, -10px, 0);
}
40%,
45% {
opacity: 1;
-webkit-transform: translate3d(0, 20px, 0);
transform: translate3d(0, 20px, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(0, -2000px, 0);
transform: translate3d(0, -2000px, 0);
}
}
@keyframes bounceOutUp {
20% {
-webkit-transform: translate3d(0, -10px, 0);
transform: translate3d(0, -10px, 0);
}
40%,
45% {
opacity: 1;
-webkit-transform: translate3d(0, 20px, 0);
transform: translate3d(0, 20px, 0);
}
to {
opacity: 0;
-webkit-transform: translate3d(0, -2000px, 0);
transform: translate3d(0, -2000px, 0);
}
}
.bounceOutup {
-webkit-animation-name: bounceOutUp;
animation-name: bounceOutUp;
}
// mask
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment