123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- this.EXPORTED_SYMBOLS = ["PopupNotifications"];
- var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
- Cu.import("resource://gre/modules/Services.jsm");
- const NOTIFICATION_EVENT_DISMISSED = "dismissed";
- const NOTIFICATION_EVENT_REMOVED = "removed";
- const NOTIFICATION_EVENT_SHOWING = "showing";
- const NOTIFICATION_EVENT_SHOWN = "shown";
- const NOTIFICATION_EVENT_SWAPPING = "swapping";
- const ICON_SELECTOR = ".notification-anchor-icon";
- const ICON_ATTRIBUTE_SHOWING = "showing";
- const PREF_SECURITY_DELAY = "security.notification_enable_delay";
- var popupNotificationsMap = new WeakMap();
- var gNotificationParents = new WeakMap;
- function getAnchorFromBrowser(aBrowser) {
- let anchor = aBrowser.getAttribute("popupnotificationanchor") ||
- aBrowser.popupnotificationanchor;
- if (anchor) {
- if (anchor instanceof Ci.nsIDOMXULElement) {
- return anchor;
- }
- return aBrowser.ownerDocument.getElementById(anchor);
- }
- return null;
- }
- function getNotificationFromElement(aElement) {
- // Need to find the associated notification object, which is a bit tricky
- // since it isn't associated with the element directly - this is kind of
- // gross and very dependent on the structure of the popupnotification
- // binding's content.
- let notificationEl;
- let parent = aElement;
- while (parent && (parent = aElement.ownerDocument.getBindingParent(parent)))
- notificationEl = parent;
- return notificationEl;
- }
- /**
- * Notification object describes a single popup notification.
- *
- * @see PopupNotifications.show()
- */
- function Notification(id, message, anchorID, mainAction, secondaryActions,
- browser, owner, options) {
- this.id = id;
- this.message = message;
- this.anchorID = anchorID;
- this.mainAction = mainAction;
- this.secondaryActions = secondaryActions || [];
- this.browser = browser;
- this.owner = owner;
- this.options = options || {};
- }
- Notification.prototype = {
- id: null,
- message: null,
- anchorID: null,
- mainAction: null,
- secondaryActions: null,
- browser: null,
- owner: null,
- options: null,
- timeShown: null,
- /**
- * Removes the notification and updates the popup accordingly if needed.
- */
- remove: function() {
- this.owner.remove(this);
- },
- get anchorElement() {
- let iconBox = this.owner.iconBox;
- let anchorElement = getAnchorFromBrowser(this.browser);
- if (!iconBox)
- return anchorElement;
- if (!anchorElement && this.anchorID)
- anchorElement = iconBox.querySelector("#"+this.anchorID);
- // Use a default anchor icon if it's available
- if (!anchorElement)
- anchorElement = iconBox.querySelector("#default-notification-icon") ||
- iconBox;
- return anchorElement;
- },
- reshow: function() {
- this.owner._reshowNotifications(this.anchorElement, this.browser);
- }
- };
- /**
- * The PopupNotifications object manages popup notifications for a given browser
- * window.
- * @param tabbrowser
- * window's <xul:tabbrowser/>. Used to observe tab switching events and
- * for determining the active browser element.
- * @param panel
- * The <xul:panel/> element to use for notifications. The panel is
- * populated with <popupnotification> children and displayed it as
- * needed.
- * @param iconBox
- * Reference to a container element that should be hidden or
- * unhidden when notifications are hidden or shown. It should be the
- * parent of anchor elements whose IDs are passed to show().
- * It is used as a fallback popup anchor if notifications specify
- * invalid or non-existent anchor IDs.
- */
- this.PopupNotifications = function PopupNotifications(tabbrowser, panel, iconBox) {
- if (!(tabbrowser instanceof Ci.nsIDOMXULElement))
- throw "Invalid tabbrowser";
- if (iconBox && !(iconBox instanceof Ci.nsIDOMXULElement))
- throw "Invalid iconBox";
- if (!(panel instanceof Ci.nsIDOMXULElement))
- throw "Invalid panel";
- this.window = tabbrowser.ownerDocument.defaultView;
- this.panel = panel;
- this.tabbrowser = tabbrowser;
- this.iconBox = iconBox;
- this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);
- this.panel.addEventListener("popuphidden", this, true);
- this.window.addEventListener("activate", this, true);
- if (this.tabbrowser.tabContainer)
- this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
- }
- PopupNotifications.prototype = {
- window: null,
- panel: null,
- tabbrowser: null,
- _iconBox: null,
- set iconBox(iconBox) {
- // Remove the listeners on the old iconBox, if needed
- if (this._iconBox) {
- this._iconBox.removeEventListener("click", this, false);
- this._iconBox.removeEventListener("keypress", this, false);
- }
- this._iconBox = iconBox;
- if (iconBox) {
- iconBox.addEventListener("click", this, false);
- iconBox.addEventListener("keypress", this, false);
- }
- },
- get iconBox() {
- return this._iconBox;
- },
- /**
- * Retrieve a Notification object associated with the browser/ID pair.
- * @param id
- * The Notification ID to search for.
- * @param browser
- * The browser whose notifications should be searched. If null, the
- * currently selected browser's notifications will be searched.
- *
- * @returns the corresponding Notification object, or null if no such
- * notification exists.
- */
- getNotification: function(id, browser) {
- let n = null;
- let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
- notifications.some(function(x) x.id == id && (n = x));
- return n;
- },
- /**
- * Adds a new popup notification.
- * @param browser
- * The <xul:browser> element associated with the notification. Must not
- * be null.
- * @param id
- * A unique ID that identifies the type of notification (e.g.
- * "geolocation"). Only one notification with a given ID can be visible
- * at a time. If a notification already exists with the given ID, it
- * will be replaced.
- * @param message
- * The text to be displayed in the notification.
- * @param anchorID
- * The ID of the element that should be used as this notification
- * popup's anchor. May be null, in which case the notification will be
- * anchored to the iconBox.
- * @param mainAction
- * A JavaScript object literal describing the notification button's
- * action. If present, it must have the following properties:
- * - label (string): the button's label.
- * - accessKey (string): the button's accessKey.
- * - callback (function): a callback to be invoked when the button is
- * pressed, is passed an object that contains the following fields:
- * - checkboxChecked: (boolean) If the optional checkbox is checked.
- * If null, the notification will not have a button, and
- * secondaryActions will be ignored.
- * @param secondaryActions
- * An optional JavaScript array describing the notification's alternate
- * actions. The array should contain objects with the same properties
- * as mainAction. These are used to populate the notification button's
- * dropdown menu.
- * @param options
- * An options JavaScript object holding additional properties for the
- * notification. The following properties are currently supported:
- * persistence: An integer. The notification will not automatically
- * dismiss for this many page loads.
- * timeout: A time in milliseconds. The notification will not
- * automatically dismiss before this time.
- * persistWhileVisible:
- * A boolean. If true, a visible notification will always
- * persist across location changes.
- * dismissed: Whether the notification should be added as a dismissed
- * notification. Dismissed notifications can be activated
- * by clicking on their anchorElement.
- * eventCallback:
- * Callback to be invoked when the notification changes
- * state. The callback's first argument is a string
- * identifying the state change:
- * "dismissed": notification has been dismissed by the
- * user (e.g. by clicking away or switching
- * tabs)
- * "removed": notification has been removed (due to
- * location change or user action)
- * "showing": notification is about to be shown
- * (this can be fired multiple times as
- * notifications are dismissed and re-shown)
- * "shown": notification has been shown (this can be fired
- * multiple times as notifications are dismissed
- * and re-shown)
- * "swapping": the docshell of the browser that created
- * the notification is about to be swapped to
- * another browser. A second parameter contains
- * the browser that is receiving the docshell,
- * so that the event callback can transfer stuff
- * specific to this notification.
- * If the callback returns true, the notification
- * will be moved to the new browser.
- * If the callback isn't implemented, returns false,
- * or doesn't return any value, the notification
- * will be removed.
- * neverShow: Indicate that no popup should be shown for this
- * notification. Useful for just showing the anchor icon.
- * removeOnDismissal:
- * Notifications with this parameter set to true will be
- * removed when they would have otherwise been dismissed
- * (i.e. any time the popup is closed due to user
- * interaction).
- * checkbox: An object that allows you to add a checkbox and
- * control its behavior with these fields:
- * label:
- * (required) Label to be shown next to the checkbox.
- * checked:
- * (optional) Whether the checkbox should be checked
- * by default. Defaults to false.
- * checkedState:
- * (optional) An object that allows you to customize
- * the notification state when the checkbox is checked.
- * disableMainAction:
- * (optional) Whether the mainAction is disabled.
- * Defaults to false.
- * warningLabel:
- * (optional) A (warning) text that is shown below the
- * checkbox. Pass null to hide.
- * uncheckedState:
- * (optional) An object that allows you to customize
- * the notification state when the checkbox is not checked.
- * Has the same attributes as checkedState.
- * popupIconURL:
- * A string. URL of the image to be displayed in the popup.
- * Normally specified in CSS using list-style-image and the
- * .popup-notification-icon[popupid=...] selector.
- * learnMoreURL:
- * A string URL. Setting this property will make the
- * prompt display a "Learn More" link that, when clicked,
- * opens the URL in a new tab.
- * @returns the Notification object corresponding to the added notification.
- */
- show: function(browser, id, message, anchorID,
- mainAction, secondaryActions, options) {
- function isInvalidAction(a) {
- return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey;
- }
- if (!browser)
- throw "PopupNotifications_show: invalid browser";
- if (!id)
- throw "PopupNotifications_show: invalid ID";
- if (mainAction && isInvalidAction(mainAction))
- throw "PopupNotifications_show: invalid mainAction";
- if (secondaryActions && secondaryActions.some(isInvalidAction))
- throw "PopupNotifications_show: invalid secondaryActions";
- let notification = new Notification(id, message, anchorID, mainAction,
- secondaryActions, browser, this, options);
- if (options && options.dismissed)
- notification.dismissed = true;
- let existingNotification = this.getNotification(id, browser);
- if (existingNotification)
- this._remove(existingNotification);
- let notifications = this._getNotificationsForBrowser(browser);
- notifications.push(notification);
- let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
- if (browser.docShell.isActive && fm.activeWindow == this.window) {
- // show panel now
- this._update(notifications, notification.anchorElement, true);
- } else {
- // Otherwise, update() will display the notification the next time the
- // relevant tab/window is selected.
- // If the tab is selected but the window is in the background, let the OS
- // tell the user that there's a notification waiting in that window.
- // At some point we might want to do something about background tabs here
- // too. When the user switches to this window, we'll show the panel if
- // this browser is a tab (thus showing the anchor icon). For
- // non-tabbrowser browsers, we need to make the icon visible now or the
- // user will not be able to open the panel.
- if (!notification.dismissed && browser.docShell.isActive) {
- this.window.getAttention();
- if (notification.anchorElement.parentNode != this.iconBox) {
- notification.anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
- }
- }
- // Notify observers that we're not showing the popup (useful for testing)
- this._notify("backgroundShow");
- }
- return notification;
- },
- /**
- * Returns true if the notification popup is currently being displayed.
- */
- get isPanelOpen() {
- let panelState = this.panel.state;
- return panelState == "showing" || panelState == "open";
- },
- /**
- * Called by the consumer to indicate that a browser's location has changed,
- * so that we can update the active notifications accordingly.
- */
- locationChange: function(aBrowser) {
- if (!aBrowser)
- throw "PopupNotifications_locationChange: invalid browser";
- let notifications = this._getNotificationsForBrowser(aBrowser);
- notifications = notifications.filter(function(notification) {
- // The persistWhileVisible option allows an open notification to persist
- // across location changes
- if (notification.options.persistWhileVisible &&
- this.isPanelOpen) {
- if ("persistence" in notification.options &&
- notification.options.persistence)
- notification.options.persistence--;
- return true;
- }
- // The persistence option allows a notification to persist across multiple
- // page loads
- if ("persistence" in notification.options &&
- notification.options.persistence) {
- notification.options.persistence--;
- return true;
- }
- // The timeout option allows a notification to persist until a certain time
- if ("timeout" in notification.options &&
- Date.now() <= notification.options.timeout) {
- return true;
- }
- this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
- return false;
- }, this);
- this._setNotificationsForBrowser(aBrowser, notifications);
- if (aBrowser.docShell.isActive) {
- // get the anchor element if the browser has defined one so it will
- // _update will handle both the tabs iconBox and non-tab permission
- // anchors.
- let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null;
- if (!anchorElement)
- anchorElement = getAnchorFromBrowser(aBrowser);
- this._update(notifications, anchorElement);
- }
- },
- /**
- * Removes a Notification.
- * @param notification
- * The Notification object to remove.
- */
- remove: function(notification) {
- this._remove(notification);
-
- if (notification.browser.docShell.isActive) {
- let notifications = this._getNotificationsForBrowser(notification.browser);
- this._update(notifications, notification.anchorElement);
- }
- },
- handleEvent: function(aEvent) {
- switch (aEvent.type) {
- case "popuphidden":
- this._onPopupHidden(aEvent);
- break;
- case "activate":
- case "TabSelect":
- let self = this;
- // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
- // handler results in the popup being hidden again for some reason...
- this.window.setTimeout(function() {
- self._update();
- }, 0);
- break;
- case "click":
- case "keypress":
- this._onIconBoxCommand(aEvent);
- break;
- }
- },
- ////////////////////////////////////////////////////////////////////////////////
- // Utility methods
- ////////////////////////////////////////////////////////////////////////////////
- _ignoreDismissal: null,
- _currentAnchorElement: null,
- /**
- * Gets notifications for the currently selected browser.
- */
- get _currentNotifications() {
- return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : [];
- },
- _remove: function(notification) {
- // This notification may already be removed, in which case let's just fail
- // silently.
- let notifications = this._getNotificationsForBrowser(notification.browser);
- if (!notifications)
- return;
- var index = notifications.indexOf(notification);
- if (index == -1)
- return;
- if (notification.browser.docShell.isActive)
- notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
- // remove the notification
- notifications.splice(index, 1);
- this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
- },
- /**
- * Dismisses the notification without removing it.
- */
- _dismiss: function() {
- let browser = this.panel.firstChild &&
- this.panel.firstChild.notification.browser;
- if (typeof this.panel.hidePopup === "function") {
- this.panel.hidePopup();
- }
- if (browser)
- browser.focus();
- },
- /**
- * Hides the notification popup.
- */
- _hidePanel: function() {
- this._ignoreDismissal = true;
- if (typeof this.panel.hidePopup === "function") {
- this.panel.hidePopup();
- }
- this._ignoreDismissal = false;
- },
- /**
- * Removes all notifications from the notification popup.
- */
- _clearPanel: function() {
- let popupnotification;
- while ((popupnotification = this.panel.lastChild)) {
- this.panel.removeChild(popupnotification);
- // If this notification was provided by the chrome document rather than
- // created ad hoc, move it back to where we got it from.
- let originalParent = gNotificationParents.get(popupnotification);
- if (originalParent) {
- popupnotification.notification = null;
- // Remove nodes dynamically added to the notification's menu button
- // in _refreshPanel. Keep popupnotificationcontent nodes; they are
- // provided by the chrome document.
- let contentNode = popupnotification.lastChild;
- while (contentNode) {
- let previousSibling = contentNode.previousSibling;
- if (contentNode.nodeName != "popupnotificationcontent")
- popupnotification.removeChild(contentNode);
- contentNode = previousSibling;
- }
- // Re-hide the notification such that it isn't rendered in the chrome
- // document. _refreshPanel will unhide it again when needed.
- popupnotification.hidden = true;
- originalParent.appendChild(popupnotification);
- }
- }
- },
- _refreshPanel: function(notificationsToShow) {
- this._clearPanel();
- const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
- notificationsToShow.forEach(function(n) {
- let doc = this.window.document;
- // Append "-notification" to the ID to try to avoid ID conflicts with other stuff
- // in the document.
- let popupnotificationID = n.id + "-notification";
- // If the chrome document provides a popupnotification with this id, use
- // that. Otherwise create it ad-hoc.
- let popupnotification = doc.getElementById(popupnotificationID);
- if (popupnotification)
- gNotificationParents.set(popupnotification, popupnotification.parentNode);
- else
- popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
- popupnotification.setAttribute("label", n.message);
- popupnotification.setAttribute("id", popupnotificationID);
- popupnotification.setAttribute("popupid", n.id);
- popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
- if (n.mainAction) {
- popupnotification.setAttribute("buttonlabel", n.mainAction.label);
- popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
- popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
- popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
- popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
- } else {
- popupnotification.removeAttribute("buttonlabel");
- popupnotification.removeAttribute("buttonaccesskey");
- popupnotification.removeAttribute("buttoncommand");
- popupnotification.removeAttribute("menucommand");
- popupnotification.removeAttribute("closeitemcommand");
- }
- if (n.options.popupIconURL)
- popupnotification.setAttribute("icon", n.options.popupIconURL);
- if (n.options.learnMoreURL)
- popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
- else
- popupnotification.removeAttribute("learnmoreurl");
- popupnotification.notification = n;
- if (n.secondaryActions) {
- n.secondaryActions.forEach(function(a) {
- let item = doc.createElementNS(XUL_NS, "menuitem");
- item.setAttribute("label", a.label);
- item.setAttribute("accesskey", a.accessKey);
- item.notification = n;
- item.action = a;
- popupnotification.appendChild(item);
- }, this);
- if (n.secondaryActions.length) {
- let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
- popupnotification.appendChild(closeItemSeparator);
- }
- }
- let checkbox = n.options.checkbox;
- if (checkbox && checkbox.label) {
- let checked = n._checkboxChecked != null ? n._checkboxChecked : !!checkbox.checked;
- popupnotification.setAttribute("checkboxhidden", "false");
- popupnotification.setAttribute("checkboxchecked", checked);
- popupnotification.setAttribute("checkboxlabel", checkbox.label);
- popupnotification.setAttribute("checkboxcommand", "PopupNotifications._onCheckboxCommand(event);");
- if (checked) {
- this._setNotificationUIState(popupnotification, checkbox.checkedState);
- } else {
- this._setNotificationUIState(popupnotification, checkbox.uncheckedState);
- }
- } else {
- popupnotification.setAttribute("checkboxhidden", "true");
- }
- this.panel.appendChild(popupnotification);
- // The popupnotification may be hidden if we got it from the chrome
- // document rather than creating it ad hoc.
- popupnotification.hidden = false;
- }, this);
- },
- _setNotificationUIState(notification, state={}) {
- notification.setAttribute("mainactiondisabled", state.disableMainAction || "false");
- if (state.warningLabel) {
- notification.setAttribute("warninglabel", state.warningLabel);
- notification.setAttribute("warninghidden", "false");
- } else {
- notification.setAttribute("warninghidden", "true");
- }
- },
- _onCheckboxCommand(event) {
- let notificationEl = getNotificationFromElement(event.originalTarget);
- let checked = notificationEl.checkbox.checked;
- let notification = notificationEl.notification;
- // Save checkbox state to be able to persist it when re-opening the doorhanger.
- notification._checkboxChecked = checked;
- if (checked) {
- this._setNotificationUIState(notificationEl, notification.options.checkbox.checkedState);
- } else {
- this._setNotificationUIState(notificationEl, notification.options.checkbox.uncheckedState);
- }
- },
- _showPanel: function(notificationsToShow, anchorElement) {
- this.panel.hidden = false;
- notificationsToShow.forEach(function(n) {
- this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
- }, this);
- this._refreshPanel(notificationsToShow);
- if (this.isPanelOpen && this._currentAnchorElement == anchorElement)
- return;
- // If the panel is already open but we're changing anchors, we need to hide
- // it first. Otherwise it can appear in the wrong spot. (_hidePanel is
- // safe to call even if the panel is already hidden.)
- this._hidePanel();
- // If the anchor element is hidden or null, use the tab as the anchor. We
- // only ever show notifications for the current browser, so we can just use
- // the current tab.
- let selectedTab = this.tabbrowser.selectedTab;
- if (anchorElement) {
- let bo = anchorElement.boxObject;
- if (bo.height == 0 && bo.width == 0)
- anchorElement = selectedTab; // hidden
- } else {
- anchorElement = selectedTab; // null
- }
- this._currentAnchorElement = anchorElement;
- // On OS X and Linux we need a different panel arrow color for
- // click-to-play plugins, so copy the popupid and use css.
- this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
- notificationsToShow.forEach(function(n) {
- // Remember the time the notification was shown for the security delay.
- n.timeShown = this.window.performance.now();
- }, this);
- this.panel.openPopup(anchorElement, "bottomcenter topleft");
- notificationsToShow.forEach(function(n) {
- this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
- }, this);
- },
- /**
- * Updates the notification state in response to window activation or tab
- * selection changes.
- *
- * @param notifications an array of Notification instances. if null,
- * notifications will be retrieved off the current
- * browser tab
- * @param anchor is a XUL element that the notifications panel will be
- * anchored to
- * @param dismissShowing if true, dismiss any currently visible notifications
- * if there are no notifications to show. Otherwise,
- * currently displayed notifications will be left alone.
- */
- _update: function(notifications, anchor, dismissShowing = false) {
- let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox);
- if (useIconBox) {
- // hide icons of the previous tab.
- this._hideIcons();
- }
- let anchorElement = anchor, notificationsToShow = [];
- if (!notifications)
- notifications = this._currentNotifications;
- let haveNotifications = notifications.length > 0;
- if (haveNotifications) {
- // Only show the notifications that have the passed-in anchor (or the
- // first notification's anchor, if none was passed in). Other
- // notifications will be shown once these are dismissed.
- anchorElement = anchor || notifications[0].anchorElement;
- if (useIconBox) {
- this._showIcons(notifications);
- this.iconBox.hidden = false;
- } else if (anchorElement) {
- anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
- // use the anchorID as a class along with the default icon class as a
- // fallback if anchorID is not defined in CSS. We always use the first
- // notifications icon, so in the case of multiple notifications we'll
- // only use the default icon
- if (anchorElement.classList.contains("notification-anchor-icon")) {
- // remove previous icon classes
- let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"")
- className = "default-notification-icon " + className;
- if (notifications.length == 1) {
- className = notifications[0].anchorID + " " + className;
- }
- anchorElement.className = className;
- }
- }
- // Also filter out notifications that have been dismissed.
- notificationsToShow = notifications.filter(function(n) {
- return !n.dismissed && n.anchorElement == anchorElement &&
- !n.options.neverShow;
- });
- }
- if (notificationsToShow.length > 0) {
- this._showPanel(notificationsToShow, anchorElement);
- } else {
- // Notify observers that we're not showing the popup (useful for testing)
- this._notify("updateNotShowing");
- // Close the panel if there are no notifications to show.
- // When called from PopupNotifications.show() we should never close the
- // panel, however. It may just be adding a dismissed notification, in
- // which case we want to continue showing any existing notifications.
- if (!dismissShowing)
- this._dismiss();
- // Only hide the iconBox if we actually have no notifications (as opposed
- // to not having any showable notifications)
- if (!haveNotifications) {
- if (useIconBox)
- this.iconBox.hidden = true;
- else if (anchorElement)
- anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
- }
- }
- },
- _showIcons: function(aCurrentNotifications) {
- for (let notification of aCurrentNotifications) {
- let anchorElm = notification.anchorElement;
- if (anchorElm) {
- anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
- }
- }
- },
- _hideIcons: function() {
- let icons = this.iconBox.querySelectorAll(ICON_SELECTOR);
- for (let icon of icons) {
- icon.removeAttribute(ICON_ATTRIBUTE_SHOWING);
- }
- },
- /**
- * Gets and sets notifications for the browser.
- */
- _getNotificationsForBrowser: function(browser) {
- let notifications = popupNotificationsMap.get(browser);
- if (!notifications) {
- // Initialize the WeakMap for the browser so callers can reference/manipulate the array.
- notifications = [];
- popupNotificationsMap.set(browser, notifications);
- }
- return notifications;
- },
- _setNotificationsForBrowser: function(browser, notifications) {
- popupNotificationsMap.set(browser, notifications);
- return notifications;
- },
- _onIconBoxCommand: function(event) {
- // Left click, space or enter only
- let type = event.type;
- if (type == "click" && event.button != 0)
- return;
- if (type == "keypress" &&
- !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
- event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN))
- return;
- if (this._currentNotifications.length == 0)
- return;
- // Get the anchor that is the immediate child of the icon box
- let anchor = event.target;
- while (anchor && anchor.parentNode != this.iconBox)
- anchor = anchor.parentNode;
- this._reshowNotifications(anchor);
- },
- _reshowNotifications: function(anchor, browser) {
- // Mark notifications anchored to this anchor as un-dismissed
- let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
- notifications.forEach(function(n) {
- if (n.anchorElement == anchor)
- n.dismissed = false;
- });
- // ...and then show them.
- this._update(notifications, anchor);
- },
- _swapBrowserNotifications: function(ourBrowser, otherBrowser) {
- // When swaping browser docshells (e.g. dragging tab to new window) we need
- // to update our notification map.
- let ourNotifications = this._getNotificationsForBrowser(ourBrowser);
- let other = otherBrowser.ownerDocument.defaultView.PopupNotifications;
- if (!other) {
- if (ourNotifications.length > 0)
- Cu.reportError("unable to swap notifications: otherBrowser doesn't support notifications");
- return;
- }
- let otherNotifications = other._getNotificationsForBrowser(otherBrowser);
- if (ourNotifications.length < 1 && otherNotifications.length < 1) {
- // No notification to swap.
- return;
- }
- otherNotifications = otherNotifications.filter(n => {
- if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) {
- n.browser = ourBrowser;
- n.owner = this;
- return true;
- }
- other._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
- return false;
- });
- ourNotifications = ourNotifications.filter(n => {
- if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) {
- n.browser = otherBrowser;
- n.owner = other;
- return true;
- }
- this._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
- return false;
- });
- this._setNotificationsForBrowser(otherBrowser, ourNotifications);
- other._setNotificationsForBrowser(ourBrowser, otherNotifications);
- if (otherNotifications.length > 0)
- this._update(otherNotifications, otherNotifications[0].anchorElement);
- if (ourNotifications.length > 0)
- other._update(ourNotifications, ourNotifications[0].anchorElement);
- },
- _fireCallback: function(n, event, ...args) {
- try {
- if (n.options.eventCallback)
- return n.options.eventCallback.call(n, event, ...args);
- } catch (error) {
- Cu.reportError(error);
- }
- return undefined;
- },
- _onPopupHidden: function(event) {
- if (event.target != this.panel || this._ignoreDismissal)
- return;
- let browser = this.panel.firstChild &&
- this.panel.firstChild.notification.browser;
- if (!browser)
- return;
- let notifications = this._getNotificationsForBrowser(browser);
- // Mark notifications as dismissed and call dismissal callbacks
- Array.forEach(this.panel.childNodes, function(nEl) {
- let notificationObj = nEl.notification;
- // Never call a dismissal handler on a notification that's been removed.
- if (notifications.indexOf(notificationObj) == -1)
- return;
- // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
- // if the notification is removed.
- if (notificationObj.options.removeOnDismissal)
- this._remove(notificationObj);
- else {
- notificationObj.dismissed = true;
- this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
- }
- }, this);
- this._clearPanel();
- this._update();
- },
- _onButtonCommand: function(event) {
- let notificationEl = getNotificationFromElement(event.originalTarget);
- if (!notificationEl)
- throw "PopupNotifications_onButtonCommand: couldn't find notification element";
- if (!notificationEl.notification)
- throw "PopupNotifications_onButtonCommand: couldn't find notification";
- let notification = notificationEl.notification;
- let timeSinceShown = this.window.performance.now() - notification.timeShown;
- // Only report the first time mainAction is triggered and remember that this occurred.
- if (!notification.timeMainActionFirstTriggered) {
- notification.timeMainActionFirstTriggered = timeSinceShown;
- }
- if (timeSinceShown < this.buttonDelay) {
- Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
- "Button click happened before the security delay: " +
- timeSinceShown + "ms");
- return;
- }
- try {
- notification.mainAction.callback.call(undefined, {
- checkboxChecked: notificationEl.checkbox.checked
- });
- } catch (error) {
- Cu.reportError(error);
- }
- this._remove(notification);
- this._update();
- },
- _onMenuCommand: function(event) {
- let target = event.originalTarget;
- if (!target.action || !target.notification)
- throw "menucommand target has no associated action/notification";
- let notificationEl = target.parentElement;
- event.stopPropagation();
-
- try {
- target.action.callback.call(undefined, {
- checkboxChecked: notificationEl.checkbox.checked
- });
- } catch (error) {
- Cu.reportError(error);
- }
- this._remove(target.notification);
- this._update();
- },
- _notify: function(topic) {
- Services.obs.notifyObservers(null, "PopupNotifications-" + topic, "");
- },
- };
|