| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- /* global $ $$remove $create $isTextInput messageBoxProxy */// dom.js
- /* global API */// msg.js
- /* global CodeMirror */
- /* global MozDocMapper */// util.js
- /* global MozSectionFinder */
- /* global MozSectionWidget */
- /* global RX_META debounce sessionStore */// toolbox.js
- /* global chromeSync */// storage-util.js
- /* global cmFactory */
- /* global editor */
- /* global linterMan */
- /* global prefs */
- /* global t */// localization.js
- 'use strict';
- /* exported SourceEditor */
- function SourceEditor() {
- const {style, /** @type DirtyReporter */dirty} = editor;
- let savedGeneration;
- let placeholderName = '';
- let prevMode = NaN;
- $$remove('.sectioned-only');
- $('#header').on('wheel', headerOnScroll);
- $('#sections').textContent = '';
- $('#sections').appendChild($create('.single-editor'));
- if (!style.id) setupNewStyle(style);
- const cm = cmFactory.create($('.single-editor'));
- const sectionFinder = MozSectionFinder(cm);
- const sectionWidget = MozSectionWidget(cm, sectionFinder);
- editor.livePreview.init(preprocess);
- createMetaCompiler(meta => {
- style.usercssData = meta;
- style.name = meta.name;
- style.url = meta.homepageURL || style.installationUrl;
- updateMeta();
- });
- updateMeta();
- cm.setValue(style.sourceCode);
- /** @namespace Editor */
- Object.assign(editor, {
- sections: sectionFinder.sections,
- replaceStyle,
- updateLivePreview,
- closestVisible: () => cm,
- getEditors: () => [cm],
- getEditorTitle: () => '',
- getValue: () => cm.getValue(),
- getSearchableInputs: () => [],
- prevEditor: nextPrevSection.bind(null, -1),
- nextEditor: nextPrevSection.bind(null, 1),
- jumpToEditor(i) {
- const sec = sectionFinder.sections[i];
- if (sec) {
- sectionFinder.updatePositions(sec);
- cm.jumpToPos(sec.start);
- cm.focus();
- }
- },
- async save() {
- if (!dirty.isDirty()) return;
- const sourceCode = cm.getValue();
- try {
- const {customName, enabled, id} = style;
- let res = !id && await API.usercss.build({sourceCode, checkDup: true, metaOnly: true});
- if (res && res.dup) {
- messageBoxProxy.alert(t('usercssAvoidOverwriting'), 'danger', t('genericError'));
- } else {
- res = await API.usercss.editSave({customName, enabled, id, sourceCode});
- if (!id) {
- editor.emit('styleChange', res.style, 'new');
- }
- // Awaiting inside `try` so that exceptions go to our `catch`
- await replaceStyle(res.style);
- }
- showLog(res);
- } catch (err) {
- const i = err.index;
- const isNameEmpty = i > 0 &&
- err.code === 'missingValue' &&
- sourceCode.slice(sourceCode.lastIndexOf('\n', i - 1), i).trim().endsWith('@name');
- return isNameEmpty
- ? saveTemplate(sourceCode)
- : showSaveError(err);
- }
- },
- scrollToEditor: () => {},
- });
- prefs.subscribeMany({
- 'editor.linter': updateLinterSwitch,
- 'editor.appliesToLineWidget': (k, val) => sectionWidget.toggle(val),
- 'editor.toc.expanded': (k, val) => sectionFinder.onOff(editor.updateToc, val),
- }, {runNow: true});
- editor.applyScrollInfo(cm);
- cm.clearHistory();
- cm.markClean();
- savedGeneration = cm.changeGeneration();
- cm.on('changes', () => {
- dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
- debounce(updateLivePreview, editor.previewDelay);
- });
- cm.on('optionChange', (cm, option) => {
- if (option !== 'mode') return;
- const mode = getModeName();
- if (mode === prevMode) return;
- prevMode = mode;
- linterMan.run();
- updateLinterSwitch();
- });
- setTimeout(linterMan.enableForEditor, 0, cm);
- if (!$isTextInput(document.activeElement)) {
- cm.focus();
- }
- editor.on('styleToggled', newStyle => {
- if (dirty.isDirty()) {
- editor.toggleStyle(newStyle.enabled);
- } else {
- style.enabled = newStyle.enabled;
- }
- updateMeta();
- updateLivePreview();
- });
- editor.on('styleChange', (newStyle, reason) => {
- if (reason === 'new') return;
- if (reason === 'config') {
- delete newStyle.sourceCode;
- delete newStyle.name;
- Object.assign(style, newStyle);
- updateLivePreview();
- return;
- }
- replaceStyle(newStyle);
- });
- async function preprocess(style) {
- const res = await API.usercss.build({
- styleId: style.id,
- sourceCode: style.sourceCode,
- assignVars: true,
- });
- showLog(res);
- delete res.style.enabled;
- return Object.assign(style, res.style);
- }
- /** Shows the console.log output from the background worker stored in `log` property */
- function showLog(data) {
- if (data.log) data.log.forEach(args => console.log(...args));
- return data;
- }
- function updateLivePreview() {
- if (!style.id) {
- return;
- }
- editor.livePreview.update(Object.assign({}, style, {sourceCode: cm.getValue()}));
- }
- function updateLinterSwitch() {
- const el = $('#editor.linter');
- el.value = getCurrentLinter();
- const cssLintOption = $('[value="csslint"]', el);
- const mode = getModeName();
- if (mode !== 'css') {
- cssLintOption.disabled = true;
- cssLintOption.title = t('linterCSSLintIncompatible', mode);
- } else {
- cssLintOption.disabled = false;
- cssLintOption.title = '';
- }
- }
- function getCurrentLinter() {
- const name = prefs.get('editor.linter');
- if (cm.getOption('mode') !== 'css' && name === 'csslint') {
- return 'stylelint';
- }
- return name;
- }
- async function setupNewStyle(style) {
- style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
- `/* ${t('usercssReplaceTemplateSectionBody')} */`;
- let section = MozDocMapper.styleToCss(style);
- if (!section.includes('@-moz-document')) {
- style.sections[0].domains = ['example.com'];
- section = MozDocMapper.styleToCss(style);
- }
- const DEFAULT_CODE = `
- /* ==UserStyle==
- @name ${''/* a trick to preserve the trailing spaces */}
- @namespace github.com/openstyles/stylus
- @version 1.0.0
- @description A new userstyle
- @author Me
- ==/UserStyle== */
- `.replace(/^\s+/gm, '');
- dirty.clear('sourceGeneration');
- style.sourceCode = '';
- placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
- let code = await chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate);
- code = code || DEFAULT_CODE;
- code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
- `${str}${space ? '' : ' '}${placeholderName}`);
- // strip the last dummy section if any, add an empty line followed by the section
- style.sourceCode = code.replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section;
- cm.startOperation();
- cm.setValue(style.sourceCode);
- cm.clearHistory();
- cm.markClean();
- cm.endOperation();
- dirty.clear('sourceGeneration');
- savedGeneration = cm.changeGeneration();
- }
- function updateMeta() {
- const name = style.customName || style.name;
- if (name !== placeholderName) {
- $('#name').value = name;
- }
- $('#enabled').checked = style.enabled;
- $('#url').href = style.url;
- editor.updateName();
- cm.setPreprocessor((style.usercssData || {}).preprocessor);
- }
- function replaceStyle(newStyle) {
- dirty.clear('name');
- const sameCode = newStyle.sourceCode === cm.getValue();
- if (sameCode) {
- savedGeneration = cm.changeGeneration();
- dirty.clear('sourceGeneration');
- updateEnvironment();
- dirty.clear('enabled');
- updateLivePreview();
- return;
- }
- Promise.resolve(messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))).then(ok => {
- if (!ok) return;
- updateEnvironment();
- if (!sameCode) {
- const cursor = cm.getCursor();
- cm.setValue(style.sourceCode);
- cm.setCursor(cursor);
- savedGeneration = cm.changeGeneration();
- }
- if (sameCode) {
- // the code is same but the environment is changed
- updateLivePreview();
- }
- dirty.clear();
- });
- function updateEnvironment() {
- if (style.id !== newStyle.id) {
- history.replaceState({}, '', `?id=${newStyle.id}`);
- }
- sessionStore.justEditedStyleId = newStyle.id;
- Object.assign(style, newStyle);
- editor.onStyleUpdated();
- updateMeta();
- }
- }
- async function saveTemplate(code) {
- if (await messageBoxProxy.confirm(t('usercssReplaceTemplateConfirmation'))) {
- const key = chromeSync.LZ_KEY.usercssTemplate;
- await chromeSync.setLZValue(key, code);
- if (await chromeSync.getLZValue(key) !== code) {
- messageBoxProxy.alert(t('syncStorageErrorSaving'));
- }
- }
- }
- function showSaveError(err) {
- err = Array.isArray(err) ? err : [err];
- const text = err.map(e => e.message || e).join('\n');
- const points = err.map(e =>
- e.index >= 0 && cm.posFromIndex(e.index) || // usercss meta parser
- e.offset >= 0 && {line: e.line - 1, ch: e.col - 1} // csslint code parser
- ).filter(Boolean);
- cm.setSelections(points.map(p => ({anchor: p, head: p})));
- messageBoxProxy.alert($create('pre', text), 'pre');
- }
- function nextPrevSection(dir) {
- // ensure the data is ready in case the user wants to jump around a lot in a large style
- sectionFinder.keepAliveFor(nextPrevSection, 10e3);
- sectionFinder.updatePositions();
- const {sections} = sectionFinder;
- const num = sections.length;
- if (!num) return;
- dir = dir < 0 ? -1 : 0;
- const pos = cm.getCursor();
- let i = sections.findIndex(sec => CodeMirror.cmpPos(sec.start, pos) > Math.min(dir, 0));
- if (i < 0 && (!dir || CodeMirror.cmpPos(sections[num - 1].start, pos) < 0)) {
- i = 0;
- }
- cm.jumpToPos(sections[(i + dir + num) % num].start);
- }
- function headerOnScroll({target, deltaY, deltaMode, shiftKey}) {
- while ((target = target.parentElement)) {
- if (deltaY < 0 && target.scrollTop ||
- deltaY > 0 && target.scrollTop + target.clientHeight < target.scrollHeight) {
- return;
- }
- }
- cm.display.scroller.scrollTop +=
- // WheelEvent.DOM_DELTA_LINE
- deltaMode === 1 ? deltaY * cm.defaultTextHeight() :
- // WheelEvent.DOM_DELTA_PAGE
- deltaMode === 2 || shiftKey ? Math.sign(deltaY) * cm.display.scroller.clientHeight :
- // WheelEvent.DOM_DELTA_PIXEL
- deltaY;
- }
- function getModeName() {
- const mode = cm.doc.mode;
- if (!mode) return '';
- return (mode.name || mode || '') +
- (mode.helperType || '');
- }
- function createMetaCompiler(onUpdated) {
- let meta = null;
- let metaIndex = null;
- let cache = [];
- linterMan.register(async (text, options, _cm) => {
- if (_cm !== cm) {
- return;
- }
- const match = text.match(RX_META);
- if (!match) {
- return [];
- }
- if (match[0] === meta && match.index === metaIndex) {
- return cache;
- }
- const {metadata, errors} = await linterMan.worker.metalint(match[0]);
- if (errors.every(err => err.code === 'unknownMeta')) {
- onUpdated(metadata);
- }
- cache = errors.map(({code, index, args, message}) => {
- const isUnknownMeta = code === 'unknownMeta';
- const typo = isUnknownMeta && args[1] ? 'Typo' : ''; // args[1] may be present but undefined
- return ({
- from: cm.posFromIndex((index || 0) + match.index),
- to: cm.posFromIndex((index || 0) + match.index),
- message: code && t(`meta_${code}${typo}`, args, false) || message,
- severity: isUnknownMeta ? 'warning' : 'error',
- rule: code,
- });
- });
- meta = match[0];
- metaIndex = match.index;
- return cache;
- });
- }
- }
|