2021-01-01 14:27:58 +00:00
|
|
|
/* global $ $$remove $create $isTextInput messageBoxProxy */// dom.js
|
|
|
|
/* global API */// msg.js
|
|
|
|
/* global CodeMirror */
|
|
|
|
/* global MozDocMapper */// util.js
|
|
|
|
/* global MozSectionFinder */
|
|
|
|
/* global MozSectionWidget */
|
2021-01-26 13:33:17 +00:00
|
|
|
/* global RX_META debounce sessionStore */// toolbox.js
|
2021-01-01 14:27:58 +00:00
|
|
|
/* global chromeSync */// storage-util.js
|
|
|
|
/* global cmFactory */
|
|
|
|
/* global editor */
|
|
|
|
/* global linterMan */
|
|
|
|
/* global prefs */
|
|
|
|
/* global t */// localization.js
|
2017-09-11 16:09:25 +00:00
|
|
|
'use strict';
|
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
/* exported SourceEditor */
|
|
|
|
function SourceEditor() {
|
2021-01-01 14:27:58 +00:00
|
|
|
const {style, /** @type DirtyReporter */dirty} = editor;
|
2020-11-08 08:12:42 +00:00
|
|
|
let savedGeneration;
|
2020-10-22 20:47:46 +00:00
|
|
|
let placeholderName = '';
|
2020-11-08 08:12:42 +00:00
|
|
|
let prevMode = NaN;
|
2020-10-22 20:47:46 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
$$remove('.sectioned-only');
|
2020-11-08 08:12:42 +00:00
|
|
|
$('#header').on('wheel', headerOnScroll);
|
2017-11-26 13:04:03 +00:00
|
|
|
$('#sections').textContent = '';
|
2017-12-03 21:12:09 +00:00
|
|
|
$('#sections').appendChild($create('.single-editor'));
|
2017-09-11 16:09:25 +00:00
|
|
|
|
2018-01-05 10:26:11 +00:00
|
|
|
if (!style.id) setupNewStyle(style);
|
2017-10-08 15:26:55 +00:00
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
const cm = cmFactory.create($('.single-editor'));
|
|
|
|
const sectionFinder = MozSectionFinder(cm);
|
2021-01-01 14:27:58 +00:00
|
|
|
const sectionWidget = MozSectionWidget(cm, sectionFinder);
|
2021-07-30 12:44:06 +00:00
|
|
|
editor.livePreview.init(preprocess);
|
2021-01-01 14:27:58 +00:00
|
|
|
createMetaCompiler(meta => {
|
|
|
|
style.usercssData = meta;
|
|
|
|
style.name = meta.name;
|
|
|
|
style.url = meta.homepageURL || style.installationUrl;
|
|
|
|
updateMeta();
|
|
|
|
});
|
|
|
|
updateMeta();
|
|
|
|
cm.setValue(style.sourceCode);
|
|
|
|
|
|
|
|
/** @namespace Editor */
|
2020-11-08 08:12:42 +00:00
|
|
|
Object.assign(editor, {
|
|
|
|
sections: sectionFinder.sections,
|
|
|
|
replaceStyle,
|
2021-01-01 14:27:58 +00:00
|
|
|
updateLivePreview,
|
|
|
|
closestVisible: () => cm,
|
2020-11-08 08:12:42 +00:00
|
|
|
getEditors: () => [cm],
|
|
|
|
getEditorTitle: () => '',
|
2021-07-30 12:44:06 +00:00
|
|
|
getValue: () => cm.getValue(),
|
2021-01-01 14:27:58 +00:00
|
|
|
getSearchableInputs: () => [],
|
2020-11-08 08:12:42 +00:00
|
|
|
prevEditor: nextPrevSection.bind(null, -1),
|
|
|
|
nextEditor: nextPrevSection.bind(null, 1),
|
|
|
|
jumpToEditor(i) {
|
|
|
|
const sec = sectionFinder.sections[i];
|
|
|
|
if (sec) {
|
|
|
|
sectionFinder.updatePositions(sec);
|
2020-11-21 19:16:15 +00:00
|
|
|
cm.jumpToPos(sec.start);
|
2021-09-21 07:12:58 +00:00
|
|
|
cm.focus();
|
2020-11-08 08:12:42 +00:00
|
|
|
}
|
|
|
|
},
|
2021-01-01 14:27:58 +00:00
|
|
|
async save() {
|
|
|
|
if (!dirty.isDirty()) return;
|
|
|
|
const sourceCode = cm.getValue();
|
|
|
|
try {
|
|
|
|
const {customName, enabled, id} = style;
|
2021-03-14 06:50:50 +00:00
|
|
|
let res = !id && await API.usercss.build({sourceCode, checkDup: true, metaOnly: true});
|
|
|
|
if (res && res.dup) {
|
2021-01-01 14:27:58 +00:00
|
|
|
messageBoxProxy.alert(t('usercssAvoidOverwriting'), 'danger', t('genericError'));
|
|
|
|
} else {
|
2021-03-14 06:50:50 +00:00
|
|
|
res = await API.usercss.editSave({customName, enabled, id, sourceCode});
|
2021-08-13 10:40:24 +00:00
|
|
|
// Awaiting inside `try` so that exceptions go to our `catch`
|
2021-03-14 06:50:50 +00:00
|
|
|
await replaceStyle(res.style);
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
2021-03-14 06:50:50 +00:00
|
|
|
showLog(res);
|
2021-01-01 14:27:58 +00:00
|
|
|
} 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: () => {},
|
2018-10-01 14:03:17 +00:00
|
|
|
});
|
2021-01-01 14:27:58 +00:00
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
prefs.subscribeMany({
|
|
|
|
'editor.linter': updateLinterSwitch,
|
|
|
|
'editor.appliesToLineWidget': (k, val) => sectionWidget.toggle(val),
|
|
|
|
'editor.toc.expanded': (k, val) => sectionFinder.onOff(editor.updateToc, val),
|
2021-01-01 14:27:58 +00:00
|
|
|
}, {runNow: true});
|
|
|
|
|
2020-11-18 11:17:15 +00:00
|
|
|
editor.applyScrollInfo(cm);
|
2020-11-08 08:12:42 +00:00
|
|
|
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;
|
2021-01-01 14:27:58 +00:00
|
|
|
linterMan.run();
|
2018-01-05 10:20:46 +00:00
|
|
|
updateLinterSwitch();
|
2017-11-22 00:38:29 +00:00
|
|
|
});
|
2021-01-01 14:27:58 +00:00
|
|
|
setTimeout(linterMan.enableForEditor, 0, cm);
|
|
|
|
if (!$isTextInput(document.activeElement)) {
|
2020-11-08 08:12:42 +00:00
|
|
|
cm.focus();
|
|
|
|
}
|
2017-09-13 08:58:03 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
async function preprocess(style) {
|
2021-03-14 06:50:50 +00:00
|
|
|
const res = await API.usercss.build({
|
2018-11-25 13:27:11 +00:00
|
|
|
styleId: style.id,
|
2018-11-07 06:09:29 +00:00
|
|
|
sourceCode: style.sourceCode,
|
2020-11-18 11:17:15 +00:00
|
|
|
assignVars: true,
|
2021-01-01 14:27:58 +00:00
|
|
|
});
|
2021-03-14 06:50:50 +00:00
|
|
|
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;
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function updateLivePreview() {
|
|
|
|
if (!style.id) {
|
|
|
|
return;
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
editor.livePreview.update(Object.assign({}, style, {sourceCode: cm.getValue()}));
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2018-01-05 10:20:46 +00:00
|
|
|
function updateLinterSwitch() {
|
|
|
|
const el = $('#editor.linter');
|
2018-10-01 14:03:17 +00:00
|
|
|
el.value = getCurrentLinter();
|
2018-01-05 10:20:46 +00:00
|
|
|
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 = '';
|
2017-10-07 10:00:25 +00:00
|
|
|
}
|
2017-10-15 19:58:02 +00:00
|
|
|
}
|
2017-10-07 10:00:25 +00:00
|
|
|
|
2018-10-01 14:03:17 +00:00
|
|
|
function getCurrentLinter() {
|
|
|
|
const name = prefs.get('editor.linter');
|
|
|
|
if (cm.getOption('mode') !== 'css' && name === 'csslint') {
|
|
|
|
return 'stylelint';
|
|
|
|
}
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
|
2020-10-22 20:47:46 +00:00
|
|
|
async function setupNewStyle(style) {
|
2018-02-22 09:41:55 +00:00
|
|
|
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
|
|
|
|
`/* ${t('usercssReplaceTemplateSectionBody')} */`;
|
2021-01-01 14:27:58 +00:00
|
|
|
let section = MozDocMapper.styleToCss(style);
|
2017-10-08 15:26:55 +00:00
|
|
|
if (!section.includes('@-moz-document')) {
|
|
|
|
style.sections[0].domains = ['example.com'];
|
2021-01-01 14:27:58 +00:00
|
|
|
section = MozDocMapper.styleToCss(style);
|
2017-10-08 15:26:55 +00:00
|
|
|
}
|
2017-11-26 13:04:03 +00:00
|
|
|
const DEFAULT_CODE = `
|
|
|
|
/* ==UserStyle==
|
2018-08-06 10:35:33 +00:00
|
|
|
@name ${''/* a trick to preserve the trailing spaces */}
|
2017-11-26 13:04:03 +00:00
|
|
|
@namespace github.com/openstyles/stylus
|
2018-08-06 10:35:33 +00:00
|
|
|
@version 1.0.0
|
2017-11-26 13:04:03 +00:00
|
|
|
@description A new userstyle
|
|
|
|
@author Me
|
|
|
|
==/UserStyle== */
|
2018-02-22 09:41:55 +00:00
|
|
|
`.replace(/^\s+/gm, '');
|
2018-01-05 10:20:46 +00:00
|
|
|
|
2017-11-26 21:45:21 +00:00
|
|
|
dirty.clear('sourceGeneration');
|
2017-11-26 13:04:03 +00:00
|
|
|
style.sourceCode = '';
|
2018-01-05 10:20:46 +00:00
|
|
|
|
2020-10-22 20:47:46 +00:00
|
|
|
placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
|
2020-11-08 10:31:07 +00:00
|
|
|
let code = await chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate);
|
2020-10-22 20:47:46 +00:00
|
|
|
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();
|
2017-10-08 15:26:55 +00:00
|
|
|
}
|
|
|
|
|
2017-11-08 23:26:51 +00:00
|
|
|
function updateMeta() {
|
2020-10-22 20:47:46 +00:00
|
|
|
const name = style.customName || style.name;
|
|
|
|
if (name !== placeholderName) {
|
|
|
|
$('#name').value = name;
|
|
|
|
}
|
2017-09-11 16:09:25 +00:00
|
|
|
$('#enabled').checked = style.enabled;
|
|
|
|
$('#url').href = style.url;
|
2020-11-08 08:12:42 +00:00
|
|
|
editor.updateName();
|
|
|
|
cm.setPreprocessor((style.usercssData || {}).preprocessor);
|
2017-09-13 09:33:32 +00:00
|
|
|
}
|
|
|
|
|
2017-11-26 21:45:21 +00:00
|
|
|
function replaceStyle(newStyle, codeIsUpdated) {
|
2020-10-11 15:12:06 +00:00
|
|
|
dirty.clear('name');
|
2017-11-26 21:45:21 +00:00
|
|
|
const sameCode = newStyle.sourceCode === cm.getValue();
|
|
|
|
if (sameCode) {
|
|
|
|
savedGeneration = cm.changeGeneration();
|
|
|
|
dirty.clear('sourceGeneration');
|
2017-10-08 15:26:55 +00:00
|
|
|
}
|
2017-11-26 21:45:21 +00:00
|
|
|
if (codeIsUpdated === false || sameCode) {
|
2017-11-29 10:34:00 +00:00
|
|
|
updateEnvironment();
|
2017-11-26 21:45:21 +00:00
|
|
|
dirty.clear('enabled');
|
2018-11-07 06:09:29 +00:00
|
|
|
updateLivePreview();
|
2017-11-26 21:45:21 +00:00
|
|
|
return;
|
2017-09-11 16:09:25 +00:00
|
|
|
}
|
2017-11-29 10:34:00 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
Promise.resolve(messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))).then(ok => {
|
2018-01-05 10:20:46 +00:00
|
|
|
if (!ok) return;
|
2017-11-29 10:34:00 +00:00
|
|
|
updateEnvironment();
|
2017-11-26 21:45:21 +00:00
|
|
|
if (!sameCode) {
|
|
|
|
const cursor = cm.getCursor();
|
|
|
|
cm.setValue(style.sourceCode);
|
|
|
|
cm.setCursor(cursor);
|
|
|
|
savedGeneration = cm.changeGeneration();
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
if (sameCode) {
|
|
|
|
// the code is same but the environment is changed
|
|
|
|
updateLivePreview();
|
|
|
|
}
|
2017-11-26 21:45:21 +00:00
|
|
|
dirty.clear();
|
|
|
|
});
|
2017-11-29 10:34:00 +00:00
|
|
|
|
|
|
|
function updateEnvironment() {
|
|
|
|
if (style.id !== newStyle.id) {
|
|
|
|
history.replaceState({}, '', `?id=${newStyle.id}`);
|
|
|
|
}
|
2020-11-18 11:17:15 +00:00
|
|
|
sessionStore.justEditedStyleId = newStyle.id;
|
2018-11-07 06:09:29 +00:00
|
|
|
Object.assign(style, newStyle);
|
2021-07-30 12:44:06 +00:00
|
|
|
editor.onStyleUpdated();
|
2017-11-29 10:34:00 +00:00
|
|
|
updateMeta();
|
|
|
|
}
|
2017-11-09 06:07:06 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
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'));
|
|
|
|
}
|
|
|
|
}
|
2018-08-18 20:17:20 +00:00
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
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');
|
2017-10-16 08:08:13 +00:00
|
|
|
}
|
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
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;
|
2017-12-02 15:29:12 +00:00
|
|
|
}
|
2020-11-21 19:16:15 +00:00
|
|
|
cm.jumpToPos(sections[(i + dir + num) % num].start);
|
2017-12-02 15:29:12 +00:00
|
|
|
}
|
|
|
|
|
2017-12-28 04:01:43 +00:00
|
|
|
function headerOnScroll({target, deltaY, deltaMode, shiftKey}) {
|
|
|
|
while ((target = target.parentElement)) {
|
|
|
|
if (deltaY < 0 && target.scrollTop ||
|
|
|
|
deltaY > 0 && target.scrollTop + target.clientHeight < target.scrollHeight) {
|
|
|
|
return;
|
|
|
|
}
|
2017-12-07 01:36:46 +00:00
|
|
|
}
|
|
|
|
cm.display.scroller.scrollTop +=
|
|
|
|
// WheelEvent.DOM_DELTA_LINE
|
2018-08-24 11:31:29 +00:00
|
|
|
deltaMode === 1 ? deltaY * cm.defaultTextHeight() :
|
2017-12-07 01:36:46 +00:00
|
|
|
// WheelEvent.DOM_DELTA_PAGE
|
|
|
|
deltaMode === 2 || shiftKey ? Math.sign(deltaY) * cm.display.scroller.clientHeight :
|
|
|
|
// WheelEvent.DOM_DELTA_PIXEL
|
|
|
|
deltaY;
|
|
|
|
}
|
|
|
|
|
2018-01-05 10:20:46 +00:00
|
|
|
function getModeName() {
|
|
|
|
const mode = cm.doc.mode;
|
2018-04-19 08:12:23 +00:00
|
|
|
if (!mode) return '';
|
|
|
|
return (mode.name || mode || '') +
|
|
|
|
(mode.helperType || '');
|
2018-01-05 10:20:46 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
function createMetaCompiler(onUpdated) {
|
|
|
|
let meta = null;
|
|
|
|
let metaIndex = null;
|
|
|
|
let cache = [];
|
|
|
|
linterMan.register(async (text, options, _cm) => {
|
|
|
|
if (_cm !== cm) {
|
|
|
|
return;
|
|
|
|
}
|
2021-01-26 13:33:17 +00:00
|
|
|
const match = text.match(RX_META);
|
2021-01-01 14:27:58 +00:00
|
|
|
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);
|
|
|
|
}
|
2021-08-01 16:00:42 +00:00
|
|
|
cache = errors.map(({code, index, args, message}) => {
|
|
|
|
const isUnknownMeta = code === 'unknownMeta';
|
2021-09-07 09:40:06 +00:00
|
|
|
const typo = isUnknownMeta && args[1] ? 'Typo' : ''; // args[1] may be present but undefined
|
2021-08-01 16:00:42 +00:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
});
|
2021-01-01 14:27:58 +00:00
|
|
|
meta = match[0];
|
|
|
|
metaIndex = match.index;
|
|
|
|
return cache;
|
|
|
|
});
|
|
|
|
}
|
2017-09-11 16:09:25 +00:00
|
|
|
}
|