/* 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 { extend } = require("sdk/core/heritage"); const { AutoRefreshHighlighter } = require("./auto-refresh"); const { CanvasFrameAnonymousContentHelper, createNode, createSVGNode, getBindingElementAndPseudo, hasPseudoClassLock, isNodeValid, moveInfobar, } = require("./utils/markup"); const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils"); const inspector = require("devtools/server/actors/inspector"); const nodeConstants = require("devtools/shared/dom-node-constants"); // Note that the order of items in this array is important because it is used // for drawing the BoxModelHighlighter's path elements correctly. const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"]; const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"]; // Width of boxmodelhighlighter guides const GUIDE_STROKE_WIDTH = 1; // FIXME: add ":visited" and ":link" after bug 713106 is fixed const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; /** * The BoxModelHighlighter draws the box model regions on top of a node. * If the node is a block box, then each region will be displayed as 1 polygon. * If the node is an inline box though, each region may be represented by 1 or * more polygons, depending on how many line boxes the inline element has. * * Usage example: * * let h = new BoxModelHighlighter(env); * h.show(node, options); * h.hide(); * h.destroy(); * * Available options: * - region {String} * "content", "padding", "border" or "margin" * This specifies the region that the guides should outline. * Defaults to "content" * - hideGuides {Boolean} * Defaults to false * - hideInfoBar {Boolean} * Defaults to false * - showOnly {String} * "content", "padding", "border" or "margin" * If set, only this region will be highlighted. Use with onlyRegionArea to * only highlight the area of the region. * - onlyRegionArea {Boolean} * This can be set to true to make each region's box only highlight the area * of the corresponding region rather than the area of nested regions too. * This is useful when used with showOnly. * * Structure: *
*
* *
*
*
*
* Node name * Node id * .someClass * :hover *
*
*
*
*
*
*/ function BoxModelHighlighter(highlighterEnv) { AutoRefreshHighlighter.call(this, highlighterEnv); this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv, this._buildMarkup.bind(this)); /** * Optionally customize each region's fill color by adding an entry to the * regionFill property: `highlighter.regionFill.margin = "red"; */ this.regionFill = {}; this.onWillNavigate = this.onWillNavigate.bind(this); this.highlighterEnv.on("will-navigate", this.onWillNavigate); } BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, { typeName: "BoxModelHighlighter", ID_CLASS_PREFIX: "box-model-", _buildMarkup: function () { let doc = this.win.document; let highlighterContainer = doc.createElement("div"); highlighterContainer.className = "highlighter-container box-model"; // Build the root wrapper, used to adapt to the page zoom. let rootWrapper = createNode(this.win, { parent: highlighterContainer, attributes: { "id": "root", "class": "root" }, prefix: this.ID_CLASS_PREFIX }); // Building the SVG element with its polygons and lines let svg = createSVGNode(this.win, { nodeType: "svg", parent: rootWrapper, attributes: { "id": "elements", "width": "100%", "height": "100%", "hidden": "true" }, prefix: this.ID_CLASS_PREFIX }); let regions = createSVGNode(this.win, { nodeType: "g", parent: svg, attributes: { "class": "regions" }, prefix: this.ID_CLASS_PREFIX }); for (let region of BOX_MODEL_REGIONS) { createSVGNode(this.win, { nodeType: "path", parent: regions, attributes: { "class": region, "id": region }, prefix: this.ID_CLASS_PREFIX }); } for (let side of BOX_MODEL_SIDES) { createSVGNode(this.win, { nodeType: "line", parent: svg, attributes: { "class": "guide-" + side, "id": "guide-" + side, "stroke-width": GUIDE_STROKE_WIDTH }, prefix: this.ID_CLASS_PREFIX }); } // Building the nodeinfo bar markup let infobarContainer = createNode(this.win, { parent: rootWrapper, attributes: { "class": "infobar-container", "id": "infobar-container", "position": "top", "hidden": "true" }, prefix: this.ID_CLASS_PREFIX }); let infobar = createNode(this.win, { parent: infobarContainer, attributes: { "class": "infobar" }, prefix: this.ID_CLASS_PREFIX }); let texthbox = createNode(this.win, { parent: infobar, attributes: { "class": "infobar-text" }, prefix: this.ID_CLASS_PREFIX }); createNode(this.win, { nodeType: "span", parent: texthbox, attributes: { "class": "infobar-tagname", "id": "infobar-tagname" }, prefix: this.ID_CLASS_PREFIX }); createNode(this.win, { nodeType: "span", parent: texthbox, attributes: { "class": "infobar-id", "id": "infobar-id" }, prefix: this.ID_CLASS_PREFIX }); createNode(this.win, { nodeType: "span", parent: texthbox, attributes: { "class": "infobar-classes", "id": "infobar-classes" }, prefix: this.ID_CLASS_PREFIX }); createNode(this.win, { nodeType: "span", parent: texthbox, attributes: { "class": "infobar-pseudo-classes", "id": "infobar-pseudo-classes" }, prefix: this.ID_CLASS_PREFIX }); createNode(this.win, { nodeType: "span", parent: texthbox, attributes: { "class": "infobar-dimensions", "id": "infobar-dimensions" }, prefix: this.ID_CLASS_PREFIX }); return highlighterContainer; }, /** * Destroy the nodes. Remove listeners. */ destroy: function () { this.highlighterEnv.off("will-navigate", this.onWillNavigate); this.markup.destroy(); AutoRefreshHighlighter.prototype.destroy.call(this); }, getElement: function (id) { return this.markup.getElement(this.ID_CLASS_PREFIX + id); }, /** * Override the AutoRefreshHighlighter's _isNodeValid method to also return true for * text nodes since these can also be highlighted. * @param {DOMNode} node * @return {Boolean} */ _isNodeValid: function (node) { return node && (isNodeValid(node) || isNodeValid(node, nodeConstants.TEXT_NODE)); }, /** * Show the highlighter on a given node */ _show: function () { if (BOX_MODEL_REGIONS.indexOf(this.options.region) == -1) { this.options.region = "content"; } let shown = this._update(); this._trackMutations(); this.emit("ready"); return shown; }, /** * Track the current node markup mutations so that the node info bar can be * updated to reflects the node's attributes */ _trackMutations: function () { if (isNodeValid(this.currentNode)) { let win = this.currentNode.ownerDocument.defaultView; this.currentNodeObserver = new win.MutationObserver(this.update); this.currentNodeObserver.observe(this.currentNode, {attributes: true}); } }, _untrackMutations: function () { if (isNodeValid(this.currentNode) && this.currentNodeObserver) { this.currentNodeObserver.disconnect(); this.currentNodeObserver = null; } }, /** * Update the highlighter on the current highlighted node (the one that was * passed as an argument to show(node)). * Should be called whenever node size or attributes change */ _update: function () { let shown = false; setIgnoreLayoutChanges(true); if (this._updateBoxModel()) { // Show the infobar only if configured to do so and the node is an element or a text // node. if (!this.options.hideInfoBar && ( this.currentNode.nodeType === this.currentNode.ELEMENT_NODE || this.currentNode.nodeType === this.currentNode.TEXT_NODE)) { this._showInfobar(); } else { this._hideInfobar(); } this._showBoxModel(); shown = true; } else { // Nothing to highlight (0px rectangle like a