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
|
2018-11-07 06:09:29 +00:00
|
|
|
closeCurrentTab messageBox debounce workerUtil
|
2020-06-22 16:14:41 +00:00
|
|
|
initBeautifyButton ignoreChromeError
|
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
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
const editorWorker = workerUtil.createWorker({
|
|
|
|
url: '/edit/editor-worker.js'
|
|
|
|
});
|
|
|
|
|
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
|
|
|
|
2017-12-07 17:26:41 +00:00
|
|
|
preinit();
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
(() => {
|
|
|
|
onDOMready().then(() => {
|
|
|
|
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
|
|
|
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
|
|
|
showHotkeyInTooltip();
|
2015-03-21 13:24:45 +00:00
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
buildThemeElement();
|
|
|
|
buildKeymapElement();
|
2017-04-17 18:06:00 +00:00
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
setupLivePrefs();
|
2017-12-02 20:41:15 +00:00
|
|
|
});
|
2017-04-20 14:00:43 +00:00
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
initEditor();
|
|
|
|
|
|
|
|
function getCodeMirrorThemes() {
|
|
|
|
if (!chrome.runtime.getPackageDirectoryEntry) {
|
|
|
|
const themes = [
|
|
|
|
chrome.i18n.getMessage('defaultTheme'),
|
2018-11-07 11:33:14 +00:00
|
|
|
...CODEMIRROR_THEMES
|
2018-11-07 06:09:29 +00:00
|
|
|
];
|
|
|
|
localStorage.codeMirrorThemes = themes.join(' ');
|
|
|
|
return Promise.resolve(themes);
|
|
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
|
|
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
|
|
|
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
|
|
|
|
themeDir.createReader().readEntries(entries => {
|
|
|
|
const themes = [
|
|
|
|
chrome.i18n.getMessage('defaultTheme')
|
|
|
|
].concat(
|
|
|
|
entries.filter(entry => entry.isFile)
|
|
|
|
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
|
|
|
.map(entry => entry.name.replace(/\.css$/, ''))
|
|
|
|
);
|
|
|
|
localStorage.codeMirrorThemes = themes.join(' ');
|
|
|
|
resolve(themes);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
|
|
|
const themeElement = $('#editor.theme');
|
|
|
|
const themeList = localStorage.codeMirrorThemes;
|
|
|
|
|
|
|
|
const optionsFromArray = options => {
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
|
|
|
themeElement.appendChild(fragment);
|
|
|
|
};
|
|
|
|
|
|
|
|
if (themeList) {
|
|
|
|
optionsFromArray(themeList.split(/\s+/));
|
|
|
|
} else {
|
|
|
|
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
|
|
|
const theme = prefs.get('editor.theme');
|
|
|
|
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
|
|
|
|
getCodeMirrorThemes().then(() => {
|
|
|
|
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
|
|
|
optionsFromArray(themes);
|
|
|
|
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function initEditor() {
|
|
|
|
return Promise.all([
|
|
|
|
initStyleData(),
|
|
|
|
onDOMready(),
|
|
|
|
prefs.initializing,
|
|
|
|
])
|
|
|
|
.then(([style]) => {
|
|
|
|
const usercss = isUsercss(style);
|
|
|
|
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
|
|
|
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
|
|
|
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
|
|
|
|
$('#preview-label').classList.toggle('hidden', !style.id);
|
2020-06-22 16:14:41 +00:00
|
|
|
initBeautifyButton($('#beautify'), () => editor.getEditors());
|
2020-08-02 04:00:44 +00:00
|
|
|
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();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
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
|
|
|
window.addEventListener('resize', () => {
|
2020-08-02 04:00:44 +00:00
|
|
|
if (!onBoundsChanged) debounce(rememberWindowSize, 100);
|
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
|
|
|
detectLayout();
|
|
|
|
});
|
|
|
|
detectLayout();
|
2018-11-28 04:54:36 +00:00
|
|
|
editor = (usercss ? createSourceEditor : createSectionsEditor)({
|
|
|
|
style,
|
|
|
|
onTitleChanged: updateTitle
|
|
|
|
});
|
|
|
|
editor.dirty.onChange(updateDirty);
|
|
|
|
return Promise.resolve(editor.ready && editor.ready())
|
|
|
|
.then(updateDirty);
|
2018-11-07 06:09:29 +00:00
|
|
|
});
|
|
|
|
}
|
2018-11-28 04:54:36 +00:00
|
|
|
|
|
|
|
function updateTitle() {
|
|
|
|
if (editor) {
|
|
|
|
const styleName = editor.getStyle().name;
|
|
|
|
const isDirty = editor.dirty.isDirty();
|
|
|
|
document.title = (isDirty ? '* ' : '') + styleName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateDirty() {
|
|
|
|
const isDirty = editor.dirty.isDirty();
|
|
|
|
document.body.classList.toggle('dirty', isDirty);
|
|
|
|
$('#save-button').disabled = !isDirty;
|
|
|
|
updateTitle();
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
})();
|
|
|
|
|
|
|
|
function preinit() {
|
2017-12-02 20:41:15 +00:00
|
|
|
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
|
2017-12-09 16:05:44 +00:00
|
|
|
new MutationObserver((mutations, observer) => {
|
|
|
|
const themeElement = $('#cm-theme');
|
|
|
|
if (themeElement) {
|
|
|
|
themeElement.href = prefs.get('editor.theme') === 'default' ? ''
|
|
|
|
: 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
|
|
|
|
observer.disconnect();
|
|
|
|
}
|
|
|
|
}).observe(document, {subtree: true, childList: true});
|
2017-12-02 20:41:15 +00:00
|
|
|
|
|
|
|
if (chrome.windows) {
|
2020-08-14 12:16:01 +00:00
|
|
|
browser.tabs.query({currentWindow: true}).then(tabs => {
|
2017-12-02 20:41:15 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
|
|
|
|
}
|
2017-07-12 19:50:13 +00:00
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
});
|
2017-07-12 19:50:13 +00:00
|
|
|
}
|
|
|
|
|
2017-12-02 20:41:15 +00:00
|
|
|
getOwnTab().then(tab => {
|
2018-11-07 06:09:29 +00:00
|
|
|
const ownTabId = tab.id;
|
2017-12-08 02:45:27 +00:00
|
|
|
|
|
|
|
// 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();
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// no windows on android
|
2017-12-02 20:41:15 +00:00
|
|
|
if (!chrome.windows) {
|
2017-09-12 19:45:33 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
// When an edit page gets attached or detached, remember its state
|
|
|
|
// so we can do the same to the next one to open.
|
|
|
|
chrome.tabs.onAttached.addListener((tabId, info) => {
|
|
|
|
if (tabId !== ownTabId) {
|
2017-09-12 19:45:33 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
if (info.newPosition !== 0) {
|
|
|
|
prefs.set('openEditInWindow', false);
|
|
|
|
return;
|
2017-09-12 19:45:33 +00:00
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
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
|
|
|
}
|
2017-12-02 20:41:15 +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 (
|
|
|
|
editor.getStyleId() === request.style.id &&
|
|
|
|
!['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':
|
2018-11-07 06:09:29 +00:00
|
|
|
if (editor.getStyleId() === 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() {
|
2017-11-28 19:19:00 +00:00
|
|
|
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
|
|
|
|
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
|
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' ||
|
|
|
|
(
|
|
|
|
event.which === 27 &&
|
|
|
|
!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 => {
|
|
|
|
if (event.which === 9 && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
|
|
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);
|
|
|
|
|
2017-12-09 15:23:18 +00:00
|
|
|
window.addEventListener('closeHelp', function _() {
|
|
|
|
window.removeEventListener('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;
|
|
|
|
});
|
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
|
|
|
prefs.subscribe(['editor.linter'], (key, value) => {
|
|
|
|
$('body').classList.toggle('linter-disabled', value === '');
|
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
window.addEventListener('scroll', fixedHeader);
|
|
|
|
}
|
|
|
|
} 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);
|
|
|
|
}
|
|
|
|
}
|