PopupNotifications.jsm 37 KB

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