feat: port ace editor to codemirror

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
liyasthomas
2021-09-09 17:47:27 +05:30
parent 8b4535c131
commit 02cf620090
10 changed files with 437 additions and 485 deletions

View File

@@ -467,7 +467,7 @@ input[type="checkbox"] {
@apply border-b; @apply border-b;
@apply border-dividerLight; @apply border-dividerLight;
@apply w-full; @apply w-full;
@apply h-auto; @apply !h-full;
} }
.CodeMirror * { .CodeMirror * {

View File

@@ -33,28 +33,23 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics" import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { GQLConnection } from "~/helpers/GQLConnection" import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network" import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "~/helpers/utils/composables" import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession" import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
export default defineComponent({ const props = defineProps<{
props: { conn: GQLConnection
conn: { }>()
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const connected = useReadonlyStream(props.conn.connected$, false)
const headers = useReadonlyStream(gqlHeaders$, [])
const url = useStream(gqlURL$, "", setGQLURL) const connected = useReadonlyStream(props.conn.connected$, false)
const headers = useReadonlyStream(gqlHeaders$, [])
const onConnectClick = () => { const url = useStream(gqlURL$, "", setGQLURL)
const onConnectClick = () => {
if (!connected.value) { if (!connected.value) {
props.conn.connect(url.value, headers.value as any) props.conn.connect(url.value, headers.value as any)
@@ -65,13 +60,5 @@ export default defineComponent({
} else { } else {
props.conn.disconnect() props.conn.disconnect()
} }
} }
return {
url,
connected,
onConnectClick,
}
},
})
</script> </script>

View File

@@ -106,19 +106,7 @@
/> />
</div> </div>
</div> </div>
<SmartAceEditor <div ref="variableEditor" class="w-full block"></div>
ref="variableEditor"
v-model="variableString"
:lang="'json'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</AppSection> </AppSection>
</SmartTab> </SmartTab>
@@ -323,6 +311,7 @@ import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { getCurrentStrategyID } from "~/helpers/network" import { getCurrentStrategyID } from "~/helpers/network"
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest" import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
import { useCodemirror } from "~/helpers/editor/codemirror" import { useCodemirror } from "~/helpers/editor/codemirror"
import "codemirror/mode/javascript/javascript"
const props = defineProps<{ const props = defineProps<{
conn: GQLConnection conn: GQLConnection
@@ -370,6 +359,16 @@ useCodemirror(bulkEditor, bulkHeaders, {
completer: null, completer: null,
}) })
const variableEditor = ref<any | null>(null)
useCodemirror(variableEditor, variableString, {
extendedEditorConfig: {
mode: "javascript",
},
linter: null,
completer: null,
})
const queryEditor = ref<any | null>(null) const queryEditor = ref<any | null>(null)
const copyQueryIcon = ref("copy") const copyQueryIcon = ref("copy")

View File

@@ -59,38 +59,6 @@
justify-center justify-center
" "
> >
<div class="flex space-x-2 pb-4">
<div class="flex flex-col space-y-4 items-end">
<span class="flex flex-1 items-center">
{{ $t("shortcut.request.send_request") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.show_all") }}
</span>
<!-- <span class="flex flex-1 items-center">
{{ $t("shortcut.general.command_menu") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.help_menu") }}
</span> -->
</div>
<div class="flex flex-col space-y-4">
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">G</span>
</div>
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">K</span>
</div>
<!-- <div class="flex">
<span class="shortcut-key">/</span>
</div>
<div class="flex">
<span class="shortcut-key">?</span>
</div> -->
</div>
</div>
<ButtonSecondary <ButtonSecondary
:label="$t('app.documentation')" :label="$t('app.documentation')"
to="https://docs.hoppscotch.io" to="https://docs.hoppscotch.io"
@@ -103,45 +71,38 @@
</AppSection> </AppSection>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { import { PropType, ref, useContext } from "@nuxtjs/composition-api"
defineComponent,
PropType,
ref,
useContext,
} from "@nuxtjs/composition-api"
import { GQLConnection } from "~/helpers/GQLConnection" import { GQLConnection } from "~/helpers/GQLConnection"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "~/helpers/utils/composables" import { useReadonlyStream } from "~/helpers/utils/composables"
import { gqlResponse$ } from "~/newstore/GQLSession" import { gqlResponse$ } from "~/newstore/GQLSession"
export default defineComponent({ defineProps({
props: {
conn: { conn: {
type: Object as PropType<GQLConnection>, type: Object as PropType<GQLConnection>,
required: true, required: true,
}, },
}, })
setup() {
const { const {
$toast, $toast,
app: { i18n }, app: { i18n },
} = useContext() } = useContext()
const t = i18n.t.bind(i18n) const t = i18n.t.bind(i18n)
const responseString = useReadonlyStream(gqlResponse$, "") const responseString = useReadonlyStream(gqlResponse$, "")
const downloadResponseIcon = ref("download") const downloadResponseIcon = ref("download")
const copyResponseIcon = ref("copy") const copyResponseIcon = ref("copy")
const copyResponse = () => { const copyResponse = () => {
copyToClipboard(responseString.value!) copyToClipboard(responseString.value!)
copyResponseIcon.value = "check" copyResponseIcon.value = "check"
setTimeout(() => (copyResponseIcon.value = "copy"), 1000) setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
} }
const downloadResponse = () => { const downloadResponse = () => {
const dataToWrite = responseString.value const dataToWrite = responseString.value
const file = new Blob([dataToWrite!], { type: "application/json" }) const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a") const a = document.createElement("a")
@@ -159,21 +120,7 @@ export default defineComponent({
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
downloadResponseIcon.value = "download" downloadResponseIcon.value = "download"
}, 1000) }, 1000)
} }
return {
responseString,
downloadResponseIcon,
copyResponseIcon,
downloadResponse,
copyResponse,
getSpecialKey: getPlatformSpecialKey,
}
},
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -9,38 +9,6 @@
justify-center justify-center
" "
> >
<div class="flex space-x-2 pb-4">
<div class="flex flex-col space-y-4 items-end">
<span class="flex flex-1 items-center">
{{ $t("shortcut.request.send_request") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.show_all") }}
</span>
<!-- <span class="flex flex-1 items-center">
{{ $t("shortcut.general.command_menu") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.help_menu") }}
</span> -->
</div>
<div class="flex flex-col space-y-4">
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">G</span>
</div>
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">K</span>
</div>
<!-- <div class="flex">
<span class="shortcut-key">/</span>
</div>
<div class="flex">
<span class="shortcut-key">?</span>
</div> -->
</div>
</div>
<ButtonSecondary <ButtonSecondary
:label="$t('app.documentation')" :label="$t('app.documentation')"
to="https://docs.hoppscotch.io" to="https://docs.hoppscotch.io"
@@ -102,26 +70,22 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import { computed } from "@nuxtjs/composition-api"
import findStatusGroup from "~/helpers/findStatusGroup" import findStatusGroup from "~/helpers/findStatusGroup"
import { getPlatformSpecialKey } from "~/helpers/platformutils" import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
export default defineComponent({ const props = defineProps<{
props: { response: HoppRESTResponse
response: { }>()
type: Object,
default: () => null, const statusCategory = computed(() => {
}, if (
}, props.response.type === "loading" ||
computed: { props.response.type === "network_fail"
statusCategory() { )
return findStatusGroup(this.response.statusCode) return ""
}, return findStatusGroup(props.response.statusCode)
},
methods: {
getSpecialKey: getPlatformSpecialKey,
},
}) })
</script> </script>

View File

@@ -45,19 +45,7 @@
</div> </div>
</div> </div>
<div class="relative"> <div class="relative">
<SmartAceEditor <div ref="htmlResponse" class="w-full block"></div>
:value="responseBodyText"
:lang="'html'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<iframe <iframe
ref="previewFrame" ref="previewFrame"
:class="{ hidden: !previewEnabled }" :class="{ hidden: !previewEnabled }"
@@ -68,26 +56,56 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import { computed, ref, useContext } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin" import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/htmlmixed/htmlmixed"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
export default defineComponent({ const props = defineProps<{
mixins: [TextContentRendererMixin], response: HoppRESTResponse
props: { }>()
response: { type: Object, default: () => {} },
}, const {
data() { $toast,
return { app: { i18n },
downloadIcon: "download", } = useContext()
copyIcon: "copy", const t = i18n.t.bind(i18n)
previewEnabled: false,
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
} }
})
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const previewEnabled = ref(false)
const previewFrame = ref<any | null>(null)
const url = ref("")
const htmlResponse = ref<any | null>(null)
useCodemirror(htmlResponse, responseBodyText, {
extendedEditorConfig: {
mode: "javascript",
readOnly: true,
}, },
methods: { linter: null,
downloadResponse() { completer: null,
const dataToWrite = this.responseBodyText })
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: "text/html" }) const file = new Blob([dataToWrite], { type: "text/html" })
const a = document.createElement("a") const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
@@ -96,48 +114,44 @@ export default defineComponent({
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}` a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()
this.downloadIcon = "check" downloadIcon.value = "check"
this.$toast.success(this.$t("state.download_started"), { $toast.success(t("state.download_started").toString(), {
icon: "downloading", icon: "downloading",
}) })
setTimeout(() => { setTimeout(() => {
document.body.removeChild(a) document.body.removeChild(a)
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
this.downloadIcon = "download" downloadIcon.value = "download"
}, 1000) }, 1000)
}, }
copyResponse() {
copyToClipboard(this.responseBodyText) const copyResponse = () => {
this.copyIcon = "check" copyToClipboard(responseBodyText.value)
this.$toast.success(this.$t("state.copied_to_clipboard"), { copyIcon.value = "check"
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste", icon: "content_paste",
}) })
setTimeout(() => (this.copyIcon = "copy"), 1000) setTimeout(() => (copyIcon.value = "copy"), 1000)
}, }
togglePreview() {
this.previewEnabled = !this.previewEnabled const togglePreview = () => {
if (this.previewEnabled) { previewEnabled.value = !previewEnabled.value
if ( if (previewEnabled.value) {
this.$refs.previewFrame.getAttribute("data-previewing-url") === if (previewFrame.value.getAttribute("data-previewing-url") === url.value)
this.url
)
return return
// Use DOMParser to parse document HTML. // Use DOMParser to parse document HTML.
const previewDocument = new DOMParser().parseFromString( const previewDocument = new DOMParser().parseFromString(
this.responseBodyText, responseBodyText.value,
"text/html" "text/html"
) )
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths. // Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
previewDocument.head.innerHTML = previewDocument.head.innerHTML =
`<base href="${this.url}">` + previewDocument.head.innerHTML `<base href="${url.value}">` + previewDocument.head.innerHTML
// Finally, set the iframe source to the resulting HTML. // Finally, set the iframe source to the resulting HTML.
this.$refs.previewFrame.srcdoc = previewFrame.value.srcdoc = previewDocument.documentElement.outerHTML
previewDocument.documentElement.outerHTML previewFrame.value.setAttribute("data-previewing-url", url.value)
this.$refs.previewFrame.setAttribute("data-previewing-url", this.url)
} }
}, }
},
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -149,5 +163,6 @@ export default defineComponent({
@apply w-full; @apply w-full;
@apply border; @apply border;
@apply border-dividerLight; @apply border-dividerLight;
@apply z-5;
} }
</style> </style>

View File

@@ -36,62 +36,67 @@
</div> </div>
</div> </div>
<div class="relative"> <div class="relative">
<SmartAceEditor <div ref="jsonResponse" class="w-full block"></div>
:value="jsonBodyText"
:lang="'json'"
:provide-outline="true"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import { computed, ref, useContext } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin" import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/javascript/javascript"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
export default defineComponent({ const props = defineProps<{
mixins: [TextContentRendererMixin], response: HoppRESTResponse
props: { }>()
response: { type: Object, default: () => {} },
}, const {
data() { $toast,
return { app: { i18n },
downloadIcon: "download", } = useContext()
copyIcon: "copy", const t = i18n.t.bind(i18n)
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
} }
}, })
computed: {
jsonBodyText() { const downloadIcon = ref("download")
const copyIcon = ref("copy")
const jsonBodyText = computed(() => {
try { try {
return JSON.stringify(JSON.parse(this.responseBodyText), null, 2) return JSON.stringify(JSON.parse(responseBodyText.value), null, 2)
} catch (e) { } catch (e) {
// Most probs invalid JSON was returned, so drop prettification (should we warn ?) // Most probs invalid JSON was returned, so drop prettification (should we warn ?)
return this.responseBodyText return responseBodyText.value
} }
})
const jsonResponse = ref<any | null>(null)
useCodemirror(jsonResponse, jsonBodyText, {
extendedEditorConfig: {
mode: "javascript",
readOnly: true,
}, },
responseType() { linter: null,
return ( completer: null,
this.response.headers.find( })
(h) => h.key.toLowerCase() === "content-type"
).value || "" const downloadResponse = () => {
) const dataToWrite = responseBodyText.value
.split(";")[0]
.toLowerCase()
},
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: "application/json" }) const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a") const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
@@ -100,24 +105,23 @@ export default defineComponent({
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}` a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()
this.downloadIcon = "check" downloadIcon.value = "check"
this.$toast.success(this.$t("state.download_started"), { $toast.success(t("state.download_started").toString(), {
icon: "downloading", icon: "downloading",
}) })
setTimeout(() => { setTimeout(() => {
document.body.removeChild(a) document.body.removeChild(a)
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
this.downloadIcon = "download" downloadIcon.value = "download"
}, 1000) }, 1000)
}, }
copyResponse() {
copyToClipboard(this.responseBodyText) const copyResponse = () => {
this.copyIcon = "check" copyToClipboard(responseBodyText.value)
this.$toast.success(this.$t("state.copied_to_clipboard"), { copyIcon.value = "check"
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste", icon: "content_paste",
}) })
setTimeout(() => (this.copyIcon = "copy"), 1000) setTimeout(() => (copyIcon.value = "copy"), 1000)
}, }
},
})
</script> </script>

View File

@@ -36,54 +36,68 @@
</div> </div>
</div> </div>
<div class="relative"> <div class="relative">
<SmartAceEditor <div ref="rawResponse" class="w-full block"></div>
:value="responseBodyText"
:lang="'plain_text'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import { ref, useContext, computed } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin" import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/yaml/yaml"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
export default defineComponent({ const props = defineProps<{
mixins: [TextContentRendererMixin], response: HoppRESTResponse
props: { }>()
response: { type: Object, default: () => {} },
}, const {
data() { $toast,
return { app: { i18n },
downloadIcon: "download", } = useContext()
copyIcon: "copy", const t = i18n.t.bind(i18n)
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
} }
}, })
computed: {
responseType() { const downloadIcon = ref("download")
const copyIcon = ref("copy")
const responseType = computed(() => {
return ( return (
this.response.headers.find( props.response.headers.find((h) => h.key.toLowerCase() === "content-type")
(h) => h.key.toLowerCase() === "content-type" .value || ""
).value || ""
) )
.split(";")[0] .split(";")[0]
.toLowerCase() .toLowerCase()
})
const rawResponse = ref<any | null>(null)
useCodemirror(rawResponse, responseBodyText, {
extendedEditorConfig: {
mode: "text/x-yaml",
readOnly: true,
}, },
}, linter: null,
methods: { completer: null,
downloadResponse() { })
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: this.responseType }) const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: responseType.value })
const a = document.createElement("a") const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url a.href = url
@@ -91,24 +105,23 @@ export default defineComponent({
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}` a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()
this.downloadIcon = "check" downloadIcon.value = "check"
this.$toast.success(this.$t("state.download_started"), { $toast.success(t("state.download_started").toString(), {
icon: "downloading", icon: "downloading",
}) })
setTimeout(() => { setTimeout(() => {
document.body.removeChild(a) document.body.removeChild(a)
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
this.downloadIcon = "download" downloadIcon.value = "download"
}, 1000) }, 1000)
}, }
copyResponse() {
copyToClipboard(this.responseBodyText) const copyResponse = () => {
this.copyIcon = "check" copyToClipboard(responseBodyText.value)
this.$toast.success(this.$t("state.copied_to_clipboard"), { copyIcon.value = "check"
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste", icon: "content_paste",
}) })
setTimeout(() => (this.copyIcon = "copy"), 1000) setTimeout(() => (copyIcon.value = "copy"), 1000)
}, }
},
})
</script> </script>

View File

@@ -36,54 +36,68 @@
</div> </div>
</div> </div>
<div class="relative"> <div class="relative">
<SmartAceEditor <div ref="xmlResponse" class="w-full block"></div>
:value="responseBodyText"
:lang="'xml'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import { computed, ref, useContext } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin" import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/xml/xml"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
export default defineComponent({ const props = defineProps<{
mixins: [TextContentRendererMixin], response: HoppRESTResponse
props: { }>()
response: { type: Object, default: () => {} },
}, const {
data() { $toast,
return { app: { i18n },
copyIcon: "copy", } = useContext()
downloadIcon: "download", const t = i18n.t.bind(i18n)
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
} }
}, })
computed: {
responseType() { const downloadIcon = ref("download")
const copyIcon = ref("copy")
const responseType = computed(() => {
return ( return (
this.response.headers.find( props.response.headers.find((h) => h.key.toLowerCase() === "content-type")
(h) => h.key.toLowerCase() === "content-type" .value || ""
).value || ""
) )
.split(";")[0] .split(";")[0]
.toLowerCase() .toLowerCase()
})
const xmlResponse = ref<any | null>(null)
useCodemirror(xmlResponse, responseBodyText, {
extendedEditorConfig: {
mode: "application/xml",
readOnly: true,
}, },
}, linter: null,
methods: { completer: null,
downloadResponse() { })
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: this.responseType }) const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: responseType.value })
const a = document.createElement("a") const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url a.href = url
@@ -91,24 +105,23 @@ export default defineComponent({
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}` a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()
this.downloadIcon = "check" downloadIcon.value = "check"
this.$toast.success(this.$t("state.download_started"), { $toast.success(t("state.download_started").toString(), {
icon: "downloading", icon: "downloading",
}) })
setTimeout(() => { setTimeout(() => {
document.body.removeChild(a) document.body.removeChild(a)
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
this.downloadIcon = "download" downloadIcon.value = "download"
}, 1000) }, 1000)
}, }
copyResponse() {
copyToClipboard(this.responseBodyText) const copyResponse = () => {
this.copyIcon = "check" copyToClipboard(responseBodyText.value)
this.$toast.success(this.$t("state.copied_to_clipboard"), { copyIcon.value = "check"
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste", icon: "content_paste",
}) })
setTimeout(() => (this.copyIcon = "copy"), 1000) setTimeout(() => (copyIcon.value = "copy"), 1000)
}, }
},
})
</script> </script>

View File

@@ -44,6 +44,7 @@ const DEFAULT_EDITOR_CONFIG: CodeMirror.EditorConfiguration = {
extraKeys: { extraKeys: {
"Ctrl-Space": "autocomplete", "Ctrl-Space": "autocomplete",
}, },
viewportMargin: Infinity,
} }
/** /**
@@ -120,6 +121,8 @@ export function useCodemirror(
cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG) cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG)
cm.value.setValue(value.value)
setTheme() setTheme()
updateEditorConfig() updateEditorConfig()
updateLinterConfig() updateLinterConfig()
@@ -139,7 +142,14 @@ export function useCodemirror(
}) })
// Reinitialize if the target ref updates // Reinitialize if the target ref updates
watch(el, initialize) watch(el, () => {
if (cm.value) {
const parent = cm.value.getWrapperElement()
parent.remove()
cm.value = null
}
initialize()
})
const setTheme = () => { const setTheme = () => {
if (cm.value) { if (cm.value) {