mirror of https://github.com/roytam1/UXP
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
843 lines
31 KiB
843 lines
31 KiB
/* 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; |
|
|
|
Components.utils.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 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; |
|
} |
|
|
|
/** |
|
* 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 Notification_remove() { |
|
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 PopupNotifications_getNotification(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. |
|
* 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) |
|
* "shown": notification has been shown (this can be fired |
|
* multiple times as notifications are dismissed |
|
* and re-shown) |
|
* 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). |
|
* 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 PopupNotifications_show(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 PopupNotifications_locationChange(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 PopupNotifications_remove(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 PopupNotifications_removeHelper(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 PopupNotifications_dismiss() { |
|
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 PopupNotifications_hide() { |
|
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 PopupNotifications_refreshPanel(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); |
|
} |
|
} |
|
|
|
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); |
|
}, |
|
|
|
_showPanel: function PopupNotifications_showPanel(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 PopupNotifications_update(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 PopupNotifications_showIcons(aCurrentNotifications) { |
|
for (let notification of aCurrentNotifications) { |
|
let anchorElm = notification.anchorElement; |
|
if (anchorElm) { |
|
anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true"); |
|
} |
|
} |
|
}, |
|
|
|
_hideIcons: function PopupNotifications_hideIcons() { |
|
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 PopupNotifications_getNotifications(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 PopupNotifications_setNotifications(browser, notifications) { |
|
popupNotificationsMap.set(browser, notifications); |
|
return notifications; |
|
}, |
|
|
|
_onIconBoxCommand: function PopupNotifications_onIconBoxCommand(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 PopupNotifications_reshowNotifications(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); |
|
}, |
|
|
|
_fireCallback: function PopupNotifications_fireCallback(n, event) { |
|
if (n.options.eventCallback) |
|
n.options.eventCallback.call(n, event); |
|
}, |
|
|
|
_onPopupHidden: function PopupNotifications_onPopupHidden(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 PopupNotifications_onButtonCommand(event) { |
|
// Need to find the associated notification object, which is a bit tricky |
|
// since it isn't associated with the button directly - this is kind of |
|
// gross and very dependent on the structure of the popupnotification |
|
// binding's content. |
|
let target = event.originalTarget; |
|
let notificationEl; |
|
let parent = target; |
|
while (parent && (parent = target.ownerDocument.getBindingParent(parent))) |
|
notificationEl = parent; |
|
|
|
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; |
|
} |
|
notification.mainAction.callback.call(); |
|
|
|
this._remove(notification); |
|
this._update(); |
|
}, |
|
|
|
_onMenuCommand: function PopupNotifications_onMenuCommand(event) { |
|
let target = event.originalTarget; |
|
if (!target.action || !target.notification) |
|
throw "menucommand target has no associated action/notification"; |
|
|
|
event.stopPropagation(); |
|
target.action.callback.call(); |
|
|
|
this._remove(target.notification); |
|
this._update(); |
|
}, |
|
|
|
_notify: function PopupNotifications_notify(topic) { |
|
Services.obs.notifyObservers(null, "PopupNotifications-" + topic, ""); |
|
}, |
|
};
|
|
|