Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / ui / TabbedPane.js
blob3b365a772da8cafcc2136a1994df2f1140800b64
1 /*
2 * Copyright (C) 2010 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 /**
32 * @extends {WebInspector.VBox}
33 * @constructor
35 WebInspector.TabbedPane = function()
37 WebInspector.VBox.call(this, true);
38 this.registerRequiredCSS("ui/tabbedPane.css");
39 this.element.classList.add("tabbed-pane");
40 this.contentElement.classList.add("tabbed-pane-shadow");
41 this.contentElement.tabIndex = -1;
42 this._headerElement = this.contentElement.createChild("div", "tabbed-pane-header toolbar-colors");
43 this._headerElement.createChild("content").select = ".tabbed-pane-header-before";
44 this._headerContentsElement = this._headerElement.createChild("div", "tabbed-pane-header-contents");
45 this._tabSlider = createElementWithClass("div", "tabbed-pane-tab-slider");
46 this._headerElement.createChild("content").select = ".tabbed-pane-header-after";
47 this._tabsElement = this._headerContentsElement.createChild("div", "tabbed-pane-header-tabs");
48 this._contentElement = this.contentElement.createChild("div", "tabbed-pane-content");
49 this._contentElement.createChild("content");
50 /** @type {!Array.<!WebInspector.TabbedPaneTab>} */
51 this._tabs = [];
52 /** @type {!Array.<!WebInspector.TabbedPaneTab>} */
53 this._tabsHistory = [];
54 /** @type {!Object.<string, !WebInspector.TabbedPaneTab>} */
55 this._tabsById = {};
56 this._currentTabLocked = false;
58 this._dropDownButton = this._createDropDownButton();
59 WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._zoomChanged, this);
62 WebInspector.TabbedPane.EventTypes = {
63 TabSelected: "TabSelected",
64 TabClosed: "TabClosed",
65 TabOrderChanged: "TabOrderChanged"
68 WebInspector.TabbedPane.prototype = {
69 /**
70 * @param {boolean} locked
72 setCurrentTabLocked: function(locked)
74 this._currentTabLocked = locked;
75 this._headerElement.classList.toggle("locked", this._currentTabLocked);
78 /**
79 * @return {?WebInspector.Widget}
81 get visibleView()
83 return this._currentTab ? this._currentTab.view : null;
86 /**
87 * @return {!Array.<!WebInspector.Widget>}
89 tabViews: function()
91 /**
92 * @param {!WebInspector.TabbedPaneTab} tab
93 * @return {!WebInspector.Widget}
95 function tabToView(tab)
97 return tab.view;
99 return this._tabs.map(tabToView);
103 * @return {?string}
105 get selectedTabId()
107 return this._currentTab ? this._currentTab.id : null;
111 * @param {boolean} shrinkableTabs
113 setShrinkableTabs: function(shrinkableTabs)
115 this._shrinkableTabs = shrinkableTabs;
119 * @param {boolean} verticalTabLayout
121 setVerticalTabLayout: function(verticalTabLayout)
123 this._verticalTabLayout = verticalTabLayout;
124 this.contentElement.classList.add("vertical-tab-layout");
125 this.invalidateConstraints();
129 * @param {boolean} closeableTabs
131 setCloseableTabs: function(closeableTabs)
133 this._closeableTabs = closeableTabs;
137 * @override
138 * @return {!Element}
140 defaultFocusedElement: function()
142 return this.visibleView ? this.visibleView.defaultFocusedElement() : this.contentElement;
145 focus: function()
147 if (this.visibleView)
148 this.visibleView.focus();
149 else
150 this.contentElement.focus();
154 * @return {!Element}
156 headerElement: function()
158 return this._headerElement;
162 * @param {string} id
163 * @return {boolean}
165 isTabCloseable: function(id)
167 var tab = this._tabsById[id];
168 return tab ? tab.isCloseable() : false;
172 * @param {!WebInspector.TabbedPaneTabDelegate} delegate
174 setTabDelegate: function(delegate)
176 var tabs = this._tabs.slice();
177 for (var i = 0; i < tabs.length; ++i)
178 tabs[i].setDelegate(delegate);
179 this._delegate = delegate;
183 * @param {string} id
184 * @param {string} tabTitle
185 * @param {!WebInspector.Widget} view
186 * @param {string=} tabTooltip
187 * @param {boolean=} userGesture
188 * @param {boolean=} isCloseable
189 * @param {number=} index
191 appendTab: function(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index)
193 isCloseable = typeof isCloseable === "boolean" ? isCloseable : this._closeableTabs;
194 var tab = new WebInspector.TabbedPaneTab(this, id, tabTitle, isCloseable, view, tabTooltip);
195 tab.setDelegate(this._delegate);
196 this._tabsById[id] = tab;
197 if (index !== undefined)
198 this._tabs.splice(index, 0, tab);
199 else
200 this._tabs.push(tab);
201 this._tabsHistory.push(tab);
202 if (this._tabsHistory[0] === tab && this.isShowing())
203 this.selectTab(tab.id, userGesture);
204 this._updateTabElements();
208 * @param {string} id
209 * @param {boolean=} userGesture
211 closeTab: function(id, userGesture)
213 this.closeTabs([id], userGesture);
217 * @param {!Array.<string>} ids
218 * @param {boolean=} userGesture
220 closeTabs: function(ids, userGesture)
222 var focused = this.hasFocus();
223 for (var i = 0; i < ids.length; ++i)
224 this._innerCloseTab(ids[i], userGesture);
225 this._updateTabElements();
226 if (this._tabsHistory.length)
227 this.selectTab(this._tabsHistory[0].id, false);
228 if (focused)
229 this.focus();
233 * @param {string} id
234 * @param {boolean=} userGesture
236 _innerCloseTab: function(id, userGesture)
238 if (!this._tabsById[id])
239 return;
240 if (userGesture && !this._tabsById[id]._closeable)
241 return;
242 if (this._currentTab && this._currentTab.id === id)
243 this._hideCurrentTab();
245 var tab = this._tabsById[id];
246 delete this._tabsById[id];
248 this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1);
249 this._tabs.splice(this._tabs.indexOf(tab), 1);
250 if (tab._shown)
251 this._hideTabElement(tab);
253 var eventData = { tabId: id, view: tab.view, isUserGesture: userGesture };
254 this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabClosed, eventData);
255 return true;
259 * @param {string} tabId
260 * @return {boolean}
262 hasTab: function(tabId)
264 return !!this._tabsById[tabId];
268 * @return {!Array.<string>}
270 allTabs: function()
272 return this._tabs.map(function (tab) { return tab.id; });
276 * @param {string} id
277 * @return {!Array.<string>}
279 otherTabs: function(id)
281 var result = [];
282 for (var i = 0; i < this._tabs.length; ++i) {
283 if (this._tabs[i].id !== id)
284 result.push(this._tabs[i].id);
286 return result;
290 * @param {string} id
291 * @return {!Array.<string>}
293 _tabsToTheRight: function(id)
295 var index = -1;
296 for (var i = 0; i < this._tabs.length; ++i) {
297 if (this._tabs[i].id === id) {
298 index = i;
299 break;
302 if (index === -1)
303 return [];
304 return this._tabs.slice(index + 1).map(function (tab) { return tab.id; });
308 * @param {string} id
309 * @param {boolean=} userGesture
310 * @return {boolean}
312 selectTab: function(id, userGesture)
314 if (this._currentTabLocked)
315 return false;
316 var focused = this.hasFocus();
317 var tab = this._tabsById[id];
318 if (!tab)
319 return false;
320 if (this._currentTab && this._currentTab.id === id)
321 return true;
323 this._hideCurrentTab();
324 this._showTab(tab);
325 this._currentTab = tab;
327 this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1);
328 this._tabsHistory.splice(0, 0, tab);
330 this._updateTabElements();
331 if (focused)
332 this.focus();
334 var eventData = { tabId: id, view: tab.view, isUserGesture: userGesture };
335 this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabSelected, eventData);
336 return true;
340 * @param {number} tabsCount
341 * @return {!Array.<string>}
343 lastOpenedTabIds: function(tabsCount)
345 function tabToTabId(tab) {
346 return tab.id;
349 return this._tabsHistory.slice(0, tabsCount).map(tabToTabId);
353 * @param {string} id
354 * @param {string} iconType
355 * @param {string=} iconTooltip
357 setTabIcon: function(id, iconType, iconTooltip)
359 var tab = this._tabsById[id];
360 if (tab._setIconType(iconType, iconTooltip))
361 this._updateTabElements();
365 * @param {string} id
366 * @param {string} className
367 * @param {boolean=} force
369 toggleTabClass: function(id, className, force)
371 var tab = this._tabsById[id];
372 if (tab._toggleClass(className, force))
373 this._updateTabElements();
377 * @param {!WebInspector.Event} event
379 _zoomChanged: function(event)
381 for (var i = 0; i < this._tabs.length; ++i)
382 delete this._tabs[i]._measuredWidth;
383 if (this.isShowing())
384 this._updateTabElements();
388 * @param {string} id
389 * @param {string} tabTitle
391 changeTabTitle: function(id, tabTitle)
393 var tab = this._tabsById[id];
394 if (tab.title === tabTitle)
395 return;
396 tab.title = tabTitle;
397 this._updateTabElements();
401 * @param {string} id
402 * @param {!WebInspector.Widget} view
404 changeTabView: function(id, view)
406 var tab = this._tabsById[id];
407 if (this._currentTab && this._currentTab.id === tab.id) {
408 if (tab.view !== view)
409 this._hideTab(tab);
410 tab.view = view;
411 this._showTab(tab);
412 } else
413 tab.view = view;
416 onResize: function()
418 this._updateTabElements();
421 headerResized: function()
423 this._updateTabElements();
426 wasShown: function()
428 var effectiveTab = this._currentTab || this._tabsHistory[0];
429 if (effectiveTab)
430 this.selectTab(effectiveTab.id);
434 * @param {boolean} enable
436 setTabSlider: function(enable)
438 this._sliderEnabled = enable;
439 this._tabSlider.classList.toggle("enabled", enable);
443 * @override
444 * @return {!Constraints}
446 calculateConstraints: function()
448 var constraints = WebInspector.VBox.prototype.calculateConstraints.call(this);
449 var minContentConstraints = new Constraints(new Size(0, 0), new Size(50, 50));
450 constraints = constraints.widthToMax(minContentConstraints).heightToMax(minContentConstraints);
451 if (this._verticalTabLayout)
452 constraints = constraints.addWidth(new Constraints(new Size(120, 0)));
453 else
454 constraints = constraints.addHeight(new Constraints(new Size(0, 30)));
455 return constraints;
458 _updateTabElements: function()
460 WebInspector.invokeOnceAfterBatchUpdate(this, this._innerUpdateTabElements);
464 * @param {string} text
466 setPlaceholderText: function(text)
468 this._noTabsMessage = text;
471 _innerUpdateTabElements: function()
473 if (!this.isShowing())
474 return;
476 if (!this._tabs.length) {
477 this._contentElement.classList.add("has-no-tabs");
478 if (this._noTabsMessage && !this._noTabsMessageElement) {
479 this._noTabsMessageElement = this._contentElement.createChild("div", "tabbed-pane-placeholder fill");
480 this._noTabsMessageElement.textContent = this._noTabsMessage;
482 } else {
483 this._contentElement.classList.remove("has-no-tabs");
484 if (this._noTabsMessageElement) {
485 this._noTabsMessageElement.remove();
486 delete this._noTabsMessageElement;
490 if (!this._measuredDropDownButtonWidth)
491 this._measureDropDownButton();
493 this._updateWidths();
494 this._updateTabsDropDown();
495 this._updateTabSlider();
499 * @param {number} index
500 * @param {!WebInspector.TabbedPaneTab} tab
502 _showTabElement: function(index, tab)
504 if (index >= this._tabsElement.children.length)
505 this._tabsElement.appendChild(tab.tabElement);
506 else
507 this._tabsElement.insertBefore(tab.tabElement, this._tabsElement.children[index]);
508 tab._shown = true;
512 * @param {!WebInspector.TabbedPaneTab} tab
514 _hideTabElement: function(tab)
516 this._tabsElement.removeChild(tab.tabElement);
517 tab._shown = false;
520 _createDropDownButton: function()
522 var dropDownContainer = createElementWithClass("div", "tabbed-pane-header-tabs-drop-down-container");
523 dropDownContainer.createChild("div", "glyph");
524 this._dropDownMenu = new WebInspector.DropDownMenu(dropDownContainer);
525 this._dropDownMenu.addEventListener(WebInspector.DropDownMenu.Events.ItemSelected, this._dropDownMenuItemSelected, this);
527 return dropDownContainer;
531 * @param {!WebInspector.Event} event
533 _dropDownMenuItemSelected: function(event)
535 var tabId = /** @type {string} */ (event.data);
536 this._lastSelectedOverflowTab = this._tabsById[tabId];
537 this.selectTab(tabId, true);
540 _totalWidth: function()
542 return this._headerContentsElement.getBoundingClientRect().width;
546 * @return {number}
548 _numberOfTabsShown: function()
550 var numTabsShown = 0;
551 for (var tab of this._tabs) {
552 if (tab._shown)
553 numTabsShown++;
555 return numTabsShown;
558 _updateTabsDropDown: function()
560 var tabsToShowIndexes = this._tabsToShowIndexes(this._tabs, this._tabsHistory, this._totalWidth(), this._measuredDropDownButtonWidth);
561 if (this._lastSelectedOverflowTab && this._numberOfTabsShown() !== tabsToShowIndexes.length) {
562 delete this._lastSelectedOverflowTab;
563 this._updateTabsDropDown();
564 return;
567 for (var i = 0; i < this._tabs.length; ++i) {
568 if (this._tabs[i]._shown && tabsToShowIndexes.indexOf(i) === -1)
569 this._hideTabElement(this._tabs[i]);
571 for (var i = 0; i < tabsToShowIndexes.length; ++i) {
572 var tab = this._tabs[tabsToShowIndexes[i]];
573 if (!tab._shown)
574 this._showTabElement(i, tab);
577 this._populateDropDownFromIndex();
580 _populateDropDownFromIndex: function()
582 if (this._dropDownButton.parentElement)
583 this._headerContentsElement.removeChild(this._dropDownButton);
585 this._dropDownMenu.clear();
587 var tabsToShow = [];
588 for (var i = 0; i < this._tabs.length; ++i) {
589 if (!this._tabs[i]._shown)
590 tabsToShow.push(this._tabs[i]);
591 continue;
594 var selectedId = null;
595 for (var i = 0; i < tabsToShow.length; ++i) {
596 var tab = tabsToShow[i];
597 this._dropDownMenu.addItem(tab.id, tab.title);
598 if (this._tabsHistory[0] === tab)
599 selectedId = tab.id;
601 if (tabsToShow.length) {
602 this._headerContentsElement.appendChild(this._dropDownButton);
603 this._dropDownMenu.selectItem(selectedId);
607 _measureDropDownButton: function()
609 this._dropDownButton.classList.add("measuring");
610 this._headerContentsElement.appendChild(this._dropDownButton);
611 this._measuredDropDownButtonWidth = this._dropDownButton.getBoundingClientRect().width;
612 this._headerContentsElement.removeChild(this._dropDownButton);
613 this._dropDownButton.classList.remove("measuring");
616 _updateWidths: function()
618 var measuredWidths = this._measureWidths();
619 var maxWidth = this._shrinkableTabs ? this._calculateMaxWidth(measuredWidths.slice(), this._totalWidth()) : Number.MAX_VALUE;
621 var i = 0;
622 for (var tabId in this._tabs) {
623 var tab = this._tabs[tabId];
624 tab.setWidth(this._verticalTabLayout ? -1 : Math.min(maxWidth, measuredWidths[i++]));
628 _measureWidths: function()
630 // Add all elements to measure into this._tabsElement
631 this._tabsElement.style.setProperty("width", "2000px");
632 var measuringTabElements = [];
633 for (var tabId in this._tabs) {
634 var tab = this._tabs[tabId];
635 if (typeof tab._measuredWidth === "number")
636 continue;
637 var measuringTabElement = tab._createTabElement(true);
638 measuringTabElement.__tab = tab;
639 measuringTabElements.push(measuringTabElement);
640 this._tabsElement.appendChild(measuringTabElement);
643 // Perform measurement
644 for (var i = 0; i < measuringTabElements.length; ++i) {
645 var width = measuringTabElements[i].getBoundingClientRect().width;
646 measuringTabElements[i].__tab._measuredWidth = Math.ceil(width);
649 // Nuke elements from the UI
650 for (var i = 0; i < measuringTabElements.length; ++i)
651 measuringTabElements[i].remove();
653 // Combine the results.
654 var measuredWidths = [];
655 for (var tabId in this._tabs)
656 measuredWidths.push(this._tabs[tabId]._measuredWidth);
657 this._tabsElement.style.removeProperty("width");
659 return measuredWidths;
663 * @param {!Array.<number>} measuredWidths
664 * @param {number} totalWidth
666 _calculateMaxWidth: function(measuredWidths, totalWidth)
668 if (!measuredWidths.length)
669 return 0;
671 measuredWidths.sort(function(x, y) { return x - y; });
673 var totalMeasuredWidth = 0;
674 for (var i = 0; i < measuredWidths.length; ++i)
675 totalMeasuredWidth += measuredWidths[i];
677 if (totalWidth >= totalMeasuredWidth)
678 return measuredWidths[measuredWidths.length - 1];
680 var totalExtraWidth = 0;
681 for (var i = measuredWidths.length - 1; i > 0; --i) {
682 var extraWidth = measuredWidths[i] - measuredWidths[i - 1];
683 totalExtraWidth += (measuredWidths.length - i) * extraWidth;
685 if (totalWidth + totalExtraWidth >= totalMeasuredWidth)
686 return measuredWidths[i - 1] + (totalWidth + totalExtraWidth - totalMeasuredWidth) / (measuredWidths.length - i);
689 return totalWidth / measuredWidths.length;
693 * @param {!Array.<!WebInspector.TabbedPaneTab>} tabsOrdered
694 * @param {!Array.<!WebInspector.TabbedPaneTab>} tabsHistory
695 * @param {number} totalWidth
696 * @param {number} measuredDropDownButtonWidth
697 * @return {!Array.<number>}
699 _tabsToShowIndexes: function(tabsOrdered, tabsHistory, totalWidth, measuredDropDownButtonWidth)
701 var tabsToShowIndexes = [];
703 var totalTabsWidth = 0;
704 var tabCount = tabsOrdered.length;
705 var tabsToLookAt = tabsOrdered.slice(0);
706 if (this._currentTab !== undefined)
707 tabsToLookAt.unshift(tabsToLookAt.splice(tabsToLookAt.indexOf(this._currentTab), 1)[0]);
708 if (this._lastSelectedOverflowTab !== undefined)
709 tabsToLookAt.unshift(tabsToLookAt.splice(tabsToLookAt.indexOf(this._lastSelectedOverflowTab), 1)[0]);
710 for (var i = 0; i < tabCount; ++i) {
711 var tab = this._automaticReorder ? tabsHistory[i] : tabsToLookAt[i];
712 totalTabsWidth += tab.width();
713 var minimalRequiredWidth = totalTabsWidth;
714 if (i !== tabCount - 1)
715 minimalRequiredWidth += measuredDropDownButtonWidth;
716 if (!this._verticalTabLayout && minimalRequiredWidth > totalWidth)
717 break;
718 tabsToShowIndexes.push(tabsOrdered.indexOf(tab));
721 tabsToShowIndexes.sort(function(x, y) { return x - y; });
723 return tabsToShowIndexes;
726 _hideCurrentTab: function()
728 if (!this._currentTab)
729 return;
731 this._hideTab(this._currentTab);
732 delete this._currentTab;
736 * @param {!WebInspector.TabbedPaneTab} tab
738 _showTab: function(tab)
740 tab.tabElement.classList.add("selected");
741 tab.view.show(this.element);
742 this._updateTabSlider();
745 _updateTabSlider: function()
747 if (!this._currentTab || !this._sliderEnabled)
748 return;
749 var left = 0;
750 for (var i = 0; i < this._tabs.length && this._currentTab !== this._tabs[i] && this._tabs[i]._shown; i++)
751 left += this._tabs[i]._measuredWidth;
752 var sliderWidth = this._currentTab._shown ? this._currentTab._measuredWidth : this._dropDownButton.offsetWidth;
753 var scaleFactor = window.devicePixelRatio >= 1.5 ? " scaleY(0.75)" : "";
754 this._tabSlider.style.transform = "translateX(" + left + "px)" + scaleFactor;
755 this._tabSlider.style.width = sliderWidth + "px";
757 if (this._tabSlider.parentElement !== this._headerContentsElement)
758 this._headerContentsElement.appendChild(this._tabSlider);
762 * @param {!WebInspector.TabbedPaneTab} tab
764 _hideTab: function(tab)
766 tab.tabElement.classList.remove("selected");
767 tab.view.detach();
771 * @override
772 * @return {!Array.<!Element>}
774 elementsToRestoreScrollPositionsFor: function()
776 return [ this._contentElement ];
780 * @param {!WebInspector.TabbedPaneTab} tab
781 * @param {number} index
783 _insertBefore: function(tab, index)
785 this._tabsElement.insertBefore(tab._tabElement || null, this._tabsElement.childNodes[index]);
786 var oldIndex = this._tabs.indexOf(tab);
787 this._tabs.splice(oldIndex, 1);
788 if (oldIndex < index)
789 --index;
790 this._tabs.splice(index, 0, tab);
791 this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabOrderChanged, this._tabs);
795 * @param {!Element} element
797 insertBeforeTabStrip: function(element)
799 element.classList.add("tabbed-pane-header-before");
800 this.element.appendChild(element);
804 * @param {!Element} element
806 appendAfterTabStrip: function(element)
808 element.classList.add("tabbed-pane-header-after");
809 this.element.appendChild(element);
812 renderWithNoHeaderBackground: function()
814 this._headerElement.classList.add("tabbed-pane-no-header-background");
818 * @param {boolean} allow
819 * @param {boolean=} automatic
821 setAllowTabReorder: function(allow, automatic)
823 this._allowTabReorder = allow;
824 this._automaticReorder = automatic;
827 __proto__: WebInspector.VBox.prototype
831 * @constructor
832 * @param {!WebInspector.TabbedPane} tabbedPane
833 * @param {string} id
834 * @param {string} title
835 * @param {boolean} closeable
836 * @param {!WebInspector.Widget} view
837 * @param {string=} tooltip
839 WebInspector.TabbedPaneTab = function(tabbedPane, id, title, closeable, view, tooltip)
841 this._closeable = closeable;
842 this._tabbedPane = tabbedPane;
843 this._id = id;
844 this._title = title;
845 this._tooltip = tooltip;
846 this._view = view;
847 this._shown = false;
848 /** @type {number} */ this._measuredWidth;
849 /** @type {!Element|undefined} */ this._tabElement;
852 WebInspector.TabbedPaneTab.prototype = {
854 * @return {string}
856 get id()
858 return this._id;
862 * @return {string}
864 get title()
866 return this._title;
869 set title(title)
871 if (title === this._title)
872 return;
873 this._title = title;
874 if (this._titleElement)
875 this._titleElement.textContent = title;
876 delete this._measuredWidth;
880 * @return {boolean}
882 isCloseable: function()
884 return this._closeable;
888 * @param {string} iconType
889 * @param {string=} iconTooltip
890 * @return {boolean}
892 _setIconType: function(iconType, iconTooltip)
894 if (iconType === this._iconType && iconTooltip === this._iconTooltip)
895 return false;
896 this._iconType = iconType;
897 this._iconTooltip = iconTooltip;
898 if (this._tabElement)
899 this._createIconElement(this._tabElement, this._titleElement);
900 delete this._measuredWidth;
901 return true;
905 * @param {string} className
906 * @param {boolean=} force
907 * @return {boolean}
909 _toggleClass: function(className, force)
911 var element = this.tabElement;
912 var hasClass = element.classList.contains(className);
913 if (hasClass === force)
914 return false;
915 element.classList.toggle(className, force);
916 delete this._measuredWidth;
917 return true;
921 * @return {!WebInspector.Widget}
923 get view()
925 return this._view;
928 set view(view)
930 this._view = view;
934 * @return {string|undefined}
936 get tooltip()
938 return this._tooltip;
941 set tooltip(tooltip)
943 this._tooltip = tooltip;
944 if (this._titleElement)
945 this._titleElement.title = tooltip || "";
949 * @return {!Element}
951 get tabElement()
953 if (!this._tabElement)
954 this._tabElement = this._createTabElement(false);
956 return this._tabElement;
960 * @return {number}
962 width: function()
964 return this._width;
968 * @param {number} width
970 setWidth: function(width)
972 this.tabElement.style.width = width === -1 ? "" : (width + "px");
973 this._width = width;
977 * @param {!WebInspector.TabbedPaneTabDelegate} delegate
979 setDelegate: function(delegate)
981 this._delegate = delegate;
985 * @param {!Element} tabElement
986 * @param {!Element} titleElement
988 _createIconElement: function(tabElement, titleElement)
990 if (tabElement.__iconElement)
991 tabElement.__iconElement.remove();
992 if (!this._iconType)
993 return;
995 var iconElement = createElementWithClass("label", "tabbed-pane-header-tab-icon", "dt-icon-label");
996 iconElement.type = this._iconType;
997 if (this._iconTooltip)
998 iconElement.title = this._iconTooltip;
999 tabElement.insertBefore(iconElement, titleElement);
1000 tabElement.__iconElement = iconElement;
1004 * @param {boolean} measuring
1005 * @return {!Element}
1007 _createTabElement: function(measuring)
1009 var tabElement = createElementWithClass("div", "tabbed-pane-header-tab");
1010 tabElement.id = "tab-" + this._id;
1011 tabElement.tabIndex = -1;
1012 tabElement.selectTabForTest = this._tabbedPane.selectTab.bind(this._tabbedPane, this.id, true);
1014 var titleElement = tabElement.createChild("span", "tabbed-pane-header-tab-title");
1015 titleElement.textContent = this.title;
1016 titleElement.title = this.tooltip || "";
1017 this._createIconElement(tabElement, titleElement);
1018 if (!measuring)
1019 this._titleElement = titleElement;
1021 if (this._closeable)
1022 tabElement.createChild("div", "tabbed-pane-close-button", "dt-close-button").gray = true;
1024 if (measuring) {
1025 tabElement.classList.add("measuring");
1026 } else {
1027 tabElement.addEventListener("click", this._tabClicked.bind(this), false);
1028 tabElement.addEventListener("mousedown", this._tabMouseDown.bind(this), false);
1029 tabElement.addEventListener("mouseup", this._tabMouseUp.bind(this), false);
1031 tabElement.addEventListener("contextmenu", this._tabContextMenu.bind(this), false);
1032 if (this._tabbedPane._allowTabReorder)
1033 WebInspector.installDragHandle(tabElement, this._startTabDragging.bind(this), this._tabDragging.bind(this), this._endTabDragging.bind(this), "-webkit-grabbing", "pointer");
1036 return tabElement;
1040 * @param {!Event} event
1042 _tabClicked: function(event)
1044 var middleButton = event.button === 1;
1045 var shouldClose = this._closeable && (middleButton || event.target.classList.contains("tabbed-pane-close-button"));
1046 if (!shouldClose) {
1047 this._tabbedPane.focus();
1048 return;
1050 this._closeTabs([this.id]);
1051 event.consume(true);
1055 * @param {!Event} event
1057 _tabMouseDown: function(event)
1059 if (event.target.classList.contains("tabbed-pane-close-button") || event.button === 1)
1060 return;
1061 this._tabbedPane.selectTab(this.id, true);
1065 * @param {!Event} event
1067 _tabMouseUp: function(event)
1069 // This is needed to prevent middle-click pasting on linux when tabs are clicked.
1070 if (event.button === 1)
1071 event.consume(true);
1075 * @param {!Array.<string>} ids
1077 _closeTabs: function(ids)
1079 if (this._delegate) {
1080 this._delegate.closeTabs(this._tabbedPane, ids);
1081 return;
1083 this._tabbedPane.closeTabs(ids, true);
1086 _tabContextMenu: function(event)
1089 * @this {WebInspector.TabbedPaneTab}
1091 function close()
1093 this._closeTabs([this.id]);
1097 * @this {WebInspector.TabbedPaneTab}
1099 function closeOthers()
1101 this._closeTabs(this._tabbedPane.otherTabs(this.id));
1105 * @this {WebInspector.TabbedPaneTab}
1107 function closeAll()
1109 this._closeTabs(this._tabbedPane.allTabs());
1113 * @this {WebInspector.TabbedPaneTab}
1115 function closeToTheRight()
1117 this._closeTabs(this._tabbedPane._tabsToTheRight(this.id));
1120 var contextMenu = new WebInspector.ContextMenu(event);
1121 if (this._closeable) {
1122 contextMenu.appendItem(WebInspector.UIString.capitalize("Close"), close.bind(this));
1123 contextMenu.appendItem(WebInspector.UIString.capitalize("Close ^others"), closeOthers.bind(this));
1124 contextMenu.appendItem(WebInspector.UIString.capitalize("Close ^tabs to the ^right"), closeToTheRight.bind(this));
1125 contextMenu.appendItem(WebInspector.UIString.capitalize("Close ^all"), closeAll.bind(this));
1127 if (this._delegate)
1128 this._delegate.onContextMenu(this.id, contextMenu);
1129 contextMenu.show();
1133 * @param {!Event} event
1134 * @return {boolean}
1136 _startTabDragging: function(event)
1138 if (event.target.classList.contains("tabbed-pane-close-button"))
1139 return false;
1140 this._dragStartX = event.pageX;
1141 return true;
1145 * @param {!Event} event
1147 _tabDragging: function(event)
1149 var tabElements = this._tabbedPane._tabsElement.childNodes;
1150 for (var i = 0; i < tabElements.length; ++i) {
1151 var tabElement = tabElements[i];
1152 if (tabElement === this._tabElement)
1153 continue;
1155 var intersects = tabElement.offsetLeft + tabElement.clientWidth > this._tabElement.offsetLeft &&
1156 this._tabElement.offsetLeft + this._tabElement.clientWidth > tabElement.offsetLeft;
1157 if (!intersects)
1158 continue;
1160 if (Math.abs(event.pageX - this._dragStartX) < tabElement.clientWidth / 2 + 5)
1161 break;
1163 if (event.pageX - this._dragStartX > 0) {
1164 tabElement = tabElement.nextSibling;
1165 ++i;
1168 var oldOffsetLeft = this._tabElement.offsetLeft;
1169 this._tabbedPane._insertBefore(this, i);
1170 this._dragStartX += this._tabElement.offsetLeft - oldOffsetLeft;
1171 break;
1174 if (!this._tabElement.previousSibling && event.pageX - this._dragStartX < 0) {
1175 this._tabElement.style.setProperty("left", "0px");
1176 return;
1178 if (!this._tabElement.nextSibling && event.pageX - this._dragStartX > 0) {
1179 this._tabElement.style.setProperty("left", "0px");
1180 return;
1183 this._tabElement.classList.add("dragging");
1184 this._tabElement.style.setProperty("left", (event.pageX - this._dragStartX) + "px");
1185 this._tabbedPane._tabSlider.remove();
1189 * @param {!Event} event
1191 _endTabDragging: function(event)
1193 this._tabElement.classList.remove("dragging");
1194 this._tabElement.style.removeProperty("left");
1195 delete this._dragStartX;
1196 this._tabbedPane._updateTabSlider();
1201 * @interface
1203 WebInspector.TabbedPaneTabDelegate = function()
1207 WebInspector.TabbedPaneTabDelegate.prototype = {
1209 * @param {!WebInspector.TabbedPane} tabbedPane
1210 * @param {!Array.<string>} ids
1212 closeTabs: function(tabbedPane, ids) { },
1215 * @param {string} tabId
1216 * @param {!WebInspector.ContextMenu} contextMenu
1218 onContextMenu: function(tabId, contextMenu) { }
1222 * @constructor
1223 * @param {!WebInspector.TabbedPane} tabbedPane
1224 * @param {string} extensionPoint
1225 * @param {function(string, !WebInspector.Widget)=} viewCallback
1227 WebInspector.ExtensibleTabbedPaneController = function(tabbedPane, extensionPoint, viewCallback)
1229 this._tabbedPane = tabbedPane;
1230 this._extensionPoint = extensionPoint;
1231 this._viewCallback = viewCallback;
1232 this._tabOrders = {};
1233 /** @type {!Object.<string, !Promise.<?WebInspector.Widget>>} */
1234 this._promiseForId = {};
1236 this._tabbedPane.addEventListener(WebInspector.TabbedPane.EventTypes.TabSelected, this._tabSelected, this);
1237 /** @type {!Map.<string, ?WebInspector.Widget>} */
1238 this._views = new Map();
1239 this._initialize();
1242 WebInspector.ExtensibleTabbedPaneController.prototype = {
1243 _initialize: function()
1245 /** @type {!Map.<string, !Runtime.Extension>} */
1246 this._extensions = new Map();
1247 var extensions = self.runtime.extensions(this._extensionPoint);
1249 for (var i = 0; i < extensions.length; ++i) {
1250 var descriptor = extensions[i].descriptor();
1251 var id = descriptor["name"];
1252 this._tabOrders[id] = i;
1253 var title = WebInspector.UIString(descriptor["title"]);
1255 this._extensions.set(id, extensions[i]);
1256 this._tabbedPane.appendTab(id, title, new WebInspector.Widget());
1261 * @param {!WebInspector.Event} event
1263 _tabSelected: function(event)
1265 var tabId = /** @type {string} */ (event.data.tabId);
1266 this.viewForId(tabId).then(viewLoaded.bind(this));
1269 * @this {WebInspector.ExtensibleTabbedPaneController}
1270 * @param {?WebInspector.Widget} view
1272 function viewLoaded(view)
1274 if (!view)
1275 return;
1276 this._tabbedPane.changeTabView(tabId, view);
1277 var shouldFocus = this._tabbedPane.visibleView.element.isSelfOrAncestor(WebInspector.currentFocusElement());
1278 if (shouldFocus)
1279 view.focus();
1284 * @return {!Array.<string>}
1286 viewIds: function()
1288 return this._extensions.keysArray();
1292 * @param {string} id
1293 * @return {!Promise.<?WebInspector.Widget>}
1295 viewForId: function(id)
1297 if (this._views.has(id))
1298 return Promise.resolve(/** @type {?WebInspector.Widget} */ (this._views.get(id)));
1299 if (!this._extensions.has(id))
1300 return Promise.resolve(/** @type {?WebInspector.Widget} */ (null));
1301 if (this._promiseForId[id])
1302 return this._promiseForId[id];
1304 var promise = this._extensions.get(id).instancePromise();
1305 this._promiseForId[id] = /** @type {!Promise.<?WebInspector.Widget>} */ (promise);
1306 return promise.then(cacheView.bind(this));
1309 * @param {!Object} object
1310 * @this {WebInspector.ExtensibleTabbedPaneController}
1312 function cacheView(object)
1314 var view = /** @type {!WebInspector.Widget} */ (object);
1315 delete this._promiseForId[id];
1316 this._views.set(id, view);
1317 if (this._viewCallback && view)
1318 this._viewCallback(id, view);
1319 return view;