Mypal/toolkit/components/reader/AboutReader.jsm

1021 lines
31 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";
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, "PluralForm", "resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
var AboutReader = function(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._docRef = Cu.getWeakReference(doc);
this._winRef = Cu.getWeakReference(win);
this._innerWindowId = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
this._article = null;
this._languagePromise = new Promise(resolve => {
this._foundLanguage = resolve;
});
if (articlePromise) {
this._articlePromise = articlePromise;
}
this._headerElementRef = Cu.getWeakReference(doc.querySelector(".reader-header"));
this._domainElementRef = Cu.getWeakReference(doc.querySelector(".reader-domain"));
this._titleElementRef = Cu.getWeakReference(doc.querySelector(".reader-title"));
this._readTimeElementRef = Cu.getWeakReference(doc.querySelector(".reader-estimated-time"));
this._creditsElementRef = Cu.getWeakReference(doc.querySelector(".reader-credits"));
this._contentElementRef = Cu.getWeakReference(doc.querySelector(".moz-reader-content"));
this._toolbarElementRef = Cu.getWeakReference(doc.querySelector(".reader-toolbar"));
this._messageElementRef = Cu.getWeakReference(doc.querySelector(".reader-message"));
this._containerElementRef = Cu.getWeakReference(doc.querySelector(".container"));
this._scrollOffset = win.pageYOffset;
doc.addEventListener("mousedown", this);
doc.addEventListener("click", this);
win.addEventListener("pagehide", this);
win.addEventListener("scroll", this);
win.addEventListener("resize", this);
win.addEventListener("AboutReaderAddButton", this, false, true);
win.addEventListener("AboutReaderRemoveButton", this, false, true);
Services.obs.addObserver(this, "inner-window-destroyed", false);
doc.addEventListener("visibilitychange", this);
this._setupStyleDropdown();
this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
// we're ready for any external setup, send a signal for that.
doc.dispatchEvent(
new win.CustomEvent("AboutReaderOnSetup", { bubbles: true, cancelable: false }));
let colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values"));
let colorSchemeOptions = colorSchemeValues.map((value) => {
return {
name: gStrings.GetStringFromName("aboutReader.colorScheme." + 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(win, this._languagePromise);
}
this._loadArticle();
let dropdown = this._toolbarElement;
let elemL10nMap = {
".minus-button": "minus",
".plus-button": "plus",
".content-width-minus-button": "contentwidthminus",
".content-width-plus-button": "contentwidthplus",
".line-height-minus-button": "lineheightminus",
".line-height-plus-button": "lineheightplus",
".light-button": "colorschemelight",
".dark-button": "colorschemedark",
".sepia-button": "colorschemesepia",
};
for (let [selector, stringID] of Object.entries(elemL10nMap)) {
dropdown.querySelector(selector).setAttribute("title",
gStrings.GetStringFromName("aboutReader.toolbar." + stringID));
}
};
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 _readTimeElement() {
return this._readTimeElementRef.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 _containerElement() {
return this._containerElementRef.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;
},
handleEvent(aEvent) {
if (!aEvent.isTrusted)
return;
let target = aEvent.target;
switch (aEvent.type) {
case "mousedown":
if (!target.closest(".dropdown-popup")) {
this._closeDropdowns();
}
break;
case "click":
if (target.classList.contains("dropdown-toggle")) {
this._toggleDropdownClicked(aEvent);
}
break;
case "scroll":
this._closeDropdowns(true);
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._windowUnloaded = true;
break;
case "AboutReaderAddButton": {
if (aEvent.detail.id && aEvent.detail.image &&
!this._doc.getElementById(aEvent.detail.id)) {
let btn = this._doc.createElement("button");
btn.setAttribute("class", "button " + aEvent.detail.id);
btn.setAttribute("style", "background-image: url('" + aEvent.detail.image + "')");
btn.setAttribute("id", aEvent.detail.id);
if (aEvent.detail.title)
btn.setAttribute("title", aEvent.detail.title);
if (aEvent.detail.text)
btn.textContent = aEvent.detail.text;
let tb = this._toolbarElement;
tb.appendChild(btn);
this._setupButton(aEvent.detail.id, button => {
var data = { article: this._article };
this._doc.dispatchEvent(
new this._win.CustomEvent("AboutReaderButtonClicked-" + button.getAttribute("id"), {detail: data, bubbles: true, cancelable: false}));
});
}
break;
}
case "AboutReaderRemoveButton": {
if (aEvent.detail.id) {
let btn = this._doc.getElementById(aEvent.detail.id);
if (btn)
btn.remove();
}
break;
}
}
},
observe(subject, topic, data) {
if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
return;
}
Services.obs.removeObserver(this, "inner-window-destroyed");
this._windowUnloaded = true;
},
_onReaderClose() {
ReaderMode.leaveReaderMode(this._win.document.docShell, this._win);
},
_setFontSize(newFontSize) {
this._fontSize = newFontSize;
let size = (10 + 2 * this._fontSize) + "px";
this._containerElement.style.setProperty("--font-size", size);
return AsyncPrefs.set("reader.font_size", this._fontSize);
},
_setupFontSizeButtons() {
const FONT_SIZE_MIN = 1;
const FONT_SIZE_MAX = 9;
// Sample text shown in Android UI.
let sampleText = this._doc.querySelector(".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.querySelector(".plus-button");
let minusButton = this._doc.querySelector(".minus-button");
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(newContentWidth) {
let containerClasses = this._containerElement.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() {
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.querySelector(".content-width-plus-button");
let minusButton = this._doc.querySelector(".content-width-minus-button");
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(newLineHeight) {
let contentClasses = this._contentElement.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() {
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.querySelector(".line-height-plus-button");
let minusButton = this._doc.querySelector(".line-height-minus-button");
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(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() {
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(enable) {
if (enable) {
this._win.addEventListener("devicelight", this);
this._luxValues = [];
this._totalLux = 0;
} else {
this._win.removeEventListener("devicelight", this);
delete this._luxValues;
delete this._totalLux;
}
},
_updateColorScheme(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(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(colorSchemePref) {
this._enableAmbientLighting(colorSchemePref === "auto");
this._setColorScheme(colorSchemePref);
AsyncPrefs.set("reader.color_scheme", colorSchemePref);
},
_setFontType(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);
},
_setToolbarVisibility(visible) {
let tb = this._toolbarElement;
if (visible) {
if (tb.style.opacity != "1") {
tb.removeAttribute("hidden");
tb.style.opacity = "1";
}
} else if (tb.style.opacity != "0") {
tb.addEventListener("transitionend", evt => {
if (tb.style.opacity == "0") {
tb.setAttribute("hidden", "");
}
}, { once: true });
tb.style.opacity = "0";
}
},
async _loadArticle() {
let url = this._getOriginalUrl();
this._showProgressDelayed();
let article;
if (this._articlePromise) {
article = await this._articlePromise;
} else {
try {
article = await 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(url) {
return ReaderMode.downloadAndParseDocument(url);
},
_requestFavicon() {
let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(this._article.url);
var self = this;
faviconUrl.then(function onResolution(favicon) {
self._loadFavicon(self._article.url, favicon.path.replace(/^favicon:/, ""));
},
function onRejection(reason) {
Cu.reportError("Error requesting favicon URL for about:reader content: " + reason);
}).catch(Cu.reportError);
},
_loadFavicon(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() {
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) {
// Set "dir" attribute on content
this._contentElement.setAttribute("dir", article.dir);
this._headerElement.setAttribute("dir", article.dir);
// The native locale could be set differently than the article's text direction.
var localeDirection = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
this._readTimeElement.setAttribute("dir", localeDirection);
this._readTimeElement.style.textAlign = article.dir == "rtl" ? "right" : "left";
}
},
_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");
}
},
_formatReadTime(slowEstimate, fastEstimate) {
let displayStringKey = "aboutReader.estimatedReadTimeRange1";
// only show one reading estimate when they are the same value
if (slowEstimate == fastEstimate) {
displayStringKey = "aboutReader.estimatedReadTimeValue1";
}
return PluralForm.get(slowEstimate, gStrings.GetStringFromName(displayStringKey))
.replace("#1", fastEstimate)
.replace("#2", slowEstimate);
},
_showError() {
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._doc.documentElement.dataset.isError = true;
this._error = true;
this._doc.dispatchEvent(
new this._win.CustomEvent("AboutReaderContentError", { bubbles: true, cancelable: false }));
},
// This function is the JS version of Java's StringUtils.stripCommonSubdomains.
_stripHost(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(article) {
this._messageElement.style.display = "none";
this._article = article;
this._domainElement.href = article.url;
let articleUri = Services.io.newURI(article.url);
this._domainElement.textContent = this._stripHost(articleUri.host);
this._creditsElement.textContent = article.byline;
this._titleElement.textContent = article.title;
this._readTimeElement.textContent = this._formatReadTime(article.readingTimeMinsSlow, article.readingTimeMinsFast);
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._foundLanguage(article.language);
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() {
this._headerElement.style.display = "none";
this._contentElement.style.display = "none";
},
_showProgressDelayed() {
this._win.setTimeout(() => {
// 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";
}, 300);
},
/**
* Returns the original article URL for this about:reader view.
*/
_getOriginalUrl(win) {
let url = win ? win.location.href : this._win.location.href;
return ReaderMode.getOriginalUrl(url) || url;
},
_setupSegmentedButton(id, options, initialValue, callback) {
let doc = this._doc;
let segmentedButton = doc.getElementsByClassName(id)[0];
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();
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);
}, true);
if (option.value === initialValue)
item.classList.add("selected");
}
},
_setupButton(id, callback, titleEntity, textEntity) {
if (titleEntity) {
this._setButtonTip(id, titleEntity);
}
let button = this._doc.getElementsByClassName(id)[0];
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(id, titleEntity) {
let button = this._doc.getElementsByClassName(id)[0];
button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
},
_setupStyleDropdown() {
let dropdownToggle = this._doc.querySelector(".style-dropdown .dropdown-toggle");
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
},
_updatePopupPosition(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(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(dropdown) {
if (dropdown.classList.contains("open")) {
return;
}
this._closeDropdowns();
dropdown.classList.add("open");
},
/*
* 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(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");
}
},
/*
* Scroll reader view to a reference
*/
_goToReference(ref) {
if (ref) {
this._win.location.hash = ref;
}
}
};