summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/grids/components
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/grids/components/Grid.js106
-rw-r--r--devtools/client/inspector/grids/components/GridDisplaySettings.js116
-rw-r--r--devtools/client/inspector/grids/components/GridItem.js173
-rw-r--r--devtools/client/inspector/grids/components/GridList.js79
-rw-r--r--devtools/client/inspector/grids/components/GridOutline.js436
-rw-r--r--devtools/client/inspector/grids/components/moz.build13
6 files changed, 923 insertions, 0 deletions
diff --git a/devtools/client/inspector/grids/components/Grid.js b/devtools/client/inspector/grids/components/Grid.js
new file mode 100644
index 0000000000..0378f1e702
--- /dev/null
+++ b/devtools/client/inspector/grids/components/Grid.js
@@ -0,0 +1,106 @@
+/* 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 {
+ createFactory,
+ 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");
+
+// Normally, we would only lazy load GridOutline, but we also lazy load
+// GridDisplaySettings and GridList because we assume the CSS grid usage is low
+// and usually will not appear on the page.
+loader.lazyGetter(this, "GridDisplaySettings", function () {
+ return createFactory(
+ require("resource://devtools/client/inspector/grids/components/GridDisplaySettings.js")
+ );
+});
+loader.lazyGetter(this, "GridList", function () {
+ return createFactory(
+ require("resource://devtools/client/inspector/grids/components/GridList.js")
+ );
+});
+loader.lazyGetter(this, "GridOutline", function () {
+ return createFactory(
+ require("resource://devtools/client/inspector/grids/components/GridOutline.js")
+ );
+});
+
+const Types = require("resource://devtools/client/inspector/grids/types.js");
+
+class Grid extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ getSwatchColorPickerTooltip: PropTypes.func.isRequired,
+ grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
+ highlighterSettings: PropTypes.shape(Types.highlighterSettings)
+ .isRequired,
+ onSetGridOverlayColor: PropTypes.func.isRequired,
+ onToggleGridHighlighter: PropTypes.func.isRequired,
+ onToggleShowGridAreas: PropTypes.func.isRequired,
+ onToggleShowGridLineNumbers: PropTypes.func.isRequired,
+ onToggleShowInfiniteLines: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ if (!this.props.grids.length) {
+ return dom.div(
+ { className: "devtools-sidepanel-no-result" },
+ getStr("layout.noGridsOnThisPage")
+ );
+ }
+
+ const {
+ dispatch,
+ getSwatchColorPickerTooltip,
+ grids,
+ highlighterSettings,
+ onSetGridOverlayColor,
+ onToggleShowGridAreas,
+ onToggleGridHighlighter,
+ onToggleShowGridLineNumbers,
+ onToggleShowInfiniteLines,
+ setSelectedNode,
+ } = this.props;
+ const highlightedGrids = grids.filter(grid => grid.highlighted);
+
+ return dom.div(
+ { id: "layout-grid-container" },
+ dom.div(
+ { className: "grid-content" },
+ GridList({
+ dispatch,
+ getSwatchColorPickerTooltip,
+ grids,
+ onSetGridOverlayColor,
+ onToggleGridHighlighter,
+ setSelectedNode,
+ }),
+ GridDisplaySettings({
+ highlighterSettings,
+ onToggleShowGridAreas,
+ onToggleShowGridLineNumbers,
+ onToggleShowInfiniteLines,
+ })
+ ),
+ highlightedGrids.length === 1
+ ? GridOutline({
+ dispatch,
+ grids,
+ })
+ : null
+ );
+ }
+}
+
+module.exports = Grid;
diff --git a/devtools/client/inspector/grids/components/GridDisplaySettings.js b/devtools/client/inspector/grids/components/GridDisplaySettings.js
new file mode 100644
index 0000000000..af525cc8c7
--- /dev/null
+++ b/devtools/client/inspector/grids/components/GridDisplaySettings.js
@@ -0,0 +1,116 @@
+/* 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 Types = require("resource://devtools/client/inspector/grids/types.js");
+
+class GridDisplaySettings extends PureComponent {
+ static get propTypes() {
+ return {
+ highlighterSettings: PropTypes.shape(Types.highlighterSettings)
+ .isRequired,
+ onToggleShowGridAreas: PropTypes.func.isRequired,
+ onToggleShowGridLineNumbers: PropTypes.func.isRequired,
+ onToggleShowInfiniteLines: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onShowGridAreasCheckboxClick =
+ this.onShowGridAreasCheckboxClick.bind(this);
+ this.onShowGridLineNumbersCheckboxClick =
+ this.onShowGridLineNumbersCheckboxClick.bind(this);
+ this.onShowInfiniteLinesCheckboxClick =
+ this.onShowInfiniteLinesCheckboxClick.bind(this);
+ }
+
+ onShowGridAreasCheckboxClick() {
+ const { highlighterSettings, onToggleShowGridAreas } = this.props;
+
+ onToggleShowGridAreas(!highlighterSettings.showGridAreasOverlay);
+ }
+
+ onShowGridLineNumbersCheckboxClick() {
+ const { highlighterSettings, onToggleShowGridLineNumbers } = this.props;
+
+ onToggleShowGridLineNumbers(!highlighterSettings.showGridLineNumbers);
+ }
+
+ onShowInfiniteLinesCheckboxClick() {
+ const { highlighterSettings, onToggleShowInfiniteLines } = this.props;
+
+ onToggleShowInfiniteLines(!highlighterSettings.showInfiniteLines);
+ }
+
+ render() {
+ const { highlighterSettings } = this.props;
+
+ return dom.div(
+ { className: "grid-container" },
+ dom.span(
+ {
+ role: "heading",
+ "aria-level": "3",
+ },
+ getStr("layout.gridDisplaySettings")
+ ),
+ dom.ul(
+ {},
+ dom.li(
+ { className: "grid-settings-item" },
+ dom.label(
+ {},
+ dom.input({
+ id: "grid-setting-show-grid-line-numbers",
+ type: "checkbox",
+ checked: highlighterSettings.showGridLineNumbers,
+ onChange: this.onShowGridLineNumbersCheckboxClick,
+ }),
+ getStr("layout.displayLineNumbers")
+ )
+ ),
+ dom.li(
+ { className: "grid-settings-item" },
+ dom.label(
+ {},
+ dom.input({
+ id: "grid-setting-show-grid-areas",
+ type: "checkbox",
+ checked: highlighterSettings.showGridAreasOverlay,
+ onChange: this.onShowGridAreasCheckboxClick,
+ }),
+ getStr("layout.displayAreaNames")
+ )
+ ),
+ dom.li(
+ { className: "grid-settings-item" },
+ dom.label(
+ {},
+ dom.input({
+ id: "grid-setting-extend-grid-lines",
+ type: "checkbox",
+ checked: highlighterSettings.showInfiniteLines,
+ onChange: this.onShowInfiniteLinesCheckboxClick,
+ }),
+ getStr("layout.extendLinesInfinitely")
+ )
+ )
+ )
+ );
+ }
+}
+
+module.exports = GridDisplaySettings;
diff --git a/devtools/client/inspector/grids/components/GridItem.js b/devtools/client/inspector/grids/components/GridItem.js
new file mode 100644
index 0000000000..984f57c6a7
--- /dev/null
+++ b/devtools/client/inspector/grids/components/GridItem.js
@@ -0,0 +1,173 @@
+/* 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 {
+ createElement,
+ createRef,
+ Fragment,
+ 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");
+
+loader.lazyGetter(this, "Rep", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .REPS.Rep;
+});
+loader.lazyGetter(this, "MODE", function () {
+ return require("resource://devtools/client/shared/components/reps/index.js")
+ .MODE;
+});
+
+loader.lazyRequireGetter(
+ this,
+ "translateNodeFrontToGrip",
+ "resource://devtools/client/inspector/shared/utils.js",
+ true
+);
+
+const Types = require("resource://devtools/client/inspector/grids/types.js");
+
+const {
+ highlightNode,
+ unhighlightNode,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
+
+class GridItem extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ getSwatchColorPickerTooltip: PropTypes.func.isRequired,
+ grid: PropTypes.shape(Types.grid).isRequired,
+ grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
+ onSetGridOverlayColor: PropTypes.func.isRequired,
+ onToggleGridHighlighter: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.swatchEl = createRef();
+
+ this.onGridCheckboxClick = this.onGridCheckboxClick.bind(this);
+ this.onGridInspectIconClick = this.onGridInspectIconClick.bind(this);
+ this.setGridColor = this.setGridColor.bind(this);
+ }
+
+ componentDidMount() {
+ const tooltip = this.props.getSwatchColorPickerTooltip();
+
+ let previousColor;
+ tooltip.addSwatch(this.swatchEl.current, {
+ onCommit: this.setGridColor,
+ onPreview: this.setGridColor,
+ onRevert: () => {
+ this.props.onSetGridOverlayColor(
+ this.props.grid.nodeFront,
+ previousColor
+ );
+ },
+ onShow: () => {
+ previousColor = this.props.grid.color;
+ },
+ });
+ }
+
+ componentWillUnmount() {
+ const tooltip = this.props.getSwatchColorPickerTooltip();
+ tooltip.removeSwatch(this.swatchEl.current);
+ }
+
+ setGridColor() {
+ const color = this.swatchEl.current.dataset.color;
+ this.props.onSetGridOverlayColor(this.props.grid.nodeFront, color);
+ }
+
+ onGridCheckboxClick() {
+ const { grid, onToggleGridHighlighter } = this.props;
+ onToggleGridHighlighter(grid.nodeFront);
+ }
+
+ onGridInspectIconClick(nodeFront) {
+ const { setSelectedNode } = this.props;
+ setSelectedNode(nodeFront, { reason: "layout-panel" });
+ nodeFront.scrollIntoView().catch(e => console.error(e));
+ }
+
+ renderSubgrids() {
+ const { grid, grids } = this.props;
+
+ if (!grid.subgrids.length) {
+ return null;
+ }
+
+ const subgrids = grids.filter(g => grid.subgrids.includes(g.id));
+
+ return dom.ul(
+ {},
+ subgrids.map(g => {
+ return createElement(GridItem, {
+ key: g.id,
+ dispatch: this.props.dispatch,
+ getSwatchColorPickerTooltip: this.props.getSwatchColorPickerTooltip,
+ grid: g,
+ grids,
+ onSetGridOverlayColor: this.props.onSetGridOverlayColor,
+ onToggleGridHighlighter: this.props.onToggleGridHighlighter,
+ setSelectedNode: this.props.setSelectedNode,
+ });
+ })
+ );
+ }
+
+ render() {
+ const { dispatch, grid } = this.props;
+
+ return createElement(
+ Fragment,
+ null,
+ dom.li(
+ {},
+ dom.label(
+ {},
+ dom.input({
+ checked: grid.highlighted,
+ disabled: grid.disabled,
+ type: "checkbox",
+ value: grid.id,
+ onChange: this.onGridCheckboxClick,
+ }),
+ Rep({
+ defaultRep: Rep.ElementNode,
+ mode: MODE.TINY,
+ object: translateNodeFrontToGrip(grid.nodeFront),
+ onDOMNodeMouseOut: () => dispatch(unhighlightNode()),
+ onDOMNodeMouseOver: () => dispatch(highlightNode(grid.nodeFront)),
+ onInspectIconClick: (_, e) => {
+ // Stoping click propagation to avoid firing onGridCheckboxClick()
+ e.stopPropagation();
+ this.onGridInspectIconClick(grid.nodeFront);
+ },
+ })
+ ),
+ dom.div({
+ className: "layout-color-swatch",
+ "data-color": grid.color,
+ ref: this.swatchEl,
+ style: {
+ backgroundColor: grid.color,
+ },
+ title: grid.color,
+ })
+ ),
+ this.renderSubgrids()
+ );
+ }
+}
+
+module.exports = GridItem;
diff --git a/devtools/client/inspector/grids/components/GridList.js b/devtools/client/inspector/grids/components/GridList.js
new file mode 100644
index 0000000000..62c7ae5b13
--- /dev/null
+++ b/devtools/client/inspector/grids/components/GridList.js
@@ -0,0 +1,79 @@
+/* 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 {
+ createFactory,
+ 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 GridItem = createFactory(
+ require("resource://devtools/client/inspector/grids/components/GridItem.js")
+);
+
+const Types = require("resource://devtools/client/inspector/grids/types.js");
+
+class GridList extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ getSwatchColorPickerTooltip: PropTypes.func.isRequired,
+ grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
+ onSetGridOverlayColor: PropTypes.func.isRequired,
+ onToggleGridHighlighter: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ dispatch,
+ getSwatchColorPickerTooltip,
+ grids,
+ onSetGridOverlayColor,
+ onToggleGridHighlighter,
+ setSelectedNode,
+ } = this.props;
+
+ return dom.div(
+ { className: "grid-container" },
+ dom.span(
+ {
+ role: "heading",
+ "aria-level": "3",
+ },
+ getStr("layout.overlayGrid")
+ ),
+ dom.ul(
+ {
+ id: "grid-list",
+ className: "devtools-monospace",
+ },
+ grids
+ // Skip subgrids since they are rendered by their parent grids in GridItem.
+ .filter(grid => !grid.isSubgrid)
+ .map(grid =>
+ GridItem({
+ dispatch,
+ key: grid.id,
+ getSwatchColorPickerTooltip,
+ grid,
+ grids,
+ onSetGridOverlayColor,
+ onToggleGridHighlighter,
+ setSelectedNode,
+ })
+ )
+ )
+ );
+ }
+}
+
+module.exports = GridList;
diff --git a/devtools/client/inspector/grids/components/GridOutline.js b/devtools/client/inspector/grids/components/GridOutline.js
new file mode 100644
index 0000000000..65771f3f45
--- /dev/null
+++ b/devtools/client/inspector/grids/components/GridOutline.js
@@ -0,0 +1,436 @@
+/* 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;
diff --git a/devtools/client/inspector/grids/components/moz.build b/devtools/client/inspector/grids/components/moz.build
new file mode 100644
index 0000000000..e938e51ad1
--- /dev/null
+++ b/devtools/client/inspector/grids/components/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "Grid.js",
+ "GridDisplaySettings.js",
+ "GridItem.js",
+ "GridList.js",
+ "GridOutline.js",
+)