Modal.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. <template>
  2. <transition name="fade" appear @leave="onTransitionLeaveStart">
  3. <div
  4. ref="modal"
  5. class="hide-scrollbar fixed inset-0 z-10 z-50 overflow-y-auto transition"
  6. >
  7. <div
  8. class="sm:block flex items-end justify-center min-h-screen text-center"
  9. >
  10. <transition name="fade" appear>
  11. <div
  12. class="bg-primaryLight opacity-90 fixed inset-0 transition"
  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="sm:h-screen sm:inline-block sm:align-middle hidden"
  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 sm:align-middle sm:rounded-xl inline-block w-full overflow-hidden text-left align-bottom transition-all transform shadow-lg"
  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 items-center justify-between pl-2 mb-4"
  45. >
  46. <h3 class="heading">{{ title }}</h3>
  47. <span class="flex">
  48. <slot name="actions"></slot>
  49. <ButtonSecondary
  50. v-if="dimissible"
  51. class="rounded"
  52. svg="x"
  53. @click.native="close"
  54. />
  55. </span>
  56. </div>
  57. <div
  58. class="max-h-md hide-scrollbar flex flex-col overflow-y-auto"
  59. :class="{ 'py-2': !fullWidth }"
  60. >
  61. <slot name="body"></slot>
  62. </div>
  63. <div
  64. v-if="hasFooterSlot"
  65. class="flex items-center justify-between flex-1 p-2 mt-4"
  66. >
  67. <slot name="footer"></slot>
  68. </div>
  69. </div>
  70. </transition>
  71. </div>
  72. </div>
  73. </transition>
  74. </template>
  75. <script lang="ts">
  76. import { defineComponent } from "@nuxtjs/composition-api"
  77. import { useKeybindingDisabler } from "~/helpers/keybindings"
  78. const PORTAL_DOM_ID = "hoppscotch-modal-portal"
  79. // Why ?
  80. const stack = (() => {
  81. const stack: number[] = []
  82. return {
  83. push: stack.push.bind(stack),
  84. pop: stack.pop.bind(stack),
  85. peek: () => (stack.length === 0 ? undefined : stack[stack.length - 1]),
  86. }
  87. })()
  88. export default defineComponent({
  89. props: {
  90. dialog: {
  91. type: Boolean,
  92. default: false,
  93. },
  94. title: {
  95. type: String,
  96. default: "",
  97. },
  98. dimissible: {
  99. type: Boolean,
  100. default: true,
  101. },
  102. placement: {
  103. type: String,
  104. default: "top",
  105. },
  106. fullWidth: {
  107. type: Boolean,
  108. default: false,
  109. },
  110. maxWidth: {
  111. type: String,
  112. default: "sm:max-w-lg",
  113. },
  114. },
  115. setup() {
  116. const { disableKeybindings, enableKeybindings } = useKeybindingDisabler()
  117. return {
  118. disableKeybindings,
  119. enableKeybindings,
  120. }
  121. },
  122. data() {
  123. return {
  124. stackId: Math.random(),
  125. // when doesn't fire on unmount, we should manually remove the modal from DOM
  126. // (for example, when the parent component of this modal gets destroyed)
  127. shouldCleanupDomOnUnmount: true,
  128. }
  129. },
  130. computed: {
  131. hasFooterSlot(): boolean {
  132. return !!this.$slots.footer
  133. },
  134. },
  135. mounted() {
  136. const $portal = this.$getPortal()
  137. $portal.appendChild(this.$refs.modal as any)
  138. stack.push(this.stackId)
  139. document.addEventListener("keydown", this.onKeyDown)
  140. this.disableKeybindings()
  141. },
  142. beforeDestroy() {
  143. const $modal = this.$refs.modal
  144. if (this.shouldCleanupDomOnUnmount && $modal) {
  145. this.$getPortal().removeChild($modal as any)
  146. }
  147. stack.pop()
  148. document.removeEventListener("keydown", this.onKeyDown)
  149. },
  150. methods: {
  151. close() {
  152. this.$emit("close")
  153. this.enableKeybindings()
  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>