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.

     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/. */
     5 "use strict";
     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");
    13 const EventEmitter = require("devtools/toolkit/event-emitter");
    14 const GUIDE_STROKE_WIDTH = 1;
    16 // Make sure the domnode type is known here
    17 require("devtools/server/actors/inspector");
    19 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
    20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    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;
    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  */
    39 /**
    40  * The HighlighterActor class
    41  */
    42 let HighlighterActor = protocol.ActorClass({
    43   typeName: "highlighter",
    45   initialize: function(inspector, autohide) {
    46     protocol.Actor.prototype.initialize.call(this, null);
    48     this._autohide = autohide;
    49     this._inspector = inspector;
    50     this._walker = this._inspector.walker;
    51     this._tabActor = this._inspector.tabActor;
    53     this._highlighterReady = this._highlighterReady.bind(this);
    54     this._highlighterHidden = this._highlighterHidden.bind(this);
    56     if (this._supportsBoxModelHighlighter()) {
    57       this._boxModelHighlighter =
    58         new BoxModelHighlighter(this._tabActor, this._inspector);
    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   },
    67   get conn() this._inspector && this._inspector.conn,
    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   },
    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   },
    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   }),
   118   _isNodeValidForHighlighting: function(node) {
   119     // Is it null or dead?
   120     let isNotDead = node && !Cu.isDeadWrapper(node);
   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     }
   131     // Is it an element node
   132     let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE;
   134     return isNotDead && isConnected && isElementNode;
   135   },
   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   }),
   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,
   159   pick: method(function() {
   160     if (this._isPicking) {
   161       return null;
   162     }
   163     this._isPicking = true;
   165     this._preventContentEvent = event => {
   166       event.stopPropagation();
   167       event.preventDefault();
   168     };
   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     };
   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     };
   192     this._tabActor.window.focus();
   193     this._startPickerListeners();
   195     return null;
   196   }),
   198   _findAndAttachElement: function(event) {
   199     let doc = event.target.ownerDocument;
   201     let x = event.clientX;
   202     let y = event.clientY;
   204     let node = doc.elementFromPoint(x, y);
   205     return this._walker.attachElement(node);
   206   },
   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   },
   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   },
   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   },
   243   _highlighterReady: function() {
   244     events.emit(this._inspector.walker, "highlighter-ready");
   245   },
   247   _highlighterHidden: function() {
   248     events.emit(this._inspector.walker, "highlighter-hide");
   249   },
   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 });
   261 exports.HighlighterActor = HighlighterActor;
   263 /**
   264  * The HighlighterFront class
   265  */
   266 let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
   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;
   317   this.layoutHelpers = new LayoutHelpers(this.win);
   318   this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
   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;
   326   EventEmitter.decorate(this);
   327   this._initMarkup();
   328 }
   330 BoxModelHighlighter.prototype = {
   331   get zoom() {
   332     return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
   333                .getInterface(Ci.nsIDOMWindowUtils).fullZoom;
   334   },
   336   _initMarkup: function() {
   337     let stack = this.browser.parentNode;
   339     this._highlighterContainer = this.chromeDoc.createElement("stack");
   340     this._highlighterContainer.className = "highlighter-container";
   342     this._svgRoot = this._createSVGNode("root", "svg", this._highlighterContainer);
   344     // Set the SVG canvas height to 0 to stop content jumping around on small
   345     // screens.
   346     this._svgRoot.setAttribute("height", "0");
   348     this._boxModelContainer = this._createSVGNode("container", "g", this._svgRoot);
   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     };
   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     };
   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);
   369     this._highlighterContainer.appendChild(this._svgRoot);
   371     let infobarContainer = this.chromeDoc.createElement("box");
   372     infobarContainer.className = "highlighter-nodeinfobar-container";
   373     this._highlighterContainer.appendChild(infobarContainer);
   375     // Insert the highlighter right after the browser
   376     stack.insertBefore(this._highlighterContainer, stack.childNodes[1]);
   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");
   384     let nodeInfobar = this.chromeDoc.createElement("hbox");
   385     nodeInfobar.className = "highlighter-nodeinfobar";
   387     let arrowBoxTop = this.chromeDoc.createElement("box");
   388     arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top";
   390     let arrowBoxBottom = this.chromeDoc.createElement("box");
   391     arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom";
   393     let tagNameLabel = this.chromeDoc.createElementNS(XHTML_NS, "span");
   394     tagNameLabel.className = "highlighter-nodeinfobar-tagname";
   396     let idLabel = this.chromeDoc.createElementNS(XHTML_NS, "span");
   397     idLabel.className = "highlighter-nodeinfobar-id";
   399     let classesBox = this.chromeDoc.createElementNS(XHTML_NS, "span");
   400     classesBox.className = "highlighter-nodeinfobar-classes";
   402     let pseudoClassesBox = this.chromeDoc.createElementNS(XHTML_NS, "span");
   403     pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes";
   405     // Add some content to force a better boundingClientRect
   406     pseudoClassesBox.textContent = "&nbsp;";
   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");
   414     texthbox.appendChild(tagNameLabel);
   415     texthbox.appendChild(idLabel);
   416     texthbox.appendChild(classesBox);
   417     texthbox.appendChild(pseudoClassesBox);
   419     nodeInfobar.appendChild(texthbox);
   421     infobarPositioner.appendChild(arrowBoxTop);
   422     infobarPositioner.appendChild(nodeInfobar);
   423     infobarPositioner.appendChild(arrowBoxBottom);
   425     infobarContainer.appendChild(infobarPositioner);
   427     let barHeight = infobarPositioner.getBoundingClientRect().height;
   429     this.nodeInfo = {
   430       tagNameLabel: tagNameLabel,
   431       idLabel: idLabel,
   432       classesBox: classesBox,
   433       pseudoClassesBox: pseudoClassesBox,
   434       positioner: infobarPositioner,
   435       barHeight: barHeight,
   436     };
   437   },
   439   _createSVGNode: function(classPostfix, nodeType, parent) {
   440     let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
   441     node.setAttribute("class", "box-model-" + classPostfix);
   443     parent.appendChild(node);
   445     return node;
   446   },
   448   /**
   449    * Destroy the nodes. Remove listeners.
   450    */
   451   destroy: function() {
   452     this.hide();
   454     this.chromeWin.clearTimeout(this.transitionDisabler);
   455     this.chromeWin.clearTimeout(this.pageEventsMuter);
   457     this.nodeInfo = null;
   459     this._highlighterContainer.remove();
   460     this._highlighterContainer = null;
   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   },
   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;
   480     this._showInfobar();
   481     this._detachPageListeners();
   482     this._attachPageListeners();
   483     this._update();
   484     this._trackMutations();
   485   },
   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   },
   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   },
   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   },
   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   },
   545   /**
   546    * Hide the infobar
   547    */
   548   _hideInfobar: function() {
   549     this.nodeInfo.positioner.setAttribute("hidden", "true");
   550   },
   552   /**
   553    * Show the infobar
   554    */
   555   _showInfobar: function() {
   556     this.nodeInfo.positioner.removeAttribute("hidden");
   557     this._updateInfobar();
   558   },
   560   /**
   561    * Hide the box model
   562    */
   563   _hideBoxModel: function() {
   564     this._svgRoot.setAttribute("hidden", "true");
   565   },
   567   /**
   568    * Show the box model
   569    */
   570   _showBoxModel: function() {
   571     this._svgRoot.removeAttribute("hidden");
   572   },
   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;
   587     options.region = options.region || "content";
   589     // TODO: Remove this polyfill
   590     this.rect =
   591       this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, "margin");
   593     if (!this.rect) {
   594       return null;
   595     }
   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);
   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);
   610         if (boxType === options.region) {
   611           this._showGuides(p1, p2, p3, p4);
   612         }
   613       }
   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   },
   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 = [];
   648     for (let arr of [allX, allY]) {
   649       for (let i = 0; i < arr.length; i++) {
   650         let val = arr[i];
   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     }
   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   },
   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;
   683       if (guide === this._guideNodes.top || guide === this._guideNodes.left) {
   684         point -= offset;
   685       } else {
   686         point += offset;
   687       }
   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   },
   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;
   716       // ID
   717       this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : "";
   719       // Classes
   720       let classes = this.nodeInfo.classesBox;
   722       classes.textContent = this.currentNode.classList.length ?
   723                               "." + Array.join(this.currentNode.classList, ".") : "";
   725       // Pseudo-classes
   726       let pseudos = PSEUDO_CLASSES.filter(pseudo => {
   727         return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo);
   728       }, this);
   730       let pseudoBox = this.nodeInfo.pseudoClassesBox;
   731       pseudoBox.textContent = pseudos.join("");
   733       this._moveInfobar();
   734     }
   735   },
   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;
   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       }
   766       let barWidth = this.nodeInfo.positioner.getBoundingClientRect().width;
   767       let left = bounds.right - bounds.width / 2 - barWidth / 2;
   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   },
   790   _attachPageListeners: function() {
   791     if (this.currentNode) {
   792       let win = this.currentNode.ownerGlobal;
   794       win.addEventListener("scroll", this, false);
   795       win.addEventListener("resize", this, false);
   796       win.addEventListener("MozAfterPaint", this, false);
   797     }
   798   },
   800   _detachPageListeners: function() {
   801     if (this.currentNode) {
   802       let win = this.currentNode.ownerGlobal;
   804       win.removeEventListener("scroll", this, false);
   805       win.removeEventListener("resize", this, false);
   806       win.removeEventListener("MozAfterPaint", this, false);
   807     }
   808   },
   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 };
   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 }
   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   },
   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   },
   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   },
   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 };
   889 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   890   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
   891 });

mercurial