Modal.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. <template>
  2. <transition name="fade" appear @leave="onTransitionLeaveStart">
  3. <div
  4. ref="modal"
  5. class="inset-0 transition z-10 z-50 fixed overflow-y-auto hide-scrollbar"
  6. >
  7. <div
  8. class="flex min-h-screen text-center items-end justify-center sm:block"
  9. >
  10. <transition name="fade" appear>
  11. <div
  12. class="bg-primaryLight opacity-90 inset-0 transition fixed"
  13. @touchstart="!dialog ? close() : null"
  14. @touchend="!dialog ? close() : null"
  15. @mouseup="!dialog ? close() : null"
  16. @mousedown="!dialog ? close() : null"
  17. ></div>
  18. </transition>
  19. <span
  20. v-if="placement === 'center'"
  21. class="hidden sm:h-screen sm:inline-block sm:align-middle"
  22. aria-hidden="true"
  23. >&#8203;</span
  24. >
  25. <transition
  26. appear
  27. enter-active-class="transition"
  28. enter-class="scale-95 translate-y-4"
  29. enter-to-class="scale-100 translate-y-0"
  30. leave-active-class="transition"
  31. leave-class="scale-100 translate-y-0"
  32. leave-to-class="scale-95 translate-y-4"
  33. >
  34. <div
  35. class="bg-primary shadow-lg text-left w-full transform transition-all inline-block overflow-hidden align-bottom sm:rounded-xl sm:align-middle"
  36. :class="[
  37. { 'mt-24 md:mb-8': placement === 'top' },
  38. { 'p-4': !fullWidth },
  39. maxWidth,
  40. ]"
  41. >
  42. <div
  43. v-if="title"
  44. class="flex mb-4 pl-2 items-center justify-between"
  45. >
  46. <h3 class="heading">{{ title }}</h3>
  47. <span class="flex">
  48. <slot name="actions"></slot>
  49. <ButtonSecondary
  50. v-if="dimissible"
  51. svg="x"
  52. @click.native="close"
  53. />
  54. </span>
  55. </div>
  56. <div
  57. class="flex flex-col max-h-md overflow-y-auto hide-scrollbar"
  58. :class="{ 'py-2': !fullWidth }"
  59. >
  60. <slot name="body"></slot>
  61. </div>
  62. <div
  63. v-if="hasFooterSlot"
  64. class="flex flex-1 mt-4 p-2 items-center justify-between"
  65. >
  66. <slot name="footer"></slot>
  67. </div>
  68. </div>
  69. </transition>
  70. </div>
  71. </div>
  72. </transition>
  73. </template>
  74. <script lang="ts">
  75. import { defineComponent, onBeforeUnmount } from "@nuxtjs/composition-api"
  76. import { useKeybindingDisabler } from "~/helpers/keybindings"
  77. const PORTAL_DOM_ID = "hoppscotch-modal-portal"
  78. // Why ?
  79. const stack = (() => {
  80. const stack: number[] = []
  81. return {
  82. push: stack.push.bind(stack),
  83. pop: stack.pop.bind(stack),
  84. peek: () => (stack.length === 0 ? undefined : stack[stack.length - 1]),
  85. }
  86. })()
  87. export default defineComponent({
  88. props: {
  89. dialog: {
  90. type: Boolean,
  91. default: false,
  92. },
  93. title: {
  94. type: String,
  95. default: "",
  96. },
  97. dimissible: {
  98. type: Boolean,
  99. default: true,
  100. },
  101. placement: {
  102. type: String,
  103. default: "top",
  104. },
  105. fullWidth: {
  106. type: Boolean,
  107. default: false,
  108. },
  109. maxWidth: {
  110. type: String,
  111. default: "sm:max-w-lg",
  112. },
  113. },
  114. setup() {
  115. const { disableKeybindings, enableKeybindings } = useKeybindingDisabler()
  116. onBeforeUnmount(() => {
  117. enableKeybindings()
  118. })
  119. return {
  120. disableKeybindings,
  121. }
  122. },
  123. data() {
  124. return {
  125. stackId: Math.random(),
  126. // when doesn't fire on unmount, we should manually remove the modal from DOM
  127. // (for example, when the parent component of this modal gets destroyed)
  128. shouldCleanupDomOnUnmount: true,
  129. }
  130. },
  131. computed: {
  132. hasFooterSlot(): boolean {
  133. return !!this.$slots.footer
  134. },
  135. },
  136. mounted() {
  137. const $portal = this.$getPortal()
  138. $portal.appendChild(this.$refs.modal as any)
  139. stack.push(this.stackId)
  140. document.addEventListener("keydown", this.onKeyDown)
  141. this.disableKeybindings()
  142. },
  143. beforeDestroy() {
  144. const $modal = this.$refs.modal
  145. if (this.shouldCleanupDomOnUnmount && $modal) {
  146. this.$getPortal().removeChild($modal as any)
  147. }
  148. stack.pop()
  149. document.removeEventListener("keydown", this.onKeyDown)
  150. },
  151. methods: {
  152. close() {
  153. this.$emit("close")
  154. },
  155. onKeyDown(e: KeyboardEvent) {
  156. if (e.key === "Escape" && this.stackId === stack.peek()) {
  157. e.preventDefault()
  158. this.close()
  159. }
  160. },
  161. onTransitionLeaveStart() {
  162. this.close()
  163. this.shouldCleanupDomOnUnmount = false
  164. },
  165. $getPortal() {
  166. let $el = document.querySelector("#" + PORTAL_DOM_ID)
  167. if ($el) {
  168. return $el
  169. }
  170. $el = document.createElement("DIV")
  171. $el.id = PORTAL_DOM_ID
  172. document.body.appendChild($el)
  173. return $el
  174. },
  175. },
  176. })
  177. </script>