Files
hoppscotch/components/smart/Modal.vue
2021-07-23 18:28:07 +00:00

151 lines
3.4 KiB
Vue

<template>
<transition name="modal" appear @leave="onTransitionLeaveStart">
<div
ref="modal"
class="
bg-primaryLight
flex
h-full
w-full
inset-0
transition
z-50
fixed
items-center
justify-center
"
@touchstart="onBackdropMouseDown"
@touchend="onBackdropMouseUp"
@mouseup="onBackdropMouseUp"
@mousedown="onBackdropMouseDown"
>
<div
class="
bg-primary
rounded
flex flex-col
max-w-md max-h-lg
flex-1
m-2
shadow-xl
p-4
transition
modal-container
relative
"
>
<div class="flex pl-2 items-center justify-between">
<slot name="header"></slot>
</div>
<div class="flex flex-col my-4 overflow-auto">
<slot name="body"></slot>
</div>
<div
v-if="hasFooterSlot"
class="flex flex-1 p-2 items-center justify-between"
>
<slot name="footer"></slot>
</div>
</div>
</div>
</transition>
</template>
<script>
const PORTAL_DOM_ID = "hoppscotch-modal-portal"
const stack = (() => {
const stack = []
return {
push: stack.push.bind(stack),
pop: stack.pop.bind(stack),
peek: () => (stack.length === 0 ? undefined : stack[stack.length - 1]),
}
})()
export default {
data() {
return {
stackId: Math.random(),
shouldCloseOnBackdropClick: true,
// when doesn't fire on unmount, we should manually remove the modal from DOM
// (for example, when the parent component of this modal gets destroyed)
shouldCleanupDomOnUnmount: true,
}
},
computed: {
hasFooterSlot() {
return !!this.$slots.footer
},
},
mounted() {
const $portal = this.$getPortal()
$portal.appendChild(this.$refs.modal)
stack.push(this.stackId)
document.addEventListener("keydown", this.onKeyDown)
},
beforeDestroy() {
const $modal = this.$refs.modal
if (this.shouldCleanupDomOnUnmount && $modal) {
this.$getPortal().removeChild($modal)
}
stack.pop()
document.removeEventListener("keydown", this.onKeyDown)
},
methods: {
close() {
this.$emit("close")
},
onBackdropMouseDown({ target }) {
this.shouldCloseOnBackdropClick =
!this.checkIfTargetInsideModalContent(target)
},
onBackdropMouseUp({ target }) {
if (
this.shouldCloseOnBackdropClick &&
!this.checkIfTargetInsideModalContent(target)
) {
this.close()
}
this.shouldCloseOnBackdropClick = true
},
checkIfTargetInsideModalContent($target) {
return $target?.closest(".modal-container")
},
onKeyDown(e) {
if (e.key === "Escape" && this.stackId === stack.peek()) {
e.preventDefault()
this.close()
}
},
onTransitionLeaveStart() {
this.shouldCleanupDomOnUnmount = false
},
$getPortal() {
let $el = document.querySelector("#" + PORTAL_DOM_ID)
if ($el) {
return $el
}
$el = document.createElement("DIV")
$el.id = PORTAL_DOM_ID
document.body.appendChild($el)
return $el
},
},
}
</script>
<style scoped lang="scss">
.modal-enter,
.modal-leave-active {
@apply opacity-0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
@apply transform;
@apply scale-90;
@apply transition;
}
</style>