tor-browser

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

flexbox.js (30234B)


      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 { apply } = require("resource://devtools/shared/layout/dom-matrix-2d.js");
     11 const {
     12  CANVAS_SIZE,
     13  DEFAULT_COLOR,
     14  clearRect,
     15  drawLine,
     16  drawRect,
     17  getCurrentMatrix,
     18  updateCanvasElement,
     19  updateCanvasPosition,
     20 } = require("resource://devtools/server/actors/highlighters/utils/canvas.js");
     21 const {
     22  CanvasFrameAnonymousContentHelper,
     23  getComputedStyle,
     24 } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
     25 const {
     26  getAbsoluteScrollOffsetsForNode,
     27  getCurrentZoom,
     28  getDisplayPixelRatio,
     29  getUntransformedQuad,
     30  getWindowDimensions,
     31  setIgnoreLayoutChanges,
     32 } = require("resource://devtools/shared/layout/utils.js");
     33 
     34 const FLEXBOX_LINES_PROPERTIES = {
     35  edge: {
     36    lineDash: [5, 3],
     37  },
     38  item: {
     39    lineDash: [0, 0],
     40  },
     41  alignItems: {
     42    lineDash: [0, 0],
     43  },
     44 };
     45 
     46 const FLEXBOX_CONTAINER_PATTERN_LINE_DASH = [5, 3]; // px
     47 const FLEXBOX_CONTAINER_PATTERN_WIDTH = 14; // px
     48 const FLEXBOX_CONTAINER_PATTERN_HEIGHT = 14; // px
     49 const FLEXBOX_JUSTIFY_CONTENT_PATTERN_WIDTH = 7; // px
     50 const FLEXBOX_JUSTIFY_CONTENT_PATTERN_HEIGHT = 7; // px
     51 
     52 /**
     53 * Cached used by `FlexboxHighlighter.getFlexContainerPattern`.
     54 */
     55 const gCachedFlexboxPattern = new Map();
     56 
     57 const FLEXBOX = "flexbox";
     58 const JUSTIFY_CONTENT = "justify-content";
     59 
     60 /**
     61 * The FlexboxHighlighter is the class that overlays a visual canvas on top of
     62 * display: [inline-]flex elements.
     63 *
     64 * @param {string} options.color
     65 *        The color that should be used to draw the highlighter for this flexbox.
     66 * Structure:
     67 * <div class="highlighter-container">
     68 *   <div id="flexbox-root" class="flexbox-root">
     69 *     <canvas id="flexbox-canvas"
     70 *             class="flexbox-canvas"
     71 *             width="4096"
     72 *             height="4096"
     73 *             hidden="true">
     74 *     </canvas>
     75 *   </div>
     76 * </div>
     77 */
     78 class FlexboxHighlighter extends AutoRefreshHighlighter {
     79  constructor(highlighterEnv) {
     80    super(highlighterEnv);
     81 
     82    this.markup = new CanvasFrameAnonymousContentHelper(
     83      this.highlighterEnv,
     84      this._buildMarkup.bind(this),
     85      {
     86        contentRootHostClassName: "devtools-highlighter-flexbox",
     87      }
     88    );
     89    this.isReady = this.markup.initialize();
     90 
     91    this.onPageHide = this.onPageHide.bind(this);
     92    this.onWillNavigate = this.onWillNavigate.bind(this);
     93 
     94    this.highlighterEnv.on("will-navigate", this.onWillNavigate);
     95 
     96    const { pageListenerTarget } = highlighterEnv;
     97    pageListenerTarget.addEventListener("pagehide", this.onPageHide);
     98 
     99    // Initialize the <canvas> position to the top left corner of the page
    100    this._canvasPosition = {
    101      x: 0,
    102      y: 0,
    103    };
    104 
    105    this._ignoreZoom = true;
    106 
    107    // Calling `updateCanvasPosition` anyway since the highlighter could be initialized
    108    // on a page that has scrolled already.
    109    updateCanvasPosition(
    110      this._canvasPosition,
    111      this._scroll,
    112      this.win,
    113      this._winDimensions
    114    );
    115  }
    116 
    117  _buildMarkup() {
    118    const container = this.markup.createNode({
    119      attributes: {
    120        class: "flexbox-highlighter-container",
    121      },
    122    });
    123 
    124    this.rootEl = this.markup.createNode({
    125      parent: container,
    126      attributes: {
    127        id: "flexbox-root",
    128        class: "flexbox-root",
    129      },
    130    });
    131 
    132    // We use a <canvas> element because there is an arbitrary number of items and texts
    133    // to draw which wouldn't be possible with HTML or SVG without having to insert and
    134    // remove the whole markup on every update.
    135    this.markup.createNode({
    136      parent: this.rootEl,
    137      nodeType: "canvas",
    138      attributes: {
    139        id: "flexbox-canvas",
    140        class: "flexbox-canvas",
    141        hidden: "true",
    142        width: CANVAS_SIZE,
    143        height: CANVAS_SIZE,
    144      },
    145    });
    146 
    147    return container;
    148  }
    149 
    150  clearCache() {
    151    gCachedFlexboxPattern.clear();
    152  }
    153 
    154  destroy() {
    155    const { highlighterEnv } = this;
    156    highlighterEnv.off("will-navigate", this.onWillNavigate);
    157 
    158    const { pageListenerTarget } = highlighterEnv;
    159 
    160    if (pageListenerTarget) {
    161      pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
    162    }
    163 
    164    this.markup.destroy();
    165    this.rootEl = null;
    166 
    167    // Clear the pattern cache to avoid dead object exceptions (Bug 1342051).
    168    this.clearCache();
    169 
    170    this.axes = null;
    171    this.crossAxisDirection = null;
    172    this.flexData = null;
    173    this.mainAxisDirection = null;
    174    this.transform = null;
    175 
    176    AutoRefreshHighlighter.prototype.destroy.call(this);
    177  }
    178 
    179  /**
    180   * Draw the justify content for a given flex item (left, top, right, bottom) position.
    181   */
    182  drawJustifyContent(left, top, right, bottom) {
    183    const { devicePixelRatio } = this.win;
    184    this.ctx.fillStyle = this.getJustifyContentPattern(devicePixelRatio);
    185    drawRect(this.ctx, left, top, right, bottom, this.currentMatrix);
    186    this.ctx.fill();
    187  }
    188 
    189  get canvas() {
    190    return this.getElement("flexbox-canvas");
    191  }
    192 
    193  get color() {
    194    return this.options.color || DEFAULT_COLOR;
    195  }
    196 
    197  get container() {
    198    return this.currentNode;
    199  }
    200 
    201  get ctx() {
    202    return this.canvas.getCanvasContext("2d");
    203  }
    204 
    205  getElement(id) {
    206    return this.markup.getElement(id);
    207  }
    208 
    209  /**
    210   * Gets the flexbox container pattern used to render the container regions.
    211   *
    212   * @param  {number} devicePixelRatio
    213   *         The device pixel ratio we want the pattern for.
    214   * @return {CanvasPattern} flex container pattern.
    215   */
    216  getFlexContainerPattern(devicePixelRatio) {
    217    let flexboxPatternMap = null;
    218 
    219    if (gCachedFlexboxPattern.has(devicePixelRatio)) {
    220      flexboxPatternMap = gCachedFlexboxPattern.get(devicePixelRatio);
    221    } else {
    222      flexboxPatternMap = new Map();
    223    }
    224 
    225    if (gCachedFlexboxPattern.has(FLEXBOX)) {
    226      return gCachedFlexboxPattern.get(FLEXBOX);
    227    }
    228 
    229    // Create the diagonal lines pattern for the rendering the flexbox gaps.
    230    const canvas = this.markup.createNode({ nodeType: "canvas" });
    231    const width = (canvas.width =
    232      FLEXBOX_CONTAINER_PATTERN_WIDTH * devicePixelRatio);
    233    const height = (canvas.height =
    234      FLEXBOX_CONTAINER_PATTERN_HEIGHT * devicePixelRatio);
    235 
    236    const ctx = canvas.getContext("2d");
    237    ctx.save();
    238    ctx.setLineDash(FLEXBOX_CONTAINER_PATTERN_LINE_DASH);
    239    ctx.beginPath();
    240    ctx.translate(0.5, 0.5);
    241 
    242    ctx.moveTo(0, 0);
    243    ctx.lineTo(width, height);
    244 
    245    ctx.strokeStyle = this.color;
    246    ctx.stroke();
    247    ctx.restore();
    248 
    249    const pattern = ctx.createPattern(canvas, "repeat");
    250    flexboxPatternMap.set(FLEXBOX, pattern);
    251    gCachedFlexboxPattern.set(devicePixelRatio, flexboxPatternMap);
    252 
    253    return pattern;
    254  }
    255 
    256  /**
    257   * Gets the flexbox justify content pattern used to render the justify content regions.
    258   *
    259   * @param  {number} devicePixelRatio
    260   *         The device pixel ratio we want the pattern for.
    261   * @return {CanvasPattern} flex justify content pattern.
    262   */
    263  getJustifyContentPattern(devicePixelRatio) {
    264    let flexboxPatternMap = null;
    265 
    266    if (gCachedFlexboxPattern.has(devicePixelRatio)) {
    267      flexboxPatternMap = gCachedFlexboxPattern.get(devicePixelRatio);
    268    } else {
    269      flexboxPatternMap = new Map();
    270    }
    271 
    272    if (flexboxPatternMap.has(JUSTIFY_CONTENT)) {
    273      return flexboxPatternMap.get(JUSTIFY_CONTENT);
    274    }
    275 
    276    // Create the inversed diagonal lines pattern
    277    // for the rendering the justify content gaps.
    278    const canvas = this.markup.createNode({ nodeType: "canvas" });
    279    const zoom = getCurrentZoom(this.win);
    280    const width = (canvas.width =
    281      FLEXBOX_JUSTIFY_CONTENT_PATTERN_WIDTH * devicePixelRatio * zoom);
    282    const height = (canvas.height =
    283      FLEXBOX_JUSTIFY_CONTENT_PATTERN_HEIGHT * devicePixelRatio * zoom);
    284 
    285    const ctx = canvas.getContext("2d");
    286    ctx.save();
    287    ctx.setLineDash(FLEXBOX_CONTAINER_PATTERN_LINE_DASH);
    288    ctx.beginPath();
    289    ctx.translate(0.5, 0.5);
    290 
    291    ctx.moveTo(0, height);
    292    ctx.lineTo(width, 0);
    293 
    294    ctx.strokeStyle = this.color;
    295    ctx.stroke();
    296    ctx.restore();
    297 
    298    const pattern = ctx.createPattern(canvas, "repeat");
    299    flexboxPatternMap.set(JUSTIFY_CONTENT, pattern);
    300    gCachedFlexboxPattern.set(devicePixelRatio, flexboxPatternMap);
    301 
    302    return pattern;
    303  }
    304 
    305  getNode(id) {
    306    return this.markup.content.root.getElementById(id);
    307  }
    308 
    309  /**
    310   * The AutoRefreshHighlighter's _hasMoved method returns true only if the
    311   * element's quads have changed. Override it so it also returns true if the
    312   * flex container and its flex items have changed.
    313   */
    314  _hasMoved() {
    315    const hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
    316 
    317    if (!this.computedStyle) {
    318      this.computedStyle = getComputedStyle(this.container);
    319    }
    320 
    321    const flex = this.container.getAsFlexContainer();
    322 
    323    const oldCrossAxisDirection = this.crossAxisDirection;
    324    this.crossAxisDirection = flex ? flex.crossAxisDirection : null;
    325    const newCrossAxisDirection = this.crossAxisDirection;
    326 
    327    const oldMainAxisDirection = this.mainAxisDirection;
    328    this.mainAxisDirection = flex ? flex.mainAxisDirection : null;
    329    const newMainAxisDirection = this.mainAxisDirection;
    330 
    331    // Concatenate the axes to simplify conditionals.
    332    this.axes = `${this.mainAxisDirection} ${this.crossAxisDirection}`;
    333 
    334    const oldFlexData = this.flexData;
    335    this.flexData = getFlexData(this.container);
    336    const hasFlexDataChanged = compareFlexData(oldFlexData, this.flexData);
    337 
    338    const oldAlignItems = this.alignItemsValue;
    339    this.alignItemsValue = this.computedStyle.alignItems;
    340    const newAlignItems = this.alignItemsValue;
    341 
    342    const oldFlexDirection = this.flexDirection;
    343    this.flexDirection = this.computedStyle.flexDirection;
    344    const newFlexDirection = this.flexDirection;
    345 
    346    const oldFlexWrap = this.flexWrap;
    347    this.flexWrap = this.computedStyle.flexWrap;
    348    const newFlexWrap = this.flexWrap;
    349 
    350    const oldJustifyContent = this.justifyContentValue;
    351    this.justifyContentValue = this.computedStyle.justifyContent;
    352    const newJustifyContent = this.justifyContentValue;
    353 
    354    const oldTransform = this.transformValue;
    355    this.transformValue = this.computedStyle.transform;
    356    const newTransform = this.transformValue;
    357 
    358    return (
    359      hasMoved ||
    360      hasFlexDataChanged ||
    361      oldAlignItems !== newAlignItems ||
    362      oldFlexDirection !== newFlexDirection ||
    363      oldFlexWrap !== newFlexWrap ||
    364      oldJustifyContent !== newJustifyContent ||
    365      oldCrossAxisDirection !== newCrossAxisDirection ||
    366      oldMainAxisDirection !== newMainAxisDirection ||
    367      oldTransform !== newTransform
    368    );
    369  }
    370 
    371  _hide() {
    372    this.alignItemsValue = null;
    373    this.computedStyle = null;
    374    this.flexData = null;
    375    this.flexDirection = null;
    376    this.flexWrap = null;
    377    this.justifyContentValue = null;
    378 
    379    setIgnoreLayoutChanges(true);
    380    this._hideFlexbox();
    381    setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
    382  }
    383 
    384  _hideFlexbox() {
    385    this.getElement("flexbox-canvas").setAttribute("hidden", "true");
    386  }
    387 
    388  /**
    389   * The <canvas>'s position needs to be updated if the page scrolls too much, in order
    390   * to give the illusion that it always covers the viewport.
    391   */
    392  _scrollUpdate() {
    393    const hasUpdated = updateCanvasPosition(
    394      this._canvasPosition,
    395      this._scroll,
    396      this.win,
    397      this._winDimensions
    398    );
    399 
    400    if (hasUpdated) {
    401      this._update();
    402    }
    403  }
    404 
    405  _show() {
    406    this._hide();
    407    return this._update();
    408  }
    409 
    410  _showFlexbox() {
    411    this.getElement("flexbox-canvas").removeAttribute("hidden");
    412  }
    413 
    414  /**
    415   * If a page hide event is triggered for current window's highlighter, hide the
    416   * highlighter.
    417   */
    418  onPageHide({ target }) {
    419    if (target.defaultView === this.win) {
    420      this.hide();
    421    }
    422  }
    423 
    424  /**
    425   * Called when the page will-navigate. Used to hide the flexbox highlighter and clear
    426   * the cached gap patterns and avoid using DeadWrapper obejcts as gap patterns the
    427   * next time.
    428   */
    429  onWillNavigate({ isTopLevel }) {
    430    this.clearCache();
    431 
    432    if (isTopLevel) {
    433      this.hide();
    434    }
    435  }
    436 
    437  renderFlexContainer() {
    438    if (!this.currentQuads.content || !this.currentQuads.content[0]) {
    439      return;
    440    }
    441 
    442    const { devicePixelRatio } = this.win;
    443    const containerQuad = getUntransformedQuad(this.container, "content");
    444    const { width, height } = containerQuad.getBounds();
    445 
    446    this.setupCanvas({
    447      lineDash: FLEXBOX_LINES_PROPERTIES.alignItems.lineDash,
    448      lineWidthMultiplier: 2,
    449    });
    450 
    451    this.ctx.fillStyle = this.getFlexContainerPattern(devicePixelRatio);
    452 
    453    drawRect(this.ctx, 0, 0, width, height, this.currentMatrix);
    454 
    455    // Find current angle of outer flex element by measuring the angle of two arbitrary
    456    // points, then rotate canvas, so the hash pattern stays 45deg to the boundary.
    457    const p1 = apply(this.currentMatrix, [0, 0]);
    458    const p2 = apply(this.currentMatrix, [1, 0]);
    459    const angleRad = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]);
    460    this.ctx.rotate(angleRad);
    461 
    462    this.ctx.fill();
    463    this.ctx.stroke();
    464    this.ctx.restore();
    465  }
    466 
    467  renderFlexItems() {
    468    if (
    469      !this.flexData ||
    470      !this.currentQuads.content ||
    471      !this.currentQuads.content[0]
    472    ) {
    473      return;
    474    }
    475 
    476    this.setupCanvas({
    477      lineDash: FLEXBOX_LINES_PROPERTIES.item.lineDash,
    478    });
    479 
    480    for (const flexLine of this.flexData.lines) {
    481      for (const flexItem of flexLine.items) {
    482        const { left, top, right, bottom } = flexItem.rect;
    483 
    484        clearRect(this.ctx, left, top, right, bottom, this.currentMatrix);
    485        drawRect(this.ctx, left, top, right, bottom, this.currentMatrix);
    486        this.ctx.stroke();
    487      }
    488    }
    489 
    490    this.ctx.restore();
    491  }
    492 
    493  renderFlexLines() {
    494    if (
    495      !this.flexData ||
    496      !this.currentQuads.content ||
    497      !this.currentQuads.content[0]
    498    ) {
    499      return;
    500    }
    501 
    502    const lineWidth = getDisplayPixelRatio(this.win);
    503    const options = { matrix: this.currentMatrix };
    504    const { width: containerWidth, height: containerHeight } =
    505      getUntransformedQuad(this.container, "content").getBounds();
    506 
    507    this.setupCanvas({
    508      useContainerScrollOffsets: true,
    509    });
    510 
    511    for (const flexLine of this.flexData.lines) {
    512      const { crossStart, crossSize } = flexLine;
    513 
    514      switch (this.axes) {
    515        case "horizontal-lr vertical-tb":
    516        case "horizontal-lr vertical-bt":
    517        case "horizontal-rl vertical-tb":
    518        case "horizontal-rl vertical-bt":
    519          clearRect(
    520            this.ctx,
    521            0,
    522            crossStart,
    523            containerWidth,
    524            crossStart + crossSize,
    525            this.currentMatrix
    526          );
    527 
    528          // Avoid drawing the start flex line when they overlap with the flex container.
    529          if (crossStart != 0) {
    530            drawLine(
    531              this.ctx,
    532              0,
    533              crossStart,
    534              containerWidth,
    535              crossStart,
    536              options
    537            );
    538            this.ctx.stroke();
    539          }
    540 
    541          // Avoid drawing the end flex line when they overlap with the flex container.
    542          if (crossStart + crossSize < containerHeight - lineWidth * 2) {
    543            drawLine(
    544              this.ctx,
    545              0,
    546              crossStart + crossSize,
    547              containerWidth,
    548              crossStart + crossSize,
    549              options
    550            );
    551            this.ctx.stroke();
    552          }
    553          break;
    554        case "vertical-tb horizontal-lr":
    555        case "vertical-bt horizontal-rl":
    556          clearRect(
    557            this.ctx,
    558            crossStart,
    559            0,
    560            crossStart + crossSize,
    561            containerHeight,
    562            this.currentMatrix
    563          );
    564 
    565          // Avoid drawing the start flex line when they overlap with the flex container.
    566          if (crossStart != 0) {
    567            drawLine(
    568              this.ctx,
    569              crossStart,
    570              0,
    571              crossStart,
    572              containerHeight,
    573              options
    574            );
    575            this.ctx.stroke();
    576          }
    577 
    578          // Avoid drawing the end flex line when they overlap with the flex container.
    579          if (crossStart + crossSize < containerWidth - lineWidth * 2) {
    580            drawLine(
    581              this.ctx,
    582              crossStart + crossSize,
    583              0,
    584              crossStart + crossSize,
    585              containerHeight,
    586              options
    587            );
    588            this.ctx.stroke();
    589          }
    590          break;
    591        case "vertical-bt horizontal-lr":
    592        case "vertical-tb horizontal-rl":
    593          clearRect(
    594            this.ctx,
    595            containerWidth - crossStart,
    596            0,
    597            containerWidth - crossStart - crossSize,
    598            containerHeight,
    599            this.currentMatrix
    600          );
    601 
    602          // Avoid drawing the start flex line when they overlap with the flex container.
    603          if (crossStart != 0) {
    604            drawLine(
    605              this.ctx,
    606              containerWidth - crossStart,
    607              0,
    608              containerWidth - crossStart,
    609              containerHeight,
    610              options
    611            );
    612            this.ctx.stroke();
    613          }
    614 
    615          // Avoid drawing the end flex line when they overlap with the flex container.
    616          if (crossStart + crossSize < containerWidth - lineWidth * 2) {
    617            drawLine(
    618              this.ctx,
    619              containerWidth - crossStart - crossSize,
    620              0,
    621              containerWidth - crossStart - crossSize,
    622              containerHeight,
    623              options
    624            );
    625            this.ctx.stroke();
    626          }
    627          break;
    628      }
    629    }
    630 
    631    this.ctx.restore();
    632  }
    633 
    634  /**
    635   * Clear the whole alignment container along the main axis for each flex item.
    636   */
    637  // eslint-disable-next-line complexity
    638  renderJustifyContent() {
    639    if (
    640      !this.flexData ||
    641      !this.currentQuads.content ||
    642      !this.currentQuads.content[0]
    643    ) {
    644      return;
    645    }
    646 
    647    const { width: containerWidth, height: containerHeight } =
    648      getUntransformedQuad(this.container, "content").getBounds();
    649 
    650    this.setupCanvas({
    651      lineDash: FLEXBOX_LINES_PROPERTIES.alignItems.lineDash,
    652      offset: (getDisplayPixelRatio(this.win) / 2) % 1,
    653      skipLineAndStroke: true,
    654      useContainerScrollOffsets: true,
    655    });
    656 
    657    for (const flexLine of this.flexData.lines) {
    658      const { crossStart, crossSize } = flexLine;
    659      let mainStart = 0;
    660 
    661      // In these two situations mainStart goes from right to left so set it's
    662      // value as appropriate.
    663      if (
    664        this.axes === "horizontal-lr vertical-bt" ||
    665        this.axes === "horizontal-rl vertical-tb"
    666      ) {
    667        mainStart = containerWidth;
    668      }
    669 
    670      for (const flexItem of flexLine.items) {
    671        const { left, top, right, bottom } = flexItem.rect;
    672 
    673        switch (this.axes) {
    674          case "horizontal-lr vertical-tb":
    675          case "horizontal-rl vertical-bt":
    676            this.drawJustifyContent(
    677              mainStart,
    678              crossStart,
    679              left,
    680              crossStart + crossSize
    681            );
    682            mainStart = right;
    683            break;
    684          case "horizontal-lr vertical-bt":
    685          case "horizontal-rl vertical-tb":
    686            this.drawJustifyContent(
    687              right,
    688              crossStart,
    689              mainStart,
    690              crossStart + crossSize
    691            );
    692            mainStart = left;
    693            break;
    694          case "vertical-tb horizontal-lr":
    695          case "vertical-bt horizontal-rl":
    696            this.drawJustifyContent(
    697              crossStart,
    698              mainStart,
    699              crossStart + crossSize,
    700              top
    701            );
    702            mainStart = bottom;
    703            break;
    704          case "vertical-bt horizontal-lr":
    705          case "vertical-tb horizontal-rl":
    706            this.drawJustifyContent(
    707              containerWidth - crossStart - crossSize,
    708              mainStart,
    709              containerWidth - crossStart,
    710              top
    711            );
    712            mainStart = bottom;
    713            break;
    714        }
    715      }
    716 
    717      // Draw the last justify-content area after the last flex item.
    718      switch (this.axes) {
    719        case "horizontal-lr vertical-tb":
    720        case "horizontal-rl vertical-bt":
    721          this.drawJustifyContent(
    722            mainStart,
    723            crossStart,
    724            containerWidth,
    725            crossStart + crossSize
    726          );
    727          break;
    728        case "horizontal-lr vertical-bt":
    729        case "horizontal-rl vertical-tb":
    730          this.drawJustifyContent(
    731            0,
    732            crossStart,
    733            mainStart,
    734            crossStart + crossSize
    735          );
    736          break;
    737        case "vertical-tb horizontal-lr":
    738        case "vertical-bt horizontal-rl":
    739          this.drawJustifyContent(
    740            crossStart,
    741            mainStart,
    742            crossStart + crossSize,
    743            containerHeight
    744          );
    745          break;
    746        case "vertical-bt horizontal-lr":
    747        case "vertical-tb horizontal-rl":
    748          this.drawJustifyContent(
    749            containerWidth - crossStart - crossSize,
    750            mainStart,
    751            containerWidth - crossStart,
    752            containerHeight
    753          );
    754          break;
    755      }
    756    }
    757 
    758    this.ctx.restore();
    759  }
    760 
    761  /**
    762   * Set up the canvas with the given options prior to drawing.
    763   *
    764   * @param {string} [options.lineDash = null]
    765   *        An Array of numbers that specify distances to alternately draw a
    766   *        line and a gap (in coordinate space units). If the number of
    767   *        elements in the array is odd, the elements of the array get copied
    768   *        and concatenated. For example, [5, 15, 25] will become
    769   *        [5, 15, 25, 5, 15, 25]. If the array is empty, the line dash list is
    770   *        cleared and line strokes return to being solid.
    771   *
    772   *        We use the following constants here:
    773   *          FLEXBOX_LINES_PROPERTIES.edge.lineDash,
    774   *          FLEXBOX_LINES_PROPERTIES.item.lineDash
    775   *          FLEXBOX_LINES_PROPERTIES.alignItems.lineDash
    776   * @param {number} [options.lineWidthMultiplier = 1]
    777   *        The width of the line.
    778   * @param {number} [options.offset = `(displayPixelRatio / 2) % 1`]
    779   *        The single line width used to obtain a crisp line.
    780   * @param {boolean} [options.skipLineAndStroke = false]
    781   *        Skip the setting of lineWidth and strokeStyle.
    782   * @param {boolean} [options.useContainerScrollOffsets = false]
    783   *        Take the flexbox container's scroll and zoom offsets into account.
    784   *        This is needed for drawing flex lines and justify content when the
    785   *        flexbox container itself is display:scroll.
    786   */
    787  setupCanvas({
    788    lineDash = null,
    789    lineWidthMultiplier = 1,
    790    offset = (getDisplayPixelRatio(this.win) / 2) % 1,
    791    skipLineAndStroke = false,
    792    useContainerScrollOffsets = false,
    793  }) {
    794    const { devicePixelRatio } = this.win;
    795    const lineWidth = getDisplayPixelRatio(this.win);
    796    const zoom = getCurrentZoom(this.win);
    797    const style = getComputedStyle(this.container);
    798    const position = style.position;
    799    let offsetX = this._canvasPosition.x;
    800    let offsetY = this._canvasPosition.y;
    801 
    802    if (useContainerScrollOffsets) {
    803      offsetX += this.container.scrollLeft / zoom;
    804      offsetY += this.container.scrollTop / zoom;
    805    }
    806 
    807    // If the flexbox container is position:fixed we need to subtract the scroll
    808    // positions of all ancestral elements.
    809    if (position === "fixed") {
    810      const { scrollLeft, scrollTop } = getAbsoluteScrollOffsetsForNode(
    811        this.container
    812      );
    813      offsetX -= scrollLeft / zoom;
    814      offsetY -= scrollTop / zoom;
    815    }
    816 
    817    const canvasX = Math.round(offsetX * devicePixelRatio * zoom);
    818    const canvasY = Math.round(offsetY * devicePixelRatio * zoom);
    819 
    820    this.ctx.save();
    821    this.ctx.translate(offset - canvasX, offset - canvasY);
    822 
    823    if (lineDash) {
    824      this.ctx.setLineDash(lineDash);
    825    }
    826 
    827    if (!skipLineAndStroke) {
    828      this.ctx.lineWidth = lineWidth * lineWidthMultiplier;
    829      this.ctx.strokeStyle = this.color;
    830    }
    831  }
    832 
    833  _update() {
    834    setIgnoreLayoutChanges(true);
    835 
    836    this._winDimensions = getWindowDimensions(this.win);
    837    const { width, height } = this._winDimensions;
    838 
    839    // Updates the <canvas> element's position and size.
    840    // It also clear the <canvas>'s drawing context.
    841    updateCanvasElement(
    842      this.canvas,
    843      this._canvasPosition,
    844      this.win.devicePixelRatio,
    845      {
    846        zoomWindow: this.win,
    847      }
    848    );
    849 
    850    // Update the current matrix used in our canvas' rendering
    851    const { currentMatrix, hasNodeTransformations } = getCurrentMatrix(
    852      this.container,
    853      this.win,
    854      {
    855        ignoreWritingModeAndTextDirection: true,
    856      }
    857    );
    858    this.currentMatrix = currentMatrix;
    859    this.hasNodeTransformations = hasNodeTransformations;
    860 
    861    if (this.prevColor != this.color) {
    862      this.clearCache();
    863    }
    864    this.renderFlexContainer();
    865    this.renderFlexLines();
    866    this.renderJustifyContent();
    867    this.renderFlexItems();
    868    this._showFlexbox();
    869    this.prevColor = this.color;
    870 
    871    const root = this.getNode("flexbox-root");
    872    root.style.setProperty("width", `${width}px`);
    873    root.style.setProperty("height", `${height}px`);
    874 
    875    setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
    876    return true;
    877  }
    878 }
    879 
    880 /**
    881 * Returns an object representation of the Flex data object and its array of FlexLine
    882 * and FlexItem objects along with the DOMRects of the flex items.
    883 *
    884 * @param  {DOMNode} container
    885 *         The flex container.
    886 * @return {object | null} representation of the Flex data object.
    887 */
    888 function getFlexData(container) {
    889  const flex = container.getAsFlexContainer();
    890 
    891  if (!flex) {
    892    return null;
    893  }
    894 
    895  return {
    896    lines: flex.getLines().map(line => {
    897      return {
    898        crossSize: line.crossSize,
    899        crossStart: line.crossStart,
    900        firstBaselineOffset: line.firstBaselineOffset,
    901        growthState: line.growthState,
    902        lastBaselineOffset: line.lastBaselineOffset,
    903        items: line.getItems().map(item => {
    904          return {
    905            crossMaxSize: item.crossMaxSize,
    906            crossMinSize: item.crossMinSize,
    907            mainBaseSize: item.mainBaseSize,
    908            mainDeltaSize: item.mainDeltaSize,
    909            mainMaxSize: item.mainMaxSize,
    910            mainMinSize: item.mainMinSize,
    911            node: item.node,
    912            rect: getRectFromFlexItemValues(item, container),
    913          };
    914        }),
    915      };
    916    }),
    917  };
    918 }
    919 
    920 /**
    921 * Given a FlexItemValues, return a DOMRect representing the flex item taking
    922 * into account its flex container's border and padding.
    923 *
    924 * @param  {FlexItemValues} item
    925 *         The FlexItemValues for which we need the DOMRect.
    926 * @param  {DOMNode}
    927 *         Flex container containing the flex item.
    928 * @return {DOMRect} representing the flex item.
    929 */
    930 function getRectFromFlexItemValues(item, container) {
    931  const rect = item.frameRect;
    932  const domRect = new DOMRect(rect.x, rect.y, rect.width, rect.height);
    933  const win = container.ownerGlobal;
    934  const style = win.getComputedStyle(container);
    935  const borderLeftWidth = parseInt(style.borderLeftWidth, 10) || 0;
    936  const borderTopWidth = parseInt(style.borderTopWidth, 10) || 0;
    937  const paddingLeft = parseInt(style.paddingLeft, 10) || 0;
    938  const paddingTop = parseInt(style.paddingTop, 10) || 0;
    939  const scrollX = container.scrollLeft || 0;
    940  const scrollY = container.scrollTop || 0;
    941 
    942  domRect.x -= paddingLeft + scrollX;
    943  domRect.y -= paddingTop + scrollY;
    944 
    945  if (style.overflow === "visible" || style.overflow === "clip") {
    946    domRect.x -= borderLeftWidth;
    947    domRect.y -= borderTopWidth;
    948  }
    949 
    950  return domRect;
    951 }
    952 
    953 /**
    954 * Returns whether or not the flex data has changed.
    955 *
    956 * @param  {Flex} oldFlexData
    957 *         The old Flex data object.
    958 * @param  {Flex} newFlexData
    959 *         The new Flex data object.
    960 * @return {boolean} true if the flex data has changed and false otherwise.
    961 */
    962 // eslint-disable-next-line complexity
    963 function compareFlexData(oldFlexData, newFlexData) {
    964  if (!oldFlexData || !newFlexData) {
    965    return true;
    966  }
    967 
    968  const oldLines = oldFlexData.lines;
    969  const newLines = newFlexData.lines;
    970 
    971  if (oldLines.length !== newLines.length) {
    972    return true;
    973  }
    974 
    975  for (let i = 0; i < oldLines.length; i++) {
    976    const oldLine = oldLines[i];
    977    const newLine = newLines[i];
    978 
    979    if (
    980      oldLine.crossSize !== newLine.crossSize ||
    981      oldLine.crossStart !== newLine.crossStart ||
    982      oldLine.firstBaselineOffset !== newLine.firstBaselineOffset ||
    983      oldLine.growthState !== newLine.growthState ||
    984      oldLine.lastBaselineOffset !== newLine.lastBaselineOffset
    985    ) {
    986      return true;
    987    }
    988 
    989    const oldItems = oldLine.items;
    990    const newItems = newLine.items;
    991 
    992    if (oldItems.length !== newItems.length) {
    993      return true;
    994    }
    995 
    996    for (let j = 0; j < oldItems.length; j++) {
    997      const oldItem = oldItems[j];
    998      const newItem = newItems[j];
    999 
   1000      if (
   1001        oldItem.crossMaxSize !== newItem.crossMaxSize ||
   1002        oldItem.crossMinSize !== newItem.crossMinSize ||
   1003        oldItem.mainBaseSize !== newItem.mainBaseSize ||
   1004        oldItem.mainDeltaSize !== newItem.mainDeltaSize ||
   1005        oldItem.mainMaxSize !== newItem.mainMaxSize ||
   1006        oldItem.mainMinSize !== newItem.mainMinSize
   1007      ) {
   1008        return true;
   1009      }
   1010 
   1011      const oldItemRect = oldItem.rect;
   1012      const newItemRect = newItem.rect;
   1013 
   1014      // We are using DOMRects so we only need to compare x, y, width and
   1015      // height (left, top, right and bottom are calculated from these values).
   1016      if (
   1017        oldItemRect.x !== newItemRect.x ||
   1018        oldItemRect.y !== newItemRect.y ||
   1019        oldItemRect.width !== newItemRect.width ||
   1020        oldItemRect.height !== newItemRect.height
   1021      ) {
   1022        return true;
   1023      }
   1024    }
   1025  }
   1026 
   1027  return false;
   1028 }
   1029 
   1030 exports.FlexboxHighlighter = FlexboxHighlighter;