| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.HBox} |
| */ |
| WebInspector.ElementsBreadcrumbs = function() |
| { |
| WebInspector.HBox.call(this, true); |
| this.registerRequiredCSS("elements/breadcrumbs.css"); |
| |
| this.crumbsElement = this.contentElement.createChild("div", "crumbs"); |
| this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false); |
| this.crumbsElement.addEventListener("mouseleave", this._mouseMovedOutOfCrumbs.bind(this), false); |
| this._nodeSymbol = Symbol("node"); |
| } |
| |
| /** @enum {string} */ |
| WebInspector.ElementsBreadcrumbs.Events = { |
| NodeSelected: "NodeSelected" |
| } |
| |
| WebInspector.ElementsBreadcrumbs.prototype = { |
| wasShown: function() |
| { |
| this.update(); |
| }, |
| |
| /** |
| * @param {!Array.<!WebInspector.DOMNode>} nodes |
| */ |
| updateNodes: function(nodes) |
| { |
| if (!nodes.length) |
| return; |
| |
| var crumbs = this.crumbsElement; |
| for (var crumb = crumbs.firstChild; crumb; crumb = crumb.nextSibling) { |
| if (nodes.indexOf(crumb[this._nodeSymbol]) !== -1) { |
| this.update(true); |
| return; |
| } |
| } |
| }, |
| |
| /** |
| * @param {?WebInspector.DOMNode} node |
| */ |
| setSelectedNode: function(node) |
| { |
| this._currentDOMNode = node; |
| this.update(); |
| }, |
| |
| _mouseMovedInCrumbs: function(event) |
| { |
| var nodeUnderMouse = event.target; |
| var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb"); |
| var node = /** @type {?WebInspector.DOMNode} */ (crumbElement ? crumbElement[this._nodeSymbol] : null); |
| if (node) |
| node.highlight(); |
| }, |
| |
| _mouseMovedOutOfCrumbs: function(event) |
| { |
| if (this._currentDOMNode) |
| WebInspector.DOMModel.hideDOMNodeHighlight(); |
| }, |
| |
| /** |
| * @param {boolean=} force |
| */ |
| update: function(force) |
| { |
| if (!this.isShowing()) |
| return; |
| |
| var currentDOMNode = this._currentDOMNode; |
| var crumbs = this.crumbsElement; |
| |
| var handled = false; |
| var crumb = crumbs.firstChild; |
| while (crumb) { |
| if (crumb[this._nodeSymbol] === currentDOMNode) { |
| crumb.classList.add("selected"); |
| handled = true; |
| } else { |
| crumb.classList.remove("selected"); |
| } |
| |
| crumb = crumb.nextSibling; |
| } |
| |
| if (handled && !force) { |
| // We don't need to rebuild the crumbs, but we need to adjust sizes |
| // to reflect the new focused or root node. |
| this.updateSizes(); |
| return; |
| } |
| |
| crumbs.removeChildren(); |
| |
| var panel = this; |
| |
| /** |
| * @param {!Event} event |
| * @this {WebInspector.ElementsBreadcrumbs} |
| */ |
| function selectCrumb(event) |
| { |
| event.preventDefault(); |
| var crumb = /** @type {!Element} */ (event.currentTarget); |
| if (!crumb.classList.contains("collapsed")) { |
| this.dispatchEventToListeners(WebInspector.ElementsBreadcrumbs.Events.NodeSelected, crumb[this._nodeSymbol]); |
| return; |
| } |
| |
| // Clicking a collapsed crumb will expose the hidden crumbs. |
| if (crumb === panel.crumbsElement.firstChild) { |
| // If the focused crumb is the first child, pick the farthest crumb |
| // that is still hidden. This allows the user to expose every crumb. |
| var currentCrumb = crumb; |
| while (currentCrumb) { |
| var hidden = currentCrumb.classList.contains("hidden"); |
| var collapsed = currentCrumb.classList.contains("collapsed"); |
| if (!hidden && !collapsed) |
| break; |
| crumb = currentCrumb; |
| currentCrumb = currentCrumb.nextSiblingElement; |
| } |
| } |
| |
| this.updateSizes(crumb); |
| } |
| |
| var boundSelectCrumb = selectCrumb.bind(this); |
| for (var current = currentDOMNode; current; current = current.parentNode) { |
| if (current.nodeType() === Node.DOCUMENT_NODE) |
| continue; |
| |
| crumb = createElementWithClass("span", "crumb"); |
| crumb[this._nodeSymbol] = current; |
| crumb.addEventListener("mousedown", boundSelectCrumb, false); |
| |
| var crumbTitle = ""; |
| switch (current.nodeType()) { |
| case Node.ELEMENT_NODE: |
| if (current.pseudoType()) |
| crumbTitle = "::" + current.pseudoType(); |
| else |
| WebInspector.DOMPresentationUtils.decorateNodeLabel(current, crumb); |
| break; |
| |
| case Node.TEXT_NODE: |
| crumbTitle = WebInspector.UIString("(text)"); |
| break; |
| |
| case Node.COMMENT_NODE: |
| crumbTitle = "<!-->"; |
| break; |
| |
| case Node.DOCUMENT_TYPE_NODE: |
| crumbTitle = "<!DOCTYPE>"; |
| break; |
| |
| case Node.DOCUMENT_FRAGMENT_NODE: |
| crumbTitle = current.shadowRootType() ? "#shadow-root" : current.nodeNameInCorrectCase(); |
| break; |
| |
| default: |
| crumbTitle = current.nodeNameInCorrectCase(); |
| } |
| |
| if (!crumb.childNodes.length) { |
| var nameElement = createElement("span"); |
| nameElement.textContent = crumbTitle; |
| crumb.appendChild(nameElement); |
| crumb.title = crumbTitle; |
| } |
| |
| if (current === currentDOMNode) |
| crumb.classList.add("selected"); |
| crumbs.insertBefore(crumb, crumbs.firstChild); |
| } |
| |
| this.updateSizes(); |
| }, |
| |
| /** |
| * @param {!Element=} focusedCrumb |
| */ |
| updateSizes: function(focusedCrumb) |
| { |
| if (!this.isShowing()) |
| return; |
| |
| var crumbs = this.crumbsElement; |
| if (!crumbs.firstChild) |
| return; |
| |
| var selectedIndex = 0; |
| var focusedIndex = 0; |
| var selectedCrumb; |
| |
| // Reset crumb styles. |
| for (var i = 0; i < crumbs.childNodes.length; ++i) { |
| var crumb = crumbs.children[i]; |
| // Find the selected crumb and index. |
| if (!selectedCrumb && crumb.classList.contains("selected")) { |
| selectedCrumb = crumb; |
| selectedIndex = i; |
| } |
| |
| // Find the focused crumb index. |
| if (crumb === focusedCrumb) |
| focusedIndex = i; |
| |
| crumb.classList.remove("compact", "collapsed", "hidden"); |
| } |
| |
| // Layout 1: Measure total and normal crumb sizes |
| var contentElementWidth = this.contentElement.offsetWidth; |
| var normalSizes = []; |
| for (var i = 0; i < crumbs.childNodes.length; ++i) { |
| var crumb = crumbs.childNodes[i]; |
| normalSizes[i] = crumb.offsetWidth; |
| } |
| |
| // Layout 2: Measure collapsed crumb sizes |
| var compactSizes = []; |
| for (var i = 0; i < crumbs.childNodes.length; ++i) { |
| var crumb = crumbs.childNodes[i]; |
| crumb.classList.add("compact"); |
| } |
| for (var i = 0; i < crumbs.childNodes.length; ++i) { |
| var crumb = crumbs.childNodes[i]; |
| compactSizes[i] = crumb.offsetWidth; |
| } |
| |
| // Layout 3: Measure collapsed crumb size |
| crumbs.firstChild.classList.add("collapsed"); |
| var collapsedSize = crumbs.firstChild.offsetWidth; |
| |
| // Clean up. |
| for (var i = 0; i < crumbs.childNodes.length; ++i) { |
| var crumb = crumbs.childNodes[i]; |
| crumb.classList.remove("compact", "collapsed"); |
| } |
| |
| function crumbsAreSmallerThanContainer() |
| { |
| var totalSize = 0; |
| for (var i = 0; i < crumbs.childNodes.length; ++i) { |
| var crumb = crumbs.childNodes[i]; |
| if (crumb.classList.contains("hidden")) |
| continue; |
| if (crumb.classList.contains("collapsed")) { |
| totalSize += collapsedSize; |
| continue; |
| } |
| totalSize += crumb.classList.contains("compact") ? compactSizes[i] : normalSizes[i]; |
| } |
| const rightPadding = 10; |
| return totalSize + rightPadding < contentElementWidth; |
| } |
| |
| if (crumbsAreSmallerThanContainer()) |
| return; // No need to compact the crumbs, they all fit at full size. |
| |
| var BothSides = 0; |
| var AncestorSide = -1; |
| var ChildSide = 1; |
| |
| /** |
| * @param {function(!Element)} shrinkingFunction |
| * @param {number} direction |
| */ |
| function makeCrumbsSmaller(shrinkingFunction, direction) |
| { |
| var significantCrumb = focusedCrumb || selectedCrumb; |
| var significantIndex = significantCrumb === selectedCrumb ? selectedIndex : focusedIndex; |
| |
| function shrinkCrumbAtIndex(index) |
| { |
| var shrinkCrumb = crumbs.children[index]; |
| if (shrinkCrumb && shrinkCrumb !== significantCrumb) |
| shrinkingFunction(shrinkCrumb); |
| if (crumbsAreSmallerThanContainer()) |
| return true; // No need to compact the crumbs more. |
| return false; |
| } |
| |
| // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs |
| // fit in the container or we run out of crumbs to shrink. |
| if (direction) { |
| // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb. |
| var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1); |
| while (index !== significantIndex) { |
| if (shrinkCrumbAtIndex(index)) |
| return true; |
| index += (direction > 0 ? 1 : -1); |
| } |
| } else { |
| // Crumbs are shrunk in order of descending distance from the signifcant crumb, |
| // with a tie going to child crumbs. |
| var startIndex = 0; |
| var endIndex = crumbs.childNodes.length - 1; |
| while (startIndex !== significantIndex || endIndex !== significantIndex) { |
| var startDistance = significantIndex - startIndex; |
| var endDistance = endIndex - significantIndex; |
| if (startDistance >= endDistance) |
| var index = startIndex++; |
| else |
| var index = endIndex--; |
| if (shrinkCrumbAtIndex(index)) |
| return true; |
| } |
| } |
| |
| // We are not small enough yet, return false so the caller knows. |
| return false; |
| } |
| |
| function coalesceCollapsedCrumbs() |
| { |
| var crumb = crumbs.firstChild; |
| var collapsedRun = false; |
| var newStartNeeded = false; |
| var newEndNeeded = false; |
| while (crumb) { |
| var hidden = crumb.classList.contains("hidden"); |
| if (!hidden) { |
| var collapsed = crumb.classList.contains("collapsed"); |
| if (collapsedRun && collapsed) { |
| crumb.classList.add("hidden"); |
| crumb.classList.remove("compact"); |
| crumb.classList.remove("collapsed"); |
| |
| if (crumb.classList.contains("start")) { |
| crumb.classList.remove("start"); |
| newStartNeeded = true; |
| } |
| |
| if (crumb.classList.contains("end")) { |
| crumb.classList.remove("end"); |
| newEndNeeded = true; |
| } |
| |
| continue; |
| } |
| |
| collapsedRun = collapsed; |
| |
| if (newEndNeeded) { |
| newEndNeeded = false; |
| crumb.classList.add("end"); |
| } |
| } else { |
| collapsedRun = true; |
| } |
| crumb = crumb.nextSibling; |
| } |
| |
| if (newStartNeeded) { |
| crumb = crumbs.lastChild; |
| while (crumb) { |
| if (!crumb.classList.contains("hidden")) { |
| crumb.classList.add("start"); |
| break; |
| } |
| crumb = crumb.previousSibling; |
| } |
| } |
| } |
| |
| /** |
| * @param {!Element} crumb |
| */ |
| function compact(crumb) |
| { |
| if (crumb.classList.contains("hidden")) |
| return; |
| crumb.classList.add("compact"); |
| } |
| |
| /** |
| * @param {!Element} crumb |
| * @param {boolean=} dontCoalesce |
| */ |
| function collapse(crumb, dontCoalesce) |
| { |
| if (crumb.classList.contains("hidden")) |
| return; |
| crumb.classList.add("collapsed"); |
| crumb.classList.remove("compact"); |
| if (!dontCoalesce) |
| coalesceCollapsedCrumbs(); |
| } |
| |
| if (!focusedCrumb) { |
| // When not focused on a crumb we can be biased and collapse less important |
| // crumbs that the user might not care much about. |
| |
| // Compact child crumbs. |
| if (makeCrumbsSmaller(compact, ChildSide)) |
| return; |
| |
| // Collapse child crumbs. |
| if (makeCrumbsSmaller(collapse, ChildSide)) |
| return; |
| } |
| |
| // Compact ancestor crumbs, or from both sides if focused. |
| if (makeCrumbsSmaller(compact, focusedCrumb ? BothSides : AncestorSide)) |
| return; |
| |
| // Collapse ancestor crumbs, or from both sides if focused. |
| if (makeCrumbsSmaller(collapse, focusedCrumb ? BothSides : AncestorSide)) |
| return; |
| |
| if (!selectedCrumb) |
| return; |
| |
| // Compact the selected crumb. |
| compact(selectedCrumb); |
| if (crumbsAreSmallerThanContainer()) |
| return; |
| |
| // Collapse the selected crumb as a last resort. Pass true to prevent coalescing. |
| collapse(selectedCrumb, true); |
| }, |
| |
| __proto__: WebInspector.HBox.prototype |
| } |