This commit is contained in:
tophf 2017-12-09 09:14:18 +03:00
parent 62053316a2
commit 7d094847f6
2 changed files with 128 additions and 174 deletions

View File

@ -193,18 +193,37 @@ window.addEventListener('storageReady', function _() {
updateIcon({id: undefined}, {}); updateIcon({id: undefined}, {});
if (FIREFOX) {
queryTabs().then(tabs =>
tabs.forEach(tab => {
if (!tab.width) {
// skip lazy-loaded tabs (width = 0) that seem to start loading on message
return;
}
const tabId = tab.id;
const frameUrls = {0: tab.url};
styleViaAPI.allFrameUrls.set(tabId, frameUrls);
chrome.webNavigation.getAllFrames({tabId}, frames => frames &&
frames.forEach(({frameId, parentFrameId, url}) => {
if (frameId) {
frameUrls[frameId] = url === 'about:blank' ? frameUrls[parentFrameId] : url;
}
}));
}));
return;
}
const NTP = 'chrome://newtab/'; const NTP = 'chrome://newtab/';
const ALL_URLS = '<all_urls>'; const ALL_URLS = '<all_urls>';
const contentScripts = chrome.runtime.getManifest().content_scripts; const contentScripts = chrome.runtime.getManifest().content_scripts;
if (!FIREFOX) { contentScripts.push({
contentScripts.push({ js: ['content/apply.js'],
js: ['content/apply.js'], matches: ['<all_urls>'],
matches: ['<all_urls>'], run_at: 'document_start',
run_at: 'document_start', match_about_blank: true,
match_about_blank: true, all_frames: true
all_frames: true });
});
}
// expand * as .*? // expand * as .*?
const wildcardAsRegExp = (s, flags) => new RegExp( const wildcardAsRegExp = (s, flags) => new RegExp(
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&') s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
@ -236,23 +255,9 @@ window.addEventListener('storageReady', function _() {
}; };
queryTabs().then(tabs => queryTabs().then(tabs =>
tabs.forEach(tab => { tabs.forEach(tab => tab.width &&
if (FIREFOX) { contentScripts.forEach(cs =>
const tabId = tab.id; setTimeout(pingCS, 0, cs, tab))));
const frameUrls = {'0': tab.url};
styleViaAPI.allFrameUrls.set(tabId, frameUrls);
chrome.webNavigation.getAllFrames({tabId}, frames => frames &&
frames.forEach(({frameId, parentFrameId, url}) => {
if (frameId) {
frameUrls[frameId] = url === 'about:blank' ? frameUrls[parentFrameId] : url;
}
}));
} else if (tab.width) {
// skip lazy-loaded aka unloaded tabs that seem to start loading on message
contentScripts.forEach(cs =>
setTimeout(pingCS, 0, cs, tab));
}
}));
}); });
// ************************************************************************* // *************************************************************************
@ -304,22 +309,27 @@ function webNavigationListenerChrome(method, data) {
function webNavigationListenerFF(method, data) { function webNavigationListenerFF(method, data) {
const {tabId, frameId, url} = data; const {tabId, frameId, url} = data;
if (url !== 'about:blank' || !frameId) { //console.log(method, data);
if (frameId === 0 || url !== 'about:blank') {
if ((!method || method === 'styleApply') &&
styleViaAPI.getFrameUrl(tabId, frameId) !== url) {
styleViaAPI.cache.delete(tabId);
}
styleViaAPI.setFrameUrl(tabId, frameId, url); styleViaAPI.setFrameUrl(tabId, frameId, url);
webNavigationListener(method, data); webNavigationListener(method, data);
return; return;
} }
const frames = styleViaAPI.allFrameUrls.get(tabId); //const frames = styleViaAPI.allFrameUrls.get(tabId);
if (Object.keys(frames).length === 1) { //if (Object.keys(frames).length === 1) {
frames[frameId] = frames['0']; // frames[frameId] = frames['0'];
webNavigationListener(method, data); // webNavigationListener(method, data);
return; // return;
} //}
chrome.webNavigation.getFrame({tabId, frameId}, info => { //chrome.webNavigation.getFrame({tabId, frameId}, info => {
const hasParent = !chrome.runtime.lastError && info.parentFrameId >= 0; // const hasParent = !chrome.runtime.lastError && info.parentFrameId >= 0;
frames[frameId] = hasParent ? frames[info.parentFrameId] : url; // frames[frameId] = hasParent ? frames[info.parentFrameId] : url;
webNavigationListener(method, data); // webNavigationListener(method, data);
}); //});
} }

View File

@ -9,7 +9,7 @@ var styleViaAPI = !CHROME &&
styleDeleted, styleDeleted,
styleUpdated, styleUpdated,
styleAdded, styleAdded,
styleReplaceAll, styleReplaceAll: styleApply,
prefChanged, prefChanged,
ping, ping,
}; };
@ -17,15 +17,11 @@ var styleViaAPI = !CHROME &&
const PONG = Promise.resolve(true); const PONG = Promise.resolve(true);
const onError = () => NOP; const onError = () => NOP;
/* <tabId>: Object
<frameId>: Object
url: String, non-enumerable
<styleId>: Array of strings
section code */
const cache = new Map(); const cache = new Map();
const allFrameUrls = new Map(); const allFrameUrls = new Map();
let observingTabs = false; chrome.tabs.onRemoved.addListener(onTabRemoved);
chrome.tabs.onReplaced.addListener(onTabReplaced);
return { return {
process, process,
@ -35,15 +31,23 @@ var styleViaAPI = !CHROME &&
cache, cache,
}; };
//////////////////// public //region public methods
function process(request, sender) { function process(request, sender) {
console.log(request.action || request.method, request.prefs || request.styles || request.style, sender.tab, sender.frameId);
const action = ACTIONS[request.action || request.method]; const action = ACTIONS[request.action || request.method];
return !action ? NOP : if (!action) {
isNaN(sender.frameId) && maybeProcessAllFrames(request, sender) || return NOP;
(action(request, sender) || NOP) }
.catch(onError) const {tab} = sender;
.then(maybeToggleObserver); if (!isNaN(sender.frameId)) {
const result = action(request, sender);
return result ? result.catch(onError) : NOP;
}
return browser.webNavigation.getAllFrames({tabId: tab.id}).then(frames =>
Promise.all((frames || []).map(({frameId}) =>
(action(request, {tab, frameId}) || NOP).catch(onError)))
).catch(onError);
} }
function getFrameUrl(tabId, frameId = 0) { function getFrameUrl(tabId, frameId = 0) {
@ -60,35 +64,33 @@ var styleViaAPI = !CHROME &&
} }
} }
//////////////////// actions //endregion
//region actions
function styleApply({id = null, styles, ignoreUrlCheck}, sender) { function styleApply({styles, disableAll}, sender) {
if (prefs.get('disableAll')) { if (disableAll) {
return; return;
} }
const {tab, frameId, url = getFrameUrl(tab.id, frameId)} = sender; const {tab: {id: tabId}, frameId, url} = sender;
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId); if (!styles || styles === 'DIY') {
if (id === null && !ignoreUrlCheck && frameStyles.url === url) { return requestStyles({matchUrl: url || getFrameUrl(tabId, frameId)}, sender);
return; }
const {tabFrames, frameStyles} = getCachedData(tabId, frameId);
const newSorted = getSortedById(styles);
if (!sameArrays(frameStyles, newSorted, sameArrays)) {
tabFrames[frameId] = newSorted;
cache.set(tabId, tabFrames);
return replaceCSS(tabId, frameId, frameStyles, newSorted);
} }
const apply = styles => {
const newFrameStyles = buildNewFrameStyles(styles, frameStyles);
if (newFrameStyles) {
tabFrames[frameId] = newFrameStyles;
cache.set(tab.id, tabFrames);
return replaceCSS(tab.id, frameId, frameStyles, newFrameStyles);
}
};
return styles ? apply(styles) || NOP :
getStyles({id, matchUrl: url, enabled: true, asHash: true}).then(apply);
} }
function styleDeleted({id}, {tab, frameId}) { function styleDeleted({id}, {tab, frameId}) {
const {frameStyles, styleSections} = getCachedData(tab.id, frameId, id); const {frameStyles} = getCachedData(tab.id, frameId);
if (styleSections.length) { const index = frameStyles.findIndex(item => item.id === id);
const oldFrameStyles = Object.assign({}, frameStyles); if (index >= 0) {
delete frameStyles[id]; const oldStyles = frameStyles.slice();
return replaceCSS(tab.id, frameId, oldFrameStyles, frameStyles); frameStyles.splice(index, 1);
return replaceCSS(tab.id, frameId, oldStyles, frameStyles);
} }
} }
@ -96,20 +98,13 @@ var styleViaAPI = !CHROME &&
return (style.enabled ? styleApply : styleDeleted)(style, sender); return (style.enabled ? styleApply : styleDeleted)(style, sender);
} }
function styleAdded({style}, sender) { function styleAdded({style: {enabled}}, sender) {
return style.enabled ? styleApply(style, sender) : NOP; return enabled && styleApply({}, sender);
}
function styleReplaceAll(request, sender) {
request.ignoreUrlCheck = true;
return styleApply(request, sender);
} }
function prefChanged({prefs}, sender) { function prefChanged({prefs}, sender) {
if ('disableAll' in prefs) { if ('disableAll' in prefs) {
disableAll(prefs.disableAll, sender); disableAll(prefs.disableAll, sender);
} else {
return NOP;
} }
} }
@ -117,7 +112,8 @@ var styleViaAPI = !CHROME &&
return PONG; return PONG;
} }
//////////////////// action helpers //endregion
//region action helpers
function disableAll(state, sender) { function disableAll(state, sender) {
if (state) { if (state) {
@ -126,113 +122,60 @@ var styleViaAPI = !CHROME &&
delete tabFrames[frameId]; delete tabFrames[frameId];
return removeCSS(tab.id, frameId, frameStyles); return removeCSS(tab.id, frameId, frameStyles);
} else { } else {
return styleApply({ignoreUrlCheck: true}, sender); return styleApply({}, sender);
} }
} }
//////////////////// observer //endregion
//region observer
function maybeToggleObserver(passthru) {
let method;
if (!observingTabs && cache.size) {
method = 'addListener';
} else if (observingTabs && !cache.size) {
method = 'removeListener';
} else {
return passthru;
}
observingTabs = !observingTabs;
chrome.webNavigation.onCommitted[method](onNavigationCommitted);
chrome.tabs.onRemoved[method](onTabRemoved);
chrome.tabs.onReplaced[method](onTabReplaced);
return passthru;
}
function onNavigationCommitted({tabId, frameId}) {
if (frameId === 0) {
onTabRemoved(tabId);
return;
}
const tabFrames = cache.get(tabId);
if (tabFrames && frameId in tabFrames) {
delete tabFrames[frameId];
if (isEmpty(tabFrames)) {
onTabRemoved(tabId);
}
}
}
function onTabRemoved(tabId) { function onTabRemoved(tabId) {
cache.delete(tabId); cache.delete(tabId);
maybeToggleObserver();
} }
function onTabReplaced(addedTabId, removedTabId) { function onTabReplaced(addedTabId, removedTabId) {
onTabRemoved(removedTabId); cache.delete(removedTabId);
} }
//////////////////// browser API //endregion
//region browser API
function replaceCSS(tabId, frameId, oldStyles, newStyles) { function replaceCSS(tabId, frameId, oldStyles, newStyles) {
console.log.apply(null, arguments);
return insertCSS(tabId, frameId, newStyles).then(() => return insertCSS(tabId, frameId, newStyles).then(() =>
removeCSS(tabId, frameId, oldStyles)); removeCSS(tabId, frameId, oldStyles));
} }
function insertCSS(tabId, frameId, frameStyles) { function insertCSS(tabId, frameId, frameStyles) {
const code = getFrameCode(frameStyles); const code = getFrameCode(frameStyles);
return code && browser.tabs.insertCSS(tabId, { return !code ? NOP :
// we cache a shallow copy of code from the sections array in order to reuse references browser.tabs.insertCSS(tabId, {
// in other places whereas the combined string gets garbage-collected code,
code, frameId,
frameId, runAt: 'document_start',
runAt: 'document_start', matchAboutBlank: true,
matchAboutBlank: true, }).catch(onError);
}).catch(onError);
} }
function removeCSS(tabId, frameId, frameStyles) { function removeCSS(tabId, frameId, frameStyles) {
const code = getFrameCode(frameStyles); const code = getFrameCode(frameStyles);
return code && browser.tabs.removeCSS(tabId, { return !code ? NOP :
code, browser.tabs.removeCSS(tabId, {
frameId, code,
matchAboutBlank: true frameId,
}).catch(onError); matchAboutBlank: true
}).catch(onError);
} }
//////////////////// utilities //endregion
//region utilities
function maybeProcessAllFrames(request, sender) { function requestStyles(options, sender) {
const {tab} = sender; options.matchUrl = options.matchUrl || sender.url;
const frameIds = Object.keys(allFrameUrls.get(tab.id) || {}); options.enabled = true;
if (frameIds.length <= 1) { options.asHash = true;
sender.frameId = 0; return getStyles(options).then(styles =>
return false; styleApply({styles}, sender));
} else {
return Promise.all(
frameIds.map(frameId =>
process(request, {tab, sender: {frameId: Number(frameId)}})));
}
}
function buildNewFrameStyles(styles, oldStyles, url) {
let allSame = true;
let newStyles = {};
for (const sections of getSortedById(styles)) {
const cachedSections = oldStyles[sections.id] || [];
const newSections = [];
let i = 0;
allSame &= sections.length === cachedSections.length;
for (const {code} of sections) {
allSame = allSame ? code === cachedSections[i] : allSame;
newSections[i++] = code;
}
newStyles[sections.id] = newSections;
}
if (!allSame) {
newStyles = Object.assign({}, oldStyles, newStyles);
defineProperty(newStyles, 'url', url);
return newStyles;
}
} }
function getSortedById(styleHash) { function getSortedById(styleHash) {
@ -242,9 +185,9 @@ var styleViaAPI = !CHROME &&
for (let k in styleHash) { for (let k in styleHash) {
k = parseInt(k); k = parseInt(k);
if (!isNaN(k)) { if (!isNaN(k)) {
const sections = styleHash[k]; const sections = styleHash[k].map(({code}) => code);
styles.push(sections); styles.push(sections);
Object.defineProperty(sections, 'id', {value: k}); defineProperty(sections, 'id', k);
needsSorting |= k < prevKey; needsSorting |= k < prevKey;
prevKey = k; prevKey = k;
} }
@ -254,23 +197,24 @@ var styleViaAPI = !CHROME &&
function getCachedData(tabId, frameId, styleId) { function getCachedData(tabId, frameId, styleId) {
const tabFrames = cache.get(tabId) || {}; const tabFrames = cache.get(tabId) || {};
const frameStyles = tabFrames[frameId] || {}; const frameStyles = tabFrames[frameId] || [];
const styleSections = styleId && frameStyles[styleId] || []; const styleSections = styleId && frameStyles.find(s => s.id === styleId) || [];
return {tabFrames, frameStyles, styleSections}; return {tabFrames, frameStyles, styleSections};
} }
function getFrameCode(frameStyles) { function getFrameCode(frameStyles) {
return [].concat(...getSortedById(frameStyles)).join('\n'); // we cache a shallow copy of code from the sections array in order to reuse references
// in other places whereas the combined string gets garbage-collected
return typeof frameStyles === 'string' ? frameStyles : [].concat(...frameStyles).join('\n');
} }
function defineProperty(obj, name, value) { function defineProperty(obj, name, value) {
return Object.defineProperty(obj, name, {value, configurable: true}); return Object.defineProperty(obj, name, {value, configurable: true});
} }
function isEmpty(obj) { function sameArrays(a, b, fn) {
for (const k in obj) { return a.length === b.length && a.every((el, i) => fn ? fn(el, b[i]) : el === b[i]);
return false;
}
return true;
} }
//endregion
})(); })();