As we somehow forgot to add some basic metadata checks to the server, we've now enabled it and added some more errors. In such situations the errors should been shown to the user to understand what's happening.
Related USw commit: d4306f2f71
388 lines
12 KiB
JavaScript
388 lines
12 KiB
JavaScript
/* global $ $create messageBoxProxy waitForSheet */// 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
|
|
/* global updateUI revokeLinking publishStyle */// usw-integration.js
|
|
'use strict';
|
|
|
|
//#region init
|
|
|
|
baseInit.ready.then(async () => {
|
|
await waitForSheet();
|
|
(editor.isUsercss ? SourceEditor : SectionsEditor)();
|
|
updateUI();
|
|
await editor.ready;
|
|
editor.ready = true;
|
|
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();
|
|
});
|
|
|
|
// enabling after init to prevent flash of validation failure on an empty name
|
|
$('#name').required = !editor.isUsercss;
|
|
$('#save-button').onclick = editor.save;
|
|
// editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work
|
|
$('#sections-list').on('click', () => $('.compact-layout') && setTimeout(editor.updateToc),
|
|
{once: true});
|
|
$('#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());
|
|
$('#revoke-link').onclick = () => revokeLinking();
|
|
$('#publish-style').onclick = () => publishStyle();
|
|
require([
|
|
'/edit/autocomplete',
|
|
'/edit/global-search',
|
|
]);
|
|
});
|
|
|
|
msg.onExtension(request => {
|
|
const {style} = request;
|
|
switch (request.method) {
|
|
case 'styleUpdated':
|
|
if (editor.style.id === style.id) {
|
|
if (!['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);
|
|
|
|
if (['success-publishing', 'success-revoke'].includes(request.reason)) {
|
|
updateUI(newStyle);
|
|
}
|
|
if (request.reason === 'publishing-failed') {
|
|
messageBoxProxy.alert(`UserStyles.world returned error: ${newStyle._usw.publishingError}`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
case 'styleDeleted':
|
|
if (editor.style.id === style.id) {
|
|
closeCurrentTab();
|
|
}
|
|
break;
|
|
case 'editDeleteText':
|
|
document.execCommand('delete');
|
|
break;
|
|
}
|
|
});
|
|
|
|
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 || {});
|
|
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify({
|
|
scrollY: window.scrollY,
|
|
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
|
|
bookmarks: (cm.state.sublimeBookmarks || []).map(b => b.find()),
|
|
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],
|
|
})),
|
|
});
|
|
const activeElement = document.activeElement;
|
|
if (activeElement) {
|
|
// blurring triggers 'change' or 'input' event if needed
|
|
activeElement.blur();
|
|
// refocus if unloading was canceled
|
|
setTimeout(() => activeElement.focus());
|
|
}
|
|
if (editor.dirty.isDirty()) {
|
|
// neither confirm() nor custom messages work in modern browsers but just in case
|
|
e.returnValue = t('styleChangesNotSaved');
|
|
}
|
|
});
|
|
|
|
//#endregion
|
|
//#region editor methods
|
|
|
|
(() => {
|
|
const toc = [];
|
|
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),
|
|
},
|
|
});
|
|
|
|
/** @namespace Editor */
|
|
Object.assign(editor, {
|
|
|
|
applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) {
|
|
if (si && si.sel) {
|
|
const bmOpts = {sublimeBookmark: true, clearWhenEmpty: false}; // copied from sublime.js
|
|
cm.operation(() => {
|
|
cm.setSelections(...si.sel, {scroll: false});
|
|
cm.scrollIntoView(cm.getCursor(), si.parentHeight / 2);
|
|
cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts));
|
|
});
|
|
}
|
|
},
|
|
|
|
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 (!toc.el) {
|
|
toc.el = $('#toc');
|
|
toc.elDetails = toc.el.closest('details');
|
|
}
|
|
if (!toc.elDetails.open) return;
|
|
const {sections} = editor;
|
|
const first = sections.indexOf(added[0]);
|
|
const elFirst = toc.el.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 = toc.el.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) {
|
|
toc.el.lastElementChild.remove();
|
|
toc.length--;
|
|
}
|
|
if (added.focus) {
|
|
const cls = 'current';
|
|
const old = $('.' + cls, toc.el);
|
|
const el = elFirst || toc.el.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);
|
|
}
|
|
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);
|
|
},
|
|
};
|
|
|
|
function createPreviewer() {
|
|
port = chrome.runtime.connect({name: 'livePreview'});
|
|
port.onDisconnect.addListener(err => {
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
function toggle(state) {
|
|
$('#preview-label').classList.toggle('hidden', !state);
|
|
}
|
|
|
|
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');
|
|
};
|
|
}
|
|
}
|
|
})();
|
|
|
|
//#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;
|
|
}
|
|
}
|
|
if (hotkey) {
|
|
extraKeys[hotkey] = 'colorpicker';
|
|
}
|
|
});
|
|
|
|
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'));
|
|
}
|
|
})();
|
|
|
|
//#endregion
|