HTMLLensRenderer.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <template>
  2. <div class="flex flex-col flex-1">
  3. <div
  4. class="
  5. bg-primary
  6. border-b border-dividerLight
  7. flex flex-1
  8. top-lowerSecondaryStickyFold
  9. pl-4
  10. z-10
  11. sticky
  12. items-center
  13. justify-between
  14. "
  15. >
  16. <label class="font-semibold text-secondaryLight">
  17. {{ $t("response.body") }}
  18. </label>
  19. <div class="flex">
  20. <ButtonSecondary
  21. v-if="response.body"
  22. v-tippy="{ theme: 'tooltip' }"
  23. :title="$t('state.linewrap')"
  24. :class="{ '!text-accent': linewrapEnabled }"
  25. svg="corner-down-left"
  26. @click.native.prevent="linewrapEnabled = !linewrapEnabled"
  27. />
  28. <ButtonSecondary
  29. v-if="response.body"
  30. v-tippy="{ theme: 'tooltip' }"
  31. :title="
  32. previewEnabled ? $t('hide.preview') : $t('response.preview_html')
  33. "
  34. :svg="!previewEnabled ? 'eye' : 'eye-off'"
  35. @click.native.prevent="togglePreview"
  36. />
  37. <ButtonSecondary
  38. v-if="response.body"
  39. ref="downloadResponse"
  40. v-tippy="{ theme: 'tooltip' }"
  41. :title="$t('action.download_file')"
  42. :svg="downloadIcon"
  43. @click.native="downloadResponse"
  44. />
  45. <ButtonSecondary
  46. v-if="response.body"
  47. ref="copyResponse"
  48. v-tippy="{ theme: 'tooltip' }"
  49. :title="$t('action.copy')"
  50. :svg="copyIcon"
  51. @click.native="copyResponse"
  52. />
  53. </div>
  54. </div>
  55. <div v-show="!previewEnabled" ref="htmlResponse"></div>
  56. <iframe
  57. v-show="previewEnabled"
  58. ref="previewFrame"
  59. class="covers-response"
  60. src="about:blank"
  61. loading="lazy"
  62. ></iframe>
  63. </div>
  64. </template>
  65. <script setup lang="ts">
  66. import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
  67. import { useCodemirror } from "~/helpers/editor/codemirror"
  68. import { copyToClipboard } from "~/helpers/utils/clipboard"
  69. import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
  70. const props = defineProps<{
  71. response: HoppRESTResponse
  72. }>()
  73. const {
  74. $toast,
  75. app: { i18n },
  76. } = useContext()
  77. const t = i18n.t.bind(i18n)
  78. const responseBodyText = computed(() => {
  79. if (
  80. props.response.type === "loading" ||
  81. props.response.type === "network_fail"
  82. )
  83. return ""
  84. if (typeof props.response.body === "string") return props.response.body
  85. else {
  86. const res = new TextDecoder("utf-8").decode(props.response.body)
  87. // HACK: Temporary trailing null character issue from the extension fix
  88. return res.replace(/\0+$/, "")
  89. }
  90. })
  91. const downloadIcon = ref("download")
  92. const copyIcon = ref("copy")
  93. const previewEnabled = ref(false)
  94. const previewFrame = ref<any | null>(null)
  95. const url = ref("")
  96. const htmlResponse = ref<any | null>(null)
  97. const linewrapEnabled = ref(true)
  98. useCodemirror(
  99. htmlResponse,
  100. responseBodyText,
  101. reactive({
  102. extendedEditorConfig: {
  103. mode: "htmlmixed",
  104. readOnly: true,
  105. lineWrapping: linewrapEnabled,
  106. },
  107. linter: null,
  108. completer: null,
  109. })
  110. )
  111. const downloadResponse = () => {
  112. const dataToWrite = responseBodyText.value
  113. const file = new Blob([dataToWrite], { type: "text/html" })
  114. const a = document.createElement("a")
  115. const url = URL.createObjectURL(file)
  116. a.href = url
  117. // TODO get uri from meta
  118. a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
  119. document.body.appendChild(a)
  120. a.click()
  121. downloadIcon.value = "check"
  122. $toast.success(`${t("state.download_started")}`, {
  123. icon: "downloading",
  124. })
  125. setTimeout(() => {
  126. document.body.removeChild(a)
  127. URL.revokeObjectURL(url)
  128. downloadIcon.value = "download"
  129. }, 1000)
  130. }
  131. const copyResponse = () => {
  132. copyToClipboard(responseBodyText.value)
  133. copyIcon.value = "check"
  134. $toast.success(`${t("state.copied_to_clipboard")}`, {
  135. icon: "content_paste",
  136. })
  137. setTimeout(() => (copyIcon.value = "copy"), 1000)
  138. }
  139. const togglePreview = () => {
  140. previewEnabled.value = !previewEnabled.value
  141. if (previewEnabled.value) {
  142. if (previewFrame.value.getAttribute("data-previewing-url") === url.value)
  143. return
  144. // Use DOMParser to parse document HTML.
  145. const previewDocument = new DOMParser().parseFromString(
  146. responseBodyText.value,
  147. "text/html"
  148. )
  149. // Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
  150. previewDocument.head.innerHTML =
  151. `<base href="${url.value}">` + previewDocument.head.innerHTML
  152. // Finally, set the iframe source to the resulting HTML.
  153. previewFrame.value.srcdoc = previewDocument.documentElement.outerHTML
  154. previewFrame.value.setAttribute("data-previewing-url", url.value)
  155. }
  156. }
  157. </script>
  158. <style lang="scss" scoped>
  159. .covers-response {
  160. @apply bg-white;
  161. @apply h-full;
  162. @apply w-full;
  163. @apply border;
  164. @apply border-dividerLight;
  165. @apply z-5;
  166. }
  167. </style>