/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { PureComponent, } = require("resource://devtools/client/shared/vendor/react.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); const { getStr, } = require("resource://devtools/client/inspector/layout/utils/l10n.js"); const { getWritingModeMatrix, getCSSMatrixTransform, } = require("resource://devtools/shared/layout/dom-matrix-2d.js"); const Types = require("resource://devtools/client/inspector/grids/types.js"); // The delay prior to executing the grid cell highlighting. const GRID_HIGHLIGHTING_DEBOUNCE = 50; // Prefs for the max number of rows/cols a grid container can have for // the outline to display. const GRID_OUTLINE_MAX_ROWS_PREF = Services.prefs.getIntPref( "devtools.gridinspector.gridOutlineMaxRows" ); const GRID_OUTLINE_MAX_COLUMNS_PREF = Services.prefs.getIntPref( "devtools.gridinspector.gridOutlineMaxColumns" ); // Move SVG grid to the right 100 units, so that it is not flushed against the edge of // layout border const TRANSLATE_X = 0; const TRANSLATE_Y = 0; const GRID_CELL_SCALE_FACTOR = 50; const VIEWPORT_MIN_HEIGHT = 100; const VIEWPORT_MAX_HEIGHT = 150; const { showGridHighlighter, } = require("resource://devtools/client/inspector/grids/actions/grid-highlighter.js"); class GridOutline extends PureComponent { static get propTypes() { return { dispatch: PropTypes.func.isRequired, grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired, }; } static getDerivedStateFromProps(props) { const selectedGrid = props.grids.find(grid => grid.highlighted); // Store the height of the grid container in the component state to prevent overflow // issues. We want to store the width of the grid container as well so that the // viewbox is only the calculated width of the grid outline. const { width, height } = selectedGrid?.gridFragments.length ? getTotalWidthAndHeight(selectedGrid) : { width: 0, height: 0 }; let showOutline; if (selectedGrid?.gridFragments.length) { const { cols, rows } = selectedGrid.gridFragments[0]; // Show the grid outline if both the rows/columns are less than or equal // to their max prefs. showOutline = cols.lines.length <= GRID_OUTLINE_MAX_COLUMNS_PREF && rows.lines.length <= GRID_OUTLINE_MAX_ROWS_PREF; } return { height, width, selectedGrid, showOutline }; } constructor(props) { super(props); this.state = { height: 0, selectedGrid: null, showOutline: true, width: 0, }; this.doHighlightCell = this.doHighlightCell.bind(this); this.getGridAreaName = this.getGridAreaName.bind(this); this.getHeight = this.getHeight.bind(this); this.onHighlightCell = this.onHighlightCell.bind(this); this.renderCannotShowOutlineText = this.renderCannotShowOutlineText.bind(this); this.renderGrid = this.renderGrid.bind(this); this.renderGridCell = this.renderGridCell.bind(this); this.renderGridOutline = this.renderGridOutline.bind(this); this.renderGridOutlineBorder = this.renderGridOutlineBorder.bind(this); this.renderOutline = this.renderOutline.bind(this); } doHighlightCell(target, hide) { const { dispatch, grids } = this.props; const name = target.dataset.gridAreaName; const id = target.dataset.gridId; const gridFragmentIndex = target.dataset.gridFragmentIndex; const rowNumber = target.dataset.gridRow; const columnNumber = target.dataset.gridColumn; const nodeFront = grids[id].nodeFront; // The options object has the following properties which corresponds to the // required parameters for showing the grid cell or area highlights. // See devtools/server/actors/highlighters/css-grid.js // { // showGridArea: String, // showGridCell: { // gridFragmentIndex: Number, // rowNumber: Number, // columnNumber: Number, // }, // } const options = { showGridArea: name, showGridCell: { gridFragmentIndex, rowNumber, columnNumber, }, }; if (hide) { // Reset the grid highlighter to default state; no options = hide cell/area outline. dispatch(showGridHighlighter(nodeFront)); } else { dispatch(showGridHighlighter(nodeFront, options)); } } /** * Returns the grid area name if the given grid cell is part of a grid area, otherwise * null. * * @param {Number} columnNumber * The column number of the grid cell. * @param {Number} rowNumber * The row number of the grid cell. * @param {Array} areas * Array of grid areas data stored in the grid fragment. * @return {String} If there is a grid area return area name, otherwise null. */ getGridAreaName(columnNumber, rowNumber, areas) { const gridArea = areas.find( area => area.rowStart <= rowNumber && area.rowEnd > rowNumber && area.columnStart <= columnNumber && area.columnEnd > columnNumber ); if (!gridArea) { return null; } return gridArea.name; } /** * Returns the height of the grid outline ranging between a minimum and maximum height. * * @return {Number} The height of the grid outline. */ getHeight() { const { height } = this.state; if (height >= VIEWPORT_MAX_HEIGHT) { return VIEWPORT_MAX_HEIGHT; } else if (height <= VIEWPORT_MIN_HEIGHT) { return VIEWPORT_MIN_HEIGHT; } return height; } /** * Displays a message text "Cannot show outline for this grid". */ renderCannotShowOutlineText() { return dom.div( { className: "grid-outline-text" }, dom.span({ className: "grid-outline-text-icon", title: getStr("layout.cannotShowGridOutline.title"), }), getStr("layout.cannotShowGridOutline") ); } /** * Renders the grid outline for the given grid container object. * * @param {Object} grid * A single grid container in the document. */ renderGrid(grid) { // TODO: We are drawing the first fragment since only one is currently being stored. // In the future we will need to iterate over all fragments of a grid. const gridFragmentIndex = 0; const { id, color, gridFragments } = grid; const { rows, cols, areas } = gridFragments[gridFragmentIndex]; const numberOfColumns = cols.lines.length - 1; const numberOfRows = rows.lines.length - 1; const rectangles = []; let x = 0; let y = 0; let width = 0; let height = 0; // Draw the cells contained within the grid outline border. for (let rowNumber = 1; rowNumber <= numberOfRows; rowNumber++) { height = GRID_CELL_SCALE_FACTOR * (rows.tracks[rowNumber - 1].breadth / 100); for ( let columnNumber = 1; columnNumber <= numberOfColumns; columnNumber++ ) { width = GRID_CELL_SCALE_FACTOR * (cols.tracks[columnNumber - 1].breadth / 100); const gridAreaName = this.getGridAreaName( columnNumber, rowNumber, areas ); const gridCell = this.renderGridCell( id, gridFragmentIndex, x, y, rowNumber, columnNumber, color, gridAreaName, width, height ); rectangles.push(gridCell); x += width; } x = 0; y += height; } // Transform the cells as needed to match the grid container's writing mode. const cellGroupStyle = {}; const writingModeMatrix = getWritingModeMatrix(this.state, grid); cellGroupStyle.transform = getCSSMatrixTransform(writingModeMatrix); const cellGroup = dom.g( { id: "grid-cell-group", style: cellGroupStyle, }, rectangles ); // Draw a rectangle that acts as the grid outline border. const border = this.renderGridOutlineBorder( this.state.width, this.state.height, color ); return [border, cellGroup]; } /** * Renders the grid cell of a grid fragment. * * @param {Number} id * The grid id stored on the grid fragment * @param {Number} gridFragmentIndex * The index of the grid fragment rendered to the document. * @param {Number} x * The x-position of the grid cell. * @param {Number} y * The y-position of the grid cell. * @param {Number} rowNumber * The row number of the grid cell. * @param {Number} columnNumber * The column number of the grid cell. * @param {String|null} gridAreaName * The grid area name or null if the grid cell is not part of a grid area. * @param {Number} width * The width of grid cell. * @param {Number} height * The height of the grid cell. */ renderGridCell( id, gridFragmentIndex, x, y, rowNumber, columnNumber, color, gridAreaName, width, height ) { return dom.rect({ key: `${id}-${rowNumber}-${columnNumber}`, className: "grid-outline-cell", "data-grid-area-name": gridAreaName, "data-grid-fragment-index": gridFragmentIndex, "data-grid-id": id, "data-grid-row": rowNumber, "data-grid-column": columnNumber, x, y, width, height, fill: "none", onMouseEnter: this.onHighlightCell, onMouseLeave: this.onHighlightCell, }); } renderGridOutline(grid) { const { color } = grid; return dom.g( { id: "grid-outline-group", className: "grid-outline-group", style: { color }, }, this.renderGrid(grid) ); } renderGridOutlineBorder(borderWidth, borderHeight, color) { return dom.rect({ key: "border", className: "grid-outline-border", x: 0, y: 0, width: borderWidth, height: borderHeight, }); } renderOutline() { const { height, selectedGrid, showOutline, width } = this.state; return showOutline ? dom.svg( { id: "grid-outline", width: "100%", height: this.getHeight(), viewBox: `${TRANSLATE_X} ${TRANSLATE_Y} ${width} ${height}`, }, this.renderGridOutline(selectedGrid) ) : this.renderCannotShowOutlineText(); } onHighlightCell({ target, type }) { // Debounce the highlighting of cells. // This way we don't end up sending many requests to the server for highlighting when // cells get hovered in a rapid succession We only send a request if the user settles // on a cell for some time. if (this.highlightTimeout) { clearTimeout(this.highlightTimeout); } this.highlightTimeout = setTimeout(() => { this.doHighlightCell(target, type === "mouseleave"); this.highlightTimeout = null; }, GRID_HIGHLIGHTING_DEBOUNCE); } render() { const { selectedGrid } = this.state; return selectedGrid?.gridFragments.length ? dom.div( { id: "grid-outline-container", className: "grid-outline-container", }, this.renderOutline() ) : null; } } /** * Get the width and height of a given grid. * * @param {Object} grid * A single grid container in the document. * @return {Object} An object like { width, height } */ function getTotalWidthAndHeight(grid) { // TODO: We are drawing the first fragment since only one is currently being stored. // In the future we will need to iterate over all fragments of a grid. const { gridFragments } = grid; const { rows, cols } = gridFragments[0]; let height = 0; for (let i = 0; i < rows.lines.length - 1; i++) { height += GRID_CELL_SCALE_FACTOR * (rows.tracks[i].breadth / 100); } let width = 0; for (let i = 0; i < cols.lines.length - 1; i++) { width += GRID_CELL_SCALE_FACTOR * (cols.tracks[i].breadth / 100); } // All writing modes other than horizontal-tb (the initial value) involve a 90 deg // rotation, so swap width and height. if (grid.writingMode != "horizontal-tb") { [width, height] = [height, width]; } return { width, height }; } module.exports = GridOutline;