michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm") michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", michael@0: "resource://gre/modules/UITelemetry.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(window, "gChromeWin", function () michael@0: window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShellTreeItem) michael@0: .rootTreeItem michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindow) michael@0: .QueryInterface(Ci.nsIDOMChromeWindow)); michael@0: michael@0: function dump(s) { michael@0: Services.console.logStringMessage("AboutReader: " + s); michael@0: } michael@0: michael@0: let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutReader.properties"); michael@0: michael@0: let AboutReader = function(doc, win) { michael@0: dump("Init()"); michael@0: michael@0: this._docRef = Cu.getWeakReference(doc); michael@0: this._winRef = Cu.getWeakReference(win); michael@0: michael@0: Services.obs.addObserver(this, "Reader:FaviconReturn", false); michael@0: Services.obs.addObserver(this, "Reader:Add", false); michael@0: Services.obs.addObserver(this, "Reader:Remove", false); michael@0: Services.obs.addObserver(this, "Reader:ListStatusReturn", false); michael@0: michael@0: this._article = null; michael@0: michael@0: dump("Feching toolbar, header and content notes from about:reader"); michael@0: this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header")); michael@0: this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain")); michael@0: this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title")); michael@0: this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits")); michael@0: this._contentElementRef = Cu.getWeakReference(doc.getElementById("reader-content")); michael@0: this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar")); michael@0: this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message")); michael@0: michael@0: this._toolbarEnabled = false; michael@0: michael@0: this._scrollOffset = win.pageYOffset; michael@0: michael@0: let body = doc.body; michael@0: body.addEventListener("touchstart", this, false); michael@0: body.addEventListener("click", this, false); michael@0: michael@0: win.addEventListener("unload", this, false); michael@0: win.addEventListener("scroll", this, false); michael@0: win.addEventListener("popstate", this, false); michael@0: win.addEventListener("resize", this, false); michael@0: michael@0: this._setupAllDropdowns(); michael@0: this._setupButton("toggle-button", this._onReaderToggle.bind(this)); michael@0: this._setupButton("share-button", this._onShare.bind(this)); michael@0: michael@0: let colorSchemeOptions = [ michael@0: { name: gStrings.GetStringFromName("aboutReader.colorSchemeDark"), michael@0: value: "dark"}, michael@0: { name: gStrings.GetStringFromName("aboutReader.colorSchemeLight"), michael@0: value: "light"}, michael@0: { name: gStrings.GetStringFromName("aboutReader.colorSchemeAuto"), michael@0: value: "auto"} michael@0: ]; michael@0: michael@0: let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); michael@0: this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorSchemePref.bind(this)); michael@0: this._setColorSchemePref(colorScheme); michael@0: michael@0: let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample"); michael@0: let fontTypeOptions = [ michael@0: { name: fontTypeSample, michael@0: description: gStrings.GetStringFromName("aboutReader.fontTypeSerif"), michael@0: value: "serif", michael@0: linkClass: "serif" }, michael@0: { name: fontTypeSample, michael@0: description: gStrings.GetStringFromName("aboutReader.fontTypeSansSerif"), michael@0: value: "sans-serif", michael@0: linkClass: "sans-serif" michael@0: }, michael@0: ]; michael@0: michael@0: let fontType = Services.prefs.getCharPref("reader.font_type"); michael@0: this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this)); michael@0: this._setFontType(fontType); michael@0: michael@0: let fontSizeSample = gStrings.GetStringFromName("aboutReader.fontSizeSample"); michael@0: let fontSizeOptions = [ michael@0: { name: fontSizeSample, michael@0: value: 1, michael@0: linkClass: "font-size1-sample" }, michael@0: { name: fontSizeSample, michael@0: value: 2, michael@0: linkClass: "font-size2-sample" }, michael@0: { name: fontSizeSample, michael@0: value: 3, michael@0: linkClass: "font-size3-sample" }, michael@0: { name: fontSizeSample, michael@0: value: 4, michael@0: linkClass: "font-size4-sample" }, michael@0: { name: fontSizeSample, michael@0: value: 5, michael@0: linkClass: "font-size5-sample" } michael@0: ]; michael@0: michael@0: let fontSize = Services.prefs.getIntPref("reader.font_size"); michael@0: this._setupSegmentedButton("font-size-buttons", fontSizeOptions, fontSize, this._setFontSize.bind(this)); michael@0: this._setFontSize(fontSize); michael@0: michael@0: dump("Decoding query arguments"); michael@0: let queryArgs = this._decodeQueryString(win.location.href); michael@0: michael@0: // Track status of reader toolbar add/remove toggle button michael@0: this._isReadingListItem = -1; michael@0: this._updateToggleButton(); michael@0: michael@0: let url = queryArgs.url; michael@0: let tabId = queryArgs.tabId; michael@0: if (tabId) { michael@0: dump("Loading from tab with ID: " + tabId + ", URL: " + url); michael@0: this._loadFromTab(tabId, url); michael@0: } else { michael@0: dump("Fetching page with URL: " + url); michael@0: this._loadFromURL(url); michael@0: } michael@0: } michael@0: michael@0: AboutReader.prototype = { michael@0: _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " + michael@0: ".content p > a:only-child > img:only-child, " + michael@0: ".content .wp-caption img, " + michael@0: ".content figure img", michael@0: michael@0: get _doc() { michael@0: return this._docRef.get(); michael@0: }, michael@0: michael@0: get _win() { michael@0: return this._winRef.get(); michael@0: }, michael@0: michael@0: get _headerElement() { michael@0: return this._headerElementRef.get(); michael@0: }, michael@0: michael@0: get _domainElement() { michael@0: return this._domainElementRef.get(); michael@0: }, michael@0: michael@0: get _titleElement() { michael@0: return this._titleElementRef.get(); michael@0: }, michael@0: michael@0: get _creditsElement() { michael@0: return this._creditsElementRef.get(); michael@0: }, michael@0: michael@0: get _contentElement() { michael@0: return this._contentElementRef.get(); michael@0: }, michael@0: michael@0: get _toolbarElement() { michael@0: return this._toolbarElementRef.get(); michael@0: }, michael@0: michael@0: get _messageElement() { michael@0: return this._messageElementRef.get(); michael@0: }, michael@0: michael@0: observe: function Reader_observe(aMessage, aTopic, aData) { michael@0: switch(aTopic) { michael@0: case "Reader:FaviconReturn": { michael@0: let args = JSON.parse(aData); michael@0: this._loadFavicon(args.url, args.faviconUrl); michael@0: Services.obs.removeObserver(this, "Reader:FaviconReturn"); michael@0: break; michael@0: } michael@0: michael@0: case "Reader:Add": { michael@0: let args = JSON.parse(aData); michael@0: if (args.url == this._article.url) { michael@0: if (this._isReadingListItem != 1) { michael@0: this._isReadingListItem = 1; michael@0: this._updateToggleButton(); michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case "Reader:Remove": { michael@0: if (aData == this._article.url) { michael@0: if (this._isReadingListItem != 0) { michael@0: this._isReadingListItem = 0; michael@0: this._updateToggleButton(); michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case "Reader:ListStatusReturn": { michael@0: let args = JSON.parse(aData); michael@0: if (args.url == this._article.url) { michael@0: if (this._isReadingListItem != args.inReadingList) { michael@0: let isInitialStateChange = (this._isReadingListItem == -1); michael@0: this._isReadingListItem = args.inReadingList; michael@0: this._updateToggleButton(); michael@0: michael@0: // Display the toolbar when all its initial component states are known michael@0: if (isInitialStateChange) { michael@0: this._setToolbarVisibility(true); michael@0: } michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function Reader_handleEvent(aEvent) { michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: switch (aEvent.type) { michael@0: case "touchstart": michael@0: this._scrolled = false; michael@0: break; michael@0: case "click": michael@0: if (!this._scrolled) michael@0: this._toggleToolbarVisibility(); michael@0: break; michael@0: case "scroll": michael@0: if (!this._scrolled) { michael@0: let isScrollingUp = this._scrollOffset > aEvent.pageY; michael@0: this._setToolbarVisibility(isScrollingUp); michael@0: this._scrollOffset = aEvent.pageY; michael@0: } michael@0: break; michael@0: case "popstate": michael@0: if (!aEvent.state) michael@0: this._closeAllDropdowns(); michael@0: break; michael@0: case "resize": michael@0: this._updateImageMargins(); michael@0: break; michael@0: michael@0: case "devicelight": michael@0: this._handleDeviceLight(aEvent.value); michael@0: break; michael@0: michael@0: case "unload": michael@0: Services.obs.removeObserver(this, "Reader:Add"); michael@0: Services.obs.removeObserver(this, "Reader:Remove"); michael@0: Services.obs.removeObserver(this, "Reader:ListStatusReturn"); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _updateToggleButton: function Reader_updateToggleButton() { michael@0: let classes = this._doc.getElementById("toggle-button").classList; michael@0: michael@0: if (this._isReadingListItem == 1) { michael@0: classes.add("on"); michael@0: } else { michael@0: classes.remove("on"); michael@0: } michael@0: }, michael@0: michael@0: _requestReadingListStatus: function Reader_requestReadingListStatus() { michael@0: gChromeWin.sendMessageToJava({ michael@0: type: "Reader:ListStatusRequest", michael@0: url: this._article.url michael@0: }); michael@0: }, michael@0: michael@0: _onReaderToggle: function Reader_onToggle() { michael@0: if (!this._article) michael@0: return; michael@0: michael@0: this._isReadingListItem = (this._isReadingListItem == 1) ? 0 : 1; michael@0: this._updateToggleButton(); michael@0: michael@0: // Create a relative timestamp for telemetry michael@0: let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; michael@0: michael@0: if (this._isReadingListItem == 1) { michael@0: gChromeWin.Reader.storeArticleInCache(this._article, function(success) { michael@0: dump("Reader:Add (in reader) success=" + success); michael@0: michael@0: let result = gChromeWin.Reader.READER_ADD_FAILED; michael@0: if (success) { michael@0: result = gChromeWin.Reader.READER_ADD_SUCCESS; michael@0: UITelemetry.addEvent("save.1", "button", uptime, "reader"); michael@0: } michael@0: michael@0: let json = JSON.stringify({ fromAboutReader: true, url: this._article.url }); michael@0: Services.obs.notifyObservers(null, "Reader:Add", json); michael@0: michael@0: gChromeWin.sendMessageToJava({ michael@0: type: "Reader:Added", michael@0: result: result, michael@0: title: this._article.title, michael@0: url: this._article.url, michael@0: length: this._article.length, michael@0: excerpt: this._article.excerpt michael@0: }); michael@0: }.bind(this)); michael@0: } else { michael@0: // In addition to removing the article from the cache (handled in michael@0: // browser.js), sending this message will cause the toggle button to be michael@0: // updated (handled in this file). michael@0: Services.obs.notifyObservers(null, "Reader:Remove", this._article.url); michael@0: michael@0: UITelemetry.addEvent("unsave.1", "button", uptime, "reader"); michael@0: } michael@0: }, michael@0: michael@0: _onShare: function Reader_onShare() { michael@0: if (!this._article) michael@0: return; michael@0: michael@0: gChromeWin.sendMessageToJava({ michael@0: type: "Reader:Share", michael@0: url: this._article.url, michael@0: title: this._article.title michael@0: }); michael@0: michael@0: // Create a relative timestamp for telemetry michael@0: let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; michael@0: UITelemetry.addEvent("share.1", "list", uptime); michael@0: }, michael@0: michael@0: _setFontSize: function Reader_setFontSize(newFontSize) { michael@0: let bodyClasses = this._doc.body.classList; michael@0: michael@0: if (this._fontSize > 0) michael@0: bodyClasses.remove("font-size" + this._fontSize); michael@0: michael@0: this._fontSize = newFontSize; michael@0: bodyClasses.add("font-size" + this._fontSize); michael@0: michael@0: Services.prefs.setIntPref("reader.font_size", this._fontSize); michael@0: }, michael@0: michael@0: _handleDeviceLight: function Reader_handleDeviceLight(newLux) { michael@0: // Desired size of the this._luxValues array. michael@0: let luxValuesSize = 10; michael@0: // Add new lux value at the front of the array. michael@0: this._luxValues.unshift(newLux); michael@0: // Add new lux value to this._totalLux for averaging later. michael@0: this._totalLux += newLux; michael@0: michael@0: // Don't update when length of array is less than luxValuesSize except when it is 1. michael@0: if (this._luxValues.length < luxValuesSize) { michael@0: // Use the first lux value to set the color scheme until our array equals luxValuesSize. michael@0: if (this._luxValues.length == 1) { michael@0: this._updateColorScheme(newLux); michael@0: } michael@0: return; michael@0: } michael@0: // Holds the average of the lux values collected in this._luxValues. michael@0: let averageLuxValue = this._totalLux/luxValuesSize; michael@0: michael@0: this._updateColorScheme(averageLuxValue); michael@0: // Pop the oldest value off the array. michael@0: let oldLux = this._luxValues.pop(); michael@0: // Subtract oldLux since it has been discarded from the array. michael@0: this._totalLux -= oldLux; michael@0: }, michael@0: michael@0: _updateColorScheme: function Reader_updateColorScheme(luxValue) { michael@0: // Upper bound value for "dark" color scheme beyond which it changes to "light". michael@0: let upperBoundDark = 50; michael@0: // Lower bound value for "light" color scheme beyond which it changes to "dark". michael@0: let lowerBoundLight = 10; michael@0: // Threshold for color scheme change. michael@0: let colorChangeThreshold = 20; michael@0: michael@0: // Ignore changes that are within a certain threshold of previous lux values. michael@0: if ((this._colorScheme === "dark" && luxValue < upperBoundDark) || michael@0: (this._colorScheme === "light" && luxValue > lowerBoundLight)) michael@0: return; michael@0: michael@0: if (luxValue < colorChangeThreshold) michael@0: this._setColorScheme("dark"); michael@0: else michael@0: this._setColorScheme("light"); michael@0: }, michael@0: michael@0: _setColorScheme: function Reader_setColorScheme(newColorScheme) { michael@0: if (this._colorScheme === newColorScheme) michael@0: return; michael@0: michael@0: let bodyClasses = this._doc.body.classList; michael@0: michael@0: if (this._colorScheme) michael@0: bodyClasses.remove(this._colorScheme); michael@0: michael@0: this._colorScheme = newColorScheme; michael@0: bodyClasses.add(this._colorScheme); michael@0: }, michael@0: michael@0: // Pref values include "dark", "light", and "auto", which automatically switches michael@0: // between light and dark color schemes based on the ambient light level. michael@0: _setColorSchemePref: function Reader_setColorSchemePref(colorSchemePref) { michael@0: if (colorSchemePref === "auto") { michael@0: this._win.addEventListener("devicelight", this, false); michael@0: this._luxValues = []; michael@0: this._totalLux = 0; michael@0: } else { michael@0: this._win.removeEventListener("devicelight", this, false); michael@0: this._setColorScheme(colorSchemePref); michael@0: delete this._luxValues; michael@0: delete this._totalLux; michael@0: } michael@0: michael@0: Services.prefs.setCharPref("reader.color_scheme", colorSchemePref); michael@0: }, michael@0: michael@0: _setFontType: function Reader_setFontType(newFontType) { michael@0: if (this._fontType === newFontType) michael@0: return; michael@0: michael@0: let bodyClasses = this._doc.body.classList; michael@0: michael@0: if (this._fontType) michael@0: bodyClasses.remove(this._fontType); michael@0: michael@0: this._fontType = newFontType; michael@0: bodyClasses.add(this._fontType); michael@0: michael@0: Services.prefs.setCharPref("reader.font_type", this._fontType); michael@0: }, michael@0: michael@0: _getToolbarVisibility: function Reader_getToolbarVisibility() { michael@0: return !this._toolbarElement.classList.contains("toolbar-hidden"); michael@0: }, michael@0: michael@0: _setToolbarVisibility: function Reader_setToolbarVisibility(visible) { michael@0: let win = this._win; michael@0: if (win.history.state) michael@0: win.history.back(); michael@0: michael@0: if (!this._toolbarEnabled) michael@0: return; michael@0: michael@0: // Don't allow visible toolbar until banner state is known michael@0: if (this._isReadingListItem == -1) michael@0: return; michael@0: michael@0: if (this._getToolbarVisibility() === visible) michael@0: return; michael@0: michael@0: this._toolbarElement.classList.toggle("toolbar-hidden"); michael@0: this._setSystemUIVisibility(visible); michael@0: michael@0: if (!visible && !this._hasUsedToolbar) { michael@0: this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar"); michael@0: if (!this._hasUsedToolbar) { michael@0: gChromeWin.NativeWindow.toast.show(gStrings.GetStringFromName("aboutReader.toolbarTip"), "short"); michael@0: michael@0: Services.prefs.setBoolPref("reader.has_used_toolbar", true); michael@0: this._hasUsedToolbar = true; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _toggleToolbarVisibility: function Reader_toggleToolbarVisibility(visible) { michael@0: this._setToolbarVisibility(!this._getToolbarVisibility()); michael@0: }, michael@0: michael@0: _setSystemUIVisibility: function Reader_setSystemUIVisibility(visible) { michael@0: gChromeWin.sendMessageToJava({ michael@0: type: "SystemUI:Visibility", michael@0: visible: visible michael@0: }); michael@0: }, michael@0: michael@0: _loadFromURL: function Reader_loadFromURL(url) { michael@0: this._showProgressDelayed(); michael@0: michael@0: gChromeWin.Reader.parseDocumentFromURL(url, function(article) { michael@0: if (article) michael@0: this._showContent(article); michael@0: else michael@0: this._showError(gStrings.GetStringFromName("aboutReader.loadError")); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: _loadFromTab: function Reader_loadFromTab(tabId, url) { michael@0: this._showProgressDelayed(); michael@0: michael@0: gChromeWin.Reader.getArticleForTab(tabId, url, function(article) { michael@0: if (article) michael@0: this._showContent(article); michael@0: else michael@0: this._showError(gStrings.GetStringFromName("aboutReader.loadError")); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: _requestFavicon: function Reader_requestFavicon() { michael@0: gChromeWin.sendMessageToJava({ michael@0: type: "Reader:FaviconRequest", michael@0: url: this._article.url michael@0: }); michael@0: }, michael@0: michael@0: _loadFavicon: function Reader_loadFavicon(url, faviconUrl) { michael@0: if (this._article.url !== url) michael@0: return; michael@0: michael@0: let doc = this._doc; michael@0: michael@0: let link = doc.createElement('link'); michael@0: link.rel = 'shortcut icon'; michael@0: link.href = faviconUrl; michael@0: michael@0: doc.getElementsByTagName('head')[0].appendChild(link); michael@0: }, michael@0: michael@0: _updateImageMargins: function Reader_updateImageMargins() { michael@0: let windowWidth = this._win.innerWidth; michael@0: let contentWidth = this._contentElement.offsetWidth; michael@0: let maxWidthStyle = windowWidth + "px !important"; michael@0: michael@0: let setImageMargins = function(img) { michael@0: if (!img._originalWidth) michael@0: img._originalWidth = img.offsetWidth; michael@0: michael@0: let imgWidth = img._originalWidth; michael@0: michael@0: // If the image is taking more than half of the screen, just make michael@0: // it fill edge-to-edge. michael@0: if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55) michael@0: imgWidth = windowWidth; michael@0: michael@0: let sideMargin = Math.max((contentWidth - windowWidth) / 2, michael@0: (contentWidth - imgWidth) / 2); michael@0: michael@0: let imageStyle = sideMargin + "px !important"; michael@0: let widthStyle = imgWidth + "px !important"; michael@0: michael@0: let cssText = "max-width: " + maxWidthStyle + ";" + michael@0: "width: " + widthStyle + ";" + michael@0: "margin-left: " + imageStyle + ";" + michael@0: "margin-right: " + imageStyle + ";"; michael@0: michael@0: img.style.cssText = cssText; michael@0: } michael@0: michael@0: let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR); michael@0: for (let i = imgs.length; --i >= 0;) { michael@0: let img = imgs[i]; michael@0: michael@0: if (img.width > 0) { michael@0: setImageMargins(img); michael@0: } else { michael@0: img.onload = function() { michael@0: setImageMargins(img); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _maybeSetTextDirection: function Read_maybeSetTextDirection(article){ michael@0: if(!article.dir) michael@0: return; michael@0: michael@0: //Set "dir" attribute on content michael@0: this._contentElement.setAttribute("dir", article.dir); michael@0: this._headerElement.setAttribute("dir", article.dir); michael@0: }, michael@0: michael@0: _showError: function Reader_showError(error) { michael@0: this._headerElement.style.display = "none"; michael@0: this._contentElement.style.display = "none"; michael@0: michael@0: this._messageElement.innerHTML = error; michael@0: this._messageElement.style.display = "block"; michael@0: michael@0: this._doc.title = error; michael@0: }, michael@0: michael@0: // This function is the JS version of Java's StringUtils.stripCommonSubdomains. michael@0: _stripHost: function Reader_stripHost(host) { michael@0: if (!host) michael@0: return host; michael@0: michael@0: let start = 0; michael@0: michael@0: if (host.startsWith("www.")) michael@0: start = 4; michael@0: else if (host.startsWith("m.")) michael@0: start = 2; michael@0: else if (host.startsWith("mobile.")) michael@0: start = 7; michael@0: michael@0: return host.substring(start); michael@0: }, michael@0: michael@0: _showContent: function Reader_showContent(article) { michael@0: this._messageElement.style.display = "none"; michael@0: michael@0: this._article = article; michael@0: michael@0: this._domainElement.href = article.url; michael@0: let articleUri = Services.io.newURI(article.url, null, null); michael@0: this._domainElement.innerHTML = this._stripHost(articleUri.host); michael@0: michael@0: this._creditsElement.innerHTML = article.byline; michael@0: michael@0: this._titleElement.textContent = article.title; michael@0: this._doc.title = article.title; michael@0: michael@0: this._headerElement.style.display = "block"; michael@0: michael@0: let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils); michael@0: let contentFragment = parserUtils.parseFragment(article.content, Ci.nsIParserUtils.SanitizerDropForms, michael@0: false, articleUri, this._contentElement); michael@0: this._contentElement.innerHTML = ""; michael@0: this._contentElement.appendChild(contentFragment); michael@0: this._updateImageMargins(); michael@0: this._maybeSetTextDirection(article); michael@0: michael@0: this._contentElement.style.display = "block"; michael@0: this._requestReadingListStatus(); michael@0: michael@0: this._toolbarEnabled = true; michael@0: this._setToolbarVisibility(true); michael@0: michael@0: this._requestFavicon(); michael@0: }, michael@0: michael@0: _hideContent: function Reader_hideContent() { michael@0: this._headerElement.style.display = "none"; michael@0: this._contentElement.style.display = "none"; michael@0: }, michael@0: michael@0: _showProgressDelayed: function Reader_showProgressDelayed() { michael@0: this._win.setTimeout(function() { michael@0: // Article has already been loaded, no need to show michael@0: // progress anymore. michael@0: if (this._article) michael@0: return; michael@0: michael@0: this._headerElement.style.display = "none"; michael@0: this._contentElement.style.display = "none"; michael@0: michael@0: this._messageElement.innerHTML = gStrings.GetStringFromName("aboutReader.loading"); michael@0: this._messageElement.style.display = "block"; michael@0: }.bind(this), 300); michael@0: }, michael@0: michael@0: _decodeQueryString: function Reader_decodeQueryString(url) { michael@0: let result = {}; michael@0: let query = url.split("?")[1]; michael@0: if (query) { michael@0: let pairs = query.split("&"); michael@0: for (let i = 0; i < pairs.length; i++) { michael@0: let [name, value] = pairs[i].split("="); michael@0: result[name] = decodeURIComponent(value); michael@0: } michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: _setupSegmentedButton: function Reader_setupSegmentedButton(id, options, initialValue, callback) { michael@0: let doc = this._doc; michael@0: let segmentedButton = doc.getElementById(id); michael@0: michael@0: for (let i = 0; i < options.length; i++) { michael@0: let option = options[i]; michael@0: michael@0: let item = doc.createElement("li"); michael@0: let link = doc.createElement("a"); michael@0: link.textContent = option.name; michael@0: item.appendChild(link); michael@0: michael@0: if (option.linkClass !== undefined) michael@0: link.classList.add(option.linkClass); michael@0: michael@0: if (option.description !== undefined) { michael@0: let description = doc.createElement("div"); michael@0: description.textContent = option.description; michael@0: item.appendChild(description); michael@0: } michael@0: michael@0: link.style.MozUserSelect = 'none'; michael@0: segmentedButton.appendChild(item); michael@0: michael@0: link.addEventListener("click", function(aEvent) { michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: aEvent.stopPropagation(); michael@0: michael@0: // Create a relative timestamp for telemetry michael@0: let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; michael@0: // Just pass the ID of the button as an extra and hope the ID doesn't change michael@0: // unless the context changes michael@0: UITelemetry.addEvent("action.1", "button", uptime, id); michael@0: michael@0: let items = segmentedButton.children; michael@0: for (let j = items.length - 1; j >= 0; j--) { michael@0: items[j].classList.remove("selected"); michael@0: } michael@0: michael@0: item.classList.add("selected"); michael@0: callback(option.value); michael@0: }.bind(this), true); michael@0: michael@0: if (option.value === initialValue) michael@0: item.classList.add("selected"); michael@0: } michael@0: }, michael@0: michael@0: _setupButton: function Reader_setupButton(id, callback) { michael@0: let button = this._doc.getElementById(id); michael@0: michael@0: button.addEventListener("click", function(aEvent) { michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: aEvent.stopPropagation(); michael@0: callback(); michael@0: }, true); michael@0: }, michael@0: michael@0: _setupAllDropdowns: function Reader_setupAllDropdowns() { michael@0: let doc = this._doc; michael@0: let win = this._win; michael@0: michael@0: let dropdowns = doc.getElementsByClassName("dropdown"); michael@0: michael@0: for (let i = dropdowns.length - 1; i >= 0; i--) { michael@0: let dropdown = dropdowns[i]; michael@0: michael@0: let dropdownToggle = dropdown.getElementsByClassName("dropdown-toggle")[0]; michael@0: let dropdownPopup = dropdown.getElementsByClassName("dropdown-popup")[0]; michael@0: michael@0: if (!dropdownToggle || !dropdownPopup) michael@0: continue; michael@0: michael@0: let dropdownArrow = doc.createElement("div"); michael@0: dropdownArrow.className = "dropdown-arrow"; michael@0: dropdownPopup.appendChild(dropdownArrow); michael@0: michael@0: let updatePopupPosition = function() { michael@0: let popupWidth = dropdownPopup.offsetWidth + 30; michael@0: let arrowWidth = dropdownArrow.offsetWidth; michael@0: let toggleWidth = dropdownToggle.offsetWidth; michael@0: let toggleLeft = dropdownToggle.offsetLeft; michael@0: michael@0: let popupShift = (toggleWidth - popupWidth) / 2; michael@0: let popupLeft = Math.max(0, Math.min(win.innerWidth - popupWidth, toggleLeft + popupShift)); michael@0: dropdownPopup.style.left = popupLeft + "px"; michael@0: michael@0: let arrowShift = (toggleWidth - arrowWidth) / 2; michael@0: let arrowLeft = toggleLeft - popupLeft + arrowShift; michael@0: dropdownArrow.style.left = arrowLeft + "px"; michael@0: }; michael@0: michael@0: win.addEventListener("resize", function(aEvent) { michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: // Wait for reflow before calculating the new position of the popup. michael@0: setTimeout(updatePopupPosition, 0); michael@0: }, true); michael@0: michael@0: dropdownToggle.addEventListener("click", function(aEvent) { michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: aEvent.stopPropagation(); michael@0: michael@0: if (!this._getToolbarVisibility()) michael@0: return; michael@0: michael@0: let dropdownClasses = dropdown.classList; michael@0: michael@0: if (dropdownClasses.contains("open")) { michael@0: win.history.back(); michael@0: } else { michael@0: updatePopupPosition(); michael@0: if (!this._closeAllDropdowns()) michael@0: this._pushDropdownState(); michael@0: michael@0: dropdownClasses.add("open"); michael@0: } michael@0: }.bind(this), true); michael@0: } michael@0: }, michael@0: michael@0: _pushDropdownState: function Reader_pushDropdownState() { michael@0: // FIXME: We're getting a NS_ERROR_UNEXPECTED error when we try michael@0: // to do win.history.pushState() here (see bug 682296). This is michael@0: // a workaround that allows us to push history state on the target michael@0: // content document. michael@0: michael@0: let doc = this._doc; michael@0: let body = doc.body; michael@0: michael@0: if (this._pushStateScript) michael@0: body.removeChild(this._pushStateScript); michael@0: michael@0: this._pushStateScript = doc.createElement('script'); michael@0: this._pushStateScript.type = "text/javascript"; michael@0: this._pushStateScript.innerHTML = 'history.pushState({ dropdown: 1 }, document.title);'; michael@0: michael@0: body.appendChild(this._pushStateScript); michael@0: }, michael@0: michael@0: _closeAllDropdowns : function Reader_closeAllDropdowns() { michael@0: let dropdowns = this._doc.querySelectorAll(".dropdown.open"); michael@0: for (let i = dropdowns.length - 1; i >= 0; i--) { michael@0: dropdowns[i].classList.remove("open"); michael@0: } michael@0: michael@0: return (dropdowns.length > 0) michael@0: } michael@0: };