Modal.vue 4.9 KB

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