diff options
Diffstat (limited to 'devtools/server/actors/highlighters/css-grid.js')
-rw-r--r-- | devtools/server/actors/highlighters/css-grid.js | 1958 |
1 files changed, 1958 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..bed5b106c5 --- /dev/null +++ b/devtools/server/actors/highlighters/css-grid.js @@ -0,0 +1,1958 @@ +/* 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("devtools/server/actors/highlighters/auto-refresh"); +const { + CANVAS_SIZE, + DEFAULT_COLOR, + drawBubbleRect, + drawLine, + drawRect, + drawRoundedRect, + getBoundsFromPoints, + getCurrentMatrix, + getPathDescriptionFromPoints, + getPointsFromDiagonal, + updateCanvasElement, + updateCanvasPosition, +} = require("devtools/server/actors/highlighters/utils/canvas"); +const { + CanvasFrameAnonymousContentHelper, + getComputedStyle, + moveInfobar, +} = require("devtools/server/actors/highlighters/utils/markup"); +const { apply } = require("devtools/shared/layout/dom-matrix-2d"); +const { + getCurrentZoom, + getDisplayPixelRatio, + getWindowDimensions, + setIgnoreLayoutChanges, +} = require("devtools/shared/layout/utils"); +const { + stringifyGridFragments, +} = require("devtools/server/actors/utils/css-grid-utils"); +const { LocalizationHelper } = require("devtools/shared/l10n"); + +const STRINGS_URI = "devtools/shared/locales/highlighters.properties"; +const L10N = new LocalizationHelper(STRINGS_URI); + +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). + */ + _hasMoved() { + const hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this); + + const oldGridData = stringifyGridFragments(this.gridData); + this.gridData = this.currentNode.getGridFragments(); + const newGridData = stringifyGridFragments(this.gridData); + + return hasMoved || oldGridData !== newGridData; + } + + /** + * 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.setStyle("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", + 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) { + const { width, height } = bounds; + const dim = + parseFloat(width.toPrecision(6)) + + " \u00D7 " + + parseFloat(height.toPrecision(6)); + const position = L10N.getFormatStr( + "grid.rowColumnPositions", + rowNumber, + 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", + 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); + + const container = this.getElement("line-infobar-container"); + moveInfobar( + container, + getBoundsFromPoints([ + { x, y }, + { x, y }, + { x, y }, + { x, y }, + ]), + this.win + ); + } +} + +exports.CssGridHighlighter = CssGridHighlighter; |