css-grid.js (59239B)
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 AutoRefreshHighlighter, 9 } = require("resource://devtools/server/actors/highlighters/auto-refresh.js"); 10 const { 11 CANVAS_SIZE, 12 DEFAULT_COLOR, 13 drawBubbleRect, 14 drawLine, 15 drawRect, 16 drawRoundedRect, 17 getBoundsFromPoints, 18 getCurrentMatrix, 19 getPathDescriptionFromPoints, 20 getPointsFromDiagonal, 21 updateCanvasElement, 22 updateCanvasPosition, 23 } = require("resource://devtools/server/actors/highlighters/utils/canvas.js"); 24 const { 25 CanvasFrameAnonymousContentHelper, 26 getComputedStyle, 27 moveInfobar, 28 } = require("resource://devtools/server/actors/highlighters/utils/markup.js"); 29 const { apply } = require("resource://devtools/shared/layout/dom-matrix-2d.js"); 30 const { 31 getCurrentZoom, 32 getDisplayPixelRatio, 33 getWindowDimensions, 34 setIgnoreLayoutChanges, 35 } = require("resource://devtools/shared/layout/utils.js"); 36 loader.lazyGetter(this, "HighlightersBundle", () => { 37 return new Localization(["devtools/shared/highlighters.ftl"], true); 38 }); 39 40 const COLUMNS = "cols"; 41 const ROWS = "rows"; 42 43 const GRID_FONT_SIZE = 10; 44 const GRID_FONT_FAMILY = "sans-serif"; 45 const GRID_AREA_NAME_FONT_SIZE = "20"; 46 47 const GRID_LINES_PROPERTIES = { 48 edge: { 49 lineDash: [0, 0], 50 alpha: 1, 51 }, 52 explicit: { 53 lineDash: [5, 3], 54 alpha: 0.75, 55 }, 56 implicit: { 57 lineDash: [2, 2], 58 alpha: 0.5, 59 }, 60 areaEdge: { 61 lineDash: [0, 0], 62 alpha: 1, 63 lineWidth: 3, 64 }, 65 }; 66 67 const GRID_GAP_PATTERN_WIDTH = 14; // px 68 const GRID_GAP_PATTERN_HEIGHT = 14; // px 69 const GRID_GAP_PATTERN_LINE_DASH = [5, 3]; // px 70 const GRID_GAP_ALPHA = 0.5; 71 72 // This is the minimum distance a line can be to the edge of the document under which we 73 // push the line number arrow to be inside the grid. This offset is enough to fit the 74 // entire arrow + a stacked arrow behind it. 75 const OFFSET_FROM_EDGE = 32; 76 // This is how much inside the grid we push the arrow. This a factor of the arrow size. 77 // The goal here is for a row and a column arrow that have both been pushed inside the 78 // grid, in a corner, not to overlap. 79 const FLIP_ARROW_INSIDE_FACTOR = 2.5; 80 81 /** 82 * Given an `edge` of a box, return the name of the edge one move to the right. 83 */ 84 function rotateEdgeRight(edge) { 85 switch (edge) { 86 case "top": 87 return "right"; 88 case "right": 89 return "bottom"; 90 case "bottom": 91 return "left"; 92 case "left": 93 return "top"; 94 default: 95 return edge; 96 } 97 } 98 99 /** 100 * Given an `edge` of a box, return the name of the edge one move to the left. 101 */ 102 function rotateEdgeLeft(edge) { 103 switch (edge) { 104 case "top": 105 return "left"; 106 case "right": 107 return "top"; 108 case "bottom": 109 return "right"; 110 case "left": 111 return "bottom"; 112 default: 113 return edge; 114 } 115 } 116 117 /** 118 * Given an `edge` of a box, return the name of the opposite edge. 119 */ 120 function reflectEdge(edge) { 121 switch (edge) { 122 case "top": 123 return "bottom"; 124 case "right": 125 return "left"; 126 case "bottom": 127 return "top"; 128 case "left": 129 return "right"; 130 default: 131 return edge; 132 } 133 } 134 135 /** 136 * Cached used by `CssGridHighlighter.getGridGapPattern`. 137 */ 138 const gCachedGridPattern = new Map(); 139 140 /** 141 * The CssGridHighlighter is the class that overlays a visual grid on top of 142 * display:[inline-]grid elements. 143 * 144 * Usage example: 145 * let h = new CssGridHighlighter(env); 146 * h.show(node, options); 147 * h.hide(); 148 * h.destroy(); 149 * 150 * @param {string} options.color 151 * The color that should be used to draw the highlighter for this grid. 152 * @param {number} options.globalAlpha 153 * The alpha (transparency) value that should be used to draw the highlighter for 154 * this grid. 155 * @param {boolean} options.showAllGridAreas 156 * Shows all the grid area highlights for the current grid if isShown is 157 * true. 158 * @param {string} options.showGridArea 159 * Shows the grid area highlight for the given area name. 160 * @param {boolean} options.showGridAreasOverlay 161 * Displays an overlay of all the grid areas for the current grid 162 * container if isShown is true. 163 * @param {object} options.showGridCell 164 * An object containing the grid fragment index, row and column numbers 165 * to the corresponding grid cell to highlight for the current grid. 166 * @param {number} options.showGridCell.gridFragmentIndex 167 * Index of the grid fragment to render the grid cell highlight. 168 * @param {number} options.showGridCell.rowNumber 169 * Row number of the grid cell to highlight. 170 * @param {number} options.showGridCell.columnNumber 171 * Column number of the grid cell to highlight. 172 * @param {object} options.showGridLineNames 173 * An object containing the grid fragment index and line number to the 174 * corresponding grid line to highlight for the current grid. 175 * @param {number} options.showGridLineNames.gridFragmentIndex 176 * Index of the grid fragment to render the grid line highlight. 177 * @param {number} options.showGridLineNames.lineNumber 178 * Line number of the grid line to highlight. 179 * @param {string} options.showGridLineNames.type 180 * The dimension type of the grid line. 181 * @param {boolean} options.showGridLineNumbers 182 * Displays the grid line numbers on the grid lines if isShown is true. 183 * @param {boolean} options.showInfiniteLines 184 * Displays an infinite line to represent the grid lines if isShown is 185 * true. 186 * @param {number} options.isParent 187 * Set to true if this is a "parent" grid, i.e. a grid with a subgrid. 188 * @param {number} options.zIndex 189 * The z-index to decide the displaying order. 190 * 191 * Structure: 192 * <div class="highlighter-container"> 193 * <canvas id="css-grid-canvas" class="css-grid-canvas"> 194 * <svg class="css-grid-elements" hidden="true"> 195 * <g class="css-grid-regions"> 196 * <path class="css-grid-areas" points="..." /> 197 * <path class="css-grid-cells" points="..." /> 198 * </g> 199 * </svg> 200 * <div class="css-grid-area-infobar-container"> 201 * <div class="css-grid-infobar"> 202 * <div class="css-grid-infobar-text"> 203 * <span class="css-grid-area-infobar-name">Grid Area Name</span> 204 * <span class="css-grid-area-infobar-dimensions">Grid Area Dimensions></span> 205 * </div> 206 * </div> 207 * </div> 208 * <div class="css-grid-cell-infobar-container"> 209 * <div class="css-grid-infobar"> 210 * <div class="css-grid-infobar-text"> 211 * <span class="css-grid-cell-infobar-position">Grid Cell Position</span> 212 * <span class="css-grid-cell-infobar-dimensions">Grid Cell Dimensions></span> 213 * </div> 214 * </div> 215 * <div class="css-grid-line-infobar-container"> 216 * <div class="css-grid-infobar"> 217 * <div class="css-grid-infobar-text"> 218 * <span class="css-grid-line-infobar-number">Grid Line Number</span> 219 * <span class="css-grid-line-infobar-names">Grid Line Names></span> 220 * </div> 221 * </div> 222 * </div> 223 * </div> 224 */ 225 226 class CssGridHighlighter extends AutoRefreshHighlighter { 227 constructor(highlighterEnv) { 228 super(highlighterEnv); 229 230 this.markup = new CanvasFrameAnonymousContentHelper( 231 this.highlighterEnv, 232 this._buildMarkup.bind(this), 233 { 234 contentRootHostClassName: "devtools-highlighter-css-grid", 235 } 236 ); 237 this.isReady = this.markup.initialize(); 238 239 this.onPageHide = this.onPageHide.bind(this); 240 this.onWillNavigate = this.onWillNavigate.bind(this); 241 242 this.highlighterEnv.on("will-navigate", this.onWillNavigate); 243 244 const { pageListenerTarget } = highlighterEnv; 245 pageListenerTarget.addEventListener("pagehide", this.onPageHide); 246 247 // Initialize the <canvas> position to the top left corner of the page. 248 this._canvasPosition = { 249 x: 0, 250 y: 0, 251 }; 252 253 // Calling `updateCanvasPosition` anyway since the highlighter could be initialized 254 // on a page that has scrolled already. 255 updateCanvasPosition( 256 this._canvasPosition, 257 this._scroll, 258 this.win, 259 this._winDimensions 260 ); 261 } 262 263 _buildMarkup() { 264 const container = this.markup.createNode({ 265 attributes: { 266 class: "highlighter-container", 267 }, 268 }); 269 270 this.rootEl = this.markup.createNode({ 271 parent: container, 272 attributes: { 273 id: "css-grid-root", 274 class: "css-grid-root", 275 }, 276 }); 277 278 // We use a <canvas> element so that we can draw an arbitrary number of lines 279 // which wouldn't be possible with HTML or SVG without having to insert and remove 280 // the whole markup on every update. 281 this.markup.createNode({ 282 parent: this.rootEl, 283 nodeType: "canvas", 284 attributes: { 285 id: "css-grid-canvas", 286 class: "css-grid-canvas", 287 hidden: "true", 288 width: CANVAS_SIZE, 289 height: CANVAS_SIZE, 290 }, 291 }); 292 293 // Build the SVG element. 294 const svg = this.markup.createSVGNode({ 295 nodeType: "svg", 296 parent: this.rootEl, 297 attributes: { 298 id: "css-grid-elements", 299 width: "100%", 300 height: "100%", 301 hidden: "true", 302 }, 303 }); 304 305 const regions = this.markup.createSVGNode({ 306 nodeType: "g", 307 parent: svg, 308 attributes: { 309 class: "css-grid-regions", 310 }, 311 }); 312 313 this.markup.createSVGNode({ 314 nodeType: "path", 315 parent: regions, 316 attributes: { 317 class: "css-grid-areas", 318 id: "css-grid-areas", 319 }, 320 }); 321 322 this.markup.createSVGNode({ 323 nodeType: "path", 324 parent: regions, 325 attributes: { 326 class: "css-grid-cells", 327 id: "css-grid-cells", 328 }, 329 }); 330 331 // Build the grid area infobar markup. 332 const areaInfobarContainer = this.markup.createNode({ 333 parent: container, 334 attributes: { 335 class: "css-grid-area-infobar-container", 336 id: "css-grid-area-infobar-container", 337 position: "top", 338 hidden: "true", 339 }, 340 }); 341 342 const areaInfobar = this.markup.createNode({ 343 parent: areaInfobarContainer, 344 attributes: { 345 class: "css-grid-infobar", 346 }, 347 }); 348 349 const areaTextbox = this.markup.createNode({ 350 parent: areaInfobar, 351 attributes: { 352 class: "css-grid-infobar-text", 353 }, 354 }); 355 this.markup.createNode({ 356 nodeType: "span", 357 parent: areaTextbox, 358 attributes: { 359 class: "css-grid-area-infobar-name", 360 id: "css-grid-area-infobar-name", 361 }, 362 }); 363 this.markup.createNode({ 364 nodeType: "span", 365 parent: areaTextbox, 366 attributes: { 367 class: "css-grid-area-infobar-dimensions", 368 id: "css-grid-area-infobar-dimensions", 369 }, 370 }); 371 372 // Build the grid cell infobar markup. 373 const cellInfobarContainer = this.markup.createNode({ 374 parent: container, 375 attributes: { 376 class: "css-grid-cell-infobar-container", 377 id: "css-grid-cell-infobar-container", 378 position: "top", 379 hidden: "true", 380 }, 381 }); 382 383 const cellInfobar = this.markup.createNode({ 384 parent: cellInfobarContainer, 385 attributes: { 386 class: "css-grid-infobar", 387 }, 388 }); 389 390 const cellTextbox = this.markup.createNode({ 391 parent: cellInfobar, 392 attributes: { 393 class: "css-grid-infobar-text", 394 }, 395 }); 396 this.markup.createNode({ 397 nodeType: "span", 398 parent: cellTextbox, 399 attributes: { 400 class: "css-grid-cell-infobar-position", 401 id: "css-grid-cell-infobar-position", 402 }, 403 }); 404 this.markup.createNode({ 405 nodeType: "span", 406 parent: cellTextbox, 407 attributes: { 408 class: "css-grid-cell-infobar-dimensions", 409 id: "css-grid-cell-infobar-dimensions", 410 }, 411 }); 412 413 // Build the grid line infobar markup. 414 const lineInfobarContainer = this.markup.createNode({ 415 parent: container, 416 attributes: { 417 class: "css-grid-line-infobar-container", 418 id: "css-grid-line-infobar-container", 419 position: "top", 420 hidden: "true", 421 }, 422 }); 423 424 const lineInfobar = this.markup.createNode({ 425 parent: lineInfobarContainer, 426 attributes: { 427 class: "css-grid-infobar", 428 }, 429 }); 430 431 const lineTextbox = this.markup.createNode({ 432 parent: lineInfobar, 433 attributes: { 434 class: "css-grid-infobar-text", 435 }, 436 }); 437 this.markup.createNode({ 438 nodeType: "span", 439 parent: lineTextbox, 440 attributes: { 441 class: "css-grid-line-infobar-number", 442 id: "css-grid-line-infobar-number", 443 }, 444 }); 445 this.markup.createNode({ 446 nodeType: "span", 447 parent: lineTextbox, 448 attributes: { 449 class: "css-grid-line-infobar-names", 450 id: "css-grid-line-infobar-names", 451 }, 452 }); 453 454 return container; 455 } 456 457 clearCache() { 458 gCachedGridPattern.clear(); 459 } 460 461 /** 462 * Clear the grid area highlights. 463 */ 464 clearGridAreas() { 465 const areas = this.getElement("css-grid-areas"); 466 areas.setAttribute("d", ""); 467 } 468 469 /** 470 * Clear the grid cell highlights. 471 */ 472 clearGridCell() { 473 const cells = this.getElement("css-grid-cells"); 474 cells.setAttribute("d", ""); 475 } 476 477 destroy() { 478 const { highlighterEnv } = this; 479 highlighterEnv.off("will-navigate", this.onWillNavigate); 480 481 const { pageListenerTarget } = highlighterEnv; 482 if (pageListenerTarget) { 483 pageListenerTarget.removeEventListener("pagehide", this.onPageHide); 484 } 485 486 this.markup.destroy(); 487 this.rootEl = null; 488 489 // Clear the pattern cache to avoid dead object exceptions (Bug 1342051). 490 this.clearCache(); 491 AutoRefreshHighlighter.prototype.destroy.call(this); 492 } 493 494 get canvas() { 495 return this.getElement("css-grid-canvas"); 496 } 497 498 get color() { 499 return this.options.color || DEFAULT_COLOR; 500 } 501 502 get ctx() { 503 return this.canvas.getCanvasContext("2d"); 504 } 505 506 get globalAlpha() { 507 return this.options.globalAlpha || 1; 508 } 509 510 getElement(id) { 511 return this.markup.getElement(id); 512 } 513 514 getFirstColLinePos(fragment) { 515 return fragment.cols.lines[0].start; 516 } 517 518 getFirstRowLinePos(fragment) { 519 return fragment.rows.lines[0].start; 520 } 521 522 /** 523 * Gets the grid gap pattern used to render the gap regions based on the device 524 * pixel ratio given. 525 * 526 * @param {number} devicePixelRatio 527 * The device pixel ratio we want the pattern for. 528 * @param {object} dimension 529 * Refers to the Map key for the grid dimension type which is either the 530 * constant COLUMNS or ROWS. 531 * @return {CanvasPattern} grid gap pattern. 532 */ 533 getGridGapPattern(devicePixelRatio, dimension) { 534 let gridPatternMap = null; 535 536 if (gCachedGridPattern.has(devicePixelRatio)) { 537 gridPatternMap = gCachedGridPattern.get(devicePixelRatio); 538 } else { 539 gridPatternMap = new Map(); 540 } 541 542 if (gridPatternMap.has(dimension)) { 543 return gridPatternMap.get(dimension); 544 } 545 546 // Create the diagonal lines pattern for the rendering the grid gaps. 547 const canvas = this.markup.createNode({ nodeType: "canvas" }); 548 const width = (canvas.width = GRID_GAP_PATTERN_WIDTH * devicePixelRatio); 549 const height = (canvas.height = GRID_GAP_PATTERN_HEIGHT * devicePixelRatio); 550 551 const ctx = canvas.getContext("2d"); 552 ctx.save(); 553 ctx.setLineDash(GRID_GAP_PATTERN_LINE_DASH); 554 ctx.beginPath(); 555 ctx.translate(0.5, 0.5); 556 557 if (dimension === COLUMNS) { 558 ctx.moveTo(0, 0); 559 ctx.lineTo(width, height); 560 } else { 561 ctx.moveTo(width, 0); 562 ctx.lineTo(0, height); 563 } 564 565 ctx.strokeStyle = this.color; 566 ctx.globalAlpha = GRID_GAP_ALPHA * this.globalAlpha; 567 ctx.stroke(); 568 ctx.restore(); 569 570 const pattern = ctx.createPattern(canvas, "repeat"); 571 572 gridPatternMap.set(dimension, pattern); 573 gCachedGridPattern.set(devicePixelRatio, gridPatternMap); 574 575 return pattern; 576 } 577 578 getLastColLinePos(fragment) { 579 return fragment.cols.lines[fragment.cols.lines.length - 1].start; 580 } 581 582 /** 583 * Get the GridLine index of the last edge of the explicit grid for a grid dimension. 584 * 585 * @param {GridTracks} tracks 586 * The grid track of a given grid dimension. 587 * @return {number} index of the last edge of the explicit grid for a grid dimension. 588 */ 589 getLastEdgeLineIndex(tracks) { 590 let trackIndex = tracks.length - 1; 591 592 // Traverse the grid track backwards until we find an explicit track. 593 while (trackIndex >= 0 && tracks[trackIndex].type != "explicit") { 594 trackIndex--; 595 } 596 597 // The grid line index is the grid track index + 1. 598 return trackIndex + 1; 599 } 600 601 getLastRowLinePos(fragment) { 602 return fragment.rows.lines[fragment.rows.lines.length - 1].start; 603 } 604 605 getNode(id) { 606 return this.markup.content.root.getElementById(id); 607 } 608 609 /** 610 * The AutoRefreshHighlighter's _hasMoved method returns true only if the 611 * element's quads have changed. Override it so it also returns true if the 612 * element's grid has changed (which can happen when you change the 613 * grid-template-* CSS properties with the highlighter displayed). This 614 * check is prone to false positives, because it does a direct object 615 * comparison of the first grid fragment structure. This structure is 616 * generated by the first call to getGridFragments, and on any subsequent 617 * calls where a reflow is needed. Since a reflow is needed when the CSS 618 * changes, this will correctly detect that the grid structure has changed. 619 * However, it's possible that the reflow could generate a novel grid 620 * fragment object containing information that is unchanged -- a false 621 * positive. 622 */ 623 _hasMoved() { 624 const hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this); 625 626 const oldFirstGridFragment = this.gridData?.[0]; 627 this.gridData = this.currentNode.getGridFragments(); 628 const newFirstGridFragment = this.gridData[0]; 629 630 return hasMoved || oldFirstGridFragment !== newFirstGridFragment; 631 } 632 633 /** 634 * Hide the highlighter, the canvas and the infobars. 635 */ 636 _hide() { 637 setIgnoreLayoutChanges(true); 638 this._hideGrid(); 639 this._hideGridElements(); 640 this._hideGridAreaInfoBar(); 641 this._hideGridCellInfoBar(); 642 this._hideGridLineInfoBar(); 643 setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement); 644 } 645 646 _hideGrid() { 647 this.getElement("css-grid-canvas").setAttribute("hidden", "true"); 648 } 649 650 _hideGridAreaInfoBar() { 651 this.getElement("css-grid-area-infobar-container").setAttribute( 652 "hidden", 653 "true" 654 ); 655 } 656 657 _hideGridCellInfoBar() { 658 this.getElement("css-grid-cell-infobar-container").setAttribute( 659 "hidden", 660 "true" 661 ); 662 } 663 664 _hideGridElements() { 665 this.getElement("css-grid-elements").setAttribute("hidden", "true"); 666 } 667 668 _hideGridLineInfoBar() { 669 this.getElement("css-grid-line-infobar-container").setAttribute( 670 "hidden", 671 "true" 672 ); 673 } 674 675 /** 676 * Checks if the current node has a CSS Grid layout. 677 * 678 * @return {boolean} true if the current node has a CSS grid layout, false otherwise. 679 */ 680 isGrid() { 681 return this.currentNode.hasGridFragments(); 682 } 683 684 /** 685 * Is a given grid fragment valid? i.e. does it actually have tracks? In some cases, we 686 * may have a fragment that defines column tracks but doesn't have any rows (or vice 687 * versa). In which case we do not want to draw anything for that fragment. 688 * 689 * @param {object} fragment 690 * @return {boolean} 691 */ 692 isValidFragment(fragment) { 693 return fragment.cols.tracks.length && fragment.rows.tracks.length; 694 } 695 696 /** 697 * The <canvas>'s position needs to be updated if the page scrolls too much, in order 698 * to give the illusion that it always covers the viewport. 699 */ 700 _scrollUpdate() { 701 const hasUpdated = updateCanvasPosition( 702 this._canvasPosition, 703 this._scroll, 704 this.win, 705 this._winDimensions 706 ); 707 708 if (hasUpdated) { 709 this._update(); 710 } 711 } 712 713 _show() { 714 if (!this.isGrid()) { 715 this.hide(); 716 return false; 717 } 718 719 // The grid pattern cache should be cleared in case the color changed. 720 this.clearCache(); 721 722 // Hide the canvas, grid element highlights and infobar. 723 this._hide(); 724 725 this.getElement("css-grid-root").setAttribute( 726 "data-is-parent-grid", 727 !!this.options.isParent 728 ); 729 730 // Set z-index. 731 this.markup.content.root.firstElementChild.style.setProperty( 732 "z-index", 733 this.options.zIndex 734 ); 735 736 // Update the grid color 737 this.markup.content.root.firstElementChild.style.setProperty( 738 "--grid-color", 739 this.color 740 ); 741 742 return this._update(); 743 } 744 745 _showGrid() { 746 this.getElement("css-grid-canvas").removeAttribute("hidden"); 747 } 748 749 _showGridAreaInfoBar() { 750 this.getElement("css-grid-area-infobar-container").removeAttribute( 751 "hidden" 752 ); 753 } 754 755 _showGridCellInfoBar() { 756 this.getElement("css-grid-cell-infobar-container").removeAttribute( 757 "hidden" 758 ); 759 } 760 761 _showGridElements() { 762 this.getElement("css-grid-elements").removeAttribute("hidden"); 763 } 764 765 _showGridLineInfoBar() { 766 this.getElement("css-grid-line-infobar-container").removeAttribute( 767 "hidden" 768 ); 769 } 770 771 /** 772 * Shows all the grid area highlights for the current grid. 773 */ 774 showAllGridAreas() { 775 this.renderGridArea(); 776 } 777 778 /** 779 * Shows the grid area highlight for the given area name. 780 * 781 * @param {string} areaName 782 * Grid area name. 783 */ 784 showGridArea(areaName) { 785 this.renderGridArea(areaName); 786 } 787 788 /** 789 * Shows the grid cell highlight for the given grid cell options. 790 * 791 * @param {number} options.gridFragmentIndex 792 * Index of the grid fragment to render the grid cell highlight. 793 * @param {number} options.rowNumber 794 * Row number of the grid cell to highlight. 795 * @param {number} options.columnNumber 796 * Column number of the grid cell to highlight. 797 */ 798 showGridCell({ gridFragmentIndex, rowNumber, columnNumber }) { 799 this.renderGridCell(gridFragmentIndex, rowNumber, columnNumber); 800 } 801 802 /** 803 * Shows the grid line highlight for the given grid line options. 804 * 805 * @param {number} options.gridFragmentIndex 806 * Index of the grid fragment to render the grid line highlight. 807 * @param {number} options.lineNumber 808 * Line number of the grid line to highlight. 809 * @param {string} options.type 810 * The dimension type of the grid line. 811 */ 812 showGridLineNames({ gridFragmentIndex, lineNumber, type }) { 813 this.renderGridLineNames(gridFragmentIndex, lineNumber, type); 814 } 815 816 /** 817 * If a page hide event is triggered for current window's highlighter, hide the 818 * highlighter. 819 */ 820 onPageHide({ target }) { 821 if (target.defaultView === this.win) { 822 this.hide(); 823 } 824 } 825 826 /** 827 * Called when the page will-navigate. Used to hide the grid highlighter and clear 828 * the cached gap patterns and avoid using DeadWrapper obejcts as gap patterns the 829 * next time. 830 */ 831 onWillNavigate({ isTopLevel }) { 832 this.clearCache(); 833 834 if (isTopLevel) { 835 this.hide(); 836 } 837 } 838 839 renderFragment(fragment) { 840 if (!this.isValidFragment(fragment)) { 841 return; 842 } 843 844 this.renderLines( 845 fragment.cols, 846 COLUMNS, 847 this.getFirstRowLinePos(fragment), 848 this.getLastRowLinePos(fragment) 849 ); 850 this.renderLines( 851 fragment.rows, 852 ROWS, 853 this.getFirstColLinePos(fragment), 854 this.getLastColLinePos(fragment) 855 ); 856 857 if (this.options.showGridAreasOverlay) { 858 this.renderGridAreaOverlay(); 859 } 860 861 // Line numbers are rendered in a 2nd step to avoid overlapping with existing lines. 862 if (this.options.showGridLineNumbers) { 863 this.renderLineNumbers( 864 fragment.cols, 865 COLUMNS, 866 this.getFirstRowLinePos(fragment) 867 ); 868 this.renderLineNumbers( 869 fragment.rows, 870 ROWS, 871 this.getFirstColLinePos(fragment) 872 ); 873 this.renderNegativeLineNumbers( 874 fragment.cols, 875 COLUMNS, 876 this.getLastRowLinePos(fragment) 877 ); 878 this.renderNegativeLineNumbers( 879 fragment.rows, 880 ROWS, 881 this.getLastColLinePos(fragment) 882 ); 883 } 884 } 885 886 /** 887 * Render the grid area highlight for the given area name or for all the grid areas. 888 * 889 * @param {string} areaName 890 * Name of the grid area to be highlighted. If no area name is provided, all 891 * the grid areas should be highlighted. 892 */ 893 renderGridArea(areaName) { 894 const { devicePixelRatio } = this.win; 895 const displayPixelRatio = getDisplayPixelRatio(this.win); 896 const paths = []; 897 898 for (let i = 0; i < this.gridData.length; i++) { 899 const fragment = this.gridData[i]; 900 901 for (const area of fragment.areas) { 902 if (areaName && areaName != area.name) { 903 continue; 904 } 905 906 const rowStart = fragment.rows.lines[area.rowStart - 1]; 907 const rowEnd = fragment.rows.lines[area.rowEnd - 1]; 908 const columnStart = fragment.cols.lines[area.columnStart - 1]; 909 const columnEnd = fragment.cols.lines[area.columnEnd - 1]; 910 911 const x1 = columnStart.start + columnStart.breadth; 912 const y1 = rowStart.start + rowStart.breadth; 913 const x2 = columnEnd.start; 914 const y2 = rowEnd.start; 915 916 const points = getPointsFromDiagonal( 917 x1, 918 y1, 919 x2, 920 y2, 921 this.currentMatrix 922 ); 923 924 // Scale down by `devicePixelRatio` since SVG element already take them into 925 // account. 926 const svgPoints = points.map(point => ({ 927 x: Math.round(point.x / devicePixelRatio), 928 y: Math.round(point.y / devicePixelRatio), 929 })); 930 931 // Scale down by `displayPixelRatio` since infobar's HTML elements already take it 932 // into account; and the zoom scaling is handled by `moveInfobar`. 933 const bounds = getBoundsFromPoints( 934 points.map(point => ({ 935 x: Math.round(point.x / displayPixelRatio), 936 y: Math.round(point.y / displayPixelRatio), 937 })) 938 ); 939 940 paths.push(getPathDescriptionFromPoints(svgPoints)); 941 942 // Update and show the info bar when only displaying a single grid area. 943 if (areaName) { 944 this._showGridAreaInfoBar(); 945 this._updateGridAreaInfobar(area, bounds); 946 } 947 } 948 } 949 950 const areas = this.getElement("css-grid-areas"); 951 areas.setAttribute("d", paths.join(" ")); 952 } 953 954 /** 955 * Render grid area name on the containing grid area cell. 956 * 957 * @param {object} fragment 958 * The grid fragment of the grid container. 959 * @param {object} area 960 * The area overlay to render on the CSS highlighter canvas. 961 */ 962 renderGridAreaName(fragment, area) { 963 const { rowStart, rowEnd, columnStart, columnEnd } = area; 964 const { devicePixelRatio } = this.win; 965 const displayPixelRatio = getDisplayPixelRatio(this.win); 966 const offset = (displayPixelRatio / 2) % 1; 967 let fontSize = GRID_AREA_NAME_FONT_SIZE * displayPixelRatio; 968 const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio); 969 const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio); 970 971 this.ctx.save(); 972 this.ctx.translate(offset - canvasX, offset - canvasY); 973 this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY; 974 this.ctx.globalAlpha = this.globalAlpha; 975 this.ctx.strokeStyle = this.color; 976 this.ctx.textAlign = "center"; 977 this.ctx.textBaseline = "middle"; 978 979 // Draw the text for the grid area name. 980 for (let rowNumber = rowStart; rowNumber < rowEnd; rowNumber++) { 981 for ( 982 let columnNumber = columnStart; 983 columnNumber < columnEnd; 984 columnNumber++ 985 ) { 986 const row = fragment.rows.tracks[rowNumber - 1]; 987 const column = fragment.cols.tracks[columnNumber - 1]; 988 989 // If the font size exceeds the bounds of the containing grid cell, size it its 990 // row or column dimension, whichever is smallest. 991 if ( 992 fontSize > column.breadth * displayPixelRatio || 993 fontSize > row.breadth * displayPixelRatio 994 ) { 995 fontSize = Math.min([column.breadth, row.breadth]); 996 this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY; 997 } 998 999 const textWidth = this.ctx.measureText(area.name).width; 1000 // The width of the character 'm' approximates the height of the text. 1001 const textHeight = this.ctx.measureText("m").width; 1002 // Padding in pixels for the line number text inside of the line number container. 1003 const padding = 3 * displayPixelRatio; 1004 1005 const boxWidth = textWidth + 2 * padding; 1006 const boxHeight = textHeight + 2 * padding; 1007 1008 let x = column.start + column.breadth / 2; 1009 let y = row.start + row.breadth / 2; 1010 1011 [x, y] = apply(this.currentMatrix, [x, y]); 1012 1013 const rectXPos = x - boxWidth / 2; 1014 const rectYPos = y - boxHeight / 2; 1015 1016 // Draw a rounded rectangle with a border width of 1 pixel, 1017 // a border color matching the grid color, and a white background. 1018 this.ctx.lineWidth = 1 * displayPixelRatio; 1019 this.ctx.strokeStyle = this.color; 1020 this.ctx.fillStyle = "white"; 1021 const radius = 2 * displayPixelRatio; 1022 drawRoundedRect( 1023 this.ctx, 1024 rectXPos, 1025 rectYPos, 1026 boxWidth, 1027 boxHeight, 1028 radius 1029 ); 1030 1031 this.ctx.fillStyle = this.color; 1032 this.ctx.fillText(area.name, x, y + padding); 1033 } 1034 } 1035 1036 this.ctx.restore(); 1037 } 1038 1039 /** 1040 * Renders the grid area overlay on the css grid highlighter canvas. 1041 */ 1042 renderGridAreaOverlay() { 1043 const padding = 1; 1044 1045 for (let i = 0; i < this.gridData.length; i++) { 1046 const fragment = this.gridData[i]; 1047 1048 for (const area of fragment.areas) { 1049 const { rowStart, rowEnd, columnStart, columnEnd, type } = area; 1050 1051 if (type === "implicit") { 1052 continue; 1053 } 1054 1055 // Draw the line edges for the grid area. 1056 const areaColStart = fragment.cols.lines[columnStart - 1]; 1057 const areaColEnd = fragment.cols.lines[columnEnd - 1]; 1058 1059 const areaRowStart = fragment.rows.lines[rowStart - 1]; 1060 const areaRowEnd = fragment.rows.lines[rowEnd - 1]; 1061 1062 const areaColStartLinePos = areaColStart.start + areaColStart.breadth; 1063 const areaRowStartLinePos = areaRowStart.start + areaRowStart.breadth; 1064 1065 this.renderLine( 1066 areaColStartLinePos + padding, 1067 areaRowStartLinePos, 1068 areaRowEnd.start, 1069 COLUMNS, 1070 "areaEdge" 1071 ); 1072 this.renderLine( 1073 areaColEnd.start - padding, 1074 areaRowStartLinePos, 1075 areaRowEnd.start, 1076 COLUMNS, 1077 "areaEdge" 1078 ); 1079 1080 this.renderLine( 1081 areaRowStartLinePos + padding, 1082 areaColStartLinePos, 1083 areaColEnd.start, 1084 ROWS, 1085 "areaEdge" 1086 ); 1087 this.renderLine( 1088 areaRowEnd.start - padding, 1089 areaColStartLinePos, 1090 areaColEnd.start, 1091 ROWS, 1092 "areaEdge" 1093 ); 1094 1095 this.renderGridAreaName(fragment, area); 1096 } 1097 } 1098 } 1099 1100 /** 1101 * Render the grid cell highlight for the given grid fragment index, row and column 1102 * number. 1103 * 1104 * @param {number} gridFragmentIndex 1105 * Index of the grid fragment to render the grid cell highlight. 1106 * @param {number} rowNumber 1107 * Row number of the grid cell to highlight. 1108 * @param {number} columnNumber 1109 * Column number of the grid cell to highlight. 1110 */ 1111 renderGridCell(gridFragmentIndex, rowNumber, columnNumber) { 1112 const fragment = this.gridData[gridFragmentIndex]; 1113 1114 if (!fragment) { 1115 return; 1116 } 1117 1118 const row = fragment.rows.tracks[rowNumber - 1]; 1119 const column = fragment.cols.tracks[columnNumber - 1]; 1120 1121 if (!row || !column) { 1122 return; 1123 } 1124 1125 const x1 = column.start; 1126 const y1 = row.start; 1127 const x2 = column.start + column.breadth; 1128 const y2 = row.start + row.breadth; 1129 1130 const { devicePixelRatio } = this.win; 1131 const displayPixelRatio = getDisplayPixelRatio(this.win); 1132 const points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix); 1133 1134 // Scale down by `devicePixelRatio` since SVG element already take them into account. 1135 const svgPoints = points.map(point => ({ 1136 x: Math.round(point.x / devicePixelRatio), 1137 y: Math.round(point.y / devicePixelRatio), 1138 })); 1139 1140 // Scale down by `displayPixelRatio` since infobar's HTML elements already take it 1141 // into account, and the zoom scaling is handled by `moveInfobar`. 1142 const bounds = getBoundsFromPoints( 1143 points.map(point => ({ 1144 x: Math.round(point.x / displayPixelRatio), 1145 y: Math.round(point.y / displayPixelRatio), 1146 })) 1147 ); 1148 1149 const cells = this.getElement("css-grid-cells"); 1150 cells.setAttribute("d", getPathDescriptionFromPoints(svgPoints)); 1151 1152 this._showGridCellInfoBar(); 1153 this._updateGridCellInfobar(rowNumber, columnNumber, bounds); 1154 } 1155 1156 /** 1157 * Render the grid gap area on the css grid highlighter canvas. 1158 * 1159 * @param {number} linePos 1160 * The line position along the x-axis for a column grid line and 1161 * y-axis for a row grid line. 1162 * @param {number} startPos 1163 * The start position of the cross side of the grid line. 1164 * @param {number} endPos 1165 * The end position of the cross side of the grid line. 1166 * @param {number} breadth 1167 * The grid line breadth value. 1168 * @param {string} dimensionType 1169 * The grid dimension type which is either the constant COLUMNS or ROWS. 1170 */ 1171 renderGridGap(linePos, startPos, endPos, breadth, dimensionType) { 1172 const { devicePixelRatio } = this.win; 1173 const displayPixelRatio = getDisplayPixelRatio(this.win); 1174 const offset = (displayPixelRatio / 2) % 1; 1175 const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio); 1176 const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio); 1177 1178 linePos = Math.round(linePos); 1179 startPos = Math.round(startPos); 1180 breadth = Math.round(breadth); 1181 1182 this.ctx.save(); 1183 this.ctx.fillStyle = this.getGridGapPattern( 1184 devicePixelRatio, 1185 dimensionType 1186 ); 1187 this.ctx.translate(offset - canvasX, offset - canvasY); 1188 1189 if (dimensionType === COLUMNS) { 1190 if (isFinite(endPos)) { 1191 endPos = Math.round(endPos); 1192 } else { 1193 endPos = this._winDimensions.height; 1194 startPos = -endPos; 1195 } 1196 drawRect( 1197 this.ctx, 1198 linePos, 1199 startPos, 1200 linePos + breadth, 1201 endPos, 1202 this.currentMatrix 1203 ); 1204 } else { 1205 if (isFinite(endPos)) { 1206 endPos = Math.round(endPos); 1207 } else { 1208 endPos = this._winDimensions.width; 1209 startPos = -endPos; 1210 } 1211 drawRect( 1212 this.ctx, 1213 startPos, 1214 linePos, 1215 endPos, 1216 linePos + breadth, 1217 this.currentMatrix 1218 ); 1219 } 1220 1221 // Find current angle of grid by measuring the angle of two arbitrary points, 1222 // then rotate canvas, so the hash pattern stays 45deg to the gridlines. 1223 const p1 = apply(this.currentMatrix, [0, 0]); 1224 const p2 = apply(this.currentMatrix, [1, 0]); 1225 const angleRad = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]); 1226 this.ctx.rotate(angleRad); 1227 1228 this.ctx.fill(); 1229 this.ctx.restore(); 1230 } 1231 1232 /** 1233 * Render the grid line name highlight for the given grid fragment index, lineNumber, 1234 * and dimensionType. 1235 * 1236 * @param {number} gridFragmentIndex 1237 * Index of the grid fragment to render the grid line highlight. 1238 * @param {number} lineNumber 1239 * Line number of the grid line to highlight. 1240 * @param {string} dimensionType 1241 * The dimension type of the grid line. 1242 */ 1243 renderGridLineNames(gridFragmentIndex, lineNumber, dimensionType) { 1244 const fragment = this.gridData[gridFragmentIndex]; 1245 1246 if (!fragment || !lineNumber || !dimensionType) { 1247 return; 1248 } 1249 1250 const { names } = fragment[dimensionType].lines[lineNumber - 1]; 1251 let linePos; 1252 1253 if (dimensionType === ROWS) { 1254 linePos = fragment.rows.lines[lineNumber - 1]; 1255 } else if (dimensionType === COLUMNS) { 1256 linePos = fragment.cols.lines[lineNumber - 1]; 1257 } 1258 1259 if (!linePos) { 1260 return; 1261 } 1262 1263 const currentZoom = getCurrentZoom(this.win); 1264 const { bounds } = this.currentQuads.content[gridFragmentIndex]; 1265 1266 const rowYPosition = fragment.rows.lines[0]; 1267 const colXPosition = fragment.rows.lines[0]; 1268 1269 const x = 1270 dimensionType === COLUMNS 1271 ? linePos.start + bounds.left / currentZoom 1272 : colXPosition.start + bounds.left / currentZoom; 1273 1274 const y = 1275 dimensionType === ROWS 1276 ? linePos.start + bounds.top / currentZoom 1277 : rowYPosition.start + bounds.top / currentZoom; 1278 1279 this._showGridLineInfoBar(); 1280 this._updateGridLineInfobar(names.join(", "), lineNumber, x, y); 1281 } 1282 1283 /** 1284 * Render the grid line number on the css grid highlighter canvas. 1285 * 1286 * @param {number} lineNumber 1287 * The grid line number. 1288 * @param {number} linePos 1289 * The line position along the x-axis for a column grid line and 1290 * y-axis for a row grid line. 1291 * @param {number} startPos 1292 * The start position of the cross side of the grid line. 1293 * @param {number} breadth 1294 * The grid line breadth value. 1295 * @param {string} dimensionType 1296 * The grid dimension type which is either the constant COLUMNS or ROWS. 1297 * @param {Boolean||undefined} isStackedLine 1298 * Boolean indicating if the line is stacked. 1299 */ 1300 // eslint-disable-next-line complexity 1301 renderGridLineNumber( 1302 lineNumber, 1303 linePos, 1304 startPos, 1305 breadth, 1306 dimensionType, 1307 isStackedLine 1308 ) { 1309 const displayPixelRatio = getDisplayPixelRatio(this.win); 1310 const { devicePixelRatio } = this.win; 1311 const offset = (displayPixelRatio / 2) % 1; 1312 const fontSize = GRID_FONT_SIZE * devicePixelRatio; 1313 const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio); 1314 const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio); 1315 1316 linePos = Math.round(linePos); 1317 startPos = Math.round(startPos); 1318 breadth = Math.round(breadth); 1319 1320 if (linePos + breadth < 0) { 1321 // Don't render the line number since the line is not visible on screen. 1322 return; 1323 } 1324 1325 this.ctx.save(); 1326 this.ctx.translate(offset - canvasX, offset - canvasY); 1327 this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY; 1328 1329 // For a general grid box, the height of the character "m" will be its minimum width 1330 // and height. If line number's text width is greater, then use the grid box's text 1331 // width instead. 1332 const textHeight = this.ctx.measureText("m").width; 1333 const textWidth = Math.max( 1334 textHeight, 1335 this.ctx.measureText(lineNumber).width 1336 ); 1337 1338 // Padding in pixels for the line number text inside of the line number container. 1339 const padding = 3 * devicePixelRatio; 1340 const offsetFromEdge = 2 * devicePixelRatio; 1341 1342 let boxWidth = textWidth + 2 * padding; 1343 let boxHeight = textHeight + 2 * padding; 1344 1345 // Calculate the x & y coordinates for the line number container, so that its arrow 1346 // tip is centered on the line (or the gap if there is one), and is offset by the 1347 // calculated padding value from the grid container edge. 1348 let x, y; 1349 1350 if (dimensionType === COLUMNS) { 1351 x = linePos + breadth / 2; 1352 y = 1353 lineNumber > 0 ? startPos - offsetFromEdge : startPos + offsetFromEdge; 1354 } else if (dimensionType === ROWS) { 1355 y = linePos + breadth / 2; 1356 x = 1357 lineNumber > 0 ? startPos - offsetFromEdge : startPos + offsetFromEdge; 1358 } 1359 1360 [x, y] = apply(this.currentMatrix, [x, y]); 1361 1362 // Draw a bubble rectangular arrow with a border width of 2 pixels, a border color 1363 // matching the grid color and a white background (the line number will be written in 1364 // black). 1365 this.ctx.lineWidth = 2 * displayPixelRatio; 1366 this.ctx.strokeStyle = this.color; 1367 this.ctx.fillStyle = "white"; 1368 this.ctx.globalAlpha = this.globalAlpha; 1369 1370 // See param definitions of drawBubbleRect. 1371 const radius = 2 * displayPixelRatio; 1372 const margin = 2 * displayPixelRatio; 1373 const arrowSize = 8 * displayPixelRatio; 1374 1375 const minBoxSize = arrowSize * 2 + padding; 1376 boxWidth = Math.max(boxWidth, minBoxSize); 1377 boxHeight = Math.max(boxHeight, minBoxSize); 1378 1379 // Determine which edge of the box to aim the line number arrow at. 1380 const boxEdge = this.getBoxEdge(dimensionType, lineNumber); 1381 1382 let { width, height } = this._winDimensions; 1383 width *= displayPixelRatio; 1384 height *= displayPixelRatio; 1385 1386 // Don't draw if the line is out of the viewport. 1387 if ( 1388 (dimensionType === ROWS && (y < 0 || y > height)) || 1389 (dimensionType === COLUMNS && (x < 0 || x > width)) 1390 ) { 1391 this.ctx.restore(); 1392 return; 1393 } 1394 1395 // If the arrow's edge (the one perpendicular to the line direction) is too close to 1396 // the edge of the viewport. Push the arrow inside the grid. 1397 const minOffsetFromEdge = OFFSET_FROM_EDGE * displayPixelRatio; 1398 switch (boxEdge) { 1399 case "left": 1400 if (x < minOffsetFromEdge) { 1401 x += FLIP_ARROW_INSIDE_FACTOR * boxWidth; 1402 } 1403 break; 1404 case "right": 1405 if (width - x < minOffsetFromEdge) { 1406 x -= FLIP_ARROW_INSIDE_FACTOR * boxWidth; 1407 } 1408 break; 1409 case "top": 1410 if (y < minOffsetFromEdge) { 1411 y += FLIP_ARROW_INSIDE_FACTOR * boxHeight; 1412 } 1413 break; 1414 case "bottom": 1415 if (height - y < minOffsetFromEdge) { 1416 y -= FLIP_ARROW_INSIDE_FACTOR * boxHeight; 1417 } 1418 break; 1419 } 1420 1421 // Offset stacked line numbers by a quarter of the box's width/height, so a part of 1422 // them remains visible behind the number that sits at the top of the stack. 1423 if (isStackedLine) { 1424 const xOffset = boxWidth / 4; 1425 const yOffset = boxHeight / 4; 1426 1427 if (lineNumber > 0) { 1428 x -= xOffset; 1429 y -= yOffset; 1430 } else { 1431 x += xOffset; 1432 y += yOffset; 1433 } 1434 } 1435 1436 // If one the edges of the arrow that's parallel to the line is too close to the edge 1437 // of the viewport (and therefore partly hidden), grow the arrow's size in the 1438 // opposite direction. 1439 // The goal is for the part that's not hidden to be exactly the size of a normal 1440 // arrow and for the arrow to keep pointing at the line (keep being centered on it). 1441 let grewBox = false; 1442 const boxWidthBeforeGrowth = boxWidth; 1443 const boxHeightBeforeGrowth = boxHeight; 1444 1445 if (dimensionType === ROWS && y <= boxHeight / 2) { 1446 grewBox = true; 1447 boxHeight = 2 * (boxHeight - y); 1448 } else if (dimensionType === ROWS && y >= height - boxHeight / 2) { 1449 grewBox = true; 1450 boxHeight = 2 * (y - height + boxHeight); 1451 } else if (dimensionType === COLUMNS && x <= boxWidth / 2) { 1452 grewBox = true; 1453 boxWidth = 2 * (boxWidth - x); 1454 } else if (dimensionType === COLUMNS && x >= width - boxWidth / 2) { 1455 grewBox = true; 1456 boxWidth = 2 * (x - width + boxWidth); 1457 } 1458 1459 // Draw the arrow box itself 1460 drawBubbleRect( 1461 this.ctx, 1462 x, 1463 y, 1464 boxWidth, 1465 boxHeight, 1466 radius, 1467 margin, 1468 arrowSize, 1469 boxEdge 1470 ); 1471 1472 // Determine the text position for it to be centered nicely inside the arrow box. 1473 switch (boxEdge) { 1474 case "left": 1475 x -= boxWidth + arrowSize + radius - boxWidth / 2; 1476 break; 1477 case "right": 1478 x += boxWidth + arrowSize + radius - boxWidth / 2; 1479 break; 1480 case "top": 1481 y -= boxHeight + arrowSize + radius - boxHeight / 2; 1482 break; 1483 case "bottom": 1484 y += boxHeight + arrowSize + radius - boxHeight / 2; 1485 break; 1486 } 1487 1488 // Do a second pass to adjust the position, along the other axis, if the box grew 1489 // during the previous step, so the text is also centered on that axis. 1490 if (grewBox) { 1491 if (dimensionType === ROWS && y <= boxHeightBeforeGrowth / 2) { 1492 y = boxHeightBeforeGrowth / 2; 1493 } else if ( 1494 dimensionType === ROWS && 1495 y >= height - boxHeightBeforeGrowth / 2 1496 ) { 1497 y = height - boxHeightBeforeGrowth / 2; 1498 } else if (dimensionType === COLUMNS && x <= boxWidthBeforeGrowth / 2) { 1499 x = boxWidthBeforeGrowth / 2; 1500 } else if ( 1501 dimensionType === COLUMNS && 1502 x >= width - boxWidthBeforeGrowth / 2 1503 ) { 1504 x = width - boxWidthBeforeGrowth / 2; 1505 } 1506 } 1507 1508 // Write the line number inside of the rectangle. 1509 this.ctx.textAlign = "center"; 1510 this.ctx.textBaseline = "middle"; 1511 this.ctx.fillStyle = "black"; 1512 const numberText = isStackedLine ? "" : lineNumber; 1513 this.ctx.fillText(numberText, x, y); 1514 this.ctx.restore(); 1515 } 1516 1517 /** 1518 * Determine which edge of a line number box to aim the line number arrow at. 1519 * 1520 * @param {string} dimensionType 1521 * The grid line dimension type which is either the constant COLUMNS or ROWS. 1522 * @param {number} lineNumber 1523 * The grid line number. 1524 * @return {string} The edge of the box: top, right, bottom or left. 1525 */ 1526 getBoxEdge(dimensionType, lineNumber) { 1527 let boxEdge; 1528 1529 if (dimensionType === COLUMNS) { 1530 boxEdge = lineNumber > 0 ? "top" : "bottom"; 1531 } else if (dimensionType === ROWS) { 1532 boxEdge = lineNumber > 0 ? "left" : "right"; 1533 } 1534 1535 // Rotate box edge as needed for writing mode and text direction. 1536 const { direction, writingMode } = getComputedStyle(this.currentNode); 1537 1538 switch (writingMode) { 1539 case "horizontal-tb": 1540 // This is the initial value. No further adjustment needed. 1541 break; 1542 case "vertical-rl": 1543 boxEdge = rotateEdgeRight(boxEdge); 1544 break; 1545 case "vertical-lr": 1546 if (dimensionType === COLUMNS) { 1547 boxEdge = rotateEdgeLeft(boxEdge); 1548 } else { 1549 boxEdge = rotateEdgeRight(boxEdge); 1550 } 1551 break; 1552 case "sideways-rl": 1553 boxEdge = rotateEdgeRight(boxEdge); 1554 break; 1555 case "sideways-lr": 1556 boxEdge = rotateEdgeLeft(boxEdge); 1557 break; 1558 default: 1559 console.error(`Unexpected writing-mode: ${writingMode}`); 1560 } 1561 1562 switch (direction) { 1563 case "ltr": 1564 // This is the initial value. No further adjustment needed. 1565 break; 1566 case "rtl": 1567 if (dimensionType === ROWS) { 1568 boxEdge = reflectEdge(boxEdge); 1569 } 1570 break; 1571 default: 1572 console.error(`Unexpected direction: ${direction}`); 1573 } 1574 1575 return boxEdge; 1576 } 1577 1578 /** 1579 * Render the grid line on the css grid highlighter canvas. 1580 * 1581 * @param {number} linePos 1582 * The line position along the x-axis for a column grid line and 1583 * y-axis for a row grid line. 1584 * @param {number} startPos 1585 * The start position of the cross side of the grid line. 1586 * @param {number} endPos 1587 * The end position of the cross side of the grid line. 1588 * @param {string} dimensionType 1589 * The grid dimension type which is either the constant COLUMNS or ROWS. 1590 * @param {string} lineType 1591 * The grid line type - "edge", "explicit", or "implicit". 1592 */ 1593 renderLine(linePos, startPos, endPos, dimensionType, lineType) { 1594 const { devicePixelRatio } = this.win; 1595 const lineWidth = getDisplayPixelRatio(this.win); 1596 const offset = (lineWidth / 2) % 1; 1597 const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio); 1598 const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio); 1599 1600 linePos = Math.round(linePos); 1601 startPos = Math.round(startPos); 1602 endPos = Math.round(endPos); 1603 1604 this.ctx.save(); 1605 this.ctx.setLineDash(GRID_LINES_PROPERTIES[lineType].lineDash); 1606 this.ctx.translate(offset - canvasX, offset - canvasY); 1607 1608 const lineOptions = { 1609 matrix: this.currentMatrix, 1610 }; 1611 1612 if (this.options.showInfiniteLines) { 1613 lineOptions.extendToBoundaries = [ 1614 canvasX, 1615 canvasY, 1616 canvasX + CANVAS_SIZE, 1617 canvasY + CANVAS_SIZE, 1618 ]; 1619 } 1620 1621 if (dimensionType === COLUMNS) { 1622 drawLine(this.ctx, linePos, startPos, linePos, endPos, lineOptions); 1623 } else { 1624 drawLine(this.ctx, startPos, linePos, endPos, linePos, lineOptions); 1625 } 1626 1627 this.ctx.strokeStyle = this.color; 1628 this.ctx.globalAlpha = 1629 GRID_LINES_PROPERTIES[lineType].alpha * this.globalAlpha; 1630 1631 if (GRID_LINES_PROPERTIES[lineType].lineWidth) { 1632 this.ctx.lineWidth = 1633 GRID_LINES_PROPERTIES[lineType].lineWidth * devicePixelRatio; 1634 } else { 1635 this.ctx.lineWidth = lineWidth; 1636 } 1637 1638 this.ctx.stroke(); 1639 this.ctx.restore(); 1640 } 1641 1642 /** 1643 * Render the grid lines given the grid dimension information of the 1644 * column or row lines. 1645 * 1646 * @param {GridDimension} gridDimension 1647 * Column or row grid dimension object. 1648 * @param {object} quad.bounds 1649 * The content bounds of the box model region quads. 1650 * @param {string} dimensionType 1651 * The grid dimension type which is either the constant COLUMNS or ROWS. 1652 * @param {number} startPos 1653 * The start position of the cross side ("left" for ROWS and "top" for COLUMNS) 1654 * of the grid dimension. 1655 * @param {number} endPos 1656 * The end position of the cross side ("left" for ROWS and "top" for COLUMNS) 1657 * of the grid dimension. 1658 */ 1659 renderLines(gridDimension, dimensionType, startPos, endPos) { 1660 const { lines, tracks } = gridDimension; 1661 const lastEdgeLineIndex = this.getLastEdgeLineIndex(tracks); 1662 1663 for (let i = 0; i < lines.length; i++) { 1664 const line = lines[i]; 1665 const linePos = line.start; 1666 1667 if (i == 0 || i == lastEdgeLineIndex) { 1668 this.renderLine(linePos, startPos, endPos, dimensionType, "edge"); 1669 } else { 1670 this.renderLine( 1671 linePos, 1672 startPos, 1673 endPos, 1674 dimensionType, 1675 tracks[i - 1].type 1676 ); 1677 } 1678 1679 // Render a second line to illustrate the gutter for non-zero breadth. 1680 if (line.breadth > 0) { 1681 this.renderGridGap( 1682 linePos, 1683 startPos, 1684 endPos, 1685 line.breadth, 1686 dimensionType 1687 ); 1688 this.renderLine( 1689 linePos + line.breadth, 1690 startPos, 1691 endPos, 1692 dimensionType, 1693 tracks[i].type 1694 ); 1695 } 1696 } 1697 } 1698 1699 /** 1700 * Render the grid lines given the grid dimension information of the 1701 * column or row lines. 1702 * 1703 * @param {GridDimension} gridDimension 1704 * Column or row grid dimension object. 1705 * @param {string} dimensionType 1706 * The grid dimension type which is either the constant COLUMNS or ROWS. 1707 * @param {number} startPos 1708 * The start position of the cross side ("left" for ROWS and "top" for COLUMNS) 1709 * of the grid dimension. 1710 */ 1711 renderLineNumbers(gridDimension, dimensionType, startPos) { 1712 const { lines, tracks } = gridDimension; 1713 1714 for (let i = 0, line; (line = lines[i++]); ) { 1715 // If you place something using negative numbers, you can trigger some implicit 1716 // grid creation above and to the left of the explicit grid (assuming a 1717 // horizontal-tb writing mode). 1718 // 1719 // The first explicit grid line gets the number of 1, and any implicit grid lines 1720 // before 1 get negative numbers. Since here we're rendering only the positive line 1721 // numbers, we have to skip any implicit grid lines before the first one that is 1722 // explicit. The API returns a 0 as the line's number for these implicit lines that 1723 // occurs before the first explicit line. 1724 if (line.number === 0) { 1725 continue; 1726 } 1727 1728 // Check for overlapping lines by measuring the track width between them. 1729 // We render a second box beneath the last overlapping 1730 // line number to indicate there are lines beneath it. 1731 const gridTrack = tracks[i - 1]; 1732 1733 if (gridTrack) { 1734 const { breadth } = gridTrack; 1735 1736 if (breadth === 0) { 1737 this.renderGridLineNumber( 1738 line.number, 1739 line.start, 1740 startPos, 1741 line.breadth, 1742 dimensionType, 1743 true 1744 ); 1745 continue; 1746 } 1747 } 1748 1749 this.renderGridLineNumber( 1750 line.number, 1751 line.start, 1752 startPos, 1753 line.breadth, 1754 dimensionType 1755 ); 1756 } 1757 } 1758 1759 /** 1760 * Render the negative grid lines given the grid dimension information of the 1761 * column or row lines. 1762 * 1763 * @param {GridDimension} gridDimension 1764 * Column or row grid dimension object. 1765 * @param {string} dimensionType 1766 * The grid dimension type which is either the constant COLUMNS or ROWS. 1767 * @param {number} startPos 1768 * The start position of the cross side ("left" for ROWS and "top" for COLUMNS) 1769 * of the grid dimension. 1770 */ 1771 renderNegativeLineNumbers(gridDimension, dimensionType, startPos) { 1772 const { lines, tracks } = gridDimension; 1773 1774 for (let i = 0, line; (line = lines[i++]); ) { 1775 const linePos = line.start; 1776 const negativeLineNumber = line.negativeNumber; 1777 1778 // Don't render any negative line number greater than -1. 1779 if (negativeLineNumber == 0) { 1780 break; 1781 } 1782 1783 // Check for overlapping lines by measuring the track width between them. 1784 // We render a second box beneath the last overlapping 1785 // line number to indicate there are lines beneath it. 1786 const gridTrack = tracks[i - 1]; 1787 if (gridTrack) { 1788 const { breadth } = gridTrack; 1789 1790 // Ensure "-1" is always visible, since it is always the largest number. 1791 if (breadth === 0 && negativeLineNumber != -1) { 1792 this.renderGridLineNumber( 1793 negativeLineNumber, 1794 linePos, 1795 startPos, 1796 line.breadth, 1797 dimensionType, 1798 true 1799 ); 1800 continue; 1801 } 1802 } 1803 1804 this.renderGridLineNumber( 1805 negativeLineNumber, 1806 linePos, 1807 startPos, 1808 line.breadth, 1809 dimensionType 1810 ); 1811 } 1812 } 1813 1814 /** 1815 * Update the highlighter on the current highlighted node (the one that was 1816 * passed as an argument to show(node)). Should be called whenever node's geometry 1817 * or grid changes. 1818 */ 1819 _update() { 1820 setIgnoreLayoutChanges(true); 1821 1822 const root = this.getNode("css-grid-root"); 1823 this._winDimensions = getWindowDimensions(this.win); 1824 const { width, height } = this._winDimensions; 1825 1826 // Updates the <canvas> element's position and size. 1827 // It also clear the <canvas>'s drawing context. 1828 updateCanvasElement( 1829 this.canvas, 1830 this._canvasPosition, 1831 this.win.devicePixelRatio 1832 ); 1833 1834 // Clear the grid area highlights. 1835 this.clearGridAreas(); 1836 this.clearGridCell(); 1837 1838 // Update the current matrix used in our canvas' rendering. 1839 const { currentMatrix, hasNodeTransformations } = getCurrentMatrix( 1840 this.currentNode, 1841 this.win 1842 ); 1843 this.currentMatrix = currentMatrix; 1844 this.hasNodeTransformations = hasNodeTransformations; 1845 1846 // Start drawing the grid fragments. 1847 for (let i = 0; i < this.gridData.length; i++) { 1848 this.renderFragment(this.gridData[i]); 1849 } 1850 1851 // Display the grid area highlights if needed. 1852 if (this.options.showAllGridAreas) { 1853 this.showAllGridAreas(); 1854 } else if (this.options.showGridArea) { 1855 this.showGridArea(this.options.showGridArea); 1856 } 1857 1858 // Display the grid cell highlights if needed. 1859 if (this.options.showGridCell) { 1860 this.showGridCell(this.options.showGridCell); 1861 } 1862 1863 // Display the grid line names if needed. 1864 if (this.options.showGridLineNames) { 1865 this.showGridLineNames(this.options.showGridLineNames); 1866 } 1867 1868 this._showGrid(); 1869 this._showGridElements(); 1870 1871 root.style.setProperty("width", `${width}px`); 1872 root.style.setProperty("height", `${height}px`); 1873 1874 setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement); 1875 return true; 1876 } 1877 1878 /** 1879 * Update the grid information displayed in the grid area info bar. 1880 * 1881 * @param {GridArea} area 1882 * The grid area object. 1883 * @param {object} bounds 1884 * A DOMRect-like object represent the grid area rectangle. 1885 */ 1886 _updateGridAreaInfobar(area, bounds) { 1887 const { width, height } = bounds; 1888 const dim = 1889 parseFloat(width.toPrecision(6)) + 1890 " \u00D7 " + 1891 parseFloat(height.toPrecision(6)); 1892 1893 this.getElement("css-grid-area-infobar-name").setTextContent(area.name); 1894 this.getElement("css-grid-area-infobar-dimensions").setTextContent(dim); 1895 1896 const container = this.getElement("css-grid-area-infobar-container"); 1897 moveInfobar(container, bounds, this.win, { 1898 position: "bottom", 1899 }); 1900 } 1901 1902 /** 1903 * Update the grid information displayed in the grid cell info bar. 1904 * 1905 * @param {number} rowNumber 1906 * The grid cell's row number. 1907 * @param {number} columnNumber 1908 * The grid cell's column number. 1909 * @param {object} bounds 1910 * A DOMRect-like object represent the grid cell rectangle. 1911 */ 1912 _updateGridCellInfobar(rowNumber, columnNumber, bounds) { 1913 const { width, height } = bounds; 1914 const dim = 1915 parseFloat(width.toPrecision(6)) + 1916 " \u00D7 " + 1917 parseFloat(height.toPrecision(6)); 1918 const position = HighlightersBundle.formatValueSync( 1919 "grid-row-column-positions", 1920 { row: rowNumber, column: columnNumber } 1921 ); 1922 1923 this.getElement("css-grid-cell-infobar-position").setTextContent(position); 1924 this.getElement("css-grid-cell-infobar-dimensions").setTextContent(dim); 1925 1926 const container = this.getElement("css-grid-cell-infobar-container"); 1927 moveInfobar(container, bounds, this.win, { 1928 position: "top", 1929 }); 1930 } 1931 1932 /** 1933 * Update the grid information displayed in the grid line info bar. 1934 * 1935 * @param {string} gridLineNames 1936 * Comma-separated string of names for the grid line. 1937 * @param {number} gridLineNumber 1938 * The grid line number. 1939 * @param {number} x 1940 * The x-coordinate of the grid line. 1941 * @param {number} y 1942 * The y-coordinate of the grid line. 1943 */ 1944 _updateGridLineInfobar(gridLineNames, gridLineNumber, x, y) { 1945 this.getElement("css-grid-line-infobar-number").setTextContent( 1946 gridLineNumber 1947 ); 1948 this.getElement("css-grid-line-infobar-names").setTextContent( 1949 gridLineNames 1950 ); 1951 1952 const container = this.getElement("css-grid-line-infobar-container"); 1953 moveInfobar( 1954 container, 1955 getBoundsFromPoints([ 1956 { x, y }, 1957 { x, y }, 1958 { x, y }, 1959 { x, y }, 1960 ]), 1961 this.win 1962 ); 1963 } 1964 } 1965 1966 exports.CssGridHighlighter = CssGridHighlighter;