Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | const {Cc, Cu, Ci} = require("chrome"); |
michael@0 | 8 | |
michael@0 | 9 | // Page size for pageup/pagedown |
michael@0 | 10 | const PAGE_SIZE = 10; |
michael@0 | 11 | const PREVIEW_AREA = 700; |
michael@0 | 12 | const DEFAULT_MAX_CHILDREN = 100; |
michael@0 | 13 | const COLLAPSE_ATTRIBUTE_LENGTH = 120; |
michael@0 | 14 | const COLLAPSE_DATA_URL_REGEX = /^data.+base64/; |
michael@0 | 15 | const COLLAPSE_DATA_URL_LENGTH = 60; |
michael@0 | 16 | const CONTAINER_FLASHING_DURATION = 500; |
michael@0 | 17 | const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000; |
michael@0 | 18 | |
michael@0 | 19 | const {UndoStack} = require("devtools/shared/undo"); |
michael@0 | 20 | const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor"); |
michael@0 | 21 | const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); |
michael@0 | 22 | const {HTMLEditor} = require("devtools/markupview/html-editor"); |
michael@0 | 23 | const promise = require("devtools/toolkit/deprecated-sync-thenables"); |
michael@0 | 24 | const {Tooltip} = require("devtools/shared/widgets/Tooltip"); |
michael@0 | 25 | const EventEmitter = require("devtools/toolkit/event-emitter"); |
michael@0 | 26 | |
michael@0 | 27 | Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); |
michael@0 | 28 | Cu.import("resource://gre/modules/devtools/Templater.jsm"); |
michael@0 | 29 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 30 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 31 | |
michael@0 | 32 | loader.lazyGetter(this, "DOMParser", function() { |
michael@0 | 33 | return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); |
michael@0 | 34 | }); |
michael@0 | 35 | loader.lazyGetter(this, "AutocompletePopup", () => { |
michael@0 | 36 | return require("devtools/shared/autocomplete-popup").AutocompletePopup |
michael@0 | 37 | }); |
michael@0 | 38 | |
michael@0 | 39 | /** |
michael@0 | 40 | * Vocabulary for the purposes of this file: |
michael@0 | 41 | * |
michael@0 | 42 | * MarkupContainer - the structure that holds an editor and its |
michael@0 | 43 | * immediate children in the markup panel. |
michael@0 | 44 | * Node - A content node. |
michael@0 | 45 | * object.elt - A UI element in the markup panel. |
michael@0 | 46 | */ |
michael@0 | 47 | |
michael@0 | 48 | /** |
michael@0 | 49 | * The markup tree. Manages the mapping of nodes to MarkupContainers, |
michael@0 | 50 | * updating based on mutations, and the undo/redo bindings. |
michael@0 | 51 | * |
michael@0 | 52 | * @param Inspector aInspector |
michael@0 | 53 | * The inspector we're watching. |
michael@0 | 54 | * @param iframe aFrame |
michael@0 | 55 | * An iframe in which the caller has kindly loaded markup-view.xhtml. |
michael@0 | 56 | */ |
michael@0 | 57 | function MarkupView(aInspector, aFrame, aControllerWindow) { |
michael@0 | 58 | this._inspector = aInspector; |
michael@0 | 59 | this.walker = this._inspector.walker; |
michael@0 | 60 | this._frame = aFrame; |
michael@0 | 61 | this.doc = this._frame.contentDocument; |
michael@0 | 62 | this._elt = this.doc.querySelector("#root"); |
michael@0 | 63 | this.htmlEditor = new HTMLEditor(this.doc); |
michael@0 | 64 | |
michael@0 | 65 | this.layoutHelpers = new LayoutHelpers(this.doc.defaultView); |
michael@0 | 66 | |
michael@0 | 67 | try { |
michael@0 | 68 | this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize"); |
michael@0 | 69 | } catch(ex) { |
michael@0 | 70 | this.maxChildren = DEFAULT_MAX_CHILDREN; |
michael@0 | 71 | } |
michael@0 | 72 | |
michael@0 | 73 | // Creating the popup to be used to show CSS suggestions. |
michael@0 | 74 | let options = { |
michael@0 | 75 | autoSelect: true, |
michael@0 | 76 | theme: "auto" |
michael@0 | 77 | }; |
michael@0 | 78 | this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options); |
michael@0 | 79 | |
michael@0 | 80 | this.undo = new UndoStack(); |
michael@0 | 81 | this.undo.installController(aControllerWindow); |
michael@0 | 82 | |
michael@0 | 83 | this._containers = new Map(); |
michael@0 | 84 | |
michael@0 | 85 | this._boundMutationObserver = this._mutationObserver.bind(this); |
michael@0 | 86 | this.walker.on("mutations", this._boundMutationObserver); |
michael@0 | 87 | |
michael@0 | 88 | this._boundOnNewSelection = this._onNewSelection.bind(this); |
michael@0 | 89 | this._inspector.selection.on("new-node-front", this._boundOnNewSelection); |
michael@0 | 90 | this._onNewSelection(); |
michael@0 | 91 | |
michael@0 | 92 | this._boundKeyDown = this._onKeyDown.bind(this); |
michael@0 | 93 | this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false); |
michael@0 | 94 | |
michael@0 | 95 | this._boundFocus = this._onFocus.bind(this); |
michael@0 | 96 | this._frame.addEventListener("focus", this._boundFocus, false); |
michael@0 | 97 | |
michael@0 | 98 | this._initPreview(); |
michael@0 | 99 | this._initTooltips(); |
michael@0 | 100 | this._initHighlighter(); |
michael@0 | 101 | |
michael@0 | 102 | EventEmitter.decorate(this); |
michael@0 | 103 | } |
michael@0 | 104 | |
michael@0 | 105 | exports.MarkupView = MarkupView; |
michael@0 | 106 | |
michael@0 | 107 | MarkupView.prototype = { |
michael@0 | 108 | _selectedContainer: null, |
michael@0 | 109 | |
michael@0 | 110 | _initTooltips: function() { |
michael@0 | 111 | this.tooltip = new Tooltip(this._inspector.panelDoc); |
michael@0 | 112 | this.tooltip.startTogglingOnHover(this._elt, |
michael@0 | 113 | this._isImagePreviewTarget.bind(this)); |
michael@0 | 114 | }, |
michael@0 | 115 | |
michael@0 | 116 | _initHighlighter: function() { |
michael@0 | 117 | // Show the box model on markup-view mousemove |
michael@0 | 118 | this._onMouseMove = this._onMouseMove.bind(this); |
michael@0 | 119 | this._elt.addEventListener("mousemove", this._onMouseMove, false); |
michael@0 | 120 | this._onMouseLeave = this._onMouseLeave.bind(this); |
michael@0 | 121 | this._elt.addEventListener("mouseleave", this._onMouseLeave, false); |
michael@0 | 122 | |
michael@0 | 123 | // Show markup-containers as hovered on toolbox "picker-node-hovered" event |
michael@0 | 124 | // which happens when the "pick" button is pressed |
michael@0 | 125 | this._onToolboxPickerHover = (event, nodeFront) => { |
michael@0 | 126 | this.showNode(nodeFront, true).then(() => { |
michael@0 | 127 | this._showContainerAsHovered(nodeFront); |
michael@0 | 128 | }); |
michael@0 | 129 | } |
michael@0 | 130 | this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover); |
michael@0 | 131 | }, |
michael@0 | 132 | |
michael@0 | 133 | _onMouseMove: function(event) { |
michael@0 | 134 | let target = event.target; |
michael@0 | 135 | |
michael@0 | 136 | // Search target for a markupContainer reference, if not found, walk up |
michael@0 | 137 | while (!target.container) { |
michael@0 | 138 | if (target.tagName.toLowerCase() === "body") { |
michael@0 | 139 | return; |
michael@0 | 140 | } |
michael@0 | 141 | target = target.parentNode; |
michael@0 | 142 | } |
michael@0 | 143 | |
michael@0 | 144 | let container = target.container; |
michael@0 | 145 | if (this._hoveredNode !== container.node) { |
michael@0 | 146 | if (container.node.nodeType !== Ci.nsIDOMNode.TEXT_NODE) { |
michael@0 | 147 | this._showBoxModel(container.node); |
michael@0 | 148 | } else { |
michael@0 | 149 | this._hideBoxModel(); |
michael@0 | 150 | } |
michael@0 | 151 | } |
michael@0 | 152 | this._showContainerAsHovered(container.node); |
michael@0 | 153 | }, |
michael@0 | 154 | |
michael@0 | 155 | _hoveredNode: null, |
michael@0 | 156 | _showContainerAsHovered: function(nodeFront) { |
michael@0 | 157 | if (this._hoveredNode !== nodeFront) { |
michael@0 | 158 | if (this._hoveredNode) { |
michael@0 | 159 | this._containers.get(this._hoveredNode).hovered = false; |
michael@0 | 160 | } |
michael@0 | 161 | this._containers.get(nodeFront).hovered = true; |
michael@0 | 162 | |
michael@0 | 163 | this._hoveredNode = nodeFront; |
michael@0 | 164 | } |
michael@0 | 165 | }, |
michael@0 | 166 | |
michael@0 | 167 | _onMouseLeave: function() { |
michael@0 | 168 | this._hideBoxModel(true); |
michael@0 | 169 | if (this._hoveredNode) { |
michael@0 | 170 | this._containers.get(this._hoveredNode).hovered = false; |
michael@0 | 171 | } |
michael@0 | 172 | this._hoveredNode = null; |
michael@0 | 173 | }, |
michael@0 | 174 | |
michael@0 | 175 | _showBoxModel: function(nodeFront, options={}) { |
michael@0 | 176 | this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options); |
michael@0 | 177 | }, |
michael@0 | 178 | |
michael@0 | 179 | _hideBoxModel: function(forceHide) { |
michael@0 | 180 | return this._inspector.toolbox.highlighterUtils.unhighlight(forceHide); |
michael@0 | 181 | }, |
michael@0 | 182 | |
michael@0 | 183 | _briefBoxModelTimer: null, |
michael@0 | 184 | _brieflyShowBoxModel: function(nodeFront, options) { |
michael@0 | 185 | let win = this._frame.contentWindow; |
michael@0 | 186 | |
michael@0 | 187 | if (this._briefBoxModelTimer) { |
michael@0 | 188 | win.clearTimeout(this._briefBoxModelTimer); |
michael@0 | 189 | this._briefBoxModelTimer = null; |
michael@0 | 190 | } |
michael@0 | 191 | |
michael@0 | 192 | this._showBoxModel(nodeFront, options); |
michael@0 | 193 | |
michael@0 | 194 | this._briefBoxModelTimer = this._frame.contentWindow.setTimeout(() => { |
michael@0 | 195 | this._hideBoxModel(); |
michael@0 | 196 | }, NEW_SELECTION_HIGHLIGHTER_TIMER); |
michael@0 | 197 | }, |
michael@0 | 198 | |
michael@0 | 199 | template: function(aName, aDest, aOptions={stack: "markup-view.xhtml"}) { |
michael@0 | 200 | let node = this.doc.getElementById("template-" + aName).cloneNode(true); |
michael@0 | 201 | node.removeAttribute("id"); |
michael@0 | 202 | template(node, aDest, aOptions); |
michael@0 | 203 | return node; |
michael@0 | 204 | }, |
michael@0 | 205 | |
michael@0 | 206 | /** |
michael@0 | 207 | * Get the MarkupContainer object for a given node, or undefined if |
michael@0 | 208 | * none exists. |
michael@0 | 209 | */ |
michael@0 | 210 | getContainer: function(aNode) { |
michael@0 | 211 | return this._containers.get(aNode); |
michael@0 | 212 | }, |
michael@0 | 213 | |
michael@0 | 214 | update: function() { |
michael@0 | 215 | let updateChildren = function(node) { |
michael@0 | 216 | this.getContainer(node).update(); |
michael@0 | 217 | for (let child of node.treeChildren()) { |
michael@0 | 218 | updateChildren(child); |
michael@0 | 219 | } |
michael@0 | 220 | }.bind(this); |
michael@0 | 221 | |
michael@0 | 222 | // Start with the documentElement |
michael@0 | 223 | let documentElement; |
michael@0 | 224 | for (let node of this._rootNode.treeChildren()) { |
michael@0 | 225 | if (node.isDocumentElement === true) { |
michael@0 | 226 | documentElement = node; |
michael@0 | 227 | break; |
michael@0 | 228 | } |
michael@0 | 229 | } |
michael@0 | 230 | |
michael@0 | 231 | // Recursively update each node starting with documentElement. |
michael@0 | 232 | updateChildren(documentElement); |
michael@0 | 233 | }, |
michael@0 | 234 | |
michael@0 | 235 | /** |
michael@0 | 236 | * Executed when the mouse hovers over a target in the markup-view and is used |
michael@0 | 237 | * to decide whether this target should be used to display an image preview |
michael@0 | 238 | * tooltip. |
michael@0 | 239 | * Delegates the actual decision to the corresponding MarkupContainer instance |
michael@0 | 240 | * if one is found. |
michael@0 | 241 | * @return the promise returned by MarkupContainer._isImagePreviewTarget |
michael@0 | 242 | */ |
michael@0 | 243 | _isImagePreviewTarget: function(target) { |
michael@0 | 244 | // From the target passed here, let's find the parent MarkupContainer |
michael@0 | 245 | // and ask it if the tooltip should be shown |
michael@0 | 246 | let parent = target, container; |
michael@0 | 247 | while (parent !== this.doc.body) { |
michael@0 | 248 | if (parent.container) { |
michael@0 | 249 | container = parent.container; |
michael@0 | 250 | break; |
michael@0 | 251 | } |
michael@0 | 252 | parent = parent.parentNode; |
michael@0 | 253 | } |
michael@0 | 254 | |
michael@0 | 255 | if (container) { |
michael@0 | 256 | // With the newly found container, delegate the tooltip content creation |
michael@0 | 257 | // and decision to show or not the tooltip |
michael@0 | 258 | return container._isImagePreviewTarget(target, this.tooltip); |
michael@0 | 259 | } |
michael@0 | 260 | }, |
michael@0 | 261 | |
michael@0 | 262 | /** |
michael@0 | 263 | * Given the known reason, should the current selection be briefly highlighted |
michael@0 | 264 | * In a few cases, we don't want to highlight the node: |
michael@0 | 265 | * - If the reason is null (used to reset the selection), |
michael@0 | 266 | * - if it's "inspector-open" (when the inspector opens up, let's not highlight |
michael@0 | 267 | * the default node) |
michael@0 | 268 | * - if it's "navigateaway" (since the page is being navigated away from) |
michael@0 | 269 | * - if it's "test" (this is a special case for mochitest. In tests, we often |
michael@0 | 270 | * need to select elements but don't necessarily want the highlighter to come |
michael@0 | 271 | * and go after a delay as this might break test scenarios) |
michael@0 | 272 | * We also do not want to start a brief highlight timeout if the node is already |
michael@0 | 273 | * being hovered over, since in that case it will already be highlighted. |
michael@0 | 274 | */ |
michael@0 | 275 | _shouldNewSelectionBeHighlighted: function() { |
michael@0 | 276 | let reason = this._inspector.selection.reason; |
michael@0 | 277 | let unwantedReasons = ["inspector-open", "navigateaway", "test"]; |
michael@0 | 278 | let isHighlitNode = this._hoveredNode === this._inspector.selection.nodeFront; |
michael@0 | 279 | return !isHighlitNode && reason && unwantedReasons.indexOf(reason) === -1; |
michael@0 | 280 | }, |
michael@0 | 281 | |
michael@0 | 282 | /** |
michael@0 | 283 | * Highlight the inspector selected node. |
michael@0 | 284 | */ |
michael@0 | 285 | _onNewSelection: function() { |
michael@0 | 286 | let selection = this._inspector.selection; |
michael@0 | 287 | |
michael@0 | 288 | this.htmlEditor.hide(); |
michael@0 | 289 | let done = this._inspector.updating("markup-view"); |
michael@0 | 290 | if (selection.isNode()) { |
michael@0 | 291 | if (this._shouldNewSelectionBeHighlighted()) { |
michael@0 | 292 | this._brieflyShowBoxModel(selection.nodeFront, {}); |
michael@0 | 293 | } |
michael@0 | 294 | |
michael@0 | 295 | this.showNode(selection.nodeFront, true).then(() => { |
michael@0 | 296 | if (selection.reason !== "treepanel") { |
michael@0 | 297 | this.markNodeAsSelected(selection.nodeFront); |
michael@0 | 298 | } |
michael@0 | 299 | done(); |
michael@0 | 300 | }, (e) => { |
michael@0 | 301 | console.error(e); |
michael@0 | 302 | done(); |
michael@0 | 303 | }); |
michael@0 | 304 | } else { |
michael@0 | 305 | this.unmarkSelectedNode(); |
michael@0 | 306 | done(); |
michael@0 | 307 | } |
michael@0 | 308 | }, |
michael@0 | 309 | |
michael@0 | 310 | /** |
michael@0 | 311 | * Create a TreeWalker to find the next/previous |
michael@0 | 312 | * node for selection. |
michael@0 | 313 | */ |
michael@0 | 314 | _selectionWalker: function(aStart) { |
michael@0 | 315 | let walker = this.doc.createTreeWalker( |
michael@0 | 316 | aStart || this._elt, |
michael@0 | 317 | Ci.nsIDOMNodeFilter.SHOW_ELEMENT, |
michael@0 | 318 | function(aElement) { |
michael@0 | 319 | if (aElement.container && |
michael@0 | 320 | aElement.container.elt === aElement && |
michael@0 | 321 | aElement.container.visible) { |
michael@0 | 322 | return Ci.nsIDOMNodeFilter.FILTER_ACCEPT; |
michael@0 | 323 | } |
michael@0 | 324 | return Ci.nsIDOMNodeFilter.FILTER_SKIP; |
michael@0 | 325 | } |
michael@0 | 326 | ); |
michael@0 | 327 | walker.currentNode = this._selectedContainer.elt; |
michael@0 | 328 | return walker; |
michael@0 | 329 | }, |
michael@0 | 330 | |
michael@0 | 331 | /** |
michael@0 | 332 | * Key handling. |
michael@0 | 333 | */ |
michael@0 | 334 | _onKeyDown: function(aEvent) { |
michael@0 | 335 | let handled = true; |
michael@0 | 336 | |
michael@0 | 337 | // Ignore keystrokes that originated in editors. |
michael@0 | 338 | if (aEvent.target.tagName.toLowerCase() === "input" || |
michael@0 | 339 | aEvent.target.tagName.toLowerCase() === "textarea") { |
michael@0 | 340 | return; |
michael@0 | 341 | } |
michael@0 | 342 | |
michael@0 | 343 | switch(aEvent.keyCode) { |
michael@0 | 344 | case Ci.nsIDOMKeyEvent.DOM_VK_H: |
michael@0 | 345 | let node = this._selectedContainer.node; |
michael@0 | 346 | if (node.hidden) { |
michael@0 | 347 | this.walker.unhideNode(node).then(() => this.nodeChanged(node)); |
michael@0 | 348 | } else { |
michael@0 | 349 | this.walker.hideNode(node).then(() => this.nodeChanged(node)); |
michael@0 | 350 | } |
michael@0 | 351 | break; |
michael@0 | 352 | case Ci.nsIDOMKeyEvent.DOM_VK_DELETE: |
michael@0 | 353 | case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE: |
michael@0 | 354 | this.deleteNode(this._selectedContainer.node); |
michael@0 | 355 | break; |
michael@0 | 356 | case Ci.nsIDOMKeyEvent.DOM_VK_HOME: |
michael@0 | 357 | let rootContainer = this._containers.get(this._rootNode); |
michael@0 | 358 | this.navigate(rootContainer.children.firstChild.container); |
michael@0 | 359 | break; |
michael@0 | 360 | case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: |
michael@0 | 361 | if (this._selectedContainer.expanded) { |
michael@0 | 362 | this.collapseNode(this._selectedContainer.node); |
michael@0 | 363 | } else { |
michael@0 | 364 | let parent = this._selectionWalker().parentNode(); |
michael@0 | 365 | if (parent) { |
michael@0 | 366 | this.navigate(parent.container); |
michael@0 | 367 | } |
michael@0 | 368 | } |
michael@0 | 369 | break; |
michael@0 | 370 | case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: |
michael@0 | 371 | if (!this._selectedContainer.expanded && |
michael@0 | 372 | this._selectedContainer.hasChildren) { |
michael@0 | 373 | this._expandContainer(this._selectedContainer); |
michael@0 | 374 | } else { |
michael@0 | 375 | let next = this._selectionWalker().nextNode(); |
michael@0 | 376 | if (next) { |
michael@0 | 377 | this.navigate(next.container); |
michael@0 | 378 | } |
michael@0 | 379 | } |
michael@0 | 380 | break; |
michael@0 | 381 | case Ci.nsIDOMKeyEvent.DOM_VK_UP: |
michael@0 | 382 | let prev = this._selectionWalker().previousNode(); |
michael@0 | 383 | if (prev) { |
michael@0 | 384 | this.navigate(prev.container); |
michael@0 | 385 | } |
michael@0 | 386 | break; |
michael@0 | 387 | case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: |
michael@0 | 388 | let next = this._selectionWalker().nextNode(); |
michael@0 | 389 | if (next) { |
michael@0 | 390 | this.navigate(next.container); |
michael@0 | 391 | } |
michael@0 | 392 | break; |
michael@0 | 393 | case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: { |
michael@0 | 394 | let walker = this._selectionWalker(); |
michael@0 | 395 | let selection = this._selectedContainer; |
michael@0 | 396 | for (let i = 0; i < PAGE_SIZE; i++) { |
michael@0 | 397 | let prev = walker.previousNode(); |
michael@0 | 398 | if (!prev) { |
michael@0 | 399 | break; |
michael@0 | 400 | } |
michael@0 | 401 | selection = prev.container; |
michael@0 | 402 | } |
michael@0 | 403 | this.navigate(selection); |
michael@0 | 404 | break; |
michael@0 | 405 | } |
michael@0 | 406 | case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: { |
michael@0 | 407 | let walker = this._selectionWalker(); |
michael@0 | 408 | let selection = this._selectedContainer; |
michael@0 | 409 | for (let i = 0; i < PAGE_SIZE; i++) { |
michael@0 | 410 | let next = walker.nextNode(); |
michael@0 | 411 | if (!next) { |
michael@0 | 412 | break; |
michael@0 | 413 | } |
michael@0 | 414 | selection = next.container; |
michael@0 | 415 | } |
michael@0 | 416 | this.navigate(selection); |
michael@0 | 417 | break; |
michael@0 | 418 | } |
michael@0 | 419 | case Ci.nsIDOMKeyEvent.DOM_VK_F2: { |
michael@0 | 420 | this.beginEditingOuterHTML(this._selectedContainer.node); |
michael@0 | 421 | break; |
michael@0 | 422 | } |
michael@0 | 423 | default: |
michael@0 | 424 | handled = false; |
michael@0 | 425 | } |
michael@0 | 426 | if (handled) { |
michael@0 | 427 | aEvent.stopPropagation(); |
michael@0 | 428 | aEvent.preventDefault(); |
michael@0 | 429 | } |
michael@0 | 430 | }, |
michael@0 | 431 | |
michael@0 | 432 | /** |
michael@0 | 433 | * Delete a node from the DOM. |
michael@0 | 434 | * This is an undoable action. |
michael@0 | 435 | */ |
michael@0 | 436 | deleteNode: function(aNode) { |
michael@0 | 437 | if (aNode.isDocumentElement || |
michael@0 | 438 | aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) { |
michael@0 | 439 | return; |
michael@0 | 440 | } |
michael@0 | 441 | |
michael@0 | 442 | let container = this._containers.get(aNode); |
michael@0 | 443 | |
michael@0 | 444 | // Retain the node so we can undo this... |
michael@0 | 445 | this.walker.retainNode(aNode).then(() => { |
michael@0 | 446 | let parent = aNode.parentNode(); |
michael@0 | 447 | let sibling = null; |
michael@0 | 448 | this.undo.do(() => { |
michael@0 | 449 | if (container.selected) { |
michael@0 | 450 | this.navigate(this._containers.get(parent)); |
michael@0 | 451 | } |
michael@0 | 452 | this.walker.removeNode(aNode).then(nextSibling => { |
michael@0 | 453 | sibling = nextSibling; |
michael@0 | 454 | }); |
michael@0 | 455 | }, () => { |
michael@0 | 456 | this.walker.insertBefore(aNode, parent, sibling); |
michael@0 | 457 | }); |
michael@0 | 458 | }).then(null, console.error); |
michael@0 | 459 | }, |
michael@0 | 460 | |
michael@0 | 461 | /** |
michael@0 | 462 | * If an editable item is focused, select its container. |
michael@0 | 463 | */ |
michael@0 | 464 | _onFocus: function(aEvent) { |
michael@0 | 465 | let parent = aEvent.target; |
michael@0 | 466 | while (!parent.container) { |
michael@0 | 467 | parent = parent.parentNode; |
michael@0 | 468 | } |
michael@0 | 469 | if (parent) { |
michael@0 | 470 | this.navigate(parent.container, true); |
michael@0 | 471 | } |
michael@0 | 472 | }, |
michael@0 | 473 | |
michael@0 | 474 | /** |
michael@0 | 475 | * Handle a user-requested navigation to a given MarkupContainer, |
michael@0 | 476 | * updating the inspector's currently-selected node. |
michael@0 | 477 | * |
michael@0 | 478 | * @param MarkupContainer aContainer |
michael@0 | 479 | * The container we're navigating to. |
michael@0 | 480 | * @param aIgnoreFocus aIgnoreFocus |
michael@0 | 481 | * If falsy, keyboard focus will be moved to the container too. |
michael@0 | 482 | */ |
michael@0 | 483 | navigate: function(aContainer, aIgnoreFocus) { |
michael@0 | 484 | if (!aContainer) { |
michael@0 | 485 | return; |
michael@0 | 486 | } |
michael@0 | 487 | |
michael@0 | 488 | let node = aContainer.node; |
michael@0 | 489 | this.markNodeAsSelected(node, "treepanel"); |
michael@0 | 490 | |
michael@0 | 491 | if (!aIgnoreFocus) { |
michael@0 | 492 | aContainer.focus(); |
michael@0 | 493 | } |
michael@0 | 494 | }, |
michael@0 | 495 | |
michael@0 | 496 | /** |
michael@0 | 497 | * Make sure a node is included in the markup tool. |
michael@0 | 498 | * |
michael@0 | 499 | * @param DOMNode aNode |
michael@0 | 500 | * The node in the content document. |
michael@0 | 501 | * @param boolean aFlashNode |
michael@0 | 502 | * Whether the newly imported node should be flashed |
michael@0 | 503 | * @returns MarkupContainer The MarkupContainer object for this element. |
michael@0 | 504 | */ |
michael@0 | 505 | importNode: function(aNode, aFlashNode) { |
michael@0 | 506 | if (!aNode) { |
michael@0 | 507 | return null; |
michael@0 | 508 | } |
michael@0 | 509 | |
michael@0 | 510 | if (this._containers.has(aNode)) { |
michael@0 | 511 | return this._containers.get(aNode); |
michael@0 | 512 | } |
michael@0 | 513 | |
michael@0 | 514 | if (aNode === this.walker.rootNode) { |
michael@0 | 515 | var container = new RootContainer(this, aNode); |
michael@0 | 516 | this._elt.appendChild(container.elt); |
michael@0 | 517 | this._rootNode = aNode; |
michael@0 | 518 | } else { |
michael@0 | 519 | var container = new MarkupContainer(this, aNode, this._inspector); |
michael@0 | 520 | if (aFlashNode) { |
michael@0 | 521 | container.flashMutation(); |
michael@0 | 522 | } |
michael@0 | 523 | } |
michael@0 | 524 | |
michael@0 | 525 | this._containers.set(aNode, container); |
michael@0 | 526 | container.childrenDirty = true; |
michael@0 | 527 | |
michael@0 | 528 | this._updateChildren(container); |
michael@0 | 529 | |
michael@0 | 530 | return container; |
michael@0 | 531 | }, |
michael@0 | 532 | |
michael@0 | 533 | /** |
michael@0 | 534 | * Mutation observer used for included nodes. |
michael@0 | 535 | */ |
michael@0 | 536 | _mutationObserver: function(aMutations) { |
michael@0 | 537 | let requiresLayoutChange = false; |
michael@0 | 538 | let reselectParent; |
michael@0 | 539 | let reselectChildIndex; |
michael@0 | 540 | |
michael@0 | 541 | for (let mutation of aMutations) { |
michael@0 | 542 | let type = mutation.type; |
michael@0 | 543 | let target = mutation.target; |
michael@0 | 544 | |
michael@0 | 545 | if (mutation.type === "documentUnload") { |
michael@0 | 546 | // Treat this as a childList change of the child (maybe the protocol |
michael@0 | 547 | // should do this). |
michael@0 | 548 | type = "childList"; |
michael@0 | 549 | target = mutation.targetParent; |
michael@0 | 550 | if (!target) { |
michael@0 | 551 | continue; |
michael@0 | 552 | } |
michael@0 | 553 | } |
michael@0 | 554 | |
michael@0 | 555 | let container = this._containers.get(target); |
michael@0 | 556 | if (!container) { |
michael@0 | 557 | // Container might not exist if this came from a load event for a node |
michael@0 | 558 | // we're not viewing. |
michael@0 | 559 | continue; |
michael@0 | 560 | } |
michael@0 | 561 | if (type === "attributes" || type === "characterData") { |
michael@0 | 562 | container.update(); |
michael@0 | 563 | |
michael@0 | 564 | // Auto refresh style properties on selected node when they change. |
michael@0 | 565 | if (type === "attributes" && container.selected) { |
michael@0 | 566 | requiresLayoutChange = true; |
michael@0 | 567 | } |
michael@0 | 568 | } else if (type === "childList") { |
michael@0 | 569 | let isFromOuterHTML = mutation.removed.some((n) => { |
michael@0 | 570 | return n === this._outerHTMLNode; |
michael@0 | 571 | }); |
michael@0 | 572 | |
michael@0 | 573 | // Keep track of which node should be reselected after mutations. |
michael@0 | 574 | if (isFromOuterHTML) { |
michael@0 | 575 | reselectParent = target; |
michael@0 | 576 | reselectChildIndex = this._outerHTMLChildIndex; |
michael@0 | 577 | |
michael@0 | 578 | delete this._outerHTMLNode; |
michael@0 | 579 | delete this._outerHTMLChildIndex; |
michael@0 | 580 | } |
michael@0 | 581 | |
michael@0 | 582 | container.childrenDirty = true; |
michael@0 | 583 | // Update the children to take care of changes in the markup view DOM. |
michael@0 | 584 | this._updateChildren(container, {flash: !isFromOuterHTML}); |
michael@0 | 585 | } |
michael@0 | 586 | } |
michael@0 | 587 | |
michael@0 | 588 | if (requiresLayoutChange) { |
michael@0 | 589 | this._inspector.immediateLayoutChange(); |
michael@0 | 590 | } |
michael@0 | 591 | this._waitForChildren().then((nodes) => { |
michael@0 | 592 | this._flashMutatedNodes(aMutations); |
michael@0 | 593 | this._inspector.emit("markupmutation", aMutations); |
michael@0 | 594 | |
michael@0 | 595 | // Since the htmlEditor is absolutely positioned, a mutation may change |
michael@0 | 596 | // the location in which it should be shown. |
michael@0 | 597 | this.htmlEditor.refresh(); |
michael@0 | 598 | |
michael@0 | 599 | // If a node has had its outerHTML set, the parent node will be selected. |
michael@0 | 600 | // Reselect the original node immediately. |
michael@0 | 601 | if (this._inspector.selection.nodeFront === reselectParent) { |
michael@0 | 602 | this.walker.children(reselectParent).then((o) => { |
michael@0 | 603 | let node = o.nodes[reselectChildIndex]; |
michael@0 | 604 | let container = this._containers.get(node); |
michael@0 | 605 | if (node && container) { |
michael@0 | 606 | this.markNodeAsSelected(node, "outerhtml"); |
michael@0 | 607 | if (container.hasChildren) { |
michael@0 | 608 | this.expandNode(node); |
michael@0 | 609 | } |
michael@0 | 610 | } |
michael@0 | 611 | }); |
michael@0 | 612 | |
michael@0 | 613 | } |
michael@0 | 614 | }); |
michael@0 | 615 | }, |
michael@0 | 616 | |
michael@0 | 617 | /** |
michael@0 | 618 | * Given a list of mutations returned by the mutation observer, flash the |
michael@0 | 619 | * corresponding containers to attract attention. |
michael@0 | 620 | */ |
michael@0 | 621 | _flashMutatedNodes: function(aMutations) { |
michael@0 | 622 | let addedOrEditedContainers = new Set(); |
michael@0 | 623 | let removedContainers = new Set(); |
michael@0 | 624 | |
michael@0 | 625 | for (let {type, target, added, removed} of aMutations) { |
michael@0 | 626 | let container = this._containers.get(target); |
michael@0 | 627 | |
michael@0 | 628 | if (container) { |
michael@0 | 629 | if (type === "attributes" || type === "characterData") { |
michael@0 | 630 | addedOrEditedContainers.add(container); |
michael@0 | 631 | } else if (type === "childList") { |
michael@0 | 632 | // If there has been removals, flash the parent |
michael@0 | 633 | if (removed.length) { |
michael@0 | 634 | removedContainers.add(container); |
michael@0 | 635 | } |
michael@0 | 636 | |
michael@0 | 637 | // If there has been additions, flash the nodes |
michael@0 | 638 | added.forEach(added => { |
michael@0 | 639 | let addedContainer = this._containers.get(added); |
michael@0 | 640 | addedOrEditedContainers.add(addedContainer); |
michael@0 | 641 | |
michael@0 | 642 | // The node may be added as a result of an append, in which case it |
michael@0 | 643 | // it will have been removed from another container first, but in |
michael@0 | 644 | // these cases we don't want to flash both the removal and the |
michael@0 | 645 | // addition |
michael@0 | 646 | removedContainers.delete(container); |
michael@0 | 647 | }); |
michael@0 | 648 | } |
michael@0 | 649 | } |
michael@0 | 650 | } |
michael@0 | 651 | |
michael@0 | 652 | for (let container of removedContainers) { |
michael@0 | 653 | container.flashMutation(); |
michael@0 | 654 | } |
michael@0 | 655 | for (let container of addedOrEditedContainers) { |
michael@0 | 656 | container.flashMutation(); |
michael@0 | 657 | } |
michael@0 | 658 | }, |
michael@0 | 659 | |
michael@0 | 660 | /** |
michael@0 | 661 | * Make sure the given node's parents are expanded and the |
michael@0 | 662 | * node is scrolled on to screen. |
michael@0 | 663 | */ |
michael@0 | 664 | showNode: function(aNode, centered) { |
michael@0 | 665 | let parent = aNode; |
michael@0 | 666 | |
michael@0 | 667 | this.importNode(aNode); |
michael@0 | 668 | |
michael@0 | 669 | while ((parent = parent.parentNode())) { |
michael@0 | 670 | this.importNode(parent); |
michael@0 | 671 | this.expandNode(parent); |
michael@0 | 672 | } |
michael@0 | 673 | |
michael@0 | 674 | return this._waitForChildren().then(() => { |
michael@0 | 675 | return this._ensureVisible(aNode); |
michael@0 | 676 | }).then(() => { |
michael@0 | 677 | // Why is this not working? |
michael@0 | 678 | this.layoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered); |
michael@0 | 679 | }); |
michael@0 | 680 | }, |
michael@0 | 681 | |
michael@0 | 682 | /** |
michael@0 | 683 | * Expand the container's children. |
michael@0 | 684 | */ |
michael@0 | 685 | _expandContainer: function(aContainer) { |
michael@0 | 686 | return this._updateChildren(aContainer, {expand: true}).then(() => { |
michael@0 | 687 | aContainer.expanded = true; |
michael@0 | 688 | }); |
michael@0 | 689 | }, |
michael@0 | 690 | |
michael@0 | 691 | /** |
michael@0 | 692 | * Expand the node's children. |
michael@0 | 693 | */ |
michael@0 | 694 | expandNode: function(aNode) { |
michael@0 | 695 | let container = this._containers.get(aNode); |
michael@0 | 696 | this._expandContainer(container); |
michael@0 | 697 | }, |
michael@0 | 698 | |
michael@0 | 699 | /** |
michael@0 | 700 | * Expand the entire tree beneath a container. |
michael@0 | 701 | * |
michael@0 | 702 | * @param aContainer The container to expand. |
michael@0 | 703 | */ |
michael@0 | 704 | _expandAll: function(aContainer) { |
michael@0 | 705 | return this._expandContainer(aContainer).then(() => { |
michael@0 | 706 | let child = aContainer.children.firstChild; |
michael@0 | 707 | let promises = []; |
michael@0 | 708 | while (child) { |
michael@0 | 709 | promises.push(this._expandAll(child.container)); |
michael@0 | 710 | child = child.nextSibling; |
michael@0 | 711 | } |
michael@0 | 712 | return promise.all(promises); |
michael@0 | 713 | }).then(null, console.error); |
michael@0 | 714 | }, |
michael@0 | 715 | |
michael@0 | 716 | /** |
michael@0 | 717 | * Expand the entire tree beneath a node. |
michael@0 | 718 | * |
michael@0 | 719 | * @param aContainer The node to expand, or null |
michael@0 | 720 | * to start from the top. |
michael@0 | 721 | */ |
michael@0 | 722 | expandAll: function(aNode) { |
michael@0 | 723 | aNode = aNode || this._rootNode; |
michael@0 | 724 | return this._expandAll(this._containers.get(aNode)); |
michael@0 | 725 | }, |
michael@0 | 726 | |
michael@0 | 727 | /** |
michael@0 | 728 | * Collapse the node's children. |
michael@0 | 729 | */ |
michael@0 | 730 | collapseNode: function(aNode) { |
michael@0 | 731 | let container = this._containers.get(aNode); |
michael@0 | 732 | container.expanded = false; |
michael@0 | 733 | }, |
michael@0 | 734 | |
michael@0 | 735 | /** |
michael@0 | 736 | * Retrieve the outerHTML for a remote node. |
michael@0 | 737 | * @param aNode The NodeFront to get the outerHTML for. |
michael@0 | 738 | * @returns A promise that will be resolved with the outerHTML. |
michael@0 | 739 | */ |
michael@0 | 740 | getNodeOuterHTML: function(aNode) { |
michael@0 | 741 | let def = promise.defer(); |
michael@0 | 742 | this.walker.outerHTML(aNode).then(longstr => { |
michael@0 | 743 | longstr.string().then(outerHTML => { |
michael@0 | 744 | longstr.release().then(null, console.error); |
michael@0 | 745 | def.resolve(outerHTML); |
michael@0 | 746 | }); |
michael@0 | 747 | }); |
michael@0 | 748 | return def.promise; |
michael@0 | 749 | }, |
michael@0 | 750 | |
michael@0 | 751 | /** |
michael@0 | 752 | * Retrieve the index of a child within its parent's children list. |
michael@0 | 753 | * @param aNode The NodeFront to find the index of. |
michael@0 | 754 | * @returns A promise that will be resolved with the integer index. |
michael@0 | 755 | * If the child cannot be found, returns -1 |
michael@0 | 756 | */ |
michael@0 | 757 | getNodeChildIndex: function(aNode) { |
michael@0 | 758 | let def = promise.defer(); |
michael@0 | 759 | let parentNode = aNode.parentNode(); |
michael@0 | 760 | |
michael@0 | 761 | // Node may have been removed from the DOM, instead of throwing an error, |
michael@0 | 762 | // return -1 indicating that it isn't inside of its parent children list. |
michael@0 | 763 | if (!parentNode) { |
michael@0 | 764 | def.resolve(-1); |
michael@0 | 765 | } else { |
michael@0 | 766 | this.walker.children(parentNode).then(children => { |
michael@0 | 767 | def.resolve(children.nodes.indexOf(aNode)); |
michael@0 | 768 | }); |
michael@0 | 769 | } |
michael@0 | 770 | |
michael@0 | 771 | return def.promise; |
michael@0 | 772 | }, |
michael@0 | 773 | |
michael@0 | 774 | /** |
michael@0 | 775 | * Retrieve the index of a child within its parent's children collection. |
michael@0 | 776 | * @param aNode The NodeFront to find the index of. |
michael@0 | 777 | * @param newValue The new outerHTML to set on the node. |
michael@0 | 778 | * @param oldValue The old outerHTML that will be reverted to find the index of. |
michael@0 | 779 | * @returns A promise that will be resolved with the integer index. |
michael@0 | 780 | * If the child cannot be found, returns -1 |
michael@0 | 781 | */ |
michael@0 | 782 | updateNodeOuterHTML: function(aNode, newValue, oldValue) { |
michael@0 | 783 | let container = this._containers.get(aNode); |
michael@0 | 784 | if (!container) { |
michael@0 | 785 | return; |
michael@0 | 786 | } |
michael@0 | 787 | |
michael@0 | 788 | this.getNodeChildIndex(aNode).then((i) => { |
michael@0 | 789 | this._outerHTMLChildIndex = i; |
michael@0 | 790 | this._outerHTMLNode = aNode; |
michael@0 | 791 | |
michael@0 | 792 | container.undo.do(() => { |
michael@0 | 793 | this.walker.setOuterHTML(aNode, newValue); |
michael@0 | 794 | }, () => { |
michael@0 | 795 | this.walker.setOuterHTML(aNode, oldValue); |
michael@0 | 796 | }); |
michael@0 | 797 | }); |
michael@0 | 798 | }, |
michael@0 | 799 | |
michael@0 | 800 | /** |
michael@0 | 801 | * Open an editor in the UI to allow editing of a node's outerHTML. |
michael@0 | 802 | * @param aNode The NodeFront to edit. |
michael@0 | 803 | */ |
michael@0 | 804 | beginEditingOuterHTML: function(aNode) { |
michael@0 | 805 | this.getNodeOuterHTML(aNode).then((oldValue)=> { |
michael@0 | 806 | let container = this._containers.get(aNode); |
michael@0 | 807 | if (!container) { |
michael@0 | 808 | return; |
michael@0 | 809 | } |
michael@0 | 810 | this.htmlEditor.show(container.tagLine, oldValue); |
michael@0 | 811 | this.htmlEditor.once("popuphidden", (e, aCommit, aValue) => { |
michael@0 | 812 | // Need to focus the <html> element instead of the frame / window |
michael@0 | 813 | // in order to give keyboard focus back to doc (from editor). |
michael@0 | 814 | this._frame.contentDocument.documentElement.focus(); |
michael@0 | 815 | |
michael@0 | 816 | if (aCommit) { |
michael@0 | 817 | this.updateNodeOuterHTML(aNode, aValue, oldValue); |
michael@0 | 818 | } |
michael@0 | 819 | }); |
michael@0 | 820 | }); |
michael@0 | 821 | }, |
michael@0 | 822 | |
michael@0 | 823 | /** |
michael@0 | 824 | * Mark the given node expanded. |
michael@0 | 825 | * @param {NodeFront} aNode The NodeFront to mark as expanded. |
michael@0 | 826 | * @param {Boolean} aExpanded Whether the expand or collapse. |
michael@0 | 827 | * @param {Boolean} aExpandDescendants Whether to expand all descendants too |
michael@0 | 828 | */ |
michael@0 | 829 | setNodeExpanded: function(aNode, aExpanded, aExpandDescendants) { |
michael@0 | 830 | if (aExpanded) { |
michael@0 | 831 | if (aExpandDescendants) { |
michael@0 | 832 | this.expandAll(aNode); |
michael@0 | 833 | } else { |
michael@0 | 834 | this.expandNode(aNode); |
michael@0 | 835 | } |
michael@0 | 836 | } else { |
michael@0 | 837 | this.collapseNode(aNode); |
michael@0 | 838 | } |
michael@0 | 839 | }, |
michael@0 | 840 | |
michael@0 | 841 | /** |
michael@0 | 842 | * Mark the given node selected, and update the inspector.selection |
michael@0 | 843 | * object's NodeFront to keep consistent state between UI and selection. |
michael@0 | 844 | * @param aNode The NodeFront to mark as selected. |
michael@0 | 845 | */ |
michael@0 | 846 | markNodeAsSelected: function(aNode, reason) { |
michael@0 | 847 | let container = this._containers.get(aNode); |
michael@0 | 848 | if (this._selectedContainer === container) { |
michael@0 | 849 | return false; |
michael@0 | 850 | } |
michael@0 | 851 | if (this._selectedContainer) { |
michael@0 | 852 | this._selectedContainer.selected = false; |
michael@0 | 853 | } |
michael@0 | 854 | this._selectedContainer = container; |
michael@0 | 855 | if (aNode) { |
michael@0 | 856 | this._selectedContainer.selected = true; |
michael@0 | 857 | } |
michael@0 | 858 | |
michael@0 | 859 | this._inspector.selection.setNodeFront(aNode, reason || "nodeselected"); |
michael@0 | 860 | return true; |
michael@0 | 861 | }, |
michael@0 | 862 | |
michael@0 | 863 | /** |
michael@0 | 864 | * Make sure that every ancestor of the selection are updated |
michael@0 | 865 | * and included in the list of visible children. |
michael@0 | 866 | */ |
michael@0 | 867 | _ensureVisible: function(node) { |
michael@0 | 868 | while (node) { |
michael@0 | 869 | let container = this._containers.get(node); |
michael@0 | 870 | let parent = node.parentNode(); |
michael@0 | 871 | if (!container.elt.parentNode) { |
michael@0 | 872 | let parentContainer = this._containers.get(parent); |
michael@0 | 873 | if (parentContainer) { |
michael@0 | 874 | parentContainer.childrenDirty = true; |
michael@0 | 875 | this._updateChildren(parentContainer, {expand: node}); |
michael@0 | 876 | } |
michael@0 | 877 | } |
michael@0 | 878 | |
michael@0 | 879 | node = parent; |
michael@0 | 880 | } |
michael@0 | 881 | return this._waitForChildren(); |
michael@0 | 882 | }, |
michael@0 | 883 | |
michael@0 | 884 | /** |
michael@0 | 885 | * Unmark selected node (no node selected). |
michael@0 | 886 | */ |
michael@0 | 887 | unmarkSelectedNode: function() { |
michael@0 | 888 | if (this._selectedContainer) { |
michael@0 | 889 | this._selectedContainer.selected = false; |
michael@0 | 890 | this._selectedContainer = null; |
michael@0 | 891 | } |
michael@0 | 892 | }, |
michael@0 | 893 | |
michael@0 | 894 | /** |
michael@0 | 895 | * Called when the markup panel initiates a change on a node. |
michael@0 | 896 | */ |
michael@0 | 897 | nodeChanged: function(aNode) { |
michael@0 | 898 | if (aNode === this._inspector.selection.nodeFront) { |
michael@0 | 899 | this._inspector.change("markupview"); |
michael@0 | 900 | } |
michael@0 | 901 | }, |
michael@0 | 902 | |
michael@0 | 903 | /** |
michael@0 | 904 | * Check if the current selection is a descendent of the container. |
michael@0 | 905 | * if so, make sure it's among the visible set for the container, |
michael@0 | 906 | * and set the dirty flag if needed. |
michael@0 | 907 | * @returns The node that should be made visible, if any. |
michael@0 | 908 | */ |
michael@0 | 909 | _checkSelectionVisible: function(aContainer) { |
michael@0 | 910 | let centered = null; |
michael@0 | 911 | let node = this._inspector.selection.nodeFront; |
michael@0 | 912 | while (node) { |
michael@0 | 913 | if (node.parentNode() === aContainer.node) { |
michael@0 | 914 | centered = node; |
michael@0 | 915 | break; |
michael@0 | 916 | } |
michael@0 | 917 | node = node.parentNode(); |
michael@0 | 918 | } |
michael@0 | 919 | |
michael@0 | 920 | return centered; |
michael@0 | 921 | }, |
michael@0 | 922 | |
michael@0 | 923 | /** |
michael@0 | 924 | * Make sure all children of the given container's node are |
michael@0 | 925 | * imported and attached to the container in the right order. |
michael@0 | 926 | * |
michael@0 | 927 | * Children need to be updated only in the following circumstances: |
michael@0 | 928 | * a) We just imported this node and have never seen its children. |
michael@0 | 929 | * container.childrenDirty will be set by importNode in this case. |
michael@0 | 930 | * b) We received a childList mutation on the node. |
michael@0 | 931 | * container.childrenDirty will be set in that case too. |
michael@0 | 932 | * c) We have changed the selection, and the path to that selection |
michael@0 | 933 | * wasn't loaded in a previous children request (because we only |
michael@0 | 934 | * grab a subset). |
michael@0 | 935 | * container.childrenDirty should be set in that case too! |
michael@0 | 936 | * |
michael@0 | 937 | * @param MarkupContainer aContainer |
michael@0 | 938 | * The markup container whose children need updating |
michael@0 | 939 | * @param Object options |
michael@0 | 940 | * Options are {expand:boolean,flash:boolean} |
michael@0 | 941 | * @return a promise that will be resolved when the children are ready |
michael@0 | 942 | * (which may be immediately). |
michael@0 | 943 | */ |
michael@0 | 944 | _updateChildren: function(aContainer, options) { |
michael@0 | 945 | let expand = options && options.expand; |
michael@0 | 946 | let flash = options && options.flash; |
michael@0 | 947 | |
michael@0 | 948 | aContainer.hasChildren = aContainer.node.hasChildren; |
michael@0 | 949 | |
michael@0 | 950 | if (!this._queuedChildUpdates) { |
michael@0 | 951 | this._queuedChildUpdates = new Map(); |
michael@0 | 952 | } |
michael@0 | 953 | |
michael@0 | 954 | if (this._queuedChildUpdates.has(aContainer)) { |
michael@0 | 955 | return this._queuedChildUpdates.get(aContainer); |
michael@0 | 956 | } |
michael@0 | 957 | |
michael@0 | 958 | if (!aContainer.childrenDirty) { |
michael@0 | 959 | return promise.resolve(aContainer); |
michael@0 | 960 | } |
michael@0 | 961 | |
michael@0 | 962 | if (!aContainer.hasChildren) { |
michael@0 | 963 | while (aContainer.children.firstChild) { |
michael@0 | 964 | aContainer.children.removeChild(aContainer.children.firstChild); |
michael@0 | 965 | } |
michael@0 | 966 | aContainer.childrenDirty = false; |
michael@0 | 967 | return promise.resolve(aContainer); |
michael@0 | 968 | } |
michael@0 | 969 | |
michael@0 | 970 | // If we're not expanded (or asked to update anyway), we're done for |
michael@0 | 971 | // now. Note that this will leave the childrenDirty flag set, so when |
michael@0 | 972 | // expanded we'll refresh the child list. |
michael@0 | 973 | if (!(aContainer.expanded || expand)) { |
michael@0 | 974 | return promise.resolve(aContainer); |
michael@0 | 975 | } |
michael@0 | 976 | |
michael@0 | 977 | // We're going to issue a children request, make sure it includes the |
michael@0 | 978 | // centered node. |
michael@0 | 979 | let centered = this._checkSelectionVisible(aContainer); |
michael@0 | 980 | |
michael@0 | 981 | // Children aren't updated yet, but clear the childrenDirty flag anyway. |
michael@0 | 982 | // If the dirty flag is re-set while we're fetching we'll need to fetch |
michael@0 | 983 | // again. |
michael@0 | 984 | aContainer.childrenDirty = false; |
michael@0 | 985 | let updatePromise = this._getVisibleChildren(aContainer, centered).then(children => { |
michael@0 | 986 | if (!this._containers) { |
michael@0 | 987 | return promise.reject("markup view destroyed"); |
michael@0 | 988 | } |
michael@0 | 989 | this._queuedChildUpdates.delete(aContainer); |
michael@0 | 990 | |
michael@0 | 991 | // If children are dirty, we got a change notification for this node |
michael@0 | 992 | // while the request was in progress, we need to do it again. |
michael@0 | 993 | if (aContainer.childrenDirty) { |
michael@0 | 994 | return this._updateChildren(aContainer, {expand: centered}); |
michael@0 | 995 | } |
michael@0 | 996 | |
michael@0 | 997 | let fragment = this.doc.createDocumentFragment(); |
michael@0 | 998 | |
michael@0 | 999 | for (let child of children.nodes) { |
michael@0 | 1000 | let container = this.importNode(child, flash); |
michael@0 | 1001 | fragment.appendChild(container.elt); |
michael@0 | 1002 | } |
michael@0 | 1003 | |
michael@0 | 1004 | while (aContainer.children.firstChild) { |
michael@0 | 1005 | aContainer.children.removeChild(aContainer.children.firstChild); |
michael@0 | 1006 | } |
michael@0 | 1007 | |
michael@0 | 1008 | if (!(children.hasFirst && children.hasLast)) { |
michael@0 | 1009 | let data = { |
michael@0 | 1010 | showing: this.strings.GetStringFromName("markupView.more.showing"), |
michael@0 | 1011 | showAll: this.strings.formatStringFromName( |
michael@0 | 1012 | "markupView.more.showAll", |
michael@0 | 1013 | [aContainer.node.numChildren.toString()], 1), |
michael@0 | 1014 | allButtonClick: () => { |
michael@0 | 1015 | aContainer.maxChildren = -1; |
michael@0 | 1016 | aContainer.childrenDirty = true; |
michael@0 | 1017 | this._updateChildren(aContainer); |
michael@0 | 1018 | } |
michael@0 | 1019 | }; |
michael@0 | 1020 | |
michael@0 | 1021 | if (!children.hasFirst) { |
michael@0 | 1022 | let span = this.template("more-nodes", data); |
michael@0 | 1023 | fragment.insertBefore(span, fragment.firstChild); |
michael@0 | 1024 | } |
michael@0 | 1025 | if (!children.hasLast) { |
michael@0 | 1026 | let span = this.template("more-nodes", data); |
michael@0 | 1027 | fragment.appendChild(span); |
michael@0 | 1028 | } |
michael@0 | 1029 | } |
michael@0 | 1030 | |
michael@0 | 1031 | aContainer.children.appendChild(fragment); |
michael@0 | 1032 | return aContainer; |
michael@0 | 1033 | }).then(null, console.error); |
michael@0 | 1034 | this._queuedChildUpdates.set(aContainer, updatePromise); |
michael@0 | 1035 | return updatePromise; |
michael@0 | 1036 | }, |
michael@0 | 1037 | |
michael@0 | 1038 | _waitForChildren: function() { |
michael@0 | 1039 | if (!this._queuedChildUpdates) { |
michael@0 | 1040 | return promise.resolve(undefined); |
michael@0 | 1041 | } |
michael@0 | 1042 | return promise.all([updatePromise for (updatePromise of this._queuedChildUpdates.values())]); |
michael@0 | 1043 | }, |
michael@0 | 1044 | |
michael@0 | 1045 | /** |
michael@0 | 1046 | * Return a list of the children to display for this container. |
michael@0 | 1047 | */ |
michael@0 | 1048 | _getVisibleChildren: function(aContainer, aCentered) { |
michael@0 | 1049 | let maxChildren = aContainer.maxChildren || this.maxChildren; |
michael@0 | 1050 | if (maxChildren == -1) { |
michael@0 | 1051 | maxChildren = undefined; |
michael@0 | 1052 | } |
michael@0 | 1053 | |
michael@0 | 1054 | return this.walker.children(aContainer.node, { |
michael@0 | 1055 | maxNodes: maxChildren, |
michael@0 | 1056 | center: aCentered |
michael@0 | 1057 | }); |
michael@0 | 1058 | }, |
michael@0 | 1059 | |
michael@0 | 1060 | /** |
michael@0 | 1061 | * Tear down the markup panel. |
michael@0 | 1062 | */ |
michael@0 | 1063 | destroy: function() { |
michael@0 | 1064 | if (this._destroyer) { |
michael@0 | 1065 | return this._destroyer; |
michael@0 | 1066 | } |
michael@0 | 1067 | |
michael@0 | 1068 | // Note that if the toolbox is closed, this will work fine, but will fail |
michael@0 | 1069 | // in case the browser is closed and will trigger a noSuchActor message. |
michael@0 | 1070 | this._destroyer = this._hideBoxModel(); |
michael@0 | 1071 | |
michael@0 | 1072 | this._hoveredNode = null; |
michael@0 | 1073 | this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover); |
michael@0 | 1074 | |
michael@0 | 1075 | this.htmlEditor.destroy(); |
michael@0 | 1076 | this.htmlEditor = null; |
michael@0 | 1077 | |
michael@0 | 1078 | this.undo.destroy(); |
michael@0 | 1079 | this.undo = null; |
michael@0 | 1080 | |
michael@0 | 1081 | this.popup.destroy(); |
michael@0 | 1082 | this.popup = null; |
michael@0 | 1083 | |
michael@0 | 1084 | this._frame.removeEventListener("focus", this._boundFocus, false); |
michael@0 | 1085 | this._boundFocus = null; |
michael@0 | 1086 | |
michael@0 | 1087 | if (this._boundUpdatePreview) { |
michael@0 | 1088 | this._frame.contentWindow.removeEventListener("scroll", |
michael@0 | 1089 | this._boundUpdatePreview, true); |
michael@0 | 1090 | this._boundUpdatePreview = null; |
michael@0 | 1091 | } |
michael@0 | 1092 | |
michael@0 | 1093 | if (this._boundResizePreview) { |
michael@0 | 1094 | this._frame.contentWindow.removeEventListener("resize", |
michael@0 | 1095 | this._boundResizePreview, true); |
michael@0 | 1096 | this._frame.contentWindow.removeEventListener("overflow", |
michael@0 | 1097 | this._boundResizePreview, true); |
michael@0 | 1098 | this._frame.contentWindow.removeEventListener("underflow", |
michael@0 | 1099 | this._boundResizePreview, true); |
michael@0 | 1100 | this._boundResizePreview = null; |
michael@0 | 1101 | } |
michael@0 | 1102 | |
michael@0 | 1103 | this._frame.contentWindow.removeEventListener("keydown", |
michael@0 | 1104 | this._boundKeyDown, false); |
michael@0 | 1105 | this._boundKeyDown = null; |
michael@0 | 1106 | |
michael@0 | 1107 | this._inspector.selection.off("new-node-front", this._boundOnNewSelection); |
michael@0 | 1108 | this._boundOnNewSelection = null; |
michael@0 | 1109 | |
michael@0 | 1110 | this.walker.off("mutations", this._boundMutationObserver) |
michael@0 | 1111 | this._boundMutationObserver = null; |
michael@0 | 1112 | |
michael@0 | 1113 | this._elt.removeEventListener("mousemove", this._onMouseMove, false); |
michael@0 | 1114 | this._elt.removeEventListener("mouseleave", this._onMouseLeave, false); |
michael@0 | 1115 | this._elt = null; |
michael@0 | 1116 | |
michael@0 | 1117 | for (let [key, container] of this._containers) { |
michael@0 | 1118 | container.destroy(); |
michael@0 | 1119 | } |
michael@0 | 1120 | this._containers = null; |
michael@0 | 1121 | |
michael@0 | 1122 | this.tooltip.destroy(); |
michael@0 | 1123 | this.tooltip = null; |
michael@0 | 1124 | |
michael@0 | 1125 | return this._destroyer; |
michael@0 | 1126 | }, |
michael@0 | 1127 | |
michael@0 | 1128 | /** |
michael@0 | 1129 | * Initialize the preview panel. |
michael@0 | 1130 | */ |
michael@0 | 1131 | _initPreview: function() { |
michael@0 | 1132 | this._previewEnabled = Services.prefs.getBoolPref("devtools.inspector.markupPreview"); |
michael@0 | 1133 | if (!this._previewEnabled) { |
michael@0 | 1134 | return; |
michael@0 | 1135 | } |
michael@0 | 1136 | |
michael@0 | 1137 | this._previewBar = this.doc.querySelector("#previewbar"); |
michael@0 | 1138 | this._preview = this.doc.querySelector("#preview"); |
michael@0 | 1139 | this._viewbox = this.doc.querySelector("#viewbox"); |
michael@0 | 1140 | |
michael@0 | 1141 | this._previewBar.classList.remove("disabled"); |
michael@0 | 1142 | |
michael@0 | 1143 | this._previewWidth = this._preview.getBoundingClientRect().width; |
michael@0 | 1144 | |
michael@0 | 1145 | this._boundResizePreview = this._resizePreview.bind(this); |
michael@0 | 1146 | this._frame.contentWindow.addEventListener("resize", |
michael@0 | 1147 | this._boundResizePreview, true); |
michael@0 | 1148 | this._frame.contentWindow.addEventListener("overflow", |
michael@0 | 1149 | this._boundResizePreview, true); |
michael@0 | 1150 | this._frame.contentWindow.addEventListener("underflow", |
michael@0 | 1151 | this._boundResizePreview, true); |
michael@0 | 1152 | |
michael@0 | 1153 | this._boundUpdatePreview = this._updatePreview.bind(this); |
michael@0 | 1154 | this._frame.contentWindow.addEventListener("scroll", |
michael@0 | 1155 | this._boundUpdatePreview, true); |
michael@0 | 1156 | this._updatePreview(); |
michael@0 | 1157 | }, |
michael@0 | 1158 | |
michael@0 | 1159 | /** |
michael@0 | 1160 | * Move the preview viewbox. |
michael@0 | 1161 | */ |
michael@0 | 1162 | _updatePreview: function() { |
michael@0 | 1163 | if (!this._previewEnabled) { |
michael@0 | 1164 | return; |
michael@0 | 1165 | } |
michael@0 | 1166 | let win = this._frame.contentWindow; |
michael@0 | 1167 | |
michael@0 | 1168 | if (win.scrollMaxY == 0) { |
michael@0 | 1169 | this._previewBar.classList.add("disabled"); |
michael@0 | 1170 | return; |
michael@0 | 1171 | } |
michael@0 | 1172 | |
michael@0 | 1173 | this._previewBar.classList.remove("disabled"); |
michael@0 | 1174 | |
michael@0 | 1175 | let ratio = this._previewWidth / PREVIEW_AREA; |
michael@0 | 1176 | let width = ratio * win.innerWidth; |
michael@0 | 1177 | |
michael@0 | 1178 | let height = ratio * (win.scrollMaxY + win.innerHeight); |
michael@0 | 1179 | let scrollTo |
michael@0 | 1180 | if (height >= win.innerHeight) { |
michael@0 | 1181 | scrollTo = -(height - win.innerHeight) * (win.scrollY / win.scrollMaxY); |
michael@0 | 1182 | this._previewBar.setAttribute("style", "height:" + height + |
michael@0 | 1183 | "px;transform:translateY(" + scrollTo + "px)"); |
michael@0 | 1184 | } else { |
michael@0 | 1185 | this._previewBar.setAttribute("style", "height:100%"); |
michael@0 | 1186 | } |
michael@0 | 1187 | |
michael@0 | 1188 | let bgSize = ~~width + "px " + ~~height + "px"; |
michael@0 | 1189 | this._preview.setAttribute("style", "background-size:" + bgSize); |
michael@0 | 1190 | |
michael@0 | 1191 | let height = ~~(win.innerHeight * ratio) + "px"; |
michael@0 | 1192 | let top = ~~(win.scrollY * ratio) + "px"; |
michael@0 | 1193 | this._viewbox.setAttribute("style", "height:" + height + |
michael@0 | 1194 | ";transform: translateY(" + top + ")"); |
michael@0 | 1195 | }, |
michael@0 | 1196 | |
michael@0 | 1197 | /** |
michael@0 | 1198 | * Hide the preview while resizing, to avoid slowness. |
michael@0 | 1199 | */ |
michael@0 | 1200 | _resizePreview: function() { |
michael@0 | 1201 | if (!this._previewEnabled) { |
michael@0 | 1202 | return; |
michael@0 | 1203 | } |
michael@0 | 1204 | let win = this._frame.contentWindow; |
michael@0 | 1205 | this._previewBar.classList.add("hide"); |
michael@0 | 1206 | win.clearTimeout(this._resizePreviewTimeout); |
michael@0 | 1207 | |
michael@0 | 1208 | win.setTimeout(function() { |
michael@0 | 1209 | this._updatePreview(); |
michael@0 | 1210 | this._previewBar.classList.remove("hide"); |
michael@0 | 1211 | }.bind(this), 1000); |
michael@0 | 1212 | } |
michael@0 | 1213 | }; |
michael@0 | 1214 | |
michael@0 | 1215 | |
michael@0 | 1216 | /** |
michael@0 | 1217 | * The main structure for storing a document node in the markup |
michael@0 | 1218 | * tree. Manages creation of the editor for the node and |
michael@0 | 1219 | * a <ul> for placing child elements, and expansion/collapsing |
michael@0 | 1220 | * of the element. |
michael@0 | 1221 | * |
michael@0 | 1222 | * @param MarkupView aMarkupView |
michael@0 | 1223 | * The markup view that owns this container. |
michael@0 | 1224 | * @param DOMNode aNode |
michael@0 | 1225 | * The node to display. |
michael@0 | 1226 | * @param Inspector aInspector |
michael@0 | 1227 | * The inspector tool container the markup-view |
michael@0 | 1228 | */ |
michael@0 | 1229 | function MarkupContainer(aMarkupView, aNode, aInspector) { |
michael@0 | 1230 | this.markup = aMarkupView; |
michael@0 | 1231 | this.doc = this.markup.doc; |
michael@0 | 1232 | this.undo = this.markup.undo; |
michael@0 | 1233 | this.node = aNode; |
michael@0 | 1234 | this._inspector = aInspector; |
michael@0 | 1235 | |
michael@0 | 1236 | if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) { |
michael@0 | 1237 | this.editor = new TextEditor(this, aNode, "text"); |
michael@0 | 1238 | } else if (aNode.nodeType == Ci.nsIDOMNode.COMMENT_NODE) { |
michael@0 | 1239 | this.editor = new TextEditor(this, aNode, "comment"); |
michael@0 | 1240 | } else if (aNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) { |
michael@0 | 1241 | this.editor = new ElementEditor(this, aNode); |
michael@0 | 1242 | } else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) { |
michael@0 | 1243 | this.editor = new DoctypeEditor(this, aNode); |
michael@0 | 1244 | } else { |
michael@0 | 1245 | this.editor = new GenericEditor(this, aNode); |
michael@0 | 1246 | } |
michael@0 | 1247 | |
michael@0 | 1248 | // The template will fill the following properties |
michael@0 | 1249 | this.elt = null; |
michael@0 | 1250 | this.expander = null; |
michael@0 | 1251 | this.tagState = null; |
michael@0 | 1252 | this.tagLine = null; |
michael@0 | 1253 | this.children = null; |
michael@0 | 1254 | this.markup.template("container", this); |
michael@0 | 1255 | this.elt.container = this; |
michael@0 | 1256 | this.children.container = this; |
michael@0 | 1257 | |
michael@0 | 1258 | // Expanding/collapsing the node on dblclick of the whole tag-line element |
michael@0 | 1259 | this._onToggle = this._onToggle.bind(this); |
michael@0 | 1260 | this.elt.addEventListener("dblclick", this._onToggle, false); |
michael@0 | 1261 | this.expander.addEventListener("click", this._onToggle, false); |
michael@0 | 1262 | |
michael@0 | 1263 | // Appending the editor element and attaching event listeners |
michael@0 | 1264 | this.tagLine.appendChild(this.editor.elt); |
michael@0 | 1265 | |
michael@0 | 1266 | this._onMouseDown = this._onMouseDown.bind(this); |
michael@0 | 1267 | this.elt.addEventListener("mousedown", this._onMouseDown, false); |
michael@0 | 1268 | |
michael@0 | 1269 | // Prepare the image preview tooltip data if any |
michael@0 | 1270 | this._prepareImagePreview(); |
michael@0 | 1271 | } |
michael@0 | 1272 | |
michael@0 | 1273 | MarkupContainer.prototype = { |
michael@0 | 1274 | toString: function() { |
michael@0 | 1275 | return "[MarkupContainer for " + this.node + "]"; |
michael@0 | 1276 | }, |
michael@0 | 1277 | |
michael@0 | 1278 | isPreviewable: function() { |
michael@0 | 1279 | if (this.node.tagName) { |
michael@0 | 1280 | let tagName = this.node.tagName.toLowerCase(); |
michael@0 | 1281 | let srcAttr = this.editor.getAttributeElement("src"); |
michael@0 | 1282 | let isImage = tagName === "img" && srcAttr; |
michael@0 | 1283 | let isCanvas = tagName === "canvas"; |
michael@0 | 1284 | |
michael@0 | 1285 | return isImage || isCanvas; |
michael@0 | 1286 | } else { |
michael@0 | 1287 | return false; |
michael@0 | 1288 | } |
michael@0 | 1289 | }, |
michael@0 | 1290 | |
michael@0 | 1291 | /** |
michael@0 | 1292 | * If the node is an image or canvas (@see isPreviewable), then get the |
michael@0 | 1293 | * image data uri from the server so that it can then later be previewed in |
michael@0 | 1294 | * a tooltip if needed. |
michael@0 | 1295 | * Stores a promise in this.tooltipData.data that resolves when the data has |
michael@0 | 1296 | * been retrieved |
michael@0 | 1297 | */ |
michael@0 | 1298 | _prepareImagePreview: function() { |
michael@0 | 1299 | if (this.isPreviewable()) { |
michael@0 | 1300 | // Get the image data for later so that when the user actually hovers over |
michael@0 | 1301 | // the element, the tooltip does contain the image |
michael@0 | 1302 | let def = promise.defer(); |
michael@0 | 1303 | |
michael@0 | 1304 | this.tooltipData = { |
michael@0 | 1305 | target: this.editor.getAttributeElement("src") || this.editor.tag, |
michael@0 | 1306 | data: def.promise |
michael@0 | 1307 | }; |
michael@0 | 1308 | |
michael@0 | 1309 | let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize"); |
michael@0 | 1310 | this.node.getImageData(maxDim).then(data => { |
michael@0 | 1311 | data.data.string().then(str => { |
michael@0 | 1312 | let res = {data: str, size: data.size}; |
michael@0 | 1313 | // Resolving the data promise and, to always keep tooltipData.data |
michael@0 | 1314 | // as a promise, create a new one that resolves immediately |
michael@0 | 1315 | def.resolve(res); |
michael@0 | 1316 | this.tooltipData.data = promise.resolve(res); |
michael@0 | 1317 | }); |
michael@0 | 1318 | }, () => { |
michael@0 | 1319 | this.tooltipData.data = promise.reject(); |
michael@0 | 1320 | }); |
michael@0 | 1321 | } |
michael@0 | 1322 | }, |
michael@0 | 1323 | |
michael@0 | 1324 | /** |
michael@0 | 1325 | * Executed by MarkupView._isImagePreviewTarget which is itself called when the |
michael@0 | 1326 | * mouse hovers over a target in the markup-view. |
michael@0 | 1327 | * Checks if the target is indeed something we want to have an image tooltip |
michael@0 | 1328 | * preview over and, if so, inserts content into the tooltip. |
michael@0 | 1329 | * @return a promise that resolves when the content has been inserted or |
michael@0 | 1330 | * rejects if no preview is required. This promise is then used by Tooltip.js |
michael@0 | 1331 | * to decide if/when to show the tooltip |
michael@0 | 1332 | */ |
michael@0 | 1333 | _isImagePreviewTarget: function(target, tooltip) { |
michael@0 | 1334 | if (!this.tooltipData || this.tooltipData.target !== target) { |
michael@0 | 1335 | return promise.reject(); |
michael@0 | 1336 | } |
michael@0 | 1337 | |
michael@0 | 1338 | return this.tooltipData.data.then(({data, size}) => { |
michael@0 | 1339 | tooltip.setImageContent(data, size); |
michael@0 | 1340 | }, () => { |
michael@0 | 1341 | tooltip.setBrokenImageContent(); |
michael@0 | 1342 | }); |
michael@0 | 1343 | }, |
michael@0 | 1344 | |
michael@0 | 1345 | copyImageDataUri: function() { |
michael@0 | 1346 | // We need to send again a request to gettooltipData even if one was sent for |
michael@0 | 1347 | // the tooltip, because we want the full-size image |
michael@0 | 1348 | this.node.getImageData().then(data => { |
michael@0 | 1349 | data.data.string().then(str => { |
michael@0 | 1350 | clipboardHelper.copyString(str, this.markup.doc); |
michael@0 | 1351 | }); |
michael@0 | 1352 | }); |
michael@0 | 1353 | }, |
michael@0 | 1354 | |
michael@0 | 1355 | /** |
michael@0 | 1356 | * True if the current node has children. The MarkupView |
michael@0 | 1357 | * will set this attribute for the MarkupContainer. |
michael@0 | 1358 | */ |
michael@0 | 1359 | _hasChildren: false, |
michael@0 | 1360 | |
michael@0 | 1361 | get hasChildren() { |
michael@0 | 1362 | return this._hasChildren; |
michael@0 | 1363 | }, |
michael@0 | 1364 | |
michael@0 | 1365 | set hasChildren(aValue) { |
michael@0 | 1366 | this._hasChildren = aValue; |
michael@0 | 1367 | if (aValue) { |
michael@0 | 1368 | this.expander.style.visibility = "visible"; |
michael@0 | 1369 | } else { |
michael@0 | 1370 | this.expander.style.visibility = "hidden"; |
michael@0 | 1371 | } |
michael@0 | 1372 | }, |
michael@0 | 1373 | |
michael@0 | 1374 | parentContainer: function() { |
michael@0 | 1375 | return this.elt.parentNode ? this.elt.parentNode.container : null; |
michael@0 | 1376 | }, |
michael@0 | 1377 | |
michael@0 | 1378 | /** |
michael@0 | 1379 | * True if the node has been visually expanded in the tree. |
michael@0 | 1380 | */ |
michael@0 | 1381 | get expanded() { |
michael@0 | 1382 | return !this.elt.classList.contains("collapsed"); |
michael@0 | 1383 | }, |
michael@0 | 1384 | |
michael@0 | 1385 | set expanded(aValue) { |
michael@0 | 1386 | if (aValue && this.elt.classList.contains("collapsed")) { |
michael@0 | 1387 | // Expanding a node means cloning its "inline" closing tag into a new |
michael@0 | 1388 | // tag-line that the user can interact with and showing the children. |
michael@0 | 1389 | if (this.editor instanceof ElementEditor) { |
michael@0 | 1390 | let closingTag = this.elt.querySelector(".close"); |
michael@0 | 1391 | if (closingTag) { |
michael@0 | 1392 | if (!this.closeTagLine) { |
michael@0 | 1393 | let line = this.markup.doc.createElement("div"); |
michael@0 | 1394 | line.classList.add("tag-line"); |
michael@0 | 1395 | |
michael@0 | 1396 | let tagState = this.markup.doc.createElement("div"); |
michael@0 | 1397 | tagState.classList.add("tag-state"); |
michael@0 | 1398 | line.appendChild(tagState); |
michael@0 | 1399 | |
michael@0 | 1400 | line.appendChild(closingTag.cloneNode(true)); |
michael@0 | 1401 | |
michael@0 | 1402 | this.closeTagLine = line; |
michael@0 | 1403 | } |
michael@0 | 1404 | this.elt.appendChild(this.closeTagLine); |
michael@0 | 1405 | } |
michael@0 | 1406 | } |
michael@0 | 1407 | this.elt.classList.remove("collapsed"); |
michael@0 | 1408 | this.expander.setAttribute("open", ""); |
michael@0 | 1409 | this.hovered = false; |
michael@0 | 1410 | } else if (!aValue) { |
michael@0 | 1411 | if (this.editor instanceof ElementEditor && this.closeTagLine) { |
michael@0 | 1412 | this.elt.removeChild(this.closeTagLine); |
michael@0 | 1413 | } |
michael@0 | 1414 | this.elt.classList.add("collapsed"); |
michael@0 | 1415 | this.expander.removeAttribute("open"); |
michael@0 | 1416 | } |
michael@0 | 1417 | }, |
michael@0 | 1418 | |
michael@0 | 1419 | _onToggle: function(event) { |
michael@0 | 1420 | this.markup.navigate(this); |
michael@0 | 1421 | if(this.hasChildren) { |
michael@0 | 1422 | this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey); |
michael@0 | 1423 | } |
michael@0 | 1424 | event.stopPropagation(); |
michael@0 | 1425 | }, |
michael@0 | 1426 | |
michael@0 | 1427 | _onMouseDown: function(event) { |
michael@0 | 1428 | let target = event.target; |
michael@0 | 1429 | |
michael@0 | 1430 | // Target may be a resource link (generated by the output-parser) |
michael@0 | 1431 | if (target.nodeName === "a") { |
michael@0 | 1432 | event.stopPropagation(); |
michael@0 | 1433 | event.preventDefault(); |
michael@0 | 1434 | let browserWin = this.markup._inspector.target |
michael@0 | 1435 | .tab.ownerDocument.defaultView; |
michael@0 | 1436 | browserWin.openUILinkIn(target.href, "tab"); |
michael@0 | 1437 | } |
michael@0 | 1438 | // Or it may be the "show more nodes" button (which already has its onclick) |
michael@0 | 1439 | // Else, it's the container itself |
michael@0 | 1440 | else if (target.nodeName !== "button") { |
michael@0 | 1441 | this.hovered = false; |
michael@0 | 1442 | this.markup.navigate(this); |
michael@0 | 1443 | event.stopPropagation(); |
michael@0 | 1444 | } |
michael@0 | 1445 | }, |
michael@0 | 1446 | |
michael@0 | 1447 | /** |
michael@0 | 1448 | * Temporarily flash the container to attract attention. |
michael@0 | 1449 | * Used for markup mutations. |
michael@0 | 1450 | */ |
michael@0 | 1451 | flashMutation: function() { |
michael@0 | 1452 | if (!this.selected) { |
michael@0 | 1453 | let contentWin = this.markup._frame.contentWindow; |
michael@0 | 1454 | this.flashed = true; |
michael@0 | 1455 | if (this._flashMutationTimer) { |
michael@0 | 1456 | contentWin.clearTimeout(this._flashMutationTimer); |
michael@0 | 1457 | this._flashMutationTimer = null; |
michael@0 | 1458 | } |
michael@0 | 1459 | this._flashMutationTimer = contentWin.setTimeout(() => { |
michael@0 | 1460 | this.flashed = false; |
michael@0 | 1461 | }, CONTAINER_FLASHING_DURATION); |
michael@0 | 1462 | } |
michael@0 | 1463 | }, |
michael@0 | 1464 | |
michael@0 | 1465 | set flashed(aValue) { |
michael@0 | 1466 | if (aValue) { |
michael@0 | 1467 | // Make sure the animation class is not here |
michael@0 | 1468 | this.tagState.classList.remove("flash-out"); |
michael@0 | 1469 | |
michael@0 | 1470 | // Change the background |
michael@0 | 1471 | this.tagState.classList.add("theme-bg-contrast"); |
michael@0 | 1472 | |
michael@0 | 1473 | // Change the text color |
michael@0 | 1474 | this.editor.elt.classList.add("theme-fg-contrast"); |
michael@0 | 1475 | [].forEach.call( |
michael@0 | 1476 | this.editor.elt.querySelectorAll("[class*=theme-fg-color]"), |
michael@0 | 1477 | span => span.classList.add("theme-fg-contrast") |
michael@0 | 1478 | ); |
michael@0 | 1479 | } else { |
michael@0 | 1480 | // Add the animation class to smoothly remove the background |
michael@0 | 1481 | this.tagState.classList.add("flash-out"); |
michael@0 | 1482 | |
michael@0 | 1483 | // Remove the background |
michael@0 | 1484 | this.tagState.classList.remove("theme-bg-contrast"); |
michael@0 | 1485 | |
michael@0 | 1486 | // Remove the text color |
michael@0 | 1487 | this.editor.elt.classList.remove("theme-fg-contrast"); |
michael@0 | 1488 | [].forEach.call( |
michael@0 | 1489 | this.editor.elt.querySelectorAll("[class*=theme-fg-color]"), |
michael@0 | 1490 | span => span.classList.remove("theme-fg-contrast") |
michael@0 | 1491 | ); |
michael@0 | 1492 | } |
michael@0 | 1493 | }, |
michael@0 | 1494 | |
michael@0 | 1495 | _hovered: false, |
michael@0 | 1496 | |
michael@0 | 1497 | /** |
michael@0 | 1498 | * Highlight the currently hovered tag + its closing tag if necessary |
michael@0 | 1499 | * (that is if the tag is expanded) |
michael@0 | 1500 | */ |
michael@0 | 1501 | set hovered(aValue) { |
michael@0 | 1502 | this.tagState.classList.remove("flash-out"); |
michael@0 | 1503 | this._hovered = aValue; |
michael@0 | 1504 | if (aValue) { |
michael@0 | 1505 | if (!this.selected) { |
michael@0 | 1506 | this.tagState.classList.add("theme-bg-darker"); |
michael@0 | 1507 | } |
michael@0 | 1508 | if (this.closeTagLine) { |
michael@0 | 1509 | this.closeTagLine.querySelector(".tag-state").classList.add( |
michael@0 | 1510 | "theme-bg-darker"); |
michael@0 | 1511 | } |
michael@0 | 1512 | } else { |
michael@0 | 1513 | this.tagState.classList.remove("theme-bg-darker"); |
michael@0 | 1514 | if (this.closeTagLine) { |
michael@0 | 1515 | this.closeTagLine.querySelector(".tag-state").classList.remove( |
michael@0 | 1516 | "theme-bg-darker"); |
michael@0 | 1517 | } |
michael@0 | 1518 | } |
michael@0 | 1519 | }, |
michael@0 | 1520 | |
michael@0 | 1521 | /** |
michael@0 | 1522 | * True if the container is visible in the markup tree. |
michael@0 | 1523 | */ |
michael@0 | 1524 | get visible() { |
michael@0 | 1525 | return this.elt.getBoundingClientRect().height > 0; |
michael@0 | 1526 | }, |
michael@0 | 1527 | |
michael@0 | 1528 | /** |
michael@0 | 1529 | * True if the container is currently selected. |
michael@0 | 1530 | */ |
michael@0 | 1531 | _selected: false, |
michael@0 | 1532 | |
michael@0 | 1533 | get selected() { |
michael@0 | 1534 | return this._selected; |
michael@0 | 1535 | }, |
michael@0 | 1536 | |
michael@0 | 1537 | set selected(aValue) { |
michael@0 | 1538 | this.tagState.classList.remove("flash-out"); |
michael@0 | 1539 | this._selected = aValue; |
michael@0 | 1540 | this.editor.selected = aValue; |
michael@0 | 1541 | if (this._selected) { |
michael@0 | 1542 | this.tagLine.setAttribute("selected", ""); |
michael@0 | 1543 | this.tagState.classList.add("theme-selected"); |
michael@0 | 1544 | } else { |
michael@0 | 1545 | this.tagLine.removeAttribute("selected"); |
michael@0 | 1546 | this.tagState.classList.remove("theme-selected"); |
michael@0 | 1547 | } |
michael@0 | 1548 | }, |
michael@0 | 1549 | |
michael@0 | 1550 | /** |
michael@0 | 1551 | * Update the container's editor to the current state of the |
michael@0 | 1552 | * viewed node. |
michael@0 | 1553 | */ |
michael@0 | 1554 | update: function() { |
michael@0 | 1555 | if (this.editor.update) { |
michael@0 | 1556 | this.editor.update(); |
michael@0 | 1557 | } |
michael@0 | 1558 | }, |
michael@0 | 1559 | |
michael@0 | 1560 | /** |
michael@0 | 1561 | * Try to put keyboard focus on the current editor. |
michael@0 | 1562 | */ |
michael@0 | 1563 | focus: function() { |
michael@0 | 1564 | let focusable = this.editor.elt.querySelector("[tabindex]"); |
michael@0 | 1565 | if (focusable) { |
michael@0 | 1566 | focusable.focus(); |
michael@0 | 1567 | } |
michael@0 | 1568 | }, |
michael@0 | 1569 | |
michael@0 | 1570 | /** |
michael@0 | 1571 | * Get rid of event listeners and references, when the container is no longer |
michael@0 | 1572 | * needed |
michael@0 | 1573 | */ |
michael@0 | 1574 | destroy: function() { |
michael@0 | 1575 | // Recursively destroy children containers |
michael@0 | 1576 | let firstChild; |
michael@0 | 1577 | while (firstChild = this.children.firstChild) { |
michael@0 | 1578 | // Not all children of a container are containers themselves |
michael@0 | 1579 | // ("show more nodes" button is one example) |
michael@0 | 1580 | if (firstChild.container) { |
michael@0 | 1581 | firstChild.container.destroy(); |
michael@0 | 1582 | } |
michael@0 | 1583 | this.children.removeChild(firstChild); |
michael@0 | 1584 | } |
michael@0 | 1585 | |
michael@0 | 1586 | // Remove event listeners |
michael@0 | 1587 | this.elt.removeEventListener("dblclick", this._onToggle, false); |
michael@0 | 1588 | this.elt.removeEventListener("mousedown", this._onMouseDown, false); |
michael@0 | 1589 | this.expander.removeEventListener("click", this._onToggle, false); |
michael@0 | 1590 | |
michael@0 | 1591 | // Destroy my editor |
michael@0 | 1592 | this.editor.destroy(); |
michael@0 | 1593 | } |
michael@0 | 1594 | }; |
michael@0 | 1595 | |
michael@0 | 1596 | |
michael@0 | 1597 | /** |
michael@0 | 1598 | * Dummy container node used for the root document element. |
michael@0 | 1599 | */ |
michael@0 | 1600 | function RootContainer(aMarkupView, aNode) { |
michael@0 | 1601 | this.doc = aMarkupView.doc; |
michael@0 | 1602 | this.elt = this.doc.createElement("ul"); |
michael@0 | 1603 | this.elt.container = this; |
michael@0 | 1604 | this.children = this.elt; |
michael@0 | 1605 | this.node = aNode; |
michael@0 | 1606 | this.toString = () => "[root container]"; |
michael@0 | 1607 | } |
michael@0 | 1608 | |
michael@0 | 1609 | RootContainer.prototype = { |
michael@0 | 1610 | hasChildren: true, |
michael@0 | 1611 | expanded: true, |
michael@0 | 1612 | update: function() {}, |
michael@0 | 1613 | destroy: function() {} |
michael@0 | 1614 | }; |
michael@0 | 1615 | |
michael@0 | 1616 | /** |
michael@0 | 1617 | * Creates an editor for simple nodes. |
michael@0 | 1618 | */ |
michael@0 | 1619 | function GenericEditor(aContainer, aNode) { |
michael@0 | 1620 | this.elt = aContainer.doc.createElement("span"); |
michael@0 | 1621 | this.elt.className = "editor"; |
michael@0 | 1622 | this.elt.textContent = aNode.nodeName; |
michael@0 | 1623 | } |
michael@0 | 1624 | |
michael@0 | 1625 | GenericEditor.prototype = { |
michael@0 | 1626 | destroy: function() {} |
michael@0 | 1627 | }; |
michael@0 | 1628 | |
michael@0 | 1629 | /** |
michael@0 | 1630 | * Creates an editor for a DOCTYPE node. |
michael@0 | 1631 | * |
michael@0 | 1632 | * @param MarkupContainer aContainer The container owning this editor. |
michael@0 | 1633 | * @param DOMNode aNode The node being edited. |
michael@0 | 1634 | */ |
michael@0 | 1635 | function DoctypeEditor(aContainer, aNode) { |
michael@0 | 1636 | this.elt = aContainer.doc.createElement("span"); |
michael@0 | 1637 | this.elt.className = "editor comment"; |
michael@0 | 1638 | this.elt.textContent = '<!DOCTYPE ' + aNode.name + |
michael@0 | 1639 | (aNode.publicId ? ' PUBLIC "' + aNode.publicId + '"': '') + |
michael@0 | 1640 | (aNode.systemId ? ' "' + aNode.systemId + '"' : '') + |
michael@0 | 1641 | '>'; |
michael@0 | 1642 | } |
michael@0 | 1643 | |
michael@0 | 1644 | DoctypeEditor.prototype = { |
michael@0 | 1645 | destroy: function() {} |
michael@0 | 1646 | }; |
michael@0 | 1647 | |
michael@0 | 1648 | /** |
michael@0 | 1649 | * Creates a simple text editor node, used for TEXT and COMMENT |
michael@0 | 1650 | * nodes. |
michael@0 | 1651 | * |
michael@0 | 1652 | * @param MarkupContainer aContainer The container owning this editor. |
michael@0 | 1653 | * @param DOMNode aNode The node being edited. |
michael@0 | 1654 | * @param string aTemplate The template id to use to build the editor. |
michael@0 | 1655 | */ |
michael@0 | 1656 | function TextEditor(aContainer, aNode, aTemplate) { |
michael@0 | 1657 | this.node = aNode; |
michael@0 | 1658 | this._selected = false; |
michael@0 | 1659 | |
michael@0 | 1660 | aContainer.markup.template(aTemplate, this); |
michael@0 | 1661 | |
michael@0 | 1662 | editableField({ |
michael@0 | 1663 | element: this.value, |
michael@0 | 1664 | stopOnReturn: true, |
michael@0 | 1665 | trigger: "dblclick", |
michael@0 | 1666 | multiline: true, |
michael@0 | 1667 | done: (aVal, aCommit) => { |
michael@0 | 1668 | if (!aCommit) { |
michael@0 | 1669 | return; |
michael@0 | 1670 | } |
michael@0 | 1671 | this.node.getNodeValue().then(longstr => { |
michael@0 | 1672 | longstr.string().then(oldValue => { |
michael@0 | 1673 | longstr.release().then(null, console.error); |
michael@0 | 1674 | |
michael@0 | 1675 | aContainer.undo.do(() => { |
michael@0 | 1676 | this.node.setNodeValue(aVal).then(() => { |
michael@0 | 1677 | aContainer.markup.nodeChanged(this.node); |
michael@0 | 1678 | }); |
michael@0 | 1679 | }, () => { |
michael@0 | 1680 | this.node.setNodeValue(oldValue).then(() => { |
michael@0 | 1681 | aContainer.markup.nodeChanged(this.node); |
michael@0 | 1682 | }) |
michael@0 | 1683 | }); |
michael@0 | 1684 | }); |
michael@0 | 1685 | }); |
michael@0 | 1686 | } |
michael@0 | 1687 | }); |
michael@0 | 1688 | |
michael@0 | 1689 | this.update(); |
michael@0 | 1690 | } |
michael@0 | 1691 | |
michael@0 | 1692 | TextEditor.prototype = { |
michael@0 | 1693 | get selected() this._selected, |
michael@0 | 1694 | set selected(aValue) { |
michael@0 | 1695 | if (aValue === this._selected) { |
michael@0 | 1696 | return; |
michael@0 | 1697 | } |
michael@0 | 1698 | this._selected = aValue; |
michael@0 | 1699 | this.update(); |
michael@0 | 1700 | }, |
michael@0 | 1701 | |
michael@0 | 1702 | update: function() { |
michael@0 | 1703 | if (!this.selected || !this.node.incompleteValue) { |
michael@0 | 1704 | let text = this.node.shortValue; |
michael@0 | 1705 | // XXX: internationalize the elliding |
michael@0 | 1706 | if (this.node.incompleteValue) { |
michael@0 | 1707 | text += "…"; |
michael@0 | 1708 | } |
michael@0 | 1709 | this.value.textContent = text; |
michael@0 | 1710 | } else { |
michael@0 | 1711 | let longstr = null; |
michael@0 | 1712 | this.node.getNodeValue().then(ret => { |
michael@0 | 1713 | longstr = ret; |
michael@0 | 1714 | return longstr.string(); |
michael@0 | 1715 | }).then(str => { |
michael@0 | 1716 | longstr.release().then(null, console.error); |
michael@0 | 1717 | if (this.selected) { |
michael@0 | 1718 | this.value.textContent = str; |
michael@0 | 1719 | } |
michael@0 | 1720 | }).then(null, console.error); |
michael@0 | 1721 | } |
michael@0 | 1722 | }, |
michael@0 | 1723 | |
michael@0 | 1724 | destroy: function() {} |
michael@0 | 1725 | }; |
michael@0 | 1726 | |
michael@0 | 1727 | /** |
michael@0 | 1728 | * Creates an editor for an Element node. |
michael@0 | 1729 | * |
michael@0 | 1730 | * @param MarkupContainer aContainer The container owning this editor. |
michael@0 | 1731 | * @param Element aNode The node being edited. |
michael@0 | 1732 | */ |
michael@0 | 1733 | function ElementEditor(aContainer, aNode) { |
michael@0 | 1734 | this.doc = aContainer.doc; |
michael@0 | 1735 | this.undo = aContainer.undo; |
michael@0 | 1736 | this.template = aContainer.markup.template.bind(aContainer.markup); |
michael@0 | 1737 | this.container = aContainer; |
michael@0 | 1738 | this.markup = this.container.markup; |
michael@0 | 1739 | this.node = aNode; |
michael@0 | 1740 | |
michael@0 | 1741 | this.attrs = {}; |
michael@0 | 1742 | |
michael@0 | 1743 | // The templates will fill the following properties |
michael@0 | 1744 | this.elt = null; |
michael@0 | 1745 | this.tag = null; |
michael@0 | 1746 | this.closeTag = null; |
michael@0 | 1747 | this.attrList = null; |
michael@0 | 1748 | this.newAttr = null; |
michael@0 | 1749 | this.closeElt = null; |
michael@0 | 1750 | |
michael@0 | 1751 | // Create the main editor |
michael@0 | 1752 | this.template("element", this); |
michael@0 | 1753 | |
michael@0 | 1754 | if (aNode.isLocal_toBeDeprecated()) { |
michael@0 | 1755 | this.rawNode = aNode.rawNode(); |
michael@0 | 1756 | } |
michael@0 | 1757 | |
michael@0 | 1758 | // Make the tag name editable (unless this is a remote node or |
michael@0 | 1759 | // a document element) |
michael@0 | 1760 | if (this.rawNode && !aNode.isDocumentElement) { |
michael@0 | 1761 | this.tag.setAttribute("tabindex", "0"); |
michael@0 | 1762 | editableField({ |
michael@0 | 1763 | element: this.tag, |
michael@0 | 1764 | trigger: "dblclick", |
michael@0 | 1765 | stopOnReturn: true, |
michael@0 | 1766 | done: this.onTagEdit.bind(this), |
michael@0 | 1767 | }); |
michael@0 | 1768 | } |
michael@0 | 1769 | |
michael@0 | 1770 | // Make the new attribute space editable. |
michael@0 | 1771 | editableField({ |
michael@0 | 1772 | element: this.newAttr, |
michael@0 | 1773 | trigger: "dblclick", |
michael@0 | 1774 | stopOnReturn: true, |
michael@0 | 1775 | contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED, |
michael@0 | 1776 | popup: this.markup.popup, |
michael@0 | 1777 | done: (aVal, aCommit) => { |
michael@0 | 1778 | if (!aCommit) { |
michael@0 | 1779 | return; |
michael@0 | 1780 | } |
michael@0 | 1781 | |
michael@0 | 1782 | try { |
michael@0 | 1783 | let doMods = this._startModifyingAttributes(); |
michael@0 | 1784 | let undoMods = this._startModifyingAttributes(); |
michael@0 | 1785 | this._applyAttributes(aVal, null, doMods, undoMods); |
michael@0 | 1786 | this.undo.do(() => { |
michael@0 | 1787 | doMods.apply(); |
michael@0 | 1788 | }, function() { |
michael@0 | 1789 | undoMods.apply(); |
michael@0 | 1790 | }); |
michael@0 | 1791 | } catch(x) { |
michael@0 | 1792 | console.error(x); |
michael@0 | 1793 | } |
michael@0 | 1794 | } |
michael@0 | 1795 | }); |
michael@0 | 1796 | |
michael@0 | 1797 | let tagName = this.node.nodeName.toLowerCase(); |
michael@0 | 1798 | this.tag.textContent = tagName; |
michael@0 | 1799 | this.closeTag.textContent = tagName; |
michael@0 | 1800 | |
michael@0 | 1801 | this.update(); |
michael@0 | 1802 | } |
michael@0 | 1803 | |
michael@0 | 1804 | ElementEditor.prototype = { |
michael@0 | 1805 | /** |
michael@0 | 1806 | * Update the state of the editor from the node. |
michael@0 | 1807 | */ |
michael@0 | 1808 | update: function() { |
michael@0 | 1809 | let attrs = this.node.attributes; |
michael@0 | 1810 | if (!attrs) { |
michael@0 | 1811 | return; |
michael@0 | 1812 | } |
michael@0 | 1813 | |
michael@0 | 1814 | // Hide all the attribute editors, they'll be re-shown if they're |
michael@0 | 1815 | // still applicable. Don't update attributes that are being |
michael@0 | 1816 | // actively edited. |
michael@0 | 1817 | let attrEditors = this.attrList.querySelectorAll(".attreditor"); |
michael@0 | 1818 | for (let i = 0; i < attrEditors.length; i++) { |
michael@0 | 1819 | if (!attrEditors[i].inplaceEditor) { |
michael@0 | 1820 | attrEditors[i].style.display = "none"; |
michael@0 | 1821 | } |
michael@0 | 1822 | } |
michael@0 | 1823 | |
michael@0 | 1824 | // Get the attribute editor for each attribute that exists on |
michael@0 | 1825 | // the node and show it. |
michael@0 | 1826 | for (let attr of attrs) { |
michael@0 | 1827 | let attribute = this._createAttribute(attr); |
michael@0 | 1828 | if (!attribute.inplaceEditor) { |
michael@0 | 1829 | attribute.style.removeProperty("display"); |
michael@0 | 1830 | } |
michael@0 | 1831 | } |
michael@0 | 1832 | }, |
michael@0 | 1833 | |
michael@0 | 1834 | _startModifyingAttributes: function() { |
michael@0 | 1835 | return this.node.startModifyingAttributes(); |
michael@0 | 1836 | }, |
michael@0 | 1837 | |
michael@0 | 1838 | /** |
michael@0 | 1839 | * Get the element used for one of the attributes of this element |
michael@0 | 1840 | * @param string attrName The name of the attribute to get the element for |
michael@0 | 1841 | * @return DOMElement |
michael@0 | 1842 | */ |
michael@0 | 1843 | getAttributeElement: function(attrName) { |
michael@0 | 1844 | return this.attrList.querySelector( |
michael@0 | 1845 | ".attreditor[data-attr=" + attrName + "] .attr-value"); |
michael@0 | 1846 | }, |
michael@0 | 1847 | |
michael@0 | 1848 | _createAttribute: function(aAttr, aBefore = null) { |
michael@0 | 1849 | // Create the template editor, which will save some variables here. |
michael@0 | 1850 | let data = { |
michael@0 | 1851 | attrName: aAttr.name, |
michael@0 | 1852 | }; |
michael@0 | 1853 | this.template("attribute", data); |
michael@0 | 1854 | var {attr, inner, name, val} = data; |
michael@0 | 1855 | |
michael@0 | 1856 | // Double quotes need to be handled specially to prevent DOMParser failing. |
michael@0 | 1857 | // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"' |
michael@0 | 1858 | // name="v'a"l'u"e" when editing -> name="v'a"l'u"e" |
michael@0 | 1859 | let editValueDisplayed = aAttr.value || ""; |
michael@0 | 1860 | let hasDoubleQuote = editValueDisplayed.contains('"'); |
michael@0 | 1861 | let hasSingleQuote = editValueDisplayed.contains("'"); |
michael@0 | 1862 | let initial = aAttr.name + '="' + editValueDisplayed + '"'; |
michael@0 | 1863 | |
michael@0 | 1864 | // Can't just wrap value with ' since the value contains both " and '. |
michael@0 | 1865 | if (hasDoubleQuote && hasSingleQuote) { |
michael@0 | 1866 | editValueDisplayed = editValueDisplayed.replace(/\"/g, """); |
michael@0 | 1867 | initial = aAttr.name + '="' + editValueDisplayed + '"'; |
michael@0 | 1868 | } |
michael@0 | 1869 | |
michael@0 | 1870 | // Wrap with ' since there are no single quotes in the attribute value. |
michael@0 | 1871 | if (hasDoubleQuote && !hasSingleQuote) { |
michael@0 | 1872 | initial = aAttr.name + "='" + editValueDisplayed + "'"; |
michael@0 | 1873 | } |
michael@0 | 1874 | |
michael@0 | 1875 | // Make the attribute editable. |
michael@0 | 1876 | editableField({ |
michael@0 | 1877 | element: inner, |
michael@0 | 1878 | trigger: "dblclick", |
michael@0 | 1879 | stopOnReturn: true, |
michael@0 | 1880 | selectAll: false, |
michael@0 | 1881 | initial: initial, |
michael@0 | 1882 | contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED, |
michael@0 | 1883 | popup: this.markup.popup, |
michael@0 | 1884 | start: (aEditor, aEvent) => { |
michael@0 | 1885 | // If the editing was started inside the name or value areas, |
michael@0 | 1886 | // select accordingly. |
michael@0 | 1887 | if (aEvent && aEvent.target === name) { |
michael@0 | 1888 | aEditor.input.setSelectionRange(0, name.textContent.length); |
michael@0 | 1889 | } else if (aEvent && aEvent.target === val) { |
michael@0 | 1890 | let length = editValueDisplayed.length; |
michael@0 | 1891 | let editorLength = aEditor.input.value.length; |
michael@0 | 1892 | let start = editorLength - (length + 1); |
michael@0 | 1893 | aEditor.input.setSelectionRange(start, start + length); |
michael@0 | 1894 | } else { |
michael@0 | 1895 | aEditor.input.select(); |
michael@0 | 1896 | } |
michael@0 | 1897 | }, |
michael@0 | 1898 | done: (aVal, aCommit) => { |
michael@0 | 1899 | if (!aCommit || aVal === initial) { |
michael@0 | 1900 | return; |
michael@0 | 1901 | } |
michael@0 | 1902 | |
michael@0 | 1903 | let doMods = this._startModifyingAttributes(); |
michael@0 | 1904 | let undoMods = this._startModifyingAttributes(); |
michael@0 | 1905 | |
michael@0 | 1906 | // Remove the attribute stored in this editor and re-add any attributes |
michael@0 | 1907 | // parsed out of the input element. Restore original attribute if |
michael@0 | 1908 | // parsing fails. |
michael@0 | 1909 | try { |
michael@0 | 1910 | this._saveAttribute(aAttr.name, undoMods); |
michael@0 | 1911 | doMods.removeAttribute(aAttr.name); |
michael@0 | 1912 | this._applyAttributes(aVal, attr, doMods, undoMods); |
michael@0 | 1913 | this.undo.do(() => { |
michael@0 | 1914 | doMods.apply(); |
michael@0 | 1915 | }, () => { |
michael@0 | 1916 | undoMods.apply(); |
michael@0 | 1917 | }) |
michael@0 | 1918 | } catch(ex) { |
michael@0 | 1919 | console.error(ex); |
michael@0 | 1920 | } |
michael@0 | 1921 | } |
michael@0 | 1922 | }); |
michael@0 | 1923 | |
michael@0 | 1924 | // Figure out where we should place the attribute. |
michael@0 | 1925 | let before = aBefore; |
michael@0 | 1926 | if (aAttr.name == "id") { |
michael@0 | 1927 | before = this.attrList.firstChild; |
michael@0 | 1928 | } else if (aAttr.name == "class") { |
michael@0 | 1929 | let idNode = this.attrs["id"]; |
michael@0 | 1930 | before = idNode ? idNode.nextSibling : this.attrList.firstChild; |
michael@0 | 1931 | } |
michael@0 | 1932 | this.attrList.insertBefore(attr, before); |
michael@0 | 1933 | |
michael@0 | 1934 | // Remove the old version of this attribute from the DOM. |
michael@0 | 1935 | let oldAttr = this.attrs[aAttr.name]; |
michael@0 | 1936 | if (oldAttr && oldAttr.parentNode) { |
michael@0 | 1937 | oldAttr.parentNode.removeChild(oldAttr); |
michael@0 | 1938 | } |
michael@0 | 1939 | |
michael@0 | 1940 | this.attrs[aAttr.name] = attr; |
michael@0 | 1941 | |
michael@0 | 1942 | let collapsedValue; |
michael@0 | 1943 | if (aAttr.value.match(COLLAPSE_DATA_URL_REGEX)) { |
michael@0 | 1944 | collapsedValue = truncateString(aAttr.value, COLLAPSE_DATA_URL_LENGTH); |
michael@0 | 1945 | } else { |
michael@0 | 1946 | collapsedValue = truncateString(aAttr.value, COLLAPSE_ATTRIBUTE_LENGTH); |
michael@0 | 1947 | } |
michael@0 | 1948 | |
michael@0 | 1949 | name.textContent = aAttr.name; |
michael@0 | 1950 | val.textContent = collapsedValue; |
michael@0 | 1951 | |
michael@0 | 1952 | return attr; |
michael@0 | 1953 | }, |
michael@0 | 1954 | |
michael@0 | 1955 | /** |
michael@0 | 1956 | * Parse a user-entered attribute string and apply the resulting |
michael@0 | 1957 | * attributes to the node. This operation is undoable. |
michael@0 | 1958 | * |
michael@0 | 1959 | * @param string aValue the user-entered value. |
michael@0 | 1960 | * @param Element aAttrNode the attribute editor that created this |
michael@0 | 1961 | * set of attributes, used to place new attributes where the |
michael@0 | 1962 | * user put them. |
michael@0 | 1963 | */ |
michael@0 | 1964 | _applyAttributes: function(aValue, aAttrNode, aDoMods, aUndoMods) { |
michael@0 | 1965 | let attrs = parseAttributeValues(aValue, this.doc); |
michael@0 | 1966 | for (let attr of attrs) { |
michael@0 | 1967 | // Create an attribute editor next to the current attribute if needed. |
michael@0 | 1968 | this._createAttribute(attr, aAttrNode ? aAttrNode.nextSibling : null); |
michael@0 | 1969 | this._saveAttribute(attr.name, aUndoMods); |
michael@0 | 1970 | aDoMods.setAttribute(attr.name, attr.value); |
michael@0 | 1971 | } |
michael@0 | 1972 | }, |
michael@0 | 1973 | |
michael@0 | 1974 | /** |
michael@0 | 1975 | * Saves the current state of the given attribute into an attribute |
michael@0 | 1976 | * modification list. |
michael@0 | 1977 | */ |
michael@0 | 1978 | _saveAttribute: function(aName, aUndoMods) { |
michael@0 | 1979 | let node = this.node; |
michael@0 | 1980 | if (node.hasAttribute(aName)) { |
michael@0 | 1981 | let oldValue = node.getAttribute(aName); |
michael@0 | 1982 | aUndoMods.setAttribute(aName, oldValue); |
michael@0 | 1983 | } else { |
michael@0 | 1984 | aUndoMods.removeAttribute(aName); |
michael@0 | 1985 | } |
michael@0 | 1986 | }, |
michael@0 | 1987 | |
michael@0 | 1988 | /** |
michael@0 | 1989 | * Called when the tag name editor has is done editing. |
michael@0 | 1990 | */ |
michael@0 | 1991 | onTagEdit: function(aVal, aCommit) { |
michael@0 | 1992 | if (!aCommit || aVal == this.rawNode.tagName) { |
michael@0 | 1993 | return; |
michael@0 | 1994 | } |
michael@0 | 1995 | |
michael@0 | 1996 | // Create a new element with the same attributes as the |
michael@0 | 1997 | // current element and prepare to replace the current node |
michael@0 | 1998 | // with it. |
michael@0 | 1999 | try { |
michael@0 | 2000 | var newElt = nodeDocument(this.rawNode).createElement(aVal); |
michael@0 | 2001 | } catch(x) { |
michael@0 | 2002 | // Failed to create a new element with that tag name, ignore |
michael@0 | 2003 | // the change. |
michael@0 | 2004 | return; |
michael@0 | 2005 | } |
michael@0 | 2006 | |
michael@0 | 2007 | let attrs = this.rawNode.attributes; |
michael@0 | 2008 | |
michael@0 | 2009 | for (let i = 0 ; i < attrs.length; i++) { |
michael@0 | 2010 | newElt.setAttribute(attrs[i].name, attrs[i].value); |
michael@0 | 2011 | } |
michael@0 | 2012 | let newFront = this.markup.walker.frontForRawNode(newElt); |
michael@0 | 2013 | let newContainer = this.markup.importNode(newFront); |
michael@0 | 2014 | |
michael@0 | 2015 | // Retain the two nodes we care about here so we can undo. |
michael@0 | 2016 | let walker = this.markup.walker; |
michael@0 | 2017 | promise.all([ |
michael@0 | 2018 | walker.retainNode(newFront), walker.retainNode(this.node) |
michael@0 | 2019 | ]).then(() => { |
michael@0 | 2020 | function swapNodes(aOld, aNew) { |
michael@0 | 2021 | aOld.parentNode.insertBefore(aNew, aOld); |
michael@0 | 2022 | while (aOld.firstChild) { |
michael@0 | 2023 | aNew.appendChild(aOld.firstChild); |
michael@0 | 2024 | } |
michael@0 | 2025 | aOld.parentNode.removeChild(aOld); |
michael@0 | 2026 | } |
michael@0 | 2027 | |
michael@0 | 2028 | this.undo.do(() => { |
michael@0 | 2029 | swapNodes(this.rawNode, newElt); |
michael@0 | 2030 | this.markup.setNodeExpanded(newFront, this.container.expanded); |
michael@0 | 2031 | if (this.container.selected) { |
michael@0 | 2032 | this.markup.navigate(newContainer); |
michael@0 | 2033 | } |
michael@0 | 2034 | }, () => { |
michael@0 | 2035 | swapNodes(newElt, this.rawNode); |
michael@0 | 2036 | this.markup.setNodeExpanded(this.node, newContainer.expanded); |
michael@0 | 2037 | if (newContainer.selected) { |
michael@0 | 2038 | this.markup.navigate(this.container); |
michael@0 | 2039 | } |
michael@0 | 2040 | }); |
michael@0 | 2041 | }).then(null, console.error); |
michael@0 | 2042 | }, |
michael@0 | 2043 | |
michael@0 | 2044 | destroy: function() {} |
michael@0 | 2045 | }; |
michael@0 | 2046 | |
michael@0 | 2047 | function nodeDocument(node) { |
michael@0 | 2048 | return node.ownerDocument || |
michael@0 | 2049 | (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null); |
michael@0 | 2050 | } |
michael@0 | 2051 | |
michael@0 | 2052 | function truncateString(str, maxLength) { |
michael@0 | 2053 | if (str.length <= maxLength) { |
michael@0 | 2054 | return str; |
michael@0 | 2055 | } |
michael@0 | 2056 | |
michael@0 | 2057 | return str.substring(0, Math.ceil(maxLength / 2)) + |
michael@0 | 2058 | "…" + |
michael@0 | 2059 | str.substring(str.length - Math.floor(maxLength / 2)); |
michael@0 | 2060 | } |
michael@0 | 2061 | /** |
michael@0 | 2062 | * Parse attribute names and values from a string. |
michael@0 | 2063 | * |
michael@0 | 2064 | * @param {String} attr |
michael@0 | 2065 | * The input string for which names/values are to be parsed. |
michael@0 | 2066 | * @param {HTMLDocument} doc |
michael@0 | 2067 | * A document that can be used to test valid attributes. |
michael@0 | 2068 | * @return {Array} |
michael@0 | 2069 | * An array of attribute names and their values. |
michael@0 | 2070 | */ |
michael@0 | 2071 | function parseAttributeValues(attr, doc) { |
michael@0 | 2072 | attr = attr.trim(); |
michael@0 | 2073 | |
michael@0 | 2074 | // Handle bad user inputs by appending a " or ' if it fails to parse without them. |
michael@0 | 2075 | let el = DOMParser.parseFromString("<div " + attr + "></div>", "text/html").body.childNodes[0] || |
michael@0 | 2076 | DOMParser.parseFromString("<div " + attr + "\"></div>", "text/html").body.childNodes[0] || |
michael@0 | 2077 | DOMParser.parseFromString("<div " + attr + "'></div>", "text/html").body.childNodes[0]; |
michael@0 | 2078 | let div = doc.createElement("div"); |
michael@0 | 2079 | |
michael@0 | 2080 | let attributes = []; |
michael@0 | 2081 | for (let attribute of el.attributes) { |
michael@0 | 2082 | // Try to set on an element in the document, throws exception on bad input. |
michael@0 | 2083 | // Prevents InvalidCharacterError - "String contains an invalid character". |
michael@0 | 2084 | try { |
michael@0 | 2085 | div.setAttribute(attribute.name, attribute.value); |
michael@0 | 2086 | attributes.push({ |
michael@0 | 2087 | name: attribute.name, |
michael@0 | 2088 | value: attribute.value |
michael@0 | 2089 | }); |
michael@0 | 2090 | } |
michael@0 | 2091 | catch(e) { } |
michael@0 | 2092 | } |
michael@0 | 2093 | |
michael@0 | 2094 | // Attributes return from DOMParser in reverse order from how they are entered. |
michael@0 | 2095 | return attributes.reverse(); |
michael@0 | 2096 | } |
michael@0 | 2097 | |
michael@0 | 2098 | loader.lazyGetter(MarkupView.prototype, "strings", () => Services.strings.createBundle( |
michael@0 | 2099 | "chrome://browser/locale/devtools/inspector.properties" |
michael@0 | 2100 | )); |
michael@0 | 2101 | |
michael@0 | 2102 | XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { |
michael@0 | 2103 | return Cc["@mozilla.org/widget/clipboardhelper;1"]. |
michael@0 | 2104 | getService(Ci.nsIClipboardHelper); |
michael@0 | 2105 | }); |