Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 this.EXPORTED_SYMBOLS = ["PopupNotifications"];
7 var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
9 Cu.import("resource://gre/modules/Services.jsm");
11 const NOTIFICATION_EVENT_DISMISSED = "dismissed";
12 const NOTIFICATION_EVENT_REMOVED = "removed";
13 const NOTIFICATION_EVENT_SHOWING = "showing";
14 const NOTIFICATION_EVENT_SHOWN = "shown";
15 const NOTIFICATION_EVENT_SWAPPING = "swapping";
17 const ICON_SELECTOR = ".notification-anchor-icon";
18 const ICON_ATTRIBUTE_SHOWING = "showing";
20 const PREF_SECURITY_DELAY = "security.notification_enable_delay";
22 let popupNotificationsMap = new WeakMap();
23 let gNotificationParents = new WeakMap;
25 function getAnchorFromBrowser(aBrowser) {
26 let anchor = aBrowser.getAttribute("popupnotificationanchor") ||
27 aBrowser.popupnotificationanchor;
28 if (anchor) {
29 if (anchor instanceof Ci.nsIDOMXULElement) {
30 return anchor;
31 }
32 return aBrowser.ownerDocument.getElementById(anchor);
33 }
34 return null;
35 }
37 /**
38 * Notification object describes a single popup notification.
39 *
40 * @see PopupNotifications.show()
41 */
42 function Notification(id, message, anchorID, mainAction, secondaryActions,
43 browser, owner, options) {
44 this.id = id;
45 this.message = message;
46 this.anchorID = anchorID;
47 this.mainAction = mainAction;
48 this.secondaryActions = secondaryActions || [];
49 this.browser = browser;
50 this.owner = owner;
51 this.options = options || {};
52 }
54 Notification.prototype = {
56 id: null,
57 message: null,
58 anchorID: null,
59 mainAction: null,
60 secondaryActions: null,
61 browser: null,
62 owner: null,
63 options: null,
64 timeShown: null,
66 /**
67 * Removes the notification and updates the popup accordingly if needed.
68 */
69 remove: function Notification_remove() {
70 this.owner.remove(this);
71 },
73 get anchorElement() {
74 let iconBox = this.owner.iconBox;
76 let anchorElement = getAnchorFromBrowser(this.browser);
78 if (!iconBox)
79 return anchorElement;
81 if (!anchorElement && this.anchorID)
82 anchorElement = iconBox.querySelector("#"+this.anchorID);
84 // Use a default anchor icon if it's available
85 if (!anchorElement)
86 anchorElement = iconBox.querySelector("#default-notification-icon") ||
87 iconBox;
89 return anchorElement;
90 },
92 reshow: function() {
93 this.owner._reshowNotifications(this.anchorElement, this.browser);
94 }
95 };
97 /**
98 * The PopupNotifications object manages popup notifications for a given browser
99 * window.
100 * @param tabbrowser
101 * window's <xul:tabbrowser/>. Used to observe tab switching events and
102 * for determining the active browser element.
103 * @param panel
104 * The <xul:panel/> element to use for notifications. The panel is
105 * populated with <popupnotification> children and displayed it as
106 * needed.
107 * @param iconBox
108 * Reference to a container element that should be hidden or
109 * unhidden when notifications are hidden or shown. It should be the
110 * parent of anchor elements whose IDs are passed to show().
111 * It is used as a fallback popup anchor if notifications specify
112 * invalid or non-existent anchor IDs.
113 */
114 this.PopupNotifications = function PopupNotifications(tabbrowser, panel, iconBox) {
115 if (!(tabbrowser instanceof Ci.nsIDOMXULElement))
116 throw "Invalid tabbrowser";
117 if (iconBox && !(iconBox instanceof Ci.nsIDOMXULElement))
118 throw "Invalid iconBox";
119 if (!(panel instanceof Ci.nsIDOMXULElement))
120 throw "Invalid panel";
122 this.window = tabbrowser.ownerDocument.defaultView;
123 this.panel = panel;
124 this.tabbrowser = tabbrowser;
125 this.iconBox = iconBox;
126 this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);
128 this.panel.addEventListener("popuphidden", this, true);
130 this.window.addEventListener("activate", this, true);
131 if (this.tabbrowser.tabContainer)
132 this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
133 }
135 PopupNotifications.prototype = {
137 window: null,
138 panel: null,
139 tabbrowser: null,
141 _iconBox: null,
142 set iconBox(iconBox) {
143 // Remove the listeners on the old iconBox, if needed
144 if (this._iconBox) {
145 this._iconBox.removeEventListener("click", this, false);
146 this._iconBox.removeEventListener("keypress", this, false);
147 }
148 this._iconBox = iconBox;
149 if (iconBox) {
150 iconBox.addEventListener("click", this, false);
151 iconBox.addEventListener("keypress", this, false);
152 }
153 },
154 get iconBox() {
155 return this._iconBox;
156 },
158 /**
159 * Enable or disable the opening/closing transition.
160 * @param state
161 * Boolean state
162 */
163 set transitionsEnabled(state) {
164 if (state) {
165 this.panel.removeAttribute("animate");
166 }
167 else {
168 this.panel.setAttribute("animate", "false");
169 }
170 },
172 /**
173 * Retrieve a Notification object associated with the browser/ID pair.
174 * @param id
175 * The Notification ID to search for.
176 * @param browser
177 * The browser whose notifications should be searched. If null, the
178 * currently selected browser's notifications will be searched.
179 *
180 * @returns the corresponding Notification object, or null if no such
181 * notification exists.
182 */
183 getNotification: function PopupNotifications_getNotification(id, browser) {
184 let n = null;
185 let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
186 notifications.some(function(x) x.id == id && (n = x));
187 return n;
188 },
190 /**
191 * Adds a new popup notification.
192 * @param browser
193 * The <xul:browser> element associated with the notification. Must not
194 * be null.
195 * @param id
196 * A unique ID that identifies the type of notification (e.g.
197 * "geolocation"). Only one notification with a given ID can be visible
198 * at a time. If a notification already exists with the given ID, it
199 * will be replaced.
200 * @param message
201 * The text to be displayed in the notification.
202 * @param anchorID
203 * The ID of the element that should be used as this notification
204 * popup's anchor. May be null, in which case the notification will be
205 * anchored to the iconBox.
206 * @param mainAction
207 * A JavaScript object literal describing the notification button's
208 * action. If present, it must have the following properties:
209 * - label (string): the button's label.
210 * - accessKey (string): the button's accessKey.
211 * - callback (function): a callback to be invoked when the button is
212 * pressed.
213 * - [optional] dismiss (boolean): If this is true, the notification
214 * will be dismissed instead of removed after running the callback.
215 * If null, the notification will not have a button, and
216 * secondaryActions will be ignored.
217 * @param secondaryActions
218 * An optional JavaScript array describing the notification's alternate
219 * actions. The array should contain objects with the same properties
220 * as mainAction. These are used to populate the notification button's
221 * dropdown menu.
222 * @param options
223 * An options JavaScript object holding additional properties for the
224 * notification. The following properties are currently supported:
225 * persistence: An integer. The notification will not automatically
226 * dismiss for this many page loads.
227 * timeout: A time in milliseconds. The notification will not
228 * automatically dismiss before this time.
229 * persistWhileVisible:
230 * A boolean. If true, a visible notification will always
231 * persist across location changes.
232 * dismissed: Whether the notification should be added as a dismissed
233 * notification. Dismissed notifications can be activated
234 * by clicking on their anchorElement.
235 * eventCallback:
236 * Callback to be invoked when the notification changes
237 * state. The callback's first argument is a string
238 * identifying the state change:
239 * "dismissed": notification has been dismissed by the
240 * user (e.g. by clicking away or switching
241 * tabs)
242 * "removed": notification has been removed (due to
243 * location change or user action)
244 * "showing": notification is about to be shown
245 * (this can be fired multiple times as
246 * notifications are dismissed and re-shown)
247 * If the callback returns true, the notification
248 * will be dismissed.
249 * "shown": notification has been shown (this can be fired
250 * multiple times as notifications are dismissed
251 * and re-shown)
252 * "swapping": the docshell of the browser that created
253 * the notification is about to be swapped to
254 * another browser. A second parameter contains
255 * the browser that is receiving the docshell,
256 * so that the event callback can transfer stuff
257 * specific to this notification.
258 * If the callback returns true, the notification
259 * will be moved to the new browser.
260 * If the callback isn't implemented, returns false,
261 * or doesn't return any value, the notification
262 * will be removed.
263 * neverShow: Indicate that no popup should be shown for this
264 * notification. Useful for just showing the anchor icon.
265 * removeOnDismissal:
266 * Notifications with this parameter set to true will be
267 * removed when they would have otherwise been dismissed
268 * (i.e. any time the popup is closed due to user
269 * interaction).
270 * hideNotNow: If true, indicates that the 'Not Now' menuitem should
271 * not be shown. If 'Not Now' is hidden, it needs to be
272 * replaced by another 'do nothing' item, so providing at
273 * least one secondary action is required; and one of the
274 * actions needs to have the 'dismiss' property set to true.
275 * popupIconURL:
276 * A string. URL of the image to be displayed in the popup.
277 * Normally specified in CSS using list-style-image and the
278 * .popup-notification-icon[popupid=...] selector.
279 * learnMoreURL:
280 * A string URL. Setting this property will make the
281 * prompt display a "Learn More" link that, when clicked,
282 * opens the URL in a new tab.
283 * @returns the Notification object corresponding to the added notification.
284 */
285 show: function PopupNotifications_show(browser, id, message, anchorID,
286 mainAction, secondaryActions, options) {
287 function isInvalidAction(a) {
288 return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey;
289 }
291 if (!browser)
292 throw "PopupNotifications_show: invalid browser";
293 if (!id)
294 throw "PopupNotifications_show: invalid ID";
295 if (mainAction && isInvalidAction(mainAction))
296 throw "PopupNotifications_show: invalid mainAction";
297 if (secondaryActions && secondaryActions.some(isInvalidAction))
298 throw "PopupNotifications_show: invalid secondaryActions";
299 if (options && options.hideNotNow &&
300 (!secondaryActions || !secondaryActions.length ||
301 !secondaryActions.concat(mainAction).some(action => action.dismiss)))
302 throw "PopupNotifications_show: 'Not Now' item hidden without replacement";
304 let notification = new Notification(id, message, anchorID, mainAction,
305 secondaryActions, browser, this, options);
307 if (options && options.dismissed)
308 notification.dismissed = true;
310 let existingNotification = this.getNotification(id, browser);
311 if (existingNotification)
312 this._remove(existingNotification);
314 let notifications = this._getNotificationsForBrowser(browser);
315 notifications.push(notification);
317 let isActive = this._isActiveBrowser(browser);
318 let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
319 if (isActive && fm.activeWindow == this.window) {
320 // show panel now
321 this._update(notifications, notification.anchorElement, true);
322 } else {
323 // Otherwise, update() will display the notification the next time the
324 // relevant tab/window is selected.
326 // If the tab is selected but the window is in the background, let the OS
327 // tell the user that there's a notification waiting in that window.
328 // At some point we might want to do something about background tabs here
329 // too. When the user switches to this window, we'll show the panel if
330 // this browser is a tab (thus showing the anchor icon). For
331 // non-tabbrowser browsers, we need to make the icon visible now or the
332 // user will not be able to open the panel.
333 if (!notification.dismissed && isActive) {
334 this.window.getAttention();
335 if (notification.anchorElement.parentNode != this.iconBox) {
336 this._updateAnchorIcon(notifications, notification.anchorElement);
337 }
338 }
340 // Notify observers that we're not showing the popup (useful for testing)
341 this._notify("backgroundShow");
342 }
344 return notification;
345 },
347 /**
348 * Returns true if the notification popup is currently being displayed.
349 */
350 get isPanelOpen() {
351 let panelState = this.panel.state;
353 return panelState == "showing" || panelState == "open";
354 },
356 /**
357 * Called by the consumer to indicate that a browser's location has changed,
358 * so that we can update the active notifications accordingly.
359 */
360 locationChange: function PopupNotifications_locationChange(aBrowser) {
361 if (!aBrowser)
362 throw "PopupNotifications_locationChange: invalid browser";
364 let notifications = this._getNotificationsForBrowser(aBrowser);
366 notifications = notifications.filter(function (notification) {
367 // The persistWhileVisible option allows an open notification to persist
368 // across location changes
369 if (notification.options.persistWhileVisible &&
370 this.isPanelOpen) {
371 if ("persistence" in notification.options &&
372 notification.options.persistence)
373 notification.options.persistence--;
374 return true;
375 }
377 // The persistence option allows a notification to persist across multiple
378 // page loads
379 if ("persistence" in notification.options &&
380 notification.options.persistence) {
381 notification.options.persistence--;
382 return true;
383 }
385 // The timeout option allows a notification to persist until a certain time
386 if ("timeout" in notification.options &&
387 Date.now() <= notification.options.timeout) {
388 return true;
389 }
391 this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
392 return false;
393 }, this);
395 this._setNotificationsForBrowser(aBrowser, notifications);
397 if (this._isActiveBrowser(aBrowser)) {
398 // get the anchor element if the browser has defined one so it will
399 // _update will handle both the tabs iconBox and non-tab permission
400 // anchors.
401 let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null;
402 if (!anchorElement)
403 anchorElement = getAnchorFromBrowser(aBrowser);
404 this._update(notifications, anchorElement);
405 }
406 },
408 /**
409 * Removes a Notification.
410 * @param notification
411 * The Notification object to remove.
412 */
413 remove: function PopupNotifications_remove(notification) {
414 this._remove(notification);
416 if (this._isActiveBrowser(notification.browser)) {
417 let notifications = this._getNotificationsForBrowser(notification.browser);
418 this._update(notifications, notification.anchorElement);
419 }
420 },
422 handleEvent: function (aEvent) {
423 switch (aEvent.type) {
424 case "popuphidden":
425 this._onPopupHidden(aEvent);
426 break;
427 case "activate":
428 case "TabSelect":
429 let self = this;
430 // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
431 // handler results in the popup being hidden again for some reason...
432 this.window.setTimeout(function () {
433 self._update();
434 }, 0);
435 break;
436 case "click":
437 case "keypress":
438 this._onIconBoxCommand(aEvent);
439 break;
440 }
441 },
443 ////////////////////////////////////////////////////////////////////////////////
444 // Utility methods
445 ////////////////////////////////////////////////////////////////////////////////
447 _ignoreDismissal: null,
448 _currentAnchorElement: null,
450 /**
451 * Gets notifications for the currently selected browser.
452 */
453 get _currentNotifications() {
454 return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : [];
455 },
457 _remove: function PopupNotifications_removeHelper(notification) {
458 // This notification may already be removed, in which case let's just fail
459 // silently.
460 let notifications = this._getNotificationsForBrowser(notification.browser);
461 if (!notifications)
462 return;
464 var index = notifications.indexOf(notification);
465 if (index == -1)
466 return;
468 if (this._isActiveBrowser(notification.browser))
469 notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
471 // remove the notification
472 notifications.splice(index, 1);
473 this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
474 },
476 /**
477 * Dismisses the notification without removing it.
478 */
479 _dismiss: function PopupNotifications_dismiss() {
480 let browser = this.panel.firstChild &&
481 this.panel.firstChild.notification.browser;
482 this.panel.hidePopup();
483 if (browser)
484 browser.focus();
485 },
487 /**
488 * Hides the notification popup.
489 */
490 _hidePanel: function PopupNotifications_hide() {
491 this._ignoreDismissal = true;
492 this.panel.hidePopup();
493 this._ignoreDismissal = false;
494 },
496 /**
497 * Removes all notifications from the notification popup.
498 */
499 _clearPanel: function () {
500 let popupnotification;
501 while ((popupnotification = this.panel.lastChild)) {
502 this.panel.removeChild(popupnotification);
504 // If this notification was provided by the chrome document rather than
505 // created ad hoc, move it back to where we got it from.
506 let originalParent = gNotificationParents.get(popupnotification);
507 if (originalParent) {
508 popupnotification.notification = null;
510 // Remove nodes dynamically added to the notification's menu button
511 // in _refreshPanel. Keep popupnotificationcontent nodes; they are
512 // provided by the chrome document.
513 let contentNode = popupnotification.lastChild;
514 while (contentNode) {
515 let previousSibling = contentNode.previousSibling;
516 if (contentNode.nodeName != "popupnotificationcontent")
517 popupnotification.removeChild(contentNode);
518 contentNode = previousSibling;
519 }
521 // Re-hide the notification such that it isn't rendered in the chrome
522 // document. _refreshPanel will unhide it again when needed.
523 popupnotification.hidden = true;
525 originalParent.appendChild(popupnotification);
526 }
527 }
528 },
530 _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
531 this._clearPanel();
533 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
535 notificationsToShow.forEach(function (n) {
536 let doc = this.window.document;
538 // Append "-notification" to the ID to try to avoid ID conflicts with other stuff
539 // in the document.
540 let popupnotificationID = n.id + "-notification";
542 // If the chrome document provides a popupnotification with this id, use
543 // that. Otherwise create it ad-hoc.
544 let popupnotification = doc.getElementById(popupnotificationID);
545 if (popupnotification)
546 gNotificationParents.set(popupnotification, popupnotification.parentNode);
547 else
548 popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
550 popupnotification.setAttribute("label", n.message);
551 popupnotification.setAttribute("id", popupnotificationID);
552 popupnotification.setAttribute("popupid", n.id);
553 popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
554 if (n.mainAction) {
555 popupnotification.setAttribute("buttonlabel", n.mainAction.label);
556 popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
557 popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
558 popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
559 popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
560 } else {
561 popupnotification.removeAttribute("buttonlabel");
562 popupnotification.removeAttribute("buttonaccesskey");
563 popupnotification.removeAttribute("buttoncommand");
564 popupnotification.removeAttribute("menucommand");
565 popupnotification.removeAttribute("closeitemcommand");
566 }
568 if (n.options.popupIconURL)
569 popupnotification.setAttribute("icon", n.options.popupIconURL);
570 if (n.options.learnMoreURL)
571 popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
572 else
573 popupnotification.removeAttribute("learnmoreurl");
575 popupnotification.notification = n;
577 if (n.secondaryActions) {
578 n.secondaryActions.forEach(function (a) {
579 let item = doc.createElementNS(XUL_NS, "menuitem");
580 item.setAttribute("label", a.label);
581 item.setAttribute("accesskey", a.accessKey);
582 item.notification = n;
583 item.action = a;
585 popupnotification.appendChild(item);
586 }, this);
588 if (n.options.hideNotNow) {
589 popupnotification.setAttribute("hidenotnow", "true");
590 }
591 else if (n.secondaryActions.length) {
592 let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
593 popupnotification.appendChild(closeItemSeparator);
594 }
595 }
597 this.panel.appendChild(popupnotification);
599 // The popupnotification may be hidden if we got it from the chrome
600 // document rather than creating it ad hoc.
601 popupnotification.hidden = false;
602 }, this);
603 },
605 _showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) {
606 this.panel.hidden = false;
608 notificationsToShow = notificationsToShow.filter(n => {
609 let dismiss = this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
610 if (dismiss)
611 n.dismissed = true;
612 return !dismiss;
613 });
614 if (!notificationsToShow.length)
615 return;
617 this._refreshPanel(notificationsToShow);
619 if (this.isPanelOpen && this._currentAnchorElement == anchorElement)
620 return;
622 // If the panel is already open but we're changing anchors, we need to hide
623 // it first. Otherwise it can appear in the wrong spot. (_hidePanel is
624 // safe to call even if the panel is already hidden.)
625 this._hidePanel();
627 // If the anchor element is hidden or null, use the tab as the anchor. We
628 // only ever show notifications for the current browser, so we can just use
629 // the current tab.
630 let selectedTab = this.tabbrowser.selectedTab;
631 if (anchorElement) {
632 let bo = anchorElement.boxObject;
633 if (bo.height == 0 && bo.width == 0)
634 anchorElement = selectedTab; // hidden
635 } else {
636 anchorElement = selectedTab; // null
637 }
639 this._currentAnchorElement = anchorElement;
641 // On OS X and Linux we need a different panel arrow color for
642 // click-to-play plugins, so copy the popupid and use css.
643 this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
644 notificationsToShow.forEach(function (n) {
645 // Remember the time the notification was shown for the security delay.
646 n.timeShown = this.window.performance.now();
647 }, this);
648 this.panel.openPopup(anchorElement, "bottomcenter topleft");
649 notificationsToShow.forEach(function (n) {
650 this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
651 }, this);
652 },
654 /**
655 * Updates the notification state in response to window activation or tab
656 * selection changes.
657 *
658 * @param notifications an array of Notification instances. if null,
659 * notifications will be retrieved off the current
660 * browser tab
661 * @param anchor is a XUL element that the notifications panel will be
662 * anchored to
663 * @param dismissShowing if true, dismiss any currently visible notifications
664 * if there are no notifications to show. Otherwise,
665 * currently displayed notifications will be left alone.
666 */
667 _update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) {
668 let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox);
669 if (useIconBox) {
670 // hide icons of the previous tab.
671 this._hideIcons();
672 }
674 let anchorElement = anchor, notificationsToShow = [];
675 if (!notifications)
676 notifications = this._currentNotifications;
677 let haveNotifications = notifications.length > 0;
678 if (haveNotifications) {
679 // Only show the notifications that have the passed-in anchor (or the
680 // first notification's anchor, if none was passed in). Other
681 // notifications will be shown once these are dismissed.
682 anchorElement = anchor || notifications[0].anchorElement;
684 if (useIconBox) {
685 this._showIcons(notifications);
686 this.iconBox.hidden = false;
687 } else if (anchorElement) {
688 this._updateAnchorIcon(notifications, anchorElement);
689 }
691 // Also filter out notifications that have been dismissed.
692 notificationsToShow = notifications.filter(function (n) {
693 return !n.dismissed && n.anchorElement == anchorElement &&
694 !n.options.neverShow;
695 });
696 }
698 if (notificationsToShow.length > 0) {
699 this._showPanel(notificationsToShow, anchorElement);
700 } else {
701 // Notify observers that we're not showing the popup (useful for testing)
702 this._notify("updateNotShowing");
704 // Close the panel if there are no notifications to show.
705 // When called from PopupNotifications.show() we should never close the
706 // panel, however. It may just be adding a dismissed notification, in
707 // which case we want to continue showing any existing notifications.
708 if (!dismissShowing)
709 this._dismiss();
711 // Only hide the iconBox if we actually have no notifications (as opposed
712 // to not having any showable notifications)
713 if (!haveNotifications) {
714 if (useIconBox)
715 this.iconBox.hidden = true;
716 else if (anchorElement)
717 anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
718 }
719 }
720 },
722 _updateAnchorIcon: function PopupNotifications_updateAnchorIcon(notifications,
723 anchorElement) {
724 anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
725 // Use the anchorID as a class along with the default icon class as a
726 // fallback if anchorID is not defined in CSS. We always use the first
727 // notifications icon, so in the case of multiple notifications we'll
728 // only use the default icon.
729 if (anchorElement.classList.contains("notification-anchor-icon")) {
730 // remove previous icon classes
731 let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"")
732 className = "default-notification-icon " + className;
733 if (notifications.length == 1) {
734 className = notifications[0].anchorID + " " + className;
735 }
736 anchorElement.className = className;
737 }
738 },
740 _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
741 for (let notification of aCurrentNotifications) {
742 let anchorElm = notification.anchorElement;
743 if (anchorElm) {
744 anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
745 }
746 }
747 },
749 _hideIcons: function PopupNotifications_hideIcons() {
750 let icons = this.iconBox.querySelectorAll(ICON_SELECTOR);
751 for (let icon of icons) {
752 icon.removeAttribute(ICON_ATTRIBUTE_SHOWING);
753 }
754 },
756 /**
757 * Gets and sets notifications for the browser.
758 */
759 _getNotificationsForBrowser: function PopupNotifications_getNotifications(browser) {
760 let notifications = popupNotificationsMap.get(browser);
761 if (!notifications) {
762 // Initialize the WeakMap for the browser so callers can reference/manipulate the array.
763 notifications = [];
764 popupNotificationsMap.set(browser, notifications);
765 }
766 return notifications;
767 },
768 _setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) {
769 popupNotificationsMap.set(browser, notifications);
770 return notifications;
771 },
773 _isActiveBrowser: function (browser) {
774 // Note: This helper only exists, because in e10s builds,
775 // we can't access the docShell of a browser from chrome.
776 return browser.docShell
777 ? browser.docShell.isActive
778 : (this.window.gBrowser.selectedBrowser == browser);
779 },
781 _onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) {
782 // Left click, space or enter only
783 let type = event.type;
784 if (type == "click" && event.button != 0)
785 return;
787 if (type == "keypress" &&
788 !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
789 event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN))
790 return;
792 if (this._currentNotifications.length == 0)
793 return;
795 // Get the anchor that is the immediate child of the icon box
796 let anchor = event.target;
797 while (anchor && anchor.parentNode != this.iconBox)
798 anchor = anchor.parentNode;
800 this._reshowNotifications(anchor);
801 },
803 _reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) {
804 // Mark notifications anchored to this anchor as un-dismissed
805 let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
806 notifications.forEach(function (n) {
807 if (n.anchorElement == anchor)
808 n.dismissed = false;
809 });
811 // ...and then show them.
812 this._update(notifications, anchor);
813 },
815 _swapBrowserNotifications: function PopupNotifications_swapBrowserNoficications(ourBrowser, otherBrowser) {
816 // When swaping browser docshells (e.g. dragging tab to new window) we need
817 // to update our notification map.
819 let ourNotifications = this._getNotificationsForBrowser(ourBrowser);
820 let other = otherBrowser.ownerDocument.defaultView.PopupNotifications;
821 if (!other) {
822 if (ourNotifications.length > 0)
823 Cu.reportError("unable to swap notifications: otherBrowser doesn't support notifications");
824 return;
825 }
826 let otherNotifications = other._getNotificationsForBrowser(otherBrowser);
827 if (ourNotifications.length < 1 && otherNotifications.length < 1) {
828 // No notification to swap.
829 return;
830 }
832 otherNotifications = otherNotifications.filter(n => {
833 if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) {
834 n.browser = ourBrowser;
835 n.owner = this;
836 return true;
837 }
838 other._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
839 return false;
840 });
842 ourNotifications = ourNotifications.filter(n => {
843 if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) {
844 n.browser = otherBrowser;
845 n.owner = other;
846 return true;
847 }
848 this._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
849 return false;
850 });
852 this._setNotificationsForBrowser(otherBrowser, ourNotifications);
853 other._setNotificationsForBrowser(ourBrowser, otherNotifications);
855 if (otherNotifications.length > 0)
856 this._update(otherNotifications, otherNotifications[0].anchorElement);
857 if (ourNotifications.length > 0)
858 other._update(ourNotifications, ourNotifications[0].anchorElement);
859 },
861 _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) {
862 try {
863 if (n.options.eventCallback)
864 return n.options.eventCallback.call(n, event, ...args);
865 } catch (error) {
866 Cu.reportError(error);
867 }
868 return undefined;
869 },
871 _onPopupHidden: function PopupNotifications_onPopupHidden(event) {
872 if (event.target != this.panel || this._ignoreDismissal)
873 return;
875 let browser = this.panel.firstChild &&
876 this.panel.firstChild.notification.browser;
877 if (!browser)
878 return;
880 let notifications = this._getNotificationsForBrowser(browser);
881 // Mark notifications as dismissed and call dismissal callbacks
882 Array.forEach(this.panel.childNodes, function (nEl) {
883 let notificationObj = nEl.notification;
884 // Never call a dismissal handler on a notification that's been removed.
885 if (notifications.indexOf(notificationObj) == -1)
886 return;
888 // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
889 // if the notification is removed.
890 if (notificationObj.options.removeOnDismissal)
891 this._remove(notificationObj);
892 else {
893 notificationObj.dismissed = true;
894 this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
895 }
896 }, this);
898 this._clearPanel();
900 this._update();
901 },
903 _onButtonCommand: function PopupNotifications_onButtonCommand(event) {
904 // Need to find the associated notification object, which is a bit tricky
905 // since it isn't associated with the button directly - this is kind of
906 // gross and very dependent on the structure of the popupnotification
907 // binding's content.
908 let target = event.originalTarget;
909 let notificationEl;
910 let parent = target;
911 while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
912 notificationEl = parent;
914 if (!notificationEl)
915 throw "PopupNotifications_onButtonCommand: couldn't find notification element";
917 if (!notificationEl.notification)
918 throw "PopupNotifications_onButtonCommand: couldn't find notification";
920 let notification = notificationEl.notification;
921 let timeSinceShown = this.window.performance.now() - notification.timeShown;
923 // Only report the first time mainAction is triggered and remember that this occurred.
924 if (!notification.timeMainActionFirstTriggered) {
925 notification.timeMainActionFirstTriggered = timeSinceShown;
926 Services.telemetry.getHistogramById("POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS").
927 add(timeSinceShown);
928 }
930 if (timeSinceShown < this.buttonDelay) {
931 Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
932 "Button click happened before the security delay: " +
933 timeSinceShown + "ms");
934 return;
935 }
936 try {
937 notification.mainAction.callback.call();
938 } catch(error) {
939 Cu.reportError(error);
940 }
942 if (notification.mainAction.dismiss) {
943 this._dismiss();
944 return;
945 }
947 this._remove(notification);
948 this._update();
949 },
951 _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
952 let target = event.originalTarget;
953 if (!target.action || !target.notification)
954 throw "menucommand target has no associated action/notification";
956 event.stopPropagation();
957 try {
958 target.action.callback.call();
959 } catch(error) {
960 Cu.reportError(error);
961 }
963 if (target.action.dismiss) {
964 this._dismiss();
965 return;
966 }
968 this._remove(target.notification);
969 this._update();
970 },
972 _notify: function PopupNotifications_notify(topic) {
973 Services.obs.notifyObservers(null, "PopupNotifications-" + topic, "");
974 },
975 };