summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/highlighters/css-grid.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/server/actors/highlighters/css-grid.js1962
1 files changed, 1962 insertions, 0 deletions
diff --git a/devtools/server/actors/highlighters/css-grid.js b/devtools/server/actors/highlighters/css-grid.js
new file mode 100644
index 0000000000..04c612eb02
--- /dev/null
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -0,0 +1,1962 @@
+/* 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:
+ * <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.isReady = this.markup.initialize();
+
+ this.onPageHide = this.onPageHide.bind(this);
+ this.onWillNavigate = this.onWillNavigate.bind(this);
+
+ this.highlighterEnv.on("will-navigate", this.onWillNavigate);
+
+ const { pageListenerTarget } = highlighterEnv;
+ pageListenerTarget.addEventListener("pagehide", this.onPageHide);
+
+ // Initialize the <canvas> position to the top left corner of the page.
+ this._canvasPosition = {
+ x: 0,
+ y: 0,
+ };
+
+ // Calling `updateCanvasPosition` anyway since the highlighter could be initialized
+ // on a page that has scrolled already.
+ updateCanvasPosition(
+ this._canvasPosition,
+ this._scroll,
+ this.win,
+ this._winDimensions
+ );
+ }
+
+ _buildMarkup() {
+ const container = this.markup.createNode({
+ attributes: {
+ class: "highlighter-container",
+ },
+ });
+
+ const root = this.markup.createNode({
+ parent: container,
+ attributes: {
+ id: "root",
+ class: "root",
+ },
+ prefix: this.ID_CLASS_PREFIX,
+ });
+
+ // We use a <canvas> element 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 <canvas>'s position needs to be updated if the page scrolls too much, in order
+ * to give the illusion that it always covers the viewport.
+ */
+ _scrollUpdate() {
+ const hasUpdated = updateCanvasPosition(
+ this._canvasPosition,
+ this._scroll,
+ this.win,
+ this._winDimensions
+ );
+
+ if (hasUpdated) {
+ this._update();
+ }
+ }
+
+ _show() {
+ 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 <canvas> element's position and size.
+ // It also clear the <canvas>'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;