зеркало из https://github.com/mozilla/gecko-dev.git
956 строки
30 KiB
JavaScript
956 строки
30 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, "Rect", "resource://gre/modules/Geometry.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
|
|
|
|
const READINGLIST_COMMAND_ID = "readingListSidebar";
|
|
|
|
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:Added", this);
|
|
this._mm.addMessageListener("Reader:Removed", this);
|
|
this._mm.addMessageListener("Sidebar:VisibilityChange", this);
|
|
this._mm.addMessageListener("ReadingList:VisibilityStatus", this);
|
|
|
|
this._docRef = Cu.getWeakReference(doc);
|
|
this._winRef = Cu.getWeakReference(win);
|
|
|
|
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("unload", this, false);
|
|
win.addEventListener("scroll", this, false);
|
|
win.addEventListener("resize", this, false);
|
|
|
|
doc.addEventListener("visibilitychange", this, false);
|
|
|
|
this._setupStyleDropdown();
|
|
this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
|
|
this._setupButton("share-button", this._onShare.bind(this), "aboutReader.toolbar.share");
|
|
|
|
try {
|
|
if (Services.prefs.getBoolPref("browser.readinglist.enabled")) {
|
|
this._setupButton("toggle-button", this._onReaderToggle.bind(this, "button"), "aboutReader.toolbar.addToReadingList");
|
|
this._setupButton("list-button", this._onList.bind(this), "aboutReader.toolbar.openReadingList");
|
|
this._setupButton("remove-button", this._onReaderToggle.bind(this, "footer"),
|
|
"aboutReader.footer.deleteThisArticle", "aboutReader.footer.deleteThisArticle");
|
|
this._doc.getElementById("reader-footer").setAttribute('readinglist-enabled', "true");
|
|
}
|
|
} catch (e) {
|
|
// Pref doesn't exist.
|
|
}
|
|
|
|
const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
|
|
if (gIsFirefoxDesktop) {
|
|
this._setupPocketButton();
|
|
} else {
|
|
this._doc.getElementById("pocket-button").hidden = true;
|
|
}
|
|
|
|
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();
|
|
|
|
// Track status of reader toolbar add/remove toggle button
|
|
this._isReadingListItem = -1;
|
|
this._updateToggleButton();
|
|
|
|
// Setup initial ReadingList button styles.
|
|
this._updateListButton();
|
|
|
|
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");
|
|
},
|
|
|
|
receiveMessage: function (message) {
|
|
switch (message.name) {
|
|
case "Reader:Added": {
|
|
// Page can be added by long-press pageAction, or by tap on banner icon.
|
|
if (message.data.url == this._article.url) {
|
|
if (this._isReadingListItem != 1) {
|
|
this._isReadingListItem = 1;
|
|
this._updateToggleButton();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "Reader:Removed": {
|
|
if (message.data.url == this._article.url) {
|
|
if (this._isReadingListItem != 0) {
|
|
this._isReadingListItem = 0;
|
|
this._updateToggleButton();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Notifys us of Sidebar updates, user clicks X to close,
|
|
// checks View -> Sidebar -> (Bookmarks, Histroy, Readinglist, etc).
|
|
case "Sidebar:VisibilityChange": {
|
|
let data = message.data;
|
|
this._updateListButtonStyle(data.isOpen && data.commandID === READINGLIST_COMMAND_ID);
|
|
break;
|
|
}
|
|
|
|
// Returns requested status of current ReadingList Sidebar.
|
|
case "ReadingList:VisibilityStatus": {
|
|
this._updateListButtonStyle(message.data.isOpen);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
handleEvent: function(aEvent) {
|
|
if (!aEvent.isTrusted)
|
|
return;
|
|
|
|
switch (aEvent.type) {
|
|
case "click":
|
|
let target = aEvent.target;
|
|
while (target && target.id != "reader-popup")
|
|
target = target.parentNode;
|
|
if (!target)
|
|
this._toggleToolbarVisibility();
|
|
break;
|
|
case "scroll":
|
|
let isScrollingUp = this._scrollOffset > aEvent.pageY;
|
|
this._setToolbarVisibility(isScrollingUp);
|
|
this._scrollOffset = aEvent.pageY;
|
|
break;
|
|
case "resize":
|
|
this._updateImageMargins();
|
|
break;
|
|
|
|
case "devicelight":
|
|
this._handleDeviceLight(aEvent.value);
|
|
break;
|
|
|
|
case "visibilitychange":
|
|
this._handleVisibilityChange();
|
|
break;
|
|
|
|
case "unload":
|
|
this._mm.removeMessageListener("Reader:Added", this);
|
|
this._mm.removeMessageListener("Reader:Removed", this);
|
|
this._mm.removeMessageListener("Sidebar:VisibilityChange", this);
|
|
this._mm.removeMessageListener("ReadingList:VisibilityStatus", this);
|
|
this._windowUnloaded = true;
|
|
break;
|
|
}
|
|
},
|
|
|
|
_updateToggleButton: function() {
|
|
let button = this._doc.getElementById("toggle-button");
|
|
|
|
if (this._isReadingListItem == 1) {
|
|
button.classList.add("on");
|
|
button.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.removeFromReadingList"));
|
|
} else {
|
|
button.classList.remove("on");
|
|
button.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.addToReadingList"));
|
|
}
|
|
this._updateFooter();
|
|
},
|
|
|
|
_requestReadingListStatus: function() {
|
|
let handleListStatusData = (message) => {
|
|
this._mm.removeMessageListener("Reader:ListStatusData", handleListStatusData);
|
|
|
|
let args = message.data;
|
|
if (args.url == this._article.url) {
|
|
if (this._isReadingListItem != args.inReadingList) {
|
|
let isInitialStateChange = (this._isReadingListItem == -1);
|
|
this._isReadingListItem = args.inReadingList;
|
|
this._updateToggleButton();
|
|
|
|
// Display the toolbar when all its initial component states are known
|
|
if (isInitialStateChange) {
|
|
// Toolbar display is updated here to avoid it appearing in the middle of the screen on page load. See bug 1145567.
|
|
this._win.setTimeout(() => {
|
|
this._toolbarElement.style.display = "block";
|
|
// Delay showing the toolbar to have a nice slide from bottom animation.
|
|
this._win.setTimeout(() => this._setToolbarVisibility(true), 200);
|
|
}, 500);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this._mm.addMessageListener("Reader:ListStatusData", handleListStatusData);
|
|
this._mm.sendAsyncMessage("Reader:ListStatusRequest", { url: this._article.url });
|
|
},
|
|
|
|
_onReaderClose: function() {
|
|
this._win.location.href = this._getOriginalUrl();
|
|
},
|
|
|
|
_onReaderToggle: function(aMethod) {
|
|
if (!this._article)
|
|
return;
|
|
|
|
if (this._isReadingListItem == 0) {
|
|
this._mm.sendAsyncMessage("Reader:AddToList", { article: this._article });
|
|
UITelemetry.addEvent("save.1", aMethod, null, "reader");
|
|
} else {
|
|
this._mm.sendAsyncMessage("Reader:RemoveFromList", { url: this._article.url });
|
|
UITelemetry.addEvent("unsave.1", aMethod, null, "reader");
|
|
}
|
|
},
|
|
|
|
_onPocketToggle: function(aMethod) {
|
|
if (!this._article)
|
|
return;
|
|
|
|
this._mm.sendAsyncMessage("Reader:AddToPocket", { article: this._article });
|
|
UITelemetry.addEvent("pocket.1", aMethod, null, "reader");
|
|
},
|
|
|
|
_onShare: function() {
|
|
if (!this._article)
|
|
return;
|
|
|
|
this._mm.sendAsyncMessage("Reader:Share", {
|
|
url: this._article.url,
|
|
title: this._article.title
|
|
});
|
|
UITelemetry.addEvent("share.1", "list", null);
|
|
},
|
|
|
|
/**
|
|
* To help introduce ReadingList, we want to automatically
|
|
* open the Desktop sidebar the first time ReaderMode is used.
|
|
*/
|
|
_showListIntro: function() {
|
|
this._mm.sendAsyncMessage("ReadingList:ShowIntro");
|
|
},
|
|
|
|
/**
|
|
* Toggle ReadingList Sidebar visibility. SidebarUI will trigger
|
|
* _updateListButtonStyle().
|
|
*/
|
|
_onList: function() {
|
|
this._mm.sendAsyncMessage("ReadingList:ToggleVisibility");
|
|
},
|
|
|
|
/**
|
|
* Request ReadingList Sidebar-button visibility status update.
|
|
* Only desktop currently responds to this message.
|
|
*/
|
|
_updateListButton: function() {
|
|
this._mm.sendAsyncMessage("ReadingList:GetVisibility");
|
|
},
|
|
|
|
/**
|
|
* Update ReadingList toggle button styles.
|
|
* @param isVisible
|
|
* What Sidebar ReadingList visibility style the List
|
|
* toggle-button should be set to reflect, and what
|
|
* button-action the tip will provide.
|
|
*/
|
|
_updateListButtonStyle: function(isVisible) {
|
|
let classes = this._doc.getElementById("list-button").classList;
|
|
if (isVisible) {
|
|
classes.add("on");
|
|
// When on, action tip is "close".
|
|
this._setButtonTip("list-button", "aboutReader.toolbar.closeReadingList");
|
|
} else {
|
|
classes.remove("on");
|
|
// When off, action tip is "open".
|
|
this._setButtonTip("list-button", "aboutReader.toolbar.openReadingList");
|
|
}
|
|
},
|
|
|
|
_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);
|
|
|
|
this._mm.sendAsyncMessage("Reader:SetIntPref", {
|
|
name: "reader.font_size",
|
|
value: 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);
|
|
},
|
|
|
|
_updateFooter: function() {
|
|
let footer = this._doc.getElementById("reader-footer");
|
|
if (!this._article || this._isReadingListItem == 0 ||
|
|
footer.getAttribute("readinglist-enabled") != "true") {
|
|
footer.style.display = "none";
|
|
return;
|
|
}
|
|
footer.style.display = null;
|
|
},
|
|
|
|
_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() {
|
|
// ReadingList / Sidebar state might change while we're not the selected tab.
|
|
if (this._doc.visibilityState === "visible") {
|
|
this._updateListButton();
|
|
}
|
|
|
|
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);
|
|
|
|
this._mm.sendAsyncMessage("Reader:SetCharPref", {
|
|
name: "reader.color_scheme",
|
|
value: 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);
|
|
|
|
this._mm.sendAsyncMessage("Reader:SetCharPref", {
|
|
name: "reader.font_type",
|
|
value: this._fontType
|
|
});
|
|
},
|
|
|
|
_getToolbarVisibility: function() {
|
|
return this._toolbarElement.hasAttribute("visible");
|
|
},
|
|
|
|
_setToolbarVisibility: function(visible) {
|
|
let dropdown = this._doc.getElementById("style-dropdown");
|
|
dropdown.classList.remove("open");
|
|
|
|
if (this._getToolbarVisibility() === visible) {
|
|
return;
|
|
}
|
|
|
|
if (visible) {
|
|
this._toolbarElement.setAttribute("visible", true);
|
|
} else {
|
|
this._toolbarElement.removeAttribute("visible");
|
|
}
|
|
this._setSystemUIVisibility(visible);
|
|
|
|
if (!visible) {
|
|
this._mm.sendAsyncMessage("Reader:ToolbarHidden");
|
|
}
|
|
this._updateFooter();
|
|
},
|
|
|
|
_toggleToolbarVisibility: function() {
|
|
this._setToolbarVisibility(!this._getToolbarVisibility());
|
|
},
|
|
|
|
_setSystemUIVisibility: function(visible) {
|
|
this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible });
|
|
},
|
|
|
|
_setupPocketButton: Task.async(function* () {
|
|
let pocketEnabledPromise = new Promise((resolve, reject) => {
|
|
let listener = (message) => {
|
|
this._mm.removeMessageListener("Reader:PocketEnabledData", listener);
|
|
resolve(message.data.enabled);
|
|
};
|
|
this._mm.addMessageListener("Reader:PocketEnabledData", listener);
|
|
this._mm.sendAsyncMessage("Reader:PocketEnabledGet");
|
|
});
|
|
|
|
let isPocketEnabled = yield pocketEnabledPromise;
|
|
if (isPocketEnabled) {
|
|
this._setupButton("pocket-button", this._onPocketToggle.bind(this, "button"));
|
|
} else {
|
|
this._doc.getElementById("pocket-button").hidden = true;
|
|
}
|
|
}),
|
|
|
|
_loadArticle: Task.async(function* () {
|
|
let url = this._getOriginalUrl();
|
|
this._showProgressDelayed();
|
|
|
|
let article;
|
|
if (this._articlePromise) {
|
|
article = yield this._articlePromise;
|
|
} else {
|
|
article = yield this._getArticle(url);
|
|
}
|
|
|
|
if (this._windowUnloaded) {
|
|
return;
|
|
}
|
|
|
|
if (article) {
|
|
this._showContent(article);
|
|
} else if (this._articlePromise) {
|
|
// If we were promised an article, show an error message if there's a failure.
|
|
this._showError();
|
|
} else {
|
|
// Otherwise, just load the original URL. We can encounter this case when
|
|
// loading an about:reader URL directly (e.g. opening a reading list item).
|
|
this._win.location.href = url;
|
|
}
|
|
}),
|
|
|
|
_getArticle: function(url) {
|
|
return new Promise((resolve, reject) => {
|
|
let listener = (message) => {
|
|
this._mm.removeMessageListener("Reader:ArticleData", listener);
|
|
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);
|
|
},
|
|
|
|
_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._maybeSetTextDirection(article);
|
|
|
|
this._contentElement.style.display = "block";
|
|
this._updateImageMargins();
|
|
this._requestReadingListStatus();
|
|
|
|
this._showListIntro();
|
|
this._requestFavicon();
|
|
this._doc.body.classList.add("loaded");
|
|
|
|
Services.obs.notifyObservers(null, "AboutReader:Ready", "");
|
|
},
|
|
|
|
_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.loading");
|
|
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();
|
|
callback();
|
|
}, 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 doc = this._doc;
|
|
let win = this._win;
|
|
|
|
let dropdown = doc.getElementById("style-dropdown");
|
|
let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
|
|
let dropdownPopup = dropdown.querySelector(".dropdown-popup");
|
|
|
|
// Helper function used to position the popup on desktop,
|
|
// where there is a vertical toolbar.
|
|
function updatePopupPosition() {
|
|
let toggleHeight = dropdownToggle.offsetHeight;
|
|
let toggleTop = dropdownToggle.offsetTop;
|
|
let popupTop = toggleTop - toggleHeight / 2;
|
|
dropdownPopup.style.top = popupTop + "px";
|
|
}
|
|
|
|
if (this._isToolbarVertical) {
|
|
win.addEventListener("resize", event => {
|
|
if (!event.isTrusted)
|
|
return;
|
|
|
|
// Wait for reflow before calculating the new position of the popup.
|
|
win.setTimeout(updatePopupPosition, 0);
|
|
}, true);
|
|
}
|
|
|
|
dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
|
|
dropdownToggle.addEventListener("click", event => {
|
|
if (!event.isTrusted)
|
|
return;
|
|
|
|
event.stopPropagation();
|
|
|
|
if (dropdown.classList.contains("open")) {
|
|
dropdown.classList.remove("open");
|
|
} else {
|
|
dropdown.classList.add("open");
|
|
if (this._isToolbarVertical) {
|
|
updatePopupPosition();
|
|
}
|
|
}
|
|
}, true);
|
|
},
|
|
};
|