AceEditor.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <template>
  2. <div class="show-if-initialized" :class="{ initialized }">
  3. <pre ref="editor" :class="styles"></pre>
  4. <div
  5. v-if="provideOutline"
  6. class="
  7. bg-primaryLight
  8. border-t border-divider
  9. flex flex-nowrap flex-1
  10. py-1
  11. px-4
  12. bottom-0
  13. z-10
  14. sticky
  15. overflow-auto
  16. hide-scrollbar
  17. "
  18. >
  19. <div
  20. v-for="(p, index) in currentPath"
  21. :key="`p-${index}`"
  22. class="
  23. cursor-pointer
  24. flex-grow-0 flex-shrink-0
  25. text-secondaryLight
  26. inline-flex
  27. items-center
  28. hover:text-secondary
  29. "
  30. >
  31. <span @click="onBlockClick(index)">
  32. {{ p }}
  33. </span>
  34. <i v-if="index + 1 !== currentPath.length" class="mx-2 material-icons">
  35. chevron_right
  36. </i>
  37. <tippy
  38. v-if="siblingDropDownIndex == index"
  39. ref="options"
  40. interactive
  41. trigger="click"
  42. theme="popover"
  43. arrow
  44. >
  45. <SmartItem
  46. v-for="(sibling, siblingIndex) in currentSibling"
  47. :key="`p-${index}-sibling-${siblingIndex}`"
  48. :label="sibling.key ? sibling.key.value : i"
  49. @click.native="goToSibling(sibling)"
  50. />
  51. </tippy>
  52. </div>
  53. </div>
  54. </div>
  55. </template>
  56. <script>
  57. import ace from "ace-builds"
  58. import "ace-builds/webpack-resolver"
  59. import { defineComponent } from "@nuxtjs/composition-api"
  60. import jsonParse from "~/helpers/jsonParse"
  61. import debounce from "~/helpers/utils/debounce"
  62. import outline from "~/helpers/outline"
  63. export default defineComponent({
  64. props: {
  65. provideOutline: {
  66. type: Boolean,
  67. default: false,
  68. required: false,
  69. },
  70. value: {
  71. type: String,
  72. default: "",
  73. },
  74. theme: {
  75. type: String,
  76. required: false,
  77. default: null,
  78. },
  79. lang: {
  80. type: String,
  81. default: "json",
  82. },
  83. lint: {
  84. type: Boolean,
  85. default: true,
  86. required: false,
  87. },
  88. options: {
  89. type: Object,
  90. default: () => {},
  91. },
  92. styles: {
  93. type: String,
  94. default: "",
  95. },
  96. },
  97. data() {
  98. return {
  99. initialized: false,
  100. editor: null,
  101. cacheValue: "",
  102. outline: outline(),
  103. currentPath: [],
  104. currentSibling: [],
  105. siblingDropDownIndex: null,
  106. }
  107. },
  108. computed: {
  109. appFontSize() {
  110. return getComputedStyle(document.documentElement).getPropertyValue(
  111. "--body-font-size"
  112. )
  113. },
  114. },
  115. watch: {
  116. value(value) {
  117. if (value !== this.cacheValue) {
  118. this.editor.session.setValue(value, 1)
  119. this.cacheValue = value
  120. if (this.lint) this.provideLinting(value)
  121. }
  122. },
  123. theme() {
  124. this.initialized = false
  125. this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
  126. this.$nextTick().then(() => {
  127. this.initialized = true
  128. })
  129. })
  130. },
  131. lang(value) {
  132. this.editor.getSession().setMode(`ace/mode/${value}`)
  133. },
  134. options(value) {
  135. this.editor.setOptions(value)
  136. },
  137. },
  138. mounted() {
  139. const editor = ace.edit(this.$refs.editor, {
  140. mode: `ace/mode/${this.lang}`,
  141. ...this.options,
  142. })
  143. // Set the theme and show the editor only after it's been set to prevent FOUC.
  144. editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
  145. this.$nextTick().then(() => {
  146. this.initialized = true
  147. })
  148. })
  149. editor.setFontSize(this.appFontSize)
  150. if (this.value) editor.setValue(this.value, 1)
  151. this.editor = editor
  152. this.cacheValue = this.value
  153. if (this.lang === "json" && this.provideOutline)
  154. this.initOutline(this.value)
  155. editor.on("change", () => {
  156. const content = editor.getValue()
  157. this.$emit("input", content)
  158. this.cacheValue = content
  159. if (this.provideOutline) debounce(this.initOutline(content), 500)
  160. if (this.lint) this.provideLinting(content)
  161. })
  162. if (this.lang === "json" && this.provideOutline) {
  163. editor.session.selection.on("changeCursor", () => {
  164. const index = editor.session.doc.positionToIndex(
  165. editor.selection.getCursor(),
  166. 0
  167. )
  168. const path = this.outline.genPath(index)
  169. if (path.success) {
  170. this.currentPath = path.res
  171. }
  172. })
  173. }
  174. // Disable linting, if lint prop is false
  175. if (this.lint) this.provideLinting(this.value)
  176. },
  177. destroyed() {
  178. this.editor.destroy()
  179. },
  180. methods: {
  181. defineTheme() {
  182. if (this.theme) {
  183. return this.theme
  184. }
  185. const strip = (str) =>
  186. str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
  187. return strip(
  188. window
  189. .getComputedStyle(document.documentElement)
  190. .getPropertyValue("--editor-theme")
  191. )
  192. },
  193. provideLinting: debounce(function (code) {
  194. if (this.lang === "json") {
  195. try {
  196. jsonParse(code)
  197. this.editor.session.setAnnotations([])
  198. } catch (e) {
  199. const pos = this.editor.session
  200. .getDocument()
  201. .indexToPosition(e.start, 0)
  202. this.editor.session.setAnnotations([
  203. {
  204. row: pos.row,
  205. column: pos.column,
  206. text: e.message,
  207. type: "error",
  208. },
  209. ])
  210. }
  211. }
  212. }, 2000),
  213. onBlockClick(index) {
  214. if (this.siblingDropDownIndex === index) {
  215. this.clearSiblingList()
  216. } else {
  217. this.currentSibling = this.outline.getSiblings(index)
  218. if (this.currentSibling.length) this.siblingDropDownIndex = index
  219. }
  220. },
  221. clearSiblingList() {
  222. this.currentSibling = []
  223. this.siblingDropDownIndex = null
  224. },
  225. goToSibling(obj) {
  226. this.clearSiblingList()
  227. if (obj.start) {
  228. const pos = this.editor.session.doc.indexToPosition(obj.start, 0)
  229. if (pos) {
  230. this.editor.session.selection.moveCursorTo(pos.row, pos.column, true)
  231. this.editor.session.selection.clearSelection()
  232. this.editor.scrollToLine(pos.row, false, true, null)
  233. }
  234. }
  235. },
  236. initOutline: debounce(function (content) {
  237. if (this.lang === "json") {
  238. try {
  239. this.outline.init(content)
  240. if (content[0] === "[") this.currentPath.push("[]")
  241. else this.currentPath.push("{}")
  242. } catch (e) {
  243. console.log("Outline error: ", e)
  244. }
  245. }
  246. }),
  247. },
  248. })
  249. </script>
  250. <style scoped lang="scss">
  251. .show-if-initialized {
  252. &.initialized {
  253. @apply opacity-100;
  254. }
  255. & > * {
  256. @apply transition-none;
  257. }
  258. }
  259. </style>