toolkit/devtools/server/actors/highlighter.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 const {Cu, Cc, Ci} = require("chrome");
michael@0 8 const Services = require("Services");
michael@0 9 const protocol = require("devtools/server/protocol");
michael@0 10 const {Arg, Option, method} = protocol;
michael@0 11 const events = require("sdk/event/core");
michael@0 12
michael@0 13 const EventEmitter = require("devtools/toolkit/event-emitter");
michael@0 14 const GUIDE_STROKE_WIDTH = 1;
michael@0 15
michael@0 16 // Make sure the domnode type is known here
michael@0 17 require("devtools/server/actors/inspector");
michael@0 18
michael@0 19 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
michael@0 20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 21
michael@0 22 // FIXME: add ":visited" and ":link" after bug 713106 is fixed
michael@0 23 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
michael@0 24 const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
michael@0 25 let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
michael@0 26 HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
michael@0 27 const XHTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 28 const SVG_NS = "http://www.w3.org/2000/svg";
michael@0 29 const HIGHLIGHTER_PICKED_TIMER = 1000;
michael@0 30 const INFO_BAR_OFFSET = 5;
michael@0 31
michael@0 32 /**
michael@0 33 * The HighlighterActor is the server-side entry points for any tool that wishes
michael@0 34 * to highlight elements in the content document.
michael@0 35 *
michael@0 36 * The highlighter can be retrieved via the inspector's getHighlighter method.
michael@0 37 */
michael@0 38
michael@0 39 /**
michael@0 40 * The HighlighterActor class
michael@0 41 */
michael@0 42 let HighlighterActor = protocol.ActorClass({
michael@0 43 typeName: "highlighter",
michael@0 44
michael@0 45 initialize: function(inspector, autohide) {
michael@0 46 protocol.Actor.prototype.initialize.call(this, null);
michael@0 47
michael@0 48 this._autohide = autohide;
michael@0 49 this._inspector = inspector;
michael@0 50 this._walker = this._inspector.walker;
michael@0 51 this._tabActor = this._inspector.tabActor;
michael@0 52
michael@0 53 this._highlighterReady = this._highlighterReady.bind(this);
michael@0 54 this._highlighterHidden = this._highlighterHidden.bind(this);
michael@0 55
michael@0 56 if (this._supportsBoxModelHighlighter()) {
michael@0 57 this._boxModelHighlighter =
michael@0 58 new BoxModelHighlighter(this._tabActor, this._inspector);
michael@0 59
michael@0 60 this._boxModelHighlighter.on("ready", this._highlighterReady);
michael@0 61 this._boxModelHighlighter.on("hide", this._highlighterHidden);
michael@0 62 } else {
michael@0 63 this._boxModelHighlighter = new SimpleOutlineHighlighter(this._tabActor);
michael@0 64 }
michael@0 65 },
michael@0 66
michael@0 67 get conn() this._inspector && this._inspector.conn,
michael@0 68
michael@0 69 /**
michael@0 70 * Can the host support the box model highlighter which requires a parent
michael@0 71 * XUL node to attach itself.
michael@0 72 */
michael@0 73 _supportsBoxModelHighlighter: function() {
michael@0 74 // Note that <browser>s on Fennec also have a XUL parentNode but the box
michael@0 75 // model highlighter doesn't display correctly on Fennec (bug 993190)
michael@0 76 return this._tabActor.browser &&
michael@0 77 !!this._tabActor.browser.parentNode &&
michael@0 78 Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
michael@0 79 },
michael@0 80
michael@0 81 destroy: function() {
michael@0 82 protocol.Actor.prototype.destroy.call(this);
michael@0 83 if (this._boxModelHighlighter) {
michael@0 84 this._boxModelHighlighter.off("ready", this._highlighterReady);
michael@0 85 this._boxModelHighlighter.off("hide", this._highlighterHidden);
michael@0 86 this._boxModelHighlighter.destroy();
michael@0 87 this._boxModelHighlighter = null;
michael@0 88 }
michael@0 89 this._autohide = null;
michael@0 90 this._inspector = null;
michael@0 91 this._walker = null;
michael@0 92 this._tabActor = null;
michael@0 93 },
michael@0 94
michael@0 95 /**
michael@0 96 * Display the box model highlighting on a given NodeActor.
michael@0 97 * There is only one instance of the box model highlighter, so calling this
michael@0 98 * method several times won't display several highlighters, it will just move
michael@0 99 * the highlighter instance to these nodes.
michael@0 100 *
michael@0 101 * @param NodeActor The node to be highlighted
michael@0 102 * @param Options See the request part for existing options. Note that not
michael@0 103 * all options may be supported by all types of highlighters.
michael@0 104 */
michael@0 105 showBoxModel: method(function(node, options={}) {
michael@0 106 if (node && this._isNodeValidForHighlighting(node.rawNode)) {
michael@0 107 this._boxModelHighlighter.show(node.rawNode, options);
michael@0 108 } else {
michael@0 109 this._boxModelHighlighter.hide();
michael@0 110 }
michael@0 111 }, {
michael@0 112 request: {
michael@0 113 node: Arg(0, "domnode"),
michael@0 114 region: Option(1)
michael@0 115 }
michael@0 116 }),
michael@0 117
michael@0 118 _isNodeValidForHighlighting: function(node) {
michael@0 119 // Is it null or dead?
michael@0 120 let isNotDead = node && !Cu.isDeadWrapper(node);
michael@0 121
michael@0 122 // Is it connected to the document?
michael@0 123 let isConnected = false;
michael@0 124 try {
michael@0 125 let doc = node.ownerDocument;
michael@0 126 isConnected = (doc && doc.defaultView && doc.documentElement.contains(node));
michael@0 127 } catch (e) {
michael@0 128 // "can't access dead object" error
michael@0 129 }
michael@0 130
michael@0 131 // Is it an element node
michael@0 132 let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE;
michael@0 133
michael@0 134 return isNotDead && isConnected && isElementNode;
michael@0 135 },
michael@0 136
michael@0 137 /**
michael@0 138 * Hide the box model highlighting if it was shown before
michael@0 139 */
michael@0 140 hideBoxModel: method(function() {
michael@0 141 this._boxModelHighlighter.hide();
michael@0 142 }, {
michael@0 143 request: {}
michael@0 144 }),
michael@0 145
michael@0 146 /**
michael@0 147 * Pick a node on click, and highlight hovered nodes in the process.
michael@0 148 *
michael@0 149 * This method doesn't respond anything interesting, however, it starts
michael@0 150 * mousemove, and click listeners on the content document to fire
michael@0 151 * events and let connected clients know when nodes are hovered over or
michael@0 152 * clicked.
michael@0 153 *
michael@0 154 * Once a node is picked, events will cease, and listeners will be removed.
michael@0 155 */
michael@0 156 _isPicking: false,
michael@0 157 _hoveredNode: null,
michael@0 158
michael@0 159 pick: method(function() {
michael@0 160 if (this._isPicking) {
michael@0 161 return null;
michael@0 162 }
michael@0 163 this._isPicking = true;
michael@0 164
michael@0 165 this._preventContentEvent = event => {
michael@0 166 event.stopPropagation();
michael@0 167 event.preventDefault();
michael@0 168 };
michael@0 169
michael@0 170 this._onPick = event => {
michael@0 171 this._preventContentEvent(event);
michael@0 172 this._stopPickerListeners();
michael@0 173 this._isPicking = false;
michael@0 174 if (this._autohide) {
michael@0 175 this._tabActor.window.setTimeout(() => {
michael@0 176 this._boxModelHighlighter.hide();
michael@0 177 }, HIGHLIGHTER_PICKED_TIMER);
michael@0 178 }
michael@0 179 events.emit(this._walker, "picker-node-picked", this._findAndAttachElement(event));
michael@0 180 };
michael@0 181
michael@0 182 this._onHovered = event => {
michael@0 183 this._preventContentEvent(event);
michael@0 184 let res = this._findAndAttachElement(event);
michael@0 185 if (this._hoveredNode !== res.node) {
michael@0 186 this._boxModelHighlighter.show(res.node.rawNode);
michael@0 187 events.emit(this._walker, "picker-node-hovered", res);
michael@0 188 this._hoveredNode = res.node;
michael@0 189 }
michael@0 190 };
michael@0 191
michael@0 192 this._tabActor.window.focus();
michael@0 193 this._startPickerListeners();
michael@0 194
michael@0 195 return null;
michael@0 196 }),
michael@0 197
michael@0 198 _findAndAttachElement: function(event) {
michael@0 199 let doc = event.target.ownerDocument;
michael@0 200
michael@0 201 let x = event.clientX;
michael@0 202 let y = event.clientY;
michael@0 203
michael@0 204 let node = doc.elementFromPoint(x, y);
michael@0 205 return this._walker.attachElement(node);
michael@0 206 },
michael@0 207
michael@0 208 /**
michael@0 209 * Get the right target for listening to mouse events while in pick mode.
michael@0 210 * - On a firefox desktop content page: tabActor is a BrowserTabActor from
michael@0 211 * which the browser property will give us a target we can use to listen to
michael@0 212 * events, even in nested iframes.
michael@0 213 * - On B2G: tabActor is a ContentActor which doesn't have a browser but
michael@0 214 * since it overrides BrowserTabActor, it does get a browser property
michael@0 215 * anyway, which points to its window object.
michael@0 216 * - When using the Browser Toolbox (to inspect firefox desktop): tabActor is
michael@0 217 * the RootActor, in which case, the window property can be used to listen
michael@0 218 * to events
michael@0 219 */
michael@0 220 _getPickerListenerTarget: function() {
michael@0 221 let actor = this._tabActor;
michael@0 222 return actor.isRootActor ? actor.window : actor.chromeEventHandler;
michael@0 223 },
michael@0 224
michael@0 225 _startPickerListeners: function() {
michael@0 226 let target = this._getPickerListenerTarget();
michael@0 227 target.addEventListener("mousemove", this._onHovered, true);
michael@0 228 target.addEventListener("click", this._onPick, true);
michael@0 229 target.addEventListener("mousedown", this._preventContentEvent, true);
michael@0 230 target.addEventListener("mouseup", this._preventContentEvent, true);
michael@0 231 target.addEventListener("dblclick", this._preventContentEvent, true);
michael@0 232 },
michael@0 233
michael@0 234 _stopPickerListeners: function() {
michael@0 235 let target = this._getPickerListenerTarget();
michael@0 236 target.removeEventListener("mousemove", this._onHovered, true);
michael@0 237 target.removeEventListener("click", this._onPick, true);
michael@0 238 target.removeEventListener("mousedown", this._preventContentEvent, true);
michael@0 239 target.removeEventListener("mouseup", this._preventContentEvent, true);
michael@0 240 target.removeEventListener("dblclick", this._preventContentEvent, true);
michael@0 241 },
michael@0 242
michael@0 243 _highlighterReady: function() {
michael@0 244 events.emit(this._inspector.walker, "highlighter-ready");
michael@0 245 },
michael@0 246
michael@0 247 _highlighterHidden: function() {
michael@0 248 events.emit(this._inspector.walker, "highlighter-hide");
michael@0 249 },
michael@0 250
michael@0 251 cancelPick: method(function() {
michael@0 252 if (this._isPicking) {
michael@0 253 this._boxModelHighlighter.hide();
michael@0 254 this._stopPickerListeners();
michael@0 255 this._isPicking = false;
michael@0 256 this._hoveredNode = null;
michael@0 257 }
michael@0 258 })
michael@0 259 });
michael@0 260
michael@0 261 exports.HighlighterActor = HighlighterActor;
michael@0 262
michael@0 263 /**
michael@0 264 * The HighlighterFront class
michael@0 265 */
michael@0 266 let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
michael@0 267
michael@0 268 /**
michael@0 269 * The BoxModelHighlighter is the class that actually draws the the box model
michael@0 270 * regions on top of a node.
michael@0 271 * It is used by the HighlighterActor.
michael@0 272 *
michael@0 273 * Usage example:
michael@0 274 *
michael@0 275 * let h = new BoxModelHighlighter(browser);
michael@0 276 * h.show(node);
michael@0 277 * h.hide();
michael@0 278 * h.destroy();
michael@0 279 *
michael@0 280 * Structure:
michael@0 281 * <stack class="highlighter-container">
michael@0 282 * <svg class="box-model-root" hidden="true">
michael@0 283 * <g class="box-model-container">
michael@0 284 * <polygon class="box-model-margin" points="317,122 747,36 747,181 317,267" />
michael@0 285 * <polygon class="box-model-border" points="317,128 747,42 747,161 317,247" />
michael@0 286 * <polygon class="box-model-padding" points="323,127 747,42 747,161 323,246" />
michael@0 287 * <polygon class="box-model-content" points="335,137 735,57 735,152 335,232" />
michael@0 288 * </g>
michael@0 289 * <line class="box-model-guide-top" x1="0" y1="592" x2="99999" y2="592" />
michael@0 290 * <line class="box-model-guide-right" x1="735" y1="0" x2="735" y2="99999" />
michael@0 291 * <line class="box-model-guide-bottom" x1="0" y1="612" x2="99999" y2="612" />
michael@0 292 * <line class="box-model-guide-left" x1="334" y1="0" x2="334" y2="99999" />
michael@0 293 * </svg>
michael@0 294 * <box class="highlighter-nodeinfobar-container">
michael@0 295 * <box class="highlighter-nodeinfobar-positioner" position="top" />
michael@0 296 * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top" />
michael@0 297 * <hbox class="highlighter-nodeinfobar">
michael@0 298 * <hbox class="highlighter-nodeinfobar-text" align="center" flex="1">
michael@0 299 * <span class="highlighter-nodeinfobar-tagname">Node name</span>
michael@0 300 * <span class="highlighter-nodeinfobar-id">Node id</span>
michael@0 301 * <span class="highlighter-nodeinfobar-classes">.someClass</span>
michael@0 302 * <span class="highlighter-nodeinfobar-pseudo-classes">:hover</span>
michael@0 303 * </hbox>
michael@0 304 * </hbox>
michael@0 305 * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
michael@0 306 * </box>
michael@0 307 * </box>
michael@0 308 * </stack>
michael@0 309 */
michael@0 310 function BoxModelHighlighter(tabActor, inspector) {
michael@0 311 this.browser = tabActor.browser;
michael@0 312 this.win = tabActor.window;
michael@0 313 this.chromeDoc = this.browser.ownerDocument;
michael@0 314 this.chromeWin = this.chromeDoc.defaultView;
michael@0 315 this._inspector = inspector;
michael@0 316
michael@0 317 this.layoutHelpers = new LayoutHelpers(this.win);
michael@0 318 this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
michael@0 319
michael@0 320 this.transitionDisabler = null;
michael@0 321 this.pageEventsMuter = null;
michael@0 322 this._update = this._update.bind(this);
michael@0 323 this.handleEvent = this.handleEvent.bind(this);
michael@0 324 this.currentNode = null;
michael@0 325
michael@0 326 EventEmitter.decorate(this);
michael@0 327 this._initMarkup();
michael@0 328 }
michael@0 329
michael@0 330 BoxModelHighlighter.prototype = {
michael@0 331 get zoom() {
michael@0 332 return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 333 .getInterface(Ci.nsIDOMWindowUtils).fullZoom;
michael@0 334 },
michael@0 335
michael@0 336 _initMarkup: function() {
michael@0 337 let stack = this.browser.parentNode;
michael@0 338
michael@0 339 this._highlighterContainer = this.chromeDoc.createElement("stack");
michael@0 340 this._highlighterContainer.className = "highlighter-container";
michael@0 341
michael@0 342 this._svgRoot = this._createSVGNode("root", "svg", this._highlighterContainer);
michael@0 343
michael@0 344 // Set the SVG canvas height to 0 to stop content jumping around on small
michael@0 345 // screens.
michael@0 346 this._svgRoot.setAttribute("height", "0");
michael@0 347
michael@0 348 this._boxModelContainer = this._createSVGNode("container", "g", this._svgRoot);
michael@0 349
michael@0 350 this._boxModelNodes = {
michael@0 351 margin: this._createSVGNode("margin", "polygon", this._boxModelContainer),
michael@0 352 border: this._createSVGNode("border", "polygon", this._boxModelContainer),
michael@0 353 padding: this._createSVGNode("padding", "polygon", this._boxModelContainer),
michael@0 354 content: this._createSVGNode("content", "polygon", this._boxModelContainer)
michael@0 355 };
michael@0 356
michael@0 357 this._guideNodes = {
michael@0 358 top: this._createSVGNode("guide-top", "line", this._svgRoot),
michael@0 359 right: this._createSVGNode("guide-right", "line", this._svgRoot),
michael@0 360 bottom: this._createSVGNode("guide-bottom", "line", this._svgRoot),
michael@0 361 left: this._createSVGNode("guide-left", "line", this._svgRoot)
michael@0 362 };
michael@0 363
michael@0 364 this._guideNodes.top.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
michael@0 365 this._guideNodes.right.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
michael@0 366 this._guideNodes.bottom.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
michael@0 367 this._guideNodes.left.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
michael@0 368
michael@0 369 this._highlighterContainer.appendChild(this._svgRoot);
michael@0 370
michael@0 371 let infobarContainer = this.chromeDoc.createElement("box");
michael@0 372 infobarContainer.className = "highlighter-nodeinfobar-container";
michael@0 373 this._highlighterContainer.appendChild(infobarContainer);
michael@0 374
michael@0 375 // Insert the highlighter right after the browser
michael@0 376 stack.insertBefore(this._highlighterContainer, stack.childNodes[1]);
michael@0 377
michael@0 378 // Building the infobar
michael@0 379 let infobarPositioner = this.chromeDoc.createElement("box");
michael@0 380 infobarPositioner.className = "highlighter-nodeinfobar-positioner";
michael@0 381 infobarPositioner.setAttribute("position", "top");
michael@0 382 infobarPositioner.setAttribute("disabled", "true");
michael@0 383
michael@0 384 let nodeInfobar = this.chromeDoc.createElement("hbox");
michael@0 385 nodeInfobar.className = "highlighter-nodeinfobar";
michael@0 386
michael@0 387 let arrowBoxTop = this.chromeDoc.createElement("box");
michael@0 388 arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top";
michael@0 389
michael@0 390 let arrowBoxBottom = this.chromeDoc.createElement("box");
michael@0 391 arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom";
michael@0 392
michael@0 393 let tagNameLabel = this.chromeDoc.createElementNS(XHTML_NS, "span");
michael@0 394 tagNameLabel.className = "highlighter-nodeinfobar-tagname";
michael@0 395
michael@0 396 let idLabel = this.chromeDoc.createElementNS(XHTML_NS, "span");
michael@0 397 idLabel.className = "highlighter-nodeinfobar-id";
michael@0 398
michael@0 399 let classesBox = this.chromeDoc.createElementNS(XHTML_NS, "span");
michael@0 400 classesBox.className = "highlighter-nodeinfobar-classes";
michael@0 401
michael@0 402 let pseudoClassesBox = this.chromeDoc.createElementNS(XHTML_NS, "span");
michael@0 403 pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes";
michael@0 404
michael@0 405 // Add some content to force a better boundingClientRect
michael@0 406 pseudoClassesBox.textContent = "&nbsp;";
michael@0 407
michael@0 408 // <hbox class="highlighter-nodeinfobar-text"/>
michael@0 409 let texthbox = this.chromeDoc.createElement("hbox");
michael@0 410 texthbox.className = "highlighter-nodeinfobar-text";
michael@0 411 texthbox.setAttribute("align", "center");
michael@0 412 texthbox.setAttribute("flex", "1");
michael@0 413
michael@0 414 texthbox.appendChild(tagNameLabel);
michael@0 415 texthbox.appendChild(idLabel);
michael@0 416 texthbox.appendChild(classesBox);
michael@0 417 texthbox.appendChild(pseudoClassesBox);
michael@0 418
michael@0 419 nodeInfobar.appendChild(texthbox);
michael@0 420
michael@0 421 infobarPositioner.appendChild(arrowBoxTop);
michael@0 422 infobarPositioner.appendChild(nodeInfobar);
michael@0 423 infobarPositioner.appendChild(arrowBoxBottom);
michael@0 424
michael@0 425 infobarContainer.appendChild(infobarPositioner);
michael@0 426
michael@0 427 let barHeight = infobarPositioner.getBoundingClientRect().height;
michael@0 428
michael@0 429 this.nodeInfo = {
michael@0 430 tagNameLabel: tagNameLabel,
michael@0 431 idLabel: idLabel,
michael@0 432 classesBox: classesBox,
michael@0 433 pseudoClassesBox: pseudoClassesBox,
michael@0 434 positioner: infobarPositioner,
michael@0 435 barHeight: barHeight,
michael@0 436 };
michael@0 437 },
michael@0 438
michael@0 439 _createSVGNode: function(classPostfix, nodeType, parent) {
michael@0 440 let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
michael@0 441 node.setAttribute("class", "box-model-" + classPostfix);
michael@0 442
michael@0 443 parent.appendChild(node);
michael@0 444
michael@0 445 return node;
michael@0 446 },
michael@0 447
michael@0 448 /**
michael@0 449 * Destroy the nodes. Remove listeners.
michael@0 450 */
michael@0 451 destroy: function() {
michael@0 452 this.hide();
michael@0 453
michael@0 454 this.chromeWin.clearTimeout(this.transitionDisabler);
michael@0 455 this.chromeWin.clearTimeout(this.pageEventsMuter);
michael@0 456
michael@0 457 this.nodeInfo = null;
michael@0 458
michael@0 459 this._highlighterContainer.remove();
michael@0 460 this._highlighterContainer = null;
michael@0 461
michael@0 462 this.rect = null;
michael@0 463 this.win = null;
michael@0 464 this.browser = null;
michael@0 465 this.chromeDoc = null;
michael@0 466 this.chromeWin = null;
michael@0 467 this.currentNode = null;
michael@0 468 },
michael@0 469
michael@0 470 /**
michael@0 471 * Show the highlighter on a given node
michael@0 472 *
michael@0 473 * @param {DOMNode} node
michael@0 474 * @param {Object} options
michael@0 475 * Object used for passing options
michael@0 476 */
michael@0 477 show: function(node, options={}) {
michael@0 478 this.currentNode = node;
michael@0 479
michael@0 480 this._showInfobar();
michael@0 481 this._detachPageListeners();
michael@0 482 this._attachPageListeners();
michael@0 483 this._update();
michael@0 484 this._trackMutations();
michael@0 485 },
michael@0 486
michael@0 487 _trackMutations: function() {
michael@0 488 if (this.currentNode) {
michael@0 489 let win = this.currentNode.ownerDocument.defaultView;
michael@0 490 this.currentNodeObserver = new win.MutationObserver(() => {
michael@0 491 this._update();
michael@0 492 });
michael@0 493 this.currentNodeObserver.observe(this.currentNode, {attributes: true});
michael@0 494 }
michael@0 495 },
michael@0 496
michael@0 497 _untrackMutations: function() {
michael@0 498 if (this.currentNode) {
michael@0 499 if (this.currentNodeObserver) {
michael@0 500 // The following may fail with a "can't access dead object" exception
michael@0 501 // when the actor is being destroyed
michael@0 502 try {
michael@0 503 this.currentNodeObserver.disconnect();
michael@0 504 } catch (e) {}
michael@0 505 this.currentNodeObserver = null;
michael@0 506 }
michael@0 507 }
michael@0 508 },
michael@0 509
michael@0 510 /**
michael@0 511 * Update the highlighter on the current highlighted node (the one that was
michael@0 512 * passed as an argument to show(node)).
michael@0 513 * Should be called whenever node size or attributes change
michael@0 514 * @param {Object} options
michael@0 515 * Object used for passing options. Valid options are:
michael@0 516 * - box: "content", "padding", "border" or "margin." This specifies
michael@0 517 * the box that the guides should outline. Default is content.
michael@0 518 */
michael@0 519 _update: function(options={}) {
michael@0 520 if (this.currentNode) {
michael@0 521 if (this._highlightBoxModel(options)) {
michael@0 522 this._showInfobar();
michael@0 523 } else {
michael@0 524 // Nothing to highlight (0px rectangle like a <script> tag for instance)
michael@0 525 this.hide();
michael@0 526 }
michael@0 527 this.emit("ready");
michael@0 528 }
michael@0 529 },
michael@0 530
michael@0 531 /**
michael@0 532 * Hide the highlighter, the outline and the infobar.
michael@0 533 */
michael@0 534 hide: function() {
michael@0 535 if (this.currentNode) {
michael@0 536 this._untrackMutations();
michael@0 537 this.currentNode = null;
michael@0 538 this._hideBoxModel();
michael@0 539 this._hideInfobar();
michael@0 540 this._detachPageListeners();
michael@0 541 }
michael@0 542 this.emit("hide");
michael@0 543 },
michael@0 544
michael@0 545 /**
michael@0 546 * Hide the infobar
michael@0 547 */
michael@0 548 _hideInfobar: function() {
michael@0 549 this.nodeInfo.positioner.setAttribute("hidden", "true");
michael@0 550 },
michael@0 551
michael@0 552 /**
michael@0 553 * Show the infobar
michael@0 554 */
michael@0 555 _showInfobar: function() {
michael@0 556 this.nodeInfo.positioner.removeAttribute("hidden");
michael@0 557 this._updateInfobar();
michael@0 558 },
michael@0 559
michael@0 560 /**
michael@0 561 * Hide the box model
michael@0 562 */
michael@0 563 _hideBoxModel: function() {
michael@0 564 this._svgRoot.setAttribute("hidden", "true");
michael@0 565 },
michael@0 566
michael@0 567 /**
michael@0 568 * Show the box model
michael@0 569 */
michael@0 570 _showBoxModel: function() {
michael@0 571 this._svgRoot.removeAttribute("hidden");
michael@0 572 },
michael@0 573
michael@0 574 /**
michael@0 575 * Highlight the box model.
michael@0 576 *
michael@0 577 * @param {Object} options
michael@0 578 * Object used for passing options. Valid options are:
michael@0 579 * - region: "content", "padding", "border" or "margin." This specifies
michael@0 580 * the region that the guides should outline. Default is content.
michael@0 581 * @return {boolean}
michael@0 582 * True if the rectangle was highlighted, false otherwise.
michael@0 583 */
michael@0 584 _highlightBoxModel: function(options) {
michael@0 585 let isShown = false;
michael@0 586
michael@0 587 options.region = options.region || "content";
michael@0 588
michael@0 589 // TODO: Remove this polyfill
michael@0 590 this.rect =
michael@0 591 this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, "margin");
michael@0 592
michael@0 593 if (!this.rect) {
michael@0 594 return null;
michael@0 595 }
michael@0 596
michael@0 597 if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) {
michael@0 598 for (let boxType in this._boxModelNodes) {
michael@0 599 // TODO: Remove this polyfill
michael@0 600 let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
michael@0 601 this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, boxType);
michael@0 602
michael@0 603 let boxNode = this._boxModelNodes[boxType];
michael@0 604 boxNode.setAttribute("points",
michael@0 605 p1.x + "," + p1.y + " " +
michael@0 606 p2.x + "," + p2.y + " " +
michael@0 607 p3.x + "," + p3.y + " " +
michael@0 608 p4.x + "," + p4.y);
michael@0 609
michael@0 610 if (boxType === options.region) {
michael@0 611 this._showGuides(p1, p2, p3, p4);
michael@0 612 }
michael@0 613 }
michael@0 614
michael@0 615 isShown = true;
michael@0 616 this._showBoxModel();
michael@0 617 } else {
michael@0 618 // Only return false if the element really is invisible.
michael@0 619 // A height of 0 and a non-0 width corresponds to a visible element that
michael@0 620 // is below the fold for instance
michael@0 621 if (this.rect.width > 0 || this.rect.height > 0) {
michael@0 622 isShown = true;
michael@0 623 this._hideBoxModel();
michael@0 624 }
michael@0 625 }
michael@0 626 return isShown;
michael@0 627 },
michael@0 628
michael@0 629 /**
michael@0 630 * We only want to show guides for horizontal and vertical edges as this helps
michael@0 631 * to line them up. This method finds these edges and displays a guide there.
michael@0 632 *
michael@0 633 * @param {DOMPoint} p1
michael@0 634 * Point 1
michael@0 635 * @param {DOMPoint} p2
michael@0 636 * Point 2
michael@0 637 * @param {DOMPoint} p3 [description]
michael@0 638 * Point 3
michael@0 639 * @param {DOMPoint} p4 [description]
michael@0 640 * Point 4
michael@0 641 */
michael@0 642 _showGuides: function(p1, p2, p3, p4) {
michael@0 643 let allX = [p1.x, p2.x, p3.x, p4.x].sort();
michael@0 644 let allY = [p1.y, p2.y, p3.y, p4.y].sort();
michael@0 645 let toShowX = [];
michael@0 646 let toShowY = [];
michael@0 647
michael@0 648 for (let arr of [allX, allY]) {
michael@0 649 for (let i = 0; i < arr.length; i++) {
michael@0 650 let val = arr[i];
michael@0 651
michael@0 652 if (i !== arr.lastIndexOf(val)) {
michael@0 653 if (arr === allX) {
michael@0 654 toShowX.push(val);
michael@0 655 } else {
michael@0 656 toShowY.push(val);
michael@0 657 }
michael@0 658 arr.splice(arr.lastIndexOf(val), 1);
michael@0 659 }
michael@0 660 }
michael@0 661 }
michael@0 662
michael@0 663 // Move guide into place or hide it if no valid co-ordinate was found.
michael@0 664 this._updateGuide(this._guideNodes.top, toShowY[0]);
michael@0 665 this._updateGuide(this._guideNodes.right, toShowX[1]);
michael@0 666 this._updateGuide(this._guideNodes.bottom, toShowY[1]);
michael@0 667 this._updateGuide(this._guideNodes.left, toShowX[0]);
michael@0 668 },
michael@0 669
michael@0 670 /**
michael@0 671 * Move a guide to the appropriate position and display it. If no point is
michael@0 672 * passed then the guide is hidden.
michael@0 673 *
michael@0 674 * @param {SVGLine} guide
michael@0 675 * The guide to update
michael@0 676 * @param {Integer} point
michael@0 677 * x or y co-ordinate. If this is undefined we hide the guide.
michael@0 678 */
michael@0 679 _updateGuide: function(guide, point=-1) {
michael@0 680 if (point > 0) {
michael@0 681 let offset = GUIDE_STROKE_WIDTH / 2;
michael@0 682
michael@0 683 if (guide === this._guideNodes.top || guide === this._guideNodes.left) {
michael@0 684 point -= offset;
michael@0 685 } else {
michael@0 686 point += offset;
michael@0 687 }
michael@0 688
michael@0 689 if (guide === this._guideNodes.top || guide === this._guideNodes.bottom) {
michael@0 690 guide.setAttribute("x1", 0);
michael@0 691 guide.setAttribute("y1", point);
michael@0 692 guide.setAttribute("x2", "100%");
michael@0 693 guide.setAttribute("y2", point);
michael@0 694 } else {
michael@0 695 guide.setAttribute("x1", point);
michael@0 696 guide.setAttribute("y1", 0);
michael@0 697 guide.setAttribute("x2", point);
michael@0 698 guide.setAttribute("y2", "100%");
michael@0 699 }
michael@0 700 guide.removeAttribute("hidden");
michael@0 701 return true;
michael@0 702 } else {
michael@0 703 guide.setAttribute("hidden", "true");
michael@0 704 return false;
michael@0 705 }
michael@0 706 },
michael@0 707
michael@0 708 /**
michael@0 709 * Update node information (tagName#id.class)
michael@0 710 */
michael@0 711 _updateInfobar: function() {
michael@0 712 if (this.currentNode) {
michael@0 713 // Tag name
michael@0 714 this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName;
michael@0 715
michael@0 716 // ID
michael@0 717 this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : "";
michael@0 718
michael@0 719 // Classes
michael@0 720 let classes = this.nodeInfo.classesBox;
michael@0 721
michael@0 722 classes.textContent = this.currentNode.classList.length ?
michael@0 723 "." + Array.join(this.currentNode.classList, ".") : "";
michael@0 724
michael@0 725 // Pseudo-classes
michael@0 726 let pseudos = PSEUDO_CLASSES.filter(pseudo => {
michael@0 727 return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo);
michael@0 728 }, this);
michael@0 729
michael@0 730 let pseudoBox = this.nodeInfo.pseudoClassesBox;
michael@0 731 pseudoBox.textContent = pseudos.join("");
michael@0 732
michael@0 733 this._moveInfobar();
michael@0 734 }
michael@0 735 },
michael@0 736
michael@0 737 /**
michael@0 738 * Move the Infobar to the right place in the highlighter.
michael@0 739 */
michael@0 740 _moveInfobar: function() {
michael@0 741 if (this.rect) {
michael@0 742 let bounds = this.rect.bounds;
michael@0 743 let winHeight = this.win.innerHeight * this.zoom;
michael@0 744 let winWidth = this.win.innerWidth * this.zoom;
michael@0 745
michael@0 746 this.nodeInfo.positioner.removeAttribute("disabled");
michael@0 747 // Can the bar be above the node?
michael@0 748 if (bounds.top < this.nodeInfo.barHeight) {
michael@0 749 // No. Can we move the toolbar under the node?
michael@0 750 if (bounds.bottom + this.nodeInfo.barHeight > winHeight) {
michael@0 751 // No. Let's move it inside.
michael@0 752 this.nodeInfo.positioner.style.top = bounds.top + "px";
michael@0 753 this.nodeInfo.positioner.setAttribute("position", "overlap");
michael@0 754 } else {
michael@0 755 // Yes. Let's move it under the node.
michael@0 756 this.nodeInfo.positioner.style.top = bounds.bottom - INFO_BAR_OFFSET + "px";
michael@0 757 this.nodeInfo.positioner.setAttribute("position", "bottom");
michael@0 758 }
michael@0 759 } else {
michael@0 760 // Yes. Let's move it on top of the node.
michael@0 761 this.nodeInfo.positioner.style.top =
michael@0 762 bounds.top + INFO_BAR_OFFSET - this.nodeInfo.barHeight + "px";
michael@0 763 this.nodeInfo.positioner.setAttribute("position", "top");
michael@0 764 }
michael@0 765
michael@0 766 let barWidth = this.nodeInfo.positioner.getBoundingClientRect().width;
michael@0 767 let left = bounds.right - bounds.width / 2 - barWidth / 2;
michael@0 768
michael@0 769 // Make sure the whole infobar is visible
michael@0 770 if (left < 0) {
michael@0 771 left = 0;
michael@0 772 this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
michael@0 773 } else {
michael@0 774 if (left + barWidth > winWidth) {
michael@0 775 left = winWidth - barWidth;
michael@0 776 this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
michael@0 777 } else {
michael@0 778 this.nodeInfo.positioner.removeAttribute("hide-arrow");
michael@0 779 }
michael@0 780 }
michael@0 781 this.nodeInfo.positioner.style.left = left + "px";
michael@0 782 } else {
michael@0 783 this.nodeInfo.positioner.style.left = "0";
michael@0 784 this.nodeInfo.positioner.style.top = "0";
michael@0 785 this.nodeInfo.positioner.setAttribute("position", "top");
michael@0 786 this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
michael@0 787 }
michael@0 788 },
michael@0 789
michael@0 790 _attachPageListeners: function() {
michael@0 791 if (this.currentNode) {
michael@0 792 let win = this.currentNode.ownerGlobal;
michael@0 793
michael@0 794 win.addEventListener("scroll", this, false);
michael@0 795 win.addEventListener("resize", this, false);
michael@0 796 win.addEventListener("MozAfterPaint", this, false);
michael@0 797 }
michael@0 798 },
michael@0 799
michael@0 800 _detachPageListeners: function() {
michael@0 801 if (this.currentNode) {
michael@0 802 let win = this.currentNode.ownerGlobal;
michael@0 803
michael@0 804 win.removeEventListener("scroll", this, false);
michael@0 805 win.removeEventListener("resize", this, false);
michael@0 806 win.removeEventListener("MozAfterPaint", this, false);
michael@0 807 }
michael@0 808 },
michael@0 809
michael@0 810 /**
michael@0 811 * Generic event handler.
michael@0 812 *
michael@0 813 * @param nsIDOMEvent aEvent
michael@0 814 * The DOM event object.
michael@0 815 */
michael@0 816 handleEvent: function(event) {
michael@0 817 switch (event.type) {
michael@0 818 case "resize":
michael@0 819 case "MozAfterPaint":
michael@0 820 case "scroll":
michael@0 821 this._update();
michael@0 822 break;
michael@0 823 }
michael@0 824 },
michael@0 825 };
michael@0 826
michael@0 827 /**
michael@0 828 * The SimpleOutlineHighlighter is a class that has the same API than the
michael@0 829 * BoxModelHighlighter, but adds a pseudo-class on the target element itself
michael@0 830 * to draw a simple outline.
michael@0 831 * It is used by the HighlighterActor too, but in case the more complex
michael@0 832 * BoxModelHighlighter can't be attached (which is the case for FirefoxOS and
michael@0 833 * Fennec targets for instance).
michael@0 834 */
michael@0 835 function SimpleOutlineHighlighter(tabActor) {
michael@0 836 this.chromeDoc = tabActor.window.document;
michael@0 837 }
michael@0 838
michael@0 839 SimpleOutlineHighlighter.prototype = {
michael@0 840 /**
michael@0 841 * Destroy the nodes. Remove listeners.
michael@0 842 */
michael@0 843 destroy: function() {
michael@0 844 this.hide();
michael@0 845 if (this.installedHelpers) {
michael@0 846 this.installedHelpers.clear();
michael@0 847 }
michael@0 848 this.chromeDoc = null;
michael@0 849 },
michael@0 850
michael@0 851 _installHelperSheet: function(node) {
michael@0 852 if (!this.installedHelpers) {
michael@0 853 this.installedHelpers = new WeakMap;
michael@0 854 }
michael@0 855 let win = node.ownerDocument.defaultView;
michael@0 856 if (!this.installedHelpers.has(win)) {
michael@0 857 let {Style} = require("sdk/stylesheet/style");
michael@0 858 let {attach} = require("sdk/content/mod");
michael@0 859 let style = Style({source: HELPER_SHEET, type: "agent"});
michael@0 860 attach(style, win);
michael@0 861 this.installedHelpers.set(win, style);
michael@0 862 }
michael@0 863 },
michael@0 864
michael@0 865 /**
michael@0 866 * Show the highlighter on a given node
michael@0 867 * @param {DOMNode} node
michael@0 868 */
michael@0 869 show: function(node) {
michael@0 870 if (!this.currentNode || node !== this.currentNode) {
michael@0 871 this.hide();
michael@0 872 this.currentNode = node;
michael@0 873 this._installHelperSheet(node);
michael@0 874 DOMUtils.addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
michael@0 875 }
michael@0 876 },
michael@0 877
michael@0 878 /**
michael@0 879 * Hide the highlighter, the outline and the infobar.
michael@0 880 */
michael@0 881 hide: function() {
michael@0 882 if (this.currentNode) {
michael@0 883 DOMUtils.removePseudoClassLock(this.currentNode, HIGHLIGHTED_PSEUDO_CLASS);
michael@0 884 this.currentNode = null;
michael@0 885 }
michael@0 886 }
michael@0 887 };
michael@0 888
michael@0 889 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
michael@0 890 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
michael@0 891 });

mercurial