1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/actors/highlighter.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,891 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {Cu, Cc, Ci} = require("chrome"); 1.11 +const Services = require("Services"); 1.12 +const protocol = require("devtools/server/protocol"); 1.13 +const {Arg, Option, method} = protocol; 1.14 +const events = require("sdk/event/core"); 1.15 + 1.16 +const EventEmitter = require("devtools/toolkit/event-emitter"); 1.17 +const GUIDE_STROKE_WIDTH = 1; 1.18 + 1.19 +// Make sure the domnode type is known here 1.20 +require("devtools/server/actors/inspector"); 1.21 + 1.22 +Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); 1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.24 + 1.25 +// FIXME: add ":visited" and ":link" after bug 713106 is fixed 1.26 +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; 1.27 +const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted"; 1.28 +let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } "; 1.29 +HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } "; 1.30 +const XHTML_NS = "http://www.w3.org/1999/xhtml"; 1.31 +const SVG_NS = "http://www.w3.org/2000/svg"; 1.32 +const HIGHLIGHTER_PICKED_TIMER = 1000; 1.33 +const INFO_BAR_OFFSET = 5; 1.34 + 1.35 +/** 1.36 + * The HighlighterActor is the server-side entry points for any tool that wishes 1.37 + * to highlight elements in the content document. 1.38 + * 1.39 + * The highlighter can be retrieved via the inspector's getHighlighter method. 1.40 + */ 1.41 + 1.42 +/** 1.43 + * The HighlighterActor class 1.44 + */ 1.45 +let HighlighterActor = protocol.ActorClass({ 1.46 + typeName: "highlighter", 1.47 + 1.48 + initialize: function(inspector, autohide) { 1.49 + protocol.Actor.prototype.initialize.call(this, null); 1.50 + 1.51 + this._autohide = autohide; 1.52 + this._inspector = inspector; 1.53 + this._walker = this._inspector.walker; 1.54 + this._tabActor = this._inspector.tabActor; 1.55 + 1.56 + this._highlighterReady = this._highlighterReady.bind(this); 1.57 + this._highlighterHidden = this._highlighterHidden.bind(this); 1.58 + 1.59 + if (this._supportsBoxModelHighlighter()) { 1.60 + this._boxModelHighlighter = 1.61 + new BoxModelHighlighter(this._tabActor, this._inspector); 1.62 + 1.63 + this._boxModelHighlighter.on("ready", this._highlighterReady); 1.64 + this._boxModelHighlighter.on("hide", this._highlighterHidden); 1.65 + } else { 1.66 + this._boxModelHighlighter = new SimpleOutlineHighlighter(this._tabActor); 1.67 + } 1.68 + }, 1.69 + 1.70 + get conn() this._inspector && this._inspector.conn, 1.71 + 1.72 + /** 1.73 + * Can the host support the box model highlighter which requires a parent 1.74 + * XUL node to attach itself. 1.75 + */ 1.76 + _supportsBoxModelHighlighter: function() { 1.77 + // Note that <browser>s on Fennec also have a XUL parentNode but the box 1.78 + // model highlighter doesn't display correctly on Fennec (bug 993190) 1.79 + return this._tabActor.browser && 1.80 + !!this._tabActor.browser.parentNode && 1.81 + Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}"; 1.82 + }, 1.83 + 1.84 + destroy: function() { 1.85 + protocol.Actor.prototype.destroy.call(this); 1.86 + if (this._boxModelHighlighter) { 1.87 + this._boxModelHighlighter.off("ready", this._highlighterReady); 1.88 + this._boxModelHighlighter.off("hide", this._highlighterHidden); 1.89 + this._boxModelHighlighter.destroy(); 1.90 + this._boxModelHighlighter = null; 1.91 + } 1.92 + this._autohide = null; 1.93 + this._inspector = null; 1.94 + this._walker = null; 1.95 + this._tabActor = null; 1.96 + }, 1.97 + 1.98 + /** 1.99 + * Display the box model highlighting on a given NodeActor. 1.100 + * There is only one instance of the box model highlighter, so calling this 1.101 + * method several times won't display several highlighters, it will just move 1.102 + * the highlighter instance to these nodes. 1.103 + * 1.104 + * @param NodeActor The node to be highlighted 1.105 + * @param Options See the request part for existing options. Note that not 1.106 + * all options may be supported by all types of highlighters. 1.107 + */ 1.108 + showBoxModel: method(function(node, options={}) { 1.109 + if (node && this._isNodeValidForHighlighting(node.rawNode)) { 1.110 + this._boxModelHighlighter.show(node.rawNode, options); 1.111 + } else { 1.112 + this._boxModelHighlighter.hide(); 1.113 + } 1.114 + }, { 1.115 + request: { 1.116 + node: Arg(0, "domnode"), 1.117 + region: Option(1) 1.118 + } 1.119 + }), 1.120 + 1.121 + _isNodeValidForHighlighting: function(node) { 1.122 + // Is it null or dead? 1.123 + let isNotDead = node && !Cu.isDeadWrapper(node); 1.124 + 1.125 + // Is it connected to the document? 1.126 + let isConnected = false; 1.127 + try { 1.128 + let doc = node.ownerDocument; 1.129 + isConnected = (doc && doc.defaultView && doc.documentElement.contains(node)); 1.130 + } catch (e) { 1.131 + // "can't access dead object" error 1.132 + } 1.133 + 1.134 + // Is it an element node 1.135 + let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE; 1.136 + 1.137 + return isNotDead && isConnected && isElementNode; 1.138 + }, 1.139 + 1.140 + /** 1.141 + * Hide the box model highlighting if it was shown before 1.142 + */ 1.143 + hideBoxModel: method(function() { 1.144 + this._boxModelHighlighter.hide(); 1.145 + }, { 1.146 + request: {} 1.147 + }), 1.148 + 1.149 + /** 1.150 + * Pick a node on click, and highlight hovered nodes in the process. 1.151 + * 1.152 + * This method doesn't respond anything interesting, however, it starts 1.153 + * mousemove, and click listeners on the content document to fire 1.154 + * events and let connected clients know when nodes are hovered over or 1.155 + * clicked. 1.156 + * 1.157 + * Once a node is picked, events will cease, and listeners will be removed. 1.158 + */ 1.159 + _isPicking: false, 1.160 + _hoveredNode: null, 1.161 + 1.162 + pick: method(function() { 1.163 + if (this._isPicking) { 1.164 + return null; 1.165 + } 1.166 + this._isPicking = true; 1.167 + 1.168 + this._preventContentEvent = event => { 1.169 + event.stopPropagation(); 1.170 + event.preventDefault(); 1.171 + }; 1.172 + 1.173 + this._onPick = event => { 1.174 + this._preventContentEvent(event); 1.175 + this._stopPickerListeners(); 1.176 + this._isPicking = false; 1.177 + if (this._autohide) { 1.178 + this._tabActor.window.setTimeout(() => { 1.179 + this._boxModelHighlighter.hide(); 1.180 + }, HIGHLIGHTER_PICKED_TIMER); 1.181 + } 1.182 + events.emit(this._walker, "picker-node-picked", this._findAndAttachElement(event)); 1.183 + }; 1.184 + 1.185 + this._onHovered = event => { 1.186 + this._preventContentEvent(event); 1.187 + let res = this._findAndAttachElement(event); 1.188 + if (this._hoveredNode !== res.node) { 1.189 + this._boxModelHighlighter.show(res.node.rawNode); 1.190 + events.emit(this._walker, "picker-node-hovered", res); 1.191 + this._hoveredNode = res.node; 1.192 + } 1.193 + }; 1.194 + 1.195 + this._tabActor.window.focus(); 1.196 + this._startPickerListeners(); 1.197 + 1.198 + return null; 1.199 + }), 1.200 + 1.201 + _findAndAttachElement: function(event) { 1.202 + let doc = event.target.ownerDocument; 1.203 + 1.204 + let x = event.clientX; 1.205 + let y = event.clientY; 1.206 + 1.207 + let node = doc.elementFromPoint(x, y); 1.208 + return this._walker.attachElement(node); 1.209 + }, 1.210 + 1.211 + /** 1.212 + * Get the right target for listening to mouse events while in pick mode. 1.213 + * - On a firefox desktop content page: tabActor is a BrowserTabActor from 1.214 + * which the browser property will give us a target we can use to listen to 1.215 + * events, even in nested iframes. 1.216 + * - On B2G: tabActor is a ContentActor which doesn't have a browser but 1.217 + * since it overrides BrowserTabActor, it does get a browser property 1.218 + * anyway, which points to its window object. 1.219 + * - When using the Browser Toolbox (to inspect firefox desktop): tabActor is 1.220 + * the RootActor, in which case, the window property can be used to listen 1.221 + * to events 1.222 + */ 1.223 + _getPickerListenerTarget: function() { 1.224 + let actor = this._tabActor; 1.225 + return actor.isRootActor ? actor.window : actor.chromeEventHandler; 1.226 + }, 1.227 + 1.228 + _startPickerListeners: function() { 1.229 + let target = this._getPickerListenerTarget(); 1.230 + target.addEventListener("mousemove", this._onHovered, true); 1.231 + target.addEventListener("click", this._onPick, true); 1.232 + target.addEventListener("mousedown", this._preventContentEvent, true); 1.233 + target.addEventListener("mouseup", this._preventContentEvent, true); 1.234 + target.addEventListener("dblclick", this._preventContentEvent, true); 1.235 + }, 1.236 + 1.237 + _stopPickerListeners: function() { 1.238 + let target = this._getPickerListenerTarget(); 1.239 + target.removeEventListener("mousemove", this._onHovered, true); 1.240 + target.removeEventListener("click", this._onPick, true); 1.241 + target.removeEventListener("mousedown", this._preventContentEvent, true); 1.242 + target.removeEventListener("mouseup", this._preventContentEvent, true); 1.243 + target.removeEventListener("dblclick", this._preventContentEvent, true); 1.244 + }, 1.245 + 1.246 + _highlighterReady: function() { 1.247 + events.emit(this._inspector.walker, "highlighter-ready"); 1.248 + }, 1.249 + 1.250 + _highlighterHidden: function() { 1.251 + events.emit(this._inspector.walker, "highlighter-hide"); 1.252 + }, 1.253 + 1.254 + cancelPick: method(function() { 1.255 + if (this._isPicking) { 1.256 + this._boxModelHighlighter.hide(); 1.257 + this._stopPickerListeners(); 1.258 + this._isPicking = false; 1.259 + this._hoveredNode = null; 1.260 + } 1.261 + }) 1.262 +}); 1.263 + 1.264 +exports.HighlighterActor = HighlighterActor; 1.265 + 1.266 +/** 1.267 + * The HighlighterFront class 1.268 + */ 1.269 +let HighlighterFront = protocol.FrontClass(HighlighterActor, {}); 1.270 + 1.271 +/** 1.272 + * The BoxModelHighlighter is the class that actually draws the the box model 1.273 + * regions on top of a node. 1.274 + * It is used by the HighlighterActor. 1.275 + * 1.276 + * Usage example: 1.277 + * 1.278 + * let h = new BoxModelHighlighter(browser); 1.279 + * h.show(node); 1.280 + * h.hide(); 1.281 + * h.destroy(); 1.282 + * 1.283 + * Structure: 1.284 + * <stack class="highlighter-container"> 1.285 + * <svg class="box-model-root" hidden="true"> 1.286 + * <g class="box-model-container"> 1.287 + * <polygon class="box-model-margin" points="317,122 747,36 747,181 317,267" /> 1.288 + * <polygon class="box-model-border" points="317,128 747,42 747,161 317,247" /> 1.289 + * <polygon class="box-model-padding" points="323,127 747,42 747,161 323,246" /> 1.290 + * <polygon class="box-model-content" points="335,137 735,57 735,152 335,232" /> 1.291 + * </g> 1.292 + * <line class="box-model-guide-top" x1="0" y1="592" x2="99999" y2="592" /> 1.293 + * <line class="box-model-guide-right" x1="735" y1="0" x2="735" y2="99999" /> 1.294 + * <line class="box-model-guide-bottom" x1="0" y1="612" x2="99999" y2="612" /> 1.295 + * <line class="box-model-guide-left" x1="334" y1="0" x2="334" y2="99999" /> 1.296 + * </svg> 1.297 + * <box class="highlighter-nodeinfobar-container"> 1.298 + * <box class="highlighter-nodeinfobar-positioner" position="top" /> 1.299 + * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top" /> 1.300 + * <hbox class="highlighter-nodeinfobar"> 1.301 + * <hbox class="highlighter-nodeinfobar-text" align="center" flex="1"> 1.302 + * <span class="highlighter-nodeinfobar-tagname">Node name</span> 1.303 + * <span class="highlighter-nodeinfobar-id">Node id</span> 1.304 + * <span class="highlighter-nodeinfobar-classes">.someClass</span> 1.305 + * <span class="highlighter-nodeinfobar-pseudo-classes">:hover</span> 1.306 + * </hbox> 1.307 + * </hbox> 1.308 + * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/> 1.309 + * </box> 1.310 + * </box> 1.311 + * </stack> 1.312 + */ 1.313 +function BoxModelHighlighter(tabActor, inspector) { 1.314 + this.browser = tabActor.browser; 1.315 + this.win = tabActor.window; 1.316 + this.chromeDoc = this.browser.ownerDocument; 1.317 + this.chromeWin = this.chromeDoc.defaultView; 1.318 + this._inspector = inspector; 1.319 + 1.320 + this.layoutHelpers = new LayoutHelpers(this.win); 1.321 + this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin); 1.322 + 1.323 + this.transitionDisabler = null; 1.324 + this.pageEventsMuter = null; 1.325 + this._update = this._update.bind(this); 1.326 + this.handleEvent = this.handleEvent.bind(this); 1.327 + this.currentNode = null; 1.328 + 1.329 + EventEmitter.decorate(this); 1.330 + this._initMarkup(); 1.331 +} 1.332 + 1.333 +BoxModelHighlighter.prototype = { 1.334 + get zoom() { 1.335 + return this.win.QueryInterface(Ci.nsIInterfaceRequestor) 1.336 + .getInterface(Ci.nsIDOMWindowUtils).fullZoom; 1.337 + }, 1.338 + 1.339 + _initMarkup: function() { 1.340 + let stack = this.browser.parentNode; 1.341 + 1.342 + this._highlighterContainer = this.chromeDoc.createElement("stack"); 1.343 + this._highlighterContainer.className = "highlighter-container"; 1.344 + 1.345 + this._svgRoot = this._createSVGNode("root", "svg", this._highlighterContainer); 1.346 + 1.347 + // Set the SVG canvas height to 0 to stop content jumping around on small 1.348 + // screens. 1.349 + this._svgRoot.setAttribute("height", "0"); 1.350 + 1.351 + this._boxModelContainer = this._createSVGNode("container", "g", this._svgRoot); 1.352 + 1.353 + this._boxModelNodes = { 1.354 + margin: this._createSVGNode("margin", "polygon", this._boxModelContainer), 1.355 + border: this._createSVGNode("border", "polygon", this._boxModelContainer), 1.356 + padding: this._createSVGNode("padding", "polygon", this._boxModelContainer), 1.357 + content: this._createSVGNode("content", "polygon", this._boxModelContainer) 1.358 + }; 1.359 + 1.360 + this._guideNodes = { 1.361 + top: this._createSVGNode("guide-top", "line", this._svgRoot), 1.362 + right: this._createSVGNode("guide-right", "line", this._svgRoot), 1.363 + bottom: this._createSVGNode("guide-bottom", "line", this._svgRoot), 1.364 + left: this._createSVGNode("guide-left", "line", this._svgRoot) 1.365 + }; 1.366 + 1.367 + this._guideNodes.top.setAttribute("stroke-width", GUIDE_STROKE_WIDTH); 1.368 + this._guideNodes.right.setAttribute("stroke-width", GUIDE_STROKE_WIDTH); 1.369 + this._guideNodes.bottom.setAttribute("stroke-width", GUIDE_STROKE_WIDTH); 1.370 + this._guideNodes.left.setAttribute("stroke-width", GUIDE_STROKE_WIDTH); 1.371 + 1.372 + this._highlighterContainer.appendChild(this._svgRoot); 1.373 + 1.374 + let infobarContainer = this.chromeDoc.createElement("box"); 1.375 + infobarContainer.className = "highlighter-nodeinfobar-container"; 1.376 + this._highlighterContainer.appendChild(infobarContainer); 1.377 + 1.378 + // Insert the highlighter right after the browser 1.379 + stack.insertBefore(this._highlighterContainer, stack.childNodes[1]); 1.380 + 1.381 + // Building the infobar 1.382 + let infobarPositioner = this.chromeDoc.createElement("box"); 1.383 + infobarPositioner.className = "highlighter-nodeinfobar-positioner"; 1.384 + infobarPositioner.setAttribute("position", "top"); 1.385 + infobarPositioner.setAttribute("disabled", "true"); 1.386 + 1.387 + let nodeInfobar = this.chromeDoc.createElement("hbox"); 1.388 + nodeInfobar.className = "highlighter-nodeinfobar"; 1.389 + 1.390 + let arrowBoxTop = this.chromeDoc.createElement("box"); 1.391 + arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"; 1.392 + 1.393 + let arrowBoxBottom = this.chromeDoc.createElement("box"); 1.394 + arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"; 1.395 + 1.396 + let tagNameLabel = this.chromeDoc.createElementNS(XHTML_NS, "span"); 1.397 + tagNameLabel.className = "highlighter-nodeinfobar-tagname"; 1.398 + 1.399 + let idLabel = this.chromeDoc.createElementNS(XHTML_NS, "span"); 1.400 + idLabel.className = "highlighter-nodeinfobar-id"; 1.401 + 1.402 + let classesBox = this.chromeDoc.createElementNS(XHTML_NS, "span"); 1.403 + classesBox.className = "highlighter-nodeinfobar-classes"; 1.404 + 1.405 + let pseudoClassesBox = this.chromeDoc.createElementNS(XHTML_NS, "span"); 1.406 + pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes"; 1.407 + 1.408 + // Add some content to force a better boundingClientRect 1.409 + pseudoClassesBox.textContent = " "; 1.410 + 1.411 + // <hbox class="highlighter-nodeinfobar-text"/> 1.412 + let texthbox = this.chromeDoc.createElement("hbox"); 1.413 + texthbox.className = "highlighter-nodeinfobar-text"; 1.414 + texthbox.setAttribute("align", "center"); 1.415 + texthbox.setAttribute("flex", "1"); 1.416 + 1.417 + texthbox.appendChild(tagNameLabel); 1.418 + texthbox.appendChild(idLabel); 1.419 + texthbox.appendChild(classesBox); 1.420 + texthbox.appendChild(pseudoClassesBox); 1.421 + 1.422 + nodeInfobar.appendChild(texthbox); 1.423 + 1.424 + infobarPositioner.appendChild(arrowBoxTop); 1.425 + infobarPositioner.appendChild(nodeInfobar); 1.426 + infobarPositioner.appendChild(arrowBoxBottom); 1.427 + 1.428 + infobarContainer.appendChild(infobarPositioner); 1.429 + 1.430 + let barHeight = infobarPositioner.getBoundingClientRect().height; 1.431 + 1.432 + this.nodeInfo = { 1.433 + tagNameLabel: tagNameLabel, 1.434 + idLabel: idLabel, 1.435 + classesBox: classesBox, 1.436 + pseudoClassesBox: pseudoClassesBox, 1.437 + positioner: infobarPositioner, 1.438 + barHeight: barHeight, 1.439 + }; 1.440 + }, 1.441 + 1.442 + _createSVGNode: function(classPostfix, nodeType, parent) { 1.443 + let node = this.chromeDoc.createElementNS(SVG_NS, nodeType); 1.444 + node.setAttribute("class", "box-model-" + classPostfix); 1.445 + 1.446 + parent.appendChild(node); 1.447 + 1.448 + return node; 1.449 + }, 1.450 + 1.451 + /** 1.452 + * Destroy the nodes. Remove listeners. 1.453 + */ 1.454 + destroy: function() { 1.455 + this.hide(); 1.456 + 1.457 + this.chromeWin.clearTimeout(this.transitionDisabler); 1.458 + this.chromeWin.clearTimeout(this.pageEventsMuter); 1.459 + 1.460 + this.nodeInfo = null; 1.461 + 1.462 + this._highlighterContainer.remove(); 1.463 + this._highlighterContainer = null; 1.464 + 1.465 + this.rect = null; 1.466 + this.win = null; 1.467 + this.browser = null; 1.468 + this.chromeDoc = null; 1.469 + this.chromeWin = null; 1.470 + this.currentNode = null; 1.471 + }, 1.472 + 1.473 + /** 1.474 + * Show the highlighter on a given node 1.475 + * 1.476 + * @param {DOMNode} node 1.477 + * @param {Object} options 1.478 + * Object used for passing options 1.479 + */ 1.480 + show: function(node, options={}) { 1.481 + this.currentNode = node; 1.482 + 1.483 + this._showInfobar(); 1.484 + this._detachPageListeners(); 1.485 + this._attachPageListeners(); 1.486 + this._update(); 1.487 + this._trackMutations(); 1.488 + }, 1.489 + 1.490 + _trackMutations: function() { 1.491 + if (this.currentNode) { 1.492 + let win = this.currentNode.ownerDocument.defaultView; 1.493 + this.currentNodeObserver = new win.MutationObserver(() => { 1.494 + this._update(); 1.495 + }); 1.496 + this.currentNodeObserver.observe(this.currentNode, {attributes: true}); 1.497 + } 1.498 + }, 1.499 + 1.500 + _untrackMutations: function() { 1.501 + if (this.currentNode) { 1.502 + if (this.currentNodeObserver) { 1.503 + // The following may fail with a "can't access dead object" exception 1.504 + // when the actor is being destroyed 1.505 + try { 1.506 + this.currentNodeObserver.disconnect(); 1.507 + } catch (e) {} 1.508 + this.currentNodeObserver = null; 1.509 + } 1.510 + } 1.511 + }, 1.512 + 1.513 + /** 1.514 + * Update the highlighter on the current highlighted node (the one that was 1.515 + * passed as an argument to show(node)). 1.516 + * Should be called whenever node size or attributes change 1.517 + * @param {Object} options 1.518 + * Object used for passing options. Valid options are: 1.519 + * - box: "content", "padding", "border" or "margin." This specifies 1.520 + * the box that the guides should outline. Default is content. 1.521 + */ 1.522 + _update: function(options={}) { 1.523 + if (this.currentNode) { 1.524 + if (this._highlightBoxModel(options)) { 1.525 + this._showInfobar(); 1.526 + } else { 1.527 + // Nothing to highlight (0px rectangle like a <script> tag for instance) 1.528 + this.hide(); 1.529 + } 1.530 + this.emit("ready"); 1.531 + } 1.532 + }, 1.533 + 1.534 + /** 1.535 + * Hide the highlighter, the outline and the infobar. 1.536 + */ 1.537 + hide: function() { 1.538 + if (this.currentNode) { 1.539 + this._untrackMutations(); 1.540 + this.currentNode = null; 1.541 + this._hideBoxModel(); 1.542 + this._hideInfobar(); 1.543 + this._detachPageListeners(); 1.544 + } 1.545 + this.emit("hide"); 1.546 + }, 1.547 + 1.548 + /** 1.549 + * Hide the infobar 1.550 + */ 1.551 + _hideInfobar: function() { 1.552 + this.nodeInfo.positioner.setAttribute("hidden", "true"); 1.553 + }, 1.554 + 1.555 + /** 1.556 + * Show the infobar 1.557 + */ 1.558 + _showInfobar: function() { 1.559 + this.nodeInfo.positioner.removeAttribute("hidden"); 1.560 + this._updateInfobar(); 1.561 + }, 1.562 + 1.563 + /** 1.564 + * Hide the box model 1.565 + */ 1.566 + _hideBoxModel: function() { 1.567 + this._svgRoot.setAttribute("hidden", "true"); 1.568 + }, 1.569 + 1.570 + /** 1.571 + * Show the box model 1.572 + */ 1.573 + _showBoxModel: function() { 1.574 + this._svgRoot.removeAttribute("hidden"); 1.575 + }, 1.576 + 1.577 + /** 1.578 + * Highlight the box model. 1.579 + * 1.580 + * @param {Object} options 1.581 + * Object used for passing options. Valid options are: 1.582 + * - region: "content", "padding", "border" or "margin." This specifies 1.583 + * the region that the guides should outline. Default is content. 1.584 + * @return {boolean} 1.585 + * True if the rectangle was highlighted, false otherwise. 1.586 + */ 1.587 + _highlightBoxModel: function(options) { 1.588 + let isShown = false; 1.589 + 1.590 + options.region = options.region || "content"; 1.591 + 1.592 + // TODO: Remove this polyfill 1.593 + this.rect = 1.594 + this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, "margin"); 1.595 + 1.596 + if (!this.rect) { 1.597 + return null; 1.598 + } 1.599 + 1.600 + if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) { 1.601 + for (let boxType in this._boxModelNodes) { 1.602 + // TODO: Remove this polyfill 1.603 + let {p1, p2, p3, p4} = boxType === "margin" ? this.rect : 1.604 + this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, boxType); 1.605 + 1.606 + let boxNode = this._boxModelNodes[boxType]; 1.607 + boxNode.setAttribute("points", 1.608 + p1.x + "," + p1.y + " " + 1.609 + p2.x + "," + p2.y + " " + 1.610 + p3.x + "," + p3.y + " " + 1.611 + p4.x + "," + p4.y); 1.612 + 1.613 + if (boxType === options.region) { 1.614 + this._showGuides(p1, p2, p3, p4); 1.615 + } 1.616 + } 1.617 + 1.618 + isShown = true; 1.619 + this._showBoxModel(); 1.620 + } else { 1.621 + // Only return false if the element really is invisible. 1.622 + // A height of 0 and a non-0 width corresponds to a visible element that 1.623 + // is below the fold for instance 1.624 + if (this.rect.width > 0 || this.rect.height > 0) { 1.625 + isShown = true; 1.626 + this._hideBoxModel(); 1.627 + } 1.628 + } 1.629 + return isShown; 1.630 + }, 1.631 + 1.632 + /** 1.633 + * We only want to show guides for horizontal and vertical edges as this helps 1.634 + * to line them up. This method finds these edges and displays a guide there. 1.635 + * 1.636 + * @param {DOMPoint} p1 1.637 + * Point 1 1.638 + * @param {DOMPoint} p2 1.639 + * Point 2 1.640 + * @param {DOMPoint} p3 [description] 1.641 + * Point 3 1.642 + * @param {DOMPoint} p4 [description] 1.643 + * Point 4 1.644 + */ 1.645 + _showGuides: function(p1, p2, p3, p4) { 1.646 + let allX = [p1.x, p2.x, p3.x, p4.x].sort(); 1.647 + let allY = [p1.y, p2.y, p3.y, p4.y].sort(); 1.648 + let toShowX = []; 1.649 + let toShowY = []; 1.650 + 1.651 + for (let arr of [allX, allY]) { 1.652 + for (let i = 0; i < arr.length; i++) { 1.653 + let val = arr[i]; 1.654 + 1.655 + if (i !== arr.lastIndexOf(val)) { 1.656 + if (arr === allX) { 1.657 + toShowX.push(val); 1.658 + } else { 1.659 + toShowY.push(val); 1.660 + } 1.661 + arr.splice(arr.lastIndexOf(val), 1); 1.662 + } 1.663 + } 1.664 + } 1.665 + 1.666 + // Move guide into place or hide it if no valid co-ordinate was found. 1.667 + this._updateGuide(this._guideNodes.top, toShowY[0]); 1.668 + this._updateGuide(this._guideNodes.right, toShowX[1]); 1.669 + this._updateGuide(this._guideNodes.bottom, toShowY[1]); 1.670 + this._updateGuide(this._guideNodes.left, toShowX[0]); 1.671 + }, 1.672 + 1.673 + /** 1.674 + * Move a guide to the appropriate position and display it. If no point is 1.675 + * passed then the guide is hidden. 1.676 + * 1.677 + * @param {SVGLine} guide 1.678 + * The guide to update 1.679 + * @param {Integer} point 1.680 + * x or y co-ordinate. If this is undefined we hide the guide. 1.681 + */ 1.682 + _updateGuide: function(guide, point=-1) { 1.683 + if (point > 0) { 1.684 + let offset = GUIDE_STROKE_WIDTH / 2; 1.685 + 1.686 + if (guide === this._guideNodes.top || guide === this._guideNodes.left) { 1.687 + point -= offset; 1.688 + } else { 1.689 + point += offset; 1.690 + } 1.691 + 1.692 + if (guide === this._guideNodes.top || guide === this._guideNodes.bottom) { 1.693 + guide.setAttribute("x1", 0); 1.694 + guide.setAttribute("y1", point); 1.695 + guide.setAttribute("x2", "100%"); 1.696 + guide.setAttribute("y2", point); 1.697 + } else { 1.698 + guide.setAttribute("x1", point); 1.699 + guide.setAttribute("y1", 0); 1.700 + guide.setAttribute("x2", point); 1.701 + guide.setAttribute("y2", "100%"); 1.702 + } 1.703 + guide.removeAttribute("hidden"); 1.704 + return true; 1.705 + } else { 1.706 + guide.setAttribute("hidden", "true"); 1.707 + return false; 1.708 + } 1.709 + }, 1.710 + 1.711 + /** 1.712 + * Update node information (tagName#id.class) 1.713 + */ 1.714 + _updateInfobar: function() { 1.715 + if (this.currentNode) { 1.716 + // Tag name 1.717 + this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName; 1.718 + 1.719 + // ID 1.720 + this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : ""; 1.721 + 1.722 + // Classes 1.723 + let classes = this.nodeInfo.classesBox; 1.724 + 1.725 + classes.textContent = this.currentNode.classList.length ? 1.726 + "." + Array.join(this.currentNode.classList, ".") : ""; 1.727 + 1.728 + // Pseudo-classes 1.729 + let pseudos = PSEUDO_CLASSES.filter(pseudo => { 1.730 + return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo); 1.731 + }, this); 1.732 + 1.733 + let pseudoBox = this.nodeInfo.pseudoClassesBox; 1.734 + pseudoBox.textContent = pseudos.join(""); 1.735 + 1.736 + this._moveInfobar(); 1.737 + } 1.738 + }, 1.739 + 1.740 + /** 1.741 + * Move the Infobar to the right place in the highlighter. 1.742 + */ 1.743 + _moveInfobar: function() { 1.744 + if (this.rect) { 1.745 + let bounds = this.rect.bounds; 1.746 + let winHeight = this.win.innerHeight * this.zoom; 1.747 + let winWidth = this.win.innerWidth * this.zoom; 1.748 + 1.749 + this.nodeInfo.positioner.removeAttribute("disabled"); 1.750 + // Can the bar be above the node? 1.751 + if (bounds.top < this.nodeInfo.barHeight) { 1.752 + // No. Can we move the toolbar under the node? 1.753 + if (bounds.bottom + this.nodeInfo.barHeight > winHeight) { 1.754 + // No. Let's move it inside. 1.755 + this.nodeInfo.positioner.style.top = bounds.top + "px"; 1.756 + this.nodeInfo.positioner.setAttribute("position", "overlap"); 1.757 + } else { 1.758 + // Yes. Let's move it under the node. 1.759 + this.nodeInfo.positioner.style.top = bounds.bottom - INFO_BAR_OFFSET + "px"; 1.760 + this.nodeInfo.positioner.setAttribute("position", "bottom"); 1.761 + } 1.762 + } else { 1.763 + // Yes. Let's move it on top of the node. 1.764 + this.nodeInfo.positioner.style.top = 1.765 + bounds.top + INFO_BAR_OFFSET - this.nodeInfo.barHeight + "px"; 1.766 + this.nodeInfo.positioner.setAttribute("position", "top"); 1.767 + } 1.768 + 1.769 + let barWidth = this.nodeInfo.positioner.getBoundingClientRect().width; 1.770 + let left = bounds.right - bounds.width / 2 - barWidth / 2; 1.771 + 1.772 + // Make sure the whole infobar is visible 1.773 + if (left < 0) { 1.774 + left = 0; 1.775 + this.nodeInfo.positioner.setAttribute("hide-arrow", "true"); 1.776 + } else { 1.777 + if (left + barWidth > winWidth) { 1.778 + left = winWidth - barWidth; 1.779 + this.nodeInfo.positioner.setAttribute("hide-arrow", "true"); 1.780 + } else { 1.781 + this.nodeInfo.positioner.removeAttribute("hide-arrow"); 1.782 + } 1.783 + } 1.784 + this.nodeInfo.positioner.style.left = left + "px"; 1.785 + } else { 1.786 + this.nodeInfo.positioner.style.left = "0"; 1.787 + this.nodeInfo.positioner.style.top = "0"; 1.788 + this.nodeInfo.positioner.setAttribute("position", "top"); 1.789 + this.nodeInfo.positioner.setAttribute("hide-arrow", "true"); 1.790 + } 1.791 + }, 1.792 + 1.793 + _attachPageListeners: function() { 1.794 + if (this.currentNode) { 1.795 + let win = this.currentNode.ownerGlobal; 1.796 + 1.797 + win.addEventListener("scroll", this, false); 1.798 + win.addEventListener("resize", this, false); 1.799 + win.addEventListener("MozAfterPaint", this, false); 1.800 + } 1.801 + }, 1.802 + 1.803 + _detachPageListeners: function() { 1.804 + if (this.currentNode) { 1.805 + let win = this.currentNode.ownerGlobal; 1.806 + 1.807 + win.removeEventListener("scroll", this, false); 1.808 + win.removeEventListener("resize", this, false); 1.809 + win.removeEventListener("MozAfterPaint", this, false); 1.810 + } 1.811 + }, 1.812 + 1.813 + /** 1.814 + * Generic event handler. 1.815 + * 1.816 + * @param nsIDOMEvent aEvent 1.817 + * The DOM event object. 1.818 + */ 1.819 + handleEvent: function(event) { 1.820 + switch (event.type) { 1.821 + case "resize": 1.822 + case "MozAfterPaint": 1.823 + case "scroll": 1.824 + this._update(); 1.825 + break; 1.826 + } 1.827 + }, 1.828 +}; 1.829 + 1.830 +/** 1.831 + * The SimpleOutlineHighlighter is a class that has the same API than the 1.832 + * BoxModelHighlighter, but adds a pseudo-class on the target element itself 1.833 + * to draw a simple outline. 1.834 + * It is used by the HighlighterActor too, but in case the more complex 1.835 + * BoxModelHighlighter can't be attached (which is the case for FirefoxOS and 1.836 + * Fennec targets for instance). 1.837 + */ 1.838 +function SimpleOutlineHighlighter(tabActor) { 1.839 + this.chromeDoc = tabActor.window.document; 1.840 +} 1.841 + 1.842 +SimpleOutlineHighlighter.prototype = { 1.843 + /** 1.844 + * Destroy the nodes. Remove listeners. 1.845 + */ 1.846 + destroy: function() { 1.847 + this.hide(); 1.848 + if (this.installedHelpers) { 1.849 + this.installedHelpers.clear(); 1.850 + } 1.851 + this.chromeDoc = null; 1.852 + }, 1.853 + 1.854 + _installHelperSheet: function(node) { 1.855 + if (!this.installedHelpers) { 1.856 + this.installedHelpers = new WeakMap; 1.857 + } 1.858 + let win = node.ownerDocument.defaultView; 1.859 + if (!this.installedHelpers.has(win)) { 1.860 + let {Style} = require("sdk/stylesheet/style"); 1.861 + let {attach} = require("sdk/content/mod"); 1.862 + let style = Style({source: HELPER_SHEET, type: "agent"}); 1.863 + attach(style, win); 1.864 + this.installedHelpers.set(win, style); 1.865 + } 1.866 + }, 1.867 + 1.868 + /** 1.869 + * Show the highlighter on a given node 1.870 + * @param {DOMNode} node 1.871 + */ 1.872 + show: function(node) { 1.873 + if (!this.currentNode || node !== this.currentNode) { 1.874 + this.hide(); 1.875 + this.currentNode = node; 1.876 + this._installHelperSheet(node); 1.877 + DOMUtils.addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS); 1.878 + } 1.879 + }, 1.880 + 1.881 + /** 1.882 + * Hide the highlighter, the outline and the infobar. 1.883 + */ 1.884 + hide: function() { 1.885 + if (this.currentNode) { 1.886 + DOMUtils.removePseudoClassLock(this.currentNode, HIGHLIGHTED_PSEUDO_CLASS); 1.887 + this.currentNode = null; 1.888 + } 1.889 + } 1.890 +}; 1.891 + 1.892 +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { 1.893 + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils) 1.894 +});