flexbox.js (30234B)
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 { apply } = require("resource://devtools/shared/layout/dom-matrix-2d.js"); 11 const { 12 CANVAS_SIZE, 13 DEFAULT_COLOR, 14 clearRect, 15 drawLine, 16 drawRect, 17 getCurrentMatrix, 18 updateCanvasElement, 19 updateCanvasPosition, 20 } = require("resource://devtools/server/actors/highlighters/utils/canvas.js"); 21 const { 22 CanvasFrameAnonymousContentHelper, 23 getComputedStyle, 24 } = require("resource://devtools/server/actors/highlighters/utils/markup.js"); 25 const { 26 getAbsoluteScrollOffsetsForNode, 27 getCurrentZoom, 28 getDisplayPixelRatio, 29 getUntransformedQuad, 30 getWindowDimensions, 31 setIgnoreLayoutChanges, 32 } = require("resource://devtools/shared/layout/utils.js"); 33 34 const FLEXBOX_LINES_PROPERTIES = { 35 edge: { 36 lineDash: [5, 3], 37 }, 38 item: { 39 lineDash: [0, 0], 40 }, 41 alignItems: { 42 lineDash: [0, 0], 43 }, 44 }; 45 46 const FLEXBOX_CONTAINER_PATTERN_LINE_DASH = [5, 3]; // px 47 const FLEXBOX_CONTAINER_PATTERN_WIDTH = 14; // px 48 const FLEXBOX_CONTAINER_PATTERN_HEIGHT = 14; // px 49 const FLEXBOX_JUSTIFY_CONTENT_PATTERN_WIDTH = 7; // px 50 const FLEXBOX_JUSTIFY_CONTENT_PATTERN_HEIGHT = 7; // px 51 52 /** 53 * Cached used by `FlexboxHighlighter.getFlexContainerPattern`. 54 */ 55 const gCachedFlexboxPattern = new Map(); 56 57 const FLEXBOX = "flexbox"; 58 const JUSTIFY_CONTENT = "justify-content"; 59 60 /** 61 * The FlexboxHighlighter is the class that overlays a visual canvas on top of 62 * display: [inline-]flex elements. 63 * 64 * @param {string} options.color 65 * The color that should be used to draw the highlighter for this flexbox. 66 * Structure: 67 * <div class="highlighter-container"> 68 * <div id="flexbox-root" class="flexbox-root"> 69 * <canvas id="flexbox-canvas" 70 * class="flexbox-canvas" 71 * width="4096" 72 * height="4096" 73 * hidden="true"> 74 * </canvas> 75 * </div> 76 * </div> 77 */ 78 class FlexboxHighlighter extends AutoRefreshHighlighter { 79 constructor(highlighterEnv) { 80 super(highlighterEnv); 81 82 this.markup = new CanvasFrameAnonymousContentHelper( 83 this.highlighterEnv, 84 this._buildMarkup.bind(this), 85 { 86 contentRootHostClassName: "devtools-highlighter-flexbox", 87 } 88 ); 89 this.isReady = this.markup.initialize(); 90 91 this.onPageHide = this.onPageHide.bind(this); 92 this.onWillNavigate = this.onWillNavigate.bind(this); 93 94 this.highlighterEnv.on("will-navigate", this.onWillNavigate); 95 96 const { pageListenerTarget } = highlighterEnv; 97 pageListenerTarget.addEventListener("pagehide", this.onPageHide); 98 99 // Initialize the <canvas> position to the top left corner of the page 100 this._canvasPosition = { 101 x: 0, 102 y: 0, 103 }; 104 105 this._ignoreZoom = true; 106 107 // Calling `updateCanvasPosition` anyway since the highlighter could be initialized 108 // on a page that has scrolled already. 109 updateCanvasPosition( 110 this._canvasPosition, 111 this._scroll, 112 this.win, 113 this._winDimensions 114 ); 115 } 116 117 _buildMarkup() { 118 const container = this.markup.createNode({ 119 attributes: { 120 class: "flexbox-highlighter-container", 121 }, 122 }); 123 124 this.rootEl = this.markup.createNode({ 125 parent: container, 126 attributes: { 127 id: "flexbox-root", 128 class: "flexbox-root", 129 }, 130 }); 131 132 // We use a <canvas> element because there is an arbitrary number of items and texts 133 // to draw which wouldn't be possible with HTML or SVG without having to insert and 134 // remove the whole markup on every update. 135 this.markup.createNode({ 136 parent: this.rootEl, 137 nodeType: "canvas", 138 attributes: { 139 id: "flexbox-canvas", 140 class: "flexbox-canvas", 141 hidden: "true", 142 width: CANVAS_SIZE, 143 height: CANVAS_SIZE, 144 }, 145 }); 146 147 return container; 148 } 149 150 clearCache() { 151 gCachedFlexboxPattern.clear(); 152 } 153 154 destroy() { 155 const { highlighterEnv } = this; 156 highlighterEnv.off("will-navigate", this.onWillNavigate); 157 158 const { pageListenerTarget } = highlighterEnv; 159 160 if (pageListenerTarget) { 161 pageListenerTarget.removeEventListener("pagehide", this.onPageHide); 162 } 163 164 this.markup.destroy(); 165 this.rootEl = null; 166 167 // Clear the pattern cache to avoid dead object exceptions (Bug 1342051). 168 this.clearCache(); 169 170 this.axes = null; 171 this.crossAxisDirection = null; 172 this.flexData = null; 173 this.mainAxisDirection = null; 174 this.transform = null; 175 176 AutoRefreshHighlighter.prototype.destroy.call(this); 177 } 178 179 /** 180 * Draw the justify content for a given flex item (left, top, right, bottom) position. 181 */ 182 drawJustifyContent(left, top, right, bottom) { 183 const { devicePixelRatio } = this.win; 184 this.ctx.fillStyle = this.getJustifyContentPattern(devicePixelRatio); 185 drawRect(this.ctx, left, top, right, bottom, this.currentMatrix); 186 this.ctx.fill(); 187 } 188 189 get canvas() { 190 return this.getElement("flexbox-canvas"); 191 } 192 193 get color() { 194 return this.options.color || DEFAULT_COLOR; 195 } 196 197 get container() { 198 return this.currentNode; 199 } 200 201 get ctx() { 202 return this.canvas.getCanvasContext("2d"); 203 } 204 205 getElement(id) { 206 return this.markup.getElement(id); 207 } 208 209 /** 210 * Gets the flexbox container pattern used to render the container regions. 211 * 212 * @param {number} devicePixelRatio 213 * The device pixel ratio we want the pattern for. 214 * @return {CanvasPattern} flex container pattern. 215 */ 216 getFlexContainerPattern(devicePixelRatio) { 217 let flexboxPatternMap = null; 218 219 if (gCachedFlexboxPattern.has(devicePixelRatio)) { 220 flexboxPatternMap = gCachedFlexboxPattern.get(devicePixelRatio); 221 } else { 222 flexboxPatternMap = new Map(); 223 } 224 225 if (gCachedFlexboxPattern.has(FLEXBOX)) { 226 return gCachedFlexboxPattern.get(FLEXBOX); 227 } 228 229 // Create the diagonal lines pattern for the rendering the flexbox gaps. 230 const canvas = this.markup.createNode({ nodeType: "canvas" }); 231 const width = (canvas.width = 232 FLEXBOX_CONTAINER_PATTERN_WIDTH * devicePixelRatio); 233 const height = (canvas.height = 234 FLEXBOX_CONTAINER_PATTERN_HEIGHT * devicePixelRatio); 235 236 const ctx = canvas.getContext("2d"); 237 ctx.save(); 238 ctx.setLineDash(FLEXBOX_CONTAINER_PATTERN_LINE_DASH); 239 ctx.beginPath(); 240 ctx.translate(0.5, 0.5); 241 242 ctx.moveTo(0, 0); 243 ctx.lineTo(width, height); 244 245 ctx.strokeStyle = this.color; 246 ctx.stroke(); 247 ctx.restore(); 248 249 const pattern = ctx.createPattern(canvas, "repeat"); 250 flexboxPatternMap.set(FLEXBOX, pattern); 251 gCachedFlexboxPattern.set(devicePixelRatio, flexboxPatternMap); 252 253 return pattern; 254 } 255 256 /** 257 * Gets the flexbox justify content pattern used to render the justify content regions. 258 * 259 * @param {number} devicePixelRatio 260 * The device pixel ratio we want the pattern for. 261 * @return {CanvasPattern} flex justify content pattern. 262 */ 263 getJustifyContentPattern(devicePixelRatio) { 264 let flexboxPatternMap = null; 265 266 if (gCachedFlexboxPattern.has(devicePixelRatio)) { 267 flexboxPatternMap = gCachedFlexboxPattern.get(devicePixelRatio); 268 } else { 269 flexboxPatternMap = new Map(); 270 } 271 272 if (flexboxPatternMap.has(JUSTIFY_CONTENT)) { 273 return flexboxPatternMap.get(JUSTIFY_CONTENT); 274 } 275 276 // Create the inversed diagonal lines pattern 277 // for the rendering the justify content gaps. 278 const canvas = this.markup.createNode({ nodeType: "canvas" }); 279 const zoom = getCurrentZoom(this.win); 280 const width = (canvas.width = 281 FLEXBOX_JUSTIFY_CONTENT_PATTERN_WIDTH * devicePixelRatio * zoom); 282 const height = (canvas.height = 283 FLEXBOX_JUSTIFY_CONTENT_PATTERN_HEIGHT * devicePixelRatio * zoom); 284 285 const ctx = canvas.getContext("2d"); 286 ctx.save(); 287 ctx.setLineDash(FLEXBOX_CONTAINER_PATTERN_LINE_DASH); 288 ctx.beginPath(); 289 ctx.translate(0.5, 0.5); 290 291 ctx.moveTo(0, height); 292 ctx.lineTo(width, 0); 293 294 ctx.strokeStyle = this.color; 295 ctx.stroke(); 296 ctx.restore(); 297 298 const pattern = ctx.createPattern(canvas, "repeat"); 299 flexboxPatternMap.set(JUSTIFY_CONTENT, pattern); 300 gCachedFlexboxPattern.set(devicePixelRatio, flexboxPatternMap); 301 302 return pattern; 303 } 304 305 getNode(id) { 306 return this.markup.content.root.getElementById(id); 307 } 308 309 /** 310 * The AutoRefreshHighlighter's _hasMoved method returns true only if the 311 * element's quads have changed. Override it so it also returns true if the 312 * flex container and its flex items have changed. 313 */ 314 _hasMoved() { 315 const hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this); 316 317 if (!this.computedStyle) { 318 this.computedStyle = getComputedStyle(this.container); 319 } 320 321 const flex = this.container.getAsFlexContainer(); 322 323 const oldCrossAxisDirection = this.crossAxisDirection; 324 this.crossAxisDirection = flex ? flex.crossAxisDirection : null; 325 const newCrossAxisDirection = this.crossAxisDirection; 326 327 const oldMainAxisDirection = this.mainAxisDirection; 328 this.mainAxisDirection = flex ? flex.mainAxisDirection : null; 329 const newMainAxisDirection = this.mainAxisDirection; 330 331 // Concatenate the axes to simplify conditionals. 332 this.axes = `${this.mainAxisDirection} ${this.crossAxisDirection}`; 333 334 const oldFlexData = this.flexData; 335 this.flexData = getFlexData(this.container); 336 const hasFlexDataChanged = compareFlexData(oldFlexData, this.flexData); 337 338 const oldAlignItems = this.alignItemsValue; 339 this.alignItemsValue = this.computedStyle.alignItems; 340 const newAlignItems = this.alignItemsValue; 341 342 const oldFlexDirection = this.flexDirection; 343 this.flexDirection = this.computedStyle.flexDirection; 344 const newFlexDirection = this.flexDirection; 345 346 const oldFlexWrap = this.flexWrap; 347 this.flexWrap = this.computedStyle.flexWrap; 348 const newFlexWrap = this.flexWrap; 349 350 const oldJustifyContent = this.justifyContentValue; 351 this.justifyContentValue = this.computedStyle.justifyContent; 352 const newJustifyContent = this.justifyContentValue; 353 354 const oldTransform = this.transformValue; 355 this.transformValue = this.computedStyle.transform; 356 const newTransform = this.transformValue; 357 358 return ( 359 hasMoved || 360 hasFlexDataChanged || 361 oldAlignItems !== newAlignItems || 362 oldFlexDirection !== newFlexDirection || 363 oldFlexWrap !== newFlexWrap || 364 oldJustifyContent !== newJustifyContent || 365 oldCrossAxisDirection !== newCrossAxisDirection || 366 oldMainAxisDirection !== newMainAxisDirection || 367 oldTransform !== newTransform 368 ); 369 } 370 371 _hide() { 372 this.alignItemsValue = null; 373 this.computedStyle = null; 374 this.flexData = null; 375 this.flexDirection = null; 376 this.flexWrap = null; 377 this.justifyContentValue = null; 378 379 setIgnoreLayoutChanges(true); 380 this._hideFlexbox(); 381 setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement); 382 } 383 384 _hideFlexbox() { 385 this.getElement("flexbox-canvas").setAttribute("hidden", "true"); 386 } 387 388 /** 389 * The <canvas>'s position needs to be updated if the page scrolls too much, in order 390 * to give the illusion that it always covers the viewport. 391 */ 392 _scrollUpdate() { 393 const hasUpdated = updateCanvasPosition( 394 this._canvasPosition, 395 this._scroll, 396 this.win, 397 this._winDimensions 398 ); 399 400 if (hasUpdated) { 401 this._update(); 402 } 403 } 404 405 _show() { 406 this._hide(); 407 return this._update(); 408 } 409 410 _showFlexbox() { 411 this.getElement("flexbox-canvas").removeAttribute("hidden"); 412 } 413 414 /** 415 * If a page hide event is triggered for current window's highlighter, hide the 416 * highlighter. 417 */ 418 onPageHide({ target }) { 419 if (target.defaultView === this.win) { 420 this.hide(); 421 } 422 } 423 424 /** 425 * Called when the page will-navigate. Used to hide the flexbox highlighter and clear 426 * the cached gap patterns and avoid using DeadWrapper obejcts as gap patterns the 427 * next time. 428 */ 429 onWillNavigate({ isTopLevel }) { 430 this.clearCache(); 431 432 if (isTopLevel) { 433 this.hide(); 434 } 435 } 436 437 renderFlexContainer() { 438 if (!this.currentQuads.content || !this.currentQuads.content[0]) { 439 return; 440 } 441 442 const { devicePixelRatio } = this.win; 443 const containerQuad = getUntransformedQuad(this.container, "content"); 444 const { width, height } = containerQuad.getBounds(); 445 446 this.setupCanvas({ 447 lineDash: FLEXBOX_LINES_PROPERTIES.alignItems.lineDash, 448 lineWidthMultiplier: 2, 449 }); 450 451 this.ctx.fillStyle = this.getFlexContainerPattern(devicePixelRatio); 452 453 drawRect(this.ctx, 0, 0, width, height, this.currentMatrix); 454 455 // Find current angle of outer flex element by measuring the angle of two arbitrary 456 // points, then rotate canvas, so the hash pattern stays 45deg to the boundary. 457 const p1 = apply(this.currentMatrix, [0, 0]); 458 const p2 = apply(this.currentMatrix, [1, 0]); 459 const angleRad = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]); 460 this.ctx.rotate(angleRad); 461 462 this.ctx.fill(); 463 this.ctx.stroke(); 464 this.ctx.restore(); 465 } 466 467 renderFlexItems() { 468 if ( 469 !this.flexData || 470 !this.currentQuads.content || 471 !this.currentQuads.content[0] 472 ) { 473 return; 474 } 475 476 this.setupCanvas({ 477 lineDash: FLEXBOX_LINES_PROPERTIES.item.lineDash, 478 }); 479 480 for (const flexLine of this.flexData.lines) { 481 for (const flexItem of flexLine.items) { 482 const { left, top, right, bottom } = flexItem.rect; 483 484 clearRect(this.ctx, left, top, right, bottom, this.currentMatrix); 485 drawRect(this.ctx, left, top, right, bottom, this.currentMatrix); 486 this.ctx.stroke(); 487 } 488 } 489 490 this.ctx.restore(); 491 } 492 493 renderFlexLines() { 494 if ( 495 !this.flexData || 496 !this.currentQuads.content || 497 !this.currentQuads.content[0] 498 ) { 499 return; 500 } 501 502 const lineWidth = getDisplayPixelRatio(this.win); 503 const options = { matrix: this.currentMatrix }; 504 const { width: containerWidth, height: containerHeight } = 505 getUntransformedQuad(this.container, "content").getBounds(); 506 507 this.setupCanvas({ 508 useContainerScrollOffsets: true, 509 }); 510 511 for (const flexLine of this.flexData.lines) { 512 const { crossStart, crossSize } = flexLine; 513 514 switch (this.axes) { 515 case "horizontal-lr vertical-tb": 516 case "horizontal-lr vertical-bt": 517 case "horizontal-rl vertical-tb": 518 case "horizontal-rl vertical-bt": 519 clearRect( 520 this.ctx, 521 0, 522 crossStart, 523 containerWidth, 524 crossStart + crossSize, 525 this.currentMatrix 526 ); 527 528 // Avoid drawing the start flex line when they overlap with the flex container. 529 if (crossStart != 0) { 530 drawLine( 531 this.ctx, 532 0, 533 crossStart, 534 containerWidth, 535 crossStart, 536 options 537 ); 538 this.ctx.stroke(); 539 } 540 541 // Avoid drawing the end flex line when they overlap with the flex container. 542 if (crossStart + crossSize < containerHeight - lineWidth * 2) { 543 drawLine( 544 this.ctx, 545 0, 546 crossStart + crossSize, 547 containerWidth, 548 crossStart + crossSize, 549 options 550 ); 551 this.ctx.stroke(); 552 } 553 break; 554 case "vertical-tb horizontal-lr": 555 case "vertical-bt horizontal-rl": 556 clearRect( 557 this.ctx, 558 crossStart, 559 0, 560 crossStart + crossSize, 561 containerHeight, 562 this.currentMatrix 563 ); 564 565 // Avoid drawing the start flex line when they overlap with the flex container. 566 if (crossStart != 0) { 567 drawLine( 568 this.ctx, 569 crossStart, 570 0, 571 crossStart, 572 containerHeight, 573 options 574 ); 575 this.ctx.stroke(); 576 } 577 578 // Avoid drawing the end flex line when they overlap with the flex container. 579 if (crossStart + crossSize < containerWidth - lineWidth * 2) { 580 drawLine( 581 this.ctx, 582 crossStart + crossSize, 583 0, 584 crossStart + crossSize, 585 containerHeight, 586 options 587 ); 588 this.ctx.stroke(); 589 } 590 break; 591 case "vertical-bt horizontal-lr": 592 case "vertical-tb horizontal-rl": 593 clearRect( 594 this.ctx, 595 containerWidth - crossStart, 596 0, 597 containerWidth - crossStart - crossSize, 598 containerHeight, 599 this.currentMatrix 600 ); 601 602 // Avoid drawing the start flex line when they overlap with the flex container. 603 if (crossStart != 0) { 604 drawLine( 605 this.ctx, 606 containerWidth - crossStart, 607 0, 608 containerWidth - crossStart, 609 containerHeight, 610 options 611 ); 612 this.ctx.stroke(); 613 } 614 615 // Avoid drawing the end flex line when they overlap with the flex container. 616 if (crossStart + crossSize < containerWidth - lineWidth * 2) { 617 drawLine( 618 this.ctx, 619 containerWidth - crossStart - crossSize, 620 0, 621 containerWidth - crossStart - crossSize, 622 containerHeight, 623 options 624 ); 625 this.ctx.stroke(); 626 } 627 break; 628 } 629 } 630 631 this.ctx.restore(); 632 } 633 634 /** 635 * Clear the whole alignment container along the main axis for each flex item. 636 */ 637 // eslint-disable-next-line complexity 638 renderJustifyContent() { 639 if ( 640 !this.flexData || 641 !this.currentQuads.content || 642 !this.currentQuads.content[0] 643 ) { 644 return; 645 } 646 647 const { width: containerWidth, height: containerHeight } = 648 getUntransformedQuad(this.container, "content").getBounds(); 649 650 this.setupCanvas({ 651 lineDash: FLEXBOX_LINES_PROPERTIES.alignItems.lineDash, 652 offset: (getDisplayPixelRatio(this.win) / 2) % 1, 653 skipLineAndStroke: true, 654 useContainerScrollOffsets: true, 655 }); 656 657 for (const flexLine of this.flexData.lines) { 658 const { crossStart, crossSize } = flexLine; 659 let mainStart = 0; 660 661 // In these two situations mainStart goes from right to left so set it's 662 // value as appropriate. 663 if ( 664 this.axes === "horizontal-lr vertical-bt" || 665 this.axes === "horizontal-rl vertical-tb" 666 ) { 667 mainStart = containerWidth; 668 } 669 670 for (const flexItem of flexLine.items) { 671 const { left, top, right, bottom } = flexItem.rect; 672 673 switch (this.axes) { 674 case "horizontal-lr vertical-tb": 675 case "horizontal-rl vertical-bt": 676 this.drawJustifyContent( 677 mainStart, 678 crossStart, 679 left, 680 crossStart + crossSize 681 ); 682 mainStart = right; 683 break; 684 case "horizontal-lr vertical-bt": 685 case "horizontal-rl vertical-tb": 686 this.drawJustifyContent( 687 right, 688 crossStart, 689 mainStart, 690 crossStart + crossSize 691 ); 692 mainStart = left; 693 break; 694 case "vertical-tb horizontal-lr": 695 case "vertical-bt horizontal-rl": 696 this.drawJustifyContent( 697 crossStart, 698 mainStart, 699 crossStart + crossSize, 700 top 701 ); 702 mainStart = bottom; 703 break; 704 case "vertical-bt horizontal-lr": 705 case "vertical-tb horizontal-rl": 706 this.drawJustifyContent( 707 containerWidth - crossStart - crossSize, 708 mainStart, 709 containerWidth - crossStart, 710 top 711 ); 712 mainStart = bottom; 713 break; 714 } 715 } 716 717 // Draw the last justify-content area after the last flex item. 718 switch (this.axes) { 719 case "horizontal-lr vertical-tb": 720 case "horizontal-rl vertical-bt": 721 this.drawJustifyContent( 722 mainStart, 723 crossStart, 724 containerWidth, 725 crossStart + crossSize 726 ); 727 break; 728 case "horizontal-lr vertical-bt": 729 case "horizontal-rl vertical-tb": 730 this.drawJustifyContent( 731 0, 732 crossStart, 733 mainStart, 734 crossStart + crossSize 735 ); 736 break; 737 case "vertical-tb horizontal-lr": 738 case "vertical-bt horizontal-rl": 739 this.drawJustifyContent( 740 crossStart, 741 mainStart, 742 crossStart + crossSize, 743 containerHeight 744 ); 745 break; 746 case "vertical-bt horizontal-lr": 747 case "vertical-tb horizontal-rl": 748 this.drawJustifyContent( 749 containerWidth - crossStart - crossSize, 750 mainStart, 751 containerWidth - crossStart, 752 containerHeight 753 ); 754 break; 755 } 756 } 757 758 this.ctx.restore(); 759 } 760 761 /** 762 * Set up the canvas with the given options prior to drawing. 763 * 764 * @param {string} [options.lineDash = null] 765 * An Array of numbers that specify distances to alternately draw a 766 * line and a gap (in coordinate space units). If the number of 767 * elements in the array is odd, the elements of the array get copied 768 * and concatenated. For example, [5, 15, 25] will become 769 * [5, 15, 25, 5, 15, 25]. If the array is empty, the line dash list is 770 * cleared and line strokes return to being solid. 771 * 772 * We use the following constants here: 773 * FLEXBOX_LINES_PROPERTIES.edge.lineDash, 774 * FLEXBOX_LINES_PROPERTIES.item.lineDash 775 * FLEXBOX_LINES_PROPERTIES.alignItems.lineDash 776 * @param {number} [options.lineWidthMultiplier = 1] 777 * The width of the line. 778 * @param {number} [options.offset = `(displayPixelRatio / 2) % 1`] 779 * The single line width used to obtain a crisp line. 780 * @param {boolean} [options.skipLineAndStroke = false] 781 * Skip the setting of lineWidth and strokeStyle. 782 * @param {boolean} [options.useContainerScrollOffsets = false] 783 * Take the flexbox container's scroll and zoom offsets into account. 784 * This is needed for drawing flex lines and justify content when the 785 * flexbox container itself is display:scroll. 786 */ 787 setupCanvas({ 788 lineDash = null, 789 lineWidthMultiplier = 1, 790 offset = (getDisplayPixelRatio(this.win) / 2) % 1, 791 skipLineAndStroke = false, 792 useContainerScrollOffsets = false, 793 }) { 794 const { devicePixelRatio } = this.win; 795 const lineWidth = getDisplayPixelRatio(this.win); 796 const zoom = getCurrentZoom(this.win); 797 const style = getComputedStyle(this.container); 798 const position = style.position; 799 let offsetX = this._canvasPosition.x; 800 let offsetY = this._canvasPosition.y; 801 802 if (useContainerScrollOffsets) { 803 offsetX += this.container.scrollLeft / zoom; 804 offsetY += this.container.scrollTop / zoom; 805 } 806 807 // If the flexbox container is position:fixed we need to subtract the scroll 808 // positions of all ancestral elements. 809 if (position === "fixed") { 810 const { scrollLeft, scrollTop } = getAbsoluteScrollOffsetsForNode( 811 this.container 812 ); 813 offsetX -= scrollLeft / zoom; 814 offsetY -= scrollTop / zoom; 815 } 816 817 const canvasX = Math.round(offsetX * devicePixelRatio * zoom); 818 const canvasY = Math.round(offsetY * devicePixelRatio * zoom); 819 820 this.ctx.save(); 821 this.ctx.translate(offset - canvasX, offset - canvasY); 822 823 if (lineDash) { 824 this.ctx.setLineDash(lineDash); 825 } 826 827 if (!skipLineAndStroke) { 828 this.ctx.lineWidth = lineWidth * lineWidthMultiplier; 829 this.ctx.strokeStyle = this.color; 830 } 831 } 832 833 _update() { 834 setIgnoreLayoutChanges(true); 835 836 this._winDimensions = getWindowDimensions(this.win); 837 const { width, height } = this._winDimensions; 838 839 // Updates the <canvas> element's position and size. 840 // It also clear the <canvas>'s drawing context. 841 updateCanvasElement( 842 this.canvas, 843 this._canvasPosition, 844 this.win.devicePixelRatio, 845 { 846 zoomWindow: this.win, 847 } 848 ); 849 850 // Update the current matrix used in our canvas' rendering 851 const { currentMatrix, hasNodeTransformations } = getCurrentMatrix( 852 this.container, 853 this.win, 854 { 855 ignoreWritingModeAndTextDirection: true, 856 } 857 ); 858 this.currentMatrix = currentMatrix; 859 this.hasNodeTransformations = hasNodeTransformations; 860 861 if (this.prevColor != this.color) { 862 this.clearCache(); 863 } 864 this.renderFlexContainer(); 865 this.renderFlexLines(); 866 this.renderJustifyContent(); 867 this.renderFlexItems(); 868 this._showFlexbox(); 869 this.prevColor = this.color; 870 871 const root = this.getNode("flexbox-root"); 872 root.style.setProperty("width", `${width}px`); 873 root.style.setProperty("height", `${height}px`); 874 875 setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement); 876 return true; 877 } 878 } 879 880 /** 881 * Returns an object representation of the Flex data object and its array of FlexLine 882 * and FlexItem objects along with the DOMRects of the flex items. 883 * 884 * @param {DOMNode} container 885 * The flex container. 886 * @return {object | null} representation of the Flex data object. 887 */ 888 function getFlexData(container) { 889 const flex = container.getAsFlexContainer(); 890 891 if (!flex) { 892 return null; 893 } 894 895 return { 896 lines: flex.getLines().map(line => { 897 return { 898 crossSize: line.crossSize, 899 crossStart: line.crossStart, 900 firstBaselineOffset: line.firstBaselineOffset, 901 growthState: line.growthState, 902 lastBaselineOffset: line.lastBaselineOffset, 903 items: line.getItems().map(item => { 904 return { 905 crossMaxSize: item.crossMaxSize, 906 crossMinSize: item.crossMinSize, 907 mainBaseSize: item.mainBaseSize, 908 mainDeltaSize: item.mainDeltaSize, 909 mainMaxSize: item.mainMaxSize, 910 mainMinSize: item.mainMinSize, 911 node: item.node, 912 rect: getRectFromFlexItemValues(item, container), 913 }; 914 }), 915 }; 916 }), 917 }; 918 } 919 920 /** 921 * Given a FlexItemValues, return a DOMRect representing the flex item taking 922 * into account its flex container's border and padding. 923 * 924 * @param {FlexItemValues} item 925 * The FlexItemValues for which we need the DOMRect. 926 * @param {DOMNode} 927 * Flex container containing the flex item. 928 * @return {DOMRect} representing the flex item. 929 */ 930 function getRectFromFlexItemValues(item, container) { 931 const rect = item.frameRect; 932 const domRect = new DOMRect(rect.x, rect.y, rect.width, rect.height); 933 const win = container.ownerGlobal; 934 const style = win.getComputedStyle(container); 935 const borderLeftWidth = parseInt(style.borderLeftWidth, 10) || 0; 936 const borderTopWidth = parseInt(style.borderTopWidth, 10) || 0; 937 const paddingLeft = parseInt(style.paddingLeft, 10) || 0; 938 const paddingTop = parseInt(style.paddingTop, 10) || 0; 939 const scrollX = container.scrollLeft || 0; 940 const scrollY = container.scrollTop || 0; 941 942 domRect.x -= paddingLeft + scrollX; 943 domRect.y -= paddingTop + scrollY; 944 945 if (style.overflow === "visible" || style.overflow === "clip") { 946 domRect.x -= borderLeftWidth; 947 domRect.y -= borderTopWidth; 948 } 949 950 return domRect; 951 } 952 953 /** 954 * Returns whether or not the flex data has changed. 955 * 956 * @param {Flex} oldFlexData 957 * The old Flex data object. 958 * @param {Flex} newFlexData 959 * The new Flex data object. 960 * @return {boolean} true if the flex data has changed and false otherwise. 961 */ 962 // eslint-disable-next-line complexity 963 function compareFlexData(oldFlexData, newFlexData) { 964 if (!oldFlexData || !newFlexData) { 965 return true; 966 } 967 968 const oldLines = oldFlexData.lines; 969 const newLines = newFlexData.lines; 970 971 if (oldLines.length !== newLines.length) { 972 return true; 973 } 974 975 for (let i = 0; i < oldLines.length; i++) { 976 const oldLine = oldLines[i]; 977 const newLine = newLines[i]; 978 979 if ( 980 oldLine.crossSize !== newLine.crossSize || 981 oldLine.crossStart !== newLine.crossStart || 982 oldLine.firstBaselineOffset !== newLine.firstBaselineOffset || 983 oldLine.growthState !== newLine.growthState || 984 oldLine.lastBaselineOffset !== newLine.lastBaselineOffset 985 ) { 986 return true; 987 } 988 989 const oldItems = oldLine.items; 990 const newItems = newLine.items; 991 992 if (oldItems.length !== newItems.length) { 993 return true; 994 } 995 996 for (let j = 0; j < oldItems.length; j++) { 997 const oldItem = oldItems[j]; 998 const newItem = newItems[j]; 999 1000 if ( 1001 oldItem.crossMaxSize !== newItem.crossMaxSize || 1002 oldItem.crossMinSize !== newItem.crossMinSize || 1003 oldItem.mainBaseSize !== newItem.mainBaseSize || 1004 oldItem.mainDeltaSize !== newItem.mainDeltaSize || 1005 oldItem.mainMaxSize !== newItem.mainMaxSize || 1006 oldItem.mainMinSize !== newItem.mainMinSize 1007 ) { 1008 return true; 1009 } 1010 1011 const oldItemRect = oldItem.rect; 1012 const newItemRect = newItem.rect; 1013 1014 // We are using DOMRects so we only need to compare x, y, width and 1015 // height (left, top, right and bottom are calculated from these values). 1016 if ( 1017 oldItemRect.x !== newItemRect.x || 1018 oldItemRect.y !== newItemRect.y || 1019 oldItemRect.width !== newItemRect.width || 1020 oldItemRect.height !== newItemRect.height 1021 ) { 1022 return true; 1023 } 1024 } 1025 } 1026 1027 return false; 1028 } 1029 1030 exports.FlexboxHighlighter = FlexboxHighlighter;