fdbfb23547
* parserlib: fast section extraction, tweaks and speedups
* csslint: "simple-not" rule
* csslint: enable and fix "selector-newline" rule
* simplify db: resolve with result
* simplify download()
* remove noCode param as it wastes more time/memory on copying
* styleManager: switch style<->data names to reflect their actual contents
* inline method bodies to avoid indirection and enable better autocomplete/hint/jump support in IDE
* upgrade getEventKeyName to handle mouse clicks
* don't trust location.href as it hides text fragment
* getAllKeys is implemented since Chrome48, FF44
* allow recoverable css errors + async'ify usercss.js
* openManage: unminimize windows
* remove the obsolete Chrome pre-65 workaround
* fix temporal dead zone in apply.js
* ff bug workaround for simple editor window
* consistent window scrolling in scrollToEditor and jumpToPos
* rework waitForSelector and collapsible <details>
* blank paint frame workaround for new Chrome
* extract stuff from edit.js and load on demand
* simplify regexpTester::isShown
* move MozDocMapper to sections-util.js
* extract fitSelectBox()
* initialize router earlier
* use helpPopup.close()
* fix autofocus in popups, follow-up to 5bb1b5ef
* clone objects in prefs.get() + cosmetics
* reuse getAll result for INC
284 lines
8.2 KiB
JavaScript
284 lines
8.2 KiB
JavaScript
/* global API */// msg.js
|
|
/* global changeQueue installed newUI */// manage.js
|
|
/* global checkUpdate handleUpdateInstalled */// updater-ui.js
|
|
/* global createStyleElement createTargetsElement getFaviconSrc */// render.js
|
|
/* global debounce getOwnTab openURL sessionStore */// toolbox.js
|
|
/* global filterAndAppend showFiltersStats */// filters.js
|
|
/* global sorter */
|
|
/* global t */// localization.js
|
|
/* global
|
|
$
|
|
$$
|
|
$entry
|
|
animateElement
|
|
getEventKeyName
|
|
messageBoxProxy
|
|
scrollElementIntoView
|
|
*/// dom.js
|
|
'use strict';
|
|
|
|
const Events = {
|
|
|
|
addEntryTitle(link) {
|
|
const style = link.closest('.entry').styleMeta;
|
|
const ucd = style.usercssData;
|
|
link.title =
|
|
`${t('dateInstalled')}: ${t.formatDate(style.installDate) || '—'}\n` +
|
|
`${t('dateUpdated')}: ${t.formatDate(style.updateDate) || '—'}\n` +
|
|
(ucd ? `UserCSS, v.${ucd.version}` : '');
|
|
},
|
|
|
|
check(event, entry) {
|
|
checkUpdate(entry, {single: true});
|
|
},
|
|
|
|
async config(event, {styleMeta}) {
|
|
await require(['/js/dlg/config-dialog']); /* global configDialog */
|
|
configDialog(styleMeta);
|
|
},
|
|
|
|
async delete(event, entry) {
|
|
const id = entry.styleId;
|
|
animateElement(entry);
|
|
const {button} = await messageBoxProxy.show({
|
|
title: t('deleteStyleConfirm'),
|
|
contents: entry.styleMeta.customName || entry.styleMeta.name,
|
|
className: 'danger center',
|
|
buttons: [t('confirmDelete'), t('confirmCancel')],
|
|
});
|
|
if (button === 0) {
|
|
API.styles.delete(id);
|
|
}
|
|
const deleteButton = $('#message-box-buttons > button');
|
|
if (deleteButton) deleteButton.removeAttribute('data-focused-via-click');
|
|
},
|
|
|
|
async edit(event, entry) {
|
|
if (event.altKey) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const key = getEventKeyName(event);
|
|
const url = $('[href]', entry).href;
|
|
const ownTab = await getOwnTab();
|
|
if (key === 'MouseL') {
|
|
sessionStore['manageStylesHistory' + ownTab.id] = url;
|
|
location.href = url;
|
|
} else if (chrome.windows && key === 'Shift-MouseL') {
|
|
API.openEditor({id: entry.styleId});
|
|
} else {
|
|
openURL({
|
|
url,
|
|
index: ownTab.index + 1,
|
|
active: key === 'Shift-MouseM' || key === 'Shift-Ctrl-MouseL',
|
|
});
|
|
}
|
|
},
|
|
|
|
expandTargets(event, entry) {
|
|
if (!entry._allTargetsRendered) {
|
|
createTargetsElement({entry, expanded: true});
|
|
setTimeout(getFaviconSrc, 0, entry);
|
|
}
|
|
this.closest('.applies-to').classList.toggle('expanded');
|
|
},
|
|
|
|
async external(event) {
|
|
// Not handling Shift-click - the built-in 'open in a new window' command
|
|
if (getEventKeyName(event) !== 'Shift-MouseL') {
|
|
const {index} = await getOwnTab();
|
|
openURL({
|
|
url: event.target.closest('a').href,
|
|
index: index + 1,
|
|
active: !event.ctrlKey || event.shiftKey,
|
|
});
|
|
}
|
|
},
|
|
|
|
entryClicked(event) {
|
|
const target = event.target;
|
|
const entry = target.closest('.entry');
|
|
for (const selector in Events.ENTRY_ROUTES) {
|
|
for (let el = target; el && el !== entry; el = el.parentElement) {
|
|
if (el.matches(selector)) {
|
|
return Events.ENTRY_ROUTES[selector].call(el, event, entry);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
lazyAddEntryTitle({type, target}) {
|
|
const cell = target.closest('h2.style-name, [data-type=age]');
|
|
if (cell) {
|
|
const link = $('.style-name-link', cell) || cell;
|
|
if (type === 'mouseover' && !link.title) {
|
|
debounce(Events.addEntryTitle, 50, link);
|
|
} else {
|
|
debounce.unregister(Events.addEntryTitle);
|
|
}
|
|
}
|
|
},
|
|
|
|
name(event, entry) {
|
|
if (newUI.enabled) Events.edit(event, entry);
|
|
},
|
|
|
|
toggle(event, entry) {
|
|
API.styles.toggle(entry.styleId, this.matches('.enable') || this.checked);
|
|
},
|
|
|
|
update(event, entry) {
|
|
const json = entry.updatedCode;
|
|
json.id = entry.styleId;
|
|
(json.usercssData ? API.usercss.install : API.styles.install)(json);
|
|
},
|
|
};
|
|
|
|
Events.ENTRY_ROUTES = {
|
|
'input, .enable, .disable': Events.toggle,
|
|
'.style-name': Events.name,
|
|
'.homepage': Events.external,
|
|
'.check-update': Events.check,
|
|
'.update': Events.update,
|
|
'.delete': Events.delete,
|
|
'.applies-to .expander': Events.expandTargets,
|
|
'.configure-usercss': Events.config,
|
|
};
|
|
|
|
/* exported handleBulkChange */
|
|
function handleBulkChange() {
|
|
for (const msg of changeQueue) {
|
|
const {id} = msg.style;
|
|
if (msg.method === 'styleDeleted') {
|
|
handleDelete(id);
|
|
changeQueue.time = performance.now();
|
|
} else {
|
|
handleUpdateForId(id, msg);
|
|
}
|
|
}
|
|
changeQueue.length = 0;
|
|
}
|
|
|
|
function handleDelete(id) {
|
|
const node = $entry(id);
|
|
if (node) {
|
|
node.remove();
|
|
if (node.matches('.can-update')) {
|
|
const btnApply = $('#apply-all-updates');
|
|
btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
|
|
}
|
|
showFiltersStats();
|
|
}
|
|
}
|
|
|
|
function handleUpdate(style, {reason, method} = {}) {
|
|
if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
|
|
let entry;
|
|
let oldEntry = $entry(style);
|
|
if (oldEntry && method === 'styleUpdated') {
|
|
handleToggledOrCodeOnly();
|
|
}
|
|
entry = entry || createStyleElement({style});
|
|
if (oldEntry) {
|
|
if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
|
|
installed.replaceChild(entry, oldEntry);
|
|
} else {
|
|
oldEntry.remove();
|
|
}
|
|
}
|
|
if ((reason === 'update' || reason === 'install') && entry.matches('.updatable')) {
|
|
handleUpdateInstalled(entry, reason);
|
|
}
|
|
filterAndAppend({entry}).then(sorter.update);
|
|
if (!entry.matches('.hidden') && reason !== 'import' && reason !== 'sync') {
|
|
animateElement(entry);
|
|
requestAnimationFrame(() => scrollElementIntoView(entry));
|
|
}
|
|
getFaviconSrc(entry);
|
|
|
|
function handleToggledOrCodeOnly() {
|
|
style.sections.forEach(s => (s.code = null));
|
|
style.sourceCode = null;
|
|
const diff = objectDiff(oldEntry.styleMeta, style)
|
|
.filter(({key, path}) => path || (!key.startsWith('original') && !key.endsWith('Date')));
|
|
if (diff.length === 0) {
|
|
// only code was modified
|
|
entry = oldEntry;
|
|
oldEntry = null;
|
|
}
|
|
if (diff.length === 1 && diff[0].key === 'enabled') {
|
|
oldEntry.classList.toggle('enabled', style.enabled);
|
|
oldEntry.classList.toggle('disabled', !style.enabled);
|
|
$$('input', oldEntry).forEach(el => (el.checked = style.enabled));
|
|
oldEntry.styleMeta = style;
|
|
entry = oldEntry;
|
|
oldEntry = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleUpdateForId(id, opts) {
|
|
handleUpdate(await API.styles.get(id), opts);
|
|
changeQueue.time = performance.now();
|
|
}
|
|
|
|
/* exported handleVisibilityChange */
|
|
function handleVisibilityChange() {
|
|
switch (document.visibilityState) {
|
|
// page restored without reloading via history navigation (currently only in FF)
|
|
// the catch here is that DOM may be outdated so we'll at least refresh the just edited style
|
|
// assuming other changes aren't important enough to justify making a complicated DOM sync
|
|
case 'visible': {
|
|
const id = sessionStore.justEditedStyleId;
|
|
if (id) {
|
|
handleUpdateForId(Number(id), {method: 'styleUpdated'});
|
|
delete sessionStore.justEditedStyleId;
|
|
}
|
|
break;
|
|
}
|
|
// going away
|
|
case 'hidden':
|
|
history.replaceState({scrollY: window.scrollY}, document.title);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function objectDiff(first, second, path = '') {
|
|
const diff = [];
|
|
for (const key in first) {
|
|
const a = first[key];
|
|
const b = second[key];
|
|
if (a === b) {
|
|
continue;
|
|
}
|
|
if (b === undefined) {
|
|
diff.push({path, key, values: [a], type: 'removed'});
|
|
continue;
|
|
}
|
|
if (a && typeof a.filter === 'function' && b && typeof b.filter === 'function') {
|
|
if (
|
|
a.length !== b.length ||
|
|
a.some((el, i) => {
|
|
const result = !el || typeof el !== 'object'
|
|
? el !== b[i]
|
|
: objectDiff(el, b[i], path + key + '[' + i + '].').length;
|
|
return result;
|
|
})
|
|
) {
|
|
diff.push({path, key, values: [a, b], type: 'changed'});
|
|
}
|
|
} else if (typeof a === 'object' && typeof b === 'object') {
|
|
diff.push(...objectDiff(a, b, path + key + '.'));
|
|
} else {
|
|
diff.push({path, key, values: [a, b], type: 'changed'});
|
|
}
|
|
}
|
|
for (const key in second) {
|
|
if (!(key in first)) {
|
|
diff.push({path, key, values: [second[key]], type: 'added'});
|
|
}
|
|
}
|
|
return diff;
|
|
}
|