code.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <template>
  2. <div class="flex flex-col">
  3. <vl-code class="editor-code flex-auto"
  4. :options="cmOptions" v-model="content" @ready="onReady"
  5. />
  6. <div class="frame-block" v-show="search.show">
  7. <button class="pull-right" @click="clearSearch">&times;</button>
  8. <form class="inline-block mr-1" @submit.prevent="goToLine()">
  9. <span v-text="i18n('labelLineNumber')"></span>
  10. <input class="w-1" v-model="search.line">
  11. </form>
  12. <form class="inline-block mr-1" @submit.prevent="findNext()">
  13. <span v-text="i18n('labelSearch')"></span>
  14. <tooltip title="Ctrl-F">
  15. <input ref="search" v-model="search.state.query">
  16. </tooltip>
  17. <tooltip title="Shift-Ctrl-G">
  18. <button type="button" @click="findNext(1)">&lt;</button>
  19. </tooltip>
  20. <tooltip title="Ctrl-G">
  21. <button type="submit">&gt;</button>
  22. </tooltip>
  23. </form>
  24. <form class="inline-block mr-1" @submit.prevent="replace()" v-if="!readonly">
  25. <span v-text="i18n('labelReplace')"></span>
  26. <input v-model="search.state.replace">
  27. <tooltip title="Shift-Ctrl-F">
  28. <button type="submit" v-text="i18n('buttonReplace')"></button>
  29. </tooltip>
  30. <tooltip title="Shift-Ctrl-R">
  31. <button type="button" v-text="i18n('buttonReplaceAll')" @click="replace(1)"></button>
  32. </tooltip>
  33. </form>
  34. </div>
  35. </div>
  36. </template>
  37. <script>
  38. import 'codemirror/lib/codemirror.css';
  39. import 'codemirror/theme/eclipse.css';
  40. import 'codemirror/mode/javascript/javascript';
  41. import 'codemirror/addon/comment/continuecomment';
  42. import 'codemirror/addon/edit/matchbrackets';
  43. import 'codemirror/addon/edit/closebrackets';
  44. import 'codemirror/addon/fold/foldcode';
  45. import 'codemirror/addon/fold/foldgutter';
  46. import 'codemirror/addon/fold/brace-fold';
  47. import 'codemirror/addon/fold/comment-fold';
  48. import 'codemirror/addon/search/match-highlighter';
  49. import 'codemirror/addon/search/searchcursor';
  50. import 'codemirror/addon/selection/active-line';
  51. import CodeMirror from 'codemirror';
  52. import { Code as VlCode } from 'vueleton';
  53. import { debounce } from 'src/common';
  54. import Tooltip from './tooltip';
  55. function getHandler(key) {
  56. return (cm) => {
  57. const { commands } = cm.state;
  58. const handle = commands && commands[key];
  59. return handle && handle();
  60. };
  61. }
  62. function indentWithTab(cm) {
  63. if (cm.somethingSelected()) {
  64. cm.indentSelection('add');
  65. } else {
  66. cm.replaceSelection(
  67. cm.getOption('indentWithTabs') ? '\t' : ' '.repeat(cm.getOption('indentUnit')),
  68. 'end', '+input');
  69. }
  70. }
  71. [
  72. 'save', 'cancel', 'find', 'findNext', 'findPrev', 'replace', 'replaceAll', 'close',
  73. ].forEach((key) => {
  74. CodeMirror.commands[key] = getHandler(key);
  75. });
  76. const cmOptions = {
  77. continueComments: true,
  78. matchBrackets: true,
  79. autoCloseBrackets: true,
  80. highlightSelectionMatches: true,
  81. lineNumbers: true,
  82. mode: 'javascript',
  83. lineWrapping: true,
  84. styleActiveLine: true,
  85. foldGutter: true,
  86. gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
  87. theme: 'eclipse',
  88. };
  89. function findNext(cm, state, reversed) {
  90. cm.operation(() => {
  91. const query = state.query || '';
  92. let cursor = cm.getSearchCursor(query, reversed ? state.posFrom : state.posTo);
  93. if (!cursor.find(reversed)) {
  94. cursor = cm.getSearchCursor(query,
  95. reversed ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
  96. if (!cursor.find(reversed)) return;
  97. }
  98. cm.setSelection(cursor.from(), cursor.to());
  99. state.posFrom = cursor.from();
  100. state.posTo = cursor.to();
  101. });
  102. }
  103. function replaceOne(cm, state) {
  104. const start = cm.getCursor('start');
  105. const end = cm.getCursor('end');
  106. state.posTo = state.posFrom;
  107. findNext(cm, state);
  108. const start2 = cm.getCursor('start');
  109. const end2 = cm.getCursor('end');
  110. if (
  111. start.line === start2.line && start.ch === start2.ch
  112. && end.line === end2.line && end.ch === end2.ch
  113. ) {
  114. cm.replaceRange(state.replace, start, end);
  115. findNext(cm, state);
  116. }
  117. }
  118. function replaceAll(cm, state) {
  119. cm.operation(() => {
  120. const query = state.query || '';
  121. for (let cursor = cm.getSearchCursor(query); cursor.findNext();) {
  122. cursor.replace(state.replace);
  123. }
  124. });
  125. }
  126. export default {
  127. props: {
  128. readonly: {
  129. type: Boolean,
  130. default: false,
  131. },
  132. value: true,
  133. commands: true,
  134. global: {
  135. type: Boolean,
  136. default: true,
  137. },
  138. },
  139. components: {
  140. VlCode,
  141. Tooltip,
  142. },
  143. data() {
  144. return {
  145. cmOptions,
  146. content: this.value,
  147. search: {
  148. show: false,
  149. state: {
  150. query: null,
  151. replace: null,
  152. },
  153. },
  154. };
  155. },
  156. watch: {
  157. content(content) {
  158. this.$emit('input', content);
  159. },
  160. value(value) {
  161. if (value === this.content) return;
  162. this.content = value;
  163. const { cm } = this;
  164. if (!cm) return;
  165. cm.getDoc().clearHistory();
  166. cm.focus();
  167. },
  168. 'search.state.query'() {
  169. this.debouncedFind();
  170. },
  171. },
  172. methods: {
  173. onReady(cm) {
  174. this.cm = cm;
  175. if (this.readonly) cm.setOption('readOnly', true);
  176. cm.state.commands = Object.assign({
  177. cancel: () => {
  178. if (this.search.show) {
  179. this.clearSearch();
  180. } else {
  181. cm.execCommand('close');
  182. }
  183. },
  184. find: this.find,
  185. findNext: this.findNext,
  186. findPrev: () => {
  187. this.findNext(1);
  188. },
  189. replace: this.replace,
  190. replaceAll: () => {
  191. this.replace(1);
  192. },
  193. }, this.commands);
  194. cm.setOption('extraKeys', {
  195. Esc: 'cancel',
  196. Tab: indentWithTab,
  197. });
  198. cm.on('keyHandled', (_cm, _name, e) => {
  199. e.stopPropagation();
  200. });
  201. this.$emit('ready', cm);
  202. },
  203. onKeyDown(e) {
  204. const name = CodeMirror.keyName(e);
  205. const { cm } = this;
  206. [
  207. cm.options.extraKeys,
  208. cm.options.keyMap,
  209. ].some((keyMap) => {
  210. let stop = false;
  211. if (keyMap) {
  212. CodeMirror.lookupKey(name, keyMap, (b) => {
  213. if (cm.state.commands[b]) {
  214. e.preventDefault();
  215. e.stopPropagation();
  216. cm.execCommand(b);
  217. stop = true;
  218. }
  219. }, cm);
  220. }
  221. return stop;
  222. });
  223. },
  224. doFind(reversed) {
  225. const { state } = this.search;
  226. const { cm } = this;
  227. if (state.query) {
  228. findNext(cm, state, reversed);
  229. }
  230. this.search.show = true;
  231. },
  232. find() {
  233. const { state } = this.search;
  234. state.posTo = state.posFrom;
  235. this.doFind();
  236. this.$nextTick(() => {
  237. const { search } = this.$refs;
  238. search.select();
  239. search.focus();
  240. });
  241. },
  242. findNext(reversed) {
  243. this.doFind(reversed);
  244. this.$nextTick(() => {
  245. this.$refs.search.focus();
  246. });
  247. },
  248. clearSearch() {
  249. const { cm } = this;
  250. cm.operation(() => {
  251. const { state } = this.search;
  252. state.posFrom = null;
  253. state.posTo = null;
  254. this.search.show = false;
  255. });
  256. cm.focus();
  257. },
  258. replace(all) {
  259. const { cm } = this;
  260. const { state } = this.search;
  261. if (!state.query) {
  262. this.find();
  263. return;
  264. }
  265. (all ? replaceAll : replaceOne)(cm, state);
  266. },
  267. goToLine() {
  268. const line = this.search.line - 1;
  269. const { cm } = this;
  270. if (!isNaN(line)) cm.setCursor(line, 0);
  271. cm.focus();
  272. },
  273. },
  274. mounted() {
  275. this.debouncedFind = debounce(this.doFind, 100);
  276. if (this.global) window.addEventListener('keydown', this.onKeyDown, false);
  277. },
  278. beforeDestroy() {
  279. if (this.global) window.removeEventListener('keydown', this.onKeyDown, false);
  280. },
  281. };
  282. </script>