2021-01-01 14:27:58 +00:00
|
|
|
/* global $ $create messageBoxProxy */// dom.js
|
|
|
|
/* global API msg */// msg.js
|
|
|
|
/* global CodeMirror */
|
|
|
|
/* global SectionsEditor */
|
|
|
|
/* global SourceEditor */
|
|
|
|
/* global baseInit */
|
|
|
|
/* global clipString createHotkeyInput helpPopup */// util.js
|
|
|
|
/* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js
|
|
|
|
/* global cmFactory */
|
|
|
|
/* global editor */
|
|
|
|
/* global linterMan */
|
|
|
|
/* global prefs */
|
|
|
|
/* global t */// localization.js
|
2017-07-12 20:44:59 +00:00
|
|
|
'use strict';
|
2015-03-14 11:51:41 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
//#region init
|
2020-11-18 11:17:15 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
baseInit.ready.then(async () => {
|
|
|
|
await new Promise(requestAnimationFrame);
|
|
|
|
(editor.isUsercss ? SourceEditor : SectionsEditor)();
|
2020-10-11 15:12:06 +00:00
|
|
|
await editor.ready;
|
2020-11-18 11:17:15 +00:00
|
|
|
editor.ready = true;
|
2021-01-01 14:27:58 +00:00
|
|
|
editor.dirty.onChange(editor.updateDirty);
|
|
|
|
|
|
|
|
prefs.subscribe('editor.toc.expanded', (k, val) => val && editor.updateToc(), {runNow: true});
|
|
|
|
prefs.subscribe('editor.linter', (key, value) => {
|
|
|
|
document.body.classList.toggle('linter-disabled', value === '');
|
|
|
|
linterMan.run();
|
|
|
|
});
|
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
|
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;
|
2021-01-01 14:27:58 +00:00
|
|
|
$('#toc').onclick = e =>
|
|
|
|
editor.jumpToEditor([...$('#toc').children].indexOf(e.target));
|
|
|
|
$('#keyMap-help').onclick = () =>
|
|
|
|
require(['/edit/show-keymap-help'], () => showKeymapHelp()); /* global showKeymapHelp */
|
|
|
|
$('#linter-settings').onclick = () =>
|
|
|
|
require(['/edit/linter-dialogs'], () => linterMan.showLintConfig());
|
|
|
|
$('#lint-help').onclick = () =>
|
|
|
|
require(['/edit/linter-dialogs'], () => linterMan.showLintHelp());
|
|
|
|
require([
|
|
|
|
'/edit/autocomplete',
|
|
|
|
'/edit/global-search',
|
|
|
|
]);
|
|
|
|
});
|
2018-11-07 06:09:29 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
msg.onExtension(request => {
|
|
|
|
const {style} = request;
|
2017-12-02 20:41:15 +00:00
|
|
|
switch (request.method) {
|
|
|
|
case 'styleUpdated':
|
2021-01-01 14:27:58 +00:00
|
|
|
if (editor.style.id === style.id &&
|
|
|
|
!['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) {
|
|
|
|
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(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':
|
2021-01-01 14:27:58 +00:00
|
|
|
if (editor.style.id === style.id) {
|
2017-12-02 20:41:15 +00:00
|
|
|
closeCurrentTab();
|
2017-07-12 19:50:13 +00:00
|
|
|
}
|
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
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
});
|
2017-08-27 11:56:04 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
window.on('beforeunload', e => {
|
|
|
|
let pos;
|
|
|
|
if (editor.isWindowed &&
|
|
|
|
document.visibilityState === 'visible' &&
|
|
|
|
prefs.get('openEditInWindow') &&
|
|
|
|
( // only if not maximized
|
|
|
|
screenX > 0 || outerWidth < screen.availWidth ||
|
|
|
|
screenY > 0 || outerHeight < screen.availHeight ||
|
|
|
|
screenX <= -10 || outerWidth >= screen.availWidth + 10 ||
|
|
|
|
screenY <= -10 || outerHeight >= screen.availHeight + 10
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
pos = {
|
|
|
|
left: screenX,
|
|
|
|
top: screenY,
|
|
|
|
width: outerWidth,
|
|
|
|
height: outerHeight,
|
|
|
|
};
|
|
|
|
prefs.set('windowPosition', pos);
|
|
|
|
}
|
|
|
|
sessionStore.windowPos = JSON.stringify(pos || {});
|
2020-11-18 11:17:15 +00:00
|
|
|
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
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
if (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
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
});
|
2015-10-29 18:20:05 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
//#endregion
|
|
|
|
//#region editor methods
|
2017-12-13 04:33:16 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
(() => {
|
|
|
|
const toc = [];
|
|
|
|
let tocElem;
|
2017-07-12 19:50:13 +00:00
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
const {dirty} = editor;
|
|
|
|
let {style} = editor;
|
|
|
|
let wasDirty = false;
|
|
|
|
|
|
|
|
Object.defineProperties(editor, {
|
|
|
|
scrollInfo: {
|
|
|
|
get: () => style.id && tryJSONparse(sessionStore['editorScrollInfo' + style.id]) || {},
|
|
|
|
},
|
|
|
|
style: {
|
|
|
|
get: () => style,
|
|
|
|
set: val => (style = val),
|
|
|
|
},
|
2017-12-13 04:33:16 +00:00
|
|
|
});
|
|
|
|
|
2021-01-01 14:27:58 +00:00
|
|
|
/** @namespace Editor */
|
|
|
|
Object.assign(editor, {
|
|
|
|
|
|
|
|
applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) {
|
|
|
|
if (si && si.sel) {
|
|
|
|
cm.operation(() => {
|
|
|
|
cm.setSelections(...si.sel, {scroll: false});
|
|
|
|
cm.scrollIntoView(cm.getCursor(), si.parentHeight / 2);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
toggleStyle() {
|
|
|
|
$('#enabled').checked = !style.enabled;
|
|
|
|
editor.updateEnabledness(!style.enabled);
|
|
|
|
},
|
|
|
|
|
|
|
|
updateDirty() {
|
|
|
|
const isDirty = dirty.isDirty();
|
|
|
|
if (wasDirty !== isDirty) {
|
|
|
|
wasDirty = isDirty;
|
|
|
|
document.body.classList.toggle('dirty', isDirty);
|
|
|
|
$('#save-button').disabled = !isDirty;
|
|
|
|
}
|
|
|
|
editor.updateTitle();
|
|
|
|
},
|
|
|
|
|
|
|
|
updateEnabledness(enabled) {
|
|
|
|
dirty.modify('enabled', style.enabled, enabled);
|
|
|
|
style.enabled = enabled;
|
|
|
|
editor.updateLivePreview();
|
|
|
|
},
|
|
|
|
|
|
|
|
updateName(isUserInput) {
|
|
|
|
if (!editor) return;
|
|
|
|
if (isUserInput) {
|
|
|
|
const {value} = $('#name');
|
|
|
|
dirty.modify('name', style[editor.nameTarget] || style.name, value);
|
|
|
|
style[editor.nameTarget] = value;
|
|
|
|
}
|
|
|
|
editor.updateTitle();
|
|
|
|
},
|
|
|
|
|
|
|
|
updateToc(added = editor.sections) {
|
|
|
|
if (!prefs.get('editor.toc.expanded')) return;
|
|
|
|
if (!tocElem) tocElem = $('#toc');
|
|
|
|
const {sections} = editor;
|
|
|
|
const first = sections.indexOf(added[0]);
|
|
|
|
const elFirst = tocElem.children[first];
|
|
|
|
if (first >= 0 && (!added.focus || !elFirst)) {
|
|
|
|
for (let el = elFirst, i = first; i < sections.length; i++) {
|
|
|
|
const entry = sections[i].tocEntry;
|
|
|
|
if (!deepEqual(entry, toc[i])) {
|
|
|
|
if (!el) el = tocElem.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) {
|
|
|
|
tocElem.lastElementChild.remove();
|
|
|
|
toc.length--;
|
|
|
|
}
|
|
|
|
if (added.focus) {
|
|
|
|
const cls = 'current';
|
|
|
|
const old = $('.' + cls, tocElem);
|
|
|
|
const el = elFirst || tocElem.children[first];
|
|
|
|
if (old && old !== el) old.classList.remove(cls);
|
|
|
|
el.classList.add(cls);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
})();
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
//#region editor livePreview
|
|
|
|
|
|
|
|
editor.livePreview = (() => {
|
|
|
|
let data;
|
|
|
|
let port;
|
|
|
|
let preprocess;
|
|
|
|
let enabled = prefs.get('editor.livePreview');
|
|
|
|
|
|
|
|
prefs.subscribe('editor.livePreview', (key, value) => {
|
|
|
|
if (!value) {
|
|
|
|
if (port) {
|
|
|
|
port.disconnect();
|
|
|
|
port = null;
|
|
|
|
}
|
|
|
|
} else if (data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
|
|
|
|
createPreviewer();
|
|
|
|
updatePreviewer(data);
|
2018-07-22 16:37:49 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
enabled = value;
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Function} [fn] - preprocessor
|
|
|
|
* @param {boolean} [show]
|
|
|
|
*/
|
|
|
|
init(fn, show) {
|
|
|
|
preprocess = fn;
|
|
|
|
if (show != null) toggle(show);
|
|
|
|
},
|
|
|
|
|
|
|
|
toggle,
|
|
|
|
|
|
|
|
update(newData) {
|
|
|
|
data = newData;
|
|
|
|
if (!port) {
|
|
|
|
if (!data.id || !data.enabled || !enabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
createPreviewer();
|
|
|
|
}
|
|
|
|
updatePreviewer(data);
|
|
|
|
},
|
2018-07-22 16:37:49 +00:00
|
|
|
};
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
function createPreviewer() {
|
|
|
|
port = chrome.runtime.connect({name: 'livePreview'});
|
|
|
|
port.onDisconnect.addListener(err => {
|
|
|
|
throw err;
|
2017-12-02 20:41:15 +00:00
|
|
|
});
|
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
function toggle(state) {
|
|
|
|
$('#preview-label').classList.toggle('hidden', !state);
|
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
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
async function updatePreviewer(data) {
|
|
|
|
const errorContainer = $('#preview-errors');
|
|
|
|
try {
|
|
|
|
port.postMessage(preprocess ? await preprocess(data) : data);
|
|
|
|
errorContainer.classList.add('hidden');
|
|
|
|
} catch (err) {
|
|
|
|
if (Array.isArray(err)) {
|
|
|
|
err = err.join('\n');
|
|
|
|
} else if (err && err.index != null) {
|
|
|
|
// FIXME: this would fail if editors[0].getValue() !== data.sourceCode
|
|
|
|
const pos = editor.getEditors()[0].posFromIndex(err.index);
|
|
|
|
err.message = `${pos.line}:${pos.ch} ${err.message || err}`;
|
|
|
|
}
|
|
|
|
errorContainer.classList.remove('hidden');
|
|
|
|
errorContainer.onclick = () => {
|
|
|
|
messageBoxProxy.alert(err.message || `${err}`, 'pre');
|
|
|
|
};
|
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
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
})();
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
//#region colorpickerHelper
|
|
|
|
|
|
|
|
(async function colorpickerHelper() {
|
|
|
|
prefs.subscribe('editor.colorpicker.hotkey', (id, hotkey) => {
|
|
|
|
CodeMirror.commands.colorpicker = invokeColorpicker;
|
|
|
|
const extraKeys = CodeMirror.defaults.extraKeys;
|
|
|
|
for (const key in extraKeys) {
|
|
|
|
if (extraKeys[key] === 'colorpicker') {
|
|
|
|
delete extraKeys[key];
|
|
|
|
break;
|
2020-11-23 20:22:41 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
}
|
|
|
|
if (hotkey) {
|
|
|
|
extraKeys[hotkey] = 'colorpicker';
|
|
|
|
}
|
2020-11-23 20:22:41 +00:00
|
|
|
});
|
2021-01-01 14:27:58 +00:00
|
|
|
|
|
|
|
prefs.subscribe('editor.colorpicker', (id, enabled) => {
|
|
|
|
const defaults = CodeMirror.defaults;
|
|
|
|
const keyName = prefs.get('editor.colorpicker.hotkey');
|
|
|
|
defaults.colorpicker = enabled;
|
|
|
|
if (enabled) {
|
|
|
|
if (keyName) {
|
|
|
|
CodeMirror.commands.colorpicker = invokeColorpicker;
|
|
|
|
defaults.extraKeys = defaults.extraKeys || {};
|
|
|
|
defaults.extraKeys[keyName] = 'colorpicker';
|
|
|
|
}
|
|
|
|
defaults.colorpicker = {
|
|
|
|
tooltip: t('colorpickerTooltip'),
|
|
|
|
popup: {
|
|
|
|
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
|
|
|
paletteLine: t('numberedLine'),
|
|
|
|
paletteHint: t('colorpickerPaletteHint'),
|
|
|
|
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
|
|
|
|
embedderCallback: state => {
|
|
|
|
['hexUppercase', 'color']
|
|
|
|
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))
|
|
|
|
.forEach(name => prefs.set('editor.colorpicker.' + name, state[name]));
|
|
|
|
},
|
|
|
|
get maxHeight() {
|
|
|
|
return prefs.get('editor.colorpicker.maxHeight');
|
|
|
|
},
|
|
|
|
set maxHeight(h) {
|
|
|
|
prefs.set('editor.colorpicker.maxHeight', h);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
if (defaults.extraKeys) {
|
|
|
|
delete defaults.extraKeys[keyName];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cmFactory.globalSetOption('colorpicker', defaults.colorpicker);
|
|
|
|
}, {runNow: true});
|
|
|
|
|
|
|
|
await baseInit.domReady;
|
|
|
|
|
|
|
|
$('#colorpicker-settings').onclick = function (event) {
|
|
|
|
event.preventDefault();
|
|
|
|
const input = createHotkeyInput('editor.colorpicker.hotkey', () => helpPopup.close());
|
|
|
|
const popup = helpPopup.show(t('helpKeyMapHotkey'), input);
|
|
|
|
const bounds = this.getBoundingClientRect();
|
|
|
|
popup.style.left = bounds.right + 10 + 'px';
|
|
|
|
popup.style.top = bounds.top - popup.clientHeight / 2 + 'px';
|
|
|
|
popup.style.right = 'auto';
|
|
|
|
input.focus();
|
|
|
|
};
|
|
|
|
|
|
|
|
function invokeColorpicker(cm) {
|
|
|
|
cm.state.colorpicker.openPopup(prefs.get('editor.colorpicker.color'));
|
2020-11-23 20:22:41 +00:00
|
|
|
}
|
2021-01-01 14:27:58 +00:00
|
|
|
})();
|
|
|
|
|
|
|
|
//#endregion
|