browser/devtools/markupview/markup-view.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/markupview/markup-view.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,2105 @@
     1.4 +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +const {Cc, Cu, Ci} = require("chrome");
    1.11 +
    1.12 +// Page size for pageup/pagedown
    1.13 +const PAGE_SIZE = 10;
    1.14 +const PREVIEW_AREA = 700;
    1.15 +const DEFAULT_MAX_CHILDREN = 100;
    1.16 +const COLLAPSE_ATTRIBUTE_LENGTH = 120;
    1.17 +const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
    1.18 +const COLLAPSE_DATA_URL_LENGTH = 60;
    1.19 +const CONTAINER_FLASHING_DURATION = 500;
    1.20 +const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
    1.21 +
    1.22 +const {UndoStack} = require("devtools/shared/undo");
    1.23 +const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
    1.24 +const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
    1.25 +const {HTMLEditor} = require("devtools/markupview/html-editor");
    1.26 +const promise = require("devtools/toolkit/deprecated-sync-thenables");
    1.27 +const {Tooltip} = require("devtools/shared/widgets/Tooltip");
    1.28 +const EventEmitter = require("devtools/toolkit/event-emitter");
    1.29 +
    1.30 +Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
    1.31 +Cu.import("resource://gre/modules/devtools/Templater.jsm");
    1.32 +Cu.import("resource://gre/modules/Services.jsm");
    1.33 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.34 +
    1.35 +loader.lazyGetter(this, "DOMParser", function() {
    1.36 + return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
    1.37 +});
    1.38 +loader.lazyGetter(this, "AutocompletePopup", () => {
    1.39 +  return require("devtools/shared/autocomplete-popup").AutocompletePopup
    1.40 +});
    1.41 +
    1.42 +/**
    1.43 + * Vocabulary for the purposes of this file:
    1.44 + *
    1.45 + * MarkupContainer - the structure that holds an editor and its
    1.46 + *  immediate children in the markup panel.
    1.47 + * Node - A content node.
    1.48 + * object.elt - A UI element in the markup panel.
    1.49 + */
    1.50 +
    1.51 +/**
    1.52 + * The markup tree.  Manages the mapping of nodes to MarkupContainers,
    1.53 + * updating based on mutations, and the undo/redo bindings.
    1.54 + *
    1.55 + * @param Inspector aInspector
    1.56 + *        The inspector we're watching.
    1.57 + * @param iframe aFrame
    1.58 + *        An iframe in which the caller has kindly loaded markup-view.xhtml.
    1.59 + */
    1.60 +function MarkupView(aInspector, aFrame, aControllerWindow) {
    1.61 +  this._inspector = aInspector;
    1.62 +  this.walker = this._inspector.walker;
    1.63 +  this._frame = aFrame;
    1.64 +  this.doc = this._frame.contentDocument;
    1.65 +  this._elt = this.doc.querySelector("#root");
    1.66 +  this.htmlEditor = new HTMLEditor(this.doc);
    1.67 +
    1.68 +  this.layoutHelpers = new LayoutHelpers(this.doc.defaultView);
    1.69 +
    1.70 +  try {
    1.71 +    this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
    1.72 +  } catch(ex) {
    1.73 +    this.maxChildren = DEFAULT_MAX_CHILDREN;
    1.74 +  }
    1.75 +
    1.76 +  // Creating the popup to be used to show CSS suggestions.
    1.77 +  let options = {
    1.78 +    autoSelect: true,
    1.79 +    theme: "auto"
    1.80 +  };
    1.81 +  this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
    1.82 +
    1.83 +  this.undo = new UndoStack();
    1.84 +  this.undo.installController(aControllerWindow);
    1.85 +
    1.86 +  this._containers = new Map();
    1.87 +
    1.88 +  this._boundMutationObserver = this._mutationObserver.bind(this);
    1.89 +  this.walker.on("mutations", this._boundMutationObserver);
    1.90 +
    1.91 +  this._boundOnNewSelection = this._onNewSelection.bind(this);
    1.92 +  this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
    1.93 +  this._onNewSelection();
    1.94 +
    1.95 +  this._boundKeyDown = this._onKeyDown.bind(this);
    1.96 +  this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
    1.97 +
    1.98 +  this._boundFocus = this._onFocus.bind(this);
    1.99 +  this._frame.addEventListener("focus", this._boundFocus, false);
   1.100 +
   1.101 +  this._initPreview();
   1.102 +  this._initTooltips();
   1.103 +  this._initHighlighter();
   1.104 +
   1.105 +  EventEmitter.decorate(this);
   1.106 +}
   1.107 +
   1.108 +exports.MarkupView = MarkupView;
   1.109 +
   1.110 +MarkupView.prototype = {
   1.111 +  _selectedContainer: null,
   1.112 +
   1.113 +  _initTooltips: function() {
   1.114 +    this.tooltip = new Tooltip(this._inspector.panelDoc);
   1.115 +    this.tooltip.startTogglingOnHover(this._elt,
   1.116 +      this._isImagePreviewTarget.bind(this));
   1.117 +  },
   1.118 +
   1.119 +  _initHighlighter: function() {
   1.120 +    // Show the box model on markup-view mousemove
   1.121 +    this._onMouseMove = this._onMouseMove.bind(this);
   1.122 +    this._elt.addEventListener("mousemove", this._onMouseMove, false);
   1.123 +    this._onMouseLeave = this._onMouseLeave.bind(this);
   1.124 +    this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
   1.125 +
   1.126 +    // Show markup-containers as hovered on toolbox "picker-node-hovered" event
   1.127 +    // which happens when the "pick" button is pressed
   1.128 +    this._onToolboxPickerHover = (event, nodeFront) => {
   1.129 +      this.showNode(nodeFront, true).then(() => {
   1.130 +        this._showContainerAsHovered(nodeFront);
   1.131 +      });
   1.132 +    }
   1.133 +    this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
   1.134 +  },
   1.135 +
   1.136 +  _onMouseMove: function(event) {
   1.137 +    let target = event.target;
   1.138 +
   1.139 +    // Search target for a markupContainer reference, if not found, walk up
   1.140 +    while (!target.container) {
   1.141 +      if (target.tagName.toLowerCase() === "body") {
   1.142 +        return;
   1.143 +      }
   1.144 +      target = target.parentNode;
   1.145 +    }
   1.146 +
   1.147 +    let container = target.container;
   1.148 +    if (this._hoveredNode !== container.node) {
   1.149 +      if (container.node.nodeType !== Ci.nsIDOMNode.TEXT_NODE) {
   1.150 +        this._showBoxModel(container.node);
   1.151 +      } else {
   1.152 +        this._hideBoxModel();
   1.153 +      }
   1.154 +    }
   1.155 +    this._showContainerAsHovered(container.node);
   1.156 +  },
   1.157 +
   1.158 +  _hoveredNode: null,
   1.159 +  _showContainerAsHovered: function(nodeFront) {
   1.160 +    if (this._hoveredNode !== nodeFront) {
   1.161 +      if (this._hoveredNode) {
   1.162 +        this._containers.get(this._hoveredNode).hovered = false;
   1.163 +      }
   1.164 +      this._containers.get(nodeFront).hovered = true;
   1.165 +
   1.166 +      this._hoveredNode = nodeFront;
   1.167 +    }
   1.168 +  },
   1.169 +
   1.170 +  _onMouseLeave: function() {
   1.171 +    this._hideBoxModel(true);
   1.172 +    if (this._hoveredNode) {
   1.173 +      this._containers.get(this._hoveredNode).hovered = false;
   1.174 +    }
   1.175 +    this._hoveredNode = null;
   1.176 +  },
   1.177 +
   1.178 +  _showBoxModel: function(nodeFront, options={}) {
   1.179 +    this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
   1.180 +  },
   1.181 +
   1.182 +  _hideBoxModel: function(forceHide) {
   1.183 +    return this._inspector.toolbox.highlighterUtils.unhighlight(forceHide);
   1.184 +  },
   1.185 +
   1.186 +  _briefBoxModelTimer: null,
   1.187 +  _brieflyShowBoxModel: function(nodeFront, options) {
   1.188 +    let win = this._frame.contentWindow;
   1.189 +
   1.190 +    if (this._briefBoxModelTimer) {
   1.191 +      win.clearTimeout(this._briefBoxModelTimer);
   1.192 +      this._briefBoxModelTimer = null;
   1.193 +    }
   1.194 +
   1.195 +    this._showBoxModel(nodeFront, options);
   1.196 +
   1.197 +    this._briefBoxModelTimer = this._frame.contentWindow.setTimeout(() => {
   1.198 +      this._hideBoxModel();
   1.199 +    }, NEW_SELECTION_HIGHLIGHTER_TIMER);
   1.200 +  },
   1.201 +
   1.202 +  template: function(aName, aDest, aOptions={stack: "markup-view.xhtml"}) {
   1.203 +    let node = this.doc.getElementById("template-" + aName).cloneNode(true);
   1.204 +    node.removeAttribute("id");
   1.205 +    template(node, aDest, aOptions);
   1.206 +    return node;
   1.207 +  },
   1.208 +
   1.209 +  /**
   1.210 +   * Get the MarkupContainer object for a given node, or undefined if
   1.211 +   * none exists.
   1.212 +   */
   1.213 +  getContainer: function(aNode) {
   1.214 +    return this._containers.get(aNode);
   1.215 +  },
   1.216 +
   1.217 +  update: function() {
   1.218 +    let updateChildren = function(node) {
   1.219 +      this.getContainer(node).update();
   1.220 +      for (let child of node.treeChildren()) {
   1.221 +        updateChildren(child);
   1.222 +      }
   1.223 +    }.bind(this);
   1.224 +
   1.225 +    // Start with the documentElement
   1.226 +    let documentElement;
   1.227 +    for (let node of this._rootNode.treeChildren()) {
   1.228 +      if (node.isDocumentElement === true) {
   1.229 +        documentElement = node;
   1.230 +        break;
   1.231 +      }
   1.232 +    }
   1.233 +
   1.234 +    // Recursively update each node starting with documentElement.
   1.235 +    updateChildren(documentElement);
   1.236 +  },
   1.237 +
   1.238 +  /**
   1.239 +   * Executed when the mouse hovers over a target in the markup-view and is used
   1.240 +   * to decide whether this target should be used to display an image preview
   1.241 +   * tooltip.
   1.242 +   * Delegates the actual decision to the corresponding MarkupContainer instance
   1.243 +   * if one is found.
   1.244 +   * @return the promise returned by MarkupContainer._isImagePreviewTarget
   1.245 +   */
   1.246 +  _isImagePreviewTarget: function(target) {
   1.247 +    // From the target passed here, let's find the parent MarkupContainer
   1.248 +    // and ask it if the tooltip should be shown
   1.249 +    let parent = target, container;
   1.250 +    while (parent !== this.doc.body) {
   1.251 +      if (parent.container) {
   1.252 +        container = parent.container;
   1.253 +        break;
   1.254 +      }
   1.255 +      parent = parent.parentNode;
   1.256 +    }
   1.257 +
   1.258 +    if (container) {
   1.259 +      // With the newly found container, delegate the tooltip content creation
   1.260 +      // and decision to show or not the tooltip
   1.261 +      return container._isImagePreviewTarget(target, this.tooltip);
   1.262 +    }
   1.263 +  },
   1.264 +
   1.265 +  /**
   1.266 +   * Given the known reason, should the current selection be briefly highlighted
   1.267 +   * In a few cases, we don't want to highlight the node:
   1.268 +   * - If the reason is null (used to reset the selection),
   1.269 +   * - if it's "inspector-open" (when the inspector opens up, let's not highlight
   1.270 +   * the default node)
   1.271 +   * - if it's "navigateaway" (since the page is being navigated away from)
   1.272 +   * - if it's "test" (this is a special case for mochitest. In tests, we often
   1.273 +   * need to select elements but don't necessarily want the highlighter to come
   1.274 +   * and go after a delay as this might break test scenarios)
   1.275 +   * We also do not want to start a brief highlight timeout if the node is already
   1.276 +   * being hovered over, since in that case it will already be highlighted.
   1.277 +   */
   1.278 +  _shouldNewSelectionBeHighlighted: function() {
   1.279 +    let reason = this._inspector.selection.reason;
   1.280 +    let unwantedReasons = ["inspector-open", "navigateaway", "test"];
   1.281 +    let isHighlitNode = this._hoveredNode === this._inspector.selection.nodeFront;
   1.282 +    return !isHighlitNode && reason && unwantedReasons.indexOf(reason) === -1;
   1.283 +  },
   1.284 +
   1.285 +  /**
   1.286 +   * Highlight the inspector selected node.
   1.287 +   */
   1.288 +  _onNewSelection: function() {
   1.289 +    let selection = this._inspector.selection;
   1.290 +
   1.291 +    this.htmlEditor.hide();
   1.292 +    let done = this._inspector.updating("markup-view");
   1.293 +    if (selection.isNode()) {
   1.294 +      if (this._shouldNewSelectionBeHighlighted()) {
   1.295 +        this._brieflyShowBoxModel(selection.nodeFront, {});
   1.296 +      }
   1.297 +
   1.298 +      this.showNode(selection.nodeFront, true).then(() => {
   1.299 +        if (selection.reason !== "treepanel") {
   1.300 +          this.markNodeAsSelected(selection.nodeFront);
   1.301 +        }
   1.302 +        done();
   1.303 +      }, (e) => {
   1.304 +        console.error(e);
   1.305 +        done();
   1.306 +      });
   1.307 +    } else {
   1.308 +      this.unmarkSelectedNode();
   1.309 +      done();
   1.310 +    }
   1.311 +  },
   1.312 +
   1.313 +  /**
   1.314 +   * Create a TreeWalker to find the next/previous
   1.315 +   * node for selection.
   1.316 +   */
   1.317 +  _selectionWalker: function(aStart) {
   1.318 +    let walker = this.doc.createTreeWalker(
   1.319 +      aStart || this._elt,
   1.320 +      Ci.nsIDOMNodeFilter.SHOW_ELEMENT,
   1.321 +      function(aElement) {
   1.322 +        if (aElement.container &&
   1.323 +            aElement.container.elt === aElement &&
   1.324 +            aElement.container.visible) {
   1.325 +          return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
   1.326 +        }
   1.327 +        return Ci.nsIDOMNodeFilter.FILTER_SKIP;
   1.328 +      }
   1.329 +    );
   1.330 +    walker.currentNode = this._selectedContainer.elt;
   1.331 +    return walker;
   1.332 +  },
   1.333 +
   1.334 +  /**
   1.335 +   * Key handling.
   1.336 +   */
   1.337 +  _onKeyDown: function(aEvent) {
   1.338 +    let handled = true;
   1.339 +
   1.340 +    // Ignore keystrokes that originated in editors.
   1.341 +    if (aEvent.target.tagName.toLowerCase() === "input" ||
   1.342 +        aEvent.target.tagName.toLowerCase() === "textarea") {
   1.343 +      return;
   1.344 +    }
   1.345 +
   1.346 +    switch(aEvent.keyCode) {
   1.347 +      case Ci.nsIDOMKeyEvent.DOM_VK_H:
   1.348 +        let node = this._selectedContainer.node;
   1.349 +        if (node.hidden) {
   1.350 +          this.walker.unhideNode(node).then(() => this.nodeChanged(node));
   1.351 +        } else {
   1.352 +          this.walker.hideNode(node).then(() => this.nodeChanged(node));
   1.353 +        }
   1.354 +        break;
   1.355 +      case Ci.nsIDOMKeyEvent.DOM_VK_DELETE:
   1.356 +      case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE:
   1.357 +        this.deleteNode(this._selectedContainer.node);
   1.358 +        break;
   1.359 +      case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
   1.360 +        let rootContainer = this._containers.get(this._rootNode);
   1.361 +        this.navigate(rootContainer.children.firstChild.container);
   1.362 +        break;
   1.363 +      case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
   1.364 +        if (this._selectedContainer.expanded) {
   1.365 +          this.collapseNode(this._selectedContainer.node);
   1.366 +        } else {
   1.367 +          let parent = this._selectionWalker().parentNode();
   1.368 +          if (parent) {
   1.369 +            this.navigate(parent.container);
   1.370 +          }
   1.371 +        }
   1.372 +        break;
   1.373 +      case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
   1.374 +        if (!this._selectedContainer.expanded &&
   1.375 +            this._selectedContainer.hasChildren) {
   1.376 +          this._expandContainer(this._selectedContainer);
   1.377 +        } else {
   1.378 +          let next = this._selectionWalker().nextNode();
   1.379 +          if (next) {
   1.380 +            this.navigate(next.container);
   1.381 +          }
   1.382 +        }
   1.383 +        break;
   1.384 +      case Ci.nsIDOMKeyEvent.DOM_VK_UP:
   1.385 +        let prev = this._selectionWalker().previousNode();
   1.386 +        if (prev) {
   1.387 +          this.navigate(prev.container);
   1.388 +        }
   1.389 +        break;
   1.390 +      case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
   1.391 +        let next = this._selectionWalker().nextNode();
   1.392 +        if (next) {
   1.393 +          this.navigate(next.container);
   1.394 +        }
   1.395 +        break;
   1.396 +      case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: {
   1.397 +        let walker = this._selectionWalker();
   1.398 +        let selection = this._selectedContainer;
   1.399 +        for (let i = 0; i < PAGE_SIZE; i++) {
   1.400 +          let prev = walker.previousNode();
   1.401 +          if (!prev) {
   1.402 +            break;
   1.403 +          }
   1.404 +          selection = prev.container;
   1.405 +        }
   1.406 +        this.navigate(selection);
   1.407 +        break;
   1.408 +      }
   1.409 +      case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: {
   1.410 +        let walker = this._selectionWalker();
   1.411 +        let selection = this._selectedContainer;
   1.412 +        for (let i = 0; i < PAGE_SIZE; i++) {
   1.413 +          let next = walker.nextNode();
   1.414 +          if (!next) {
   1.415 +            break;
   1.416 +          }
   1.417 +          selection = next.container;
   1.418 +        }
   1.419 +        this.navigate(selection);
   1.420 +        break;
   1.421 +      }
   1.422 +      case Ci.nsIDOMKeyEvent.DOM_VK_F2: {
   1.423 +        this.beginEditingOuterHTML(this._selectedContainer.node);
   1.424 +        break;
   1.425 +      }
   1.426 +      default:
   1.427 +        handled = false;
   1.428 +    }
   1.429 +    if (handled) {
   1.430 +      aEvent.stopPropagation();
   1.431 +      aEvent.preventDefault();
   1.432 +    }
   1.433 +  },
   1.434 +
   1.435 +  /**
   1.436 +   * Delete a node from the DOM.
   1.437 +   * This is an undoable action.
   1.438 +   */
   1.439 +  deleteNode: function(aNode) {
   1.440 +    if (aNode.isDocumentElement ||
   1.441 +        aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
   1.442 +      return;
   1.443 +    }
   1.444 +
   1.445 +    let container = this._containers.get(aNode);
   1.446 +
   1.447 +    // Retain the node so we can undo this...
   1.448 +    this.walker.retainNode(aNode).then(() => {
   1.449 +      let parent = aNode.parentNode();
   1.450 +      let sibling = null;
   1.451 +      this.undo.do(() => {
   1.452 +        if (container.selected) {
   1.453 +          this.navigate(this._containers.get(parent));
   1.454 +        }
   1.455 +        this.walker.removeNode(aNode).then(nextSibling => {
   1.456 +          sibling = nextSibling;
   1.457 +        });
   1.458 +      }, () => {
   1.459 +        this.walker.insertBefore(aNode, parent, sibling);
   1.460 +      });
   1.461 +    }).then(null, console.error);
   1.462 +  },
   1.463 +
   1.464 +  /**
   1.465 +   * If an editable item is focused, select its container.
   1.466 +   */
   1.467 +  _onFocus: function(aEvent) {
   1.468 +    let parent = aEvent.target;
   1.469 +    while (!parent.container) {
   1.470 +      parent = parent.parentNode;
   1.471 +    }
   1.472 +    if (parent) {
   1.473 +      this.navigate(parent.container, true);
   1.474 +    }
   1.475 +  },
   1.476 +
   1.477 +  /**
   1.478 +   * Handle a user-requested navigation to a given MarkupContainer,
   1.479 +   * updating the inspector's currently-selected node.
   1.480 +   *
   1.481 +   * @param MarkupContainer aContainer
   1.482 +   *        The container we're navigating to.
   1.483 +   * @param aIgnoreFocus aIgnoreFocus
   1.484 +   *        If falsy, keyboard focus will be moved to the container too.
   1.485 +   */
   1.486 +  navigate: function(aContainer, aIgnoreFocus) {
   1.487 +    if (!aContainer) {
   1.488 +      return;
   1.489 +    }
   1.490 +
   1.491 +    let node = aContainer.node;
   1.492 +    this.markNodeAsSelected(node, "treepanel");
   1.493 +
   1.494 +    if (!aIgnoreFocus) {
   1.495 +      aContainer.focus();
   1.496 +    }
   1.497 +  },
   1.498 +
   1.499 +  /**
   1.500 +   * Make sure a node is included in the markup tool.
   1.501 +   *
   1.502 +   * @param DOMNode aNode
   1.503 +   *        The node in the content document.
   1.504 +   * @param boolean aFlashNode
   1.505 +   *        Whether the newly imported node should be flashed
   1.506 +   * @returns MarkupContainer The MarkupContainer object for this element.
   1.507 +   */
   1.508 +  importNode: function(aNode, aFlashNode) {
   1.509 +    if (!aNode) {
   1.510 +      return null;
   1.511 +    }
   1.512 +
   1.513 +    if (this._containers.has(aNode)) {
   1.514 +      return this._containers.get(aNode);
   1.515 +    }
   1.516 +
   1.517 +    if (aNode === this.walker.rootNode) {
   1.518 +      var container = new RootContainer(this, aNode);
   1.519 +      this._elt.appendChild(container.elt);
   1.520 +      this._rootNode = aNode;
   1.521 +    } else {
   1.522 +      var container = new MarkupContainer(this, aNode, this._inspector);
   1.523 +      if (aFlashNode) {
   1.524 +        container.flashMutation();
   1.525 +      }
   1.526 +    }
   1.527 +
   1.528 +    this._containers.set(aNode, container);
   1.529 +    container.childrenDirty = true;
   1.530 +
   1.531 +    this._updateChildren(container);
   1.532 +
   1.533 +    return container;
   1.534 +  },
   1.535 +
   1.536 +  /**
   1.537 +   * Mutation observer used for included nodes.
   1.538 +   */
   1.539 +  _mutationObserver: function(aMutations) {
   1.540 +    let requiresLayoutChange = false;
   1.541 +    let reselectParent;
   1.542 +    let reselectChildIndex;
   1.543 +
   1.544 +    for (let mutation of aMutations) {
   1.545 +      let type = mutation.type;
   1.546 +      let target = mutation.target;
   1.547 +
   1.548 +      if (mutation.type === "documentUnload") {
   1.549 +        // Treat this as a childList change of the child (maybe the protocol
   1.550 +        // should do this).
   1.551 +        type = "childList";
   1.552 +        target = mutation.targetParent;
   1.553 +        if (!target) {
   1.554 +          continue;
   1.555 +        }
   1.556 +      }
   1.557 +
   1.558 +      let container = this._containers.get(target);
   1.559 +      if (!container) {
   1.560 +        // Container might not exist if this came from a load event for a node
   1.561 +        // we're not viewing.
   1.562 +        continue;
   1.563 +      }
   1.564 +      if (type === "attributes" || type === "characterData") {
   1.565 +        container.update();
   1.566 +
   1.567 +        // Auto refresh style properties on selected node when they change.
   1.568 +        if (type === "attributes" && container.selected) {
   1.569 +          requiresLayoutChange = true;
   1.570 +        }
   1.571 +      } else if (type === "childList") {
   1.572 +        let isFromOuterHTML = mutation.removed.some((n) => {
   1.573 +          return n === this._outerHTMLNode;
   1.574 +        });
   1.575 +
   1.576 +        // Keep track of which node should be reselected after mutations.
   1.577 +        if (isFromOuterHTML) {
   1.578 +          reselectParent = target;
   1.579 +          reselectChildIndex = this._outerHTMLChildIndex;
   1.580 +
   1.581 +          delete this._outerHTMLNode;
   1.582 +          delete this._outerHTMLChildIndex;
   1.583 +        }
   1.584 +
   1.585 +        container.childrenDirty = true;
   1.586 +        // Update the children to take care of changes in the markup view DOM.
   1.587 +        this._updateChildren(container, {flash: !isFromOuterHTML});
   1.588 +      }
   1.589 +    }
   1.590 +
   1.591 +    if (requiresLayoutChange) {
   1.592 +      this._inspector.immediateLayoutChange();
   1.593 +    }
   1.594 +    this._waitForChildren().then((nodes) => {
   1.595 +      this._flashMutatedNodes(aMutations);
   1.596 +      this._inspector.emit("markupmutation", aMutations);
   1.597 +
   1.598 +      // Since the htmlEditor is absolutely positioned, a mutation may change
   1.599 +      // the location in which it should be shown.
   1.600 +      this.htmlEditor.refresh();
   1.601 +
   1.602 +      // If a node has had its outerHTML set, the parent node will be selected.
   1.603 +      // Reselect the original node immediately.
   1.604 +      if (this._inspector.selection.nodeFront === reselectParent) {
   1.605 +        this.walker.children(reselectParent).then((o) => {
   1.606 +          let node = o.nodes[reselectChildIndex];
   1.607 +          let container = this._containers.get(node);
   1.608 +          if (node && container) {
   1.609 +            this.markNodeAsSelected(node, "outerhtml");
   1.610 +            if (container.hasChildren) {
   1.611 +              this.expandNode(node);
   1.612 +            }
   1.613 +          }
   1.614 +        });
   1.615 +
   1.616 +      }
   1.617 +    });
   1.618 +  },
   1.619 +
   1.620 +  /**
   1.621 +   * Given a list of mutations returned by the mutation observer, flash the
   1.622 +   * corresponding containers to attract attention.
   1.623 +   */
   1.624 +  _flashMutatedNodes: function(aMutations) {
   1.625 +    let addedOrEditedContainers = new Set();
   1.626 +    let removedContainers = new Set();
   1.627 +
   1.628 +    for (let {type, target, added, removed} of aMutations) {
   1.629 +      let container = this._containers.get(target);
   1.630 +
   1.631 +      if (container) {
   1.632 +        if (type === "attributes" || type === "characterData") {
   1.633 +          addedOrEditedContainers.add(container);
   1.634 +        } else if (type === "childList") {
   1.635 +          // If there has been removals, flash the parent
   1.636 +          if (removed.length) {
   1.637 +            removedContainers.add(container);
   1.638 +          }
   1.639 +
   1.640 +          // If there has been additions, flash the nodes
   1.641 +          added.forEach(added => {
   1.642 +            let addedContainer = this._containers.get(added);
   1.643 +            addedOrEditedContainers.add(addedContainer);
   1.644 +
   1.645 +            // The node may be added as a result of an append, in which case it
   1.646 +            // it will have been removed from another container first, but in
   1.647 +            // these cases we don't want to flash both the removal and the
   1.648 +            // addition
   1.649 +            removedContainers.delete(container);
   1.650 +          });
   1.651 +        }
   1.652 +      }
   1.653 +    }
   1.654 +
   1.655 +    for (let container of removedContainers) {
   1.656 +      container.flashMutation();
   1.657 +    }
   1.658 +    for (let container of addedOrEditedContainers) {
   1.659 +      container.flashMutation();
   1.660 +    }
   1.661 +  },
   1.662 +
   1.663 +  /**
   1.664 +   * Make sure the given node's parents are expanded and the
   1.665 +   * node is scrolled on to screen.
   1.666 +   */
   1.667 +  showNode: function(aNode, centered) {
   1.668 +    let parent = aNode;
   1.669 +
   1.670 +    this.importNode(aNode);
   1.671 +
   1.672 +    while ((parent = parent.parentNode())) {
   1.673 +      this.importNode(parent);
   1.674 +      this.expandNode(parent);
   1.675 +    }
   1.676 +
   1.677 +    return this._waitForChildren().then(() => {
   1.678 +      return this._ensureVisible(aNode);
   1.679 +    }).then(() => {
   1.680 +      // Why is this not working?
   1.681 +      this.layoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered);
   1.682 +    });
   1.683 +  },
   1.684 +
   1.685 +  /**
   1.686 +   * Expand the container's children.
   1.687 +   */
   1.688 +  _expandContainer: function(aContainer) {
   1.689 +    return this._updateChildren(aContainer, {expand: true}).then(() => {
   1.690 +      aContainer.expanded = true;
   1.691 +    });
   1.692 +  },
   1.693 +
   1.694 +  /**
   1.695 +   * Expand the node's children.
   1.696 +   */
   1.697 +  expandNode: function(aNode) {
   1.698 +    let container = this._containers.get(aNode);
   1.699 +    this._expandContainer(container);
   1.700 +  },
   1.701 +
   1.702 +  /**
   1.703 +   * Expand the entire tree beneath a container.
   1.704 +   *
   1.705 +   * @param aContainer The container to expand.
   1.706 +   */
   1.707 +  _expandAll: function(aContainer) {
   1.708 +    return this._expandContainer(aContainer).then(() => {
   1.709 +      let child = aContainer.children.firstChild;
   1.710 +      let promises = [];
   1.711 +      while (child) {
   1.712 +        promises.push(this._expandAll(child.container));
   1.713 +        child = child.nextSibling;
   1.714 +      }
   1.715 +      return promise.all(promises);
   1.716 +    }).then(null, console.error);
   1.717 +  },
   1.718 +
   1.719 +  /**
   1.720 +   * Expand the entire tree beneath a node.
   1.721 +   *
   1.722 +   * @param aContainer The node to expand, or null
   1.723 +   *        to start from the top.
   1.724 +   */
   1.725 +  expandAll: function(aNode) {
   1.726 +    aNode = aNode || this._rootNode;
   1.727 +    return this._expandAll(this._containers.get(aNode));
   1.728 +  },
   1.729 +
   1.730 +  /**
   1.731 +   * Collapse the node's children.
   1.732 +   */
   1.733 +  collapseNode: function(aNode) {
   1.734 +    let container = this._containers.get(aNode);
   1.735 +    container.expanded = false;
   1.736 +  },
   1.737 +
   1.738 +  /**
   1.739 +   * Retrieve the outerHTML for a remote node.
   1.740 +   * @param aNode The NodeFront to get the outerHTML for.
   1.741 +   * @returns A promise that will be resolved with the outerHTML.
   1.742 +   */
   1.743 +  getNodeOuterHTML: function(aNode) {
   1.744 +    let def = promise.defer();
   1.745 +    this.walker.outerHTML(aNode).then(longstr => {
   1.746 +      longstr.string().then(outerHTML => {
   1.747 +        longstr.release().then(null, console.error);
   1.748 +        def.resolve(outerHTML);
   1.749 +      });
   1.750 +    });
   1.751 +    return def.promise;
   1.752 +  },
   1.753 +
   1.754 +  /**
   1.755 +   * Retrieve the index of a child within its parent's children list.
   1.756 +   * @param aNode The NodeFront to find the index of.
   1.757 +   * @returns A promise that will be resolved with the integer index.
   1.758 +   *          If the child cannot be found, returns -1
   1.759 +   */
   1.760 +  getNodeChildIndex: function(aNode) {
   1.761 +    let def = promise.defer();
   1.762 +    let parentNode = aNode.parentNode();
   1.763 +
   1.764 +    // Node may have been removed from the DOM, instead of throwing an error,
   1.765 +    // return -1 indicating that it isn't inside of its parent children list.
   1.766 +    if (!parentNode) {
   1.767 +      def.resolve(-1);
   1.768 +    } else {
   1.769 +      this.walker.children(parentNode).then(children => {
   1.770 +        def.resolve(children.nodes.indexOf(aNode));
   1.771 +      });
   1.772 +    }
   1.773 +
   1.774 +    return def.promise;
   1.775 +  },
   1.776 +
   1.777 +  /**
   1.778 +   * Retrieve the index of a child within its parent's children collection.
   1.779 +   * @param aNode The NodeFront to find the index of.
   1.780 +   * @param newValue The new outerHTML to set on the node.
   1.781 +   * @param oldValue The old outerHTML that will be reverted to find the index of.
   1.782 +   * @returns A promise that will be resolved with the integer index.
   1.783 +   *          If the child cannot be found, returns -1
   1.784 +   */
   1.785 +  updateNodeOuterHTML: function(aNode, newValue, oldValue) {
   1.786 +    let container = this._containers.get(aNode);
   1.787 +    if (!container) {
   1.788 +      return;
   1.789 +    }
   1.790 +
   1.791 +    this.getNodeChildIndex(aNode).then((i) => {
   1.792 +      this._outerHTMLChildIndex = i;
   1.793 +      this._outerHTMLNode = aNode;
   1.794 +
   1.795 +      container.undo.do(() => {
   1.796 +        this.walker.setOuterHTML(aNode, newValue);
   1.797 +      }, () => {
   1.798 +        this.walker.setOuterHTML(aNode, oldValue);
   1.799 +      });
   1.800 +    });
   1.801 +  },
   1.802 +
   1.803 +  /**
   1.804 +   * Open an editor in the UI to allow editing of a node's outerHTML.
   1.805 +   * @param aNode The NodeFront to edit.
   1.806 +   */
   1.807 +  beginEditingOuterHTML: function(aNode) {
   1.808 +    this.getNodeOuterHTML(aNode).then((oldValue)=> {
   1.809 +      let container = this._containers.get(aNode);
   1.810 +      if (!container) {
   1.811 +        return;
   1.812 +      }
   1.813 +      this.htmlEditor.show(container.tagLine, oldValue);
   1.814 +      this.htmlEditor.once("popuphidden", (e, aCommit, aValue) => {
   1.815 +        // Need to focus the <html> element instead of the frame / window
   1.816 +        // in order to give keyboard focus back to doc (from editor).
   1.817 +        this._frame.contentDocument.documentElement.focus();
   1.818 +
   1.819 +        if (aCommit) {
   1.820 +          this.updateNodeOuterHTML(aNode, aValue, oldValue);
   1.821 +        }
   1.822 +      });
   1.823 +    });
   1.824 +  },
   1.825 +
   1.826 +  /**
   1.827 +   * Mark the given node expanded.
   1.828 +   * @param {NodeFront} aNode The NodeFront to mark as expanded.
   1.829 +   * @param {Boolean} aExpanded Whether the expand or collapse.
   1.830 +   * @param {Boolean} aExpandDescendants Whether to expand all descendants too
   1.831 +   */
   1.832 +  setNodeExpanded: function(aNode, aExpanded, aExpandDescendants) {
   1.833 +    if (aExpanded) {
   1.834 +      if (aExpandDescendants) {
   1.835 +        this.expandAll(aNode);
   1.836 +      } else {
   1.837 +        this.expandNode(aNode);
   1.838 +      }
   1.839 +    } else {
   1.840 +      this.collapseNode(aNode);
   1.841 +    }
   1.842 +  },
   1.843 +
   1.844 +  /**
   1.845 +   * Mark the given node selected, and update the inspector.selection
   1.846 +   * object's NodeFront to keep consistent state between UI and selection.
   1.847 +   * @param aNode The NodeFront to mark as selected.
   1.848 +   */
   1.849 +  markNodeAsSelected: function(aNode, reason) {
   1.850 +    let container = this._containers.get(aNode);
   1.851 +    if (this._selectedContainer === container) {
   1.852 +      return false;
   1.853 +    }
   1.854 +    if (this._selectedContainer) {
   1.855 +      this._selectedContainer.selected = false;
   1.856 +    }
   1.857 +    this._selectedContainer = container;
   1.858 +    if (aNode) {
   1.859 +      this._selectedContainer.selected = true;
   1.860 +    }
   1.861 +
   1.862 +    this._inspector.selection.setNodeFront(aNode, reason || "nodeselected");
   1.863 +    return true;
   1.864 +  },
   1.865 +
   1.866 +  /**
   1.867 +   * Make sure that every ancestor of the selection are updated
   1.868 +   * and included in the list of visible children.
   1.869 +   */
   1.870 +  _ensureVisible: function(node) {
   1.871 +    while (node) {
   1.872 +      let container = this._containers.get(node);
   1.873 +      let parent = node.parentNode();
   1.874 +      if (!container.elt.parentNode) {
   1.875 +        let parentContainer = this._containers.get(parent);
   1.876 +        if (parentContainer) {
   1.877 +          parentContainer.childrenDirty = true;
   1.878 +          this._updateChildren(parentContainer, {expand: node});
   1.879 +        }
   1.880 +      }
   1.881 +
   1.882 +      node = parent;
   1.883 +    }
   1.884 +    return this._waitForChildren();
   1.885 +  },
   1.886 +
   1.887 +  /**
   1.888 +   * Unmark selected node (no node selected).
   1.889 +   */
   1.890 +  unmarkSelectedNode: function() {
   1.891 +    if (this._selectedContainer) {
   1.892 +      this._selectedContainer.selected = false;
   1.893 +      this._selectedContainer = null;
   1.894 +    }
   1.895 +  },
   1.896 +
   1.897 +  /**
   1.898 +   * Called when the markup panel initiates a change on a node.
   1.899 +   */
   1.900 +  nodeChanged: function(aNode) {
   1.901 +    if (aNode === this._inspector.selection.nodeFront) {
   1.902 +      this._inspector.change("markupview");
   1.903 +    }
   1.904 +  },
   1.905 +
   1.906 +  /**
   1.907 +   * Check if the current selection is a descendent of the container.
   1.908 +   * if so, make sure it's among the visible set for the container,
   1.909 +   * and set the dirty flag if needed.
   1.910 +   * @returns The node that should be made visible, if any.
   1.911 +   */
   1.912 +  _checkSelectionVisible: function(aContainer) {
   1.913 +    let centered = null;
   1.914 +    let node = this._inspector.selection.nodeFront;
   1.915 +    while (node) {
   1.916 +      if (node.parentNode() === aContainer.node) {
   1.917 +        centered = node;
   1.918 +        break;
   1.919 +      }
   1.920 +      node = node.parentNode();
   1.921 +    }
   1.922 +
   1.923 +    return centered;
   1.924 +  },
   1.925 +
   1.926 +  /**
   1.927 +   * Make sure all children of the given container's node are
   1.928 +   * imported and attached to the container in the right order.
   1.929 +   *
   1.930 +   * Children need to be updated only in the following circumstances:
   1.931 +   * a) We just imported this node and have never seen its children.
   1.932 +   *    container.childrenDirty will be set by importNode in this case.
   1.933 +   * b) We received a childList mutation on the node.
   1.934 +   *    container.childrenDirty will be set in that case too.
   1.935 +   * c) We have changed the selection, and the path to that selection
   1.936 +   *    wasn't loaded in a previous children request (because we only
   1.937 +   *    grab a subset).
   1.938 +   *    container.childrenDirty should be set in that case too!
   1.939 +   *
   1.940 +   * @param MarkupContainer aContainer
   1.941 +   *        The markup container whose children need updating
   1.942 +   * @param Object options
   1.943 +   *        Options are {expand:boolean,flash:boolean}
   1.944 +   * @return a promise that will be resolved when the children are ready
   1.945 +   * (which may be immediately).
   1.946 +   */
   1.947 +  _updateChildren: function(aContainer, options) {
   1.948 +    let expand = options && options.expand;
   1.949 +    let flash = options && options.flash;
   1.950 +
   1.951 +    aContainer.hasChildren = aContainer.node.hasChildren;
   1.952 +
   1.953 +    if (!this._queuedChildUpdates) {
   1.954 +      this._queuedChildUpdates = new Map();
   1.955 +    }
   1.956 +
   1.957 +    if (this._queuedChildUpdates.has(aContainer)) {
   1.958 +      return this._queuedChildUpdates.get(aContainer);
   1.959 +    }
   1.960 +
   1.961 +    if (!aContainer.childrenDirty) {
   1.962 +      return promise.resolve(aContainer);
   1.963 +    }
   1.964 +
   1.965 +    if (!aContainer.hasChildren) {
   1.966 +      while (aContainer.children.firstChild) {
   1.967 +        aContainer.children.removeChild(aContainer.children.firstChild);
   1.968 +      }
   1.969 +      aContainer.childrenDirty = false;
   1.970 +      return promise.resolve(aContainer);
   1.971 +    }
   1.972 +
   1.973 +    // If we're not expanded (or asked to update anyway), we're done for
   1.974 +    // now.  Note that this will leave the childrenDirty flag set, so when
   1.975 +    // expanded we'll refresh the child list.
   1.976 +    if (!(aContainer.expanded || expand)) {
   1.977 +      return promise.resolve(aContainer);
   1.978 +    }
   1.979 +
   1.980 +    // We're going to issue a children request, make sure it includes the
   1.981 +    // centered node.
   1.982 +    let centered = this._checkSelectionVisible(aContainer);
   1.983 +
   1.984 +    // Children aren't updated yet, but clear the childrenDirty flag anyway.
   1.985 +    // If the dirty flag is re-set while we're fetching we'll need to fetch
   1.986 +    // again.
   1.987 +    aContainer.childrenDirty = false;
   1.988 +    let updatePromise = this._getVisibleChildren(aContainer, centered).then(children => {
   1.989 +      if (!this._containers) {
   1.990 +        return promise.reject("markup view destroyed");
   1.991 +      }
   1.992 +      this._queuedChildUpdates.delete(aContainer);
   1.993 +
   1.994 +      // If children are dirty, we got a change notification for this node
   1.995 +      // while the request was in progress, we need to do it again.
   1.996 +      if (aContainer.childrenDirty) {
   1.997 +        return this._updateChildren(aContainer, {expand: centered});
   1.998 +      }
   1.999 +
  1.1000 +      let fragment = this.doc.createDocumentFragment();
  1.1001 +
  1.1002 +      for (let child of children.nodes) {
  1.1003 +        let container = this.importNode(child, flash);
  1.1004 +        fragment.appendChild(container.elt);
  1.1005 +      }
  1.1006 +
  1.1007 +      while (aContainer.children.firstChild) {
  1.1008 +        aContainer.children.removeChild(aContainer.children.firstChild);
  1.1009 +      }
  1.1010 +
  1.1011 +      if (!(children.hasFirst && children.hasLast)) {
  1.1012 +        let data = {
  1.1013 +          showing: this.strings.GetStringFromName("markupView.more.showing"),
  1.1014 +          showAll: this.strings.formatStringFromName(
  1.1015 +                    "markupView.more.showAll",
  1.1016 +                    [aContainer.node.numChildren.toString()], 1),
  1.1017 +          allButtonClick: () => {
  1.1018 +            aContainer.maxChildren = -1;
  1.1019 +            aContainer.childrenDirty = true;
  1.1020 +            this._updateChildren(aContainer);
  1.1021 +          }
  1.1022 +        };
  1.1023 +
  1.1024 +        if (!children.hasFirst) {
  1.1025 +          let span = this.template("more-nodes", data);
  1.1026 +          fragment.insertBefore(span, fragment.firstChild);
  1.1027 +        }
  1.1028 +        if (!children.hasLast) {
  1.1029 +          let span = this.template("more-nodes", data);
  1.1030 +          fragment.appendChild(span);
  1.1031 +        }
  1.1032 +      }
  1.1033 +
  1.1034 +      aContainer.children.appendChild(fragment);
  1.1035 +      return aContainer;
  1.1036 +    }).then(null, console.error);
  1.1037 +    this._queuedChildUpdates.set(aContainer, updatePromise);
  1.1038 +    return updatePromise;
  1.1039 +  },
  1.1040 +
  1.1041 +  _waitForChildren: function() {
  1.1042 +    if (!this._queuedChildUpdates) {
  1.1043 +      return promise.resolve(undefined);
  1.1044 +    }
  1.1045 +    return promise.all([updatePromise for (updatePromise of this._queuedChildUpdates.values())]);
  1.1046 +  },
  1.1047 +
  1.1048 +  /**
  1.1049 +   * Return a list of the children to display for this container.
  1.1050 +   */
  1.1051 +  _getVisibleChildren: function(aContainer, aCentered) {
  1.1052 +    let maxChildren = aContainer.maxChildren || this.maxChildren;
  1.1053 +    if (maxChildren == -1) {
  1.1054 +      maxChildren = undefined;
  1.1055 +    }
  1.1056 +
  1.1057 +    return this.walker.children(aContainer.node, {
  1.1058 +      maxNodes: maxChildren,
  1.1059 +      center: aCentered
  1.1060 +    });
  1.1061 +  },
  1.1062 +
  1.1063 +  /**
  1.1064 +   * Tear down the markup panel.
  1.1065 +   */
  1.1066 +  destroy: function() {
  1.1067 +    if (this._destroyer) {
  1.1068 +      return this._destroyer;
  1.1069 +    }
  1.1070 +
  1.1071 +    // Note that if the toolbox is closed, this will work fine, but will fail
  1.1072 +    // in case the browser is closed and will trigger a noSuchActor message.
  1.1073 +    this._destroyer = this._hideBoxModel();
  1.1074 +
  1.1075 +    this._hoveredNode = null;
  1.1076 +    this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover);
  1.1077 +
  1.1078 +    this.htmlEditor.destroy();
  1.1079 +    this.htmlEditor = null;
  1.1080 +
  1.1081 +    this.undo.destroy();
  1.1082 +    this.undo = null;
  1.1083 +
  1.1084 +    this.popup.destroy();
  1.1085 +    this.popup = null;
  1.1086 +
  1.1087 +    this._frame.removeEventListener("focus", this._boundFocus, false);
  1.1088 +    this._boundFocus = null;
  1.1089 +
  1.1090 +    if (this._boundUpdatePreview) {
  1.1091 +      this._frame.contentWindow.removeEventListener("scroll",
  1.1092 +        this._boundUpdatePreview, true);
  1.1093 +      this._boundUpdatePreview = null;
  1.1094 +    }
  1.1095 +
  1.1096 +    if (this._boundResizePreview) {
  1.1097 +      this._frame.contentWindow.removeEventListener("resize",
  1.1098 +        this._boundResizePreview, true);
  1.1099 +      this._frame.contentWindow.removeEventListener("overflow",
  1.1100 +        this._boundResizePreview, true);
  1.1101 +      this._frame.contentWindow.removeEventListener("underflow",
  1.1102 +        this._boundResizePreview, true);
  1.1103 +      this._boundResizePreview = null;
  1.1104 +    }
  1.1105 +
  1.1106 +    this._frame.contentWindow.removeEventListener("keydown",
  1.1107 +      this._boundKeyDown, false);
  1.1108 +    this._boundKeyDown = null;
  1.1109 +
  1.1110 +    this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
  1.1111 +    this._boundOnNewSelection = null;
  1.1112 +
  1.1113 +    this.walker.off("mutations", this._boundMutationObserver)
  1.1114 +    this._boundMutationObserver = null;
  1.1115 +
  1.1116 +    this._elt.removeEventListener("mousemove", this._onMouseMove, false);
  1.1117 +    this._elt.removeEventListener("mouseleave", this._onMouseLeave, false);
  1.1118 +    this._elt = null;
  1.1119 +
  1.1120 +    for (let [key, container] of this._containers) {
  1.1121 +      container.destroy();
  1.1122 +    }
  1.1123 +    this._containers = null;
  1.1124 +
  1.1125 +    this.tooltip.destroy();
  1.1126 +    this.tooltip = null;
  1.1127 +
  1.1128 +    return this._destroyer;
  1.1129 +  },
  1.1130 +
  1.1131 +  /**
  1.1132 +   * Initialize the preview panel.
  1.1133 +   */
  1.1134 +  _initPreview: function() {
  1.1135 +    this._previewEnabled = Services.prefs.getBoolPref("devtools.inspector.markupPreview");
  1.1136 +    if (!this._previewEnabled) {
  1.1137 +      return;
  1.1138 +    }
  1.1139 +
  1.1140 +    this._previewBar = this.doc.querySelector("#previewbar");
  1.1141 +    this._preview = this.doc.querySelector("#preview");
  1.1142 +    this._viewbox = this.doc.querySelector("#viewbox");
  1.1143 +
  1.1144 +    this._previewBar.classList.remove("disabled");
  1.1145 +
  1.1146 +    this._previewWidth = this._preview.getBoundingClientRect().width;
  1.1147 +
  1.1148 +    this._boundResizePreview = this._resizePreview.bind(this);
  1.1149 +    this._frame.contentWindow.addEventListener("resize",
  1.1150 +      this._boundResizePreview, true);
  1.1151 +    this._frame.contentWindow.addEventListener("overflow",
  1.1152 +      this._boundResizePreview, true);
  1.1153 +    this._frame.contentWindow.addEventListener("underflow",
  1.1154 +      this._boundResizePreview, true);
  1.1155 +
  1.1156 +    this._boundUpdatePreview = this._updatePreview.bind(this);
  1.1157 +    this._frame.contentWindow.addEventListener("scroll",
  1.1158 +      this._boundUpdatePreview, true);
  1.1159 +    this._updatePreview();
  1.1160 +  },
  1.1161 +
  1.1162 +  /**
  1.1163 +   * Move the preview viewbox.
  1.1164 +   */
  1.1165 +  _updatePreview: function() {
  1.1166 +    if (!this._previewEnabled) {
  1.1167 +      return;
  1.1168 +    }
  1.1169 +    let win = this._frame.contentWindow;
  1.1170 +
  1.1171 +    if (win.scrollMaxY == 0) {
  1.1172 +      this._previewBar.classList.add("disabled");
  1.1173 +      return;
  1.1174 +    }
  1.1175 +
  1.1176 +    this._previewBar.classList.remove("disabled");
  1.1177 +
  1.1178 +    let ratio = this._previewWidth / PREVIEW_AREA;
  1.1179 +    let width = ratio * win.innerWidth;
  1.1180 +
  1.1181 +    let height = ratio * (win.scrollMaxY + win.innerHeight);
  1.1182 +    let scrollTo
  1.1183 +    if (height >= win.innerHeight) {
  1.1184 +      scrollTo = -(height - win.innerHeight) * (win.scrollY / win.scrollMaxY);
  1.1185 +      this._previewBar.setAttribute("style", "height:" + height +
  1.1186 +        "px;transform:translateY(" + scrollTo + "px)");
  1.1187 +    } else {
  1.1188 +      this._previewBar.setAttribute("style", "height:100%");
  1.1189 +    }
  1.1190 +
  1.1191 +    let bgSize = ~~width + "px " + ~~height + "px";
  1.1192 +    this._preview.setAttribute("style", "background-size:" + bgSize);
  1.1193 +
  1.1194 +    let height = ~~(win.innerHeight * ratio) + "px";
  1.1195 +    let top = ~~(win.scrollY * ratio) + "px";
  1.1196 +    this._viewbox.setAttribute("style", "height:" + height +
  1.1197 +      ";transform: translateY(" + top + ")");
  1.1198 +  },
  1.1199 +
  1.1200 +  /**
  1.1201 +   * Hide the preview while resizing, to avoid slowness.
  1.1202 +   */
  1.1203 +  _resizePreview: function() {
  1.1204 +    if (!this._previewEnabled) {
  1.1205 +      return;
  1.1206 +    }
  1.1207 +    let win = this._frame.contentWindow;
  1.1208 +    this._previewBar.classList.add("hide");
  1.1209 +    win.clearTimeout(this._resizePreviewTimeout);
  1.1210 +
  1.1211 +    win.setTimeout(function() {
  1.1212 +      this._updatePreview();
  1.1213 +      this._previewBar.classList.remove("hide");
  1.1214 +    }.bind(this), 1000);
  1.1215 +  }
  1.1216 +};
  1.1217 +
  1.1218 +
  1.1219 +/**
  1.1220 + * The main structure for storing a document node in the markup
  1.1221 + * tree.  Manages creation of the editor for the node and
  1.1222 + * a <ul> for placing child elements, and expansion/collapsing
  1.1223 + * of the element.
  1.1224 + *
  1.1225 + * @param MarkupView aMarkupView
  1.1226 + *        The markup view that owns this container.
  1.1227 + * @param DOMNode aNode
  1.1228 + *        The node to display.
  1.1229 + * @param Inspector aInspector
  1.1230 + *        The inspector tool container the markup-view
  1.1231 + */
  1.1232 +function MarkupContainer(aMarkupView, aNode, aInspector) {
  1.1233 +  this.markup = aMarkupView;
  1.1234 +  this.doc = this.markup.doc;
  1.1235 +  this.undo = this.markup.undo;
  1.1236 +  this.node = aNode;
  1.1237 +  this._inspector = aInspector;
  1.1238 +
  1.1239 +  if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
  1.1240 +    this.editor = new TextEditor(this, aNode, "text");
  1.1241 +  } else if (aNode.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
  1.1242 +    this.editor = new TextEditor(this, aNode, "comment");
  1.1243 +  } else if (aNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
  1.1244 +    this.editor = new ElementEditor(this, aNode);
  1.1245 +  } else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
  1.1246 +    this.editor = new DoctypeEditor(this, aNode);
  1.1247 +  } else {
  1.1248 +    this.editor = new GenericEditor(this, aNode);
  1.1249 +  }
  1.1250 +
  1.1251 +  // The template will fill the following properties
  1.1252 +  this.elt = null;
  1.1253 +  this.expander = null;
  1.1254 +  this.tagState = null;
  1.1255 +  this.tagLine = null;
  1.1256 +  this.children = null;
  1.1257 +  this.markup.template("container", this);
  1.1258 +  this.elt.container = this;
  1.1259 +  this.children.container = this;
  1.1260 +
  1.1261 +  // Expanding/collapsing the node on dblclick of the whole tag-line element
  1.1262 +  this._onToggle = this._onToggle.bind(this);
  1.1263 +  this.elt.addEventListener("dblclick", this._onToggle, false);
  1.1264 +  this.expander.addEventListener("click", this._onToggle, false);
  1.1265 +
  1.1266 +  // Appending the editor element and attaching event listeners
  1.1267 +  this.tagLine.appendChild(this.editor.elt);
  1.1268 +
  1.1269 +  this._onMouseDown = this._onMouseDown.bind(this);
  1.1270 +  this.elt.addEventListener("mousedown", this._onMouseDown, false);
  1.1271 +
  1.1272 +  // Prepare the image preview tooltip data if any
  1.1273 +  this._prepareImagePreview();
  1.1274 +}
  1.1275 +
  1.1276 +MarkupContainer.prototype = {
  1.1277 +  toString: function() {
  1.1278 +    return "[MarkupContainer for " + this.node + "]";
  1.1279 +  },
  1.1280 +
  1.1281 +  isPreviewable: function() {
  1.1282 +    if (this.node.tagName) {
  1.1283 +      let tagName = this.node.tagName.toLowerCase();
  1.1284 +      let srcAttr = this.editor.getAttributeElement("src");
  1.1285 +      let isImage = tagName === "img" && srcAttr;
  1.1286 +      let isCanvas = tagName === "canvas";
  1.1287 +
  1.1288 +      return isImage || isCanvas;
  1.1289 +    } else {
  1.1290 +      return false;
  1.1291 +    }
  1.1292 +  },
  1.1293 +
  1.1294 +  /**
  1.1295 +   * If the node is an image or canvas (@see isPreviewable), then get the
  1.1296 +   * image data uri from the server so that it can then later be previewed in
  1.1297 +   * a tooltip if needed.
  1.1298 +   * Stores a promise in this.tooltipData.data that resolves when the data has
  1.1299 +   * been retrieved
  1.1300 +   */
  1.1301 +  _prepareImagePreview: function() {
  1.1302 +    if (this.isPreviewable()) {
  1.1303 +      // Get the image data for later so that when the user actually hovers over
  1.1304 +      // the element, the tooltip does contain the image
  1.1305 +      let def = promise.defer();
  1.1306 +
  1.1307 +      this.tooltipData = {
  1.1308 +        target: this.editor.getAttributeElement("src") || this.editor.tag,
  1.1309 +        data: def.promise
  1.1310 +      };
  1.1311 +
  1.1312 +      let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
  1.1313 +      this.node.getImageData(maxDim).then(data => {
  1.1314 +        data.data.string().then(str => {
  1.1315 +          let res = {data: str, size: data.size};
  1.1316 +          // Resolving the data promise and, to always keep tooltipData.data
  1.1317 +          // as a promise, create a new one that resolves immediately
  1.1318 +          def.resolve(res);
  1.1319 +          this.tooltipData.data = promise.resolve(res);
  1.1320 +        });
  1.1321 +      }, () => {
  1.1322 +        this.tooltipData.data = promise.reject();
  1.1323 +      });
  1.1324 +    }
  1.1325 +  },
  1.1326 +
  1.1327 +  /**
  1.1328 +   * Executed by MarkupView._isImagePreviewTarget which is itself called when the
  1.1329 +   * mouse hovers over a target in the markup-view.
  1.1330 +   * Checks if the target is indeed something we want to have an image tooltip
  1.1331 +   * preview over and, if so, inserts content into the tooltip.
  1.1332 +   * @return a promise that resolves when the content has been inserted or
  1.1333 +   * rejects if no preview is required. This promise is then used by Tooltip.js
  1.1334 +   * to decide if/when to show the tooltip
  1.1335 +   */
  1.1336 +  _isImagePreviewTarget: function(target, tooltip) {
  1.1337 +    if (!this.tooltipData || this.tooltipData.target !== target) {
  1.1338 +      return promise.reject();
  1.1339 +    }
  1.1340 +
  1.1341 +    return this.tooltipData.data.then(({data, size}) => {
  1.1342 +      tooltip.setImageContent(data, size);
  1.1343 +    }, () => {
  1.1344 +      tooltip.setBrokenImageContent();
  1.1345 +    });
  1.1346 +  },
  1.1347 +
  1.1348 +  copyImageDataUri: function() {
  1.1349 +    // We need to send again a request to gettooltipData even if one was sent for
  1.1350 +    // the tooltip, because we want the full-size image
  1.1351 +    this.node.getImageData().then(data => {
  1.1352 +      data.data.string().then(str => {
  1.1353 +        clipboardHelper.copyString(str, this.markup.doc);
  1.1354 +      });
  1.1355 +    });
  1.1356 +  },
  1.1357 +
  1.1358 +  /**
  1.1359 +   * True if the current node has children.  The MarkupView
  1.1360 +   * will set this attribute for the MarkupContainer.
  1.1361 +   */
  1.1362 +  _hasChildren: false,
  1.1363 +
  1.1364 +  get hasChildren() {
  1.1365 +    return this._hasChildren;
  1.1366 +  },
  1.1367 +
  1.1368 +  set hasChildren(aValue) {
  1.1369 +    this._hasChildren = aValue;
  1.1370 +    if (aValue) {
  1.1371 +      this.expander.style.visibility = "visible";
  1.1372 +    } else {
  1.1373 +      this.expander.style.visibility = "hidden";
  1.1374 +    }
  1.1375 +  },
  1.1376 +
  1.1377 +  parentContainer: function() {
  1.1378 +    return this.elt.parentNode ? this.elt.parentNode.container : null;
  1.1379 +  },
  1.1380 +
  1.1381 +  /**
  1.1382 +   * True if the node has been visually expanded in the tree.
  1.1383 +   */
  1.1384 +  get expanded() {
  1.1385 +    return !this.elt.classList.contains("collapsed");
  1.1386 +  },
  1.1387 +
  1.1388 +  set expanded(aValue) {
  1.1389 +    if (aValue && this.elt.classList.contains("collapsed")) {
  1.1390 +      // Expanding a node means cloning its "inline" closing tag into a new
  1.1391 +      // tag-line that the user can interact with and showing the children.
  1.1392 +      if (this.editor instanceof ElementEditor) {
  1.1393 +        let closingTag = this.elt.querySelector(".close");
  1.1394 +        if (closingTag) {
  1.1395 +          if (!this.closeTagLine) {
  1.1396 +            let line = this.markup.doc.createElement("div");
  1.1397 +            line.classList.add("tag-line");
  1.1398 +
  1.1399 +            let tagState = this.markup.doc.createElement("div");
  1.1400 +            tagState.classList.add("tag-state");
  1.1401 +            line.appendChild(tagState);
  1.1402 +
  1.1403 +            line.appendChild(closingTag.cloneNode(true));
  1.1404 +
  1.1405 +            this.closeTagLine = line;
  1.1406 +          }
  1.1407 +          this.elt.appendChild(this.closeTagLine);
  1.1408 +        }
  1.1409 +      }
  1.1410 +      this.elt.classList.remove("collapsed");
  1.1411 +      this.expander.setAttribute("open", "");
  1.1412 +      this.hovered = false;
  1.1413 +    } else if (!aValue) {
  1.1414 +      if (this.editor instanceof ElementEditor && this.closeTagLine) {
  1.1415 +        this.elt.removeChild(this.closeTagLine);
  1.1416 +      }
  1.1417 +      this.elt.classList.add("collapsed");
  1.1418 +      this.expander.removeAttribute("open");
  1.1419 +    }
  1.1420 +  },
  1.1421 +
  1.1422 +  _onToggle: function(event) {
  1.1423 +    this.markup.navigate(this);
  1.1424 +    if(this.hasChildren) {
  1.1425 +      this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey);
  1.1426 +    }
  1.1427 +    event.stopPropagation();
  1.1428 +  },
  1.1429 +
  1.1430 +  _onMouseDown: function(event) {
  1.1431 +    let target = event.target;
  1.1432 +
  1.1433 +    // Target may be a resource link (generated by the output-parser)
  1.1434 +    if (target.nodeName === "a") {
  1.1435 +      event.stopPropagation();
  1.1436 +      event.preventDefault();
  1.1437 +      let browserWin = this.markup._inspector.target
  1.1438 +                           .tab.ownerDocument.defaultView;
  1.1439 +      browserWin.openUILinkIn(target.href, "tab");
  1.1440 +    }
  1.1441 +    // Or it may be the "show more nodes" button (which already has its onclick)
  1.1442 +    // Else, it's the container itself
  1.1443 +    else if (target.nodeName !== "button") {
  1.1444 +      this.hovered = false;
  1.1445 +      this.markup.navigate(this);
  1.1446 +      event.stopPropagation();
  1.1447 +    }
  1.1448 +  },
  1.1449 +
  1.1450 +  /**
  1.1451 +   * Temporarily flash the container to attract attention.
  1.1452 +   * Used for markup mutations.
  1.1453 +   */
  1.1454 +  flashMutation: function() {
  1.1455 +    if (!this.selected) {
  1.1456 +      let contentWin = this.markup._frame.contentWindow;
  1.1457 +      this.flashed = true;
  1.1458 +      if (this._flashMutationTimer) {
  1.1459 +        contentWin.clearTimeout(this._flashMutationTimer);
  1.1460 +        this._flashMutationTimer = null;
  1.1461 +      }
  1.1462 +      this._flashMutationTimer = contentWin.setTimeout(() => {
  1.1463 +        this.flashed = false;
  1.1464 +      }, CONTAINER_FLASHING_DURATION);
  1.1465 +    }
  1.1466 +  },
  1.1467 +
  1.1468 +  set flashed(aValue) {
  1.1469 +    if (aValue) {
  1.1470 +      // Make sure the animation class is not here
  1.1471 +      this.tagState.classList.remove("flash-out");
  1.1472 +
  1.1473 +      // Change the background
  1.1474 +      this.tagState.classList.add("theme-bg-contrast");
  1.1475 +
  1.1476 +      // Change the text color
  1.1477 +      this.editor.elt.classList.add("theme-fg-contrast");
  1.1478 +      [].forEach.call(
  1.1479 +        this.editor.elt.querySelectorAll("[class*=theme-fg-color]"),
  1.1480 +        span => span.classList.add("theme-fg-contrast")
  1.1481 +      );
  1.1482 +    } else {
  1.1483 +      // Add the animation class to smoothly remove the background
  1.1484 +      this.tagState.classList.add("flash-out");
  1.1485 +
  1.1486 +      // Remove the background
  1.1487 +      this.tagState.classList.remove("theme-bg-contrast");
  1.1488 +
  1.1489 +      // Remove the text color
  1.1490 +      this.editor.elt.classList.remove("theme-fg-contrast");
  1.1491 +      [].forEach.call(
  1.1492 +        this.editor.elt.querySelectorAll("[class*=theme-fg-color]"),
  1.1493 +        span => span.classList.remove("theme-fg-contrast")
  1.1494 +      );
  1.1495 +    }
  1.1496 +  },
  1.1497 +
  1.1498 +  _hovered: false,
  1.1499 +
  1.1500 +  /**
  1.1501 +   * Highlight the currently hovered tag + its closing tag if necessary
  1.1502 +   * (that is if the tag is expanded)
  1.1503 +   */
  1.1504 +  set hovered(aValue) {
  1.1505 +    this.tagState.classList.remove("flash-out");
  1.1506 +    this._hovered = aValue;
  1.1507 +    if (aValue) {
  1.1508 +      if (!this.selected) {
  1.1509 +        this.tagState.classList.add("theme-bg-darker");
  1.1510 +      }
  1.1511 +      if (this.closeTagLine) {
  1.1512 +        this.closeTagLine.querySelector(".tag-state").classList.add(
  1.1513 +          "theme-bg-darker");
  1.1514 +      }
  1.1515 +    } else {
  1.1516 +      this.tagState.classList.remove("theme-bg-darker");
  1.1517 +      if (this.closeTagLine) {
  1.1518 +        this.closeTagLine.querySelector(".tag-state").classList.remove(
  1.1519 +          "theme-bg-darker");
  1.1520 +      }
  1.1521 +    }
  1.1522 +  },
  1.1523 +
  1.1524 +  /**
  1.1525 +   * True if the container is visible in the markup tree.
  1.1526 +   */
  1.1527 +  get visible() {
  1.1528 +    return this.elt.getBoundingClientRect().height > 0;
  1.1529 +  },
  1.1530 +
  1.1531 +  /**
  1.1532 +   * True if the container is currently selected.
  1.1533 +   */
  1.1534 +  _selected: false,
  1.1535 +
  1.1536 +  get selected() {
  1.1537 +    return this._selected;
  1.1538 +  },
  1.1539 +
  1.1540 +  set selected(aValue) {
  1.1541 +    this.tagState.classList.remove("flash-out");
  1.1542 +    this._selected = aValue;
  1.1543 +    this.editor.selected = aValue;
  1.1544 +    if (this._selected) {
  1.1545 +      this.tagLine.setAttribute("selected", "");
  1.1546 +      this.tagState.classList.add("theme-selected");
  1.1547 +    } else {
  1.1548 +      this.tagLine.removeAttribute("selected");
  1.1549 +      this.tagState.classList.remove("theme-selected");
  1.1550 +    }
  1.1551 +  },
  1.1552 +
  1.1553 +  /**
  1.1554 +   * Update the container's editor to the current state of the
  1.1555 +   * viewed node.
  1.1556 +   */
  1.1557 +  update: function() {
  1.1558 +    if (this.editor.update) {
  1.1559 +      this.editor.update();
  1.1560 +    }
  1.1561 +  },
  1.1562 +
  1.1563 +  /**
  1.1564 +   * Try to put keyboard focus on the current editor.
  1.1565 +   */
  1.1566 +  focus: function() {
  1.1567 +    let focusable = this.editor.elt.querySelector("[tabindex]");
  1.1568 +    if (focusable) {
  1.1569 +      focusable.focus();
  1.1570 +    }
  1.1571 +  },
  1.1572 +
  1.1573 +  /**
  1.1574 +   * Get rid of event listeners and references, when the container is no longer
  1.1575 +   * needed
  1.1576 +   */
  1.1577 +  destroy: function() {
  1.1578 +    // Recursively destroy children containers
  1.1579 +    let firstChild;
  1.1580 +    while (firstChild = this.children.firstChild) {
  1.1581 +      // Not all children of a container are containers themselves
  1.1582 +      // ("show more nodes" button is one example)
  1.1583 +      if (firstChild.container) {
  1.1584 +        firstChild.container.destroy();
  1.1585 +      }
  1.1586 +      this.children.removeChild(firstChild);
  1.1587 +    }
  1.1588 +
  1.1589 +    // Remove event listeners
  1.1590 +    this.elt.removeEventListener("dblclick", this._onToggle, false);
  1.1591 +    this.elt.removeEventListener("mousedown", this._onMouseDown, false);
  1.1592 +    this.expander.removeEventListener("click", this._onToggle, false);
  1.1593 +
  1.1594 +    // Destroy my editor
  1.1595 +    this.editor.destroy();
  1.1596 +  }
  1.1597 +};
  1.1598 +
  1.1599 +
  1.1600 +/**
  1.1601 + * Dummy container node used for the root document element.
  1.1602 + */
  1.1603 +function RootContainer(aMarkupView, aNode) {
  1.1604 +  this.doc = aMarkupView.doc;
  1.1605 +  this.elt = this.doc.createElement("ul");
  1.1606 +  this.elt.container = this;
  1.1607 +  this.children = this.elt;
  1.1608 +  this.node = aNode;
  1.1609 +  this.toString = () => "[root container]";
  1.1610 +}
  1.1611 +
  1.1612 +RootContainer.prototype = {
  1.1613 +  hasChildren: true,
  1.1614 +  expanded: true,
  1.1615 +  update: function() {},
  1.1616 +  destroy: function() {}
  1.1617 +};
  1.1618 +
  1.1619 +/**
  1.1620 + * Creates an editor for simple nodes.
  1.1621 + */
  1.1622 +function GenericEditor(aContainer, aNode) {
  1.1623 +  this.elt = aContainer.doc.createElement("span");
  1.1624 +  this.elt.className = "editor";
  1.1625 +  this.elt.textContent = aNode.nodeName;
  1.1626 +}
  1.1627 +
  1.1628 +GenericEditor.prototype = {
  1.1629 +  destroy: function() {}
  1.1630 +};
  1.1631 +
  1.1632 +/**
  1.1633 + * Creates an editor for a DOCTYPE node.
  1.1634 + *
  1.1635 + * @param MarkupContainer aContainer The container owning this editor.
  1.1636 + * @param DOMNode aNode The node being edited.
  1.1637 + */
  1.1638 +function DoctypeEditor(aContainer, aNode) {
  1.1639 +  this.elt = aContainer.doc.createElement("span");
  1.1640 +  this.elt.className = "editor comment";
  1.1641 +  this.elt.textContent = '<!DOCTYPE ' + aNode.name +
  1.1642 +     (aNode.publicId ? ' PUBLIC "' +  aNode.publicId + '"': '') +
  1.1643 +     (aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
  1.1644 +     '>';
  1.1645 +}
  1.1646 +
  1.1647 +DoctypeEditor.prototype = {
  1.1648 +  destroy: function() {}
  1.1649 +};
  1.1650 +
  1.1651 +/**
  1.1652 + * Creates a simple text editor node, used for TEXT and COMMENT
  1.1653 + * nodes.
  1.1654 + *
  1.1655 + * @param MarkupContainer aContainer The container owning this editor.
  1.1656 + * @param DOMNode aNode The node being edited.
  1.1657 + * @param string aTemplate The template id to use to build the editor.
  1.1658 + */
  1.1659 +function TextEditor(aContainer, aNode, aTemplate) {
  1.1660 +  this.node = aNode;
  1.1661 +  this._selected = false;
  1.1662 +
  1.1663 +  aContainer.markup.template(aTemplate, this);
  1.1664 +
  1.1665 +  editableField({
  1.1666 +    element: this.value,
  1.1667 +    stopOnReturn: true,
  1.1668 +    trigger: "dblclick",
  1.1669 +    multiline: true,
  1.1670 +    done: (aVal, aCommit) => {
  1.1671 +      if (!aCommit) {
  1.1672 +        return;
  1.1673 +      }
  1.1674 +      this.node.getNodeValue().then(longstr => {
  1.1675 +        longstr.string().then(oldValue => {
  1.1676 +          longstr.release().then(null, console.error);
  1.1677 +
  1.1678 +          aContainer.undo.do(() => {
  1.1679 +            this.node.setNodeValue(aVal).then(() => {
  1.1680 +              aContainer.markup.nodeChanged(this.node);
  1.1681 +            });
  1.1682 +          }, () => {
  1.1683 +            this.node.setNodeValue(oldValue).then(() => {
  1.1684 +              aContainer.markup.nodeChanged(this.node);
  1.1685 +            })
  1.1686 +          });
  1.1687 +        });
  1.1688 +      });
  1.1689 +    }
  1.1690 +  });
  1.1691 +
  1.1692 +  this.update();
  1.1693 +}
  1.1694 +
  1.1695 +TextEditor.prototype = {
  1.1696 +  get selected() this._selected,
  1.1697 +  set selected(aValue) {
  1.1698 +    if (aValue === this._selected) {
  1.1699 +      return;
  1.1700 +    }
  1.1701 +    this._selected = aValue;
  1.1702 +    this.update();
  1.1703 +  },
  1.1704 +
  1.1705 +  update: function() {
  1.1706 +    if (!this.selected || !this.node.incompleteValue) {
  1.1707 +      let text = this.node.shortValue;
  1.1708 +      // XXX: internationalize the elliding
  1.1709 +      if (this.node.incompleteValue) {
  1.1710 +        text += "…";
  1.1711 +      }
  1.1712 +      this.value.textContent = text;
  1.1713 +    } else {
  1.1714 +      let longstr = null;
  1.1715 +      this.node.getNodeValue().then(ret => {
  1.1716 +        longstr = ret;
  1.1717 +        return longstr.string();
  1.1718 +      }).then(str => {
  1.1719 +        longstr.release().then(null, console.error);
  1.1720 +        if (this.selected) {
  1.1721 +          this.value.textContent = str;
  1.1722 +        }
  1.1723 +      }).then(null, console.error);
  1.1724 +    }
  1.1725 +  },
  1.1726 +
  1.1727 +  destroy: function() {}
  1.1728 +};
  1.1729 +
  1.1730 +/**
  1.1731 + * Creates an editor for an Element node.
  1.1732 + *
  1.1733 + * @param MarkupContainer aContainer The container owning this editor.
  1.1734 + * @param Element aNode The node being edited.
  1.1735 + */
  1.1736 +function ElementEditor(aContainer, aNode) {
  1.1737 +  this.doc = aContainer.doc;
  1.1738 +  this.undo = aContainer.undo;
  1.1739 +  this.template = aContainer.markup.template.bind(aContainer.markup);
  1.1740 +  this.container = aContainer;
  1.1741 +  this.markup = this.container.markup;
  1.1742 +  this.node = aNode;
  1.1743 +
  1.1744 +  this.attrs = {};
  1.1745 +
  1.1746 +  // The templates will fill the following properties
  1.1747 +  this.elt = null;
  1.1748 +  this.tag = null;
  1.1749 +  this.closeTag = null;
  1.1750 +  this.attrList = null;
  1.1751 +  this.newAttr = null;
  1.1752 +  this.closeElt = null;
  1.1753 +
  1.1754 +  // Create the main editor
  1.1755 +  this.template("element", this);
  1.1756 +
  1.1757 +  if (aNode.isLocal_toBeDeprecated()) {
  1.1758 +    this.rawNode = aNode.rawNode();
  1.1759 +  }
  1.1760 +
  1.1761 +  // Make the tag name editable (unless this is a remote node or
  1.1762 +  // a document element)
  1.1763 +  if (this.rawNode && !aNode.isDocumentElement) {
  1.1764 +    this.tag.setAttribute("tabindex", "0");
  1.1765 +    editableField({
  1.1766 +      element: this.tag,
  1.1767 +      trigger: "dblclick",
  1.1768 +      stopOnReturn: true,
  1.1769 +      done: this.onTagEdit.bind(this),
  1.1770 +    });
  1.1771 +  }
  1.1772 +
  1.1773 +  // Make the new attribute space editable.
  1.1774 +  editableField({
  1.1775 +    element: this.newAttr,
  1.1776 +    trigger: "dblclick",
  1.1777 +    stopOnReturn: true,
  1.1778 +    contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
  1.1779 +    popup: this.markup.popup,
  1.1780 +    done: (aVal, aCommit) => {
  1.1781 +      if (!aCommit) {
  1.1782 +        return;
  1.1783 +      }
  1.1784 +
  1.1785 +      try {
  1.1786 +        let doMods = this._startModifyingAttributes();
  1.1787 +        let undoMods = this._startModifyingAttributes();
  1.1788 +        this._applyAttributes(aVal, null, doMods, undoMods);
  1.1789 +        this.undo.do(() => {
  1.1790 +          doMods.apply();
  1.1791 +        }, function() {
  1.1792 +          undoMods.apply();
  1.1793 +        });
  1.1794 +      } catch(x) {
  1.1795 +        console.error(x);
  1.1796 +      }
  1.1797 +    }
  1.1798 +  });
  1.1799 +
  1.1800 +  let tagName = this.node.nodeName.toLowerCase();
  1.1801 +  this.tag.textContent = tagName;
  1.1802 +  this.closeTag.textContent = tagName;
  1.1803 +
  1.1804 +  this.update();
  1.1805 +}
  1.1806 +
  1.1807 +ElementEditor.prototype = {
  1.1808 +  /**
  1.1809 +   * Update the state of the editor from the node.
  1.1810 +   */
  1.1811 +  update: function() {
  1.1812 +    let attrs = this.node.attributes;
  1.1813 +    if (!attrs) {
  1.1814 +      return;
  1.1815 +    }
  1.1816 +
  1.1817 +    // Hide all the attribute editors, they'll be re-shown if they're
  1.1818 +    // still applicable.  Don't update attributes that are being
  1.1819 +    // actively edited.
  1.1820 +    let attrEditors = this.attrList.querySelectorAll(".attreditor");
  1.1821 +    for (let i = 0; i < attrEditors.length; i++) {
  1.1822 +      if (!attrEditors[i].inplaceEditor) {
  1.1823 +        attrEditors[i].style.display = "none";
  1.1824 +      }
  1.1825 +    }
  1.1826 +
  1.1827 +    // Get the attribute editor for each attribute that exists on
  1.1828 +    // the node and show it.
  1.1829 +    for (let attr of attrs) {
  1.1830 +      let attribute = this._createAttribute(attr);
  1.1831 +      if (!attribute.inplaceEditor) {
  1.1832 +        attribute.style.removeProperty("display");
  1.1833 +      }
  1.1834 +    }
  1.1835 +  },
  1.1836 +
  1.1837 +  _startModifyingAttributes: function() {
  1.1838 +    return this.node.startModifyingAttributes();
  1.1839 +  },
  1.1840 +
  1.1841 +  /**
  1.1842 +   * Get the element used for one of the attributes of this element
  1.1843 +   * @param string attrName The name of the attribute to get the element for
  1.1844 +   * @return DOMElement
  1.1845 +   */
  1.1846 +  getAttributeElement: function(attrName) {
  1.1847 +    return this.attrList.querySelector(
  1.1848 +      ".attreditor[data-attr=" + attrName + "] .attr-value");
  1.1849 +  },
  1.1850 +
  1.1851 +  _createAttribute: function(aAttr, aBefore = null) {
  1.1852 +    // Create the template editor, which will save some variables here.
  1.1853 +    let data = {
  1.1854 +      attrName: aAttr.name,
  1.1855 +    };
  1.1856 +    this.template("attribute", data);
  1.1857 +    var {attr, inner, name, val} = data;
  1.1858 +
  1.1859 +    // Double quotes need to be handled specially to prevent DOMParser failing.
  1.1860 +    // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
  1.1861 +    // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
  1.1862 +    let editValueDisplayed = aAttr.value || "";
  1.1863 +    let hasDoubleQuote = editValueDisplayed.contains('"');
  1.1864 +    let hasSingleQuote = editValueDisplayed.contains("'");
  1.1865 +    let initial = aAttr.name + '="' + editValueDisplayed + '"';
  1.1866 +
  1.1867 +    // Can't just wrap value with ' since the value contains both " and '.
  1.1868 +    if (hasDoubleQuote && hasSingleQuote) {
  1.1869 +        editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
  1.1870 +        initial = aAttr.name + '="' + editValueDisplayed + '"';
  1.1871 +    }
  1.1872 +
  1.1873 +    // Wrap with ' since there are no single quotes in the attribute value.
  1.1874 +    if (hasDoubleQuote && !hasSingleQuote) {
  1.1875 +        initial = aAttr.name + "='" + editValueDisplayed + "'";
  1.1876 +    }
  1.1877 +
  1.1878 +    // Make the attribute editable.
  1.1879 +    editableField({
  1.1880 +      element: inner,
  1.1881 +      trigger: "dblclick",
  1.1882 +      stopOnReturn: true,
  1.1883 +      selectAll: false,
  1.1884 +      initial: initial,
  1.1885 +      contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
  1.1886 +      popup: this.markup.popup,
  1.1887 +      start: (aEditor, aEvent) => {
  1.1888 +        // If the editing was started inside the name or value areas,
  1.1889 +        // select accordingly.
  1.1890 +        if (aEvent && aEvent.target === name) {
  1.1891 +          aEditor.input.setSelectionRange(0, name.textContent.length);
  1.1892 +        } else if (aEvent && aEvent.target === val) {
  1.1893 +          let length = editValueDisplayed.length;
  1.1894 +          let editorLength = aEditor.input.value.length;
  1.1895 +          let start = editorLength - (length + 1);
  1.1896 +          aEditor.input.setSelectionRange(start, start + length);
  1.1897 +        } else {
  1.1898 +          aEditor.input.select();
  1.1899 +        }
  1.1900 +      },
  1.1901 +      done: (aVal, aCommit) => {
  1.1902 +        if (!aCommit || aVal === initial) {
  1.1903 +          return;
  1.1904 +        }
  1.1905 +
  1.1906 +        let doMods = this._startModifyingAttributes();
  1.1907 +        let undoMods = this._startModifyingAttributes();
  1.1908 +
  1.1909 +        // Remove the attribute stored in this editor and re-add any attributes
  1.1910 +        // parsed out of the input element. Restore original attribute if
  1.1911 +        // parsing fails.
  1.1912 +        try {
  1.1913 +          this._saveAttribute(aAttr.name, undoMods);
  1.1914 +          doMods.removeAttribute(aAttr.name);
  1.1915 +          this._applyAttributes(aVal, attr, doMods, undoMods);
  1.1916 +          this.undo.do(() => {
  1.1917 +            doMods.apply();
  1.1918 +          }, () => {
  1.1919 +            undoMods.apply();
  1.1920 +          })
  1.1921 +        } catch(ex) {
  1.1922 +          console.error(ex);
  1.1923 +        }
  1.1924 +      }
  1.1925 +    });
  1.1926 +
  1.1927 +    // Figure out where we should place the attribute.
  1.1928 +    let before = aBefore;
  1.1929 +    if (aAttr.name == "id") {
  1.1930 +      before = this.attrList.firstChild;
  1.1931 +    } else if (aAttr.name == "class") {
  1.1932 +      let idNode = this.attrs["id"];
  1.1933 +      before = idNode ? idNode.nextSibling : this.attrList.firstChild;
  1.1934 +    }
  1.1935 +    this.attrList.insertBefore(attr, before);
  1.1936 +
  1.1937 +    // Remove the old version of this attribute from the DOM.
  1.1938 +    let oldAttr = this.attrs[aAttr.name];
  1.1939 +    if (oldAttr && oldAttr.parentNode) {
  1.1940 +      oldAttr.parentNode.removeChild(oldAttr);
  1.1941 +    }
  1.1942 +
  1.1943 +    this.attrs[aAttr.name] = attr;
  1.1944 +
  1.1945 +    let collapsedValue;
  1.1946 +    if (aAttr.value.match(COLLAPSE_DATA_URL_REGEX)) {
  1.1947 +      collapsedValue = truncateString(aAttr.value, COLLAPSE_DATA_URL_LENGTH);
  1.1948 +    } else {
  1.1949 +      collapsedValue = truncateString(aAttr.value, COLLAPSE_ATTRIBUTE_LENGTH);
  1.1950 +    }
  1.1951 +
  1.1952 +    name.textContent = aAttr.name;
  1.1953 +    val.textContent = collapsedValue;
  1.1954 +
  1.1955 +    return attr;
  1.1956 +  },
  1.1957 +
  1.1958 +  /**
  1.1959 +   * Parse a user-entered attribute string and apply the resulting
  1.1960 +   * attributes to the node.  This operation is undoable.
  1.1961 +   *
  1.1962 +   * @param string aValue the user-entered value.
  1.1963 +   * @param Element aAttrNode the attribute editor that created this
  1.1964 +   *        set of attributes, used to place new attributes where the
  1.1965 +   *        user put them.
  1.1966 +   */
  1.1967 +  _applyAttributes: function(aValue, aAttrNode, aDoMods, aUndoMods) {
  1.1968 +    let attrs = parseAttributeValues(aValue, this.doc);
  1.1969 +    for (let attr of attrs) {
  1.1970 +      // Create an attribute editor next to the current attribute if needed.
  1.1971 +      this._createAttribute(attr, aAttrNode ? aAttrNode.nextSibling : null);
  1.1972 +      this._saveAttribute(attr.name, aUndoMods);
  1.1973 +      aDoMods.setAttribute(attr.name, attr.value);
  1.1974 +    }
  1.1975 +  },
  1.1976 +
  1.1977 +  /**
  1.1978 +   * Saves the current state of the given attribute into an attribute
  1.1979 +   * modification list.
  1.1980 +   */
  1.1981 +  _saveAttribute: function(aName, aUndoMods) {
  1.1982 +    let node = this.node;
  1.1983 +    if (node.hasAttribute(aName)) {
  1.1984 +      let oldValue = node.getAttribute(aName);
  1.1985 +      aUndoMods.setAttribute(aName, oldValue);
  1.1986 +    } else {
  1.1987 +      aUndoMods.removeAttribute(aName);
  1.1988 +    }
  1.1989 +  },
  1.1990 +
  1.1991 +  /**
  1.1992 +   * Called when the tag name editor has is done editing.
  1.1993 +   */
  1.1994 +  onTagEdit: function(aVal, aCommit) {
  1.1995 +    if (!aCommit || aVal == this.rawNode.tagName) {
  1.1996 +      return;
  1.1997 +    }
  1.1998 +
  1.1999 +    // Create a new element with the same attributes as the
  1.2000 +    // current element and prepare to replace the current node
  1.2001 +    // with it.
  1.2002 +    try {
  1.2003 +      var newElt = nodeDocument(this.rawNode).createElement(aVal);
  1.2004 +    } catch(x) {
  1.2005 +      // Failed to create a new element with that tag name, ignore
  1.2006 +      // the change.
  1.2007 +      return;
  1.2008 +    }
  1.2009 +
  1.2010 +    let attrs = this.rawNode.attributes;
  1.2011 +
  1.2012 +    for (let i = 0 ; i < attrs.length; i++) {
  1.2013 +      newElt.setAttribute(attrs[i].name, attrs[i].value);
  1.2014 +    }
  1.2015 +    let newFront = this.markup.walker.frontForRawNode(newElt);
  1.2016 +    let newContainer = this.markup.importNode(newFront);
  1.2017 +
  1.2018 +    // Retain the two nodes we care about here so we can undo.
  1.2019 +    let walker = this.markup.walker;
  1.2020 +    promise.all([
  1.2021 +      walker.retainNode(newFront), walker.retainNode(this.node)
  1.2022 +    ]).then(() => {
  1.2023 +      function swapNodes(aOld, aNew) {
  1.2024 +        aOld.parentNode.insertBefore(aNew, aOld);
  1.2025 +        while (aOld.firstChild) {
  1.2026 +          aNew.appendChild(aOld.firstChild);
  1.2027 +        }
  1.2028 +        aOld.parentNode.removeChild(aOld);
  1.2029 +      }
  1.2030 +
  1.2031 +      this.undo.do(() => {
  1.2032 +        swapNodes(this.rawNode, newElt);
  1.2033 +        this.markup.setNodeExpanded(newFront, this.container.expanded);
  1.2034 +        if (this.container.selected) {
  1.2035 +          this.markup.navigate(newContainer);
  1.2036 +        }
  1.2037 +      }, () => {
  1.2038 +        swapNodes(newElt, this.rawNode);
  1.2039 +        this.markup.setNodeExpanded(this.node, newContainer.expanded);
  1.2040 +        if (newContainer.selected) {
  1.2041 +          this.markup.navigate(this.container);
  1.2042 +        }
  1.2043 +      });
  1.2044 +    }).then(null, console.error);
  1.2045 +  },
  1.2046 +
  1.2047 +  destroy: function() {}
  1.2048 +};
  1.2049 +
  1.2050 +function nodeDocument(node) {
  1.2051 +  return node.ownerDocument ||
  1.2052 +    (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
  1.2053 +}
  1.2054 +
  1.2055 +function truncateString(str, maxLength) {
  1.2056 +  if (str.length <= maxLength) {
  1.2057 +    return str;
  1.2058 +  }
  1.2059 +
  1.2060 +  return str.substring(0, Math.ceil(maxLength / 2)) +
  1.2061 +         "…" +
  1.2062 +         str.substring(str.length - Math.floor(maxLength / 2));
  1.2063 +}
  1.2064 +/**
  1.2065 + * Parse attribute names and values from a string.
  1.2066 + *
  1.2067 + * @param  {String} attr
  1.2068 + *         The input string for which names/values are to be parsed.
  1.2069 + * @param  {HTMLDocument} doc
  1.2070 + *         A document that can be used to test valid attributes.
  1.2071 + * @return {Array}
  1.2072 + *         An array of attribute names and their values.
  1.2073 + */
  1.2074 +function parseAttributeValues(attr, doc) {
  1.2075 +  attr = attr.trim();
  1.2076 +
  1.2077 +  // Handle bad user inputs by appending a " or ' if it fails to parse without them.
  1.2078 +  let el = DOMParser.parseFromString("<div " + attr + "></div>", "text/html").body.childNodes[0] ||
  1.2079 +           DOMParser.parseFromString("<div " + attr + "\"></div>", "text/html").body.childNodes[0] ||
  1.2080 +           DOMParser.parseFromString("<div " + attr + "'></div>", "text/html").body.childNodes[0];
  1.2081 +  let div = doc.createElement("div");
  1.2082 +
  1.2083 +  let attributes = [];
  1.2084 +  for (let attribute of el.attributes) {
  1.2085 +    // Try to set on an element in the document, throws exception on bad input.
  1.2086 +    // Prevents InvalidCharacterError - "String contains an invalid character".
  1.2087 +    try {
  1.2088 +      div.setAttribute(attribute.name, attribute.value);
  1.2089 +      attributes.push({
  1.2090 +        name: attribute.name,
  1.2091 +        value: attribute.value
  1.2092 +      });
  1.2093 +    }
  1.2094 +    catch(e) { }
  1.2095 +  }
  1.2096 +
  1.2097 +  // Attributes return from DOMParser in reverse order from how they are entered.
  1.2098 +  return attributes.reverse();
  1.2099 +}
  1.2100 +
  1.2101 +loader.lazyGetter(MarkupView.prototype, "strings", () => Services.strings.createBundle(
  1.2102 +  "chrome://browser/locale/devtools/inspector.properties"
  1.2103 +));
  1.2104 +
  1.2105 +XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
  1.2106 +  return Cc["@mozilla.org/widget/clipboardhelper;1"].
  1.2107 +    getService(Ci.nsIClipboardHelper);
  1.2108 +});

mercurial