tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

tooltips-overlay.js (17117B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /**
      8 * The tooltip overlays are tooltips that appear when hovering over property values and
      9 * editor tooltips that appear when clicking swatch based editors.
     10 */
     11 
     12 const flags = require("resource://devtools/shared/flags.js");
     13 const {
     14  VIEW_NODE_CSS_QUERY_CONTAINER,
     15  VIEW_NODE_CSS_SELECTOR_WARNINGS,
     16  VIEW_NODE_FONT_TYPE,
     17  VIEW_NODE_IMAGE_URL_TYPE,
     18  VIEW_NODE_INACTIVE_CSS,
     19  VIEW_NODE_VALUE_TYPE,
     20  VIEW_NODE_VARIABLE_TYPE,
     21 } = require("resource://devtools/client/inspector/shared/node-types.js");
     22 
     23 loader.lazyRequireGetter(
     24  this,
     25  "getCssVariableColor",
     26  "resource://devtools/client/shared/theme.js",
     27  true
     28 );
     29 loader.lazyRequireGetter(
     30  this,
     31  "HTMLTooltip",
     32  "resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js",
     33  true
     34 );
     35 loader.lazyRequireGetter(
     36  this,
     37  ["getImageDimensions", "setImageTooltip", "setBrokenImageTooltip"],
     38  "resource://devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js",
     39  true
     40 );
     41 loader.lazyRequireGetter(
     42  this,
     43  "setVariableTooltip",
     44  "resource://devtools/client/shared/widgets/tooltip/VariableTooltipHelper.js",
     45  true
     46 );
     47 loader.lazyRequireGetter(
     48  this,
     49  "InactiveCssTooltipHelper",
     50  "resource://devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper.js",
     51  false
     52 );
     53 loader.lazyRequireGetter(
     54  this,
     55  "CssCompatibilityTooltipHelper",
     56  "resource://devtools/client/shared/widgets/tooltip/css-compatibility-tooltip-helper.js",
     57  false
     58 );
     59 loader.lazyRequireGetter(
     60  this,
     61  "CssQueryContainerTooltipHelper",
     62  "resource://devtools/client/shared/widgets/tooltip/css-query-container-tooltip-helper.js",
     63  false
     64 );
     65 loader.lazyRequireGetter(
     66  this,
     67  "CssSelectorWarningsTooltipHelper",
     68  "resource://devtools/client/shared/widgets/tooltip/css-selector-warnings-tooltip-helper.js",
     69  false
     70 );
     71 
     72 const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
     73 
     74 // Types of existing tooltips
     75 const TOOLTIP_CSS_COMPATIBILITY = "css-compatibility";
     76 const TOOLTIP_CSS_QUERY_CONTAINER = "css-query-info";
     77 const TOOLTIP_CSS_SELECTOR_WARNINGS = "css-selector-warnings";
     78 const TOOLTIP_FONTFAMILY_TYPE = "font-family";
     79 const TOOLTIP_IMAGE_TYPE = "image";
     80 const TOOLTIP_INACTIVE_CSS = "inactive-css";
     81 const TOOLTIP_VARIABLE_TYPE = "variable";
     82 
     83 /**
     84 * Manages all tooltips in the style-inspector.
     85 */
     86 class TooltipsOverlay {
     87  /**
     88   * @param {CssRuleView|CssComputedView} view
     89   *        Either the rule-view or computed-view panel
     90   */
     91  constructor(view) {
     92    this.view = view;
     93    this._instances = new Map();
     94 
     95    this._onNewSelection = this._onNewSelection.bind(this);
     96    this.view.inspector.selection.on("new-node-front", this._onNewSelection);
     97 
     98    this.addToView();
     99  }
    100  get isEditing() {
    101    for (const [, tooltip] of this._instances) {
    102      if (typeof tooltip.isEditing == "function" && tooltip.isEditing()) {
    103        return true;
    104      }
    105    }
    106    return false;
    107  }
    108 
    109  /**
    110   * Add the tooltips overlay to the view. This will start tracking mouse
    111   * movements and display tooltips when needed
    112   */
    113  addToView() {
    114    if (this._isStarted || this._isDestroyed) {
    115      return;
    116    }
    117 
    118    this._isStarted = true;
    119 
    120    this.inactiveCssTooltipHelper = new InactiveCssTooltipHelper();
    121    this.compatibilityTooltipHelper = new CssCompatibilityTooltipHelper();
    122    this.cssQueryContainerTooltipHelper = new CssQueryContainerTooltipHelper();
    123    this.cssSelectorWarningsTooltipHelper =
    124      new CssSelectorWarningsTooltipHelper();
    125 
    126    // Instantiate the interactiveTooltip and preview tooltip when the
    127    // rule/computed view is hovered over in order to call
    128    // `tooltip.startTogglingOnHover`. This will allow the tooltip to be shown
    129    // when an appropriate element is hovered over.
    130    for (const type of ["interactiveTooltip", "previewTooltip"]) {
    131      if (flags.testing) {
    132        this.getTooltip(type);
    133      } else {
    134        // Lazily get the preview tooltip to avoid loading HTMLTooltip.
    135        this.view.element.addEventListener(
    136          "mousemove",
    137          () => {
    138            this.getTooltip(type);
    139          },
    140          { once: true }
    141        );
    142      }
    143    }
    144  }
    145 
    146  /**
    147   * Lazily fetch and initialize the different tooltips that are used in the inspector.
    148   * These tooltips are attached to the toolbox document if they require a popup panel.
    149   * Otherwise, it is attached to the inspector panel document if it is an inline editor.
    150   *
    151   * @param {string} name
    152   *        Identifier name for the tooltip
    153   */
    154  getTooltip(name) {
    155    let tooltip = this._instances.get(name);
    156    if (tooltip) {
    157      return tooltip;
    158    }
    159    const { doc } = this.view.inspector.toolbox;
    160    switch (name) {
    161      case "colorPicker": {
    162        const SwatchColorPickerTooltip = require("resource://devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js");
    163        tooltip = new SwatchColorPickerTooltip(doc, this.view.inspector);
    164        break;
    165      }
    166      case "cubicBezier": {
    167        const SwatchCubicBezierTooltip = require("resource://devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js");
    168        tooltip = new SwatchCubicBezierTooltip(doc);
    169        break;
    170      }
    171      case "linearEaseFunction": {
    172        const SwatchLinearEasingFunctionTooltip = require("devtools/client/shared/widgets/tooltip/SwatchLinearEasingFunctionTooltip");
    173        tooltip = new SwatchLinearEasingFunctionTooltip(doc);
    174        break;
    175      }
    176      case "filterEditor": {
    177        const SwatchFilterTooltip = require("resource://devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js");
    178        tooltip = new SwatchFilterTooltip(doc);
    179        break;
    180      }
    181      case "interactiveTooltip":
    182        tooltip = new HTMLTooltip(doc, {
    183          type: "doorhanger",
    184          useXulWrapper: true,
    185          noAutoHide: true,
    186        });
    187        tooltip.startTogglingOnHover(
    188          this.view.element,
    189          this.onInteractiveTooltipTargetHover.bind(this),
    190          {
    191            interactive: true,
    192          }
    193        );
    194        break;
    195      case "previewTooltip":
    196        tooltip = new HTMLTooltip(doc, {
    197          type: "arrow",
    198          useXulWrapper: true,
    199        });
    200        tooltip.startTogglingOnHover(
    201          this.view.element,
    202          this._onPreviewTooltipTargetHover.bind(this)
    203        );
    204        break;
    205      default:
    206        throw new Error(`Unsupported tooltip '${name}'`);
    207    }
    208    this._instances.set(name, tooltip);
    209    return tooltip;
    210  }
    211 
    212  /**
    213   * Remove the tooltips overlay from the view. This will stop tracking mouse
    214   * movements and displaying tooltips
    215   */
    216  removeFromView() {
    217    if (!this._isStarted || this._isDestroyed) {
    218      return;
    219    }
    220 
    221    for (const [, tooltip] of this._instances) {
    222      tooltip.destroy();
    223    }
    224 
    225    this.inactiveCssTooltipHelper.destroy();
    226    this.compatibilityTooltipHelper.destroy();
    227 
    228    this._isStarted = false;
    229  }
    230 
    231  /**
    232   * Given a hovered node info, find out which type of tooltip should be shown,
    233   * if any
    234   *
    235   * @param {object} nodeInfo
    236   * @return {string} The tooltip type to be shown, or null
    237   */
    238  _getTooltipType({ type, value: prop }) {
    239    let tooltipType = null;
    240 
    241    // Image preview tooltip
    242    if (type === VIEW_NODE_IMAGE_URL_TYPE) {
    243      tooltipType = TOOLTIP_IMAGE_TYPE;
    244    }
    245 
    246    // Font preview tooltip
    247    if (
    248      (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") ||
    249      type === VIEW_NODE_FONT_TYPE
    250    ) {
    251      const value = prop.value.toLowerCase();
    252      if (value !== "inherit" && value !== "unset" && value !== "initial") {
    253        tooltipType = TOOLTIP_FONTFAMILY_TYPE;
    254      }
    255    }
    256 
    257    // Inactive CSS tooltip
    258    if (type === VIEW_NODE_INACTIVE_CSS) {
    259      tooltipType = TOOLTIP_INACTIVE_CSS;
    260    }
    261 
    262    // Variable preview tooltip
    263    if (type === VIEW_NODE_VARIABLE_TYPE) {
    264      tooltipType = TOOLTIP_VARIABLE_TYPE;
    265    }
    266 
    267    // Container info tooltip
    268    if (type === VIEW_NODE_CSS_QUERY_CONTAINER) {
    269      tooltipType = TOOLTIP_CSS_QUERY_CONTAINER;
    270    }
    271 
    272    // Selector warnings info tooltip
    273    if (type === VIEW_NODE_CSS_SELECTOR_WARNINGS) {
    274      tooltipType = TOOLTIP_CSS_SELECTOR_WARNINGS;
    275    }
    276 
    277    return tooltipType;
    278  }
    279 
    280  _removePreviousInstances() {
    281    for (const tooltip of this._instances.values()) {
    282      if (tooltip.isVisible()) {
    283        if (tooltip.revert) {
    284          tooltip.revert();
    285        }
    286        tooltip.hide();
    287      }
    288    }
    289  }
    290 
    291  /**
    292   * Executed by the tooltip when the pointer hovers over an element of the
    293   * view. Used to decide whether the tooltip should be shown or not and to
    294   * actually put content in it.
    295   * Checks if the hovered target is a css value we support tooltips for.
    296   *
    297   * @param {DOMNode} target The currently hovered node
    298   * @return {Promise}
    299   */
    300  async _onPreviewTooltipTargetHover(target) {
    301    const nodeInfo = this.view.getNodeInfo(target);
    302    if (!nodeInfo) {
    303      // The hovered node isn't something we care about
    304      return false;
    305    }
    306 
    307    const type = this._getTooltipType(nodeInfo);
    308    if (!type) {
    309      // There is no tooltip type defined for the hovered node
    310      return false;
    311    }
    312 
    313    this._removePreviousInstances();
    314 
    315    const inspector = this.view.inspector;
    316 
    317    if (type === TOOLTIP_IMAGE_TYPE) {
    318      try {
    319        await this._setImagePreviewTooltip(nodeInfo.value.url);
    320      } catch (e) {
    321        await setBrokenImageTooltip(
    322          this.getTooltip("previewTooltip"),
    323          this.view.inspector.panelDoc
    324        );
    325      }
    326 
    327      this.sendOpenScalarToTelemetry(type);
    328 
    329      return true;
    330    }
    331 
    332    if (type === TOOLTIP_FONTFAMILY_TYPE) {
    333      const font = nodeInfo.value.value;
    334      const nodeFront = inspector.selection.nodeFront;
    335      await this._setFontPreviewTooltip(font, nodeFront);
    336 
    337      this.sendOpenScalarToTelemetry(type);
    338 
    339      if (nodeInfo.type === VIEW_NODE_FONT_TYPE) {
    340        // If the hovered element is on the font family span, anchor
    341        // the tooltip on the whole property value instead.
    342        return target.parentNode;
    343      }
    344      return true;
    345    }
    346 
    347    if (
    348      type === TOOLTIP_VARIABLE_TYPE &&
    349      nodeInfo.value.value.startsWith("--")
    350    ) {
    351      const {
    352        variable,
    353        registeredProperty,
    354        startingStyleVariable,
    355        variableComputed,
    356        outputParserOptions,
    357        cssProperties,
    358        value,
    359      } = nodeInfo.value;
    360      await this._setVariablePreviewTooltip({
    361        topSectionText: variable,
    362        computed: variableComputed,
    363        registeredProperty,
    364        startingStyle: startingStyleVariable,
    365        outputParserOptions,
    366        cssProperties,
    367        variableName: value,
    368      });
    369 
    370      this.sendOpenScalarToTelemetry(type);
    371 
    372      return true;
    373    }
    374 
    375    return false;
    376  }
    377 
    378  /**
    379   * Executed by the tooltip when the pointer hovers over an element of the
    380   * view. Used to decide whether the tooltip should be shown or not and to
    381   * actually put content in it.
    382   * Checks if the hovered target is a css value we support tooltips for.
    383   *
    384   * @param  {DOMNode} target
    385   *         The currently hovered node
    386   * @return {boolean}
    387   *         true if shown, false otherwise.
    388   */
    389  async onInteractiveTooltipTargetHover(target) {
    390    if (target.classList.contains("ruleview-compatibility-warning")) {
    391      const nodeCompatibilityInfo =
    392        await this.view.getNodeCompatibilityInfo(target);
    393 
    394      await this.compatibilityTooltipHelper.setContent(
    395        nodeCompatibilityInfo,
    396        this.getTooltip("interactiveTooltip")
    397      );
    398 
    399      this.sendOpenScalarToTelemetry(TOOLTIP_CSS_COMPATIBILITY);
    400      return true;
    401    }
    402 
    403    const nodeInfo = this.view.getNodeInfo(target);
    404    if (!nodeInfo) {
    405      // The hovered node isn't something we care about.
    406      return false;
    407    }
    408 
    409    const type = this._getTooltipType(nodeInfo);
    410    if (!type) {
    411      // There is no tooltip type defined for the hovered node.
    412      return false;
    413    }
    414 
    415    this._removePreviousInstances();
    416 
    417    if (type === TOOLTIP_INACTIVE_CSS) {
    418      // Ensure this is the correct node and not a parent.
    419      if (!target.classList.contains("ruleview-inactive-css-warning")) {
    420        return false;
    421      }
    422 
    423      await this.inactiveCssTooltipHelper.setContent(
    424        nodeInfo.value,
    425        this.getTooltip("interactiveTooltip")
    426      );
    427 
    428      this.sendOpenScalarToTelemetry(type);
    429 
    430      return true;
    431    }
    432 
    433    if (type === TOOLTIP_CSS_QUERY_CONTAINER) {
    434      // Ensure this is the correct node and not a parent.
    435      if (!target.closest(".container-query .container-query-declaration")) {
    436        return false;
    437      }
    438 
    439      await this.cssQueryContainerTooltipHelper.setContent(
    440        nodeInfo.value,
    441        this.getTooltip("interactiveTooltip")
    442      );
    443 
    444      this.sendOpenScalarToTelemetry(type);
    445 
    446      return true;
    447    }
    448 
    449    if (type === TOOLTIP_CSS_SELECTOR_WARNINGS) {
    450      await this.cssSelectorWarningsTooltipHelper.setContent(
    451        nodeInfo.value,
    452        this.getTooltip("interactiveTooltip")
    453      );
    454 
    455      this.sendOpenScalarToTelemetry(type);
    456 
    457      return true;
    458    }
    459 
    460    return false;
    461  }
    462 
    463  /**
    464   * Send a telemetry Scalar showing that a tooltip of `type` has been opened.
    465   *
    466   * @param {string} type
    467   *        The node type from `devtools/client/inspector/shared/node-types` or the Tooltip type.
    468   */
    469  sendOpenScalarToTelemetry(type) {
    470    Glean.devtoolsTooltip.shown[type].add(1);
    471  }
    472 
    473  /**
    474   * Set the content of the preview tooltip to display an image preview. The image URL can
    475   * be relative, a call will be made to the debuggee to retrieve the image content as an
    476   * imageData URI.
    477   *
    478   * @param {string} imageUrl
    479   *        The image url value (may be relative or absolute).
    480   * @return {Promise} A promise that resolves when the preview tooltip content is ready
    481   */
    482  async _setImagePreviewTooltip(imageUrl) {
    483    const doc = this.view.inspector.panelDoc;
    484    const maxDim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
    485 
    486    let naturalWidth, naturalHeight;
    487    if (imageUrl.startsWith("data:")) {
    488      // If the imageUrl already is a data-url, save ourselves a round-trip
    489      const size = await getImageDimensions(doc, imageUrl);
    490      naturalWidth = size.naturalWidth;
    491      naturalHeight = size.naturalHeight;
    492    } else {
    493      const inspectorFront = this.view.inspector.inspectorFront;
    494      const { data, size } = await inspectorFront.getImageDataFromURL(
    495        imageUrl,
    496        maxDim
    497      );
    498      imageUrl = await data.string();
    499      naturalWidth = size.naturalWidth;
    500      naturalHeight = size.naturalHeight;
    501    }
    502 
    503    await setImageTooltip(this.getTooltip("previewTooltip"), doc, imageUrl, {
    504      maxDim,
    505      naturalWidth,
    506      naturalHeight,
    507    });
    508  }
    509 
    510  /**
    511   * Set the content of the preview tooltip to display a font family preview.
    512   *
    513   * @param {string} font
    514   *        The font family value.
    515   * @param {object} nodeFront
    516   *        The NodeActor that will used to retrieve the dataURL for the font
    517   *        family tooltip contents.
    518   * @return {Promise} A promise that resolves when the preview tooltip content is ready
    519   */
    520  async _setFontPreviewTooltip(font, nodeFront) {
    521    if (
    522      !font ||
    523      !nodeFront ||
    524      typeof nodeFront.getFontFamilyDataURL !== "function"
    525    ) {
    526      throw new Error("Unable to create font preview tooltip content.");
    527    }
    528 
    529    font = font.replace(/"/g, "'");
    530    font = font.replace("!important", "");
    531    font = font.trim();
    532 
    533    const fillStyle = getCssVariableColor(
    534      "--theme-body-color",
    535      this.view.inspector.panelWin
    536    );
    537    const { data, size: maxDim } = await nodeFront.getFontFamilyDataURL(
    538      font,
    539      fillStyle
    540    );
    541 
    542    const imageUrl = await data.string();
    543    const doc = this.view.inspector.panelDoc;
    544    const { naturalWidth, naturalHeight } = await getImageDimensions(
    545      doc,
    546      imageUrl
    547    );
    548 
    549    await setImageTooltip(this.getTooltip("previewTooltip"), doc, imageUrl, {
    550      hideDimensionLabel: true,
    551      hideCheckeredBackground: true,
    552      maxDim,
    553      naturalWidth,
    554      naturalHeight,
    555    });
    556  }
    557 
    558  /**
    559   * Set the content of the preview tooltip to display a variable preview.
    560   *
    561   * @param {object} tooltipParams
    562   *        See VariableTooltipHelper#setVariableTooltip `params`.
    563   * @return {Promise} A promise that resolves when the preview tooltip content is ready
    564   */
    565  async _setVariablePreviewTooltip(tooltipParams) {
    566    const doc = this.view.inspector.panelDoc;
    567    await setVariableTooltip(
    568      this.getTooltip("previewTooltip"),
    569      doc,
    570      tooltipParams
    571    );
    572  }
    573 
    574  _onNewSelection() {
    575    for (const [, tooltip] of this._instances) {
    576      tooltip.hide();
    577    }
    578  }
    579 
    580  /**
    581   * Destroy this overlay instance, removing it from the view
    582   */
    583  destroy() {
    584    this.removeFromView();
    585 
    586    this.view.inspector.selection.off("new-node-front", this._onNewSelection);
    587    this.view = null;
    588 
    589    this._isDestroyed = true;
    590  }
    591 }
    592 
    593 module.exports = TooltipsOverlay;