/* 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 {
CANVAS_SIZE,
DEFAULT_COLOR,
drawBubbleRect,
drawLine,
drawRect,
drawRoundedRect,
getBoundsFromPoints,
getCurrentMatrix,
getPathDescriptionFromPoints,
getPointsFromDiagonal,
updateCanvasElement,
updateCanvasPosition,
} = require("resource://devtools/server/actors/highlighters/utils/canvas.js");
const {
CanvasFrameAnonymousContentHelper,
getComputedStyle,
moveInfobar,
} = require("resource://devtools/server/actors/highlighters/utils/markup.js");
const { apply } = require("resource://devtools/shared/layout/dom-matrix-2d.js");
const {
getCurrentZoom,
getDisplayPixelRatio,
getWindowDimensions,
setIgnoreLayoutChanges,
} = require("resource://devtools/shared/layout/utils.js");
loader.lazyGetter(this, "HighlightersBundle", () => {
return new Localization(["devtools/shared/highlighters.ftl"], true);
});
const COLUMNS = "cols";
const ROWS = "rows";
const GRID_FONT_SIZE = 10;
const GRID_FONT_FAMILY = "sans-serif";
const GRID_AREA_NAME_FONT_SIZE = "20";
const GRID_LINES_PROPERTIES = {
edge: {
lineDash: [0, 0],
alpha: 1,
},
explicit: {
lineDash: [5, 3],
alpha: 0.75,
},
implicit: {
lineDash: [2, 2],
alpha: 0.5,
},
areaEdge: {
lineDash: [0, 0],
alpha: 1,
lineWidth: 3,
},
};
const GRID_GAP_PATTERN_WIDTH = 14; // px
const GRID_GAP_PATTERN_HEIGHT = 14; // px
const GRID_GAP_PATTERN_LINE_DASH = [5, 3]; // px
const GRID_GAP_ALPHA = 0.5;
// This is the minimum distance a line can be to the edge of the document under which we
// push the line number arrow to be inside the grid. This offset is enough to fit the
// entire arrow + a stacked arrow behind it.
const OFFSET_FROM_EDGE = 32;
// This is how much inside the grid we push the arrow. This a factor of the arrow size.
// The goal here is for a row and a column arrow that have both been pushed inside the
// grid, in a corner, not to overlap.
const FLIP_ARROW_INSIDE_FACTOR = 2.5;
/**
* Given an `edge` of a box, return the name of the edge one move to the right.
*/
function rotateEdgeRight(edge) {
switch (edge) {
case "top":
return "right";
case "right":
return "bottom";
case "bottom":
return "left";
case "left":
return "top";
default:
return edge;
}
}
/**
* Given an `edge` of a box, return the name of the edge one move to the left.
*/
function rotateEdgeLeft(edge) {
switch (edge) {
case "top":
return "left";
case "right":
return "top";
case "bottom":
return "right";
case "left":
return "bottom";
default:
return edge;
}
}
/**
* Given an `edge` of a box, return the name of the opposite edge.
*/
function reflectEdge(edge) {
switch (edge) {
case "top":
return "bottom";
case "right":
return "left";
case "bottom":
return "top";
case "left":
return "right";
default:
return edge;
}
}
/**
* Cached used by `CssGridHighlighter.getGridGapPattern`.
*/
const gCachedGridPattern = new Map();
/**
* The CssGridHighlighter is the class that overlays a visual grid on top of
* display:[inline-]grid elements.
*
* Usage example:
* let h = new CssGridHighlighter(env);
* h.show(node, options);
* h.hide();
* h.destroy();
*
* @param {String} options.color
* The color that should be used to draw the highlighter for this grid.
* @param {Number} options.globalAlpha
* The alpha (transparency) value that should be used to draw the highlighter for
* this grid.
* @param {Boolean} options.showAllGridAreas
* Shows all the grid area highlights for the current grid if isShown is
* true.
* @param {String} options.showGridArea
* Shows the grid area highlight for the given area name.
* @param {Boolean} options.showGridAreasOverlay
* Displays an overlay of all the grid areas for the current grid
* container if isShown is true.
* @param {Object} options.showGridCell
* An object containing the grid fragment index, row and column numbers
* to the corresponding grid cell to highlight for the current grid.
* @param {Number} options.showGridCell.gridFragmentIndex
* Index of the grid fragment to render the grid cell highlight.
* @param {Number} options.showGridCell.rowNumber
* Row number of the grid cell to highlight.
* @param {Number} options.showGridCell.columnNumber
* Column number of the grid cell to highlight.
* @param {Object} options.showGridLineNames
* An object containing the grid fragment index and line number to the
* corresponding grid line to highlight for the current grid.
* @param {Number} options.showGridLineNames.gridFragmentIndex
* Index of the grid fragment to render the grid line highlight.
* @param {Number} options.showGridLineNames.lineNumber
* Line number of the grid line to highlight.
* @param {String} options.showGridLineNames.type
* The dimension type of the grid line.
* @param {Boolean} options.showGridLineNumbers
* Displays the grid line numbers on the grid lines if isShown is true.
* @param {Boolean} options.showInfiniteLines
* Displays an infinite line to represent the grid lines if isShown is
* true.
* @param {Number} options.zIndex
* The z-index to decide the displaying order.
*
* Structure:
*
*
*
*
*
*
*
*
*
*
*
* Grid Area Name
* Grid Area Dimensions>
*
*
*
*
*
*
* Grid Cell Position
* Grid Cell Dimensions>
*
*
*
*
*
* Grid Line Number
* Grid Line Names>
*
*
*
*
*/
class CssGridHighlighter extends AutoRefreshHighlighter {
constructor(highlighterEnv) {
super(highlighterEnv);
this.ID_CLASS_PREFIX = "css-grid-";
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 position to the top left corner of the page.
this._canvasPosition = {
x: 0,
y: 0,
};
// 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 element so that we can draw an arbitrary number of lines
// 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,
});
// Build the SVG element.
const svg = this.markup.createSVGNode({
nodeType: "svg",
parent: root,
attributes: {
id: "elements",
width: "100%",
height: "100%",
hidden: "true",
},
prefix: this.ID_CLASS_PREFIX,
});
const regions = this.markup.createSVGNode({
nodeType: "g",
parent: svg,
attributes: {
class: "regions",
},
prefix: this.ID_CLASS_PREFIX,
});
this.markup.createSVGNode({
nodeType: "path",
parent: regions,
attributes: {
class: "areas",
id: "areas",
},
prefix: this.ID_CLASS_PREFIX,
});
this.markup.createSVGNode({
nodeType: "path",
parent: regions,
attributes: {
class: "cells",
id: "cells",
},
prefix: this.ID_CLASS_PREFIX,
});
// Build the grid area infobar markup.
const areaInfobarContainer = this.markup.createNode({
parent: container,
attributes: {
class: "area-infobar-container",
id: "area-infobar-container",
position: "top",
hidden: "true",
},
prefix: this.ID_CLASS_PREFIX,
});
const areaInfobar = this.markup.createNode({
parent: areaInfobarContainer,
attributes: {
class: "infobar",
},
prefix: this.ID_CLASS_PREFIX,
});
const areaTextbox = this.markup.createNode({
parent: areaInfobar,
attributes: {
class: "infobar-text",
},
prefix: this.ID_CLASS_PREFIX,
});
this.markup.createNode({
nodeType: "span",
parent: areaTextbox,
attributes: {
class: "area-infobar-name",
id: "area-infobar-name",
},
prefix: this.ID_CLASS_PREFIX,
});
this.markup.createNode({
nodeType: "span",
parent: areaTextbox,
attributes: {
class: "area-infobar-dimensions",
id: "area-infobar-dimensions",
},
prefix: this.ID_CLASS_PREFIX,
});
// Build the grid cell infobar markup.
const cellInfobarContainer = this.markup.createNode({
parent: container,
attributes: {
class: "cell-infobar-container",
id: "cell-infobar-container",
position: "top",
hidden: "true",
},
prefix: this.ID_CLASS_PREFIX,
});
const cellInfobar = this.markup.createNode({
parent: cellInfobarContainer,
attributes: {
class: "infobar",
},
prefix: this.ID_CLASS_PREFIX,
});
const cellTextbox = this.markup.createNode({
parent: cellInfobar,
attributes: {
class: "infobar-text",
},
prefix: this.ID_CLASS_PREFIX,
});
this.markup.createNode({
nodeType: "span",
parent: cellTextbox,
attributes: {
class: "cell-infobar-position",
id: "cell-infobar-position",
},
prefix: this.ID_CLASS_PREFIX,
});
this.markup.createNode({
nodeType: "span",
parent: cellTextbox,
attributes: {
class: "cell-infobar-dimensions",
id: "cell-infobar-dimensions",
},
prefix: this.ID_CLASS_PREFIX,
});
// Build the grid line infobar markup.
const lineInfobarContainer = this.markup.createNode({
parent: container,
attributes: {
class: "line-infobar-container",
id: "line-infobar-container",
position: "top",
hidden: "true",
},
prefix: this.ID_CLASS_PREFIX,
});
const lineInfobar = this.markup.createNode({
parent: lineInfobarContainer,
attributes: {
class: "infobar",
},
prefix: this.ID_CLASS_PREFIX,
});
const lineTextbox = this.markup.createNode({
parent: lineInfobar,
attributes: {
class: "infobar-text",
},
prefix: this.ID_CLASS_PREFIX,
});
this.markup.createNode({
nodeType: "span",
parent: lineTextbox,
attributes: {
class: "line-infobar-number",
id: "line-infobar-number",
},
prefix: this.ID_CLASS_PREFIX,
});
this.markup.createNode({
nodeType: "span",
parent: lineTextbox,
attributes: {
class: "line-infobar-names",
id: "line-infobar-names",
},
prefix: this.ID_CLASS_PREFIX,
});
return container;
}
clearCache() {
gCachedGridPattern.clear();
}
/**
* Clear the grid area highlights.
*/
clearGridAreas() {
const areas = this.getElement("areas");
areas.setAttribute("d", "");
}
/**
* Clear the grid cell highlights.
*/
clearGridCell() {
const cells = this.getElement("cells");
cells.setAttribute("d", "");
}
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();
AutoRefreshHighlighter.prototype.destroy.call(this);
}
get canvas() {
return this.getElement("canvas");
}
get color() {
return this.options.color || DEFAULT_COLOR;
}
get ctx() {
return this.canvas.getCanvasContext("2d");
}
get globalAlpha() {
return this.options.globalAlpha || 1;
}
getElement(id) {
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
}
getFirstColLinePos(fragment) {
return fragment.cols.lines[0].start;
}
getFirstRowLinePos(fragment) {
return fragment.rows.lines[0].start;
}
/**
* Gets the grid gap pattern used to render the gap regions based on the device
* pixel ratio given.
*
* @param {Number} devicePixelRatio
* The device pixel ratio we want the pattern for.
* @param {Object} dimension
* Refers to the Map key for the grid dimension type which is either the
* constant COLUMNS or ROWS.
* @return {CanvasPattern} grid gap pattern.
*/
getGridGapPattern(devicePixelRatio, dimension) {
let gridPatternMap = null;
if (gCachedGridPattern.has(devicePixelRatio)) {
gridPatternMap = gCachedGridPattern.get(devicePixelRatio);
} else {
gridPatternMap = new Map();
}
if (gridPatternMap.has(dimension)) {
return gridPatternMap.get(dimension);
}
// Create the diagonal lines pattern for the rendering the grid gaps.
const canvas = this.markup.createNode({ nodeType: "canvas" });
const width = (canvas.width = GRID_GAP_PATTERN_WIDTH * devicePixelRatio);
const height = (canvas.height = GRID_GAP_PATTERN_HEIGHT * devicePixelRatio);
const ctx = canvas.getContext("2d");
ctx.save();
ctx.setLineDash(GRID_GAP_PATTERN_LINE_DASH);
ctx.beginPath();
ctx.translate(0.5, 0.5);
if (dimension === COLUMNS) {
ctx.moveTo(0, 0);
ctx.lineTo(width, height);
} else {
ctx.moveTo(width, 0);
ctx.lineTo(0, height);
}
ctx.strokeStyle = this.color;
ctx.globalAlpha = GRID_GAP_ALPHA * this.globalAlpha;
ctx.stroke();
ctx.restore();
const pattern = ctx.createPattern(canvas, "repeat");
gridPatternMap.set(dimension, pattern);
gCachedGridPattern.set(devicePixelRatio, gridPatternMap);
return pattern;
}
getLastColLinePos(fragment) {
return fragment.cols.lines[fragment.cols.lines.length - 1].start;
}
/**
* Get the GridLine index of the last edge of the explicit grid for a grid dimension.
*
* @param {GridTracks} tracks
* The grid track of a given grid dimension.
* @return {Number} index of the last edge of the explicit grid for a grid dimension.
*/
getLastEdgeLineIndex(tracks) {
let trackIndex = tracks.length - 1;
// Traverse the grid track backwards until we find an explicit track.
while (trackIndex >= 0 && tracks[trackIndex].type != "explicit") {
trackIndex--;
}
// The grid line index is the grid track index + 1.
return trackIndex + 1;
}
getLastRowLinePos(fragment) {
return fragment.rows.lines[fragment.rows.lines.length - 1].start;
}
/**
* The AutoRefreshHighlighter's _hasMoved method returns true only if the
* element's quads have changed. Override it so it also returns true if the
* element's grid has changed (which can happen when you change the
* grid-template-* CSS properties with the highlighter displayed). This
* check is prone to false positives, because it does a direct object
* comparison of the first grid fragment structure. This structure is
* generated by the first call to getGridFragments, and on any subsequent
* calls where a reflow is needed. Since a reflow is needed when the CSS
* changes, this will correctly detect that the grid structure has changed.
* However, it's possible that the reflow could generate a novel grid
* fragment object containing information that is unchanged -- a false
* positive.
*/
_hasMoved() {
const hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
const oldFirstGridFragment = this.gridData?.[0];
this.gridData = this.currentNode.getGridFragments();
const newFirstGridFragment = this.gridData[0];
return hasMoved || oldFirstGridFragment !== newFirstGridFragment;
}
/**
* Hide the highlighter, the canvas and the infobars.
*/
_hide() {
setIgnoreLayoutChanges(true);
this._hideGrid();
this._hideGridElements();
this._hideGridAreaInfoBar();
this._hideGridCellInfoBar();
this._hideGridLineInfoBar();
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
}
_hideGrid() {
this.getElement("canvas").setAttribute("hidden", "true");
}
_hideGridAreaInfoBar() {
this.getElement("area-infobar-container").setAttribute("hidden", "true");
}
_hideGridCellInfoBar() {
this.getElement("cell-infobar-container").setAttribute("hidden", "true");
}
_hideGridElements() {
this.getElement("elements").setAttribute("hidden", "true");
}
_hideGridLineInfoBar() {
this.getElement("line-infobar-container").setAttribute("hidden", "true");
}
/**
* Checks if the current node has a CSS Grid layout.
*
* @return {Boolean} true if the current node has a CSS grid layout, false otherwise.
*/
isGrid() {
return this.currentNode.hasGridFragments();
}
/**
* Is a given grid fragment valid? i.e. does it actually have tracks? In some cases, we
* may have a fragment that defines column tracks but doesn't have any rows (or vice
* versa). In which case we do not want to draw anything for that fragment.
*
* @param {Object} fragment
* @return {Boolean}
*/
isValidFragment(fragment) {
return fragment.cols.tracks.length && fragment.rows.tracks.length;
}
/**
* The '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() {
if (!this.isGrid()) {
this.hide();
return false;
}
// The grid pattern cache should be cleared in case the color changed.
this.clearCache();
// Hide the canvas, grid element highlights and infobar.
this._hide();
return this._update();
}
_showGrid() {
this.getElement("canvas").removeAttribute("hidden");
}
_showGridAreaInfoBar() {
this.getElement("area-infobar-container").removeAttribute("hidden");
}
_showGridCellInfoBar() {
this.getElement("cell-infobar-container").removeAttribute("hidden");
}
_showGridElements() {
this.getElement("elements").removeAttribute("hidden");
}
_showGridLineInfoBar() {
this.getElement("line-infobar-container").removeAttribute("hidden");
}
/**
* Shows all the grid area highlights for the current grid.
*/
showAllGridAreas() {
this.renderGridArea();
}
/**
* Shows the grid area highlight for the given area name.
*
* @param {String} areaName
* Grid area name.
*/
showGridArea(areaName) {
this.renderGridArea(areaName);
}
/**
* Shows the grid cell highlight for the given grid cell options.
*
* @param {Number} options.gridFragmentIndex
* Index of the grid fragment to render the grid cell highlight.
* @param {Number} options.rowNumber
* Row number of the grid cell to highlight.
* @param {Number} options.columnNumber
* Column number of the grid cell to highlight.
*/
showGridCell({ gridFragmentIndex, rowNumber, columnNumber }) {
this.renderGridCell(gridFragmentIndex, rowNumber, columnNumber);
}
/**
* Shows the grid line highlight for the given grid line options.
*
* @param {Number} options.gridFragmentIndex
* Index of the grid fragment to render the grid line highlight.
* @param {Number} options.lineNumber
* Line number of the grid line to highlight.
* @param {String} options.type
* The dimension type of the grid line.
*/
showGridLineNames({ gridFragmentIndex, lineNumber, type }) {
this.renderGridLineNames(gridFragmentIndex, lineNumber, type);
}
/**
* 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 grid 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();
}
}
renderFragment(fragment) {
if (!this.isValidFragment(fragment)) {
return;
}
this.renderLines(
fragment.cols,
COLUMNS,
this.getFirstRowLinePos(fragment),
this.getLastRowLinePos(fragment)
);
this.renderLines(
fragment.rows,
ROWS,
this.getFirstColLinePos(fragment),
this.getLastColLinePos(fragment)
);
if (this.options.showGridAreasOverlay) {
this.renderGridAreaOverlay();
}
// Line numbers are rendered in a 2nd step to avoid overlapping with existing lines.
if (this.options.showGridLineNumbers) {
this.renderLineNumbers(
fragment.cols,
COLUMNS,
this.getFirstRowLinePos(fragment)
);
this.renderLineNumbers(
fragment.rows,
ROWS,
this.getFirstColLinePos(fragment)
);
this.renderNegativeLineNumbers(
fragment.cols,
COLUMNS,
this.getLastRowLinePos(fragment)
);
this.renderNegativeLineNumbers(
fragment.rows,
ROWS,
this.getLastColLinePos(fragment)
);
}
}
/**
* Render the grid area highlight for the given area name or for all the grid areas.
*
* @param {String} areaName
* Name of the grid area to be highlighted. If no area name is provided, all
* the grid areas should be highlighted.
*/
renderGridArea(areaName) {
const { devicePixelRatio } = this.win;
const displayPixelRatio = getDisplayPixelRatio(this.win);
const paths = [];
for (let i = 0; i < this.gridData.length; i++) {
const fragment = this.gridData[i];
for (const area of fragment.areas) {
if (areaName && areaName != area.name) {
continue;
}
const rowStart = fragment.rows.lines[area.rowStart - 1];
const rowEnd = fragment.rows.lines[area.rowEnd - 1];
const columnStart = fragment.cols.lines[area.columnStart - 1];
const columnEnd = fragment.cols.lines[area.columnEnd - 1];
const x1 = columnStart.start + columnStart.breadth;
const y1 = rowStart.start + rowStart.breadth;
const x2 = columnEnd.start;
const y2 = rowEnd.start;
const points = getPointsFromDiagonal(
x1,
y1,
x2,
y2,
this.currentMatrix
);
// Scale down by `devicePixelRatio` since SVG element already take them into
// account.
const svgPoints = points.map(point => ({
x: Math.round(point.x / devicePixelRatio),
y: Math.round(point.y / devicePixelRatio),
}));
// Scale down by `displayPixelRatio` since infobar's HTML elements already take it
// into account; and the zoom scaling is handled by `moveInfobar`.
const bounds = getBoundsFromPoints(
points.map(point => ({
x: Math.round(point.x / displayPixelRatio),
y: Math.round(point.y / displayPixelRatio),
}))
);
paths.push(getPathDescriptionFromPoints(svgPoints));
// Update and show the info bar when only displaying a single grid area.
if (areaName) {
this._showGridAreaInfoBar();
this._updateGridAreaInfobar(area, bounds);
}
}
}
const areas = this.getElement("areas");
areas.setAttribute("d", paths.join(" "));
}
/**
* Render grid area name on the containing grid area cell.
*
* @param {Object} fragment
* The grid fragment of the grid container.
* @param {Object} area
* The area overlay to render on the CSS highlighter canvas.
*/
renderGridAreaName(fragment, area) {
const { rowStart, rowEnd, columnStart, columnEnd } = area;
const { devicePixelRatio } = this.win;
const displayPixelRatio = getDisplayPixelRatio(this.win);
const offset = (displayPixelRatio / 2) % 1;
let fontSize = GRID_AREA_NAME_FONT_SIZE * displayPixelRatio;
const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
this.ctx.save();
this.ctx.translate(offset - canvasX, offset - canvasY);
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
this.ctx.globalAlpha = this.globalAlpha;
this.ctx.strokeStyle = this.color;
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
// Draw the text for the grid area name.
for (let rowNumber = rowStart; rowNumber < rowEnd; rowNumber++) {
for (
let columnNumber = columnStart;
columnNumber < columnEnd;
columnNumber++
) {
const row = fragment.rows.tracks[rowNumber - 1];
const column = fragment.cols.tracks[columnNumber - 1];
// If the font size exceeds the bounds of the containing grid cell, size it its
// row or column dimension, whichever is smallest.
if (
fontSize > column.breadth * displayPixelRatio ||
fontSize > row.breadth * displayPixelRatio
) {
fontSize = Math.min([column.breadth, row.breadth]);
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
}
const textWidth = this.ctx.measureText(area.name).width;
// The width of the character 'm' approximates the height of the text.
const textHeight = this.ctx.measureText("m").width;
// Padding in pixels for the line number text inside of the line number container.
const padding = 3 * displayPixelRatio;
const boxWidth = textWidth + 2 * padding;
const boxHeight = textHeight + 2 * padding;
let x = column.start + column.breadth / 2;
let y = row.start + row.breadth / 2;
[x, y] = apply(this.currentMatrix, [x, y]);
const rectXPos = x - boxWidth / 2;
const rectYPos = y - boxHeight / 2;
// Draw a rounded rectangle with a border width of 1 pixel,
// a border color matching the grid color, and a white background.
this.ctx.lineWidth = 1 * displayPixelRatio;
this.ctx.strokeStyle = this.color;
this.ctx.fillStyle = "white";
const radius = 2 * displayPixelRatio;
drawRoundedRect(
this.ctx,
rectXPos,
rectYPos,
boxWidth,
boxHeight,
radius
);
this.ctx.fillStyle = this.color;
this.ctx.fillText(area.name, x, y + padding);
}
}
this.ctx.restore();
}
/**
* Renders the grid area overlay on the css grid highlighter canvas.
*/
renderGridAreaOverlay() {
const padding = 1;
for (let i = 0; i < this.gridData.length; i++) {
const fragment = this.gridData[i];
for (const area of fragment.areas) {
const { rowStart, rowEnd, columnStart, columnEnd, type } = area;
if (type === "implicit") {
continue;
}
// Draw the line edges for the grid area.
const areaColStart = fragment.cols.lines[columnStart - 1];
const areaColEnd = fragment.cols.lines[columnEnd - 1];
const areaRowStart = fragment.rows.lines[rowStart - 1];
const areaRowEnd = fragment.rows.lines[rowEnd - 1];
const areaColStartLinePos = areaColStart.start + areaColStart.breadth;
const areaRowStartLinePos = areaRowStart.start + areaRowStart.breadth;
this.renderLine(
areaColStartLinePos + padding,
areaRowStartLinePos,
areaRowEnd.start,
COLUMNS,
"areaEdge"
);
this.renderLine(
areaColEnd.start - padding,
areaRowStartLinePos,
areaRowEnd.start,
COLUMNS,
"areaEdge"
);
this.renderLine(
areaRowStartLinePos + padding,
areaColStartLinePos,
areaColEnd.start,
ROWS,
"areaEdge"
);
this.renderLine(
areaRowEnd.start - padding,
areaColStartLinePos,
areaColEnd.start,
ROWS,
"areaEdge"
);
this.renderGridAreaName(fragment, area);
}
}
}
/**
* Render the grid cell highlight for the given grid fragment index, row and column
* number.
*
* @param {Number} gridFragmentIndex
* Index of the grid fragment to render the grid cell highlight.
* @param {Number} rowNumber
* Row number of the grid cell to highlight.
* @param {Number} columnNumber
* Column number of the grid cell to highlight.
*/
renderGridCell(gridFragmentIndex, rowNumber, columnNumber) {
const fragment = this.gridData[gridFragmentIndex];
if (!fragment) {
return;
}
const row = fragment.rows.tracks[rowNumber - 1];
const column = fragment.cols.tracks[columnNumber - 1];
if (!row || !column) {
return;
}
const x1 = column.start;
const y1 = row.start;
const x2 = column.start + column.breadth;
const y2 = row.start + row.breadth;
const { devicePixelRatio } = this.win;
const displayPixelRatio = getDisplayPixelRatio(this.win);
const points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix);
// Scale down by `devicePixelRatio` since SVG element already take them into account.
const svgPoints = points.map(point => ({
x: Math.round(point.x / devicePixelRatio),
y: Math.round(point.y / devicePixelRatio),
}));
// Scale down by `displayPixelRatio` since infobar's HTML elements already take it
// into account, and the zoom scaling is handled by `moveInfobar`.
const bounds = getBoundsFromPoints(
points.map(point => ({
x: Math.round(point.x / displayPixelRatio),
y: Math.round(point.y / displayPixelRatio),
}))
);
const cells = this.getElement("cells");
cells.setAttribute("d", getPathDescriptionFromPoints(svgPoints));
this._showGridCellInfoBar();
this._updateGridCellInfobar(rowNumber, columnNumber, bounds);
}
/**
* Render the grid gap area on the css grid highlighter canvas.
*
* @param {Number} linePos
* The line position along the x-axis for a column grid line and
* y-axis for a row grid line.
* @param {Number} startPos
* The start position of the cross side of the grid line.
* @param {Number} endPos
* The end position of the cross side of the grid line.
* @param {Number} breadth
* The grid line breadth value.
* @param {String} dimensionType
* The grid dimension type which is either the constant COLUMNS or ROWS.
*/
renderGridGap(linePos, startPos, endPos, breadth, dimensionType) {
const { devicePixelRatio } = this.win;
const displayPixelRatio = getDisplayPixelRatio(this.win);
const offset = (displayPixelRatio / 2) % 1;
const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
linePos = Math.round(linePos);
startPos = Math.round(startPos);
breadth = Math.round(breadth);
this.ctx.save();
this.ctx.fillStyle = this.getGridGapPattern(
devicePixelRatio,
dimensionType
);
this.ctx.translate(offset - canvasX, offset - canvasY);
if (dimensionType === COLUMNS) {
if (isFinite(endPos)) {
endPos = Math.round(endPos);
} else {
endPos = this._winDimensions.height;
startPos = -endPos;
}
drawRect(
this.ctx,
linePos,
startPos,
linePos + breadth,
endPos,
this.currentMatrix
);
} else {
if (isFinite(endPos)) {
endPos = Math.round(endPos);
} else {
endPos = this._winDimensions.width;
startPos = -endPos;
}
drawRect(
this.ctx,
startPos,
linePos,
endPos,
linePos + breadth,
this.currentMatrix
);
}
// Find current angle of grid by measuring the angle of two arbitrary points,
// then rotate canvas, so the hash pattern stays 45deg to the gridlines.
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.restore();
}
/**
* Render the grid line name highlight for the given grid fragment index, lineNumber,
* and dimensionType.
*
* @param {Number} gridFragmentIndex
* Index of the grid fragment to render the grid line highlight.
* @param {Number} lineNumber
* Line number of the grid line to highlight.
* @param {String} dimensionType
* The dimension type of the grid line.
*/
renderGridLineNames(gridFragmentIndex, lineNumber, dimensionType) {
const fragment = this.gridData[gridFragmentIndex];
if (!fragment || !lineNumber || !dimensionType) {
return;
}
const { names } = fragment[dimensionType].lines[lineNumber - 1];
let linePos;
if (dimensionType === ROWS) {
linePos = fragment.rows.lines[lineNumber - 1];
} else if (dimensionType === COLUMNS) {
linePos = fragment.cols.lines[lineNumber - 1];
}
if (!linePos) {
return;
}
const currentZoom = getCurrentZoom(this.win);
const { bounds } = this.currentQuads.content[gridFragmentIndex];
const rowYPosition = fragment.rows.lines[0];
const colXPosition = fragment.rows.lines[0];
const x =
dimensionType === COLUMNS
? linePos.start + bounds.left / currentZoom
: colXPosition.start + bounds.left / currentZoom;
const y =
dimensionType === ROWS
? linePos.start + bounds.top / currentZoom
: rowYPosition.start + bounds.top / currentZoom;
this._showGridLineInfoBar();
this._updateGridLineInfobar(names.join(", "), lineNumber, x, y);
}
/**
* Render the grid line number on the css grid highlighter canvas.
*
* @param {Number} lineNumber
* The grid line number.
* @param {Number} linePos
* The line position along the x-axis for a column grid line and
* y-axis for a row grid line.
* @param {Number} startPos
* The start position of the cross side of the grid line.
* @param {Number} breadth
* The grid line breadth value.
* @param {String} dimensionType
* The grid dimension type which is either the constant COLUMNS or ROWS.
* @param {Boolean||undefined} isStackedLine
* Boolean indicating if the line is stacked.
*/
// eslint-disable-next-line complexity
renderGridLineNumber(
lineNumber,
linePos,
startPos,
breadth,
dimensionType,
isStackedLine
) {
const displayPixelRatio = getDisplayPixelRatio(this.win);
const { devicePixelRatio } = this.win;
const offset = (displayPixelRatio / 2) % 1;
const fontSize = GRID_FONT_SIZE * devicePixelRatio;
const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
linePos = Math.round(linePos);
startPos = Math.round(startPos);
breadth = Math.round(breadth);
if (linePos + breadth < 0) {
// Don't render the line number since the line is not visible on screen.
return;
}
this.ctx.save();
this.ctx.translate(offset - canvasX, offset - canvasY);
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
// For a general grid box, the height of the character "m" will be its minimum width
// and height. If line number's text width is greater, then use the grid box's text
// width instead.
const textHeight = this.ctx.measureText("m").width;
const textWidth = Math.max(
textHeight,
this.ctx.measureText(lineNumber).width
);
// Padding in pixels for the line number text inside of the line number container.
const padding = 3 * devicePixelRatio;
const offsetFromEdge = 2 * devicePixelRatio;
let boxWidth = textWidth + 2 * padding;
let boxHeight = textHeight + 2 * padding;
// Calculate the x & y coordinates for the line number container, so that its arrow
// tip is centered on the line (or the gap if there is one), and is offset by the
// calculated padding value from the grid container edge.
let x, y;
if (dimensionType === COLUMNS) {
x = linePos + breadth / 2;
y =
lineNumber > 0 ? startPos - offsetFromEdge : startPos + offsetFromEdge;
} else if (dimensionType === ROWS) {
y = linePos + breadth / 2;
x =
lineNumber > 0 ? startPos - offsetFromEdge : startPos + offsetFromEdge;
}
[x, y] = apply(this.currentMatrix, [x, y]);
// Draw a bubble rectangular arrow with a border width of 2 pixels, a border color
// matching the grid color and a white background (the line number will be written in
// black).
this.ctx.lineWidth = 2 * displayPixelRatio;
this.ctx.strokeStyle = this.color;
this.ctx.fillStyle = "white";
this.ctx.globalAlpha = this.globalAlpha;
// See param definitions of drawBubbleRect.
const radius = 2 * displayPixelRatio;
const margin = 2 * displayPixelRatio;
const arrowSize = 8 * displayPixelRatio;
const minBoxSize = arrowSize * 2 + padding;
boxWidth = Math.max(boxWidth, minBoxSize);
boxHeight = Math.max(boxHeight, minBoxSize);
// Determine which edge of the box to aim the line number arrow at.
const boxEdge = this.getBoxEdge(dimensionType, lineNumber);
let { width, height } = this._winDimensions;
width *= displayPixelRatio;
height *= displayPixelRatio;
// Don't draw if the line is out of the viewport.
if (
(dimensionType === ROWS && (y < 0 || y > height)) ||
(dimensionType === COLUMNS && (x < 0 || x > width))
) {
this.ctx.restore();
return;
}
// If the arrow's edge (the one perpendicular to the line direction) is too close to
// the edge of the viewport. Push the arrow inside the grid.
const minOffsetFromEdge = OFFSET_FROM_EDGE * displayPixelRatio;
switch (boxEdge) {
case "left":
if (x < minOffsetFromEdge) {
x += FLIP_ARROW_INSIDE_FACTOR * boxWidth;
}
break;
case "right":
if (width - x < minOffsetFromEdge) {
x -= FLIP_ARROW_INSIDE_FACTOR * boxWidth;
}
break;
case "top":
if (y < minOffsetFromEdge) {
y += FLIP_ARROW_INSIDE_FACTOR * boxHeight;
}
break;
case "bottom":
if (height - y < minOffsetFromEdge) {
y -= FLIP_ARROW_INSIDE_FACTOR * boxHeight;
}
break;
}
// Offset stacked line numbers by a quarter of the box's width/height, so a part of
// them remains visible behind the number that sits at the top of the stack.
if (isStackedLine) {
const xOffset = boxWidth / 4;
const yOffset = boxHeight / 4;
if (lineNumber > 0) {
x -= xOffset;
y -= yOffset;
} else {
x += xOffset;
y += yOffset;
}
}
// If one the edges of the arrow that's parallel to the line is too close to the edge
// of the viewport (and therefore partly hidden), grow the arrow's size in the
// opposite direction.
// The goal is for the part that's not hidden to be exactly the size of a normal
// arrow and for the arrow to keep pointing at the line (keep being centered on it).
let grewBox = false;
const boxWidthBeforeGrowth = boxWidth;
const boxHeightBeforeGrowth = boxHeight;
if (dimensionType === ROWS && y <= boxHeight / 2) {
grewBox = true;
boxHeight = 2 * (boxHeight - y);
} else if (dimensionType === ROWS && y >= height - boxHeight / 2) {
grewBox = true;
boxHeight = 2 * (y - height + boxHeight);
} else if (dimensionType === COLUMNS && x <= boxWidth / 2) {
grewBox = true;
boxWidth = 2 * (boxWidth - x);
} else if (dimensionType === COLUMNS && x >= width - boxWidth / 2) {
grewBox = true;
boxWidth = 2 * (x - width + boxWidth);
}
// Draw the arrow box itself
drawBubbleRect(
this.ctx,
x,
y,
boxWidth,
boxHeight,
radius,
margin,
arrowSize,
boxEdge
);
// Determine the text position for it to be centered nicely inside the arrow box.
switch (boxEdge) {
case "left":
x -= boxWidth + arrowSize + radius - boxWidth / 2;
break;
case "right":
x += boxWidth + arrowSize + radius - boxWidth / 2;
break;
case "top":
y -= boxHeight + arrowSize + radius - boxHeight / 2;
break;
case "bottom":
y += boxHeight + arrowSize + radius - boxHeight / 2;
break;
}
// Do a second pass to adjust the position, along the other axis, if the box grew
// during the previous step, so the text is also centered on that axis.
if (grewBox) {
if (dimensionType === ROWS && y <= boxHeightBeforeGrowth / 2) {
y = boxHeightBeforeGrowth / 2;
} else if (
dimensionType === ROWS &&
y >= height - boxHeightBeforeGrowth / 2
) {
y = height - boxHeightBeforeGrowth / 2;
} else if (dimensionType === COLUMNS && x <= boxWidthBeforeGrowth / 2) {
x = boxWidthBeforeGrowth / 2;
} else if (
dimensionType === COLUMNS &&
x >= width - boxWidthBeforeGrowth / 2
) {
x = width - boxWidthBeforeGrowth / 2;
}
}
// Write the line number inside of the rectangle.
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
this.ctx.fillStyle = "black";
const numberText = isStackedLine ? "" : lineNumber;
this.ctx.fillText(numberText, x, y);
this.ctx.restore();
}
/**
* Determine which edge of a line number box to aim the line number arrow at.
*
* @param {String} dimensionType
* The grid line dimension type which is either the constant COLUMNS or ROWS.
* @param {Number} lineNumber
* The grid line number.
* @return {String} The edge of the box: top, right, bottom or left.
*/
getBoxEdge(dimensionType, lineNumber) {
let boxEdge;
if (dimensionType === COLUMNS) {
boxEdge = lineNumber > 0 ? "top" : "bottom";
} else if (dimensionType === ROWS) {
boxEdge = lineNumber > 0 ? "left" : "right";
}
// Rotate box edge as needed for writing mode and text direction.
const { direction, writingMode } = getComputedStyle(this.currentNode);
switch (writingMode) {
case "horizontal-tb":
// This is the initial value. No further adjustment needed.
break;
case "vertical-rl":
boxEdge = rotateEdgeRight(boxEdge);
break;
case "vertical-lr":
if (dimensionType === COLUMNS) {
boxEdge = rotateEdgeLeft(boxEdge);
} else {
boxEdge = rotateEdgeRight(boxEdge);
}
break;
case "sideways-rl":
boxEdge = rotateEdgeRight(boxEdge);
break;
case "sideways-lr":
boxEdge = rotateEdgeLeft(boxEdge);
break;
default:
console.error(`Unexpected writing-mode: ${writingMode}`);
}
switch (direction) {
case "ltr":
// This is the initial value. No further adjustment needed.
break;
case "rtl":
if (dimensionType === ROWS) {
boxEdge = reflectEdge(boxEdge);
}
break;
default:
console.error(`Unexpected direction: ${direction}`);
}
return boxEdge;
}
/**
* Render the grid line on the css grid highlighter canvas.
*
* @param {Number} linePos
* The line position along the x-axis for a column grid line and
* y-axis for a row grid line.
* @param {Number} startPos
* The start position of the cross side of the grid line.
* @param {Number} endPos
* The end position of the cross side of the grid line.
* @param {String} dimensionType
* The grid dimension type which is either the constant COLUMNS or ROWS.
* @param {String} lineType
* The grid line type - "edge", "explicit", or "implicit".
*/
renderLine(linePos, startPos, endPos, dimensionType, lineType) {
const { devicePixelRatio } = this.win;
const lineWidth = getDisplayPixelRatio(this.win);
const offset = (lineWidth / 2) % 1;
const canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
const canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
linePos = Math.round(linePos);
startPos = Math.round(startPos);
endPos = Math.round(endPos);
this.ctx.save();
this.ctx.setLineDash(GRID_LINES_PROPERTIES[lineType].lineDash);
this.ctx.translate(offset - canvasX, offset - canvasY);
const lineOptions = {
matrix: this.currentMatrix,
};
if (this.options.showInfiniteLines) {
lineOptions.extendToBoundaries = [
canvasX,
canvasY,
canvasX + CANVAS_SIZE,
canvasY + CANVAS_SIZE,
];
}
if (dimensionType === COLUMNS) {
drawLine(this.ctx, linePos, startPos, linePos, endPos, lineOptions);
} else {
drawLine(this.ctx, startPos, linePos, endPos, linePos, lineOptions);
}
this.ctx.strokeStyle = this.color;
this.ctx.globalAlpha =
GRID_LINES_PROPERTIES[lineType].alpha * this.globalAlpha;
if (GRID_LINES_PROPERTIES[lineType].lineWidth) {
this.ctx.lineWidth =
GRID_LINES_PROPERTIES[lineType].lineWidth * devicePixelRatio;
} else {
this.ctx.lineWidth = lineWidth;
}
this.ctx.stroke();
this.ctx.restore();
}
/**
* Render the grid lines given the grid dimension information of the
* column or row lines.
*
* @param {GridDimension} gridDimension
* Column or row grid dimension object.
* @param {Object} quad.bounds
* The content bounds of the box model region quads.
* @param {String} dimensionType
* The grid dimension type which is either the constant COLUMNS or ROWS.
* @param {Number} startPos
* The start position of the cross side ("left" for ROWS and "top" for COLUMNS)
* of the grid dimension.
* @param {Number} endPos
* The end position of the cross side ("left" for ROWS and "top" for COLUMNS)
* of the grid dimension.
*/
renderLines(gridDimension, dimensionType, startPos, endPos) {
const { lines, tracks } = gridDimension;
const lastEdgeLineIndex = this.getLastEdgeLineIndex(tracks);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const linePos = line.start;
if (i == 0 || i == lastEdgeLineIndex) {
this.renderLine(linePos, startPos, endPos, dimensionType, "edge");
} else {
this.renderLine(
linePos,
startPos,
endPos,
dimensionType,
tracks[i - 1].type
);
}
// Render a second line to illustrate the gutter for non-zero breadth.
if (line.breadth > 0) {
this.renderGridGap(
linePos,
startPos,
endPos,
line.breadth,
dimensionType
);
this.renderLine(
linePos + line.breadth,
startPos,
endPos,
dimensionType,
tracks[i].type
);
}
}
}
/**
* Render the grid lines given the grid dimension information of the
* column or row lines.
*
* @param {GridDimension} gridDimension
* Column or row grid dimension object.
* @param {String} dimensionType
* The grid dimension type which is either the constant COLUMNS or ROWS.
* @param {Number} startPos
* The start position of the cross side ("left" for ROWS and "top" for COLUMNS)
* of the grid dimension.
*/
renderLineNumbers(gridDimension, dimensionType, startPos) {
const { lines, tracks } = gridDimension;
for (let i = 0, line; (line = lines[i++]); ) {
// If you place something using negative numbers, you can trigger some implicit
// grid creation above and to the left of the explicit grid (assuming a
// horizontal-tb writing mode).
//
// The first explicit grid line gets the number of 1, and any implicit grid lines
// before 1 get negative numbers. Since here we're rendering only the positive line
// numbers, we have to skip any implicit grid lines before the first one that is
// explicit. The API returns a 0 as the line's number for these implicit lines that
// occurs before the first explicit line.
if (line.number === 0) {
continue;
}
// Check for overlapping lines by measuring the track width between them.
// We render a second box beneath the last overlapping
// line number to indicate there are lines beneath it.
const gridTrack = tracks[i - 1];
if (gridTrack) {
const { breadth } = gridTrack;
if (breadth === 0) {
this.renderGridLineNumber(
line.number,
line.start,
startPos,
line.breadth,
dimensionType,
true
);
continue;
}
}
this.renderGridLineNumber(
line.number,
line.start,
startPos,
line.breadth,
dimensionType
);
}
}
/**
* Render the negative grid lines given the grid dimension information of the
* column or row lines.
*
* @param {GridDimension} gridDimension
* Column or row grid dimension object.
* @param {String} dimensionType
* The grid dimension type which is either the constant COLUMNS or ROWS.
* @param {Number} startPos
* The start position of the cross side ("left" for ROWS and "top" for COLUMNS)
* of the grid dimension.
*/
renderNegativeLineNumbers(gridDimension, dimensionType, startPos) {
const { lines, tracks } = gridDimension;
for (let i = 0, line; (line = lines[i++]); ) {
const linePos = line.start;
const negativeLineNumber = line.negativeNumber;
// Don't render any negative line number greater than -1.
if (negativeLineNumber == 0) {
break;
}
// Check for overlapping lines by measuring the track width between them.
// We render a second box beneath the last overlapping
// line number to indicate there are lines beneath it.
const gridTrack = tracks[i - 1];
if (gridTrack) {
const { breadth } = gridTrack;
// Ensure "-1" is always visible, since it is always the largest number.
if (breadth === 0 && negativeLineNumber != -1) {
this.renderGridLineNumber(
negativeLineNumber,
linePos,
startPos,
line.breadth,
dimensionType,
true
);
continue;
}
}
this.renderGridLineNumber(
negativeLineNumber,
linePos,
startPos,
line.breadth,
dimensionType
);
}
}
/**
* Update the highlighter on the current highlighted node (the one that was
* passed as an argument to show(node)). Should be called whenever node's geometry
* or grid changes.
*/
_update() {
setIgnoreLayoutChanges(true);
// Set z-index.
this.markup.content.root.firstElementChild.style.setProperty(
"z-index",
this.options.zIndex
);
const root = this.getElement("root");
const cells = this.getElement("cells");
const areas = this.getElement("areas");
// Set the grid cells and areas fill to the current grid colour.
cells.setAttribute("style", `fill: ${this.color}`);
areas.setAttribute("style", `fill: ${this.color}`);
// 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 element's position and size.
// It also clear the 's drawing context.
updateCanvasElement(
this.canvas,
this._canvasPosition,
this.win.devicePixelRatio
);
// Clear the grid area highlights.
this.clearGridAreas();
this.clearGridCell();
// Update the current matrix used in our canvas' rendering.
const { currentMatrix, hasNodeTransformations } = getCurrentMatrix(
this.currentNode,
this.win
);
this.currentMatrix = currentMatrix;
this.hasNodeTransformations = hasNodeTransformations;
// Start drawing the grid fragments.
for (let i = 0; i < this.gridData.length; i++) {
this.renderFragment(this.gridData[i]);
}
// Display the grid area highlights if needed.
if (this.options.showAllGridAreas) {
this.showAllGridAreas();
} else if (this.options.showGridArea) {
this.showGridArea(this.options.showGridArea);
}
// Display the grid cell highlights if needed.
if (this.options.showGridCell) {
this.showGridCell(this.options.showGridCell);
}
// Display the grid line names if needed.
if (this.options.showGridLineNames) {
this.showGridLineNames(this.options.showGridLineNames);
}
this._showGrid();
this._showGridElements();
root.setAttribute(
"style",
`position: absolute; width: ${width}px; height: ${height}px; overflow: hidden`
);
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
return true;
}
/**
* Update the grid information displayed in the grid area info bar.
*
* @param {GridArea} area
* The grid area object.
* @param {Object} bounds
* A DOMRect-like object represent the grid area rectangle.
*/
_updateGridAreaInfobar(area, bounds) {
const { width, height } = bounds;
const dim =
parseFloat(width.toPrecision(6)) +
" \u00D7 " +
parseFloat(height.toPrecision(6));
this.getElement("area-infobar-name").setTextContent(area.name);
this.getElement("area-infobar-dimensions").setTextContent(dim);
const container = this.getElement("area-infobar-container");
moveInfobar(container, bounds, this.win, {
position: "bottom",
});
}
/**
* Update the grid information displayed in the grid cell info bar.
*
* @param {Number} rowNumber
* The grid cell's row number.
* @param {Number} columnNumber
* The grid cell's column number.
* @param {Object} bounds
* A DOMRect-like object represent the grid cell rectangle.
*/
_updateGridCellInfobar(rowNumber, columnNumber, bounds) {
const { width, height } = bounds;
const dim =
parseFloat(width.toPrecision(6)) +
" \u00D7 " +
parseFloat(height.toPrecision(6));
const position = HighlightersBundle.formatValueSync(
"grid-row-column-positions",
{ row: rowNumber, column: columnNumber }
);
this.getElement("cell-infobar-position").setTextContent(position);
this.getElement("cell-infobar-dimensions").setTextContent(dim);
const container = this.getElement("cell-infobar-container");
moveInfobar(container, bounds, this.win, {
position: "top",
});
}
/**
* Update the grid information displayed in the grid line info bar.
*
* @param {String} gridLineNames
* Comma-separated string of names for the grid line.
* @param {Number} gridLineNumber
* The grid line number.
* @param {Number} x
* The x-coordinate of the grid line.
* @param {Number} y
* The y-coordinate of the grid line.
*/
_updateGridLineInfobar(gridLineNames, gridLineNumber, x, y) {
this.getElement("line-infobar-number").setTextContent(gridLineNumber);
this.getElement("line-infobar-names").setTextContent(gridLineNames);
const container = this.getElement("line-infobar-container");
moveInfobar(
container,
getBoundsFromPoints([
{ x, y },
{ x, y },
{ x, y },
{ x, y },
]),
this.win
);
}
}
exports.CssGridHighlighter = CssGridHighlighter;