123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- <template>
- <div
- class="flex items-center flex-1 flex-shrink-0 overflow-auto whitespace-nowrap hide-scrollbar"
- >
- <div
- ref="editor"
- :placeholder="placeholder"
- class="flex flex-1"
- :class="styles"
- @keydown.enter.prevent="emit('enter', $event)"
- @keyup="emit('keyup', $event)"
- @click="emit('click', $event)"
- @keydown="emit('keydown', $event)"
- ></div>
- </div>
- </template>
- <script setup lang="ts">
- import {
- ref,
- onMounted,
- watch,
- nextTick,
- computed,
- Ref,
- } from "@nuxtjs/composition-api"
- import {
- EditorView,
- placeholder as placeholderExt,
- ViewPlugin,
- ViewUpdate,
- keymap,
- } from "@codemirror/view"
- import { EditorState, Extension } from "@codemirror/state"
- import clone from "lodash/clone"
- import { tooltips } from "@codemirror/tooltip"
- import { history, historyKeymap } from "@codemirror/history"
- import { HoppRESTVar } from "@hoppscotch/data"
- import { inputTheme } from "~/helpers/editor/themes/baseTheme"
- import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
- import { useReadonlyStream } from "~/helpers/utils/composables"
- import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
- import { HoppReactiveVarPlugin } from "~/helpers/editor/extensions/HoppVariable"
- import { restVars$ } from "~/newstore/RESTSession"
- const props = withDefaults(
- defineProps<{
- value: string
- placeholder: string
- styles: string
- envs: { key: string; value: string; source: string }[] | null
- vars: { key: string; value: string }[] | null
- focus: boolean
- readonly: boolean
- }>(),
- {
- value: "",
- placeholder: "",
- styles: "",
- envs: null,
- vars: null,
- focus: false,
- readonly: false,
- }
- )
- const emit = defineEmits<{
- (e: "input", data: string): void
- (e: "change", data: string): void
- (e: "paste", data: { prevValue: string; pastedValue: string }): void
- (e: "enter", ev: any): void
- (e: "keyup", ev: any): void
- (e: "keydown", ev: any): void
- (e: "click", ev: any): void
- }>()
- const cachedValue = ref(props.value)
- const view = ref<EditorView>()
- const editor = ref<any | null>(null)
- watch(
- () => props.value,
- (newVal) => {
- const singleLinedText = newVal.replaceAll("\n", "")
- const currDoc = view.value?.state.doc
- .toJSON()
- .join(view.value.state.lineBreak)
- if (cachedValue.value !== singleLinedText || newVal !== currDoc) {
- cachedValue.value = singleLinedText
- view.value?.dispatch({
- filter: false,
- changes: {
- from: 0,
- to: view.value.state.doc.length,
- insert: singleLinedText,
- },
- })
- }
- },
- {
- immediate: true,
- flush: "sync",
- }
- )
- let clipboardEv: ClipboardEvent | null = null
- let pastedValue: string | null = null
- const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
- AggregateEnvironment[]
- >
- const aggregateVars = useReadonlyStream(restVars$, []) as Ref<HoppRESTVar[]>
- const envVars = computed(() =>
- props.envs
- ? props.envs.map((x) => ({
- key: x.key,
- value: x.value,
- sourceEnv: x.source,
- }))
- : aggregateEnvs.value
- )
- const varVars = computed(() =>
- props.vars
- ? props.vars.map((x) => ({
- key: x.key,
- value: x.value,
- }))
- : aggregateVars.value
- )
- const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
- const varTooltipPlugin = new HoppReactiveVarPlugin(varVars, view)
- const initView = (el: any) => {
- const extensions: Extension = [
- EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
- EditorView.updateListener.of((update) => {
- if (props.readonly) {
- update.view.contentDOM.inputMode = "none"
- }
- }),
- EditorState.changeFilter.of(() => !props.readonly),
- inputTheme,
- props.readonly
- ? EditorView.theme({
- ".cm-content": {
- caretColor: "var(--secondary-dark-color) !important",
- color: "var(--secondary-dark-color) !important",
- backgroundColor: "var(--divider-color) !important",
- opacity: 0.25,
- },
- })
- : EditorView.theme({}),
- tooltips({
- position: "absolute",
- }),
- envTooltipPlugin,
- varTooltipPlugin,
- placeholderExt(props.placeholder),
- EditorView.domEventHandlers({
- paste(ev) {
- clipboardEv = ev
- pastedValue = ev.clipboardData?.getData("text") ?? ""
- },
- drop(ev) {
- ev.preventDefault()
- },
- }),
- ViewPlugin.fromClass(
- class {
- update(update: ViewUpdate) {
- if (props.readonly) return
- if (update.docChanged) {
- const prevValue = clone(cachedValue.value)
- cachedValue.value = update.state.doc
- .toJSON()
- .join(update.state.lineBreak)
- // We do not update the cache directly in this case (to trigger value watcher to dispatch)
- // So, we desync cachedValue a bit so we can trigger updates
- const value = clone(cachedValue.value).replaceAll("\n", "")
- emit("input", value)
- emit("change", value)
- const pasted = !!update.transactions.find((txn) =>
- txn.isUserEvent("input.paste")
- )
- if (pasted && clipboardEv) {
- const pastedVal = pastedValue
- nextTick(() => {
- emit("paste", {
- pastedValue: pastedVal!,
- prevValue,
- })
- })
- } else {
- clipboardEv = null
- pastedValue = null
- }
- }
- }
- }
- ),
- history(),
- keymap.of([...historyKeymap]),
- ]
- view.value = new EditorView({
- parent: el,
- state: EditorState.create({
- doc: props.value,
- extensions,
- }),
- })
- }
- onMounted(() => {
- if (editor.value) {
- if (!view.value) initView(editor.value)
- }
- })
- watch(editor, () => {
- if (editor.value) {
- if (!view.value) initView(editor.value)
- } else {
- view.value?.destroy()
- view.value = undefined
- }
- })
- </script>
|