Sat, 03 Jan 2015 20:18:00 +0100
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 = " "; |
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 | }); |