stylus/background/style-via-api.js
tophf fdbfb23547
API groups + use executeScript for early injection (#1149)
* 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
2021-01-01 17:27:58 +03:00

234 lines
6.6 KiB
JavaScript

/* global API */// msg.js
/* global addAPI */// common.js
/* global isEmptyObj */// toolbox.js
/* global prefs */
'use strict';
/**
* Uses chrome.tabs.insertCSS
*/
(() => {
const ACTIONS = {
styleApply,
styleDeleted,
styleUpdated,
styleAdded,
styleReplaceAll,
prefChanged,
updateCount,
};
const NOP = new Error('NOP');
const onError = () => {};
/* <tabId>: Object
<frameId>: Object
url: String, non-enumerable
<styleId>: Array of strings
section code */
const cache = new Map();
let observingTabs = false;
addAPI(/** @namespace API */ {
async styleViaAPI(request) {
try {
const fn = ACTIONS[request.method];
return fn ? fn(request, this.sender) : NOP;
} catch (e) {}
maybeToggleObserver();
},
});
function updateCount(request, sender) {
const {tab, frameId} = sender;
if (frameId) {
throw new Error('we do not count styles for frames');
}
const {frameStyles} = getCachedData(tab.id, frameId);
API.updateIconBadge.call({sender}, Object.keys(frameStyles));
}
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
if (prefs.get('disableAll')) {
return NOP;
}
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
return NOP;
}
return API.styles.getSectionsByUrl(url, id).then(sections => {
const tasks = [];
for (const section of Object.values(sections)) {
const styleId = section.id;
const code = section.code.join('\n');
if (code === (frameStyles[styleId] || []).join('\n')) {
continue;
}
frameStyles[styleId] = section.code;
tasks.push(
browser.tabs.insertCSS(tab.id, {
code,
frameId,
runAt: 'document_start',
matchAboutBlank: true,
}).catch(onError));
}
if (!removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles)) {
Object.defineProperty(frameStyles, 'url', {value: url, configurable: true});
tabFrames[frameId] = frameStyles;
cache.set(tab.id, tabFrames);
}
return Promise.all(tasks);
})
.then(() => updateCount(null, {tab, frameId}));
}
function styleDeleted({style: {id}}, {tab, frameId}) {
const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id);
const code = styleSections.join('\n');
if (code && !duplicateCodeExists({frameStyles, id, code})) {
delete frameStyles[id];
removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles);
return removeCSS(tab.id, frameId, code)
.then(() => updateCount(null, {tab, frameId}));
} else {
return NOP;
}
}
function styleUpdated({style}, sender) {
if (!style.enabled) {
return styleDeleted({style}, sender);
}
const {tab, frameId} = sender;
const {frameStyles, styleSections} = getCachedData(tab.id, frameId, style.id);
const code = styleSections.join('\n');
return styleApply(style, sender).then(code && (() => {
if (!duplicateCodeExists({frameStyles, code, id: null})) {
return removeCSS(tab.id, frameId, code);
}
}));
}
function styleAdded({style}, sender) {
return style.enabled ? styleApply(style, sender) : NOP;
}
function styleReplaceAll(request, sender) {
const {tab, frameId} = sender;
const oldStylesCode = getFrameStylesJoined(sender);
return styleApply({ignoreUrlCheck: true}, sender).then(() => {
const newStylesCode = getFrameStylesJoined(sender);
const tasks = oldStylesCode
.filter(code => !newStylesCode.includes(code))
.map(code => removeCSS(tab.id, frameId, code));
return Promise.all(tasks);
});
}
function prefChanged({prefs}, sender) {
if ('disableAll' in prefs) {
if (!prefs.disableAll) {
return styleApply({}, sender);
}
const {tab, frameId} = sender;
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
if (isEmptyObj(frameStyles)) {
return NOP;
}
removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
const tasks = Object.keys(frameStyles)
.map(id => removeCSS(tab.id, frameId, frameStyles[id].join('\n')));
return Promise.all(tasks);
} else {
return NOP;
}
}
/* utilities */
function maybeToggleObserver() {
let method;
if (!observingTabs && cache.size) {
method = 'addListener';
} else if (observingTabs && !cache.size) {
method = 'removeListener';
} else {
return;
}
observingTabs = !observingTabs;
chrome.webNavigation.onCommitted[method](onNavigationCommitted);
chrome.tabs.onRemoved[method](onTabRemoved);
chrome.tabs.onReplaced[method](onTabReplaced);
}
function onNavigationCommitted({tabId, frameId}) {
if (frameId === 0) {
onTabRemoved(tabId);
return;
}
const tabFrames = cache.get(tabId);
if (tabFrames && frameId in tabFrames) {
delete tabFrames[frameId];
if (isEmptyObj(tabFrames)) {
onTabRemoved(tabId);
}
}
}
function onTabRemoved(tabId) {
cache.delete(tabId);
maybeToggleObserver();
}
function onTabReplaced(addedTabId, removedTabId) {
onTabRemoved(removedTabId);
}
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
if (isEmptyObj(frameStyles)) {
delete tabFrames[frameId];
if (isEmptyObj(tabFrames)) {
cache.delete(tabId);
}
return true;
}
}
function getCachedData(tabId, frameId, styleId) {
const tabFrames = cache.get(tabId) || {};
const frameStyles = tabFrames[frameId] || {};
const styleSections = styleId && frameStyles[styleId] || [];
return {tabFrames, frameStyles, styleSections};
}
function getFrameStylesJoined({
tab,
frameId,
frameStyles = getCachedData(tab.id, frameId).frameStyles,
}) {
return Object.keys(frameStyles).map(id => frameStyles[id].join('\n'));
}
function duplicateCodeExists({
tab,
frameId,
frameStyles = getCachedData(tab.id, frameId).frameStyles,
frameStylesCode = {},
id,
code = frameStylesCode[id] || frameStyles[id].join('\n'),
}) {
id = String(id);
for (const styleId in frameStyles) {
if (id !== styleId &&
code === (frameStylesCode[styleId] || frameStyles[styleId].join('\n'))) {
return true;
}
}
}
function removeCSS(tabId, frameId, code) {
return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true})
.catch(onError);
}
})();