forked from mirrors/gecko-dev
		
	MozReview-Commit-ID: 9KJazfZqviT --HG-- extra : rebase_source : cd50d2889f4a18a6ae20170433851a243b484ee9
		
			
				
	
	
		
			1310 lines
		
	
	
	
		
			48 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1310 lines
		
	
	
	
		
			48 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/. */
 | 
						||
 | 
						||
/* eslint-env mozilla/frame-script */
 | 
						||
 | 
						||
"use strict";
 | 
						||
 | 
						||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 | 
						||
Cu.import("resource://gre/modules/Services.jsm");
 | 
						||
 | 
						||
const ONBOARDING_CSS_URL = "resource://onboarding/onboarding.css";
 | 
						||
const ABOUT_HOME_URL = "about:home";
 | 
						||
const ABOUT_NEWTAB_URL = "about:newtab";
 | 
						||
const BUNDLE_URI = "chrome://onboarding/locale/onboarding.properties";
 | 
						||
const UITOUR_JS_URI = "resource://onboarding/lib/UITour-lib.js";
 | 
						||
const TOUR_AGENT_JS_URI = "resource://onboarding/onboarding-tour-agent.js";
 | 
						||
const BRAND_SHORT_NAME = Services.strings
 | 
						||
                     .createBundle("chrome://branding/locale/brand.properties")
 | 
						||
                     .GetStringFromName("brandShortName");
 | 
						||
const PROMPT_COUNT_PREF = "browser.onboarding.notification.prompt-count";
 | 
						||
const ONBOARDING_DIALOG_ID = "onboarding-overlay-dialog";
 | 
						||
const ONBOARDING_MIN_WIDTH_PX = 960;
 | 
						||
const SPEECH_BUBBLE_MIN_WIDTH_PX = 1130;
 | 
						||
const SPEECH_BUBBLE_NEWTOUR_STRING_ID = "onboarding.overlay-icon-tooltip2";
 | 
						||
const SPEECH_BUBBLE_UPDATETOUR_STRING_ID = "onboarding.overlay-icon-tooltip-updated2";
 | 
						||
const ICON_STATE_WATERMARK = "watermark";
 | 
						||
const ICON_STATE_DEFAULT = "default";
 | 
						||
/**
 | 
						||
 * Add any number of tours, key is the tourId, value should follow the format below
 | 
						||
 * "tourId": { // The short tour id which could be saved in pref
 | 
						||
 *   // The unique tour id
 | 
						||
 *   id: "onboarding-tour-addons",
 | 
						||
 *   // (optional) mark tour as complete instantly when the user enters the tour
 | 
						||
 *   instantComplete: false,
 | 
						||
 *   // The string id of tour name which would be displayed on the navigation bar
 | 
						||
 *   tourNameId: "onboarding.tour-addon",
 | 
						||
 *   // The method returing strings used on tour notification
 | 
						||
 *   getNotificationStrings(bundle):
 | 
						||
 *     - title: // The string of tour notification title
 | 
						||
 *     - message: // The string of tour notification message
 | 
						||
 *     - button: // The string of tour notification action button title
 | 
						||
 *   // Return a div appended with elements for this tours.
 | 
						||
 *   // Each tour should contain the following 3 sections in the div:
 | 
						||
 *   // .onboarding-tour-description, .onboarding-tour-content, .onboarding-tour-button-container.
 | 
						||
 *   // Add onboarding-no-button css class in the div if this tour does not need a button container.
 | 
						||
 *   // If there was a .onboarding-tour-action-button present and was clicked, tour would be marked as completed.
 | 
						||
 *   getPage() {},
 | 
						||
 * },
 | 
						||
 **/
 | 
						||
var onboardingTourset = {
 | 
						||
  "private": {
 | 
						||
    id: "onboarding-tour-private-browsing",
 | 
						||
    tourNameId: "onboarding.tour-private-browsing",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-private-browsing.title"),
 | 
						||
        message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-private-browsing.message2"),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win) {
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-private-browsing.title2"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-private-browsing.description3"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <img src="resource://onboarding/img/figure_private.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
        <aside class="onboarding-tour-button-container">
 | 
						||
          <button id="onboarding-tour-private-browsing-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-private-browsing.button"></button>
 | 
						||
        </aside>
 | 
						||
      `;
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
  "addons": {
 | 
						||
    id: "onboarding-tour-addons",
 | 
						||
    tourNameId: "onboarding.tour-addons",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-addons.title"),
 | 
						||
        message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-addons.message", [BRAND_SHORT_NAME], 1),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win) {
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-addons.title2"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-addons.description2"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <img src="resource://onboarding/img/figure_addons.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
        <aside class="onboarding-tour-button-container">
 | 
						||
          <button id="onboarding-tour-addons-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-addons.button"></button>
 | 
						||
        </aside>
 | 
						||
      `;
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
  "customize": {
 | 
						||
    id: "onboarding-tour-customize",
 | 
						||
    tourNameId: "onboarding.tour-customize",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-customize.title"),
 | 
						||
        message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-customize.message", [BRAND_SHORT_NAME], 1),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win) {
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-customize.title2"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-customize.description2"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <img src="resource://onboarding/img/figure_customize.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
        <aside class="onboarding-tour-button-container">
 | 
						||
          <button id="onboarding-tour-customize-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-customize.button"></button>
 | 
						||
        </aside>
 | 
						||
      `;
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
  "default": {
 | 
						||
    id: "onboarding-tour-default-browser",
 | 
						||
    instantComplete: true,
 | 
						||
    tourNameId: "onboarding.tour-default-browser",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.formatStringFromName("onboarding.notification.onboarding-tour-default-browser.title", [BRAND_SHORT_NAME], 1),
 | 
						||
        message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-default-browser.message", [BRAND_SHORT_NAME], 1),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win, bundle) {
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      let setFromBackGround = bundle.formatStringFromName("onboarding.tour-default-browser.win7.button", [BRAND_SHORT_NAME], 1);
 | 
						||
      let setFromPanel = bundle.GetStringFromName("onboarding.tour-default-browser.button");
 | 
						||
      let isDefaultMessage = bundle.GetStringFromName("onboarding.tour-default-browser.is-default.message");
 | 
						||
      let isDefault2ndMessage = bundle.formatStringFromName("onboarding.tour-default-browser.is-default.2nd-message", [BRAND_SHORT_NAME], 1);
 | 
						||
      // eslint-disable-next-line no-unsanitized/property
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-default-browser.title2"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-default-browser.description2"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <img src="resource://onboarding/img/figure_default.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
        <aside class="onboarding-tour-button-container">
 | 
						||
          <button id="onboarding-tour-default-browser-button" class="onboarding-tour-action-button"
 | 
						||
            data-bg="${setFromBackGround}" data-panel="${setFromPanel}"></button>
 | 
						||
          <div id="onboarding-tour-is-default-browser-msg" class="onboarding-hidden">${isDefaultMessage}<br/>${isDefault2ndMessage}</div>
 | 
						||
        </aside>
 | 
						||
      `;
 | 
						||
 | 
						||
      div.addEventListener("beforeshow", () => {
 | 
						||
        win.document.dispatchEvent(new Event("Agent:CanSetDefaultBrowserInBackground"));
 | 
						||
      });
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
  "sync": {
 | 
						||
    id: "onboarding-tour-sync",
 | 
						||
    instantComplete: true,
 | 
						||
    tourNameId: "onboarding.tour-sync2",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-sync.title"),
 | 
						||
        message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-sync.message"),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win, bundle) {
 | 
						||
      const STATE_LOGOUT = "logged-out";
 | 
						||
      const STATE_LOGIN = "logged-in";
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      div.classList.add("onboarding-no-button");
 | 
						||
      div.dataset.loginState = STATE_LOGOUT;
 | 
						||
      // The email validation pattern used in the form comes from IETF rfc5321,
 | 
						||
      // which is identical to server-side checker of Firefox Account. See
 | 
						||
      // discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1378770#c6
 | 
						||
      // for detail.
 | 
						||
      let emailRegex = "^[\\w.!#$%&’*+\\/=?^`{|}~-]{1,64}@[a-z\\d](?:[a-z\\d-]{0,253}[a-z\\d])?(?:\\.[a-z\\d](?:[a-z\\d-]{0,253}[a-z\\d])?)+$";
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-sync.title2" class="show-on-logged-out"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-sync.description2" class="show-on-logged-out"></p>
 | 
						||
          <h1 data-l10n-id="onboarding.tour-sync.logged-in.title" class="show-on-logged-in"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-sync.logged-in.description" class="show-on-logged-in"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <form class="show-on-logged-out">
 | 
						||
            <h3 data-l10n-id="onboarding.tour-sync.form.title"></h3>
 | 
						||
            <p data-l10n-id="onboarding.tour-sync.form.description"></p>
 | 
						||
            <input id="onboarding-tour-sync-email-input" type="email" required="true"></input><br />
 | 
						||
            <button id="onboarding-tour-sync-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-sync.button"></button>
 | 
						||
          </form>
 | 
						||
          <img src="resource://onboarding/img/figure_sync.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
      `;
 | 
						||
      let emailInput = div.querySelector("#onboarding-tour-sync-email-input");
 | 
						||
      emailInput.placeholder =
 | 
						||
        bundle.GetStringFromName("onboarding.tour-sync.email-input.placeholder");
 | 
						||
      emailInput.pattern = emailRegex;
 | 
						||
 | 
						||
      div.addEventListener("beforeshow", () => {
 | 
						||
        function loginStatusListener(msg) {
 | 
						||
          removeMessageListener("Onboarding:ResponseLoginStatus", loginStatusListener);
 | 
						||
          div.dataset.loginState = msg.data.isLoggedIn ? STATE_LOGIN : STATE_LOGOUT;
 | 
						||
        }
 | 
						||
        sendMessageToChrome("get-login-status");
 | 
						||
        addMessageListener("Onboarding:ResponseLoginStatus", loginStatusListener);
 | 
						||
      });
 | 
						||
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
  "library": {
 | 
						||
    id: "onboarding-tour-library",
 | 
						||
    tourNameId: "onboarding.tour-library",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-library.title"),
 | 
						||
        message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-library.message", [BRAND_SHORT_NAME], 1),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win) {
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-library.title"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-library.description2"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <img src="resource://onboarding/img/figure_library.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
        <aside class="onboarding-tour-button-container">
 | 
						||
          <button id="onboarding-tour-library-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-library.button2"></button>
 | 
						||
        </aside>
 | 
						||
      `;
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
  "singlesearch": {
 | 
						||
    id: "onboarding-tour-singlesearch",
 | 
						||
    tourNameId: "onboarding.tour-singlesearch",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-singlesearch.title"),
 | 
						||
        message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-singlesearch.message"),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win, bundle) {
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-singlesearch.title"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-singlesearch.description"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <img src="resource://onboarding/img/figure_singlesearch.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
        <aside class="onboarding-tour-button-container">
 | 
						||
          <button id="onboarding-tour-singlesearch-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-singlesearch.button"></button>
 | 
						||
        </aside>
 | 
						||
      `;
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
  "performance": {
 | 
						||
    id: "onboarding-tour-performance",
 | 
						||
    instantComplete: true,
 | 
						||
    tourNameId: "onboarding.tour-performance",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-performance.title"),
 | 
						||
        message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-performance.message", [BRAND_SHORT_NAME], 1),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win, bundle) {
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-performance.title"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-performance.description"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <img src="resource://onboarding/img/figure_performance.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
      `;
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
  "screenshots": {
 | 
						||
    id: "onboarding-tour-screenshots",
 | 
						||
    tourNameId: "onboarding.tour-screenshots",
 | 
						||
    getNotificationStrings(bundle) {
 | 
						||
      return {
 | 
						||
        title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-screenshots.title"),
 | 
						||
        message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-screenshots.message", [BRAND_SHORT_NAME], 1),
 | 
						||
        button: bundle.GetStringFromName("onboarding.button.learnMore"),
 | 
						||
      };
 | 
						||
    },
 | 
						||
    getPage(win, bundle) {
 | 
						||
      let div = win.document.createElement("div");
 | 
						||
      // Screenshot tour opens the screenshot page directly, see below a#onboarding-tour-screenshots-button.
 | 
						||
      // The screenshots page should be responsible for highlighting the Screenshots button
 | 
						||
      div.innerHTML = `
 | 
						||
        <section class="onboarding-tour-description">
 | 
						||
          <h1 data-l10n-id="onboarding.tour-screenshots.title"></h1>
 | 
						||
          <p data-l10n-id="onboarding.tour-screenshots.description"></p>
 | 
						||
        </section>
 | 
						||
        <section class="onboarding-tour-content">
 | 
						||
          <img src="resource://onboarding/img/figure_screenshots.svg" role="presentation"/>
 | 
						||
        </section>
 | 
						||
        <aside class="onboarding-tour-button-container">
 | 
						||
          <a id="onboarding-tour-screenshots-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-screenshots.button"
 | 
						||
             href="https://screenshots.firefox.com/#tour" target="_blank"></a>
 | 
						||
        </aside>
 | 
						||
      `;
 | 
						||
      return div;
 | 
						||
    },
 | 
						||
  },
 | 
						||
};
 | 
						||
 | 
						||
/**
 | 
						||
 * @param {String} action the action to ask the chrome to do
 | 
						||
 * @param {Array | Object} params the parameters for the action
 | 
						||
 */
 | 
						||
function sendMessageToChrome(action, params) {
 | 
						||
  sendAsyncMessage("Onboarding:OnContentMessage", {
 | 
						||
    action, params
 | 
						||
  });
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * Template code for talking to `PingCentre`
 | 
						||
 * @param {Object} data the payload for the telemetry
 | 
						||
 */
 | 
						||
function telemetry(data) {
 | 
						||
   sendMessageToChrome("ping-centre", {data});
 | 
						||
}
 | 
						||
 | 
						||
function registerNewTelemetrySession(data) {
 | 
						||
  telemetry(Object.assign(data, {
 | 
						||
    event: "onboarding-register-session",
 | 
						||
  }));
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * The script won't be initialized if we turned off onboarding by
 | 
						||
 * setting "browser.onboarding.enabled" to false.
 | 
						||
 */
 | 
						||
class Onboarding {
 | 
						||
  constructor(contentWindow) {
 | 
						||
    this.init(contentWindow);
 | 
						||
  }
 | 
						||
 | 
						||
  async init(contentWindow) {
 | 
						||
    this._window = contentWindow;
 | 
						||
    // session_key is used for telemetry to track the current tab.
 | 
						||
    // The number will renew after reloading the page.
 | 
						||
    this._session_key = Date.now();
 | 
						||
    this._tours = [];
 | 
						||
    this._tourType = Services.prefs.getStringPref("browser.onboarding.tour-type", "update");
 | 
						||
 | 
						||
    let tourIds = this._getTourIDList();
 | 
						||
    tourIds.forEach(tourId => {
 | 
						||
      if (onboardingTourset[tourId]) {
 | 
						||
        this._tours.push(onboardingTourset[tourId]);
 | 
						||
      }
 | 
						||
    });
 | 
						||
 | 
						||
    if (this._tours.length === 0) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    // We want to create and append elements after CSS is loaded so
 | 
						||
    // no flash of style changes and no additional reflow.
 | 
						||
    await this._loadCSS();
 | 
						||
    this._bundle = Services.strings.createBundle(BUNDLE_URI);
 | 
						||
 | 
						||
    this._loadJS(UITOUR_JS_URI);
 | 
						||
 | 
						||
    this._window.addEventListener("resize", this);
 | 
						||
 | 
						||
    // Destroy on unloading. This is to ensure we remove all the stuff we left.
 | 
						||
    // No any leak out there.
 | 
						||
    this._window.addEventListener("unload", () => this.destroy());
 | 
						||
 | 
						||
    this.uiInitialized = false;
 | 
						||
    this._resizeTimerId =
 | 
						||
      this._window.requestIdleCallback(() => this._resizeUI());
 | 
						||
    registerNewTelemetrySession({
 | 
						||
      page: this._window.location.href,
 | 
						||
      session_key: this._session_key,
 | 
						||
      tour_type: this._tourType,
 | 
						||
    });
 | 
						||
    telemetry({
 | 
						||
      event: "onboarding-session-begin",
 | 
						||
      session_key: this._session_key,
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  _resizeUI() {
 | 
						||
    let width = this._window.document.body.getBoundingClientRect().width;
 | 
						||
    if (width < ONBOARDING_MIN_WIDTH_PX) {
 | 
						||
      // Don't show the overlay UI before we get to a better, responsive design.
 | 
						||
      this.destroy();
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    this._initUI();
 | 
						||
    if (this._isFirstSession && width >= SPEECH_BUBBLE_MIN_WIDTH_PX) {
 | 
						||
      this._overlayIcon.classList.add("onboarding-speech-bubble");
 | 
						||
    } else {
 | 
						||
      this._overlayIcon.classList.remove("onboarding-speech-bubble");
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  _initUI() {
 | 
						||
    if (this.uiInitialized) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    this.uiInitialized = true;
 | 
						||
    this._tourItems = [];
 | 
						||
    this._tourPages = [];
 | 
						||
 | 
						||
    let { body } = this._window.document;
 | 
						||
    this._overlayIcon = this._renderOverlayButton();
 | 
						||
    this._overlayIcon.addEventListener("click", this);
 | 
						||
    this._overlayIcon.addEventListener("keypress", this);
 | 
						||
    body.insertBefore(this._overlayIcon, body.firstChild);
 | 
						||
 | 
						||
    this._overlay = this._renderOverlay();
 | 
						||
    this._overlay.addEventListener("click", this);
 | 
						||
    this._overlay.addEventListener("keypress", this);
 | 
						||
    body.appendChild(this._overlay);
 | 
						||
 | 
						||
    this._loadJS(TOUR_AGENT_JS_URI);
 | 
						||
 | 
						||
    this._initPrefObserver();
 | 
						||
    this._onIconStateChange(Services.prefs.getStringPref("browser.onboarding.state", ICON_STATE_DEFAULT));
 | 
						||
 | 
						||
    // Doing tour notification takes some effort. Let's do it on idle.
 | 
						||
    this._window.requestIdleCallback(() => this._initNotification());
 | 
						||
  }
 | 
						||
 | 
						||
  _getTourIDList() {
 | 
						||
    let tours = Services.prefs.getStringPref(`browser.onboarding.${this._tourType}tour`, "");
 | 
						||
    return tours.split(",").filter(tourId => tourId !== "").map(tourId => tourId.trim());
 | 
						||
  }
 | 
						||
 | 
						||
  _initNotification() {
 | 
						||
    let doc = this._window.document;
 | 
						||
    if (doc.hidden) {
 | 
						||
      // When the preloaded-browser feature is on,
 | 
						||
      // it would preload a hidden about:newtab in the background.
 | 
						||
      // We don't want to show notification in that hidden state.
 | 
						||
      let onVisible = () => {
 | 
						||
        if (!doc.hidden) {
 | 
						||
          doc.removeEventListener("visibilitychange", onVisible);
 | 
						||
          this.showNotification();
 | 
						||
        }
 | 
						||
      };
 | 
						||
      doc.addEventListener("visibilitychange", onVisible);
 | 
						||
    } else {
 | 
						||
      this.showNotification();
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  _initPrefObserver() {
 | 
						||
    if (this._prefsObserved) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    this._prefsObserved = new Map();
 | 
						||
    this._prefsObserved.set("browser.onboarding.state", () => {
 | 
						||
      this._onIconStateChange(Services.prefs.getStringPref("browser.onboarding.state", ICON_STATE_DEFAULT));
 | 
						||
    });
 | 
						||
    this._tours.forEach(tour => {
 | 
						||
      let tourId = tour.id;
 | 
						||
      this._prefsObserved.set(`browser.onboarding.tour.${tourId}.completed`, () => {
 | 
						||
        this.markTourCompletionState(tourId);
 | 
						||
        this._checkWatermarkByTours();
 | 
						||
      });
 | 
						||
    });
 | 
						||
    for (let [name, callback] of this._prefsObserved) {
 | 
						||
      Services.prefs.addObserver(name, callback);
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  _checkWatermarkByTours() {
 | 
						||
    let tourDone = this._tours.every(tour => this.isTourCompleted(tour.id));
 | 
						||
    if (tourDone) {
 | 
						||
      sendMessageToChrome("set-prefs", [{
 | 
						||
        name: "browser.onboarding.state",
 | 
						||
        value: ICON_STATE_WATERMARK
 | 
						||
      }]);
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  _clearPrefObserver() {
 | 
						||
    if (this._prefsObserved) {
 | 
						||
      for (let [name, callback] of this._prefsObserved) {
 | 
						||
        Services.prefs.removeObserver(name, callback);
 | 
						||
      }
 | 
						||
      this._prefsObserved = null;
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Find a tour that should be selected. It is either a first tour that was not
 | 
						||
   * yet complete or the first one in the tab list.
 | 
						||
   */
 | 
						||
  get _firstUncompleteTour() {
 | 
						||
    return this._tours.find(tour => !this.isTourCompleted(tour.id)) ||
 | 
						||
           this._tours[0];
 | 
						||
  }
 | 
						||
 | 
						||
  handleClick(target) {
 | 
						||
    let { id, classList } = target;
 | 
						||
    // Only containers receive pointer events in onboarding tour tab list,
 | 
						||
    // actual semantic tab is their first child.
 | 
						||
    if (classList.contains("onboarding-tour-item-container")) {
 | 
						||
      ({ id, classList } = target.firstChild);
 | 
						||
    }
 | 
						||
 | 
						||
    switch (id) {
 | 
						||
      case "onboarding-overlay-button":
 | 
						||
        this.showOverlay();
 | 
						||
        this.gotoPage(this._firstUncompleteTour.id);
 | 
						||
        break;
 | 
						||
      case "onboarding-skip-tour-button":
 | 
						||
        this.hideNotification();
 | 
						||
        this.hideOverlay();
 | 
						||
        this.skipTour();
 | 
						||
        break;
 | 
						||
      case "onboarding-overlay-close-btn":
 | 
						||
      // If the clicking target is directly on the outer-most overlay,
 | 
						||
      // that means clicking outside the tour content area.
 | 
						||
      // Let's toggle the overlay.
 | 
						||
      case "onboarding-overlay":
 | 
						||
        this.hideOverlay();
 | 
						||
        break;
 | 
						||
      case "onboarding-notification-close-btn":
 | 
						||
        let tour_id = this._notificationBar.dataset.targetTourId;
 | 
						||
        this.hideNotification();
 | 
						||
        this._removeTourFromNotificationQueue(tour_id);
 | 
						||
        telemetry({
 | 
						||
          event: "notification-close-button-click",
 | 
						||
          tour_id,
 | 
						||
          session_key: this._session_key,
 | 
						||
        });
 | 
						||
        break;
 | 
						||
      case "onboarding-notification-action-btn":
 | 
						||
        let tourId = this._notificationBar.dataset.targetTourId;
 | 
						||
        this.showOverlay();
 | 
						||
        this.gotoPage(tourId);
 | 
						||
        telemetry({
 | 
						||
          event: "notification-cta-click",
 | 
						||
          tour_id: tourId,
 | 
						||
          session_key: this._session_key,
 | 
						||
        });
 | 
						||
        this._removeTourFromNotificationQueue(tourId);
 | 
						||
        break;
 | 
						||
    }
 | 
						||
    if (classList.contains("onboarding-tour-item")) {
 | 
						||
      this.gotoPage(id);
 | 
						||
      // Keep focus (not visible) on current item for potential keyboard
 | 
						||
      // navigation.
 | 
						||
      target.focus();
 | 
						||
    } else if (classList.contains("onboarding-tour-action-button")) {
 | 
						||
      let activeItem = this._tourItems.find(item => item.classList.contains("onboarding-active"));
 | 
						||
      this.setToursCompleted([ activeItem.id ]);
 | 
						||
      telemetry({
 | 
						||
        event: "overlay-cta-click",
 | 
						||
        tour_id: activeItem.id,
 | 
						||
        session_key: this._session_key,
 | 
						||
      });
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Wrap keyboard focus within the dialog.
 | 
						||
   * When moving forward, focus on the first element when the current focused
 | 
						||
   * element is the last one.
 | 
						||
   * When moving backward, focus on the last element when the current focused
 | 
						||
   * element is the first one.
 | 
						||
   * Do nothing if focus is moving in the middle of the list of dialog's focusable
 | 
						||
   * elements.
 | 
						||
   *
 | 
						||
   * @param  {DOMNode} current  currently focused element
 | 
						||
   * @param  {Boolean} back     direction
 | 
						||
   * @return {DOMNode}          newly focused element if any
 | 
						||
   */
 | 
						||
  wrapMoveFocus(current, back) {
 | 
						||
    let elms = [...this._dialog.querySelectorAll(
 | 
						||
      `button, input[type="checkbox"], input[type="email"], [tabindex="0"]`)];
 | 
						||
    let next;
 | 
						||
    if (back) {
 | 
						||
      if (elms.indexOf(current) === 0) {
 | 
						||
        next = elms[elms.length - 1];
 | 
						||
        next.focus();
 | 
						||
      }
 | 
						||
    } else if (elms.indexOf(current) === elms.length - 1) {
 | 
						||
      next = elms[0];
 | 
						||
      next.focus();
 | 
						||
    }
 | 
						||
    return next;
 | 
						||
  }
 | 
						||
 | 
						||
  handleKeypress(event) {
 | 
						||
    let { target, key, shiftKey } = event;
 | 
						||
 | 
						||
    if (target === this._overlayIcon) {
 | 
						||
      if ([" ", "Enter"].includes(key)) {
 | 
						||
        // Remember that the dialog was opened with a keyboard.
 | 
						||
        this._overlayIcon.dataset.keyboardFocus = true;
 | 
						||
        this.handleClick(target);
 | 
						||
        event.preventDefault();
 | 
						||
      }
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    // Currently focused item could be tab container if previous navigation was done
 | 
						||
    // via mouse.
 | 
						||
    if (target.classList.contains("onboarding-tour-item-container")) {
 | 
						||
      target = target.firstChild;
 | 
						||
    }
 | 
						||
    let targetIndex;
 | 
						||
    switch (key) {
 | 
						||
      case " ":
 | 
						||
      case "Enter":
 | 
						||
        // Assume that the handle function should be identical for keyboard
 | 
						||
        // activation if there is a click handler for the target.
 | 
						||
        if (target.classList.contains("onboarding-tour-item")) {
 | 
						||
          this.handleClick(target);
 | 
						||
          target.focus();
 | 
						||
        }
 | 
						||
        break;
 | 
						||
      case "ArrowUp":
 | 
						||
        // Go to and focus on the previous tab if it's available.
 | 
						||
        targetIndex = this._tourItems.indexOf(target);
 | 
						||
        if (targetIndex > 0) {
 | 
						||
          let previous = this._tourItems[targetIndex - 1];
 | 
						||
          this.handleClick(previous);
 | 
						||
          previous.focus();
 | 
						||
        }
 | 
						||
        event.preventDefault();
 | 
						||
        break;
 | 
						||
      case "ArrowDown":
 | 
						||
        // Go to and focus on the next tab if it's available.
 | 
						||
        targetIndex = this._tourItems.indexOf(target);
 | 
						||
        if (targetIndex > -1 && targetIndex < this._tourItems.length - 1) {
 | 
						||
          let next = this._tourItems[targetIndex + 1];
 | 
						||
          this.handleClick(next);
 | 
						||
          next.focus();
 | 
						||
        }
 | 
						||
        event.preventDefault();
 | 
						||
        break;
 | 
						||
      case "Escape":
 | 
						||
        this.hideOverlay();
 | 
						||
        break;
 | 
						||
      case "Tab":
 | 
						||
        let next = this.wrapMoveFocus(target, shiftKey);
 | 
						||
        // If focus was wrapped, prevent Tab key default action.
 | 
						||
        if (next) {
 | 
						||
          event.preventDefault();
 | 
						||
        }
 | 
						||
        break;
 | 
						||
      default:
 | 
						||
        break;
 | 
						||
    }
 | 
						||
    event.stopPropagation();
 | 
						||
  }
 | 
						||
 | 
						||
  handleEvent(evt) {
 | 
						||
    switch (evt.type) {
 | 
						||
      case "resize":
 | 
						||
        this._window.cancelIdleCallback(this._resizeTimerId);
 | 
						||
        this._resizeTimerId =
 | 
						||
          this._window.requestIdleCallback(() => this._resizeUI());
 | 
						||
        break;
 | 
						||
      case "keypress":
 | 
						||
        this.handleKeypress(evt);
 | 
						||
        break;
 | 
						||
      case "click":
 | 
						||
        this.handleClick(evt.target);
 | 
						||
        break;
 | 
						||
      default:
 | 
						||
        break;
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  destroy() {
 | 
						||
    if (!this.uiInitialized) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    this.uiInitialized = false;
 | 
						||
 | 
						||
    this._overlayIcon.dispatchEvent(new this._window.CustomEvent("Agent:Destroy"));
 | 
						||
 | 
						||
    this._clearPrefObserver();
 | 
						||
    this._overlayIcon.remove();
 | 
						||
    this._overlay.remove();
 | 
						||
    if (this._notificationBar) {
 | 
						||
      this._notificationBar.remove();
 | 
						||
    }
 | 
						||
    this._tourItems = this._tourPages =
 | 
						||
    this._overlayIcon = this._overlay = this._notificationBar = null;
 | 
						||
    telemetry({
 | 
						||
      event: "onboarding-session-end",
 | 
						||
      session_key: this._session_key,
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  _onIconStateChange(state) {
 | 
						||
    switch (state) {
 | 
						||
      case ICON_STATE_DEFAULT:
 | 
						||
        this._overlayIcon.classList.remove("onboarding-watermark");
 | 
						||
        break;
 | 
						||
      case ICON_STATE_WATERMARK:
 | 
						||
        this._overlayIcon.classList.add("onboarding-watermark");
 | 
						||
        break;
 | 
						||
    }
 | 
						||
    return true;
 | 
						||
  }
 | 
						||
 | 
						||
  showOverlay() {
 | 
						||
    if (this._tourItems.length == 0) {
 | 
						||
      // Lazy loading until first toggle.
 | 
						||
      this._loadTours(this._tours);
 | 
						||
    }
 | 
						||
 | 
						||
    this.hideNotification();
 | 
						||
    this.toggleModal(this._overlay.classList.toggle("onboarding-opened"));
 | 
						||
    telemetry({
 | 
						||
      event: "overlay-session-begin",
 | 
						||
      session_key: this._session_key
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  hideOverlay() {
 | 
						||
    this.toggleModal(this._overlay.classList.toggle("onboarding-opened"));
 | 
						||
    telemetry({
 | 
						||
      event: "overlay-session-end",
 | 
						||
      session_key: this._session_key,
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Set modal dialog state and properties for accessibility purposes.
 | 
						||
   * @param  {Boolean} opened  whether the dialog is opened or closed.
 | 
						||
   */
 | 
						||
  toggleModal(opened) {
 | 
						||
    let { document: doc } = this._window;
 | 
						||
    if (opened) {
 | 
						||
      // Set aria-hidden to true for the rest of the document.
 | 
						||
      [...doc.body.children].forEach(
 | 
						||
        child => child.id !== "onboarding-overlay" &&
 | 
						||
                 child.setAttribute("aria-hidden", true));
 | 
						||
      // When dialog is opened with the keyboard, focus on the first
 | 
						||
      // uncomplete tour because it will be the selected tour.
 | 
						||
      if (this._overlayIcon.dataset.keyboardFocus) {
 | 
						||
        doc.getElementById(this._firstUncompleteTour.id).focus();
 | 
						||
      } else {
 | 
						||
        // When the dialog is opened with the mouse, focus on the dialog
 | 
						||
        // itself to avoid visible keyboard focus styling.
 | 
						||
        this._dialog.focus();
 | 
						||
      }
 | 
						||
    } else {
 | 
						||
      // Remove all set aria-hidden attributes.
 | 
						||
      [...doc.body.children].forEach(
 | 
						||
        child => child.removeAttribute("aria-hidden"));
 | 
						||
      // If dialog was opened with a keyboard, set the focus back to the overlay
 | 
						||
      // button.
 | 
						||
      if (this._overlayIcon.dataset.keyboardFocus) {
 | 
						||
        delete this._overlayIcon.dataset.keyboardFocus;
 | 
						||
        this._overlayIcon.focus();
 | 
						||
      } else {
 | 
						||
        this._window.document.activeElement.blur();
 | 
						||
      }
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  gotoPage(tourId) {
 | 
						||
    let targetPageId = `${tourId}-page`;
 | 
						||
    for (let page of this._tourPages) {
 | 
						||
      if (page.id === targetPageId) {
 | 
						||
        page.style.display = "";
 | 
						||
        page.dispatchEvent(new this._window.CustomEvent("beforeshow"));
 | 
						||
      } else {
 | 
						||
        page.style.display = "none";
 | 
						||
      }
 | 
						||
    }
 | 
						||
    for (let tab of this._tourItems) {
 | 
						||
      if (tab.id == tourId) {
 | 
						||
        tab.classList.add("onboarding-active");
 | 
						||
        tab.setAttribute("aria-selected", true);
 | 
						||
        telemetry({
 | 
						||
          event: "overlay-nav-click",
 | 
						||
          tour_id: tourId,
 | 
						||
          session_key: this._session_key,
 | 
						||
        });
 | 
						||
 | 
						||
        // Some tours should complete instantly upon showing.
 | 
						||
        if (tab.getAttribute("data-instant-complete")) {
 | 
						||
          this.setToursCompleted([tourId]);
 | 
						||
        }
 | 
						||
      } else {
 | 
						||
        tab.classList.remove("onboarding-active");
 | 
						||
        tab.setAttribute("aria-selected", false);
 | 
						||
      }
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  isTourCompleted(tourId) {
 | 
						||
    return Services.prefs.getBoolPref(`browser.onboarding.tour.${tourId}.completed`, false);
 | 
						||
  }
 | 
						||
 | 
						||
  setToursCompleted(tourIds) {
 | 
						||
    let params = [];
 | 
						||
    tourIds.forEach(id => {
 | 
						||
      if (!this.isTourCompleted(id)) {
 | 
						||
        params.push({
 | 
						||
          name: `browser.onboarding.tour.${id}.completed`,
 | 
						||
          value: true
 | 
						||
        });
 | 
						||
      }
 | 
						||
    });
 | 
						||
    if (params.length > 0) {
 | 
						||
      sendMessageToChrome("set-prefs", params);
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  markTourCompletionState(tourId) {
 | 
						||
    // We are doing lazy load so there might be no items.
 | 
						||
    if (!this._tourItems || this._tourItems.length === 0) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    let completed = this.isTourCompleted(tourId);
 | 
						||
    let targetItem = this._tourItems.find(item => item.id == tourId);
 | 
						||
    let completedTextId = `onboarding-complete-${tourId}-text`;
 | 
						||
    // Accessibility: Text version of the auxiliary information about the tour
 | 
						||
    // item completion is provided via an invisible node with an aria-label that
 | 
						||
    // the tab is pointing to via aria-described by.
 | 
						||
    let completedText = targetItem.querySelector(`#${completedTextId}`);
 | 
						||
    if (completed) {
 | 
						||
      targetItem.classList.add("onboarding-complete");
 | 
						||
      if (!completedText) {
 | 
						||
        completedText = this._window.document.createElement("span");
 | 
						||
        completedText.id = completedTextId;
 | 
						||
        completedText.setAttribute("aria-label",
 | 
						||
          this._bundle.GetStringFromName("onboarding.complete"));
 | 
						||
        targetItem.appendChild(completedText);
 | 
						||
        targetItem.setAttribute("aria-describedby", completedTextId);
 | 
						||
      }
 | 
						||
    } else {
 | 
						||
      targetItem.classList.remove("onboarding-complete");
 | 
						||
      targetItem.removeAttribute("aria-describedby");
 | 
						||
      if (completedText) {
 | 
						||
        completedText.remove();
 | 
						||
      }
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  get _isFirstSession() {
 | 
						||
    // Should only directly return on the "false" case. Consider:
 | 
						||
    //   1. On the 1st session, `_firstSession` is true
 | 
						||
    //   2. During the 1st session, user resizes window so that the UI is destroyed
 | 
						||
    //   3. After the 1st mute session, user resizes window so that the UI is re-init
 | 
						||
    if (this._firstSession === false) {
 | 
						||
      return false;
 | 
						||
    }
 | 
						||
    this._firstSession = true;
 | 
						||
 | 
						||
    // There is a queue, which means we had prompted tour notifications before. Therefore this is not the 1st session.
 | 
						||
    if (Services.prefs.prefHasUserValue("browser.onboarding.notification.tour-ids-queue")) {
 | 
						||
      this._firstSession = false;
 | 
						||
    }
 | 
						||
 | 
						||
    // When this is set to 0 on purpose, always judge as not the 1st session
 | 
						||
    if (Services.prefs.getIntPref("browser.onboarding.notification.mute-duration-on-first-session-ms") === 0) {
 | 
						||
      this._firstSession = false;
 | 
						||
    }
 | 
						||
 | 
						||
    return this._firstSession;
 | 
						||
  }
 | 
						||
 | 
						||
  _getLastTourChangeTime() {
 | 
						||
    return 1000 * Services.prefs.getIntPref("browser.onboarding.notification.last-time-of-changing-tour-sec", 0);
 | 
						||
  }
 | 
						||
 | 
						||
  _muteNotificationOnFirstSession(lastTourChangeTime) {
 | 
						||
    if (!this._isFirstSession) {
 | 
						||
      return false;
 | 
						||
    }
 | 
						||
 | 
						||
    if (lastTourChangeTime <= 0) {
 | 
						||
      sendMessageToChrome("set-prefs", [{
 | 
						||
        name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
 | 
						||
        value: Math.floor(Date.now() / 1000)
 | 
						||
      }]);
 | 
						||
      return true;
 | 
						||
    }
 | 
						||
    let muteDuration = Services.prefs.getIntPref("browser.onboarding.notification.mute-duration-on-first-session-ms");
 | 
						||
    return Date.now() - lastTourChangeTime <= muteDuration;
 | 
						||
  }
 | 
						||
 | 
						||
  _isTimeForNextTourNotification(lastTourChangeTime) {
 | 
						||
    let promptCount = Services.prefs.getIntPref("browser.onboarding.notification.prompt-count", 0);
 | 
						||
    let maxCount = Services.prefs.getIntPref("browser.onboarding.notification.max-prompt-count-per-tour");
 | 
						||
    if (promptCount >= maxCount) {
 | 
						||
      return true;
 | 
						||
    }
 | 
						||
 | 
						||
    let maxTime = Services.prefs.getIntPref("browser.onboarding.notification.max-life-time-per-tour-ms");
 | 
						||
    if (lastTourChangeTime && Date.now() - lastTourChangeTime >= maxTime) {
 | 
						||
      return true;
 | 
						||
    }
 | 
						||
 | 
						||
    return false;
 | 
						||
  }
 | 
						||
 | 
						||
  _removeTourFromNotificationQueue(tourId) {
 | 
						||
    let params = [];
 | 
						||
    let queue = this._getNotificationQueue();
 | 
						||
    params.push({
 | 
						||
      name: "browser.onboarding.notification.tour-ids-queue",
 | 
						||
      value: queue.filter(id => id != tourId).join(",")
 | 
						||
    });
 | 
						||
    params.push({
 | 
						||
      name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
 | 
						||
      value: 0
 | 
						||
    });
 | 
						||
    params.push({
 | 
						||
      name: "browser.onboarding.notification.prompt-count",
 | 
						||
      value: 0
 | 
						||
    });
 | 
						||
    sendMessageToChrome("set-prefs", params);
 | 
						||
  }
 | 
						||
 | 
						||
  _getNotificationQueue() {
 | 
						||
    let queue = "";
 | 
						||
    if (Services.prefs.prefHasUserValue("browser.onboarding.notification.tour-ids-queue")) {
 | 
						||
      queue = Services.prefs.getStringPref("browser.onboarding.notification.tour-ids-queue");
 | 
						||
    } else {
 | 
						||
      // For each tour, it only gets 2 chances to prompt with notification
 | 
						||
      // (each chance includes 8 impressions or 5-days max life time)
 | 
						||
      // if user never interact with it.
 | 
						||
      // Assume there are tour #0 ~ #5. Here would form the queue as
 | 
						||
      // "#0,#1,#2,#3,#4,#5,#0,#1,#2,#3,#4,#5".
 | 
						||
      // Then we would loop through this queue and remove prompted tour from the queue
 | 
						||
      // until the queue is empty.
 | 
						||
      let ids = this._tours.map(tour => tour.id).join(",");
 | 
						||
      queue = `${ids},${ids}`;
 | 
						||
      sendMessageToChrome("set-prefs", [{
 | 
						||
        name: "browser.onboarding.notification.tour-ids-queue",
 | 
						||
        value: queue
 | 
						||
      }]);
 | 
						||
    }
 | 
						||
    return queue ? queue.split(",") : [];
 | 
						||
  }
 | 
						||
 | 
						||
  showNotification() {
 | 
						||
    if (Services.prefs.getBoolPref("browser.onboarding.notification.finished", false)) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    let lastTime = this._getLastTourChangeTime();
 | 
						||
    if (this._muteNotificationOnFirstSession(lastTime)) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    // After the notification mute on the 1st session,
 | 
						||
    // we don't want to show the speech bubble by default
 | 
						||
    this._overlayIcon.classList.remove("onboarding-speech-bubble");
 | 
						||
 | 
						||
    let queue = this._getNotificationQueue();
 | 
						||
    let totalMaxTime = Services.prefs.getIntPref("browser.onboarding.notification.max-life-time-all-tours-ms");
 | 
						||
    if (lastTime && Date.now() - lastTime >= totalMaxTime) {
 | 
						||
      // Reach total max life time for all tour notifications.
 | 
						||
      // Clear the queue so that we would finish tour notifications below
 | 
						||
      queue = [];
 | 
						||
    }
 | 
						||
 | 
						||
    let startQueueLength = queue.length;
 | 
						||
    // See if need to move on to the next tour
 | 
						||
    if (queue.length > 0 && this._isTimeForNextTourNotification(lastTime)) {
 | 
						||
      queue.shift();
 | 
						||
    }
 | 
						||
    // We don't want to prompt the completed tour.
 | 
						||
    while (queue.length > 0 && this.isTourCompleted(queue[0])) {
 | 
						||
      queue.shift();
 | 
						||
    }
 | 
						||
 | 
						||
    if (queue.length == 0) {
 | 
						||
      sendMessageToChrome("set-prefs", [
 | 
						||
        {
 | 
						||
          name: "browser.onboarding.notification.finished",
 | 
						||
          value: true
 | 
						||
        },
 | 
						||
        {
 | 
						||
          name: "browser.onboarding.notification.tour-ids-queue",
 | 
						||
          value: ""
 | 
						||
        },
 | 
						||
        {
 | 
						||
          name: "browser.onboarding.state",
 | 
						||
          value: ICON_STATE_WATERMARK
 | 
						||
        }
 | 
						||
      ]);
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    let targetTourId = queue[0];
 | 
						||
    let targetTour = this._tours.find(tour => tour.id == targetTourId);
 | 
						||
 | 
						||
    // Show the target tour notification
 | 
						||
    this._notificationBar = this._renderNotificationBar();
 | 
						||
    this._notificationBar.addEventListener("click", this);
 | 
						||
    this._notificationBar.dataset.targetTourId = targetTour.id;
 | 
						||
    let notificationStrings = targetTour.getNotificationStrings(this._bundle);
 | 
						||
    let actionBtn = this._notificationBar.querySelector("#onboarding-notification-action-btn");
 | 
						||
    actionBtn.textContent = notificationStrings.button;
 | 
						||
    let tourTitle = this._notificationBar.querySelector("#onboarding-notification-tour-title");
 | 
						||
    tourTitle.textContent = notificationStrings.title;
 | 
						||
    let tourMessage = this._notificationBar.querySelector("#onboarding-notification-tour-message");
 | 
						||
    tourMessage.textContent = notificationStrings.message;
 | 
						||
    this._notificationBar.classList.add("onboarding-opened");
 | 
						||
    this._window.document.body.appendChild(this._notificationBar);
 | 
						||
 | 
						||
    let params = [];
 | 
						||
    if (startQueueLength != queue.length) {
 | 
						||
      // We just change tour so update the time, the count and the queue
 | 
						||
      params.push({
 | 
						||
        name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
 | 
						||
        value: Math.floor(Date.now() / 1000)
 | 
						||
      });
 | 
						||
      params.push({
 | 
						||
        name: PROMPT_COUNT_PREF,
 | 
						||
        value: 1
 | 
						||
      });
 | 
						||
      params.push({
 | 
						||
        name: "browser.onboarding.notification.tour-ids-queue",
 | 
						||
        value: queue.join(",")
 | 
						||
      });
 | 
						||
    } else {
 | 
						||
      let promptCount = Services.prefs.getIntPref(PROMPT_COUNT_PREF, 0);
 | 
						||
      params.push({
 | 
						||
        name: PROMPT_COUNT_PREF,
 | 
						||
        value: promptCount + 1
 | 
						||
      });
 | 
						||
    }
 | 
						||
    sendMessageToChrome("set-prefs", params);
 | 
						||
    telemetry({
 | 
						||
      event: "notification-session-begin",
 | 
						||
      session_key: this._session_key
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  hideNotification() {
 | 
						||
    if (this._notificationBar) {
 | 
						||
      if (this._notificationBar.classList.contains("onboarding-opened")) {
 | 
						||
        this._notificationBar.classList.remove("onboarding-opened");
 | 
						||
        telemetry({
 | 
						||
          event: "notification-session-end",
 | 
						||
          tour_id: this._notificationBar.dataset.targetTourId,
 | 
						||
          session_key: this._session_key,
 | 
						||
        });
 | 
						||
      }
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  _renderNotificationBar() {
 | 
						||
    let footer = this._window.document.createElement("footer");
 | 
						||
    footer.id = "onboarding-notification-bar";
 | 
						||
    footer.setAttribute("aria-live", "polite");
 | 
						||
    footer.setAttribute("aria-labelledby", "onboarding-notification-tour-title");
 | 
						||
    // We use `innerHTML` for more friendly reading.
 | 
						||
    // The security should be fine because this is not from an external input.
 | 
						||
    footer.innerHTML = `
 | 
						||
      <section id="onboarding-notification-message-section" role="presentation">
 | 
						||
        <div id="onboarding-notification-tour-icon" role="presentation"></div>
 | 
						||
        <div id="onboarding-notification-body" role="presentation">
 | 
						||
          <h1 id="onboarding-notification-tour-title"></h1>
 | 
						||
          <p id="onboarding-notification-tour-message"></p>
 | 
						||
        </div>
 | 
						||
        <button id="onboarding-notification-action-btn" class="onboarding-action-button"></button>
 | 
						||
      </section>
 | 
						||
      <button id="onboarding-notification-close-btn" class="onboarding-close-btn"></button>
 | 
						||
    `;
 | 
						||
 | 
						||
    let closeBtn = footer.querySelector("#onboarding-notification-close-btn");
 | 
						||
    closeBtn.setAttribute("title",
 | 
						||
      this._bundle.GetStringFromName("onboarding.notification-close-button-tooltip"));
 | 
						||
    return footer;
 | 
						||
  }
 | 
						||
 | 
						||
  skipTour() {
 | 
						||
    this.setToursCompleted(this._tours.map(tour => tour.id));
 | 
						||
    sendMessageToChrome("set-prefs", [
 | 
						||
      {
 | 
						||
        name: "browser.onboarding.notification.finished",
 | 
						||
        value: true
 | 
						||
      },
 | 
						||
      {
 | 
						||
        name: "browser.onboarding.state",
 | 
						||
        value: ICON_STATE_WATERMARK
 | 
						||
      }
 | 
						||
    ]);
 | 
						||
    telemetry({
 | 
						||
      event: "overlay-skip-tour",
 | 
						||
      session_key: this._session_key
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  _renderOverlay() {
 | 
						||
    let div = this._window.document.createElement("div");
 | 
						||
    div.id = "onboarding-overlay";
 | 
						||
    // We use `innerHTML` for more friendly reading.
 | 
						||
    // The security should be fine because this is not from an external input.
 | 
						||
    div.innerHTML = `
 | 
						||
      <div role="dialog" tabindex="-1" aria-labelledby="onboarding-header">
 | 
						||
        <header id="onboarding-header"></header>
 | 
						||
        <nav>
 | 
						||
          <ul id="onboarding-tour-list" role="tablist"></ul>
 | 
						||
        </nav>
 | 
						||
        <footer id="onboarding-footer"></footer>
 | 
						||
        <button id="onboarding-overlay-close-btn" class="onboarding-close-btn"></button>
 | 
						||
      </div>
 | 
						||
    `;
 | 
						||
 | 
						||
    this._dialog = div.querySelector(`[role="dialog"]`);
 | 
						||
    this._dialog.id = ONBOARDING_DIALOG_ID;
 | 
						||
    div.querySelector("#onboarding-header").textContent =
 | 
						||
      this._bundle.GetStringFromName("onboarding.overlay-title2");
 | 
						||
    // support show/hide skip tour button via pref
 | 
						||
    if (!Services.prefs.getBoolPref("browser.onboarding.skip-tour-button.hide", false)) {
 | 
						||
      let footer = div.querySelector("#onboarding-footer");
 | 
						||
      let skipButton = this._window.document.createElement("button");
 | 
						||
      skipButton.id = "onboarding-skip-tour-button";
 | 
						||
      skipButton.classList.add("onboarding-action-button");
 | 
						||
      skipButton.textContent = this._bundle.GetStringFromName("onboarding.skip-tour-button-label");
 | 
						||
      footer.appendChild(skipButton);
 | 
						||
    }
 | 
						||
    let closeBtn = div.querySelector("#onboarding-overlay-close-btn");
 | 
						||
    closeBtn.setAttribute("title",
 | 
						||
      this._bundle.GetStringFromName("onboarding.overlay-close-button-tooltip"));
 | 
						||
    return div;
 | 
						||
  }
 | 
						||
 | 
						||
  _renderOverlayButton() {
 | 
						||
    let button = this._window.document.createElement("button");
 | 
						||
    // support customize speech bubble string via pref
 | 
						||
    let tooltipStringPrefId = "";
 | 
						||
    let defaultTourStringId = "";
 | 
						||
    if (this._tourType === "new") {
 | 
						||
      tooltipStringPrefId = "browser.onboarding.newtour.tooltip";
 | 
						||
      defaultTourStringId = SPEECH_BUBBLE_NEWTOUR_STRING_ID;
 | 
						||
    } else {
 | 
						||
      tooltipStringPrefId = "browser.onboarding.updatetour.tooltip";
 | 
						||
      defaultTourStringId = SPEECH_BUBBLE_UPDATETOUR_STRING_ID;
 | 
						||
    }
 | 
						||
    let tooltip = "";
 | 
						||
    try {
 | 
						||
      let tooltipStringId = Services.prefs.getStringPref(tooltipStringPrefId, defaultTourStringId);
 | 
						||
      tooltip = this._bundle.formatStringFromName(tooltipStringId, [BRAND_SHORT_NAME], 1);
 | 
						||
    } catch (e) {
 | 
						||
      Cu.reportError(`the provided ${tooltipStringPrefId} string is in wrong format `, e);
 | 
						||
      // fallback to defaultTourStringId to proceed
 | 
						||
      tooltip = this._bundle.formatStringFromName(defaultTourStringId, [BRAND_SHORT_NAME], 1);
 | 
						||
    }
 | 
						||
    button.setAttribute("aria-label", tooltip);
 | 
						||
    button.id = "onboarding-overlay-button";
 | 
						||
    button.setAttribute("aria-haspopup", true);
 | 
						||
    button.setAttribute("aria-controls", `${ONBOARDING_DIALOG_ID}`);
 | 
						||
    let defaultImg = this._window.document.createElement("img");
 | 
						||
    defaultImg.id = "onboarding-overlay-button-icon";
 | 
						||
    defaultImg.setAttribute("role", "presentation");
 | 
						||
    defaultImg.src = Services.prefs.getStringPref("browser.onboarding.default-icon-src",
 | 
						||
      "chrome://branding/content/icon64.png");
 | 
						||
    button.appendChild(defaultImg);
 | 
						||
    let watermarkImg = this._window.document.createElement("img");
 | 
						||
    watermarkImg.id = "onboarding-overlay-button-watermark-icon";
 | 
						||
    watermarkImg.setAttribute("role", "presentation");
 | 
						||
    watermarkImg.src = Services.prefs.getStringPref("browser.onboarding.watermark-icon-src",
 | 
						||
      "resource://onboarding/img/watermark.svg");
 | 
						||
    button.appendChild(watermarkImg);
 | 
						||
    return button;
 | 
						||
  }
 | 
						||
 | 
						||
  _loadTours(tours) {
 | 
						||
    let itemsFrag = this._window.document.createDocumentFragment();
 | 
						||
    let pagesFrag = this._window.document.createDocumentFragment();
 | 
						||
    for (let tour of tours) {
 | 
						||
      // Create tour navigation items dynamically
 | 
						||
      let li = this._window.document.createElement("li");
 | 
						||
      // List item should have no semantics. It is just a container for an
 | 
						||
      // actual tab.
 | 
						||
      li.setAttribute("role", "presentation");
 | 
						||
      li.className = "onboarding-tour-item-container";
 | 
						||
      // Focusable but not tabbable.
 | 
						||
      li.tabIndex = -1;
 | 
						||
 | 
						||
      let tab = this._window.document.createElement("span");
 | 
						||
      tab.id = tour.id;
 | 
						||
      tab.textContent = this._bundle.GetStringFromName(tour.tourNameId);
 | 
						||
      tab.className = "onboarding-tour-item";
 | 
						||
      if (tour.instantComplete) {
 | 
						||
        tab.dataset.instantComplete = true;
 | 
						||
      }
 | 
						||
      tab.tabIndex = 0;
 | 
						||
      tab.setAttribute("role", "tab");
 | 
						||
 | 
						||
      let tourPanelId = `${tour.id}-page`;
 | 
						||
      tab.setAttribute("aria-controls", tourPanelId);
 | 
						||
 | 
						||
      li.appendChild(tab);
 | 
						||
      itemsFrag.appendChild(li);
 | 
						||
      // Dynamically create tour pages
 | 
						||
      let div = tour.getPage(this._window, this._bundle);
 | 
						||
 | 
						||
      // Do a traverse for elements in the page that need to be localized.
 | 
						||
      let l10nElements = div.querySelectorAll("[data-l10n-id]");
 | 
						||
      for (let i = 0; i < l10nElements.length; i++) {
 | 
						||
        let element = l10nElements[i];
 | 
						||
        // We always put brand short name as the first argument for it's the
 | 
						||
        // only and frequently used arguments in our l10n case. Rewrite it if
 | 
						||
        // other arguments appear.
 | 
						||
        element.textContent = this._bundle.formatStringFromName(
 | 
						||
                                element.dataset.l10nId, [BRAND_SHORT_NAME], 1);
 | 
						||
      }
 | 
						||
 | 
						||
      div.id = tourPanelId;
 | 
						||
      div.classList.add("onboarding-tour-page");
 | 
						||
      div.setAttribute("role", "tabpanel");
 | 
						||
      div.setAttribute("aria-labelledby", tour.id);
 | 
						||
      div.style.display = "none";
 | 
						||
      pagesFrag.appendChild(div);
 | 
						||
      // Cache elements in arrays for later use to avoid cost of querying elements
 | 
						||
      this._tourItems.push(tab);
 | 
						||
      this._tourPages.push(div);
 | 
						||
 | 
						||
      this.markTourCompletionState(tour.id);
 | 
						||
    }
 | 
						||
 | 
						||
    let ul = this._window.document.getElementById("onboarding-tour-list");
 | 
						||
    ul.appendChild(itemsFrag);
 | 
						||
    let footer = this._window.document.getElementById("onboarding-footer");
 | 
						||
    this._dialog.insertBefore(pagesFrag, footer);
 | 
						||
  }
 | 
						||
 | 
						||
  _loadCSS() {
 | 
						||
    // Returning a Promise so we can inform caller of loading complete
 | 
						||
    // by resolving it.
 | 
						||
    return new Promise(resolve => {
 | 
						||
      let doc = this._window.document;
 | 
						||
      let link = doc.createElement("link");
 | 
						||
      link.rel = "stylesheet";
 | 
						||
      link.type = "text/css";
 | 
						||
      link.href = ONBOARDING_CSS_URL;
 | 
						||
      link.addEventListener("load", resolve);
 | 
						||
      doc.head.appendChild(link);
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  _loadJS(uri) {
 | 
						||
    let doc = this._window.document;
 | 
						||
    let script = doc.createElement("script");
 | 
						||
    script.type = "text/javascript";
 | 
						||
    script.src = uri;
 | 
						||
    doc.head.appendChild(script);
 | 
						||
  }
 | 
						||
}
 | 
						||
 | 
						||
// Load onboarding module only when we enable it.
 | 
						||
if (Services.prefs.getBoolPref("browser.onboarding.enabled", false)) {
 | 
						||
  addEventListener("load", function onLoad(evt) {
 | 
						||
    if (!content || evt.target != content.document) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    let window = evt.target.defaultView;
 | 
						||
    let location = window.location.href;
 | 
						||
    if (location == ABOUT_NEWTAB_URL || location == ABOUT_HOME_URL) {
 | 
						||
      // We just want to run tests as quickly as possible
 | 
						||
      // so in the automation test, we don't do `requestIdleCallback`.
 | 
						||
      if (Cu.isInAutomation) {
 | 
						||
        new Onboarding(window);
 | 
						||
        return;
 | 
						||
      }
 | 
						||
      window.requestIdleCallback(() => {
 | 
						||
        new Onboarding(window);
 | 
						||
      });
 | 
						||
    }
 | 
						||
  }, true);
 | 
						||
}
 |