431 lines
13 KiB
JavaScript
431 lines
13 KiB
JavaScript
/* global $ $create $createLink $$remove showSpinner */// dom.js
|
|
/* global API */// msg.js
|
|
/* global URLS closeCurrentTab deepEqual */// toolbox.js
|
|
/* global messageBox */
|
|
/* global prefs */
|
|
/* global preinit */
|
|
/* global t */// localization.js
|
|
/* global requireOrdered */// polyfill
|
|
'use strict';
|
|
|
|
let cm;
|
|
let initialUrl;
|
|
let installed;
|
|
let installedDup;
|
|
let liveReload;
|
|
let tabId;
|
|
let vars;
|
|
|
|
// "History back" in Firefox (for now) restores the old DOM including the messagebox,
|
|
// which stays after installing since we don't want to wait for the fadeout animation before resolving.
|
|
document.on('visibilitychange', () => {
|
|
$$remove('#message-box:not(.config-dialog)');
|
|
if (installed) liveReload.onToggled();
|
|
});
|
|
|
|
setTimeout(() => !cm && showSpinner($('#header')), 200);
|
|
|
|
/*
|
|
* Preinit starts to download as early as possible,
|
|
* then the critical rendering path scripts are loaded in html,
|
|
* then the meta of the downloaded code is parsed in the background worker,
|
|
* then CodeMirror scripts/css are added so they can load while the worker runs in parallel,
|
|
* then the meta response arrives from API and is immediately displayed in CodeMirror,
|
|
* then the sections of code are parsed in the background worker and displayed.
|
|
*/
|
|
(async function init() {
|
|
const theme = prefs.get('editor.theme');
|
|
if (theme !== 'default') {
|
|
require([`/vendor/codemirror/theme/${theme}.css`]); // not awaiting as it may be absent
|
|
}
|
|
const scriptsReady = requireOrdered(
|
|
/* global CodeMirror */
|
|
["/codemirror/base.js"] // codemirror-base
|
|
).then(() => require([
|
|
'/vendor/codemirror/keymap/emacs',
|
|
'/vendor/codemirror/keymap/vim', // TODO: load conditionally
|
|
'/vendor/codemirror/lib/codemirror.css',
|
|
'/vendor/codemirror/addon/fold/foldgutter.css',
|
|
'/js/cmpver', /* global compareVersion */
|
|
'/js/sections-util', /* global styleCodeEmpty */
|
|
'/js/color/color-converter',
|
|
'/edit/codemirror-default.css',
|
|
])).then(() => require([
|
|
'/edit/codemirror-default',
|
|
'/js/color/color-view',
|
|
]));
|
|
|
|
({tabId, initialUrl} = preinit);
|
|
liveReload = initLiveReload();
|
|
|
|
const [
|
|
{dup, style, error, sourceCode},
|
|
hasFileAccess,
|
|
] = await Promise.all([
|
|
preinit.ready,
|
|
API.data.get('hasFileAccess'),
|
|
]);
|
|
if (!style && sourceCode == null) {
|
|
messageBox.alert(isNaN(error) ? `${error}` : 'HTTP Error ' + error, 'pre');
|
|
return;
|
|
}
|
|
await scriptsReady;
|
|
cm = CodeMirror($('.main'), {
|
|
value: sourceCode || style.sourceCode,
|
|
readOnly: true,
|
|
colorpicker: true,
|
|
theme,
|
|
});
|
|
window.on('resize', adjustCodeHeight);
|
|
if (error) {
|
|
showBuildError(error);
|
|
}
|
|
if (!style) {
|
|
return;
|
|
}
|
|
const data = style.usercssData;
|
|
const dupData = dup && dup.usercssData;
|
|
const versionTest = dup && compareVersion(data.version, dupData.version);
|
|
|
|
updateMeta(style, dup);
|
|
|
|
// update UI
|
|
if (versionTest < 0) {
|
|
$('.actions').parentNode.insertBefore(
|
|
$create('.warning', t('versionInvalidOlder')),
|
|
$('.actions')
|
|
);
|
|
}
|
|
$('button.install').onclick = () => {
|
|
(!dup ?
|
|
Promise.resolve(true) :
|
|
messageBox.confirm($create('span', t('styleInstallOverwrite', [
|
|
data.name + (dup.customName ? ` (${dup.customName})` : ''),
|
|
dupData.version,
|
|
data.version,
|
|
])))
|
|
).then(ok => ok &&
|
|
API.usercss.install(style)
|
|
.then(install)
|
|
.catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre'))
|
|
);
|
|
};
|
|
|
|
// set updateUrl
|
|
const checker = $('.set-update-url input[type=checkbox]');
|
|
const updateUrl = new URL(style.updateUrl || initialUrl);
|
|
if (dup && dup.updateUrl === updateUrl.href) {
|
|
checker.checked = true;
|
|
// there is no way to "unset" updateUrl, you can only overwrite it.
|
|
checker.disabled = true;
|
|
} else if (updateUrl.protocol !== 'file:' || hasFileAccess) {
|
|
checker.checked = true;
|
|
style.updateUrl = updateUrl.href;
|
|
}
|
|
checker.onchange = () => {
|
|
style.updateUrl = checker.checked ? updateUrl.href : null;
|
|
};
|
|
checker.onchange();
|
|
$('.set-update-url p').textContent = updateUrl.href.length < 300 ? updateUrl.href :
|
|
updateUrl.href.slice(0, 300) + '...';
|
|
|
|
// set prefer scheme
|
|
const preferScheme = $('.set-prefer-scheme select');
|
|
preferScheme.onchange = () => {
|
|
style.preferScheme = preferScheme.value;
|
|
};
|
|
preferScheme.onchange();
|
|
|
|
if (URLS.isLocalhost(initialUrl)) {
|
|
$('.live-reload input').onchange = liveReload.onToggled;
|
|
} else {
|
|
$('.live-reload').remove();
|
|
}
|
|
})();
|
|
|
|
function updateMeta(style, dup = installedDup) {
|
|
installedDup = dup;
|
|
const data = style.usercssData;
|
|
const dupData = dup && dup.usercssData;
|
|
const versionTest = dup && compareVersion(data.version, dupData.version);
|
|
|
|
cm.setPreprocessor(data.preprocessor);
|
|
|
|
const installButtonLabel = t(
|
|
installed ? 'installButtonInstalled' :
|
|
!dup ? 'installButton' :
|
|
versionTest > 0 ? 'installButtonUpdate' : 'installButtonReinstall'
|
|
);
|
|
document.title = `${installButtonLabel} ${data.name}`;
|
|
|
|
$('.install').textContent = installButtonLabel;
|
|
$('.install').classList.add(
|
|
installed ? 'installed' :
|
|
!dup ? 'install' :
|
|
versionTest > 0 ? 'update' :
|
|
'reinstall');
|
|
$('.set-update-url').title = dup && dup.updateUrl && t('installUpdateFrom', dup.updateUrl) || '';
|
|
$('.meta-name').textContent = data.name;
|
|
$('.meta-version').textContent = data.version;
|
|
$('.meta-description').textContent = data.description;
|
|
$('.set-prefer-scheme select').value =
|
|
style.preferScheme === 'dark' ? 'dark' :
|
|
style.preferScheme === 'light' ? 'light' : 'none';
|
|
|
|
if (data.author) {
|
|
$('.meta-author').parentNode.style.display = '';
|
|
$('.meta-author').textContent = '';
|
|
$('.meta-author').appendChild(makeAuthor(data.author));
|
|
} else {
|
|
$('.meta-author').parentNode.style.display = 'none';
|
|
}
|
|
|
|
$('.meta-license').parentNode.style.display = data.license ? '' : 'none';
|
|
$('.meta-license').textContent = data.license;
|
|
|
|
$('.applies-to').textContent = '';
|
|
getAppliesTo(style).then(list =>
|
|
$('.applies-to').append(...list.map(s => $create('li', s))));
|
|
|
|
$('.external-link').textContent = '';
|
|
const externalLink = makeExternalLink();
|
|
if (externalLink) {
|
|
$('.external-link').appendChild(externalLink);
|
|
}
|
|
|
|
Object.assign($('.configure-usercss'), {
|
|
hidden: !data.vars,
|
|
onclick: openConfigDialog,
|
|
});
|
|
if (!data.vars) {
|
|
$$remove('#message-box.config-dialog');
|
|
} else if (!deepEqual(data.vars, vars)) {
|
|
vars = data.vars;
|
|
// Use the user-customized vars from the installed style
|
|
for (const [dk, dv] of Object.entries(dup && dupData.vars || {})) {
|
|
const v = vars[dk];
|
|
if (v && v.type === dv.type) {
|
|
v.value = dv.value;
|
|
}
|
|
}
|
|
openConfigDialog();
|
|
}
|
|
|
|
$('#header').dataset.arrivedFast = performance.now() < 500;
|
|
$('#header').classList.add('meta-init');
|
|
$('#header').classList.remove('meta-init-error');
|
|
|
|
setTimeout(() => $$remove('.lds-spinner'), 1000);
|
|
showError('');
|
|
requestAnimationFrame(adjustCodeHeight);
|
|
if (dup) enablePostActions();
|
|
|
|
function makeAuthor(text) {
|
|
const match = text.match(/^(.+?)(?:\s+<(.+?)>)?(?:\s+\((.+?)\))?$/);
|
|
if (!match) {
|
|
return document.createTextNode(text);
|
|
}
|
|
const [, name, email, url] = match;
|
|
const frag = document.createDocumentFragment();
|
|
if (email) {
|
|
frag.appendChild($createLink(`mailto:${email}`, name));
|
|
} else {
|
|
frag.appendChild($create('span', name));
|
|
}
|
|
if (url) {
|
|
frag.appendChild($createLink(url,
|
|
$create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'},
|
|
$create('SVG:path', {
|
|
d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z',
|
|
}))
|
|
));
|
|
}
|
|
return frag;
|
|
}
|
|
|
|
function makeExternalLink() {
|
|
const urls = [
|
|
data.homepageURL && [data.homepageURL, t('externalHomepage')],
|
|
data.supportURL && [data.supportURL, t('externalSupport')],
|
|
];
|
|
return (data.homepageURL || data.supportURL) && (
|
|
$create('div', [
|
|
$create('h3', t('externalLink')),
|
|
$create('ul', urls.map(args => args &&
|
|
$create('li',
|
|
$createLink(...args)
|
|
)
|
|
)),
|
|
]));
|
|
}
|
|
|
|
async function openConfigDialog() {
|
|
await require(['/js/dlg/config-dialog']); /* global configDialog */
|
|
configDialog(style);
|
|
}
|
|
}
|
|
|
|
function showError(err) {
|
|
$('.warnings').textContent = '';
|
|
$('.warnings').classList.toggle('visible', Boolean(err));
|
|
$('.container').classList.toggle('has-warnings', Boolean(err));
|
|
err = Array.isArray(err) ? err : [err];
|
|
if (err[0]) {
|
|
let i;
|
|
if ((i = err[0].index) >= 0 ||
|
|
(i = err[0].offset) >= 0) {
|
|
cm.jumpToPos(cm.posFromIndex(i));
|
|
cm.setSelections(err.map(e => {
|
|
const pos = e.index >= 0 && cm.posFromIndex(e.index) || // usercss meta parser
|
|
e.offset >= 0 && {line: e.line - 1, ch: e.col - 1}; // csslint code parser
|
|
return pos && {anchor: pos, head: pos};
|
|
}).filter(Boolean));
|
|
cm.focus();
|
|
}
|
|
$('.warnings').appendChild(
|
|
$create('.warning', [
|
|
t('parseUsercssError'),
|
|
'\n',
|
|
...err.map(e => e.message ? $create('pre', e.message) : e || 'Unknown error'),
|
|
]));
|
|
}
|
|
adjustCodeHeight();
|
|
}
|
|
|
|
function showBuildError(error) {
|
|
$('#header').classList.add('meta-init-error');
|
|
console.error(error);
|
|
showError(error);
|
|
}
|
|
|
|
function install(style) {
|
|
installed = style;
|
|
|
|
$$remove('.warning');
|
|
$('button.install').disabled = true;
|
|
$('button.install').classList.add('installed');
|
|
$('#live-reload-install-hint').classList.toggle('hidden', !liveReload.enabled);
|
|
$('h2.installed').classList.add('active');
|
|
$('.set-update-url input[type=checkbox]').disabled = true;
|
|
$('.set-update-url').title = style.updateUrl ?
|
|
t('installUpdateFrom', style.updateUrl) : '';
|
|
$('.set-prefer-scheme select').disabled = true;
|
|
enablePostActions();
|
|
updateMeta(style);
|
|
}
|
|
|
|
function enablePostActions() {
|
|
const {id} = installed || installedDup;
|
|
sessionStorage.justEditedStyleId = id;
|
|
$('h2.installed').hidden = !installed;
|
|
$('.installed-actions').hidden = false;
|
|
$('.installed-actions a[href*="edit.html"]').search = `?id=${id}`;
|
|
$('#delete').onclick = async () => {
|
|
if (await messageBox.confirm(t('deleteStyleConfirm'), 'danger center', t('confirmDelete'))) {
|
|
await API.styles.delete(id);
|
|
if (tabId < 0 && history.length > 1) {
|
|
history.back();
|
|
} else {
|
|
closeCurrentTab();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
async function getAppliesTo(style) {
|
|
if (style.sectionsPromise) {
|
|
try {
|
|
style.sections = await style.sectionsPromise;
|
|
} catch (error) {
|
|
showBuildError(error);
|
|
return [];
|
|
} finally {
|
|
delete style.sectionsPromise;
|
|
}
|
|
}
|
|
let numGlobals = 0;
|
|
const res = [];
|
|
const TARGETS = ['urls', 'urlPrefixes', 'domains', 'regexps'];
|
|
for (const section of style.sections) {
|
|
const targets = [].concat(...TARGETS.map(t => section[t]).filter(Boolean));
|
|
res.push(...targets);
|
|
numGlobals += !targets.length && !styleCodeEmpty(section.code);
|
|
}
|
|
res.sort();
|
|
if (!res.length || numGlobals) {
|
|
res.push(t('appliesToEverything'));
|
|
}
|
|
return [...new Set(res)];
|
|
}
|
|
|
|
function adjustCodeHeight() {
|
|
// Chrome-only bug (apparently): it doesn't limit the scroller element height
|
|
const scroller = cm.display.scroller;
|
|
const prevWindowHeight = adjustCodeHeight.prevWindowHeight;
|
|
if (scroller.scrollHeight === scroller.clientHeight ||
|
|
prevWindowHeight && window.innerHeight !== prevWindowHeight) {
|
|
adjustCodeHeight.prevWindowHeight = window.innerHeight;
|
|
cm.setSize(null, $('.main').offsetHeight - $('.warnings').offsetHeight);
|
|
}
|
|
}
|
|
|
|
function initLiveReload() {
|
|
const DELAY = 500;
|
|
let isEnabled = false;
|
|
let timer = 0;
|
|
const getData = preinit.getData;
|
|
let sequence = preinit.ready;
|
|
return {
|
|
get enabled() {
|
|
return isEnabled;
|
|
},
|
|
onToggled(e) {
|
|
if (e) isEnabled = e.target.checked;
|
|
if (installed || installedDup) {
|
|
if (isEnabled) {
|
|
check({force: true});
|
|
} else {
|
|
stop();
|
|
}
|
|
$('.install').disabled = isEnabled;
|
|
Object.assign($('#live-reload-install-hint'), {
|
|
hidden: !isEnabled,
|
|
textContent: t(`liveReloadInstallHint${tabId >= 0 ? 'FF' : ''}`),
|
|
});
|
|
}
|
|
},
|
|
};
|
|
function check(opts) {
|
|
getData(opts)
|
|
.then(update, logError)
|
|
.then(() => {
|
|
timer = 0;
|
|
start();
|
|
});
|
|
}
|
|
function logError(error) {
|
|
console.warn(t('liveReloadError', error));
|
|
}
|
|
function start() {
|
|
timer = timer || setTimeout(check, DELAY);
|
|
}
|
|
function stop() {
|
|
clearTimeout(timer);
|
|
timer = 0;
|
|
}
|
|
function update(code) {
|
|
if (code == null) return;
|
|
sequence = sequence.catch(console.error).then(() => {
|
|
const {id} = installed || installedDup;
|
|
const scrollInfo = cm.getScrollInfo();
|
|
const cursor = cm.getCursor();
|
|
cm.setValue(code);
|
|
cm.setCursor(cursor);
|
|
cm.scrollTo(scrollInfo.left, scrollInfo.top);
|
|
return API.usercss.install({id, sourceCode: code})
|
|
.then(updateMeta)
|
|
.catch(showError);
|
|
});
|
|
}
|
|
}
|