2022-03-24 14:08:04 +00:00
|
|
|
/* global $$ $ $create messageBoxProxy setupLivePrefs */// dom.js
|
2021-01-01 14:27:58 +00:00
|
|
|
/* global API */// msg.js
|
|
|
|
/* global CODEMIRROR_THEMES */
|
|
|
|
/* global CodeMirror */
|
|
|
|
/* global MozDocMapper */// sections-util.js
|
2022-02-20 15:34:51 +00:00
|
|
|
/* global chromeSync */// storage-util.js
|
2021-01-01 14:27:58 +00:00
|
|
|
/* global initBeautifyButton */// beautify.js
|
|
|
|
/* global prefs */
|
|
|
|
/* global t */// localization.js
|
2022-02-14 19:19:20 +00:00
|
|
|
/* global FIREFOX getOwnTab sessionStore tryJSONparse tryURL */// toolbox.js
|
2021-01-01 14:27:58 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type Editor
|
|
|
|
* @namespace Editor
|
|
|
|
*/
|
2021-12-29 19:57:22 +00:00
|
|
|
const editor = {
|
2021-07-30 12:44:06 +00:00
|
|
|
style: null,
|
2021-01-01 14:27:58 +00:00
|
|
|
dirty: DirtyReporter(),
|
|
|
|
isUsercss: false,
|
|
|
|
isWindowed: false,
|
2022-03-24 14:08:04 +00:00
|
|
|
livePreview: LivePreview(),
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @type {'customName'|'name'} */
|
|
|
|
nameTarget: 'name',
|
|
|
|
previewDelay: 200, // Chrome devtools uses 200
|
2022-01-10 16:12:29 +00:00
|
|
|
saving: false,
|
2021-01-01 14:27:58 +00:00
|
|
|
scrollInfo: null,
|
|
|
|
|
2021-12-29 19:57:22 +00:00
|
|
|
cancel: () => location.assign('/manage.html'),
|
|
|
|
|
|
|
|
updateClass() {
|
2022-02-14 19:19:20 +00:00
|
|
|
$.rootCL.toggle('is-new-style', !editor.style.id);
|
2021-07-30 12:44:06 +00:00
|
|
|
},
|
|
|
|
|
2022-03-24 14:08:04 +00:00
|
|
|
updateTheme(name) {
|
|
|
|
if (!CODEMIRROR_THEMES[name]) {
|
|
|
|
name = 'default';
|
|
|
|
prefs.set('editor.theme', name);
|
|
|
|
}
|
|
|
|
$('#cm-theme').dataset.theme = name;
|
|
|
|
$('#cm-theme').textContent = CODEMIRROR_THEMES[name] || '';
|
|
|
|
},
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
updateTitle(isDirty = editor.dirty.isDirty()) {
|
|
|
|
const {customName, name} = editor.style;
|
|
|
|
document.title = `${
|
|
|
|
isDirty ? '* ' : ''
|
|
|
|
}${
|
|
|
|
customName || name || t('styleMissingName')
|
|
|
|
} - Stylus`; // the suffix enables external utilities to process our windows e.g. pin on top
|
|
|
|
},
|
2021-12-29 19:57:22 +00:00
|
|
|
};
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
//#region pre-init
|
|
|
|
|
2022-03-24 14:08:04 +00:00
|
|
|
(() => {
|
2022-02-14 19:19:20 +00:00
|
|
|
const mqCompact = matchMedia('(max-width: 850px)');
|
|
|
|
const toggleCompact = mq => $.rootCL.toggle('compact-layout', mq.matches);
|
|
|
|
mqCompact.on('change', toggleCompact);
|
|
|
|
toggleCompact(mqCompact);
|
2022-03-24 14:08:04 +00:00
|
|
|
Object.assign(editor, /** @namespace Editor */ {
|
2022-02-14 19:19:20 +00:00
|
|
|
mqCompact,
|
2022-03-24 14:08:04 +00:00
|
|
|
styleReady: prefs.ready.then(loadStyle),
|
|
|
|
});
|
2021-01-01 14:27:58 +00:00
|
|
|
async function loadStyle() {
|
|
|
|
const params = new URLSearchParams(location.search);
|
2022-02-20 15:34:51 +00:00
|
|
|
let id = Number(params.get('id'));
|
2021-01-01 14:27:58 +00:00
|
|
|
const style = id && await API.styles.get(id) || {
|
2022-02-20 15:34:51 +00:00
|
|
|
id: id = null, // resetting the non-existent id
|
2021-01-01 14:27:58 +00:00
|
|
|
name: params.get('domain') ||
|
2021-05-27 11:18:28 +00:00
|
|
|
tryURL(params.get('url-prefix')).hostname ||
|
2021-01-01 14:27:58 +00:00
|
|
|
'',
|
|
|
|
enabled: true,
|
|
|
|
sections: [
|
|
|
|
MozDocMapper.toSection([...params], {code: ''}),
|
|
|
|
],
|
|
|
|
};
|
|
|
|
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
|
2022-02-20 15:34:51 +00:00
|
|
|
const isUC = Boolean(style.usercssData || !id && prefs.get('newStyleAsUsercss'));
|
|
|
|
Object.assign(editor, /** @namespace Editor */ {
|
|
|
|
style,
|
|
|
|
isUsercss: isUC,
|
|
|
|
template: isUC && !id && chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate), // promise
|
|
|
|
});
|
2021-12-29 19:57:22 +00:00
|
|
|
editor.updateClass();
|
2022-03-24 14:08:04 +00:00
|
|
|
editor.updateTheme(prefs.get('editor.theme'));
|
2021-01-01 14:27:58 +00:00
|
|
|
editor.updateTitle(false);
|
2022-02-20 15:34:51 +00:00
|
|
|
$.rootCL.add(isUC ? 'usercss' : 'sectioned');
|
|
|
|
sessionStore.justEditedStyleId = id || '';
|
2021-01-01 14:27:58 +00:00
|
|
|
// no such style so let's clear the invalid URL parameters
|
2022-02-20 15:34:51 +00:00
|
|
|
if (!id) history.replaceState({}, '', location.pathname);
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
//#region init header
|
|
|
|
|
2022-03-24 14:08:04 +00:00
|
|
|
/* exported EditorHeader */
|
|
|
|
function EditorHeader() {
|
2021-01-01 14:27:58 +00:00
|
|
|
initBeautifyButton($('#beautify'));
|
|
|
|
initKeymapElement();
|
|
|
|
initNameArea();
|
|
|
|
initThemeElement();
|
|
|
|
setupLivePrefs();
|
|
|
|
|
2022-03-24 14:08:04 +00:00
|
|
|
window.on('load', () => {
|
2021-01-01 14:27:58 +00:00
|
|
|
prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});
|
|
|
|
window.on('showHotkeyInTooltip', showHotkeyInTooltip);
|
2022-03-24 14:08:04 +00:00
|
|
|
}, {once: true});
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
function findKeyForCommand(command, map) {
|
|
|
|
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
|
|
|
let key = Object.keys(map).find(k => map[k] === command);
|
|
|
|
if (key) {
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
|
|
|
key = ft && findKeyForCommand(command, ft);
|
|
|
|
if (key) {
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
function initNameArea() {
|
|
|
|
const nameEl = $('#name');
|
|
|
|
const resetEl = $('#reset-name');
|
|
|
|
const isCustomName = editor.style.updateUrl || editor.isUsercss;
|
|
|
|
editor.nameTarget = isCustomName ? 'customName' : 'name';
|
|
|
|
nameEl.placeholder = t(editor.isUsercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
|
|
|
nameEl.title = isCustomName ? t('customNameHint') : '';
|
|
|
|
nameEl.on('input', () => {
|
|
|
|
editor.updateName(true);
|
|
|
|
resetEl.hidden = false;
|
|
|
|
});
|
|
|
|
resetEl.hidden = !editor.style.customName;
|
|
|
|
resetEl.onclick = () => {
|
|
|
|
const {style} = editor;
|
|
|
|
nameEl.focus();
|
|
|
|
nameEl.select();
|
|
|
|
// trying to make it undoable via Ctrl-Z
|
|
|
|
if (!document.execCommand('insertText', false, style.name)) {
|
|
|
|
nameEl.value = style.name;
|
|
|
|
editor.updateName(true);
|
|
|
|
}
|
|
|
|
style.customName = null; // to delete it from db
|
|
|
|
resetEl.hidden = true;
|
|
|
|
};
|
|
|
|
const enabledEl = $('#enabled');
|
|
|
|
enabledEl.onchange = () => editor.updateEnabledness(enabledEl.checked);
|
|
|
|
}
|
|
|
|
|
|
|
|
function initThemeElement() {
|
|
|
|
$('#editor.theme').append(...[
|
|
|
|
$create('option', {value: 'default'}, t('defaultTheme')),
|
2022-03-24 14:08:04 +00:00
|
|
|
...Object.keys(CODEMIRROR_THEMES).map(s => $create('option', s)),
|
2021-01-01 14:27:58 +00:00
|
|
|
]);
|
|
|
|
// move the theme after built-in CSS so that its same-specificity selectors win
|
|
|
|
document.head.appendChild($('#cm-theme'));
|
|
|
|
}
|
|
|
|
|
|
|
|
function initKeymapElement() {
|
|
|
|
// move 'pc' or 'mac' prefix to the end of the displayed label
|
|
|
|
const maps = Object.keys(CodeMirror.keyMap)
|
|
|
|
.map(name => ({
|
|
|
|
value: name,
|
|
|
|
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
|
|
|
|
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
|
|
|
|
}))
|
|
|
|
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
|
|
|
|
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
let bin = fragment;
|
|
|
|
let groupName;
|
|
|
|
// group suffixed maps in <optgroup>
|
|
|
|
maps.forEach(({value, name}, i) => {
|
|
|
|
groupName = !name.includes('-') ? name : groupName;
|
|
|
|
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
|
|
|
|
if (groupWithNext) {
|
|
|
|
if (bin === fragment) {
|
|
|
|
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const el = bin.appendChild($create('option', {value}, name));
|
|
|
|
if (value === prefs.defaults['editor.keyMap']) {
|
|
|
|
el.dataset.default = '';
|
|
|
|
el.title = t('defaultTheme');
|
|
|
|
}
|
|
|
|
if (!groupWithNext) bin = fragment;
|
|
|
|
});
|
|
|
|
const selector = $('#editor.keyMap');
|
|
|
|
selector.textContent = '';
|
|
|
|
selector.appendChild(fragment);
|
|
|
|
selector.value = prefs.get('editor.keyMap');
|
|
|
|
}
|
|
|
|
|
|
|
|
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
|
|
|
const extraKeys = CodeMirror.defaults.extraKeys;
|
|
|
|
for (const el of $$('[data-hotkey-tooltip]')) {
|
|
|
|
if (el._hotkeyTooltipKeyMap !== mapName) {
|
|
|
|
el._hotkeyTooltipKeyMap = mapName;
|
|
|
|
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
|
|
|
const cmd = el.dataset.hotkeyTooltip;
|
|
|
|
const key = cmd[0] === '=' ? cmd.slice(1) :
|
|
|
|
findKeyForCommand(cmd, mapName) ||
|
|
|
|
extraKeys && findKeyForCommand(cmd, extraKeys);
|
|
|
|
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
|
|
|
if (el.title !== newTitle) el.title = newTitle;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-03-24 14:08:04 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
//#endregion
|
|
|
|
//#region init windowed mode
|
|
|
|
|
|
|
|
(() => {
|
|
|
|
let ownTabId;
|
|
|
|
if (chrome.windows) {
|
|
|
|
initWindowedMode();
|
|
|
|
const pos = tryJSONparse(sessionStore.windowPos);
|
|
|
|
delete sessionStore.windowPos;
|
|
|
|
// resize the window on 'undo close'
|
|
|
|
if (pos && pos.left != null) {
|
|
|
|
chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-29 19:57:22 +00:00
|
|
|
getOwnTab().then(tab => {
|
2021-01-01 14:27:58 +00:00
|
|
|
ownTabId = tab.id;
|
|
|
|
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
|
2021-12-29 19:57:22 +00:00
|
|
|
editor.cancel = () => history.back();
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
async function initWindowedMode() {
|
|
|
|
chrome.tabs.onAttached.addListener(onTabAttached);
|
|
|
|
const isSimple = (await browser.windows.getCurrent()).type === 'popup';
|
|
|
|
if (isSimple) require(['/edit/embedded-popup']);
|
|
|
|
editor.isWindowed = isSimple || (
|
|
|
|
history.length === 1 &&
|
|
|
|
await prefs.ready && prefs.get('openEditInWindow') &&
|
|
|
|
(await browser.windows.getAll()).length > 1 &&
|
|
|
|
(await browser.tabs.query({currentWindow: true})).length === 1
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onTabAttached(tabId, info) {
|
|
|
|
if (tabId !== ownTabId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (info.newPosition !== 0) {
|
|
|
|
prefs.set('openEditInWindow', false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const win = await browser.windows.get(info.newWindowId, {populate: true});
|
|
|
|
// If there's only one tab in this window, it's been dragged to new window
|
|
|
|
const openEditInWindow = win.tabs.length === 1;
|
|
|
|
// FF-only because Chrome retardedly resets the size during dragging
|
|
|
|
if (openEditInWindow && FIREFOX) {
|
|
|
|
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
|
|
|
|
}
|
|
|
|
prefs.set('openEditInWindow', openEditInWindow);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
//#region internals
|
|
|
|
|
|
|
|
/** @returns DirtyReporter */
|
|
|
|
function DirtyReporter() {
|
|
|
|
const data = new Map();
|
|
|
|
const listeners = new Set();
|
2022-01-23 09:44:25 +00:00
|
|
|
const dataListeners = new Set();
|
2021-01-01 14:27:58 +00:00
|
|
|
const notifyChange = wasDirty => {
|
2022-01-23 09:44:25 +00:00
|
|
|
const isDirty = data.size > 0;
|
|
|
|
const flipped = isDirty !== wasDirty;
|
|
|
|
if (flipped) {
|
|
|
|
listeners.forEach(cb => cb(isDirty));
|
|
|
|
}
|
|
|
|
if (flipped || isDirty) {
|
|
|
|
dataListeners.forEach(cb => cb(isDirty));
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
/** @namespace DirtyReporter */
|
|
|
|
return {
|
|
|
|
add(obj, value) {
|
|
|
|
const wasDirty = data.size > 0;
|
|
|
|
const saved = data.get(obj);
|
|
|
|
if (!saved) {
|
|
|
|
data.set(obj, {type: 'add', newValue: value});
|
|
|
|
} else if (saved.type === 'remove') {
|
|
|
|
if (saved.savedValue === value) {
|
|
|
|
data.delete(obj);
|
|
|
|
} else {
|
|
|
|
saved.newValue = value;
|
|
|
|
saved.type = 'modify';
|
|
|
|
}
|
2022-01-23 09:44:25 +00:00
|
|
|
} else {
|
|
|
|
return;
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
notifyChange(wasDirty);
|
|
|
|
},
|
2022-01-23 09:44:25 +00:00
|
|
|
clear(...objs) {
|
|
|
|
if (data.size && (
|
|
|
|
objs.length
|
|
|
|
? objs.map(data.delete, data).includes(true)
|
|
|
|
: (data.clear(), true)
|
|
|
|
)) {
|
|
|
|
notifyChange(true);
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
has(key) {
|
|
|
|
return data.has(key);
|
|
|
|
},
|
|
|
|
isDirty() {
|
|
|
|
return data.size > 0;
|
|
|
|
},
|
|
|
|
modify(obj, oldValue, newValue) {
|
|
|
|
const wasDirty = data.size > 0;
|
|
|
|
const saved = data.get(obj);
|
|
|
|
if (!saved) {
|
|
|
|
if (oldValue !== newValue) {
|
|
|
|
data.set(obj, {type: 'modify', savedValue: oldValue, newValue});
|
2022-01-23 09:44:25 +00:00
|
|
|
} else {
|
|
|
|
return;
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
} else if (saved.type === 'modify') {
|
|
|
|
if (saved.savedValue === newValue) {
|
|
|
|
data.delete(obj);
|
|
|
|
} else {
|
|
|
|
saved.newValue = newValue;
|
|
|
|
}
|
|
|
|
} else if (saved.type === 'add') {
|
|
|
|
saved.newValue = newValue;
|
2022-01-23 09:44:25 +00:00
|
|
|
} else {
|
|
|
|
return;
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
notifyChange(wasDirty);
|
|
|
|
},
|
|
|
|
onChange(cb, add = true) {
|
|
|
|
listeners[add ? 'add' : 'delete'](cb);
|
|
|
|
},
|
2022-01-23 09:44:25 +00:00
|
|
|
onDataChange(cb, add = true) {
|
|
|
|
dataListeners[add ? 'add' : 'delete'](cb);
|
|
|
|
},
|
2021-01-01 14:27:58 +00:00
|
|
|
remove(obj, value) {
|
|
|
|
const wasDirty = data.size > 0;
|
|
|
|
const saved = data.get(obj);
|
|
|
|
if (!saved) {
|
|
|
|
data.set(obj, {type: 'remove', savedValue: value});
|
|
|
|
} else if (saved.type === 'add') {
|
|
|
|
data.delete(obj);
|
|
|
|
} else if (saved.type === 'modify') {
|
|
|
|
saved.type = 'remove';
|
2022-01-23 09:44:25 +00:00
|
|
|
} else {
|
|
|
|
return;
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
notifyChange(wasDirty);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-03-24 14:08:04 +00:00
|
|
|
function LivePreview() {
|
|
|
|
let data;
|
|
|
|
let port;
|
|
|
|
let preprocess;
|
|
|
|
let enabled = prefs.get('editor.livePreview');
|
|
|
|
|
2022-03-27 15:47:28 +00:00
|
|
|
const el = $('#preview-errors');
|
|
|
|
el.onclick = () => messageBoxProxy.alert(el.title, 'pre');
|
|
|
|
|
2022-03-24 14:08:04 +00:00
|
|
|
prefs.subscribe('editor.livePreview', (key, value) => {
|
|
|
|
if (!value) {
|
|
|
|
if (port) {
|
|
|
|
port.disconnect();
|
|
|
|
port = null;
|
|
|
|
}
|
|
|
|
} else if (data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
|
|
|
|
createPreviewer();
|
|
|
|
updatePreviewer(data);
|
|
|
|
}
|
|
|
|
enabled = value;
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Function} [fn] - preprocessor
|
|
|
|
*/
|
|
|
|
init(fn) {
|
|
|
|
preprocess = fn;
|
|
|
|
},
|
|
|
|
|
|
|
|
update(newData) {
|
|
|
|
data = newData;
|
|
|
|
if (!port) {
|
|
|
|
if (!data.id || !data.enabled || !enabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
createPreviewer();
|
|
|
|
}
|
|
|
|
updatePreviewer(data);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
function createPreviewer() {
|
|
|
|
port = chrome.runtime.connect({name: 'livePreview'});
|
|
|
|
port.onDisconnect.addListener(err => {
|
|
|
|
throw err;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function updatePreviewer(data) {
|
|
|
|
try {
|
|
|
|
port.postMessage(preprocess ? await preprocess(data) : data);
|
2022-03-27 15:47:28 +00:00
|
|
|
el.hidden = true;
|
2022-03-24 14:08:04 +00:00
|
|
|
} catch (err) {
|
|
|
|
if (Array.isArray(err)) {
|
|
|
|
err = err.join('\n');
|
|
|
|
} else if (err && err.index != null) {
|
|
|
|
// FIXME: this would fail if editors[0].getValue() !== data.sourceCode
|
|
|
|
const pos = editor.getEditors()[0].posFromIndex(err.index);
|
|
|
|
err.message = `${pos.line}:${pos.ch} ${err.message || err}`;
|
|
|
|
}
|
2022-03-27 15:47:28 +00:00
|
|
|
el.title = err.message || `${err}`;
|
|
|
|
el.hidden = false;
|
2022-03-24 14:08:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
//#endregion
|