UNPKG

@kpi4me/golden-layout

Version:

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

519 lines (447 loc) 16.3 kB
lm.items.Stack = function( layoutManager, config, parent ) { lm.items.AbstractContentItem.call( this, layoutManager, config, parent ); this.element = $( '<div class="lm_item lm_stack"></div>' ); this._activeContentItem = null; var cfg = layoutManager.config; this._header = { // defaults' reconstruction from old configuration style show: cfg.settings.hasHeaders === true && config.hasHeaders !== false, popout: cfg.settings.showPopoutIcon && cfg.labels.popout, maximise: cfg.settings.showMaximiseIcon && cfg.labels.maximise, close: cfg.settings.showCloseIcon && cfg.labels.close, minimise: cfg.labels.minimise, }; if( cfg.header ) // load simplified version of header configuration (https://github.com/deepstreamIO/golden-layout/pull/245) lm.utils.copy( this._header, cfg.header ); if( config.header ) // load from stack lm.utils.copy( this._header, config.header ); if( config.content && config.content[ 0 ] && config.content[ 0 ].header ) // load from component if stack omitted lm.utils.copy( this._header, config.content[ 0 ].header ); this._dropZones = {}; this._dropSegment = null; this._contentAreaDimensions = null; this._dropIndex = null; this.isStack = true; this.childElementContainer = $( '<div class="lm_items"></div>' ); this.header = new lm.controls.Header( layoutManager, this ); this.element.append( this.header.element ); this.element.append( this.childElementContainer ); this._setupHeaderPosition(); this._$validateClosability(); }; lm.utils.extend( lm.items.Stack, lm.items.AbstractContentItem ); lm.utils.copy( lm.items.Stack.prototype, { setSize: function() { var i, headerSize = this._header.show ? this.layoutManager.config.dimensions.headerHeight : 0, contentWidth = this.element.width() - (this._sided ? headerSize : 0), contentHeight = this.element.height() - (!this._sided ? headerSize : 0); this.childElementContainer.width( contentWidth ); this.childElementContainer.height( contentHeight ); for( i = 0; i < this.contentItems.length; i++ ) { this.contentItems[ i ].element.width( contentWidth ).height( contentHeight ); } this.emit( 'resize' ); this.emitBubblingEvent( 'stateChanged' ); }, _$init: function() { var i, initialItem; if( this.isInitialised === true ) return; lm.items.AbstractContentItem.prototype._$init.call( this ); for( i = 0; i < this.contentItems.length; i++ ) { this.header.createTab( this.contentItems[ i ] ); this.contentItems[ i ]._$hide(); } if( this.contentItems.length > 0 ) { initialItem = this.contentItems[ this.config.activeItemIndex || 0 ]; if( !initialItem ) { initialItem = this.contentItems[0]; } this.setActiveContentItem( initialItem ); } }, setActiveContentItem: function( contentItem ) { if( lm.utils.indexOf( contentItem, this.contentItems ) === -1 ) { throw new Error( 'contentItem is not a child of this stack' ); } if( this._activeContentItem !== null ) { this._activeContentItem._$hide(); } this._activeContentItem = contentItem; this.header.setActiveContentItem( contentItem ); contentItem._$show(); this.emit( 'activeContentItemChanged', contentItem ); this.layoutManager.emit( 'activeContentItemChanged', contentItem ); this.emitBubblingEvent( 'stateChanged' ); }, getActiveContentItem: function() { return this.header.activeContentItem; }, addChild: function( contentItem, index ) { contentItem = this.layoutManager._$normalizeContentItem( contentItem, this ); lm.items.AbstractContentItem.prototype.addChild.call( this, contentItem, index ); this.childElementContainer.append( contentItem.element ); this.header.createTab( contentItem, index ); this.setActiveContentItem( contentItem ); this.callDownwards( 'setSize' ); this._$validateClosability(); this.emitBubblingEvent( 'stateChanged' ); }, removeChild: function( contentItem, keepChild ) { var index = lm.utils.indexOf( contentItem, this.contentItems ); lm.items.AbstractContentItem.prototype.removeChild.call( this, contentItem, keepChild ); this.header.removeTab( contentItem ); if (this.header.activeContentItem === contentItem) { if (this.contentItems.length > 0) { this.setActiveContentItem(this.contentItems[Math.max(index - 1, 0)]); } else { this._activeContentItem = null; } } else if (this.config.activeItemIndex >= this.contentItems.length) { if (this.contentItems.length > 0) { var activeIndex = lm.utils.indexOf( this.getActiveContentItem(), this.contentItems ); this.config.activeItemIndex = Math.max(activeIndex, 0); } } this._$validateClosability(); this.emitBubblingEvent( 'stateChanged' ); }, /** * Validates that the stack is still closable or not. If a stack is able * to close, but has a non closable component added to it, the stack is no * longer closable until all components are closable. * * @returns {void} */ _$validateClosability: function() { var contentItem, isClosable, len, i; isClosable = this.header._isClosable(); for( i = 0, len = this.contentItems.length; i < len; i++ ) { if( !isClosable ) { break; } isClosable = this.contentItems[ i ].config.isClosable; } this.header._$setClosable( isClosable ); }, _$destroy: function() { lm.items.AbstractContentItem.prototype._$destroy.call( this ); this.header._$destroy(); }, /** * Ok, this one is going to be the tricky one: The user has dropped {contentItem} onto this stack. * * It was dropped on either the stacks header or the top, right, bottom or left bit of the content area * (which one of those is stored in this._dropSegment). Now, if the user has dropped on the header the case * is relatively clear: We add the item to the existing stack... job done (might be good to have * tab reordering at some point, but lets not sweat it right now) * * If the item was dropped on the content part things are a bit more complicated. If it was dropped on either the * top or bottom region we need to create a new column and place the items accordingly. * Unless, of course if the stack is already within a column... in which case we want * to add the newly created item to the existing column... * either prepend or append it, depending on wether its top or bottom. * * Same thing for rows and left / right drop segments... so in total there are 9 things that can potentially happen * (left, top, right, bottom) * is child of the right parent (row, column) + header drop * * @param {lm.item} contentItem * * @returns {void} */ _$onDrop: function( contentItem ) { /* * The item was dropped on the header area. Just add it as a child of this stack and * get the hell out of this logic */ if( this._dropSegment === 'header' ) { this._resetHeaderDropZone(); this.addChild( contentItem, this._dropIndex ); return; } /* * The stack is empty. Let's just add the element. */ if( this._dropSegment === 'body' ) { this.addChild( contentItem ); return; } /* * The item was dropped on the top-, left-, bottom- or right- part of the content. Let's * aggregate some conditions to make the if statements later on more readable */ var isVertical = this._dropSegment === 'top' || this._dropSegment === 'bottom', isHorizontal = this._dropSegment === 'left' || this._dropSegment === 'right', insertBefore = this._dropSegment === 'top' || this._dropSegment === 'left', hasCorrectParent = ( isVertical && this.parent.isColumn ) || ( isHorizontal && this.parent.isRow ), type = isVertical ? 'column' : 'row', dimension = isVertical ? 'height' : 'width', index, stack, rowOrColumn; /* * The content item can be either a component or a stack. If it is a component, wrap it into a stack */ if( contentItem.isComponent ) { stack = this.layoutManager.createContentItem( { type: 'stack', header: contentItem.config.header || {} }, this ); stack._$init(); stack.addChild( contentItem ); contentItem = stack; } /* * If the item is dropped on top or bottom of a column or left and right of a row, it's already * layd out in the correct way. Just add it as a child */ if( hasCorrectParent ) { index = lm.utils.indexOf( this, this.parent.contentItems ); this.parent.addChild( contentItem, insertBefore ? index : index + 1, true ); this.config[ dimension ] *= 0.5; contentItem.config[ dimension ] = this.config[ dimension ]; this.parent.callDownwards( 'setSize' ); /* * This handles items that are dropped on top or bottom of a row or left / right of a column. We need * to create the appropriate contentItem for them to live in */ } else { type = isVertical ? 'column' : 'row'; rowOrColumn = this.layoutManager.createContentItem( { type: type }, this ); this.parent.replaceChild( this, rowOrColumn ); rowOrColumn.addChild( contentItem, insertBefore ? 0 : undefined, true ); rowOrColumn.addChild( this, insertBefore ? undefined : 0, true ); this.config[ dimension ] = 50; contentItem.config[ dimension ] = 50; rowOrColumn.callDownwards( 'setSize' ); } }, /** * If the user hovers above the header part of the stack, indicate drop positions for tabs. * otherwise indicate which segment of the body the dragged item would be dropped on * * @param {Int} x Absolute Screen X * @param {Int} y Absolute Screen Y * * @returns {void} */ _$highlightDropZone: function( x, y ) { var segment, area; for( segment in this._contentAreaDimensions ) { area = this._contentAreaDimensions[ segment ].hoverArea; if( area.x1 < x && area.x2 > x && area.y1 < y && area.y2 > y ) { if( segment === 'header' ) { this._dropSegment = 'header'; this._highlightHeaderDropZone( this._sided ? y : x ); } else { this._resetHeaderDropZone(); this._highlightBodyDropZone( segment ); } return; } } }, _$getArea: function() { if( this.element.is( ':visible' ) === false ) { return null; } var getArea = lm.items.AbstractContentItem.prototype._$getArea, headerArea = getArea.call( this, this.header.element ), contentArea = getArea.call( this, this.childElementContainer ), contentWidth = contentArea.x2 - contentArea.x1, contentHeight = contentArea.y2 - contentArea.y1; this._contentAreaDimensions = { header: { hoverArea: { x1: headerArea.x1, y1: headerArea.y1, x2: headerArea.x2, y2: headerArea.y2 }, highlightArea: { x1: headerArea.x1, y1: headerArea.y1, x2: headerArea.x2, y2: headerArea.y2 } } }; /** * If this Stack is a parent to rows, columns or other stacks only its * header is a valid dropzone. */ if( this._activeContentItem && this._activeContentItem.isComponent === false ) { return headerArea; } /** * Highlight the entire body if the stack is empty */ if( this.contentItems.length === 0 ) { this._contentAreaDimensions.body = { hoverArea: { x1: contentArea.x1, y1: contentArea.y1, x2: contentArea.x2, y2: contentArea.y2 }, highlightArea: { x1: contentArea.x1, y1: contentArea.y1, x2: contentArea.x2, y2: contentArea.y2 } }; return getArea.call( this, this.element ); } this._contentAreaDimensions.left = { hoverArea: { x1: contentArea.x1, y1: contentArea.y1, x2: contentArea.x1 + contentWidth * 0.25, y2: contentArea.y2 }, highlightArea: { x1: contentArea.x1, y1: contentArea.y1, x2: contentArea.x1 + contentWidth * 0.5, y2: contentArea.y2 } }; this._contentAreaDimensions.top = { hoverArea: { x1: contentArea.x1 + contentWidth * 0.25, y1: contentArea.y1, x2: contentArea.x1 + contentWidth * 0.75, y2: contentArea.y1 + contentHeight * 0.5 }, highlightArea: { x1: contentArea.x1, y1: contentArea.y1, x2: contentArea.x2, y2: contentArea.y1 + contentHeight * 0.5 } }; this._contentAreaDimensions.right = { hoverArea: { x1: contentArea.x1 + contentWidth * 0.75, y1: contentArea.y1, x2: contentArea.x2, y2: contentArea.y2 }, highlightArea: { x1: contentArea.x1 + contentWidth * 0.5, y1: contentArea.y1, x2: contentArea.x2, y2: contentArea.y2 } }; this._contentAreaDimensions.bottom = { hoverArea: { x1: contentArea.x1 + contentWidth * 0.25, y1: contentArea.y1 + contentHeight * 0.5, x2: contentArea.x1 + contentWidth * 0.75, y2: contentArea.y2 }, highlightArea: { x1: contentArea.x1, y1: contentArea.y1 + contentHeight * 0.5, x2: contentArea.x2, y2: contentArea.y2 } }; return getArea.call( this, this.element ); }, _highlightHeaderDropZone: function( x ) { var i, tabElement, tabsLength = this.header.tabs.length, isAboveTab = false, tabTop, tabLeft, offset, placeHolderLeft, headerOffset, tabWidth, halfX; // Empty stack if( tabsLength === 0 ) { headerOffset = this.header.element.offset(); this.layoutManager.dropTargetIndicator.highlightArea( { x1: headerOffset.left, x2: headerOffset.left + 100, y1: headerOffset.top + this.header.element.height() - 20, y2: headerOffset.top + this.header.element.height() } ); return; } for( i = 0; i < tabsLength; i++ ) { tabElement = this.header.tabs[ i ].element; offset = tabElement.offset(); if( this._sided ) { tabLeft = offset.top; tabTop = offset.left; tabWidth = tabElement.height(); } else { tabLeft = offset.left; tabTop = offset.top; tabWidth = tabElement.width(); } if( x > tabLeft && x < tabLeft + tabWidth ) { isAboveTab = true; break; } } if( isAboveTab === false && x < tabLeft ) { return; } halfX = tabLeft + tabWidth / 2; if( x < halfX ) { this._dropIndex = i; tabElement.before( this.layoutManager.tabDropPlaceholder ); } else { this._dropIndex = Math.min( i + 1, tabsLength ); tabElement.after( this.layoutManager.tabDropPlaceholder ); } if( this._sided ) { placeHolderTop = this.layoutManager.tabDropPlaceholder.offset().top; this.layoutManager.dropTargetIndicator.highlightArea( { x1: tabTop, x2: tabTop + tabElement.innerHeight(), y1: placeHolderTop, y2: placeHolderTop + this.layoutManager.tabDropPlaceholder.width() } ); return; } placeHolderLeft = this.layoutManager.tabDropPlaceholder.offset().left; this.layoutManager.dropTargetIndicator.highlightArea( { x1: placeHolderLeft, x2: placeHolderLeft + this.layoutManager.tabDropPlaceholder.width(), y1: tabTop, y2: tabTop + tabElement.innerHeight() } ); }, _resetHeaderDropZone: function() { this.layoutManager.tabDropPlaceholder.remove(); }, _setupHeaderPosition: function() { var side = [ 'right', 'left', 'bottom' ].indexOf( this._header.show ) >= 0 && this._header.show; this.header.element.toggle( !!this._header.show ); this._side = side; this._sided = [ 'right', 'left' ].indexOf( this._side ) >= 0; this.element.removeClass( 'lm_left lm_right lm_bottom' ); if( this._side ) this.element.addClass( 'lm_' + this._side ); if( this.element.find( '.lm_header' ).length && this.childElementContainer ) { var headerPosition = [ 'right', 'bottom' ].indexOf( this._side ) >= 0 ? 'before' : 'after'; this.header.element[ headerPosition ]( this.childElementContainer ); this.callDownwards( 'setSize' ); } }, _highlightBodyDropZone: function( segment ) { var highlightArea = this._contentAreaDimensions[ segment ].highlightArea; this.layoutManager.dropTargetIndicator.highlightArea( highlightArea ); this._dropSegment = segment; } } );