stylus/messaging.js
tophf 42f7b11bac broadcast only meta for styleUpdated/styleAdded
apply/popup/manage use only meta for these two methods,
editor may need the full code but can fetch it directly,
so we send just the meta to avoid spamming lots of tabs with huge styles
2017-04-18 12:46:36 +03:00

295 lines
8.1 KiB
JavaScript

/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
'use strict';
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
const KEEP_CHANNEL_OPEN = true;
const FIREFOX = /Firefox/.test(navigator.userAgent);
const OPERA = /OPR/.test(navigator.userAgent);
const URLS = {
ownOrigin: chrome.runtime.getURL(''),
optionsUI: [
chrome.runtime.getURL('options/index.html'),
'chrome://extensions/?options=' + chrome.runtime.id,
],
configureCommands:
OPERA ? 'opera://settings/configureCommands'
: 'chrome://extensions/configureCommands',
// CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
// https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
chromeWebStore: FIREFOX ? 'N/A' : 'https://chrome.google.com/webstore/',
};
const RX_SUPPORTED_URLS = new RegExp(
'^(file|ftps?|http)://|' +
`^https://${FIREFOX ? '' : '(?!chrome\\.google\\.com/webstore)'}|` +
'^' + URLS.ownOrigin);
let BG = chrome.extension.getBackgroundPage();
if (!BG || BG != window) {
document.documentElement.classList.toggle('firefox', FIREFOX);
document.documentElement.classList.toggle('opera', OPERA);
}
function notifyAllTabs(msg) {
const originalMessage = msg;
if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') {
// apply/popup/manage use only meta for these two methods,
// editor may need the full code but can fetch it directly,
// so we send just the meta to avoid spamming lots of tabs with huge styles
msg = Object.assign({}, msg, {
style: getStyleWithNoCode(msg.style)
});
}
const affectsAll = !msg.affects || msg.affects.all;
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
const affectsTabs = affectsAll || affectsOwnOriginOnly;
const affectsIcon = affectsAll || msg.affects.icon;
const affectsPopup = affectsAll || msg.affects.popup;
if (affectsTabs || affectsIcon) {
// list all tabs including chrome-extension:// which can be ours
chrome.tabs.query(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}, tabs => {
for (const tab of tabs) {
if (affectsTabs || URLS.optionsUI.includes(tab.url)) {
chrome.tabs.sendMessage(tab.id, msg);
}
if (affectsIcon && BG) {
BG.updateIcon(tab);
}
}
});
}
// notify self: the message no longer is sent to the origin in new Chrome
if (typeof onRuntimeMessage != 'undefined') {
onRuntimeMessage(originalMessage);
}
// notify apply.js on own pages
if (typeof applyOnMessage != 'undefined') {
applyOnMessage(originalMessage);
}
// notify background page and all open popups
if (affectsPopup || msg.prefs) {
chrome.runtime.sendMessage(msg);
}
}
function getActiveTab() {
return new Promise(resolve =>
chrome.tabs.query({currentWindow: true, active: true}, tabs =>
resolve(tabs[0])));
}
function getActiveTabRealURL() {
return getActiveTab()
.then(getTabRealURL);
}
function getTabRealURL(tab) {
return new Promise(resolve => {
if (tab.url != 'chrome://newtab/') {
resolve(tab.url);
} else {
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
resolve(frame && frame.url || '');
});
}
});
}
// opens a tab or activates the already opened one,
// reuses the New Tab page if it's focused now
function openURL({url, currentWindow = true}) {
if (!url.includes('://')) {
url = chrome.runtime.getURL(url);
}
return new Promise(resolve => {
// [some] chromium forks don't handle their fake branded protocols
url = url.replace(/^(opera|vivaldi)/, 'chrome');
// API doesn't handle the hash-fragment part
chrome.tabs.query({url: url.replace(/#.*/, ''), currentWindow}, tabs => {
for (const tab of tabs) {
if (tab.url == url) {
activateTab(tab).then(resolve);
return;
}
}
getActiveTab().then(tab => {
if (tab && tab.url == 'chrome://newtab/') {
chrome.tabs.update({url}, resolve);
} else {
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
}
});
});
});
}
function activateTab(tab) {
return Promise.all([
new Promise(resolve => {
chrome.tabs.update(tab.id, {active: true}, resolve);
}),
new Promise(resolve => {
chrome.windows.update(tab.windowId, {focused: true}, resolve);
}),
]);
}
function stringAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
}
function ignoreChromeError() {
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
}
function getStyleWithNoCode(style) {
const stripped = Object.assign({}, style, {sections: []});
for (const section of style.sections) {
stripped.sections.push(Object.assign({}, section, {code: null}));
}
return stripped;
}
// js engine can't optimize the entire function if it contains try-catch
// so we should keep it isolated from normal code in a minimal wrapper
// Update: might get fixed in V8 TurboFan in the future
function tryCatch(func, ...args) {
try {
return func(...args);
} catch (e) {}
}
function tryRegExp(regexp) {
try {
return new RegExp(regexp);
} catch (e) {}
}
function tryJSONparse(jsonString) {
try {
return JSON.parse(jsonString);
} catch (e) {}
}
function debounce(fn, delay, ...args) {
const timers = debounce.timers = debounce.timers || new Map();
debounce.run = debounce.run || ((fn, ...args) => {
timers.delete(fn);
fn(...args);
});
clearTimeout(timers.get(fn));
timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
}
function deepCopy(obj) {
return obj !== null && obj !== undefined && typeof obj == 'object'
? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj)
: obj;
}
function deepMerge(target, ...args) {
const isArray = typeof target.slice == 'function';
for (const obj of args) {
if (isArray && obj !== null && obj !== undefined) {
for (const element of obj) {
target.push(deepCopy(element));
}
continue;
}
for (const k in obj) {
const value = obj[k];
if (k in target && typeof value == 'object' && value !== null) {
deepMerge(target[k], value);
} else {
target[k] = deepCopy(value);
}
}
}
return target;
}
function sessionStorageHash(name) {
return {
name,
value: tryCatch(JSON.parse, sessionStorage[name]) || {},
set(k, v) {
this.value[k] = v;
this.updateStorage();
},
unset(k) {
delete this.value[k];
this.updateStorage();
},
updateStorage() {
sessionStorage[this.name] = JSON.stringify(this.value);
}
};
}
function onBackgroundReady() {
return BG ? Promise.resolve() : new Promise(ping);
function ping(resolve) {
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
if (health !== undefined) {
BG = chrome.extension.getBackgroundPage();
resolve();
} else {
ping(resolve);
}
});
}
}
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
function getStylesSafe(options) {
return new Promise(resolve => {
if (BG) {
BG.getStyles(options, resolve);
} else {
onBackgroundReady().then(() =>
BG.getStyles(options, resolve));
}
});
}
function saveStyleSafe(style) {
return onBackgroundReady()
.then(() => BG.saveStyle(BG.deepCopy(style)))
.then(savedStyle => {
if (style.notify === false) {
handleUpdate(savedStyle, style);
}
return savedStyle;
});
}
function deleteStyleSafe({id, notify = true} = {}) {
return onBackgroundReady()
.then(() => BG.deleteStyle({id, notify}))
.then(() => {
if (!notify) {
handleDelete(id);
}
return id;
});
}