436 lines
12 KiB
JavaScript
436 lines
12 KiB
JavaScript
/* 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.mjs");
|
|
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
|
|
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
|
|
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) {
|
|
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;
|