diff options
Diffstat (limited to 'toolkit/components/reader/AboutReader.jsm')
-rw-r--r-- | toolkit/components/reader/AboutReader.jsm | 314 |
1 files changed, 182 insertions, 132 deletions
diff --git a/toolkit/components/reader/AboutReader.jsm b/toolkit/components/reader/AboutReader.jsm index 1fb9db123..ae1ff9d60 100644 --- a/toolkit/components/reader/AboutReader.jsm +++ b/toolkit/components/reader/AboutReader.jsm @@ -15,8 +15,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AsyncPrefs", "resource://gre/modules/AsyncPrefs.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties"); @@ -45,45 +44,49 @@ var AboutReader = function(mm, win, articlePromise) { .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.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._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("click", this, false); + doc.addEventListener("click", this); - win.addEventListener("pagehide", this, false); - win.addEventListener("scroll", this, false); - win.addEventListener("resize", this, false); + win.addEventListener("pagehide", this); + win.addEventListener("scroll", this); + win.addEventListener("resize", this); Services.obs.addObserver(this, "inner-window-destroyed", false); - doc.addEventListener("visibilitychange", this, false); + doc.addEventListener("visibilitychange", this); this._setupStyleDropdown(); this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close"); - const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; - if (gIsFirefoxDesktop) { - // we're ready for any external setup, send a signal for that. - this._mm.sendAsyncMessage("Reader:OnSetup"); - } + // we're ready for any external setup, send a signal for that. + this._mm.sendAsyncMessage("Reader:OnSetup"); let colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values")); let colorSchemeOptions = colorSchemeValues.map((value) => { - return { name: gStrings.GetStringFromName("aboutReader.colorScheme." + value), - value: value, - itemClass: value + "-button" }; + return { + name: gStrings.GetStringFromName("aboutReader.colorScheme." + value), + value, + itemClass: value + "-button" + }; }); let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); @@ -114,7 +117,7 @@ var AboutReader = function(mm, win, articlePromise) { this._setupLineHeightButtons(); if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) { - new NarrateControls(mm, win); + new NarrateControls(mm, win, this._languagePromise); } this._loadArticle(); @@ -146,6 +149,10 @@ AboutReader.prototype = { return this._titleElementRef.get(); }, + get _readTimeElement() { + return this._readTimeElementRef.get(); + }, + get _creditsElement() { return this._creditsElementRef.get(); }, @@ -162,6 +169,10 @@ AboutReader.prototype = { return this._messageElementRef.get(); }, + get _containerElement() { + return this._containerElementRef.get(); + }, + get _isToolbarVertical() { if (this._toolbarVertical !== undefined) { return this._toolbarVertical; @@ -178,7 +189,7 @@ AboutReader.prototype = { return _viewId; }, - receiveMessage: function (message) { + receiveMessage (message) { switch (message.name) { // Triggered by Android user pressing BACK while the banner font-dropdown is open. case "Reader:CloseDropdown": { @@ -191,14 +202,14 @@ AboutReader.prototype = { if (message.data.id && message.data.image && !this._doc.getElementById(message.data.id)) { let btn = this._doc.createElement("button"); - btn.setAttribute("class", "button"); + btn.setAttribute("class", "button " + message.data.id); btn.setAttribute("style", "background-image: url('" + message.data.image + "')"); btn.setAttribute("id", message.data.id); if (message.data.title) btn.setAttribute("title", message.data.title); if (message.data.text) btn.textContent = message.data.text; - let tb = this._doc.getElementById("reader-toolbar"); + let tb = this._toolbarElement; tb.appendChild(btn); this._setupButton(message.data.id, button => { this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article }); @@ -220,18 +231,21 @@ AboutReader.prototype = { } }, - handleEvent: function(aEvent) { + handleEvent(aEvent) { if (!aEvent.isTrusted) return; switch (aEvent.type) { case "click": let target = aEvent.target; - if (target.classList.contains('dropdown-toggle')) { + if (target.classList.contains("dropdown-toggle")) { this._toggleDropdownClicked(aEvent); - } else if (!target.closest('.dropdown-popup')) { + } else if (!target.closest(".dropdown-popup")) { this._closeDropdowns(); } + if (target.tagName == "A" && !target.classList.contains("reader-domain")) { + this._linkClicked(aEvent); + } break; case "scroll": this._closeDropdowns(true); @@ -243,7 +257,7 @@ AboutReader.prototype = { this._updateImageMargins(); if (this._isToolbarVertical) { this._win.setTimeout(() => { - for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) { + for (let dropdown of this._doc.querySelectorAll(".dropdown.open")) { this._updatePopupPosition(dropdown); } }, 0); @@ -271,12 +285,12 @@ AboutReader.prototype = { } }, - observe: function(subject, topic, data) { + observe(subject, topic, data) { if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) { return; } - Services.obs.removeObserver(this, "inner-window-destroyed", false); + Services.obs.removeObserver(this, "inner-window-destroyed"); this._mm.removeMessageListener("Reader:CloseDropdown", this); this._mm.removeMessageListener("Reader:AddButton", this); @@ -284,12 +298,12 @@ AboutReader.prototype = { this._windowUnloaded = true; }, - _onReaderClose: function() { + _onReaderClose() { ReaderMode.leaveReaderMode(this._mm.docShell, this._win); }, - _setFontSize: function(newFontSize) { - let containerClasses = this._doc.getElementById("container").classList; + _setFontSize(newFontSize) { + let containerClasses = this._containerElement.classList; if (this._fontSize > 0) containerClasses.remove("font-size" + this._fontSize); @@ -299,19 +313,19 @@ AboutReader.prototype = { return AsyncPrefs.set("reader.font_size", this._fontSize); }, - _setupFontSizeButtons: function() { + _setupFontSizeButtons() { const FONT_SIZE_MIN = 1; const FONT_SIZE_MAX = 9; // Sample text shown in Android UI. - let sampleText = this._doc.getElementById("font-size-sample"); + 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.getElementById("font-size-plus"); - let minusButton = this._doc.getElementById("font-size-minus"); + let plusButton = this._doc.querySelector(".plus-button"); + let minusButton = this._doc.querySelector(".minus-button"); function updateControls() { if (currentSize === FONT_SIZE_MIN) { @@ -360,8 +374,8 @@ AboutReader.prototype = { }, true); }, - _setContentWidth: function(newContentWidth) { - let containerClasses = this._doc.getElementById("container").classList; + _setContentWidth(newContentWidth) { + let containerClasses = this._containerElement.classList; if (this._contentWidth > 0) containerClasses.remove("content-width" + this._contentWidth); @@ -371,15 +385,15 @@ AboutReader.prototype = { return AsyncPrefs.set("reader.content_width", this._contentWidth); }, - _setupContentWidthButtons: function() { + _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.getElementById("content-width-plus"); - let minusButton = this._doc.getElementById("content-width-minus"); + 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) { @@ -428,8 +442,8 @@ AboutReader.prototype = { }, true); }, - _setLineHeight: function(newLineHeight) { - let contentClasses = this._doc.getElementById("moz-reader-content").classList; + _setLineHeight(newLineHeight) { + let contentClasses = this._contentElement.classList; if (this._lineHeight > 0) contentClasses.remove("line-height" + this._lineHeight); @@ -439,15 +453,15 @@ AboutReader.prototype = { return AsyncPrefs.set("reader.line_height", this._lineHeight); }, - _setupLineHeightButtons: function() { + _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.getElementById("line-height-plus"); - let minusButton = this._doc.getElementById("line-height-minus"); + 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) { @@ -496,7 +510,7 @@ AboutReader.prototype = { }, true); }, - _handleDeviceLight: function(newLux) { + _handleDeviceLight(newLux) { // Desired size of the this._luxValues array. let luxValuesSize = 10; // Add new lux value at the front of the array. @@ -513,7 +527,7 @@ AboutReader.prototype = { return; } // Holds the average of the lux values collected in this._luxValues. - let averageLuxValue = this._totalLux/luxValuesSize; + let averageLuxValue = this._totalLux / luxValuesSize; this._updateColorScheme(averageLuxValue); // Pop the oldest value off the array. @@ -522,7 +536,7 @@ AboutReader.prototype = { this._totalLux -= oldLux; }, - _handleVisibilityChange: function() { + _handleVisibilityChange() { let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); if (colorScheme != "auto") { return; @@ -533,19 +547,19 @@ AboutReader.prototype = { }, // Setup or teardown the ambient light tracking system. - _enableAmbientLighting: function(enable) { + _enableAmbientLighting(enable) { if (enable) { - this._win.addEventListener("devicelight", this, false); + this._win.addEventListener("devicelight", this); this._luxValues = []; this._totalLux = 0; } else { - this._win.removeEventListener("devicelight", this, false); + this._win.removeEventListener("devicelight", this); delete this._luxValues; delete this._totalLux; } }, - _updateColorScheme: function(luxValue) { + _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". @@ -564,7 +578,7 @@ AboutReader.prototype = { this._setColorScheme("light"); }, - _setColorScheme: function(newColorScheme) { + _setColorScheme(newColorScheme) { // "auto" is not a real color scheme if (this._colorScheme === newColorScheme || newColorScheme === "auto") return; @@ -580,14 +594,14 @@ AboutReader.prototype = { // 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) { + _setColorSchemePref(colorSchemePref) { this._enableAmbientLighting(colorSchemePref === "auto"); this._setColorScheme(colorSchemePref); AsyncPrefs.set("reader.color_scheme", colorSchemePref); }, - _setFontType: function(newFontType) { + _setFontType(newFontType) { if (this._fontType === newFontType) return; @@ -602,20 +616,38 @@ AboutReader.prototype = { AsyncPrefs.set("reader.font_type", this._fontType); }, - _setSystemUIVisibility: function(visible) { + _setSystemUIVisibility(visible) { this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible }); }, - _loadArticle: Task.async(function* () { + _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 = yield this._articlePromise; + article = await this._articlePromise; } else { try { - article = yield this._getArticle(url); + article = await this._getArticle(url); } catch (e) { if (e && e.newURL) { let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL); @@ -638,24 +670,26 @@ AboutReader.prototype = { } this._showContent(article); - }), - - _getArticle: function(url) { - return new Promise((resolve, reject) => { - let listener = (message) => { - this._mm.removeMessageListener("Reader:ArticleData", listener); - if (message.data.newURL) { - reject({ newURL: message.data.newURL }); - return; - } - resolve(message.data.article); - }; - this._mm.addMessageListener("Reader:ArticleData", listener); - this._mm.sendAsyncMessage("Reader:ArticleGet", { url: url }); - }); }, - _requestFavicon: function() { + _getArticle(url) { + // return new Promise((resolve, reject) => { + // let listener = (message) => { + // this._mm.removeMessageListener("Reader:ArticleData", listener); + // if (message.data.newURL) { + // reject({ newURL: message.data.newURL }); + // return; + // } + // resolve(message.data.article); + // }; + // this._mm.addMessageListener("Reader:ArticleData", listener); + // this._mm.sendAsyncMessage("Reader:ArticleGet", { url }); + // }); + //} + return ReaderMode.downloadAndParseDocument(url); + }, + + _requestFavicon() { let handleFaviconReturn = (message) => { this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn); this._loadFavicon(message.data.url, message.data.faviconUrl); @@ -665,20 +699,20 @@ AboutReader.prototype = { this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url }); }, - _loadFavicon: function(url, faviconUrl) { + _loadFavicon(url, faviconUrl) { if (this._article.url !== url) return; let doc = this._doc; - let link = doc.createElement('link'); - link.rel = 'shortcut icon'; + let link = doc.createElement("link"); + link.rel = "shortcut icon"; link.href = faviconUrl; - doc.getElementsByTagName('head')[0].appendChild(link); + doc.getElementsByTagName("head")[0].appendChild(link); }, - _updateImageMargins: function() { + _updateImageMargins() { let windowWidth = this._win.innerWidth; let bodyWidth = this._doc.body.clientWidth; @@ -691,7 +725,7 @@ AboutReader.prototype = { } // If the image is at least half as wide as the body, center it on desktop. - if (img.naturalWidth >= bodyWidth/2) { + if (img.naturalWidth >= bodyWidth / 2) { img.setAttribute("moz-reader-center", true); } else { img.removeAttribute("moz-reader-center"); @@ -713,30 +747,32 @@ AboutReader.prototype = { }, _maybeSetTextDirection: function Read_maybeSetTextDirection(article) { - if (!article.dir) - return; + 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"; + } + }, + + _formatReadTime(slowEstimate, fastEstimate) { + let displayStringKey = "aboutReader.estimatedReadTimeRange1"; - // Set "dir" attribute on content - this._contentElement.setAttribute("dir", article.dir); - this._headerElement.setAttribute("dir", article.dir); - }, - - _fixLocalLinks() { - // We need to do this because preprocessing the content through nsIParserUtils - // gives back a DOM with a <base> element. That influences how these URLs get - // resolved, making them no longer match the document URI (which is - // about:reader?url=...). To fix this, make all the hash URIs absolute. This - // is hacky, but the alternative of removing the base element has potential - // security implications if Readability has not successfully made all the URLs - // absolute, so we pick just fixing these in-document links explicitly. - let localLinks = this._contentElement.querySelectorAll("a[href^='#']"); - for (let localLink of localLinks) { - // Have to get the attribute because .href provides an absolute URI. - localLink.href = this._doc.documentURI + localLink.getAttribute("href"); + // 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: function() { + _showError() { this._headerElement.style.display = "none"; this._contentElement.style.display = "none"; @@ -746,11 +782,16 @@ AboutReader.prototype = { 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: function(host) { + _stripHost(host) { if (!host) return host; @@ -766,17 +807,18 @@ AboutReader.prototype = { return host.substring(start); }, - _showContent: function(article) { + _showContent(article) { this._messageElement.style.display = "none"; this._article = article; this._domainElement.href = article.url; - let articleUri = Services.io.newURI(article.url, null, null); + 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"; @@ -787,8 +829,8 @@ AboutReader.prototype = { 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(); @@ -804,13 +846,13 @@ AboutReader.prototype = { new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false })); }, - _hideContent: function() { + _hideContent() { this._headerElement.style.display = "none"; this._contentElement.style.display = "none"; }, - _showProgressDelayed: function() { - this._win.setTimeout(function() { + _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. @@ -823,20 +865,20 @@ AboutReader.prototype = { this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2"); this._messageElement.style.display = "block"; - }.bind(this), 300); + }, 300); }, /** * Returns the original article URL for this about:reader view. */ - _getOriginalUrl: function(win) { + _getOriginalUrl(win) { let url = win ? win.location.href : this._win.location.href; return ReaderMode.getOriginalUrl(url) || url; }, - _setupSegmentedButton: function(id, options, initialValue, callback) { + _setupSegmentedButton(id, options, initialValue, callback) { let doc = this._doc; - let segmentedButton = doc.getElementById(id); + let segmentedButton = doc.getElementsByClassName(id)[0]; for (let i = 0; i < options.length; i++) { let option = options[i]; @@ -867,10 +909,6 @@ AboutReader.prototype = { 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"); @@ -878,19 +916,19 @@ AboutReader.prototype = { item.classList.add("selected"); callback(option.value); - }.bind(this), true); + }, true); if (option.value === initialValue) item.classList.add("selected"); } }, - _setupButton: function(id, callback, titleEntity, textEntity) { + _setupButton(id, callback, titleEntity, textEntity) { if (titleEntity) { this._setButtonTip(id, titleEntity); } - let button = this._doc.getElementById(id); + let button = this._doc.getElementsByClassName(id)[0]; if (textEntity) { button.textContent = gStrings.GetStringFromName(textEntity); } @@ -910,17 +948,17 @@ AboutReader.prototype = { * and dynamically as button state changes. * @param Localizable string providing UI element usage tip. */ - _setButtonTip: function(id, titleEntity) { - let button = this._doc.getElementById(id); + _setButtonTip(id, titleEntity) { + let button = this._doc.getElementsByClassName(id)[0]; button.setAttribute("title", gStrings.GetStringFromName(titleEntity)); }, - _setupStyleDropdown: function() { - let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle"); + _setupStyleDropdown() { + let dropdownToggle = this._doc.querySelector(".style-dropdown .dropdown-toggle"); dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls")); }, - _updatePopupPosition: function(dropdown) { + _updatePopupPosition(dropdown) { let dropdownToggle = dropdown.querySelector(".dropdown-toggle"); let dropdownPopup = dropdown.querySelector(".dropdown-popup"); @@ -931,8 +969,8 @@ AboutReader.prototype = { dropdownPopup.style.top = popupTop + "px"; }, - _toggleDropdownClicked: function(event) { - let dropdown = event.target.closest('.dropdown'); + _toggleDropdownClicked(event) { + let dropdown = event.target.closest(".dropdown"); if (!dropdown) return; @@ -952,7 +990,7 @@ AboutReader.prototype = { /* * If the ReaderView banner font-dropdown is closed, open it. */ - _openDropdown: function(dropdown) { + _openDropdown(dropdown) { if (dropdown.classList.contains("open")) { return; } @@ -969,7 +1007,7 @@ AboutReader.prototype = { * dropdowns because the page is scrolling, allow popups to stay open with * the keep-open class. */ - _closeDropdowns: function(scrolling) { + _closeDropdowns(scrolling) { let selector = ".dropdown.open"; if (scrolling) { selector += ":not(.keep-open)"; @@ -987,6 +1025,18 @@ AboutReader.prototype = { }, /* + * Override link handling for same-page references so we don't exit Reader View. + */ + _linkClicked(event) { + var originalUrl = Services.io.newURI(this._getOriginalUrl(), null, null); + var targetUrl = Services.io.newURI(event.target.href, null, null); + if (originalUrl.specIgnoringRef == targetUrl.specIgnoringRef) { + event.preventDefault(); + this._goToReference(targetUrl.ref); + } + }, + + /* * Scroll reader view to a reference */ _goToReference(ref) { |