fune/browser/base/content/upgradeDialog.js

314 lines
11 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 {
AddonManager,
AppConstants,
document: gDoc,
getShellService,
Services,
} = window.docShell.chromeEventHandler.ownerGlobal;
// Number of height pixels to switch to compact mode to avoid showing scrollbars
// on the non-compact mode. This is based on the natural height of the full-size
// "accented" content while also accounting for the dialog container margins.
const COMPACT_MODE_HEIGHT = 649;
const SHELL = getShellService();
const IS_DEFAULT = SHELL.isDefaultBrowser();
const NEED_PIN = SHELL.doesAppNeedPin();
// Strings for various elements with matching ids on each screen.
const SCREEN_STRINGS = [
NEED_PIN.then(pin =>
pin
? {
title: "upgrade-dialog-pin-title",
subtitle: "upgrade-dialog-pin-subtitle",
primary: "upgrade-dialog-pin-primary-button",
secondary: "upgrade-dialog-pin-secondary-button",
}
: {
title: "upgrade-dialog-new-title",
subtitle: "upgrade-dialog-new-subtitle",
primary: IS_DEFAULT
? "upgrade-dialog-new-primary-theme-button"
: "upgrade-dialog-new-primary-default-button",
secondary: "upgrade-dialog-new-secondary-button",
}
),
{
title: "upgrade-dialog-default-title-2",
subtitle: "upgrade-dialog-default-subtitle-2",
primary: "upgrade-dialog-default-primary-button-2",
secondary: "upgrade-dialog-default-secondary-button",
},
{
title: "upgrade-dialog-theme-title-2",
primary: "upgrade-dialog-theme-primary-button",
secondary: "upgrade-dialog-theme-secondary-button",
},
];
// Themes that can be selected by the button with matching index.
const THEME_IDS = [
"default-theme@mozilla.org",
"firefox-compact-light@mozilla.org",
"firefox-compact-dark@mozilla.org",
"firefox-alpenglow@mozilla.org",
];
// Callbacks to run when the dialog closes (both from this file or externally).
const CLEANUP = [];
addEventListener("pagehide", () => CLEANUP.forEach(f => f()), { once: true });
// Save the previous theme to revert to it.
let gPrevTheme = AddonManager.getAddonsByTypes(["theme"]).then(addons => {
for (const { id, isActive, screenshots } of addons) {
if (isActive) {
// Only show the "keep" theme option for "other" themes.
if (!THEME_IDS.includes(id)) {
THEME_IDS.push(id);
}
// Assume we need to revert the theme unless cleared.
CLEANUP.push(() => gPrevTheme && enableTheme(id));
return {
id,
swatch: screenshots?.[0].url,
};
}
}
// If there were no active themes, the default will be selected.
return { id: THEME_IDS[0] };
});
// Helper to switch themes.
async function enableTheme(id) {
(await AddonManager.getAddonByID(id)).enable();
}
// Helper to show the theme in chrome with an adjusted modal backdrop.
function adjustModalBackdrop() {
const { classList } = gDoc.getElementById("window-modal-dialog");
classList.add("showToolbar");
CLEANUP.push(() => classList.remove("showToolbar"));
}
// Helper to record various events from the dialog content.
function recordEvent(obj, val) {
Services.telemetry.recordEvent("upgrade_dialog", "content", obj, `${val}`);
}
// Assume the dialog closes from an external trigger unless this helper is used.
let gCloseReason = "external";
CLEANUP.push(() => recordEvent("close", gCloseReason));
function closeDialog(reason) {
gCloseReason = reason;
close();
}
// Detect quit requests to proactively dismiss to allow the quit prompt to show
// as otherwise gDialogBox queues the prompt as these share the same display.
const QUIT_TOPIC = "quit-application-requested";
const QUIT_OBSERVER = () => closeDialog(QUIT_TOPIC);
Services.obs.addObserver(QUIT_OBSERVER, QUIT_TOPIC);
CLEANUP.push(() => Services.obs.removeObserver(QUIT_OBSERVER, QUIT_TOPIC));
// Hook up dynamic behaviors of the dialog.
function onLoad(ready) {
// Change content for Windows 7 because non-light themes aren't quite right.
const win7Content = AppConstants.isPlatformAndVersionAtMost("win", "6.1");
const { body } = document;
const title = document.getElementById("title");
const subtitle = document.getElementById("subtitle");
const items = document.querySelector(".items");
const themes = document.querySelector(".themes");
const primary = document.getElementById("primary");
const secondary = document.getElementById("secondary");
const steps = document.querySelector(".steps");
// Update the screen content and handle actions.
let current = -1;
(async function advance({ target } = {}) {
// Record which button was clicked.
if (target) {
recordEvent("button", target.dataset.l10nId);
}
// Move to the next screen and perform screen-specific behavior.
const strings = await SCREEN_STRINGS[++current];
// Set the correct target for keyboard focus.
let toFocus = primary;
switch (current) {
// Handle initial / first screen setup.
case 0:
// Wait for main button clicks on each screen.
primary.addEventListener("click", advance);
secondary.addEventListener("click", advance);
// Check parent window's height to determine if we should be compact.
if (gDoc.ownerGlobal.outerHeight < COMPACT_MODE_HEIGHT) {
body.classList.add("compact");
recordEvent("show", "compact");
}
// Windows 7 has a single screen so hide steps.
if (win7Content) {
steps.style.visibility = "hidden";
if (IS_DEFAULT) {
strings.primary = "upgrade-dialog-new-primary-win7-button";
secondary.style.display = "none";
}
}
// Avoid distracting the user with items for "pin" content.
let removeDefaultScreen = true;
if (await NEED_PIN) {
items.remove();
removeDefaultScreen = IS_DEFAULT;
}
// Keep the second screen only if we need to both pin and set default.
if (removeDefaultScreen) {
SCREEN_STRINGS.splice(1, 1);
}
// NB: We keep the screen count for win7 at 2, so actions are handled in
// "default" case. Send telemetry to be able to identify what users see.
recordEvent("show", `${SCREEN_STRINGS.length}-screens`);
// Copy the initial step indicator enough times for each screen.
for (let i = SCREEN_STRINGS.length; i > 1; i--) {
steps.append(steps.lastChild.cloneNode(true));
}
steps.lastChild.classList.add("current");
break;
// Handle actions and setup for not-first and not-last screens.
default:
const { l10nId } = primary.dataset;
if (target === primary) {
switch (l10nId) {
case "upgrade-dialog-new-primary-default-button":
case "upgrade-dialog-default-primary-button-2":
SHELL.setAsDefault();
break;
case "upgrade-dialog-pin-primary-button":
SHELL.pinToTaskbar();
break;
}
} else if (
target === secondary &&
l10nId === "upgrade-dialog-new-primary-theme-button"
) {
closeDialog("early");
return;
}
// First screen is the only screen for Windows 7.
if (win7Content) {
closeDialog("win7");
return;
}
// Prepare theme screen content only when we're moving to the last one.
if (current !== SCREEN_STRINGS.length - 1) {
break;
}
// Prepare the initial theme selection and wait for theme button clicks.
const { id, swatch } = await gPrevTheme;
toFocus = themes.children[THEME_IDS.indexOf(id)];
toFocus.checked = true;
themes.addEventListener("click", ({ target: button }) => {
// Ignore clicks on whitespace of the container around theme buttons.
if (button.parentNode === themes) {
// Enable the theme of the corresponding button position.
const index = [...themes.children].indexOf(button);
enableTheme(THEME_IDS[index]);
recordEvent("theme", index);
}
});
// Remove the last "keep" theme option if the user didn't customize.
if (themes.childElementCount > THEME_IDS.length) {
themes.removeChild(themes.lastElementChild);
} else if (swatch) {
themes.lastElementChild.style.setProperty(
"--theme-swatch",
`url("${swatch}")`
);
}
// Load resource: theme swatches with permission.
[...themes.children].forEach(input => {
new Image().src = getComputedStyle(
input,
"::before"
).backgroundImage.match(/resource:[^"]+/)?.[0];
});
// Update content and backdrop for theme screen.
subtitle.remove();
items.remove();
themes.classList.remove("hidden");
adjustModalBackdrop();
break;
// Handle the last (theme) screen actions.
case SCREEN_STRINGS.length:
// New theme is confirmed, so don't revert to previous.
if (target === primary) {
gPrevTheme = null;
}
closeDialog("complete");
return;
}
// Update various elements reused across screens.
steps.prepend(steps.lastChild);
// Update strings for reused elements that change between screens.
await document.l10n.ready;
const translatedElements = [];
for (let el of [title, subtitle, primary, secondary]) {
const stringId = strings[el.id];
if (stringId) {
document.l10n.setAttributes(el, stringId);
translatedElements.push(el);
}
}
// Wait for initial translations to load before getting sizing information.
await document.l10n.translateElements(translatedElements);
requestAnimationFrame(() => {
// Ensure the correct button is focused on each screen.
toFocus.focus({ preventFocusRing: true });
// Save first screen height, so later screens can flex and anchor content.
if (current === 0) {
body.style.minHeight = getComputedStyle(body).height;
// Indicate to SubDialog that we're done sizing the first screen.
ready();
}
// Let testing know the screen is ready to continue.
dispatchEvent(new CustomEvent("ready"));
});
// Record which screen was shown identified by the primary button.
recordEvent("show", primary.dataset.l10nId);
})();
}
document.mozSubdialogReady = new Promise(resolve =>
document.addEventListener("DOMContentLoaded", () => onLoad(resolve), {
once: true,
})
);