browser/devtools/inspector/breadcrumbs.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/inspector/breadcrumbs.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,708 @@
     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 +const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
    1.13 +const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
    1.14 +
    1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.16 +Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
    1.17 +Cu.import("resource://gre/modules/Services.jsm");
    1.18 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    1.19 +const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
    1.20 +const MAX_LABEL_LENGTH = 40;
    1.21 +
    1.22 +let promise = require("devtools/toolkit/deprecated-sync-thenables");
    1.23 +
    1.24 +const LOW_PRIORITY_ELEMENTS = {
    1.25 +  "HEAD": true,
    1.26 +  "BASE": true,
    1.27 +  "BASEFONT": true,
    1.28 +  "ISINDEX": true,
    1.29 +  "LINK": true,
    1.30 +  "META": true,
    1.31 +  "SCRIPT": true,
    1.32 +  "STYLE": true,
    1.33 +  "TITLE": true,
    1.34 +};
    1.35 +
    1.36 +function resolveNextTick(value) {
    1.37 +  let deferred = promise.defer();
    1.38 +  Services.tm.mainThread.dispatch(() => {
    1.39 +    try {
    1.40 +      deferred.resolve(value);
    1.41 +    } catch(ex) {
    1.42 +      console.error(ex);
    1.43 +    }
    1.44 +  }, Ci.nsIThread.DISPATCH_NORMAL);
    1.45 +  return deferred.promise;
    1.46 +}
    1.47 +
    1.48 +///////////////////////////////////////////////////////////////////////////
    1.49 +//// HTML Breadcrumbs
    1.50 +
    1.51 +/**
    1.52 + * Display the ancestors of the current node and its children.
    1.53 + * Only one "branch" of children are displayed (only one line).
    1.54 + *
    1.55 + * FIXME: Bug 822388 - Use the BreadcrumbsWidget in the Inspector.
    1.56 + *
    1.57 + * Mechanism:
    1.58 + * . If no nodes displayed yet:
    1.59 + *    then display the ancestor of the selected node and the selected node;
    1.60 + *   else select the node;
    1.61 + * . If the selected node is the last node displayed, append its first (if any).
    1.62 + */
    1.63 +function HTMLBreadcrumbs(aInspector)
    1.64 +{
    1.65 +  this.inspector = aInspector;
    1.66 +  this.selection = this.inspector.selection;
    1.67 +  this.chromeWin = this.inspector.panelWin;
    1.68 +  this.chromeDoc = this.inspector.panelDoc;
    1.69 +  this.DOMHelpers = new DOMHelpers(this.chromeWin);
    1.70 +  this._init();
    1.71 +}
    1.72 +
    1.73 +exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
    1.74 +
    1.75 +HTMLBreadcrumbs.prototype = {
    1.76 +  get walker() this.inspector.walker,
    1.77 +
    1.78 +  _init: function BC__init()
    1.79 +  {
    1.80 +    this.container = this.chromeDoc.getElementById("inspector-breadcrumbs");
    1.81 +
    1.82 +    // These separators are used for CSS purposes only, and are positioned
    1.83 +    // off screen, but displayed with -moz-element.
    1.84 +    this.separators = this.chromeDoc.createElement("box");
    1.85 +    this.separators.className = "breadcrumb-separator-container";
    1.86 +    this.separators.innerHTML =
    1.87 +                      "<box id='breadcrumb-separator-before'></box>" +
    1.88 +                      "<box id='breadcrumb-separator-after'></box>" +
    1.89 +                      "<box id='breadcrumb-separator-normal'></box>";
    1.90 +    this.container.parentNode.appendChild(this.separators);
    1.91 +
    1.92 +    this.container.addEventListener("mousedown", this, true);
    1.93 +    this.container.addEventListener("keypress", this, true);
    1.94 +
    1.95 +    // We will save a list of already displayed nodes in this array.
    1.96 +    this.nodeHierarchy = [];
    1.97 +
    1.98 +    // Last selected node in nodeHierarchy.
    1.99 +    this.currentIndex = -1;
   1.100 +
   1.101 +    // By default, hide the arrows. We let the <scrollbox> show them
   1.102 +    // in case of overflow.
   1.103 +    this.container.removeAttribute("overflows");
   1.104 +    this.container._scrollButtonUp.collapsed = true;
   1.105 +    this.container._scrollButtonDown.collapsed = true;
   1.106 +
   1.107 +    this.onscrollboxreflow = function() {
   1.108 +      if (this.container._scrollButtonDown.collapsed)
   1.109 +        this.container.removeAttribute("overflows");
   1.110 +      else
   1.111 +        this.container.setAttribute("overflows", true);
   1.112 +    }.bind(this);
   1.113 +
   1.114 +    this.container.addEventListener("underflow", this.onscrollboxreflow, false);
   1.115 +    this.container.addEventListener("overflow", this.onscrollboxreflow, false);
   1.116 +
   1.117 +    this.update = this.update.bind(this);
   1.118 +    this.updateSelectors = this.updateSelectors.bind(this);
   1.119 +    this.selection.on("new-node-front", this.update);
   1.120 +    this.selection.on("pseudoclass", this.updateSelectors);
   1.121 +    this.selection.on("attribute-changed", this.updateSelectors);
   1.122 +    this.inspector.on("markupmutation", this.update);
   1.123 +    this.update();
   1.124 +  },
   1.125 +
   1.126 +  /**
   1.127 +   * Include in a promise's then() chain to reject the chain
   1.128 +   * when the breadcrumbs' selection has changed while the promise
   1.129 +   * was outstanding.
   1.130 +   */
   1.131 +  selectionGuard: function() {
   1.132 +    let selection = this.selection.nodeFront;
   1.133 +    return (result) => {
   1.134 +      if (selection != this.selection.nodeFront) {
   1.135 +        return promise.reject("selection-changed");
   1.136 +      }
   1.137 +      return result;
   1.138 +    }
   1.139 +  },
   1.140 +
   1.141 +  /**
   1.142 +   * Print any errors (except selection guard errors).
   1.143 +   */
   1.144 +  selectionGuardEnd: function(err) {
   1.145 +    if (err != "selection-changed") {
   1.146 +      console.error(err);
   1.147 +    }
   1.148 +    promise.reject(err);
   1.149 +  },
   1.150 +
   1.151 +  /**
   1.152 +   * Build a string that represents the node: tagName#id.class1.class2.
   1.153 +   *
   1.154 +   * @param aNode The node to pretty-print
   1.155 +   * @returns a string
   1.156 +   */
   1.157 +  prettyPrintNodeAsText: function BC_prettyPrintNodeText(aNode)
   1.158 +  {
   1.159 +    let text = aNode.tagName.toLowerCase();
   1.160 +    if (aNode.id) {
   1.161 +      text += "#" + aNode.id;
   1.162 +    }
   1.163 +
   1.164 +    if (aNode.className) {
   1.165 +      let classList = aNode.className.split(/\s+/);
   1.166 +      for (let i = 0; i < classList.length; i++) {
   1.167 +        text += "." + classList[i];
   1.168 +      }
   1.169 +    }
   1.170 +
   1.171 +    for (let pseudo of aNode.pseudoClassLocks) {
   1.172 +      text += pseudo;
   1.173 +    }
   1.174 +
   1.175 +    return text;
   1.176 +  },
   1.177 +
   1.178 +
   1.179 +  /**
   1.180 +   * Build <label>s that represent the node:
   1.181 +   *   <label class="breadcrumbs-widget-item-tag">tagName</label>
   1.182 +   *   <label class="breadcrumbs-widget-item-id">#id</label>
   1.183 +   *   <label class="breadcrumbs-widget-item-classes">.class1.class2</label>
   1.184 +   *
   1.185 +   * @param aNode The node to pretty-print
   1.186 +   * @returns a document fragment.
   1.187 +   */
   1.188 +  prettyPrintNodeAsXUL: function BC_prettyPrintNodeXUL(aNode)
   1.189 +  {
   1.190 +    let fragment = this.chromeDoc.createDocumentFragment();
   1.191 +
   1.192 +    let tagLabel = this.chromeDoc.createElement("label");
   1.193 +    tagLabel.className = "breadcrumbs-widget-item-tag plain";
   1.194 +
   1.195 +    let idLabel = this.chromeDoc.createElement("label");
   1.196 +    idLabel.className = "breadcrumbs-widget-item-id plain";
   1.197 +
   1.198 +    let classesLabel = this.chromeDoc.createElement("label");
   1.199 +    classesLabel.className = "breadcrumbs-widget-item-classes plain";
   1.200 +
   1.201 +    let pseudosLabel = this.chromeDoc.createElement("label");
   1.202 +    pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain";
   1.203 +
   1.204 +    let tagText = aNode.tagName.toLowerCase();
   1.205 +    let idText = aNode.id ? ("#" + aNode.id) : "";
   1.206 +    let classesText = "";
   1.207 +
   1.208 +    if (aNode.className) {
   1.209 +      let classList = aNode.className.split(/\s+/);
   1.210 +      for (let i = 0; i < classList.length; i++) {
   1.211 +        classesText += "." + classList[i];
   1.212 +      }
   1.213 +    }
   1.214 +
   1.215 +    // XXX: Until we have pseudoclass lock in the node.
   1.216 +    for (let pseudo of aNode.pseudoClassLocks) {
   1.217 +
   1.218 +    }
   1.219 +
   1.220 +    // Figure out which element (if any) needs ellipsing.
   1.221 +    // Substring for that element, then clear out any extras
   1.222 +    // (except for pseudo elements).
   1.223 +    let maxTagLength = MAX_LABEL_LENGTH;
   1.224 +    let maxIdLength = MAX_LABEL_LENGTH - tagText.length;
   1.225 +    let maxClassLength = MAX_LABEL_LENGTH - tagText.length - idText.length;
   1.226 +
   1.227 +    if (tagText.length > maxTagLength) {
   1.228 +       tagText = tagText.substr(0, maxTagLength) + ELLIPSIS;
   1.229 +       idText = classesText = "";
   1.230 +    } else if (idText.length > maxIdLength) {
   1.231 +       idText = idText.substr(0, maxIdLength) + ELLIPSIS;
   1.232 +       classesText = "";
   1.233 +    } else if (classesText.length > maxClassLength) {
   1.234 +      classesText = classesText.substr(0, maxClassLength) + ELLIPSIS;
   1.235 +    }
   1.236 +
   1.237 +    tagLabel.textContent = tagText;
   1.238 +    idLabel.textContent = idText;
   1.239 +    classesLabel.textContent = classesText;
   1.240 +    pseudosLabel.textContent = aNode.pseudoClassLocks.join("");
   1.241 +
   1.242 +    fragment.appendChild(tagLabel);
   1.243 +    fragment.appendChild(idLabel);
   1.244 +    fragment.appendChild(classesLabel);
   1.245 +    fragment.appendChild(pseudosLabel);
   1.246 +
   1.247 +    return fragment;
   1.248 +  },
   1.249 +
   1.250 +  /**
   1.251 +   * Open the sibling menu.
   1.252 +   *
   1.253 +   * @param aButton the button representing the node.
   1.254 +   * @param aNode the node we want the siblings from.
   1.255 +   */
   1.256 +  openSiblingMenu: function BC_openSiblingMenu(aButton, aNode)
   1.257 +  {
   1.258 +    // We make sure that the targeted node is selected
   1.259 +    // because we want to use the nodemenu that only works
   1.260 +    // for inspector.selection
   1.261 +    this.selection.setNodeFront(aNode, "breadcrumbs");
   1.262 +
   1.263 +    let title = this.chromeDoc.createElement("menuitem");
   1.264 +    title.setAttribute("label", this.inspector.strings.GetStringFromName("breadcrumbs.siblings"));
   1.265 +    title.setAttribute("disabled", "true");
   1.266 +
   1.267 +    let separator = this.chromeDoc.createElement("menuseparator");
   1.268 +
   1.269 +    let items = [title, separator];
   1.270 +
   1.271 +    this.walker.siblings(aNode, {
   1.272 +      whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
   1.273 +    }).then(siblings => {
   1.274 +      let nodes = siblings.nodes;
   1.275 +      for (let i = 0; i < nodes.length; i++) {
   1.276 +        let item = this.chromeDoc.createElement("menuitem");
   1.277 +        if (nodes[i] === aNode) {
   1.278 +          item.setAttribute("disabled", "true");
   1.279 +          item.setAttribute("checked", "true");
   1.280 +        }
   1.281 +
   1.282 +        item.setAttribute("type", "radio");
   1.283 +        item.setAttribute("label", this.prettyPrintNodeAsText(nodes[i]));
   1.284 +
   1.285 +        let selection = this.selection;
   1.286 +        item.onmouseup = (function(aNode) {
   1.287 +          return function() {
   1.288 +            selection.setNodeFront(aNode, "breadcrumbs");
   1.289 +          }
   1.290 +        })(nodes[i]);
   1.291 +
   1.292 +        items.push(item);
   1.293 +        this.inspector.showNodeMenu(aButton, "before_start", items);
   1.294 +      }
   1.295 +    });
   1.296 +  },
   1.297 +
   1.298 +  /**
   1.299 +   * Generic event handler.
   1.300 +   *
   1.301 +   * @param nsIDOMEvent event
   1.302 +   *        The DOM event object.
   1.303 +   */
   1.304 +  handleEvent: function BC_handleEvent(event)
   1.305 +  {
   1.306 +    if (event.type == "mousedown" && event.button == 0) {
   1.307 +      // on Click and Hold, open the Siblings menu
   1.308 +
   1.309 +      let timer;
   1.310 +      let container = this.container;
   1.311 +
   1.312 +      function openMenu(event) {
   1.313 +        cancelHold();
   1.314 +        let target = event.originalTarget;
   1.315 +        if (target.tagName == "button") {
   1.316 +          target.onBreadcrumbsHold();
   1.317 +        }
   1.318 +      }
   1.319 +
   1.320 +      function handleClick(event) {
   1.321 +        cancelHold();
   1.322 +        let target = event.originalTarget;
   1.323 +        if (target.tagName == "button") {
   1.324 +          target.onBreadcrumbsClick();
   1.325 +        }
   1.326 +      }
   1.327 +
   1.328 +      let window = this.chromeWin;
   1.329 +      function cancelHold(event) {
   1.330 +        window.clearTimeout(timer);
   1.331 +        container.removeEventListener("mouseout", cancelHold, false);
   1.332 +        container.removeEventListener("mouseup", handleClick, false);
   1.333 +      }
   1.334 +
   1.335 +      container.addEventListener("mouseout", cancelHold, false);
   1.336 +      container.addEventListener("mouseup", handleClick, false);
   1.337 +      timer = window.setTimeout(openMenu, 500, event);
   1.338 +    }
   1.339 +
   1.340 +    if (event.type == "keypress" && this.selection.isElementNode()) {
   1.341 +      let node = null;
   1.342 +
   1.343 +
   1.344 +      this._keyPromise = this._keyPromise || promise.resolve(null);
   1.345 +
   1.346 +      this._keyPromise = (this._keyPromise || promise.resolve(null)).then(() => {
   1.347 +        switch (event.keyCode) {
   1.348 +          case this.chromeWin.KeyEvent.DOM_VK_LEFT:
   1.349 +            if (this.currentIndex != 0) {
   1.350 +              node = promise.resolve(this.nodeHierarchy[this.currentIndex - 1].node);
   1.351 +            }
   1.352 +            break;
   1.353 +          case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
   1.354 +            if (this.currentIndex < this.nodeHierarchy.length - 1) {
   1.355 +              node = promise.resolve(this.nodeHierarchy[this.currentIndex + 1].node);
   1.356 +            }
   1.357 +            break;
   1.358 +          case this.chromeWin.KeyEvent.DOM_VK_UP:
   1.359 +            node = this.walker.previousSibling(this.selection.nodeFront, {
   1.360 +              whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
   1.361 +            });
   1.362 +            break;
   1.363 +          case this.chromeWin.KeyEvent.DOM_VK_DOWN:
   1.364 +            node = this.walker.nextSibling(this.selection.nodeFront, {
   1.365 +              whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
   1.366 +            });
   1.367 +            break;
   1.368 +        }
   1.369 +
   1.370 +        return node.then((node) => {
   1.371 +          if (node) {
   1.372 +            this.selection.setNodeFront(node, "breadcrumbs");
   1.373 +          }
   1.374 +        });
   1.375 +      });
   1.376 +      event.preventDefault();
   1.377 +      event.stopPropagation();
   1.378 +    }
   1.379 +  },
   1.380 +
   1.381 +  /**
   1.382 +   * Remove nodes and delete properties.
   1.383 +   */
   1.384 +  destroy: function BC_destroy()
   1.385 +  {
   1.386 +    this.selection.off("new-node-front", this.update);
   1.387 +    this.selection.off("pseudoclass", this.updateSelectors);
   1.388 +    this.selection.off("attribute-changed", this.updateSelectors);
   1.389 +    this.inspector.off("markupmutation", this.update);
   1.390 +
   1.391 +    this.container.removeEventListener("underflow", this.onscrollboxreflow, false);
   1.392 +    this.container.removeEventListener("overflow", this.onscrollboxreflow, false);
   1.393 +    this.onscrollboxreflow = null;
   1.394 +
   1.395 +    this.empty();
   1.396 +    this.container.removeEventListener("mousedown", this, true);
   1.397 +    this.container.removeEventListener("keypress", this, true);
   1.398 +    this.container = null;
   1.399 +
   1.400 +    this.separators.remove();
   1.401 +    this.separators = null;
   1.402 +
   1.403 +    this.nodeHierarchy = null;
   1.404 +  },
   1.405 +
   1.406 +  /**
   1.407 +   * Empty the breadcrumbs container.
   1.408 +   */
   1.409 +  empty: function BC_empty()
   1.410 +  {
   1.411 +    while (this.container.hasChildNodes()) {
   1.412 +      this.container.removeChild(this.container.firstChild);
   1.413 +    }
   1.414 +  },
   1.415 +
   1.416 +  /**
   1.417 +   * Set which button represent the selected node.
   1.418 +   *
   1.419 +   * @param aIdx Index of the displayed-button to select
   1.420 +   */
   1.421 +  setCursor: function BC_setCursor(aIdx)
   1.422 +  {
   1.423 +    // Unselect the previously selected button
   1.424 +    if (this.currentIndex > -1 && this.currentIndex < this.nodeHierarchy.length) {
   1.425 +      this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked");
   1.426 +    }
   1.427 +    if (aIdx > -1) {
   1.428 +      this.nodeHierarchy[aIdx].button.setAttribute("checked", "true");
   1.429 +      if (this.hadFocus)
   1.430 +        this.nodeHierarchy[aIdx].button.focus();
   1.431 +    }
   1.432 +    this.currentIndex = aIdx;
   1.433 +  },
   1.434 +
   1.435 +  /**
   1.436 +   * Get the index of the node in the cache.
   1.437 +   *
   1.438 +   * @param aNode
   1.439 +   * @returns integer the index, -1 if not found
   1.440 +   */
   1.441 +  indexOf: function BC_indexOf(aNode)
   1.442 +  {
   1.443 +    let i = this.nodeHierarchy.length - 1;
   1.444 +    for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
   1.445 +      if (this.nodeHierarchy[i].node === aNode) {
   1.446 +        return i;
   1.447 +      }
   1.448 +    }
   1.449 +    return -1;
   1.450 +  },
   1.451 +
   1.452 +  /**
   1.453 +   * Remove all the buttons and their references in the cache
   1.454 +   * after a given index.
   1.455 +   *
   1.456 +   * @param aIdx
   1.457 +   */
   1.458 +  cutAfter: function BC_cutAfter(aIdx)
   1.459 +  {
   1.460 +    while (this.nodeHierarchy.length > (aIdx + 1)) {
   1.461 +      let toRemove = this.nodeHierarchy.pop();
   1.462 +      this.container.removeChild(toRemove.button);
   1.463 +    }
   1.464 +  },
   1.465 +
   1.466 +  /**
   1.467 +   * Build a button representing the node.
   1.468 +   *
   1.469 +   * @param aNode The node from the page.
   1.470 +   * @returns aNode The <button>.
   1.471 +   */
   1.472 +  buildButton: function BC_buildButton(aNode)
   1.473 +  {
   1.474 +    let button = this.chromeDoc.createElement("button");
   1.475 +    button.appendChild(this.prettyPrintNodeAsXUL(aNode));
   1.476 +    button.className = "breadcrumbs-widget-item";
   1.477 +
   1.478 +    button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(aNode));
   1.479 +
   1.480 +    button.onkeypress = function onBreadcrumbsKeypress(e) {
   1.481 +      if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
   1.482 +          e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN)
   1.483 +        button.click();
   1.484 +    }
   1.485 +
   1.486 +    button.onBreadcrumbsClick = function onBreadcrumbsClick() {
   1.487 +      this.selection.setNodeFront(aNode, "breadcrumbs");
   1.488 +    }.bind(this);
   1.489 +
   1.490 +    button.onclick = (function _onBreadcrumbsRightClick(event) {
   1.491 +      button.focus();
   1.492 +      if (event.button == 2) {
   1.493 +        this.openSiblingMenu(button, aNode);
   1.494 +      }
   1.495 +    }).bind(this);
   1.496 +
   1.497 +    button.onBreadcrumbsHold = (function _onBreadcrumbsHold() {
   1.498 +      this.openSiblingMenu(button, aNode);
   1.499 +    }).bind(this);
   1.500 +    return button;
   1.501 +  },
   1.502 +
   1.503 +  /**
   1.504 +   * Connecting the end of the breadcrumbs to a node.
   1.505 +   *
   1.506 +   * @param aNode The node to reach.
   1.507 +   */
   1.508 +  expand: function BC_expand(aNode)
   1.509 +  {
   1.510 +      let fragment = this.chromeDoc.createDocumentFragment();
   1.511 +      let toAppend = aNode;
   1.512 +      let lastButtonInserted = null;
   1.513 +      let originalLength = this.nodeHierarchy.length;
   1.514 +      let stopNode = null;
   1.515 +      if (originalLength > 0) {
   1.516 +        stopNode = this.nodeHierarchy[originalLength - 1].node;
   1.517 +      }
   1.518 +      while (toAppend && toAppend != stopNode) {
   1.519 +        if (toAppend.tagName) {
   1.520 +          let button = this.buildButton(toAppend);
   1.521 +          fragment.insertBefore(button, lastButtonInserted);
   1.522 +          lastButtonInserted = button;
   1.523 +          this.nodeHierarchy.splice(originalLength, 0, {node: toAppend, button: button});
   1.524 +        }
   1.525 +        toAppend = toAppend.parentNode();
   1.526 +      }
   1.527 +      this.container.appendChild(fragment, this.container.firstChild);
   1.528 +  },
   1.529 +
   1.530 +  /**
   1.531 +   * Get a child of a node that can be displayed in the breadcrumbs
   1.532 +   * and that is probably visible. See LOW_PRIORITY_ELEMENTS.
   1.533 +   *
   1.534 +   * @param aNode The parent node.
   1.535 +   * @returns nsIDOMNode|null
   1.536 +   */
   1.537 +  getInterestingFirstNode: function BC_getInterestingFirstNode(aNode)
   1.538 +  {
   1.539 +    let deferred = promise.defer();
   1.540 +
   1.541 +    var fallback = null;
   1.542 +
   1.543 +    var moreChildren = () => {
   1.544 +      this.walker.children(aNode, {
   1.545 +        start: fallback,
   1.546 +        maxNodes: 10,
   1.547 +        whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
   1.548 +      }).then(this.selectionGuard()).then(response => {
   1.549 +        for (let node of response.nodes) {
   1.550 +          if (!(node.tagName in LOW_PRIORITY_ELEMENTS)) {
   1.551 +            deferred.resolve(node);
   1.552 +            return;
   1.553 +          }
   1.554 +          if (!fallback) {
   1.555 +            fallback = node;
   1.556 +          }
   1.557 +        }
   1.558 +        if (response.hasLast) {
   1.559 +          deferred.resolve(fallback);
   1.560 +          return;
   1.561 +        } else {
   1.562 +          moreChildren();
   1.563 +        }
   1.564 +      }).then(null, this.selectionGuardEnd);
   1.565 +    }
   1.566 +    moreChildren();
   1.567 +    return deferred.promise;
   1.568 +  },
   1.569 +
   1.570 +  /**
   1.571 +   * Find the "youngest" ancestor of a node which is already in the breadcrumbs.
   1.572 +   *
   1.573 +   * @param aNode
   1.574 +   * @returns Index of the ancestor in the cache
   1.575 +   */
   1.576 +  getCommonAncestor: function BC_getCommonAncestor(aNode)
   1.577 +  {
   1.578 +    let node = aNode;
   1.579 +    while (node) {
   1.580 +      let idx = this.indexOf(node);
   1.581 +      if (idx > -1) {
   1.582 +        return idx;
   1.583 +      } else {
   1.584 +        node = node.parentNode();
   1.585 +      }
   1.586 +    }
   1.587 +    return -1;
   1.588 +  },
   1.589 +
   1.590 +  /**
   1.591 +   * Make sure that the latest node in the breadcrumbs is not the selected node
   1.592 +   * if the selected node still has children.
   1.593 +   */
   1.594 +  ensureFirstChild: function BC_ensureFirstChild()
   1.595 +  {
   1.596 +    // If the last displayed node is the selected node
   1.597 +    if (this.currentIndex == this.nodeHierarchy.length - 1) {
   1.598 +      let node = this.nodeHierarchy[this.currentIndex].node;
   1.599 +      return this.getInterestingFirstNode(node).then(child => {
   1.600 +        // If the node has a child
   1.601 +        if (child) {
   1.602 +          // Show this child
   1.603 +          this.expand(child);
   1.604 +        }
   1.605 +      });
   1.606 +    }
   1.607 +
   1.608 +    return resolveNextTick(true);
   1.609 +  },
   1.610 +
   1.611 +  /**
   1.612 +   * Ensure the selected node is visible.
   1.613 +   */
   1.614 +  scroll: function BC_scroll()
   1.615 +  {
   1.616 +    // FIXME bug 684352: make sure its immediate neighbors are visible too.
   1.617 +
   1.618 +    let scrollbox = this.container;
   1.619 +    let element = this.nodeHierarchy[this.currentIndex].button;
   1.620 +
   1.621 +    // Repeated calls to ensureElementIsVisible would interfere with each other
   1.622 +    // and may sometimes result in incorrect scroll positions.
   1.623 +    this.chromeWin.clearTimeout(this._ensureVisibleTimeout);
   1.624 +    this._ensureVisibleTimeout = this.chromeWin.setTimeout(function() {
   1.625 +      scrollbox.ensureElementIsVisible(element);
   1.626 +    }, ENSURE_SELECTION_VISIBLE_DELAY);
   1.627 +  },
   1.628 +
   1.629 +  updateSelectors: function BC_updateSelectors()
   1.630 +  {
   1.631 +    for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
   1.632 +      let crumb = this.nodeHierarchy[i];
   1.633 +      let button = crumb.button;
   1.634 +
   1.635 +      while(button.hasChildNodes()) {
   1.636 +        button.removeChild(button.firstChild);
   1.637 +      }
   1.638 +      button.appendChild(this.prettyPrintNodeAsXUL(crumb.node));
   1.639 +      button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node));
   1.640 +    }
   1.641 +  },
   1.642 +
   1.643 +  /**
   1.644 +   * Update the breadcrumbs display when a new node is selected.
   1.645 +   */
   1.646 +  update: function BC_update(reason)
   1.647 +  {
   1.648 +    if (reason !== "markupmutation") {
   1.649 +      this.inspector.hideNodeMenu();
   1.650 +    }
   1.651 +
   1.652 +    let cmdDispatcher = this.chromeDoc.commandDispatcher;
   1.653 +    this.hadFocus = (cmdDispatcher.focusedElement &&
   1.654 +                     cmdDispatcher.focusedElement.parentNode == this.container);
   1.655 +
   1.656 +    if (!this.selection.isConnected()) {
   1.657 +      this.cutAfter(-1); // remove all the crumbs
   1.658 +      return;
   1.659 +    }
   1.660 +
   1.661 +    if (!this.selection.isElementNode()) {
   1.662 +      this.setCursor(-1); // no selection
   1.663 +      return;
   1.664 +    }
   1.665 +
   1.666 +    let idx = this.indexOf(this.selection.nodeFront);
   1.667 +
   1.668 +    // Is the node already displayed in the breadcrumbs?
   1.669 +    // (and there are no mutations that need re-display of the crumbs)
   1.670 +    if (idx > -1 && reason !== "markupmutation") {
   1.671 +      // Yes. We select it.
   1.672 +      this.setCursor(idx);
   1.673 +    } else {
   1.674 +      // No. Is the breadcrumbs display empty?
   1.675 +      if (this.nodeHierarchy.length > 0) {
   1.676 +        // No. We drop all the element that are not direct ancestors
   1.677 +        // of the selection
   1.678 +        let parent = this.selection.nodeFront.parentNode();
   1.679 +        let idx = this.getCommonAncestor(parent);
   1.680 +        this.cutAfter(idx);
   1.681 +      }
   1.682 +      // we append the missing button between the end of the breadcrumbs display
   1.683 +      // and the current node.
   1.684 +      this.expand(this.selection.nodeFront);
   1.685 +
   1.686 +      // we select the current node button
   1.687 +      idx = this.indexOf(this.selection.nodeFront);
   1.688 +      this.setCursor(idx);
   1.689 +    }
   1.690 +
   1.691 +    let doneUpdating = this.inspector.updating("breadcrumbs");
   1.692 +    // Add the first child of the very last node of the breadcrumbs if possible.
   1.693 +    this.ensureFirstChild().then(this.selectionGuard()).then(() => {
   1.694 +      this.updateSelectors();
   1.695 +
   1.696 +      // Make sure the selected node and its neighbours are visible.
   1.697 +      this.scroll();
   1.698 +      return resolveNextTick().then(() => {
   1.699 +        this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
   1.700 +        doneUpdating();
   1.701 +      });
   1.702 +    }).then(null, err => {
   1.703 +      doneUpdating(this.selection.nodeFront);
   1.704 +      this.selectionGuardEnd(err);
   1.705 +    });
   1.706 +  }
   1.707 +};
   1.708 +
   1.709 +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   1.710 +  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
   1.711 +});

mercurial