AboutReaderChild.sys.mjs (7749B)
1 /* vim: set ts=2 sw=2 sts=2 et tw=80: */ 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/. */ 5 6 const lazy = {}; 7 8 ChromeUtils.defineESModuleGetters(lazy, { 9 AboutReader: "moz-src:///toolkit/components/reader/AboutReader.sys.mjs", 10 ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs", 11 Readerable: "resource://gre/modules/Readerable.sys.mjs", 12 }); 13 14 var gUrlsToDocContentType = new Map(); 15 var gUrlsToDocTitle = new Map(); 16 17 export class AboutReaderChild extends JSWindowActorChild { 18 constructor() { 19 super(); 20 21 this._reader = null; 22 this._articlePromise = null; 23 this._isLeavingReaderableReaderMode = false; 24 } 25 26 didDestroy() { 27 this.cancelPotentialPendingReadabilityCheck(); 28 this.readerModeHidden(); 29 } 30 31 readerModeHidden() { 32 if (this._reader) { 33 this._reader.clearActor(); 34 } 35 this._reader = null; 36 } 37 38 async receiveMessage(message) { 39 switch (message.name) { 40 case "Reader:ToggleReaderMode": 41 if (!this.isAboutReader) { 42 gUrlsToDocContentType.set( 43 this.document.URL, 44 this.document.contentType 45 ); 46 gUrlsToDocTitle.set(this.document.URL, this.document.title); 47 this._articlePromise = lazy.ReaderMode.parseDocument( 48 this.document 49 ).catch(console.error); 50 51 // Get the article data and cache it in the parent process. The reader mode 52 // page will retrieve it when it has loaded. 53 let article = await this._articlePromise; 54 this.sendAsyncMessage("Reader:EnterReaderMode", article); 55 } else { 56 this.closeReaderMode(); 57 } 58 break; 59 60 case "Reader:PushState": 61 this.updateReaderButton(!!(message.data && message.data.isArticle)); 62 break; 63 case "Reader:EnterReaderMode": { 64 lazy.ReaderMode.enterReaderMode(this.docShell, this.contentWindow); 65 break; 66 } 67 case "Reader:LeaveReaderMode": { 68 lazy.ReaderMode.leaveReaderMode(this.docShell, this.contentWindow); 69 break; 70 } 71 } 72 73 // Forward the message to the reader if it has been created. 74 if (this._reader) { 75 this._reader.receiveMessage(message); 76 } 77 } 78 79 get isAboutReader() { 80 if (!this.document) { 81 return false; 82 } 83 return this.document.documentURI.startsWith("about:reader"); 84 } 85 86 get isReaderableAboutReader() { 87 return this.isAboutReader && !this.document.documentElement.dataset.isError; 88 } 89 90 handleEvent(aEvent) { 91 if (aEvent.originalTarget.defaultView != this.contentWindow) { 92 return; 93 } 94 95 switch (aEvent.type) { 96 case "DOMContentLoaded": 97 if (!this.isAboutReader) { 98 this.updateReaderButton(); 99 return; 100 } 101 102 if (this.document.body) { 103 let url = this.document.documentURI; 104 if (!this._articlePromise) { 105 url = decodeURIComponent(url.substr("about:reader?url=".length)); 106 this._articlePromise = this.sendQuery("Reader:GetCachedArticle", { 107 url, 108 }); 109 } 110 // Update the toolbar icon to show the "reader active" icon. 111 this.sendAsyncMessage("Reader:UpdateReaderButton"); 112 let docContentType = 113 gUrlsToDocContentType.get(url) === "text/plain" 114 ? "text/plain" 115 : "document"; 116 117 let docTitle = gUrlsToDocTitle.get(url); 118 this._reader = new lazy.AboutReader( 119 this, 120 this._articlePromise, 121 docContentType, 122 docTitle 123 ); 124 this._articlePromise = null; 125 } 126 break; 127 128 case "pagehide": 129 this.cancelPotentialPendingReadabilityCheck(); 130 // this._isLeavingReaderableReaderMode is used here to keep the Reader Mode icon 131 // visible in the location bar when transitioning from reader-mode page 132 // back to the readable source page. 133 this.sendAsyncMessage("Reader:UpdateReaderButton", { 134 isArticle: this._isLeavingReaderableReaderMode, 135 }); 136 this._isLeavingReaderableReaderMode = false; 137 break; 138 139 case "pageshow": 140 // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded" 141 // event, so we need to rely on "pageshow" in this case. 142 if (aEvent.persisted && this.canDoReadabilityCheck()) { 143 this.performReadabilityCheckNow(); 144 } 145 break; 146 } 147 } 148 149 /** 150 * NB: this function will update the state of the reader button asynchronously 151 * after the next mozAfterPaint call (assuming reader mode is enabled and 152 * this is a suitable document). Calling it on things which won't be 153 * painted is not going to work. 154 */ 155 updateReaderButton(forceNonArticle) { 156 if (!this.canDoReadabilityCheck()) { 157 return; 158 } 159 160 this.scheduleReadabilityCheckPostPaint(forceNonArticle); 161 } 162 163 canDoReadabilityCheck() { 164 return ( 165 lazy.Readerable.isEnabledForParseOnLoad && 166 !this.isAboutReader && 167 this.contentWindow && 168 this.contentWindow.windowRoot && 169 this.contentWindow.HTMLDocument.isInstance(this.document) && 170 !this.document.mozSyntheticDocument 171 ); 172 } 173 174 cancelPotentialPendingReadabilityCheck() { 175 if (this._pendingReadabilityCheck) { 176 if (this._listenerWindow) { 177 this._listenerWindow.removeEventListener( 178 "MozAfterPaint", 179 this._pendingReadabilityCheck 180 ); 181 } 182 delete this._pendingReadabilityCheck; 183 delete this._listenerWindow; 184 } 185 } 186 187 scheduleReadabilityCheckPostPaint(forceNonArticle) { 188 if (this._pendingReadabilityCheck) { 189 // We need to stop this check before we re-add one because we don't know 190 // if forceNonArticle was true or false last time. 191 this.cancelPotentialPendingReadabilityCheck(); 192 } 193 this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind( 194 this, 195 forceNonArticle 196 ); 197 198 this._listenerWindow = this.contentWindow.windowRoot; 199 this.contentWindow.windowRoot.addEventListener( 200 "MozAfterPaint", 201 this._pendingReadabilityCheck 202 ); 203 } 204 205 onPaintWhenWaitedFor(forceNonArticle, event) { 206 // In non-e10s, we'll get called for paints other than ours, and so it's 207 // possible that this page hasn't been laid out yet, in which case we 208 // should wait until we get an event that does relate to our layout. We 209 // determine whether any of our this.contentWindow got painted by checking 210 // if there are any painted rects. 211 if (!event.clientRects.length) { 212 return; 213 } 214 215 this.performReadabilityCheckNow(forceNonArticle); 216 } 217 218 performReadabilityCheckNow(forceNonArticle) { 219 this.cancelPotentialPendingReadabilityCheck(); 220 221 // Ignore errors from actors that have been unloaded before the 222 // paint event timer fires. 223 let document; 224 try { 225 document = this.document; 226 } catch (ex) { 227 return; 228 } 229 230 // Only send updates when there are articles; there's no point updating with 231 // |false| all the time. 232 if ( 233 lazy.Readerable.shouldCheckUri(document.baseURIObject, true) && 234 lazy.Readerable.isProbablyReaderable(document) 235 ) { 236 this.sendAsyncMessage("Reader:UpdateReaderButton", { 237 isArticle: true, 238 }); 239 } else if (forceNonArticle) { 240 this.sendAsyncMessage("Reader:UpdateReaderButton", { 241 isArticle: false, 242 }); 243 } 244 } 245 246 closeReaderMode() { 247 if (this.isAboutReader) { 248 this._isLeavingReaderableReaderMode = this.isReaderableAboutReader; 249 this.sendAsyncMessage("Reader:LeaveReaderMode", {}); 250 } 251 } 252 }