|
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/. */ |
|
4 |
|
5 this.EXPORTED_SYMBOLS = ["PopupNotifications"]; |
|
6 |
|
7 var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils; |
|
8 |
|
9 Cu.import("resource://gre/modules/Services.jsm"); |
|
10 |
|
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"; |
|
16 |
|
17 const ICON_SELECTOR = ".notification-anchor-icon"; |
|
18 const ICON_ATTRIBUTE_SHOWING = "showing"; |
|
19 |
|
20 const PREF_SECURITY_DELAY = "security.notification_enable_delay"; |
|
21 |
|
22 let popupNotificationsMap = new WeakMap(); |
|
23 let gNotificationParents = new WeakMap; |
|
24 |
|
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 } |
|
36 |
|
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 } |
|
53 |
|
54 Notification.prototype = { |
|
55 |
|
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, |
|
65 |
|
66 /** |
|
67 * Removes the notification and updates the popup accordingly if needed. |
|
68 */ |
|
69 remove: function Notification_remove() { |
|
70 this.owner.remove(this); |
|
71 }, |
|
72 |
|
73 get anchorElement() { |
|
74 let iconBox = this.owner.iconBox; |
|
75 |
|
76 let anchorElement = getAnchorFromBrowser(this.browser); |
|
77 |
|
78 if (!iconBox) |
|
79 return anchorElement; |
|
80 |
|
81 if (!anchorElement && this.anchorID) |
|
82 anchorElement = iconBox.querySelector("#"+this.anchorID); |
|
83 |
|
84 // Use a default anchor icon if it's available |
|
85 if (!anchorElement) |
|
86 anchorElement = iconBox.querySelector("#default-notification-icon") || |
|
87 iconBox; |
|
88 |
|
89 return anchorElement; |
|
90 }, |
|
91 |
|
92 reshow: function() { |
|
93 this.owner._reshowNotifications(this.anchorElement, this.browser); |
|
94 } |
|
95 }; |
|
96 |
|
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"; |
|
121 |
|
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); |
|
127 |
|
128 this.panel.addEventListener("popuphidden", this, true); |
|
129 |
|
130 this.window.addEventListener("activate", this, true); |
|
131 if (this.tabbrowser.tabContainer) |
|
132 this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true); |
|
133 } |
|
134 |
|
135 PopupNotifications.prototype = { |
|
136 |
|
137 window: null, |
|
138 panel: null, |
|
139 tabbrowser: null, |
|
140 |
|
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 }, |
|
157 |
|
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 }, |
|
171 |
|
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 }, |
|
189 |
|
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 } |
|
290 |
|
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"; |
|
303 |
|
304 let notification = new Notification(id, message, anchorID, mainAction, |
|
305 secondaryActions, browser, this, options); |
|
306 |
|
307 if (options && options.dismissed) |
|
308 notification.dismissed = true; |
|
309 |
|
310 let existingNotification = this.getNotification(id, browser); |
|
311 if (existingNotification) |
|
312 this._remove(existingNotification); |
|
313 |
|
314 let notifications = this._getNotificationsForBrowser(browser); |
|
315 notifications.push(notification); |
|
316 |
|
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. |
|
325 |
|
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 } |
|
339 |
|
340 // Notify observers that we're not showing the popup (useful for testing) |
|
341 this._notify("backgroundShow"); |
|
342 } |
|
343 |
|
344 return notification; |
|
345 }, |
|
346 |
|
347 /** |
|
348 * Returns true if the notification popup is currently being displayed. |
|
349 */ |
|
350 get isPanelOpen() { |
|
351 let panelState = this.panel.state; |
|
352 |
|
353 return panelState == "showing" || panelState == "open"; |
|
354 }, |
|
355 |
|
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"; |
|
363 |
|
364 let notifications = this._getNotificationsForBrowser(aBrowser); |
|
365 |
|
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 } |
|
376 |
|
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 } |
|
384 |
|
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 } |
|
390 |
|
391 this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED); |
|
392 return false; |
|
393 }, this); |
|
394 |
|
395 this._setNotificationsForBrowser(aBrowser, notifications); |
|
396 |
|
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 }, |
|
407 |
|
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); |
|
415 |
|
416 if (this._isActiveBrowser(notification.browser)) { |
|
417 let notifications = this._getNotificationsForBrowser(notification.browser); |
|
418 this._update(notifications, notification.anchorElement); |
|
419 } |
|
420 }, |
|
421 |
|
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 }, |
|
442 |
|
443 //////////////////////////////////////////////////////////////////////////////// |
|
444 // Utility methods |
|
445 //////////////////////////////////////////////////////////////////////////////// |
|
446 |
|
447 _ignoreDismissal: null, |
|
448 _currentAnchorElement: null, |
|
449 |
|
450 /** |
|
451 * Gets notifications for the currently selected browser. |
|
452 */ |
|
453 get _currentNotifications() { |
|
454 return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : []; |
|
455 }, |
|
456 |
|
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; |
|
463 |
|
464 var index = notifications.indexOf(notification); |
|
465 if (index == -1) |
|
466 return; |
|
467 |
|
468 if (this._isActiveBrowser(notification.browser)) |
|
469 notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING); |
|
470 |
|
471 // remove the notification |
|
472 notifications.splice(index, 1); |
|
473 this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED); |
|
474 }, |
|
475 |
|
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 }, |
|
486 |
|
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 }, |
|
495 |
|
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); |
|
503 |
|
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; |
|
509 |
|
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 } |
|
520 |
|
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; |
|
524 |
|
525 originalParent.appendChild(popupnotification); |
|
526 } |
|
527 } |
|
528 }, |
|
529 |
|
530 _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) { |
|
531 this._clearPanel(); |
|
532 |
|
533 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
534 |
|
535 notificationsToShow.forEach(function (n) { |
|
536 let doc = this.window.document; |
|
537 |
|
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"; |
|
541 |
|
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"); |
|
549 |
|
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 } |
|
567 |
|
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"); |
|
574 |
|
575 popupnotification.notification = n; |
|
576 |
|
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; |
|
584 |
|
585 popupnotification.appendChild(item); |
|
586 }, this); |
|
587 |
|
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 } |
|
596 |
|
597 this.panel.appendChild(popupnotification); |
|
598 |
|
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 }, |
|
604 |
|
605 _showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) { |
|
606 this.panel.hidden = false; |
|
607 |
|
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; |
|
616 |
|
617 this._refreshPanel(notificationsToShow); |
|
618 |
|
619 if (this.isPanelOpen && this._currentAnchorElement == anchorElement) |
|
620 return; |
|
621 |
|
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(); |
|
626 |
|
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 } |
|
638 |
|
639 this._currentAnchorElement = anchorElement; |
|
640 |
|
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 }, |
|
653 |
|
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 } |
|
673 |
|
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; |
|
683 |
|
684 if (useIconBox) { |
|
685 this._showIcons(notifications); |
|
686 this.iconBox.hidden = false; |
|
687 } else if (anchorElement) { |
|
688 this._updateAnchorIcon(notifications, anchorElement); |
|
689 } |
|
690 |
|
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 } |
|
697 |
|
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"); |
|
703 |
|
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(); |
|
710 |
|
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 }, |
|
721 |
|
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 }, |
|
739 |
|
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 }, |
|
748 |
|
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 }, |
|
755 |
|
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 }, |
|
772 |
|
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 }, |
|
780 |
|
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; |
|
786 |
|
787 if (type == "keypress" && |
|
788 !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE || |
|
789 event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN)) |
|
790 return; |
|
791 |
|
792 if (this._currentNotifications.length == 0) |
|
793 return; |
|
794 |
|
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; |
|
799 |
|
800 this._reshowNotifications(anchor); |
|
801 }, |
|
802 |
|
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 }); |
|
810 |
|
811 // ...and then show them. |
|
812 this._update(notifications, anchor); |
|
813 }, |
|
814 |
|
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. |
|
818 |
|
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 } |
|
831 |
|
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 }); |
|
841 |
|
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 }); |
|
851 |
|
852 this._setNotificationsForBrowser(otherBrowser, ourNotifications); |
|
853 other._setNotificationsForBrowser(ourBrowser, otherNotifications); |
|
854 |
|
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 }, |
|
860 |
|
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 }, |
|
870 |
|
871 _onPopupHidden: function PopupNotifications_onPopupHidden(event) { |
|
872 if (event.target != this.panel || this._ignoreDismissal) |
|
873 return; |
|
874 |
|
875 let browser = this.panel.firstChild && |
|
876 this.panel.firstChild.notification.browser; |
|
877 if (!browser) |
|
878 return; |
|
879 |
|
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; |
|
887 |
|
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); |
|
897 |
|
898 this._clearPanel(); |
|
899 |
|
900 this._update(); |
|
901 }, |
|
902 |
|
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; |
|
913 |
|
914 if (!notificationEl) |
|
915 throw "PopupNotifications_onButtonCommand: couldn't find notification element"; |
|
916 |
|
917 if (!notificationEl.notification) |
|
918 throw "PopupNotifications_onButtonCommand: couldn't find notification"; |
|
919 |
|
920 let notification = notificationEl.notification; |
|
921 let timeSinceShown = this.window.performance.now() - notification.timeShown; |
|
922 |
|
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 } |
|
929 |
|
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 } |
|
941 |
|
942 if (notification.mainAction.dismiss) { |
|
943 this._dismiss(); |
|
944 return; |
|
945 } |
|
946 |
|
947 this._remove(notification); |
|
948 this._update(); |
|
949 }, |
|
950 |
|
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"; |
|
955 |
|
956 event.stopPropagation(); |
|
957 try { |
|
958 target.action.callback.call(); |
|
959 } catch(error) { |
|
960 Cu.reportError(error); |
|
961 } |
|
962 |
|
963 if (target.action.dismiss) { |
|
964 this._dismiss(); |
|
965 return; |
|
966 } |
|
967 |
|
968 this._remove(target.notification); |
|
969 this._update(); |
|
970 }, |
|
971 |
|
972 _notify: function PopupNotifications_notify(topic) { |
|
973 Services.obs.notifyObservers(null, "PopupNotifications-" + topic, ""); |
|
974 }, |
|
975 }; |