forked from mirrors/gecko-dev
424 lines
14 KiB
JavaScript
424 lines
14 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,
|
|
document: gDoc,
|
|
Services,
|
|
XPCOMUtils,
|
|
} = window.docShell.chromeEventHandler.ownerGlobal;
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
BuiltInThemes: "resource:///modules/BuiltInThemes.jsm",
|
|
});
|
|
|
|
const HOMEPAGE_PREF = "browser.startup.homepage";
|
|
const NEWTAB_PREF = "browser.newtabpage.enabled";
|
|
|
|
// Strings for various elements with matching ids on each screen.
|
|
const SCREEN_STRINGS = [
|
|
{
|
|
title: "upgrade-dialog-start-title",
|
|
subtitle: "upgrade-dialog-start-subtitle",
|
|
primary: "upgrade-dialog-colorway-primary-button",
|
|
secondary: "upgrade-dialog-colorway-secondary-button",
|
|
},
|
|
{
|
|
title: "upgrade-dialog-thankyou-title",
|
|
subtitle: "upgrade-dialog-thankyou-subtitle",
|
|
primary: "upgrade-dialog-thankyou-primary-button",
|
|
},
|
|
];
|
|
|
|
// Themes that can be selected by the button with matching index.
|
|
const THEME_IDS = [
|
|
[
|
|
"firefox-compact-light@mozilla.org",
|
|
"default-theme@mozilla.org",
|
|
"firefox-compact-dark@mozilla.org",
|
|
],
|
|
[
|
|
"abstract-soft-colorway@mozilla.org",
|
|
"abstract-balanced-colorway@mozilla.org",
|
|
"abstract-bold-colorway@mozilla.org",
|
|
],
|
|
[
|
|
"cheers-soft-colorway@mozilla.org",
|
|
"cheers-balanced-colorway@mozilla.org",
|
|
"cheers-bold-colorway@mozilla.org",
|
|
],
|
|
[
|
|
"foto-soft-colorway@mozilla.org",
|
|
"foto-balanced-colorway@mozilla.org",
|
|
"foto-bold-colorway@mozilla.org",
|
|
],
|
|
[
|
|
"lush-soft-colorway@mozilla.org",
|
|
"lush-balanced-colorway@mozilla.org",
|
|
"lush-bold-colorway@mozilla.org",
|
|
],
|
|
[
|
|
"graffiti-soft-colorway@mozilla.org",
|
|
"graffiti-balanced-colorway@mozilla.org",
|
|
"graffiti-bold-colorway@mozilla.org",
|
|
],
|
|
[
|
|
"elemental-soft-colorway@mozilla.org",
|
|
"elemental-balanced-colorway@mozilla.org",
|
|
"elemental-bold-colorway@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 } of addons) {
|
|
if (isActive) {
|
|
// Assume we need to revert the theme unless cleared.
|
|
CLEANUP.push(() => gPrevTheme && enableTheme(id));
|
|
return { id };
|
|
}
|
|
}
|
|
|
|
// If there were no active themes, the default will be selected.
|
|
return { id: THEME_IDS[0][1] };
|
|
});
|
|
|
|
// Helper to switch themes.
|
|
async function enableTheme(id) {
|
|
await BuiltInThemes.ensureBuiltInThemes();
|
|
// The UI shows a fixed set of themes even when expired, so silently skip.
|
|
(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));
|
|
|
|
// Helper to trigger transitions with animation frames.
|
|
function triggerTransition(callback) {
|
|
requestAnimationFrame(() => requestAnimationFrame(callback));
|
|
}
|
|
|
|
// Hook up dynamic behaviors of the dialog.
|
|
function onLoad(ready) {
|
|
// Testing doesn't have time to overwrite this new window's random method.
|
|
if (Cu.isInAutomation) {
|
|
Math.random = () => 0;
|
|
}
|
|
|
|
const { body } = document;
|
|
const logo = document.querySelector(".logo");
|
|
const title = document.getElementById("title");
|
|
const subtitle = document.getElementById("subtitle");
|
|
const colorways = document.querySelector(".colorways");
|
|
const themes = document.querySelector(".themes");
|
|
const variations = document.querySelector(".variations");
|
|
const checkbox = document.querySelector(".checkbox");
|
|
const primary = document.getElementById("primary");
|
|
const secondary = document.getElementById("secondary");
|
|
|
|
// Show a new set of colorway variations based on the selected theme.
|
|
function showVariations(themeRadio) {
|
|
let l10nIds, themeName;
|
|
const { l10nArgs } = themeRadio.dataset;
|
|
if (l10nArgs) {
|
|
// Directly set the header with unlocalized colorway name.
|
|
const { colorwayName } = JSON.parse(l10nArgs);
|
|
variations.firstElementChild.textContent = colorwayName;
|
|
|
|
l10nIds = [
|
|
"upgrade-dialog-colorway-variation-soft",
|
|
"upgrade-dialog-colorway-variation-balanced",
|
|
"upgrade-dialog-colorway-variation-bold",
|
|
];
|
|
themeName = colorwayName.toLowerCase();
|
|
} else {
|
|
l10nIds = [
|
|
"upgrade-dialog-colorway-default-theme",
|
|
"upgrade-dialog-theme-light",
|
|
"upgrade-dialog-colorway-theme-auto",
|
|
"upgrade-dialog-theme-dark",
|
|
];
|
|
themeName = "default";
|
|
}
|
|
|
|
// Show the appropriate variation options and header text too.
|
|
l10nIds.reduceRight((node, l10nId) => {
|
|
// Clear the previous id as textContent might have changed.
|
|
node.dataset.l10nId = "";
|
|
document.l10n.setAttributes(node, l10nId);
|
|
return node.previousElementSibling;
|
|
}, variations.lastElementChild);
|
|
|
|
// Transition in the background image.
|
|
variations.classList = `variations ${themeName} in`;
|
|
triggerTransition(() => variations.classList.remove("in"));
|
|
|
|
// Let testing know the variations are set.
|
|
dispatchEvent(new CustomEvent("variations"));
|
|
}
|
|
|
|
// Watch for fluent-dom translating variation inputs to set aria-label. This
|
|
// is because using the textContent as the label for an <input type="radio">
|
|
// is non-standard and not exposed by a11y. The correct fix is to change the
|
|
// l10n strings to include aria-label, but we don't want to change the l10n
|
|
// strings in beta. We can't manually set aria-label too early because Fluent
|
|
// will clobber it, so we watch for mutation.
|
|
new MutationObserver(list =>
|
|
list.forEach(({ target }) => {
|
|
if (target.type === "radio" && !target.hasAttribute("aria-label")) {
|
|
target.setAttribute("aria-label", target.textContent);
|
|
}
|
|
})
|
|
).observe(variations, { attributes: true, subtree: true });
|
|
|
|
// Prepare showing the colorways screen.
|
|
function showColorways() {
|
|
// Use bold variant (index 2) if current theme is dark; otherwise soft (0).
|
|
variations.querySelectorAll("input")[
|
|
2 * matchMedia("(prefers-color-scheme: dark)").matches
|
|
].checked = true;
|
|
|
|
// Enable the theme and variant based on the current selection.
|
|
const getVariantIndex = () =>
|
|
[...variations.children].indexOf(variations.querySelector(":checked")) -
|
|
1;
|
|
const enableVariant = () =>
|
|
enableTheme(
|
|
THEME_IDS[variations.getAttribute("next") ?? 0][getVariantIndex()]
|
|
);
|
|
|
|
// Prepare random theme selection that's not (first) default.
|
|
const random = Math.floor(Math.random() * (THEME_IDS.length - 1)) + 1;
|
|
const selected = themes.children[random];
|
|
selected.checked = true;
|
|
recordEvent("show", `random-${random}`);
|
|
|
|
// Transition in the starting random theme.
|
|
triggerTransition(() => variations.setAttribute("next", random));
|
|
setTimeout(() => {
|
|
enableVariant();
|
|
showVariations(selected);
|
|
}, 400);
|
|
|
|
// Wait for variation button clicks.
|
|
variations.addEventListener("click", ({ target: button }) => {
|
|
// Ignore clicks of whitespace / not-radio-button.
|
|
if (button.type === "radio") {
|
|
enableVariant();
|
|
recordEvent("theme", `variant-${getVariantIndex()}`);
|
|
}
|
|
});
|
|
|
|
// Wait for theme button clicks.
|
|
let nextButton;
|
|
themes.addEventListener("click", ({ target: button }) => {
|
|
// Ignore clicks on whitespace of the container around theme buttons.
|
|
if (button.parentNode === themes) {
|
|
// Cover up content with the next color circle.
|
|
variations.setAttribute("next", [...themes.children].indexOf(button));
|
|
|
|
// Start a transition out while avoiding duplicate transitions.
|
|
if (!nextButton) {
|
|
variations.classList.add("out");
|
|
setTimeout(() => {
|
|
variations.classList.remove("out");
|
|
|
|
// Enable the theme of the now-selected (next) color.
|
|
enableVariant();
|
|
recordEvent("theme", `theme-${variations.getAttribute("next")}`);
|
|
|
|
// Transition in the next variations.
|
|
showVariations(nextButton);
|
|
nextButton = null;
|
|
}, 500);
|
|
}
|
|
|
|
// Save the currently selected button to activate after transition.
|
|
nextButton = button;
|
|
}
|
|
});
|
|
|
|
// 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.
|
|
colorways.classList.remove("hidden");
|
|
adjustModalBackdrop();
|
|
|
|
// Show checkbox to revert homepage or newtab if customized.
|
|
if (
|
|
Services.prefs.prefHasUserValue(HOMEPAGE_PREF) ||
|
|
Services.prefs.prefHasUserValue(NEWTAB_PREF)
|
|
) {
|
|
checkbox.classList.remove("hidden");
|
|
recordEvent("show", checkbox.lastElementChild.dataset.l10nId);
|
|
} else {
|
|
checkbox.remove();
|
|
checkbox.firstElementChild.checked = false;
|
|
}
|
|
|
|
return selected;
|
|
}
|
|
|
|
// Handle completion of colorways screen.
|
|
function removeColorways() {
|
|
body.classList.add("confetti");
|
|
logo.classList.remove("hidden");
|
|
colorways.remove();
|
|
checkbox.remove();
|
|
}
|
|
|
|
// Handle checkbox being checked.
|
|
function handleCheckbox() {
|
|
// Revert both homepage and newtab if still checked (potentially doing
|
|
// nothing if each pref is already the default value).
|
|
if (checkbox.firstElementChild.checked) {
|
|
Services.prefs.clearUserPref(HOMEPAGE_PREF);
|
|
Services.prefs.clearUserPref(NEWTAB_PREF);
|
|
recordEvent("button", checkbox.lastElementChild.dataset.l10nId);
|
|
}
|
|
}
|
|
|
|
// Update the screen content and handle actions.
|
|
let busy = false;
|
|
let current = -1;
|
|
(async function advance({ target } = {}) {
|
|
// Record which button was clicked.
|
|
if (target) {
|
|
recordEvent("button", (busy ? "busy:" : "") + target.dataset.l10nId);
|
|
}
|
|
|
|
// Disallow multiple concurrent advances, e.g., double click while the
|
|
// first callback is still busy awaiting.
|
|
if (busy) {
|
|
return;
|
|
}
|
|
busy = true;
|
|
|
|
// Set the correct target for keyboard focus.
|
|
let toFocus = primary;
|
|
|
|
// Move to the next screen and perform screen-specific behavior / setup.
|
|
if (++current === 0) {
|
|
// Wait for main button clicks on each screen.
|
|
primary.addEventListener("click", advance);
|
|
secondary.addEventListener("click", advance);
|
|
|
|
recordEvent("show", `${SCREEN_STRINGS.length}-screens`);
|
|
await document.l10n.ready;
|
|
|
|
// Make sure we have the previous theme before randomly selecting new one.
|
|
await gPrevTheme;
|
|
toFocus = showColorways();
|
|
} else {
|
|
// Handle actions and setup for not-first and not-last screens.
|
|
const { l10nId } = target.dataset;
|
|
switch (l10nId) {
|
|
// New theme is confirmed, so don't revert to previous.
|
|
case "upgrade-dialog-colorway-primary-button":
|
|
gPrevTheme = null;
|
|
handleCheckbox();
|
|
removeColorways();
|
|
break;
|
|
|
|
// Immediately revert to existing theme.
|
|
case "upgrade-dialog-colorway-secondary-button":
|
|
enableTheme((await gPrevTheme).id);
|
|
removeColorways();
|
|
break;
|
|
|
|
// User manually completed the last step.
|
|
case "upgrade-dialog-thankyou-primary-button":
|
|
closeDialog("complete");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Automatically close the last screen.
|
|
if (current === SCREEN_STRINGS.length - 1) {
|
|
setTimeout(() => closeDialog("autoclose"), 20000);
|
|
}
|
|
|
|
// Update strings for reused elements that change between screens.
|
|
const strings = SCREEN_STRINGS[current];
|
|
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);
|
|
el.classList.remove("hidden");
|
|
el.disabled = false;
|
|
} else {
|
|
el.classList.add("hidden");
|
|
// Disabled inputs take up space to avoid shifting layout.
|
|
el.disabled = true;
|
|
// Avoid screen readers from seeing this too.
|
|
el.textContent = "";
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
busy = false;
|
|
})();
|
|
}
|
|
|
|
// Indicate when we're ready to show and size (async localized) content.
|
|
document.mozSubdialogReady = new Promise(resolve =>
|
|
document.addEventListener("DOMContentLoaded", () => onLoad(resolve), {
|
|
once: true,
|
|
})
|
|
);
|