2020-11-08 08:12:42 +00:00
|
|
|
/* global
|
|
|
|
$
|
|
|
|
$$
|
|
|
|
$create
|
|
|
|
API
|
|
|
|
clipString
|
|
|
|
closeCurrentTab
|
|
|
|
CodeMirror
|
|
|
|
CODEMIRROR_THEMES
|
|
|
|
debounce
|
|
|
|
deepEqual
|
|
|
|
DirtyReporter
|
|
|
|
DocFuncMapper
|
|
|
|
FIREFOX
|
|
|
|
getOwnTab
|
|
|
|
initBeautifyButton
|
|
|
|
linter
|
|
|
|
messageBox
|
|
|
|
moveFocus
|
|
|
|
msg
|
|
|
|
onDOMready
|
|
|
|
prefs
|
|
|
|
rerouteHotkeys
|
|
|
|
SectionsEditor
|
2020-11-18 11:17:15 +00:00
|
|
|
sessionStore
|
2020-11-08 08:12:42 +00:00
|
|
|
setupLivePrefs
|
|
|
|
SourceEditor
|
|
|
|
t
|
|
|
|
tryCatch
|
|
|
|
tryJSONparse
|
|
|
|
*/
|
2017-07-12 20:44:59 +00:00
|
|
|
'use strict';
|
2015-03-14 11:51:41 +00:00
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
/** @type {EditorBase|SourceEditor|SectionsEditor} */
|
|
|
|
const editor = {
|
|
|
|
isUsercss: false,
|
|
|
|
previewDelay: 200, // Chrome devtools uses 200
|
|
|
|
};
|
2020-10-26 12:36:18 +00:00
|
|
|
let isWindowed;
|
2020-11-08 08:12:42 +00:00
|
|
|
let headerHeight;
|
linter and compact layout improvements (#749)
* linter and compact layout improvements
Closes #748
While investigating the best way to fix linter scrolling, when I double-checked the compact layout, an old bug I meant to fix a long time ago was immediately apparent. Basically, the linter adds/removes errors as you type, causing the editor to bounce up and down, making it practically unusable.
This PR fixes scrolling, and also collapses options and the linter in the compact layout, but always shows the collapsed linter so you're aware of the error count without the content jumping. It also collapses options in the non-compact layout if the viewport is too short to accommodate them, factoring in the min-height of the linter. All automatic collapsing factors in whether a linter is active so they can adjust accordingly, and disables the setting of collapsed state prefs, since we're deciding the pref anyway, and this allows for re-expanding on resize based on the previous pref.
It's quite possible I failed to account for certain scenarios, so try to break it. Also think it's problematic for the linter to not always be visible if enabled, so I hooked up a 40px fixed header on scroll with just the linter in it for the compact layout.
A few other little details are included. I removed redundant line and column numbers spelled out at the end of lint messages to prevent horizontal overflow. I noticed that the expand/collapse prefs do not toggle correctly when clicking directly on the details-marker arrow. Simplest solution was covering them with the `h2` (we may wanna hook up the manager as well). Also, unrelated, but I switched to opacity to hide resizing sectioned editors, because `visibility: hidden;` breaks editor auto-focus.
If either of you guys wanna fix any bugs, or improve any code, feel free to just commit to this PR directly.
* linter and compact layout improvements
* linter and compact layout improvements
* No usercss scroll listener and delay header check
* Some code tweaks
2019-08-04 17:09:50 +00:00
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
window.on('beforeunload', beforeUnload);
|
2018-11-07 06:09:29 +00:00
|
|
|
msg.onExtension(onRuntimeMessage);
|
2017-12-02 20:41:15 +00:00
|
|
|
|
2020-10-13 18:19:12 +00:00
|
|
|
lazyInit();
|
2017-12-07 17:26:41 +00:00
|
|
|
|
2020-10-11 15:12:06 +00:00
|
|
|
(async function init() {
|
2020-11-08 08:12:42 +00:00
|
|
|
let style;
|
|
|
|
let nameTarget;
|
|
|
|
let wasDirty = false;
|
|
|
|
const dirty = new DirtyReporter();
|
|
|
|
await Promise.all([
|
|
|
|
initStyle(),
|
|
|
|
prefs.initializing
|
|
|
|
.then(initTheme),
|
2020-10-11 15:12:06 +00:00
|
|
|
onDOMready(),
|
|
|
|
]);
|
2020-11-18 11:17:15 +00:00
|
|
|
const scrollInfo = style.id && tryJSONparse(sessionStore['editorScrollInfo' + style.id]);
|
2020-11-08 08:12:42 +00:00
|
|
|
/** @namespace EditorBase */
|
|
|
|
Object.assign(editor, {
|
|
|
|
style,
|
|
|
|
dirty,
|
2020-11-18 11:17:15 +00:00
|
|
|
scrollInfo,
|
2020-11-08 08:12:42 +00:00
|
|
|
updateName,
|
|
|
|
updateToc,
|
|
|
|
toggleStyle,
|
2020-11-18 11:17:15 +00:00
|
|
|
applyScrollInfo(cm, si = ((scrollInfo || {}).cms || [])[0]) {
|
|
|
|
if (si && si.sel) {
|
|
|
|
cm.operation(() => {
|
|
|
|
cm.setSelections(...si.sel, {scroll: false});
|
|
|
|
cm.scrollIntoView(cm.getCursor(), si.parentHeight / 2);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
2020-11-08 08:12:42 +00:00
|
|
|
});
|
|
|
|
prefs.subscribe('editor.linter', updateLinter);
|
|
|
|
prefs.subscribe('editor.keyMap', showHotkeyInTooltip);
|
|
|
|
window.on('showHotkeyInTooltip', showHotkeyInTooltip);
|
2020-10-11 15:12:06 +00:00
|
|
|
showHotkeyInTooltip();
|
|
|
|
buildThemeElement();
|
|
|
|
buildKeymapElement();
|
|
|
|
setupLivePrefs();
|
2020-10-13 18:19:12 +00:00
|
|
|
initNameArea();
|
2020-10-11 15:12:06 +00:00
|
|
|
initBeautifyButton($('#beautify'), () => editor.getEditors());
|
|
|
|
initResizeListener();
|
|
|
|
detectLayout();
|
|
|
|
|
|
|
|
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
|
|
|
$('#preview-label').classList.toggle('hidden', !style.id);
|
2020-11-08 08:12:42 +00:00
|
|
|
const toc = [];
|
|
|
|
const elToc = $('#toc');
|
|
|
|
elToc.onclick = e => editor.jumpToEditor([...elToc.children].indexOf(e.target));
|
2020-11-18 11:17:15 +00:00
|
|
|
if (editor.isUsercss) {
|
|
|
|
SourceEditor();
|
|
|
|
} else {
|
|
|
|
SectionsEditor();
|
|
|
|
}
|
2020-11-08 08:12:42 +00:00
|
|
|
prefs.subscribe('editor.toc.expanded', (k, val) => val && editor.updateToc(), {now: true});
|
2020-10-11 15:12:06 +00:00
|
|
|
dirty.onChange(updateDirty);
|
2020-11-18 11:17:15 +00:00
|
|
|
|
2020-10-11 15:12:06 +00:00
|
|
|
await editor.ready;
|
2020-11-18 11:17:15 +00:00
|
|
|
editor.ready = true;
|
2017-04-20 14:00:43 +00:00
|
|
|
|
2020-11-18 11:17:15 +00:00
|
|
|
setTimeout(() => editor.getEditors().forEach(linter.enableForEditor));
|
2020-10-11 15:12:06 +00:00
|
|
|
// enabling after init to prevent flash of validation failure on an empty name
|
2020-11-08 08:12:42 +00:00
|
|
|
$('#name').required = !editor.isUsercss;
|
2020-10-11 15:12:06 +00:00
|
|
|
$('#save-button').onclick = editor.save;
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
async function initStyle() {
|
|
|
|
const params = new URLSearchParams(location.search);
|
|
|
|
const id = Number(params.get('id'));
|
|
|
|
style = id ? await API.getStyle(id) : initEmptyStyle(params);
|
|
|
|
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
|
|
|
|
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
|
|
|
|
document.documentElement.classList.toggle('usercss', editor.isUsercss);
|
2020-11-18 11:17:15 +00:00
|
|
|
sessionStore.justEditedStyleId = style.id || '';
|
2020-11-08 08:12:42 +00:00
|
|
|
// no such style so let's clear the invalid URL parameters
|
|
|
|
if (!style.id) history.replaceState({}, '', location.pathname);
|
|
|
|
updateTitle(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
function initEmptyStyle(params) {
|
|
|
|
return {
|
|
|
|
name: params.get('domain') ||
|
|
|
|
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
|
|
|
|
'',
|
|
|
|
enabled: true,
|
|
|
|
sections: [
|
|
|
|
DocFuncMapper.toSection([...params], {code: ''}),
|
|
|
|
],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-10-13 18:19:12 +00:00
|
|
|
function initNameArea() {
|
2020-10-11 15:12:06 +00:00
|
|
|
const nameEl = $('#name');
|
|
|
|
const resetEl = $('#reset-name');
|
2020-11-08 08:12:42 +00:00
|
|
|
const isCustomName = style.updateUrl || editor.isUsercss;
|
2020-10-11 15:12:06 +00:00
|
|
|
nameTarget = isCustomName ? 'customName' : 'name';
|
2020-11-08 08:12:42 +00:00
|
|
|
nameEl.placeholder = t(editor.isUsercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
2020-10-11 15:12:06 +00:00
|
|
|
nameEl.title = isCustomName ? t('customNameHint') : '';
|
2020-11-08 08:12:42 +00:00
|
|
|
nameEl.on('input', () => {
|
2020-10-22 20:48:17 +00:00
|
|
|
updateName(true);
|
2020-10-11 15:12:06 +00:00
|
|
|
resetEl.hidden = false;
|
2018-11-07 06:09:29 +00:00
|
|
|
});
|
2020-10-11 15:12:06 +00:00
|
|
|
resetEl.hidden = !style.customName;
|
|
|
|
resetEl.onclick = () => {
|
|
|
|
const style = editor.style;
|
|
|
|
nameEl.focus();
|
|
|
|
nameEl.select();
|
|
|
|
// trying to make it undoable via Ctrl-Z
|
|
|
|
if (!document.execCommand('insertText', false, style.name)) {
|
|
|
|
nameEl.value = style.name;
|
2020-10-22 20:48:17 +00:00
|
|
|
updateName(true);
|
2020-10-11 15:12:06 +00:00
|
|
|
}
|
|
|
|
style.customName = null; // to delete it from db
|
|
|
|
resetEl.hidden = true;
|
|
|
|
};
|
|
|
|
const enabledEl = $('#enabled');
|
|
|
|
enabledEl.onchange = () => updateEnabledness(enabledEl.checked);
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
function initResizeListener() {
|
|
|
|
const {onBoundsChanged} = chrome.windows || {};
|
|
|
|
if (onBoundsChanged) {
|
|
|
|
// * movement is reported even if the window wasn't resized
|
|
|
|
// * fired just once when done so debounce is not needed
|
2020-11-11 11:53:40 +00:00
|
|
|
onBoundsChanged.addListener(async wnd => {
|
2020-11-08 08:12:42 +00:00
|
|
|
// getting the current window id as it may change if the user attached/detached the tab
|
2020-11-11 11:53:40 +00:00
|
|
|
const {id} = await browser.windows.getCurrent();
|
|
|
|
if (id === wnd.id) saveWindowPos();
|
2020-11-08 08:12:42 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
window.on('resize', () => {
|
|
|
|
if (!onBoundsChanged) debounce(saveWindowPos, 100);
|
|
|
|
detectLayout();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function initTheme() {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
const theme = prefs.get('editor.theme');
|
|
|
|
const el = $('#cm-theme');
|
|
|
|
if (theme === 'default') {
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
// preload the theme so CodeMirror can use the correct metrics
|
|
|
|
el.href = `vendor/codemirror/theme/${theme}.css`;
|
|
|
|
el.on('load', resolve, {once: true});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-07 06:09:29 +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 buildThemeElement() {
|
2020-10-11 15:12:06 +00:00
|
|
|
CODEMIRROR_THEMES.unshift(chrome.i18n.getMessage('defaultTheme'));
|
|
|
|
$('#editor.theme').append(...CODEMIRROR_THEMES.map(s => $create('option', s)));
|
2020-10-13 18:19:12 +00:00
|
|
|
// move the theme after built-in CSS so that its same-specificity selectors win
|
|
|
|
document.head.appendChild($('#cm-theme'));
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function buildKeymapElement() {
|
|
|
|
// 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;
|
|
|
|
});
|
|
|
|
$('#editor.keyMap').appendChild(fragment);
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-11 15:12:06 +00:00
|
|
|
function toggleStyle() {
|
|
|
|
$('#enabled').checked = !style.enabled;
|
|
|
|
updateEnabledness(!style.enabled);
|
2018-11-28 04:54:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function updateDirty() {
|
2020-10-11 15:12:06 +00:00
|
|
|
const isDirty = dirty.isDirty();
|
|
|
|
if (wasDirty !== isDirty) {
|
|
|
|
wasDirty = isDirty;
|
|
|
|
document.body.classList.toggle('dirty', isDirty);
|
|
|
|
$('#save-button').disabled = !isDirty;
|
|
|
|
}
|
2018-11-28 04:54:36 +00:00
|
|
|
updateTitle();
|
|
|
|
}
|
2020-10-11 15:12:06 +00:00
|
|
|
|
|
|
|
function updateEnabledness(enabled) {
|
|
|
|
dirty.modify('enabled', style.enabled, enabled);
|
|
|
|
style.enabled = enabled;
|
|
|
|
editor.updateLivePreview();
|
|
|
|
}
|
|
|
|
|
2020-10-22 20:48:17 +00:00
|
|
|
function updateName(isUserInput) {
|
2020-10-11 15:12:06 +00:00
|
|
|
if (!editor) return;
|
2020-10-22 20:48:17 +00:00
|
|
|
if (isUserInput) {
|
|
|
|
const {value} = $('#name');
|
|
|
|
dirty.modify('name', style[nameTarget] || style.name, value);
|
|
|
|
style[nameTarget] = value;
|
|
|
|
}
|
2020-11-08 08:12:42 +00:00
|
|
|
updateTitle();
|
2020-10-11 15:12:06 +00:00
|
|
|
}
|
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
function updateTitle(isDirty = dirty.isDirty()) {
|
|
|
|
document.title = `${isDirty ? '* ' : ''}${style.customName || style.name}`;
|
2020-10-11 15:12:06 +00:00
|
|
|
}
|
2020-10-13 18:19:12 +00:00
|
|
|
|
|
|
|
function updateLinter(key, value) {
|
|
|
|
$('body').classList.toggle('linter-disabled', value === '');
|
|
|
|
linter.run();
|
|
|
|
}
|
2020-11-08 08:12:42 +00:00
|
|
|
|
|
|
|
function updateToc(added = editor.sections) {
|
|
|
|
const {sections} = editor;
|
|
|
|
const first = sections.indexOf(added[0]);
|
2020-11-18 11:17:15 +00:00
|
|
|
const elFirst = elToc.children[first];
|
|
|
|
if (first >= 0 && (!added.focus || !elFirst)) {
|
|
|
|
for (let el = elFirst, i = first; i < sections.length; i++) {
|
2020-11-08 08:12:42 +00:00
|
|
|
const entry = sections[i].tocEntry;
|
|
|
|
if (!deepEqual(entry, toc[i])) {
|
|
|
|
if (!el) el = elToc.appendChild($create('li', {tabIndex: 0}));
|
|
|
|
el.tabIndex = entry.removed ? -1 : 0;
|
|
|
|
toc[i] = Object.assign({}, entry);
|
|
|
|
const s = el.textContent = clipString(entry.label) || (
|
|
|
|
entry.target == null
|
|
|
|
? t('appliesToEverything')
|
|
|
|
: clipString(entry.target) + (entry.numTargets > 1 ? ', ...' : ''));
|
|
|
|
if (s.length > 30) el.title = s;
|
|
|
|
}
|
|
|
|
el = el.nextElementSibling;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
while (toc.length > sections.length) {
|
|
|
|
elToc.lastElementChild.remove();
|
|
|
|
toc.length--;
|
|
|
|
}
|
2020-11-18 11:17:15 +00:00
|
|
|
if (added.focus) {
|
|
|
|
const cls = 'current';
|
|
|
|
const old = $('.' + cls, elToc);
|
|
|
|
const el = elFirst || elToc.children[first];
|
|
|
|
if (old && old !== el) old.classList.remove(cls);
|
|
|
|
el.classList.add(cls);
|
|
|
|
}
|
2020-11-08 08:12:42 +00:00
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
})();
|
|
|
|
|
2020-10-13 18:19:12 +00:00
|
|
|
/* Stuff not needed for the main init so we can let it run at its own tempo */
|
2020-10-26 12:36:18 +00:00
|
|
|
function lazyInit() {
|
|
|
|
let ownTabId;
|
2020-11-11 11:53:40 +00:00
|
|
|
// not using `await` so we don't block the subsequent code
|
|
|
|
getOwnTab().then(patchHistoryBack);
|
|
|
|
// no windows on android
|
|
|
|
if (chrome.windows) {
|
|
|
|
restoreWindowSize();
|
|
|
|
detectWindowedState();
|
|
|
|
chrome.tabs.onAttached.addListener(onAttached);
|
|
|
|
}
|
|
|
|
async function patchHistoryBack(tab) {
|
2020-10-26 12:36:18 +00:00
|
|
|
ownTabId = tab.id;
|
|
|
|
// use browser history back when 'back to manage' is clicked
|
2020-11-18 11:17:15 +00:00
|
|
|
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
|
2020-10-26 12:36:18 +00:00
|
|
|
await onDOMready();
|
2020-10-13 18:19:12 +00:00
|
|
|
$('#cancel-button').onclick = event => {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
history.back();
|
|
|
|
};
|
2020-10-26 12:36:18 +00:00
|
|
|
}
|
2020-10-13 18:19:12 +00:00
|
|
|
}
|
2020-11-11 11:53:40 +00:00
|
|
|
/** resize on 'undo close' */
|
|
|
|
function restoreWindowSize() {
|
2020-11-18 11:17:15 +00:00
|
|
|
const pos = tryJSONparse(sessionStore.windowPos);
|
|
|
|
delete sessionStore.windowPos;
|
2020-11-11 11:53:40 +00:00
|
|
|
if (pos && pos.left != null && chrome.windows) {
|
|
|
|
chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
|
|
|
|
}
|
2020-10-26 12:36:18 +00:00
|
|
|
}
|
2020-11-11 11:53:40 +00:00
|
|
|
async function detectWindowedState() {
|
|
|
|
isWindowed =
|
|
|
|
prefs.get('openEditInWindow') &&
|
|
|
|
history.length === 1 &&
|
|
|
|
browser.windows.getAll().length > 1 &&
|
|
|
|
(await browser.tabs.query({currentWindow: true})).length === 1;
|
2020-10-13 18:19:12 +00:00
|
|
|
}
|
2020-11-11 11:53:40 +00:00
|
|
|
async function onAttached(tabId, info) {
|
2020-10-13 18:19:12 +00:00
|
|
|
if (tabId !== ownTabId) {
|
2017-09-12 19:45:33 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-10-13 18:19:12 +00:00
|
|
|
if (info.newPosition !== 0) {
|
|
|
|
prefs.set('openEditInWindow', false);
|
|
|
|
return;
|
|
|
|
}
|
2020-11-11 11:53:40 +00:00
|
|
|
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);
|
|
|
|
}
|
2015-03-08 06:21:43 +00:00
|
|
|
}
|
|
|
|
|
2017-12-02 20:41:15 +00:00
|
|
|
function onRuntimeMessage(request) {
|
|
|
|
switch (request.method) {
|
|
|
|
case 'styleUpdated':
|
2018-11-07 06:09:29 +00:00
|
|
|
if (
|
2020-10-11 15:12:06 +00:00
|
|
|
editor.style.id === request.style.id &&
|
2018-11-07 06:09:29 +00:00
|
|
|
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
|
|
|
|
.includes(request.reason)
|
|
|
|
) {
|
|
|
|
Promise.resolve(
|
|
|
|
request.codeIsUpdated === false ?
|
|
|
|
request.style : API.getStyle(request.style.id)
|
|
|
|
)
|
|
|
|
.then(newStyle => {
|
|
|
|
editor.replaceStyle(newStyle, request.codeIsUpdated);
|
|
|
|
});
|
2017-07-12 19:50:13 +00:00
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
break;
|
|
|
|
case 'styleDeleted':
|
2020-10-11 15:12:06 +00:00
|
|
|
if (editor.style.id === request.style.id) {
|
2017-12-02 20:41:15 +00:00
|
|
|
closeCurrentTab();
|
2017-07-12 19:50:13 +00:00
|
|
|
break;
|
|
|
|
}
|
2017-08-15 19:13:58 +00:00
|
|
|
break;
|
2017-12-02 20:41:15 +00:00
|
|
|
case 'editDeleteText':
|
|
|
|
document.execCommand('delete');
|
|
|
|
break;
|
2017-07-12 19:50:13 +00:00
|
|
|
}
|
2017-08-27 11:56:04 +00:00
|
|
|
}
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
function beforeUnload(e) {
|
2020-11-18 11:17:15 +00:00
|
|
|
sessionStore.windowPos = JSON.stringify(canSaveWindowPos() && prefs.get('windowPosition'));
|
|
|
|
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify({
|
|
|
|
scrollY: window.scrollY,
|
|
|
|
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
|
|
|
|
focus: cm.hasFocus(),
|
|
|
|
height: cm.display.wrapper.style.height.replace('100vh', ''),
|
|
|
|
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
|
|
|
|
sel: cm.isClean() && [cm.doc.sel.ranges, cm.doc.sel.primIndex],
|
|
|
|
})),
|
|
|
|
});
|
2018-08-02 17:54:40 +00:00
|
|
|
const activeElement = document.activeElement;
|
|
|
|
if (activeElement) {
|
|
|
|
// blurring triggers 'change' or 'input' event if needed
|
|
|
|
activeElement.blur();
|
|
|
|
// refocus if unloading was canceled
|
|
|
|
setTimeout(() => activeElement.focus());
|
2017-07-12 19:50:13 +00:00
|
|
|
}
|
2018-11-28 04:54:36 +00:00
|
|
|
if (editor && editor.dirty.isDirty()) {
|
2018-08-02 17:54:40 +00:00
|
|
|
// neither confirm() nor custom messages work in modern browsers but just in case
|
2018-11-07 06:09:29 +00:00
|
|
|
e.returnValue = t('styleChangesNotSaved');
|
2017-10-08 16:43:00 +00:00
|
|
|
}
|
2015-10-29 18:20:05 +00:00
|
|
|
}
|
|
|
|
|
2017-11-28 17:03:50 +00:00
|
|
|
function showHelp(title = '', body) {
|
2017-07-19 12:09:29 +00:00
|
|
|
const div = $('#help-popup');
|
2017-12-04 16:14:04 +00:00
|
|
|
div.className = '';
|
2017-12-13 04:33:16 +00:00
|
|
|
|
2017-11-28 17:03:50 +00:00
|
|
|
const contents = $('.contents', div);
|
|
|
|
contents.textContent = '';
|
|
|
|
if (body) {
|
2020-11-18 11:17:15 +00:00
|
|
|
contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
|
2017-11-28 17:03:50 +00:00
|
|
|
}
|
2017-07-12 19:50:13 +00:00
|
|
|
|
2017-12-13 04:33:16 +00:00
|
|
|
$('.title', div).textContent = title;
|
2017-07-12 19:50:13 +00:00
|
|
|
|
2017-12-13 04:33:16 +00:00
|
|
|
showHelp.close = showHelp.close || (event => {
|
2017-12-02 21:22:03 +00:00
|
|
|
const canClose =
|
|
|
|
!event ||
|
|
|
|
event.type === 'click' ||
|
|
|
|
(
|
2020-10-13 18:14:54 +00:00
|
|
|
event.key === 'Escape' &&
|
2017-12-02 21:22:03 +00:00
|
|
|
!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey &&
|
|
|
|
!$('.CodeMirror-hints, #message-box') &&
|
|
|
|
(
|
|
|
|
!document.activeElement ||
|
2018-01-01 07:34:16 +00:00
|
|
|
!document.activeElement.closest('#search-replace-dialog') &&
|
2017-12-02 21:22:03 +00:00
|
|
|
document.activeElement.matches(':not(input), .can-close-on-esc')
|
|
|
|
)
|
|
|
|
);
|
|
|
|
if (!canClose) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (event && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
|
2017-12-13 04:33:16 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
messageBox.confirm(t('confirmDiscardChanges'))
|
|
|
|
.then(ok => ok && showHelp.close());
|
|
|
|
});
|
2017-12-02 21:22:03 +00:00
|
|
|
return;
|
2017-07-12 19:50:13 +00:00
|
|
|
}
|
2017-12-13 04:33:16 +00:00
|
|
|
if (div.contains(document.activeElement) && showHelp.originalFocus) {
|
|
|
|
showHelp.originalFocus.focus();
|
|
|
|
}
|
2017-12-02 21:22:03 +00:00
|
|
|
div.style.display = '';
|
|
|
|
contents.textContent = '';
|
|
|
|
clearTimeout(contents.timer);
|
2020-11-08 08:12:42 +00:00
|
|
|
window.off('keydown', showHelp.close, true);
|
2017-12-02 21:22:03 +00:00
|
|
|
window.dispatchEvent(new Event('closeHelp'));
|
2017-12-13 04:33:16 +00:00
|
|
|
});
|
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
window.on('keydown', showHelp.close, true);
|
2018-08-07 17:11:11 +00:00
|
|
|
$('.dismiss', div).onclick = showHelp.close;
|
2017-12-13 04:33:16 +00:00
|
|
|
|
|
|
|
// reset any inline styles
|
|
|
|
div.style = 'display: block';
|
|
|
|
|
|
|
|
showHelp.originalFocus = document.activeElement;
|
|
|
|
return div;
|
2015-01-30 17:05:06 +00:00
|
|
|
}
|
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
/* exported showCodeMirrorPopup */
|
2015-07-13 17:44:46 +00:00
|
|
|
function showCodeMirrorPopup(title, html, options) {
|
2017-07-12 20:44:59 +00:00
|
|
|
const popup = showHelp(title, html);
|
|
|
|
popup.classList.add('big');
|
2017-07-12 19:50:13 +00:00
|
|
|
|
2017-12-09 15:23:18 +00:00
|
|
|
let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
|
2017-07-12 20:44:59 +00:00
|
|
|
mode: 'css',
|
2017-07-12 19:50:13 +00:00
|
|
|
lineNumbers: true,
|
2018-07-22 17:08:13 +00:00
|
|
|
lineWrapping: prefs.get('editor.lineWrapping'),
|
2017-07-12 19:50:13 +00:00
|
|
|
foldGutter: true,
|
2017-07-12 20:44:59 +00:00
|
|
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
2017-07-12 19:50:13 +00:00
|
|
|
matchBrackets: true,
|
|
|
|
styleActiveLine: true,
|
2017-07-12 20:44:59 +00:00
|
|
|
theme: prefs.get('editor.theme'),
|
2020-11-18 11:17:15 +00:00
|
|
|
keyMap: prefs.get('editor.keyMap'),
|
2017-07-12 19:50:13 +00:00
|
|
|
}, options));
|
2017-12-02 20:41:15 +00:00
|
|
|
cm.focus();
|
2018-11-07 06:09:29 +00:00
|
|
|
rerouteHotkeys(false);
|
2018-07-22 16:37:49 +00:00
|
|
|
|
|
|
|
document.documentElement.style.pointerEvents = 'none';
|
|
|
|
popup.style.pointerEvents = 'auto';
|
|
|
|
|
|
|
|
const onKeyDown = event => {
|
2020-10-13 18:14:54 +00:00
|
|
|
if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
2018-07-22 16:37:49 +00:00
|
|
|
const search = $('#search-replace-dialog');
|
|
|
|
const area = search && search.contains(document.activeElement) ? search : popup;
|
|
|
|
moveFocus(area, event.shiftKey ? -1 : 1);
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
};
|
2020-11-08 08:12:42 +00:00
|
|
|
window.on('keydown', onKeyDown, true);
|
2018-07-22 16:37:49 +00:00
|
|
|
|
2020-11-08 08:12:42 +00:00
|
|
|
window.on('closeHelp', () => {
|
|
|
|
window.off('keydown', onKeyDown, true);
|
2018-07-22 16:37:49 +00:00
|
|
|
document.documentElement.style.removeProperty('pointer-events');
|
2018-11-07 06:09:29 +00:00
|
|
|
rerouteHotkeys(true);
|
2017-12-09 15:23:18 +00:00
|
|
|
cm = popup.codebox = null;
|
2020-10-13 18:14:54 +00:00
|
|
|
}, {once: true});
|
2018-07-22 16:37:49 +00:00
|
|
|
|
2017-07-12 19:50:13 +00:00
|
|
|
return popup;
|
2015-07-13 17:44:46 +00:00
|
|
|
}
|
|
|
|
|
2020-10-26 12:36:18 +00:00
|
|
|
function canSaveWindowPos() {
|
|
|
|
return isWindowed &&
|
2017-12-02 20:41:15 +00:00
|
|
|
document.visibilityState === 'visible' &&
|
|
|
|
prefs.get('openEditInWindow') &&
|
2020-10-26 12:36:18 +00:00
|
|
|
!isWindowMaximized();
|
|
|
|
}
|
|
|
|
|
|
|
|
function saveWindowPos() {
|
|
|
|
if (canSaveWindowPos()) {
|
2017-12-02 20:41:15 +00:00
|
|
|
prefs.set('windowPosition', {
|
|
|
|
left: window.screenX,
|
|
|
|
top: window.screenY,
|
|
|
|
width: window.outerWidth,
|
|
|
|
height: window.outerHeight,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
linter and compact layout improvements (#749)
* linter and compact layout improvements
Closes #748
While investigating the best way to fix linter scrolling, when I double-checked the compact layout, an old bug I meant to fix a long time ago was immediately apparent. Basically, the linter adds/removes errors as you type, causing the editor to bounce up and down, making it practically unusable.
This PR fixes scrolling, and also collapses options and the linter in the compact layout, but always shows the collapsed linter so you're aware of the error count without the content jumping. It also collapses options in the non-compact layout if the viewport is too short to accommodate them, factoring in the min-height of the linter. All automatic collapsing factors in whether a linter is active so they can adjust accordingly, and disables the setting of collapsed state prefs, since we're deciding the pref anyway, and this allows for re-expanding on resize based on the previous pref.
It's quite possible I failed to account for certain scenarios, so try to break it. Also think it's problematic for the linter to not always be visible if enabled, so I hooked up a 40px fixed header on scroll with just the linter in it for the compact layout.
A few other little details are included. I removed redundant line and column numbers spelled out at the end of lint messages to prevent horizontal overflow. I noticed that the expand/collapse prefs do not toggle correctly when clicking directly on the details-marker arrow. Simplest solution was covering them with the `h2` (we may wanna hook up the manager as well). Also, unrelated, but I switched to opacity to hide resizing sectioned editors, because `visibility: hidden;` breaks editor auto-focus.
If either of you guys wanna fix any bugs, or improve any code, feel free to just commit to this PR directly.
* linter and compact layout improvements
* linter and compact layout improvements
* No usercss scroll listener and delay header check
* Some code tweaks
2019-08-04 17:09:50 +00:00
|
|
|
function fixedHeader() {
|
2020-11-08 08:12:42 +00:00
|
|
|
const headerFixed = $('.fixed-header');
|
|
|
|
if (!headerFixed) headerHeight = $('#header').clientHeight;
|
|
|
|
const scrollPoint = headerHeight - 43;
|
|
|
|
if (window.scrollY >= scrollPoint && !headerFixed) {
|
|
|
|
$('body').style.setProperty('--fixed-padding', ` ${headerHeight}px`);
|
linter and compact layout improvements (#749)
* linter and compact layout improvements
Closes #748
While investigating the best way to fix linter scrolling, when I double-checked the compact layout, an old bug I meant to fix a long time ago was immediately apparent. Basically, the linter adds/removes errors as you type, causing the editor to bounce up and down, making it practically unusable.
This PR fixes scrolling, and also collapses options and the linter in the compact layout, but always shows the collapsed linter so you're aware of the error count without the content jumping. It also collapses options in the non-compact layout if the viewport is too short to accommodate them, factoring in the min-height of the linter. All automatic collapsing factors in whether a linter is active so they can adjust accordingly, and disables the setting of collapsed state prefs, since we're deciding the pref anyway, and this allows for re-expanding on resize based on the previous pref.
It's quite possible I failed to account for certain scenarios, so try to break it. Also think it's problematic for the linter to not always be visible if enabled, so I hooked up a 40px fixed header on scroll with just the linter in it for the compact layout.
A few other little details are included. I removed redundant line and column numbers spelled out at the end of lint messages to prevent horizontal overflow. I noticed that the expand/collapse prefs do not toggle correctly when clicking directly on the details-marker arrow. Simplest solution was covering them with the `h2` (we may wanna hook up the manager as well). Also, unrelated, but I switched to opacity to hide resizing sectioned editors, because `visibility: hidden;` breaks editor auto-focus.
If either of you guys wanna fix any bugs, or improve any code, feel free to just commit to this PR directly.
* linter and compact layout improvements
* linter and compact layout improvements
* No usercss scroll listener and delay header check
* Some code tweaks
2019-08-04 17:09:50 +00:00
|
|
|
$('body').classList.add('fixed-header');
|
2020-11-08 08:12:42 +00:00
|
|
|
} else if (window.scrollY < scrollPoint && headerFixed) {
|
linter and compact layout improvements (#749)
* linter and compact layout improvements
Closes #748
While investigating the best way to fix linter scrolling, when I double-checked the compact layout, an old bug I meant to fix a long time ago was immediately apparent. Basically, the linter adds/removes errors as you type, causing the editor to bounce up and down, making it practically unusable.
This PR fixes scrolling, and also collapses options and the linter in the compact layout, but always shows the collapsed linter so you're aware of the error count without the content jumping. It also collapses options in the non-compact layout if the viewport is too short to accommodate them, factoring in the min-height of the linter. All automatic collapsing factors in whether a linter is active so they can adjust accordingly, and disables the setting of collapsed state prefs, since we're deciding the pref anyway, and this allows for re-expanding on resize based on the previous pref.
It's quite possible I failed to account for certain scenarios, so try to break it. Also think it's problematic for the linter to not always be visible if enabled, so I hooked up a 40px fixed header on scroll with just the linter in it for the compact layout.
A few other little details are included. I removed redundant line and column numbers spelled out at the end of lint messages to prevent horizontal overflow. I noticed that the expand/collapse prefs do not toggle correctly when clicking directly on the details-marker arrow. Simplest solution was covering them with the `h2` (we may wanna hook up the manager as well). Also, unrelated, but I switched to opacity to hide resizing sectioned editors, because `visibility: hidden;` breaks editor auto-focus.
If either of you guys wanna fix any bugs, or improve any code, feel free to just commit to this PR directly.
* linter and compact layout improvements
* linter and compact layout improvements
* No usercss scroll listener and delay header check
* Some code tweaks
2019-08-04 17:09:50 +00:00
|
|
|
$('body').classList.remove('fixed-header');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function detectLayout() {
|
|
|
|
const compact = window.innerWidth <= 850;
|
|
|
|
if (compact) {
|
2020-11-08 08:12:42 +00:00
|
|
|
document.body.classList.add('compact-layout');
|
|
|
|
if (!editor.isUsercss) {
|
|
|
|
debounce(fixedHeader, 250);
|
|
|
|
window.on('scroll', fixedHeader, {passive: true});
|
linter and compact layout improvements (#749)
* linter and compact layout improvements
Closes #748
While investigating the best way to fix linter scrolling, when I double-checked the compact layout, an old bug I meant to fix a long time ago was immediately apparent. Basically, the linter adds/removes errors as you type, causing the editor to bounce up and down, making it practically unusable.
This PR fixes scrolling, and also collapses options and the linter in the compact layout, but always shows the collapsed linter so you're aware of the error count without the content jumping. It also collapses options in the non-compact layout if the viewport is too short to accommodate them, factoring in the min-height of the linter. All automatic collapsing factors in whether a linter is active so they can adjust accordingly, and disables the setting of collapsed state prefs, since we're deciding the pref anyway, and this allows for re-expanding on resize based on the previous pref.
It's quite possible I failed to account for certain scenarios, so try to break it. Also think it's problematic for the linter to not always be visible if enabled, so I hooked up a 40px fixed header on scroll with just the linter in it for the compact layout.
A few other little details are included. I removed redundant line and column numbers spelled out at the end of lint messages to prevent horizontal overflow. I noticed that the expand/collapse prefs do not toggle correctly when clicking directly on the details-marker arrow. Simplest solution was covering them with the `h2` (we may wanna hook up the manager as well). Also, unrelated, but I switched to opacity to hide resizing sectioned editors, because `visibility: hidden;` breaks editor auto-focus.
If either of you guys wanna fix any bugs, or improve any code, feel free to just commit to this PR directly.
* linter and compact layout improvements
* linter and compact layout improvements
* No usercss scroll listener and delay header check
* Some code tweaks
2019-08-04 17:09:50 +00:00
|
|
|
}
|
|
|
|
} else {
|
2020-11-08 08:12:42 +00:00
|
|
|
document.body.classList.remove('compact-layout', 'fixed-header');
|
|
|
|
window.off('scroll', fixedHeader);
|
|
|
|
}
|
|
|
|
for (const type of ['options', 'toc', 'lint']) {
|
|
|
|
const el = $(`details[data-pref="editor.${type}.expanded"]`);
|
|
|
|
el.open = compact ? false : prefs.get(el.dataset.pref);
|
linter and compact layout improvements (#749)
* linter and compact layout improvements
Closes #748
While investigating the best way to fix linter scrolling, when I double-checked the compact layout, an old bug I meant to fix a long time ago was immediately apparent. Basically, the linter adds/removes errors as you type, causing the editor to bounce up and down, making it practically unusable.
This PR fixes scrolling, and also collapses options and the linter in the compact layout, but always shows the collapsed linter so you're aware of the error count without the content jumping. It also collapses options in the non-compact layout if the viewport is too short to accommodate them, factoring in the min-height of the linter. All automatic collapsing factors in whether a linter is active so they can adjust accordingly, and disables the setting of collapsed state prefs, since we're deciding the pref anyway, and this allows for re-expanding on resize based on the previous pref.
It's quite possible I failed to account for certain scenarios, so try to break it. Also think it's problematic for the linter to not always be visible if enabled, so I hooked up a 40px fixed header on scroll with just the linter in it for the compact layout.
A few other little details are included. I removed redundant line and column numbers spelled out at the end of lint messages to prevent horizontal overflow. I noticed that the expand/collapse prefs do not toggle correctly when clicking directly on the details-marker arrow. Simplest solution was covering them with the `h2` (we may wanna hook up the manager as well). Also, unrelated, but I switched to opacity to hide resizing sectioned editors, because `visibility: hidden;` breaks editor auto-focus.
If either of you guys wanna fix any bugs, or improve any code, feel free to just commit to this PR directly.
* linter and compact layout improvements
* linter and compact layout improvements
* No usercss scroll listener and delay header check
* Some code tweaks
2019-08-04 17:09:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-02 20:41:15 +00:00
|
|
|
function isWindowMaximized() {
|
|
|
|
return (
|
|
|
|
window.screenX <= 0 &&
|
|
|
|
window.screenY <= 0 &&
|
|
|
|
window.outerWidth >= screen.availWidth &&
|
|
|
|
window.outerHeight >= screen.availHeight &&
|
|
|
|
|
|
|
|
window.screenX > -10 &&
|
|
|
|
window.screenY > -10 &&
|
|
|
|
window.outerWidth < screen.availWidth + 10 &&
|
|
|
|
window.outerHeight < screen.availHeight + 10
|
|
|
|
);
|
|
|
|
}
|