EnvInput.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <template>
  2. <div
  3. class="flex items-center flex-1 flex-shrink-0 overflow-auto whitespace-nowrap hide-scrollbar"
  4. >
  5. <div
  6. ref="editor"
  7. :placeholder="placeholder"
  8. class="flex flex-1"
  9. :class="styles"
  10. @keydown.enter.prevent="emit('enter', $event)"
  11. @keyup="emit('keyup', $event)"
  12. @click="emit('click', $event)"
  13. @keydown="emit('keydown', $event)"
  14. ></div>
  15. </div>
  16. </template>
  17. <script setup lang="ts">
  18. import {
  19. ref,
  20. onMounted,
  21. watch,
  22. nextTick,
  23. computed,
  24. Ref,
  25. } from "@nuxtjs/composition-api"
  26. import {
  27. EditorView,
  28. placeholder as placeholderExt,
  29. ViewPlugin,
  30. ViewUpdate,
  31. } from "@codemirror/view"
  32. import { EditorState, Extension } from "@codemirror/state"
  33. import clone from "lodash/clone"
  34. import { tooltips } from "@codemirror/tooltip"
  35. import { inputTheme } from "~/helpers/editor/themes/baseTheme"
  36. import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
  37. import { useReadonlyStream } from "~/helpers/utils/composables"
  38. import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
  39. const props = withDefaults(
  40. defineProps<{
  41. value: string
  42. placeholder: string
  43. styles: string
  44. envs: { key: string; value: string; source: string }[] | null
  45. focus: boolean
  46. }>(),
  47. {
  48. value: "",
  49. placeholder: "",
  50. styles: "",
  51. envs: null,
  52. focus: false,
  53. }
  54. )
  55. const emit = defineEmits<{
  56. (e: "input", data: string): void
  57. (e: "change", data: string): void
  58. (e: "paste", data: { prevValue: string; pastedValue: string }): void
  59. (e: "enter", ev: any): void
  60. (e: "keyup", ev: any): void
  61. (e: "keydown", ev: any): void
  62. (e: "click", ev: any): void
  63. }>()
  64. const cachedValue = ref(props.value)
  65. const view = ref<EditorView>()
  66. const editor = ref<any | null>(null)
  67. watch(
  68. () => props.value,
  69. (newVal) => {
  70. const singleLinedText = newVal.replaceAll("\n", "")
  71. const currDoc = view.value?.state.doc
  72. .toJSON()
  73. .join(view.value.state.lineBreak)
  74. if (cachedValue.value !== singleLinedText || newVal !== currDoc) {
  75. cachedValue.value = singleLinedText
  76. view.value?.dispatch({
  77. filter: false,
  78. changes: {
  79. from: 0,
  80. to: view.value.state.doc.length,
  81. insert: singleLinedText,
  82. },
  83. })
  84. }
  85. },
  86. {
  87. immediate: true,
  88. flush: "sync",
  89. }
  90. )
  91. let clipboardEv: ClipboardEvent | null = null
  92. let pastedValue: string | null = null
  93. const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
  94. AggregateEnvironment[]
  95. >
  96. const envVars = computed(() =>
  97. props.envs
  98. ? props.envs.map((x) => ({
  99. key: x.key,
  100. value: x.value,
  101. sourceEnv: x.source,
  102. }))
  103. : aggregateEnvs.value
  104. )
  105. const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
  106. const initView = (el: any) => {
  107. const extensions: Extension = [
  108. inputTheme,
  109. tooltips({
  110. position: "absolute",
  111. }),
  112. envTooltipPlugin,
  113. placeholderExt(props.placeholder),
  114. EditorView.domEventHandlers({
  115. paste(ev) {
  116. clipboardEv = ev
  117. pastedValue = ev.clipboardData?.getData("text") ?? ""
  118. },
  119. drop(ev) {
  120. ev.preventDefault()
  121. },
  122. }),
  123. ViewPlugin.fromClass(
  124. class {
  125. update(update: ViewUpdate) {
  126. if (update.docChanged) {
  127. const prevValue = clone(cachedValue.value)
  128. cachedValue.value = update.state.doc
  129. .toJSON()
  130. .join(update.state.lineBreak)
  131. // We do not update the cache directly in this case (to trigger value watcher to dispatch)
  132. // So, we desync cachedValue a bit so we can trigger updates
  133. const value = clone(cachedValue.value).replaceAll("\n", "")
  134. emit("input", value)
  135. emit("change", value)
  136. const pasted = !!update.transactions.find((txn) =>
  137. txn.isUserEvent("input.paste")
  138. )
  139. if (pasted && clipboardEv) {
  140. const pastedVal = pastedValue
  141. nextTick(() => {
  142. emit("paste", {
  143. pastedValue: pastedVal!,
  144. prevValue,
  145. })
  146. })
  147. } else {
  148. clipboardEv = null
  149. pastedValue = null
  150. }
  151. }
  152. }
  153. }
  154. ),
  155. ]
  156. view.value = new EditorView({
  157. parent: el,
  158. state: EditorState.create({
  159. doc: props.value,
  160. extensions,
  161. }),
  162. })
  163. }
  164. onMounted(() => {
  165. if (editor.value) {
  166. if (!view.value) initView(editor.value)
  167. }
  168. })
  169. watch(editor, () => {
  170. if (editor.value) {
  171. if (!view.value) initView(editor.value)
  172. } else {
  173. view.value?.destroy()
  174. view.value = undefined
  175. }
  176. })
  177. </script>