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 +});