HTMLLensRenderer.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. <template>
  2. <div>
  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. ></iframe>
  62. </div>
  63. </template>
  64. <script setup lang="ts">
  65. import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
  66. import { useCodemirror } from "~/helpers/editor/codemirror"
  67. import { copyToClipboard } from "~/helpers/utils/clipboard"
  68. import "codemirror/mode/xml/xml"
  69. import "codemirror/mode/javascript/javascript"
  70. import "codemirror/mode/css/css"
  71. import "codemirror/mode/htmlmixed/htmlmixed"
  72. import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
  73. const props = defineProps<{
  74. response: HoppRESTResponse
  75. }>()
  76. const {
  77. $toast,
  78. app: { i18n },
  79. } = useContext()
  80. const t = i18n.t.bind(i18n)
  81. const responseBodyText = computed(() => {
  82. if (
  83. props.response.type === "loading" ||
  84. props.response.type === "network_fail"
  85. )
  86. return ""
  87. if (typeof props.response.body === "string") return props.response.body
  88. else {
  89. const res = new TextDecoder("utf-8").decode(props.response.body)
  90. // HACK: Temporary trailing null character issue from the extension fix
  91. return res.replace(/\0+$/, "")
  92. }
  93. })
  94. const downloadIcon = ref("download")
  95. const copyIcon = ref("copy")
  96. const previewEnabled = ref(false)
  97. const previewFrame = ref<any | null>(null)
  98. const url = ref("")
  99. const htmlResponse = ref<any | null>(null)
  100. const linewrapEnabled = ref(true)
  101. useCodemirror(
  102. htmlResponse,
  103. responseBodyText,
  104. reactive({
  105. extendedEditorConfig: {
  106. mode: "htmlmixed",
  107. readOnly: true,
  108. lineWrapping: linewrapEnabled,
  109. },
  110. linter: null,
  111. completer: null,
  112. })
  113. )
  114. const downloadResponse = () => {
  115. const dataToWrite = responseBodyText.value
  116. const file = new Blob([dataToWrite], { type: "text/html" })
  117. const a = document.createElement("a")
  118. const url = URL.createObjectURL(file)
  119. a.href = url
  120. // TODO get uri from meta
  121. a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
  122. document.body.appendChild(a)
  123. a.click()
  124. downloadIcon.value = "check"
  125. $toast.success(`${t("state.download_started")}`, {
  126. icon: "downloading",
  127. })
  128. setTimeout(() => {
  129. document.body.removeChild(a)
  130. URL.revokeObjectURL(url)
  131. downloadIcon.value = "download"
  132. }, 1000)
  133. }
  134. const copyResponse = () => {
  135. copyToClipboard(responseBodyText.value)
  136. copyIcon.value = "check"
  137. $toast.success(`${t("state.copied_to_clipboard")}`, {
  138. icon: "content_paste",
  139. })
  140. setTimeout(() => (copyIcon.value = "copy"), 1000)
  141. }
  142. const togglePreview = () => {
  143. previewEnabled.value = !previewEnabled.value
  144. if (previewEnabled.value) {
  145. if (previewFrame.value.getAttribute("data-previewing-url") === url.value)
  146. return
  147. // Use DOMParser to parse document HTML.
  148. const previewDocument = new DOMParser().parseFromString(
  149. responseBodyText.value,
  150. "text/html"
  151. )
  152. // Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
  153. previewDocument.head.innerHTML =
  154. `<base href="${url.value}">` + previewDocument.head.innerHTML
  155. // Finally, set the iframe source to the resulting HTML.
  156. previewFrame.value.srcdoc = previewDocument.documentElement.outerHTML
  157. previewFrame.value.setAttribute("data-previewing-url", url.value)
  158. }
  159. }
  160. </script>
  161. <style lang="scss" scoped>
  162. .covers-response {
  163. @apply bg-white;
  164. @apply min-h-64;
  165. @apply h-full;
  166. @apply w-full;
  167. @apply border;
  168. @apply border-dividerLight;
  169. @apply z-5;
  170. }
  171. </style>