1 /* 2 * Copyright (C) 2010 Google 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 are 6 * met: 7 * 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. 17 * 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. 29 */ 30 31 /** 32 * @extends {WebInspector.View} 33 * @constructor 34 */ 35 WebInspector.TabbedPane = function() 36 { 37 WebInspector.View.call(this); 38 this.element.classList.add("tabbed-pane", "vbox"); 39 this._headerElement = this.element.createChild("div", "tabbed-pane-header"); 40 this._headerContentsElement = this._headerElement.createChild("div", "tabbed-pane-header-contents"); 41 this._tabsElement = this._headerContentsElement.createChild("div", "tabbed-pane-header-tabs"); 42 this._contentElement = this.element.createChild("div", "tabbed-pane-content scroll-target"); 43 this._tabs = []; 44 this._tabsHistory = []; 45 this._tabsById = {}; 46 47 this._dropDownButton = this._createDropDownButton(); 48 } 49 50 WebInspector.TabbedPane.EventTypes = { 51 TabSelected: "TabSelected", 52 TabClosed: "TabClosed" 53 } 54 55 WebInspector.TabbedPane.prototype = { 56 /** 57 * @return {!WebInspector.View} 58 */ 59 get visibleView() 60 { 61 return this._currentTab ? this._currentTab.view : null; 62 }, 63 64 /** 65 * @return {string} 66 */ 67 get selectedTabId() 68 { 69 return this._currentTab ? this._currentTab.id : null; 70 }, 71 72 /** 73 * @type {boolean} shrinkableTabs 74 */ 75 set shrinkableTabs(shrinkableTabs) 76 { 77 this._shrinkableTabs = shrinkableTabs; 78 }, 79 80 /** 81 * @type {boolean} verticalTabLayout 82 */ 83 set verticalTabLayout(verticalTabLayout) 84 { 85 this._verticalTabLayout = verticalTabLayout; 86 }, 87 88 /** 89 * @type {boolean} closeableTabs 90 */ 91 set closeableTabs(closeableTabs) 92 { 93 this._closeableTabs = closeableTabs; 94 }, 95 96 /** 97 * @param {boolean} retainTabsOrder 98 */ 99 setRetainTabsOrder: function(retainTabsOrder) 100 { 101 this._retainTabsOrder = retainTabsOrder; 102 }, 103 104 /** 105 * @return {?Element} 106 */ 107 defaultFocusedElement: function() 108 { 109 return this.visibleView ? this.visibleView.defaultFocusedElement() : null; 110 }, 111 112 /** 113 * @return {!Element} 114 */ 115 headerElement: function() 116 { 117 return this._headerElement; 118 }, 119 120 /** 121 * @param {string} id 122 * @return {boolean} 123 */ 124 isTabCloseable: function(id) 125 { 126 var tab = this._tabsById[id]; 127 return tab ? tab.isCloseable() : false; 128 }, 129 130 /** 131 * @param {!WebInspector.TabbedPaneTabDelegate} delegate 132 */ 133 setTabDelegate: function(delegate) 134 { 135 var tabs = this._tabs.slice(); 136 for (var i = 0; i < tabs.length; ++i) 137 tabs[i].setDelegate(delegate); 138 this._delegate = delegate; 139 }, 140 141 /** 142 * @param {string} id 143 * @param {string} tabTitle 144 * @param {!WebInspector.View} view 145 * @param {string=} tabTooltip 146 * @param {boolean=} userGesture 147 * @param {boolean=} isCloseable 148 */ 149 appendTab: function(id, tabTitle, view, tabTooltip, userGesture, isCloseable) 150 { 151 isCloseable = typeof isCloseable === "boolean" ? isCloseable : this._closeableTabs; 152 var tab = new WebInspector.TabbedPaneTab(this, id, tabTitle, isCloseable, view, tabTooltip); 153 tab.setDelegate(this._delegate); 154 this._tabsById[id] = tab; 155 156 this._tabs.push(tab); 157 this._tabsHistory.push(tab); 158 159 if (this._tabsHistory[0] === tab) 160 this.selectTab(tab.id, userGesture); 161 162 this._updateTabElements(); 163 }, 164 165 /** 166 * @param {string} id 167 * @param {boolean=} userGesture 168 */ 169 closeTab: function(id, userGesture) 170 { 171 this.closeTabs([id], userGesture); 172 }, 173 174 /** 175 * @param {!Array.<string>} ids 176 * @param {boolean=} userGesture 177 */ 178 closeTabs: function(ids, userGesture) 179 { 180 for (var i = 0; i < ids.length; ++i) 181 this._innerCloseTab(ids[i], userGesture); 182 this._updateTabElements(); 183 if (this._tabsHistory.length) 184 this.selectTab(this._tabsHistory[0].id, false); 185 }, 186 187 /** 188 * @param {string} id 189 * @param {boolean=} userGesture 190 */ 191 _innerCloseTab: function(id, userGesture) 192 { 193 if (!this._tabsById[id]) 194 return; 195 if (userGesture && !this._tabsById[id]._closeable) 196 return; 197 if (this._currentTab && this._currentTab.id === id) 198 this._hideCurrentTab(); 199 200 var tab = this._tabsById[id]; 201 delete this._tabsById[id]; 202 203 this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1); 204 this._tabs.splice(this._tabs.indexOf(tab), 1); 205 if (tab._shown) 206 this._hideTabElement(tab); 207 208 var eventData = { tabId: id, view: tab.view, isUserGesture: userGesture }; 209 this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabClosed, eventData); 210 return true; 211 }, 212 213 /** 214 * @param {string} tabId 215 * @return {boolean} 216 */ 217 hasTab: function(tabId) 218 { 219 return !!this._tabsById[tabId]; 220 }, 221 222 /** 223 * @return {!Array.<string>} 224 */ 225 allTabs: function() 226 { 227 var result = []; 228 var tabs = this._tabs.slice(); 229 for (var i = 0; i < tabs.length; ++i) 230 result.push(tabs[i].id); 231 return result; 232 }, 233 234 /** 235 * @param {string} id 236 * @return {!Array.<string>} 237 */ 238 otherTabs: function(id) 239 { 240 var result = []; 241 var tabs = this._tabs.slice(); 242 for (var i = 0; i < tabs.length; ++i) { 243 if (tabs[i].id !== id) 244 result.push(tabs[i].id); 245 } 246 return result; 247 }, 248 249 /** 250 * @param {string} id 251 * @param {boolean=} userGesture 252 */ 253 selectTab: function(id, userGesture) 254 { 255 var tab = this._tabsById[id]; 256 if (!tab) 257 return; 258 if (this._currentTab && this._currentTab.id === id) 259 return; 260 261 this._hideCurrentTab(); 262 this._showTab(tab); 263 this._currentTab = tab; 264 265 this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1); 266 this._tabsHistory.splice(0, 0, tab); 267 268 this._updateTabElements(); 269 270 var eventData = { tabId: id, view: tab.view, isUserGesture: userGesture }; 271 this.dispatchEventToListeners(WebInspector.TabbedPane.EventTypes.TabSelected, eventData); 272 return true; 273 }, 274 275 /** 276 * @param {number} tabsCount 277 * @return {!Array.<string>} 278 */ 279 lastOpenedTabIds: function(tabsCount) 280 { 281 function tabToTabId(tab) { 282 return tab.id; 283 } 284 285 return this._tabsHistory.slice(0, tabsCount).map(tabToTabId); 286 }, 287 288 /** 289 * @param {string} id 290 * @param {string} iconClass 291 * @param {string=} iconTooltip 292 */ 293 setTabIcon: function(id, iconClass, iconTooltip) 294 { 295 var tab = this._tabsById[id]; 296 if (tab._setIconClass(iconClass, iconTooltip)) 297 this._updateTabElements(); 298 }, 299 300 /** 301 * @param {string} id 302 * @param {string} tabTitle 303 */ 304 changeTabTitle: function(id, tabTitle) 305 { 306 var tab = this._tabsById[id]; 307 if (tab.title === tabTitle) 308 return; 309 tab.title = tabTitle; 310 this._updateTabElements(); 311 }, 312 313 /** 314 * @param {string} id 315 * @param {!WebInspector.View} view 316 */ 317 changeTabView: function(id, view) 318 { 319 var tab = this._tabsById[id]; 320 if (this._currentTab && this._currentTab.id === tab.id) { 321 if (tab.view !== view) 322 this._hideTab(tab); 323 tab.view = view; 324 this._showTab(tab); 325 } else 326 tab.view = view; 327 }, 328 329 /** 330 * @param {string} id 331 * @param {string=} tabTooltip 332 */ 333 changeTabTooltip: function(id, tabTooltip) 334 { 335 var tab = this._tabsById[id]; 336 tab.tooltip = tabTooltip; 337 }, 338 339 onResize: function() 340 { 341 this._updateTabElements(); 342 }, 343 344 headerResized: function() 345 { 346 this._updateTabElements(); 347 }, 348 349 _updateTabElements: function() 350 { 351 WebInspector.invokeOnceAfterBatchUpdate(this, this._innerUpdateTabElements); 352 }, 353 354 /** 355 * @param {string} text 356 */ 357 setPlaceholderText: function(text) 358 { 359 this._noTabsMessage = text; 360 }, 361 362 _innerUpdateTabElements: function() 363 { 364 if (!this.isShowing()) 365 return; 366 367 if (!this._tabs.length) { 368 this._contentElement.classList.add("has-no-tabs"); 369 if (this._noTabsMessage && !this._noTabsMessageElement) { 370 this._noTabsMessageElement = this._contentElement.createChild("div", "tabbed-pane-placeholder fill"); 371 this._noTabsMessageElement.textContent = this._noTabsMessage; 372 } 373 } else { 374 this._contentElement.classList.remove("has-no-tabs"); 375 if (this._noTabsMessageElement) { 376 this._noTabsMessageElement.remove(); 377 delete this._noTabsMessageElement; 378 } 379 } 380 381 if (!this._measuredDropDownButtonWidth) 382 this._measureDropDownButton(); 383 384 this._updateWidths(); 385 this._updateTabsDropDown(); 386 }, 387 388 /** 389 * @param {number} index 390 * @param {!WebInspector.TabbedPaneTab} tab 391 */ 392 _showTabElement: function(index, tab) 393 { 394 if (index >= this._tabsElement.children.length) 395 this._tabsElement.appendChild(tab.tabElement); 396 else 397 this._tabsElement.insertBefore(tab.tabElement, this._tabsElement.children[index]); 398 tab._shown = true; 399 }, 400 401 /** 402 * @param {!WebInspector.TabbedPaneTab} tab 403 */ 404 _hideTabElement: function(tab) 405 { 406 this._tabsElement.removeChild(tab.tabElement); 407 tab._shown = false; 408 }, 409 410 _createDropDownButton: function() 411 { 412 var dropDownContainer = document.createElement("div"); 413 dropDownContainer.classList.add("tabbed-pane-header-tabs-drop-down-container"); 414 var dropDownButton = dropDownContainer.createChild("div", "tabbed-pane-header-tabs-drop-down"); 415 dropDownButton.appendChild(document.createTextNode("\u00bb")); 416 this._tabsSelect = dropDownButton.createChild("select", "tabbed-pane-header-tabs-drop-down-select"); 417 this._tabsSelect.addEventListener("change", this._tabsSelectChanged.bind(this), false); 418 return dropDownContainer; 419 }, 420 421 _totalWidth: function() 422 { 423 return this._headerContentsElement.getBoundingClientRect().width; 424 }, 425 426 _updateTabsDropDown: function() 427 { 428 var tabsToShowIndexes = this._tabsToShowIndexes(this._tabs, this._tabsHistory, this._totalWidth(), this._measuredDropDownButtonWidth); 429 430 for (var i = 0; i < this._tabs.length; ++i) { 431 if (this._tabs[i]._shown && tabsToShowIndexes.indexOf(i) === -1) 432 this._hideTabElement(this._tabs[i]); 433 } 434 for (var i = 0; i < tabsToShowIndexes.length; ++i) { 435 var tab = this._tabs[tabsToShowIndexes[i]]; 436 if (!tab._shown) 437 this._showTabElement(i, tab); 438 } 439 440 this._populateDropDownFromIndex(); 441 }, 442 443 _populateDropDownFromIndex: function() 444 { 445 if (this._dropDownButton.parentElement) 446 this._headerContentsElement.removeChild(this._dropDownButton); 447 448 this._tabsSelect.removeChildren(); 449 var tabsToShow = []; 450 for (var i = 0; i < this._tabs.length; ++i) { 451 if (!this._tabs[i]._shown) 452 tabsToShow.push(this._tabs[i]); 453 continue; 454 } 455 456 function compareFunction(tab1, tab2) 457 { 458 return tab1.title.localeCompare(tab2.title); 459 } 460 if (!this._retainTabsOrder) 461 tabsToShow.sort(compareFunction); 462 463 var selectedIndex = -1; 464 for (var i = 0; i < tabsToShow.length; ++i) { 465 var option = new Option(tabsToShow[i].title); 466 option.tab = tabsToShow[i]; 467 this._tabsSelect.appendChild(option); 468 if (this._tabsHistory[0] === tabsToShow[i]) 469 selectedIndex = i; 470 } 471 if (this._tabsSelect.options.length) { 472 this._headerContentsElement.appendChild(this._dropDownButton); 473 this._tabsSelect.selectedIndex = selectedIndex; 474 } 475 }, 476 477 _tabsSelectChanged: function() 478 { 479 var options = this._tabsSelect.options; 480 var selectedOption = options[this._tabsSelect.selectedIndex]; 481 this.selectTab(selectedOption.tab.id, true); 482 }, 483 484 _measureDropDownButton: function() 485 { 486 this._dropDownButton.classList.add("measuring"); 487 this._headerContentsElement.appendChild(this._dropDownButton); 488 this._measuredDropDownButtonWidth = this._dropDownButton.getBoundingClientRect().width; 489 this._headerContentsElement.removeChild(this._dropDownButton); 490 this._dropDownButton.classList.remove("measuring"); 491 }, 492 493 _updateWidths: function() 494 { 495 var measuredWidths = this._measureWidths(); 496 var maxWidth = this._shrinkableTabs ? this._calculateMaxWidth(measuredWidths.slice(), this._totalWidth()) : Number.MAX_VALUE; 497 498 var i = 0; 499 for (var tabId in this._tabs) { 500 var tab = this._tabs[tabId]; 501 tab.setWidth(this._verticalTabLayout ? -1 : Math.min(maxWidth, measuredWidths[i++])); 502 } 503 }, 504 505 _measureWidths: function() 506 { 507 // Add all elements to measure into this._tabsElement 508 this._tabsElement.style.setProperty("width", "2000px"); 509 var measuringTabElements = []; 510 for (var tabId in this._tabs) { 511 var tab = this._tabs[tabId]; 512 if (typeof tab._measuredWidth === "number") 513 continue; 514 var measuringTabElement = tab._createTabElement(true); 515 measuringTabElement.__tab = tab; 516 measuringTabElements.push(measuringTabElement); 517 this._tabsElement.appendChild(measuringTabElement); 518 } 519 520 // Perform measurement 521 for (var i = 0; i < measuringTabElements.length; ++i) 522 measuringTabElements[i].__tab._measuredWidth = measuringTabElements[i].getBoundingClientRect().width; 523 524 // Nuke elements from the UI 525 for (var i = 0; i < measuringTabElements.length; ++i) 526 measuringTabElements[i].remove(); 527 528 // Combine the results. 529 var measuredWidths = []; 530 for (var tabId in this._tabs) 531 measuredWidths.push(this._tabs[tabId]._measuredWidth); 532 this._tabsElement.style.removeProperty("width"); 533 534 return measuredWidths; 535 }, 536 537 /** 538 * @param {!Array.<number>} measuredWidths 539 * @param {number} totalWidth 540 */ 541 _calculateMaxWidth: function(measuredWidths, totalWidth) 542 { 543 if (!measuredWidths.length) 544 return 0; 545 546 measuredWidths.sort(function(x, y) { return x - y }); 547 548 var totalMeasuredWidth = 0; 549 for (var i = 0; i < measuredWidths.length; ++i) 550 totalMeasuredWidth += measuredWidths[i]; 551 552 if (totalWidth >= totalMeasuredWidth) 553 return measuredWidths[measuredWidths.length - 1]; 554 555 var totalExtraWidth = 0; 556 for (var i = measuredWidths.length - 1; i > 0; --i) { 557 var extraWidth = measuredWidths[i] - measuredWidths[i - 1]; 558 totalExtraWidth += (measuredWidths.length - i) * extraWidth; 559 560 if (totalWidth + totalExtraWidth >= totalMeasuredWidth) 561 return measuredWidths[i - 1] + (totalWidth + totalExtraWidth - totalMeasuredWidth) / (measuredWidths.length - i); 562 } 563 564 return totalWidth / measuredWidths.length; 565 }, 566 567 /** 568 * @param {!Array.<!WebInspector.TabbedPaneTab>} tabsOrdered 569 * @param {!Array.<!WebInspector.TabbedPaneTab>} tabsHistory 570 * @param {number} totalWidth 571 * @param {number} measuredDropDownButtonWidth 572 * @return {!Array.<number>} 573 */ 574 _tabsToShowIndexes: function(tabsOrdered, tabsHistory, totalWidth, measuredDropDownButtonWidth) 575 { 576 var tabsToShowIndexes = []; 577 578 var totalTabsWidth = 0; 579 var tabCount = tabsOrdered.length; 580 for (var i = 0; i < tabCount; ++i) { 581 var tab = this._retainTabsOrder ? tabsOrdered[i] : tabsHistory[i]; 582 totalTabsWidth += tab.width(); 583 var minimalRequiredWidth = totalTabsWidth; 584 if (i !== tabCount - 1) 585 minimalRequiredWidth += measuredDropDownButtonWidth; 586 if (!this._verticalTabLayout && minimalRequiredWidth > totalWidth) 587 break; 588 tabsToShowIndexes.push(tabsOrdered.indexOf(tab)); 589 } 590 591 tabsToShowIndexes.sort(function(x, y) { return x - y }); 592 593 return tabsToShowIndexes; 594 }, 595 596 _hideCurrentTab: function() 597 { 598 if (!this._currentTab) 599 return; 600 601 this._hideTab(this._currentTab); 602 delete this._currentTab; 603 }, 604 605 /** 606 * @param {!WebInspector.TabbedPaneTab} tab 607 */ 608 _showTab: function(tab) 609 { 610 tab.tabElement.classList.add("selected"); 611 tab.view.show(this._contentElement); 612 }, 613 614 /** 615 * @param {!WebInspector.TabbedPaneTab} tab 616 */ 617 _hideTab: function(tab) 618 { 619 tab.tabElement.classList.remove("selected"); 620 tab.view.detach(); 621 }, 622 623 /** 624 * @override 625 */ 626 canHighlightPosition: function() 627 { 628 return this._currentTab && this._currentTab.view && this._currentTab.view.canHighlightPosition(); 629 }, 630 631 /** 632 * @override 633 */ 634 highlightPosition: function(line, column) 635 { 636 if (this.canHighlightPosition()) 637 this._currentTab.view.highlightPosition(line, column); 638 }, 639 640 /** 641 * @return {!Array.<!Element>} 642 */ 643 elementsToRestoreScrollPositionsFor: function() 644 { 645 return [ this._contentElement ]; 646 }, 647 648 /** 649 * @param {!WebInspector.TabbedPaneTab} tab 650 * @param {number} index 651 */ 652 _insertBefore: function(tab, index) 653 { 654 this._tabsElement.insertBefore(tab._tabElement, this._tabsElement.childNodes[index]); 655 var oldIndex = this._tabs.indexOf(tab); 656 this._tabs.splice(oldIndex, 1); 657 if (oldIndex < index) 658 --index; 659 this._tabs.splice(index, 0, tab); 660 }, 661 662 __proto__: WebInspector.View.prototype 663 } 664 665 666 /** 667 * @constructor 668 * @param {!WebInspector.TabbedPane} tabbedPane 669 * @param {string} id 670 * @param {string} title 671 * @param {boolean} closeable 672 * @param {!WebInspector.View} view 673 * @param {string=} tooltip 674 */ 675 WebInspector.TabbedPaneTab = function(tabbedPane, id, title, closeable, view, tooltip) 676 { 677 this._closeable = closeable; 678 this._tabbedPane = tabbedPane; 679 this._id = id; 680 this._title = title; 681 this._tooltip = tooltip; 682 this._view = view; 683 this._shown = false; 684 /** @type {number} */ this._measuredWidth; 685 /** @type {!Element|undefined} */ this._tabElement; 686 } 687 688 WebInspector.TabbedPaneTab.prototype = { 689 /** 690 * @return {string} 691 */ 692 get id() 693 { 694 return this._id; 695 }, 696 697 /** 698 * @return {string} 699 */ 700 get title() 701 { 702 return this._title; 703 }, 704 705 set title(title) 706 { 707 if (title === this._title) 708 return; 709 this._title = title; 710 if (this._titleElement) 711 this._titleElement.textContent = title; 712 delete this._measuredWidth; 713 }, 714 715 /** 716 * @return {string} 717 */ 718 iconClass: function() 719 { 720 return this._iconClass; 721 }, 722 723 /** 724 * @return {boolean} 725 */ 726 isCloseable: function() 727 { 728 return this._closeable; 729 }, 730 731 /** 732 * @param {string} iconClass 733 * @param {string} iconTooltip 734 * @return {boolean} 735 */ 736 _setIconClass: function(iconClass, iconTooltip) 737 { 738 if (iconClass === this._iconClass && iconTooltip === this._iconTooltip) 739 return false; 740 this._iconClass = iconClass; 741 this._iconTooltip = iconTooltip; 742 if (this._iconElement) 743 this._iconElement.remove(); 744 if (this._iconClass && this._tabElement) 745 this._iconElement = this._createIconElement(this._tabElement, this._titleElement); 746 delete this._measuredWidth; 747 return true; 748 }, 749 750 /** 751 * @return {!WebInspector.View} 752 */ 753 get view() 754 { 755 return this._view; 756 }, 757 758 set view(view) 759 { 760 this._view = view; 761 }, 762 763 /** 764 * @return {string|undefined} 765 */ 766 get tooltip() 767 { 768 return this._tooltip; 769 }, 770 771 set tooltip(tooltip) 772 { 773 this._tooltip = tooltip; 774 if (this._titleElement) 775 this._titleElement.title = tooltip || ""; 776 }, 777 778 /** 779 * @return {!Element} 780 */ 781 get tabElement() 782 { 783 if (!this._tabElement) 784 this._tabElement = this._createTabElement(false); 785 786 return this._tabElement; 787 }, 788 789 /** 790 * @return {number} 791 */ 792 width: function() 793 { 794 return this._width; 795 }, 796 797 /** 798 * @param {number} width 799 */ 800 setWidth: function(width) 801 { 802 this.tabElement.style.width = width === -1 ? "" : (width + "px"); 803 this._width = width; 804 }, 805 806 /** 807 * @param {!WebInspector.TabbedPaneTabDelegate} delegate 808 */ 809 setDelegate: function(delegate) 810 { 811 this._delegate = delegate; 812 }, 813 814 _createIconElement: function(tabElement, titleElement) 815 { 816 var iconElement = document.createElement("span"); 817 iconElement.className = "tabbed-pane-header-tab-icon " + this._iconClass; 818 if (this._iconTooltip) 819 iconElement.title = this._iconTooltip; 820 tabElement.insertBefore(iconElement, titleElement); 821 return iconElement; 822 }, 823 824 /** 825 * @param {boolean} measuring 826 * @return {!Element} 827 */ 828 _createTabElement: function(measuring) 829 { 830 var tabElement = document.createElement("div"); 831 tabElement.classList.add("tabbed-pane-header-tab"); 832 tabElement.id = "tab-" + this._id; 833 tabElement.tabIndex = -1; 834 tabElement.selectTabForTest = this._tabbedPane.selectTab.bind(this._tabbedPane, this.id, true); 835 836 var titleElement = tabElement.createChild("span", "tabbed-pane-header-tab-title"); 837 titleElement.textContent = this.title; 838 titleElement.title = this.tooltip || ""; 839 if (this._iconClass) 840 this._createIconElement(tabElement, titleElement); 841 if (!measuring) 842 this._titleElement = titleElement; 843 844 if (this._closeable) 845 tabElement.createChild("div", "close-button-gray"); 846 847 if (measuring) { 848 tabElement.classList.add("measuring"); 849 } else { 850 tabElement.addEventListener("click", this._tabClicked.bind(this), false); 851 tabElement.addEventListener("mousedown", this._tabMouseDown.bind(this), false); 852 tabElement.addEventListener("mouseup", this._tabMouseUp.bind(this), false); 853 854 if (this._closeable) { 855 tabElement.addEventListener("contextmenu", this._tabContextMenu.bind(this), false); 856 WebInspector.installDragHandle(tabElement, this._startTabDragging.bind(this), this._tabDragging.bind(this), this._endTabDragging.bind(this), "pointer"); 857 } 858 } 859 860 return tabElement; 861 }, 862 863 /** 864 * @param {?Event} event 865 */ 866 _tabClicked: function(event) 867 { 868 var middleButton = event.button === 1; 869 var shouldClose = this._closeable && (middleButton || event.target.classList.contains("close-button-gray")); 870 if (!shouldClose) { 871 this._tabbedPane.focus(); 872 return; 873 } 874 this._closeTabs([this.id]); 875 event.consume(true); 876 }, 877 878 /** 879 * @param {?Event} event 880 */ 881 _tabMouseDown: function(event) 882 { 883 if (event.target.classList.contains("close-button-gray") || event.button === 1) 884 return; 885 this._tabbedPane.selectTab(this.id, true); 886 }, 887 888 /** 889 * @param {?Event} event 890 */ 891 _tabMouseUp: function(event) 892 { 893 // This is needed to prevent middle-click pasting on linux when tabs are clicked. 894 if (event.button === 1) 895 event.consume(true); 896 }, 897 898 /** 899 * @param {!Array.<string>} ids 900 */ 901 _closeTabs: function(ids) 902 { 903 if (this._delegate) { 904 this._delegate.closeTabs(this._tabbedPane, ids); 905 return; 906 } 907 this._tabbedPane.closeTabs(ids, true); 908 }, 909 910 _tabContextMenu: function(event) 911 { 912 /** 913 * @this {WebInspector.TabbedPaneTab} 914 */ 915 function close() 916 { 917 this._closeTabs([this.id]); 918 } 919 920 /** 921 * @this {WebInspector.TabbedPaneTab} 922 */ 923 function closeOthers() 924 { 925 this._closeTabs(this._tabbedPane.otherTabs(this.id)); 926 } 927 928 /** 929 * @this {WebInspector.TabbedPaneTab} 930 */ 931 function closeAll() 932 { 933 this._closeTabs(this._tabbedPane.allTabs()); 934 } 935 936 var contextMenu = new WebInspector.ContextMenu(event); 937 contextMenu.appendItem(WebInspector.UIString("Close"), close.bind(this)); 938 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Close others" : "Close Others"), closeOthers.bind(this)); 939 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Close all" : "Close All"), closeAll.bind(this)); 940 contextMenu.show(); 941 }, 942 943 /** 944 * @param {!Event} event 945 * @return {boolean} 946 */ 947 _startTabDragging: function(event) 948 { 949 if (event.target.classList.contains("close-button-gray")) 950 return false; 951 this._dragStartX = event.pageX; 952 return true; 953 }, 954 955 /** 956 * @param {!Event} event 957 */ 958 _tabDragging: function(event) 959 { 960 var tabElements = this._tabbedPane._tabsElement.childNodes; 961 for (var i = 0; i < tabElements.length; ++i) { 962 var tabElement = tabElements[i]; 963 if (tabElement === this._tabElement) 964 continue; 965 966 var intersects = tabElement.offsetLeft + tabElement.clientWidth > this._tabElement.offsetLeft && 967 this._tabElement.offsetLeft + this._tabElement.clientWidth > tabElement.offsetLeft; 968 if (!intersects) 969 continue; 970 971 if (Math.abs(event.pageX - this._dragStartX) < tabElement.clientWidth / 2 + 5) 972 break; 973 974 if (event.pageX - this._dragStartX > 0) { 975 tabElement = tabElement.nextSibling; 976 ++i; 977 } 978 979 var oldOffsetLeft = this._tabElement.offsetLeft; 980 this._tabbedPane._insertBefore(this, i); 981 this._dragStartX += this._tabElement.offsetLeft - oldOffsetLeft; 982 break; 983 } 984 985 if (!this._tabElement.previousSibling && event.pageX - this._dragStartX < 0) { 986 this._tabElement.style.setProperty("left", "0px"); 987 return; 988 } 989 if (!this._tabElement.nextSibling && event.pageX - this._dragStartX > 0) { 990 this._tabElement.style.setProperty("left", "0px"); 991 return; 992 } 993 994 this._tabElement.style.setProperty("position", "relative"); 995 this._tabElement.style.setProperty("left", (event.pageX - this._dragStartX) + "px"); 996 }, 997 998 /** 999 * @param {!Event} event 1000 */ 1001 _endTabDragging: function(event) 1002 { 1003 this._tabElement.style.removeProperty("position"); 1004 this._tabElement.style.removeProperty("left"); 1005 delete this._dragStartX; 1006 } 1007 } 1008 1009 /** 1010 * @interface 1011 */ 1012 WebInspector.TabbedPaneTabDelegate = function() 1013 { 1014 } 1015 1016 WebInspector.TabbedPaneTabDelegate.prototype = { 1017 /** 1018 * @param {!WebInspector.TabbedPane} tabbedPane 1019 * @param {!Array.<string>} ids 1020 */ 1021 closeTabs: function(tabbedPane, ids) { } 1022 } 1023