1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/chrome/content/aboutReader.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,832 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils; 1.9 + 1.10 +Cu.import("resource://gre/modules/Services.jsm") 1.11 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.12 + 1.13 +XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", 1.14 + "resource://gre/modules/UITelemetry.jsm"); 1.15 + 1.16 +XPCOMUtils.defineLazyGetter(window, "gChromeWin", function () 1.17 + window.QueryInterface(Ci.nsIInterfaceRequestor) 1.18 + .getInterface(Ci.nsIWebNavigation) 1.19 + .QueryInterface(Ci.nsIDocShellTreeItem) 1.20 + .rootTreeItem 1.21 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.22 + .getInterface(Ci.nsIDOMWindow) 1.23 + .QueryInterface(Ci.nsIDOMChromeWindow)); 1.24 + 1.25 +function dump(s) { 1.26 + Services.console.logStringMessage("AboutReader: " + s); 1.27 +} 1.28 + 1.29 +let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutReader.properties"); 1.30 + 1.31 +let AboutReader = function(doc, win) { 1.32 + dump("Init()"); 1.33 + 1.34 + this._docRef = Cu.getWeakReference(doc); 1.35 + this._winRef = Cu.getWeakReference(win); 1.36 + 1.37 + Services.obs.addObserver(this, "Reader:FaviconReturn", false); 1.38 + Services.obs.addObserver(this, "Reader:Add", false); 1.39 + Services.obs.addObserver(this, "Reader:Remove", false); 1.40 + Services.obs.addObserver(this, "Reader:ListStatusReturn", false); 1.41 + 1.42 + this._article = null; 1.43 + 1.44 + dump("Feching toolbar, header and content notes from about:reader"); 1.45 + this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header")); 1.46 + this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain")); 1.47 + this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title")); 1.48 + this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits")); 1.49 + this._contentElementRef = Cu.getWeakReference(doc.getElementById("reader-content")); 1.50 + this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar")); 1.51 + this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message")); 1.52 + 1.53 + this._toolbarEnabled = false; 1.54 + 1.55 + this._scrollOffset = win.pageYOffset; 1.56 + 1.57 + let body = doc.body; 1.58 + body.addEventListener("touchstart", this, false); 1.59 + body.addEventListener("click", this, false); 1.60 + 1.61 + win.addEventListener("unload", this, false); 1.62 + win.addEventListener("scroll", this, false); 1.63 + win.addEventListener("popstate", this, false); 1.64 + win.addEventListener("resize", this, false); 1.65 + 1.66 + this._setupAllDropdowns(); 1.67 + this._setupButton("toggle-button", this._onReaderToggle.bind(this)); 1.68 + this._setupButton("share-button", this._onShare.bind(this)); 1.69 + 1.70 + let colorSchemeOptions = [ 1.71 + { name: gStrings.GetStringFromName("aboutReader.colorSchemeDark"), 1.72 + value: "dark"}, 1.73 + { name: gStrings.GetStringFromName("aboutReader.colorSchemeLight"), 1.74 + value: "light"}, 1.75 + { name: gStrings.GetStringFromName("aboutReader.colorSchemeAuto"), 1.76 + value: "auto"} 1.77 + ]; 1.78 + 1.79 + let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); 1.80 + this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorSchemePref.bind(this)); 1.81 + this._setColorSchemePref(colorScheme); 1.82 + 1.83 + let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample"); 1.84 + let fontTypeOptions = [ 1.85 + { name: fontTypeSample, 1.86 + description: gStrings.GetStringFromName("aboutReader.fontTypeSerif"), 1.87 + value: "serif", 1.88 + linkClass: "serif" }, 1.89 + { name: fontTypeSample, 1.90 + description: gStrings.GetStringFromName("aboutReader.fontTypeSansSerif"), 1.91 + value: "sans-serif", 1.92 + linkClass: "sans-serif" 1.93 + }, 1.94 + ]; 1.95 + 1.96 + let fontType = Services.prefs.getCharPref("reader.font_type"); 1.97 + this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this)); 1.98 + this._setFontType(fontType); 1.99 + 1.100 + let fontSizeSample = gStrings.GetStringFromName("aboutReader.fontSizeSample"); 1.101 + let fontSizeOptions = [ 1.102 + { name: fontSizeSample, 1.103 + value: 1, 1.104 + linkClass: "font-size1-sample" }, 1.105 + { name: fontSizeSample, 1.106 + value: 2, 1.107 + linkClass: "font-size2-sample" }, 1.108 + { name: fontSizeSample, 1.109 + value: 3, 1.110 + linkClass: "font-size3-sample" }, 1.111 + { name: fontSizeSample, 1.112 + value: 4, 1.113 + linkClass: "font-size4-sample" }, 1.114 + { name: fontSizeSample, 1.115 + value: 5, 1.116 + linkClass: "font-size5-sample" } 1.117 + ]; 1.118 + 1.119 + let fontSize = Services.prefs.getIntPref("reader.font_size"); 1.120 + this._setupSegmentedButton("font-size-buttons", fontSizeOptions, fontSize, this._setFontSize.bind(this)); 1.121 + this._setFontSize(fontSize); 1.122 + 1.123 + dump("Decoding query arguments"); 1.124 + let queryArgs = this._decodeQueryString(win.location.href); 1.125 + 1.126 + // Track status of reader toolbar add/remove toggle button 1.127 + this._isReadingListItem = -1; 1.128 + this._updateToggleButton(); 1.129 + 1.130 + let url = queryArgs.url; 1.131 + let tabId = queryArgs.tabId; 1.132 + if (tabId) { 1.133 + dump("Loading from tab with ID: " + tabId + ", URL: " + url); 1.134 + this._loadFromTab(tabId, url); 1.135 + } else { 1.136 + dump("Fetching page with URL: " + url); 1.137 + this._loadFromURL(url); 1.138 + } 1.139 +} 1.140 + 1.141 +AboutReader.prototype = { 1.142 + _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " + 1.143 + ".content p > a:only-child > img:only-child, " + 1.144 + ".content .wp-caption img, " + 1.145 + ".content figure img", 1.146 + 1.147 + get _doc() { 1.148 + return this._docRef.get(); 1.149 + }, 1.150 + 1.151 + get _win() { 1.152 + return this._winRef.get(); 1.153 + }, 1.154 + 1.155 + get _headerElement() { 1.156 + return this._headerElementRef.get(); 1.157 + }, 1.158 + 1.159 + get _domainElement() { 1.160 + return this._domainElementRef.get(); 1.161 + }, 1.162 + 1.163 + get _titleElement() { 1.164 + return this._titleElementRef.get(); 1.165 + }, 1.166 + 1.167 + get _creditsElement() { 1.168 + return this._creditsElementRef.get(); 1.169 + }, 1.170 + 1.171 + get _contentElement() { 1.172 + return this._contentElementRef.get(); 1.173 + }, 1.174 + 1.175 + get _toolbarElement() { 1.176 + return this._toolbarElementRef.get(); 1.177 + }, 1.178 + 1.179 + get _messageElement() { 1.180 + return this._messageElementRef.get(); 1.181 + }, 1.182 + 1.183 + observe: function Reader_observe(aMessage, aTopic, aData) { 1.184 + switch(aTopic) { 1.185 + case "Reader:FaviconReturn": { 1.186 + let args = JSON.parse(aData); 1.187 + this._loadFavicon(args.url, args.faviconUrl); 1.188 + Services.obs.removeObserver(this, "Reader:FaviconReturn"); 1.189 + break; 1.190 + } 1.191 + 1.192 + case "Reader:Add": { 1.193 + let args = JSON.parse(aData); 1.194 + if (args.url == this._article.url) { 1.195 + if (this._isReadingListItem != 1) { 1.196 + this._isReadingListItem = 1; 1.197 + this._updateToggleButton(); 1.198 + } 1.199 + } 1.200 + break; 1.201 + } 1.202 + 1.203 + case "Reader:Remove": { 1.204 + if (aData == this._article.url) { 1.205 + if (this._isReadingListItem != 0) { 1.206 + this._isReadingListItem = 0; 1.207 + this._updateToggleButton(); 1.208 + } 1.209 + } 1.210 + break; 1.211 + } 1.212 + 1.213 + case "Reader:ListStatusReturn": { 1.214 + let args = JSON.parse(aData); 1.215 + if (args.url == this._article.url) { 1.216 + if (this._isReadingListItem != args.inReadingList) { 1.217 + let isInitialStateChange = (this._isReadingListItem == -1); 1.218 + this._isReadingListItem = args.inReadingList; 1.219 + this._updateToggleButton(); 1.220 + 1.221 + // Display the toolbar when all its initial component states are known 1.222 + if (isInitialStateChange) { 1.223 + this._setToolbarVisibility(true); 1.224 + } 1.225 + } 1.226 + } 1.227 + break; 1.228 + } 1.229 + } 1.230 + }, 1.231 + 1.232 + handleEvent: function Reader_handleEvent(aEvent) { 1.233 + if (!aEvent.isTrusted) 1.234 + return; 1.235 + 1.236 + switch (aEvent.type) { 1.237 + case "touchstart": 1.238 + this._scrolled = false; 1.239 + break; 1.240 + case "click": 1.241 + if (!this._scrolled) 1.242 + this._toggleToolbarVisibility(); 1.243 + break; 1.244 + case "scroll": 1.245 + if (!this._scrolled) { 1.246 + let isScrollingUp = this._scrollOffset > aEvent.pageY; 1.247 + this._setToolbarVisibility(isScrollingUp); 1.248 + this._scrollOffset = aEvent.pageY; 1.249 + } 1.250 + break; 1.251 + case "popstate": 1.252 + if (!aEvent.state) 1.253 + this._closeAllDropdowns(); 1.254 + break; 1.255 + case "resize": 1.256 + this._updateImageMargins(); 1.257 + break; 1.258 + 1.259 + case "devicelight": 1.260 + this._handleDeviceLight(aEvent.value); 1.261 + break; 1.262 + 1.263 + case "unload": 1.264 + Services.obs.removeObserver(this, "Reader:Add"); 1.265 + Services.obs.removeObserver(this, "Reader:Remove"); 1.266 + Services.obs.removeObserver(this, "Reader:ListStatusReturn"); 1.267 + break; 1.268 + } 1.269 + }, 1.270 + 1.271 + _updateToggleButton: function Reader_updateToggleButton() { 1.272 + let classes = this._doc.getElementById("toggle-button").classList; 1.273 + 1.274 + if (this._isReadingListItem == 1) { 1.275 + classes.add("on"); 1.276 + } else { 1.277 + classes.remove("on"); 1.278 + } 1.279 + }, 1.280 + 1.281 + _requestReadingListStatus: function Reader_requestReadingListStatus() { 1.282 + gChromeWin.sendMessageToJava({ 1.283 + type: "Reader:ListStatusRequest", 1.284 + url: this._article.url 1.285 + }); 1.286 + }, 1.287 + 1.288 + _onReaderToggle: function Reader_onToggle() { 1.289 + if (!this._article) 1.290 + return; 1.291 + 1.292 + this._isReadingListItem = (this._isReadingListItem == 1) ? 0 : 1; 1.293 + this._updateToggleButton(); 1.294 + 1.295 + // Create a relative timestamp for telemetry 1.296 + let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; 1.297 + 1.298 + if (this._isReadingListItem == 1) { 1.299 + gChromeWin.Reader.storeArticleInCache(this._article, function(success) { 1.300 + dump("Reader:Add (in reader) success=" + success); 1.301 + 1.302 + let result = gChromeWin.Reader.READER_ADD_FAILED; 1.303 + if (success) { 1.304 + result = gChromeWin.Reader.READER_ADD_SUCCESS; 1.305 + UITelemetry.addEvent("save.1", "button", uptime, "reader"); 1.306 + } 1.307 + 1.308 + let json = JSON.stringify({ fromAboutReader: true, url: this._article.url }); 1.309 + Services.obs.notifyObservers(null, "Reader:Add", json); 1.310 + 1.311 + gChromeWin.sendMessageToJava({ 1.312 + type: "Reader:Added", 1.313 + result: result, 1.314 + title: this._article.title, 1.315 + url: this._article.url, 1.316 + length: this._article.length, 1.317 + excerpt: this._article.excerpt 1.318 + }); 1.319 + }.bind(this)); 1.320 + } else { 1.321 + // In addition to removing the article from the cache (handled in 1.322 + // browser.js), sending this message will cause the toggle button to be 1.323 + // updated (handled in this file). 1.324 + Services.obs.notifyObservers(null, "Reader:Remove", this._article.url); 1.325 + 1.326 + UITelemetry.addEvent("unsave.1", "button", uptime, "reader"); 1.327 + } 1.328 + }, 1.329 + 1.330 + _onShare: function Reader_onShare() { 1.331 + if (!this._article) 1.332 + return; 1.333 + 1.334 + gChromeWin.sendMessageToJava({ 1.335 + type: "Reader:Share", 1.336 + url: this._article.url, 1.337 + title: this._article.title 1.338 + }); 1.339 + 1.340 + // Create a relative timestamp for telemetry 1.341 + let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; 1.342 + UITelemetry.addEvent("share.1", "list", uptime); 1.343 + }, 1.344 + 1.345 + _setFontSize: function Reader_setFontSize(newFontSize) { 1.346 + let bodyClasses = this._doc.body.classList; 1.347 + 1.348 + if (this._fontSize > 0) 1.349 + bodyClasses.remove("font-size" + this._fontSize); 1.350 + 1.351 + this._fontSize = newFontSize; 1.352 + bodyClasses.add("font-size" + this._fontSize); 1.353 + 1.354 + Services.prefs.setIntPref("reader.font_size", this._fontSize); 1.355 + }, 1.356 + 1.357 + _handleDeviceLight: function Reader_handleDeviceLight(newLux) { 1.358 + // Desired size of the this._luxValues array. 1.359 + let luxValuesSize = 10; 1.360 + // Add new lux value at the front of the array. 1.361 + this._luxValues.unshift(newLux); 1.362 + // Add new lux value to this._totalLux for averaging later. 1.363 + this._totalLux += newLux; 1.364 + 1.365 + // Don't update when length of array is less than luxValuesSize except when it is 1. 1.366 + if (this._luxValues.length < luxValuesSize) { 1.367 + // Use the first lux value to set the color scheme until our array equals luxValuesSize. 1.368 + if (this._luxValues.length == 1) { 1.369 + this._updateColorScheme(newLux); 1.370 + } 1.371 + return; 1.372 + } 1.373 + // Holds the average of the lux values collected in this._luxValues. 1.374 + let averageLuxValue = this._totalLux/luxValuesSize; 1.375 + 1.376 + this._updateColorScheme(averageLuxValue); 1.377 + // Pop the oldest value off the array. 1.378 + let oldLux = this._luxValues.pop(); 1.379 + // Subtract oldLux since it has been discarded from the array. 1.380 + this._totalLux -= oldLux; 1.381 + }, 1.382 + 1.383 + _updateColorScheme: function Reader_updateColorScheme(luxValue) { 1.384 + // Upper bound value for "dark" color scheme beyond which it changes to "light". 1.385 + let upperBoundDark = 50; 1.386 + // Lower bound value for "light" color scheme beyond which it changes to "dark". 1.387 + let lowerBoundLight = 10; 1.388 + // Threshold for color scheme change. 1.389 + let colorChangeThreshold = 20; 1.390 + 1.391 + // Ignore changes that are within a certain threshold of previous lux values. 1.392 + if ((this._colorScheme === "dark" && luxValue < upperBoundDark) || 1.393 + (this._colorScheme === "light" && luxValue > lowerBoundLight)) 1.394 + return; 1.395 + 1.396 + if (luxValue < colorChangeThreshold) 1.397 + this._setColorScheme("dark"); 1.398 + else 1.399 + this._setColorScheme("light"); 1.400 + }, 1.401 + 1.402 + _setColorScheme: function Reader_setColorScheme(newColorScheme) { 1.403 + if (this._colorScheme === newColorScheme) 1.404 + return; 1.405 + 1.406 + let bodyClasses = this._doc.body.classList; 1.407 + 1.408 + if (this._colorScheme) 1.409 + bodyClasses.remove(this._colorScheme); 1.410 + 1.411 + this._colorScheme = newColorScheme; 1.412 + bodyClasses.add(this._colorScheme); 1.413 + }, 1.414 + 1.415 + // Pref values include "dark", "light", and "auto", which automatically switches 1.416 + // between light and dark color schemes based on the ambient light level. 1.417 + _setColorSchemePref: function Reader_setColorSchemePref(colorSchemePref) { 1.418 + if (colorSchemePref === "auto") { 1.419 + this._win.addEventListener("devicelight", this, false); 1.420 + this._luxValues = []; 1.421 + this._totalLux = 0; 1.422 + } else { 1.423 + this._win.removeEventListener("devicelight", this, false); 1.424 + this._setColorScheme(colorSchemePref); 1.425 + delete this._luxValues; 1.426 + delete this._totalLux; 1.427 + } 1.428 + 1.429 + Services.prefs.setCharPref("reader.color_scheme", colorSchemePref); 1.430 + }, 1.431 + 1.432 + _setFontType: function Reader_setFontType(newFontType) { 1.433 + if (this._fontType === newFontType) 1.434 + return; 1.435 + 1.436 + let bodyClasses = this._doc.body.classList; 1.437 + 1.438 + if (this._fontType) 1.439 + bodyClasses.remove(this._fontType); 1.440 + 1.441 + this._fontType = newFontType; 1.442 + bodyClasses.add(this._fontType); 1.443 + 1.444 + Services.prefs.setCharPref("reader.font_type", this._fontType); 1.445 + }, 1.446 + 1.447 + _getToolbarVisibility: function Reader_getToolbarVisibility() { 1.448 + return !this._toolbarElement.classList.contains("toolbar-hidden"); 1.449 + }, 1.450 + 1.451 + _setToolbarVisibility: function Reader_setToolbarVisibility(visible) { 1.452 + let win = this._win; 1.453 + if (win.history.state) 1.454 + win.history.back(); 1.455 + 1.456 + if (!this._toolbarEnabled) 1.457 + return; 1.458 + 1.459 + // Don't allow visible toolbar until banner state is known 1.460 + if (this._isReadingListItem == -1) 1.461 + return; 1.462 + 1.463 + if (this._getToolbarVisibility() === visible) 1.464 + return; 1.465 + 1.466 + this._toolbarElement.classList.toggle("toolbar-hidden"); 1.467 + this._setSystemUIVisibility(visible); 1.468 + 1.469 + if (!visible && !this._hasUsedToolbar) { 1.470 + this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar"); 1.471 + if (!this._hasUsedToolbar) { 1.472 + gChromeWin.NativeWindow.toast.show(gStrings.GetStringFromName("aboutReader.toolbarTip"), "short"); 1.473 + 1.474 + Services.prefs.setBoolPref("reader.has_used_toolbar", true); 1.475 + this._hasUsedToolbar = true; 1.476 + } 1.477 + } 1.478 + }, 1.479 + 1.480 + _toggleToolbarVisibility: function Reader_toggleToolbarVisibility(visible) { 1.481 + this._setToolbarVisibility(!this._getToolbarVisibility()); 1.482 + }, 1.483 + 1.484 + _setSystemUIVisibility: function Reader_setSystemUIVisibility(visible) { 1.485 + gChromeWin.sendMessageToJava({ 1.486 + type: "SystemUI:Visibility", 1.487 + visible: visible 1.488 + }); 1.489 + }, 1.490 + 1.491 + _loadFromURL: function Reader_loadFromURL(url) { 1.492 + this._showProgressDelayed(); 1.493 + 1.494 + gChromeWin.Reader.parseDocumentFromURL(url, function(article) { 1.495 + if (article) 1.496 + this._showContent(article); 1.497 + else 1.498 + this._showError(gStrings.GetStringFromName("aboutReader.loadError")); 1.499 + }.bind(this)); 1.500 + }, 1.501 + 1.502 + _loadFromTab: function Reader_loadFromTab(tabId, url) { 1.503 + this._showProgressDelayed(); 1.504 + 1.505 + gChromeWin.Reader.getArticleForTab(tabId, url, function(article) { 1.506 + if (article) 1.507 + this._showContent(article); 1.508 + else 1.509 + this._showError(gStrings.GetStringFromName("aboutReader.loadError")); 1.510 + }.bind(this)); 1.511 + }, 1.512 + 1.513 + _requestFavicon: function Reader_requestFavicon() { 1.514 + gChromeWin.sendMessageToJava({ 1.515 + type: "Reader:FaviconRequest", 1.516 + url: this._article.url 1.517 + }); 1.518 + }, 1.519 + 1.520 + _loadFavicon: function Reader_loadFavicon(url, faviconUrl) { 1.521 + if (this._article.url !== url) 1.522 + return; 1.523 + 1.524 + let doc = this._doc; 1.525 + 1.526 + let link = doc.createElement('link'); 1.527 + link.rel = 'shortcut icon'; 1.528 + link.href = faviconUrl; 1.529 + 1.530 + doc.getElementsByTagName('head')[0].appendChild(link); 1.531 + }, 1.532 + 1.533 + _updateImageMargins: function Reader_updateImageMargins() { 1.534 + let windowWidth = this._win.innerWidth; 1.535 + let contentWidth = this._contentElement.offsetWidth; 1.536 + let maxWidthStyle = windowWidth + "px !important"; 1.537 + 1.538 + let setImageMargins = function(img) { 1.539 + if (!img._originalWidth) 1.540 + img._originalWidth = img.offsetWidth; 1.541 + 1.542 + let imgWidth = img._originalWidth; 1.543 + 1.544 + // If the image is taking more than half of the screen, just make 1.545 + // it fill edge-to-edge. 1.546 + if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55) 1.547 + imgWidth = windowWidth; 1.548 + 1.549 + let sideMargin = Math.max((contentWidth - windowWidth) / 2, 1.550 + (contentWidth - imgWidth) / 2); 1.551 + 1.552 + let imageStyle = sideMargin + "px !important"; 1.553 + let widthStyle = imgWidth + "px !important"; 1.554 + 1.555 + let cssText = "max-width: " + maxWidthStyle + ";" + 1.556 + "width: " + widthStyle + ";" + 1.557 + "margin-left: " + imageStyle + ";" + 1.558 + "margin-right: " + imageStyle + ";"; 1.559 + 1.560 + img.style.cssText = cssText; 1.561 + } 1.562 + 1.563 + let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR); 1.564 + for (let i = imgs.length; --i >= 0;) { 1.565 + let img = imgs[i]; 1.566 + 1.567 + if (img.width > 0) { 1.568 + setImageMargins(img); 1.569 + } else { 1.570 + img.onload = function() { 1.571 + setImageMargins(img); 1.572 + } 1.573 + } 1.574 + } 1.575 + }, 1.576 + 1.577 + _maybeSetTextDirection: function Read_maybeSetTextDirection(article){ 1.578 + if(!article.dir) 1.579 + return; 1.580 + 1.581 + //Set "dir" attribute on content 1.582 + this._contentElement.setAttribute("dir", article.dir); 1.583 + this._headerElement.setAttribute("dir", article.dir); 1.584 + }, 1.585 + 1.586 + _showError: function Reader_showError(error) { 1.587 + this._headerElement.style.display = "none"; 1.588 + this._contentElement.style.display = "none"; 1.589 + 1.590 + this._messageElement.innerHTML = error; 1.591 + this._messageElement.style.display = "block"; 1.592 + 1.593 + this._doc.title = error; 1.594 + }, 1.595 + 1.596 + // This function is the JS version of Java's StringUtils.stripCommonSubdomains. 1.597 + _stripHost: function Reader_stripHost(host) { 1.598 + if (!host) 1.599 + return host; 1.600 + 1.601 + let start = 0; 1.602 + 1.603 + if (host.startsWith("www.")) 1.604 + start = 4; 1.605 + else if (host.startsWith("m.")) 1.606 + start = 2; 1.607 + else if (host.startsWith("mobile.")) 1.608 + start = 7; 1.609 + 1.610 + return host.substring(start); 1.611 + }, 1.612 + 1.613 + _showContent: function Reader_showContent(article) { 1.614 + this._messageElement.style.display = "none"; 1.615 + 1.616 + this._article = article; 1.617 + 1.618 + this._domainElement.href = article.url; 1.619 + let articleUri = Services.io.newURI(article.url, null, null); 1.620 + this._domainElement.innerHTML = this._stripHost(articleUri.host); 1.621 + 1.622 + this._creditsElement.innerHTML = article.byline; 1.623 + 1.624 + this._titleElement.textContent = article.title; 1.625 + this._doc.title = article.title; 1.626 + 1.627 + this._headerElement.style.display = "block"; 1.628 + 1.629 + let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils); 1.630 + let contentFragment = parserUtils.parseFragment(article.content, Ci.nsIParserUtils.SanitizerDropForms, 1.631 + false, articleUri, this._contentElement); 1.632 + this._contentElement.innerHTML = ""; 1.633 + this._contentElement.appendChild(contentFragment); 1.634 + this._updateImageMargins(); 1.635 + this._maybeSetTextDirection(article); 1.636 + 1.637 + this._contentElement.style.display = "block"; 1.638 + this._requestReadingListStatus(); 1.639 + 1.640 + this._toolbarEnabled = true; 1.641 + this._setToolbarVisibility(true); 1.642 + 1.643 + this._requestFavicon(); 1.644 + }, 1.645 + 1.646 + _hideContent: function Reader_hideContent() { 1.647 + this._headerElement.style.display = "none"; 1.648 + this._contentElement.style.display = "none"; 1.649 + }, 1.650 + 1.651 + _showProgressDelayed: function Reader_showProgressDelayed() { 1.652 + this._win.setTimeout(function() { 1.653 + // Article has already been loaded, no need to show 1.654 + // progress anymore. 1.655 + if (this._article) 1.656 + return; 1.657 + 1.658 + this._headerElement.style.display = "none"; 1.659 + this._contentElement.style.display = "none"; 1.660 + 1.661 + this._messageElement.innerHTML = gStrings.GetStringFromName("aboutReader.loading"); 1.662 + this._messageElement.style.display = "block"; 1.663 + }.bind(this), 300); 1.664 + }, 1.665 + 1.666 + _decodeQueryString: function Reader_decodeQueryString(url) { 1.667 + let result = {}; 1.668 + let query = url.split("?")[1]; 1.669 + if (query) { 1.670 + let pairs = query.split("&"); 1.671 + for (let i = 0; i < pairs.length; i++) { 1.672 + let [name, value] = pairs[i].split("="); 1.673 + result[name] = decodeURIComponent(value); 1.674 + } 1.675 + } 1.676 + 1.677 + return result; 1.678 + }, 1.679 + 1.680 + _setupSegmentedButton: function Reader_setupSegmentedButton(id, options, initialValue, callback) { 1.681 + let doc = this._doc; 1.682 + let segmentedButton = doc.getElementById(id); 1.683 + 1.684 + for (let i = 0; i < options.length; i++) { 1.685 + let option = options[i]; 1.686 + 1.687 + let item = doc.createElement("li"); 1.688 + let link = doc.createElement("a"); 1.689 + link.textContent = option.name; 1.690 + item.appendChild(link); 1.691 + 1.692 + if (option.linkClass !== undefined) 1.693 + link.classList.add(option.linkClass); 1.694 + 1.695 + if (option.description !== undefined) { 1.696 + let description = doc.createElement("div"); 1.697 + description.textContent = option.description; 1.698 + item.appendChild(description); 1.699 + } 1.700 + 1.701 + link.style.MozUserSelect = 'none'; 1.702 + segmentedButton.appendChild(item); 1.703 + 1.704 + link.addEventListener("click", function(aEvent) { 1.705 + if (!aEvent.isTrusted) 1.706 + return; 1.707 + 1.708 + aEvent.stopPropagation(); 1.709 + 1.710 + // Create a relative timestamp for telemetry 1.711 + let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; 1.712 + // Just pass the ID of the button as an extra and hope the ID doesn't change 1.713 + // unless the context changes 1.714 + UITelemetry.addEvent("action.1", "button", uptime, id); 1.715 + 1.716 + let items = segmentedButton.children; 1.717 + for (let j = items.length - 1; j >= 0; j--) { 1.718 + items[j].classList.remove("selected"); 1.719 + } 1.720 + 1.721 + item.classList.add("selected"); 1.722 + callback(option.value); 1.723 + }.bind(this), true); 1.724 + 1.725 + if (option.value === initialValue) 1.726 + item.classList.add("selected"); 1.727 + } 1.728 + }, 1.729 + 1.730 + _setupButton: function Reader_setupButton(id, callback) { 1.731 + let button = this._doc.getElementById(id); 1.732 + 1.733 + button.addEventListener("click", function(aEvent) { 1.734 + if (!aEvent.isTrusted) 1.735 + return; 1.736 + 1.737 + aEvent.stopPropagation(); 1.738 + callback(); 1.739 + }, true); 1.740 + }, 1.741 + 1.742 + _setupAllDropdowns: function Reader_setupAllDropdowns() { 1.743 + let doc = this._doc; 1.744 + let win = this._win; 1.745 + 1.746 + let dropdowns = doc.getElementsByClassName("dropdown"); 1.747 + 1.748 + for (let i = dropdowns.length - 1; i >= 0; i--) { 1.749 + let dropdown = dropdowns[i]; 1.750 + 1.751 + let dropdownToggle = dropdown.getElementsByClassName("dropdown-toggle")[0]; 1.752 + let dropdownPopup = dropdown.getElementsByClassName("dropdown-popup")[0]; 1.753 + 1.754 + if (!dropdownToggle || !dropdownPopup) 1.755 + continue; 1.756 + 1.757 + let dropdownArrow = doc.createElement("div"); 1.758 + dropdownArrow.className = "dropdown-arrow"; 1.759 + dropdownPopup.appendChild(dropdownArrow); 1.760 + 1.761 + let updatePopupPosition = function() { 1.762 + let popupWidth = dropdownPopup.offsetWidth + 30; 1.763 + let arrowWidth = dropdownArrow.offsetWidth; 1.764 + let toggleWidth = dropdownToggle.offsetWidth; 1.765 + let toggleLeft = dropdownToggle.offsetLeft; 1.766 + 1.767 + let popupShift = (toggleWidth - popupWidth) / 2; 1.768 + let popupLeft = Math.max(0, Math.min(win.innerWidth - popupWidth, toggleLeft + popupShift)); 1.769 + dropdownPopup.style.left = popupLeft + "px"; 1.770 + 1.771 + let arrowShift = (toggleWidth - arrowWidth) / 2; 1.772 + let arrowLeft = toggleLeft - popupLeft + arrowShift; 1.773 + dropdownArrow.style.left = arrowLeft + "px"; 1.774 + }; 1.775 + 1.776 + win.addEventListener("resize", function(aEvent) { 1.777 + if (!aEvent.isTrusted) 1.778 + return; 1.779 + 1.780 + // Wait for reflow before calculating the new position of the popup. 1.781 + setTimeout(updatePopupPosition, 0); 1.782 + }, true); 1.783 + 1.784 + dropdownToggle.addEventListener("click", function(aEvent) { 1.785 + if (!aEvent.isTrusted) 1.786 + return; 1.787 + 1.788 + aEvent.stopPropagation(); 1.789 + 1.790 + if (!this._getToolbarVisibility()) 1.791 + return; 1.792 + 1.793 + let dropdownClasses = dropdown.classList; 1.794 + 1.795 + if (dropdownClasses.contains("open")) { 1.796 + win.history.back(); 1.797 + } else { 1.798 + updatePopupPosition(); 1.799 + if (!this._closeAllDropdowns()) 1.800 + this._pushDropdownState(); 1.801 + 1.802 + dropdownClasses.add("open"); 1.803 + } 1.804 + }.bind(this), true); 1.805 + } 1.806 + }, 1.807 + 1.808 + _pushDropdownState: function Reader_pushDropdownState() { 1.809 + // FIXME: We're getting a NS_ERROR_UNEXPECTED error when we try 1.810 + // to do win.history.pushState() here (see bug 682296). This is 1.811 + // a workaround that allows us to push history state on the target 1.812 + // content document. 1.813 + 1.814 + let doc = this._doc; 1.815 + let body = doc.body; 1.816 + 1.817 + if (this._pushStateScript) 1.818 + body.removeChild(this._pushStateScript); 1.819 + 1.820 + this._pushStateScript = doc.createElement('script'); 1.821 + this._pushStateScript.type = "text/javascript"; 1.822 + this._pushStateScript.innerHTML = 'history.pushState({ dropdown: 1 }, document.title);'; 1.823 + 1.824 + body.appendChild(this._pushStateScript); 1.825 + }, 1.826 + 1.827 + _closeAllDropdowns : function Reader_closeAllDropdowns() { 1.828 + let dropdowns = this._doc.querySelectorAll(".dropdown.open"); 1.829 + for (let i = dropdowns.length - 1; i >= 0; i--) { 1.830 + dropdowns[i].classList.remove("open"); 1.831 + } 1.832 + 1.833 + return (dropdowns.length > 0) 1.834 + } 1.835 +};