Web Inspector: REGRESSION: no context menu items work when context menu clicking...
[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 WI.TabBar = class TabBar extends WI.View
27 {
28     constructor(element, tabBarItems)
29     {
30         super(element);
31
32         this.element.classList.add("tab-bar");
33         this.element.setAttribute("role", "tablist");
34         this.element.addEventListener("mousedown", this._handleMouseDown.bind(this));
35         this.element.addEventListener("click", this._handleClick.bind(this));
36         this.element.addEventListener("mouseleave", this._handleMouseLeave.bind(this));
37         this.element.addEventListener("contextmenu", this._handleContextMenu.bind(this));
38
39         this.element.createChild("div", "top-border");
40
41         this._tabBarItems = [];
42         this._hiddenTabBarItems = [];
43
44         if (tabBarItems) {
45             for (let tabBarItem in tabBarItems)
46                 this.addTabBarItem(tabBarItem);
47         }
48
49         this._tabPickerTabBarItem = new WI.PinnedTabBarItem("Images/TabPicker.svg", WI.UIString("Show hidden tabs"));
50         this._tabPickerTabBarItem.element.classList.add("tab-picker", "hidden");
51         this.addTabBarItem(this._tabPickerTabBarItem, {suppressAnimations: true});
52     }
53
54     // Public
55
56     addTabBarItem(tabBarItem, options = {})
57     {
58         return this.insertTabBarItem(tabBarItem, this._tabBarItems.length, options);
59     }
60
61     insertTabBarItem(tabBarItem, index, options = {})
62     {
63         console.assert(tabBarItem instanceof WI.TabBarItem);
64         if (!(tabBarItem instanceof WI.TabBarItem))
65             return null;
66
67         if (tabBarItem.parentTabBar === this)
68             return null;
69
70         if (this._tabAnimatedClosedSinceMouseEnter) {
71             // Delay adding the new tab until we can expand the tabs after a closed tab.
72             this._finishExpandingTabsAfterClose().then(() => {
73                 this.insertTabBarItem(tabBarItem, index, options);
74             });
75             return null;
76         }
77
78         if (tabBarItem.parentTabBar)
79             tabBarItem.parentTabBar.removeTabBarItem(tabBarItem);
80
81         tabBarItem.parentTabBar = this;
82
83         if (tabBarItem instanceof WI.GeneralTabBarItem)
84             index = Number.constrain(index, 0, this.normalTabCount);
85         else
86             index = Number.constrain(index, this.normalTabCount, this._tabBarItems.length);
87
88         if (this.element.classList.contains("animating")) {
89             requestAnimationFrame(removeStyles.bind(this));
90             options.suppressAnimations = true;
91         }
92
93         var beforeTabSizesAndPositions;
94         if (!options.suppressAnimations)
95             beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
96
97         this._tabBarItems.splice(index, 0, tabBarItem);
98
99         var nextSibling = this._tabBarItems[index + 1];
100         let nextSiblingElement = nextSibling ? nextSibling.element : this._tabBarItems.lastValue.element;
101
102         if (this.element.contains(nextSiblingElement))
103             this.element.insertBefore(tabBarItem.element, nextSiblingElement);
104         else
105             this.element.appendChild(tabBarItem.element);
106
107         this.element.classList.toggle("single-tab", !this._hasMoreThanOneNormalTab());
108
109         tabBarItem.element.style.left = null;
110         tabBarItem.element.style.width = null;
111
112         function animateTabs()
113         {
114             this.element.classList.add("animating");
115             this.element.classList.add("inserting-tab");
116
117             this._applyTabBarItemSizesAndPositions(afterTabSizesAndPositions);
118
119             this.element.addEventListener("webkitTransitionEnd", removeStylesListener);
120         }
121
122         function removeStyles()
123         {
124             this.element.classList.remove("static-layout");
125             this.element.classList.remove("animating");
126             this.element.classList.remove("inserting-tab");
127
128             tabBarItem.element.classList.remove("being-inserted");
129
130             this._clearTabBarItemSizesAndPositions();
131
132             this.element.removeEventListener("webkitTransitionEnd", removeStylesListener);
133         }
134
135         if (!options.suppressAnimations) {
136             var afterTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
137
138             this.updateLayout();
139
140             let tabBarItems = this._tabBarItemsFromLeftToRight();
141             let previousTabBarItem = tabBarItems[tabBarItems.indexOf(tabBarItem) - 1] || null;
142             let previousTabBarItemSizeAndPosition = previousTabBarItem ? beforeTabSizesAndPositions.get(previousTabBarItem) : null;
143
144             if (previousTabBarItemSizeAndPosition)
145                 beforeTabSizesAndPositions.set(tabBarItem, {left: previousTabBarItemSizeAndPosition.left + previousTabBarItemSizeAndPosition.width, width: 0});
146             else
147                 beforeTabSizesAndPositions.set(tabBarItem, {left: 0, width: 0});
148
149             this.element.classList.add("static-layout");
150             tabBarItem.element.classList.add("being-inserted");
151
152             this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions);
153
154             var removeStylesListener = removeStyles.bind(this);
155
156             requestAnimationFrame(animateTabs.bind(this));
157         } else
158             this.needsLayout();
159
160         this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemAdded, {tabBarItem});
161
162         return tabBarItem;
163     }
164
165     removeTabBarItem(tabBarItemOrIndex, options = {})
166     {
167         let tabBarItem = this._findTabBarItem(tabBarItemOrIndex);
168         if (!tabBarItem || tabBarItem instanceof WI.PinnedTabBarItem)
169             return null;
170
171         if (!tabBarItem.isEphemeral && this.normalTabCount === 1)
172             return null;
173
174         tabBarItem.parentTabBar = null;
175
176         if (this._selectedTabBarItem === tabBarItem) {
177             var index = this._tabBarItems.indexOf(tabBarItem);
178             var nextTabBarItem = this._tabBarItems[index + 1];
179             if (!nextTabBarItem || nextTabBarItem instanceof WI.PinnedTabBarItem)
180                 nextTabBarItem = this._tabBarItems[index - 1];
181
182             this.selectedTabBarItem = nextTabBarItem;
183         }
184
185         if (this.element.classList.contains("animating")) {
186             requestAnimationFrame(removeStyles.bind(this));
187             options.suppressAnimations = true;
188         }
189
190         var beforeTabSizesAndPositions;
191         if (!options.suppressAnimations)
192             beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
193
194         // Subtract 1 from normalTabCount since arrays begin indexing at 0.
195         let wasLastNormalTab = this._tabBarItems.indexOf(tabBarItem) === this.normalTabCount - 1;
196
197         this._tabBarItems.remove(tabBarItem);
198         tabBarItem.element.remove();
199
200         var hasMoreThanOneNormalTab = this._hasMoreThanOneNormalTab();
201         this.element.classList.toggle("single-tab", !hasMoreThanOneNormalTab);
202
203         if (!hasMoreThanOneNormalTab || wasLastNormalTab || !options.suppressExpansion) {
204             if (!options.suppressAnimations) {
205                 this._tabAnimatedClosedSinceMouseEnter = true;
206                 this._finishExpandingTabsAfterClose(beforeTabSizesAndPositions);
207             } else
208                 this.needsLayout();
209
210             this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemRemoved, {tabBarItem});
211             return tabBarItem;
212         }
213
214         var lastNormalTabBarItem;
215
216         function animateTabs()
217         {
218             this.element.classList.add("animating");
219             this.element.classList.add("closing-tab");
220
221             // For RTL, we need to place extra space between pinned tab and first normal tab.
222             // From left to right there is pinned tabs, extra space, then normal tabs. Compute
223             // how much extra space we need to additionally add for normal tab items.
224             let extraSpaceBetweenNormalAndPinnedTabs = 0;
225             if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) {
226                 extraSpaceBetweenNormalAndPinnedTabs = this.element.getBoundingClientRect().width;
227                 for (let currentTabBarItem of this._tabBarItemsFromLeftToRight())
228                     extraSpaceBetweenNormalAndPinnedTabs -= currentTabBarItem.element.getBoundingClientRect().width;
229             }
230
231             let left = 0;
232             for (let currentTabBarItem of this._tabBarItemsFromLeftToRight()) {
233                 let sizeAndPosition = beforeTabSizesAndPositions.get(currentTabBarItem);
234
235                 if (!(currentTabBarItem instanceof WI.PinnedTabBarItem)) {
236                     currentTabBarItem.element.style.left = extraSpaceBetweenNormalAndPinnedTabs + left + "px";
237                     left += sizeAndPosition.width;
238                     lastNormalTabBarItem = currentTabBarItem;
239                 } else
240                     left = sizeAndPosition.left + sizeAndPosition.width;
241             }
242
243             // The selected tab and last tab need to draw a right border as well, so make them 1px wider.
244             if (this._selectedTabBarItem)
245                 this._selectedTabBarItem.element.style.width = (parseFloat(this._selectedTabBarItem.element.style.width) + 1) + "px";
246
247             if (lastNormalTabBarItem !== this._selectedTabBarItem)
248                 lastNormalTabBarItem.element.style.width = (parseFloat(lastNormalTabBarItem.element.style.width) + 1) + "px";
249
250             this.element.addEventListener("webkitTransitionEnd", removeStylesListener);
251         }
252
253         function removeStyles()
254         {
255             // The selected tab needs to stop drawing the right border, so make it 1px smaller. Only if it isn't the last.
256             if (this._selectedTabBarItem && this._selectedTabBarItem !== lastNormalTabBarItem)
257                 this._selectedTabBarItem.element.style.width = (parseFloat(this._selectedTabBarItem.element.style.width) - 1) + "px";
258
259             this.element.classList.remove("animating");
260             this.element.classList.remove("closing-tab");
261
262             this.updateLayout();
263
264             this.element.removeEventListener("webkitTransitionEnd", removeStylesListener);
265         }
266
267         if (!options.suppressAnimations) {
268             this.element.classList.add("static-layout");
269
270             this._tabAnimatedClosedSinceMouseEnter = true;
271
272             this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions);
273
274             var removeStylesListener = removeStyles.bind(this);
275
276             requestAnimationFrame(animateTabs.bind(this));
277         } else
278             this.needsLayout();
279
280         this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemRemoved, {tabBarItem});
281
282         return tabBarItem;
283     }
284
285     selectPreviousTab()
286     {
287         if (this._tabBarItems.length <= 1)
288             return;
289
290         var startIndex = this._tabBarItems.indexOf(this._selectedTabBarItem);
291         var newIndex = startIndex;
292         do {
293             if (newIndex === 0)
294                 newIndex = this._tabBarItems.length - 1;
295             else
296                 newIndex--;
297
298             if (!(this._tabBarItems[newIndex] instanceof WI.PinnedTabBarItem))
299                 break;
300         } while (newIndex !== startIndex);
301
302         if (newIndex === startIndex)
303             return;
304
305         this.selectedTabBarItem = this._tabBarItems[newIndex];
306     }
307
308     selectNextTab()
309     {
310         if (this._tabBarItems.length <= 1)
311             return;
312
313         var startIndex = this._tabBarItems.indexOf(this._selectedTabBarItem);
314         var newIndex = startIndex;
315         do {
316             if (newIndex === this._tabBarItems.length - 1)
317                 newIndex = 0;
318             else
319                 newIndex++;
320
321             if (!(this._tabBarItems[newIndex] instanceof WI.PinnedTabBarItem))
322                 break;
323         } while (newIndex !== startIndex);
324
325         if (newIndex === startIndex)
326             return;
327
328         this.selectedTabBarItem = this._tabBarItems[newIndex];
329     }
330
331     get selectedTabBarItem()
332     {
333         return this._selectedTabBarItem;
334     }
335
336     set selectedTabBarItem(tabBarItemOrIndex)
337     {
338         let tabBarItem = this._findTabBarItem(tabBarItemOrIndex);
339         if (tabBarItem === this._tabPickerTabBarItem) {
340             // Get the last normal tab item if the item is not selectable.
341             tabBarItem = this._tabBarItems[this.normalTabCount - 1];
342         }
343
344         if (this._selectedTabBarItem === tabBarItem)
345             return;
346
347         if (this._selectedTabBarItem)
348             this._selectedTabBarItem.selected = false;
349
350         this._selectedTabBarItem = tabBarItem || null;
351
352         if (this._selectedTabBarItem) {
353             this._selectedTabBarItem.selected = true;
354             if (this._selectedTabBarItem.element.classList.contains("hidden"))
355                 this.needsLayout();
356         }
357
358         this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemSelected);
359     }
360
361     get tabBarItems()
362     {
363         return this._tabBarItems;
364     }
365
366     get normalTabCount()
367     {
368         return this._tabBarItems.filter((item) => !(item instanceof WI.PinnedTabBarItem)).length;
369     }
370
371     get saveableTabCount()
372     {
373         return this._tabBarItems.filter((item) => item.representedObject && item.representedObject.constructor.shouldSaveTab()).length;
374     }
375
376     // Protected
377
378     layout()
379     {
380         if (this.element.classList.contains("static-layout"))
381             return;
382
383         this.element.classList.add("calculate-width");
384         this.element.classList.remove("collapsed");
385
386         function forceItemHidden(item, hidden) {
387             item.element.classList.toggle("hidden", !!hidden);
388         }
389
390         for (let item of this._tabBarItems)
391             forceItemHidden(item, item === this._tabPickerTabBarItem);
392
393         function measureItemWidth(item) {
394             if (!item[WI.TabBar.CachedWidthSymbol])
395                 item[WI.TabBar.CachedWidthSymbol] = item.element.realOffsetWidth;
396             return item[WI.TabBar.CachedWidthSymbol];
397         }
398
399         let recalculateItemWidths = () => {
400             return this._tabBarItems.reduce((total, item) => {
401                 item[WI.TabBar.CachedWidthSymbol] = undefined;
402                 return total + measureItemWidth(item);
403             }, 0);
404         };
405
406         this._hiddenTabBarItems = [];
407
408         let totalItemWidth = recalculateItemWidths();
409         let barWidth = this.element.realOffsetWidth;
410
411         if (totalItemWidth > barWidth) {
412             this.element.classList.add("collapsed");
413             totalItemWidth = recalculateItemWidths();
414             if (totalItemWidth > barWidth) {
415                 forceItemHidden(this._tabPickerTabBarItem, false);
416                 totalItemWidth += measureItemWidth(this._tabPickerTabBarItem);
417             }
418
419             let tabBarItems = this._tabBarItemsFromLeftToRight();
420             let index = tabBarItems.length;
421             while (totalItemWidth > barWidth && --index >= 0) {
422                 let item = tabBarItems[index];
423                 if (item === this.selectedTabBarItem || item instanceof WI.PinnedTabBarItem)
424                     continue;
425
426                 totalItemWidth -= measureItemWidth(item);
427                 forceItemHidden(item, true);
428
429                 this._hiddenTabBarItems.push(item);
430             }
431         }
432
433         this.element.classList.remove("calculate-width");
434     }
435
436     // Private
437
438     _tabBarItemsFromLeftToRight()
439     {
440         return WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? this._tabBarItems : this._tabBarItems.slice().reverse();
441     }
442
443     _findTabBarItem(tabBarItemOrIndex)
444     {
445         if (typeof tabBarItemOrIndex === "number")
446             return this._tabBarItems[tabBarItemOrIndex] || null;
447
448         if (tabBarItemOrIndex instanceof WI.TabBarItem) {
449             if (this._tabBarItems.includes(tabBarItemOrIndex))
450                 return tabBarItemOrIndex;
451         }
452
453         return null;
454     }
455
456     _hasMoreThanOneNormalTab()
457     {
458         let normalTabCount = 0;
459         for (let tabBarItem of this._tabBarItems) {
460             if (tabBarItem instanceof WI.PinnedTabBarItem)
461                 continue;
462
463             ++normalTabCount;
464             if (normalTabCount >= 2)
465                 return true;
466         }
467
468         return false;
469     }
470
471     _recordTabBarItemSizesAndPositions()
472     {
473         var tabBarItemSizesAndPositions = new Map;
474
475         const barRect = this.element.getBoundingClientRect();
476
477         for (var tabBarItem of this._tabBarItems) {
478             var boundingRect = tabBarItem.element.getBoundingClientRect();
479             tabBarItemSizesAndPositions.set(tabBarItem, {left: boundingRect.left - barRect.left, width: boundingRect.width});
480         }
481
482         return tabBarItemSizesAndPositions;
483     }
484
485     _applyTabBarItemSizesAndPositions(tabBarItemSizesAndPositions, skipTabBarItem)
486     {
487         for (var [tabBarItem, sizeAndPosition] of tabBarItemSizesAndPositions) {
488             if (skipTabBarItem && tabBarItem === skipTabBarItem)
489                 continue;
490             tabBarItem.element.style.left = sizeAndPosition.left + "px";
491             tabBarItem.element.style.width = sizeAndPosition.width + "px";
492         }
493     }
494
495     _clearTabBarItemSizesAndPositions(skipTabBarItem)
496     {
497         for (var tabBarItem of this._tabBarItems) {
498             if (skipTabBarItem && tabBarItem === skipTabBarItem)
499                 continue;
500             tabBarItem.element.style.left = null;
501             tabBarItem.element.style.width = null;
502         }
503     }
504
505     _finishExpandingTabsAfterClose(beforeTabSizesAndPositions)
506     {
507         return new Promise(function(resolve, reject) {
508             console.assert(this._tabAnimatedClosedSinceMouseEnter);
509             this._tabAnimatedClosedSinceMouseEnter = false;
510
511             if (!beforeTabSizesAndPositions)
512                 beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
513
514             this.element.classList.remove("static-layout");
515             this._clearTabBarItemSizesAndPositions();
516
517             var afterTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
518
519             this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions);
520             this.element.classList.add("static-layout");
521
522             function animateTabs()
523             {
524                 this.element.classList.add("static-layout");
525                 this.element.classList.add("animating");
526                 this.element.classList.add("expanding-tabs");
527
528                 this._applyTabBarItemSizesAndPositions(afterTabSizesAndPositions);
529
530                 this.element.addEventListener("webkitTransitionEnd", removeStylesListener);
531             }
532
533             function removeStyles()
534             {
535                 this.element.classList.remove("static-layout");
536                 this.element.classList.remove("animating");
537                 this.element.classList.remove("expanding-tabs");
538
539                 this._clearTabBarItemSizesAndPositions();
540
541                 this.updateLayout();
542
543                 this.element.removeEventListener("webkitTransitionEnd", removeStylesListener);
544
545                 resolve();
546             }
547
548             var removeStylesListener = removeStyles.bind(this);
549
550             requestAnimationFrame(animateTabs.bind(this));
551         }.bind(this));
552     }
553
554     _handleMouseDown(event)
555     {
556         // Only consider left mouse clicks for tab movement.
557         if (event.button !== 0 || event.ctrlKey)
558             return;
559
560         let itemElement = event.target.closest("." + WI.TabBarItem.StyleClassName);
561         if (!itemElement)
562             return;
563
564         let tabBarItem = itemElement[WI.TabBarItem.ElementReferenceSymbol];
565         if (!tabBarItem)
566             return;
567
568         if (tabBarItem.disabled)
569             return;
570
571         if (tabBarItem === this._tabPickerTabBarItem) {
572             if (!this._hiddenTabBarItems.length)
573                 return;
574
575             if (this._ignoreTabPickerMouseDown)
576                 return;
577
578             this._ignoreTabPickerMouseDown = true;
579
580             let contextMenu = WI.ContextMenu.createFromEvent(event);
581             contextMenu.addBeforeShowCallback(() => {
582                 this._ignoreTabPickerMouseDown = false;
583             });
584
585             for (let item of this._hiddenTabBarItems) {
586                 contextMenu.appendItem(item.title, () => {
587                     this.selectedTabBarItem = item;
588                 });
589             }
590
591             contextMenu.show();
592             return;
593         }
594
595         let closeButtonElement = event.target.closest("." + WI.TabBarItem.CloseButtonStyleClassName);
596         if (closeButtonElement)
597             return;
598
599         this.selectedTabBarItem = tabBarItem;
600
601         if (tabBarItem instanceof WI.PinnedTabBarItem || !this._hasMoreThanOneNormalTab())
602             return;
603
604         this._firstNormalTabItemIndex = 0;
605         for (let i = 0; i < this._tabBarItems.length; ++i) {
606             if (this._tabBarItems[i] instanceof WI.PinnedTabBarItem)
607                 continue;
608
609             this._firstNormalTabItemIndex = i;
610             break;
611         }
612
613         this._mouseIsDown = true;
614
615         this._mouseMovedEventListener = this._handleMouseMoved.bind(this);
616         this._mouseUpEventListener = this._handleMouseUp.bind(this);
617
618         // Register these listeners on the document so we can track the mouse if it leaves the tab bar.
619         document.addEventListener("mousemove", this._mouseMovedEventListener, true);
620         document.addEventListener("mouseup", this._mouseUpEventListener, true);
621
622         event.preventDefault();
623         event.stopPropagation();
624     }
625
626     _handleClick(event)
627     {
628         var itemElement = event.target.closest("." + WI.TabBarItem.StyleClassName);
629         if (!itemElement)
630             return;
631
632         var tabBarItem = itemElement[WI.TabBarItem.ElementReferenceSymbol];
633         if (!tabBarItem)
634             return;
635
636         if (tabBarItem.disabled)
637             return;
638
639         const clickedMiddleButton = event.button === 1;
640
641         var closeButtonElement = event.target.closest("." + WI.TabBarItem.CloseButtonStyleClassName);
642         if (closeButtonElement || clickedMiddleButton) {
643             // Disallow closing the only tab.
644             if (this.element.classList.contains("single-tab"))
645                 return;
646
647             if (!event.altKey) {
648                 this.removeTabBarItem(tabBarItem, {suppressExpansion: true});
649                 return;
650             }
651
652             for (let i = this._tabBarItems.length - 1; i >= 0; --i) {
653                 let item = this._tabBarItems[i];
654                 if (item === tabBarItem || item instanceof WI.PinnedTabBarItem)
655                     continue;
656                 this.removeTabBarItem(item);
657             }
658         }
659     }
660
661     _handleMouseMoved(event)
662     {
663         console.assert(event.button === 0);
664         console.assert(this._mouseIsDown);
665         if (!this._mouseIsDown)
666             return;
667
668         console.assert(this._selectedTabBarItem);
669         if (!this._selectedTabBarItem)
670             return;
671
672         event.preventDefault();
673         event.stopPropagation();
674
675         if (!this.element.classList.contains("static-layout")) {
676             this._applyTabBarItemSizesAndPositions(this._recordTabBarItemSizesAndPositions());
677             this.element.classList.add("static-layout");
678             this.element.classList.add("dragging-tab");
679         }
680
681         if (this._mouseOffset === undefined)
682             this._mouseOffset = event.pageX - this._selectedTabBarItem.element.totalOffsetLeft;
683
684         var tabBarMouseOffset = event.pageX - this.element.totalOffsetLeft;
685         var newLeft = tabBarMouseOffset - this._mouseOffset;
686
687         this._selectedTabBarItem.element.style.left = newLeft + "px";
688
689         var selectedTabMidX = newLeft + (this._selectedTabBarItem.element.realOffsetWidth / 2);
690
691         var currentIndex = this._tabBarItems.indexOf(this._selectedTabBarItem);
692         var newIndex = currentIndex;
693
694         for (let tabBarItem of this._tabBarItems) {
695             if (tabBarItem === this._selectedTabBarItem)
696                 continue;
697
698             var tabBarItemRect = tabBarItem.element.getBoundingClientRect();
699
700             if (selectedTabMidX < tabBarItemRect.left || selectedTabMidX > tabBarItemRect.right)
701                 continue;
702
703             newIndex = this._tabBarItems.indexOf(tabBarItem);
704             break;
705         }
706
707         // Subtract 1 from normalTabCount since arrays begin indexing at 0.
708         newIndex = Number.constrain(newIndex, this._firstNormalTabItemIndex, this.normalTabCount - 1);
709
710         if (currentIndex === newIndex)
711             return;
712
713         this._tabBarItems.splice(currentIndex, 1);
714         this._tabBarItems.splice(newIndex, 0, this._selectedTabBarItem);
715
716         let nextSibling = this._tabBarItems[newIndex + 1];
717         let nextSiblingElement = nextSibling ? nextSibling.element : null;
718
719         this.element.insertBefore(this._selectedTabBarItem.element, nextSiblingElement);
720
721         // FIXME: Animate the tabs that move to make room for the selected tab. This was causing me trouble when I tried.
722
723         let left = 0;
724         for (let tabBarItem of this._tabBarItemsFromLeftToRight()) {
725             if (tabBarItem !== this._selectedTabBarItem && parseFloat(tabBarItem.element.style.left) !== left)
726                 tabBarItem.element.style.left = left + "px";
727             left += parseFloat(tabBarItem.element.style.width);
728         }
729     }
730
731     _handleMouseUp(event)
732     {
733         console.assert(event.button === 0);
734         console.assert(this._mouseIsDown);
735         if (!this._mouseIsDown)
736             return;
737
738         this.element.classList.remove("dragging-tab");
739
740         if (!this._tabAnimatedClosedSinceMouseEnter) {
741             this.element.classList.remove("static-layout");
742             this._clearTabBarItemSizesAndPositions();
743         } else {
744             let left = 0;
745             for (let tabBarItem of this._tabBarItemsFromLeftToRight()) {
746                 if (tabBarItem === this._selectedTabBarItem)
747                     tabBarItem.element.style.left = left + "px";
748                 left += parseFloat(tabBarItem.element.style.width);
749             }
750         }
751
752         this._mouseIsDown = false;
753         this._mouseOffset = undefined;
754
755         document.removeEventListener("mousemove", this._mouseMovedEventListener, true);
756         document.removeEventListener("mouseup", this._mouseUpEventListener, true);
757
758         this._mouseMovedEventListener = null;
759         this._mouseUpEventListener = null;
760
761         event.preventDefault();
762         event.stopPropagation();
763
764         this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemsReordered);
765     }
766
767     _handleMouseLeave(event)
768     {
769         if (this._mouseIsDown || !this._tabAnimatedClosedSinceMouseEnter || !this.element.classList.contains("static-layout") || this.element.classList.contains("animating"))
770             return;
771
772         // This event can still fire when the mouse is inside the element if DOM nodes are added, removed or generally change inside.
773         // Check if the mouse really did leave the element by checking the bounds.
774         // FIXME: Is this a WebKit bug or correct behavior?
775         const barRect = this.element.getBoundingClientRect();
776         if (event.pageY > barRect.top && event.pageY < barRect.bottom && event.pageX > barRect.left && event.pageX < barRect.right)
777             return;
778
779         this._finishExpandingTabsAfterClose();
780     }
781
782     _handleContextMenu(event)
783     {
784         let contextMenu = WI.ContextMenu.createFromEvent(event);
785
786         for (let tabClass of WI.knownTabClasses()) {
787             if (!tabClass.isTabAllowed() || tabClass.tabInfo().isEphemeral)
788                 continue;
789
790             let openTabBarItem = null;
791             for (let tabBarItem of this._tabBarItems) {
792                 let tabContentView = tabBarItem.representedObject;
793                 if (!(tabContentView instanceof WI.TabContentView))
794                     continue;
795
796                 if (tabContentView.type === tabClass.Type) {
797                     openTabBarItem = tabBarItem;
798                     break;
799                 }
800             }
801
802             let checked = !!openTabBarItem;
803             let disabled = checked && this.normalTabCount === 1;
804             contextMenu.appendCheckboxItem(tabClass.tabInfo().title, () => {
805                 if (openTabBarItem)
806                     this.removeTabBarItem(openTabBarItem);
807                 else
808                     WI.createNewTabWithType(tabClass.Type, {shouldShowNewTab: true});
809             }, checked, disabled);
810         }
811     }
812 };
813
814 WI.TabBar.CachedWidthSymbol = Symbol("cached-width");
815
816 WI.TabBar.Event = {
817     TabBarItemSelected: "tab-bar-tab-bar-item-selected",
818     TabBarItemAdded: "tab-bar-tab-bar-item-added",
819     TabBarItemRemoved: "tab-bar-tab-bar-item-removed",
820     TabBarItemsReordered: "tab-bar-tab-bar-items-reordered",
821     OpenDefaultTab: "tab-bar-open-default-tab"
822 };