Files
tubestation/browser/components/firefoxview/featureCallout.mjs

275 lines
7.9 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 { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const lazy = {};
XPCOMUtils.defineLazyModuleGetters(lazy, {
AboutWelcomeParent: "resource:///actors/AboutWelcomeParent.jsm",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"featureTourProgress",
"browser.firefox-view.feature-tour",
'{"message":"","screen":"","complete":true}',
null,
val => JSON.parse(val)
);
function _addCalloutLinkElements() {
function addStylesheet(href) {
const link = document.head.appendChild(document.createElement("link"));
link.rel = "stylesheet";
link.href = href;
}
function addLocalization(hrefs) {
hrefs.forEach(href => {
// eslint-disable-next-line no-undef
MozXULElement.insertFTLIfNeeded(href);
});
}
// Update styling to be compatible with about:welcome bundle
addStylesheet(
"chrome://activity-stream/content/aboutwelcome/aboutwelcome.css"
);
addLocalization([
"browser/newtab/onboarding.ftl",
"browser/spotlight.ftl",
"branding/brand.ftl",
"browser/branding/brandings.ftl",
"browser/newtab/asrouter.ftl",
]);
}
let CURRENT_SCREEN;
let POSITION_HANDLER;
let CONFIG;
const CONTAINER_ID = "root";
const MESSAGES = [
{
id: "FIREFOX_VIEW_FEATURE_TOUR",
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
{
id: "FEATURE_CALLOUT_1",
parent_selector: "#tabpickup-steps",
content: {
position: "callout",
title: "Hop between devices with tab pickup",
subtitle:
"Quickly grab open tabs from your phone and open them here for maximum flow.",
logo: {
imageURL:
"chrome://activity-stream/content/data/content/assets/heart.webp",
},
primary_button: {
label: "Next",
action: {
navigate: true,
},
},
dismiss_button: {
action: {
navigate: true,
},
},
},
},
{
id: "FEATURE_CALLOUT_2",
parent_selector: "#recently-closed-tabs-container",
content: {
position: "callout",
title: "Get back your closed tabs in a snap",
subtitle:
"All your closed tabs will magically show up here. Never worry about accidentally closing a site again.",
primary_button: {
label: "Next",
action: {
navigate: true,
},
},
dismiss_button: {
action: {
navigate: true,
},
},
},
},
{
id: "FEATURE_CALLOUT_3",
parent_selector: "#colorways",
content: {
position: "callout",
title: "Add a splash of color",
subtitle:
"Paint your browser with colorways, only in Firefox. Choose the shade that speaks to you.",
logo: {
imageURL:
"chrome://activity-stream/content/data/content/assets/heart.webp",
},
primary_button: {
label: "Finish",
action: {
navigate: true,
},
},
dismiss_button: {
action: {
navigate: true,
},
},
},
},
],
},
];
function _createContainer() {
let container = document.createElement("div");
container.classList.add("onboardingContainer", "featureCallout");
container.id = CONTAINER_ID;
document.body.appendChild(container);
return container;
}
function _positionCallout() {
let container = document.getElementById(CONTAINER_ID);
let parentEl = document.querySelector(CURRENT_SCREEN?.parent_selector);
if (!container || !parentEl) {
return;
}
function getOffset(el) {
const rect = el.getBoundingClientRect();
return {
left: rect.left + window.scrollX,
bottom: rect.bottom + window.scrollY,
};
}
// Set callout's position relative to parent element
const margin = 15;
let containerTop = getOffset(parentEl).bottom - margin;
container.style.top = `${Math.min(
window.innerHeight - container.offsetHeight - margin,
containerTop
)}px`;
let leftOffset = (parentEl.offsetWidth - container.offsetWidth) / 2;
let containerLeft = getOffset(parentEl).left + leftOffset;
container.style.left = `${Math.min(
window.innerWidth - container.offsetWidth - margin,
Math.max(containerLeft, margin)
)}px`;
}
function _addPositionListeners() {
POSITION_HANDLER = () => {
_positionCallout(CURRENT_SCREEN.parent_selector);
};
window.addEventListener("scroll", POSITION_HANDLER);
window.addEventListener("resize", POSITION_HANDLER);
}
function _removePositionListeners() {
if (POSITION_HANDLER) {
window.removeEventListener("scroll", POSITION_HANDLER);
window.removeEventListener("resize", POSITION_HANDLER);
}
}
function _setupWindowFunctions() {
const AWParent = new lazy.AboutWelcomeParent();
const browser = document.chromeEventHandler;
const receive = name => data =>
AWParent.onContentMessage(`AWPage:${name}`, data, browser);
// Expose top level functions expected by the bundle.
window.AWGetDefaultSites = () => {};
window.AWGetFeatureConfig = () => CONFIG;
window.AWGetFxAMetricsFlowURI = () => {};
window.AWGetImportableSites = () => "[]";
window.AWGetRegion = receive("GET_REGION");
window.AWGetSelectedTheme = receive("GET_SELECTED_THEME");
// Do not send telemetry if message config sets metrics as 'block'.
window.AWSendEventTelemetry =
CONFIG?.metrics === "block" ? () => {} : receive("TELEMETRY_EVENT");
window.AWSendToDeviceEmailsSupported = receive(
"SEND_TO_DEVICE_EMAILS_SUPPORTED"
);
window.AWSendToParent = (name, data) => receive(name)(data);
window.AWNewScreen = screenIndex => {
CURRENT_SCREEN = CONFIG.screens?.[screenIndex];
if (CURRENT_SCREEN) {
_positionCallout();
}
};
window.AWFinish = () => {
document.getElementById(CONTAINER_ID).remove();
_removePositionListeners();
};
}
async function _addScriptsAndRender(container) {
// Add React script
async function getReactReady() {
return new Promise(function(resolve) {
let reactScript = document.createElement("script");
reactScript.src = "resource://activity-stream/vendor/react.js";
container.appendChild(reactScript);
reactScript.addEventListener("load", resolve);
});
}
// Add ReactDom script
async function getDomReady() {
return new Promise(function(resolve) {
let domScript = document.createElement("script");
domScript.src = "resource://activity-stream/vendor/react-dom.js";
container.appendChild(domScript);
domScript.addEventListener("load", resolve);
});
}
// Load React, then React Dom
await getReactReady();
await getDomReady();
// Load the bundle to render the content as configured.
let bundleScript = document.createElement("script");
bundleScript.src =
"resource://activity-stream/aboutwelcome/aboutwelcome.bundle.js";
container.appendChild(bundleScript);
}
/**
* Render content based on about:welcome multistage template.
*/
export async function showFeatureCallout(messageId) {
// Don't show the feature tour if user has already completed it.
if (lazy.featureTourProgress.complete) {
return;
}
CONFIG = MESSAGES.find(m => m.id === messageId);
if (!CONFIG) {
return;
}
_addCalloutLinkElements();
let container = _createContainer();
_setupWindowFunctions();
// This results in rendering the Feature Callout
await _addScriptsAndRender(container);
// Add handlers for repositioning callout
_addPositionListeners();
}