1033 lines
30 KiB
JavaScript
1033 lines
30 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const {
|
|
AutoRefreshHighlighter,
|
|
} = require("resource://devtools/server/actors/highlighters/auto-refresh.js");
|
|
const { apply } = require("resource://devtools/shared/layout/dom-matrix-2d.js");
|
|
const {
|
|
CANVAS_SIZE,
|
|
DEFAULT_COLOR,
|
|
clearRect,
|
|
drawLine,
|
|
drawRect,
|
|
getCurrentMatrix,
|
|
updateCanvasElement,
|
|
updateCanvasPosition,
|
|
} = require("resource://devtools/server/actors/highlighters/utils/canvas.js");
|
|
const {
|
|
CanvasFrameAnonymousContentHelper,
|
|
getComputedStyle,
|
|
} = require("resource://devtools/server/actors/highlighters/utils/markup.js");
|
|
const {
|
|
getAbsoluteScrollOffsetsForNode,
|
|
getCurrentZoom,
|
|
getDisplayPixelRatio,
|
|
getUntransformedQuad,
|
|
getWindowDimensions,
|
|
setIgnoreLayoutChanges,
|
|
} = require("resource://devtools/shared/layout/utils.js");
|
|
|
|
const FLEXBOX_LINES_PROPERTIES = {
|
|
edge: {
|
|
lineDash: [5, 3],
|
|
},
|
|
item: {
|
|
lineDash: [0, 0],
|
|
},
|
|
alignItems: {
|
|
lineDash: [0, 0],
|
|
},
|
|
};
|
|
|
|
const FLEXBOX_CONTAINER_PATTERN_LINE_DASH = [5, 3]; // px
|
|
const FLEXBOX_CONTAINER_PATTERN_WIDTH = 14; // px
|
|
const FLEXBOX_CONTAINER_PATTERN_HEIGHT = 14; // px
|
|
const FLEXBOX_JUSTIFY_CONTENT_PATTERN_WIDTH = 7; // px
|
|
const FLEXBOX_JUSTIFY_CONTENT_PATTERN_HEIGHT = 7; // px
|
|
|
|
/**
|
|
* Cached used by `FlexboxHighlighter.getFlexContainerPattern`.
|
|
*/
|
|
const gCachedFlexboxPattern = new Map();
|
|
|
|
const FLEXBOX = "flexbox";
|
|
const JUSTIFY_CONTENT = "justify-content";
|
|
|
|
/**
|
|
* The FlexboxHighlighter is the class that overlays a visual canvas on top of
|
|
* display: [inline-]flex elements.
|
|
*
|
|
* @param {String} options.color
|
|
* The color that should be used to draw the highlighter for this flexbox.
|
|
* Structure:
|
|
* <div class="highlighter-container">
|
|
* <div id="flexbox-root" class="flexbox-root">
|
|
* <canvas id="flexbox-canvas"
|
|
* class="flexbox-canvas"
|
|
* width="4096"
|
|
* height="4096"
|
|
* hidden="true">
|
|
* </canvas>
|
|
* </div>
|
|
* </div>
|
|
*/
|
|
class FlexboxHighlighter extends AutoRefreshHighlighter {
|
|
constructor(highlighterEnv) {
|
|
super(highlighterEnv);
|
|
|
|
this.ID_CLASS_PREFIX = "flexbox-";
|
|
|
|
this.markup = new CanvasFrameAnonymousContentHelper(
|
|
this.highlighterEnv,
|
|
this._buildMarkup.bind(this)
|
|
);
|
|
this.isReady = this.markup.initialize();
|
|
|
|
this.onPageHide = this.onPageHide.bind(this);
|
|
this.onWillNavigate = this.onWillNavigate.bind(this);
|
|
|
|
this.highlighterEnv.on("will-navigate", this.onWillNavigate);
|
|
|
|
const { pageListenerTarget } = highlighterEnv;
|
|
pageListenerTarget.addEventListener("pagehide", this.onPageHide);
|
|
|
|
// Initialize the <canvas> position to the top left corner of the page
|
|
this._canvasPosition = {
|
|
x: 0,
|
|
y: 0,
|
|
};
|
|
|
|
this._ignoreZoom = true;
|
|
|
|
// Calling `updateCanvasPosition` anyway since the highlighter could be initialized
|
|
// on a page that has scrolled already.
|
|
updateCanvasPosition(
|
|
this._canvasPosition,
|
|
this._scroll,
|
|
this.win,
|
|
this._winDimensions
|
|
);
|
|
}
|
|
|
|
_buildMarkup() {
|
|
const container = this.markup.createNode({
|
|
attributes: {
|
|
class: "highlighter-container",
|
|
},
|
|
});
|
|
|
|
const root = this.markup.createNode({
|
|
parent: container,
|
|
attributes: {
|
|
id: "root",
|
|
class: "root",
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX,
|
|
});
|
|
|
|
// We use a <canvas> element because there is an arbitrary number of items and texts
|
|
// to draw which wouldn't be possible with HTML or SVG without having to insert and
|
|
// remove the whole markup on every update.
|
|
this.markup.createNode({
|
|
parent: root,
|
|
nodeType: "canvas",
|
|
attributes: {
|
|
id: "canvas",
|
|
class: "canvas",
|
|
hidden: "true",
|
|
width: CANVAS_SIZE,
|
|
height: CANVAS_SIZE,
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX,
|
|
});
|
|
|
|
return container;
|
|
}
|
|
|
|
clearCache() {
|
|
gCachedFlexboxPattern.clear();
|
|
}
|
|
|
|
destroy() {
|
|
const { highlighterEnv } = this;
|
|
highlighterEnv.off("will-navigate", this.onWillNavigate);
|
|
|
|
const { pageListenerTarget } = highlighterEnv;
|
|
|
|
if (pageListenerTarget) {
|
|
pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
|
|
}
|
|
|
|
this.markup.destroy();
|
|
|
|
// Clear the pattern cache to avoid dead object exceptions (Bug 1342051).
|
|
this.clearCache();
|
|
|
|
this.axes = null;
|
|
this.crossAxisDirection = null;
|
|
this.flexData = null;
|
|
this.mainAxisDirection = null;
|
|
this.transform = null;
|
|
|
|
AutoRefreshHighlighter.prototype.destroy.call(this);
|
|
}
|
|
|
|
/**
|
|
* Draw the justify content for a given flex item (left, top, right, bottom) position.
|
|
*/
|
|
drawJustifyContent(left, top, right, bottom) {
|
|
const { devicePixelRatio } = this.win;
|
|
this.ctx.fillStyle = this.getJustifyContentPattern(devicePixelRatio);
|
|
drawRect(this.ctx, left, top, right, bottom, this.currentMatrix);
|
|
this.ctx.fill();
|
|
}
|
|
|
|
get canvas() {
|
|
return this.getElement("canvas");
|
|
}
|
|
|
|
get color() {
|
|
return this.options.color || DEFAULT_COLOR;
|
|
}
|
|
|
|
get container() {
|
|
return this.currentNode;
|
|
}
|
|
|
|
get ctx() {
|
|
return this.canvas.getCanvasContext("2d");
|
|
}
|
|
|
|
getElement(id) {
|
|
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
|
|
}
|
|
|
|
/**
|
|
* Gets the flexbox container pattern used to render the container regions.
|
|
*
|
|
* @param {Number} devicePixelRatio
|
|
* The device pixel ratio we want the pattern for.
|
|
* @return {CanvasPattern} flex container pattern.
|
|
*/
|
|
getFlexContainerPattern(devicePixelRatio) {
|
|
let flexboxPatternMap = null;
|
|
|
|
if (gCachedFlexboxPattern.has(devicePixelRatio)) {
|
|
flexboxPatternMap = gCachedFlexboxPattern.get(devicePixelRatio);
|
|
} else {
|
|
flexboxPatternMap = new Map();
|
|
}
|
|
|
|
if (gCachedFlexboxPattern.has(FLEXBOX)) {
|
|
return gCachedFlexboxPattern.get(FLEXBOX);
|
|
}
|
|
|
|
// Create the diagonal lines pattern for the rendering the flexbox gaps.
|
|
const canvas = this.markup.createNode({ nodeType: "canvas" });
|
|
const width = (canvas.width =
|
|
FLEXBOX_CONTAINER_PATTERN_WIDTH * devicePixelRatio);
|
|
const height = (canvas.height =
|
|
FLEXBOX_CONTAINER_PATTERN_HEIGHT * devicePixelRatio);
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.save();
|
|
ctx.setLineDash(FLEXBOX_CONTAINER_PATTERN_LINE_DASH);
|
|
ctx.beginPath();
|
|
ctx.translate(0.5, 0.5);
|
|
|
|
ctx.moveTo(0, 0);
|
|
ctx.lineTo(width, height);
|
|
|
|
ctx.strokeStyle = this.color;
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
const pattern = ctx.createPattern(canvas, "repeat");
|
|
flexboxPatternMap.set(FLEXBOX, pattern);
|
|
gCachedFlexboxPattern.set(devicePixelRatio, flexboxPatternMap);
|
|
|
|
return pattern;
|
|
}
|
|
|
|
/**
|
|
* Gets the flexbox justify content pattern used to render the justify content regions.
|
|
*
|
|
* @param {Number} devicePixelRatio
|
|
* The device pixel ratio we want the pattern for.
|
|
* @return {CanvasPattern} flex justify content pattern.
|
|
*/
|
|
getJustifyContentPattern(devicePixelRatio) {
|
|
let flexboxPatternMap = null;
|
|
|
|
if (gCachedFlexboxPattern.has(devicePixelRatio)) {
|
|
flexboxPatternMap = gCachedFlexboxPattern.get(devicePixelRatio);
|
|
} else {
|
|
flexboxPatternMap = new Map();
|
|
}
|
|
|
|
if (flexboxPatternMap.has(JUSTIFY_CONTENT)) {
|
|
return flexboxPatternMap.get(JUSTIFY_CONTENT);
|
|
}
|
|
|
|
// Create the inversed diagonal lines pattern
|
|
// for the rendering the justify content gaps.
|
|
const canvas = this.markup.createNode({ nodeType: "canvas" });
|
|
const zoom = getCurrentZoom(this.win);
|
|
const width = (canvas.width =
|
|
FLEXBOX_JUSTIFY_CONTENT_PATTERN_WIDTH * devicePixelRatio * zoom);
|
|
const height = (canvas.height =
|
|
FLEXBOX_JUSTIFY_CONTENT_PATTERN_HEIGHT * devicePixelRatio * zoom);
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.save();
|
|
ctx.setLineDash(FLEXBOX_CONTAINER_PATTERN_LINE_DASH);
|
|
ctx.beginPath();
|
|
ctx.translate(0.5, 0.5);
|
|
|
|
ctx.moveTo(0, height);
|
|
ctx.lineTo(width, 0);
|
|
|
|
ctx.strokeStyle = this.color;
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
const pattern = ctx.createPattern(canvas, "repeat");
|
|
flexboxPatternMap.set(JUSTIFY_CONTENT, pattern);
|
|
gCachedFlexboxPattern.set(devicePixelRatio, flexboxPatternMap);
|
|
|
|
return pattern;
|
|
}
|
|
|
|
/**
|
|
* The AutoRefreshHighlighter's _hasMoved method returns true only if the
|
|
* element's quads have changed. Override it so it also returns true if the
|
|
* flex container and its flex items have changed.
|
|
*/
|
|
_hasMoved() {
|
|
const hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
|
|
|
|
if (!this.computedStyle) {
|
|
this.computedStyle = getComputedStyle(this.container);
|
|
}
|
|
|
|
const flex = this.container.getAsFlexContainer();
|
|
|
|
const oldCrossAxisDirection = this.crossAxisDirection;
|
|
this.crossAxisDirection = flex ? flex.crossAxisDirection : null;
|
|
const newCrossAxisDirection = this.crossAxisDirection;
|
|
|
|
const oldMainAxisDirection = this.mainAxisDirection;
|
|
this.mainAxisDirection = flex ? flex.mainAxisDirection : null;
|
|
const newMainAxisDirection = this.mainAxisDirection;
|
|
|
|
// Concatenate the axes to simplify conditionals.
|
|
this.axes = `${this.mainAxisDirection} ${this.crossAxisDirection}`;
|
|
|
|
const oldFlexData = this.flexData;
|
|
this.flexData = getFlexData(this.container);
|
|
const hasFlexDataChanged = compareFlexData(oldFlexData, this.flexData);
|
|
|
|
const oldAlignItems = this.alignItemsValue;
|
|
this.alignItemsValue = this.computedStyle.alignItems;
|
|
const newAlignItems = this.alignItemsValue;
|
|
|
|
const oldFlexDirection = this.flexDirection;
|
|
this.flexDirection = this.computedStyle.flexDirection;
|
|
const newFlexDirection = this.flexDirection;
|
|
|
|
const oldFlexWrap = this.flexWrap;
|
|
this.flexWrap = this.computedStyle.flexWrap;
|
|
const newFlexWrap = this.flexWrap;
|
|
|
|
const oldJustifyContent = this.justifyContentValue;
|
|
this.justifyContentValue = this.computedStyle.justifyContent;
|
|
const newJustifyContent = this.justifyContentValue;
|
|
|
|
const oldTransform = this.transformValue;
|
|
this.transformValue = this.computedStyle.transform;
|
|
const newTransform = this.transformValue;
|
|
|
|
return (
|
|
hasMoved ||
|
|
hasFlexDataChanged ||
|
|
oldAlignItems !== newAlignItems ||
|
|
oldFlexDirection !== newFlexDirection ||
|
|
oldFlexWrap !== newFlexWrap ||
|
|
oldJustifyContent !== newJustifyContent ||
|
|
oldCrossAxisDirection !== newCrossAxisDirection ||
|
|
oldMainAxisDirection !== newMainAxisDirection ||
|
|
oldTransform !== newTransform
|
|
);
|
|
}
|
|
|
|
_hide() {
|
|
this.alignItemsValue = null;
|
|
this.computedStyle = null;
|
|
this.flexData = null;
|
|
this.flexDirection = null;
|
|
this.flexWrap = null;
|
|
this.justifyContentValue = null;
|
|
|
|
setIgnoreLayoutChanges(true);
|
|
this._hideFlexbox();
|
|
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
|
|
}
|
|
|
|
_hideFlexbox() {
|
|
this.getElement("canvas").setAttribute("hidden", "true");
|
|
}
|
|
|
|
/**
|
|
* The <canvas>'s position needs to be updated if the page scrolls too much, in order
|
|
* to give the illusion that it always covers the viewport.
|
|
*/
|
|
_scrollUpdate() {
|
|
const hasUpdated = updateCanvasPosition(
|
|
this._canvasPosition,
|
|
this._scroll,
|
|
this.win,
|
|
this._winDimensions
|
|
);
|
|
|
|
if (hasUpdated) {
|
|
this._update();
|
|
}
|
|
}
|
|
|
|
_show() {
|
|
this._hide();
|
|
return this._update();
|
|
}
|
|
|
|
_showFlexbox() {
|
|
this.getElement("canvas").removeAttribute("hidden");
|
|
}
|
|
|
|
/**
|
|
* If a page hide event is triggered for current window's highlighter, hide the
|
|
* highlighter.
|
|
*/
|
|
onPageHide({ target }) {
|
|
if (target.defaultView === this.win) {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when the page will-navigate. Used to hide the flexbox highlighter and clear
|
|
* the cached gap patterns and avoid using DeadWrapper obejcts as gap patterns the
|
|
* next time.
|
|
*/
|
|
onWillNavigate({ isTopLevel }) {
|
|
this.clearCache();
|
|
|
|
if (isTopLevel) {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
renderFlexContainer() {
|
|
if (!this.currentQuads.content || !this.currentQuads.content[0]) {
|
|
return;
|
|
}
|
|
|
|
const { devicePixelRatio } = this.win;
|
|
const containerQuad = getUntransformedQuad(this.container, "content");
|
|
const { width, height } = containerQuad.getBounds();
|
|
|
|
this.setupCanvas({
|
|
lineDash: FLEXBOX_LINES_PROPERTIES.alignItems.lineDash,
|
|
lineWidthMultiplier: 2,
|
|
});
|
|
|
|
this.ctx.fillStyle = this.getFlexContainerPattern(devicePixelRatio);
|
|
|
|
drawRect(this.ctx, 0, 0, width, height, this.currentMatrix);
|
|
|
|
// Find current angle of outer flex element by measuring the angle of two arbitrary
|
|
// points, then rotate canvas, so the hash pattern stays 45deg to the boundary.
|
|
const p1 = apply(this.currentMatrix, [0, 0]);
|
|
const p2 = apply(this.currentMatrix, [1, 0]);
|
|
const angleRad = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]);
|
|
this.ctx.rotate(angleRad);
|
|
|
|
this.ctx.fill();
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
}
|
|
|
|
renderFlexItems() {
|
|
if (
|
|
!this.flexData ||
|
|
!this.currentQuads.content ||
|
|
!this.currentQuads.content[0]
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.setupCanvas({
|
|
lineDash: FLEXBOX_LINES_PROPERTIES.item.lineDash,
|
|
});
|
|
|
|
for (const flexLine of this.flexData.lines) {
|
|
for (const flexItem of flexLine.items) {
|
|
const { left, top, right, bottom } = flexItem.rect;
|
|
|
|
clearRect(this.ctx, left, top, right, bottom, this.currentMatrix);
|
|
drawRect(this.ctx, left, top, right, bottom, this.currentMatrix);
|
|
this.ctx.stroke();
|
|
}
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
renderFlexLines() {
|
|
if (
|
|
!this.flexData ||
|
|
!this.currentQuads.content ||
|
|
!this.currentQuads.content[0]
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const lineWidth = getDisplayPixelRatio(this.win);
|
|
const options = { matrix: this.currentMatrix };
|
|
const { width: containerWidth, height: containerHeight } =
|
|
getUntransformedQuad(this.container, "content").getBounds();
|
|
|
|
this.setupCanvas({
|
|
useContainerScrollOffsets: true,
|
|
});
|
|
|
|
for (const flexLine of this.flexData.lines) {
|
|
const { crossStart, crossSize } = flexLine;
|
|
|
|
switch (this.axes) {
|
|
case "horizontal-lr vertical-tb":
|
|
case "horizontal-lr vertical-bt":
|
|
case "horizontal-rl vertical-tb":
|
|
case "horizontal-rl vertical-bt":
|
|
clearRect(
|
|
this.ctx,
|
|
0,
|
|
crossStart,
|
|
containerWidth,
|
|
crossStart + crossSize,
|
|
this.currentMatrix
|
|
);
|
|
|
|
// Avoid drawing the start flex line when they overlap with the flex container.
|
|
if (crossStart != 0) {
|
|
drawLine(
|
|
this.ctx,
|
|
0,
|
|
crossStart,
|
|
containerWidth,
|
|
crossStart,
|
|
options
|
|
);
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
// Avoid drawing the end flex line when they overlap with the flex container.
|
|
if (crossStart + crossSize < containerHeight - lineWidth * 2) {
|
|
drawLine(
|
|
this.ctx,
|
|
0,
|
|
crossStart + crossSize,
|
|
containerWidth,
|
|
crossStart + crossSize,
|
|
options
|
|
);
|
|
this.ctx.stroke();
|
|
}
|
|
break;
|
|
case "vertical-tb horizontal-lr":
|
|
case "vertical-bt horizontal-rl":
|
|
clearRect(
|
|
this.ctx,
|
|
crossStart,
|
|
0,
|
|
crossStart + crossSize,
|
|
containerHeight,
|
|
this.currentMatrix
|
|
);
|
|
|
|
// Avoid drawing the start flex line when they overlap with the flex container.
|
|
if (crossStart != 0) {
|
|
drawLine(
|
|
this.ctx,
|
|
crossStart,
|
|
0,
|
|
crossStart,
|
|
containerHeight,
|
|
options
|
|
);
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
// Avoid drawing the end flex line when they overlap with the flex container.
|
|
if (crossStart + crossSize < containerWidth - lineWidth * 2) {
|
|
drawLine(
|
|
this.ctx,
|
|
crossStart + crossSize,
|
|
0,
|
|
crossStart + crossSize,
|
|
containerHeight,
|
|
options
|
|
);
|
|
this.ctx.stroke();
|
|
}
|
|
break;
|
|
case "vertical-bt horizontal-lr":
|
|
case "vertical-tb horizontal-rl":
|
|
clearRect(
|
|
this.ctx,
|
|
containerWidth - crossStart,
|
|
0,
|
|
containerWidth - crossStart - crossSize,
|
|
containerHeight,
|
|
this.currentMatrix
|
|
);
|
|
|
|
// Avoid drawing the start flex line when they overlap with the flex container.
|
|
if (crossStart != 0) {
|
|
drawLine(
|
|
this.ctx,
|
|
containerWidth - crossStart,
|
|
0,
|
|
containerWidth - crossStart,
|
|
containerHeight,
|
|
options
|
|
);
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
// Avoid drawing the end flex line when they overlap with the flex container.
|
|
if (crossStart + crossSize < containerWidth - lineWidth * 2) {
|
|
drawLine(
|
|
this.ctx,
|
|
containerWidth - crossStart - crossSize,
|
|
0,
|
|
containerWidth - crossStart - crossSize,
|
|
containerHeight,
|
|
options
|
|
);
|
|
this.ctx.stroke();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* Clear the whole alignment container along the main axis for each flex item.
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
renderJustifyContent() {
|
|
if (
|
|
!this.flexData ||
|
|
!this.currentQuads.content ||
|
|
!this.currentQuads.content[0]
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const { width: containerWidth, height: containerHeight } =
|
|
getUntransformedQuad(this.container, "content").getBounds();
|
|
|
|
this.setupCanvas({
|
|
lineDash: FLEXBOX_LINES_PROPERTIES.alignItems.lineDash,
|
|
offset: (getDisplayPixelRatio(this.win) / 2) % 1,
|
|
skipLineAndStroke: true,
|
|
useContainerScrollOffsets: true,
|
|
});
|
|
|
|
for (const flexLine of this.flexData.lines) {
|
|
const { crossStart, crossSize } = flexLine;
|
|
let mainStart = 0;
|
|
|
|
// In these two situations mainStart goes from right to left so set it's
|
|
// value as appropriate.
|
|
if (
|
|
this.axes === "horizontal-lr vertical-bt" ||
|
|
this.axes === "horizontal-rl vertical-tb"
|
|
) {
|
|
mainStart = containerWidth;
|
|
}
|
|
|
|
for (const flexItem of flexLine.items) {
|
|
const { left, top, right, bottom } = flexItem.rect;
|
|
|
|
switch (this.axes) {
|
|
case "horizontal-lr vertical-tb":
|
|
case "horizontal-rl vertical-bt":
|
|
this.drawJustifyContent(
|
|
mainStart,
|
|
crossStart,
|
|
left,
|
|
crossStart + crossSize
|
|
);
|
|
mainStart = right;
|
|
break;
|
|
case "horizontal-lr vertical-bt":
|
|
case "horizontal-rl vertical-tb":
|
|
this.drawJustifyContent(
|
|
right,
|
|
crossStart,
|
|
mainStart,
|
|
crossStart + crossSize
|
|
);
|
|
mainStart = left;
|
|
break;
|
|
case "vertical-tb horizontal-lr":
|
|
case "vertical-bt horizontal-rl":
|
|
this.drawJustifyContent(
|
|
crossStart,
|
|
mainStart,
|
|
crossStart + crossSize,
|
|
top
|
|
);
|
|
mainStart = bottom;
|
|
break;
|
|
case "vertical-bt horizontal-lr":
|
|
case "vertical-tb horizontal-rl":
|
|
this.drawJustifyContent(
|
|
containerWidth - crossStart - crossSize,
|
|
mainStart,
|
|
containerWidth - crossStart,
|
|
top
|
|
);
|
|
mainStart = bottom;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Draw the last justify-content area after the last flex item.
|
|
switch (this.axes) {
|
|
case "horizontal-lr vertical-tb":
|
|
case "horizontal-rl vertical-bt":
|
|
this.drawJustifyContent(
|
|
mainStart,
|
|
crossStart,
|
|
containerWidth,
|
|
crossStart + crossSize
|
|
);
|
|
break;
|
|
case "horizontal-lr vertical-bt":
|
|
case "horizontal-rl vertical-tb":
|
|
this.drawJustifyContent(
|
|
0,
|
|
crossStart,
|
|
mainStart,
|
|
crossStart + crossSize
|
|
);
|
|
break;
|
|
case "vertical-tb horizontal-lr":
|
|
case "vertical-bt horizontal-rl":
|
|
this.drawJustifyContent(
|
|
crossStart,
|
|
mainStart,
|
|
crossStart + crossSize,
|
|
containerHeight
|
|
);
|
|
break;
|
|
case "vertical-bt horizontal-lr":
|
|
case "vertical-tb horizontal-rl":
|
|
this.drawJustifyContent(
|
|
containerWidth - crossStart - crossSize,
|
|
mainStart,
|
|
containerWidth - crossStart,
|
|
containerHeight
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* Set up the canvas with the given options prior to drawing.
|
|
*
|
|
* @param {String} [options.lineDash = null]
|
|
* An Array of numbers that specify distances to alternately draw a
|
|
* line and a gap (in coordinate space units). If the number of
|
|
* elements in the array is odd, the elements of the array get copied
|
|
* and concatenated. For example, [5, 15, 25] will become
|
|
* [5, 15, 25, 5, 15, 25]. If the array is empty, the line dash list is
|
|
* cleared and line strokes return to being solid.
|
|
*
|
|
* We use the following constants here:
|
|
* FLEXBOX_LINES_PROPERTIES.edge.lineDash,
|
|
* FLEXBOX_LINES_PROPERTIES.item.lineDash
|
|
* FLEXBOX_LINES_PROPERTIES.alignItems.lineDash
|
|
* @param {Number} [options.lineWidthMultiplier = 1]
|
|
* The width of the line.
|
|
* @param {Number} [options.offset = `(displayPixelRatio / 2) % 1`]
|
|
* The single line width used to obtain a crisp line.
|
|
* @param {Boolean} [options.skipLineAndStroke = false]
|
|
* Skip the setting of lineWidth and strokeStyle.
|
|
* @param {Boolean} [options.useContainerScrollOffsets = false]
|
|
* Take the flexbox container's scroll and zoom offsets into account.
|
|
* This is needed for drawing flex lines and justify content when the
|
|
* flexbox container itself is display:scroll.
|
|
*/
|
|
setupCanvas({
|
|
lineDash = null,
|
|
lineWidthMultiplier = 1,
|
|
offset = (getDisplayPixelRatio(this.win) / 2) % 1,
|
|
skipLineAndStroke = false,
|
|
useContainerScrollOffsets = false,
|
|
}) {
|
|
const { devicePixelRatio } = this.win;
|
|
const lineWidth = getDisplayPixelRatio(this.win);
|
|
const zoom = getCurrentZoom(this.win);
|
|
const style = getComputedStyle(this.container);
|
|
const position = style.position;
|
|
let offsetX = this._canvasPosition.x;
|
|
let offsetY = this._canvasPosition.y;
|
|
|
|
if (useContainerScrollOffsets) {
|
|
offsetX += this.container.scrollLeft / zoom;
|
|
offsetY += this.container.scrollTop / zoom;
|
|
}
|
|
|
|
// If the flexbox container is position:fixed we need to subtract the scroll
|
|
// positions of all ancestral elements.
|
|
if (position === "fixed") {
|
|
const { scrollLeft, scrollTop } = getAbsoluteScrollOffsetsForNode(
|
|
this.container
|
|
);
|
|
offsetX -= scrollLeft / zoom;
|
|
offsetY -= scrollTop / zoom;
|
|
}
|
|
|
|
const canvasX = Math.round(offsetX * devicePixelRatio * zoom);
|
|
const canvasY = Math.round(offsetY * devicePixelRatio * zoom);
|
|
|
|
this.ctx.save();
|
|
this.ctx.translate(offset - canvasX, offset - canvasY);
|
|
|
|
if (lineDash) {
|
|
this.ctx.setLineDash(lineDash);
|
|
}
|
|
|
|
if (!skipLineAndStroke) {
|
|
this.ctx.lineWidth = lineWidth * lineWidthMultiplier;
|
|
this.ctx.strokeStyle = this.color;
|
|
}
|
|
}
|
|
|
|
_update() {
|
|
setIgnoreLayoutChanges(true);
|
|
|
|
const root = this.getElement("root");
|
|
|
|
// Hide the root element and force the reflow in order to get the proper window's
|
|
// dimensions without increasing them.
|
|
root.setAttribute("style", "display: none");
|
|
this.win.document.documentElement.offsetWidth;
|
|
this._winDimensions = getWindowDimensions(this.win);
|
|
const { width, height } = this._winDimensions;
|
|
|
|
// Updates the <canvas> element's position and size.
|
|
// It also clear the <canvas>'s drawing context.
|
|
updateCanvasElement(
|
|
this.canvas,
|
|
this._canvasPosition,
|
|
this.win.devicePixelRatio,
|
|
{
|
|
zoomWindow: this.win,
|
|
}
|
|
);
|
|
|
|
// Update the current matrix used in our canvas' rendering
|
|
const { currentMatrix, hasNodeTransformations } = getCurrentMatrix(
|
|
this.container,
|
|
this.win,
|
|
{
|
|
ignoreWritingModeAndTextDirection: true,
|
|
}
|
|
);
|
|
this.currentMatrix = currentMatrix;
|
|
this.hasNodeTransformations = hasNodeTransformations;
|
|
|
|
if (this.prevColor != this.color) {
|
|
this.clearCache();
|
|
}
|
|
this.renderFlexContainer();
|
|
this.renderFlexLines();
|
|
this.renderJustifyContent();
|
|
this.renderFlexItems();
|
|
this._showFlexbox();
|
|
this.prevColor = this.color;
|
|
|
|
root.setAttribute(
|
|
"style",
|
|
`position: absolute; width: ${width}px; height: ${height}px; overflow: hidden`
|
|
);
|
|
|
|
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an object representation of the Flex data object and its array of FlexLine
|
|
* and FlexItem objects along with the DOMRects of the flex items.
|
|
*
|
|
* @param {DOMNode} container
|
|
* The flex container.
|
|
* @return {Object|null} representation of the Flex data object.
|
|
*/
|
|
function getFlexData(container) {
|
|
const flex = container.getAsFlexContainer();
|
|
|
|
if (!flex) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
lines: flex.getLines().map(line => {
|
|
return {
|
|
crossSize: line.crossSize,
|
|
crossStart: line.crossStart,
|
|
firstBaselineOffset: line.firstBaselineOffset,
|
|
growthState: line.growthState,
|
|
lastBaselineOffset: line.lastBaselineOffset,
|
|
items: line.getItems().map(item => {
|
|
return {
|
|
crossMaxSize: item.crossMaxSize,
|
|
crossMinSize: item.crossMinSize,
|
|
mainBaseSize: item.mainBaseSize,
|
|
mainDeltaSize: item.mainDeltaSize,
|
|
mainMaxSize: item.mainMaxSize,
|
|
mainMinSize: item.mainMinSize,
|
|
node: item.node,
|
|
rect: getRectFromFlexItemValues(item, container),
|
|
};
|
|
}),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Given a FlexItemValues, return a DOMRect representing the flex item taking
|
|
* into account its flex container's border and padding.
|
|
*
|
|
* @param {FlexItemValues} item
|
|
* The FlexItemValues for which we need the DOMRect.
|
|
* @param {DOMNode}
|
|
* Flex container containing the flex item.
|
|
* @return {DOMRect} representing the flex item.
|
|
*/
|
|
function getRectFromFlexItemValues(item, container) {
|
|
const rect = item.frameRect;
|
|
const domRect = new DOMRect(rect.x, rect.y, rect.width, rect.height);
|
|
const win = container.ownerGlobal;
|
|
const style = win.getComputedStyle(container);
|
|
const borderLeftWidth = parseInt(style.borderLeftWidth, 10) || 0;
|
|
const borderTopWidth = parseInt(style.borderTopWidth, 10) || 0;
|
|
const paddingLeft = parseInt(style.paddingLeft, 10) || 0;
|
|
const paddingTop = parseInt(style.paddingTop, 10) || 0;
|
|
const scrollX = container.scrollLeft || 0;
|
|
const scrollY = container.scrollTop || 0;
|
|
|
|
domRect.x -= paddingLeft + scrollX;
|
|
domRect.y -= paddingTop + scrollY;
|
|
|
|
if (style.overflow === "visible" || style.overflow === "clip") {
|
|
domRect.x -= borderLeftWidth;
|
|
domRect.y -= borderTopWidth;
|
|
}
|
|
|
|
return domRect;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the flex data has changed.
|
|
*
|
|
* @param {Flex} oldFlexData
|
|
* The old Flex data object.
|
|
* @param {Flex} newFlexData
|
|
* The new Flex data object.
|
|
* @return {Boolean} true if the flex data has changed and false otherwise.
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
function compareFlexData(oldFlexData, newFlexData) {
|
|
if (!oldFlexData || !newFlexData) {
|
|
return true;
|
|
}
|
|
|
|
const oldLines = oldFlexData.lines;
|
|
const newLines = newFlexData.lines;
|
|
|
|
if (oldLines.length !== newLines.length) {
|
|
return true;
|
|
}
|
|
|
|
for (let i = 0; i < oldLines.length; i++) {
|
|
const oldLine = oldLines[i];
|
|
const newLine = newLines[i];
|
|
|
|
if (
|
|
oldLine.crossSize !== newLine.crossSize ||
|
|
oldLine.crossStart !== newLine.crossStart ||
|
|
oldLine.firstBaselineOffset !== newLine.firstBaselineOffset ||
|
|
oldLine.growthState !== newLine.growthState ||
|
|
oldLine.lastBaselineOffset !== newLine.lastBaselineOffset
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const oldItems = oldLine.items;
|
|
const newItems = newLine.items;
|
|
|
|
if (oldItems.length !== newItems.length) {
|
|
return true;
|
|
}
|
|
|
|
for (let j = 0; j < oldItems.length; j++) {
|
|
const oldItem = oldItems[j];
|
|
const newItem = newItems[j];
|
|
|
|
if (
|
|
oldItem.crossMaxSize !== newItem.crossMaxSize ||
|
|
oldItem.crossMinSize !== newItem.crossMinSize ||
|
|
oldItem.mainBaseSize !== newItem.mainBaseSize ||
|
|
oldItem.mainDeltaSize !== newItem.mainDeltaSize ||
|
|
oldItem.mainMaxSize !== newItem.mainMaxSize ||
|
|
oldItem.mainMinSize !== newItem.mainMinSize
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const oldItemRect = oldItem.rect;
|
|
const newItemRect = newItem.rect;
|
|
|
|
// We are using DOMRects so we only need to compare x, y, width and
|
|
// height (left, top, right and bottom are calculated from these values).
|
|
if (
|
|
oldItemRect.x !== newItemRect.x ||
|
|
oldItemRect.y !== newItemRect.y ||
|
|
oldItemRect.width !== newItemRect.width ||
|
|
oldItemRect.height !== newItemRect.height
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
exports.FlexboxHighlighter = FlexboxHighlighter;
|