tor-browser

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

GridOutline.js (12673B)


      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  PureComponent,
      9 } = require("resource://devtools/client/shared/vendor/react.mjs");
     10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     12 const {
     13  getStr,
     14 } = require("resource://devtools/client/inspector/layout/utils/l10n.js");
     15 const {
     16  getWritingModeMatrix,
     17  getCSSMatrixTransform,
     18 } = require("resource://devtools/shared/layout/dom-matrix-2d.js");
     19 
     20 const Types = require("resource://devtools/client/inspector/grids/types.js");
     21 
     22 // The delay prior to executing the grid cell highlighting.
     23 const GRID_HIGHLIGHTING_DEBOUNCE = 50;
     24 
     25 // Prefs for the max number of rows/cols a grid container can have for
     26 // the outline to display.
     27 const GRID_OUTLINE_MAX_ROWS_PREF = Services.prefs.getIntPref(
     28  "devtools.gridinspector.gridOutlineMaxRows"
     29 );
     30 const GRID_OUTLINE_MAX_COLUMNS_PREF = Services.prefs.getIntPref(
     31  "devtools.gridinspector.gridOutlineMaxColumns"
     32 );
     33 
     34 // Move SVG grid to the right 100 units, so that it is not flushed against the edge of
     35 // layout border
     36 const TRANSLATE_X = 0;
     37 const TRANSLATE_Y = 0;
     38 
     39 const GRID_CELL_SCALE_FACTOR = 50;
     40 
     41 const VIEWPORT_MIN_HEIGHT = 100;
     42 const VIEWPORT_MAX_HEIGHT = 150;
     43 
     44 const {
     45  showGridHighlighter,
     46 } = require("resource://devtools/client/inspector/grids/actions/grid-highlighter.js");
     47 
     48 class GridOutline extends PureComponent {
     49  static get propTypes() {
     50    return {
     51      dispatch: PropTypes.func.isRequired,
     52      grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
     53    };
     54  }
     55 
     56  static getDerivedStateFromProps(props) {
     57    const selectedGrid = props.grids.find(grid => grid.highlighted);
     58 
     59    // Store the height of the grid container in the component state to prevent overflow
     60    // issues. We want to store the width of the grid container as well so that the
     61    // viewbox is only the calculated width of the grid outline.
     62    const { width, height } = selectedGrid?.gridFragments.length
     63      ? getTotalWidthAndHeight(selectedGrid)
     64      : { width: 0, height: 0 };
     65    let showOutline;
     66 
     67    if (selectedGrid?.gridFragments.length) {
     68      const { cols, rows } = selectedGrid.gridFragments[0];
     69 
     70      // Show the grid outline if both the rows/columns are less than or equal
     71      // to their max prefs.
     72      showOutline =
     73        cols.lines.length <= GRID_OUTLINE_MAX_COLUMNS_PREF &&
     74        rows.lines.length <= GRID_OUTLINE_MAX_ROWS_PREF;
     75    }
     76 
     77    return { height, width, selectedGrid, showOutline };
     78  }
     79 
     80  constructor(props) {
     81    super(props);
     82 
     83    this.state = {
     84      height: 0,
     85      selectedGrid: null,
     86      showOutline: true,
     87      width: 0,
     88    };
     89 
     90    this.doHighlightCell = this.doHighlightCell.bind(this);
     91    this.getGridAreaName = this.getGridAreaName.bind(this);
     92    this.getHeight = this.getHeight.bind(this);
     93    this.onHighlightCell = this.onHighlightCell.bind(this);
     94    this.renderCannotShowOutlineText =
     95      this.renderCannotShowOutlineText.bind(this);
     96    this.renderGrid = this.renderGrid.bind(this);
     97    this.renderGridCell = this.renderGridCell.bind(this);
     98    this.renderGridOutline = this.renderGridOutline.bind(this);
     99    this.renderGridOutlineBorder = this.renderGridOutlineBorder.bind(this);
    100    this.renderOutline = this.renderOutline.bind(this);
    101  }
    102 
    103  doHighlightCell(target, hide) {
    104    const { dispatch, grids } = this.props;
    105    const name = target.dataset.gridAreaName;
    106    const id = target.dataset.gridId;
    107    const gridFragmentIndex = target.dataset.gridFragmentIndex;
    108    const rowNumber = target.dataset.gridRow;
    109    const columnNumber = target.dataset.gridColumn;
    110    const nodeFront = grids[id].nodeFront;
    111 
    112    // The options object has the following properties which corresponds to the
    113    // required parameters for showing the grid cell or area highlights.
    114    // See devtools/server/actors/highlighters/css-grid.js
    115    // {
    116    //   showGridArea: String,
    117    //   showGridCell: {
    118    //     gridFragmentIndex: Number,
    119    //     rowNumber: Number,
    120    //     columnNumber: Number,
    121    //   },
    122    // }
    123    const options = {
    124      showGridArea: name,
    125      showGridCell: {
    126        gridFragmentIndex,
    127        rowNumber,
    128        columnNumber,
    129      },
    130    };
    131 
    132    if (hide) {
    133      // Reset the grid highlighter to default state; no options = hide cell/area outline.
    134      dispatch(showGridHighlighter(nodeFront));
    135    } else {
    136      dispatch(showGridHighlighter(nodeFront, options));
    137    }
    138  }
    139 
    140  /**
    141   * Returns the grid area name if the given grid cell is part of a grid area, otherwise
    142   * null.
    143   *
    144   * @param  {number} columnNumber
    145   *         The column number of the grid cell.
    146   * @param  {number} rowNumber
    147   *         The row number of the grid cell.
    148   * @param  {Array} areas
    149   *         Array of grid areas data stored in the grid fragment.
    150   * @return {string} If there is a grid area return area name, otherwise null.
    151   */
    152  getGridAreaName(columnNumber, rowNumber, areas) {
    153    const gridArea = areas.find(
    154      area =>
    155        area.rowStart <= rowNumber &&
    156        area.rowEnd > rowNumber &&
    157        area.columnStart <= columnNumber &&
    158        area.columnEnd > columnNumber
    159    );
    160 
    161    if (!gridArea) {
    162      return null;
    163    }
    164 
    165    return gridArea.name;
    166  }
    167 
    168  /**
    169   * Returns the height of the grid outline ranging between a minimum and maximum height.
    170   *
    171   * @return {number} The height of the grid outline.
    172   */
    173  getHeight() {
    174    const { height } = this.state;
    175 
    176    if (height >= VIEWPORT_MAX_HEIGHT) {
    177      return VIEWPORT_MAX_HEIGHT;
    178    } else if (height <= VIEWPORT_MIN_HEIGHT) {
    179      return VIEWPORT_MIN_HEIGHT;
    180    }
    181 
    182    return height;
    183  }
    184 
    185  /**
    186   * Displays a message text "Cannot show outline for this grid".
    187   */
    188  renderCannotShowOutlineText() {
    189    return dom.div(
    190      { className: "grid-outline-text" },
    191      dom.span({
    192        className: "grid-outline-text-icon",
    193        title: getStr("layout.cannotShowGridOutline.title"),
    194      }),
    195      getStr("layout.cannotShowGridOutline")
    196    );
    197  }
    198 
    199  /**
    200   * Renders the grid outline for the given grid container object.
    201   *
    202   * @param  {object} grid
    203   *         A single grid container in the document.
    204   */
    205  renderGrid(grid) {
    206    // TODO: We are drawing the first fragment since only one is currently being stored.
    207    // In the future we will need to iterate over all fragments of a grid.
    208    const gridFragmentIndex = 0;
    209    const { id, color, gridFragments } = grid;
    210    const { rows, cols, areas } = gridFragments[gridFragmentIndex];
    211 
    212    const numberOfColumns = cols.lines.length - 1;
    213    const numberOfRows = rows.lines.length - 1;
    214    const rectangles = [];
    215    let x = 0;
    216    let y = 0;
    217    let width = 0;
    218    let height = 0;
    219 
    220    // Draw the cells contained within the grid outline border.
    221    for (let rowNumber = 1; rowNumber <= numberOfRows; rowNumber++) {
    222      height =
    223        GRID_CELL_SCALE_FACTOR * (rows.tracks[rowNumber - 1].breadth / 100);
    224 
    225      for (
    226        let columnNumber = 1;
    227        columnNumber <= numberOfColumns;
    228        columnNumber++
    229      ) {
    230        width =
    231          GRID_CELL_SCALE_FACTOR *
    232          (cols.tracks[columnNumber - 1].breadth / 100);
    233 
    234        const gridAreaName = this.getGridAreaName(
    235          columnNumber,
    236          rowNumber,
    237          areas
    238        );
    239        const gridCell = this.renderGridCell(
    240          id,
    241          gridFragmentIndex,
    242          x,
    243          y,
    244          rowNumber,
    245          columnNumber,
    246          color,
    247          gridAreaName,
    248          width,
    249          height
    250        );
    251 
    252        rectangles.push(gridCell);
    253        x += width;
    254      }
    255 
    256      x = 0;
    257      y += height;
    258    }
    259 
    260    // Transform the cells as needed to match the grid container's writing mode.
    261    const cellGroupStyle = {};
    262    const writingModeMatrix = getWritingModeMatrix(this.state, grid);
    263    cellGroupStyle.transform = getCSSMatrixTransform(writingModeMatrix);
    264    const cellGroup = dom.g(
    265      {
    266        id: "grid-cell-group",
    267        style: cellGroupStyle,
    268      },
    269      rectangles
    270    );
    271 
    272    // Draw a rectangle that acts as the grid outline border.
    273    const border = this.renderGridOutlineBorder(
    274      this.state.width,
    275      this.state.height,
    276      color
    277    );
    278 
    279    return [border, cellGroup];
    280  }
    281 
    282  /**
    283   * Renders the grid cell of a grid fragment.
    284   *
    285   * @param  {number} id
    286   *         The grid id stored on the grid fragment
    287   * @param  {number} gridFragmentIndex
    288   *         The index of the grid fragment rendered to the document.
    289   * @param  {number} x
    290   *         The x-position of the grid cell.
    291   * @param  {number} y
    292   *         The y-position of the grid cell.
    293   * @param  {number} rowNumber
    294   *         The row number of the grid cell.
    295   * @param  {number} columnNumber
    296   *         The column number of the grid cell.
    297   * @param  {string | null} gridAreaName
    298   *         The grid area name or null if the grid cell is not part of a grid area.
    299   * @param  {number} width
    300   *         The width of grid cell.
    301   * @param  {number} height
    302   *         The height of the grid cell.
    303   */
    304  renderGridCell(
    305    id,
    306    gridFragmentIndex,
    307    x,
    308    y,
    309    rowNumber,
    310    columnNumber,
    311    color,
    312    gridAreaName,
    313    width,
    314    height
    315  ) {
    316    return dom.rect({
    317      key: `${id}-${rowNumber}-${columnNumber}`,
    318      className: "grid-outline-cell",
    319      "data-grid-area-name": gridAreaName,
    320      "data-grid-fragment-index": gridFragmentIndex,
    321      "data-grid-id": id,
    322      "data-grid-row": rowNumber,
    323      "data-grid-column": columnNumber,
    324      x,
    325      y,
    326      width,
    327      height,
    328      fill: "none",
    329      onMouseEnter: this.onHighlightCell,
    330      onMouseLeave: this.onHighlightCell,
    331    });
    332  }
    333 
    334  renderGridOutline(grid) {
    335    const { color } = grid;
    336 
    337    return dom.g(
    338      {
    339        id: "grid-outline-group",
    340        className: "grid-outline-group",
    341        style: { color },
    342      },
    343      this.renderGrid(grid)
    344    );
    345  }
    346 
    347  renderGridOutlineBorder(borderWidth, borderHeight) {
    348    return dom.rect({
    349      key: "border",
    350      className: "grid-outline-border",
    351      x: 0,
    352      y: 0,
    353      width: borderWidth,
    354      height: borderHeight,
    355    });
    356  }
    357 
    358  renderOutline() {
    359    const { height, selectedGrid, showOutline, width } = this.state;
    360 
    361    return showOutline
    362      ? dom.svg(
    363          {
    364            id: "grid-outline",
    365            width: "100%",
    366            height: this.getHeight(),
    367            viewBox: `${TRANSLATE_X} ${TRANSLATE_Y} ${width} ${height}`,
    368          },
    369          this.renderGridOutline(selectedGrid)
    370        )
    371      : this.renderCannotShowOutlineText();
    372  }
    373 
    374  onHighlightCell({ target, type }) {
    375    // Debounce the highlighting of cells.
    376    // This way we don't end up sending many requests to the server for highlighting when
    377    // cells get hovered in a rapid succession We only send a request if the user settles
    378    // on a cell for some time.
    379    if (this.highlightTimeout) {
    380      clearTimeout(this.highlightTimeout);
    381    }
    382 
    383    this.highlightTimeout = setTimeout(() => {
    384      this.doHighlightCell(target, type === "mouseleave");
    385      this.highlightTimeout = null;
    386    }, GRID_HIGHLIGHTING_DEBOUNCE);
    387  }
    388 
    389  render() {
    390    const { selectedGrid } = this.state;
    391 
    392    return selectedGrid?.gridFragments.length
    393      ? dom.div(
    394          {
    395            id: "grid-outline-container",
    396            className: "grid-outline-container",
    397          },
    398          this.renderOutline()
    399        )
    400      : null;
    401  }
    402 }
    403 
    404 /**
    405 * Get the width and height of a given grid.
    406 *
    407 * @param  {object} grid
    408 *         A single grid container in the document.
    409 * @return {object} An object like { width, height }
    410 */
    411 function getTotalWidthAndHeight(grid) {
    412  // TODO: We are drawing the first fragment since only one is currently being stored.
    413  // In the future we will need to iterate over all fragments of a grid.
    414  const { gridFragments } = grid;
    415  const { rows, cols } = gridFragments[0];
    416 
    417  let height = 0;
    418  for (let i = 0; i < rows.lines.length - 1; i++) {
    419    height += GRID_CELL_SCALE_FACTOR * (rows.tracks[i].breadth / 100);
    420  }
    421 
    422  let width = 0;
    423  for (let i = 0; i < cols.lines.length - 1; i++) {
    424    width += GRID_CELL_SCALE_FACTOR * (cols.tracks[i].breadth / 100);
    425  }
    426 
    427  // All writing modes other than horizontal-tb (the initial value) involve a 90 deg
    428  // rotation, so swap width and height.
    429  if (grid.writingMode != "horizontal-tb") {
    430    [width, height] = [height, width];
    431  }
    432 
    433  return { width, height };
    434 }
    435 
    436 module.exports = GridOutline;