/* 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,
getCurrentZoom,
} = 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