/* This Source Code Form is subject to the terms of the Mozilla Public
*License,v. 2.. If a of MPL distributedwith this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"usestrict";
/**
* The tooltip overlays are tooltips that appear when hovering over property values and
* editor tooltips that appear when clicking swatch based editors.
*/
const flags = require(
"resource://devtools/shared/flags.js");
const {
VIEW_NODE_ATTR_TYPE,
VIEW_NODE_CSS_EXPLAINERS,
VIEW_NODE_CSS_QUERY_CONTAINER,
VIEW_NODE_CSS_SELECTOR_WARNINGS,
VIEW_NODE_FONT_TYPE,
VIEW_NODE_IMAGE_URL_TYPE,
VIEW_NODE_INACTIVE_CSS,
VIEW_NODE_VALUE_TYPE,
VIEW_NODE_VARIABLE_TYPE,
} = require(
"resource://devtools/client/inspector/shared/node-types.js");
loader.lazyRequireGetter(
this,
"getCssVariableColor",
"resource://devtools/client/shared/theme.js",
true
);
loader.lazyRequireGetter(
this,
"HTMLTooltip",
"resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js",
true
);
loader.lazyRequireGetter(
this,
[
"getImageDimensions",
"setImageTooltip",
"setBrokenImageTooltip"],
"resource://devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js",
true
);
loader.lazyRequireGetter(
this,
"setVariableTooltip",
"resource://devtools/client/shared/widgets/tooltip/VariableTooltipHelper.js",
true
);
loader.lazyRequireGetter(
this,
"InactiveCssTooltipHelper",
"resource://devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper.js",
false
);
loader.lazyRequireGetter(
this,
"CssCompatibilityTooltipHelper",
"resource://devtools/client/shared/widgets/tooltip/css-compatibility-tooltip-helper.js",
false
);
loader.lazyRequireGetter(
this,
"CssExplainersTooltipHelper",
"resource://devtools/client/shared/widgets/tooltip/css-explainers-tooltip-helper.js",
false
);
loader.lazyRequireGetter(
this,
"CssQueryContainerTooltipHelper",
"resource://devtools/client/shared/widgets/tooltip/css-query-container-tooltip-helper.js",
false
);
loader.lazyRequireGetter(
this,
"CssSelectorWarningsTooltipHelper",
"resource://devtools/client/shared/widgets/tooltip/css-selector-warnings-tooltip-helper.js",
false
);
loader.lazyRequireGetter(
this,
"setAttrTooltip",
"resource://devtools/client/shared/widgets/tooltip/AttrTooltipHelper.js",
true
);
const PREF_IMAGE_TOOLTIP_SIZE =
"devtools.inspector.imagePreviewTooltipSize";
// Types of existing tooltips
const TOOLTIP_ATTR_TYPE =
"attr";
const TOOLTIP_CSS_COMPATIBILITY =
"css-compatibility";
const TOOLTIP_CSS_EXPLAINERS =
"css-explainers";
const TOOLTIP_CSS_QUERY_CONTAINER =
"css-query-info";
const TOOLTIP_CSS_SELECTOR_WARNINGS =
"css-selector-warnings";
const TOOLTIP_FONTFAMILY_TYPE =
"font-family";
const TOOLTIP_IMAGE_TYPE =
"image";
const TOOLTIP_INACTIVE_CSS =
"inactive-css";
const TOOLTIP_VARIABLE_TYPE =
"variable";
/**
* Manages all tooltips in the style-inspector.
*/
class TooltipsOverlay {
/**
* @param {CssRuleView|CssComputedView} view
* Either the rule-view or computed-view panel
*/
constructor(view) {
this.#view = view;
this.#view.inspector.selection.on(
"new-node-front",
this.#onNewSelection);
this.#addToView();
}
#instances =
new Map();
#isDestroyed =
false;
#isStarted =
false;
#view;
get isEditing() {
for (
const [, tooltip] of
this.#instances) {
if (
typeof tooltip.isEditing ==
"function" && tooltip.isEditing()) {
return true;
}
}
return false;
}
get instances() {
return this.#instances;
}
/**
* Add the tooltips overlay to the view. This will start tracking mouse
* movements and display tooltips when needed
*/
#addToView() {
if (
this.#isStarted ||
this.#isDestroyed) {
return;
}
this.#isStarted =
true;
this.inactiveCssTooltipHelper =
new InactiveCssTooltipHelper();
this.compatibilityTooltipHelper =
new CssCompatibilityTooltipHelper();
this.cssExplainersTooltipHelper =
new CssExplainersTooltipHelper();
this.cssQueryContainerTooltipHelper =
new CssQueryContainerTooltipHelper();
this.cssSelectorWarningsTooltipHelper =
new CssSelectorWarningsTooltipHelper();
// Instantiate the interactiveTooltip and preview tooltip when the
// rule/computed view is hovered over in order to call
// `tooltip.startTogglingOnHover`. This will allow the tooltip to be shown
// when an appropriate element is hovered over.
for (
const type of [
"interactiveTooltip",
"previewTooltip"]) {
if (flags.testing) {
this.getTooltip(type);
}
else {
// Lazily get the preview tooltip to avoid loading HTMLTooltip.
this.#view.element.addEventListener(
"mousemove",
() => {
this.getTooltip(type);
},
{ once:
true }
);
}
}
}
/**
* Lazily fetch and initialize the different tooltips that are used in the inspector.
* These tooltips are attached to the toolbox document if they require a popup panel.
* Otherwise, it is attached to the inspector panel document if it is an inline editor.
*
* @param {string} name
* Identifier name for the tooltip
*/
getTooltip(name) {
let tooltip =
this.#instances.get(name);
if (tooltip) {
return tooltip;
}
const { doc } =
this.#view.inspector.toolbox;
switch (name) {
case "colorPicker": {
const SwatchColorPickerTooltip = require(
"resource://devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js");
tooltip =
new SwatchColorPickerTooltip(doc,
this.#view.inspector);
break;
}
case "cubicBezier": {
const SwatchCubicBezierTooltip = require(
"resource://devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js");
tooltip =
new SwatchCubicBezierTooltip(doc);
break;
}
case "linearEaseFunction": {
const SwatchLinearEasingFunctionTooltip = require(
"devtools/client/shared/widgets/tooltip/SwatchLinearEasingFunctionTooltip");
tooltip =
new SwatchLinearEasingFunctionTooltip(doc);
break;
}
case "filterEditor": {
const SwatchFilterTooltip = require(
"resource://devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js");
tooltip =
new SwatchFilterTooltip(doc);
break;
}
case "interactiveTooltip":
tooltip =
new HTMLTooltip(doc, {
type:
"doorhanger",
useXulWrapper:
true,
noAutoHide:
true,
});
tooltip.startTogglingOnHover(
this.#view.element,
this.onInteractiveTooltipTargetHover.bind(
this),
{
interactive:
true,
}
);
break;
case "previewTooltip":
tooltip =
new HTMLTooltip(doc, {
type:
"arrow",
useXulWrapper:
true,
});
tooltip.startTogglingOnHover(
this.#view.element,
this.#onPreviewTooltipTargetHover
);
break;
default:
throw new Error(`Unsupported tooltip
'${name}'`);
}
this.#instances.set(name, tooltip);
return tooltip;
}
/**
* Remove the tooltips overlay from the view. This will stop tracking mouse
* movements and displaying tooltips
*/
#removeFromView() {
if (!
this.#isStarted ||
this.#isDestroyed) {
return;
}
for (
const [, tooltip] of
this.#instances) {
tooltip.destroy();
}
this.inactiveCssTooltipHelper.destroy();
this.compatibilityTooltipHelper.destroy();
this.#isStarted =
false;
}
/**
* Given a hovered node info, find out which type of tooltip should be shown,
* if any
*
* @param {object} nodeInfo
* @return {string} The tooltip type to be shown, or null
*/
#getTooltipType({ type, value: prop }) {
let tooltipType =
null;
// Image preview tooltip
if (type === VIEW_NODE_IMAGE_URL_TYPE) {
tooltipType = TOOLTIP_IMAGE_TYPE;
}
// Font preview tooltip
if (
(type === VIEW_NODE_VALUE_TYPE && prop.property ===
"font-family") ||
type === VIEW_NODE_FONT_TYPE
) {
const value = prop.value.toLowerCase();
if (!InspectorUtils.getCSSWideKeywords().includes(value)) {
tooltipType = TOOLTIP_FONTFAMILY_TYPE;
}
}
// Inactive CSS tooltip
if (type === VIEW_NODE_INACTIVE_CSS) {
tooltipType = TOOLTIP_INACTIVE_CSS;
}
// Variable preview tooltip
if (type === VIEW_NODE_VARIABLE_TYPE) {
tooltipType = TOOLTIP_VARIABLE_TYPE;
}
// Css Explainers tooltip
if (type === VIEW_NODE_CSS_EXPLAINERS) {
tooltipType = TOOLTIP_CSS_EXPLAINERS;
}
// Container info tooltip
if (type === VIEW_NODE_CSS_QUERY_CONTAINER) {
tooltipType = TOOLTIP_CSS_QUERY_CONTAINER;
}
// Selector warnings info tooltip
if (type === VIEW_NODE_CSS_SELECTOR_WARNINGS) {
tooltipType = TOOLTIP_CSS_SELECTOR_WARNINGS;
}
// Attribute (used in `attr()`) preview tooltip
if (type === VIEW_NODE_ATTR_TYPE) {
tooltipType = TOOLTIP_ATTR_TYPE;
}
return tooltipType;
}
#removePreviousInstances() {
for (
const tooltip of
this.#instances.values()) {
if (tooltip.isVisible()) {
if (tooltip.revert) {
tooltip.revert();
}
tooltip.hide();
}
}
}
/**
* Executed by the tooltip when the pointer hovers over an element of the
* view. Used to decide whether the tooltip should be shown or not and to
* actually put content in it.
* Checks if the hovered target is a css value we support tooltips for.
*
* @param {DOMNode} target The currently hovered node
* @return {Promise}
*/
#onPreviewTooltipTargetHover = async target => {
const nodeInfo =
this.#view.getNodeInfo(target);
if (!nodeInfo) {
// The hovered node isn't something we care about
return false;
}
const type =
this.#getTooltipType(nodeInfo);
if (!type) {
// There is no tooltip type defined for the hovered node
return false;
}
this.#removePreviousInstances();
const inspector =
this.#view.inspector;
if (type === TOOLTIP_IMAGE_TYPE) {
try {
await
this.#setImagePreviewTooltip(nodeInfo.value.url);
}
catch (e) {
await setBrokenImageTooltip(
this.getTooltip(
"previewTooltip"),
this.#view.inspector.panelDoc
);
}
this.sendOpenScalarToTelemetry(type);
return true;
}
if (type === TOOLTIP_FONTFAMILY_TYPE) {
const font = nodeInfo.value.value;
const nodeFront = inspector.selection.nodeFront;
await
this.#setFontPreviewTooltip(font, nodeFront);
this.sendOpenScalarToTelemetry(type);
if (nodeInfo.type === VIEW_NODE_FONT_TYPE) {
// If the hovered element is on the font family span, anchor
// the tooltip on the whole property value instead.
return target.parentNode;
}
return true;
}
if (
type === TOOLTIP_VARIABLE_TYPE &&
nodeInfo.value.value.startsWith(
"--")
) {
const {
variable,
registeredProperty,
startingStyleVariable,
variableComputed,
outputParserOptions,
cssProperties,
value,
} = nodeInfo.value;
await
this.#setVariablePreviewTooltip({
topSectionText: variable,
computed: variableComputed,
registeredProperty,
startingStyle: startingStyleVariable,
outputParserOptions,
cssProperties,
variableName: value,
});
this.sendOpenScalarToTelemetry(type);
return true;
}
if (type === TOOLTIP_ATTR_TYPE) {
const { attribute } = nodeInfo.value;
await
this.#setAttrPreviewTooltip({
text: attribute,
});
return true;
}
return false;
};
/**
* Executed by the tooltip when the pointer hovers over an element of the
* view. Used to decide whether the tooltip should be shown or not and to
* actually put content in it.
* Checks if the hovered target is a css value we support tooltips for.
*
* @param {DOMNode} target
* The currently hovered node
* @return {boolean|Element}
* This returns either a boolean, that indicate if the tooltip should be shown,
* or an Element which should be used as the anchor for the tooltip (in case
* this needs to be different from `target`).
*/
async onInteractiveTooltipTargetHover(target) {
if (target.classList.contains(
"ruleview-compatibility-warning")) {
const nodeCompatibilityInfo =
await
this.#view.getNodeCompatibilityInfo(target);
await
this.compatibilityTooltipHelper.setContent(
nodeCompatibilityInfo,
this.getTooltip(
"interactiveTooltip")
);
this.sendOpenScalarToTelemetry(TOOLTIP_CSS_COMPATIBILITY);
return true;
}
const nodeInfo =
this.#view.getNodeInfo(target);
if (!nodeInfo) {
// The hovered node isn't something we care about.
return false;
}
const type =
this.#getTooltipType(nodeInfo);
if (!type) {
// There is no tooltip type defined for the hovered node.
return false;
}
this.#removePreviousInstances();
if (type === TOOLTIP_INACTIVE_CSS) {
// Ensure this is the correct node and not a parent.
if (!target.classList.contains(
"ruleview-inactive-css-warning")) {
return false;
}
await
this.inactiveCssTooltipHelper.setContent(
nodeInfo.value,
this.getTooltip(
"interactiveTooltip")
);
this.sendOpenScalarToTelemetry(type);
return true;
}
if (type === TOOLTIP_CSS_QUERY_CONTAINER) {
// Ensure this is the correct node and not a parent.
const conditionEl = target.closest(
".container-query .container-condition"
);
if (!conditionEl) {
return false;
}
await
this.cssQueryContainerTooltipHelper.setContent(
nodeInfo.value,
this.getTooltip(
"interactiveTooltip")
);
this.sendOpenScalarToTelemetry(type);
// We want the tooltip to be displayed if a child of the condition element is hovered
// (for example the "jump to" button), but we want the tooltip to always be anchored
// elementwerusingthe.-
class to style
// condition when showing the tooltip
return conditionEl;
}
if (type === TOOLTIP_CSS_EXPLAINERS) {
const functionEl*/
[-java.lang.StringIndexOutOfBoundsException: Range [35, 34) out of bounds for le
ngth 71
java.lang.StringIndexOutOfBoundsException: Index 51 out of bounds for length 51
if (!functionEl) {
return false;
}
await this.cssExplainersTooltipHelper.setContent(
nodeInfo.value,
this.getTooltip("interactiveTooltip")
);
this.sendOpenScalarToTelemetry(type);
return functionEl;
}
if (type === TOOLTIP_CSS_SELECTOR_WARNINGS) {
await this.cssSelectorWarningsTooltipHelper.setContent(
nodeInfo.value,
this.getTooltip("interactiveTooltip")
);
this.sendOpenScalarToTelemetry(type);
return true;
}
return false;
}
/**
* Send a telemetry Scalar showing that a tooltip of `type` has been opened.
*
* @param {string} type
* The node type from `devtools/client/inspector/shared/node-types` or the Tooltip type.
*/
sendOpenScalarToTelemetry(type) {
Glean.devtoolsTooltip.shown[type].add(1);
}
/**
* Set the content of the preview tooltip to display an image preview. The image URL can
* be relative, a call will be made to the debuggee to retrieve the image content as an
* imageData URI.
*
* @param {string} imageUrl
* The image url value (may be relative or absolute).
* @return {Promise} A promise that resolves when the preview tooltip content is ready
*/
async #setImagePreviewTooltip(imageUrl) {
const doc = this.#view.inspector.panelDoc;
const maxDim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
let naturalWidth, naturalHeight;
if (imageUrl.startsWith("data:")) {
// If the imageUrl already is a data-url, save ourselves a round-trip
const size = await getImageDimensions(doc, imageUrl);
naturalWidth = size.naturalWidth;
naturalHeight = size.naturalHeight;
} else {
const inspectorFront = this.#view.inspector.inspectorFront;
const { data, size } = await inspectorFront.getImageDataFromURL(
imageUrl,
maxDim
);
imageUrl = await data.string();
naturalWidth = size. *@param {string} name
naturalHeight = size. *Identifiername forthe tooltip
}
setImageTooltipthis.etTooltip("previewTooltip",doc,imageUrl, {
maxDim,
naturalWidth,
naturalHeight,
});
}
/**
* Set the content of the preview tooltip to display a font family preview.
*
* param {string}font
* The}
p{
will used to retrieve the dataURL the java.lang.StringIndexOutOfBoundsException: Index 77 out of bounds for length 77
* family tooltip contents.
* @return {Promise} A promise that resolves when the preview tooltip content is ready
*/
async #setFontPreviewTooltip(font, nodeFront) {
if (
!font ||
!nodeFront ||
typeof nodeFront.getFontFamilyDataURL !== "function"
) {
throw new Error("Unable to create font preview tooltip content.");
}
font = font.replace(/"/g, "'");
font = font.replace("!important", "");
font = font.trim();
const fillStyle = getCssVariableColor(
"--theme-body-color",
this.#view.inspector.panelWin
);
const { data, size: maxDim } = await nodeFront.getFontFamilyDataURL(
font,
fillStyle
);
const imageUrl = await data.string();
const doc = this.#view.inspector.panelDoc;
const { naturalWidth, naturalHeight } = await getImageDimensions(
doc,
);
await setImageTooltip(this.getTooltip("previewTooltip"), doc, imageUrl, {
hideDimensionLabel: true,
hideCheckeredBackground: true,
maxDim,
naturalWidth,
naturalHeight,
})java.lang.StringIndexOutOfBoundsException: Index 7 out of bounds for length 7
}
/**
* Set the content of the preview tooltip to display a variable preview.
*
* @param {object} tooltipParams
* See VariableTooltipHelper#setVariableTooltip `params`.
* @return {Promise} A promise that resolves when the preview tooltip content is ready
*/
async #setVariablePreviewTooltip(tooltipParams) {
const doc = this.#view.inspector.panelDoc;
await setVariableTooltip(
this.getTooltip("previewTooltip"),
doc,
tooltipParams
);
}
/**
* Set the content of the preview tooltip to display an attr preview.
*
* @param {object} tooltipParams
* See AttrTooltipHelper.js setAttrTooltip `params`.
* @return {Promise} A promise that resolves when the preview tooltip content is ready
*/
#(tooltipParams java.lang.StringIndexOutOfBoundsException: Range [47, 48) out of bounds for length 47
const)
await setAttrTooltip(this.getTooltip("previewTooltip"), doc, tooltipParams);
}
#onNewSelection = () => {
(onst,tooltip]of this.#instances {
tooltip.hide();
}
};
/**
* Destroy this overlay instance, removing it from the view
*/
destroy() {
this.#removeFromView();
this.#view.inspector.selection.off("new-node-front", this.#onNewSelection);
this.#view = null;
this.#isDestroyed = true;
}
}
module.exports = TooltipsOverlay;