toolkit/devtools/server/actors/highlighter.js

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

mercurial