Bug 1687263: part 4) Defer and in some cases avoid removing spellchecking-ranges...
[gecko.git] / browser / actors / AboutReaderParent.jsm
blob8c01daf1df2dd013fc887b43f59839db719ff306
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/. */
6 "use strict";
8 var EXPORTED_SYMBOLS = ["AboutReaderParent"];
10 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
12 ChromeUtils.defineModuleGetter(
13   this,
14   "PlacesUtils",
15   "resource://gre/modules/PlacesUtils.jsm"
17 ChromeUtils.defineModuleGetter(
18   this,
19   "ReaderMode",
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 {
41   didDestroy() {
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);
48     }
49   }
51   isReaderMode() {
52     return this.manager.documentURI.spec.startsWith("about:reader");
53   }
55   static addMessageListener(name, listener) {
56     if (!gListeners.has(name)) {
57       gListeners.set(name, new Set([listener]));
58     } else {
59       gListeners.get(name).add(listener);
60     }
61   }
63   static removeMessageListener(name, listener) {
64     if (!gListeners.has(name)) {
65       return;
66     }
68     gListeners.get(name).delete(listener);
69   }
71   static broadcastAsyncMessage(name, data) {
72     for (let actor of gAllActors) {
73       // Ignore errors for actors that might not be valid yet or anymore.
74       try {
75         actor.sendAsyncMessage(name, data);
76       } catch (ex) {}
77     }
78   }
80   callListeners(message) {
81     let listeners = gListeners.get(message.name);
82     if (!listeners) {
83       return;
84     }
86     message.target = this.browsingContext.embedderElement;
87     for (let listener of listeners.values()) {
88       try {
89         listener.receiveMessage(message);
90       } catch (e) {
91         Cu.reportError(e);
92       }
93     }
94   }
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);
101         break;
102       }
103       case "Reader:LeaveReaderMode": {
104         this.leaveReaderMode();
105         break;
106       }
107       case "Reader:GetCachedArticle": {
108         let cachedArticle = gCachedArticles.get(message.data.url);
109         gCachedArticles.delete(message.data.url);
110         return cachedArticle;
111       }
112       case "Reader:FaviconRequest": {
113         try {
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(
119               uri,
120               iconUri => {
121                 if (iconUri) {
122                   iconUri = PlacesUtils.favicons.getFaviconLinkForIcon(iconUri);
123                   resolve({
124                     url: message.data.url,
125                     faviconUrl: iconUri.pathQueryRef.replace(/^favicon:/, ""),
126                   });
127                 } else {
128                   resolve(null);
129                 }
130               },
131               preferredWidth
132             );
133           });
135           this.callListeners(message);
136           return result;
137         } catch (ex) {
138           Cu.reportError(
139             "Error requesting favicon URL for about:reader content: " + ex
140           );
141         }
143         break;
144       }
146       case "Reader:UpdateReaderButton": {
147         let browser = this.browsingContext.embedderElement;
148         if (!browser) {
149           return undefined;
150         }
152         if (message.data && message.data.isArticle !== undefined) {
153           browser.isArticle = message.data.isArticle;
154         }
155         this.updateReaderButton(browser);
156         this.callListeners(message);
157         break;
158       }
160       default:
161         this.callListeners(message);
162         break;
163     }
165     return undefined;
166   }
168   static updateReaderButton(browser) {
169     let windowGlobal = browser.browsingContext.currentWindowGlobal;
170     let actor = windowGlobal.getActor("AboutReader");
171     actor.updateReaderButton(browser);
172   }
174   updateReaderButton(browser) {
175     let tabBrowser = browser.getTabBrowser();
176     if (!tabBrowser || browser != tabBrowser.selectedBrowser) {
177       return;
178     }
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(
197         "accesskey",
198         gStringBundle.GetStringFromName("readerView.close.accesskey")
199       );
201       key.setAttribute("disabled", false);
203       Services.obs.notifyObservers(null, "reader-mode-available");
204     } else {
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(
214         "accesskey",
215         gStringBundle.GetStringFromName("readerView.enter.accesskey")
216       );
218       key.setAttribute("disabled", !browser.isArticle);
220       if (browser.isArticle) {
221         Services.obs.notifyObservers(null, "reader-mode-available");
222       }
223     }
224   }
226   static forceShowReaderIcon(browser) {
227     browser.isArticle = true;
228     AboutReaderParent.updateReaderButton(browser);
229   }
231   static buttonClick(event) {
232     if (event.button != 0) {
233       return;
234     }
235     AboutReaderParent.toggleReaderMode(event);
236   }
238   static toggleReaderMode(event) {
239     let win = event.target.ownerGlobal;
240     if (win.gBrowser) {
241       let browser = win.gBrowser.selectedBrowser;
243       let windowGlobal = browser.browsingContext.currentWindowGlobal;
244       let actor = windowGlobal.getActor("AboutReader");
245       if (actor) {
246         if (actor.isReaderMode()) {
247           gAllActors.delete(this);
248         }
249         actor.sendAsyncMessage("Reader:ToggleReaderMode", {});
250       }
251     }
252   }
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);
262       }
263     }
265     return false;
266   }
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);
273       return;
274     }
276     this.sendAsyncMessage("Reader:EnterReaderMode", {});
277   }
279   leaveReaderMode() {
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);
285       return;
286     }
288     this.sendAsyncMessage("Reader:LeaveReaderMode", {});
289   }
291   /**
292    * Gets an article for a given URL. This method will download and parse a document.
293    *
294    * @param url The article URL.
295    * @param browser The browser where the article is currently loaded.
296    * @return {Promise}
297    * @resolves JS object representing the article, or null if no article is found.
298    */
299   async _getArticle(url, browser) {
300     return ReaderMode.downloadAndParseDocument(url).catch(e => {
301       if (e && e.newURL) {
302         // Pass up the error so we can navigate the browser in question to the new URL:
303         throw e;
304       }
305       Cu.reportError("Error downloading and parsing document: " + e);
306       return null;
307     });
308   }