fune/toolkit/components/extensions/ext-browser-content.js
Kris Maglione 0cd3e0a862 Bug 1368189: Move more code out of ExtensionUtils.jsm. r=mixedpuppy
Also removes some dead code.

A lot of the code in ExtensionUtils.jsm is not needed in all processes, and a
lot of the rest isn't needed until extension code runs. Most of it winds up
being loaded into all processes way earlier than necessary.

MozReview-Commit-ID: CMRjCPOjRF2

--HG--
extra : rebase_source : 37718eaf05a22b8ccb95f633cf7454bd7975cdce
2017-05-26 15:44:41 -07:00

294 lines
9.3 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionCommon",
"resource://gre/modules/ExtensionCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "require",
"resource://devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
getWinUtils,
} = ExtensionUtils;
/* eslint-env mozilla/frame-script */
// Minimum time between two resizes.
const RESIZE_TIMEOUT = 100;
/**
* Check if the provided color is fully opaque.
*
* @param {string} color
* Any valid CSS color.
* @returns {boolean} true if the color is opaque.
*/
const isOpaque = function(color) {
try {
if (/(rgba|hsla)/i.test(color)) {
// Match .123456, 123.456, 123456 with an optional % sign.
let numberRe = /(\.\d+|\d+\.?\d*)%?/g;
// hsla/rgba, opacity is the last number in the color string (can be a percentage).
let opacity = color.match(numberRe)[3];
// Convert to [0, 1] space if the opacity was expressed as a percentage.
if (opacity.includes("%")) {
opacity = opacity.slice(0, -1);
opacity = opacity / 100;
}
return opacity * 1 >= 1;
} else if (/^#[a-f0-9]{4}$/i.test(color)) {
// Hex color with 4 characters, opacity is one if last character is F
return color.toUpperCase().endsWith("F");
} else if (/^#[a-f0-9]{8}$/i.test(color)) {
// Hex color with 8 characters, opacity is one if last 2 characters are FF
return color.toUpperCase().endsWith("FF");
}
} catch (e) {
// Invalid color.
}
return true;
};
const BrowserListener = {
init({allowScriptsToClose, blockParser, fixedWidth, maxHeight, maxWidth, stylesheets, isInline}) {
this.fixedWidth = fixedWidth;
this.stylesheets = stylesheets || [];
this.isInline = isInline;
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
this.blockParser = blockParser;
this.needsResize = fixedWidth || maxHeight || maxWidth;
this.oldBackground = null;
if (allowScriptsToClose) {
getWinUtils(content).allowScriptsToClose();
}
// Force external links to open in tabs.
docShell.isAppTab = true;
if (this.blockParser) {
this.blockingPromise = new Promise(resolve => {
this.unblockParser = resolve;
});
addEventListener("DOMDocElementInserted", this, true);
}
addEventListener("load", this, true);
addEventListener("DOMWindowCreated", this, true);
addEventListener("DOMContentLoaded", this, true);
addEventListener("DOMWindowClose", this, true);
addEventListener("MozScrolledAreaChanged", this, true);
},
destroy() {
if (this.blockParser) {
removeEventListener("DOMDocElementInserted", this, true);
}
removeEventListener("load", this, true);
removeEventListener("DOMWindowCreated", this, true);
removeEventListener("DOMContentLoaded", this, true);
removeEventListener("DOMWindowClose", this, true);
removeEventListener("MozScrolledAreaChanged", this, true);
},
receiveMessage({name, data}) {
if (name === "Extension:InitBrowser") {
this.init(data);
} else if (name === "Extension:UnblockParser") {
if (this.unblockParser) {
this.unblockParser();
this.blockingPromise = null;
}
}
},
loadStylesheets() {
let winUtils = getWinUtils(content);
for (let url of this.stylesheets) {
winUtils.addSheet(ExtensionCommon.stylesheetMap.get(url), winUtils.AGENT_SHEET);
}
},
handleEvent(event) {
switch (event.type) {
case "DOMDocElementInserted":
if (this.blockingPromise) {
event.target.blockParsing(this.blockingPromise);
}
break;
case "DOMWindowCreated":
if (event.target === content.document) {
this.loadStylesheets();
}
break;
case "DOMWindowClose":
if (event.target === content) {
event.preventDefault();
sendAsyncMessage("Extension:DOMWindowClose");
}
break;
case "DOMContentLoaded":
if (event.target === content.document) {
sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href});
if (this.needsResize) {
this.handleDOMChange(true);
}
}
break;
case "load":
if (event.target.contentWindow === content) {
// For about:addons inline <browser>s, we currently receive a load
// event on the <browser> element, but no load or DOMContentLoaded
// events from the content window.
// Inline browsers don't receive the "DOMWindowCreated" event, so this
// is a workaround to load the stylesheets.
if (this.isInline) {
this.loadStylesheets();
}
sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href});
} else if (event.target !== content.document) {
break;
}
if (!this.needsResize) {
break;
}
// We use a capturing listener, so we get this event earlier than any
// load listeners in the content page. Resizing after a timeout ensures
// that we calculate the size after the entire event cycle has completed
// (unless someone spins the event loop, anyway), and hopefully after
// the content has made any modifications.
Promise.resolve().then(() => {
this.handleDOMChange(true);
});
// Mutation observer to make sure the panel shrinks when the content does.
new content.MutationObserver(this.handleDOMChange.bind(this)).observe(
content.document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
});
break;
case "MozScrolledAreaChanged":
if (this.needsResize) {
this.handleDOMChange();
}
break;
}
},
// Resizes the browser to match the preferred size of the content (debounced).
handleDOMChange(ignoreThrottling = false) {
if (ignoreThrottling && this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
this.resizeTimeout = null;
}
if (this.resizeTimeout == null) {
this.resizeTimeout = setTimeout(() => {
try {
if (content) {
this._handleDOMChange("delayed");
}
} finally {
this.resizeTimeout = null;
}
}, RESIZE_TIMEOUT);
this._handleDOMChange();
}
},
_handleDOMChange(detail) {
let doc = content.document;
let body = doc.body;
if (!body || doc.compatMode === "BackCompat") {
// In quirks mode, the root element is used as the scroll frame, and the
// body lies about its scroll geometry, and returns the values for the
// root instead.
body = doc.documentElement;
}
let result;
if (this.fixedWidth) {
// If we're in a fixed-width area (namely a slide-in subview of the main
// menu panel), we need to calculate the view height based on the
// preferred height of the content document's root scrollable element at the
// current width, rather than the complete preferred dimensions of the
// content window.
// Compensate for any offsets (margin, padding, ...) between the scroll
// area of the body and the outer height of the document.
let getHeight = elem => elem.getBoundingClientRect(elem).height;
let bodyPadding = getHeight(doc.documentElement) - getHeight(body);
let height = Math.ceil(body.scrollHeight + bodyPadding);
result = {height, detail};
} else {
let background = doc.defaultView.getComputedStyle(body).backgroundColor;
if (!isOpaque(background)) {
// Ignore non-opaque backgrounds.
background = null;
}
if (background !== this.oldBackground) {
sendAsyncMessage("Extension:BrowserBackgroundChanged", {background});
}
this.oldBackground = background;
// Adjust the size of the browser based on its content's preferred size.
let {contentViewer} = docShell;
let ratio = content.devicePixelRatio;
let w = {}, h = {};
contentViewer.getContentSizeConstrained(this.maxWidth * ratio,
this.maxHeight * ratio,
w, h);
let width = Math.ceil(w.value / ratio);
let height = Math.ceil(h.value / ratio);
result = {width, height, detail};
}
sendAsyncMessage("Extension:BrowserResized", result);
},
};
addMessageListener("Extension:InitBrowser", BrowserListener);
addMessageListener("Extension:UnblockParser", BrowserListener);