mobile/android/chrome/content/aboutReader.js

branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
equal deleted inserted replaced
-1:000000000000 0:79db87912be7
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 };

mercurial