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