|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils; |
|
6 |
|
7 Cu.import("resource://gre/modules/Services.jsm") |
|
8 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
9 |
|
10 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", |
|
11 "resource://gre/modules/UITelemetry.jsm"); |
|
12 |
|
13 XPCOMUtils.defineLazyGetter(window, "gChromeWin", function () |
|
14 window.QueryInterface(Ci.nsIInterfaceRequestor) |
|
15 .getInterface(Ci.nsIWebNavigation) |
|
16 .QueryInterface(Ci.nsIDocShellTreeItem) |
|
17 .rootTreeItem |
|
18 .QueryInterface(Ci.nsIInterfaceRequestor) |
|
19 .getInterface(Ci.nsIDOMWindow) |
|
20 .QueryInterface(Ci.nsIDOMChromeWindow)); |
|
21 |
|
22 function dump(s) { |
|
23 Services.console.logStringMessage("AboutReader: " + s); |
|
24 } |
|
25 |
|
26 let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutReader.properties"); |
|
27 |
|
28 let AboutReader = function(doc, win) { |
|
29 dump("Init()"); |
|
30 |
|
31 this._docRef = Cu.getWeakReference(doc); |
|
32 this._winRef = Cu.getWeakReference(win); |
|
33 |
|
34 Services.obs.addObserver(this, "Reader:FaviconReturn", false); |
|
35 Services.obs.addObserver(this, "Reader:Add", false); |
|
36 Services.obs.addObserver(this, "Reader:Remove", false); |
|
37 Services.obs.addObserver(this, "Reader:ListStatusReturn", false); |
|
38 |
|
39 this._article = null; |
|
40 |
|
41 dump("Feching toolbar, header and content notes from about:reader"); |
|
42 this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header")); |
|
43 this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain")); |
|
44 this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title")); |
|
45 this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits")); |
|
46 this._contentElementRef = Cu.getWeakReference(doc.getElementById("reader-content")); |
|
47 this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar")); |
|
48 this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message")); |
|
49 |
|
50 this._toolbarEnabled = false; |
|
51 |
|
52 this._scrollOffset = win.pageYOffset; |
|
53 |
|
54 let body = doc.body; |
|
55 body.addEventListener("touchstart", this, false); |
|
56 body.addEventListener("click", this, false); |
|
57 |
|
58 win.addEventListener("unload", this, false); |
|
59 win.addEventListener("scroll", this, false); |
|
60 win.addEventListener("popstate", this, false); |
|
61 win.addEventListener("resize", this, false); |
|
62 |
|
63 this._setupAllDropdowns(); |
|
64 this._setupButton("toggle-button", this._onReaderToggle.bind(this)); |
|
65 this._setupButton("share-button", this._onShare.bind(this)); |
|
66 |
|
67 let colorSchemeOptions = [ |
|
68 { name: gStrings.GetStringFromName("aboutReader.colorSchemeDark"), |
|
69 value: "dark"}, |
|
70 { name: gStrings.GetStringFromName("aboutReader.colorSchemeLight"), |
|
71 value: "light"}, |
|
72 { name: gStrings.GetStringFromName("aboutReader.colorSchemeAuto"), |
|
73 value: "auto"} |
|
74 ]; |
|
75 |
|
76 let colorScheme = Services.prefs.getCharPref("reader.color_scheme"); |
|
77 this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorSchemePref.bind(this)); |
|
78 this._setColorSchemePref(colorScheme); |
|
79 |
|
80 let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample"); |
|
81 let fontTypeOptions = [ |
|
82 { name: fontTypeSample, |
|
83 description: gStrings.GetStringFromName("aboutReader.fontTypeSerif"), |
|
84 value: "serif", |
|
85 linkClass: "serif" }, |
|
86 { name: fontTypeSample, |
|
87 description: gStrings.GetStringFromName("aboutReader.fontTypeSansSerif"), |
|
88 value: "sans-serif", |
|
89 linkClass: "sans-serif" |
|
90 }, |
|
91 ]; |
|
92 |
|
93 let fontType = Services.prefs.getCharPref("reader.font_type"); |
|
94 this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this)); |
|
95 this._setFontType(fontType); |
|
96 |
|
97 let fontSizeSample = gStrings.GetStringFromName("aboutReader.fontSizeSample"); |
|
98 let fontSizeOptions = [ |
|
99 { name: fontSizeSample, |
|
100 value: 1, |
|
101 linkClass: "font-size1-sample" }, |
|
102 { name: fontSizeSample, |
|
103 value: 2, |
|
104 linkClass: "font-size2-sample" }, |
|
105 { name: fontSizeSample, |
|
106 value: 3, |
|
107 linkClass: "font-size3-sample" }, |
|
108 { name: fontSizeSample, |
|
109 value: 4, |
|
110 linkClass: "font-size4-sample" }, |
|
111 { name: fontSizeSample, |
|
112 value: 5, |
|
113 linkClass: "font-size5-sample" } |
|
114 ]; |
|
115 |
|
116 let fontSize = Services.prefs.getIntPref("reader.font_size"); |
|
117 this._setupSegmentedButton("font-size-buttons", fontSizeOptions, fontSize, this._setFontSize.bind(this)); |
|
118 this._setFontSize(fontSize); |
|
119 |
|
120 dump("Decoding query arguments"); |
|
121 let queryArgs = this._decodeQueryString(win.location.href); |
|
122 |
|
123 // Track status of reader toolbar add/remove toggle button |
|
124 this._isReadingListItem = -1; |
|
125 this._updateToggleButton(); |
|
126 |
|
127 let url = queryArgs.url; |
|
128 let tabId = queryArgs.tabId; |
|
129 if (tabId) { |
|
130 dump("Loading from tab with ID: " + tabId + ", URL: " + url); |
|
131 this._loadFromTab(tabId, url); |
|
132 } else { |
|
133 dump("Fetching page with URL: " + url); |
|
134 this._loadFromURL(url); |
|
135 } |
|
136 } |
|
137 |
|
138 AboutReader.prototype = { |
|
139 _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " + |
|
140 ".content p > a:only-child > img:only-child, " + |
|
141 ".content .wp-caption img, " + |
|
142 ".content figure img", |
|
143 |
|
144 get _doc() { |
|
145 return this._docRef.get(); |
|
146 }, |
|
147 |
|
148 get _win() { |
|
149 return this._winRef.get(); |
|
150 }, |
|
151 |
|
152 get _headerElement() { |
|
153 return this._headerElementRef.get(); |
|
154 }, |
|
155 |
|
156 get _domainElement() { |
|
157 return this._domainElementRef.get(); |
|
158 }, |
|
159 |
|
160 get _titleElement() { |
|
161 return this._titleElementRef.get(); |
|
162 }, |
|
163 |
|
164 get _creditsElement() { |
|
165 return this._creditsElementRef.get(); |
|
166 }, |
|
167 |
|
168 get _contentElement() { |
|
169 return this._contentElementRef.get(); |
|
170 }, |
|
171 |
|
172 get _toolbarElement() { |
|
173 return this._toolbarElementRef.get(); |
|
174 }, |
|
175 |
|
176 get _messageElement() { |
|
177 return this._messageElementRef.get(); |
|
178 }, |
|
179 |
|
180 observe: function Reader_observe(aMessage, aTopic, aData) { |
|
181 switch(aTopic) { |
|
182 case "Reader:FaviconReturn": { |
|
183 let args = JSON.parse(aData); |
|
184 this._loadFavicon(args.url, args.faviconUrl); |
|
185 Services.obs.removeObserver(this, "Reader:FaviconReturn"); |
|
186 break; |
|
187 } |
|
188 |
|
189 case "Reader:Add": { |
|
190 let args = JSON.parse(aData); |
|
191 if (args.url == this._article.url) { |
|
192 if (this._isReadingListItem != 1) { |
|
193 this._isReadingListItem = 1; |
|
194 this._updateToggleButton(); |
|
195 } |
|
196 } |
|
197 break; |
|
198 } |
|
199 |
|
200 case "Reader:Remove": { |
|
201 if (aData == this._article.url) { |
|
202 if (this._isReadingListItem != 0) { |
|
203 this._isReadingListItem = 0; |
|
204 this._updateToggleButton(); |
|
205 } |
|
206 } |
|
207 break; |
|
208 } |
|
209 |
|
210 case "Reader:ListStatusReturn": { |
|
211 let args = JSON.parse(aData); |
|
212 if (args.url == this._article.url) { |
|
213 if (this._isReadingListItem != args.inReadingList) { |
|
214 let isInitialStateChange = (this._isReadingListItem == -1); |
|
215 this._isReadingListItem = args.inReadingList; |
|
216 this._updateToggleButton(); |
|
217 |
|
218 // Display the toolbar when all its initial component states are known |
|
219 if (isInitialStateChange) { |
|
220 this._setToolbarVisibility(true); |
|
221 } |
|
222 } |
|
223 } |
|
224 break; |
|
225 } |
|
226 } |
|
227 }, |
|
228 |
|
229 handleEvent: function Reader_handleEvent(aEvent) { |
|
230 if (!aEvent.isTrusted) |
|
231 return; |
|
232 |
|
233 switch (aEvent.type) { |
|
234 case "touchstart": |
|
235 this._scrolled = false; |
|
236 break; |
|
237 case "click": |
|
238 if (!this._scrolled) |
|
239 this._toggleToolbarVisibility(); |
|
240 break; |
|
241 case "scroll": |
|
242 if (!this._scrolled) { |
|
243 let isScrollingUp = this._scrollOffset > aEvent.pageY; |
|
244 this._setToolbarVisibility(isScrollingUp); |
|
245 this._scrollOffset = aEvent.pageY; |
|
246 } |
|
247 break; |
|
248 case "popstate": |
|
249 if (!aEvent.state) |
|
250 this._closeAllDropdowns(); |
|
251 break; |
|
252 case "resize": |
|
253 this._updateImageMargins(); |
|
254 break; |
|
255 |
|
256 case "devicelight": |
|
257 this._handleDeviceLight(aEvent.value); |
|
258 break; |
|
259 |
|
260 case "unload": |
|
261 Services.obs.removeObserver(this, "Reader:Add"); |
|
262 Services.obs.removeObserver(this, "Reader:Remove"); |
|
263 Services.obs.removeObserver(this, "Reader:ListStatusReturn"); |
|
264 break; |
|
265 } |
|
266 }, |
|
267 |
|
268 _updateToggleButton: function Reader_updateToggleButton() { |
|
269 let classes = this._doc.getElementById("toggle-button").classList; |
|
270 |
|
271 if (this._isReadingListItem == 1) { |
|
272 classes.add("on"); |
|
273 } else { |
|
274 classes.remove("on"); |
|
275 } |
|
276 }, |
|
277 |
|
278 _requestReadingListStatus: function Reader_requestReadingListStatus() { |
|
279 gChromeWin.sendMessageToJava({ |
|
280 type: "Reader:ListStatusRequest", |
|
281 url: this._article.url |
|
282 }); |
|
283 }, |
|
284 |
|
285 _onReaderToggle: function Reader_onToggle() { |
|
286 if (!this._article) |
|
287 return; |
|
288 |
|
289 this._isReadingListItem = (this._isReadingListItem == 1) ? 0 : 1; |
|
290 this._updateToggleButton(); |
|
291 |
|
292 // Create a relative timestamp for telemetry |
|
293 let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; |
|
294 |
|
295 if (this._isReadingListItem == 1) { |
|
296 gChromeWin.Reader.storeArticleInCache(this._article, function(success) { |
|
297 dump("Reader:Add (in reader) success=" + success); |
|
298 |
|
299 let result = gChromeWin.Reader.READER_ADD_FAILED; |
|
300 if (success) { |
|
301 result = gChromeWin.Reader.READER_ADD_SUCCESS; |
|
302 UITelemetry.addEvent("save.1", "button", uptime, "reader"); |
|
303 } |
|
304 |
|
305 let json = JSON.stringify({ fromAboutReader: true, url: this._article.url }); |
|
306 Services.obs.notifyObservers(null, "Reader:Add", json); |
|
307 |
|
308 gChromeWin.sendMessageToJava({ |
|
309 type: "Reader:Added", |
|
310 result: result, |
|
311 title: this._article.title, |
|
312 url: this._article.url, |
|
313 length: this._article.length, |
|
314 excerpt: this._article.excerpt |
|
315 }); |
|
316 }.bind(this)); |
|
317 } else { |
|
318 // In addition to removing the article from the cache (handled in |
|
319 // browser.js), sending this message will cause the toggle button to be |
|
320 // updated (handled in this file). |
|
321 Services.obs.notifyObservers(null, "Reader:Remove", this._article.url); |
|
322 |
|
323 UITelemetry.addEvent("unsave.1", "button", uptime, "reader"); |
|
324 } |
|
325 }, |
|
326 |
|
327 _onShare: function Reader_onShare() { |
|
328 if (!this._article) |
|
329 return; |
|
330 |
|
331 gChromeWin.sendMessageToJava({ |
|
332 type: "Reader:Share", |
|
333 url: this._article.url, |
|
334 title: this._article.title |
|
335 }); |
|
336 |
|
337 // Create a relative timestamp for telemetry |
|
338 let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; |
|
339 UITelemetry.addEvent("share.1", "list", uptime); |
|
340 }, |
|
341 |
|
342 _setFontSize: function Reader_setFontSize(newFontSize) { |
|
343 let bodyClasses = this._doc.body.classList; |
|
344 |
|
345 if (this._fontSize > 0) |
|
346 bodyClasses.remove("font-size" + this._fontSize); |
|
347 |
|
348 this._fontSize = newFontSize; |
|
349 bodyClasses.add("font-size" + this._fontSize); |
|
350 |
|
351 Services.prefs.setIntPref("reader.font_size", this._fontSize); |
|
352 }, |
|
353 |
|
354 _handleDeviceLight: function Reader_handleDeviceLight(newLux) { |
|
355 // Desired size of the this._luxValues array. |
|
356 let luxValuesSize = 10; |
|
357 // Add new lux value at the front of the array. |
|
358 this._luxValues.unshift(newLux); |
|
359 // Add new lux value to this._totalLux for averaging later. |
|
360 this._totalLux += newLux; |
|
361 |
|
362 // Don't update when length of array is less than luxValuesSize except when it is 1. |
|
363 if (this._luxValues.length < luxValuesSize) { |
|
364 // Use the first lux value to set the color scheme until our array equals luxValuesSize. |
|
365 if (this._luxValues.length == 1) { |
|
366 this._updateColorScheme(newLux); |
|
367 } |
|
368 return; |
|
369 } |
|
370 // Holds the average of the lux values collected in this._luxValues. |
|
371 let averageLuxValue = this._totalLux/luxValuesSize; |
|
372 |
|
373 this._updateColorScheme(averageLuxValue); |
|
374 // Pop the oldest value off the array. |
|
375 let oldLux = this._luxValues.pop(); |
|
376 // Subtract oldLux since it has been discarded from the array. |
|
377 this._totalLux -= oldLux; |
|
378 }, |
|
379 |
|
380 _updateColorScheme: function Reader_updateColorScheme(luxValue) { |
|
381 // Upper bound value for "dark" color scheme beyond which it changes to "light". |
|
382 let upperBoundDark = 50; |
|
383 // Lower bound value for "light" color scheme beyond which it changes to "dark". |
|
384 let lowerBoundLight = 10; |
|
385 // Threshold for color scheme change. |
|
386 let colorChangeThreshold = 20; |
|
387 |
|
388 // Ignore changes that are within a certain threshold of previous lux values. |
|
389 if ((this._colorScheme === "dark" && luxValue < upperBoundDark) || |
|
390 (this._colorScheme === "light" && luxValue > lowerBoundLight)) |
|
391 return; |
|
392 |
|
393 if (luxValue < colorChangeThreshold) |
|
394 this._setColorScheme("dark"); |
|
395 else |
|
396 this._setColorScheme("light"); |
|
397 }, |
|
398 |
|
399 _setColorScheme: function Reader_setColorScheme(newColorScheme) { |
|
400 if (this._colorScheme === newColorScheme) |
|
401 return; |
|
402 |
|
403 let bodyClasses = this._doc.body.classList; |
|
404 |
|
405 if (this._colorScheme) |
|
406 bodyClasses.remove(this._colorScheme); |
|
407 |
|
408 this._colorScheme = newColorScheme; |
|
409 bodyClasses.add(this._colorScheme); |
|
410 }, |
|
411 |
|
412 // Pref values include "dark", "light", and "auto", which automatically switches |
|
413 // between light and dark color schemes based on the ambient light level. |
|
414 _setColorSchemePref: function Reader_setColorSchemePref(colorSchemePref) { |
|
415 if (colorSchemePref === "auto") { |
|
416 this._win.addEventListener("devicelight", this, false); |
|
417 this._luxValues = []; |
|
418 this._totalLux = 0; |
|
419 } else { |
|
420 this._win.removeEventListener("devicelight", this, false); |
|
421 this._setColorScheme(colorSchemePref); |
|
422 delete this._luxValues; |
|
423 delete this._totalLux; |
|
424 } |
|
425 |
|
426 Services.prefs.setCharPref("reader.color_scheme", colorSchemePref); |
|
427 }, |
|
428 |
|
429 _setFontType: function Reader_setFontType(newFontType) { |
|
430 if (this._fontType === newFontType) |
|
431 return; |
|
432 |
|
433 let bodyClasses = this._doc.body.classList; |
|
434 |
|
435 if (this._fontType) |
|
436 bodyClasses.remove(this._fontType); |
|
437 |
|
438 this._fontType = newFontType; |
|
439 bodyClasses.add(this._fontType); |
|
440 |
|
441 Services.prefs.setCharPref("reader.font_type", this._fontType); |
|
442 }, |
|
443 |
|
444 _getToolbarVisibility: function Reader_getToolbarVisibility() { |
|
445 return !this._toolbarElement.classList.contains("toolbar-hidden"); |
|
446 }, |
|
447 |
|
448 _setToolbarVisibility: function Reader_setToolbarVisibility(visible) { |
|
449 let win = this._win; |
|
450 if (win.history.state) |
|
451 win.history.back(); |
|
452 |
|
453 if (!this._toolbarEnabled) |
|
454 return; |
|
455 |
|
456 // Don't allow visible toolbar until banner state is known |
|
457 if (this._isReadingListItem == -1) |
|
458 return; |
|
459 |
|
460 if (this._getToolbarVisibility() === visible) |
|
461 return; |
|
462 |
|
463 this._toolbarElement.classList.toggle("toolbar-hidden"); |
|
464 this._setSystemUIVisibility(visible); |
|
465 |
|
466 if (!visible && !this._hasUsedToolbar) { |
|
467 this._hasUsedToolbar = Services.prefs.getBoolPref("reader.has_used_toolbar"); |
|
468 if (!this._hasUsedToolbar) { |
|
469 gChromeWin.NativeWindow.toast.show(gStrings.GetStringFromName("aboutReader.toolbarTip"), "short"); |
|
470 |
|
471 Services.prefs.setBoolPref("reader.has_used_toolbar", true); |
|
472 this._hasUsedToolbar = true; |
|
473 } |
|
474 } |
|
475 }, |
|
476 |
|
477 _toggleToolbarVisibility: function Reader_toggleToolbarVisibility(visible) { |
|
478 this._setToolbarVisibility(!this._getToolbarVisibility()); |
|
479 }, |
|
480 |
|
481 _setSystemUIVisibility: function Reader_setSystemUIVisibility(visible) { |
|
482 gChromeWin.sendMessageToJava({ |
|
483 type: "SystemUI:Visibility", |
|
484 visible: visible |
|
485 }); |
|
486 }, |
|
487 |
|
488 _loadFromURL: function Reader_loadFromURL(url) { |
|
489 this._showProgressDelayed(); |
|
490 |
|
491 gChromeWin.Reader.parseDocumentFromURL(url, function(article) { |
|
492 if (article) |
|
493 this._showContent(article); |
|
494 else |
|
495 this._showError(gStrings.GetStringFromName("aboutReader.loadError")); |
|
496 }.bind(this)); |
|
497 }, |
|
498 |
|
499 _loadFromTab: function Reader_loadFromTab(tabId, url) { |
|
500 this._showProgressDelayed(); |
|
501 |
|
502 gChromeWin.Reader.getArticleForTab(tabId, url, function(article) { |
|
503 if (article) |
|
504 this._showContent(article); |
|
505 else |
|
506 this._showError(gStrings.GetStringFromName("aboutReader.loadError")); |
|
507 }.bind(this)); |
|
508 }, |
|
509 |
|
510 _requestFavicon: function Reader_requestFavicon() { |
|
511 gChromeWin.sendMessageToJava({ |
|
512 type: "Reader:FaviconRequest", |
|
513 url: this._article.url |
|
514 }); |
|
515 }, |
|
516 |
|
517 _loadFavicon: function Reader_loadFavicon(url, faviconUrl) { |
|
518 if (this._article.url !== url) |
|
519 return; |
|
520 |
|
521 let doc = this._doc; |
|
522 |
|
523 let link = doc.createElement('link'); |
|
524 link.rel = 'shortcut icon'; |
|
525 link.href = faviconUrl; |
|
526 |
|
527 doc.getElementsByTagName('head')[0].appendChild(link); |
|
528 }, |
|
529 |
|
530 _updateImageMargins: function Reader_updateImageMargins() { |
|
531 let windowWidth = this._win.innerWidth; |
|
532 let contentWidth = this._contentElement.offsetWidth; |
|
533 let maxWidthStyle = windowWidth + "px !important"; |
|
534 |
|
535 let setImageMargins = function(img) { |
|
536 if (!img._originalWidth) |
|
537 img._originalWidth = img.offsetWidth; |
|
538 |
|
539 let imgWidth = img._originalWidth; |
|
540 |
|
541 // If the image is taking more than half of the screen, just make |
|
542 // it fill edge-to-edge. |
|
543 if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55) |
|
544 imgWidth = windowWidth; |
|
545 |
|
546 let sideMargin = Math.max((contentWidth - windowWidth) / 2, |
|
547 (contentWidth - imgWidth) / 2); |
|
548 |
|
549 let imageStyle = sideMargin + "px !important"; |
|
550 let widthStyle = imgWidth + "px !important"; |
|
551 |
|
552 let cssText = "max-width: " + maxWidthStyle + ";" + |
|
553 "width: " + widthStyle + ";" + |
|
554 "margin-left: " + imageStyle + ";" + |
|
555 "margin-right: " + imageStyle + ";"; |
|
556 |
|
557 img.style.cssText = cssText; |
|
558 } |
|
559 |
|
560 let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR); |
|
561 for (let i = imgs.length; --i >= 0;) { |
|
562 let img = imgs[i]; |
|
563 |
|
564 if (img.width > 0) { |
|
565 setImageMargins(img); |
|
566 } else { |
|
567 img.onload = function() { |
|
568 setImageMargins(img); |
|
569 } |
|
570 } |
|
571 } |
|
572 }, |
|
573 |
|
574 _maybeSetTextDirection: function Read_maybeSetTextDirection(article){ |
|
575 if(!article.dir) |
|
576 return; |
|
577 |
|
578 //Set "dir" attribute on content |
|
579 this._contentElement.setAttribute("dir", article.dir); |
|
580 this._headerElement.setAttribute("dir", article.dir); |
|
581 }, |
|
582 |
|
583 _showError: function Reader_showError(error) { |
|
584 this._headerElement.style.display = "none"; |
|
585 this._contentElement.style.display = "none"; |
|
586 |
|
587 this._messageElement.innerHTML = error; |
|
588 this._messageElement.style.display = "block"; |
|
589 |
|
590 this._doc.title = error; |
|
591 }, |
|
592 |
|
593 // This function is the JS version of Java's StringUtils.stripCommonSubdomains. |
|
594 _stripHost: function Reader_stripHost(host) { |
|
595 if (!host) |
|
596 return host; |
|
597 |
|
598 let start = 0; |
|
599 |
|
600 if (host.startsWith("www.")) |
|
601 start = 4; |
|
602 else if (host.startsWith("m.")) |
|
603 start = 2; |
|
604 else if (host.startsWith("mobile.")) |
|
605 start = 7; |
|
606 |
|
607 return host.substring(start); |
|
608 }, |
|
609 |
|
610 _showContent: function Reader_showContent(article) { |
|
611 this._messageElement.style.display = "none"; |
|
612 |
|
613 this._article = article; |
|
614 |
|
615 this._domainElement.href = article.url; |
|
616 let articleUri = Services.io.newURI(article.url, null, null); |
|
617 this._domainElement.innerHTML = this._stripHost(articleUri.host); |
|
618 |
|
619 this._creditsElement.innerHTML = article.byline; |
|
620 |
|
621 this._titleElement.textContent = article.title; |
|
622 this._doc.title = article.title; |
|
623 |
|
624 this._headerElement.style.display = "block"; |
|
625 |
|
626 let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils); |
|
627 let contentFragment = parserUtils.parseFragment(article.content, Ci.nsIParserUtils.SanitizerDropForms, |
|
628 false, articleUri, this._contentElement); |
|
629 this._contentElement.innerHTML = ""; |
|
630 this._contentElement.appendChild(contentFragment); |
|
631 this._updateImageMargins(); |
|
632 this._maybeSetTextDirection(article); |
|
633 |
|
634 this._contentElement.style.display = "block"; |
|
635 this._requestReadingListStatus(); |
|
636 |
|
637 this._toolbarEnabled = true; |
|
638 this._setToolbarVisibility(true); |
|
639 |
|
640 this._requestFavicon(); |
|
641 }, |
|
642 |
|
643 _hideContent: function Reader_hideContent() { |
|
644 this._headerElement.style.display = "none"; |
|
645 this._contentElement.style.display = "none"; |
|
646 }, |
|
647 |
|
648 _showProgressDelayed: function Reader_showProgressDelayed() { |
|
649 this._win.setTimeout(function() { |
|
650 // Article has already been loaded, no need to show |
|
651 // progress anymore. |
|
652 if (this._article) |
|
653 return; |
|
654 |
|
655 this._headerElement.style.display = "none"; |
|
656 this._contentElement.style.display = "none"; |
|
657 |
|
658 this._messageElement.innerHTML = gStrings.GetStringFromName("aboutReader.loading"); |
|
659 this._messageElement.style.display = "block"; |
|
660 }.bind(this), 300); |
|
661 }, |
|
662 |
|
663 _decodeQueryString: function Reader_decodeQueryString(url) { |
|
664 let result = {}; |
|
665 let query = url.split("?")[1]; |
|
666 if (query) { |
|
667 let pairs = query.split("&"); |
|
668 for (let i = 0; i < pairs.length; i++) { |
|
669 let [name, value] = pairs[i].split("="); |
|
670 result[name] = decodeURIComponent(value); |
|
671 } |
|
672 } |
|
673 |
|
674 return result; |
|
675 }, |
|
676 |
|
677 _setupSegmentedButton: function Reader_setupSegmentedButton(id, options, initialValue, callback) { |
|
678 let doc = this._doc; |
|
679 let segmentedButton = doc.getElementById(id); |
|
680 |
|
681 for (let i = 0; i < options.length; i++) { |
|
682 let option = options[i]; |
|
683 |
|
684 let item = doc.createElement("li"); |
|
685 let link = doc.createElement("a"); |
|
686 link.textContent = option.name; |
|
687 item.appendChild(link); |
|
688 |
|
689 if (option.linkClass !== undefined) |
|
690 link.classList.add(option.linkClass); |
|
691 |
|
692 if (option.description !== undefined) { |
|
693 let description = doc.createElement("div"); |
|
694 description.textContent = option.description; |
|
695 item.appendChild(description); |
|
696 } |
|
697 |
|
698 link.style.MozUserSelect = 'none'; |
|
699 segmentedButton.appendChild(item); |
|
700 |
|
701 link.addEventListener("click", function(aEvent) { |
|
702 if (!aEvent.isTrusted) |
|
703 return; |
|
704 |
|
705 aEvent.stopPropagation(); |
|
706 |
|
707 // Create a relative timestamp for telemetry |
|
708 let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; |
|
709 // Just pass the ID of the button as an extra and hope the ID doesn't change |
|
710 // unless the context changes |
|
711 UITelemetry.addEvent("action.1", "button", uptime, id); |
|
712 |
|
713 let items = segmentedButton.children; |
|
714 for (let j = items.length - 1; j >= 0; j--) { |
|
715 items[j].classList.remove("selected"); |
|
716 } |
|
717 |
|
718 item.classList.add("selected"); |
|
719 callback(option.value); |
|
720 }.bind(this), true); |
|
721 |
|
722 if (option.value === initialValue) |
|
723 item.classList.add("selected"); |
|
724 } |
|
725 }, |
|
726 |
|
727 _setupButton: function Reader_setupButton(id, callback) { |
|
728 let button = this._doc.getElementById(id); |
|
729 |
|
730 button.addEventListener("click", function(aEvent) { |
|
731 if (!aEvent.isTrusted) |
|
732 return; |
|
733 |
|
734 aEvent.stopPropagation(); |
|
735 callback(); |
|
736 }, true); |
|
737 }, |
|
738 |
|
739 _setupAllDropdowns: function Reader_setupAllDropdowns() { |
|
740 let doc = this._doc; |
|
741 let win = this._win; |
|
742 |
|
743 let dropdowns = doc.getElementsByClassName("dropdown"); |
|
744 |
|
745 for (let i = dropdowns.length - 1; i >= 0; i--) { |
|
746 let dropdown = dropdowns[i]; |
|
747 |
|
748 let dropdownToggle = dropdown.getElementsByClassName("dropdown-toggle")[0]; |
|
749 let dropdownPopup = dropdown.getElementsByClassName("dropdown-popup")[0]; |
|
750 |
|
751 if (!dropdownToggle || !dropdownPopup) |
|
752 continue; |
|
753 |
|
754 let dropdownArrow = doc.createElement("div"); |
|
755 dropdownArrow.className = "dropdown-arrow"; |
|
756 dropdownPopup.appendChild(dropdownArrow); |
|
757 |
|
758 let updatePopupPosition = function() { |
|
759 let popupWidth = dropdownPopup.offsetWidth + 30; |
|
760 let arrowWidth = dropdownArrow.offsetWidth; |
|
761 let toggleWidth = dropdownToggle.offsetWidth; |
|
762 let toggleLeft = dropdownToggle.offsetLeft; |
|
763 |
|
764 let popupShift = (toggleWidth - popupWidth) / 2; |
|
765 let popupLeft = Math.max(0, Math.min(win.innerWidth - popupWidth, toggleLeft + popupShift)); |
|
766 dropdownPopup.style.left = popupLeft + "px"; |
|
767 |
|
768 let arrowShift = (toggleWidth - arrowWidth) / 2; |
|
769 let arrowLeft = toggleLeft - popupLeft + arrowShift; |
|
770 dropdownArrow.style.left = arrowLeft + "px"; |
|
771 }; |
|
772 |
|
773 win.addEventListener("resize", function(aEvent) { |
|
774 if (!aEvent.isTrusted) |
|
775 return; |
|
776 |
|
777 // Wait for reflow before calculating the new position of the popup. |
|
778 setTimeout(updatePopupPosition, 0); |
|
779 }, true); |
|
780 |
|
781 dropdownToggle.addEventListener("click", function(aEvent) { |
|
782 if (!aEvent.isTrusted) |
|
783 return; |
|
784 |
|
785 aEvent.stopPropagation(); |
|
786 |
|
787 if (!this._getToolbarVisibility()) |
|
788 return; |
|
789 |
|
790 let dropdownClasses = dropdown.classList; |
|
791 |
|
792 if (dropdownClasses.contains("open")) { |
|
793 win.history.back(); |
|
794 } else { |
|
795 updatePopupPosition(); |
|
796 if (!this._closeAllDropdowns()) |
|
797 this._pushDropdownState(); |
|
798 |
|
799 dropdownClasses.add("open"); |
|
800 } |
|
801 }.bind(this), true); |
|
802 } |
|
803 }, |
|
804 |
|
805 _pushDropdownState: function Reader_pushDropdownState() { |
|
806 // FIXME: We're getting a NS_ERROR_UNEXPECTED error when we try |
|
807 // to do win.history.pushState() here (see bug 682296). This is |
|
808 // a workaround that allows us to push history state on the target |
|
809 // content document. |
|
810 |
|
811 let doc = this._doc; |
|
812 let body = doc.body; |
|
813 |
|
814 if (this._pushStateScript) |
|
815 body.removeChild(this._pushStateScript); |
|
816 |
|
817 this._pushStateScript = doc.createElement('script'); |
|
818 this._pushStateScript.type = "text/javascript"; |
|
819 this._pushStateScript.innerHTML = 'history.pushState({ dropdown: 1 }, document.title);'; |
|
820 |
|
821 body.appendChild(this._pushStateScript); |
|
822 }, |
|
823 |
|
824 _closeAllDropdowns : function Reader_closeAllDropdowns() { |
|
825 let dropdowns = this._doc.querySelectorAll(".dropdown.open"); |
|
826 for (let i = dropdowns.length - 1; i >= 0; i--) { |
|
827 dropdowns[i].classList.remove("open"); |
|
828 } |
|
829 |
|
830 return (dropdowns.length > 0) |
|
831 } |
|
832 }; |