2018-11-07 06:09:29 +00:00
|
|
|
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
|
2020-08-14 12:16:01 +00:00
|
|
|
createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch
|
2020-10-13 18:19:12 +00:00
|
|
|
closeCurrentTab messageBox debounce
|
|
|
|
initBeautifyButton ignoreChromeError dirtyReporter linter
|
2018-11-07 11:33:14 +00:00
|
|
|
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
|
2018-11-07 06:09:29 +00:00
|
|
|
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
|
2017-07-12 20:44:59 +00:00
|
|
|
'use strict';
|
2015-03-14 11:51:41 +00:00
|
|
|
|
2017-07-12 20:44:59 +00:00
|
|
|
let saveSizeOnClose;
|
2015-01-30 17:05:06 +00:00
|
|
|
|
2015-03-21 13:24:45 +00:00
|
|
|
// direct & reverse mapping of @-moz-document keywords and internal property names
|
2017-07-12 20:44:59 +00:00
|
|
|
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
2018-11-07 06:09:29 +00:00
|
|
|
const CssToProperty = Object.entries(propertyToCss)
|
|
|
|
.reduce((o, v) => {
|
|
|
|
o[v[1]] = v[0];
|
|
|
|
return o;
|
|
|
|
}, {});
|
2015-03-21 13:24:45 +00:00
|
|
|
|
2017-09-11 16:09:25 +00:00
|
|
|
let editor;
|
|
|
|
|
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
|
|
|
let scrollPointTimer;
|
|
|
|
|
2018-08-02 17:54:40 +00:00
|
|
|
document.addEventListener('visibilitychange', beforeUnload);
|
2018-11-07 06:09:29 +00:00
|
|
|
window.addEventListener('beforeunload', beforeUnload);
|
|
|
|
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() {
|
|
|
|
const [style] = await Promise.all([
|
|
|
|
initStyleData(),
|
|
|
|
onDOMready(),
|
2020-10-13 18:19:12 +00:00
|
|
|
prefs.initializing.then(() => 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.addEventListener('load', resolve, {once: true});
|
|
|
|
}
|
|
|
|
})),
|
2020-10-11 15:12:06 +00:00
|
|
|
]);
|
|
|
|
const usercss = isUsercss(style);
|
|
|
|
const dirty = dirtyReporter();
|
|
|
|
let wasDirty = false;
|
|
|
|
let nameTarget;
|
2015-03-21 13:24:45 +00:00
|
|
|
|
2020-10-13 18:19:12 +00:00
|
|
|
prefs.subscribe(['editor.linter'], updateLinter);
|
2020-10-11 15:12:06 +00:00
|
|
|
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
|
|
|
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
|
|
|
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();
|
|
|
|
updateTitle();
|
|
|
|
|
|
|
|
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
|
|
|
$('#preview-label').classList.toggle('hidden', !style.id);
|
|
|
|
|
|
|
|
editor = (usercss ? createSourceEditor : createSectionsEditor)({
|
|
|
|
style,
|
|
|
|
dirty,
|
|
|
|
updateName,
|
|
|
|
toggleStyle,
|
2017-12-02 20:41:15 +00:00
|
|
|
});
|
2020-10-11 15:12:06 +00:00
|
|
|
dirty.onChange(updateDirty);
|
|
|
|
await editor.ready;
|
2017-04-20 14:00:43 +00:00
|
|
|
|
2020-10-11 15:12:06 +00:00
|
|
|
// enabling after init to prevent flash of validation failure on an empty name
|
|
|
|
$('#name').required = !usercss;
|
|
|
|
$('#save-button').onclick = editor.save;
|
2018-11-07 06:09:29 +00:00
|
|
|
|
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');
|
|
|
|
const isCustomName = style.updateUrl || usercss;
|
|
|
|
nameTarget = isCustomName ? 'customName' : 'name';
|
|
|
|
nameEl.placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
|
|
|
nameEl.title = isCustomName ? t('customNameHint') : '';
|
|
|
|
nameEl.addEventListener('input', () => {
|
|
|
|
updateName();
|
|
|
|
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;
|
|
|
|
updateName();
|
|
|
|
}
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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 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
|
|
|
|
onBoundsChanged.addListener(wnd => {
|
|
|
|
// getting the current window id as it may change if the user attached/detached the tab
|
|
|
|
chrome.windows.getCurrent(ownWnd => {
|
|
|
|
if (wnd.id === ownWnd.id) rememberWindowSize();
|
2018-11-28 04:54:36 +00:00
|
|
|
});
|
2018-11-07 06:09:29 +00:00
|
|
|
});
|
2020-10-11 15:12:06 +00:00
|
|
|
}
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
if (!onBoundsChanged) debounce(rememberWindowSize, 100);
|
|
|
|
detectLayout();
|
|
|
|
});
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|
2018-11-28 04:54:36 +00:00
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateName() {
|
|
|
|
if (!editor) return;
|
|
|
|
const {value} = $('#name');
|
|
|
|
dirty.modify('name', style[nameTarget] || style.name, value);
|
|
|
|
style[nameTarget] = value;
|
|
|
|
updateTitle({});
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateTitle() {
|
|
|
|
document.title = `${dirty.isDirty() ? '* ' : ''}${style.customName || style.name}`;
|
|
|
|
}
|
2020-10-13 18:19:12 +00:00
|
|
|
|
|
|
|
function updateLinter(key, value) {
|
|
|
|
$('body').classList.toggle('linter-disabled', value === '');
|
|
|
|
linter.run();
|
|
|
|
}
|
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 */
|
|
|
|
async function lazyInit() {
|
|
|
|
const ownTabId = (await getOwnTab()).id;
|
|
|
|
// use browser history back when 'back to manage' is clicked
|
|
|
|
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
|
|
|
|
onDOMready().then(() => {
|
|
|
|
$('#cancel-button').onclick = event => {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
history.back();
|
|
|
|
};
|
2017-12-02 20:41:15 +00:00
|
|
|
});
|
2017-07-12 19:50:13 +00:00
|
|
|
}
|
2020-10-13 18:19:12 +00:00
|
|
|
// no windows on android
|
|
|
|
if (!chrome.windows) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const tabs = await browser.tabs.query({currentWindow: true});
|
|
|
|
const windowId = tabs[0].windowId;
|
|
|
|
if (prefs.get('openEditInWindow')) {
|
|
|
|
if (
|
|
|
|
/true/.test(sessionStorage.saveSizeOnClose) &&
|
|
|
|
'left' in prefs.get('windowPosition', {}) &&
|
|
|
|
!isWindowMaximized()
|
|
|
|
) {
|
|
|
|
// window was reopened via Ctrl-Shift-T etc.
|
|
|
|
chrome.windows.update(windowId, prefs.get('windowPosition'));
|
|
|
|
}
|
|
|
|
if (tabs.length === 1 && window.history.length === 1) {
|
|
|
|
chrome.windows.getAll(windows => {
|
|
|
|
if (windows.length > 1) {
|
|
|
|
sessionStorageHash('saveSizeOnClose').set(windowId, true);
|
|
|
|
saveSizeOnClose = true;
|
|
|
|
}
|
2017-12-08 02:45:27 +00:00
|
|
|
});
|
2020-10-13 18:19:12 +00:00
|
|
|
} else {
|
|
|
|
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
|
2017-12-08 02:45:27 +00:00
|
|
|
}
|
2020-10-13 18:19:12 +00:00
|
|
|
}
|
|
|
|
chrome.tabs.onAttached.addListener((tabId, info) => {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
chrome.windows.get(info.newWindowId, {populate: true}, win => {
|
|
|
|
// If there's only one tab in this window, it's been dragged to new window
|
|
|
|
const openEditInWindow = win.tabs.length === 1;
|
|
|
|
if (openEditInWindow && FIREFOX) {
|
|
|
|
// FF-only because Chrome retardedly resets the size during dragging
|
|
|
|
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
|
2017-09-12 19:45:33 +00:00
|
|
|
}
|
2020-10-13 18:19:12 +00:00
|
|
|
prefs.set('openEditInWindow', openEditInWindow);
|
2017-09-12 19:45:33 +00:00
|
|
|
});
|
2017-12-02 20:41:15 +00:00
|
|
|
});
|
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) {
|
2018-08-02 17:54:40 +00:00
|
|
|
document.removeEventListener('visibilitychange', beforeUnload);
|
2018-11-07 06:09:29 +00:00
|
|
|
document.removeEventListener('beforeunload', beforeUnload);
|
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-08-02 17:54:40 +00:00
|
|
|
/**
|
|
|
|
* Invoked for 'visibilitychange' event by default.
|
|
|
|
* Invoked for 'beforeunload' event when the style is modified and unsaved.
|
|
|
|
* See https://developers.google.com/web/updates/2018/07/page-lifecycle-api#legacy-lifecycle-apis-to-avoid
|
|
|
|
* > Never add a beforeunload listener unconditionally or use it as an end-of-session signal.
|
|
|
|
* > Only add it when a user has unsaved work, and remove it as soon as that work has been saved.
|
|
|
|
*/
|
2018-11-07 06:09:29 +00:00
|
|
|
function beforeUnload(e) {
|
2018-08-02 17:54:40 +00:00
|
|
|
if (saveSizeOnClose) rememberWindowSize();
|
|
|
|
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-12-02 20:41:15 +00:00
|
|
|
function isUsercss(style) {
|
|
|
|
return (
|
|
|
|
style.usercssData ||
|
|
|
|
!style.id && prefs.get('newStyleAsUsercss')
|
|
|
|
);
|
2015-06-23 16:24:53 +00:00
|
|
|
}
|
|
|
|
|
2017-11-26 13:04:03 +00:00
|
|
|
function initStyleData() {
|
2020-10-13 18:14:54 +00:00
|
|
|
const params = new URLSearchParams(location.search);
|
2018-10-02 12:22:18 +00:00
|
|
|
const id = Number(params.get('id'));
|
2017-11-26 13:04:03 +00:00
|
|
|
const createEmptyStyle = () => ({
|
2018-02-14 02:53:35 +00:00
|
|
|
name: params.get('domain') ||
|
|
|
|
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
|
|
|
|
'',
|
2017-11-26 13:04:03 +00:00
|
|
|
enabled: true,
|
|
|
|
sections: [
|
|
|
|
Object.assign({code: ''},
|
|
|
|
...Object.keys(CssToProperty)
|
|
|
|
.map(name => ({
|
|
|
|
[CssToProperty[name]]: params.get(name) && [params.get(name)] || []
|
|
|
|
}))
|
|
|
|
)
|
|
|
|
],
|
2017-09-12 11:47:32 +00:00
|
|
|
});
|
2018-10-02 12:22:18 +00:00
|
|
|
return fetchStyle()
|
|
|
|
.then(style => {
|
2018-11-07 06:09:29 +00:00
|
|
|
if (style.id) sessionStorage.justEditedStyleId = style.id;
|
2017-12-07 17:26:41 +00:00
|
|
|
// we set "usercss" class on <html> when <body> is empty
|
|
|
|
// so there'll be no flickering of the elements that depend on it
|
|
|
|
if (isUsercss(style)) {
|
|
|
|
document.documentElement.classList.add('usercss');
|
|
|
|
}
|
|
|
|
// strip URL parameters when invoked for a non-existent id
|
2018-11-07 06:09:29 +00:00
|
|
|
if (!style.id) {
|
2017-12-07 17:26:41 +00:00
|
|
|
history.replaceState({}, document.title, location.pathname);
|
|
|
|
}
|
|
|
|
return style;
|
|
|
|
});
|
2018-10-02 12:22:18 +00:00
|
|
|
|
|
|
|
function fetchStyle() {
|
|
|
|
if (id) {
|
2018-11-07 06:09:29 +00:00
|
|
|
return API.getStyle(id);
|
2018-10-02 12:22:18 +00:00
|
|
|
}
|
|
|
|
return Promise.resolve(createEmptyStyle());
|
|
|
|
}
|
2015-01-30 17:05:06 +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) {
|
|
|
|
contents.appendChild(typeof body === 'string' ? tHTML(body) : body);
|
|
|
|
}
|
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);
|
2017-12-13 04:33:16 +00:00
|
|
|
window.removeEventListener('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
|
|
|
});
|
|
|
|
|
2018-08-07 17:11:11 +00:00
|
|
|
window.addEventListener('keydown', showHelp.close, true);
|
|
|
|
$('.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
|
|
|
}
|
|
|
|
|
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'),
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
window.addEventListener('keydown', onKeyDown, true);
|
|
|
|
|
2020-10-13 18:14:54 +00:00
|
|
|
window.addEventListener('closeHelp', () => {
|
2018-07-22 16:37:49 +00:00
|
|
|
window.removeEventListener('keydown', onKeyDown, true);
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-12-02 20:41:15 +00:00
|
|
|
function rememberWindowSize() {
|
|
|
|
if (
|
|
|
|
document.visibilityState === 'visible' &&
|
|
|
|
prefs.get('openEditInWindow') &&
|
|
|
|
!isWindowMaximized()
|
|
|
|
) {
|
|
|
|
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() {
|
|
|
|
const scrollPoint = $('#header').clientHeight - 40;
|
|
|
|
const linterEnabled = prefs.get('editor.linter') !== '';
|
|
|
|
if (window.scrollY >= scrollPoint && !$('.fixed-header') && linterEnabled) {
|
|
|
|
$('body').classList.add('fixed-header');
|
|
|
|
} else if (window.scrollY < 40 && linterEnabled) {
|
|
|
|
$('body').classList.remove('fixed-header');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function detectLayout() {
|
|
|
|
const body = $('body');
|
|
|
|
const options = $('#options');
|
|
|
|
const lint = $('#lint');
|
|
|
|
const compact = window.innerWidth <= 850;
|
|
|
|
const shortViewportLinter = window.innerHeight < 692;
|
|
|
|
const shortViewportNoLinter = window.innerHeight < 554;
|
|
|
|
const linterEnabled = prefs.get('editor.linter') !== '';
|
|
|
|
if (compact) {
|
|
|
|
body.classList.add('compact-layout');
|
|
|
|
options.removeAttribute('open');
|
|
|
|
options.classList.add('ignore-pref');
|
|
|
|
lint.removeAttribute('open');
|
|
|
|
lint.classList.add('ignore-pref');
|
|
|
|
if (!$('.usercss')) {
|
|
|
|
clearTimeout(scrollPointTimer);
|
|
|
|
scrollPointTimer = setTimeout(() => {
|
|
|
|
const scrollPoint = $('#header').clientHeight - 40;
|
|
|
|
if (window.scrollY >= scrollPoint && !$('.fixed-header') && linterEnabled) {
|
|
|
|
body.classList.add('fixed-header');
|
|
|
|
}
|
|
|
|
}, 250);
|
2020-10-11 14:13:25 +00:00
|
|
|
window.addEventListener('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 {
|
|
|
|
body.classList.remove('compact-layout');
|
|
|
|
body.classList.remove('fixed-header');
|
|
|
|
window.removeEventListener('scroll', fixedHeader);
|
|
|
|
if (shortViewportLinter && linterEnabled || shortViewportNoLinter && !linterEnabled) {
|
|
|
|
options.removeAttribute('open');
|
|
|
|
options.classList.add('ignore-pref');
|
|
|
|
if (prefs.get('editor.lint.expanded')) {
|
|
|
|
lint.setAttribute('open', '');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
options.classList.remove('ignore-pref');
|
|
|
|
lint.classList.remove('ignore-pref');
|
|
|
|
if (prefs.get('editor.options.expanded')) {
|
|
|
|
options.setAttribute('open', '');
|
|
|
|
}
|
|
|
|
if (prefs.get('editor.lint.expanded')) {
|
|
|
|
lint.setAttribute('open', '');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleContextMenuDelete(event) {
|
|
|
|
if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) {
|
|
|
|
chrome.contextMenus.update('editor.contextDelete', {
|
|
|
|
enabled: Boolean(
|
|
|
|
this.selectionStart !== this.selectionEnd ||
|
|
|
|
this.somethingSelected && this.somethingSelected()
|
|
|
|
),
|
|
|
|
}, ignoreChromeError);
|
|
|
|
}
|
|
|
|
}
|