tor-browser

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

flexbox.js (18068B)


      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 { throttle } = require("resource://devtools/shared/throttle.js");
      8 
      9 const {
     10  clearFlexbox,
     11  updateFlexbox,
     12  updateFlexboxColor,
     13  updateFlexboxHighlighted,
     14 } = require("resource://devtools/client/inspector/flexbox/actions/flexbox.js");
     15 const flexboxReducer = require("resource://devtools/client/inspector/flexbox/reducers/flexbox.js");
     16 
     17 loader.lazyRequireGetter(
     18  this,
     19  "parseURL",
     20  "resource://devtools/client/shared/source-utils.js",
     21  true
     22 );
     23 loader.lazyRequireGetter(
     24  this,
     25  "asyncStorage",
     26  "resource://devtools/shared/async-storage.js"
     27 );
     28 
     29 const FLEXBOX_COLOR = "#9400FF";
     30 
     31 class FlexboxInspector {
     32  constructor(inspector, window) {
     33    this.document = window.document;
     34    this.inspector = inspector;
     35    this.selection = inspector.selection;
     36    this.store = inspector.store;
     37 
     38    this.store.injectReducer("flexbox", flexboxReducer);
     39 
     40    this.onHighlighterShown = this.onHighlighterShown.bind(this);
     41    this.onHighlighterHidden = this.onHighlighterHidden.bind(this);
     42    this.onNavigate = this.onNavigate.bind(this);
     43    this.onReflow = throttle(this.onReflow, 500, this);
     44    this.onSetFlexboxOverlayColor = this.onSetFlexboxOverlayColor.bind(this);
     45    this.onSidebarSelect = this.onSidebarSelect.bind(this);
     46    this.onUpdatePanel = this.onUpdatePanel.bind(this);
     47 
     48    this.init();
     49  }
     50 
     51  init() {
     52    if (!this.inspector) {
     53      return;
     54    }
     55 
     56    this.inspector.highlighters.on(
     57      "highlighter-shown",
     58      this.onHighlighterShown
     59    );
     60    this.inspector.highlighters.on(
     61      "highlighter-hidden",
     62      this.onHighlighterHidden
     63    );
     64    this.inspector.sidebar.on("select", this.onSidebarSelect);
     65 
     66    this.onSidebarSelect();
     67  }
     68 
     69  destroy() {
     70    this.selection.off("new-node-front", this.onUpdatePanel);
     71    this.inspector.off("new-root", this.onNavigate);
     72    this.inspector.off("reflow-in-selected-target", this.onReflow);
     73    this.inspector.highlighters.off(
     74      "highlighter-shown",
     75      this.onHighlighterShown
     76    );
     77    this.inspector.highlighters.off(
     78      "highlighter-hidden",
     79      this.onHighlighterHidden
     80    );
     81    this.inspector.sidebar.off("select", this.onSidebarSelect);
     82 
     83    this._customHostColors = null;
     84    this._overlayColor = null;
     85    this.document = null;
     86    this.inspector = null;
     87    this.selection = null;
     88    this.store = null;
     89  }
     90 
     91  getComponentProps() {
     92    return {
     93      onSetFlexboxOverlayColor: this.onSetFlexboxOverlayColor,
     94    };
     95  }
     96 
     97  /**
     98   * Returns an object containing the custom flexbox colors for different hosts.
     99   *
    100   * @return {object} that maps a host name to a custom flexbox color for a given host.
    101   */
    102  async getCustomHostColors() {
    103    if (this._customHostColors) {
    104      return this._customHostColors;
    105    }
    106 
    107    // Cache the custom host colors to avoid refetching from async storage.
    108    this._customHostColors =
    109      (await asyncStorage.getItem("flexboxInspectorHostColors")) || {};
    110    return this._customHostColors;
    111  }
    112 
    113  /**
    114   * Returns the flex container properties for a given node. If the given node is a flex
    115   * item, it attempts to fetch the flex container of the parent node of the given node.
    116   *
    117   * @param  {NodeFront} nodeFront
    118   *         The NodeFront to fetch the flex container properties.
    119   * @param  {boolean} onlyLookAtParents
    120   *         Whether or not to only consider the parent node of the given node.
    121   * @return {object} consisting of the given node's flex container's properties.
    122   */
    123  async getFlexContainerProps(nodeFront, onlyLookAtParents = false) {
    124    const layoutFront = await nodeFront.walkerFront.getLayoutInspector();
    125    const flexboxFront = await layoutFront.getCurrentFlexbox(
    126      nodeFront,
    127      onlyLookAtParents
    128    );
    129 
    130    if (!flexboxFront) {
    131      return null;
    132    }
    133 
    134    // If the FlexboxFront doesn't yet have access to the NodeFront for its container,
    135    // then get it from the walker. This happens when the walker hasn't seen this
    136    // particular DOM Node in the tree yet or when we are connected to an older server.
    137    let containerNodeFront = flexboxFront.containerNodeFront;
    138    if (!containerNodeFront) {
    139      containerNodeFront = await flexboxFront.walkerFront.getNodeFromActor(
    140        flexboxFront.actorID,
    141        ["containerEl"]
    142      );
    143    }
    144 
    145    const flexItems = await this.getFlexItems(flexboxFront);
    146 
    147    // If the current selected node is a flex item, display its flex item sizing
    148    // properties.
    149    let flexItemShown = null;
    150    if (onlyLookAtParents) {
    151      flexItemShown = this.selection.nodeFront.actorID;
    152    } else {
    153      const selectedFlexItem = flexItems.find(
    154        item => item.nodeFront === this.selection.nodeFront
    155      );
    156      if (selectedFlexItem) {
    157        flexItemShown = selectedFlexItem.nodeFront.actorID;
    158      }
    159    }
    160 
    161    return {
    162      actorID: flexboxFront.actorID,
    163      flexItems,
    164      flexItemShown,
    165      isFlexItemContainer: onlyLookAtParents,
    166      nodeFront: containerNodeFront,
    167      properties: flexboxFront.properties,
    168    };
    169  }
    170 
    171  /**
    172   * Returns an array of flex items object for the given flex container front.
    173   *
    174   * @param  {FlexboxFront} flexboxFront
    175   *         A flex container FlexboxFront.
    176   * @return {Array} of objects containing the flex item front properties.
    177   */
    178  async getFlexItems(flexboxFront) {
    179    const flexItemFronts = await flexboxFront.getFlexItems();
    180    const flexItems = [];
    181 
    182    for (const flexItemFront of flexItemFronts) {
    183      // Fetch the NodeFront of the flex items.
    184      let itemNodeFront = flexItemFront.nodeFront;
    185      if (!itemNodeFront) {
    186        itemNodeFront = await flexItemFront.walkerFront.getNodeFromActor(
    187          flexItemFront.actorID,
    188          ["element"]
    189        );
    190      }
    191 
    192      flexItems.push({
    193        actorID: flexItemFront.actorID,
    194        computedStyle: flexItemFront.computedStyle,
    195        flexItemSizing: flexItemFront.flexItemSizing,
    196        nodeFront: itemNodeFront,
    197        properties: flexItemFront.properties,
    198      });
    199    }
    200 
    201    return flexItems;
    202  }
    203 
    204  /**
    205   * Returns the custom overlay color for the current host or the default flexbox color.
    206   *
    207   * @return {string} overlay color.
    208   */
    209  async getOverlayColor() {
    210    if (this._overlayColor) {
    211      return this._overlayColor;
    212    }
    213 
    214    // Cache the overlay color for the current host to avoid repeatably parsing the host
    215    // and fetching the custom color from async storage.
    216    const customColors = await this.getCustomHostColors();
    217    const currentUrl = this.inspector.currentTarget.url;
    218    // Get the hostname, if there is no hostname, fall back on protocol
    219    // ex: `data:` uri, and `about:` pages
    220    const hostname =
    221      parseURL(currentUrl).hostname || parseURL(currentUrl).protocol;
    222    this._overlayColor = customColors[hostname]
    223      ? customColors[hostname]
    224      : FLEXBOX_COLOR;
    225    return this._overlayColor;
    226  }
    227 
    228  /**
    229   * Returns true if the layout panel is visible, and false otherwise.
    230   */
    231  isPanelVisible() {
    232    return (
    233      this.inspector &&
    234      this.inspector.toolbox &&
    235      this.inspector.sidebar &&
    236      this.inspector.toolbox.currentToolId === "inspector" &&
    237      this.inspector.sidebar.getCurrentTabID() === "layoutview"
    238    );
    239  }
    240 
    241  /**
    242   * Handler for "highlighter-shown" events emitted by HighlightersOverlay.
    243   * If the event is dispatched on behalf of a flex highlighter, toggle the
    244   * corresponding flex container's highlighted state in the Redux store.
    245   *
    246   * @param {object} data
    247   *        Object with data associated with the highlighter event.
    248   *        {NodeFront} data.nodeFront
    249   *        The NodeFront of the flex container element for which the flexbox
    250   *        highlighter is shown for.
    251   *        {String} data.type
    252   *        Highlighter type
    253   */
    254  onHighlighterShown(data) {
    255    if (data.type === this.inspector.highlighters.TYPES.FLEXBOX) {
    256      this.onHighlighterChange(true, data.nodeFront);
    257    }
    258  }
    259 
    260  /**
    261   * Handler for "highlighter-shown" events emitted by HighlightersOverlay.
    262   * If the event is dispatched on behalf of a flex highlighter, toggle the
    263   * corresponding flex container's highlighted state in the Redux store.
    264   *
    265   * @param {object} data
    266   *        Object with data associated with the highlighter event.
    267   *        {NodeFront} data.nodeFront
    268   *        The NodeFront of the flex container element for which the flexbox
    269   *        highlighter was previously shown for.
    270   *        {String} data.type
    271   *        Highlighter type
    272   */
    273  onHighlighterHidden(data) {
    274    if (data.type === this.inspector.highlighters.TYPES.FLEXBOX) {
    275      this.onHighlighterChange(false, data.nodeFront);
    276    }
    277  }
    278 
    279  /**
    280   * Updates the flex container highlighted state in the Redux store if the provided
    281   * NodeFront is the current selected flex container.
    282   *
    283   * @param  {boolean} highlighted
    284   *         Whether the change is to highlight or hide the overlay.
    285   * @param  {NodeFront} nodeFront
    286   *         The NodeFront of the flex container element for which the flexbox
    287   *         highlighter is shown for.
    288   */
    289  onHighlighterChange(highlighted, nodeFront) {
    290    const { flexbox } = this.store.getState();
    291 
    292    if (
    293      flexbox.flexContainer.nodeFront === nodeFront &&
    294      flexbox.highlighted !== highlighted
    295    ) {
    296      this.store.dispatch(updateFlexboxHighlighted(highlighted));
    297    }
    298  }
    299 
    300  /**
    301   * Handler for the "new-root" event fired by the inspector. Clears the cached overlay
    302   * color for the flexbox highlighter and updates the panel.
    303   */
    304  onNavigate() {
    305    this._overlayColor = null;
    306    this.onUpdatePanel();
    307  }
    308 
    309  /**
    310   * Handler for reflow events fired by the inspector when a node is selected. On reflows,
    311   * update the flexbox panel because the shape of the flexbox on the page may have
    312   * changed.
    313   */
    314  async onReflow() {
    315    if (
    316      !this.isPanelVisible() ||
    317      !this.store ||
    318      !this.selection.nodeFront ||
    319      this._isUpdating
    320    ) {
    321      return;
    322    }
    323 
    324    try {
    325      const flexContainer = await this.getFlexContainerProps(
    326        this.selection.nodeFront
    327      );
    328 
    329      // Clear the flexbox panel if there is no flex container for the current node
    330      // selection.
    331      if (!flexContainer) {
    332        this.store.dispatch(clearFlexbox());
    333        return;
    334      }
    335 
    336      const { flexbox } = this.store.getState();
    337 
    338      // Compare the new flexbox state of the current selected nodeFront with the old
    339      // flexbox state to determine if we need to update.
    340      if (hasFlexContainerChanged(flexbox.flexContainer, flexContainer)) {
    341        this.update(flexContainer);
    342        return;
    343      }
    344 
    345      let flexItemContainer = null;
    346      // If the current selected node is also the flex container node, check if it is
    347      // a flex item of a parent flex container.
    348      if (flexContainer.nodeFront === this.selection.nodeFront) {
    349        flexItemContainer = await this.getFlexContainerProps(
    350          this.selection.nodeFront,
    351          true
    352        );
    353      }
    354 
    355      // Compare the new and old state of the parent flex container properties.
    356      if (
    357        hasFlexContainerChanged(flexbox.flexItemContainer, flexItemContainer)
    358      ) {
    359        this.update(flexContainer, flexItemContainer);
    360      }
    361    } catch (e) {
    362      // This call might fail if called asynchrously after the toolbox is finished
    363      // closing.
    364    }
    365  }
    366 
    367  /**
    368   * Handler for a change in the flexbox overlay color picker for a flex container.
    369   *
    370   * @param  {string} color
    371   *         A hex string representing the color to use.
    372   */
    373  async onSetFlexboxOverlayColor(color) {
    374    this.store.dispatch(updateFlexboxColor(color));
    375 
    376    const { flexbox } = this.store.getState();
    377 
    378    if (flexbox.highlighted) {
    379      this.inspector.highlighters.showFlexboxHighlighter(
    380        flexbox.flexContainer.nodeFront
    381      );
    382    }
    383 
    384    this._overlayColor = color;
    385 
    386    const currentUrl = this.inspector.currentTarget.url;
    387    // Get the hostname, if there is no hostname, fall back on protocol
    388    // ex: `data:` uri, and `about:` pages
    389    const hostname =
    390      parseURL(currentUrl).hostname || parseURL(currentUrl).protocol;
    391    const customColors = await this.getCustomHostColors();
    392    customColors[hostname] = color;
    393    this._customHostColors = customColors;
    394    await asyncStorage.setItem("flexboxInspectorHostColors", customColors);
    395  }
    396 
    397  /**
    398   * Handler for the inspector sidebar "select" event. Updates the flexbox panel if it
    399   * is visible.
    400   */
    401  onSidebarSelect() {
    402    if (!this.isPanelVisible()) {
    403      this.inspector.off("reflow-in-selected-target", this.onReflow);
    404      this.inspector.off("new-root", this.onNavigate);
    405      this.selection.off("new-node-front", this.onUpdatePanel);
    406      return;
    407    }
    408 
    409    this.inspector.on("reflow-in-selected-target", this.onReflow);
    410    this.inspector.on("new-root", this.onNavigate);
    411    this.selection.on("new-node-front", this.onUpdatePanel);
    412 
    413    this.update();
    414  }
    415 
    416  /**
    417   * Handler for "new-root" event fired by the inspector and "new-node-front" event fired
    418   * by the inspector selection. Updates the flexbox panel if it is visible.
    419   *
    420   * @param  {object}
    421   *         This callback is sometimes executed on "new-node-front" events which means
    422   *         that a first param is passed here (the nodeFront), which we don't care about.
    423   * @param  {string} reason
    424   *         On "new-node-front" events, a reason is passed here, and we need it to detect
    425   *         if this update was caused by a node selection from the markup-view.
    426   */
    427  onUpdatePanel(_, reason) {
    428    if (!this.isPanelVisible()) {
    429      return;
    430    }
    431 
    432    this.update(null, null, reason === "treepanel");
    433  }
    434 
    435  /**
    436   * Updates the flexbox panel by dispatching the new flexbox data. This is called when
    437   * the layout view becomes visible or a new node is selected and needs to be update
    438   * with new flexbox data.
    439   *
    440   * @param  {object | null} flexContainer
    441   *         An object consisting of the current flex container's flex items and
    442   *         properties.
    443   * @param  {object | null} flexItemContainer
    444   *         An object consisting of the parent flex container's flex items and
    445   *         properties.
    446   * @param  {boolean} initiatedByMarkupViewSelection
    447   *         True if the update was due to a node selection in the markup-view.
    448   */
    449  async update(
    450    flexContainer,
    451    flexItemContainer,
    452    initiatedByMarkupViewSelection
    453  ) {
    454    this._isUpdating = true;
    455 
    456    // Stop refreshing if the inspector or store is already destroyed or no node is
    457    // selected.
    458    if (!this.inspector || !this.store || !this.selection.nodeFront) {
    459      this._isUpdating = false;
    460      return;
    461    }
    462 
    463    try {
    464      // Fetch the current flexbox if no flexbox front was passed into this update.
    465      if (!flexContainer) {
    466        flexContainer = await this.getFlexContainerProps(
    467          this.selection.nodeFront
    468        );
    469      }
    470 
    471      // Clear the flexbox panel if there is no flex container for the current node
    472      // selection.
    473      if (!flexContainer) {
    474        this.store.dispatch(clearFlexbox());
    475        this._isUpdating = false;
    476        return;
    477      }
    478 
    479      if (
    480        !flexItemContainer &&
    481        flexContainer.nodeFront === this.selection.nodeFront
    482      ) {
    483        flexItemContainer = await this.getFlexContainerProps(
    484          this.selection.nodeFront,
    485          true
    486        );
    487      }
    488 
    489      const highlighted =
    490        flexContainer.nodeFront ===
    491        this.inspector.highlighters.getNodeForActiveHighlighter(
    492          this.inspector.highlighters.TYPES.FLEXBOX
    493        );
    494      const color = await this.getOverlayColor();
    495 
    496      this.store.dispatch(
    497        updateFlexbox({
    498          color,
    499          flexContainer,
    500          flexItemContainer,
    501          highlighted,
    502          initiatedByMarkupViewSelection,
    503        })
    504      );
    505    } catch (e) {
    506      // This call might fail if called asynchrously after the toolbox is finished
    507      // closing.
    508    }
    509 
    510    this._isUpdating = false;
    511  }
    512 }
    513 
    514 /**
    515 * For a given flex container object, returns the flex container properties that can be
    516 * used to check if 2 flex container objects are the same.
    517 *
    518 * @param  {object | null} flexContainer
    519 *         Object consisting of the flex container's properties.
    520 * @return {object | null} consisting of the comparable flex container's properties.
    521 */
    522 function getComparableFlexContainerProperties(flexContainer) {
    523  if (!flexContainer) {
    524    return null;
    525  }
    526 
    527  return {
    528    flexItems: getComparableFlexItemsProperties(flexContainer.flexItems),
    529    nodeFront: flexContainer.nodeFront.actorID,
    530    properties: flexContainer.properties,
    531  };
    532 }
    533 
    534 /**
    535 * Given an array of flex item objects, returns the relevant flex item properties that can
    536 * be compared to check if any changes has occurred.
    537 *
    538 * @param  {Array} flexItems
    539 *         Array of objects containing the flex item properties.
    540 * @return {Array} of objects consisting of the comparable flex item's properties.
    541 */
    542 function getComparableFlexItemsProperties(flexItems) {
    543  return flexItems.map(item => {
    544    return {
    545      computedStyle: item.computedStyle,
    546      flexItemSizing: item.flexItemSizing,
    547      nodeFront: item.nodeFront.actorID,
    548      properties: item.properties,
    549    };
    550  });
    551 }
    552 
    553 /**
    554 * Compares the old and new flex container properties
    555 *
    556 * @param  {object} oldFlexContainer
    557 *         Object consisting of the old flex container's properties.
    558 * @param  {object} newFlexContainer
    559 *         Object consisting of the new flex container's properties.
    560 * @return {boolean} true if the flex container properties are the same, false otherwise.
    561 */
    562 function hasFlexContainerChanged(oldFlexContainer, newFlexContainer) {
    563  return (
    564    JSON.stringify(getComparableFlexContainerProperties(oldFlexContainer)) !==
    565    JSON.stringify(getComparableFlexContainerProperties(newFlexContainer))
    566  );
    567 }
    568 
    569 module.exports = FlexboxInspector;