UNPKG

@kpi4me/golden-layout

Version:

A multi-screen javascript Layout manager https://golden-layout.com

424 lines (370 loc) 12.7 kB
/** * This class represents a header above a Stack ContentItem. * * @param {lm.LayoutManager} layoutManager * @param {lm.item.AbstractContentItem} parent */ lm.controls.Header = function( layoutManager, parent ) { lm.utils.EventEmitter.call( this ); this.layoutManager = layoutManager; this.element = $( lm.controls.Header._template ); if( this.layoutManager.config.settings.selectionEnabled === true ) { this.element.addClass( 'lm_selectable' ); this.element.on( 'click touchstart', lm.utils.fnBind( this._onHeaderClick, this ) ); } this.tabsContainer = this.element.find( '.lm_tabs' ); this.tabDropdownContainer = this.element.find( '.lm_tabdropdown_list' ); this.tabDropdownContainer.hide(); this.controlsContainer = this.element.find( '.lm_controls' ); this.parent = parent; this.parent.on( 'resize', this._updateTabSizes, this ); this.tabs = []; this.activeContentItem = null; this.closeButton = null; this.tabDropdownButton = null; this.hideAdditionalTabsDropdown = lm.utils.fnBind(this._hideAdditionalTabsDropdown, this); $( document ).mouseup(this.hideAdditionalTabsDropdown); this._lastVisibleTabIndex = -1; this._tabControlOffset = this.layoutManager.config.settings.tabControlOffset; this._createControls(); }; lm.controls.Header._template = [ '<div class="lm_header">', '<ul class="lm_tabs"></ul>', '<ul class="lm_controls"></ul>', '<ul class="lm_tabdropdown_list"></ul>', '</div>' ].join( '' ); lm.utils.copy( lm.controls.Header.prototype, { /** * Creates a new tab and associates it with a contentItem * * @param {lm.item.AbstractContentItem} contentItem * @param {Integer} index The position of the tab * * @returns {void} */ createTab: function( contentItem, index ) { var tab, i; //If there's already a tab relating to the //content item, don't do anything for( i = 0; i < this.tabs.length; i++ ) { if( this.tabs[ i ].contentItem === contentItem ) { return; } } tab = new lm.controls.Tab( this, contentItem ); if( this.tabs.length === 0 ) { this.tabs.push( tab ); this.tabsContainer.append( tab.element ); return; } if( index === undefined ) { index = this.tabs.length; } if( index > 0 ) { this.tabs[ index - 1 ].element.after( tab.element ); } else { this.tabs[ 0 ].element.before( tab.element ); } this.tabs.splice( index, 0, tab ); this._updateTabSizes(); }, /** * Finds a tab based on the contentItem its associated with and removes it. * * @param {lm.item.AbstractContentItem} contentItem * * @returns {void} */ removeTab: function( contentItem ) { for( var i = 0; i < this.tabs.length; i++ ) { if( this.tabs[ i ].contentItem === contentItem ) { this.tabs[ i ]._$destroy(); this.tabs.splice( i, 1 ); return; } } throw new Error( 'contentItem is not controlled by this header' ); }, /** * The programmatical equivalent of clicking a Tab. * * @param {lm.item.AbstractContentItem} contentItem */ setActiveContentItem: function( contentItem ) { var i, j, isActive, activeTab; for( i = 0; i < this.tabs.length; i++ ) { isActive = this.tabs[ i ].contentItem === contentItem; this.tabs[ i ].setActive( isActive ); if( isActive === true ) { this.activeContentItem = contentItem; this.parent.config.activeItemIndex = i; } } if (this.layoutManager.config.settings.reorderOnTabMenuClick) { /** * If the tab selected was in the dropdown, move everything down one to make way for this one to be the first. * This will make sure the most used tabs stay visible. */ if (this._lastVisibleTabIndex !== -1 && this.parent.config.activeItemIndex > this._lastVisibleTabIndex) { const activeCI = this.parent.contentItems[this.parent.config.activeItemIndex]; activeTab = this.tabs[this.parent.config.activeItemIndex]; for (j = this.parent.config.activeItemIndex; j > 0; j--) { this.tabs[j] = this.tabs[j - 1]; this.parent.contentItems[j] = this.parent.contentItems[j - 1]; } this.tabs[0] = activeTab; this.parent.contentItems[0] = activeCI; this.parent.config.activeItemIndex = 0; } } this._updateTabSizes(); this.parent.emitBubblingEvent( 'stateChanged' ); }, /** * Programmatically operate with header position. * * @param {string} position one of ('top','left','right','bottom') to set or empty to get it. * * @returns {string} previous header position */ position: function( position ) { var previous = this.parent._header.show; if( previous && !this.parent._side ) previous = 'top'; if( position !== undefined && this.parent._header.show != position ) { this.parent._header.show = position; this.parent._setupHeaderPosition(); } return previous; }, /** * Programmatically set closability. * * @package private * @param {Boolean} isClosable Whether to enable/disable closability. * * @returns {Boolean} Whether the action was successful */ _$setClosable: function( isClosable ) { if( this.closeButton && this._isClosable() ) { this.closeButton.element[ isClosable ? "show" : "hide" ](); return true; } return false; }, /** * Destroys the entire header * * @package private * * @returns {void} */ _$destroy: function() { this.emit( 'destroy', this ); for( var i = 0; i < this.tabs.length; i++ ) { this.tabs[ i ]._$destroy(); } $( document ).off('mouseup', this.hideAdditionalTabsDropdown); this.element.remove(); }, /** * get settings from header * * @returns {string} when exists */ _getHeaderSetting: function( name ) { if( name in this.parent._header ) return this.parent._header[ name ]; }, /** * Creates the popout, maximise and close buttons in the header's top right corner * * @returns {void} */ _createControls: function() { var closeStack, popout, label, maximiseLabel, minimiseLabel, maximise, maximiseButton, tabDropdownLabel, showTabDropdown; /** * Dropdown to show additional tabs. */ showTabDropdown = lm.utils.fnBind( this._showAdditionalTabsDropdown, this ); tabDropdownLabel = this.layoutManager.config.labels.tabDropdown; this.tabDropdownButton = new lm.controls.HeaderButton( this, tabDropdownLabel, 'lm_tabdropdown', showTabDropdown ); this.tabDropdownButton.element.hide(); /** * Popout control to launch component in new window. */ if( this._getHeaderSetting( 'popout' ) ) { popout = lm.utils.fnBind( this._onPopoutClick, this ); label = this._getHeaderSetting( 'popout' ); new lm.controls.HeaderButton( this, label, 'lm_popout', popout ); } /** * Maximise control - set the component to the full size of the layout */ if( this._getHeaderSetting( 'maximise' ) ) { maximise = lm.utils.fnBind( this.parent.toggleMaximise, this.parent ); maximiseLabel = this._getHeaderSetting( 'maximise' ); minimiseLabel = this._getHeaderSetting( 'minimise' ); maximiseButton = new lm.controls.HeaderButton( this, maximiseLabel, 'lm_maximise', maximise ); this.parent.on( 'maximised', function() { maximiseButton.element.attr( 'title', minimiseLabel ); } ); this.parent.on( 'minimised', function() { maximiseButton.element.attr( 'title', maximiseLabel ); } ); } /** * Close button */ if( this._isClosable() ) { closeStack = lm.utils.fnBind( this.parent.remove, this.parent ); label = this._getHeaderSetting( 'close' ); this.closeButton = new lm.controls.HeaderButton( this, label, 'lm_close', closeStack ); } }, /** * Shows drop down for additional tabs when there are too many to display. * * @returns {void} */ _showAdditionalTabsDropdown: function() { this.tabDropdownContainer.show(); }, /** * Hides drop down for additional tabs when there are too many to display. * * @returns {void} */ _hideAdditionalTabsDropdown: function( e ) { this.tabDropdownContainer.hide(); }, /** * Checks whether the header is closable based on the parent config and * the global config. * * @returns {Boolean} Whether the header is closable. */ _isClosable: function() { return this.parent.config.isClosable && this.layoutManager.config.settings.showCloseIcon; }, _onPopoutClick: function() { if( this.layoutManager.config.settings.popoutWholeStack === true ) { this.parent.popout(); } else { this.activeContentItem.popout(); } }, /** * Invoked when the header's background is clicked (not it's tabs or controls) * * @param {jQuery DOM event} event * * @returns {void} */ _onHeaderClick: function( event ) { if( event.target === this.element[ 0 ] ) { this.parent.select(); } }, /** * Pushes the tabs to the tab dropdown if the available space is not sufficient * * @returns {void} */ _updateTabSizes: function(showTabMenu) { if( this.tabs.length === 0 ) { return; } //Show the menu based on function argument this.tabDropdownButton.element.toggle(showTabMenu === true); var size = function( val ) { return val ? 'width' : 'height'; }; this.element.css( size( !this.parent._sided ), '' ); this.element[ size( this.parent._sided ) ]( this.layoutManager.config.dimensions.headerHeight ); var availableWidth = this.element.outerWidth() - this.controlsContainer.outerWidth() - this._tabControlOffset, cumulativeTabWidth = 0, visibleTabWidth = 0, tabElement, i, j, marginLeft, overlap = 0, tabWidth, tabOverlapAllowance = this.layoutManager.config.settings.tabOverlapAllowance, tabOverlapAllowanceExceeded = false, activeIndex = (this.activeContentItem ? this.tabs.indexOf(this.activeContentItem.tab) : 0), activeTab = this.tabs[activeIndex]; if( this.parent._sided ) availableWidth = this.element.outerHeight() - this.controlsContainer.outerHeight() - this._tabControlOffset; this._lastVisibleTabIndex = -1; for( i = 0; i < this.tabs.length; i++ ) { tabElement = this.tabs[ i ].element; //Put the tab in the tabContainer so its true width can be checked this.tabsContainer.append( tabElement ); tabWidth = tabElement.outerWidth() + parseInt( tabElement.css( 'margin-right' ), 10 ); cumulativeTabWidth += tabWidth; //Include the active tab's width if it isn't already //This is to ensure there is room to show the active tab if (activeIndex <= i) { visibleTabWidth = cumulativeTabWidth; } else { visibleTabWidth = cumulativeTabWidth + activeTab.element.outerWidth() + parseInt(activeTab.element.css('margin-right'), 10); } // If the tabs won't fit, check the overlap allowance. if( visibleTabWidth > availableWidth ) { //Once allowance is exceeded, all remaining tabs go to menu. if (!tabOverlapAllowanceExceeded) { //No overlap for first tab or active tab //Overlap spreads among non-active, non-first tabs if (activeIndex > 0 && activeIndex <= i) { overlap = ( visibleTabWidth - availableWidth ) / (i - 1); } else { overlap = ( visibleTabWidth - availableWidth ) / i; } //Check overlap against allowance. if (overlap < tabOverlapAllowance) { for ( j = 0; j <= i; j++ ) { marginLeft = (j !== activeIndex && j !== 0) ? '-' + overlap + 'px' : ''; this.tabs[j].element.css({'z-index': i - j, 'margin-left': marginLeft}); } this._lastVisibleTabIndex = i; this.tabsContainer.append(tabElement); } else { tabOverlapAllowanceExceeded = true; } } else if (i === activeIndex) { //Active tab should show even if allowance exceeded. (We left room.) tabElement.css({'z-index': 'auto', 'margin-left': ''}); this.tabsContainer.append(tabElement); } if (tabOverlapAllowanceExceeded && i !== activeIndex) { if (showTabMenu) { //Tab menu already shown, so we just add to it. tabElement.css({'z-index': 'auto', 'margin-left': ''}); this.tabDropdownContainer.append(tabElement); } else { //We now know the tab menu must be shown, so we have to recalculate everything. this._updateTabSizes(true); return; } } } else { this._lastVisibleTabIndex = i; tabElement.css({'z-index': 'auto', 'margin-left': ''}); this.tabsContainer.append( tabElement ); } } } } );