tor-browser

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

node-tabbing-order.js (9883B)


      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 loader.lazyRequireGetter(
      8  this,
      9  ["setIgnoreLayoutChanges", "getCurrentZoom"],
     10  "resource://devtools/shared/layout/utils.js",
     11  true
     12 );
     13 loader.lazyRequireGetter(
     14  this,
     15  "AutoRefreshHighlighter",
     16  "resource://devtools/server/actors/highlighters/auto-refresh.js",
     17  true
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  ["CanvasFrameAnonymousContentHelper"],
     22  "resource://devtools/server/actors/highlighters/utils/markup.js",
     23  true
     24 );
     25 
     26 /**
     27 * The NodeTabbingOrderHighlighter draws an outline around a node (based on its
     28 * border bounds).
     29 *
     30 * Usage example:
     31 *
     32 * const h = new NodeTabbingOrderHighlighter(env);
     33 * await h.isReady();
     34 * h.show(node, options);
     35 * h.hide();
     36 * h.destroy();
     37 *
     38 * @param {number} options.index
     39 *        Tabbing index value to be displayed in the highlighter info bar.
     40 */
     41 class NodeTabbingOrderHighlighter extends AutoRefreshHighlighter {
     42  constructor(highlighterEnv) {
     43    super(highlighterEnv);
     44 
     45    this._doNotStartRefreshLoop = true;
     46    this.markup = new CanvasFrameAnonymousContentHelper(
     47      this.highlighterEnv,
     48      this._buildMarkup.bind(this),
     49      {
     50        contentRootHostClassName: "devtools-highlighter-tabbing-order",
     51      }
     52    );
     53    this.isReady = this.markup.initialize();
     54  }
     55 
     56  _buildMarkup() {
     57    this.rootEl = this.markup.createNode({
     58      attributes: {
     59        id: "tabbing-order-root",
     60        class: "tabbing-order-root highlighter-container tabbing-order",
     61        "aria-hidden": "true",
     62      },
     63    });
     64 
     65    const container = this.markup.createNode({
     66      parent: this.rootEl,
     67      attributes: {
     68        id: "tabbing-order-container",
     69        width: "100%",
     70        height: "100%",
     71        hidden: "true",
     72      },
     73    });
     74 
     75    // Building the SVG element
     76    this.markup.createNode({
     77      parent: container,
     78      attributes: {
     79        class: "tabbing-order-bounds",
     80        id: "tabbing-order-bounds",
     81      },
     82    });
     83 
     84    // Building the nodeinfo bar markup
     85 
     86    const infobarContainer = this.markup.createNode({
     87      parent: this.rootEl,
     88      attributes: {
     89        class: "tabbing-order-infobar-container",
     90        id: "tabbing-order-infobar-container",
     91        position: "top",
     92        hidden: "true",
     93      },
     94    });
     95 
     96    const infobar = this.markup.createNode({
     97      parent: infobarContainer,
     98      attributes: {
     99        class: "tabbing-order-infobar",
    100      },
    101    });
    102 
    103    this.markup.createNode({
    104      parent: infobar,
    105      attributes: {
    106        class: "tabbing-order-infobar-text",
    107        id: "tabbing-order-infobar-text",
    108      },
    109    });
    110 
    111    return this.rootEl;
    112  }
    113 
    114  /**
    115   * Destroy the nodes. Remove listeners.
    116   */
    117  destroy() {
    118    this.markup.destroy();
    119    this.rootEl = null;
    120 
    121    AutoRefreshHighlighter.prototype.destroy.call(this);
    122  }
    123 
    124  getElement(id) {
    125    return this.markup.getElement(id);
    126  }
    127 
    128  /**
    129   * Update focused styling for a node tabbing index highlight.
    130   *
    131   * @param {boolean} focused
    132   *        Indicates if the highlighted node needs to be focused.
    133   */
    134  updateFocus(focused) {
    135    const root = this.getElement("tabbing-order-root");
    136    root.classList?.toggle("focused", focused);
    137  }
    138 
    139  /**
    140   * Show the highlighter on a given node
    141   */
    142  _show() {
    143    return this._update();
    144  }
    145 
    146  /**
    147   * Update the highlighter on the current highlighted node (the one that was
    148   * passed as an argument to show(node)).
    149   * Should be called whenever node size or attributes change
    150   */
    151  _update() {
    152    let shown = false;
    153    setIgnoreLayoutChanges(true);
    154 
    155    if (this._updateTabbingOrder()) {
    156      this._showInfobar();
    157      this._showTabbingOrder();
    158      shown = true;
    159      setIgnoreLayoutChanges(
    160        false,
    161        this.highlighterEnv.window.document.documentElement
    162      );
    163    } else {
    164      // Nothing to highlight (0px rectangle like a <script> tag for instance)
    165      this._hide();
    166    }
    167 
    168    return shown;
    169  }
    170 
    171  /**
    172   * Hide the highlighter, the outline and the infobar.
    173   */
    174  _hide() {
    175    setIgnoreLayoutChanges(true);
    176 
    177    this._hideTabbingOrder();
    178    this._hideInfobar();
    179 
    180    setIgnoreLayoutChanges(
    181      false,
    182      this.highlighterEnv.window.document.documentElement
    183    );
    184  }
    185 
    186  /**
    187   * Hide the infobar
    188   */
    189  _hideInfobar() {
    190    this.getElement("tabbing-order-infobar-container").setAttribute(
    191      "hidden",
    192      "true"
    193    );
    194  }
    195 
    196  /**
    197   * Show the infobar
    198   */
    199  _showInfobar() {
    200    if (!this.currentNode) {
    201      return;
    202    }
    203 
    204    this.getElement("tabbing-order-infobar-container").removeAttribute(
    205      "hidden"
    206    );
    207    this.getElement("tabbing-order-infobar-text").setTextContent(
    208      this.options.index
    209    );
    210    const bounds = this._getBounds();
    211    const container = this.getElement("tabbing-order-infobar-container");
    212 
    213    moveInfobar(container, bounds, this.win);
    214  }
    215 
    216  /**
    217   * Hide the tabbing order highlighter
    218   */
    219  _hideTabbingOrder() {
    220    this.getElement("tabbing-order-container").setAttribute("hidden", "true");
    221  }
    222 
    223  /**
    224   * Show the tabbing order highlighter
    225   */
    226  _showTabbingOrder() {
    227    this.getElement("tabbing-order-container").removeAttribute("hidden");
    228  }
    229 
    230  /**
    231   * Calculate border bounds based on the quads returned by getAdjustedQuads.
    232   *
    233   * @return {object} A bounds object {bottom,height,left,right,top,width,x,y}
    234   */
    235  _getBorderBounds() {
    236    const quads = this.currentQuads.border;
    237    if (!quads || !quads.length) {
    238      return null;
    239    }
    240 
    241    const bounds = {
    242      bottom: -Infinity,
    243      height: 0,
    244      left: Infinity,
    245      right: -Infinity,
    246      top: Infinity,
    247      width: 0,
    248      x: 0,
    249      y: 0,
    250    };
    251 
    252    for (const q of quads) {
    253      bounds.bottom = Math.max(bounds.bottom, q.bounds.bottom);
    254      bounds.top = Math.min(bounds.top, q.bounds.top);
    255      bounds.left = Math.min(bounds.left, q.bounds.left);
    256      bounds.right = Math.max(bounds.right, q.bounds.right);
    257    }
    258    bounds.x = bounds.left;
    259    bounds.y = bounds.top;
    260    bounds.width = bounds.right - bounds.left;
    261    bounds.height = bounds.bottom - bounds.top;
    262 
    263    return bounds;
    264  }
    265 
    266  /**
    267   * Update the tabbing order index as per the current node.
    268   *
    269   * @return {boolean}
    270   *         True if the current node has a tabbing order index to be
    271   *         highlighted
    272   */
    273  _updateTabbingOrder() {
    274    if (!this._nodeNeedsHighlighting()) {
    275      this._hideTabbingOrder();
    276      return false;
    277    }
    278 
    279    const boundsEl = this.getElement("tabbing-order-bounds");
    280    const { left, top, width, height } = this._getBounds();
    281    boundsEl.setAttribute(
    282      "style",
    283      `top: ${top}px; left: ${left}px; width: ${width}px; height: ${height}px;`
    284    );
    285 
    286    // Un-zoom the root wrapper if the page was zoomed.
    287    this.markup.scaleRootElement(this.currentNode, "tabbing-order-container");
    288 
    289    return true;
    290  }
    291 
    292  /**
    293   * Can the current node be highlighted? Does it have quads.
    294   *
    295   * @return {boolean}
    296   */
    297  _nodeNeedsHighlighting() {
    298    return (
    299      this.currentQuads.margin.length ||
    300      this.currentQuads.border.length ||
    301      this.currentQuads.padding.length ||
    302      this.currentQuads.content.length
    303    );
    304  }
    305 
    306  _getBounds() {
    307    const borderBounds = this._getBorderBounds();
    308    let bounds = {
    309      bottom: 0,
    310      height: 0,
    311      left: 0,
    312      right: 0,
    313      top: 0,
    314      width: 0,
    315      x: 0,
    316      y: 0,
    317    };
    318 
    319    if (!borderBounds) {
    320      // Invisible element such as a script tag.
    321      return bounds;
    322    }
    323 
    324    const { bottom, height, left, right, top, width, x, y } = borderBounds;
    325    if (width > 0 || height > 0) {
    326      bounds = { bottom, height, left, right, top, width, x, y };
    327    }
    328 
    329    return bounds;
    330  }
    331 }
    332 
    333 /**
    334 * Move the infobar to the right place in the highlighter. The infobar is used
    335 * to display element's tabbing order index.
    336 *
    337 * @param  {DOMNode} container
    338 *         The container element which will be used to position the infobar.
    339 * @param  {object} bounds
    340 *         The content bounds of the container element.
    341 * @param  {Window} win
    342 *         The window object.
    343 */
    344 function moveInfobar(container, bounds, win) {
    345  const zoom = getCurrentZoom(win);
    346  const { computedStyle } = container;
    347  const margin = 2;
    348  const arrowSize =
    349    parseFloat(
    350      computedStyle.getPropertyValue("--highlighter-bubble-arrow-size")
    351    ) - 2;
    352  const containerHeight = parseFloat(computedStyle.getPropertyValue("height"));
    353  const containerWidth = parseFloat(computedStyle.getPropertyValue("width"));
    354 
    355  const topBoundary = margin;
    356  const bottomBoundary =
    357    win.document.scrollingElement.scrollHeight - containerHeight - margin - 1;
    358  const leftBoundary = containerWidth / 2 + margin;
    359 
    360  let top = bounds.y - containerHeight - arrowSize;
    361  let left = bounds.x + bounds.width / 2;
    362  const bottom = bounds.bottom + arrowSize;
    363  let positionAttribute = "top";
    364 
    365  const canBePlacedOnTop = top >= topBoundary;
    366  const canBePlacedOnBottom = bottomBoundary - bottom > 0;
    367 
    368  if (!canBePlacedOnTop && canBePlacedOnBottom) {
    369    top = bottom;
    370    positionAttribute = "bottom";
    371  }
    372 
    373  let hideArrow = false;
    374  if (top < topBoundary) {
    375    hideArrow = true;
    376    top = topBoundary;
    377  } else if (top > bottomBoundary) {
    378    hideArrow = true;
    379    top = bottomBoundary;
    380  }
    381 
    382  if (left < leftBoundary) {
    383    hideArrow = true;
    384    left = leftBoundary;
    385  }
    386 
    387  if (hideArrow) {
    388    container.setAttribute("hide-arrow", "true");
    389  } else {
    390    container.removeAttribute("hide-arrow");
    391  }
    392 
    393  container.setAttribute(
    394    "style",
    395    `
    396     position: absolute;
    397     transform-origin: 0 0;
    398     transform: scale(${1 / zoom}) translate(calc(${left}px - 50%), ${top}px)`
    399  );
    400 
    401  container.setAttribute("position", positionAttribute);
    402 }
    403 
    404 exports.NodeTabbingOrderHighlighter = NodeTabbingOrderHighlighter;