/* 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/. */
"use strict";
const { BrowserLoader } = ChromeUtils.importESModule(
"resource://devtools/shared/loader/browser-loader.sys.mjs"
);
loader.lazyRequireGetter(
this,
"openDocLink",
"resource://devtools/client/shared/link.js",
true
);
class CssCompatibilityTooltipHelper {
constructor() {
this.addTab = this.addTab.bind(this);
}
#currentTooltip = null;
#currentUrl = null;
#createElement(doc, tag, classList = [], attributeList = {}) {
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const newElement = doc.createElementNS(XHTML_NS, tag);
for (const elementClass of classList) {
newElement.classList.add(elementClass);
}
for (const key in attributeList) {
newElement.setAttribute(key, attributeList[key]);
}
return newElement;
}
/*
* Attach the UnsupportedBrowserList component to the
* ".compatibility-browser-list-wrapper" div to render the
* unsupported browser list
*/
#renderUnsupportedBrowserList(container, unsupportedBrowsers) {
// Mount the ReactDOM only if the unsupported browser
// list is not empty. Else "compatibility-browser-list-wrapper"
// is not defined. For example, for property clip,
// unsupportedBrowsers is an empty array
if (!unsupportedBrowsers.length) {
return;
}
const { require } = BrowserLoader({
baseURI: "resource://devtools/client/shared/widgets/tooltip/",
window: this.#currentTooltip.doc.defaultView,
});
const {
createFactory,
createElement,
} = require("resource://devtools/client/shared/vendor/react.js");
const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js");
const UnsupportedBrowserList = createFactory(
require("resource://devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js")
);
const unsupportedBrowserList = createElement(UnsupportedBrowserList, {
browsers: unsupportedBrowsers,
});
ReactDOM.render(
unsupportedBrowserList,
container.querySelector(".compatibility-browser-list-wrapper")
);
}
/*
* Get the first paragraph for the compatibility tooltip
* Return a subtree similar to:
*
*
*/
#getCompatibilityMessage(doc, data) {
const { msgId, property } = data;
return this.#createElement(doc, "p", [], {
"data-l10n-id": msgId,
"data-l10n-args": JSON.stringify({ property }),
});
}
/**
* Gets the paragraph elements related to the browserList.
* This returns an array with following subtree:
* [
* ,
*
*
*
* ]
* The first element is the message and the second element is the
* unsupported browserList itself.
* If the unsupportedBrowser is an empty array, we return an empty
* array back.
*/
#getBrowserListContainer(doc, unsupportedBrowsers) {
if (!unsupportedBrowsers.length) {
return null;
}
const browserList = this.#createElement(doc, "p");
const browserListWrapper = this.#createElement(doc, "div", [
"compatibility-browser-list-wrapper",
]);
browserList.appendChild(browserListWrapper);
return browserList;
}
/*
* This is the learn more message element linking to the MDN documentation
* for the particular incompatible CSS declaration.
* The element returned is:
*
*
*
*/
#getLearnMoreMessage(doc, { rootProperty }) {
const learnMoreMessage = this.#createElement(doc, "p", [], {
"data-l10n-id": "css-compatibility-learn-more-message",
"data-l10n-args": JSON.stringify({ rootProperty }),
});
learnMoreMessage.appendChild(
this.#createElement(doc, "span", ["link"], {
"data-l10n-name": "link",
})
);
return learnMoreMessage;
}
/**
* Fill the tooltip with inactive CSS information.
*
* @param {Object} data
* An object in the following format: {
* // Type of compatibility issue
* type: ,
* // The CSS declaration that has compatibility issues
* // The raw CSS declaration name that has compatibility issues
* declaration: ,
* property: ,
* // Alias to the given CSS property
* alias: ,
* // Link to MDN documentation for the particular CSS rule
* url: ,
* deprecated: ,
* experimental: ,
* // An array of all the browsers that don't support the given CSS rule
* unsupportedBrowsers: ,
* }
* @param {HTMLTooltip} tooltip
* The tooltip we are targetting.
*/
async setContent(data, tooltip) {
const fragment = this.getTemplate(data, tooltip);
const { doc } = tooltip;
tooltip.panel.innerHTML = "";
tooltip.panel.addEventListener("click", this.addTab);
tooltip.once("hidden", () => {
tooltip.panel.removeEventListener("click", this.addTab);
});
// Because Fluent is async we need to manually translate the fragment and
// then insert it into the tooltip. This is needed in order for the tooltip
// to size to the contents properly and for tests.
await doc.l10n.translateFragment(fragment);
doc.l10n.pauseObserving();
tooltip.panel.appendChild(fragment);
doc.l10n.resumeObserving();
// Size the content.
tooltip.setContentSize({ width: 267, height: Infinity });
}
/**
* Get the template that the Fluent string will be merged with. This template
* looks like this:
*
*
*
* @param {Object} data
* An object in the following format: {
* // Type of compatibility issue
* type: ,
* // The CSS declaration that has compatibility issues
* // The raw CSS declaration name that has compatibility issues
* declaration: ,
* property: ,
* // Alias to the given CSS property
* alias: ,
* // Link to MDN documentation for the particular CSS rule
* url: ,
* // Link to the spec for the particular CSS rule
* specUrl: ,
* deprecated: ,
* experimental: ,
* // An array of all the browsers that don't support the given CSS rule
* unsupportedBrowsers: ,
* }
* @param {HTMLTooltip} tooltip
* The tooltip we are targetting.
*/
getTemplate(data, tooltip) {
const { doc } = tooltip;
const { specUrl, url, unsupportedBrowsers } = data;
this.#currentTooltip = tooltip;
this.#currentUrl = url
? `${url}?utm_source=devtools&utm_medium=inspector-css-compatibility&utm_campaign=default`
: specUrl;
const templateNode = this.#createElement(doc, "template");
const tooltipContainer = this.#createElement(doc, "div", [
"devtools-tooltip-css-compatibility",
]);
tooltipContainer.appendChild(this.#getCompatibilityMessage(doc, data));
const browserListContainer = this.#getBrowserListContainer(
doc,
unsupportedBrowsers
);
if (browserListContainer) {
tooltipContainer.appendChild(browserListContainer);
this.#renderUnsupportedBrowserList(tooltipContainer, unsupportedBrowsers);
}
if (this.#currentUrl) {
tooltipContainer.appendChild(this.#getLearnMoreMessage(doc, data));
}
templateNode.content.appendChild(tooltipContainer);
return doc.importNode(templateNode.content, true);
}
/**
* Hide the tooltip, open `this.#currentUrl` in a new tab and focus it.
*
* @param {DOMEvent} event
* The click event originating from the tooltip.
*
*/
addTab(event) {
// The XUL panel swallows click events so handlers can't be added directly
// to the link span. As a workaround we listen to all click events in the
// panel and if a link span is clicked we proceed.
if (event.target.className !== "link") {
return;
}
const tooltip = this.#currentTooltip;
tooltip.hide();
const isMacOS = Services.appinfo.OS === "Darwin";
openDocLink(this.#currentUrl, {
relatedToCurrent: true,
inBackground: isMacOS ? event.metaKey : event.ctrlKey,
});
}
destroy() {
this.#currentTooltip = null;
this.#currentUrl = null;
}
}
module.exports = CssCompatibilityTooltipHelper;