vm-editor.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <template>
  2. <section>
  3. <h3 v-text="i18n('labelEditor')"></h3>
  4. <div class="mr-1c flex center-items">
  5. <span v-text="i18n('labelTheme')"/>
  6. <select v-model="theme" :disabled="busy" :title="css">
  7. <option :value="DEFAULT" v-text="i18n('labelRunAtDefault')"/>
  8. <option value="" v-text="i18n('labelBadgeNone')"/>
  9. <option v-for="name in themes" :key="name" v-text="name"/>
  10. </select>
  11. <a :href="ghURL" target="_blank">&nearr;</a>
  12. <p v-text="error"/>
  13. </div>
  14. <p v-html="i18n('descEditorOptions')" class="my-1"/>
  15. <setting-text name="editor" ref="editor" :json="true" :has-reset="true" @save="onSave">
  16. <button v-text="i18n('buttonShowEditorState')" @click="toggleStateHint"/>
  17. </setting-text>
  18. <pre v-text="hint" class="monospace-font dim-hint" />
  19. </section>
  20. </template>
  21. <script>
  22. import options from '@/common/options';
  23. import hookSetting from '@/common/hook-setting';
  24. import { showMessage } from '@/common/ui';
  25. import SettingText from '@/common/ui/setting-text';
  26. const keyThemeCSS = 'editorTheme';
  27. const keyThemeNAME = 'editorThemeName';
  28. const THEMES = process.env.CODEMIRROR_THEMES;
  29. const gh = 'github.com';
  30. const ghREPO = 'codemirror/CodeMirror';
  31. const ghBRANCH = 'master';
  32. const ghPATH = 'theme';
  33. const ghURL = `https://${gh}/${ghREPO}/tree/${ghBRANCH}/${ghPATH}`;
  34. const DEFAULT = 'default';
  35. const previewLINES = 20;
  36. const previewLENGTH = 100;
  37. const makeTextPreview = css => (
  38. css
  39. ? css.split('\n', previewLINES + 1).map((s, i) => (
  40. i === previewLINES && (
  41. '...'
  42. ) || s.length > previewLENGTH && (
  43. `${s.slice(0, previewLENGTH)}...`
  44. ) || s
  45. )).join('\n')
  46. : null
  47. );
  48. export default {
  49. data() {
  50. return {
  51. hint: null,
  52. busy: false,
  53. error: null,
  54. css: null,
  55. theme: null,
  56. themes: THEMES,
  57. DEFAULT,
  58. ghURL,
  59. };
  60. },
  61. components: {
  62. SettingText,
  63. },
  64. beforeUnmount() {
  65. this.revokers.forEach(revoke => revoke());
  66. this.revokers = null;
  67. },
  68. async mounted() {
  69. this.$refs.editor.$el.addEventListener('dblclick', this.toggleBoolean);
  70. if (!this.revokers) {
  71. this.css = makeTextPreview(options.get(keyThemeCSS));
  72. this.revokers = [
  73. hookSetting(keyThemeNAME, val => { this.theme = val ?? DEFAULT; }),
  74. ];
  75. await options.ready; // Waiting for hookSetting to set the value before watching for changes
  76. this.$watch('theme', async val => {
  77. const url = val && val !== DEFAULT
  78. && `https://raw.githubusercontent.com/${ghREPO}/${ghBRANCH}/${ghPATH}/${val}.css`;
  79. const css = url && await this.fetch(url);
  80. options.set(keyThemeNAME, !url || css ? val : DEFAULT);
  81. options.set(keyThemeCSS, css || '');
  82. this.css = makeTextPreview(css);
  83. });
  84. }
  85. },
  86. methods: {
  87. async fetch(url, method = 'text') {
  88. const el = document.activeElement;
  89. this.busy = true;
  90. try {
  91. const res = await (await fetch(url))[method]();
  92. this.error = null;
  93. return res;
  94. } catch (e) {
  95. this.error = e.message || e.code || `${e}`;
  96. } finally {
  97. this.busy = false;
  98. this.$nextTick(() => el?.focus());
  99. }
  100. },
  101. onSave() {
  102. showMessage({ text: this.$refs.editor.error || this.i18n('msgSavedEditorOptions') });
  103. },
  104. toggleBoolean(event) {
  105. const el = /** @type {HTMLTextAreaElement} */ event.target;
  106. const { selectionStart: start, selectionEnd: end, value } = el;
  107. const toggled = { false: 'true', true: 'false' }[value.slice(start, end)];
  108. // FF can't run execCommand on textarea, https://bugzil.la/1220696#c24
  109. if (toggled && !document.execCommand('insertText', false, toggled)) {
  110. el.value = value.slice(0, start) + toggled + value.slice(end);
  111. el.setSelectionRange(start + toggled.length, start + toggled.length);
  112. el.dispatchEvent(new Event('input'));
  113. el.onblur = () => el.dispatchEvent(new Event('change'));
  114. }
  115. },
  116. async toggleStateHint() {
  117. if (this.hint) {
  118. this.hint = null;
  119. return;
  120. }
  121. const HIDE_OPTS = [
  122. // we activate only one mode: js
  123. 'mode',
  124. // duh
  125. 'value',
  126. // these accept only a function
  127. 'configureMouse',
  128. 'lineNumberFormatter',
  129. 'specialCharPlaceholder',
  130. ];
  131. const opts = {};
  132. Object.entries({
  133. ...(await import('codemirror')).default.defaults,
  134. ...(await import('@/common/ui/code')).default.data().cmDefaults,
  135. ...options.get('editor'),
  136. })
  137. // sort by keys alphabetically to make it more readable
  138. .sort(([a], [b]) => (a < b ? -1 : a > b))
  139. .filter(([key, val]) => !HIDE_OPTS.includes(key) && typeof val !== 'function')
  140. .forEach(([key, val]) => { opts[key] = val; });
  141. this.hint = JSON.stringify(opts, null, ' ');
  142. setTimeout(() => {
  143. if (this.$el.getBoundingClientRect().bottom > window.innerHeight) {
  144. this.$el.scrollIntoView({ behavior: 'smooth' });
  145. }
  146. });
  147. },
  148. },
  149. };
  150. </script>
  151. <style>
  152. .dim-hint {
  153. font-size: .85rem;
  154. color: var(--fill-8);
  155. }
  156. </style>