fune/devtools/server/actors/highlighters/css-grid.js
2017-07-26 18:27:36 -06:00

1840 lines
59 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 Services = require("Services");
const { AutoRefreshHighlighter } = require("./auto-refresh");
const {
CanvasFrameAnonymousContentHelper,
createNode,
createSVGNode,
moveInfobar,
} = require("./utils/markup");
const {
getCurrentZoom,
getDisplayPixelRatio,
setIgnoreLayoutChanges,
getViewportDimensions,
} = require("devtools/shared/layout/utils");
const {
identity,
apply,
translate,
multiply,
scale,
isIdentity,
getNodeTransformationMatrix,
} = require("devtools/shared/layout/dom-matrix-2d");
const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const LAYOUT_STRINGS_URI = "devtools/client/locales/layout.properties";
const LAYOUT_L10N = new LocalizationHelper(LAYOUT_STRINGS_URI);
const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
const NEGATIVE_LINE_NUMBERS_PREF = "devtools.gridinspector.showNegativeLineNumbers";
const DEFAULT_GRID_COLOR = "#4B0082";
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;
/**
* Cached used by `CssGridHighlighter.getGridGapPattern`.
*/
const gCachedGridPattern = new Map();
// We create a <canvas> element that has always 4096x4096 physical pixels, to displays
// our grid's overlay.
// Then, we move the element around when needed, to give the perception that it always
// covers the screen (See bug 1345434).
//
// This canvas size value is the safest we can use because most GPUs can handle it.
// It's also far from the maximum canvas memory allocation limit (4096x4096x4 is
// 67.108.864 bytes, where the limit is 500.000.000 bytes, see:
// http://searchfox.org/mozilla-central/source/gfx/thebes/gfxPrefs.h#401).
//
// Note:
// Once bug 1232491 lands, we could try to refactor this code to use the values from
// the displayport API instead.
//
// Using a fixed value should also solve bug 1348293.
const CANVAS_SIZE = 4096;
/**
* Returns an array containing the four coordinates of a rectangle, given its diagonal
* as input; optionally applying a matrix, and a function to each of the coordinates'
* value.
*
* @param {Number} x1
* The x-axis coordinate of the rectangle's diagonal start point.
* @param {Number} y1
* The y-axis coordinate of the rectangle's diagonal start point.
* @param {Number} x2
* The x-axis coordinate of the rectangle's diagonal end point.
* @param {Number} y2
* The y-axis coordinate of the rectangle's diagonal end point.
* @param {Array} [matrix=identity()]
* A transformation matrix to apply.
* @return {Array}
* The rect four corners' points transformed by the matrix given.
*/
function getPointsFromDiagonal(x1, y1, x2, y2, matrix = identity()) {
return [
[x1, y1],
[x2, y1],
[x2, y2],
[x1, y2]
].map(point => {
let transformedPoint = apply(matrix, point);
return {x: transformedPoint[0], y: transformedPoint[1]};
});
}
/**
* Takes an array of four points and returns a DOMRect-like object, represent the
* boundaries defined by the points given.
*
* @param {Array} points
* The four points.
* @return {Object}
* A DOMRect-like object.
*/
function getBoundsFromPoints(points) {
let bounds = {};
bounds.left = Math.min(points[0].x, points[1].x, points[2].x, points[3].x);
bounds.right = Math.max(points[0].x, points[1].x, points[2].x, points[3].x);
bounds.top = Math.min(points[0].y, points[1].y, points[2].y, points[3].y);
bounds.bottom = Math.max(points[0].y, points[1].y, points[2].y, points[3].y);
bounds.x = bounds.left;
bounds.y = bounds.top;
bounds.width = bounds.right - bounds.left;
bounds.height = bounds.bottom - bounds.top;
return bounds;
}
/**
* Takes an array of four points and returns a string represent a path description.
*
* @param {Array} points
* The four points.
* @return {String}
* A Path Description that can be used in svg's <path> element.
*/
function getPathDescriptionFromPoints(points) {
return "M" + points[0].x + "," + points[0].y + " " +
"L" + points[1].x + "," + points[1].y + " " +
"L" + points[2].x + "," + points[2].y + " " +
"L" + points[3].x + "," + points[3].y;
}
/**
* Draws a line to the context given, applying a transformation matrix if passed.
*
* @param {CanvasRenderingContext2D} ctx
* The 2d canvas context.
* @param {Number} x1
* The x-axis of the coordinate for the begin of the line.
* @param {Number} y1
* The y-axis of the coordinate for the begin of the line.
* @param {Number} x2
* The x-axis of the coordinate for the end of the line.
* @param {Number} y2
* The y-axis of the coordinate for the end of the line.
* @param {Object} [options]
* The options object.
* @param {Array} [options.matrix=identity()]
* The transformation matrix to apply.
* @param {Array} [options.extendToBoundaries]
* If set, the line will be extended to reach the boundaries specified.
*/
function drawLine(ctx, x1, y1, x2, y2, options) {
let matrix = options.matrix || identity();
let p1 = apply(matrix, [x1, y1]);
let p2 = apply(matrix, [x2, y2]);
x1 = p1[0];
y1 = p1[1];
x2 = p2[0];
y2 = p2[1];
if (options.extendToBoundaries) {
if (p1[1] === p2[1]) {
x1 = options.extendToBoundaries[0];
x2 = options.extendToBoundaries[2];
} else {
y1 = options.extendToBoundaries[1];
x1 = (p2[0] - p1[0]) * (y1 - p1[1]) / (p2[1] - p1[1]) + p1[0];
y2 = options.extendToBoundaries[3];
x2 = (p2[0] - p1[0]) * (y2 - p1[1]) / (p2[1] - p1[1]) + p1[0];
}
}
ctx.moveTo(Math.round(x1), Math.round(y1));
ctx.lineTo(Math.round(x2), Math.round(y2));
}
/**
* Draws a rect to the context given, applying a transformation matrix if passed.
* The coordinates are the start and end points of the rectangle's diagonal.
*
* @param {CanvasRenderingContext2D} ctx
* The 2d canvas context.
* @param {Number} x1
* The x-axis coordinate of the rectangle's diagonal start point.
* @param {Number} y1
* The y-axis coordinate of the rectangle's diagonal start point.
* @param {Number} x2
* The x-axis coordinate of the rectangle's diagonal end point.
* @param {Number} y2
* The y-axis coordinate of the rectangle's diagonal end point.
* @param {Array} [matrix=identity()]
* The transformation matrix to apply.
*/
function drawRect(ctx, x1, y1, x2, y2, matrix = identity()) {
let p = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
ctx.beginPath();
ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
ctx.closePath();
}
/**
* Utility method to draw a rounded rectangle in the provided canvas context.
*
* @param {CanvasRenderingContext2D} ctx
* The 2d canvas context.
* @param {Number} x
* The x-axis origin of the rectangle.
* @param {Number} y
* The y-axis origin of the rectangle.
* @param {Number} width
* The width of the rectangle.
* @param {Number} height
* The height of the rectangle.
* @param {Number} radius
* The radius of the rounding.
*/
function drawRoundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
ctx.arcTo(x, y + height, x + radius, y + height, radius);
ctx.lineTo(x + width - radius, y + height);
ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
ctx.lineTo(x + width, y + radius);
ctx.arcTo(x + width, y, x + width - radius, y, radius);
ctx.lineTo(x + radius, y);
ctx.arcTo(x, y, x, y + radius, radius);
ctx.stroke();
ctx.fill();
}
/**
* 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();
*
* Available Options:
* - color(colorValue)
* @param {String} colorValue
* The color that should be used to draw the highlighter for this grid.
* - showAllGridAreas(isShown)
* @param {Boolean} isShown
* Shows all the grid area highlights for the current grid if isShown is true.
* - showGridArea(areaName)
* @param {String} areaName
* Shows the grid area highlight for the given area name.
* - showGridAreasOverlay(isShown)
* @param {Boolean} isShown
* Displays an overlay of all the grid areas for the current grid container if
* isShown is true.
* - showGridCell({ gridFragmentIndex: Number, rowNumber: Number, columnNumber: Number })
* @param {Object} { gridFragmentIndex: Number, rowNumber: Number,
* columnNumber: Number }
* An object containing the grid fragment index, row and column numbers to the
* corresponding grid cell to highlight for the current grid.
* - showGridLineNames({ gridFragmentIndex: Number, lineNumber: Number,
* type: String })
* @param {Object} { gridFragmentIndex: Number, lineNumber: Number }
* An object containing the grid fragment index and line number to the
* corresponding grid line to highlight for the current grid.
* - showGridLineNumbers(isShown)
* @param {Boolean} isShown
* Displays the grid line numbers on the grid lines if isShown is true.
* - showInfiniteLines(isShown)
* @param {Boolean} isShown
* Displays an infinite line to represent the grid lines if isShown is true.
*
* Structure:
* <div class="highlighter-container">
* <canvas id="css-grid-canvas" class="css-grid-canvas">
* <svg class="css-grid-elements" hidden="true">
* <g class="css-grid-regions">
* <path class="css-grid-areas" points="..." />
* <path class="css-grid-cells" points="..." />
* </g>
* </svg>
* <div class="css-grid-area-infobar-container">
* <div class="css-grid-infobar">
* <div class="css-grid-infobar-text">
* <span class="css-grid-area-infobar-name">Grid Area Name</span>
* <span class="css-grid-area-infobar-dimensions">Grid Area Dimensions></span>
* </div>
* </div>
* </div>
* <div class="css-grid-cell-infobar-container">
* <div class="css-grid-infobar">
* <div class="css-grid-infobar-text">
* <span class="css-grid-cell-infobar-position">Grid Cell Position</span>
* <span class="css-grid-cell-infobar-dimensions">Grid Cell Dimensions></span>
* </div>
* </div>
* <div class="css-grid-line-infobar-container">
* <div class="css-grid-infobar">
* <div class="css-grid-infobar-text">
* <span class="css-grid-line-infobar-number">Grid Line Number</span>
* <span class="css-grid-line-infobar-names">Grid Line Names></span>
* </div>
* </div>
* </div>
* </div>
*/
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.onPageHide = this.onPageHide.bind(this);
this.onWillNavigate = this.onWillNavigate.bind(this);
this.highlighterEnv.on("will-navigate", this.onWillNavigate);
let { 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
};
// Calling `calculateCanvasPosition` anyway since the highlighter could be initialized
// on a page that has scrolled already.
this.calculateCanvasPosition();
}
_buildMarkup() {
let container = createNode(this.win, {
attributes: {
"class": "highlighter-container"
}
});
let root = createNode(this.win, {
parent: container,
attributes: {
"id": "root",
"class": "root"
},
prefix: this.ID_CLASS_PREFIX
});
// We use a <canvas> 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.
createNode(this.win, {
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
let svg = createSVGNode(this.win, {
nodeType: "svg",
parent: root,
attributes: {
"id": "elements",
"width": "100%",
"height": "100%",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
let regions = createSVGNode(this.win, {
nodeType: "g",
parent: svg,
attributes: {
"class": "regions"
},
prefix: this.ID_CLASS_PREFIX
});
createSVGNode(this.win, {
nodeType: "path",
parent: regions,
attributes: {
"class": "areas",
"id": "areas"
},
prefix: this.ID_CLASS_PREFIX
});
createSVGNode(this.win, {
nodeType: "path",
parent: regions,
attributes: {
"class": "cells",
"id": "cells"
},
prefix: this.ID_CLASS_PREFIX
});
// Building the grid area infobar markup
let areaInfobarContainer = createNode(this.win, {
parent: container,
attributes: {
"class": "area-infobar-container",
"id": "area-infobar-container",
"position": "top",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
let areaInfobar = createNode(this.win, {
parent: areaInfobarContainer,
attributes: {
"class": "infobar"
},
prefix: this.ID_CLASS_PREFIX
});
let areaTextbox = createNode(this.win, {
parent: areaInfobar,
attributes: {
"class": "infobar-text"
},
prefix: this.ID_CLASS_PREFIX
});
createNode(this.win, {
nodeType: "span",
parent: areaTextbox,
attributes: {
"class": "area-infobar-name",
"id": "area-infobar-name"
},
prefix: this.ID_CLASS_PREFIX
});
createNode(this.win, {
nodeType: "span",
parent: areaTextbox,
attributes: {
"class": "area-infobar-dimensions",
"id": "area-infobar-dimensions"
},
prefix: this.ID_CLASS_PREFIX
});
// Building the grid cell infobar markup
let cellInfobarContainer = createNode(this.win, {
parent: container,
attributes: {
"class": "cell-infobar-container",
"id": "cell-infobar-container",
"position": "top",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
let cellInfobar = createNode(this.win, {
parent: cellInfobarContainer,
attributes: {
"class": "infobar"
},
prefix: this.ID_CLASS_PREFIX
});
let cellTextbox = createNode(this.win, {
parent: cellInfobar,
attributes: {
"class": "infobar-text"
},
prefix: this.ID_CLASS_PREFIX
});
createNode(this.win, {
nodeType: "span",
parent: cellTextbox,
attributes: {
"class": "cell-infobar-position",
"id": "cell-infobar-position"
},
prefix: this.ID_CLASS_PREFIX
});
createNode(this.win, {
nodeType: "span",
parent: cellTextbox,
attributes: {
"class": "cell-infobar-dimensions",
"id": "cell-infobar-dimensions"
},
prefix: this.ID_CLASS_PREFIX
});
// Building the grid line infobar markup
let lineInfobarContainer = createNode(this.win, {
parent: container,
attributes: {
"class": "line-infobar-container",
"id": "line-infobar-container",
"position": "top",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
let lineInfobar = createNode(this.win, {
parent: lineInfobarContainer,
attributes: {
"class": "infobar"
},
prefix: this.ID_CLASS_PREFIX
});
let lineTextbox = createNode(this.win, {
parent: lineInfobar,
attributes: {
"class": "infobar-text"
},
prefix: this.ID_CLASS_PREFIX
});
createNode(this.win, {
nodeType: "span",
parent: lineTextbox,
attributes: {
"class": "line-infobar-number",
"id": "line-infobar-number"
},
prefix: this.ID_CLASS_PREFIX
});
createNode(this.win, {
nodeType: "span",
parent: lineTextbox,
attributes: {
"class": "line-infobar-names",
"id": "line-infobar-names"
},
prefix: this.ID_CLASS_PREFIX
});
return container;
}
destroy() {
let { highlighterEnv } = this;
highlighterEnv.off("will-navigate", this.onWillNavigate);
let { 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);
}
getElement(id) {
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
}
get ctx() {
return this.canvas.getCanvasContext("2d");
}
get canvas() {
return this.getElement("canvas");
}
get color() {
return this.options.color || DEFAULT_GRID_COLOR;
}
/**
* 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.
let canvas = createNode(this.win, { nodeType: "canvas" });
let width = canvas.width = GRID_GAP_PATTERN_WIDTH * devicePixelRatio;
let height = canvas.height = GRID_GAP_PATTERN_HEIGHT * devicePixelRatio;
let ctx = canvas.getContext("2d");
ctx.save();
ctx.setLineDash(GRID_GAP_PATTERN_LINE_DASH);
ctx.beginPath();
ctx.translate(.5, .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;
ctx.stroke();
ctx.restore();
let pattern = ctx.createPattern(canvas, "repeat");
gridPatternMap.set(dimension, pattern);
gCachedGridPattern.set(devicePixelRatio, gridPatternMap);
return pattern;
}
onPageHide({ target }) {
// If a page hide event is triggered for current window's highlighter, hide the
// highlighter.
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();
}
}
_show() {
if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) && !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();
}
_clearCache() {
gCachedGridPattern.clear();
}
/**
* Shows the grid area highlight for the given area name.
*
* @param {String} areaName
* Grid area name.
*/
showGridArea(areaName) {
this.renderGridArea(areaName);
}
/**
* Shows all the grid area highlights for the current grid.
*/
showAllGridAreas() {
this.renderGridArea();
}
/**
* Clear the grid area highlights.
*/
clearGridAreas() {
let areas = this.getElement("areas");
areas.setAttribute("d", "");
}
/**
* 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);
}
/**
* Clear the grid cell highlights.
*/
clearGridCell() {
let cells = this.getElement("cells");
cells.setAttribute("d", "");
}
/**
* 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.getGridFragments().length > 0;
}
/**
* 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 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).
*/
_hasMoved() {
let hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
let oldGridData = stringifyGridFragments(this.gridData);
this.gridData = this.currentNode.getGridFragments();
let newGridData = stringifyGridFragments(this.gridData);
return hasMoved || oldGridData !== newGridData;
}
/**
* 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);
let root = this.getElement("root");
let cells = this.getElement("cells");
let areas = this.getElement("areas");
// 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;
// Set the grid cells and areas fill to the current grid colour.
cells.setAttribute("style", `fill: ${this.color}`);
areas.setAttribute("style", `fill: ${this.color}`);
let { width, height } = this._winDimensions;
// Updates the <canvas> element's position and size.
// It also clear the <canvas>'s drawing context.
this.updateCanvasElement();
// Clear the grid area highlights.
this.clearGridAreas();
this.clearGridCell();
// Update the current matrix used in our canvas' rendering
this.updateCurrentMatrix();
// 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) {
let { width, height } = bounds;
let 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);
let container = this.getElement("area-infobar-container");
moveInfobar(container, bounds, this.win, {
position: "bottom",
hideIfOffscreen: true
});
}
/**
* 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) {
let { width, height } = bounds;
let dim = parseFloat(width.toPrecision(6)) +
" \u00D7 " +
parseFloat(height.toPrecision(6));
let position = LAYOUT_L10N.getFormatStr("layout.rowColumnPositions",
rowNumber, columnNumber);
this.getElement("cell-infobar-position").setTextContent(position);
this.getElement("cell-infobar-dimensions").setTextContent(dim);
let container = this.getElement("cell-infobar-container");
moveInfobar(container, bounds, this.win, {
position: "top",
hideIfOffscreen: true
});
}
/**
* 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);
let container = this.getElement("line-infobar-container");
moveInfobar(container,
getBoundsFromPoints([{x, y}, {x, y}, {x, y}, {x, y}]), this.win);
}
/**
* 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() {
let hasPositionChanged = this.calculateCanvasPosition();
if (hasPositionChanged) {
this._update();
}
}
/**
* This method is responsible to do the math that updates the <canvas>'s position,
* in accordance with the page's scroll, document's size, canvas size, and
* viewport's size.
* It's called when a page's scroll is detected.
*
* @return {Boolean} `true` if the <canvas> position was updated, `false` otherwise.
*/
calculateCanvasPosition() {
let cssCanvasSize = CANVAS_SIZE / this.win.devicePixelRatio;
let viewportSize = getViewportDimensions(this.win);
let documentSize = this._winDimensions;
let pageX = this._scroll.x;
let pageY = this._scroll.y;
let canvasWidth = cssCanvasSize;
let canvasHeight = cssCanvasSize;
let hasUpdated = false;
// Those values indicates the relative horizontal and vertical space the page can
// scroll before we have to reposition the <canvas>; they're 1/4 of the delta between
// the canvas' size and the viewport's size: that's because we want to consider both
// sides (top/bottom, left/right; so 1/2 for each side) and also we don't want to
// shown the edges of the canvas in case of fast scrolling (to avoid showing undraw
// areas, therefore another 1/2 here).
let bufferSizeX = (canvasWidth - viewportSize.width) >> 2;
let bufferSizeY = (canvasHeight - viewportSize.height) >> 2;
let { x, y } = this._canvasPosition;
// Defines the boundaries for the canvas.
let topBoundary = 0;
let bottomBoundary = documentSize.height - canvasHeight;
let leftBoundary = 0;
let rightBoundary = documentSize.width - canvasWidth;
// Defines the thresholds that triggers the canvas' position to be updated.
let topThreshold = pageY - bufferSizeY;
let bottomThreshold = pageY - canvasHeight + viewportSize.height + bufferSizeY;
let leftThreshold = pageX - bufferSizeX;
let rightThreshold = pageX - canvasWidth + viewportSize.width + bufferSizeX;
if (y < bottomBoundary && y < bottomThreshold) {
this._canvasPosition.y = Math.min(topThreshold, bottomBoundary);
hasUpdated = true;
} else if (y > topBoundary && y > topThreshold) {
this._canvasPosition.y = Math.max(bottomThreshold, topBoundary);
hasUpdated = true;
}
if (x < rightBoundary && x < rightThreshold) {
this._canvasPosition.x = Math.min(leftThreshold, rightBoundary);
hasUpdated = true;
} else if (x > leftBoundary && x > leftThreshold) {
this._canvasPosition.x = Math.max(rightThreshold, leftBoundary);
hasUpdated = true;
}
return hasUpdated;
}
/**
* Updates the <canvas> element's style in accordance with the current window's
* devicePixelRatio, and the position calculated in `calculateCanvasPosition`; it also
* clears the drawing context.
*/
updateCanvasElement() {
let size = CANVAS_SIZE / this.win.devicePixelRatio;
let { x, y } = this._canvasPosition;
// Resize the canvas taking the dpr into account so as to have crisp lines, and
// translating it to give the perception that it always covers the viewport.
this.canvas.setAttribute("style",
`width:${size}px;height:${size}px; transform: translate(${x}px, ${y}px);`);
this.ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
}
/**
* Updates the current matrices for both canvas drawing and SVG, taking in account the
* following transformations, in this order:
* 1. The scale given by the display pixel ratio.
* 2. The translation to the top left corner of the element.
* 3. The scale given by the current zoom.
* 4. The translation given by the top and left padding of the element.
* 5. Any CSS transformation applied directly to the element (only 2D
* transformation; the 3D transformation are flattened, see `dom-matrix-2d` module
* for further details.)
*
* The transformations of the element's ancestors are not currently computed (see
* bug 1355675).
*/
updateCurrentMatrix() {
let computedStyle = this.currentNode.ownerGlobal.getComputedStyle(this.currentNode);
let paddingTop = parseFloat(computedStyle.paddingTop);
let paddingLeft = parseFloat(computedStyle.paddingLeft);
let borderTop = parseFloat(computedStyle.borderTopWidth);
let borderLeft = parseFloat(computedStyle.borderLeftWidth);
let nodeMatrix = getNodeTransformationMatrix(this.currentNode,
this.win.document.documentElement);
let m = identity();
// First, we scale based on the device pixel ratio.
m = multiply(m, scale(this.win.devicePixelRatio));
// Then, we apply the current node's transformation matrix, relative to the
// inspected window's root element, but only if it's not a identity matrix.
if (isIdentity(nodeMatrix)) {
this.hasNodeTransformations = false;
} else {
m = multiply(m, nodeMatrix);
this.hasNodeTransformations = true;
}
// Finally, we translate the origin based on the node's padding and border values.
m = multiply(m, translate(paddingLeft + borderLeft, paddingTop + borderTop));
this.currentMatrix = m;
}
getFirstRowLinePos(fragment) {
return fragment.rows.lines[0].start;
}
getLastRowLinePos(fragment) {
return fragment.rows.lines[fragment.rows.lines.length - 1].start;
}
getFirstColLinePos(fragment) {
return fragment.cols.lines[0].start;
}
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;
}
renderFragment(fragment) {
if (!this.isValidFragment(fragment)) {
return;
}
this.renderLines(fragment.cols, COLUMNS, "left", "top", "height",
this.getFirstRowLinePos(fragment),
this.getLastRowLinePos(fragment));
this.renderLines(fragment.rows, ROWS, "top", "left", "width",
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, "left", "top",
this.getFirstRowLinePos(fragment));
this.renderLineNumbers(fragment.rows, ROWS, "top", "left",
this.getFirstColLinePos(fragment));
if (Services.prefs.getBoolPref(NEGATIVE_LINE_NUMBERS_PREF)) {
this.renderNegativeLineNumbers(fragment.cols, COLUMNS, "left", "top",
this.getLastRowLinePos(fragment));
this.renderNegativeLineNumbers(fragment.rows, ROWS, "top", "left",
this.getLastColLinePos(fragment));
}
}
}
/**
* Render the negative grid lines given the grid dimension information of the
* column or row lines.
*
* See @param for renderLines.
*/
renderNegativeLineNumbers(gridDimension, dimensionType, mainSide, crossSide,
startPos) {
let lineStartPos = startPos;
const { lines } = gridDimension;
for (let i = 0, line = lines[i]; i < lines.length; line = lines[++i]) {
let linePos = line.start;
const negativeLineNumber = i - lines.length;
this.renderGridLineNumber(negativeLineNumber, linePos, lineStartPos, line.breadth,
dimensionType);
}
}
/**
* Renders the grid area overlay on the css grid highlighter canvas.
*/
renderGridAreaOverlay() {
let padding = 1;
for (let i = 0; i < this.gridData.length; i++) {
let fragment = this.gridData[i];
for (let area of fragment.areas) {
let { 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);
}
}
this.ctx.restore();
}
/**
* 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) {
let { rowStart, rowEnd, columnStart, columnEnd } = area;
let { devicePixelRatio } = this.win;
let displayPixelRatio = getDisplayPixelRatio(this.win);
let offset = (displayPixelRatio / 2) % 1;
let fontSize = (GRID_AREA_NAME_FONT_SIZE * displayPixelRatio);
this.ctx.save();
let canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
let canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
this.ctx.translate(offset - canvasX, offset - canvasY);
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
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++) {
let row = fragment.rows.tracks[rowNumber - 1];
let column = fragment.cols.tracks[columnNumber - 1];
// Check if the font size is exceeds the bounds of the containing grid cell.
if (fontSize > (column.breadth * displayPixelRatio) ||
fontSize > (row.breadth * displayPixelRatio)) {
fontSize = (column.breadth + row.breadth) / 2;
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
}
let textWidth = this.ctx.measureText(area.name).width;
// The width of the character 'm' approximates the height of the text.
let textHeight = this.ctx.measureText("m").width;
// Padding in pixels for the line number text inside of the line number container.
let padding = 3 * displayPixelRatio;
let boxWidth = textWidth + 2 * padding;
let 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]);
let rectXPos = x - boxWidth / 2;
let 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";
let 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();
}
/**
* 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 {String} mainSide
* The main side of the given grid dimension - "top" for rows and
* "left" for columns.
* @param {String} crossSide
* The cross side of the given grid dimension - "left" for rows and
* "top" for columns.
* @param {String} mainSize
* The main size of the given grid dimension - "width" for rows and
* "height" for columns.
* @param {Number} startPos
* The start position of the cross side of the grid dimension.
* @param {Number} endPos
* The end position of the cross side of the grid dimension.
*/
renderLines(gridDimension, dimensionType, mainSide, crossSide,
mainSize, startPos, endPos) {
let lineStartPos = startPos;
let lineEndPos = endPos;
let lastEdgeLineIndex = this.getLastEdgeLineIndex(gridDimension.tracks);
for (let i = 0; i < gridDimension.lines.length; i++) {
let line = gridDimension.lines[i];
let linePos = line.start;
if (i == 0 || i == lastEdgeLineIndex) {
this.renderLine(linePos, lineStartPos, lineEndPos, dimensionType, "edge");
} else {
this.renderLine(linePos, lineStartPos, lineEndPos, dimensionType,
gridDimension.tracks[i - 1].type);
}
// Render a second line to illustrate the gutter for non-zero breadth.
if (line.breadth > 0) {
this.renderGridGap(linePos, lineStartPos, lineEndPos, line.breadth,
dimensionType);
this.renderLine(linePos + line.breadth, lineStartPos, lineEndPos, dimensionType,
gridDimension.tracks[i].type);
}
}
}
/**
* Render the grid lines given the grid dimension information of the
* column or row lines.
*
* see @param for renderLines.
*/
renderLineNumbers(gridDimension, dimensionType, mainSide, crossSide,
startPos) {
let lineStartPos = startPos;
// Keep track of the number of collapsed lines per line position
let stackedLines = [];
for (let i = 0; i < gridDimension.lines.length; i++) {
let line = gridDimension.lines[i];
let linePos = line.start;
// 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; any implicit grid lines
// before 1 get negative numbers, but do not get any positivity numbers.
// Since here we're rendering only the positive line numbers, we have to skip any
// implicit grid lines before the first tha is explicit.
// For such lines the API returns always 0 as line's number.
if (line.number === 0) {
continue;
}
// Check for overlapping lines. We render a second box beneath the last overlapping
// line number to indicate there are lines beneath it.
const gridLine = gridDimension.tracks[line.number - 1];
if (gridLine) {
const { breadth } = gridLine;
if (breadth === 0) {
stackedLines.push(gridDimension.lines[i].number);
if (stackedLines.length > 0) {
this.renderGridLineNumber(line.number, linePos, lineStartPos, line.breadth,
dimensionType, 1);
}
continue;
}
}
this.renderGridLineNumber(line.number, linePos, lineStartPos, line.breadth,
dimensionType);
}
}
/**
* 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) {
let { devicePixelRatio } = this.win;
let lineWidth = getDisplayPixelRatio(this.win);
let offset = (lineWidth / 2) % 1;
let x = Math.round(this._canvasPosition.x * devicePixelRatio);
let y = 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.beginPath();
this.ctx.translate(offset - x, offset - y);
let lineOptions = {
matrix: this.currentMatrix
};
if (this.options.showInfiniteLines) {
lineOptions.extendToBoundaries = [x, y, x + CANVAS_SIZE, y + 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;
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 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 {Number||undefined} stackedLineIndex
* The line index position of the stacked line.
*/
renderGridLineNumber(lineNumber, linePos, startPos, breadth, dimensionType,
stackedLineIndex) {
let displayPixelRatio = getDisplayPixelRatio(this.win);
let { devicePixelRatio } = this.win;
let offset = (displayPixelRatio / 2) % 1;
linePos = Math.round(linePos);
startPos = Math.round(startPos);
breadth = Math.round(breadth);
if (linePos + breadth < 0) {
// The line is not visible on screen, don't render the line number
return;
}
this.ctx.save();
let canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
let canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
this.ctx.translate(offset - canvasX, offset - canvasY);
let fontSize = (GRID_FONT_SIZE * displayPixelRatio);
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
let textWidth = this.ctx.measureText(lineNumber).width;
// The width of the character 'm' approximates the height of the text.
let textHeight = this.ctx.measureText("m").width;
// Padding in pixels for the line number text inside of the line number container.
let padding = 3 * displayPixelRatio;
let boxWidth = textWidth + 2 * padding;
let boxHeight = textHeight + 2 * padding;
// Calculate the x & y coordinates for the line number container, so that it is
// centered on the line, and in the middle of the gap if there is any.
let x, y;
let startOffset = (boxHeight + 2) / devicePixelRatio;
if (Services.prefs.getBoolPref(NEGATIVE_LINE_NUMBERS_PREF)) {
// If the line number is negative, offset it from the grid container edge,
// (downwards if its a column, rightwards if its a row).
if (lineNumber < 0) {
startPos += startOffset;
} else {
startPos -= startOffset;
}
}
if (dimensionType === COLUMNS) {
x = linePos + breadth / 2;
y = startPos;
} else {
x = startPos;
y = linePos + breadth / 2;
}
[x, y] = apply(this.currentMatrix, [x, y]);
x -= boxWidth / 2;
y -= boxHeight / 2;
if (stackedLineIndex) {
// Offset the stacked line number by half of the box's width/height
const xOffset = boxWidth / 4;
const yOffset = boxHeight / 4;
x += xOffset;
y += yOffset;
}
if (!this.hasNodeTransformations) {
x = Math.max(x, padding);
y = Math.max(y, padding);
}
// Draw a rounded rectangle 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";
let radius = 2 * displayPixelRatio;
drawRoundedRect(this.ctx, x, y, boxWidth, boxHeight, radius);
// Write the line number inside of the rectangle.
this.ctx.fillStyle = "black";
const numberText = stackedLineIndex ? "" : lineNumber;
this.ctx.fillText(numberText, x + padding, y + textHeight + padding);
this.ctx.restore();
}
/**
* 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) {
let { devicePixelRatio } = this.win;
let displayPixelRatio = getDisplayPixelRatio(this.win);
let offset = (displayPixelRatio / 2) % 1;
let canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
let 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);
}
this.ctx.fill();
this.ctx.restore();
}
/**
* 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) {
let paths = [];
let { devicePixelRatio } = this.win;
let displayPixelRatio = getDisplayPixelRatio(this.win);
for (let i = 0; i < this.gridData.length; i++) {
let fragment = this.gridData[i];
for (let area of fragment.areas) {
if (areaName && areaName != area.name) {
continue;
}
let rowStart = fragment.rows.lines[area.rowStart - 1];
let rowEnd = fragment.rows.lines[area.rowEnd - 1];
let columnStart = fragment.cols.lines[area.columnStart - 1];
let columnEnd = fragment.cols.lines[area.columnEnd - 1];
let x1 = columnStart.start + columnStart.breadth;
let y1 = rowStart.start + rowStart.breadth;
let x2 = columnEnd.start;
let y2 = rowEnd.start;
let points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix);
// Scale down by `devicePixelRatio` since SVG element already take them into
// account.
let 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`.
let 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);
}
}
}
let areas = this.getElement("areas");
areas.setAttribute("d", paths.join(" "));
}
/**
* 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) {
let fragment = this.gridData[gridFragmentIndex];
if (!fragment) {
return;
}
let row = fragment.rows.tracks[rowNumber - 1];
let column = fragment.cols.tracks[columnNumber - 1];
if (!row || !column) {
return;
}
let x1 = column.start;
let y1 = row.start;
let x2 = column.start + column.breadth;
let y2 = row.start + row.breadth;
let { devicePixelRatio } = this.win;
let displayPixelRatio = getDisplayPixelRatio(this.win);
let points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix);
// Scale down by `devicePixelRatio` since SVG element already take them into account.
let 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`.
let bounds = getBoundsFromPoints(points.map(point => ({
x: Math.round(point.x / displayPixelRatio),
y: Math.round(point.y / displayPixelRatio)
})));
let cells = this.getElement("cells");
cells.setAttribute("d", getPathDescriptionFromPoints(svgPoints));
this._showGridCellInfoBar();
this._updateGridCellInfobar(rowNumber, columnNumber, bounds);
}
/**
* 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) {
let 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;
}
let currentZoom = getCurrentZoom(this.win);
let { bounds } = this.currentQuads.content[gridFragmentIndex];
const rowYPosition = fragment.rows.lines[0];
const colXPosition = fragment.rows.lines[0];
let x = dimensionType === COLUMNS
? linePos.start + (bounds.left / currentZoom)
: colXPosition.start + (bounds.left / currentZoom);
let y = dimensionType === ROWS
? linePos.start + (bounds.top / currentZoom)
: rowYPosition.start + (bounds.top / currentZoom);
this._showGridLineInfoBar();
this._updateGridLineInfobar(names.join(", "), lineNumber, x, y);
}
/**
* 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");
}
_showGrid() {
this.getElement("canvas").removeAttribute("hidden");
}
_hideGridElements() {
this.getElement("elements").setAttribute("hidden", "true");
}
_showGridElements() {
this.getElement("elements").removeAttribute("hidden");
}
_hideGridAreaInfoBar() {
this.getElement("area-infobar-container").setAttribute("hidden", "true");
}
_showGridAreaInfoBar() {
this.getElement("area-infobar-container").removeAttribute("hidden");
}
_hideGridCellInfoBar() {
this.getElement("cell-infobar-container").setAttribute("hidden", "true");
}
_showGridCellInfoBar() {
this.getElement("cell-infobar-container").removeAttribute("hidden");
}
_hideGridLineInfoBar() {
this.getElement("line-infobar-container").setAttribute("hidden", "true");
}
_showGridLineInfoBar() {
this.getElement("line-infobar-container").removeAttribute("hidden");
}
}
exports.CssGridHighlighter = CssGridHighlighter;