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;