1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 * @extends {WebInspector.HBox}
9 WebInspector.ElementsBreadcrumbs = function()
11 WebInspector.HBox.call(this, true);
12 this.registerRequiredCSS("elements/breadcrumbs.css");
14 this.crumbsElement = this.contentElement.createChild("div", "crumbs");
15 this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false);
16 this.crumbsElement.addEventListener("mouseleave", this._mouseMovedOutOfCrumbs.bind(this), false);
17 this._nodeSymbol = Symbol("node");
21 WebInspector.ElementsBreadcrumbs.Events = {
22 NodeSelected: "NodeSelected"
25 WebInspector.ElementsBreadcrumbs.prototype = {
32 * @param {!Array.<!WebInspector.DOMNode>} nodes
34 updateNodes: function(nodes)
39 var crumbs = this.crumbsElement;
40 for (var crumb = crumbs.firstChild; crumb; crumb = crumb.nextSibling) {
41 if (nodes.indexOf(crumb[this._nodeSymbol]) !== -1) {
49 * @param {?WebInspector.DOMNode} node
51 setSelectedNode: function(node)
53 this._currentDOMNode = node;
57 _mouseMovedInCrumbs: function(event)
59 var nodeUnderMouse = event.target;
60 var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb");
61 var node = /** @type {?WebInspector.DOMNode} */ (crumbElement ? crumbElement[this._nodeSymbol] : null);
66 _mouseMovedOutOfCrumbs: function(event)
68 if (this._currentDOMNode)
69 WebInspector.DOMModel.hideDOMNodeHighlight();
73 * @param {boolean=} force
75 update: function(force)
77 if (!this.isShowing())
80 var currentDOMNode = this._currentDOMNode;
81 var crumbs = this.crumbsElement;
84 var crumb = crumbs.firstChild;
86 if (crumb[this._nodeSymbol] === currentDOMNode) {
87 crumb.classList.add("selected");
90 crumb.classList.remove("selected");
93 crumb = crumb.nextSibling;
96 if (handled && !force) {
97 // We don't need to rebuild the crumbs, but we need to adjust sizes
98 // to reflect the new focused or root node.
103 crumbs.removeChildren();
108 * @param {!Event} event
109 * @this {WebInspector.ElementsBreadcrumbs}
111 function selectCrumb(event)
113 event.preventDefault();
114 var crumb = /** @type {!Element} */ (event.currentTarget);
115 if (!crumb.classList.contains("collapsed")) {
116 this.dispatchEventToListeners(WebInspector.ElementsBreadcrumbs.Events.NodeSelected, crumb[this._nodeSymbol]);
120 // Clicking a collapsed crumb will expose the hidden crumbs.
121 if (crumb === panel.crumbsElement.firstChild) {
122 // If the focused crumb is the first child, pick the farthest crumb
123 // that is still hidden. This allows the user to expose every crumb.
124 var currentCrumb = crumb;
125 while (currentCrumb) {
126 var hidden = currentCrumb.classList.contains("hidden");
127 var collapsed = currentCrumb.classList.contains("collapsed");
128 if (!hidden && !collapsed)
130 crumb = currentCrumb;
131 currentCrumb = currentCrumb.nextSiblingElement;
135 this.updateSizes(crumb);
138 var boundSelectCrumb = selectCrumb.bind(this);
139 for (var current = currentDOMNode; current; current = current.parentNode) {
140 if (current.nodeType() === Node.DOCUMENT_NODE)
143 crumb = createElementWithClass("span", "crumb");
144 crumb[this._nodeSymbol] = current;
145 crumb.addEventListener("mousedown", boundSelectCrumb, false);
148 switch (current.nodeType()) {
149 case Node.ELEMENT_NODE:
150 if (current.pseudoType())
151 crumbTitle = "::" + current.pseudoType();
153 WebInspector.DOMPresentationUtils.decorateNodeLabel(current, crumb);
157 crumbTitle = WebInspector.UIString("(text)");
160 case Node.COMMENT_NODE:
161 crumbTitle = "<!-->";
164 case Node.DOCUMENT_TYPE_NODE:
165 crumbTitle = "<!DOCTYPE>";
168 case Node.DOCUMENT_FRAGMENT_NODE:
169 crumbTitle = current.shadowRootType() ? "#shadow-root" : current.nodeNameInCorrectCase();
173 crumbTitle = current.nodeNameInCorrectCase();
176 if (!crumb.childNodes.length) {
177 var nameElement = createElement("span");
178 nameElement.textContent = crumbTitle;
179 crumb.appendChild(nameElement);
180 crumb.title = crumbTitle;
183 if (current === currentDOMNode)
184 crumb.classList.add("selected");
185 crumbs.insertBefore(crumb, crumbs.firstChild);
192 * @param {!Element=} focusedCrumb
194 updateSizes: function(focusedCrumb)
196 if (!this.isShowing())
199 var crumbs = this.crumbsElement;
200 if (!crumbs.firstChild)
203 var selectedIndex = 0;
204 var focusedIndex = 0;
207 // Reset crumb styles.
208 for (var i = 0; i < crumbs.childNodes.length; ++i) {
209 var crumb = crumbs.childNodes[i];
210 // Find the selected crumb and index.
211 if (!selectedCrumb && crumb.classList.contains("selected")) {
212 selectedCrumb = crumb;
216 // Find the focused crumb index.
217 if (crumb === focusedCrumb)
220 crumb.classList.remove("compact", "collapsed", "hidden");
223 // Layout 1: Measure total and normal crumb sizes
224 var contentElementWidth = this.contentElement.offsetWidth;
225 var normalSizes = [];
226 for (var i = 0; i < crumbs.childNodes.length; ++i) {
227 var crumb = crumbs.childNodes[i];
228 normalSizes[i] = crumb.offsetWidth;
231 // Layout 2: Measure collapsed crumb sizes
232 var compactSizes = [];
233 for (var i = 0; i < crumbs.childNodes.length; ++i) {
234 var crumb = crumbs.childNodes[i];
235 crumb.classList.add("compact");
237 for (var i = 0; i < crumbs.childNodes.length; ++i) {
238 var crumb = crumbs.childNodes[i];
239 compactSizes[i] = crumb.offsetWidth;
242 // Layout 3: Measure collapsed crumb size
243 crumbs.firstChild.classList.add("collapsed");
244 var collapsedSize = crumbs.firstChild.offsetWidth;
247 for (var i = 0; i < crumbs.childNodes.length; ++i) {
248 var crumb = crumbs.childNodes[i];
249 crumb.classList.remove("compact", "collapsed");
252 function crumbsAreSmallerThanContainer()
255 for (var i = 0; i < crumbs.childNodes.length; ++i) {
256 var crumb = crumbs.childNodes[i];
257 if (crumb.classList.contains("hidden"))
259 if (crumb.classList.contains("collapsed")) {
260 totalSize += collapsedSize;
263 totalSize += crumb.classList.contains("compact") ? compactSizes[i] : normalSizes[i];
265 const rightPadding = 10;
266 return totalSize + rightPadding < contentElementWidth;
269 if (crumbsAreSmallerThanContainer())
270 return; // No need to compact the crumbs, they all fit at full size.
273 var AncestorSide = -1;
277 * @param {function(!Element)} shrinkingFunction
278 * @param {number} direction
280 function makeCrumbsSmaller(shrinkingFunction, direction)
282 var significantCrumb = focusedCrumb || selectedCrumb;
283 var significantIndex = significantCrumb === selectedCrumb ? selectedIndex : focusedIndex;
285 function shrinkCrumbAtIndex(index)
287 var shrinkCrumb = crumbs.childNodes[index];
288 if (shrinkCrumb && shrinkCrumb !== significantCrumb)
289 shrinkingFunction(shrinkCrumb);
290 if (crumbsAreSmallerThanContainer())
291 return true; // No need to compact the crumbs more.
295 // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
296 // fit in the container or we run out of crumbs to shrink.
298 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
299 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
300 while (index !== significantIndex) {
301 if (shrinkCrumbAtIndex(index))
303 index += (direction > 0 ? 1 : -1);
306 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
307 // with a tie going to child crumbs.
309 var endIndex = crumbs.childNodes.length - 1;
310 while (startIndex != significantIndex || endIndex != significantIndex) {
311 var startDistance = significantIndex - startIndex;
312 var endDistance = endIndex - significantIndex;
313 if (startDistance >= endDistance)
314 var index = startIndex++;
316 var index = endIndex--;
317 if (shrinkCrumbAtIndex(index))
322 // We are not small enough yet, return false so the caller knows.
326 function coalesceCollapsedCrumbs()
328 var crumb = crumbs.firstChild;
329 var collapsedRun = false;
330 var newStartNeeded = false;
331 var newEndNeeded = false;
333 var hidden = crumb.classList.contains("hidden");
335 var collapsed = crumb.classList.contains("collapsed");
336 if (collapsedRun && collapsed) {
337 crumb.classList.add("hidden");
338 crumb.classList.remove("compact");
339 crumb.classList.remove("collapsed");
341 if (crumb.classList.contains("start")) {
342 crumb.classList.remove("start");
343 newStartNeeded = true;
346 if (crumb.classList.contains("end")) {
347 crumb.classList.remove("end");
354 collapsedRun = collapsed;
357 newEndNeeded = false;
358 crumb.classList.add("end");
363 crumb = crumb.nextSibling;
366 if (newStartNeeded) {
367 crumb = crumbs.lastChild;
369 if (!crumb.classList.contains("hidden")) {
370 crumb.classList.add("start");
373 crumb = crumb.previousSibling;
379 * @param {!Element} crumb
381 function compact(crumb)
383 if (crumb.classList.contains("hidden"))
385 crumb.classList.add("compact");
389 * @param {!Element} crumb
390 * @param {boolean=} dontCoalesce
392 function collapse(crumb, dontCoalesce)
394 if (crumb.classList.contains("hidden"))
396 crumb.classList.add("collapsed");
397 crumb.classList.remove("compact");
399 coalesceCollapsedCrumbs();
403 // When not focused on a crumb we can be biased and collapse less important
404 // crumbs that the user might not care much about.
406 // Compact child crumbs.
407 if (makeCrumbsSmaller(compact, ChildSide))
410 // Collapse child crumbs.
411 if (makeCrumbsSmaller(collapse, ChildSide))
415 // Compact ancestor crumbs, or from both sides if focused.
416 if (makeCrumbsSmaller(compact, focusedCrumb ? BothSides : AncestorSide))
419 // Collapse ancestor crumbs, or from both sides if focused.
420 if (makeCrumbsSmaller(collapse, focusedCrumb ? BothSides : AncestorSide))
426 // Compact the selected crumb.
427 compact(selectedCrumb);
428 if (crumbsAreSmallerThanContainer())
431 // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
432 collapse(selectedCrumb, true);
435 __proto__: WebInspector.HBox.prototype