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 ChromeUtils.defineESModuleGetters(lazy, {
9 PageActions: "resource:///modules/PageActions.sys.mjs",
10 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
11 ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
14 // A set of all of the AboutReaderParent actors that exist.
15 // See bug 1631146 for a request for a less manual way of doing this.
16 let gAllActors = new Set();
18 // A map of message names to listeners that listen to messages
19 // received by the AboutReaderParent actors.
20 let gListeners = new Map();
22 // As a reader mode document could be loaded in a different process than
23 // the source article, temporarily cache the article data here in the
24 // parent while switching to it.
25 let gCachedArticles = new Map();
27 export class AboutReaderParent extends JSWindowActorParent {
29 gAllActors.delete(this);
31 if (this.isReaderMode()) {
32 let url = this.manager.documentURI.spec;
33 url = decodeURIComponent(url.substr("about:reader?url=".length));
34 gCachedArticles.delete(url);
39 return this.manager.documentURI.spec.startsWith("about:reader");
42 static addMessageListener(name, listener) {
43 if (!gListeners.has(name)) {
44 gListeners.set(name, new Set([listener]));
46 gListeners.get(name).add(listener);
50 static removeMessageListener(name, listener) {
51 if (!gListeners.has(name)) {
55 gListeners.get(name).delete(listener);
58 static broadcastAsyncMessage(name, data) {
59 for (let actor of gAllActors) {
60 // Ignore errors for actors that might not be valid yet or anymore.
62 actor.sendAsyncMessage(name, data);
67 callListeners(message) {
68 let listeners = gListeners.get(message.name);
73 message.target = this.browsingContext.embedderElement;
74 for (let listener of listeners.values()) {
76 listener.receiveMessage(message);
83 async receiveMessage(message) {
84 switch (message.name) {
85 case "Reader:EnterReaderMode": {
86 gCachedArticles.set(message.data.url, message.data);
87 this.enterReaderMode(message.data.url);
90 case "Reader:LeaveReaderMode": {
91 this.leaveReaderMode();
94 case "Reader:GetCachedArticle": {
95 let cachedArticle = gCachedArticles.get(message.data.url);
96 gCachedArticles.delete(message.data.url);
99 case "Reader:FaviconRequest": {
101 let preferredWidth = message.data.preferredWidth || 0;
102 let uri = Services.io.newURI(message.data.url);
104 let result = await new Promise(resolve => {
105 lazy.PlacesUtils.favicons.getFaviconURLForPage(
110 url: message.data.url,
111 faviconUrl: iconUri.spec,
121 this.callListeners(message);
125 "Error requesting favicon URL for about:reader content: ",
133 case "Reader:UpdateReaderButton": {
134 let browser = this.browsingContext.embedderElement;
139 if (message.data && message.data.isArticle !== undefined) {
140 browser.isArticle = message.data.isArticle;
142 this.updateReaderButton(browser);
143 this.callListeners(message);
148 gCachedArticles.set(message.data.newURL, message.data.article);
149 // This is setup as a query so we can navigate the page after we've
150 // cached the relevant info in the parent.
155 this.callListeners(message);
162 static updateReaderButton(browser) {
163 let windowGlobal = browser.browsingContext.currentWindowGlobal;
164 let actor = windowGlobal.getActor("AboutReader");
165 actor.updateReaderButton(browser);
168 updateReaderButton(browser) {
169 let tabBrowser = browser.getTabBrowser();
170 if (!tabBrowser || browser != tabBrowser.selectedBrowser) {
174 let doc = browser.ownerGlobal.document;
175 let button = doc.getElementById("reader-mode-button");
176 let menuitem = doc.getElementById("menu_readerModeItem");
177 let key = doc.getElementById("key_toggleReaderMode");
178 if (this.isReaderMode()) {
179 gAllActors.add(this);
181 button.setAttribute("readeractive", true);
182 button.hidden = false;
183 doc.l10n.setAttributes(button, "reader-view-close-button");
185 menuitem.hidden = false;
186 doc.l10n.setAttributes(menuitem, "menu-view-close-readerview");
188 key.setAttribute("disabled", false);
190 Services.obs.notifyObservers(null, "reader-mode-available");
192 button.removeAttribute("readeractive");
193 button.hidden = !browser.isArticle;
194 doc.l10n.setAttributes(button, "reader-view-enter-button");
196 menuitem.hidden = !browser.isArticle;
197 doc.l10n.setAttributes(menuitem, "menu-view-enter-readerview");
199 key.setAttribute("disabled", !browser.isArticle);
201 if (browser.isArticle) {
202 Services.obs.notifyObservers(null, "reader-mode-available");
206 if (!button.hidden) {
207 lazy.PageActions.sendPlacedInUrlbarTrigger(button);
211 static forceShowReaderIcon(browser) {
212 browser.isArticle = true;
213 AboutReaderParent.updateReaderButton(browser);
216 static buttonClick(event) {
217 if (event.button != 0) {
220 AboutReaderParent.toggleReaderMode(event);
223 static toggleReaderMode(event) {
224 let win = event.target.ownerGlobal;
226 let browser = win.gBrowser.selectedBrowser;
228 let windowGlobal = browser.browsingContext.currentWindowGlobal;
229 let actor = windowGlobal.getActor("AboutReader");
231 if (actor.isReaderMode()) {
232 gAllActors.delete(this);
234 actor.sendAsyncMessage("Reader:ToggleReaderMode", {});
239 hasReaderModeEntryAtOffset(url, offset) {
240 if (Services.appinfo.sessionHistoryInParent) {
241 let browsingContext = this.browsingContext;
242 if (browsingContext.childSessionHistory.canGo(offset)) {
243 let shistory = browsingContext.sessionHistory;
244 let nextEntry = shistory.getEntryAtIndex(shistory.index + offset);
245 let nextURL = nextEntry.URI.spec;
246 return nextURL && (nextURL == url || !url);
253 enterReaderMode(url) {
254 let readerURL = "about:reader?url=" + encodeURIComponent(url);
255 if (this.hasReaderModeEntryAtOffset(readerURL, +1)) {
256 let browsingContext = this.browsingContext;
257 browsingContext.childSessionHistory.go(+1);
261 this.sendAsyncMessage("Reader:EnterReaderMode", {});
265 let browsingContext = this.browsingContext;
266 let url = browsingContext.currentWindowGlobal.documentURI.spec;
267 let originalURL = lazy.ReaderMode.getOriginalUrl(url);
268 if (this.hasReaderModeEntryAtOffset(originalURL, -1)) {
269 browsingContext.childSessionHistory.go(-1);
273 this.sendAsyncMessage("Reader:LeaveReaderMode", {});
277 * Gets an article for a given URL. This method will download and parse a document.
279 * @param url The article URL.
280 * @param browser The browser where the article is currently loaded.
282 * @resolves JS object representing the article, or null if no article is found.
284 async _getArticle(url, browser) {
285 return lazy.ReaderMode.downloadAndParseDocument(url).catch(e => {
287 // Pass up the error so we can navigate the browser in question to the new URL:
290 console.error("Error downloading and parsing document: ", e);