gecko/toolkit/devtools/server/actors/highlighter.js

888 lines
28 KiB
JavaScript

/* 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 {Cu, Cc, Ci} = require("chrome");
const Services = require("Services");
const protocol = require("devtools/server/protocol");
const {Arg, Option, method} = protocol;
const events = require("sdk/event/core");
const EventEmitter = require("devtools/toolkit/event-emitter");
const GUIDE_STROKE_WIDTH = 1;
// Make sure the domnode type is known here
require("devtools/server/actors/inspector");
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
// FIXME: add ":visited" and ":link" after bug 713106 is fixed
const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const SVG_NS = "http://www.w3.org/2000/svg";
const HIGHLIGHTER_PICKED_TIMER = 1000;
const INFO_BAR_OFFSET = 5;
/**
* The HighlighterActor is the server-side entry points for any tool that wishes
* to highlight elements in the content document.
*
* The highlighter can be retrieved via the inspector's getHighlighter method.
*/
/**
* The HighlighterActor class
*/
let HighlighterActor = protocol.ActorClass({
typeName: "highlighter",
initialize: function(inspector, autohide) {
protocol.Actor.prototype.initialize.call(this, null);
this._autohide = autohide;
this._inspector = inspector;
this._walker = this._inspector.walker;
this._tabActor = this._inspector.tabActor;
this._highlighterReady = this._highlighterReady.bind(this);
this._highlighterHidden = this._highlighterHidden.bind(this);
if (this._supportsBoxModelHighlighter()) {
this._boxModelHighlighter =
new BoxModelHighlighter(this._tabActor, this._inspector);
this._boxModelHighlighter.on("ready", this._highlighterReady);
this._boxModelHighlighter.on("hide", this._highlighterHidden);
} else {
this._boxModelHighlighter = new SimpleOutlineHighlighter(this._tabActor);
}
},
get conn() this._inspector && this._inspector.conn,
/**
* Can the host support the box model highlighter which requires a parent
* XUL node to attach itself.
*/
_supportsBoxModelHighlighter: function() {
// Note that <browser>s on Fennec also have a XUL parentNode but the box
// model highlighter doesn't display correctly on Fennec (bug 993190)
return this._tabActor.browser &&
!!this._tabActor.browser.parentNode &&
Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
},
destroy: function() {
protocol.Actor.prototype.destroy.call(this);
if (this._boxModelHighlighter) {
this._boxModelHighlighter.off("ready", this._highlighterReady);
this._boxModelHighlighter.off("hide", this._highlighterHidden);
this._boxModelHighlighter.destroy();
this._boxModelHighlighter = null;
}
this._autohide = null;
this._inspector = null;
this._walker = null;
this._tabActor = null;
},
/**
* Display the box model highlighting on a given NodeActor.
* There is only one instance of the box model highlighter, so calling this
* 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: method(function(node, options={}) {
if (node && this._isNodeValidForHighlighting(node.rawNode)) {
this._boxModelHighlighter.show(node.rawNode, options);
} else {
this._boxModelHighlighter.hide();
}
}, {
request: {
node: Arg(0, "domnode"),
region: Option(1)
}
}),
_isNodeValidForHighlighting: function(node) {
// Is it null or dead?
let isNotDead = node && !Cu.isDeadWrapper(node);
// Is it connected to the document?
let isConnected = false;
try {
let doc = node.ownerDocument;
isConnected = (doc && doc.defaultView && doc.documentElement.contains(node));
} catch (e) {
// "can't access dead object" error
}
// Is it an element node
let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE;
return isNotDead && isConnected && isElementNode;
},
/**
* Hide the box model highlighting if it was shown before
*/
hideBoxModel: method(function() {
this._boxModelHighlighter.hide();
}, {
request: {}
}),
/**
* Pick a node on click, and highlight hovered nodes in the process.
*
* This method doesn't respond anything interesting, however, it starts
* mousemove, and click listeners on the content document to fire
* events and let connected clients know when nodes are hovered over or
* clicked.
*
* Once a node is picked, events will cease, and listeners will be removed.
*/
_isPicking: false,
_hoveredNode: null,
pick: method(function() {
if (this._isPicking) {
return null;
}
this._isPicking = true;
this._preventContentEvent = event => {
event.stopPropagation();
event.preventDefault();
};
this._onPick = event => {
this._preventContentEvent(event);
this._stopPickerListeners();
this._isPicking = false;
if (this._autohide) {
this._tabActor.window.setTimeout(() => {
this._boxModelHighlighter.hide();
}, HIGHLIGHTER_PICKED_TIMER);
}
events.emit(this._walker, "picker-node-picked", this._findAndAttachElement(event));
};
this._onHovered = event => {
this._preventContentEvent(event);
let res = this._findAndAttachElement(event);
if (this._hoveredNode !== res.node) {
this._boxModelHighlighter.show(res.node.rawNode);
events.emit(this._walker, "picker-node-hovered", res);
this._hoveredNode = res.node;
}
};
this._tabActor.window.focus();
this._startPickerListeners();
return null;
}),
_findAndAttachElement: function(event) {
let doc = event.target.ownerDocument;
let x = event.clientX;
let y = event.clientY;
let node = doc.elementFromPoint(x, y);
return this._walker.attachElement(node);
},
/**
* Get the right target for listening to mouse events while in pick mode.
* - On a firefox desktop content page: tabActor is a BrowserTabActor from
* which the browser property will give us a target we can use to listen to
* events, even in nested iframes.
* - On B2G: tabActor is a ContentActor which doesn't have a browser but
* since it overrides BrowserTabActor, it does get a browser property
* anyway, which points to its window object.
* - When using the Browser Toolbox (to inspect firefox desktop): tabActor is
* the RootActor, in which case, the window property can be used to listen
* to events
*/
_getPickerListenerTarget: function() {
let actor = this._tabActor;
return actor.isRootActor ? actor.window : actor.chromeEventHandler;
},
_startPickerListeners: function() {
let target = this._getPickerListenerTarget();
target.addEventListener("mousemove", this._onHovered, true);
target.addEventListener("click", this._onPick, true);
target.addEventListener("mousedown", this._preventContentEvent, true);
target.addEventListener("mouseup", this._preventContentEvent, true);
target.addEventListener("dblclick", this._preventContentEvent, true);
},
_stopPickerListeners: function() {
let target = this._getPickerListenerTarget();
target.removeEventListener("mousemove", this._onHovered, true);
target.removeEventListener("click", this._onPick, true);
target.removeEventListener("mousedown", this._preventContentEvent, true);
target.removeEventListener("mouseup", this._preventContentEvent, true);
target.removeEventListener("dblclick", this._preventContentEvent, true);
},
_highlighterReady: function() {
events.emit(this._inspector.walker, "highlighter-ready");
},
_highlighterHidden: function() {
events.emit(this._inspector.walker, "highlighter-hide");
},
cancelPick: method(function() {
if (this._isPicking) {
this._boxModelHighlighter.hide();
this._stopPickerListeners();
this._isPicking = false;
this._hoveredNode = null;
}
})
});
exports.HighlighterActor = HighlighterActor;
/**
* The HighlighterFront class
*/
let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
/**
* The BoxModelHighlighter is the class that actually draws the the box model
* regions on top of a node.
* It is used by the HighlighterActor.
*
* Usage example:
*
* let h = new BoxModelHighlighter(browser);
* h.show(node);
* h.hide();
* h.destroy();
*
* Structure:
* <stack class="highlighter-container">
* <svg class="box-model-root" hidden="true">
* <g class="box-model-container">
* <polygon class="box-model-margin" points="317,122 747,36 747,181 317,267" />
* <polygon class="box-model-border" points="317,128 747,42 747,161 317,247" />
* <polygon class="box-model-padding" points="323,127 747,42 747,161 323,246" />
* <polygon class="box-model-content" points="335,137 735,57 735,152 335,232" />
* </g>
* <line class="box-model-guide-top" x1="0" y1="592" x2="99999" y2="592" />
* <line class="box-model-guide-right" x1="735" y1="0" x2="735" y2="99999" />
* <line class="box-model-guide-bottom" x1="0" y1="612" x2="99999" y2="612" />
* <line class="box-model-guide-left" x1="334" y1="0" x2="334" y2="99999" />
* </svg>
* <box class="highlighter-nodeinfobar-container">
* <box class="highlighter-nodeinfobar-positioner" position="top" />
* <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top" />
* <hbox class="highlighter-nodeinfobar">
* <hbox class="highlighter-nodeinfobar-text" align="center" flex="1">
* <span class="highlighter-nodeinfobar-tagname">Node name</span>
* <span class="highlighter-nodeinfobar-id">Node id</span>
* <span class="highlighter-nodeinfobar-classes">.someClass</span>
* <span class="highlighter-nodeinfobar-pseudo-classes">:hover</span>
* </hbox>
* </hbox>
* <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
* </box>
* </box>
* </stack>
*/
function BoxModelHighlighter(tabActor, inspector) {
this.browser = tabActor.browser;
this.win = tabActor.window;
this.chromeDoc = this.browser.ownerDocument;
this.chromeWin = this.chromeDoc.defaultView;
this._inspector = inspector;
this.layoutHelpers = new LayoutHelpers(this.win);
this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
this.transitionDisabler = null;
this.pageEventsMuter = null;
this._update = this._update.bind(this);
this.handleEvent = this.handleEvent.bind(this);
this.currentNode = null;
EventEmitter.decorate(this);
this._initMarkup();
}
BoxModelHighlighter.prototype = {
get zoom() {
return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
},
_initMarkup: function() {
let stack = this.browser.parentNode;
this._highlighterContainer = this.chromeDoc.createElement("stack");
this._highlighterContainer.className = "highlighter-container";
this._svgRoot = this._createSVGNode("root", "svg", this._highlighterContainer);
this._boxModelContainer = this._createSVGNode("container", "g", this._svgRoot);
this._boxModelNodes = {
margin: this._createSVGNode("margin", "polygon", this._boxModelContainer),
border: this._createSVGNode("border", "polygon", this._boxModelContainer),
padding: this._createSVGNode("padding", "polygon", this._boxModelContainer),
content: this._createSVGNode("content", "polygon", this._boxModelContainer)
};
this._guideNodes = {
top: this._createSVGNode("guide-top", "line", this._svgRoot),
right: this._createSVGNode("guide-right", "line", this._svgRoot),
bottom: this._createSVGNode("guide-bottom", "line", this._svgRoot),
left: this._createSVGNode("guide-left", "line", this._svgRoot)
};
this._guideNodes.top.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
this._guideNodes.right.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
this._guideNodes.bottom.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
this._guideNodes.left.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
this._highlighterContainer.appendChild(this._svgRoot);
let infobarContainer = this.chromeDoc.createElement("box");
infobarContainer.className = "highlighter-nodeinfobar-container";
this._highlighterContainer.appendChild(infobarContainer);
// Insert the highlighter right after the browser
stack.insertBefore(this._highlighterContainer, stack.childNodes[1]);
// Building the infobar
let infobarPositioner = this.chromeDoc.createElement("box");
infobarPositioner.className = "highlighter-nodeinfobar-positioner";
infobarPositioner.setAttribute("position", "top");
infobarPositioner.setAttribute("disabled", "true");
let nodeInfobar = this.chromeDoc.createElement("hbox");
nodeInfobar.className = "highlighter-nodeinfobar";
let arrowBoxTop = this.chromeDoc.createElement("box");
arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top";
let arrowBoxBottom = this.chromeDoc.createElement("box");
arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom";
let tagNameLabel = this.chromeDoc.createElementNS(XHTML_NS, "span");
tagNameLabel.className = "highlighter-nodeinfobar-tagname";
let idLabel = this.chromeDoc.createElementNS(XHTML_NS, "span");
idLabel.className = "highlighter-nodeinfobar-id";
let classesBox = this.chromeDoc.createElementNS(XHTML_NS, "span");
classesBox.className = "highlighter-nodeinfobar-classes";
let pseudoClassesBox = this.chromeDoc.createElementNS(XHTML_NS, "span");
pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes";
// Add some content to force a better boundingClientRect
pseudoClassesBox.textContent = "&nbsp;";
// <hbox class="highlighter-nodeinfobar-text"/>
let texthbox = this.chromeDoc.createElement("hbox");
texthbox.className = "highlighter-nodeinfobar-text";
texthbox.setAttribute("align", "center");
texthbox.setAttribute("flex", "1");
texthbox.appendChild(tagNameLabel);
texthbox.appendChild(idLabel);
texthbox.appendChild(classesBox);
texthbox.appendChild(pseudoClassesBox);
nodeInfobar.appendChild(texthbox);
infobarPositioner.appendChild(arrowBoxTop);
infobarPositioner.appendChild(nodeInfobar);
infobarPositioner.appendChild(arrowBoxBottom);
infobarContainer.appendChild(infobarPositioner);
let barHeight = infobarPositioner.getBoundingClientRect().height;
this.nodeInfo = {
tagNameLabel: tagNameLabel,
idLabel: idLabel,
classesBox: classesBox,
pseudoClassesBox: pseudoClassesBox,
positioner: infobarPositioner,
barHeight: barHeight,
};
},
_createSVGNode: function(classPostfix, nodeType, parent) {
let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
node.setAttribute("class", "box-model-" + classPostfix);
parent.appendChild(node);
return node;
},
/**
* Destroy the nodes. Remove listeners.
*/
destroy: function() {
this.hide();
this.chromeWin.clearTimeout(this.transitionDisabler);
this.chromeWin.clearTimeout(this.pageEventsMuter);
this.nodeInfo = null;
this._highlighterContainer.remove();
this._highlighterContainer = null;
this.rect = null;
this.win = null;
this.browser = null;
this.chromeDoc = null;
this.chromeWin = null;
this.currentNode = null;
},
/**
* Show the highlighter on a given node
*
* @param {DOMNode} node
* @param {Object} options
* Object used for passing options
*/
show: function(node, options={}) {
this.currentNode = node;
this._showInfobar();
this._detachPageListeners();
this._attachPageListeners();
this._update();
this._trackMutations();
},
_trackMutations: function() {
if (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 (this.currentNode) {
if (this.currentNodeObserver) {
// The following may fail with a "can't access dead object" exception
// when the actor is being destroyed
try {
this.currentNodeObserver.disconnect();
} catch (e) {}
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
* @param {Object} options
* Object used for passing options. Valid options are:
* - box: "content", "padding", "border" or "margin." This specifies
* the box that the guides should outline. Default is content.
*/
_update: function(options={}) {
if (this.currentNode) {
if (this._highlightBoxModel(options)) {
this._showInfobar();
} else {
// Nothing to highlight (0px rectangle like a <script> tag for instance)
this.hide();
}
this.emit("ready");
}
},
/**
* Hide the highlighter, the outline and the infobar.
*/
hide: function() {
if (this.currentNode) {
this._untrackMutations();
this.currentNode = null;
this._hideBoxModel();
this._hideInfobar();
this._detachPageListeners();
}
this.emit("hide");
},
/**
* Hide the infobar
*/
_hideInfobar: function() {
this.nodeInfo.positioner.setAttribute("hidden", "true");
},
/**
* Show the infobar
*/
_showInfobar: function() {
this.nodeInfo.positioner.removeAttribute("hidden");
this._updateInfobar();
},
/**
* Hide the box model
*/
_hideBoxModel: function() {
this._svgRoot.setAttribute("hidden", "true");
},
/**
* Show the box model
*/
_showBoxModel: function() {
this._svgRoot.removeAttribute("hidden");
},
/**
* Highlight the box model.
*
* @param {Object} options
* Object used for passing options. Valid options are:
* - region: "content", "padding", "border" or "margin." This specifies
* the region that the guides should outline. Default is content.
* @return {boolean}
* True if the rectangle was highlighted, false otherwise.
*/
_highlightBoxModel: function(options) {
let isShown = false;
options.region = options.region || "content";
// TODO: Remove this polyfill
this.rect =
this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, "margin");
if (!this.rect) {
return null;
}
if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) {
for (let boxType in this._boxModelNodes) {
// TODO: Remove this polyfill
let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, boxType);
let boxNode = this._boxModelNodes[boxType];
boxNode.setAttribute("points",
p1.x + "," + p1.y + " " +
p2.x + "," + p2.y + " " +
p3.x + "," + p3.y + " " +
p4.x + "," + p4.y);
if (boxType === options.region) {
this._showGuides(p1, p2, p3, p4);
}
}
isShown = true;
this._showBoxModel();
} else {
// Only return false if the element really is invisible.
// A height of 0 and a non-0 width corresponds to a visible element that
// is below the fold for instance
if (this.rect.width > 0 || this.rect.height > 0) {
isShown = true;
this._hideBoxModel();
}
}
return isShown;
},
/**
* We only want to show guides for horizontal and vertical edges as this helps
* to line them up. This method finds these edges and displays a guide there.
*
* @param {DOMPoint} p1
* Point 1
* @param {DOMPoint} p2
* Point 2
* @param {DOMPoint} p3 [description]
* Point 3
* @param {DOMPoint} p4 [description]
* Point 4
*/
_showGuides: function(p1, p2, p3, p4) {
let allX = [p1.x, p2.x, p3.x, p4.x].sort();
let allY = [p1.y, p2.y, p3.y, p4.y].sort();
let toShowX = [];
let toShowY = [];
for (let arr of [allX, allY]) {
for (let i = 0; i < arr.length; i++) {
let val = arr[i];
if (i !== arr.lastIndexOf(val)) {
if (arr === allX) {
toShowX.push(val);
} else {
toShowY.push(val);
}
arr.splice(arr.lastIndexOf(val), 1);
}
}
}
// Move guide into place or hide it if no valid co-ordinate was found.
this._updateGuide(this._guideNodes.top, toShowY[0]);
this._updateGuide(this._guideNodes.right, toShowX[1]);
this._updateGuide(this._guideNodes.bottom, toShowY[1]);
this._updateGuide(this._guideNodes.left, toShowX[0]);
},
/**
* Move a guide to the appropriate position and display it. If no point is
* passed then the guide is hidden.
*
* @param {SVGLine} guide
* The guide to update
* @param {Integer} point
* x or y co-ordinate. If this is undefined we hide the guide.
*/
_updateGuide: function(guide, point=-1) {
if (point > 0) {
let offset = GUIDE_STROKE_WIDTH / 2;
if (guide === this._guideNodes.top || guide === this._guideNodes.left) {
point -= offset;
} else {
point += offset;
}
if (guide === this._guideNodes.top || guide === this._guideNodes.bottom) {
guide.setAttribute("x1", 0);
guide.setAttribute("y1", point);
guide.setAttribute("x2", "100%");
guide.setAttribute("y2", point);
} else {
guide.setAttribute("x1", point);
guide.setAttribute("y1", 0);
guide.setAttribute("x2", point);
guide.setAttribute("y2", "100%");
}
guide.removeAttribute("hidden");
return true;
} else {
guide.setAttribute("hidden", "true");
return false;
}
},
/**
* Update node information (tagName#id.class)
*/
_updateInfobar: function() {
if (this.currentNode) {
// Tag name
this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName;
// ID
this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : "";
// Classes
let classes = this.nodeInfo.classesBox;
classes.textContent = this.currentNode.classList.length ?
"." + Array.join(this.currentNode.classList, ".") : "";
// Pseudo-classes
let pseudos = PSEUDO_CLASSES.filter(pseudo => {
return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo);
}, this);
let pseudoBox = this.nodeInfo.pseudoClassesBox;
pseudoBox.textContent = pseudos.join("");
this._moveInfobar();
}
},
/**
* Move the Infobar to the right place in the highlighter.
*/
_moveInfobar: function() {
if (this.rect) {
let bounds = this.rect.bounds;
let winHeight = this.win.innerHeight * this.zoom;
let winWidth = this.win.innerWidth * this.zoom;
this.nodeInfo.positioner.removeAttribute("disabled");
// Can the bar be above the node?
if (bounds.top < this.nodeInfo.barHeight) {
// No. Can we move the toolbar under the node?
if (bounds.bottom + this.nodeInfo.barHeight > winHeight) {
// No. Let's move it inside.
this.nodeInfo.positioner.style.top = bounds.top + "px";
this.nodeInfo.positioner.setAttribute("position", "overlap");
} else {
// Yes. Let's move it under the node.
this.nodeInfo.positioner.style.top = bounds.bottom - INFO_BAR_OFFSET + "px";
this.nodeInfo.positioner.setAttribute("position", "bottom");
}
} else {
// Yes. Let's move it on top of the node.
this.nodeInfo.positioner.style.top =
bounds.top + INFO_BAR_OFFSET - this.nodeInfo.barHeight + "px";
this.nodeInfo.positioner.setAttribute("position", "top");
}
let barWidth = this.nodeInfo.positioner.getBoundingClientRect().width;
let left = bounds.right - bounds.width / 2 - barWidth / 2;
// Make sure the whole infobar is visible
if (left < 0) {
left = 0;
this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
} else {
if (left + barWidth > winWidth) {
left = winWidth - barWidth;
this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
} else {
this.nodeInfo.positioner.removeAttribute("hide-arrow");
}
}
this.nodeInfo.positioner.style.left = left + "px";
} else {
this.nodeInfo.positioner.style.left = "0";
this.nodeInfo.positioner.style.top = "0";
this.nodeInfo.positioner.setAttribute("position", "top");
this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
}
},
_attachPageListeners: function() {
if (this.currentNode) {
let win = this.currentNode.ownerGlobal;
win.addEventListener("scroll", this, false);
win.addEventListener("resize", this, false);
win.addEventListener("MozAfterPaint", this, false);
}
},
_detachPageListeners: function() {
if (this.currentNode) {
let win = this.currentNode.ownerGlobal;
win.removeEventListener("scroll", this, false);
win.removeEventListener("resize", this, false);
win.removeEventListener("MozAfterPaint", this, false);
}
},
/**
* Generic event handler.
*
* @param nsIDOMEvent aEvent
* The DOM event object.
*/
handleEvent: function(event) {
switch (event.type) {
case "resize":
case "MozAfterPaint":
case "scroll":
this._update();
break;
}
},
};
/**
* The SimpleOutlineHighlighter is a class that has the same API than the
* BoxModelHighlighter, but adds a pseudo-class on the target element itself
* to draw a simple outline.
* It is used by the HighlighterActor too, but in case the more complex
* BoxModelHighlighter can't be attached (which is the case for FirefoxOS and
* Fennec targets for instance).
*/
function SimpleOutlineHighlighter(tabActor) {
this.chromeDoc = tabActor.window.document;
}
SimpleOutlineHighlighter.prototype = {
/**
* Destroy the nodes. Remove listeners.
*/
destroy: function() {
this.hide();
if (this.installedHelpers) {
this.installedHelpers.clear();
}
this.chromeDoc = null;
},
_installHelperSheet: function(node) {
if (!this.installedHelpers) {
this.installedHelpers = new WeakMap;
}
let win = node.ownerDocument.defaultView;
if (!this.installedHelpers.has(win)) {
let {Style} = require("sdk/stylesheet/style");
let {attach} = require("sdk/content/mod");
let style = Style({source: HELPER_SHEET, type: "agent"});
attach(style, win);
this.installedHelpers.set(win, style);
}
},
/**
* Show the highlighter on a given node
* @param {DOMNode} node
*/
show: function(node) {
if (!this.currentNode || node !== this.currentNode) {
this.hide();
this.currentNode = node;
this._installHelperSheet(node);
DOMUtils.addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
}
},
/**
* Hide the highlighter, the outline and the infobar.
*/
hide: function() {
if (this.currentNode) {
DOMUtils.removePseudoClassLock(this.currentNode, HIGHLIGHTED_PSEUDO_CLASS);
this.currentNode = null;
}
}
};
XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
});