Modal.vue 4.9 KB

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