forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			272 lines
		
	
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			272 lines
		
	
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
|   clearTimeout: "resource://gre/modules/Timer.sys.mjs",
 | |
|   requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
 | |
|   setTimeout: "resource://gre/modules/Timer.sys.mjs",
 | |
| });
 | |
| 
 | |
| let notificationsByWindow = new WeakMap();
 | |
| 
 | |
| export class _ToolbarBadgeHub {
 | |
|   constructor() {
 | |
|     this.id = "toolbar-badge-hub";
 | |
|     this.state = {};
 | |
|     this.removeAllNotifications = this.removeAllNotifications.bind(this);
 | |
|     this.removeToolbarNotification = this.removeToolbarNotification.bind(this);
 | |
|     this.addToolbarNotification = this.addToolbarNotification.bind(this);
 | |
|     this.registerBadgeToAllWindows = this.registerBadgeToAllWindows.bind(this);
 | |
|     this._sendPing = this._sendPing.bind(this);
 | |
|     this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this);
 | |
| 
 | |
|     this._handleMessageRequest = null;
 | |
|     this._addImpression = null;
 | |
|     this._blockMessageById = null;
 | |
|     this._sendTelemetry = null;
 | |
|     this._initialized = false;
 | |
|   }
 | |
| 
 | |
|   async init(
 | |
|     waitForInitialized,
 | |
|     {
 | |
|       handleMessageRequest,
 | |
|       addImpression,
 | |
|       blockMessageById,
 | |
|       unblockMessageById,
 | |
|       sendTelemetry,
 | |
|     }
 | |
|   ) {
 | |
|     if (this._initialized) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._initialized = true;
 | |
|     this._handleMessageRequest = handleMessageRequest;
 | |
|     this._blockMessageById = blockMessageById;
 | |
|     this._unblockMessageById = unblockMessageById;
 | |
|     this._addImpression = addImpression;
 | |
|     this._sendTelemetry = sendTelemetry;
 | |
|     // Need to wait for ASRouter to initialize before trying to fetch messages
 | |
|     await waitForInitialized;
 | |
|     this.messageRequest({
 | |
|       triggerId: "toolbarBadgeUpdate",
 | |
|       template: "toolbar_badge",
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   maybeInsertFTL(win) {
 | |
|     win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
 | |
|   }
 | |
| 
 | |
|   _clearBadgeTimeout() {
 | |
|     if (this.state.showBadgeTimeoutId) {
 | |
|       lazy.clearTimeout(this.state.showBadgeTimeoutId);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   removeAllNotifications(event) {
 | |
|     if (event) {
 | |
|       // ignore right clicks
 | |
|       if (
 | |
|         (event.type === "mousedown" || event.type === "click") &&
 | |
|         event.button !== 0
 | |
|       ) {
 | |
|         return;
 | |
|       }
 | |
|       // ignore keyboard access that is not one of the usual accessor keys
 | |
|       if (
 | |
|         event.type === "keypress" &&
 | |
|         event.key !== " " &&
 | |
|         event.key !== "Enter"
 | |
|       ) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       event.target.removeEventListener(
 | |
|         "mousedown",
 | |
|         this.removeAllNotifications
 | |
|       );
 | |
|       event.target.removeEventListener("keypress", this.removeAllNotifications);
 | |
|       // If we have an event it means the user interacted with the badge
 | |
|       // we should send telemetry
 | |
|       if (this.state.notification) {
 | |
|         this.sendUserEventTelemetry("CLICK", this.state.notification);
 | |
|       }
 | |
|     }
 | |
|     // Will call uninit on every window
 | |
|     lazy.EveryWindow.unregisterCallback(this.id);
 | |
|     if (this.state.notification) {
 | |
|       this._blockMessageById(this.state.notification.id);
 | |
|     }
 | |
|     this._clearBadgeTimeout();
 | |
|     this.state = {};
 | |
|   }
 | |
| 
 | |
|   removeToolbarNotification(toolbarButton) {
 | |
|     // Remove it from the element that displays the badge
 | |
|     toolbarButton
 | |
|       .querySelector(".toolbarbutton-badge")
 | |
|       .classList.remove("feature-callout");
 | |
|     toolbarButton.removeAttribute("badged");
 | |
|     // Remove id used for for aria-label badge description
 | |
|     const notificationDescription = toolbarButton.querySelector(
 | |
|       "#toolbarbutton-notification-description"
 | |
|     );
 | |
|     if (notificationDescription) {
 | |
|       notificationDescription.remove();
 | |
|       toolbarButton.removeAttribute("aria-labelledby");
 | |
|       toolbarButton.removeAttribute("aria-describedby");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   addToolbarNotification(win, message) {
 | |
|     const document = win.browser.ownerDocument;
 | |
|     let toolbarbutton = document.getElementById(message.content.target);
 | |
|     if (toolbarbutton) {
 | |
|       const badge = toolbarbutton.querySelector(".toolbarbutton-badge");
 | |
|       badge.classList.add("feature-callout");
 | |
|       toolbarbutton.setAttribute("badged", true);
 | |
|       // If we have additional aria-label information for the notification
 | |
|       // we add this content to the hidden `toolbarbutton-text` node.
 | |
|       // We then use `aria-labelledby` to link this description to the button
 | |
|       // that received the notification badge.
 | |
|       if (message.content.badgeDescription) {
 | |
|         // Insert strings as soon as we know we're showing them
 | |
|         this.maybeInsertFTL(win);
 | |
|         toolbarbutton.setAttribute(
 | |
|           "aria-labelledby",
 | |
|           `toolbarbutton-notification-description ${message.content.target}`
 | |
|         );
 | |
|         // Because tooltiptext is different to the label, it gets duplicated as
 | |
|         // the description. Setting `describedby` to the same value as
 | |
|         // `labelledby` will be detected by the a11y code and the description
 | |
|         // will be removed.
 | |
|         toolbarbutton.setAttribute(
 | |
|           "aria-describedby",
 | |
|           `toolbarbutton-notification-description ${message.content.target}`
 | |
|         );
 | |
|         const descriptionEl = document.createElement("span");
 | |
|         descriptionEl.setAttribute(
 | |
|           "id",
 | |
|           "toolbarbutton-notification-description"
 | |
|         );
 | |
|         descriptionEl.hidden = true;
 | |
|         document.l10n.setAttributes(
 | |
|           descriptionEl,
 | |
|           message.content.badgeDescription.string_id
 | |
|         );
 | |
|         toolbarbutton.appendChild(descriptionEl);
 | |
|       }
 | |
|       // `mousedown` event required because of the `onmousedown` defined on
 | |
|       // the button that prevents `click` events from firing
 | |
|       toolbarbutton.addEventListener("mousedown", this.removeAllNotifications);
 | |
|       // `keypress` event required for keyboard accessibility
 | |
|       toolbarbutton.addEventListener("keypress", this.removeAllNotifications);
 | |
|       this.state = { notification: { id: message.id } };
 | |
| 
 | |
|       // Impression should be added when the badge becomes visible
 | |
|       this._addImpression(message);
 | |
|       // Send a telemetry ping when adding the notification badge
 | |
|       this.sendUserEventTelemetry("IMPRESSION", message);
 | |
| 
 | |
|       return toolbarbutton;
 | |
|     }
 | |
| 
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   registerBadgeToAllWindows(message) {
 | |
|     lazy.EveryWindow.registerCallback(
 | |
|       this.id,
 | |
|       win => {
 | |
|         if (notificationsByWindow.has(win)) {
 | |
|           // nothing to do
 | |
|           return;
 | |
|         }
 | |
|         const el = this.addToolbarNotification(win, message);
 | |
|         notificationsByWindow.set(win, el);
 | |
|       },
 | |
|       win => {
 | |
|         const el = notificationsByWindow.get(win);
 | |
|         if (el) {
 | |
|           this.removeToolbarNotification(el);
 | |
|         }
 | |
|         notificationsByWindow.delete(win);
 | |
|       }
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   registerBadgeNotificationListener(message, options = {}) {
 | |
|     // We need to clear any existing notifications and only show
 | |
|     // the one set by devtools
 | |
|     if (options.force) {
 | |
|       this.removeAllNotifications();
 | |
|       // When debugging immediately show the badge
 | |
|       this.registerBadgeToAllWindows(message);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (message.content.delay) {
 | |
|       this.state.showBadgeTimeoutId = lazy.setTimeout(() => {
 | |
|         lazy.requestIdleCallback(() => this.registerBadgeToAllWindows(message));
 | |
|       }, message.content.delay);
 | |
|     } else {
 | |
|       this.registerBadgeToAllWindows(message);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async messageRequest({ triggerId, template }) {
 | |
|     const telemetryObject = { triggerId };
 | |
|     TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
 | |
|     const message = await this._handleMessageRequest({
 | |
|       triggerId,
 | |
|       template,
 | |
|     });
 | |
|     TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
 | |
|     if (message) {
 | |
|       this.registerBadgeNotificationListener(message);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _sendPing(ping) {
 | |
|     this._sendTelemetry({
 | |
|       type: "TOOLBAR_BADGE_TELEMETRY",
 | |
|       data: { action: "badge_user_event", ...ping },
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   sendUserEventTelemetry(event, message) {
 | |
|     const win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|     // Only send pings for non private browsing windows
 | |
|     if (
 | |
|       win &&
 | |
|       !lazy.PrivateBrowsingUtils.isBrowserPrivate(
 | |
|         win.ownerGlobal.gBrowser.selectedBrowser
 | |
|       )
 | |
|     ) {
 | |
|       this._sendPing({
 | |
|         message_id: message.id,
 | |
|         event,
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   uninit() {
 | |
|     this._clearBadgeTimeout();
 | |
|     this.state = {};
 | |
|     this._initialized = false;
 | |
|     notificationsByWindow = new WeakMap();
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate
 | |
|  * message requests and render messages.
 | |
|  */
 | |
| export const ToolbarBadgeHub = new _ToolbarBadgeHub();
 | 
