summaryrefslogtreecommitdiff
path: root/toolkit/components/reader/AboutReader.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/reader/AboutReader.jsm')
-rw-r--r--toolkit/components/reader/AboutReader.jsm997
1 files changed, 997 insertions, 0 deletions
diff --git a/toolkit/components/reader/AboutReader.jsm b/toolkit/components/reader/AboutReader.jsm
new file mode 100644
index 0000000000..1fb9db1233
--- /dev/null
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -0,0 +1,997 @@
+/* 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";
+
+var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AboutReader" ];
+
+Cu.import("resource://gre/modules/ReaderMode.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncPrefs", "resource://gre/modules/AsyncPrefs.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
+
+var AboutReader = function(mm, win, articlePromise) {
+ let url = this._getOriginalUrl(win);
+ if (!(url.startsWith("http://") || url.startsWith("https://"))) {
+ let errorMsg = "Only http:// and https:// URLs can be loaded in about:reader.";
+ if (Services.prefs.getBoolPref("reader.errors.includeURLs"))
+ errorMsg += " Tried to load: " + url + ".";
+ Cu.reportError(errorMsg);
+ win.location.href = "about:blank";
+ return;
+ }
+
+ let doc = win.document;
+
+ this._mm = mm;
+ this._mm.addMessageListener("Reader:CloseDropdown", this);
+ this._mm.addMessageListener("Reader:AddButton", this);
+ this._mm.addMessageListener("Reader:RemoveButton", this);
+ this._mm.addMessageListener("Reader:GetStoredArticleData", this);
+
+ this._docRef = Cu.getWeakReference(doc);
+ this._winRef = Cu.getWeakReference(win);
+ this._innerWindowId = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+
+ this._article = null;
+
+ if (articlePromise) {
+ this._articlePromise = articlePromise;
+ }
+
+ this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header"));
+ this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain"));
+ this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title"));
+ this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits"));
+ this._contentElementRef = Cu.getWeakReference(doc.getElementById("moz-reader-content"));
+ this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
+ this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
+
+ this._scrollOffset = win.pageYOffset;
+
+ doc.addEventListener("click", this, false);
+
+ win.addEventListener("pagehide", this, false);
+ win.addEventListener("scroll", this, false);
+ win.addEventListener("resize", this, false);
+
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+
+ doc.addEventListener("visibilitychange", this, false);
+
+ this._setupStyleDropdown();
+ this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
+
+ const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+ if (gIsFirefoxDesktop) {
+ // we're ready for any external setup, send a signal for that.
+ this._mm.sendAsyncMessage("Reader:OnSetup");
+ }
+
+ let colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values"));
+ let colorSchemeOptions = colorSchemeValues.map((value) => {
+ return { name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
+ value: value,
+ itemClass: value + "-button" };
+ });
+
+ let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
+ this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorSchemePref.bind(this));
+ this._setColorSchemePref(colorScheme);
+
+ let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample");
+ let fontTypeOptions = [
+ { name: fontTypeSample,
+ description: gStrings.GetStringFromName("aboutReader.fontType.sans-serif"),
+ value: "sans-serif",
+ itemClass: "sans-serif-button"
+ },
+ { name: fontTypeSample,
+ description: gStrings.GetStringFromName("aboutReader.fontType.serif"),
+ value: "serif",
+ itemClass: "serif-button" },
+ ];
+
+ let fontType = Services.prefs.getCharPref("reader.font_type");
+ this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this));
+ this._setFontType(fontType);
+
+ this._setupFontSizeButtons();
+
+ this._setupContentWidthButtons();
+
+ this._setupLineHeightButtons();
+
+ if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
+ new NarrateControls(mm, win);
+ }
+
+ this._loadArticle();
+};
+
+AboutReader.prototype = {
+ _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " +
+ ".content p > a:only-child > img:only-child, " +
+ ".content .wp-caption img, " +
+ ".content figure img",
+
+ get _doc() {
+ return this._docRef.get();
+ },
+
+ get _win() {
+ return this._winRef.get();
+ },
+
+ get _headerElement() {
+ return this._headerElementRef.get();
+ },
+
+ get _domainElement() {
+ return this._domainElementRef.get();
+ },
+
+ get _titleElement() {
+ return this._titleElementRef.get();
+ },
+
+ get _creditsElement() {
+ return this._creditsElementRef.get();
+ },
+
+ get _contentElement() {
+ return this._contentElementRef.get();
+ },
+
+ get _toolbarElement() {
+ return this._toolbarElementRef.get();
+ },
+
+ get _messageElement() {
+ return this._messageElementRef.get();
+ },
+
+ get _isToolbarVertical() {
+ if (this._toolbarVertical !== undefined) {
+ return this._toolbarVertical;
+ }
+ return this._toolbarVertical = Services.prefs.getBoolPref("reader.toolbar.vertical");
+ },
+
+ // Provides unique view Id.
+ get viewId() {
+ let _viewId = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ Object.defineProperty(this, "viewId", { value: _viewId });
+
+ return _viewId;
+ },
+
+ receiveMessage: function (message) {
+ switch (message.name) {
+ // Triggered by Android user pressing BACK while the banner font-dropdown is open.
+ case "Reader:CloseDropdown": {
+ // Just close it.
+ this._closeDropdowns();
+ break;
+ }
+
+ case "Reader:AddButton": {
+ if (message.data.id && message.data.image &&
+ !this._doc.getElementById(message.data.id)) {
+ let btn = this._doc.createElement("button");
+ btn.setAttribute("class", "button");
+ btn.setAttribute("style", "background-image: url('" + message.data.image + "')");
+ btn.setAttribute("id", message.data.id);
+ if (message.data.title)
+ btn.setAttribute("title", message.data.title);
+ if (message.data.text)
+ btn.textContent = message.data.text;
+ let tb = this._doc.getElementById("reader-toolbar");
+ tb.appendChild(btn);
+ this._setupButton(message.data.id, button => {
+ this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article });
+ });
+ }
+ break;
+ }
+ case "Reader:RemoveButton": {
+ if (message.data.id) {
+ let btn = this._doc.getElementById(message.data.id);
+ if (btn)
+ btn.remove();
+ }
+ break;
+ }
+ case "Reader:GetStoredArticleData": {
+ this._mm.sendAsyncMessage("Reader:StoredArticleData", { article: this._article });
+ }
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ if (!aEvent.isTrusted)
+ return;
+
+ switch (aEvent.type) {
+ case "click":
+ let target = aEvent.target;
+ if (target.classList.contains('dropdown-toggle')) {
+ this._toggleDropdownClicked(aEvent);
+ } else if (!target.closest('.dropdown-popup')) {
+ this._closeDropdowns();
+ }
+ break;
+ case "scroll":
+ this._closeDropdowns(true);
+ let isScrollingUp = this._scrollOffset > aEvent.pageY;
+ this._setSystemUIVisibility(isScrollingUp);
+ this._scrollOffset = aEvent.pageY;
+ break;
+ case "resize":
+ this._updateImageMargins();
+ if (this._isToolbarVertical) {
+ this._win.setTimeout(() => {
+ for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) {
+ this._updatePopupPosition(dropdown);
+ }
+ }, 0);
+ }
+ break;
+
+ case "devicelight":
+ this._handleDeviceLight(aEvent.value);
+ break;
+
+ case "visibilitychange":
+ this._handleVisibilityChange();
+ break;
+
+ case "pagehide":
+ // Close the Banners Font-dropdown, cleanup Android BackPressListener.
+ this._closeDropdowns();
+
+ this._mm.removeMessageListener("Reader:CloseDropdown", this);
+ this._mm.removeMessageListener("Reader:AddButton", this);
+ this._mm.removeMessageListener("Reader:RemoveButton", this);
+ this._mm.removeMessageListener("Reader:GetStoredArticleData", this);
+ this._windowUnloaded = true;
+ break;
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
+ return;
+ }
+
+ Services.obs.removeObserver(this, "inner-window-destroyed", false);
+
+ this._mm.removeMessageListener("Reader:CloseDropdown", this);
+ this._mm.removeMessageListener("Reader:AddButton", this);
+ this._mm.removeMessageListener("Reader:RemoveButton", this);
+ this._windowUnloaded = true;
+ },
+
+ _onReaderClose: function() {
+ ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
+ },
+
+ _setFontSize: function(newFontSize) {
+ let containerClasses = this._doc.getElementById("container").classList;
+
+ if (this._fontSize > 0)
+ containerClasses.remove("font-size" + this._fontSize);
+
+ this._fontSize = newFontSize;
+ containerClasses.add("font-size" + this._fontSize);
+ return AsyncPrefs.set("reader.font_size", this._fontSize);
+ },
+
+ _setupFontSizeButtons: function() {
+ const FONT_SIZE_MIN = 1;
+ const FONT_SIZE_MAX = 9;
+
+ // Sample text shown in Android UI.
+ let sampleText = this._doc.getElementById("font-size-sample");
+ sampleText.textContent = gStrings.GetStringFromName("aboutReader.fontTypeSample");
+
+ let currentSize = Services.prefs.getIntPref("reader.font_size");
+ currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
+
+ let plusButton = this._doc.getElementById("font-size-plus");
+ let minusButton = this._doc.getElementById("font-size-minus");
+
+ function updateControls() {
+ if (currentSize === FONT_SIZE_MIN) {
+ minusButton.setAttribute("disabled", true);
+ } else {
+ minusButton.removeAttribute("disabled");
+ }
+ if (currentSize === FONT_SIZE_MAX) {
+ plusButton.setAttribute("disabled", true);
+ } else {
+ plusButton.removeAttribute("disabled");
+ }
+ }
+
+ updateControls();
+ this._setFontSize(currentSize);
+
+ plusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentSize >= FONT_SIZE_MAX) {
+ return;
+ }
+
+ currentSize++;
+ updateControls();
+ this._setFontSize(currentSize);
+ }, true);
+
+ minusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentSize <= FONT_SIZE_MIN) {
+ return;
+ }
+
+ currentSize--;
+ updateControls();
+ this._setFontSize(currentSize);
+ }, true);
+ },
+
+ _setContentWidth: function(newContentWidth) {
+ let containerClasses = this._doc.getElementById("container").classList;
+
+ if (this._contentWidth > 0)
+ containerClasses.remove("content-width" + this._contentWidth);
+
+ this._contentWidth = newContentWidth;
+ containerClasses.add("content-width" + this._contentWidth);
+ return AsyncPrefs.set("reader.content_width", this._contentWidth);
+ },
+
+ _setupContentWidthButtons: function() {
+ const CONTENT_WIDTH_MIN = 1;
+ const CONTENT_WIDTH_MAX = 9;
+
+ let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
+ currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth));
+
+ let plusButton = this._doc.getElementById("content-width-plus");
+ let minusButton = this._doc.getElementById("content-width-minus");
+
+ function updateControls() {
+ if (currentContentWidth === CONTENT_WIDTH_MIN) {
+ minusButton.setAttribute("disabled", true);
+ } else {
+ minusButton.removeAttribute("disabled");
+ }
+ if (currentContentWidth === CONTENT_WIDTH_MAX) {
+ plusButton.setAttribute("disabled", true);
+ } else {
+ plusButton.removeAttribute("disabled");
+ }
+ }
+
+ updateControls();
+ this._setContentWidth(currentContentWidth);
+
+ plusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentContentWidth >= CONTENT_WIDTH_MAX) {
+ return;
+ }
+
+ currentContentWidth++;
+ updateControls();
+ this._setContentWidth(currentContentWidth);
+ }, true);
+
+ minusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentContentWidth <= CONTENT_WIDTH_MIN) {
+ return;
+ }
+
+ currentContentWidth--;
+ updateControls();
+ this._setContentWidth(currentContentWidth);
+ }, true);
+ },
+
+ _setLineHeight: function(newLineHeight) {
+ let contentClasses = this._doc.getElementById("moz-reader-content").classList;
+
+ if (this._lineHeight > 0)
+ contentClasses.remove("line-height" + this._lineHeight);
+
+ this._lineHeight = newLineHeight;
+ contentClasses.add("line-height" + this._lineHeight);
+ return AsyncPrefs.set("reader.line_height", this._lineHeight);
+ },
+
+ _setupLineHeightButtons: function() {
+ const LINE_HEIGHT_MIN = 1;
+ const LINE_HEIGHT_MAX = 9;
+
+ let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
+ currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight));
+
+ let plusButton = this._doc.getElementById("line-height-plus");
+ let minusButton = this._doc.getElementById("line-height-minus");
+
+ function updateControls() {
+ if (currentLineHeight === LINE_HEIGHT_MIN) {
+ minusButton.setAttribute("disabled", true);
+ } else {
+ minusButton.removeAttribute("disabled");
+ }
+ if (currentLineHeight === LINE_HEIGHT_MAX) {
+ plusButton.setAttribute("disabled", true);
+ } else {
+ plusButton.removeAttribute("disabled");
+ }
+ }
+
+ updateControls();
+ this._setLineHeight(currentLineHeight);
+
+ plusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentLineHeight >= LINE_HEIGHT_MAX) {
+ return;
+ }
+
+ currentLineHeight++;
+ updateControls();
+ this._setLineHeight(currentLineHeight);
+ }, true);
+
+ minusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentLineHeight <= LINE_HEIGHT_MIN) {
+ return;
+ }
+
+ currentLineHeight--;
+ updateControls();
+ this._setLineHeight(currentLineHeight);
+ }, true);
+ },
+
+ _handleDeviceLight: function(newLux) {
+ // Desired size of the this._luxValues array.
+ let luxValuesSize = 10;
+ // Add new lux value at the front of the array.
+ this._luxValues.unshift(newLux);
+ // Add new lux value to this._totalLux for averaging later.
+ this._totalLux += newLux;
+
+ // Don't update when length of array is less than luxValuesSize except when it is 1.
+ if (this._luxValues.length < luxValuesSize) {
+ // Use the first lux value to set the color scheme until our array equals luxValuesSize.
+ if (this._luxValues.length == 1) {
+ this._updateColorScheme(newLux);
+ }
+ return;
+ }
+ // Holds the average of the lux values collected in this._luxValues.
+ let averageLuxValue = this._totalLux/luxValuesSize;
+
+ this._updateColorScheme(averageLuxValue);
+ // Pop the oldest value off the array.
+ let oldLux = this._luxValues.pop();
+ // Subtract oldLux since it has been discarded from the array.
+ this._totalLux -= oldLux;
+ },
+
+ _handleVisibilityChange: function() {
+ let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
+ if (colorScheme != "auto") {
+ return;
+ }
+
+ // Turn off the ambient light sensor if the page is hidden
+ this._enableAmbientLighting(!this._doc.hidden);
+ },
+
+ // Setup or teardown the ambient light tracking system.
+ _enableAmbientLighting: function(enable) {
+ if (enable) {
+ this._win.addEventListener("devicelight", this, false);
+ this._luxValues = [];
+ this._totalLux = 0;
+ } else {
+ this._win.removeEventListener("devicelight", this, false);
+ delete this._luxValues;
+ delete this._totalLux;
+ }
+ },
+
+ _updateColorScheme: function(luxValue) {
+ // Upper bound value for "dark" color scheme beyond which it changes to "light".
+ let upperBoundDark = 50;
+ // Lower bound value for "light" color scheme beyond which it changes to "dark".
+ let lowerBoundLight = 10;
+ // Threshold for color scheme change.
+ let colorChangeThreshold = 20;
+
+ // Ignore changes that are within a certain threshold of previous lux values.
+ if ((this._colorScheme === "dark" && luxValue < upperBoundDark) ||
+ (this._colorScheme === "light" && luxValue > lowerBoundLight))
+ return;
+
+ if (luxValue < colorChangeThreshold)
+ this._setColorScheme("dark");
+ else
+ this._setColorScheme("light");
+ },
+
+ _setColorScheme: function(newColorScheme) {
+ // "auto" is not a real color scheme
+ if (this._colorScheme === newColorScheme || newColorScheme === "auto")
+ return;
+
+ let bodyClasses = this._doc.body.classList;
+
+ if (this._colorScheme)
+ bodyClasses.remove(this._colorScheme);
+
+ this._colorScheme = newColorScheme;
+ bodyClasses.add(this._colorScheme);
+ },
+
+ // Pref values include "dark", "light", and "auto", which automatically switches
+ // between light and dark color schemes based on the ambient light level.
+ _setColorSchemePref: function(colorSchemePref) {
+ this._enableAmbientLighting(colorSchemePref === "auto");
+ this._setColorScheme(colorSchemePref);
+
+ AsyncPrefs.set("reader.color_scheme", colorSchemePref);
+ },
+
+ _setFontType: function(newFontType) {
+ if (this._fontType === newFontType)
+ return;
+
+ let bodyClasses = this._doc.body.classList;
+
+ if (this._fontType)
+ bodyClasses.remove(this._fontType);
+
+ this._fontType = newFontType;
+ bodyClasses.add(this._fontType);
+
+ AsyncPrefs.set("reader.font_type", this._fontType);
+ },
+
+ _setSystemUIVisibility: function(visible) {
+ this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible });
+ },
+
+ _loadArticle: Task.async(function* () {
+ let url = this._getOriginalUrl();
+ this._showProgressDelayed();
+
+ let article;
+ if (this._articlePromise) {
+ article = yield this._articlePromise;
+ } else {
+ try {
+ article = yield this._getArticle(url);
+ } catch (e) {
+ if (e && e.newURL) {
+ let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
+ this._win.location.replace(readerURL);
+ return;
+ }
+ }
+ }
+
+ if (this._windowUnloaded) {
+ return;
+ }
+
+ // Replace the loading message with an error message if there's a failure.
+ // Users are supposed to navigate away by themselves (because we cannot
+ // remove ourselves from session history.)
+ if (!article) {
+ this._showError();
+ return;
+ }
+
+ this._showContent(article);
+ }),
+
+ _getArticle: function(url) {
+ return new Promise((resolve, reject) => {
+ let listener = (message) => {
+ this._mm.removeMessageListener("Reader:ArticleData", listener);
+ if (message.data.newURL) {
+ reject({ newURL: message.data.newURL });
+ return;
+ }
+ resolve(message.data.article);
+ };
+ this._mm.addMessageListener("Reader:ArticleData", listener);
+ this._mm.sendAsyncMessage("Reader:ArticleGet", { url: url });
+ });
+ },
+
+ _requestFavicon: function() {
+ let handleFaviconReturn = (message) => {
+ this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
+ this._loadFavicon(message.data.url, message.data.faviconUrl);
+ };
+
+ this._mm.addMessageListener("Reader:FaviconReturn", handleFaviconReturn);
+ this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
+ },
+
+ _loadFavicon: function(url, faviconUrl) {
+ if (this._article.url !== url)
+ return;
+
+ let doc = this._doc;
+
+ let link = doc.createElement('link');
+ link.rel = 'shortcut icon';
+ link.href = faviconUrl;
+
+ doc.getElementsByTagName('head')[0].appendChild(link);
+ },
+
+ _updateImageMargins: function() {
+ let windowWidth = this._win.innerWidth;
+ let bodyWidth = this._doc.body.clientWidth;
+
+ let setImageMargins = function(img) {
+ // If the image is at least as wide as the window, make it fill edge-to-edge on mobile.
+ if (img.naturalWidth >= windowWidth) {
+ img.setAttribute("moz-reader-full-width", true);
+ } else {
+ img.removeAttribute("moz-reader-full-width");
+ }
+
+ // If the image is at least half as wide as the body, center it on desktop.
+ if (img.naturalWidth >= bodyWidth/2) {
+ img.setAttribute("moz-reader-center", true);
+ } else {
+ img.removeAttribute("moz-reader-center");
+ }
+ };
+
+ let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR);
+ for (let i = imgs.length; --i >= 0;) {
+ let img = imgs[i];
+
+ if (img.naturalWidth > 0) {
+ setImageMargins(img);
+ } else {
+ img.onload = function() {
+ setImageMargins(img);
+ };
+ }
+ }
+ },
+
+ _maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
+ if (!article.dir)
+ return;
+
+ // Set "dir" attribute on content
+ this._contentElement.setAttribute("dir", article.dir);
+ this._headerElement.setAttribute("dir", article.dir);
+ },
+
+ _fixLocalLinks() {
+ // We need to do this because preprocessing the content through nsIParserUtils
+ // gives back a DOM with a <base> element. That influences how these URLs get
+ // resolved, making them no longer match the document URI (which is
+ // about:reader?url=...). To fix this, make all the hash URIs absolute. This
+ // is hacky, but the alternative of removing the base element has potential
+ // security implications if Readability has not successfully made all the URLs
+ // absolute, so we pick just fixing these in-document links explicitly.
+ let localLinks = this._contentElement.querySelectorAll("a[href^='#']");
+ for (let localLink of localLinks) {
+ // Have to get the attribute because .href provides an absolute URI.
+ localLink.href = this._doc.documentURI + localLink.getAttribute("href");
+ }
+ },
+
+ _showError: function() {
+ this._headerElement.style.display = "none";
+ this._contentElement.style.display = "none";
+
+ let errorMessage = gStrings.GetStringFromName("aboutReader.loadError");
+ this._messageElement.textContent = errorMessage;
+ this._messageElement.style.display = "block";
+
+ this._doc.title = errorMessage;
+
+ this._error = true;
+ },
+
+ // This function is the JS version of Java's StringUtils.stripCommonSubdomains.
+ _stripHost: function(host) {
+ if (!host)
+ return host;
+
+ let start = 0;
+
+ if (host.startsWith("www."))
+ start = 4;
+ else if (host.startsWith("m."))
+ start = 2;
+ else if (host.startsWith("mobile."))
+ start = 7;
+
+ return host.substring(start);
+ },
+
+ _showContent: function(article) {
+ this._messageElement.style.display = "none";
+
+ this._article = article;
+
+ this._domainElement.href = article.url;
+ let articleUri = Services.io.newURI(article.url, null, null);
+ this._domainElement.textContent = this._stripHost(articleUri.host);
+ this._creditsElement.textContent = article.byline;
+
+ this._titleElement.textContent = article.title;
+ this._doc.title = article.title;
+
+ this._headerElement.style.display = "block";
+
+ let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
+ let contentFragment = parserUtils.parseFragment(article.content,
+ Ci.nsIParserUtils.SanitizerDropForms | Ci.nsIParserUtils.SanitizerAllowStyle,
+ false, articleUri, this._contentElement);
+ this._contentElement.innerHTML = "";
+ this._contentElement.appendChild(contentFragment);
+ this._fixLocalLinks();
+ this._maybeSetTextDirection(article);
+
+ this._contentElement.style.display = "block";
+ this._updateImageMargins();
+
+ this._requestFavicon();
+ this._doc.body.classList.add("loaded");
+
+ this._goToReference(articleUri.ref);
+
+ Services.obs.notifyObservers(this._win, "AboutReader:Ready", "");
+
+ this._doc.dispatchEvent(
+ new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
+ },
+
+ _hideContent: function() {
+ this._headerElement.style.display = "none";
+ this._contentElement.style.display = "none";
+ },
+
+ _showProgressDelayed: function() {
+ this._win.setTimeout(function() {
+ // No need to show progress if the article has been loaded,
+ // if the window has been unloaded, or if there was an error
+ // trying to load the article.
+ if (this._article || this._windowUnloaded || this._error) {
+ return;
+ }
+
+ this._headerElement.style.display = "none";
+ this._contentElement.style.display = "none";
+
+ this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
+ this._messageElement.style.display = "block";
+ }.bind(this), 300);
+ },
+
+ /**
+ * Returns the original article URL for this about:reader view.
+ */
+ _getOriginalUrl: function(win) {
+ let url = win ? win.location.href : this._win.location.href;
+ return ReaderMode.getOriginalUrl(url) || url;
+ },
+
+ _setupSegmentedButton: function(id, options, initialValue, callback) {
+ let doc = this._doc;
+ let segmentedButton = doc.getElementById(id);
+
+ for (let i = 0; i < options.length; i++) {
+ let option = options[i];
+
+ let item = doc.createElement("button");
+
+ // Put the name in a div so that Android can hide it.
+ let div = doc.createElement("div");
+ div.textContent = option.name;
+ div.classList.add("name");
+ item.appendChild(div);
+
+ if (option.itemClass !== undefined)
+ item.classList.add(option.itemClass);
+
+ if (option.description !== undefined) {
+ let description = doc.createElement("div");
+ description.textContent = option.description;
+ description.classList.add("description");
+ item.appendChild(description);
+ }
+
+ segmentedButton.appendChild(item);
+
+ item.addEventListener("click", function(aEvent) {
+ if (!aEvent.isTrusted)
+ return;
+
+ aEvent.stopPropagation();
+
+ // Just pass the ID of the button as an extra and hope the ID doesn't change
+ // unless the context changes
+ UITelemetry.addEvent("action.1", "button", null, id);
+
+ let items = segmentedButton.children;
+ for (let j = items.length - 1; j >= 0; j--) {
+ items[j].classList.remove("selected");
+ }
+
+ item.classList.add("selected");
+ callback(option.value);
+ }.bind(this), true);
+
+ if (option.value === initialValue)
+ item.classList.add("selected");
+ }
+ },
+
+ _setupButton: function(id, callback, titleEntity, textEntity) {
+ if (titleEntity) {
+ this._setButtonTip(id, titleEntity);
+ }
+
+ let button = this._doc.getElementById(id);
+ if (textEntity) {
+ button.textContent = gStrings.GetStringFromName(textEntity);
+ }
+ button.removeAttribute("hidden");
+ button.addEventListener("click", function(aEvent) {
+ if (!aEvent.isTrusted)
+ return;
+
+ aEvent.stopPropagation();
+ let btn = aEvent.target;
+ callback(btn);
+ }, true);
+ },
+
+ /**
+ * Sets a toolTip for a button. Performed at initial button setup
+ * and dynamically as button state changes.
+ * @param Localizable string providing UI element usage tip.
+ */
+ _setButtonTip: function(id, titleEntity) {
+ let button = this._doc.getElementById(id);
+ button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
+ },
+
+ _setupStyleDropdown: function() {
+ let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle");
+ dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
+ },
+
+ _updatePopupPosition: function(dropdown) {
+ let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
+ let dropdownPopup = dropdown.querySelector(".dropdown-popup");
+
+ let toggleHeight = dropdownToggle.offsetHeight;
+ let toggleTop = dropdownToggle.offsetTop;
+ let popupTop = toggleTop - toggleHeight / 2;
+
+ dropdownPopup.style.top = popupTop + "px";
+ },
+
+ _toggleDropdownClicked: function(event) {
+ let dropdown = event.target.closest('.dropdown');
+
+ if (!dropdown)
+ return;
+
+ event.stopPropagation();
+
+ if (dropdown.classList.contains("open")) {
+ this._closeDropdowns();
+ } else {
+ this._openDropdown(dropdown);
+ if (this._isToolbarVertical) {
+ this._updatePopupPosition(dropdown);
+ }
+ }
+ },
+
+ /*
+ * If the ReaderView banner font-dropdown is closed, open it.
+ */
+ _openDropdown: function(dropdown) {
+ if (dropdown.classList.contains("open")) {
+ return;
+ }
+
+ this._closeDropdowns();
+
+ // Trigger BackPressListener initialization in Android.
+ dropdown.classList.add("open");
+ this._mm.sendAsyncMessage("Reader:DropdownOpened", this.viewId);
+ },
+
+ /*
+ * If the ReaderView has open dropdowns, close them. If we are closing the
+ * dropdowns because the page is scrolling, allow popups to stay open with
+ * the keep-open class.
+ */
+ _closeDropdowns: function(scrolling) {
+ let selector = ".dropdown.open";
+ if (scrolling) {
+ selector += ":not(.keep-open)";
+ }
+
+ let openDropdowns = this._doc.querySelectorAll(selector);
+ for (let dropdown of openDropdowns) {
+ dropdown.classList.remove("open");
+ }
+
+ // Trigger BackPressListener cleanup in Android.
+ if (openDropdowns.length) {
+ this._mm.sendAsyncMessage("Reader:DropdownClosed", this.viewId);
+ }
+ },
+
+ /*
+ * Scroll reader view to a reference
+ */
+ _goToReference(ref) {
+ if (ref) {
+ this._win.location.hash = ref;
+ }
+ }
+};