browser/devtools/inspector/breadcrumbs.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 const {Cc, Cu, Ci} = require("chrome");
     9 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
    10 const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
    12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    13 Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
    14 Cu.import("resource://gre/modules/Services.jsm");
    15 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    16 const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
    17 const MAX_LABEL_LENGTH = 40;
    19 let promise = require("devtools/toolkit/deprecated-sync-thenables");
    21 const LOW_PRIORITY_ELEMENTS = {
    22   "HEAD": true,
    23   "BASE": true,
    24   "BASEFONT": true,
    25   "ISINDEX": true,
    26   "LINK": true,
    27   "META": true,
    28   "SCRIPT": true,
    29   "STYLE": true,
    30   "TITLE": true,
    31 };
    33 function resolveNextTick(value) {
    34   let deferred = promise.defer();
    35   Services.tm.mainThread.dispatch(() => {
    36     try {
    37       deferred.resolve(value);
    38     } catch(ex) {
    39       console.error(ex);
    40     }
    41   }, Ci.nsIThread.DISPATCH_NORMAL);
    42   return deferred.promise;
    43 }
    45 ///////////////////////////////////////////////////////////////////////////
    46 //// HTML Breadcrumbs
    48 /**
    49  * Display the ancestors of the current node and its children.
    50  * Only one "branch" of children are displayed (only one line).
    51  *
    52  * FIXME: Bug 822388 - Use the BreadcrumbsWidget in the Inspector.
    53  *
    54  * Mechanism:
    55  * . If no nodes displayed yet:
    56  *    then display the ancestor of the selected node and the selected node;
    57  *   else select the node;
    58  * . If the selected node is the last node displayed, append its first (if any).
    59  */
    60 function HTMLBreadcrumbs(aInspector)
    61 {
    62   this.inspector = aInspector;
    63   this.selection = this.inspector.selection;
    64   this.chromeWin = this.inspector.panelWin;
    65   this.chromeDoc = this.inspector.panelDoc;
    66   this.DOMHelpers = new DOMHelpers(this.chromeWin);
    67   this._init();
    68 }
    70 exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
    72 HTMLBreadcrumbs.prototype = {
    73   get walker() this.inspector.walker,
    75   _init: function BC__init()
    76   {
    77     this.container = this.chromeDoc.getElementById("inspector-breadcrumbs");
    79     // These separators are used for CSS purposes only, and are positioned
    80     // off screen, but displayed with -moz-element.
    81     this.separators = this.chromeDoc.createElement("box");
    82     this.separators.className = "breadcrumb-separator-container";
    83     this.separators.innerHTML =
    84                       "<box id='breadcrumb-separator-before'></box>" +
    85                       "<box id='breadcrumb-separator-after'></box>" +
    86                       "<box id='breadcrumb-separator-normal'></box>";
    87     this.container.parentNode.appendChild(this.separators);
    89     this.container.addEventListener("mousedown", this, true);
    90     this.container.addEventListener("keypress", this, true);
    92     // We will save a list of already displayed nodes in this array.
    93     this.nodeHierarchy = [];
    95     // Last selected node in nodeHierarchy.
    96     this.currentIndex = -1;
    98     // By default, hide the arrows. We let the <scrollbox> show them
    99     // in case of overflow.
   100     this.container.removeAttribute("overflows");
   101     this.container._scrollButtonUp.collapsed = true;
   102     this.container._scrollButtonDown.collapsed = true;
   104     this.onscrollboxreflow = function() {
   105       if (this.container._scrollButtonDown.collapsed)
   106         this.container.removeAttribute("overflows");
   107       else
   108         this.container.setAttribute("overflows", true);
   109     }.bind(this);
   111     this.container.addEventListener("underflow", this.onscrollboxreflow, false);
   112     this.container.addEventListener("overflow", this.onscrollboxreflow, false);
   114     this.update = this.update.bind(this);
   115     this.updateSelectors = this.updateSelectors.bind(this);
   116     this.selection.on("new-node-front", this.update);
   117     this.selection.on("pseudoclass", this.updateSelectors);
   118     this.selection.on("attribute-changed", this.updateSelectors);
   119     this.inspector.on("markupmutation", this.update);
   120     this.update();
   121   },
   123   /**
   124    * Include in a promise's then() chain to reject the chain
   125    * when the breadcrumbs' selection has changed while the promise
   126    * was outstanding.
   127    */
   128   selectionGuard: function() {
   129     let selection = this.selection.nodeFront;
   130     return (result) => {
   131       if (selection != this.selection.nodeFront) {
   132         return promise.reject("selection-changed");
   133       }
   134       return result;
   135     }
   136   },
   138   /**
   139    * Print any errors (except selection guard errors).
   140    */
   141   selectionGuardEnd: function(err) {
   142     if (err != "selection-changed") {
   143       console.error(err);
   144     }
   145     promise.reject(err);
   146   },
   148   /**
   149    * Build a string that represents the node: tagName#id.class1.class2.
   150    *
   151    * @param aNode The node to pretty-print
   152    * @returns a string
   153    */
   154   prettyPrintNodeAsText: function BC_prettyPrintNodeText(aNode)
   155   {
   156     let text = aNode.tagName.toLowerCase();
   157     if (aNode.id) {
   158       text += "#" + aNode.id;
   159     }
   161     if (aNode.className) {
   162       let classList = aNode.className.split(/\s+/);
   163       for (let i = 0; i < classList.length; i++) {
   164         text += "." + classList[i];
   165       }
   166     }
   168     for (let pseudo of aNode.pseudoClassLocks) {
   169       text += pseudo;
   170     }
   172     return text;
   173   },
   176   /**
   177    * Build <label>s that represent the node:
   178    *   <label class="breadcrumbs-widget-item-tag">tagName</label>
   179    *   <label class="breadcrumbs-widget-item-id">#id</label>
   180    *   <label class="breadcrumbs-widget-item-classes">.class1.class2</label>
   181    *
   182    * @param aNode The node to pretty-print
   183    * @returns a document fragment.
   184    */
   185   prettyPrintNodeAsXUL: function BC_prettyPrintNodeXUL(aNode)
   186   {
   187     let fragment = this.chromeDoc.createDocumentFragment();
   189     let tagLabel = this.chromeDoc.createElement("label");
   190     tagLabel.className = "breadcrumbs-widget-item-tag plain";
   192     let idLabel = this.chromeDoc.createElement("label");
   193     idLabel.className = "breadcrumbs-widget-item-id plain";
   195     let classesLabel = this.chromeDoc.createElement("label");
   196     classesLabel.className = "breadcrumbs-widget-item-classes plain";
   198     let pseudosLabel = this.chromeDoc.createElement("label");
   199     pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain";
   201     let tagText = aNode.tagName.toLowerCase();
   202     let idText = aNode.id ? ("#" + aNode.id) : "";
   203     let classesText = "";
   205     if (aNode.className) {
   206       let classList = aNode.className.split(/\s+/);
   207       for (let i = 0; i < classList.length; i++) {
   208         classesText += "." + classList[i];
   209       }
   210     }
   212     // XXX: Until we have pseudoclass lock in the node.
   213     for (let pseudo of aNode.pseudoClassLocks) {
   215     }
   217     // Figure out which element (if any) needs ellipsing.
   218     // Substring for that element, then clear out any extras
   219     // (except for pseudo elements).
   220     let maxTagLength = MAX_LABEL_LENGTH;
   221     let maxIdLength = MAX_LABEL_LENGTH - tagText.length;
   222     let maxClassLength = MAX_LABEL_LENGTH - tagText.length - idText.length;
   224     if (tagText.length > maxTagLength) {
   225        tagText = tagText.substr(0, maxTagLength) + ELLIPSIS;
   226        idText = classesText = "";
   227     } else if (idText.length > maxIdLength) {
   228        idText = idText.substr(0, maxIdLength) + ELLIPSIS;
   229        classesText = "";
   230     } else if (classesText.length > maxClassLength) {
   231       classesText = classesText.substr(0, maxClassLength) + ELLIPSIS;
   232     }
   234     tagLabel.textContent = tagText;
   235     idLabel.textContent = idText;
   236     classesLabel.textContent = classesText;
   237     pseudosLabel.textContent = aNode.pseudoClassLocks.join("");
   239     fragment.appendChild(tagLabel);
   240     fragment.appendChild(idLabel);
   241     fragment.appendChild(classesLabel);
   242     fragment.appendChild(pseudosLabel);
   244     return fragment;
   245   },
   247   /**
   248    * Open the sibling menu.
   249    *
   250    * @param aButton the button representing the node.
   251    * @param aNode the node we want the siblings from.
   252    */
   253   openSiblingMenu: function BC_openSiblingMenu(aButton, aNode)
   254   {
   255     // We make sure that the targeted node is selected
   256     // because we want to use the nodemenu that only works
   257     // for inspector.selection
   258     this.selection.setNodeFront(aNode, "breadcrumbs");
   260     let title = this.chromeDoc.createElement("menuitem");
   261     title.setAttribute("label", this.inspector.strings.GetStringFromName("breadcrumbs.siblings"));
   262     title.setAttribute("disabled", "true");
   264     let separator = this.chromeDoc.createElement("menuseparator");
   266     let items = [title, separator];
   268     this.walker.siblings(aNode, {
   269       whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
   270     }).then(siblings => {
   271       let nodes = siblings.nodes;
   272       for (let i = 0; i < nodes.length; i++) {
   273         let item = this.chromeDoc.createElement("menuitem");
   274         if (nodes[i] === aNode) {
   275           item.setAttribute("disabled", "true");
   276           item.setAttribute("checked", "true");
   277         }
   279         item.setAttribute("type", "radio");
   280         item.setAttribute("label", this.prettyPrintNodeAsText(nodes[i]));
   282         let selection = this.selection;
   283         item.onmouseup = (function(aNode) {
   284           return function() {
   285             selection.setNodeFront(aNode, "breadcrumbs");
   286           }
   287         })(nodes[i]);
   289         items.push(item);
   290         this.inspector.showNodeMenu(aButton, "before_start", items);
   291       }
   292     });
   293   },
   295   /**
   296    * Generic event handler.
   297    *
   298    * @param nsIDOMEvent event
   299    *        The DOM event object.
   300    */
   301   handleEvent: function BC_handleEvent(event)
   302   {
   303     if (event.type == "mousedown" && event.button == 0) {
   304       // on Click and Hold, open the Siblings menu
   306       let timer;
   307       let container = this.container;
   309       function openMenu(event) {
   310         cancelHold();
   311         let target = event.originalTarget;
   312         if (target.tagName == "button") {
   313           target.onBreadcrumbsHold();
   314         }
   315       }
   317       function handleClick(event) {
   318         cancelHold();
   319         let target = event.originalTarget;
   320         if (target.tagName == "button") {
   321           target.onBreadcrumbsClick();
   322         }
   323       }
   325       let window = this.chromeWin;
   326       function cancelHold(event) {
   327         window.clearTimeout(timer);
   328         container.removeEventListener("mouseout", cancelHold, false);
   329         container.removeEventListener("mouseup", handleClick, false);
   330       }
   332       container.addEventListener("mouseout", cancelHold, false);
   333       container.addEventListener("mouseup", handleClick, false);
   334       timer = window.setTimeout(openMenu, 500, event);
   335     }
   337     if (event.type == "keypress" && this.selection.isElementNode()) {
   338       let node = null;
   341       this._keyPromise = this._keyPromise || promise.resolve(null);
   343       this._keyPromise = (this._keyPromise || promise.resolve(null)).then(() => {
   344         switch (event.keyCode) {
   345           case this.chromeWin.KeyEvent.DOM_VK_LEFT:
   346             if (this.currentIndex != 0) {
   347               node = promise.resolve(this.nodeHierarchy[this.currentIndex - 1].node);
   348             }
   349             break;
   350           case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
   351             if (this.currentIndex < this.nodeHierarchy.length - 1) {
   352               node = promise.resolve(this.nodeHierarchy[this.currentIndex + 1].node);
   353             }
   354             break;
   355           case this.chromeWin.KeyEvent.DOM_VK_UP:
   356             node = this.walker.previousSibling(this.selection.nodeFront, {
   357               whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
   358             });
   359             break;
   360           case this.chromeWin.KeyEvent.DOM_VK_DOWN:
   361             node = this.walker.nextSibling(this.selection.nodeFront, {
   362               whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
   363             });
   364             break;
   365         }
   367         return node.then((node) => {
   368           if (node) {
   369             this.selection.setNodeFront(node, "breadcrumbs");
   370           }
   371         });
   372       });
   373       event.preventDefault();
   374       event.stopPropagation();
   375     }
   376   },
   378   /**
   379    * Remove nodes and delete properties.
   380    */
   381   destroy: function BC_destroy()
   382   {
   383     this.selection.off("new-node-front", this.update);
   384     this.selection.off("pseudoclass", this.updateSelectors);
   385     this.selection.off("attribute-changed", this.updateSelectors);
   386     this.inspector.off("markupmutation", this.update);
   388     this.container.removeEventListener("underflow", this.onscrollboxreflow, false);
   389     this.container.removeEventListener("overflow", this.onscrollboxreflow, false);
   390     this.onscrollboxreflow = null;
   392     this.empty();
   393     this.container.removeEventListener("mousedown", this, true);
   394     this.container.removeEventListener("keypress", this, true);
   395     this.container = null;
   397     this.separators.remove();
   398     this.separators = null;
   400     this.nodeHierarchy = null;
   401   },
   403   /**
   404    * Empty the breadcrumbs container.
   405    */
   406   empty: function BC_empty()
   407   {
   408     while (this.container.hasChildNodes()) {
   409       this.container.removeChild(this.container.firstChild);
   410     }
   411   },
   413   /**
   414    * Set which button represent the selected node.
   415    *
   416    * @param aIdx Index of the displayed-button to select
   417    */
   418   setCursor: function BC_setCursor(aIdx)
   419   {
   420     // Unselect the previously selected button
   421     if (this.currentIndex > -1 && this.currentIndex < this.nodeHierarchy.length) {
   422       this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked");
   423     }
   424     if (aIdx > -1) {
   425       this.nodeHierarchy[aIdx].button.setAttribute("checked", "true");
   426       if (this.hadFocus)
   427         this.nodeHierarchy[aIdx].button.focus();
   428     }
   429     this.currentIndex = aIdx;
   430   },
   432   /**
   433    * Get the index of the node in the cache.
   434    *
   435    * @param aNode
   436    * @returns integer the index, -1 if not found
   437    */
   438   indexOf: function BC_indexOf(aNode)
   439   {
   440     let i = this.nodeHierarchy.length - 1;
   441     for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
   442       if (this.nodeHierarchy[i].node === aNode) {
   443         return i;
   444       }
   445     }
   446     return -1;
   447   },
   449   /**
   450    * Remove all the buttons and their references in the cache
   451    * after a given index.
   452    *
   453    * @param aIdx
   454    */
   455   cutAfter: function BC_cutAfter(aIdx)
   456   {
   457     while (this.nodeHierarchy.length > (aIdx + 1)) {
   458       let toRemove = this.nodeHierarchy.pop();
   459       this.container.removeChild(toRemove.button);
   460     }
   461   },
   463   /**
   464    * Build a button representing the node.
   465    *
   466    * @param aNode The node from the page.
   467    * @returns aNode The <button>.
   468    */
   469   buildButton: function BC_buildButton(aNode)
   470   {
   471     let button = this.chromeDoc.createElement("button");
   472     button.appendChild(this.prettyPrintNodeAsXUL(aNode));
   473     button.className = "breadcrumbs-widget-item";
   475     button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(aNode));
   477     button.onkeypress = function onBreadcrumbsKeypress(e) {
   478       if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
   479           e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN)
   480         button.click();
   481     }
   483     button.onBreadcrumbsClick = function onBreadcrumbsClick() {
   484       this.selection.setNodeFront(aNode, "breadcrumbs");
   485     }.bind(this);
   487     button.onclick = (function _onBreadcrumbsRightClick(event) {
   488       button.focus();
   489       if (event.button == 2) {
   490         this.openSiblingMenu(button, aNode);
   491       }
   492     }).bind(this);
   494     button.onBreadcrumbsHold = (function _onBreadcrumbsHold() {
   495       this.openSiblingMenu(button, aNode);
   496     }).bind(this);
   497     return button;
   498   },
   500   /**
   501    * Connecting the end of the breadcrumbs to a node.
   502    *
   503    * @param aNode The node to reach.
   504    */
   505   expand: function BC_expand(aNode)
   506   {
   507       let fragment = this.chromeDoc.createDocumentFragment();
   508       let toAppend = aNode;
   509       let lastButtonInserted = null;
   510       let originalLength = this.nodeHierarchy.length;
   511       let stopNode = null;
   512       if (originalLength > 0) {
   513         stopNode = this.nodeHierarchy[originalLength - 1].node;
   514       }
   515       while (toAppend && toAppend != stopNode) {
   516         if (toAppend.tagName) {
   517           let button = this.buildButton(toAppend);
   518           fragment.insertBefore(button, lastButtonInserted);
   519           lastButtonInserted = button;
   520           this.nodeHierarchy.splice(originalLength, 0, {node: toAppend, button: button});
   521         }
   522         toAppend = toAppend.parentNode();
   523       }
   524       this.container.appendChild(fragment, this.container.firstChild);
   525   },
   527   /**
   528    * Get a child of a node that can be displayed in the breadcrumbs
   529    * and that is probably visible. See LOW_PRIORITY_ELEMENTS.
   530    *
   531    * @param aNode The parent node.
   532    * @returns nsIDOMNode|null
   533    */
   534   getInterestingFirstNode: function BC_getInterestingFirstNode(aNode)
   535   {
   536     let deferred = promise.defer();
   538     var fallback = null;
   540     var moreChildren = () => {
   541       this.walker.children(aNode, {
   542         start: fallback,
   543         maxNodes: 10,
   544         whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
   545       }).then(this.selectionGuard()).then(response => {
   546         for (let node of response.nodes) {
   547           if (!(node.tagName in LOW_PRIORITY_ELEMENTS)) {
   548             deferred.resolve(node);
   549             return;
   550           }
   551           if (!fallback) {
   552             fallback = node;
   553           }
   554         }
   555         if (response.hasLast) {
   556           deferred.resolve(fallback);
   557           return;
   558         } else {
   559           moreChildren();
   560         }
   561       }).then(null, this.selectionGuardEnd);
   562     }
   563     moreChildren();
   564     return deferred.promise;
   565   },
   567   /**
   568    * Find the "youngest" ancestor of a node which is already in the breadcrumbs.
   569    *
   570    * @param aNode
   571    * @returns Index of the ancestor in the cache
   572    */
   573   getCommonAncestor: function BC_getCommonAncestor(aNode)
   574   {
   575     let node = aNode;
   576     while (node) {
   577       let idx = this.indexOf(node);
   578       if (idx > -1) {
   579         return idx;
   580       } else {
   581         node = node.parentNode();
   582       }
   583     }
   584     return -1;
   585   },
   587   /**
   588    * Make sure that the latest node in the breadcrumbs is not the selected node
   589    * if the selected node still has children.
   590    */
   591   ensureFirstChild: function BC_ensureFirstChild()
   592   {
   593     // If the last displayed node is the selected node
   594     if (this.currentIndex == this.nodeHierarchy.length - 1) {
   595       let node = this.nodeHierarchy[this.currentIndex].node;
   596       return this.getInterestingFirstNode(node).then(child => {
   597         // If the node has a child
   598         if (child) {
   599           // Show this child
   600           this.expand(child);
   601         }
   602       });
   603     }
   605     return resolveNextTick(true);
   606   },
   608   /**
   609    * Ensure the selected node is visible.
   610    */
   611   scroll: function BC_scroll()
   612   {
   613     // FIXME bug 684352: make sure its immediate neighbors are visible too.
   615     let scrollbox = this.container;
   616     let element = this.nodeHierarchy[this.currentIndex].button;
   618     // Repeated calls to ensureElementIsVisible would interfere with each other
   619     // and may sometimes result in incorrect scroll positions.
   620     this.chromeWin.clearTimeout(this._ensureVisibleTimeout);
   621     this._ensureVisibleTimeout = this.chromeWin.setTimeout(function() {
   622       scrollbox.ensureElementIsVisible(element);
   623     }, ENSURE_SELECTION_VISIBLE_DELAY);
   624   },
   626   updateSelectors: function BC_updateSelectors()
   627   {
   628     for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
   629       let crumb = this.nodeHierarchy[i];
   630       let button = crumb.button;
   632       while(button.hasChildNodes()) {
   633         button.removeChild(button.firstChild);
   634       }
   635       button.appendChild(this.prettyPrintNodeAsXUL(crumb.node));
   636       button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node));
   637     }
   638   },
   640   /**
   641    * Update the breadcrumbs display when a new node is selected.
   642    */
   643   update: function BC_update(reason)
   644   {
   645     if (reason !== "markupmutation") {
   646       this.inspector.hideNodeMenu();
   647     }
   649     let cmdDispatcher = this.chromeDoc.commandDispatcher;
   650     this.hadFocus = (cmdDispatcher.focusedElement &&
   651                      cmdDispatcher.focusedElement.parentNode == this.container);
   653     if (!this.selection.isConnected()) {
   654       this.cutAfter(-1); // remove all the crumbs
   655       return;
   656     }
   658     if (!this.selection.isElementNode()) {
   659       this.setCursor(-1); // no selection
   660       return;
   661     }
   663     let idx = this.indexOf(this.selection.nodeFront);
   665     // Is the node already displayed in the breadcrumbs?
   666     // (and there are no mutations that need re-display of the crumbs)
   667     if (idx > -1 && reason !== "markupmutation") {
   668       // Yes. We select it.
   669       this.setCursor(idx);
   670     } else {
   671       // No. Is the breadcrumbs display empty?
   672       if (this.nodeHierarchy.length > 0) {
   673         // No. We drop all the element that are not direct ancestors
   674         // of the selection
   675         let parent = this.selection.nodeFront.parentNode();
   676         let idx = this.getCommonAncestor(parent);
   677         this.cutAfter(idx);
   678       }
   679       // we append the missing button between the end of the breadcrumbs display
   680       // and the current node.
   681       this.expand(this.selection.nodeFront);
   683       // we select the current node button
   684       idx = this.indexOf(this.selection.nodeFront);
   685       this.setCursor(idx);
   686     }
   688     let doneUpdating = this.inspector.updating("breadcrumbs");
   689     // Add the first child of the very last node of the breadcrumbs if possible.
   690     this.ensureFirstChild().then(this.selectionGuard()).then(() => {
   691       this.updateSelectors();
   693       // Make sure the selected node and its neighbours are visible.
   694       this.scroll();
   695       return resolveNextTick().then(() => {
   696         this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
   697         doneUpdating();
   698       });
   699     }).then(null, err => {
   700       doneUpdating(this.selection.nodeFront);
   701       this.selectionGuardEnd(err);
   702     });
   703   }
   704 };
   706 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   707   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
   708 });

mercurial