--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -221,21 +221,17 @@ MarkupView.prototype = {
if (target.tagName.toLowerCase() === "body") {
return;
}
target = target.parentNode;
}
let container = target.container;
if (this._hoveredNode !== container.node) {
- if (container.node.nodeType !== nodeConstants.TEXT_NODE) {
- this._showBoxModel(container.node);
- } else {
- this._hideBoxModel();
- }
+ this._showBoxModel(container.node);
}
this._showContainerAsHovered(container.node);
this.emit("node-hover");
},
/**
* If focus is moved outside of the markup view document and there is a
--- a/devtools/client/inspector/test/browser_inspector_highlighter-comments.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js
@@ -46,42 +46,61 @@ add_task(function* () {
info("Hovering over #id3 and waiting for highlighter to appear.");
yield hoverElement("#id3");
yield assertHighlighterShownOn("#id3");
info("Hovering over hidden #id4 and ensuring highlighter doesn't appear.");
yield hoverElement("#id4");
yield assertHighlighterHidden();
+ info("Hovering over a text node and waiting for highlighter to appear.");
+ yield hoverTextNode("Visible text node");
+ yield assertHighlighterShownOnTextNode("body", 14);
+
function hoverContainer(container) {
let promise = inspector.toolbox.once("node-highlight");
+
EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
markupView.doc.defaultView);
return promise;
}
function* hoverElement(selector) {
- info("Hovering node " + selector + " in the markup view");
+ info(`Hovering node ${selector} in the markup view`);
let container = yield getContainerForSelector(selector, inspector);
return hoverContainer(container);
}
function hoverComment() {
info("Hovering the comment node in the markup view");
for (let [node, container] of markupView._containers) {
if (node.nodeType === Ci.nsIDOMNode.COMMENT_NODE) {
return hoverContainer(container);
}
}
return null;
}
+ function hoverTextNode(text) {
+ info(`Hovering the text node "${text}" in the markup view`);
+ let container = [...markupView._containers].filter(([nodeFront]) => {
+ return nodeFront.nodeType === Ci.nsIDOMNode.TEXT_NODE &&
+ nodeFront._form.nodeValue.trim() === text.trim();
+ })[0][1];
+ return hoverContainer(container);
+ }
+
function* assertHighlighterShownOn(selector) {
ok((yield testActor.assertHighlightedNode(selector)),
"Highlighter is shown on the right node: " + selector);
}
+ function* assertHighlighterShownOnTextNode(parentSelector, childNodeIndex) {
+ ok((yield testActor.assertHighlightedTextNode(parentSelector, childNodeIndex)),
+ "Highlighter is shown on the right text node");
+ }
+
function* assertHighlighterHidden() {
let isVisible = yield testActor.isHighlighting();
ok(!isVisible, "Highlighter is hidden");
}
});
--- a/devtools/client/inspector/test/doc_inspector_highlighter-comments.html
+++ b/devtools/client/inspector/test/doc_inspector_highlighter-comments.html
@@ -9,10 +9,11 @@
<div id="id1">Visible div 1</div>
<!-- Invisible comment node -->
<div id="id2">Visible div 2</div>
<script type="text/javascript">
/* Invisible script node */
</script>
<div id="id3">Visible div 3</div>
<div id="id4" style="display:none;">Invisible div node</div>
+ Visible text node
</body>
</html>
--- a/devtools/client/shared/test/test-actor.js
+++ b/devtools/client/shared/test/test-actor.js
@@ -255,16 +255,25 @@ var testSpec = protocol.generateActorSpe
getNodeRect: {
request: {
selector: Arg(0, "string")
},
response: {
value: RetVal("json")
}
},
+ getTextNodeRect: {
+ request: {
+ parentSelector: Arg(0, "string"),
+ childNodeIndex: Arg(1, "number")
+ },
+ response: {
+ value: RetVal("json")
+ }
+ },
getNodeInfo: {
request: {
selector: Arg(0, "string")
},
response: {
value: RetVal("json")
}
},
@@ -712,16 +721,22 @@ var TestActor = exports.TestActor = prot
return deferred.promise;
},
getNodeRect: Task.async(function* (selector) {
let node = this._querySelector(selector);
return getRect(this.content, node, this.content);
}),
+ getTextNodeRect: Task.async(function* (parentSelector, childNodeIndex) {
+ let parentNode = this._querySelector(parentSelector);
+ let node = parentNode.childNodes[childNodeIndex];
+ return getAdjustedQuads(this.content, node)[0].bounds;
+ }),
+
/**
* Get information about a DOM element, identified by a selector.
* @param {String} selector The CSS selector to get the node (can be an array
* of selectors to get elements in an iframe).
* @return {Object} data Null if selector didn't match any node, otherwise:
* - {String} tagName.
* - {String} namespaceURI.
* - {Number} numChildren The number of children in the element.
@@ -886,85 +901,69 @@ var TestActorFront = exports.TestActorFr
ret.guides = {};
for (let guide of ["top", "right", "bottom", "left"]) {
ret.guides[guide] = yield this._getGuideStatus(guide);
}
return ret;
}),
+ /**
+ * Check that the box-model highlighter is currently highlighting the node matching the
+ * given selector.
+ * @param {String} selector
+ * @return {Boolean}
+ */
assertHighlightedNode: Task.async(function* (selector) {
- // Taken and tweaked from:
- // https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
- function isLeft(p0, p1, p2) {
- let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) -
- ((p2[0] - p0[0]) * (p1[1] - p0[1]));
- return l;
- }
- function isInside(point, polygon) {
- if (polygon.length === 0) {
- return false;
- }
-
- var n = polygon.length;
- var newPoints = polygon.slice(0);
- newPoints.push(polygon[0]);
- var wn = 0; // wn counter
+ let rect = yield this.getNodeRect(selector);
+ return yield this.isNodeRectHighlighted(rect);
+ }),
- // loop through all edges of the polygon
- for (var i = 0; i < n; i++) {
- // Accept points on the edges
- let r = isLeft(newPoints[i], newPoints[i + 1], point);
- if (r === 0) {
- return true;
- }
- if (newPoints[i][1] <= point[1]) {
- if (newPoints[i + 1][1] > point[1] && r > 0) {
- wn++;
- }
- } else {
- if (newPoints[i + 1][1] <= point[1] && r < 0) {
- wn--;
- }
- }
- }
- if (wn === 0) {
- dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
- }
- // the point is outside only when this winding number wn===0, otherwise it's inside
- return wn !== 0;
+ /**
+ * Check that the box-model highlighter is currently highlighting the text node that can
+ * be found at a given index within the list of childNodes of a parent element matching
+ * the given selector.
+ * @param {String} parentSelector
+ * @param {Number} childNodeIndex
+ * @return {Boolean}
+ */
+ assertHighlightedTextNode: Task.async(function* (parentSelector, childNodeIndex) {
+ let rect = yield this.getTextNodeRect(parentSelector, childNodeIndex);
+ return yield this.isNodeRectHighlighted(rect);
+ }),
+
+ /**
+ * Check that the box-model highlighter is currently highlighting the given rect.
+ * @param {Object} rect
+ * @return {Boolean}
+ */
+ isNodeRectHighlighted: Task.async(function* ({ left, top, width, height }) {
+ let {visible, border} = yield this._getBoxModelStatus();
+ let points = border.points;
+ if (!visible) {
+ return false;
}
- let {visible, border} = yield this._getBoxModelStatus();
- let points = border.points;
- if (visible) {
- // Check that the node is within the box model
- let { left, top, width, height } = yield this.getNodeRect(selector);
- let right = left + width;
- let bottom = top + height;
+ // Check that the node is within the box model
+ let right = left + width;
+ let bottom = top + height;
- // Converts points dictionnary into an array
- let list = [];
- for (var i = 1; i <= 4; i++) {
- let p = points["p" + i];
- list.push([p.x, p.y]);
- }
- points = list;
+ // Converts points dictionnary into an array
+ let list = [];
+ for (let i = 1; i <= 4; i++) {
+ let p = points["p" + i];
+ list.push([p.x, p.y]);
+ }
+ points = list;
- // Check that each point of the node is within the box model
- if (!isInside([left, top], points) ||
- !isInside([right, top], points) ||
- !isInside([right, bottom], points) ||
- !isInside([left, bottom], points)) {
- return false;
- }
- return true;
- } else {
- return false;
- }
+ // Check that each point of the node is within the box model
+ return isInside([left, top], points) &&
+ isInside([right, top], points) &&
+ isInside([right, bottom], points) &&
+ isInside([left, bottom], points);
}),
/**
* Get the coordinate (points attribute) from one of the polygon elements in the
* box model highlighter.
*/
_getPointsForRegion: Task.async(function* (region) {
let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d");
@@ -1081,8 +1080,54 @@ var TestActorFront = exports.TestActorFr
points.push(polygon.trim().split(" ").map(i => {
return i.replace(/M|L/, "").split(",");
}));
}
return {d, points};
})
});
+
+/**
+ * Check whether a point is included in a polygon.
+ * Taken and tweaked from:
+ * https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
+ * @param {Array} point [x,y] coordinates
+ * @param {Array} polygon An array of [x,y] points
+ * @return {Boolean}
+ */
+function isInside(point, polygon) {
+ if (polygon.length === 0) {
+ return false;
+ }
+
+ const n = polygon.length;
+ const newPoints = polygon.slice(0);
+ newPoints.push(polygon[0]);
+ let wn = 0;
+
+ // loop through all edges of the polygon
+ for (let i = 0; i < n; i++) {
+ // Accept points on the edges
+ let r = isLeft(newPoints[i], newPoints[i + 1], point);
+ if (r === 0) {
+ return true;
+ }
+ if (newPoints[i][1] <= point[1]) {
+ if (newPoints[i + 1][1] > point[1] && r > 0) {
+ wn++;
+ }
+ } else if (newPoints[i + 1][1] <= point[1] && r < 0) {
+ wn--;
+ }
+ }
+ if (wn === 0) {
+ dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
+ }
+ // the point is outside only when this winding number wn===0, otherwise it's inside
+ return wn !== 0;
+}
+
+function isLeft(p0, p1, p2) {
+ let l = ((p1[0] - p0[0]) * (p2[1] - p0[1])) -
+ ((p2[0] - p0[0]) * (p1[1] - p0[1]));
+ return l;
+}
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -8,17 +8,17 @@ const { Ci } = require("chrome");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
const EventEmitter = require("devtools/shared/event-emitter");
const events = require("sdk/event/core");
const protocol = require("devtools/shared/protocol");
const Services = require("Services");
const { isWindowIncluded } = require("devtools/shared/layout/utils");
const { highlighterSpec, customHighlighterSpec } = require("devtools/shared/specs/highlighters");
-const { isXUL, isNodeValid } = require("./highlighters/utils/markup");
+const { isXUL } = require("./highlighters/utils/markup");
const { SimpleOutlineHighlighter } = require("./highlighters/simple-outline");
const HIGHLIGHTER_PICKED_TIMER = 1000;
const IS_OSX = Services.appinfo.OS === "Darwin";
/**
* The registration mechanism for highlighters provide a quick way to
* have modular highlighters, instead of a hard coded list.
@@ -177,19 +177,17 @@ var HighlighterActor = exports.Highlight
* method several times won't display several highlighters, it will just move
* the highlighter instance to these nodes.
*
* @param NodeActor The node to be highlighted
* @param Options See the request part for existing options. Note that not
* all options may be supported by all types of highlighters.
*/
showBoxModel: function (node, options = {}) {
- if (node && isNodeValid(node.rawNode)) {
- this._highlighter.show(node.rawNode, options);
- } else {
+ if (!node || !this._highlighter.show(node.rawNode, options)) {
this._highlighter.hide();
}
},
/**
* Hide the box model highlighting if it was shown before
*/
hideBoxModel: function () {
@@ -463,17 +461,17 @@ var CustomHighlighterActor = exports.Cus
* to run the provided CSS selector on.
*
* @param {NodeActor} The node to be highlighted
* @param {Object} Options for the custom highlighter
* @return {Boolean} True, if the highlighter has been successfully shown
* (FF41+)
*/
show: function (node, options) {
- if (!node || !isNodeValid(node.rawNode) || !this._highlighter) {
+ if (!node || !this._highlighter) {
return false;
}
return this._highlighter.show(node.rawNode, options);
},
/**
* Hide the highlighter if it was shown before
--- a/devtools/server/actors/highlighters/auto-refresh.js
+++ b/devtools/server/actors/highlighters/auto-refresh.js
@@ -60,17 +60,17 @@ AutoRefreshHighlighter.prototype = {
* @param {DOMNode} node
* @param {Object} options
* Object used for passing options
*/
show: function (node, options = {}) {
let isSameNode = node === this.currentNode;
let isSameOptions = this._isSameOptions(options);
- if (!isNodeValid(node) || (isSameNode && isSameOptions)) {
+ if (!this._isNodeValid(node) || (isSameNode && isSameOptions)) {
return false;
}
this.options = options;
this._stopRefreshLoop();
this.currentNode = node;
this._updateAdjustedQuads();
@@ -82,30 +82,41 @@ AutoRefreshHighlighter.prototype = {
}
return shown;
},
/**
* Hide the highlighter
*/
hide: function () {
- if (!isNodeValid(this.currentNode)) {
+ if (!this._isNodeValid(this.currentNode)) {
return;
}
this._hide();
this._stopRefreshLoop();
this.currentNode = null;
this.currentQuads = {};
this.options = null;
this.emit("hidden");
},
/**
+ * Whether the current node is valid for this highlighter type.
+ * This is implemented by default to check if the node is an element node. Highlighter
+ * sub-classes should override this method if they want to highlight other node types.
+ * @param {DOMNode} node
+ * @return {Boolean}
+ */
+ _isNodeValid: function (node) {
+ return isNodeValid(node);
+ },
+
+ /**
* Are the provided options the same as the currently stored options?
* Returns false if there are no options stored currently.
*/
_isSameOptions: function (options) {
if (!this.options) {
return false;
}
@@ -146,17 +157,17 @@ AutoRefreshHighlighter.prototype = {
let newQuads = JSON.stringify(this.currentQuads);
return oldQuads !== newQuads;
},
/**
* Update the highlighter if the node has moved since the last update.
*/
update: function () {
- if (!isNodeValid(this.currentNode) || !this._hasMoved()) {
+ if (!this._isNodeValid(this.currentNode) || !this._hasMoved()) {
return;
}
this._update();
this.emit("updated");
},
_show: function () {
--- a/devtools/server/actors/highlighters/box-model.js
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -2,21 +2,27 @@
* 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, moveInfobar,
- getBindingElementAndPseudo, hasPseudoClassLock, getComputedStyle,
- createSVGNode, createNode, isNodeValid } = require("./utils/markup");
+ 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
@@ -89,34 +95,23 @@ function BoxModelHighlighter(highlighter
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._currentNode = null;
}
BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
typeName: "BoxModelHighlighter",
ID_CLASS_PREFIX: "box-model-",
- get currentNode() {
- return this._currentNode;
- },
-
- set currentNode(node) {
- this._currentNode = node;
- this._computedStyle = null;
- },
-
_buildMarkup: function () {
let doc = this.win.document;
let highlighterContainer = doc.createElement("div");
highlighterContainer.className = "highlighter-container";
// Build the root wrapper, used to adapt to the page zoom.
let rootWrapper = createNode(this.win, {
@@ -253,27 +248,34 @@ BoxModelHighlighter.prototype = extend(A
return highlighterContainer;
},
/**
* Destroy the nodes. Remove listeners.
*/
destroy: function () {
AutoRefreshHighlighter.prototype.destroy.call(this);
-
this.markup.destroy();
-
- this._currentNode = null;
},
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();
@@ -306,17 +308,19 @@ BoxModelHighlighter.prototype = extend(A
* 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()) {
- if (!this.options.hideInfoBar) {
+ // Show the infobar only if configured to do so and the node is an element.
+ if (!this.options.hideInfoBar &&
+ this.currentNode.nodeType === this.currentNode.ELEMENT_NODE) {
this._showInfobar();
} else {
this._hideInfobar();
}
this._showBoxModel();
shown = true;
} else {
// Nothing to highlight (0px rectangle like a <script> tag for instance)
@@ -514,30 +518,25 @@ BoxModelHighlighter.prototype = extend(A
"L" + np3.x + "," + np3.y + " " +
"L" + np2.x + "," + np2.y + " " +
"L" + np1.x + "," + np1.y;
}
return path;
},
+ /**
+ * Can the current node be highlighted? Does it have quads.
+ * @return {Boolean}
+ */
_nodeNeedsHighlighting: function () {
- let hasNoQuads = !this.currentQuads.margin.length &&
- !this.currentQuads.border.length &&
- !this.currentQuads.padding.length &&
- !this.currentQuads.content.length;
- if (!isNodeValid(this.currentNode) || hasNoQuads) {
- return false;
- }
-
- if (!this._computedStyle) {
- this._computedStyle = getComputedStyle(this.currentNode);
- }
-
- return this._computedStyle.getPropertyValue("display") !== "none";
+ return this.currentQuads.margin.length ||
+ this.currentQuads.border.length ||
+ this.currentQuads.padding.length ||
+ this.currentQuads.content.length;
},
_getOuterBounds: function () {
for (let region of ["margin", "border", "padding", "content"]) {
let quad = this._getOuterQuad(region);
if (!quad) {
// Invisible element such as a script tag.
--- a/devtools/server/actors/highlighters/geometry-editor.js
+++ b/devtools/server/actors/highlighters/geometry-editor.js
@@ -1,22 +1,19 @@
/* 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, getCSSStyleRules, getComputedStyle,
- createSVGNode, createNode } = require("./utils/markup");
-
-const { setIgnoreLayoutChanges,
- getAdjustedQuads } = require("devtools/shared/layout/utils");
+const { CanvasFrameAnonymousContentHelper, getCSSStyleRules, getComputedStyle,
+ createSVGNode, createNode } = require("./utils/markup");
+const { setIgnoreLayoutChanges, getAdjustedQuads } = require("devtools/shared/layout/utils");
const GEOMETRY_LABEL_SIZE = 6;
// List of all DOM Events subscribed directly to the document from the
// Geometry Editor highlighter
const DOM_EVENTS = ["mousemove", "mouseup", "pagehide"];
const _dragging = Symbol("geometry/dragging");
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -116,28 +116,35 @@ function installHelperSheet(win, source,
let {Style} = require("sdk/stylesheet/style");
let {attach} = require("sdk/content/mod");
let style = Style({source, type});
attach(style, win);
installedHelperSheets.set(win.document, style);
}
exports.installHelperSheet = installHelperSheet;
-function isNodeValid(node) {
- // Is it null or dead?
+/**
+ * Returns true if a DOM node is "valid", where "valid" means that the node isn't a dead
+ * object wrapper, is still attached to a document, and is of a given type.
+ * @param {DOMNode} node
+ * @param {Number} nodeType Optional, defaults to ELEMENT_NODE
+ * @return {Boolean}
+ */
+function isNodeValid(node, nodeType = Ci.nsIDOMNode.ELEMENT_NODE) {
+ // Is it still alive?
if (!node || Cu.isDeadWrapper(node)) {
return false;
}
- // Is it an element node
- if (node.nodeType !== node.ELEMENT_NODE) {
+ // Is it of the right type?
+ if (node.nodeType !== nodeType) {
return false;
}
- // Is the document inaccessible?
+ // Is its document accessible?
let doc = node.ownerDocument;
if (!doc || !doc.defaultView) {
return false;
}
// Is the node connected to the document? Using getBindingParent adds
// support for anonymous elements generated by a node in the document.
let bindingParent = getRootBindingParent(node);