1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 const flags = require("resource://devtools/shared/flags.js");
8 const { ELLIPSIS } = require("resource://devtools/shared/l10n.js");
9 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
11 loader.lazyRequireGetter(
14 "resource://devtools/client/shared/key-shortcuts.js"
17 const MAX_LABEL_LENGTH = 40;
19 const NS_XHTML = "http://www.w3.org/1999/xhtml";
20 const SCROLL_REPEAT_MS = 100;
22 // Some margin may be required for visible element detection.
23 const SCROLL_MARGIN = 1;
25 const SHADOW_ROOT_TAGNAME = "#shadow-root";
28 * Component to replicate functionality of XUL arrowscrollbox
31 * @param {Window} win The window containing the breadcrumbs
32 * @parem {DOMNode} container The element in which to put the scroll box
34 function ArrowScrollBox(win, container) {
36 this.doc = win.document;
37 this.container = container;
38 EventEmitter.decorate(this);
42 ArrowScrollBox.prototype = {
43 // Scroll behavior, exposed for testing
44 scrollBehavior: "smooth",
47 * Build the HTML, add to the DOM and start listening to
53 this.onScroll = this.onScroll.bind(this);
54 this.onStartBtnClick = this.onStartBtnClick.bind(this);
55 this.onEndBtnClick = this.onEndBtnClick.bind(this);
56 this.onStartBtnDblClick = this.onStartBtnDblClick.bind(this);
57 this.onEndBtnDblClick = this.onEndBtnDblClick.bind(this);
58 this.onUnderflow = this.onUnderflow.bind(this);
59 this.onOverflow = this.onOverflow.bind(this);
61 this.inner.addEventListener("scroll", this.onScroll);
62 this.startBtn.addEventListener("mousedown", this.onStartBtnClick);
63 this.endBtn.addEventListener("mousedown", this.onEndBtnClick);
64 this.startBtn.addEventListener("dblclick", this.onStartBtnDblClick);
65 this.endBtn.addEventListener("dblclick", this.onEndBtnDblClick);
67 // Overflow and underflow are moz specific events
68 this.inner.addEventListener("underflow", this.onUnderflow);
69 this.inner.addEventListener("overflow", this.onOverflow);
73 * Scroll to the specified element using the current scroll behavior
74 * @param {Element} element element to scroll
75 * @param {String} block desired alignment of element after scrolling
77 scrollToElement(element, block) {
78 element.scrollIntoView({ block, behavior: this.scrollBehavior });
82 * Call the given function once; then continuously
83 * while the mouse button is held
84 * @param {Function} repeatFn the function to repeat while the button is held
86 clickOrHold(repeatFn) {
88 const container = this.container;
90 function handleClick() {
95 const window = this.win;
96 function cancelHold() {
97 window.clearTimeout(timer);
98 container.removeEventListener("mouseout", cancelHold);
99 container.removeEventListener("mouseup", handleClick);
102 function repeated() {
104 timer = window.setTimeout(repeated, SCROLL_REPEAT_MS);
107 container.addEventListener("mouseout", cancelHold);
108 container.addEventListener("mouseup", handleClick);
109 timer = window.setTimeout(repeated, SCROLL_REPEAT_MS);
113 * When start button is dbl clicked scroll to first element
115 onStartBtnDblClick() {
116 const children = this.inner.childNodes;
117 if (children.length < 1) {
121 const element = this.inner.childNodes[0];
122 this.scrollToElement(element, "start");
126 * When end button is dbl clicked scroll to last element
129 const children = this.inner.childNodes;
130 if (children.length < 1) {
134 const element = children[children.length - 1];
135 this.scrollToElement(element, "start");
139 * When start arrow button is clicked scroll towards first element
142 const scrollToStart = () => {
143 const element = this.getFirstInvisibleElement();
148 this.scrollToElement(element, "start");
151 this.clickOrHold(scrollToStart);
155 * When end arrow button is clicked scroll towards last element
158 const scrollToEnd = () => {
159 const element = this.getLastInvisibleElement();
164 this.scrollToElement(element, "end");
167 this.clickOrHold(scrollToEnd);
171 * Event handler for scrolling, update the
172 * enabled/disabled status of the arrow buttons
175 const first = this.getFirstInvisibleElement();
177 this.startBtn.setAttribute("disabled", "true");
179 this.startBtn.removeAttribute("disabled");
182 const last = this.getLastInvisibleElement();
184 this.endBtn.setAttribute("disabled", "true");
186 this.endBtn.removeAttribute("disabled");
191 * On underflow, make the arrow buttons invisible
194 this.startBtn.style.visibility = "collapse";
195 this.endBtn.style.visibility = "collapse";
196 this.emit("underflow");
200 * On overflow, show the arrow buttons
203 this.startBtn.style.visibility = "visible";
204 this.endBtn.style.visibility = "visible";
205 this.emit("overflow");
209 * Check whether the element is to the left of its container but does
210 * not also span the entire container.
211 * @param {Number} left the left scroll point of the container
212 * @param {Number} right the right edge of the container
213 * @param {Number} elementLeft the left edge of the element
214 * @param {Number} elementRight the right edge of the element
216 elementLeftOfContainer(left, right, elementLeft, elementRight) {
218 elementLeft < left - SCROLL_MARGIN && elementRight < right - SCROLL_MARGIN
223 * Check whether the element is to the right of its container but does
224 * not also span the entire container.
225 * @param {Number} left the left scroll point of the container
226 * @param {Number} right the right edge of the container
227 * @param {Number} elementLeft the left edge of the element
228 * @param {Number} elementRight the right edge of the element
230 elementRightOfContainer(left, right, elementLeft, elementRight) {
232 elementLeft > left + SCROLL_MARGIN && elementRight > right + SCROLL_MARGIN
237 * Get the first (i.e. furthest left for LTR)
238 * non or partly visible element in the scroll box
240 getFirstInvisibleElement() {
241 const elementsList = Array.from(this.inner.childNodes).reverse();
243 const predicate = this.elementLeftOfContainer;
244 return this.findFirstWithBounds(elementsList, predicate);
248 * Get the last (i.e. furthest right for LTR)
249 * non or partly visible element in the scroll box
251 getLastInvisibleElement() {
252 const predicate = this.elementRightOfContainer;
253 return this.findFirstWithBounds(this.inner.childNodes, predicate);
257 * Find the first element that matches the given predicate, called with bounds
259 * @param {Array} elements an ordered list of elements
260 * @param {Function} predicate a function to be called with bounds
263 findFirstWithBounds(elements, predicate) {
264 const left = this.inner.scrollLeft;
265 const right = left + this.inner.clientWidth;
266 for (const element of elements) {
267 const elementLeft = element.offsetLeft - element.parentElement.offsetLeft;
268 const elementRight = elementLeft + element.offsetWidth;
270 // Check that the starting edge of the element is out of the visible area
271 // and that the ending edge does not span the whole container
272 if (predicate(left, right, elementLeft, elementRight)) {
281 * Build the HTML for the scroll box and insert it into the DOM
284 this.startBtn = this.createElement(
289 this.createElement("div", "toolbarbutton-icon", this.startBtn);
293 "arrowscrollbox-overflow-start-indicator",
296 this.inner = this.createElement(
298 "html-arrowscrollbox-inner",
303 "arrowscrollbox-overflow-end-indicator",
307 this.endBtn = this.createElement(
312 this.createElement("div", "toolbarbutton-icon", this.endBtn);
316 * Create an XHTML element with the given class name, and append it to the
318 * @param {String} tagName name of the tag to create
319 * @param {String} className class of the element
320 * @param {DOMNode} parent the parent node to which it should be appended
321 * @return {DOMNode} The new element
323 createElement(tagName, className, parent) {
324 const el = this.doc.createElementNS(NS_XHTML, tagName);
325 el.className = className;
327 parent.appendChild(el);
334 * Remove event handlers and clean up
337 this.inner.removeEventListener("scroll", this.onScroll);
338 this.startBtn.removeEventListener("mousedown", this.onStartBtnClick);
339 this.endBtn.removeEventListener("mousedown", this.onEndBtnClick);
340 this.startBtn.removeEventListener("dblclick", this.onStartBtnDblClick);
341 this.endBtn.removeEventListener("dblclick", this.onRightBtnDblClick);
343 // Overflow and underflow are moz specific events
344 this.inner.removeEventListener("underflow", this.onUnderflow);
345 this.inner.removeEventListener("overflow", this.onOverflow);
350 * Display the ancestors of the current node and its children.
351 * Only one "branch" of children are displayed (only one line).
354 * - If no nodes displayed yet:
355 * then display the ancestor of the selected node and the selected node;
356 * else select the node;
357 * - If the selected node is the last node displayed, append its first (if any).
359 * @param {InspectorPanel} inspector The inspector hosting this widget.
361 function HTMLBreadcrumbs(inspector) {
362 this.inspector = inspector;
363 this.selection = this.inspector.selection;
364 this.win = this.inspector.panelWin;
365 this.doc = this.inspector.panelDoc;
369 exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
371 HTMLBreadcrumbs.prototype = {
373 return this.inspector.walker;
377 this.outer = this.doc.getElementById("inspector-breadcrumbs");
378 this.arrowScrollBox = new ArrowScrollBox(this.win, this.outer);
380 this.container = this.arrowScrollBox.inner;
381 this.scroll = this.scroll.bind(this);
382 this.arrowScrollBox.on("overflow", this.scroll);
384 this.outer.addEventListener("click", this, true);
385 this.outer.addEventListener("mouseover", this, true);
386 this.outer.addEventListener("mouseout", this, true);
387 this.outer.addEventListener("focus", this, true);
389 this.handleShortcut = this.handleShortcut.bind(this);
392 // In tests, we start listening immediately to avoid having to simulate a focus.
393 this.initKeyShortcuts();
395 this.outer.addEventListener(
398 this.initKeyShortcuts();
404 // We will save a list of already displayed nodes in this array.
405 this.nodeHierarchy = [];
407 // Last selected node in nodeHierarchy.
408 this.currentIndex = -1;
410 // Used to build a unique breadcrumb button Id.
411 this.breadcrumbsWidgetItemId = 0;
413 this.update = this.update.bind(this);
414 this.updateWithMutations = this.updateWithMutations.bind(this);
415 this.updateSelectors = this.updateSelectors.bind(this);
416 this.selection.on("new-node-front", this.update);
417 this.selection.on("pseudoclass", this.updateSelectors);
418 this.selection.on("attribute-changed", this.updateSelectors);
419 this.inspector.on("markupmutation", this.updateWithMutations);
424 this.shortcuts = new KeyShortcuts({ window: this.win, target: this.outer });
425 this.shortcuts.on("Right", this.handleShortcut);
426 this.shortcuts.on("Left", this.handleShortcut);
430 * Build a string that represents the node: tagName#id.class1.class2.
431 * @param {NodeFront} node The node to pretty-print
434 prettyPrintNodeAsText(node) {
435 let text = node.isShadowRoot ? SHADOW_ROOT_TAGNAME : node.displayName;
436 if (node.isMarkerPseudoElement) {
438 } else if (node.isBeforePseudoElement) {
440 } else if (node.isAfterPseudoElement) {
445 text += "#" + node.id;
448 if (node.className) {
449 const classList = node.className.split(/\s+/);
450 for (let i = 0; i < classList.length; i++) {
451 text += "." + classList[i];
455 for (const pseudo of node.pseudoClassLocks) {
463 * Build <span>s that represent the node:
464 * <span class="breadcrumbs-widget-item-tag">tagName</span>
465 * <span class="breadcrumbs-widget-item-id">#id</span>
466 * <span class="breadcrumbs-widget-item-classes">.class1.class2</span>
467 * @param {NodeFront} node The node to pretty-print
468 * @returns {DocumentFragment}
470 prettyPrintNodeAsXHTML(node) {
471 const tagLabel = this.doc.createElementNS(NS_XHTML, "span");
472 tagLabel.className = "breadcrumbs-widget-item-tag plain";
474 const idLabel = this.doc.createElementNS(NS_XHTML, "span");
475 idLabel.className = "breadcrumbs-widget-item-id plain";
477 const classesLabel = this.doc.createElementNS(NS_XHTML, "span");
478 classesLabel.className = "breadcrumbs-widget-item-classes plain";
480 const pseudosLabel = this.doc.createElementNS(NS_XHTML, "span");
481 pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain";
483 let tagText = node.isShadowRoot ? SHADOW_ROOT_TAGNAME : node.displayName;
484 if (node.isMarkerPseudoElement) {
485 tagText = "::marker";
486 } else if (node.isBeforePseudoElement) {
487 tagText = "::before";
488 } else if (node.isAfterPseudoElement) {
491 let idText = node.id ? "#" + node.id : "";
492 let classesText = "";
494 if (node.className) {
495 const classList = node.className.split(/\s+/);
496 for (let i = 0; i < classList.length; i++) {
497 classesText += "." + classList[i];
501 // Figure out which element (if any) needs ellipsing.
502 // Substring for that element, then clear out any extras
503 // (except for pseudo elements).
504 const maxTagLength = MAX_LABEL_LENGTH;
505 const maxIdLength = MAX_LABEL_LENGTH - tagText.length;
506 const maxClassLength = MAX_LABEL_LENGTH - tagText.length - idText.length;
508 if (tagText.length > maxTagLength) {
509 tagText = tagText.substr(0, maxTagLength) + ELLIPSIS;
510 idText = classesText = "";
511 } else if (idText.length > maxIdLength) {
512 idText = idText.substr(0, maxIdLength) + ELLIPSIS;
514 } else if (classesText.length > maxClassLength) {
515 classesText = classesText.substr(0, maxClassLength) + ELLIPSIS;
518 tagLabel.textContent = tagText;
519 idLabel.textContent = idText;
520 classesLabel.textContent = classesText;
521 pseudosLabel.textContent = node.pseudoClassLocks.join("");
523 const fragment = this.doc.createDocumentFragment();
524 fragment.appendChild(tagLabel);
525 fragment.appendChild(idLabel);
526 fragment.appendChild(classesLabel);
527 fragment.appendChild(pseudosLabel);
533 * Generic event handler.
534 * @param {DOMEvent} event.
537 if (event.type == "click" && event.button == 0) {
538 this.handleClick(event);
539 } else if (event.type == "mouseover") {
540 this.handleMouseOver(event);
541 } else if (event.type == "mouseout") {
542 this.handleMouseOut(event);
543 } else if (event.type == "focus") {
544 this.handleFocus(event);
549 * Focus event handler. When breadcrumbs container gets focus,
550 * aria-activedescendant needs to be updated to currently selected
551 * breadcrumb. Ensures that the focus stays on the container at all times.
552 * @param {DOMEvent} event.
555 event.stopPropagation();
557 const node = this.nodeHierarchy[this.currentIndex];
559 this.outer.setAttribute("aria-activedescendant", node.button.id);
561 this.outer.removeAttribute("aria-activedescendant");
568 * On click navigate to the correct node.
569 * @param {DOMEvent} event.
572 const target = event.originalTarget;
573 if (target.tagName == "button") {
574 target.onBreadcrumbsClick();
579 * On mouse over, highlight the corresponding content DOM Node.
580 * @param {DOMEvent} event.
582 handleMouseOver(event) {
583 const target = event.originalTarget;
584 if (target.tagName == "button") {
585 target.onBreadcrumbsHover();
590 * On mouse out, make sure to unhighlight.
591 * @param {DOMEvent} event.
593 handleMouseOut(event) {
594 this.inspector.highlighters.hideHighlighterType(
595 this.inspector.highlighters.TYPES.BOXMODEL
600 * Handle a keyboard shortcut supported by the breadcrumbs widget.
602 * @param {String} name
603 * Name of the keyboard shortcut received.
604 * @param {DOMEvent} event
605 * Original event that triggered the shortcut.
607 handleShortcut(event) {
608 if (!this.selection.isElementNode()) {
612 event.preventDefault();
613 event.stopPropagation();
615 this.keyPromise = (this.keyPromise || Promise.resolve(null)).then(() => {
618 const isLeft = event.code === "ArrowLeft";
619 const isRight = event.code === "ArrowRight";
621 if (isLeft && this.currentIndex != 0) {
622 currentnode = this.nodeHierarchy[this.currentIndex - 1];
623 } else if (isRight && this.currentIndex < this.nodeHierarchy.length - 1) {
624 currentnode = this.nodeHierarchy[this.currentIndex + 1];
629 this.outer.setAttribute("aria-activedescendant", currentnode.button.id);
630 return this.selection.setNodeFront(currentnode.node, {
631 reason: "breadcrumbs",
637 * Remove nodes and clean up.
640 this.selection.off("new-node-front", this.update);
641 this.selection.off("pseudoclass", this.updateSelectors);
642 this.selection.off("attribute-changed", this.updateSelectors);
643 this.inspector.off("markupmutation", this.updateWithMutations);
645 this.container.removeEventListener("click", this, true);
646 this.container.removeEventListener("mouseover", this, true);
647 this.container.removeEventListener("mouseout", this, true);
648 this.container.removeEventListener("focus", this, true);
650 if (this.shortcuts) {
651 this.shortcuts.destroy();
656 this.arrowScrollBox.off("overflow", this.scroll);
657 this.arrowScrollBox.destroy();
658 this.arrowScrollBox = null;
660 this.container = null;
661 this.nodeHierarchy = null;
663 this.isDestroyed = true;
667 * Empty the breadcrumbs container.
670 while (this.container.hasChildNodes()) {
671 this.container.firstChild.remove();
676 * Set which button represent the selected node.
677 * @param {Number} index Index of the displayed-button to select.
680 // Unselect the previously selected button
682 this.currentIndex > -1 &&
683 this.currentIndex < this.nodeHierarchy.length
685 this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked");
688 this.nodeHierarchy[index].button.setAttribute("checked", "true");
690 // Unset active active descendant when all buttons are unselected.
691 this.outer.removeAttribute("aria-activedescendant");
693 this.currentIndex = index;
697 * Get the index of the node in the cache.
698 * @param {NodeFront} node.
699 * @returns {Number} The index for this node or -1 if not found.
702 for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
703 if (this.nodeHierarchy[i].node === node) {
711 * Remove all the buttons and their references in the cache after a given
713 * @param {Number} index.
716 while (this.nodeHierarchy.length > index + 1) {
717 const toRemove = this.nodeHierarchy.pop();
718 this.container.removeChild(toRemove.button);
723 * Build a button representing the node.
724 * @param {NodeFront} node The node from the page.
725 * @return {DOMNode} The <button> for this node.
728 const button = this.doc.createElementNS(NS_XHTML, "button");
729 button.appendChild(this.prettyPrintNodeAsXHTML(node));
730 button.className = "breadcrumbs-widget-item";
731 button.id = "breadcrumbs-widget-item-" + this.breadcrumbsWidgetItemId++;
733 button.setAttribute("tabindex", "-1");
734 button.setAttribute("title", this.prettyPrintNodeAsText(node));
736 button.onclick = () => {
740 button.onBreadcrumbsClick = () => {
741 this.selection.setNodeFront(node, { reason: "breadcrumbs" });
744 button.onBreadcrumbsHover = () => {
745 this.inspector.highlighters.showHighlighterTypeForNode(
746 this.inspector.highlighters.TYPES.BOXMODEL,
755 * Connecting the end of the breadcrumbs to a node.
756 * @param {NodeFront} node The node to reach.
759 const fragment = this.doc.createDocumentFragment();
760 let lastButtonInserted = null;
761 const originalLength = this.nodeHierarchy.length;
763 if (originalLength > 0) {
764 stopNode = this.nodeHierarchy[originalLength - 1].node;
766 while (node && node != stopNode) {
767 if (node.tagName || node.isShadowRoot) {
768 const button = this.buildButton(node);
769 fragment.insertBefore(button, lastButtonInserted);
770 lastButtonInserted = button;
771 this.nodeHierarchy.splice(originalLength, 0, {
774 currentPrettyPrintText: this.prettyPrintNodeAsText(node),
777 node = node.parentOrHost();
779 this.container.appendChild(fragment, this.container.firstChild);
783 * Find the "youngest" ancestor of a node which is already in the breadcrumbs.
784 * @param {NodeFront} node.
785 * @return {Number} Index of the ancestor in the cache, or -1 if not found.
787 getCommonAncestor(node) {
789 const idx = this.indexOf(node);
793 node = node.parentNode();
799 * Ensure the selected node is visible.
802 // FIXME bug 684352: make sure its immediate neighbors are visible too.
803 if (!this.isDestroyed) {
804 const element = this.nodeHierarchy[this.currentIndex].button;
805 this.arrowScrollBox.scrollToElement(element, "end");
810 * Update all button outputs.
813 if (this.isDestroyed) {
817 for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
818 const { node, button, currentPrettyPrintText } = this.nodeHierarchy[i];
820 // If the output of the node doesn't change, skip the update.
821 const textOutput = this.prettyPrintNodeAsText(node);
822 if (currentPrettyPrintText === textOutput) {
826 // Otherwise, update the whole markup for the button.
827 while (button.hasChildNodes()) {
828 button.firstChild.remove();
830 button.appendChild(this.prettyPrintNodeAsXHTML(node));
831 button.setAttribute("title", textOutput);
833 this.nodeHierarchy[i].currentPrettyPrintText = textOutput;
838 * Given a list of mutation changes (passed by the markupmutation event),
839 * decide whether or not they are "interesting" to the current state of the
840 * breadcrumbs widget, i.e. at least one of them should cause part of the
841 * widget to be updated.
842 * @param {Array} mutations The mutations array.
845 _hasInterestingMutations(mutations) {
846 if (!mutations || !mutations.length) {
850 for (const { type, added, removed, target, attributeName } of mutations) {
851 if (type === "childList") {
852 // Only interested in childList mutations if the added or removed
853 // nodes are currently displayed.
855 added.some(node => this.indexOf(node) > -1) ||
856 removed.some(node => this.indexOf(node) > -1)
858 } else if (type === "attributes" && this.indexOf(target) > -1) {
859 // Only interested in attributes mutations if the target is
860 // currently displayed, and the attribute is either id or class.
861 return attributeName === "class" || attributeName === "id";
865 // Catch all return in case the mutations array was empty, or in case none
866 // of the changes iterated above were interesting.
871 * Update the breadcrumbs display when a new node is selected and there are
873 * @param {Array} mutations An array of mutations in case this was called as
874 * the "markupmutation" event listener.
876 updateWithMutations(mutations) {
877 return this.update("markupmutation", mutations);
881 * Update the breadcrumbs display when a new node is selected.
882 * @param {String} reason The reason for the update, if any.
883 * @param {Array} mutations An array of mutations in case this was called as
884 * the "markupmutation" event listener.
886 update(reason, mutations) {
887 if (this.isDestroyed) {
891 const hasInterestingMutations = this._hasInterestingMutations(mutations);
892 if (reason === "markupmutation" && !hasInterestingMutations) {
896 if (!this.selection.isConnected()) {
897 // remove all the crumbs
902 // If this was an interesting deletion; then trim the breadcrumb trail
904 if (reason === "markupmutation") {
905 for (const { type, removed } of mutations) {
906 if (type !== "childList") {
910 for (const node of removed) {
911 const removedIndex = this.indexOf(node);
912 if (removedIndex > -1) {
913 this.cutAfter(removedIndex - 1);
920 if (!this.selection.isElementNode() && !this.selection.isShadowRootNode()) {
924 // Since something changed, notify the interested parties.
925 this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
930 let idx = this.indexOf(this.selection.nodeFront);
932 // Is the node already displayed in the breadcrumbs?
933 // (and there are no mutations that need re-display of the crumbs)
934 if (idx > -1 && !hasInterestingMutations) {
935 // Yes. We select it.
938 // No. Is the breadcrumbs display empty?
939 if (this.nodeHierarchy.length) {
940 // No. We drop all the element that are not direct ancestors
942 const parent = this.selection.nodeFront.parentNode();
943 const ancestorIdx = this.getCommonAncestor(parent);
944 this.cutAfter(ancestorIdx);
946 // we append the missing button between the end of the breadcrumbs display
947 // and the current node.
948 this.expand(this.selection.nodeFront);
950 // we select the current node button
951 idx = this.indexOf(this.selection.nodeFront);
955 const doneUpdating = this.inspector.updating("breadcrumbs");
957 this.updateSelectors();
959 // Make sure the selected node and its neighbours are visible.
963 this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
966 // Only log this as an error if we haven't been destroyed in the meantime.
967 if (!this.isDestroyed) {