Add: navigator-util

This commit is contained in:
eight 2018-10-07 21:20:39 +08:00
parent b5107b78a5
commit 5b3b4e680f
10 changed files with 121 additions and 381 deletions

View File

@ -38,13 +38,14 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
// in the foreground thus auto-closing the popup (in Chrome)
openURL,
// FIXME: who use this?
closeTab: (msg, sender, respond) => {
chrome.tabs.remove(msg.tabId || sender.tab.id, () => {
if (chrome.runtime.lastError && msg.tabId !== sender.tab.id) {
respond(new Error(chrome.runtime.lastError.message));
}
});
return KEEP_CHANNEL_OPEN;
return true;
},
optionsCustomizeHotkeys() {
@ -71,6 +72,7 @@ if (FIREFOX) {
const frameId = port.sender.frameId;
const options = tryJSONparse(port.name.slice(MSG_GET_STYLES_LEN));
port.disconnect();
// FIXME: getStylesFallback?
getStyles(options).then(styles => {
if (!styles.length) return;
chrome.tabs.executeScript(tabId, {
@ -87,39 +89,28 @@ if (FIREFOX) {
});
}
{
const listener =
URLS.chromeProtectsNTP
? webNavigationListenerChrome
: webNavigationListener;
chrome.webNavigation.onBeforeNavigate.addListener(data =>
listener(null, data));
chrome.webNavigation.onCommitted.addListener(data =>
listener('styleApply', data));
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
listener('styleReplaceAll', data));
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
listener('styleReplaceAll', data));
if (FIREFOX) {
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
chrome.webNavigation.onCommitted.addListener(webNavUsercssInstallerFF, {
url: [
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.css'},
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.styl'},
]
});
// FF misses some about:blank iframes so we inject our content script explicitly
chrome.webNavigation.onDOMContentLoaded.addListener(webNavIframeHelperFF, {
url: [
{urlEquals: 'about:blank'},
]
});
navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
if (type === 'committed') {
// styles would be updated when content script is injected.
return;
}
msg.sendTab(tabId, {method: 'urlChanged'}, {frameId});
});
if (FIREFOX) {
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
navigatorUtil.onCommitted(webNavUsercssInstallerFF, {
url: [
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.css'},
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.styl'},
]
});
// FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
url: [
{urlEquals: 'about:blank'},
]
});
}
if (chrome.contextMenus) {
@ -149,6 +140,8 @@ prefs.subscribe(['iconset'], () =>
styles: {},
}));
chrome.navigator.
// *************************************************************************
chrome.runtime.onInstalled.addListener(({reason}) => {
if (reason !== 'update') return;
@ -297,74 +290,7 @@ window.addEventListener('storageReady', function _() {
}));
});
// *************************************************************************
{
const getStylesForFrame = (msg, sender) => {
const stylesTask = getStyles(msg);
if (!sender || !sender.frameId) return stylesTask;
return Promise.all([
stylesTask,
getTab(sender.tab.id),
]).then(([styles, tab]) => {
if (tab) styles.exposeIframes = tab.url.replace(/(\/\/[^/]*).*/, '$1');
return styles;
});
};
const updateAPI = (_, enabled) => {
window.API_METHODS.getStylesForFrame = enabled ? getStylesForFrame : getStyles;
};
prefs.subscribe(['exposeIframes'], updateAPI);
updateAPI(null, prefs.readOnlyValues.exposeIframes);
}
// *************************************************************************
function webNavigationListener(method, {url, tabId, frameId}) {
Promise.all([
getStyles({matchUrl: url, asHash: true}),
frameId && prefs.readOnlyValues.exposeIframes && getTab(tabId),
]).then(([styles, tab]) => {
if (method && URLS.supported(url) && tabId >= 0) {
if (method === 'styleApply') {
handleCssTransitionBug({tabId, frameId, url, styles});
}
if (tab) styles.exposeIframes = tab.url.replace(/(\/\/[^/]*).*/, '$1');
msg.sendTab(
tabId,
{
method,
// ping own page so it retrieves the styles directly
styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
},
{frameId}
);
}
// main page frame id is 0
if (frameId === 0) {
tabIcons.delete(tabId);
updateIcon({tab: {id: tabId, url}, styles});
}
});
}
function webNavigationListenerChrome(method, data) {
// Chrome 61.0.3161+ doesn't run content scripts on NTP
if (
!data.url.startsWith('https://www.google.') ||
!data.url.includes('/_/chrome/newtab?')
) {
webNavigationListener(method, data);
return;
}
getTab(data.tabId).then(tab => {
if (tab.url === 'chrome://newtab/') {
data.url = tab.url;
}
webNavigationListener(method, data);
});
}
// FIXME: implement exposeIframes in apply.js
function webNavUsercssInstallerFF(data) {
const {tabId} = data;

View File

@ -0,0 +1,68 @@
'use strict';
const navigatorUtil = (() => {
const handler = {
urlChange: null
};
let listeners;
const tabGet = promisify(chrome.tabs.get.bind(chrome.tabs));
return extendNative({onUrlChange});
function onUrlChange(fn) {
initUrlChange();
handler.urlChange.push(fn);
}
function initUrlChange() {
if (!handler.urlChange) {
return;
}
handler.urlChange = [];
chrome.webNavigation.onCommitted.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'committed'));
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated'));
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated'));
}
function fixNTPUrl(data) {
if (
!CHROME ||
!URLS.chromeProtectsNTP ||
!data.url.startsWith('https://www.google.') ||
!data.url.includes('/_/chrome/newtab?')
) {
return Promise.resolve();
}
return tabGet(data.tabId)
.then(tab => {
if (tab.url === 'chrome://newtab/') {
data.url = tab.url;
}
});
}
function executeCallbacks(callbacks, data, type) {
for (const cb of callbacks) {
cb(data, type);
}
}
function extendNative(target) {
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) {
return target[prop];
}
return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]);
}
});
}
})();

View File

@ -1,226 +0,0 @@
/*
global API_METHODS cachedStyles
global getStyles filterStyles invalidateCache normalizeStyleSections
global updateIcon
*/
'use strict';
(() => {
const previewFromTabs = new Map();
/**
* When style id and state is provided, only that style is propagated.
* Otherwise all styles are replaced and the toolbar icon is updated.
* @param {Object} [msg]
* @param {{id:Number, enabled?:Boolean, sections?: (Array|String)}} [msg.style] -
* style to propagate
* @param {Boolean} [msg.codeIsUpdated]
* @returns {Promise<void>}
*/
API_METHODS.refreshAllTabs = (msg = {}) =>
Promise.all([
queryTabs(),
maybeParseUsercss(msg),
getStyles(),
]).then(([tabs, style]) =>
new Promise(resolve => {
if (style) msg.style.sections = normalizeStyleSections(style);
run(tabs, msg, resolve);
}));
function run(tabs, msg, resolve) {
const {style, codeIsUpdated, refreshOwnTabs} = msg;
// the style was updated/saved so we need to remove the old copy of the original style
if (msg.method === 'styleUpdated' && msg.reason !== 'editPreview') {
for (const [tabId, original] of previewFromTabs.entries()) {
if (style.id === original.id) {
previewFromTabs.delete(tabId);
}
}
if (!previewFromTabs.size) {
unregisterTabListeners();
}
}
if (!style) {
msg = {method: 'styleReplaceAll'};
// live preview puts the code in cachedStyles, saves the original in previewFromTabs,
// and if preview is being disabled, but the style is already deleted, we bail out
} else if (msg.reason === 'editPreview' && !updateCache(msg)) {
return;
// simple style update:
// * if disabled, apply.js will remove the element
// * if toggled and code is unchanged, apply.js will toggle the element
} else if (!style.enabled || codeIsUpdated === false) {
msg = {
method: 'styleUpdated',
reason: msg.reason,
style: {
id: style.id,
enabled: style.enabled,
},
codeIsUpdated,
};
// live preview normal operation, the new code is already in cachedStyles
} else {
msg.method = 'styleApply';
msg.style = {id: msg.style.id};
}
if (!tabs || !tabs.length) {
resolve();
return;
}
const last = tabs[tabs.length - 1];
for (const tab of tabs) {
if (FIREFOX && !tab.width) continue;
if (refreshOwnTabs === false && tab.url.startsWith(URLS.ownOrigin)) continue;
chrome.webNavigation.getAllFrames({tabId: tab.id}, frames =>
refreshFrame(tab, frames, msg, tab === last && resolve));
}
}
function refreshFrame(tab, frames, msg, resolve) {
ignoreChromeError();
if (!frames || !frames.length) {
frames = [{
frameId: 0,
url: tab.url,
}];
}
msg.tabId = tab.id;
const styleId = msg.style && msg.style.id;
for (const frame of frames) {
const styles = filterStyles({
matchUrl: getFrameUrl(frame, frames),
asHash: true,
id: styleId,
});
msg = Object.assign({}, msg);
msg.frameId = frame.frameId;
if (msg.method !== 'styleUpdated') {
msg.styles = styles;
}
if (msg.method === 'styleApply' && !styles.length) {
// remove the style from a previously matching frame
invokeOrPostpone(tab.active, sendMessage, {
method: 'styleUpdated',
reason: 'editPreview',
style: {
id: styleId,
enabled: false,
},
tabId: tab.id,
frameId: frame.frameId,
}, ignoreChromeError);
} else {
invokeOrPostpone(tab.active, sendMessage, msg, ignoreChromeError);
}
if (!frame.frameId) {
setTimeout(updateIcon, 0, {
tab,
styles: msg.method === 'styleReplaceAll' ? styles : undefined,
});
}
}
if (resolve) resolve();
}
function getFrameUrl(frame, frames) {
while (frame.url === 'about:blank' && frame.frameId > 0) {
const parent = frames.find(f => f.frameId === frame.parentFrameId);
if (!parent) break;
frame.url = parent.url;
frame = parent;
}
return (frame || frames[0]).url;
}
function maybeParseUsercss({style}) {
if (style && typeof style.sections === 'string') {
return API_METHODS.parseUsercss({sourceCode: style.sections});
}
}
function updateCache(msg) {
const {style, tabId, restoring} = msg;
const spoofed = !restoring && previewFromTabs.get(tabId);
const original = cachedStyles.byId.get(style.id);
if (style.sections && !restoring) {
if (!previewFromTabs.size) {
registerTabListeners();
}
if (!spoofed) {
previewFromTabs.set(tabId, Object.assign({}, original));
}
} else {
previewFromTabs.delete(tabId);
if (!previewFromTabs.size) {
unregisterTabListeners();
}
if (!original) {
return;
}
if (!restoring) {
msg.style = spoofed || original;
}
}
invalidateCache({updated: msg.style});
return true;
}
function registerTabListeners() {
chrome.tabs.onRemoved.addListener(onTabRemoved);
chrome.tabs.onReplaced.addListener(onTabReplaced);
chrome.webNavigation.onCommitted.addListener(onTabNavigated);
}
function unregisterTabListeners() {
chrome.tabs.onRemoved.removeListener(onTabRemoved);
chrome.tabs.onReplaced.removeListener(onTabReplaced);
chrome.webNavigation.onCommitted.removeListener(onTabNavigated);
}
function onTabRemoved(tabId) {
const style = previewFromTabs.get(tabId);
if (style) {
API_METHODS.refreshAllTabs({
style,
tabId,
reason: 'editPreview',
restoring: true,
});
}
}
function onTabReplaced(addedTabId, removedTabId) {
onTabRemoved(removedTabId);
}
function onTabNavigated({tabId}) {
onTabRemoved(tabId);
}
})();

View File

@ -154,11 +154,12 @@ const styleManager = (() => {
}
function ensurePrepared(methods) {
for (const [name, fn] in Object.entries(methods)) {
methods[name] = (...args) =>
const prepared = {};
for (const [name, fn] of Object.entries(methods)) {
prepared[name] = (...args) =>
preparing.then(() => fn(...args));
}
return methods;
return prepared;
}
function createNewStyle() {

View File

@ -38,16 +38,14 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
return NOP;
}
return getStyles({id, matchUrl: url, asHash: true}).then(styles => {
const filter = {enabled: true};
if (id !== null) {
filter.id = id;
}
return styleManager.getSectionsByUrl(url, filter).then(sections => {
const tasks = [];
for (const styleId in styles) {
if (isNaN(parseInt(styleId))) {
continue;
}
// shallow-extract code from the sections array in order to reuse references
// in other places whereas the combined string gets garbage-collected
const styleSections = styles[styleId].map(section => section.code);
const code = styleSections.join('\n');
for (const section of Object.values(sections)) {
const code = section.code;
if (!code) {
delete frameStyles[styleId];
continue;
@ -55,7 +53,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
if (code === (frameStyles[styleId] || []).join('\n')) {
continue;
}
frameStyles[styleId] = styleSections;
frameStyles[styleId] = [code];
tasks.push(
browser.tabs.insertCSS(tab.id, {
code,

View File

@ -51,7 +51,7 @@ global API_METHODS
checkingAll = true;
retrying.clear();
const port = observe && chrome.runtime.connect({name: 'updater'});
return getStyles({}).then(styles => {
return styleManager.getAllStyles().then(styles => {
styles = styles.filter(style => style.updateUrl);
if (port) port.postMessage({count: styles.length});
log('');

View File

@ -147,6 +147,10 @@
}
break;
case 'urlChanged':
// TODO
break;
case 'ping':
return true;
}

View File

@ -5,9 +5,6 @@ global onRuntimeMessage applyOnMessage
*/
'use strict';
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
const KEEP_CHANNEL_OPEN = true;
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
const OPERA = Boolean(chrome.app) && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
const VIVALDI = Boolean(chrome.app) && navigator.userAgent.includes('Vivaldi');
@ -72,14 +69,9 @@ const URLS = {
),
};
let BG = chrome.extension.getBackgroundPage();
if (BG && !BG.getStyles && BG !== window) {
// own page like editor/manage is being loaded on browser startup
// before the background page has been fully initialized;
// it'll be resolved in onBackgroundReady() instead
BG = null;
}
if (!BG || BG !== window) {
const IS_BG = chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window;
if (!IS_BG) {
if (FIREFOX) {
document.documentElement.classList.add('firefox');
} else if (OPERA) {
@ -93,8 +85,10 @@ if (!BG || BG !== window) {
getActiveTab().then(tab =>
window.API.updateIcon({tab}));
}
} else if (!BG.API_METHODS) {
BG.API_METHODS = {};
}
if (IS_BG) {
window.API_METHODS = {};
}
const FIREFOX_NO_DOM_STORAGE = FIREFOX && !tryCatch(() => localStorage);
@ -104,32 +98,6 @@ if (FIREFOX_NO_DOM_STORAGE) {
Object.defineProperty(window, 'sessionStorage', {value: {}});
}
function sendMessage(msg, callback) {
/*
Promise mode [default]:
- rejects on receiving {__ERROR__: message} created by background.js::onRuntimeMessage
- automatically suppresses chrome.runtime.lastError because it's autogenerated
by browserAction.setText which lacks a callback param in chrome API
Standard callback mode:
- enabled by passing a second param
*/
const {tabId, frameId} = msg;
const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage;
const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg];
if (callback) {
fn(...args, callback);
} else {
return new Promise((resolve, reject) => {
fn(...args, r => {
const err = r && r.__ERROR__;
(err ? reject : resolve)(err || r);
ignoreChromeError();
});
});
}
}
function queryTabs(options = {}) {
return new Promise(resolve =>
chrome.tabs.query(options, tabs =>

View File

@ -3,6 +3,7 @@
// eslint-disable-next-line no-var
var prefs = new function Prefs() {
const BG = undefined;
const defaults = {
'openEditInWindow': false, // new editor opens in a own browser window
'windowPosition': {}, // detached window position

View File

@ -36,12 +36,12 @@
"js/cache.js",
"background/db.js",
"background/style-manager.js",
"background/navigator-util.js",
"background/background.js",
"background/usercss-helper.js",
"background/style-via-api.js",
"background/search-db.js",
"background/update.js",
"background/refresh-all-tabs.js",
"background/openusercss-api.js",
"vendor/semver-bundle/semver.js",
"vendor-overwrites/colorpicker/colorconverter.js"