Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / elements / ElementsBreadcrumbs.js
blob0b1c4e4d737abc1beec7efaaf86a0025af4d49b3
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.
5 /**
6  * @constructor
7  * @extends {WebInspector.HBox}
8  */
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");
20 /** @enum {string} */
21 WebInspector.ElementsBreadcrumbs.Events = {
22     NodeSelected: "NodeSelected"
25 WebInspector.ElementsBreadcrumbs.prototype = {
26     wasShown: function()
27     {
28         this.update();
29     },
31     /**
32      * @param {!Array.<!WebInspector.DOMNode>} nodes
33      */
34     updateNodes: function(nodes)
35     {
36         if (!nodes.length)
37             return;
39         var crumbs = this.crumbsElement;
40         for (var crumb = crumbs.firstChild; crumb; crumb = crumb.nextSibling) {
41             if (nodes.indexOf(crumb[this._nodeSymbol]) !== -1) {
42                 this.update(true);
43                 return;
44             }
45         }
46     },
48     /**
49      * @param {?WebInspector.DOMNode} node
50      */
51     setSelectedNode: function(node)
52     {
53         this._currentDOMNode = node;
54         this.update();
55     },
57     _mouseMovedInCrumbs: function(event)
58     {
59         var nodeUnderMouse = event.target;
60         var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb");
61         var node = /** @type {?WebInspector.DOMNode} */ (crumbElement ? crumbElement[this._nodeSymbol] : null);
62         if (node)
63             node.highlight();
64     },
66     _mouseMovedOutOfCrumbs: function(event)
67     {
68         if (this._currentDOMNode)
69             WebInspector.DOMModel.hideDOMNodeHighlight();
70     },
72     /**
73      * @param {boolean=} force
74      */
75     update: function(force)
76     {
77         if (!this.isShowing())
78             return;
80         var currentDOMNode = this._currentDOMNode;
81         var crumbs = this.crumbsElement;
83         var handled = false;
84         var crumb = crumbs.firstChild;
85         while (crumb) {
86             if (crumb[this._nodeSymbol] === currentDOMNode) {
87                 crumb.classList.add("selected");
88                 handled = true;
89             } else {
90                 crumb.classList.remove("selected");
91             }
93             crumb = crumb.nextSibling;
94         }
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.
99             this.updateSizes();
100             return;
101         }
103         crumbs.removeChildren();
105         var panel = this;
107         /**
108          * @param {!Event} event
109          * @this {WebInspector.ElementsBreadcrumbs}
110          */
111         function selectCrumb(event)
112         {
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]);
117                 return;
118             }
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)
129                         break;
130                     crumb = currentCrumb;
131                     currentCrumb = currentCrumb.nextSiblingElement;
132                 }
133             }
135             this.updateSizes(crumb);
136         }
138         var boundSelectCrumb = selectCrumb.bind(this);
139         for (var current = currentDOMNode; current; current = current.parentNode) {
140             if (current.nodeType() === Node.DOCUMENT_NODE)
141                 continue;
143             crumb = createElementWithClass("span", "crumb");
144             crumb[this._nodeSymbol] = current;
145             crumb.addEventListener("mousedown", boundSelectCrumb, false);
147             var crumbTitle = "";
148             switch (current.nodeType()) {
149                 case Node.ELEMENT_NODE:
150                     if (current.pseudoType())
151                         crumbTitle = "::" + current.pseudoType();
152                     else
153                         WebInspector.DOMPresentationUtils.decorateNodeLabel(current, crumb);
154                     break;
156                 case Node.TEXT_NODE:
157                     crumbTitle = WebInspector.UIString("(text)");
158                     break;
160                 case Node.COMMENT_NODE:
161                     crumbTitle = "<!-->";
162                     break;
164                 case Node.DOCUMENT_TYPE_NODE:
165                     crumbTitle = "<!DOCTYPE>";
166                     break;
168                 case Node.DOCUMENT_FRAGMENT_NODE:
169                     crumbTitle = current.shadowRootType() ? "#shadow-root" : current.nodeNameInCorrectCase();
170                     break;
172                 default:
173                     crumbTitle = current.nodeNameInCorrectCase();
174             }
176             if (!crumb.childNodes.length) {
177                 var nameElement = createElement("span");
178                 nameElement.textContent = crumbTitle;
179                 crumb.appendChild(nameElement);
180                 crumb.title = crumbTitle;
181             }
183             if (current === currentDOMNode)
184                 crumb.classList.add("selected");
185             crumbs.insertBefore(crumb, crumbs.firstChild);
186         }
188         this.updateSizes();
189     },
191     /**
192      * @param {!Element=} focusedCrumb
193      */
194     updateSizes: function(focusedCrumb)
195     {
196         if (!this.isShowing())
197             return;
199         var crumbs = this.crumbsElement;
200         if (!crumbs.firstChild)
201             return;
203         var selectedIndex = 0;
204         var focusedIndex = 0;
205         var selectedCrumb;
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;
213                 selectedIndex = i;
214             }
216             // Find the focused crumb index.
217             if (crumb === focusedCrumb)
218                 focusedIndex = i;
220             crumb.classList.remove("compact", "collapsed", "hidden");
221         }
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;
229         }
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");
236         }
237         for (var i = 0; i < crumbs.childNodes.length; ++i) {
238             var crumb = crumbs.childNodes[i];
239             compactSizes[i] = crumb.offsetWidth;
240         }
242         // Layout 3: Measure collapsed crumb size
243         crumbs.firstChild.classList.add("collapsed");
244         var collapsedSize = crumbs.firstChild.offsetWidth;
246         // Clean up.
247         for (var i = 0; i < crumbs.childNodes.length; ++i) {
248             var crumb = crumbs.childNodes[i];
249             crumb.classList.remove("compact", "collapsed");
250         }
252         function crumbsAreSmallerThanContainer()
253         {
254             var totalSize = 0;
255             for (var i = 0; i < crumbs.childNodes.length; ++i) {
256                 var crumb = crumbs.childNodes[i];
257                 if (crumb.classList.contains("hidden"))
258                     continue;
259                 if (crumb.classList.contains("collapsed")) {
260                     totalSize += collapsedSize;
261                     continue;
262                 }
263                 totalSize += crumb.classList.contains("compact") ? compactSizes[i] : normalSizes[i];
264             }
265             const rightPadding = 10;
266             return totalSize + rightPadding < contentElementWidth;
267         }
269         if (crumbsAreSmallerThanContainer())
270             return; // No need to compact the crumbs, they all fit at full size.
272         var BothSides = 0;
273         var AncestorSide = -1;
274         var ChildSide = 1;
276         /**
277          * @param {function(!Element)} shrinkingFunction
278          * @param {number} direction
279          */
280         function makeCrumbsSmaller(shrinkingFunction, direction)
281         {
282             var significantCrumb = focusedCrumb || selectedCrumb;
283             var significantIndex = significantCrumb === selectedCrumb ? selectedIndex : focusedIndex;
285             function shrinkCrumbAtIndex(index)
286             {
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.
292                 return false;
293             }
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.
297             if (direction) {
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))
302                         return true;
303                     index += (direction > 0 ? 1 : -1);
304                 }
305             } else {
306                 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
307                 // with a tie going to child crumbs.
308                 var startIndex = 0;
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++;
315                     else
316                         var index = endIndex--;
317                     if (shrinkCrumbAtIndex(index))
318                         return true;
319                 }
320             }
322             // We are not small enough yet, return false so the caller knows.
323             return false;
324         }
326         function coalesceCollapsedCrumbs()
327         {
328             var crumb = crumbs.firstChild;
329             var collapsedRun = false;
330             var newStartNeeded = false;
331             var newEndNeeded = false;
332             while (crumb) {
333                 var hidden = crumb.classList.contains("hidden");
334                 if (!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;
344                         }
346                         if (crumb.classList.contains("end")) {
347                             crumb.classList.remove("end");
348                             newEndNeeded = true;
349                         }
351                         continue;
352                     }
354                     collapsedRun = collapsed;
356                     if (newEndNeeded) {
357                         newEndNeeded = false;
358                         crumb.classList.add("end");
359                     }
360                 } else {
361                     collapsedRun = true;
362                 }
363                 crumb = crumb.nextSibling;
364             }
366             if (newStartNeeded) {
367                 crumb = crumbs.lastChild;
368                 while (crumb) {
369                     if (!crumb.classList.contains("hidden")) {
370                         crumb.classList.add("start");
371                         break;
372                     }
373                     crumb = crumb.previousSibling;
374                 }
375             }
376         }
378         /**
379          * @param {!Element} crumb
380          */
381         function compact(crumb)
382         {
383             if (crumb.classList.contains("hidden"))
384                 return;
385             crumb.classList.add("compact");
386         }
388         /**
389          * @param {!Element} crumb
390          * @param {boolean=} dontCoalesce
391          */
392         function collapse(crumb, dontCoalesce)
393         {
394             if (crumb.classList.contains("hidden"))
395                 return;
396             crumb.classList.add("collapsed");
397             crumb.classList.remove("compact");
398             if (!dontCoalesce)
399                 coalesceCollapsedCrumbs();
400         }
402         if (!focusedCrumb) {
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))
408                 return;
410             // Collapse child crumbs.
411             if (makeCrumbsSmaller(collapse, ChildSide))
412                 return;
413         }
415         // Compact ancestor crumbs, or from both sides if focused.
416         if (makeCrumbsSmaller(compact, focusedCrumb ? BothSides : AncestorSide))
417             return;
419         // Collapse ancestor crumbs, or from both sides if focused.
420         if (makeCrumbsSmaller(collapse, focusedCrumb ? BothSides : AncestorSide))
421             return;
423         if (!selectedCrumb)
424             return;
426         // Compact the selected crumb.
427         compact(selectedCrumb);
428         if (crumbsAreSmallerThanContainer())
429             return;
431         // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
432         collapse(selectedCrumb, true);
433     },
435     __proto__: WebInspector.HBox.prototype