Web Inspector: Add a dedicated Network tab that is always live
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TabBar.js
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.TabBar = class TabBar extends WebInspector.Object
27 {
28     constructor(element, tabBarItems)
29     {
30         super();
31
32         this._element = element || document.createElement("div");
33         this._element.classList.add("tab-bar");
34         this._element.tabIndex = 0;
35
36         var topBorderElement = document.createElement("div");
37         topBorderElement.classList.add("top-border");
38         this._element.appendChild(topBorderElement);
39
40         this._element.addEventListener("mousedown", this._handleMouseDown.bind(this));
41         this._element.addEventListener("click", this._handleClick.bind(this));
42         this._element.addEventListener("mouseleave", this._handleMouseLeave.bind(this));
43
44         this._tabBarItems = [];
45
46         if (tabBarItems) {
47             for (var tabBarItem in tabBarItems)
48                 this.addTabBarItem(tabBarItem);
49         }
50     }
51
52     // Public
53
54     get newTabItem()
55     {
56         return this._newTabItem || null;
57     }
58
59     set newTabItem(newTabItem)
60     {
61         if (!this._handleNewTabClickListener)
62             this._handleNewTabClickListener = this._handleNewTabClick.bind(this);
63
64         if (!this._handleNewTabMouseEnterListener)
65             this._handleNewTabMouseEnterListener = this._handleNewTabMouseEnter.bind(this);
66
67         if (this._newTabItem) {
68             this._newTabItem.element.classList.remove("new-tab-button");
69             this._newTabItem.element.removeEventListener("click", this._handleNewTabClickListener);
70             this._newTabItem.element.removeEventListener("mouseenter", this._handleNewTabMouseEnterListener);
71             this.removeTabBarItem(this._newTabItem, true);
72         }
73
74         if (newTabItem) {
75             newTabItem.element.classList.add("new-tab-button");
76             newTabItem.element.addEventListener("click", this._handleNewTabClickListener);
77             newTabItem.element.addEventListener("mouseenter", this._handleNewTabMouseEnterListener);
78             this.addTabBarItem(newTabItem, true);
79         }
80
81         this._newTabItem = newTabItem || null;
82     }
83
84     addTabBarItem(tabBarItem, doNotAnimate)
85     {
86         return this.insertTabBarItem(tabBarItem, this._tabBarItems.length, doNotAnimate);
87     }
88
89     insertTabBarItem(tabBarItem, index, doNotAnimate)
90     {
91         console.assert(tabBarItem instanceof WebInspector.TabBarItem);
92         if (!(tabBarItem instanceof WebInspector.TabBarItem))
93             return null;
94
95         if (tabBarItem.parentTabBar === this)
96             return;
97
98         if (this._tabAnimatedClosedSinceMouseEnter) {
99             // Delay adding the new tab until we can expand the tabs after a closed tab.
100             this._finishExpandingTabsAfterClose().then(function() {
101                 this.insertTabBarItem(tabBarItem, index, doNotAnimate);
102             }.bind(this));
103             return;
104         }
105
106         if (tabBarItem.parentTabBar)
107             tabBarItem.parentTabBar.removeTabBarItem(tabBarItem);
108
109         tabBarItem.parentTabBar = this;
110
111         var lastIndex = this._newTabItem ? this._tabBarItems.length - 1 : this._tabBarItems.length;
112         index = Math.max(0, Math.min(index, lastIndex));
113
114         if (this._element.classList.contains("animating")) {
115             requestAnimationFrame(removeStyles.bind(this));
116             doNotAnimate = true;
117         }
118
119         var beforeTabSizesAndPositions;
120         if (!doNotAnimate)
121             beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
122
123         this._tabBarItems.splice(index, 0, tabBarItem);
124
125         var nextSibling = this._tabBarItems[index + 1];
126         var nextSiblingElement = nextSibling ? nextSibling.element : (this._newTabItem ? this._newTabItem.element : null);
127
128         this._element.insertBefore(tabBarItem.element, nextSiblingElement);
129
130         this._element.classList.toggle("single-tab", !this._hasMoreThanOneNormalTab());
131
132         tabBarItem.element.style.left = null;
133         tabBarItem.element.style.width = null;
134
135         function animateTabs()
136         {
137             this._element.classList.add("animating");
138             this._element.classList.add("inserting-tab");
139
140             this._applyTabBarItemSizesAndPositions(afterTabSizesAndPositions);
141
142             this._element.addEventListener("webkitTransitionEnd", removeStylesListener);
143         }
144
145         function removeStyles()
146         {
147             this._element.classList.remove("static-layout");
148             this._element.classList.remove("animating");
149             this._element.classList.remove("inserting-tab");
150
151             tabBarItem.element.classList.remove("being-inserted");
152
153             this._clearTabBarItemSizesAndPositions();
154
155             this._element.removeEventListener("webkitTransitionEnd", removeStylesListener);
156         }
157
158         if (!doNotAnimate) {
159             var afterTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
160
161             this.updateLayout();
162
163             var previousTabBarItem = this._tabBarItems[this._tabBarItems.indexOf(tabBarItem) - 1] || null;
164             var previousTabBarItemSizeAndPosition = previousTabBarItem ? beforeTabSizesAndPositions.get(previousTabBarItem) : null;
165
166             if (previousTabBarItemSizeAndPosition)
167                 beforeTabSizesAndPositions.set(tabBarItem, {left: previousTabBarItemSizeAndPosition.left + previousTabBarItemSizeAndPosition.width, width: 0});
168             else
169                 beforeTabSizesAndPositions.set(tabBarItem, {left: 0, width: 0});
170
171             this._element.classList.add("static-layout");
172             tabBarItem.element.classList.add("being-inserted");
173
174             this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions);
175
176             var removeStylesListener = removeStyles.bind(this);
177
178             requestAnimationFrame(animateTabs.bind(this));
179         } else
180             this.updateLayoutSoon();
181
182         this.dispatchEventToListeners(WebInspector.TabBar.Event.TabBarItemAdded, {tabBarItem});
183
184         return tabBarItem;
185     }
186
187     removeTabBarItem(tabBarItemOrIndex, doNotAnimate, doNotExpand)
188     {
189         var tabBarItem = this._findTabBarItem(tabBarItemOrIndex);
190         if (!tabBarItem)
191             return null;
192
193         tabBarItem.parentTabBar = null;
194
195         if (tabBarItem === this._newTabItem)
196             this.newTabItem = null;
197
198         if (this._selectedTabBarItem === tabBarItem) {
199             var index = this._tabBarItems.indexOf(tabBarItem);
200             var nextTabBarItem = this._tabBarItems[index + 1];
201             if (!nextTabBarItem || nextTabBarItem.pinned)
202                 nextTabBarItem = this._tabBarItems[index - 1];
203
204             this.selectedTabBarItem = nextTabBarItem;
205         }
206
207         if (this._element.classList.contains("animating")) {
208             requestAnimationFrame(removeStyles.bind(this));
209             doNotAnimate = true;
210         }
211
212         var beforeTabSizesAndPositions;
213         if (!doNotAnimate)
214             beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
215
216         var wasLastNormalTab = this._tabBarItems.indexOf(tabBarItem) === (this._newTabItem ? this._tabBarItems.length - 2 : this._tabBarItems.length - 1);
217
218         this._tabBarItems.remove(tabBarItem);
219         tabBarItem.element.remove();
220
221         var hasMoreThanOneNormalTab = this._hasMoreThanOneNormalTab();
222         this._element.classList.toggle("single-tab", !hasMoreThanOneNormalTab);
223
224         if (!hasMoreThanOneNormalTab || wasLastNormalTab || !doNotExpand) {
225             if (!doNotAnimate) {
226                 this._tabAnimatedClosedSinceMouseEnter = true;
227                 this._finishExpandingTabsAfterClose(beforeTabSizesAndPositions);
228             } else
229                 this.updateLayoutSoon();
230
231             this.dispatchEventToListeners(WebInspector.TabBar.Event.TabBarItemRemoved, {tabBarItem});
232
233             return tabBarItem;
234         }
235
236         var lastNormalTabBarItem;
237
238         function animateTabs()
239         {
240             this._element.classList.add("animating");
241             this._element.classList.add("closing-tab");
242
243             var left = 0;
244             for (var currentTabBarItem of this._tabBarItems) {
245                 var sizeAndPosition = beforeTabSizesAndPositions.get(currentTabBarItem);
246
247                 if (!currentTabBarItem.pinned) {
248                     currentTabBarItem.element.style.left = left + "px";
249                     left += sizeAndPosition.width;
250                     lastNormalTabBarItem = currentTabBarItem;
251                 } else
252                     left = sizeAndPosition.left + sizeAndPosition.width;
253             }
254
255             // The selected tab and last tab need to draw a right border as well, so make them 1px wider.
256             if (this._selectedTabBarItem)
257                 this._selectedTabBarItem.element.style.width = (parseFloat(this._selectedTabBarItem.element.style.width) + 1) + "px";
258
259             if (lastNormalTabBarItem !== this._selectedTabBarItem)
260                 lastNormalTabBarItem.element.style.width = (parseFloat(lastNormalTabBarItem.element.style.width) + 1) + "px";
261
262             this._element.addEventListener("webkitTransitionEnd", removeStylesListener);
263         }
264
265         function removeStyles()
266         {
267             // The selected tab needs to stop drawing the right border, so make it 1px smaller. Only if it isn't the last.
268             if (this._selectedTabBarItem && this._selectedTabBarItem !== lastNormalTabBarItem)
269                 this._selectedTabBarItem.element.style.width = (parseFloat(this._selectedTabBarItem.element.style.width) - 1) + "px";
270
271             this._element.classList.remove("animating");
272             this._element.classList.remove("closing-tab");
273
274             this.updateLayout();
275
276             this._element.removeEventListener("webkitTransitionEnd", removeStylesListener);
277         }
278
279         if (!doNotAnimate) {
280             this._element.classList.add("static-layout");
281
282             this._tabAnimatedClosedSinceMouseEnter = true;
283
284             this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions);
285
286             var removeStylesListener = removeStyles.bind(this);
287
288             requestAnimationFrame(animateTabs.bind(this));
289         } else
290             this.updateLayoutSoon();
291
292         this.dispatchEventToListeners(WebInspector.TabBar.Event.TabBarItemRemoved, {tabBarItem});
293
294         return tabBarItem;
295     }
296
297     selectPreviousTab()
298     {
299         if (this._tabBarItems.length <= 1)
300             return;
301
302         var startIndex = this._tabBarItems.indexOf(this._selectedTabBarItem);
303         var newIndex = startIndex;
304         do {
305             if (newIndex === 0)
306                 newIndex = this._tabBarItems.length - 1;
307             else
308                 newIndex--;
309
310             if (!this._tabBarItems[newIndex].pinned)
311                 break;
312         } while (newIndex !== startIndex);
313
314         if (newIndex === startIndex)
315             return;
316
317         this.selectedTabBarItem = this._tabBarItems[newIndex];
318     }
319
320     selectNextTab()
321     {
322         if (this._tabBarItems.length <= 1)
323             return;
324
325         var startIndex = this._tabBarItems.indexOf(this._selectedTabBarItem);
326         var newIndex = startIndex;
327         do {
328             if (newIndex === this._tabBarItems.length - 1)
329                 newIndex = 0;
330             else
331                 newIndex++;
332
333             if (!this._tabBarItems[newIndex].pinned)
334                 break;
335         } while (newIndex !== startIndex);
336
337         if (newIndex === startIndex)
338             return;
339
340         this.selectedTabBarItem = this._tabBarItems[newIndex];
341     }
342
343     updateLayoutSoon()
344     {
345         if (this._updateLayoutIdentifier)
346             return;
347
348         this._needsLayout = true;
349
350         function update()
351         {
352             this._updateLayoutIdentifier = undefined;
353
354             if (this._needsLayout)
355                 this.updateLayout();
356         }
357
358         this._updateLayoutIdentifier = requestAnimationFrame(update.bind(this));
359     }
360
361     updateLayout()
362     {
363         if (this._updateLayoutIdentifier) {
364             cancelAnimationFrame(this._updateLayoutIdentifier);
365             this._updateLayoutIdentifier = undefined;
366         }
367
368         if (this._element.classList.contains("static-layout"))
369             return;
370
371         this._needsLayout = false;
372
373         this._element.classList.remove("hide-titles");
374         this._element.classList.remove("collapsed");
375
376         var firstNormalTabItem = null;
377         for (var tabItem of this._tabBarItems) {
378             if (tabItem.pinned)
379                 continue;
380             firstNormalTabItem = tabItem;
381             break;
382         }
383
384         if (!firstNormalTabItem)
385             return;
386
387         if (firstNormalTabItem.element.offsetWidth >= 120)
388             return;
389
390         this._element.classList.add("collapsed");
391
392         if (firstNormalTabItem.element.offsetWidth >= 60)
393             return;
394
395         this._element.classList.add("hide-titles");
396     }
397
398     get selectedTabBarItem()
399     {
400         return this._selectedTabBarItem;
401     }
402
403     set selectedTabBarItem(tabBarItemOrIndex)
404     {
405         var tabBarItem = this._findTabBarItem(tabBarItemOrIndex);
406         if (tabBarItem === this._newTabItem)
407             tabBarItem = this._tabBarItems[this._tabBarItems.length - 2];
408
409         if (this._selectedTabBarItem === tabBarItem)
410             return;
411
412         if (this._selectedTabBarItem)
413             this._selectedTabBarItem.selected = false;
414
415         this._selectedTabBarItem = tabBarItem || null;
416
417         if (this._selectedTabBarItem)
418             this._selectedTabBarItem.selected = true;
419
420         this.dispatchEventToListeners(WebInspector.TabBar.Event.TabBarItemSelected);
421     }
422
423     get tabBarItems()
424     {
425         return this._tabBarItems;
426     }
427
428     get element()
429     {
430         return this._element;
431     }
432
433     // Private
434
435     _findTabBarItem(tabBarItemOrIndex)
436     {
437         if (typeof tabBarItemOrIndex === "number")
438             return this._tabBarItems[tabBarItemOrIndex] || null;
439
440         if (tabBarItemOrIndex instanceof WebInspector.TabBarItem) {
441             if (this._tabBarItems.includes(tabBarItemOrIndex))
442                 return tabBarItemOrIndex;
443         }
444
445         return null;
446     }
447
448     _hasMoreThanOneNormalTab()
449     {
450         var normalTabCount = 0;
451         for (var tabBarItem of this._tabBarItems) {
452             if (tabBarItem.pinned)
453                 continue;
454             ++normalTabCount;
455             if (normalTabCount >= 2)
456                 return true;
457         }
458
459         return false;
460     }
461
462     _recordTabBarItemSizesAndPositions()
463     {
464         var tabBarItemSizesAndPositions = new Map;
465
466         var barRect = this._element.getBoundingClientRect();
467
468         for (var tabBarItem of this._tabBarItems) {
469             var boundingRect = tabBarItem.element.getBoundingClientRect();
470             tabBarItemSizesAndPositions.set(tabBarItem, {left: boundingRect.left - barRect.left, width: boundingRect.width});
471         }
472
473         return tabBarItemSizesAndPositions;
474     }
475
476     _applyTabBarItemSizesAndPositions(tabBarItemSizesAndPositions, skipTabBarItem)
477     {
478         for (var [tabBarItem, sizeAndPosition] of tabBarItemSizesAndPositions) {
479             if (skipTabBarItem && tabBarItem === skipTabBarItem)
480                 continue;
481             tabBarItem.element.style.left = sizeAndPosition.left + "px";
482             tabBarItem.element.style.width = sizeAndPosition.width + "px";
483         }
484     }
485
486     _clearTabBarItemSizesAndPositions(skipTabBarItem)
487     {
488         for (var tabBarItem of this._tabBarItems) {
489             if (skipTabBarItem && tabBarItem === skipTabBarItem)
490                 continue;
491             tabBarItem.element.style.left = null;
492             tabBarItem.element.style.width = null;
493         }
494     }
495
496     _finishExpandingTabsAfterClose(beforeTabSizesAndPositions)
497     {
498         return new Promise(function(resolve, reject) {
499             console.assert(this._tabAnimatedClosedSinceMouseEnter);
500             this._tabAnimatedClosedSinceMouseEnter = false;
501
502             if (!beforeTabSizesAndPositions)
503                 beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
504
505             this._element.classList.remove("static-layout");
506             this._clearTabBarItemSizesAndPositions();
507
508             var afterTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
509
510             this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions);
511
512             function animateTabs()
513             {
514                 this._element.classList.add("static-layout");
515                 this._element.classList.add("animating");
516                 this._element.classList.add("expanding-tabs");
517
518                 this._applyTabBarItemSizesAndPositions(afterTabSizesAndPositions);
519
520                 this._element.addEventListener("webkitTransitionEnd", removeStylesListener);
521             }
522
523             function removeStyles()
524             {
525                 this._element.classList.remove("static-layout");
526                 this._element.classList.remove("animating");
527                 this._element.classList.remove("expanding-tabs");
528
529                 this._clearTabBarItemSizesAndPositions();
530
531                 this.updateLayout();
532
533                 this._element.removeEventListener("webkitTransitionEnd", removeStylesListener);
534
535                 resolve();
536             }
537
538             var removeStylesListener = removeStyles.bind(this);
539
540             requestAnimationFrame(animateTabs.bind(this));
541         }.bind(this));
542     }
543
544     _handleMouseDown(event)
545     {
546         // Only handle left mouse clicks.
547         if (event.button !== 0 || event.ctrlKey)
548             return;
549
550         var itemElement = event.target.enclosingNodeOrSelfWithClass(WebInspector.TabBarItem.StyleClassName);
551         if (!itemElement)
552             return;
553
554         var tabBarItem = itemElement[WebInspector.TabBarItem.ElementReferenceSymbol];
555         if (!tabBarItem)
556             return;
557
558         if (tabBarItem.disabled)
559             return;
560
561         if (tabBarItem === this._newTabItem)
562             return;
563
564         var closeButtonElement = event.target.enclosingNodeOrSelfWithClass(WebInspector.TabBarItem.CloseButtonStyleClassName);
565         if (closeButtonElement)
566             return;
567
568         this.selectedTabBarItem = tabBarItem;
569
570         if (tabBarItem.pinned || !this._hasMoreThanOneNormalTab())
571             return;
572
573         this._firstNormalTabItemIndex = 0;
574         for (var i = 0; i < this._tabBarItems.length; ++i) {
575             if (this._tabBarItems[i].pinned)
576                 continue;
577             this._firstNormalTabItemIndex = i;
578             break;
579         }
580
581         this._mouseIsDown = true;
582
583         this._mouseMovedEventListener = this._handleMouseMoved.bind(this);
584         this._mouseUpEventListener = this._handleMouseUp.bind(this);
585
586         // Register these listeners on the document so we can track the mouse if it leaves the tab bar.
587         document.addEventListener("mousemove", this._mouseMovedEventListener, true);
588         document.addEventListener("mouseup", this._mouseUpEventListener, true);
589
590         event.preventDefault();
591         event.stopPropagation();
592     }
593
594     _handleClick(event)
595     {
596         var itemElement = event.target.enclosingNodeOrSelfWithClass(WebInspector.TabBarItem.StyleClassName);
597         if (!itemElement)
598             return;
599
600         var tabBarItem = itemElement[WebInspector.TabBarItem.ElementReferenceSymbol];
601         if (!tabBarItem)
602             return;
603
604         if (tabBarItem.disabled)
605             return;
606
607         var closeButtonElement = event.target.enclosingNodeOrSelfWithClass(WebInspector.TabBarItem.CloseButtonStyleClassName);
608         if (closeButtonElement)
609             this.removeTabBarItem(tabBarItem, false, true);
610     }
611
612     _handleMouseMoved(event)
613     {
614         console.assert(event.button === 0);
615         console.assert(this._mouseIsDown);
616         if (!this._mouseIsDown)
617             return;
618
619         console.assert(this._selectedTabBarItem);
620         if (!this._selectedTabBarItem)
621             return;
622
623         event.preventDefault();
624         event.stopPropagation();
625
626         if (!this._element.classList.contains("static-layout")) {
627             this._applyTabBarItemSizesAndPositions(this._recordTabBarItemSizesAndPositions());
628             this._element.classList.add("static-layout");
629             this._element.classList.add("dragging-tab");
630         }
631
632         if (this._mouseOffset === undefined)
633             this._mouseOffset = event.pageX - this._selectedTabBarItem.element.totalOffsetLeft;
634
635         var tabBarMouseOffset = event.pageX - this.element.totalOffsetLeft;
636         var newLeft = tabBarMouseOffset - this._mouseOffset;
637
638         this._selectedTabBarItem.element.style.left = newLeft + "px";
639
640         var selectedTabMidX = newLeft + (this._selectedTabBarItem.element.realOffsetWidth / 2);
641
642         var currentIndex = this._tabBarItems.indexOf(this._selectedTabBarItem);
643         var newIndex = currentIndex;
644
645         for (var tabBarItem of this._tabBarItems) {
646             if (tabBarItem === this._selectedTabBarItem)
647                 continue;
648
649             var tabBarItemRect = tabBarItem.element.getBoundingClientRect();
650
651             if (selectedTabMidX < tabBarItemRect.left || selectedTabMidX > tabBarItemRect.right)
652                 continue;
653
654             newIndex = this._tabBarItems.indexOf(tabBarItem);
655             break;
656         }
657
658         newIndex = Math.max(this._firstNormalTabItemIndex, newIndex);
659         newIndex = Math.min(this._newTabItem ? this._tabBarItems.length - 2 : this._tabBarItems.length - 1, newIndex);
660
661         if (currentIndex === newIndex)
662             return;
663
664         this._tabBarItems.splice(currentIndex, 1);
665         this._tabBarItems.splice(newIndex, 0, this._selectedTabBarItem);
666
667         var nextSibling = this._tabBarItems[newIndex + 1];
668         var nextSiblingElement = nextSibling ? nextSibling.element : (this._newTabItem ? this._newTabItem.element : null);
669
670         this._element.insertBefore(this._selectedTabBarItem.element, nextSiblingElement);
671
672         // FIXME: Animate the tabs that move to make room for the selected tab. This was causing me trouble when I tried.
673
674         var left = 0;
675         for (var tabBarItem of this._tabBarItems) {
676             if (tabBarItem !== this._selectedTabBarItem && tabBarItem !== this._newTabItem && parseFloat(tabBarItem.element.style.left) !== left)
677                 tabBarItem.element.style.left = left + "px";
678             left += parseFloat(tabBarItem.element.style.width);
679         }
680     }
681
682     _handleMouseUp(event)
683     {
684         console.assert(event.button === 0);
685         console.assert(this._mouseIsDown);
686         if (!this._mouseIsDown)
687             return;
688
689         this._element.classList.remove("dragging-tab");
690
691         if (!this._tabAnimatedClosedSinceMouseEnter) {
692             this._element.classList.remove("static-layout");
693             this._clearTabBarItemSizesAndPositions();
694         } else {
695             var left = 0;
696             for (var tabBarItem of this._tabBarItems) {
697                 if (tabBarItem === this._selectedTabBarItem)
698                     tabBarItem.element.style.left = left + "px";
699                 left += parseFloat(tabBarItem.element.style.width);
700             }
701         }
702
703         this._mouseIsDown = false;
704         this._mouseOffset = undefined;
705
706         document.removeEventListener("mousemove", this._mouseMovedEventListener, true);
707         document.removeEventListener("mouseup", this._mouseUpEventListener, true);
708
709         this._mouseMovedEventListener = null;
710         this._mouseUpEventListener = null;
711
712         event.preventDefault();
713         event.stopPropagation();
714
715         this.dispatchEventToListeners(WebInspector.TabBar.Event.TabBarItemsReordered);
716     }
717
718     _handleMouseLeave(event)
719     {
720         if (this._mouseIsDown || !this._tabAnimatedClosedSinceMouseEnter || !this._element.classList.contains("static-layout") || this._element.classList.contains("animating"))
721             return;
722
723         // This event can still fire when the mouse is inside the element if DOM nodes are added, removed or generally change inside.
724         // Check if the mouse really did leave the element by checking the bounds.
725         // FIXME: Is this a WebKit bug or correct behavior?
726         var barRect = this._element.getBoundingClientRect();
727         var newTabItemRect = this._newTabItem ? this._newTabItem.element.getBoundingClientRect() : null;
728         if (event.pageY > barRect.top && event.pageY < barRect.bottom && event.pageX > barRect.left && event.pageX < (newTabItemRect ? newTabItemRect.right : barRect.right))
729             return;
730
731         this._finishExpandingTabsAfterClose();
732     }
733
734     _handleNewTabClick(event)
735     {
736         if (this._newTabItem.disabled)
737             return;
738         this.dispatchEventToListeners(WebInspector.TabBar.Event.NewTabItemClicked);
739     }
740
741     _handleNewTabMouseEnter(event)
742     {
743         if (!this._tabAnimatedClosedSinceMouseEnter || !this._element.classList.contains("static-layout") || this._element.classList.contains("animating"))
744             return;
745
746         this._finishExpandingTabsAfterClose();
747     }
748 };
749
750 WebInspector.TabBar.Event = {
751     TabBarItemSelected: "tab-bar-tab-bar-item-selected",
752     TabBarItemAdded: "tab-bar-tab-bar-item-added",
753     TabBarItemRemoved: "tab-bar-tab-bar-item-removed",
754     TabBarItemsReordered: "tab-bar-tab-bar-items-reordered",
755     NewTabItemClicked: "tab-bar-new-tab-item-clicked"
756 };