tor-browser

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

css-grid.js (59239B)


      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 const {
      8  AutoRefreshHighlighter,
      9 } = require("resource://devtools/server/actors/highlighters/auto-refresh.js");
     10 const {
     11  CANVAS_SIZE,
     12  DEFAULT_COLOR,
     13  drawBubbleRect,
     14  drawLine,
     15  drawRect,
     16  drawRoundedRect,
     17  getBoundsFromPoints,
     18  getCurrentMatrix,
     19  getPathDescriptionFromPoints,
     20  getPointsFromDiagonal,
     21  updateCanvasElement,
     22  updateCanvasPosition,
     23 } = require("resource://devtools/server/actors/highlighters/utils/canvas.js");
     24 const {
     25  CanvasFrameAnonymousContentHelper,
     26  getComputedStyle,
     27  moveInfobar,
     28 } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
     29 const { apply } = require("resource://devtools/shared/layout/dom-matrix-2d.js");
     30 const {
     31  getCurrentZoom,
     32  getDisplayPixelRatio,
     33  getWindowDimensions,
     34  setIgnoreLayoutChanges,
     35 } = require("resource://devtools/shared/layout/utils.js");
     36 loader.lazyGetter(this, "HighlightersBundle", () => {
     37  return new Localization(["devtools/shared/highlighters.ftl"], true);
     38 });
     39 
     40 const COLUMNS = "cols";
     41 const ROWS = "rows";
     42 
     43 const GRID_FONT_SIZE = 10;
     44 const GRID_FONT_FAMILY = "sans-serif";
     45 const GRID_AREA_NAME_FONT_SIZE = "20";
     46 
     47 const GRID_LINES_PROPERTIES = {
     48  edge: {
     49    lineDash: [0, 0],
     50    alpha: 1,
     51  },
     52  explicit: {
     53    lineDash: [5, 3],
     54    alpha: 0.75,
     55  },
     56  implicit: {
     57    lineDash: [2, 2],
     58    alpha: 0.5,
     59  },
     60  areaEdge: {
     61    lineDash: [0, 0],
     62    alpha: 1,
     63    lineWidth: 3,
     64  },
     65 };
     66 
     67 const GRID_GAP_PATTERN_WIDTH = 14; // px
     68 const GRID_GAP_PATTERN_HEIGHT = 14; // px
     69 const GRID_GAP_PATTERN_LINE_DASH = [5, 3]; // px
     70 const GRID_GAP_ALPHA = 0.5;
     71 
     72 // This is the minimum distance a line can be to the edge of the document under which we
     73 // push the line number arrow to be inside the grid. This offset is enough to fit the
     74 // entire arrow + a stacked arrow behind it.
     75 const OFFSET_FROM_EDGE = 32;
     76 // This is how much inside the grid we push the arrow. This a factor of the arrow size.
     77 // The goal here is for a row and a column arrow that have both been pushed inside the
     78 // grid, in a corner, not to overlap.
     79 const FLIP_ARROW_INSIDE_FACTOR = 2.5;
     80 
     81 /**
     82 * Given an `edge` of a box, return the name of the edge one move to the right.
     83 */
     84 function rotateEdgeRight(edge) {
     85  switch (edge) {
     86    case "top":
     87      return "right";
     88    case "right":
     89      return "bottom";
     90    case "bottom":
     91      return "left";
     92    case "left":
     93      return "top";
     94    default:
     95      return edge;
     96  }
     97 }
     98 
     99 /**
    100 * Given an `edge` of a box, return the name of the edge one move to the left.
    101 */
    102 function rotateEdgeLeft(edge) {
    103  switch (edge) {
    104    case "top":
    105      return "left";
    106    case "right":
    107      return "top";
    108    case "bottom":
    109      return "right";
    110    case "left":
    111      return "bottom";
    112    default:
    113      return edge;
    114  }
    115 }
    116 
    117 /**
    118 * Given an `edge` of a box, return the name of the opposite edge.
    119 */
    120 function reflectEdge(edge) {
    121  switch (edge) {
    122    case "top":
    123      return "bottom";
    124    case "right":
    125      return "left";
    126    case "bottom":
    127      return "top";
    128    case "left":
    129      return "right";
    130    default:
    131      return edge;
    132  }
    133 }
    134 
    135 /**
    136 * Cached used by `CssGridHighlighter.getGridGapPattern`.
    137 */
    138 const gCachedGridPattern = new Map();
    139 
    140 /**
    141 * The CssGridHighlighter is the class that overlays a visual grid on top of
    142 * display:[inline-]grid elements.
    143 *
    144 * Usage example:
    145 * let h = new CssGridHighlighter(env);
    146 * h.show(node, options);
    147 * h.hide();
    148 * h.destroy();
    149 *
    150 * @param {string} options.color
    151 *        The color that should be used to draw the highlighter for this grid.
    152 * @param {number} options.globalAlpha
    153 *        The alpha (transparency) value that should be used to draw the highlighter for
    154 *        this grid.
    155 * @param {boolean} options.showAllGridAreas
    156 *        Shows all the grid area highlights for the current grid if isShown is
    157 *        true.
    158 * @param {string} options.showGridArea
    159 *        Shows the grid area highlight for the given area name.
    160 * @param {boolean} options.showGridAreasOverlay
    161 *        Displays an overlay of all the grid areas for the current grid
    162 *        container if isShown is true.
    163 * @param {object} options.showGridCell
    164 *        An object containing the grid fragment index, row and column numbers
    165 *        to the corresponding grid cell to highlight for the current grid.
    166 * @param {number} options.showGridCell.gridFragmentIndex
    167 *        Index of the grid fragment to render the grid cell highlight.
    168 * @param {number} options.showGridCell.rowNumber
    169 *        Row number of the grid cell to highlight.
    170 * @param {number} options.showGridCell.columnNumber
    171 *        Column number of the grid cell to highlight.
    172 * @param {object} options.showGridLineNames
    173 *        An object containing the grid fragment index and line number to the
    174 *        corresponding grid line to highlight for the current grid.
    175 * @param {number} options.showGridLineNames.gridFragmentIndex
    176 *        Index of the grid fragment to render the grid line highlight.
    177 * @param {number} options.showGridLineNames.lineNumber
    178 *        Line number of the grid line to highlight.
    179 * @param {string} options.showGridLineNames.type
    180 *        The dimension type of the grid line.
    181 * @param {boolean} options.showGridLineNumbers
    182 *        Displays the grid line numbers on the grid lines if isShown is true.
    183 * @param {boolean} options.showInfiniteLines
    184 *        Displays an infinite line to represent the grid lines if isShown is
    185 *        true.
    186 * @param {number} options.isParent
    187 *        Set to true if this is a "parent" grid, i.e. a grid with a subgrid.
    188 * @param {number} options.zIndex
    189 *        The z-index to decide the displaying order.
    190 *
    191 * Structure:
    192 * <div class="highlighter-container">
    193 *   <canvas id="css-grid-canvas" class="css-grid-canvas">
    194 *   <svg class="css-grid-elements" hidden="true">
    195 *     <g class="css-grid-regions">
    196 *       <path class="css-grid-areas" points="..." />
    197 *       <path class="css-grid-cells" points="..." />
    198 *     </g>
    199 *   </svg>
    200 *   <div class="css-grid-area-infobar-container">
    201 *     <div class="css-grid-infobar">
    202 *       <div class="css-grid-infobar-text">
    203 *         <span class="css-grid-area-infobar-name">Grid Area Name</span>
    204 *         <span class="css-grid-area-infobar-dimensions">Grid Area Dimensions></span>
    205 *       </div>
    206 *     </div>
    207 *   </div>
    208 *   <div class="css-grid-cell-infobar-container">
    209 *     <div class="css-grid-infobar">
    210 *       <div class="css-grid-infobar-text">
    211 *         <span class="css-grid-cell-infobar-position">Grid Cell Position</span>
    212 *         <span class="css-grid-cell-infobar-dimensions">Grid Cell Dimensions></span>
    213 *       </div>
    214 *     </div>
    215 *   <div class="css-grid-line-infobar-container">
    216 *     <div class="css-grid-infobar">
    217 *       <div class="css-grid-infobar-text">
    218 *         <span class="css-grid-line-infobar-number">Grid Line Number</span>
    219 *         <span class="css-grid-line-infobar-names">Grid Line Names></span>
    220 *       </div>
    221 *     </div>
    222 *   </div>
    223 * </div>
    224 */
    225 
    226 class CssGridHighlighter extends AutoRefreshHighlighter {
    227  constructor(highlighterEnv) {
    228    super(highlighterEnv);
    229 
    230    this.markup = new CanvasFrameAnonymousContentHelper(
    231      this.highlighterEnv,
    232      this._buildMarkup.bind(this),
    233      {
    234        contentRootHostClassName: "devtools-highlighter-css-grid",
    235      }
    236    );
    237    this.isReady = this.markup.initialize();
    238 
    239    this.onPageHide = this.onPageHide.bind(this);
    240    this.onWillNavigate = this.onWillNavigate.bind(this);
    241 
    242    this.highlighterEnv.on("will-navigate", this.onWillNavigate);
    243 
    244    const { pageListenerTarget } = highlighterEnv;
    245    pageListenerTarget.addEventListener("pagehide", this.onPageHide);
    246 
    247    // Initialize the <canvas> position to the top left corner of the page.
    248    this._canvasPosition = {
    249      x: 0,
    250      y: 0,
    251    };
    252 
    253    // Calling `updateCanvasPosition` anyway since the highlighter could be initialized
    254    // on a page that has scrolled already.
    255    updateCanvasPosition(
    256      this._canvasPosition,
    257      this._scroll,
    258      this.win,
    259      this._winDimensions
    260    );
    261  }
    262 
    263  _buildMarkup() {
    264    const container = this.markup.createNode({
    265      attributes: {
    266        class: "highlighter-container",
    267      },
    268    });
    269 
    270    this.rootEl = this.markup.createNode({
    271      parent: container,
    272      attributes: {
    273        id: "css-grid-root",
    274        class: "css-grid-root",
    275      },
    276    });
    277 
    278    // We use a <canvas> element so that we can draw an arbitrary number of lines
    279    // which wouldn't be possible with HTML or SVG without having to insert and remove
    280    // the whole markup on every update.
    281    this.markup.createNode({
    282      parent: this.rootEl,
    283      nodeType: "canvas",
    284      attributes: {
    285        id: "css-grid-canvas",
    286        class: "css-grid-canvas",
    287        hidden: "true",
    288        width: CANVAS_SIZE,
    289        height: CANVAS_SIZE,
    290      },
    291    });
    292 
    293    // Build the SVG element.
    294    const svg = this.markup.createSVGNode({
    295      nodeType: "svg",
    296      parent: this.rootEl,
    297      attributes: {
    298        id: "css-grid-elements",
    299        width: "100%",
    300        height: "100%",
    301        hidden: "true",
    302      },
    303    });
    304 
    305    const regions = this.markup.createSVGNode({
    306      nodeType: "g",
    307      parent: svg,
    308      attributes: {
    309        class: "css-grid-regions",
    310      },
    311    });
    312 
    313    this.markup.createSVGNode({
    314      nodeType: "path",
    315      parent: regions,
    316      attributes: {
    317        class: "css-grid-areas",
    318        id: "css-grid-areas",
    319      },
    320    });
    321 
    322    this.markup.createSVGNode({
    323      nodeType: "path",
    324      parent: regions,
    325      attributes: {
    326        class: "css-grid-cells",
    327        id: "css-grid-cells",
    328      },
    329    });
    330 
    331    // Build the grid area infobar markup.
    332    const areaInfobarContainer = this.markup.createNode({
    333      parent: container,
    334      attributes: {
    335        class: "css-grid-area-infobar-container",
    336        id: "css-grid-area-infobar-container",
    337        position: "top",
    338        hidden: "true",
    339      },
    340    });
    341 
    342    const areaInfobar = this.markup.createNode({
    343      parent: areaInfobarContainer,
    344      attributes: {
    345        class: "css-grid-infobar",
    346      },
    347    });
    348 
    349    const areaTextbox = this.markup.createNode({
    350      parent: areaInfobar,
    351      attributes: {
    352        class: "css-grid-infobar-text",
    353      },
    354    });
    355    this.markup.createNode({
    356      nodeType: "span",
    357      parent: areaTextbox,
    358      attributes: {
    359        class: "css-grid-area-infobar-name",
    360        id: "css-grid-area-infobar-name",
    361      },
    362    });
    363    this.markup.createNode({
    364      nodeType: "span",
    365      parent: areaTextbox,
    366      attributes: {
    367        class: "css-grid-area-infobar-dimensions",
    368        id: "css-grid-area-infobar-dimensions",
    369      },
    370    });
    371 
    372    // Build the grid cell infobar markup.
    373    const cellInfobarContainer = this.markup.createNode({
    374      parent: container,
    375      attributes: {
    376        class: "css-grid-cell-infobar-container",
    377        id: "css-grid-cell-infobar-container",
    378        position: "top",
    379        hidden: "true",
    380      },
    381    });
    382 
    383    const cellInfobar = this.markup.createNode({
    384      parent: cellInfobarContainer,
    385      attributes: {
    386        class: "css-grid-infobar",
    387      },
    388    });
    389 
    390    const cellTextbox = this.markup.createNode({
    391      parent: cellInfobar,
    392      attributes: {
    393        class: "css-grid-infobar-text",
    394      },
    395    });
    396    this.markup.createNode({
    397      nodeType: "span",
    398      parent: cellTextbox,
    399      attributes: {
    400        class: "css-grid-cell-infobar-position",
    401        id: "css-grid-cell-infobar-position",
    402      },
    403    });
    404    this.markup.createNode({
    405      nodeType: "span",
    406      parent: cellTextbox,
    407      attributes: {
    408        class: "css-grid-cell-infobar-dimensions",
    409        id: "css-grid-cell-infobar-dimensions",
    410      },
    411    });
    412 
    413    // Build the grid line infobar markup.
    414    const lineInfobarContainer = this.markup.createNode({
    415      parent: container,
    416      attributes: {
    417        class: "css-grid-line-infobar-container",
    418        id: "css-grid-line-infobar-container",
    419        position: "top",
    420        hidden: "true",
    421      },
    422    });
    423 
    424    const lineInfobar = this.markup.createNode({
    425      parent: lineInfobarContainer,
    426      attributes: {
    427        class: "css-grid-infobar",
    428      },
    429    });
    430 
    431    const lineTextbox = this.markup.createNode({
    432      parent: lineInfobar,
    433      attributes: {
    434        class: "css-grid-infobar-text",
    435      },
    436    });
    437    this.markup.createNode({
    438      nodeType: "span",
    439      parent: lineTextbox,
    440      attributes: {
    441        class: "css-grid-line-infobar-number",
    442        id: "css-grid-line-infobar-number",
    443      },
    444    });
    445    this.markup.createNode({
    446      nodeType: "span",
    447      parent: lineTextbox,
    448      attributes: {
    449        class: "css-grid-line-infobar-names",
    450        id: "css-grid-line-infobar-names",
    451      },
    452    });
    453 
    454    return container;
    455  }
    456 
    457  clearCache() {
    458    gCachedGridPattern.clear();
    459  }
    460 
    461  /**
    462   * Clear the grid area highlights.
    463   */
    464  clearGridAreas() {
    465    const areas = this.getElement("css-grid-areas");
    466    areas.setAttribute("d", "");
    467  }
    468 
    469  /**
    470   * Clear the grid cell highlights.
    471   */
    472  clearGridCell() {
    473    const cells = this.getElement("css-grid-cells");
    474    cells.setAttribute("d", "");
    475  }
    476 
    477  destroy() {
    478    const { highlighterEnv } = this;
    479    highlighterEnv.off("will-navigate", this.onWillNavigate);
    480 
    481    const { pageListenerTarget } = highlighterEnv;
    482    if (pageListenerTarget) {
    483      pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
    484    }
    485 
    486    this.markup.destroy();
    487    this.rootEl = null;
    488 
    489    // Clear the pattern cache to avoid dead object exceptions (Bug 1342051).
    490    this.clearCache();
    491    AutoRefreshHighlighter.prototype.destroy.call(this);
    492  }
    493 
    494  get canvas() {
    495    return this.getElement("css-grid-canvas");
    496  }
    497 
    498  get color() {
    499    return this.options.color || DEFAULT_COLOR;
    500  }
    501 
    502  get ctx() {
    503    return this.canvas.getCanvasContext("2d");
    504  }
    505 
    506  get globalAlpha() {
    507    return this.options.globalAlpha || 1;
    508  }
    509 
    510  getElement(id) {
    511    return this.markup.getElement(id);
    512  }
    513 
    514  getFirstColLinePos(fragment) {
    515    return fragment.cols.lines[0].start;
    516  }
    517 
    518  getFirstRowLinePos(fragment) {
    519    return fragment.rows.lines[0].start;
    520  }
    521 
    522  /**
    523   * Gets the grid gap pattern used to render the gap regions based on the device
    524   * pixel ratio given.
    525   *
    526   * @param  {number} devicePixelRatio
    527   *         The device pixel ratio we want the pattern for.
    528   * @param  {object} dimension
    529   *         Refers to the Map key for the grid dimension type which is either the
    530   *         constant COLUMNS or ROWS.
    531   * @return {CanvasPattern} grid gap pattern.
    532   */
    533  getGridGapPattern(devicePixelRatio, dimension) {
    534    let gridPatternMap = null;
    535 
    536    if (gCachedGridPattern.has(devicePixelRatio)) {
    537      gridPatternMap = gCachedGridPattern.get(devicePixelRatio);
    538    } else {
    539      gridPatternMap = new Map();
    540    }
    541 
    542    if (gridPatternMap.has(dimension)) {
    543      return gridPatternMap.get(dimension);
    544    }
    545 
    546    // Create the diagonal lines pattern for the rendering the grid gaps.
    547    const canvas = this.markup.createNode({ nodeType: "canvas" });
    548    const width = (canvas.width = GRID_GAP_PATTERN_WIDTH * devicePixelRatio);
    549    const height = (canvas.height = GRID_GAP_PATTERN_HEIGHT * devicePixelRatio);
    550 
    551    const ctx = canvas.getContext("2d");
    552    ctx.save();
    553    ctx.setLineDash(GRID_GAP_PATTERN_LINE_DASH);
    554    ctx.beginPath();
    555    ctx.translate(0.5, 0.5);
    556 
    557    if (dimension === COLUMNS) {
    558      ctx.moveTo(0, 0);
    559      ctx.lineTo(width, height);
    560    } else {
    561      ctx.moveTo(width, 0);
    562      ctx.lineTo(0, height);
    563    }
    564 
    565    ctx.strokeStyle = this.color;
    566    ctx.globalAlpha = GRID_GAP_ALPHA * this.globalAlpha;
    567    ctx.stroke();
    568    ctx.restore();
    569 
    570    const pattern = ctx.createPattern(canvas, "repeat");
    571 
    572    gridPatternMap.set(dimension, pattern);
    573    gCachedGridPattern.set(devicePixelRatio, gridPatternMap);
    574 
    575    return pattern;
    576  }
    577 
    578  getLastColLinePos(fragment) {
    579    return fragment.cols.lines[fragment.cols.lines.length - 1].start;
    580  }
    581 
    582  /**
    583   * Get the GridLine index of the last edge of the explicit grid for a grid dimension.
    584   *
    585   * @param  {GridTracks} tracks
    586   *         The grid track of a given grid dimension.
    587   * @return {number} index of the last edge of the explicit grid for a grid dimension.
    588   */
    589  getLastEdgeLineIndex(tracks) {
    590    let trackIndex = tracks.length - 1;
    591 
    592    // Traverse the grid track backwards until we find an explicit track.
    593    while (trackIndex >= 0 && tracks[trackIndex].type != "explicit") {
    594      trackIndex--;
    595    }
    596 
    597    // The grid line index is the grid track index + 1.
    598    return trackIndex + 1;
    599  }
    600 
    601  getLastRowLinePos(fragment) {
    602    return fragment.rows.lines[fragment.rows.lines.length - 1].start;
    603  }
    604 
    605  getNode(id) {
    606    return this.markup.content.root.getElementById(id);
    607  }
    608 
    609  /**
    610   * The AutoRefreshHighlighter's _hasMoved method returns true only if the
    611   * element's quads have changed. Override it so it also returns true if the
    612   * element's grid has changed (which can happen when you change the
    613   * grid-template-* CSS properties with the highlighter displayed). This
    614   * check is prone to false positives, because it does a direct object
    615   * comparison of the first grid fragment structure. This structure is
    616   * generated by the first call to getGridFragments, and on any subsequent
    617   * calls where a reflow is needed. Since a reflow is needed when the CSS
    618   * changes, this will correctly detect that the grid structure has changed.
    619   * However, it's possible that the reflow could generate a novel grid
    620   * fragment object containing information that is unchanged -- a false
    621   * positive.
    622   */
    623  _hasMoved() {
    624    const hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
    625 
    626    const oldFirstGridFragment = this.gridData?.[0];
    627    this.gridData = this.currentNode.getGridFragments();
    628    const newFirstGridFragment = this.gridData[0];
    629 
    630    return hasMoved || oldFirstGridFragment !== newFirstGridFragment;
    631  }
    632 
    633  /**
    634   * Hide the highlighter, the canvas and the infobars.
    635   */
    636  _hide() {
    637    setIgnoreLayoutChanges(true);
    638    this._hideGrid();
    639    this._hideGridElements();
    640    this._hideGridAreaInfoBar();
    641    this._hideGridCellInfoBar();
    642    this._hideGridLineInfoBar();
    643    setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
    644  }
    645 
    646  _hideGrid() {
    647    this.getElement("css-grid-canvas").setAttribute("hidden", "true");
    648  }
    649 
    650  _hideGridAreaInfoBar() {
    651    this.getElement("css-grid-area-infobar-container").setAttribute(
    652      "hidden",
    653      "true"
    654    );
    655  }
    656 
    657  _hideGridCellInfoBar() {
    658    this.getElement("css-grid-cell-infobar-container").setAttribute(
    659      "hidden",
    660      "true"
    661    );
    662  }
    663 
    664  _hideGridElements() {
    665    this.getElement("css-grid-elements").setAttribute("hidden", "true");
    666  }
    667 
    668  _hideGridLineInfoBar() {
    669    this.getElement("css-grid-line-infobar-container").setAttribute(
    670      "hidden",
    671      "true"
    672    );
    673  }
    674 
    675  /**
    676   * Checks if the current node has a CSS Grid layout.
    677   *
    678   * @return {boolean} true if the current node has a CSS grid layout, false otherwise.
    679   */
    680  isGrid() {
    681    return this.currentNode.hasGridFragments();
    682  }
    683 
    684  /**
    685   * Is a given grid fragment valid? i.e. does it actually have tracks? In some cases, we
    686   * may have a fragment that defines column tracks but doesn't have any rows (or vice
    687   * versa). In which case we do not want to draw anything for that fragment.
    688   *
    689   * @param  {object} fragment
    690   * @return {boolean}
    691   */
    692  isValidFragment(fragment) {
    693    return fragment.cols.tracks.length && fragment.rows.tracks.length;
    694  }
    695 
    696  /**
    697   * The <canvas>'s position needs to be updated if the page scrolls too much, in order
    698   * to give the illusion that it always covers the viewport.
    699   */
    700  _scrollUpdate() {
    701    const hasUpdated = updateCanvasPosition(
    702      this._canvasPosition,
    703      this._scroll,
    704      this.win,
    705      this._winDimensions
    706    );
    707 
    708    if (hasUpdated) {
    709      this._update();
    710    }
    711  }
    712 
    713  _show() {
    714    if (!this.isGrid()) {
    715      this.hide();
    716      return false;
    717    }
    718 
    719    // The grid pattern cache should be cleared in case the color changed.
    720    this.clearCache();
    721 
    722    // Hide the canvas, grid element highlights and infobar.
    723    this._hide();
    724 
    725    this.getElement("css-grid-root").setAttribute(
    726      "data-is-parent-grid",
    727      !!this.options.isParent
    728    );
    729 
    730    // Set z-index.
    731    this.markup.content.root.firstElementChild.style.setProperty(
    732      "z-index",
    733      this.options.zIndex
    734    );
    735 
    736    // Update the grid color
    737    this.markup.content.root.firstElementChild.style.setProperty(
    738      "--grid-color",
    739      this.color
    740    );
    741 
    742    return this._update();
    743  }
    744 
    745  _showGrid() {
    746    this.getElement("css-grid-canvas").removeAttribute("hidden");
    747  }
    748 
    749  _showGridAreaInfoBar() {
    750    this.getElement("css-grid-area-infobar-container").removeAttribute(
    751      "hidden"
    752    );
    753  }
    754 
    755  _showGridCellInfoBar() {
    756    this.getElement("css-grid-cell-infobar-container").removeAttribute(
    757      "hidden"
    758    );
    759  }
    760 
    761  _showGridElements() {
    762    this.getElement("css-grid-elements").removeAttribute("hidden");
    763  }
    764 
    765  _showGridLineInfoBar() {
    766    this.getElement("css-grid-line-infobar-container").removeAttribute(
    767      "hidden"
    768    );
    769  }
    770 
    771  /**
    772   * Shows all the grid area highlights for the current grid.
    773   */
    774  showAllGridAreas() {
    775    this.renderGridArea();
    776  }
    777 
    778  /**
    779   * Shows the grid area highlight for the given area name.
    780   *
    781   * @param  {string} areaName
    782   *         Grid area name.
    783   */
    784  showGridArea(areaName) {
    785    this.renderGridArea(areaName);
    786  }
    787 
    788  /**
    789   * Shows the grid cell highlight for the given grid cell options.
    790   *
    791   * @param  {number} options.gridFragmentIndex
    792   *         Index of the grid fragment to render the grid cell highlight.
    793   * @param  {number} options.rowNumber
    794   *         Row number of the grid cell to highlight.
    795   * @param  {number} options.columnNumber
    796   *         Column number of the grid cell to highlight.
    797   */
    798  showGridCell({ gridFragmentIndex, rowNumber, columnNumber }) {
    799    this.renderGridCell(gridFragmentIndex, rowNumber, columnNumber);
    800  }
    801 
    802  /**
    803   * Shows the grid line highlight for the given grid line options.
    804   *
    805   * @param  {number} options.gridFragmentIndex
    806   *         Index of the grid fragment to render the grid line highlight.
    807   * @param  {number} options.lineNumber
    808   *         Line number of the grid line to highlight.
    809   * @param  {string} options.type
    810   *         The dimension type of the grid line.
    811   */
    812  showGridLineNames({ gridFragmentIndex, lineNumber, type }) {
    813    this.renderGridLineNames(gridFragmentIndex, lineNumber, type);
    814  }
    815 
    816  /**
    817   * If a page hide event is triggered for current window's highlighter, hide the
    818   * highlighter.
    819   */
    820  onPageHide({ target }) {
    821    if (target.defaultView === this.win) {
    822      this.hide();
    823    }
    824  }
    825 
    826  /**
    827   * Called when the page will-navigate. Used to hide the grid highlighter and clear
    828   * the cached gap patterns and avoid using DeadWrapper obejcts as gap patterns the
    829   * next time.
    830   */
    831  onWillNavigate({ isTopLevel }) {
    832    this.clearCache();
    833 
    834    if (isTopLevel) {
    835      this.hide();
    836    }
    837  }
    838 
    839  renderFragment(fragment) {
    840    if (!this.isValidFragment(fragment)) {
    841      return;
    842    }
    843 
    844    this.renderLines(
    845      fragment.cols,
    846      COLUMNS,
    847      this.getFirstRowLinePos(fragment),
    848      this.getLastRowLinePos(fragment)
    849    );
    850    this.renderLines(
    851      fragment.rows,
    852      ROWS,
    853      this.getFirstColLinePos(fragment),
    854      this.getLastColLinePos(fragment)
    855    );
    856 
    857    if (this.options.showGridAreasOverlay) {
    858      this.renderGridAreaOverlay();
    859    }
    860 
    861    // Line numbers are rendered in a 2nd step to avoid overlapping with existing lines.
    862    if (this.options.showGridLineNumbers) {
    863      this.renderLineNumbers(
    864        fragment.cols,
    865        COLUMNS,
    866        this.getFirstRowLinePos(fragment)
    867      );
    868      this.renderLineNumbers(
    869        fragment.rows,
    870        ROWS,
    871        this.getFirstColLinePos(fragment)
    872      );
    873      this.renderNegativeLineNumbers(
    874        fragment.cols,
    875        COLUMNS,
    876        this.getLastRowLinePos(fragment)
    877      );
    878      this.renderNegativeLineNumbers(
    879        fragment.rows,
    880        ROWS,
    881        this.getLastColLinePos(fragment)
    882      );
    883    }
    884  }
    885 
    886  /**
    887   * Render the grid area highlight for the given area name or for all the grid areas.
    888   *
    889   * @param  {string} areaName
    890   *         Name of the grid area to be highlighted. If no area name is provided, all
    891   *         the grid areas should be highlighted.
    892   */
    893  renderGridArea(areaName) {
    894    const { devicePixelRatio } = this.win;
    895    const displayPixelRatio = getDisplayPixelRatio(this.win);
    896    const paths = [];
    897 
    898    for (let i = 0; i < this.gridData.length; i++) {
    899      const fragment = this.gridData[i];
    900 
    901      for (const area of fragment.areas) {
    902        if (areaName && areaName != area.name) {
    903          continue;
    904        }
    905 
    906        const rowStart = fragment.rows.lines[area.rowStart - 1];
    907        const rowEnd = fragment.rows.lines[area.rowEnd - 1];
    908        const columnStart = fragment.cols.lines[area.columnStart - 1];
    909        const columnEnd = fragment.cols.lines[area.columnEnd - 1];
    910 
    911        const x1 = columnStart.start + columnStart.breadth;
    912        const y1 = rowStart.start + rowStart.breadth;
    913        const x2 = columnEnd.start;
    914        const y2 = rowEnd.start;
    915 
    916        const points = getPointsFromDiagonal(
    917          x1,
    918          y1,
    919          x2,
    920          y2,
    921          this.currentMatrix
    922        );
    923 
    924        // Scale down by `devicePixelRatio` since SVG element already take them into
    925        // account.
    926        const svgPoints = points.map(point => ({
    927          x: Math.round(point.x / devicePixelRatio),
    928          y: Math.round(point.y / devicePixelRatio),
    929        }));
    930 
    931        // Scale down by `displayPixelRatio` since infobar's HTML elements already take it
    932        // into account; and the zoom scaling is handled by `moveInfobar`.
    933        const bounds = getBoundsFromPoints(
    934          points.map(point => ({
    935            x: Math.round(point.x / displayPixelRatio),
    936            y: Math.round(point.y / displayPixelRatio),
    937          }))
    938        );
    939 
    940        paths.push(getPathDescriptionFromPoints(svgPoints));
    941 
    942        // Update and show the info bar when only displaying a single grid area.
    943        if (areaName) {
    944          this._showGridAreaInfoBar();
    945          this._updateGridAreaInfobar(area, bounds);
    946        }
    947      }
    948    }
    949 
    950    const areas = this.getElement("css-grid-areas");
    951    areas.setAttribute("d", paths.join(" "));
    952  }
    953 
    954  /**
    955   * Render grid area name on the containing grid area cell.
    956   *
    957   * @param  {object} fragment
    958   *         The grid fragment of the grid container.
    959   * @param  {object} area
    960   *         The area overlay to render on the CSS highlighter canvas.
    961   */
    962  renderGridAreaName(fragment, area) {
    963    const { rowStart, rowEnd, columnStart, columnEnd } = area;
    964    const { devicePixelRatio } = this.win;
    965    const displayPixelRatio = getDisplayPixelRatio(this.win);
    966    const offset = (displayPixelRatio / 2) % 1;
    967    let fontSize = GRID_AREA_NAME_FONT_SIZE * displayPixelRatio;
    968    const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
    969    const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
    970 
    971    this.ctx.save();
    972    this.ctx.translate(offset - canvasX, offset - canvasY);
    973    this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
    974    this.ctx.globalAlpha = this.globalAlpha;
    975    this.ctx.strokeStyle = this.color;
    976    this.ctx.textAlign = "center";
    977    this.ctx.textBaseline = "middle";
    978 
    979    // Draw the text for the grid area name.
    980    for (let rowNumber = rowStart; rowNumber < rowEnd; rowNumber++) {
    981      for (
    982        let columnNumber = columnStart;
    983        columnNumber < columnEnd;
    984        columnNumber++
    985      ) {
    986        const row = fragment.rows.tracks[rowNumber - 1];
    987        const column = fragment.cols.tracks[columnNumber - 1];
    988 
    989        // If the font size exceeds the bounds of the containing grid cell, size it its
    990        // row or column dimension, whichever is smallest.
    991        if (
    992          fontSize > column.breadth * displayPixelRatio ||
    993          fontSize > row.breadth * displayPixelRatio
    994        ) {
    995          fontSize = Math.min([column.breadth, row.breadth]);
    996          this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
    997        }
    998 
    999        const textWidth = this.ctx.measureText(area.name).width;
   1000        // The width of the character 'm' approximates the height of the text.
   1001        const textHeight = this.ctx.measureText("m").width;
   1002        // Padding in pixels for the line number text inside of the line number container.
   1003        const padding = 3 * displayPixelRatio;
   1004 
   1005        const boxWidth = textWidth + 2 * padding;
   1006        const boxHeight = textHeight + 2 * padding;
   1007 
   1008        let x = column.start + column.breadth / 2;
   1009        let y = row.start + row.breadth / 2;
   1010 
   1011        [x, y] = apply(this.currentMatrix, [x, y]);
   1012 
   1013        const rectXPos = x - boxWidth / 2;
   1014        const rectYPos = y - boxHeight / 2;
   1015 
   1016        // Draw a rounded rectangle with a border width of 1 pixel,
   1017        // a border color matching the grid color, and a white background.
   1018        this.ctx.lineWidth = 1 * displayPixelRatio;
   1019        this.ctx.strokeStyle = this.color;
   1020        this.ctx.fillStyle = "white";
   1021        const radius = 2 * displayPixelRatio;
   1022        drawRoundedRect(
   1023          this.ctx,
   1024          rectXPos,
   1025          rectYPos,
   1026          boxWidth,
   1027          boxHeight,
   1028          radius
   1029        );
   1030 
   1031        this.ctx.fillStyle = this.color;
   1032        this.ctx.fillText(area.name, x, y + padding);
   1033      }
   1034    }
   1035 
   1036    this.ctx.restore();
   1037  }
   1038 
   1039  /**
   1040   * Renders the grid area overlay on the css grid highlighter canvas.
   1041   */
   1042  renderGridAreaOverlay() {
   1043    const padding = 1;
   1044 
   1045    for (let i = 0; i < this.gridData.length; i++) {
   1046      const fragment = this.gridData[i];
   1047 
   1048      for (const area of fragment.areas) {
   1049        const { rowStart, rowEnd, columnStart, columnEnd, type } = area;
   1050 
   1051        if (type === "implicit") {
   1052          continue;
   1053        }
   1054 
   1055        // Draw the line edges for the grid area.
   1056        const areaColStart = fragment.cols.lines[columnStart - 1];
   1057        const areaColEnd = fragment.cols.lines[columnEnd - 1];
   1058 
   1059        const areaRowStart = fragment.rows.lines[rowStart - 1];
   1060        const areaRowEnd = fragment.rows.lines[rowEnd - 1];
   1061 
   1062        const areaColStartLinePos = areaColStart.start + areaColStart.breadth;
   1063        const areaRowStartLinePos = areaRowStart.start + areaRowStart.breadth;
   1064 
   1065        this.renderLine(
   1066          areaColStartLinePos + padding,
   1067          areaRowStartLinePos,
   1068          areaRowEnd.start,
   1069          COLUMNS,
   1070          "areaEdge"
   1071        );
   1072        this.renderLine(
   1073          areaColEnd.start - padding,
   1074          areaRowStartLinePos,
   1075          areaRowEnd.start,
   1076          COLUMNS,
   1077          "areaEdge"
   1078        );
   1079 
   1080        this.renderLine(
   1081          areaRowStartLinePos + padding,
   1082          areaColStartLinePos,
   1083          areaColEnd.start,
   1084          ROWS,
   1085          "areaEdge"
   1086        );
   1087        this.renderLine(
   1088          areaRowEnd.start - padding,
   1089          areaColStartLinePos,
   1090          areaColEnd.start,
   1091          ROWS,
   1092          "areaEdge"
   1093        );
   1094 
   1095        this.renderGridAreaName(fragment, area);
   1096      }
   1097    }
   1098  }
   1099 
   1100  /**
   1101   * Render the grid cell highlight for the given grid fragment index, row and column
   1102   * number.
   1103   *
   1104   * @param  {number} gridFragmentIndex
   1105   *         Index of the grid fragment to render the grid cell highlight.
   1106   * @param  {number} rowNumber
   1107   *         Row number of the grid cell to highlight.
   1108   * @param  {number} columnNumber
   1109   *         Column number of the grid cell to highlight.
   1110   */
   1111  renderGridCell(gridFragmentIndex, rowNumber, columnNumber) {
   1112    const fragment = this.gridData[gridFragmentIndex];
   1113 
   1114    if (!fragment) {
   1115      return;
   1116    }
   1117 
   1118    const row = fragment.rows.tracks[rowNumber - 1];
   1119    const column = fragment.cols.tracks[columnNumber - 1];
   1120 
   1121    if (!row || !column) {
   1122      return;
   1123    }
   1124 
   1125    const x1 = column.start;
   1126    const y1 = row.start;
   1127    const x2 = column.start + column.breadth;
   1128    const y2 = row.start + row.breadth;
   1129 
   1130    const { devicePixelRatio } = this.win;
   1131    const displayPixelRatio = getDisplayPixelRatio(this.win);
   1132    const points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix);
   1133 
   1134    // Scale down by `devicePixelRatio` since SVG element already take them into account.
   1135    const svgPoints = points.map(point => ({
   1136      x: Math.round(point.x / devicePixelRatio),
   1137      y: Math.round(point.y / devicePixelRatio),
   1138    }));
   1139 
   1140    // Scale down by `displayPixelRatio` since infobar's HTML elements already take it
   1141    // into account, and the zoom scaling is handled by `moveInfobar`.
   1142    const bounds = getBoundsFromPoints(
   1143      points.map(point => ({
   1144        x: Math.round(point.x / displayPixelRatio),
   1145        y: Math.round(point.y / displayPixelRatio),
   1146      }))
   1147    );
   1148 
   1149    const cells = this.getElement("css-grid-cells");
   1150    cells.setAttribute("d", getPathDescriptionFromPoints(svgPoints));
   1151 
   1152    this._showGridCellInfoBar();
   1153    this._updateGridCellInfobar(rowNumber, columnNumber, bounds);
   1154  }
   1155 
   1156  /**
   1157   * Render the grid gap area on the css grid highlighter canvas.
   1158   *
   1159   * @param  {number} linePos
   1160   *         The line position along the x-axis for a column grid line and
   1161   *         y-axis for a row grid line.
   1162   * @param  {number} startPos
   1163   *         The start position of the cross side of the grid line.
   1164   * @param  {number} endPos
   1165   *         The end position of the cross side of the grid line.
   1166   * @param  {number} breadth
   1167   *         The grid line breadth value.
   1168   * @param  {string} dimensionType
   1169   *         The grid dimension type which is either the constant COLUMNS or ROWS.
   1170   */
   1171  renderGridGap(linePos, startPos, endPos, breadth, dimensionType) {
   1172    const { devicePixelRatio } = this.win;
   1173    const displayPixelRatio = getDisplayPixelRatio(this.win);
   1174    const offset = (displayPixelRatio / 2) % 1;
   1175    const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
   1176    const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
   1177 
   1178    linePos = Math.round(linePos);
   1179    startPos = Math.round(startPos);
   1180    breadth = Math.round(breadth);
   1181 
   1182    this.ctx.save();
   1183    this.ctx.fillStyle = this.getGridGapPattern(
   1184      devicePixelRatio,
   1185      dimensionType
   1186    );
   1187    this.ctx.translate(offset - canvasX, offset - canvasY);
   1188 
   1189    if (dimensionType === COLUMNS) {
   1190      if (isFinite(endPos)) {
   1191        endPos = Math.round(endPos);
   1192      } else {
   1193        endPos = this._winDimensions.height;
   1194        startPos = -endPos;
   1195      }
   1196      drawRect(
   1197        this.ctx,
   1198        linePos,
   1199        startPos,
   1200        linePos + breadth,
   1201        endPos,
   1202        this.currentMatrix
   1203      );
   1204    } else {
   1205      if (isFinite(endPos)) {
   1206        endPos = Math.round(endPos);
   1207      } else {
   1208        endPos = this._winDimensions.width;
   1209        startPos = -endPos;
   1210      }
   1211      drawRect(
   1212        this.ctx,
   1213        startPos,
   1214        linePos,
   1215        endPos,
   1216        linePos + breadth,
   1217        this.currentMatrix
   1218      );
   1219    }
   1220 
   1221    // Find current angle of grid by measuring the angle of two arbitrary points,
   1222    // then rotate canvas, so the hash pattern stays 45deg to the gridlines.
   1223    const p1 = apply(this.currentMatrix, [0, 0]);
   1224    const p2 = apply(this.currentMatrix, [1, 0]);
   1225    const angleRad = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]);
   1226    this.ctx.rotate(angleRad);
   1227 
   1228    this.ctx.fill();
   1229    this.ctx.restore();
   1230  }
   1231 
   1232  /**
   1233   * Render the grid line name highlight for the given grid fragment index, lineNumber,
   1234   * and dimensionType.
   1235   *
   1236   * @param  {number} gridFragmentIndex
   1237   *         Index of the grid fragment to render the grid line highlight.
   1238   * @param  {number} lineNumber
   1239   *         Line number of the grid line to highlight.
   1240   * @param  {string} dimensionType
   1241   *         The dimension type of the grid line.
   1242   */
   1243  renderGridLineNames(gridFragmentIndex, lineNumber, dimensionType) {
   1244    const fragment = this.gridData[gridFragmentIndex];
   1245 
   1246    if (!fragment || !lineNumber || !dimensionType) {
   1247      return;
   1248    }
   1249 
   1250    const { names } = fragment[dimensionType].lines[lineNumber - 1];
   1251    let linePos;
   1252 
   1253    if (dimensionType === ROWS) {
   1254      linePos = fragment.rows.lines[lineNumber - 1];
   1255    } else if (dimensionType === COLUMNS) {
   1256      linePos = fragment.cols.lines[lineNumber - 1];
   1257    }
   1258 
   1259    if (!linePos) {
   1260      return;
   1261    }
   1262 
   1263    const currentZoom = getCurrentZoom(this.win);
   1264    const { bounds } = this.currentQuads.content[gridFragmentIndex];
   1265 
   1266    const rowYPosition = fragment.rows.lines[0];
   1267    const colXPosition = fragment.rows.lines[0];
   1268 
   1269    const x =
   1270      dimensionType === COLUMNS
   1271        ? linePos.start + bounds.left / currentZoom
   1272        : colXPosition.start + bounds.left / currentZoom;
   1273 
   1274    const y =
   1275      dimensionType === ROWS
   1276        ? linePos.start + bounds.top / currentZoom
   1277        : rowYPosition.start + bounds.top / currentZoom;
   1278 
   1279    this._showGridLineInfoBar();
   1280    this._updateGridLineInfobar(names.join(", "), lineNumber, x, y);
   1281  }
   1282 
   1283  /**
   1284   * Render the grid line number on the css grid highlighter canvas.
   1285   *
   1286   * @param  {number} lineNumber
   1287   *         The grid line number.
   1288   * @param  {number} linePos
   1289   *         The line position along the x-axis for a column grid line and
   1290   *         y-axis for a row grid line.
   1291   * @param  {number} startPos
   1292   *         The start position of the cross side of the grid line.
   1293   * @param  {number} breadth
   1294   *         The grid line breadth value.
   1295   * @param  {string} dimensionType
   1296   *         The grid dimension type which is either the constant COLUMNS or ROWS.
   1297   * @param  {Boolean||undefined} isStackedLine
   1298   *         Boolean indicating if the line is stacked.
   1299   */
   1300  // eslint-disable-next-line complexity
   1301  renderGridLineNumber(
   1302    lineNumber,
   1303    linePos,
   1304    startPos,
   1305    breadth,
   1306    dimensionType,
   1307    isStackedLine
   1308  ) {
   1309    const displayPixelRatio = getDisplayPixelRatio(this.win);
   1310    const { devicePixelRatio } = this.win;
   1311    const offset = (displayPixelRatio / 2) % 1;
   1312    const fontSize = GRID_FONT_SIZE * devicePixelRatio;
   1313    const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
   1314    const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
   1315 
   1316    linePos = Math.round(linePos);
   1317    startPos = Math.round(startPos);
   1318    breadth = Math.round(breadth);
   1319 
   1320    if (linePos + breadth < 0) {
   1321      // Don't render the line number since the line is not visible on screen.
   1322      return;
   1323    }
   1324 
   1325    this.ctx.save();
   1326    this.ctx.translate(offset - canvasX, offset - canvasY);
   1327    this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
   1328 
   1329    // For a general grid box, the height of the character "m" will be its minimum width
   1330    // and height. If line number's text width is greater, then use the grid box's text
   1331    // width instead.
   1332    const textHeight = this.ctx.measureText("m").width;
   1333    const textWidth = Math.max(
   1334      textHeight,
   1335      this.ctx.measureText(lineNumber).width
   1336    );
   1337 
   1338    // Padding in pixels for the line number text inside of the line number container.
   1339    const padding = 3 * devicePixelRatio;
   1340    const offsetFromEdge = 2 * devicePixelRatio;
   1341 
   1342    let boxWidth = textWidth + 2 * padding;
   1343    let boxHeight = textHeight + 2 * padding;
   1344 
   1345    // Calculate the x & y coordinates for the line number container, so that its arrow
   1346    // tip is centered on the line (or the gap if there is one), and is offset by the
   1347    // calculated padding value from the grid container edge.
   1348    let x, y;
   1349 
   1350    if (dimensionType === COLUMNS) {
   1351      x = linePos + breadth / 2;
   1352      y =
   1353        lineNumber > 0 ? startPos - offsetFromEdge : startPos + offsetFromEdge;
   1354    } else if (dimensionType === ROWS) {
   1355      y = linePos + breadth / 2;
   1356      x =
   1357        lineNumber > 0 ? startPos - offsetFromEdge : startPos + offsetFromEdge;
   1358    }
   1359 
   1360    [x, y] = apply(this.currentMatrix, [x, y]);
   1361 
   1362    // Draw a bubble rectangular arrow with a border width of 2 pixels, a border color
   1363    // matching the grid color and a white background (the line number will be written in
   1364    // black).
   1365    this.ctx.lineWidth = 2 * displayPixelRatio;
   1366    this.ctx.strokeStyle = this.color;
   1367    this.ctx.fillStyle = "white";
   1368    this.ctx.globalAlpha = this.globalAlpha;
   1369 
   1370    // See param definitions of drawBubbleRect.
   1371    const radius = 2 * displayPixelRatio;
   1372    const margin = 2 * displayPixelRatio;
   1373    const arrowSize = 8 * displayPixelRatio;
   1374 
   1375    const minBoxSize = arrowSize * 2 + padding;
   1376    boxWidth = Math.max(boxWidth, minBoxSize);
   1377    boxHeight = Math.max(boxHeight, minBoxSize);
   1378 
   1379    // Determine which edge of the box to aim the line number arrow at.
   1380    const boxEdge = this.getBoxEdge(dimensionType, lineNumber);
   1381 
   1382    let { width, height } = this._winDimensions;
   1383    width *= displayPixelRatio;
   1384    height *= displayPixelRatio;
   1385 
   1386    // Don't draw if the line is out of the viewport.
   1387    if (
   1388      (dimensionType === ROWS && (y < 0 || y > height)) ||
   1389      (dimensionType === COLUMNS && (x < 0 || x > width))
   1390    ) {
   1391      this.ctx.restore();
   1392      return;
   1393    }
   1394 
   1395    // If the arrow's edge (the one perpendicular to the line direction) is too close to
   1396    // the edge of the viewport. Push the arrow inside the grid.
   1397    const minOffsetFromEdge = OFFSET_FROM_EDGE * displayPixelRatio;
   1398    switch (boxEdge) {
   1399      case "left":
   1400        if (x < minOffsetFromEdge) {
   1401          x += FLIP_ARROW_INSIDE_FACTOR * boxWidth;
   1402        }
   1403        break;
   1404      case "right":
   1405        if (width - x < minOffsetFromEdge) {
   1406          x -= FLIP_ARROW_INSIDE_FACTOR * boxWidth;
   1407        }
   1408        break;
   1409      case "top":
   1410        if (y < minOffsetFromEdge) {
   1411          y += FLIP_ARROW_INSIDE_FACTOR * boxHeight;
   1412        }
   1413        break;
   1414      case "bottom":
   1415        if (height - y < minOffsetFromEdge) {
   1416          y -= FLIP_ARROW_INSIDE_FACTOR * boxHeight;
   1417        }
   1418        break;
   1419    }
   1420 
   1421    // Offset stacked line numbers by a quarter of the box's width/height, so a part of
   1422    // them remains visible behind the number that sits at the top of the stack.
   1423    if (isStackedLine) {
   1424      const xOffset = boxWidth / 4;
   1425      const yOffset = boxHeight / 4;
   1426 
   1427      if (lineNumber > 0) {
   1428        x -= xOffset;
   1429        y -= yOffset;
   1430      } else {
   1431        x += xOffset;
   1432        y += yOffset;
   1433      }
   1434    }
   1435 
   1436    // If one the edges of the arrow that's parallel to the line is too close to the edge
   1437    // of the viewport (and therefore partly hidden), grow the arrow's size in the
   1438    // opposite direction.
   1439    // The goal is for the part that's not hidden to be exactly the size of a normal
   1440    // arrow and for the arrow to keep pointing at the line (keep being centered on it).
   1441    let grewBox = false;
   1442    const boxWidthBeforeGrowth = boxWidth;
   1443    const boxHeightBeforeGrowth = boxHeight;
   1444 
   1445    if (dimensionType === ROWS && y <= boxHeight / 2) {
   1446      grewBox = true;
   1447      boxHeight = 2 * (boxHeight - y);
   1448    } else if (dimensionType === ROWS && y >= height - boxHeight / 2) {
   1449      grewBox = true;
   1450      boxHeight = 2 * (y - height + boxHeight);
   1451    } else if (dimensionType === COLUMNS && x <= boxWidth / 2) {
   1452      grewBox = true;
   1453      boxWidth = 2 * (boxWidth - x);
   1454    } else if (dimensionType === COLUMNS && x >= width - boxWidth / 2) {
   1455      grewBox = true;
   1456      boxWidth = 2 * (x - width + boxWidth);
   1457    }
   1458 
   1459    // Draw the arrow box itself
   1460    drawBubbleRect(
   1461      this.ctx,
   1462      x,
   1463      y,
   1464      boxWidth,
   1465      boxHeight,
   1466      radius,
   1467      margin,
   1468      arrowSize,
   1469      boxEdge
   1470    );
   1471 
   1472    // Determine the text position for it to be centered nicely inside the arrow box.
   1473    switch (boxEdge) {
   1474      case "left":
   1475        x -= boxWidth + arrowSize + radius - boxWidth / 2;
   1476        break;
   1477      case "right":
   1478        x += boxWidth + arrowSize + radius - boxWidth / 2;
   1479        break;
   1480      case "top":
   1481        y -= boxHeight + arrowSize + radius - boxHeight / 2;
   1482        break;
   1483      case "bottom":
   1484        y += boxHeight + arrowSize + radius - boxHeight / 2;
   1485        break;
   1486    }
   1487 
   1488    // Do a second pass to adjust the position, along the other axis, if the box grew
   1489    // during the previous step, so the text is also centered on that axis.
   1490    if (grewBox) {
   1491      if (dimensionType === ROWS && y <= boxHeightBeforeGrowth / 2) {
   1492        y = boxHeightBeforeGrowth / 2;
   1493      } else if (
   1494        dimensionType === ROWS &&
   1495        y >= height - boxHeightBeforeGrowth / 2
   1496      ) {
   1497        y = height - boxHeightBeforeGrowth / 2;
   1498      } else if (dimensionType === COLUMNS && x <= boxWidthBeforeGrowth / 2) {
   1499        x = boxWidthBeforeGrowth / 2;
   1500      } else if (
   1501        dimensionType === COLUMNS &&
   1502        x >= width - boxWidthBeforeGrowth / 2
   1503      ) {
   1504        x = width - boxWidthBeforeGrowth / 2;
   1505      }
   1506    }
   1507 
   1508    // Write the line number inside of the rectangle.
   1509    this.ctx.textAlign = "center";
   1510    this.ctx.textBaseline = "middle";
   1511    this.ctx.fillStyle = "black";
   1512    const numberText = isStackedLine ? "" : lineNumber;
   1513    this.ctx.fillText(numberText, x, y);
   1514    this.ctx.restore();
   1515  }
   1516 
   1517  /**
   1518   * Determine which edge of a line number box to aim the line number arrow at.
   1519   *
   1520   * @param  {string} dimensionType
   1521   *         The grid line dimension type which is either the constant COLUMNS or ROWS.
   1522   * @param  {number} lineNumber
   1523   *         The grid line number.
   1524   * @return {string} The edge of the box: top, right, bottom or left.
   1525   */
   1526  getBoxEdge(dimensionType, lineNumber) {
   1527    let boxEdge;
   1528 
   1529    if (dimensionType === COLUMNS) {
   1530      boxEdge = lineNumber > 0 ? "top" : "bottom";
   1531    } else if (dimensionType === ROWS) {
   1532      boxEdge = lineNumber > 0 ? "left" : "right";
   1533    }
   1534 
   1535    // Rotate box edge as needed for writing mode and text direction.
   1536    const { direction, writingMode } = getComputedStyle(this.currentNode);
   1537 
   1538    switch (writingMode) {
   1539      case "horizontal-tb":
   1540        // This is the initial value.  No further adjustment needed.
   1541        break;
   1542      case "vertical-rl":
   1543        boxEdge = rotateEdgeRight(boxEdge);
   1544        break;
   1545      case "vertical-lr":
   1546        if (dimensionType === COLUMNS) {
   1547          boxEdge = rotateEdgeLeft(boxEdge);
   1548        } else {
   1549          boxEdge = rotateEdgeRight(boxEdge);
   1550        }
   1551        break;
   1552      case "sideways-rl":
   1553        boxEdge = rotateEdgeRight(boxEdge);
   1554        break;
   1555      case "sideways-lr":
   1556        boxEdge = rotateEdgeLeft(boxEdge);
   1557        break;
   1558      default:
   1559        console.error(`Unexpected writing-mode: ${writingMode}`);
   1560    }
   1561 
   1562    switch (direction) {
   1563      case "ltr":
   1564        // This is the initial value.  No further adjustment needed.
   1565        break;
   1566      case "rtl":
   1567        if (dimensionType === ROWS) {
   1568          boxEdge = reflectEdge(boxEdge);
   1569        }
   1570        break;
   1571      default:
   1572        console.error(`Unexpected direction: ${direction}`);
   1573    }
   1574 
   1575    return boxEdge;
   1576  }
   1577 
   1578  /**
   1579   * Render the grid line on the css grid highlighter canvas.
   1580   *
   1581   * @param  {number} linePos
   1582   *         The line position along the x-axis for a column grid line and
   1583   *         y-axis for a row grid line.
   1584   * @param  {number} startPos
   1585   *         The start position of the cross side of the grid line.
   1586   * @param  {number} endPos
   1587   *         The end position of the cross side of the grid line.
   1588   * @param  {string} dimensionType
   1589   *         The grid dimension type which is either the constant COLUMNS or ROWS.
   1590   * @param  {string} lineType
   1591   *         The grid line type - "edge", "explicit", or "implicit".
   1592   */
   1593  renderLine(linePos, startPos, endPos, dimensionType, lineType) {
   1594    const { devicePixelRatio } = this.win;
   1595    const lineWidth = getDisplayPixelRatio(this.win);
   1596    const offset = (lineWidth / 2) % 1;
   1597    const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
   1598    const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
   1599 
   1600    linePos = Math.round(linePos);
   1601    startPos = Math.round(startPos);
   1602    endPos = Math.round(endPos);
   1603 
   1604    this.ctx.save();
   1605    this.ctx.setLineDash(GRID_LINES_PROPERTIES[lineType].lineDash);
   1606    this.ctx.translate(offset - canvasX, offset - canvasY);
   1607 
   1608    const lineOptions = {
   1609      matrix: this.currentMatrix,
   1610    };
   1611 
   1612    if (this.options.showInfiniteLines) {
   1613      lineOptions.extendToBoundaries = [
   1614        canvasX,
   1615        canvasY,
   1616        canvasX + CANVAS_SIZE,
   1617        canvasY + CANVAS_SIZE,
   1618      ];
   1619    }
   1620 
   1621    if (dimensionType === COLUMNS) {
   1622      drawLine(this.ctx, linePos, startPos, linePos, endPos, lineOptions);
   1623    } else {
   1624      drawLine(this.ctx, startPos, linePos, endPos, linePos, lineOptions);
   1625    }
   1626 
   1627    this.ctx.strokeStyle = this.color;
   1628    this.ctx.globalAlpha =
   1629      GRID_LINES_PROPERTIES[lineType].alpha * this.globalAlpha;
   1630 
   1631    if (GRID_LINES_PROPERTIES[lineType].lineWidth) {
   1632      this.ctx.lineWidth =
   1633        GRID_LINES_PROPERTIES[lineType].lineWidth * devicePixelRatio;
   1634    } else {
   1635      this.ctx.lineWidth = lineWidth;
   1636    }
   1637 
   1638    this.ctx.stroke();
   1639    this.ctx.restore();
   1640  }
   1641 
   1642  /**
   1643   * Render the grid lines given the grid dimension information of the
   1644   * column or row lines.
   1645   *
   1646   * @param  {GridDimension} gridDimension
   1647   *         Column or row grid dimension object.
   1648   * @param  {object} quad.bounds
   1649   *         The content bounds of the box model region quads.
   1650   * @param  {string} dimensionType
   1651   *         The grid dimension type which is either the constant COLUMNS or ROWS.
   1652   * @param  {number} startPos
   1653   *         The start position of the cross side ("left" for ROWS and "top" for COLUMNS)
   1654   *         of the grid dimension.
   1655   * @param  {number} endPos
   1656   *         The end position of the cross side ("left" for ROWS and "top" for COLUMNS)
   1657   *         of the grid dimension.
   1658   */
   1659  renderLines(gridDimension, dimensionType, startPos, endPos) {
   1660    const { lines, tracks } = gridDimension;
   1661    const lastEdgeLineIndex = this.getLastEdgeLineIndex(tracks);
   1662 
   1663    for (let i = 0; i < lines.length; i++) {
   1664      const line = lines[i];
   1665      const linePos = line.start;
   1666 
   1667      if (i == 0 || i == lastEdgeLineIndex) {
   1668        this.renderLine(linePos, startPos, endPos, dimensionType, "edge");
   1669      } else {
   1670        this.renderLine(
   1671          linePos,
   1672          startPos,
   1673          endPos,
   1674          dimensionType,
   1675          tracks[i - 1].type
   1676        );
   1677      }
   1678 
   1679      // Render a second line to illustrate the gutter for non-zero breadth.
   1680      if (line.breadth > 0) {
   1681        this.renderGridGap(
   1682          linePos,
   1683          startPos,
   1684          endPos,
   1685          line.breadth,
   1686          dimensionType
   1687        );
   1688        this.renderLine(
   1689          linePos + line.breadth,
   1690          startPos,
   1691          endPos,
   1692          dimensionType,
   1693          tracks[i].type
   1694        );
   1695      }
   1696    }
   1697  }
   1698 
   1699  /**
   1700   * Render the grid lines given the grid dimension information of the
   1701   * column or row lines.
   1702   *
   1703   * @param  {GridDimension} gridDimension
   1704   *         Column or row grid dimension object.
   1705   * @param  {string} dimensionType
   1706   *         The grid dimension type which is either the constant COLUMNS or ROWS.
   1707   * @param  {number} startPos
   1708   *         The start position of the cross side ("left" for ROWS and "top" for COLUMNS)
   1709   *         of the grid dimension.
   1710   */
   1711  renderLineNumbers(gridDimension, dimensionType, startPos) {
   1712    const { lines, tracks } = gridDimension;
   1713 
   1714    for (let i = 0, line; (line = lines[i++]); ) {
   1715      // If you place something using negative numbers, you can trigger some implicit
   1716      // grid creation above and to the left of the explicit grid (assuming a
   1717      // horizontal-tb writing mode).
   1718      //
   1719      // The first explicit grid line gets the number of 1, and any implicit grid lines
   1720      // before 1 get negative numbers. Since here we're rendering only the positive line
   1721      // numbers, we have to skip any implicit grid lines before the first one that is
   1722      // explicit. The API returns a 0 as the line's number for these implicit lines that
   1723      // occurs before the first explicit line.
   1724      if (line.number === 0) {
   1725        continue;
   1726      }
   1727 
   1728      // Check for overlapping lines by measuring the track width between them.
   1729      // We render a second box beneath the last overlapping
   1730      // line number to indicate there are lines beneath it.
   1731      const gridTrack = tracks[i - 1];
   1732 
   1733      if (gridTrack) {
   1734        const { breadth } = gridTrack;
   1735 
   1736        if (breadth === 0) {
   1737          this.renderGridLineNumber(
   1738            line.number,
   1739            line.start,
   1740            startPos,
   1741            line.breadth,
   1742            dimensionType,
   1743            true
   1744          );
   1745          continue;
   1746        }
   1747      }
   1748 
   1749      this.renderGridLineNumber(
   1750        line.number,
   1751        line.start,
   1752        startPos,
   1753        line.breadth,
   1754        dimensionType
   1755      );
   1756    }
   1757  }
   1758 
   1759  /**
   1760   * Render the negative grid lines given the grid dimension information of the
   1761   * column or row lines.
   1762   *
   1763   * @param  {GridDimension} gridDimension
   1764   *         Column or row grid dimension object.
   1765   * @param  {string} dimensionType
   1766   *         The grid dimension type which is either the constant COLUMNS or ROWS.
   1767   * @param  {number} startPos
   1768   *         The start position of the cross side ("left" for ROWS and "top" for COLUMNS)
   1769   *         of the grid dimension.
   1770   */
   1771  renderNegativeLineNumbers(gridDimension, dimensionType, startPos) {
   1772    const { lines, tracks } = gridDimension;
   1773 
   1774    for (let i = 0, line; (line = lines[i++]); ) {
   1775      const linePos = line.start;
   1776      const negativeLineNumber = line.negativeNumber;
   1777 
   1778      // Don't render any negative line number greater than -1.
   1779      if (negativeLineNumber == 0) {
   1780        break;
   1781      }
   1782 
   1783      // Check for overlapping lines by measuring the track width between them.
   1784      // We render a second box beneath the last overlapping
   1785      // line number to indicate there are lines beneath it.
   1786      const gridTrack = tracks[i - 1];
   1787      if (gridTrack) {
   1788        const { breadth } = gridTrack;
   1789 
   1790        // Ensure "-1" is always visible, since it is always the largest number.
   1791        if (breadth === 0 && negativeLineNumber != -1) {
   1792          this.renderGridLineNumber(
   1793            negativeLineNumber,
   1794            linePos,
   1795            startPos,
   1796            line.breadth,
   1797            dimensionType,
   1798            true
   1799          );
   1800          continue;
   1801        }
   1802      }
   1803 
   1804      this.renderGridLineNumber(
   1805        negativeLineNumber,
   1806        linePos,
   1807        startPos,
   1808        line.breadth,
   1809        dimensionType
   1810      );
   1811    }
   1812  }
   1813 
   1814  /**
   1815   * Update the highlighter on the current highlighted node (the one that was
   1816   * passed as an argument to show(node)). Should be called whenever node's geometry
   1817   * or grid changes.
   1818   */
   1819  _update() {
   1820    setIgnoreLayoutChanges(true);
   1821 
   1822    const root = this.getNode("css-grid-root");
   1823    this._winDimensions = getWindowDimensions(this.win);
   1824    const { width, height } = this._winDimensions;
   1825 
   1826    // Updates the <canvas> element's position and size.
   1827    // It also clear the <canvas>'s drawing context.
   1828    updateCanvasElement(
   1829      this.canvas,
   1830      this._canvasPosition,
   1831      this.win.devicePixelRatio
   1832    );
   1833 
   1834    // Clear the grid area highlights.
   1835    this.clearGridAreas();
   1836    this.clearGridCell();
   1837 
   1838    // Update the current matrix used in our canvas' rendering.
   1839    const { currentMatrix, hasNodeTransformations } = getCurrentMatrix(
   1840      this.currentNode,
   1841      this.win
   1842    );
   1843    this.currentMatrix = currentMatrix;
   1844    this.hasNodeTransformations = hasNodeTransformations;
   1845 
   1846    // Start drawing the grid fragments.
   1847    for (let i = 0; i < this.gridData.length; i++) {
   1848      this.renderFragment(this.gridData[i]);
   1849    }
   1850 
   1851    // Display the grid area highlights if needed.
   1852    if (this.options.showAllGridAreas) {
   1853      this.showAllGridAreas();
   1854    } else if (this.options.showGridArea) {
   1855      this.showGridArea(this.options.showGridArea);
   1856    }
   1857 
   1858    // Display the grid cell highlights if needed.
   1859    if (this.options.showGridCell) {
   1860      this.showGridCell(this.options.showGridCell);
   1861    }
   1862 
   1863    // Display the grid line names if needed.
   1864    if (this.options.showGridLineNames) {
   1865      this.showGridLineNames(this.options.showGridLineNames);
   1866    }
   1867 
   1868    this._showGrid();
   1869    this._showGridElements();
   1870 
   1871    root.style.setProperty("width", `${width}px`);
   1872    root.style.setProperty("height", `${height}px`);
   1873 
   1874    setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
   1875    return true;
   1876  }
   1877 
   1878  /**
   1879   * Update the grid information displayed in the grid area info bar.
   1880   *
   1881   * @param  {GridArea} area
   1882   *         The grid area object.
   1883   * @param  {object} bounds
   1884   *         A DOMRect-like object represent the grid area rectangle.
   1885   */
   1886  _updateGridAreaInfobar(area, bounds) {
   1887    const { width, height } = bounds;
   1888    const dim =
   1889      parseFloat(width.toPrecision(6)) +
   1890      " \u00D7 " +
   1891      parseFloat(height.toPrecision(6));
   1892 
   1893    this.getElement("css-grid-area-infobar-name").setTextContent(area.name);
   1894    this.getElement("css-grid-area-infobar-dimensions").setTextContent(dim);
   1895 
   1896    const container = this.getElement("css-grid-area-infobar-container");
   1897    moveInfobar(container, bounds, this.win, {
   1898      position: "bottom",
   1899    });
   1900  }
   1901 
   1902  /**
   1903   * Update the grid information displayed in the grid cell info bar.
   1904   *
   1905   * @param  {number} rowNumber
   1906   *         The grid cell's row number.
   1907   * @param  {number} columnNumber
   1908   *         The grid cell's column number.
   1909   * @param  {object} bounds
   1910   *         A DOMRect-like object represent the grid cell rectangle.
   1911   */
   1912  _updateGridCellInfobar(rowNumber, columnNumber, bounds) {
   1913    const { width, height } = bounds;
   1914    const dim =
   1915      parseFloat(width.toPrecision(6)) +
   1916      " \u00D7 " +
   1917      parseFloat(height.toPrecision(6));
   1918    const position = HighlightersBundle.formatValueSync(
   1919      "grid-row-column-positions",
   1920      { row: rowNumber, column: columnNumber }
   1921    );
   1922 
   1923    this.getElement("css-grid-cell-infobar-position").setTextContent(position);
   1924    this.getElement("css-grid-cell-infobar-dimensions").setTextContent(dim);
   1925 
   1926    const container = this.getElement("css-grid-cell-infobar-container");
   1927    moveInfobar(container, bounds, this.win, {
   1928      position: "top",
   1929    });
   1930  }
   1931 
   1932  /**
   1933   * Update the grid information displayed in the grid line info bar.
   1934   *
   1935   * @param  {string} gridLineNames
   1936   *         Comma-separated string of names for the grid line.
   1937   * @param  {number} gridLineNumber
   1938   *         The grid line number.
   1939   * @param  {number} x
   1940   *         The x-coordinate of the grid line.
   1941   * @param  {number} y
   1942   *         The y-coordinate of the grid line.
   1943   */
   1944  _updateGridLineInfobar(gridLineNames, gridLineNumber, x, y) {
   1945    this.getElement("css-grid-line-infobar-number").setTextContent(
   1946      gridLineNumber
   1947    );
   1948    this.getElement("css-grid-line-infobar-names").setTextContent(
   1949      gridLineNames
   1950    );
   1951 
   1952    const container = this.getElement("css-grid-line-infobar-container");
   1953    moveInfobar(
   1954      container,
   1955      getBoundsFromPoints([
   1956        { x, y },
   1957        { x, y },
   1958        { x, y },
   1959        { x, y },
   1960      ]),
   1961      this.win
   1962    );
   1963  }
   1964 }
   1965 
   1966 exports.CssGridHighlighter = CssGridHighlighter;