1
0

source-editor.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /* global CodeMirror dirtyReporter initLint beautify showKeyMapHelp */
  2. /* global showToggleStyleHelp goBackToManage updateLintReportIfEnabled */
  3. /* global hotkeyRerouter setupAutocomplete */
  4. /* global editors linterConfig updateLinter regExpTester mozParser */
  5. /* global makeLink createAppliesToLineWidget messageBox */
  6. 'use strict';
  7. function createSourceEditor(style) {
  8. // a flag for isTouched()
  9. let hadBeenSaved = false;
  10. document.documentElement.classList.add('usercss');
  11. $('#sections').textContent = '';
  12. $('#name').disabled = true;
  13. $('#mozilla-format-heading').parentNode.remove();
  14. $('#sections').appendChild(
  15. $element({className: 'single-editor'})
  16. );
  17. $('#header').appendChild($element({
  18. id: 'footer',
  19. appendChild: makeLink('https://github.com/openstyles/stylus/wiki/Usercss', t('externalUsercssDocument'))
  20. }));
  21. const dirty = dirtyReporter();
  22. dirty.onChange(() => {
  23. const DIRTY = dirty.isDirty();
  24. document.body.classList.toggle('dirty', DIRTY);
  25. $('#save-button').disabled = !DIRTY;
  26. updateTitle();
  27. });
  28. // normalize style
  29. if (!style.id) {
  30. setupNewStyle(style);
  31. } else {
  32. // style might be an object reference to background page
  33. style = deepCopy(style);
  34. }
  35. const cm = CodeMirror($('.single-editor'));
  36. editors.push(cm);
  37. updateMeta().then(() => {
  38. initLint();
  39. initLinterSwitch();
  40. cm.setValue(style.sourceCode);
  41. cm.clearHistory();
  42. cm.markClean();
  43. initHooks();
  44. initAppliesToLineWidget();
  45. // focus must be the last action, otherwise the style is duplicated on saving
  46. cm.focus();
  47. });
  48. function initAppliesToLineWidget() {
  49. const PREF_NAME = 'editor.appliesToLineWidget';
  50. const widget = createAppliesToLineWidget(cm);
  51. const optionEl = buildOption();
  52. $('#options').insertBefore(optionEl, $('#options > .option.aligned'));
  53. widget.toggle(prefs.get(PREF_NAME));
  54. prefs.subscribe([PREF_NAME], (key, value) => {
  55. widget.toggle(value);
  56. optionEl.checked = value;
  57. });
  58. optionEl.addEventListener('change', e => {
  59. prefs.set(PREF_NAME, e.target.checked);
  60. });
  61. function buildOption() {
  62. return $element({className: 'option', appendChild: [
  63. $element({
  64. tag: 'input',
  65. type: 'checkbox',
  66. id: PREF_NAME,
  67. checked: prefs.get(PREF_NAME)
  68. }),
  69. $element({
  70. tag: 'label',
  71. htmlFor: PREF_NAME,
  72. textContent: ' ' + t('appliesLineWidgetLabel'),
  73. title: t('appliesLineWidgetWarning')
  74. })
  75. ]});
  76. }
  77. }
  78. function initLinterSwitch() {
  79. const linterEl = $('#editor.linter');
  80. cm.on('optionChange', (cm, option) => {
  81. if (option !== 'mode') {
  82. return;
  83. }
  84. updateLinter();
  85. update();
  86. });
  87. linterEl.addEventListener('change', update);
  88. update();
  89. function update() {
  90. linterEl.value = linterConfig.getDefault();
  91. const cssLintOption = linterEl.querySelector('[value="csslint"]');
  92. if (cm.getOption('mode') !== 'css') {
  93. cssLintOption.disabled = true;
  94. cssLintOption.title = t('linterCSSLintIncompatible', cm.getOption('mode'));
  95. } else {
  96. cssLintOption.disabled = false;
  97. cssLintOption.title = '';
  98. }
  99. }
  100. }
  101. function setupNewStyle(style) {
  102. style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) + '/* Insert code here... */';
  103. let section = mozParser.format(style);
  104. if (!section.includes('@-moz-document')) {
  105. style.sections[0].domains = ['example.com'];
  106. section = mozParser.format(style);
  107. }
  108. const sourceCode = `/* ==UserStyle==
  109. @name New Style - ${Date.now()}
  110. @namespace github.com/openstyles/stylus
  111. @version 0.1.0
  112. @description A new userstyle
  113. @author Me
  114. ==/UserStyle== */
  115. ${section}
  116. `;
  117. dirty.modify('source', '', sourceCode);
  118. style.sourceCode = sourceCode;
  119. }
  120. function initHooks() {
  121. // sidebar commands
  122. $('#save-button').onclick = save;
  123. $('#beautify').onclick = beautify;
  124. $('#keyMap-help').onclick = showKeyMapHelp;
  125. $('#toggle-style-help').onclick = showToggleStyleHelp;
  126. $('#cancel-button').onclick = goBackToManage;
  127. // enable
  128. $('#enabled').onchange = e => {
  129. const value = e.target.checked;
  130. dirty.modify('enabled', style.enabled, value);
  131. style.enabled = value;
  132. };
  133. // source
  134. cm.on('change', () => {
  135. const value = cm.getValue();
  136. dirty.modify('source', style.sourceCode, value);
  137. style.sourceCode = value;
  138. updateLintReportIfEnabled(cm);
  139. });
  140. // hotkeyRerouter
  141. cm.on('focus', () => {
  142. hotkeyRerouter.setState(false);
  143. });
  144. cm.on('blur', () => {
  145. hotkeyRerouter.setState(true);
  146. });
  147. // autocomplete
  148. if (prefs.get('editor.autocompleteOnTyping')) {
  149. setupAutocomplete(cm);
  150. }
  151. }
  152. function updateMeta() {
  153. $('#name').value = style.name;
  154. $('#enabled').checked = style.enabled;
  155. $('#url').href = style.url;
  156. const {usercssData: {preprocessor} = {}} = style;
  157. // beautify only works with regular CSS
  158. $('#beautify').disabled = cm.getOption('mode') !== 'css';
  159. updateTitle();
  160. return cm.setPreprocessor(preprocessor);
  161. }
  162. function updateTitle() {
  163. // title depends on dirty and style meta
  164. if (!style.id) {
  165. document.title = t('addStyleTitle');
  166. } else {
  167. document.title = (dirty.isDirty() ? '* ' : '') + t('editStyleTitle', [style.name]);
  168. }
  169. }
  170. function replaceStyle(newStyle) {
  171. if (!style.id && newStyle.id) {
  172. history.replaceState({}, '', `?id=${newStyle.id}`);
  173. }
  174. style = deepCopy(newStyle);
  175. updateMeta();
  176. if (style.sourceCode !== cm.getValue()) {
  177. const cursor = cm.getCursor();
  178. cm.setValue(style.sourceCode);
  179. cm.setCursor(cursor);
  180. }
  181. dirty.clear();
  182. hadBeenSaved = false;
  183. }
  184. function setStyleDirty(newStyle) {
  185. dirty.clear();
  186. dirty.modify('source', newStyle.sourceCode, style.sourceCode);
  187. dirty.modify('enabled', newStyle.enabled, style.enabled);
  188. }
  189. function toggleStyle() {
  190. const value = !style.enabled;
  191. dirty.modify('enabled', style.enabled, value);
  192. style.enabled = value;
  193. updateMeta();
  194. // save when toggle enable state?
  195. save();
  196. }
  197. function save() {
  198. if (!dirty.isDirty()) {
  199. return;
  200. }
  201. return onBackgroundReady()
  202. .then(() => BG.usercssHelper.save({
  203. reason: 'editSave',
  204. id: style.id,
  205. enabled: style.enabled,
  206. sourceCode: style.sourceCode
  207. }))
  208. .then(replaceStyle)
  209. .then(() => {
  210. hadBeenSaved = true;
  211. })
  212. .catch(err => {
  213. const contents = [String(err)];
  214. if (Number.isInteger(err.index)) {
  215. const pos = cm.posFromIndex(err.index);
  216. contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
  217. contents.push($element({
  218. tag: 'pre',
  219. textContent: drawLinePointer(pos)
  220. }));
  221. }
  222. console.error(err);
  223. messageBox.alert(contents);
  224. });
  225. function drawLinePointer(pos) {
  226. const SIZE = 60;
  227. const line = cm.getLine(pos.line);
  228. const pointer = ' '.repeat(pos.ch) + '^';
  229. const start = Math.max(Math.min(pos.ch - SIZE / 2, line.length - SIZE), 0);
  230. const end = Math.min(Math.max(pos.ch + SIZE / 2, SIZE), line.length);
  231. const leftPad = start !== 0 ? '...' : '';
  232. const rightPad = end !== line.length ? '...' : '';
  233. return leftPad + line.slice(start, end) + rightPad + '\n' +
  234. ' '.repeat(leftPad.length) + pointer.slice(start, end);
  235. }
  236. }
  237. function isTouched() {
  238. // indicate that the editor had been touched by the user
  239. return dirty.isDirty() || hadBeenSaved;
  240. }
  241. function replaceMeta(newStyle) {
  242. style.enabled = newStyle.enabled;
  243. dirty.clear('enabled');
  244. updateMeta();
  245. }
  246. return {
  247. replaceStyle,
  248. replaceMeta,
  249. setStyleDirty,
  250. save,
  251. toggleStyle,
  252. isDirty: dirty.isDirty,
  253. getStyle: () => style,
  254. isTouched
  255. };
  256. }