1 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 var EXPORTED_SYMBOLS = ["AboutReaderParent"];
10 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
12 ChromeUtils.defineModuleGetter(
15 "resource://gre/modules/PlacesUtils.jsm"
17 ChromeUtils.defineModuleGetter(
20 "resource://gre/modules/ReaderMode.jsm"
23 const gStringBundle = Services.strings.createBundle(
24 "chrome://global/locale/aboutReader.properties"
27 // A set of all of the AboutReaderParent actors that exist.
28 // See bug 1631146 for a request for a less manual way of doing this.
29 let gAllActors = new Set();
31 // A map of message names to listeners that listen to messages
32 // received by the AboutReaderParent actors.
33 let gListeners = new Map();
35 // As a reader mode document could be loaded in a different process than
36 // the source article, temporarily cache the article data here in the
37 // parent while switching to it.
38 let gCachedArticles = new Map();
40 class AboutReaderParent extends JSWindowActorParent {
42 gAllActors.delete(this);
44 if (this.isReaderMode()) {
45 let url = this.manager.documentURI.spec;
46 url = decodeURIComponent(url.substr("about:reader?url=".length));
47 gCachedArticles.delete(url);
52 return this.manager.documentURI.spec.startsWith("about:reader");
55 static addMessageListener(name, listener) {
56 if (!gListeners.has(name)) {
57 gListeners.set(name, new Set([listener]));
59 gListeners.get(name).add(listener);
63 static removeMessageListener(name, listener) {
64 if (!gListeners.has(name)) {
68 gListeners.get(name).delete(listener);
71 static broadcastAsyncMessage(name, data) {
72 for (let actor of gAllActors) {
73 // Ignore errors for actors that might not be valid yet or anymore.
75 actor.sendAsyncMessage(name, data);
80 callListeners(message) {
81 let listeners = gListeners.get(message.name);
86 message.target = this.browsingContext.embedderElement;
87 for (let listener of listeners.values()) {
89 listener.receiveMessage(message);
96 async receiveMessage(message) {
97 switch (message.name) {
98 case "Reader:EnterReaderMode": {
99 gCachedArticles.set(message.data.url, message.data);
100 this.enterReaderMode(message.data.url);
103 case "Reader:LeaveReaderMode": {
104 this.leaveReaderMode();
107 case "Reader:GetCachedArticle": {
108 let cachedArticle = gCachedArticles.get(message.data.url);
109 gCachedArticles.delete(message.data.url);
110 return cachedArticle;
112 case "Reader:FaviconRequest": {
114 let preferredWidth = message.data.preferredWidth || 0;
115 let uri = Services.io.newURI(message.data.url);
117 let result = await new Promise(resolve => {
118 PlacesUtils.favicons.getFaviconURLForPage(
122 iconUri = PlacesUtils.favicons.getFaviconLinkForIcon(iconUri);
124 url: message.data.url,
125 faviconUrl: iconUri.pathQueryRef.replace(/^favicon:/, ""),
135 this.callListeners(message);
139 "Error requesting favicon URL for about:reader content: " + ex
146 case "Reader:UpdateReaderButton": {
147 let browser = this.browsingContext.embedderElement;
152 if (message.data && message.data.isArticle !== undefined) {
153 browser.isArticle = message.data.isArticle;
155 this.updateReaderButton(browser);
156 this.callListeners(message);
161 this.callListeners(message);
168 static updateReaderButton(browser) {
169 let windowGlobal = browser.browsingContext.currentWindowGlobal;
170 let actor = windowGlobal.getActor("AboutReader");
171 actor.updateReaderButton(browser);
174 updateReaderButton(browser) {
175 let tabBrowser = browser.getTabBrowser();
176 if (!tabBrowser || browser != tabBrowser.selectedBrowser) {
180 let win = browser.ownerGlobal;
182 let button = win.document.getElementById("reader-mode-button");
183 let menuitem = win.document.getElementById("menu_readerModeItem");
184 let key = win.document.getElementById("key_toggleReaderMode");
185 if (this.isReaderMode()) {
186 gAllActors.add(this);
188 let closeText = gStringBundle.GetStringFromName("readerView.close");
190 button.setAttribute("readeractive", true);
191 button.hidden = false;
192 button.setAttribute("aria-label", closeText);
194 menuitem.setAttribute("label", closeText);
195 menuitem.hidden = false;
196 menuitem.setAttribute(
198 gStringBundle.GetStringFromName("readerView.close.accesskey")
201 key.setAttribute("disabled", false);
203 Services.obs.notifyObservers(null, "reader-mode-available");
205 let enterText = gStringBundle.GetStringFromName("readerView.enter");
207 button.removeAttribute("readeractive");
208 button.hidden = !browser.isArticle;
209 button.setAttribute("aria-label", enterText);
211 menuitem.setAttribute("label", enterText);
212 menuitem.hidden = !browser.isArticle;
213 menuitem.setAttribute(
215 gStringBundle.GetStringFromName("readerView.enter.accesskey")
218 key.setAttribute("disabled", !browser.isArticle);
220 if (browser.isArticle) {
221 Services.obs.notifyObservers(null, "reader-mode-available");
226 static forceShowReaderIcon(browser) {
227 browser.isArticle = true;
228 AboutReaderParent.updateReaderButton(browser);
231 static buttonClick(event) {
232 if (event.button != 0) {
235 AboutReaderParent.toggleReaderMode(event);
238 static toggleReaderMode(event) {
239 let win = event.target.ownerGlobal;
241 let browser = win.gBrowser.selectedBrowser;
243 let windowGlobal = browser.browsingContext.currentWindowGlobal;
244 let actor = windowGlobal.getActor("AboutReader");
246 if (actor.isReaderMode()) {
247 gAllActors.delete(this);
249 actor.sendAsyncMessage("Reader:ToggleReaderMode", {});
254 hasReaderModeEntryAtOffset(url, offset) {
255 if (Services.appinfo.sessionHistoryInParent) {
256 let browsingContext = this.browsingContext;
257 if (browsingContext.childSessionHistory.canGo(offset)) {
258 let shistory = browsingContext.sessionHistory;
259 let nextEntry = shistory.getEntryAtIndex(shistory.index + offset);
260 let nextURL = nextEntry.URI.spec;
261 return nextURL && (nextURL == url || !url);
268 enterReaderMode(url) {
269 let readerURL = "about:reader?url=" + encodeURIComponent(url);
270 if (this.hasReaderModeEntryAtOffset(readerURL, +1)) {
271 let browsingContext = this.browsingContext;
272 browsingContext.childSessionHistory.go(+1);
276 this.sendAsyncMessage("Reader:EnterReaderMode", {});
280 let browsingContext = this.browsingContext;
281 let url = browsingContext.currentWindowGlobal.documentURI.spec;
282 let originalURL = ReaderMode.getOriginalUrl(url);
283 if (this.hasReaderModeEntryAtOffset(originalURL, -1)) {
284 browsingContext.childSessionHistory.go(-1);
288 this.sendAsyncMessage("Reader:LeaveReaderMode", {});
292 * Gets an article for a given URL. This method will download and parse a document.
294 * @param url The article URL.
295 * @param browser The browser where the article is currently loaded.
297 * @resolves JS object representing the article, or null if no article is found.
299 async _getArticle(url, browser) {
300 return ReaderMode.downloadAndParseDocument(url).catch(e => {
302 // Pass up the error so we can navigate the browser in question to the new URL:
305 Cu.reportError("Error downloading and parsing document: " + e);