Skip to content

Instantly share code, notes, and snippets.

@serkanserttop
Last active April 4, 2017 07:50
Show Gist options
  • Save serkanserttop/1330a4ebe82f052fbb5b5f4b5d75c4dc to your computer and use it in GitHub Desktop.
Save serkanserttop/1330a4ebe82f052fbb5b5f4b5d75c4dc to your computer and use it in GitHub Desktop.
(function($){var lm={"config":{},"container":{},"controls":{},"errors":{},"items":{},"utils":{}};
lm.utils.F = function () {};
lm.utils.extend = function( subClass, superClass ) {
subClass.prototype = lm.utils.createObject( superClass.prototype );
subClass.prototype.contructor = subClass;
};
lm.utils.createObject = function( prototype ) {
if( typeof Object.create === 'function' ) {
return Object.create( prototype );
} else {
lm.utils.F.prototype = prototype;
return new lm.utils.F();
}
};
lm.utils.objectKeys = function( object ) {
var keys, key;
if( typeof Object.keys === 'function' ) {
return Object.keys( object );
} else {
keys = [];
for( key in object ) {
keys.push( key );
}
return keys;
}
};
lm.utils.getHashValue = function( key ) {
var matches = location.hash.match( new RegExp( key + '=([^&]*)' ) );
return matches ? matches[ 1 ] : null;
};
lm.utils.getQueryStringParam = function( param ) {
if( window.location.hash ) {
return lm.utils.getHashValue( param );
} else if( !window.location.search ) {
return null;
}
var keyValuePairs = window.location.search.substr( 1 ).split( '&' ),
params = {},
pair,
i;
for( i = 0; i < keyValuePairs.length; i++ ) {
pair = keyValuePairs[ i ].split( '=' );
params[ pair[ 0 ] ] = pair[ 1 ];
}
return params[ param ] || null;
};
lm.utils.copy = function( target, source ) {
for( var key in source ) {
target[ key ] = source[ key ];
}
return target;
};
/**
* This is based on Paul Irish's shim, but looks quite odd in comparison. Why?
* Because
* a) it shouldn't affect the global requestAnimationFrame function
* b) it shouldn't pass on the time that has passed
*
* @param {Function} fn
*
* @returns {void}
*/
lm.utils.animFrame = function( fn ){
return ( window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
})(function(){
fn();
});
};
lm.utils.indexOf = function( needle, haystack ) {
if( !( haystack instanceof Array ) ) {
throw new Error( 'Haystack is not an Array' );
}
if( haystack.indexOf ) {
return haystack.indexOf( needle );
} else {
for( var i = 0; i < haystack.length; i++ ) {
if( haystack[ i ] === needle ) {
return i;
}
}
return -1;
}
};
if ( typeof /./ != 'function' && typeof Int8Array != 'object' ) {
lm.utils.isFunction = function ( obj ) {
return typeof obj == 'function' || false;
};
} else {
lm.utils.isFunction = function ( obj ) {
return toString.call(obj) === '[object Function]';
};
}
lm.utils.fnBind = function( fn, context, boundArgs ) {
if( Function.prototype.bind !== undefined ) {
return Function.prototype.bind.apply( fn, [ context ].concat( boundArgs || [] ) );
}
var bound = function () {
// Join the already applied arguments to the now called ones (after converting to an array again).
var args = ( boundArgs || [] ).concat(Array.prototype.slice.call(arguments, 0));
// If not being called as a constructor
if (!(this instanceof bound)){
// return the result of the function called bound to target and partially applied.
return fn.apply(context, args);
}
// If being called as a constructor, apply the function bound to self.
fn.apply(this, args);
};
// Attach the prototype of the function to our newly created function.
bound.prototype = fn.prototype;
return bound;
};
lm.utils.removeFromArray = function( item, array ) {
var index = lm.utils.indexOf( item, array );
if( index === -1 ) {
throw new Error( 'Can\'t remove item from array. Item is not in the array' );
}
array.splice( index, 1 );
};
lm.utils.now = function() {
if( typeof Date.now === 'function' ) {
return Date.now();
} else {
return ( new Date() ).getTime();
}
};
lm.utils.getUniqueId = function() {
return ( Math.random() * 1000000000000000 )
.toString(36)
.replace( '.', '' );
};
/**
* A basic XSS filter. It is ultimately up to the
* implementing developer to make sure their particular
* applications and usecases are save from cross site scripting attacks
*
* @param {String} input
* @param {Boolean} keepTags
*
* @returns {String} filtered input
*/
lm.utils.filterXss = function( input, keepTags ) {
var output = input
.replace( /javascript/gi, 'j&#97;vascript' )
.replace( /expression/gi, 'expr&#101;ssion' )
.replace( /onload/gi, 'onlo&#97;d' )
.replace( /script/gi, '&#115;cript' )
.replace( /onerror/gi, 'on&#101;rror' );
if( keepTags === true ) {
return output;
} else {
return output
.replace( />/g, '&gt;' )
.replace( /</g, '&lt;' );
}
};
/**
* Removes html tags from a string
*
* @param {String} input
*
* @returns {String} input without tags
*/
lm.utils.stripTags = function( input ) {
return $.trim( input.replace( /(<([^>]+)>)/ig, '' ) );
};
/**
* A generic and very fast EventEmitter
* implementation. On top of emitting the
* actual event it emits an
*
* lm.utils.EventEmitter.ALL_EVENT
*
* event for every event triggered. This allows
* to hook into it and proxy events forwards
*
* @constructor
*/
lm.utils.EventEmitter = function()
{
this._mSubscriptions = { };
this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ] = [];
/**
* Listen for events
*
* @param {String} sEvent The name of the event to listen to
* @param {Function} fCallback The callback to execute when the event occurs
* @param {[Object]} oContext The value of the this pointer within the callback function
*
* @returns {void}
*/
this.on = function( sEvent, fCallback, oContext )
{
if ( !lm.utils.isFunction(fCallback) ) {
throw new Error( 'Tried to listen to event ' + sEvent + ' with non-function callback ' + fCallback );
}
if( !this._mSubscriptions[ sEvent ] )
{
this._mSubscriptions[ sEvent ] = [];
}
this._mSubscriptions[ sEvent ].push({ fn: fCallback, ctx: oContext });
};
/**
* Emit an event and notify listeners
*
* @param {String} sEvent The name of the event
* @param {Mixed} various additional arguments that will be passed to the listener
*
* @returns {void}
*/
this.emit = function( sEvent )
{
var i, ctx, args;
args = Array.prototype.slice.call( arguments, 1 );
if( this._mSubscriptions[ sEvent ] ) {
for( i = 0; i < this._mSubscriptions[ sEvent ].length; i++ )
{
ctx = this._mSubscriptions[ sEvent ][ i ].ctx || {};
this._mSubscriptions[ sEvent ][ i ].fn.apply( ctx, args );
}
}
args.unshift( sEvent );
for( i = 0; i < this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ].length; i++ )
{
ctx = this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ][ i ].ctx || {};
this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ][ i ].fn.apply( ctx, args );
}
};
/**
* Removes a listener for an event, or all listeners if no callback and context is provided.
*
* @param {String} sEvent The name of the event
* @param {Function} fCallback The previously registered callback method (optional)
* @param {Object} oContext The previously registered context (optional)
*
* @returns {void}
*/
this.unbind = function( sEvent, fCallback, oContext )
{
if( !this._mSubscriptions[ sEvent ] ) {
throw new Error( 'No subscribtions to unsubscribe for event ' + sEvent );
}
var i, bUnbound = false;
for( i = 0; i < this._mSubscriptions[ sEvent ].length; i++ )
{
if
(
( !fCallback || this._mSubscriptions[ sEvent ][ i ].fn === fCallback ) &&
( !oContext || oContext === this._mSubscriptions[ sEvent ][ i ].ctx )
)
{
this._mSubscriptions[ sEvent ].splice( i, 1 );
bUnbound = true;
}
}
if( bUnbound === false )
{
throw new Error( 'Nothing to unbind for ' + sEvent );
}
};
/**
* Alias for unbind
*/
this.off = this.unbind;
/**
* Alias for emit
*/
this.trigger = this.emit;
};
/**
* The name of the event that's triggered for every other event
*
* usage
*
* myEmitter.on( lm.utils.EventEmitter.ALL_EVENT, function( eventName, argsArray ){
* //do stuff
* });
*
* @type {String}
*/
lm.utils.EventEmitter.ALL_EVENT = '__all';
lm.utils.DragListener = function(eElement, nButtonCode)
{
lm.utils.EventEmitter.call(this);
this._eElement = $(eElement);
this._oDocument = $(document);
this._eBody = $(document.body);
this._nButtonCode = nButtonCode || 0;
/**
* The delay after which to start the drag in milliseconds
*/
this._nDelay = 200;
/**
* The distance the mouse needs to be moved to qualify as a drag
*/
this._nDistance = 10;//TODO - works better with delay only
this._nX = 0;
this._nY = 0;
this._nOriginalX = 0;
this._nOriginalY = 0;
this._bDragging = false;
this._fMove = lm.utils.fnBind( this.onMouseMove, this );
this._fUp = lm.utils.fnBind( this.onMouseUp, this );
this._fDown = lm.utils.fnBind( this.onMouseDown, this );
this._eElement.on( 'mousedown touchstart', this._fDown );
};
lm.utils.DragListener.timeout = null;
lm.utils.copy( lm.utils.DragListener.prototype, {
destroy: function() {
this._eElement.unbind( 'mousedown touchstart', this._fDown );
},
onMouseDown: function(oEvent)
{
oEvent.preventDefault();
if (oEvent.button == 0) {
var coordinates = this._getCoordinates( oEvent );
this._nOriginalX = coordinates.x;
this._nOriginalY = coordinates.y;
this._oDocument.on( 'mousemove touchmove', this._fMove );
this._oDocument.one( 'mouseup touchend', this._fUp );
this._timeout = setTimeout( lm.utils.fnBind( this._startDrag, this ), this._nDelay );
}
},
onMouseMove: function(oEvent)
{
if (this._timeout != null) {
oEvent.preventDefault();
var coordinates = this._getCoordinates(oEvent);
this._nX = coordinates.x - this._nOriginalX;
this._nY = coordinates.y - this._nOriginalY;
if (this._bDragging === false) {
if (
Math.abs(this._nX) > this._nDistance ||
Math.abs(this._nY) > this._nDistance
) {
clearTimeout(this._timeout);
this._startDrag();
}
}
if (this._bDragging) {
this.emit('drag', this._nX, this._nY, oEvent);
}
}
},
onMouseUp: function(oEvent)
{
if(this._timeout != null) {
clearTimeout( this._timeout );
this._eBody.removeClass( 'lm_dragging' );
this._eElement.removeClass( 'lm_dragging' );
this._oDocument.find( 'iframe' ).css( 'pointer-events', '' );
this._oDocument.unbind( 'mousemove touchmove', this._fMove );
if( this._bDragging === true ) {
this._bDragging = false;
this.emit( 'dragStop', oEvent, this._nOriginalX + this._nX );
}
}
},
_startDrag: function()
{
this._bDragging = true;
this._eBody.addClass( 'lm_dragging' );
this._eElement.addClass( 'lm_dragging' );
this._oDocument.find( 'iframe' ).css( 'pointer-events', 'none' );
this.emit('dragStart', this._nOriginalX, this._nOriginalY);
},
_getCoordinates: function( event ) {
var coordinates = {};
if( event.type.substr( 0, 5 ) === 'touch' ) {
coordinates.x = event.originalEvent.targetTouches[ 0 ].pageX;
coordinates.y = event.originalEvent.targetTouches[ 0 ].pageY;
} else {
coordinates.x = event.pageX;
coordinates.y = event.pageY;
}
return coordinates;
}
});
/**
* The main class that will be exposed as GoldenLayout.
*
* @public
* @constructor
* @param {GoldenLayout config} config
* @param {[DOM element container]} container Can be a jQuery selector string or a Dom element. Defaults to body
*
* @returns {VOID}
*/
lm.LayoutManager = function( config, container ) {
if( !$ || typeof $.noConflict !== 'function' ) {
var errorMsg = 'jQuery is missing as dependency for GoldenLayout. ';
errorMsg += 'Please either expose $ on GoldenLayout\'s scope (e.g. window) or add "jquery" to ';
errorMsg += 'your paths when using RequireJS/AMD';
throw new Error( errorMsg );
}
lm.utils.EventEmitter.call( this );
this.isInitialised = false;
this._isFullPage = false;
this._resizeTimeoutId = null;
this._components = { 'lm-react-component': lm.utils.ReactComponentHandler };
this._itemAreas = [];
this._resizeFunction = lm.utils.fnBind( this._onResize, this );
this._unloadFunction = lm.utils.fnBind( this._onUnload, this );
this._maximisedItem = null;
this._maximisePlaceholder = $( '<div class="lm_maximise_place"></div>' );
this._creationTimeoutPassed = false;
this._subWindowsCreated = false;
this._dragSources = [];
this.width = null;
this.height = null;
this.root = null;
this.openPopouts = [];
this.selectedItem = null;
this.isSubWindow = false;
this.eventHub = new lm.utils.EventHub( this );
this.config = this._createConfig( config );
this.container = container;
this.dropTargetIndicator = null;
this.transitionIndicator = null;
this.tabDropPlaceholder = $( '<div class="lm_drop_tab_placeholder"></div>' );
if( this.isSubWindow === true ) {
$( 'body' ).css( 'visibility', 'hidden' );
}
this._typeToItem = {
'column': lm.utils.fnBind( lm.items.RowOrColumn, this, [ true ] ),
'row': lm.utils.fnBind( lm.items.RowOrColumn, this, [ false ] ),
'stack': lm.items.Stack,
'component': lm.items.Component
};
};
/**
* Hook that allows to access private classes
*/
lm.LayoutManager.__lm = lm;
/**
* Takes a GoldenLayout configuration object and
* replaces its keys and values recursively with
* one letter codes
*
* @static
* @public
* @param {Object} config A GoldenLayout config object
*
* @returns {Object} minified config
*/
lm.LayoutManager.minifyConfig = function( config ) {
return ( new lm.utils.ConfigMinifier() ).minifyConfig( config );
};
/**
* Takes a configuration Object that was previously minified
* using minifyConfig and returns its original version
*
* @static
* @public
* @param {Object} minifiedConfig
*
* @returns {Object} the original configuration
*/
lm.LayoutManager.unminifyConfig = function( config ) {
return ( new lm.utils.ConfigMinifier() ).unminifyConfig( config );
};
lm.utils.copy( lm.LayoutManager.prototype, {
/**
* Register a component with the layout manager. If a configuration node
* of type component is reached it will look up componentName and create the
* associated component
*
* {
* type: "component",
* componentName: "EquityNewsFeed",
* componentState: { "feedTopic": "us-bluechips" }
* }
*
* @public
* @param {String} name
* @param {Function} constructor
*
* @returns {void}
*/
registerComponent: function( name, constructor ) {
if( typeof constructor !== 'function' ) {
throw new Error( 'Please register a constructor function' );
}
if( this._components[ name ] !== undefined ) {
throw new Error( 'Component ' + name + ' is already registered' );
}
this._components[ name ] = constructor;
},
/**
* Creates a layout configuration object based on the the current state
*
* @public
* @returns {Object} GoldenLayout configuration
*/
toConfig: function( root ) {
var config, next, i;
if( this.isInitialised === false ) {
throw new Error( 'Can\'t create config, layout not yet initialised' );
}
if( root && !( root instanceof lm.items.AbstractContentItem ) ){
throw new Error( 'Root must be a ContentItem' );
}
/*
* settings & labels
*/
config = {
settings: lm.utils.copy( {}, this.config.settings ),
dimensions: lm.utils.copy( {}, this.config.dimensions ),
labels: lm.utils.copy( {}, this.config.labels )
};
/*
* Content
*/
config.content = [];
next = function( configNode, item ) {
var key, i;
for( key in item.config ) {
if( key !== 'content' ) {
configNode[ key ] = item.config[ key ];
}
}
if( item.contentItems.length ) {
configNode.content = [];
for( i = 0; i < item.contentItems.length; i++ ) {
configNode.content[ i ] = {};
next( configNode.content[ i ], item.contentItems[ i ] );
}
}
};
if( root ) {
next( config, { contentItems: [ root ] } );
} else {
next( config, this.root );
}
/*
* Retrieve config for subwindows
*/
this._$reconcilePopoutWindows();
config.openPopouts = [];
for( i = 0; i < this.openPopouts.length; i++ ) {
config.openPopouts.push( this.openPopouts[ i ].toConfig() );
}
/*
* Add maximised item
*/
config.maximisedItemId = this._maximisedItem ? '__glMaximised' : null;
return config;
},
/**
* Returns a previously registered component
*
* @public
* @param {String} name The name used
*
* @returns {Function}
*/
getComponent: function( name ) {
if( this._components[ name ] === undefined ) {
throw new lm.errors.ConfigurationError( 'Unknown component "' + name + '"' );
}
return this._components[ name ];
},
/**
* Creates the actual layout. Must be called after all initial components
* are registered. Recurses through the configuration and sets up
* the item tree.
*
* If called before the document is ready it adds itself as a listener
* to the document.ready event
*
* @public
*
* @returns {void}
*/
init: function() {
/**
* Create the popout windows straight away. If popouts are blocked
* an error is thrown on the same 'thread' rather than a timeout and can
* be caught. This also prevents any further initilisation from taking place.
*/
if( this._subWindowsCreated === false ) {
this._createSubWindows();
this._subWindowsCreated = true;
}
/**
* If the document isn't ready yet, wait for it.
*/
if( document.readyState === 'loading' || document.body === null ) {
$(document).ready( lm.utils.fnBind( this.init, this ));
return;
}
/**
* If this is a subwindow, wait a few milliseconds for the original
* page's js calls to be executed, then replace the bodies content
* with GoldenLayout
*/
if( this.isSubWindow === true && this._creationTimeoutPassed === false ) {
setTimeout( lm.utils.fnBind( this.init, this ), 7 );
this._creationTimeoutPassed = true;
return;
}
if( this.isSubWindow === true ) {
this._adjustToWindowMode();
}
this._setContainer();
this.dropTargetIndicator = new lm.controls.DropTargetIndicator( this.container );
this.transitionIndicator = new lm.controls.TransitionIndicator();
this.updateSize();
this._create( this.config );
this._bindEvents();
this.isInitialised = true;
this.emit( 'initialised' );
},
/**
* Updates the layout managers size
*
* @public
* @param {[int]} width height in pixels
* @param {[int]} height width in pixels
*
* @returns {void}
*/
updateSize: function( width, height ) {
if( arguments.length === 2 ) {
this.width = width;
this.height = height;
} else {
this.width = this.container.width();
this.height = this.container.height();
}
if( this.isInitialised === true ) {
this.root.callDownwards( 'setSize' );
if( this._maximisedItem ) {
this._maximisedItem.element.width( this.container.width() );
this._maximisedItem.element.height( this.container.height() );
this._maximisedItem.callDownwards( 'setSize' );
}
}
},
/**
* Destroys the LayoutManager instance itself as well as every ContentItem
* within it. After this is called nothing should be left of the LayoutManager.
*
* @public
* @returns {void}
*/
destroy: function() {
if( this.isInitialised === false ) {
return;
}
this._onUnload();
$( window ).off( 'resize', this._resizeFunction );
$( window ).off( 'unload beforeunload', this._unloadFunction );
this.root.callDownwards( '_$destroy', [], true );
this.root.contentItems = [];
this.tabDropPlaceholder.remove();
this.dropTargetIndicator.destroy();
this.transitionIndicator.destroy();
this.eventHub.destroy();
this._dragSources.forEach(function (dragSource) {
dragSource._dragListener.destroy();
dragSource._element = null;
dragSource._itemConfig = null;
dragSource._dragListener = null;
});
this._dragSources = [];
},
/**
* Recursively creates new item tree structures based on a provided
* ItemConfiguration object
*
* @public
* @param {Object} config ItemConfig
* @param {[ContentItem]} parent The item the newly created item should be a child of
*
* @returns {lm.items.ContentItem}
*/
createContentItem: function( config, parent ) {
var typeErrorMsg, contentItem;
if( typeof config.type !== 'string' ) {
throw new lm.errors.ConfigurationError( 'Missing parameter \'type\'', config );
}
if (config.type === 'react-component') {
config.type = 'component';
config.componentName = 'lm-react-component';
}
if( !this._typeToItem[ config.type ] ) {
typeErrorMsg = 'Unknown type \'' + config.type + '\'. ' +
'Valid types are ' + lm.utils.objectKeys( this._typeToItem ).join( ',' );
throw new lm.errors.ConfigurationError( typeErrorMsg );
}
/**
* We add an additional stack around every component that's not within a stack anyways.
*/
if(
// If this is a component
config.type === 'component' &&
// and it's not already within a stack
!( parent instanceof lm.items.Stack ) &&
// and we have a parent
!!parent &&
// and it's not the topmost item in a new window
!( this.isSubWindow === true && parent instanceof lm.items.Root )
) {
config = {
type: 'stack',
width: config.width,
height: config.height,
content: [ config ]
};
}
contentItem = new this._typeToItem[ config.type ]( this, config, parent );
return contentItem;
},
/**
* Creates a popout window with the specified content and dimensions
*
* @param {Object|lm.itemsAbstractContentItem} configOrContentItem
* @param {[Object]} dimensions A map with width, height, left and top
* @param {[String]} parentId the id of the element this item will be appended to
* when popIn is called
* @param {[Number]} indexInParent The position of this item within its parent element
* @returns {lm.controls.BrowserPopout}
*/
createPopout: function( configOrContentItem, dimensions, parentId, indexInParent ) {
var config = configOrContentItem,
isItem = configOrContentItem instanceof lm.items.AbstractContentItem,
self = this,
windowLeft,
windowTop,
offset,
parent,
child,
browserPopout;
parentId = parentId || null;
if( isItem ) {
config = this.toConfig( configOrContentItem ).content;
parentId = lm.utils.getUniqueId();
/**
* If the item is the only component within a stack or for some
* other reason the only child of its parent the parent will be destroyed
* when the child is removed.
*
* In order to support this we move up the tree until we find something
* that will remain after the item is being popped out
*/
parent = configOrContentItem.parent;
child = configOrContentItem;
while( parent.contentItems.length === 1 && !parent.isRoot ) {
parent = parent.parent;
child = child.parent;
}
parent.addId( parentId );
if( isNaN( indexInParent ) ) {
indexInParent = lm.utils.indexOf( child, parent.contentItems );
}
} else {
if( !( config instanceof Array ) ) {
config = [ config ];
}
}
if( !dimensions && isItem ) {
windowLeft = window.screenX || window.screenLeft;
windowTop = window.screenY || window.screenTop;
offset = configOrContentItem.element.offset();
dimensions = {
left: windowLeft + offset.left,
top: windowTop + offset.top,
width: configOrContentItem.element.width(),
height: configOrContentItem.element.height()
};
}
if( !dimensions && !isItem ) {
dimensions = {
left: window.screenX || window.screenLeft + 20,
top: window.screenY || window.screenTop + 20,
width: 500,
height: 309
};
}
if( isItem ) {
configOrContentItem.remove();
}
browserPopout = new lm.controls.BrowserPopout( config, dimensions, parentId, indexInParent, this );
browserPopout.on( 'initialised', function(){
self.emit( 'windowOpened', browserPopout );
});
browserPopout.on( 'closed', function(){
self._$reconcilePopoutWindows();
});
this.openPopouts.push( browserPopout );
return browserPopout;
},
/**
* Attaches DragListener to any given DOM element
* and turns it into a way of creating new ContentItems
* by 'dragging' the DOM element into the layout
*
* @param {jQuery DOM element} element
* @param {Object|Function} itemConfig for the new item to be created, or a function which will provide it
*
* @returns {void}
*/
createDragSource: function( element, itemConfig ) {
this.config.settings.constrainDragToContainer = false;
var dragSource = new lm.controls.DragSource( $( element ), itemConfig, this );
this._dragSources.push(dragSource);
return dragSource;
},
/**
* Programmatically selects an item. This deselects
* the currently selected item, selects the specified item
* and emits a selectionChanged event
*
* @param {lm.item.AbstractContentItem} item#
* @param {[Boolean]} _$silent Wheather to notify the item of its selection
* @event selectionChanged
*
* @returns {VOID}
*/
selectItem: function( item, _$silent ) {
if( this.config.settings.selectionEnabled !== true ) {
throw new Error( 'Please set selectionEnabled to true to use this feature' );
}
if( item === this.selectedItem ) {
return;
}
if( this.selectedItem !== null ) {
this.selectedItem.deselect();
}
if( item && _$silent !== true ) {
item.select();
}
this.selectedItem = item;
this.emit( 'selectionChanged', item );
},
/*************************
* PACKAGE PRIVATE
*************************/
_$maximiseItem: function( contentItem ) {
if( this._maximisedItem !== null ) {
this._$minimiseItem( this._maximisedItem );
}
this._maximisedItem = contentItem;
this._maximisedItem.addId( '__glMaximised' );
contentItem.element.addClass( 'lm_maximised' );
contentItem.element.after( this._maximisePlaceholder );
this.root.element.prepend( contentItem.element );
contentItem.element.width( this.container.width() );
contentItem.element.height( this.container.height() );
contentItem.callDownwards( 'setSize' );
this._maximisedItem.emit( 'maximised' );
this.emit( 'stateChanged' );
},
_$minimiseItem: function( contentItem ) {
contentItem.element.removeClass( 'lm_maximised' );
contentItem.removeId( '__glMaximised' );
this._maximisePlaceholder.after( contentItem.element );
this._maximisePlaceholder.remove();
contentItem.parent.callDownwards( 'setSize' );
this._maximisedItem = null;
contentItem.emit( 'minimised' );
this.emit( 'stateChanged' );
},
/**
* This method is used to get around sandboxed iframe restrictions.
* If 'allow-top-navigation' is not specified in the iframe's 'sandbox' attribute
* (as is the case with codepens) the parent window is forbidden from calling certain
* methods on the child, such as window.close() or setting document.location.href.
*
* This prevented GoldenLayout popouts from popping in in codepens. The fix is to call
* _$closeWindow on the child window's gl instance which (after a timeout to disconnect
* the invoking method from the close call) closes itself.
*
* @packagePrivate
*
* @returns {void}
*/
_$closeWindow: function() {
window.setTimeout(function(){
window.close();
}, 1);
},
_$getArea: function( x, y ) {
// console.log('getArea', this);
var i, area, smallestSurface = Infinity, mathingArea = null;
for( i = 0; i < this._itemAreas.length; i++ ) {
area = this._itemAreas[ i ];
if(
x > area.x1 &&
x < area.x2 &&
y > area.y1 &&
y < area.y2 &&
smallestSurface > area.surface
){
smallestSurface = area.surface;
mathingArea = area;
}
}
return mathingArea;
},
_$calculateItemAreas: function() {
var i, area, allContentItems = this._getAllContentItems();
this._itemAreas = [];
/**
* If the last item is dragged out, highlight the entire container size to
* allow to re-drop it. allContentItems[ 0 ] === this.root at this point
*
* Don't include root into the possible drop areas though otherwise since it
* will used for every gap in the layout, e.g. splitters
*/
if( allContentItems.length === 1 ) {
this._itemAreas.push( this.root._$getArea() );
return;
}
for( i = 0; i < allContentItems.length; i++ ) {
if( !( allContentItems[ i ].isStack ) ) {
continue;
}
area = allContentItems[ i ]._$getArea();
if( area === null ) {
continue;
} else if( area instanceof Array ) {
this._itemAreas = this._itemAreas.concat( area );
} else {
this._itemAreas.push( area );
}
}
},
/**
* Takes a contentItem or a configuration and optionally a parent
* item and returns an initialised instance of the contentItem.
* If the contentItem is a function, it is first called
*
* @packagePrivate
*
* @param {lm.items.AbtractContentItem|Object|Function} contentItemOrConfig
* @param {lm.items.AbtractContentItem} parent Only necessary when passing in config
*
* @returns {lm.items.AbtractContentItem}
*/
_$normalizeContentItem: function( contentItemOrConfig, parent ) {
if( !contentItemOrConfig ) {
throw new Error( 'No content item defined' );
}
if( lm.utils.isFunction( contentItemOrConfig ) ) {
contentItemOrConfig = contentItemOrConfig();
}
if( contentItemOrConfig instanceof lm.items.AbstractContentItem ) {
return contentItemOrConfig;
}
if( $.isPlainObject( contentItemOrConfig ) && contentItemOrConfig.type ) {
var newContentItem = this.createContentItem( contentItemOrConfig, parent );
newContentItem.callDownwards( '_$init' );
return newContentItem;
} else {
throw new Error( 'Invalid contentItem' );
}
},
/**
* Iterates through the array of open popout windows and removes the ones
* that are effectively closed. This is necessary due to the lack of reliably
* listening for window.close / unload events in a cross browser compatible fashion.
*
* @packagePrivate
*
* @returns {void}
*/
_$reconcilePopoutWindows: function() {
var openPopouts = [], i;
for( i = 0; i < this.openPopouts.length; i++ ) {
if( this.openPopouts[ i ].getWindow().closed === false ) {
openPopouts.push( this.openPopouts[ i ] );
} else {
this.emit( 'windowClosed', this.openPopouts[ i ] );
}
}
if( this.openPopouts.length !== openPopouts.length ) {
this.emit( 'stateChanged' );
this.openPopouts = openPopouts;
}
},
/***************************
* PRIVATE
***************************/
/**
* Returns a flattened array of all content items,
* regardles of level or type
*
* @private
*
* @returns {void}
*/
_getAllContentItems: function() {
var allContentItems = [];
var addChildren = function( contentItem ) {
allContentItems.push( contentItem );
if( contentItem.contentItems instanceof Array ) {
for( var i = 0; i < contentItem.contentItems.length; i++ ) {
addChildren( contentItem.contentItems[ i ] );
}
}
};
addChildren( this.root );
return allContentItems;
},
/**
* Binds to DOM/BOM events on init
*
* @private
*
* @returns {void}
*/
_bindEvents: function() {
if( this._isFullPage ) {
$(window).resize( this._resizeFunction );
}
$(window).on( 'unload beforeunload', this._unloadFunction );
},
/**
* Debounces resize events
*
* @private
*
* @returns {void}
*/
_onResize: function() {
clearTimeout( this._resizeTimeoutId );
this._resizeTimeoutId = setTimeout(lm.utils.fnBind( this.updateSize, this ), 100 );
},
/**
* Extends the default config with the user specific settings and applies
* derivations. Please note that there's a seperate method (AbstractContentItem._extendItemNode)
* that deals with the extension of item configs
*
* @param {Object} config
* @static
* @returns {Object} config
*/
_createConfig: function( config ) {
var windowConfigKey = lm.utils.getQueryStringParam( 'gl-window' );
if( windowConfigKey ) {
this.isSubWindow = true;
config = localStorage.getItem( windowConfigKey );
config = JSON.parse( config );
config = ( new lm.utils.ConfigMinifier() ).unminifyConfig( config );
localStorage.removeItem( windowConfigKey );
}
config = $.extend( true, {}, lm.config.defaultConfig, config );
var nextNode = function( node ) {
for( var key in node ) {
if( key !== 'props' && typeof node[ key ] === 'object' ) {
nextNode( node[ key ] );
}
else if( key === 'type' && node[ key ] === 'react-component' ) {
node.type = 'component';
node.componentName = 'lm-react-component';
}
}
}
nextNode( config );
if( config.settings.hasHeaders === false ) {
config.dimensions.headerHeight = 0;
}
return config;
},
/**
* This is executed when GoldenLayout detects that it is run
* within a previously opened popout window.
*
* @private
*
* @returns {void}
*/
_adjustToWindowMode: function() {
var popInButton = $( '<div class="lm_popin" title="' + this.config.labels.popin + '">' +
'<div class="lm_icon"></div>' +
'<div class="lm_bg"></div>' +
'</div>');
popInButton.click(lm.utils.fnBind(function(){
this.emit( 'popIn' );
}, this));
document.title = lm.utils.stripTags( this.config.content[ 0 ].title );
$( 'head' ).append( $( 'body link, body style, template, .gl_keep' ) );
this.container = $( 'body' )
.html( '' )
.css( 'visibility', 'visible' )
.append( popInButton );
/*
* This seems a bit pointless, but actually causes a reflow/re-evaluation getting around
* slickgrid's "Cannot find stylesheet." bug in chrome
*/
var x = document.body.offsetHeight; // jshint ignore:line
/*
* Expose this instance on the window object
* to allow the opening window to interact with
* it
*/
window.__glInstance = this;
},
/**
* Creates Subwindows (if there are any). Throws an error
* if popouts are blocked.
*
* @returns {void}
*/
_createSubWindows: function() {
var i, popout;
for( i = 0; i < this.config.openPopouts.length; i++ ) {
popout = this.config.openPopouts[ i ];
this.createPopout(
popout.content,
popout.dimensions,
popout.parentId,
popout.indexInParent
);
}
},
/**
* Determines what element the layout will be created in
*
* @private
*
* @returns {void}
*/
_setContainer: function() {
var container = $( this.container || 'body' );
if( container.length === 0 ) {
throw new Error( 'GoldenLayout container not found' );
}
if( container.length > 1 ) {
throw new Error( 'GoldenLayout more than one container element specified' );
}
if( container[ 0 ] === document.body ) {
this._isFullPage = true;
$( 'html, body' ).css({
height: '100%',
margin:0,
padding: 0,
overflow: 'hidden'
});
}
this.container = container;
},
/**
* Kicks of the initial, recursive creation chain
*
* @param {Object} config GoldenLayout Config
*
* @returns {void}
*/
_create: function( config ) {
var errorMsg;
if( !( config.content instanceof Array ) ) {
if( config.content === undefined ) {
errorMsg = 'Missing setting \'content\' on top level of configuration';
} else {
errorMsg = 'Configuration parameter \'content\' must be an array';
}
throw new lm.errors.ConfigurationError( errorMsg, config );
}
if( config.content.length > 1 ) {
errorMsg = 'Top level content can\'t contain more then one element.';
throw new lm.errors.ConfigurationError( errorMsg, config );
}
this.root = new lm.items.Root( this, { content: config.content }, this.container );
this.root.callDownwards( '_$init' );
if( config.maximisedItemId === '__glMaximised' ) {
this.root.getItemsById( config.maximisedItemId )[ 0 ].toggleMaximise();
}
},
/**
* Called when the window is closed or the user navigates away
* from the page
*
* @returns {void}
*/
_onUnload: function() {
if( this.config.settings.closePopoutsOnUnload === true ) {
for( var i = 0; i < this.openPopouts.length; i++ ) {
this.openPopouts[ i ].close();
}
}
}
});
/**
* Expose the Layoutmanager as the single entrypoint using UMD
*/
(function () {
/* global define */
if ( typeof define === 'function' && define.amd) {
define([ 'jquery' ], function( jquery ){ $ = jquery; return lm.LayoutManager; }); // jshint ignore:line
} else if (typeof exports === 'object') {
module.exports = lm.LayoutManager;
} else {
window.GoldenLayout = lm.LayoutManager;
}
})();
lm.config.itemDefaultConfig = {
isClosable: true,
reorderEnabled: true,
title: ''
};
lm.config.defaultConfig = {
openPopouts:[],
settings:{
hasHeaders: true,
constrainDragToContainer: true,
reorderEnabled: true,
selectionEnabled: false,
popoutWholeStack: false,
blockedPopoutsThrowError: true,
closePopoutsOnUnload: true,
showPopoutIcon: true,
showMaximiseIcon: true,
showCloseIcon: true
},
dimensions: {
borderWidth: 5,
minItemHeight: 10,
minItemWidth: 10,
headerHeight: 20,
dragProxyWidth: 300,
dragProxyHeight: 200
},
labels: {
close: 'close',
maximise: 'maximise',
minimise: 'minimise',
popout: 'open in new window',
popin: 'pop in'
}
};
lm.container.ItemContainer = function( config, parent, layoutManager ) {
lm.utils.EventEmitter.call( this );
this.width = null;
this.height = null;
this.title = config.componentName;
this.parent = parent;
this.layoutManager = layoutManager;
this.isHidden = false;
this._config = config;
this._element = $([
'<div class="lm_item_container">',
'<div class="lm_content"></div>',
'</div>'
].join( '' ));
this._contentElement = this._element.find( '.lm_content' );
};
lm.utils.copy( lm.container.ItemContainer.prototype, {
/**
* Get the inner DOM element the container's content
* is intended to live in
*
* @returns {DOM element}
*/
getElement: function() {
return this._contentElement;
},
/**
* Hide the container. Notifies the containers content first
* and then hides the DOM node. If the container is already hidden
* this should have no effect
*
* @returns {void}
*/
hide: function() {
this.emit( 'hide' );
this.isHidden = true;
this._element.hide();
},
/**
* Shows a previously hidden container. Notifies the
* containers content first and then shows the DOM element.
* If the container is already visible this has no effect.
*
* @returns {void}
*/
show: function() {
this.emit( 'show' );
this.isHidden = false;
this._element.show();
// call shown only if the container has a valid size
if(this.height != 0 || this.width != 0) {
this.emit( 'shown' );
}
},
/**
* Set the size from within the container. Traverses up
* the item tree until it finds a row or column element
* and resizes its items accordingly.
*
* If this container isn't a descendant of a row or column
* it returns false
* @todo Rework!!!
* @param {Number} width The new width in pixel
* @param {Number} height The new height in pixel
*
* @returns {Boolean} resizeSuccesful
*/
setSize: function( width, height ) {
var rowOrColumn = this.parent,
rowOrColumnChild = this,
totalPixel,
percentage,
direction,
newSize,
delta,
i;
while( !rowOrColumn.isColumn && !rowOrColumn.isRow ) {
rowOrColumnChild = rowOrColumn;
rowOrColumn = rowOrColumn.parent;
/**
* No row or column has been found
*/
if( rowOrColumn.isRoot ) {
return false;
}
}
direction = rowOrColumn.isColumn ? "height" : "width";
newSize = direction === "height" ? height : width;
totalPixel = this[direction] * ( 1 / ( rowOrColumnChild.config[direction] / 100 ) );
percentage = ( newSize / totalPixel ) * 100;
delta = ( rowOrColumnChild.config[direction] - percentage ) / rowOrColumn.contentItems.length;
for( i = 0; i < rowOrColumn.contentItems.length; i++ ) {
if( rowOrColumn.contentItems[ i ] === rowOrColumnChild ) {
rowOrColumn.contentItems[ i ].config[direction] = percentage;
} else {
rowOrColumn.contentItems[ i ].config[direction] += delta;
}
}
rowOrColumn.callDownwards( 'setSize' );
return true;
},
/**
* Closes the container if it is closable. Can be called by
* both the component within at as well as the contentItem containing
* it. Emits a close event before the container itself is closed.
*
* @returns {void}
*/
close: function() {
if( this._config.isClosable ) {
this.emit( 'close' );
this.parent.close();
}
},
/**
* Returns the current state object
*
* @returns {Object} state
*/
getState: function() {
return this._config.componentState;
},
/**
* Merges the provided state into the current one
*
* @param {Object} state
*
* @returns {void}
*/
extendState: function( state ) {
this.setState( $.extend( true, this.getState(), state ) );
},
/**
* Notifies the layout manager of a stateupdate
*
* @param {serialisable} state
*/
setState: function( state ) {
this._config.componentState = state;
this.parent.emitBubblingEvent( 'stateChanged' );
},
/**
* Set's the components title
*
* @param {String} title
*/
setTitle: function( title ) {
this.parent.setTitle( title );
},
/**
* Set's the containers size. Called by the container's component.
* To set the size programmatically from within the container please
* use the public setSize method
*
* @param {[Int]} width in px
* @param {[Int]} height in px
*
* @returns {void}
*/
_$setSize: function( width, height ) {
if( width !== this.width || height !== this.height ) {
this.width = width;
this.height = height;
this._contentElement.width( this.width ).height( this.height );
this.emit( 'resize' );
}
}
});
/**
* Pops a content item out into a new browser window.
* This is achieved by
*
* - Creating a new configuration with the content item as root element
* - Serializing and minifying the configuration
* - Opening the current window's URL with the configuration as a GET parameter
* - GoldenLayout when opened in the new window will look for the GET parameter
* and use it instead of the provided configuration
*
* @param {Object} config GoldenLayout item config
* @param {Object} dimensions A map with width, height, top and left
* @param {String} parentId The id of the element the item will be appended to on popIn
* @param {Number} indexInParent The position of this element within its parent
* @param {lm.LayoutManager} layoutManager
*/
lm.controls.BrowserPopout = function( config, dimensions, parentId, indexInParent, layoutManager ) {
lm.utils.EventEmitter.call( this );
this.isInitialised = false;
this._config = config;
this._dimensions = dimensions;
this._parentId = parentId;
this._indexInParent = indexInParent;
this._layoutManager = layoutManager;
this._popoutWindow = null;
this._id = null;
this._createWindow();
};
lm.utils.copy( lm.controls.BrowserPopout.prototype, {
toConfig: function() {
return {
dimensions:{
width: this.getGlInstance().width,
height: this.getGlInstance().height,
left: this._popoutWindow.screenX || this._popoutWindow.screenLeft,
top: this._popoutWindow.screenY || this._popoutWindow.screenTop
},
content: this.getGlInstance().toConfig().content,
parentId: this._parentId,
indexInParent: this._indexInParent
};
},
getGlInstance: function() {
return this._popoutWindow.__glInstance;
},
getWindow: function() {
return this._popoutWindow;
},
close: function() {
if( this.getGlInstance() ) {
this.getGlInstance()._$closeWindow();
} else {
try{
this.getWindow().close();
} catch( e ){}
}
},
/**
* Returns the popped out item to its original position. If the original
* parent isn't available anymore it falls back to the layout's topmost element
*/
popIn: function() {
var childConfig,
parentItem,
index = this._indexInParent;
if( this._parentId ) {
/*
* The $.extend call seems a bit pointless, but it's crucial to
* copy the config returned by this.getGlInstance().toConfig()
* onto a new object. Internet Explorer keeps the references
* to objects on the child window, resulting in the following error
* once the child window is closed:
*
* The callee (server [not server application]) is not available and disappeared
*/
childConfig = $.extend( true, {}, this.getGlInstance().toConfig() ).content[ 0 ];
parentItem = this._layoutManager.root.getItemsById( this._parentId )[ 0 ];
/*
* Fallback if parentItem is not available. Either add it to the topmost
* item or make it the topmost item if the layout is empty
*/
if( !parentItem ) {
if( this._layoutManager.root.contentItems.length > 0 ) {
parentItem = this._layoutManager.root.contentItems[ 0 ];
} else {
parentItem = this._layoutManager.root;
}
index = 0;
}
}
parentItem.addChild( childConfig, this._indexInParent );
this.close();
},
/**
* Creates the URL and window parameter
* and opens a new window
*
* @private
*
* @returns {void}
*/
_createWindow: function() {
var checkReadyInterval,
url = this._createUrl(),
/**
* Bogus title to prevent re-usage of existing window with the
* same title. The actual title will be set by the new window's
* GoldenLayout instance if it detects that it is in subWindowMode
*/
title = Math.floor( Math.random() * 1000000 ).toString( 36 ),
/**
* The options as used in the window.open string
*/
options = this._serializeWindowOptions({
width: this._dimensions.width,
height: this._dimensions.height,
innerWidth: this._dimensions.width,
innerHeight: this._dimensions.height,
menubar: 'no',
toolbar: 'no',
location: 'no',
personalbar: 'no',
resizable: 'yes',
scrollbars: 'no',
status: 'no'
});
this._popoutWindow = window.open( url, title, options );
if( !this._popoutWindow ) {
if( this._layoutManager.config.settings.blockedPopoutsThrowError === true ) {
var error = new Error( 'Popout blocked' );
error.type = 'popoutBlocked';
throw error;
} else {
return;
}
}
$( this._popoutWindow )
.on( 'load', lm.utils.fnBind( this._positionWindow, this ) )
.on( 'unload beforeunload', lm.utils.fnBind( this._onClose, this ) );
/**
* Polling the childwindow to find out if GoldenLayout has been initialised
* doesn't seem optimal, but the alternatives - adding a callback to the parent
* window or raising an event on the window object - both would introduce knowledge
* about the parent to the child window which we'd rather avoid
*/
checkReadyInterval = setInterval(lm.utils.fnBind(function(){
if( this._popoutWindow.__glInstance && this._popoutWindow.__glInstance.isInitialised ) {
this._onInitialised();
clearInterval( checkReadyInterval );
}
}, this ), 10 );
},
/**
* Serialises a map of key:values to a window options string
*
* @param {Object} windowOptions
*
* @returns {String} serialised window options
*/
_serializeWindowOptions: function( windowOptions ) {
var windowOptionsString = [], key;
for( key in windowOptions ) {
windowOptionsString.push( key + '=' + windowOptions[ key ] );
}
return windowOptionsString.join( ',' );
},
/**
* Creates the URL for the new window, including the
* config GET parameter
*
* @returns {String} URL
*/
_createUrl: function() {
var config = { content: this._config },
storageKey = 'gl-window-config-' + lm.utils.getUniqueId(),
urlParts;
config = ( new lm.utils.ConfigMinifier() ).minifyConfig( config );
try{
localStorage.setItem( storageKey, JSON.stringify( config ) );
} catch( e ) {
throw new Error( 'Error while writing to localStorage ' + e.toString() );
}
urlParts = document.location.href.split( '?' );
// URL doesn't contain GET-parameters
if( urlParts.length === 1 ) {
return urlParts[ 0 ] + '?gl-window=' + storageKey;
// URL contains GET-parameters
} else {
return document.location.href + '&gl-window=' + storageKey;
}
},
/**
* Move the newly created window roughly to
* where the component used to be.
*
* @private
*
* @returns {void}
*/
_positionWindow: function() {
this._popoutWindow.moveTo( this._dimensions.left, this._dimensions.top );
this._popoutWindow.focus();
},
/**
* Callback when the new window is opened and the GoldenLayout instance
* within it is initialised
*
* @returns {void}
*/
_onInitialised: function() {
this.isInitialised = true;
this.getGlInstance().on( 'popIn', this.popIn, this );
this.emit( 'initialised' );
},
/**
* Invoked 50ms after the window unload event
*
* @private
*
* @returns {void}
*/
_onClose: function() {
setTimeout( lm.utils.fnBind( this.emit, this, [ 'closed' ] ), 50 );
}
});
/**
* This class creates a temporary container
* for the component whilst it is being dragged
* and handles drag events
*
* @constructor
* @private
*
* @param {Number} x The initial x position
* @param {Number} y The initial y position
* @param {lm.utils.DragListener} dragListener
* @param {lm.LayoutManager} layoutManager
* @param {lm.item.AbstractContentItem} contentItem
* @param {lm.item.AbstractContentItem} originalParent
*/
lm.controls.DragProxy = function( x, y, dragListener, layoutManager, contentItem, originalParent ) {
lm.utils.EventEmitter.call( this );
this._dragListener = dragListener;
this._layoutManager = layoutManager;
this._contentItem = contentItem;
this._originalParent = originalParent;
this._area = null;
this._lastValidArea = null;
this._dragListener.on( 'drag', this._onDrag, this );
this._dragListener.on( 'dragStop', this._onDrop, this );
this.element = $( lm.controls.DragProxy._template );
this.element.css({ left: x, top: y });
this.element.find( '.lm_tab' ).attr( 'title', lm.utils.stripTags( this._contentItem.config.title ) );
this.element.find( '.lm_title' ).html( this._contentItem.config.title );
this.childElementContainer = this.element.find( '.lm_content' );
this.childElementContainer.append( contentItem.element );
this._updateTree();
this._layoutManager._$calculateItemAreas();
this._setDimensions();
$( document.body ).append( this.element );
var offset = this._layoutManager.container.offset();
this._minX = offset.left;
this._minY = offset.top;
this._maxX = this._layoutManager.container.width() + this._minX;
this._maxY = this._layoutManager.container.height() + this._minY;
this._width = this.element.width();
this._height = this.element.height();
this._setDropPosition( x, y );
};
lm.controls.DragProxy._template = '<div class="lm_dragProxy">' +
'<div class="lm_header">' +
'<ul class="lm_tabs">' +
'<li class="lm_tab lm_active"><i class="lm_left"></i>' +
'<span class="lm_title"></span>' +
'<i class="lm_right"></i></li>' +
'</ul>' +
'</div>' +
'<div class="lm_content"></div>' +
'</div>';
lm.utils.copy( lm.controls.DragProxy.prototype, {
/**
* Callback on every mouseMove event during a drag. Determines if the drag is
* still within the valid drag area and calls the layoutManager to highlight the
* current drop area
*
* @param {Number} offsetX The difference from the original x position in px
* @param {Number} offsetY The difference from the original y position in px
* @param {jQuery DOM event} event
*
* @private
*
* @returns {void}
*/
_onDrag: function( offsetX, offsetY, event ) {
var x = event.pageX,
y = event.pageY,
isWithinContainer = x > this._minX && x < this._maxX && y > this._minY && y < this._maxY;
// TODO
var isWithinConstrainedArea = true;
// console.log('_onDrag', this);
if (typeof this._contentItem.config.constrainDragArea !== 'undefined') {
// var filtered = this._layoutManager._itemAreas.filter(item => item.contentItem.config.id === this._contentItem.config.constrainDragArea);
// if (filtered.length > 0) {
// // var o = this._contentItem.layoutManager.getItemsById(this._contentItem.config.constrainDragArea)._$getArea();
// var o = filtered[0]._$getArea();
// isWithinConstrainedArea = x > o.x1 && x < o.x2 && y > o.y1 && y < o.y2;
// }
var o = this._contentItem.config.constrainDragArea._$getArea();
isWithinConstrainedArea = x > o.x1 && x < o.x2 && y > o.y1 && y < o.y2;
}
// console.log('_onDrag TODO', this._contentItem.config);
// _contentItem.config.constrainDragArea
if( !isWithinContainer && this._layoutManager.config.settings.constrainDragToContainer === true ) {
return;
}
if( !isWithinConstrainedArea ) {
return;
}
this._setDropPosition( x, y );
},
/**
* Sets the target position, highlighting the appropriate area
*
* @param {Number} x The x position in px
* @param {Number} y The y position in px
*
* @private
*
* @returns {void}
*/
_setDropPosition: function( x, y ) {
// console.log('this', this);
this.element.css({ left: x, top: y });
this._area = this._layoutManager._$getArea( x, y );
if( this._area !== null ) {
this._lastValidArea = this._area;
this._area.contentItem._$highlightDropZone( x, y, this._area );
}
},
/**
* Callback when the drag has finished. Determines the drop area
* and adds the child to it
*
* @private
*
* @returns {void}
*/
_onDrop: function() {
this._layoutManager.dropTargetIndicator.hide();
// console.log('this', this);
// _contentItem.config.constrainDragArea
/*
* Valid drop area found
*/
// TODO
if( /*false &&*/ this._area !== null ) {
this._area.contentItem._$onDrop( this._contentItem );
/**
* No valid drop area available at present, but one has been found before.
* Use it
*/
} else if( /*false &&*/ this._lastValidArea !== null ) {
this._lastValidArea.contentItem._$onDrop( this._contentItem );
/**
* No valid drop area found during the duration of the drag. Return
* content item to its original position if a original parent is provided.
* (Which is not the case if the drag had been initiated by createDragSource)
*/
} else if ( this._originalParent ){
this._originalParent.addChild( this._contentItem );
/**
* The drag didn't ultimately end up with adding the content item to
* any container. In order to ensure clean up happens, destroy the
* content item.
*/
} else {
this._contentItem._$destroy();
}
this.element.remove();
this._layoutManager.emit( 'itemDropped', this._contentItem );
},
/**
* Removes the item from its original position within the tree
*
* @private
*
* @returns {void}
*/
_updateTree: function() {
/**
* parent is null if the drag had been initiated by a external drag source
*/
if( this._contentItem.parent ) {
this._contentItem.parent.removeChild( this._contentItem, true );
}
this._contentItem._$setParent( this );
},
/**
* Updates the Drag Proxie's dimensions
*
* @private
*
* @returns {void}
*/
_setDimensions: function() {
var dimensions = this._layoutManager.config.dimensions,
width = dimensions.dragProxyWidth,
height = dimensions.dragProxyHeight - dimensions.headerHeight;
this.childElementContainer.width( width );
this.childElementContainer.height( height );
this._contentItem.element.width( width );
this._contentItem.element.height( height );
this._contentItem.callDownwards( '_$show' );
this._contentItem.callDownwards( 'setSize' );
}
});
/**
* Allows for any DOM item to create a component on drag
* start tobe dragged into the Layout
*
* @param {jQuery element} element
* @param {Object} itemConfig the configuration for the contentItem that will be created
* @param {LayoutManager} layoutManager
*
* @constructor
*/
lm.controls.DragSource = function( element, itemConfig, layoutManager ) {
this._element = element;
this._itemConfig = itemConfig;
this._layoutManager = layoutManager;
this._dragListener = null;
this._createDragListener();
};
lm.utils.copy( lm.controls.DragSource.prototype, {
/**
* Called initially and after every drag
*
* @returns {void}
*/
_createDragListener: function() {
if( this._dragListener !== null ) {
this._dragListener.destroy();
}
this._dragListener = new lm.utils.DragListener( this._element );
this._dragListener.on( 'dragStart', this._onDragStart, this );
this._dragListener.on( 'dragStop', this._createDragListener, this );
},
/**
* Callback for the DragListener's dragStart event
*
* @param {int} x the x position of the mouse on dragStart
* @param {int} y the x position of the mouse on dragStart
*
* @returns {void}
*/
_onDragStart: function( x, y ) {
var itemConfig = this._itemConfig;
if( lm.utils.isFunction( itemConfig ) ) {
itemConfig = itemConfig();
}
var contentItem = this._layoutManager._$normalizeContentItem( $.extend( true, {}, itemConfig ) ),
dragProxy = new lm.controls.DragProxy( x, y, this._dragListener, this._layoutManager, contentItem, null );
this._layoutManager.transitionIndicator.transitionElements( this._element, dragProxy.element );
}
});
lm.controls.DropTargetIndicator = function() {
this.element = $( lm.controls.DropTargetIndicator._template );
$(document.body).append( this.element );
};
lm.controls.DropTargetIndicator._template = '<div class="lm_dropTargetIndicator"><div class="lm_inner"></div></div>';
lm.utils.copy( lm.controls.DropTargetIndicator.prototype, {
destroy: function() {
this.element.remove();
},
highlight: function( x1, y1, x2, y2 ) {
this.highlightArea({ x1:x1, y1:y1, x2:x2, y2:y2 });
},
highlightArea: function( area ) {
this.element.css({
left: area.x1,
top: area.y1,
width: area.x2 - area.x1,
height: area.y2 - area.y1
}).show();
},
hide: function() {
this.element.hide();
}
});
/**
* This class represents a header above a Stack ContentItem.
*
* @param {lm.LayoutManager} layoutManager
* @param {lm.item.AbstractContentItem} parent
*/
lm.controls.Header = function( layoutManager, parent, config ) {
// console.log('itemDefaultConfig', lm.config.itemDefaultConfig);
lm.utils.EventEmitter.call( this );
this.layoutManager = layoutManager;
this.element = $( lm.controls.Header._template );
// if( this.layoutManager.config.settings.selectionEnabled === true ) {
if( config.selectionEnabled === true ||
!(this.layoutManager.config.settings.selectionEnabled === false) &&
!(config.selectionEnabled === false)
) {
this.element.addClass( 'lm_selectable' );
this.element.click( lm.utils.fnBind( this._onHeaderClick, this ) );
}
this.element.height( layoutManager.config.dimensions.headerHeight );
this.tabsContainer = this.element.find( '.lm_tabs' );
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._createControls();
};
lm.controls.Header._template = [
'<div class="lm_header">',
'<ul class="lm_tabs"></ul>',
'<ul class="lm_controls"></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, isActive;
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;
}
}
this._updateTabSizes();
this.parent.emitBubblingEvent( 'stateChanged' );
},
/**
* 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' );
for( var i = 0; i < this.tabs.length; i++ ) {
this.tabs[ i ]._$destroy();
}
this.element.remove();
},
/**
* 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;
/**
* Popout control to launch component in new window.
*/
if( this.layoutManager.config.settings.showPopoutIcon ) {
popout = lm.utils.fnBind( this._onPopoutClick, this );
label = this.layoutManager.config.labels.popout;
new lm.controls.HeaderButton( this, label, 'lm_popout', popout );
}
/**
* Maximise control - set the component to the full size of the layout
*/
if( this.layoutManager.config.settings.showMaximiseIcon ) {
maximise = lm.utils.fnBind( this.parent.toggleMaximise, this.parent );
maximiseLabel = this.layoutManager.config.labels.maximise;
minimiseLabel = this.layoutManager.config.labels.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.layoutManager.config.labels.close;
this.closeButton = new lm.controls.HeaderButton( this, label, 'lm_close', closeStack );
}
},
/**
* 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();
}
},
/**
* Shrinks the tabs if the available space is not sufficient
*
* @returns {void}
*/
_updateTabSizes: function() {
if( this.tabs.length === 0 ) {
return;
}
var availableWidth = this.element.outerWidth() - this.controlsContainer.outerWidth(),
totalTabWidth = 0,
tabElement,
i,
marginLeft,
gap;
for( i = 0; i < this.tabs.length; i++ ) {
tabElement = this.tabs[ i ].element;
/*
* In order to show every tab's close icon, decrement the z-index from left to right
*/
tabElement.css( 'z-index', this.tabs.length - i );
totalTabWidth += tabElement.outerWidth() + parseInt( tabElement.css( 'margin-right' ), 10 );
}
gap = ( totalTabWidth - availableWidth ) / ( this.tabs.length - 1 );
for( i = 0; i < this.tabs.length; i++ ) {
/*
* The active tab keeps it's original width
*/
if( !this.tabs[ i ].isActive && gap > 0 ) {
marginLeft = '-' + Math.floor( gap )+ 'px';
} else {
marginLeft = '';
}
this.tabs[ i ].element.css( 'margin-left', marginLeft );
}
if( availableWidth < totalTabWidth ) {
this.element.css( 'overflow', 'hidden' );
} else {
this.element.css( 'overflow', 'visible' );
}
}
});
lm.controls.HeaderButton = function( header, label, cssClass, action ) {
this._header = header;
this.element = $( '<li class="' + cssClass + '" title="' + label + '"></li>' );
this._header.on( 'destroy', this._$destroy, this );
this._action = action;
this.element.click( this._action );
this._header.controlsContainer.append( this.element );
};
lm.utils.copy( lm.controls.HeaderButton.prototype, {
_$destroy: function() {
this.element.off();
this.element.remove();
}
});
lm.controls.Splitter = function( isVertical, size ) {
this._isVertical = isVertical;
this._size = size;
this.element = this._createElement();
this._dragListener = new lm.utils.DragListener( this.element );
};
lm.utils.copy( lm.controls.Splitter.prototype, {
on: function( event, callback, context ) {
this._dragListener.on( event, callback, context );
},
_$destroy: function() {
this.element.remove();
},
_createElement: function() {
var element = $( '<div class="lm_splitter"><div class="lm_drag_handle"></div></div>' );
element.addClass( 'lm_' + ( this._isVertical ? 'vertical' : 'horizontal' ) );
element[ this._isVertical ? 'height' : 'width' ]( this._size );
return element;
}
});
/**
* Represents an individual tab within a Stack's header
*
* @param {lm.controls.Header} header
* @param {lm.items.AbstractContentItem} contentItem
*
* @constructor
*/
lm.controls.Tab = function( header, contentItem ) {
this.header = header;
this.contentItem = contentItem;
this.element = $( lm.controls.Tab._template );
this.titleElement = this.element.find( '.lm_title' );
this.closeElement = this.element.find( '.lm_close_tab' );
this.closeElement[ contentItem.config.isClosable ? 'show' : 'hide' ]();
this.isActive = false;
this.setTitle( contentItem.config.title );
this.contentItem.on( 'titleChanged', this.setTitle, this );
this._layoutManager = this.contentItem.layoutManager;
if(
// this._layoutManager.config.settings.reorderEnabled === true &&
// contentItem.config.reorderEnabled === true
contentItem.config.reorderEnabled === true ||
!(this._layoutManager.config.settings.reorderEnabled === false) &&
!(contentItem.config.reorderEnabled === false)
) {
this._dragListener = new lm.utils.DragListener( this.element );
this._dragListener.on( 'dragStart', this._onDragStart, this );
}
this._onTabClickFn = lm.utils.fnBind( this._onTabClick, this );
this._onCloseClickFn = lm.utils.fnBind( this._onCloseClick, this );
this.element.click( this._onTabClickFn );
if( this.contentItem.config.isClosable ) {
this.closeElement.click( this._onCloseClickFn );
} else {
this.closeElement.remove();
}
this.contentItem.tab = this;
this.contentItem.emit( 'tab', this );
this.contentItem.layoutManager.emit( 'tabCreated', this );
if( this.contentItem.isComponent ) {
this.contentItem.container.tab = this;
this.contentItem.container.emit( 'tab', this );
}
};
/**
* The tab's html template
*
* @type {String}
*/
lm.controls.Tab._template = '<li class="lm_tab"><i class="lm_left"></i>' +
'<span class="lm_title"></span><div class="lm_close_tab"></div>' +
'<i class="lm_right"></i></li>';
lm.utils.copy( lm.controls.Tab.prototype,{
/**
* Sets the tab's title to the provided string and sets
* its title attribute to a pure text representation (without
* html tags) of the same string.
*
* @public
* @param {String} title can contain html
*/
setTitle: function( title ) {
this.element.attr( 'title', lm.utils.stripTags( title ) );
this.titleElement.html( title );
},
/**
* Sets this tab's active state. To programmatically
* switch tabs, use header.setActiveContentItem( item ) instead.
*
* @public
* @param {Boolean} isActive
*/
setActive: function( isActive ) {
if( isActive === this.isActive ) {
return;
}
this.isActive = isActive;
if( isActive ) {
this.element.addClass( 'lm_active' );
} else {
this.element.removeClass( 'lm_active');
}
},
/**
* Destroys the tab
*
* @private
* @returns {void}
*/
_$destroy: function() {
this.element.off( 'click', this._onTabClickFn );
this.closeElement.off( 'click', this._onCloseClickFn );
if( this._dragListener ) {
this._dragListener.off( 'dragStart', this._onDragStart );
this._dragListener = null;
}
this.element.remove();
},
/**
* Callback for the DragListener
*
* @param {Number} x The tabs absolute x position
* @param {Number} y The tabs absolute y position
*
* @private
* @returns {void}
*/
_onDragStart: function( x, y ) {
if( this.contentItem.parent.isMaximised === true ) {
this.contentItem.parent.toggleMaximise();
}
new lm.controls.DragProxy(
x,
y,
this._dragListener,
this._layoutManager,
this.contentItem,
this.header.parent
);
},
/**
* Callback when the tab is clicked
*
* @param {jQuery DOM event} event
*
* @private
* @returns {void}
*/
_onTabClick: function( event ) {
// left mouse button
if( event.button === 0 ) {
var activeContentItem = this.header.parent.getActiveContentItem();
if (this.contentItem !== activeContentItem) {
this.header.parent.setActiveContentItem( this.contentItem );
}
// middle mouse button
} else if( event.button === 1 && this.contentItem.config.isClosable ) {
this._onCloseClick( event );
}
},
/**
* Callback when the tab's close button is
* clicked
*
* @param {jQuery DOM event} event
*
* @private
* @returns {void}
*/
_onCloseClick: function( event ) {
event.stopPropagation();
this.header.parent.removeChild( this.contentItem );
}
});
lm.controls.TransitionIndicator = function() {
this._element = $( '<div class="lm_transition_indicator"></div>' );
$( document.body ).append( this._element );
this._toElement = null;
this._fromDimensions = null;
this._totalAnimationDuration = 200;
this._animationStartTime = null;
};
lm.utils.copy( lm.controls.TransitionIndicator.prototype, {
destroy: function() {
this._element.remove();
},
transitionElements: function( fromElement, toElement ) {
/**
* TODO - This is not quite as cool as expected. Review.
*/
return;
this._toElement = toElement;
this._animationStartTime = lm.utils.now();
this._fromDimensions = this._measure( fromElement );
this._fromDimensions.opacity = 0.8;
this._element.show().css( this._fromDimensions );
lm.utils.animFrame( lm.utils.fnBind( this._nextAnimationFrame, this ) );
},
_nextAnimationFrame: function() {
var toDimensions = this._measure( this._toElement ),
animationProgress = ( lm.utils.now() - this._animationStartTime ) / this._totalAnimationDuration,
currentFrameStyles = {},
cssProperty;
if( animationProgress >= 1 ) {
this._element.hide();
return;
}
toDimensions.opacity = 0;
for( cssProperty in this._fromDimensions ) {
currentFrameStyles[ cssProperty ] = this._fromDimensions[ cssProperty ] +
( toDimensions[ cssProperty] - this._fromDimensions[ cssProperty ] ) *
animationProgress;
}
this._element.css( currentFrameStyles );
lm.utils.animFrame( lm.utils.fnBind( this._nextAnimationFrame, this ) );
},
_measure: function( element ) {
var offset = element.offset();
return {
left: offset.left,
top: offset.top,
width: element.outerWidth(),
height: element.outerHeight()
};
}
});
lm.errors.ConfigurationError = function( message, node ) {
Error.call( this );
this.name = 'Configuration Error';
this.message = message;
this.node = node;
};
lm.errors.ConfigurationError.prototype = new Error();
/**
* This is the baseclass that all content items inherit from.
* Most methods provide a subset of what the sub-classes do.
*
* It also provides a number of functions for tree traversal
*
* @param {lm.LayoutManager} layoutManager
* @param {item node configuration} config
* @param {lm.item} parent
*
* @event stateChanged
* @event beforeItemDestroyed
* @event itemDestroyed
* @event itemCreated
* @event componentCreated
* @event rowCreated
* @event columnCreated
* @event stackCreated
*
* @constructor
*/
lm.items.AbstractContentItem = function( layoutManager, config, parent ) {
lm.utils.EventEmitter.call( this );
this.config = this._extendItemNode( config );
this.type = config.type;
this.contentItems = [];
this.parent = parent;
this.isInitialised = false;
this.isMaximised = false;
this.isRoot = false;
this.isRow = false;
this.isColumn = false;
this.isStack = false;
this.isComponent = false;
this.layoutManager = layoutManager;
this._pendingEventPropagations = {};
this._throttledEvents = [ 'stateChanged' ];
this.on( lm.utils.EventEmitter.ALL_EVENT, this._propagateEvent, this );
if( config.content ) {
this._createContentItems( config );
}
};
lm.utils.copy( lm.items.AbstractContentItem.prototype, {
/**
* Set the size of the component and its children, called recursively
*
* @abstract
* @returns void
*/
setSize: function() {
throw new Error( 'Abstract Method' );
},
/**
* Calls a method recursively downwards on the tree
*
* @param {String} functionName the name of the function to be called
* @param {[Array]}functionArguments optional arguments that are passed to every function
* @param {[bool]} bottomUp Call methods from bottom to top, defaults to false
* @param {[bool]} skipSelf Don't invoke the method on the class that calls it, defaults to false
*
* @returns {void}
*/
callDownwards: function( functionName, functionArguments, bottomUp, skipSelf ) {
var i;
if( bottomUp !== true && skipSelf !== true ) {
this[ functionName ].apply( this, functionArguments || [] );
}
for( i = 0; i < this.contentItems.length; i++ ) {
this.contentItems[ i ].callDownwards( functionName, functionArguments, bottomUp );
}
if( bottomUp === true && skipSelf !== true ) {
this[ functionName ].apply( this, functionArguments || [] );
}
},
/**
* Removes a child node (and its children) from the tree
*
* @param {lm.items.ContentItem} contentItem
*
* @returns {void}
*/
removeChild: function( contentItem, keepChild ) {
/*
* Get the position of the item that's to be removed within all content items this node contains
*/
var index = lm.utils.indexOf( contentItem, this.contentItems );
/*
* Make sure the content item to be removed is actually a child of this item
*/
if( index === -1 ) {
throw new Error( 'Can\'t remove child item. Unknown content item' );
}
/**
* Call ._$destroy on the content item. This also calls ._$destroy on all its children
*/
if( keepChild !== true ) {
this.contentItems[ index ]._$destroy();
}
/**
* Remove the content item from this nodes array of children
*/
this.contentItems.splice( index, 1 );
/**
* Remove the item from the configuration
*/
this.config.content.splice( index, 1 );
/**
* If this node still contains other content items, adjust their size
*/
if( this.contentItems.length > 0 ) {
this.callDownwards( 'setSize' );
/**
* If this was the last content item, remove this node as well
*/
} else if( !(this instanceof lm.items.Root) && this.config.isClosable === true ) {
this.parent.removeChild( this );
}
},
/**
* Sets up the tree structure for the newly added child
* The responsibility for the actual DOM manipulations lies
* with the concrete item
*
* @param {lm.items.AbstractContentItem} contentItem
* @param {[Int]} index If omitted item will be appended
*/
addChild: function( contentItem, index ) {
if ( index === undefined ) {
index = this.contentItems.length;
}
this.contentItems.splice( index, 0, contentItem );
if( this.config.content === undefined ) {
this.config.content = [];
}
this.config.content.splice( index, 0, contentItem.config );
contentItem.parent = this;
if( contentItem.parent.isInitialised === true && contentItem.isInitialised === false ) {
contentItem._$init();
}
},
/**
* Replaces oldChild with newChild. This used to use jQuery.replaceWith... which for
* some reason removes all event listeners, so isn't really an option.
*
* @param {lm.item.AbstractContentItem} oldChild
* @param {lm.item.AbstractContentItem} newChild
*
* @returns {void}
*/
replaceChild: function( oldChild, newChild, _$destroyOldChild ) {
newChild = this.layoutManager._$normalizeContentItem( newChild );
var index = lm.utils.indexOf( oldChild, this.contentItems ),
parentNode = oldChild.element[ 0 ].parentNode;
if( index === -1 ) {
throw new Error( 'Can\'t replace child. oldChild is not child of this' );
}
parentNode.replaceChild( newChild.element[ 0 ], oldChild.element[ 0 ] );
/*
* Optionally destroy the old content item
*/
if( _$destroyOldChild === true ) {
oldChild.parent = null;
oldChild._$destroy();
}
/*
* Wire the new contentItem into the tree
*/
this.contentItems[ index ] = newChild;
newChild.parent = this;
/*
* Update tab reference
*/
if ( this.isStack ) {
this.header.tabs[ index ].contentItem = newChild;
}
//TODO This doesn't update the config... refactor to leave item nodes untouched after creation
if( newChild.parent.isInitialised === true && newChild.isInitialised === false ) {
newChild._$init();
}
this.callDownwards( 'setSize' );
},
/**
* Convenience method.
* Shorthand for this.parent.removeChild( this )
*
* @returns {void}
*/
remove: function() {
this.parent.removeChild( this );
},
/**
* Removes the component from the layout and creates a new
* browser window with the component and its children inside
*
* @returns {lm.controls.BrowserPopout}
*/
popout: function() {
var browserPopout = this.layoutManager.createPopout( this );
this.emitBubblingEvent( 'stateChanged' );
return browserPopout;
},
/**
* Maximises the Item or minimises it if it is already maximised
*
* @returns {void}
*/
toggleMaximise: function() {
if( this.isMaximised === true ) {
this.layoutManager._$minimiseItem( this );
} else {
this.layoutManager._$maximiseItem( this );
}
this.isMaximised = !this.isMaximised;
this.emitBubblingEvent( 'stateChanged' );
},
/**
* Selects the item if it is not already selected
*
* @returns {void}
*/
select: function() {
if( this.layoutManager.selectedItem !== this ) {
this.layoutManager.selectItem( this, true );
this.element.addClass( 'lm_selected' );
}
},
/**
* De-selects the item if it is selected
*
* @returns {void}
*/
deselect: function() {
if( this.layoutManager.selectedItem === this ) {
this.layoutManager.selectedItem = null;
this.element.removeClass( 'lm_selected' );
}
},
/**
* Set this component's title
*
* @public
* @param {String} title
*
* @returns {void}
*/
setTitle: function( title ) {
this.config.title = title;
this.emit( 'titleChanged', title );
this.emit( 'stateChanged' );
},
/**
* Checks whether a provided id is present
*
* @public
* @param {String} id
*
* @returns {Boolean} isPresent
*/
hasId: function( id ) {
if( !this.config.id ) {
return false;
} else if( typeof this.config.id === 'string' ) {
return this.config.id === id;
} else if( this.config.id instanceof Array ) {
return lm.utils.indexOf( id, this.config.id ) !== -1;
}
},
/**
* Adds an id. Adds it as a string if the component doesn't
* have an id yet or creates/uses an array
*
* @public
* @param {String} id
*
* @returns {void}
*/
addId: function( id ) {
if( this.hasId( id ) ) {
return;
}
if( !this.config.id ) {
this.config.id = id;
} else if( typeof this.config.id === 'string' ) {
this.config.id = [ this.config.id, id ];
} else if( this.config.id instanceof Array ) {
this.config.id.push( id );
}
},
/**
* Removes an existing id. Throws an error
* if the id is not present
*
* @public
* @param {String} id
*
* @returns {void}
*/
removeId: function( id ) {
if( !this.hasId( id ) ) {
throw new Error( 'Id not found' );
}
if( typeof this.config.id === 'string' ) {
delete this.config.id;
} else if( this.config.id instanceof Array ) {
var index = lm.utils.indexOf( id, this.config.id );
this.config.id.splice( index, 1 );
}
},
/****************************************
* SELECTOR
****************************************/
getItemsByFilter: function( filter ) {
var result = [],
next = function( contentItem ) {
for( var i = 0; i < contentItem.contentItems.length; i++ ) {
if( filter( contentItem.contentItems[ i ] ) === true ) {
result.push( contentItem.contentItems[ i ] );
}
next( contentItem.contentItems[ i ] );
}
};
next( this );
return result;
},
getItemsById: function( id ) {
return this.getItemsByFilter( function( item ){
if( item.config.id instanceof Array ) {
return lm.utils.indexOf( id, item.config.id ) !== -1;
} else {
return item.config.id === id;
}
});
},
getItemsByType: function( type ) {
return this._$getItemsByProperty( 'type', type );
},
getComponentsByName: function( componentName ) {
var components = this._$getItemsByProperty( 'componentName', componentName ),
instances = [],
i;
for( i = 0; i < components.length; i++ ) {
instances.push( components[ i ].instance );
}
return instances;
},
/****************************************
* PACKAGE PRIVATE
****************************************/
_$getItemsByProperty: function( key, value ) {
return this.getItemsByFilter( function( item ){
return item[ key ] === value;
});
},
_$setParent: function( parent ) {
this.parent = parent;
},
_$highlightDropZone: function( x, y, area ) {
this.layoutManager.dropTargetIndicator.highlightArea( area );
},
_$onDrop: function( contentItem ) {
this.addChild( contentItem );
},
_$hide: function() {
this._callOnActiveComponents( 'hide' );
this.element.hide();
this.layoutManager.updateSize();
},
_$show: function() {
this._callOnActiveComponents( 'show' );
this.element.show();
this.layoutManager.updateSize();
this._callOnActiveComponents( 'shown' );
},
_callOnActiveComponents: function( methodName ) {
var stacks = this.getItemsByType( 'stack' ),
activeContentItem,
i;
for( i = 0; i < stacks.length; i++ ) {
activeContentItem = stacks[ i ].getActiveContentItem();
if( activeContentItem && activeContentItem.isComponent ) {
activeContentItem.container[ methodName ]();
}
}
},
/**
* Destroys this item ands its children
*
* @returns {void}
*/
_$destroy: function() {
this.emitBubblingEvent( 'beforeItemDestroyed' );
this.callDownwards( '_$destroy', [], true, true );
this.element.remove();
this.emitBubblingEvent( 'itemDestroyed' );
},
/**
* Returns the area the component currently occupies in the format
*
* {
* x1: int
* xy: int
* y1: int
* y2: int
* contentItem: contentItem
* }
*/
_$getArea: function( element ) {
element = element || this.element;
var offset = element.offset(),
width = element.width(),
height = element.height();
return {
x1: offset.left,
y1: offset.top,
x2: offset.left + width,
y2: offset.top + height,
surface: width * height,
contentItem: this
};
},
/**
* The tree of content items is created in two steps: First all content items are instantiated,
* then init is called recursively from top to bottem. This is the basic init function,
* it can be used, extended or overwritten by the content items
*
* Its behaviour depends on the content item
*
* @package private
*
* @returns {void}
*/
_$init: function() {
var i;
this.setSize();
for( i = 0; i < this.contentItems.length; i++ ) {
this.childElementContainer.append( this.contentItems[ i ].element );
}
this.isInitialised = true;
this.emitBubblingEvent( 'itemCreated' );
this.emitBubblingEvent( this.type + 'Created' );
},
/**
* Emit an event that bubbles up the item tree.
*
* @param {String} name The name of the event
*
* @returns {void}
*/
emitBubblingEvent: function( name ) {
var event = new lm.utils.BubblingEvent( name, this );
this.emit( name, event );
},
/**
* Private method, creates all content items for this node at initialisation time
* PLEASE NOTE, please see addChild for adding contentItems add runtime
* @private
* @param {configuration item node} config
*
* @returns {void}
*/
_createContentItems: function( config ) {
var oContentItem, i;
if( !( config.content instanceof Array ) ) {
throw new lm.errors.ConfigurationError( 'content must be an Array', config );
}
for( i = 0; i < config.content.length; i++ ) {
oContentItem = this.layoutManager.createContentItem( config.content[ i ], this );
this.contentItems.push( oContentItem );
}
},
/**
* Extends an item configuration node with default settings
* @private
* @param {configuration item node} config
*
* @returns {configuration item node} extended config
*/
_extendItemNode: function( config ) {
for( var key in lm.config.itemDefaultConfig ) {
if( config[ key ] === undefined ) {
config[ key ] = lm.config.itemDefaultConfig[ key ];
}
}
return config;
},
/**
* Called for every event on the item tree. Decides whether the event is a bubbling
* event and propagates it to its parent
*
* @param {String} name the name of the event
* @param {lm.utils.BubblingEvent} event
*
* @returns {void}
*/
_propagateEvent: function( name, event ) {
if( event instanceof lm.utils.BubblingEvent &&
event.isPropagationStopped === false &&
this.isInitialised === true ) {
/**
* In some cases (e.g. if an element is created from a DragSource) it
* doesn't have a parent and is not below root. If that's the case
* propagate the bubbling event from the top level of the substree directly
* to the layoutManager
*/
if( this.isRoot === false && this.parent ) {
this.parent.emit.apply( this.parent, Array.prototype.slice.call( arguments, 0 ) );
} else {
this._scheduleEventPropagationToLayoutManager( name, event );
}
}
},
/**
* All raw events bubble up to the root element. Some events that
* are propagated to - and emitted by - the layoutManager however are
* only string-based, batched and sanitized to make them more usable
*
* @param {String} name the name of the event
*
* @private
* @returns {void}
*/
_scheduleEventPropagationToLayoutManager: function( name, event ) {
if( lm.utils.indexOf( name, this._throttledEvents ) === -1 ) {
this.layoutManager.emit( name, event.origin );
} else {
if( this._pendingEventPropagations[ name ] !== true ) {
this._pendingEventPropagations[ name ] = true;
lm.utils.animFrame( lm.utils.fnBind( this._propagateEventToLayoutManager, this, [ name, event ] ) );
}
}
},
/**
* Callback for events scheduled by _scheduleEventPropagationToLayoutManager
*
* @param {String} name the name of the event
*
* @private
* @returns {void}
*/
_propagateEventToLayoutManager: function( name, event ) {
this._pendingEventPropagations[ name ] = false;
this.layoutManager.emit( name, event );
}
});
/**
* @param {[type]} layoutManager [description]
* @param {[type]} config [description]
* @param {[type]} parent [description]
*/
lm.items.Component = function( layoutManager, config, parent ) {
lm.items.AbstractContentItem.call( this, layoutManager, config, parent );
var ComponentConstructor = layoutManager.getComponent( this.config.componentName ),
componentConfig = $.extend( true, {}, this.config.componentState || {} );
componentConfig.componentName = this.config.componentName;
this.componentName = this.config.componentName;
if( this.config.title === '' ) {
this.config.title = this.config.componentName;
}
this.isComponent = true;
this.container = new lm.container.ItemContainer( this.config, this, layoutManager );
this.instance = new ComponentConstructor( this.container, componentConfig );
this.element = this.container._element;
};
lm.utils.extend( lm.items.Component, lm.items.AbstractContentItem );
lm.utils.copy( lm.items.Component.prototype, {
close: function() {
this.parent.removeChild( this );
},
setSize: function() {
this.container._$setSize( this.element.width(), this.element.height() );
},
_$init: function() {
lm.items.AbstractContentItem.prototype._$init.call( this );
this.container.emit( 'open' );
},
_$hide: function() {
this.container.hide();
lm.items.AbstractContentItem.prototype._$hide.call( this );
},
_$show: function() {
this.container.show();
lm.items.AbstractContentItem.prototype._$show.call( this );
},
_$shown: function() {
this.container.shown();
lm.items.AbstractContentItem.prototype._$shown.call( this );
},
_$destroy: function() {
this.container.emit( 'destroy' );
lm.items.AbstractContentItem.prototype._$destroy.call( this );
},
/**
* Dragging onto a component directly is not an option
*
* @returns null
*/
_$getArea: function() {
return null;
}
});
lm.items.Root = function( layoutManager, config, containerElement ) {
lm.items.AbstractContentItem.call( this, layoutManager, config, null );
this.isRoot = true;
this.type = 'root';
this.element = $( '<div class="lm_goldenlayout lm_item lm_root"></div>' );
this.childElementContainer = this.element;
this._containerElement = containerElement;
this._containerElement.append( this.element );
};
lm.utils.extend( lm.items.Root, lm.items.AbstractContentItem );
lm.utils.copy( lm.items.Root.prototype, {
addChild: function( contentItem ) {
if( this.contentItems.length > 0 ) {
throw new Error( 'Root node can only have a single child' );
}
contentItem = this.layoutManager._$normalizeContentItem( contentItem, this );
this.childElementContainer.append( contentItem.element );
lm.items.AbstractContentItem.prototype.addChild.call( this, contentItem );
this.callDownwards( 'setSize' );
this.emitBubblingEvent( 'stateChanged' );
},
setSize: function() {
var width = this._containerElement.width(),
height = this._containerElement.height();
this.element.width( width );
this.element.height( height );
/*
* Root can be empty
*/
if( this.contentItems[ 0 ] ) {
this.contentItems[ 0 ].element.width( width );
this.contentItems[ 0 ].element.height( height );
}
},
_$onDrop: function( contentItem ) {
var stack;
if( contentItem.isComponent === true ) {
stack = this.layoutManager.createContentItem( {type: 'stack' }, this );
stack.addChild( contentItem );
this.addChild( stack );
} else {
this.addChild( contentItem );
}
}
});
lm.items.RowOrColumn = function( isColumn, layoutManager, config, parent ) {
lm.items.AbstractContentItem.call( this, layoutManager, config, parent );
this.isRow = !isColumn;
this.isColumn = isColumn;
this.element = $( '<div class="lm_item lm_' + ( isColumn ? 'column' : 'row' ) + '"></div>' );
this.childElementContainer = this.element;
this._splitterSize = layoutManager.config.dimensions.borderWidth;
this._isColumn = isColumn;
this._dimension = isColumn ? 'height' : 'width';
this._splitter = [];
this._splitterPosition = null;
this._splitterMinPosition = null;
this._splitterMaxPosition = null;
};
lm.utils.extend( lm.items.RowOrColumn, lm.items.AbstractContentItem );
lm.utils.copy( lm.items.RowOrColumn.prototype, {
/**
* Add a new contentItem to the Row or Column
*
* @param {lm.item.AbstractContentItem} contentItem
* @param {[int]} index The position of the new item within the Row or Column.
* If no index is provided the item will be added to the end
* @param {[bool]} _$suspendResize If true the items won't be resized. This will leave the item in
* an inconsistent state and is only intended to be used if multiple
* children need to be added in one go and resize is called afterwards
*
* @returns {void}
*/
addChild: function( contentItem, index, _$suspendResize ) {
var newItemSize, itemSize, i, splitterElement;
contentItem = this.layoutManager._$normalizeContentItem( contentItem, this );
if( index === undefined ) {
index = this.contentItems.length;
}
if( this.contentItems.length > 0 ) {
splitterElement = this._createSplitter( Math.max( 0, index - 1 ) ).element;
if( index > 0 ) {
this.contentItems[ index - 1 ].element.after( splitterElement );
splitterElement.after( contentItem.element );
} else {
this.contentItems[ 0 ].element.before( splitterElement );
splitterElement.before( contentItem.element );
}
} else {
this.childElementContainer.append( contentItem.element );
}
lm.items.AbstractContentItem.prototype.addChild.call( this, contentItem, index );
newItemSize = ( 1 / this.contentItems.length ) * 100;
if( _$suspendResize === true ) {
this.emitBubblingEvent( 'stateChanged' );
return;
}
for( i = 0; i < this.contentItems.length; i++ ) {
if( this.contentItems[ i ] === contentItem ) {
contentItem.config[ this._dimension ] = newItemSize;
} else {
itemSize = this.contentItems[ i ].config[ this._dimension ] *= ( 100 - newItemSize ) / 100;
this.contentItems[ i ].config[ this._dimension ] = itemSize;
}
}
this.callDownwards( 'setSize' );
this.emitBubblingEvent( 'stateChanged' );
},
/**
* Removes a child of this element
*
* @param {lm.items.AbstractContentItem} contentItem
* @param {boolean} keepChild If true the child will be removed, but not destroyed
*
* @returns {void}
*/
removeChild: function( contentItem, keepChild ) {
var removedItemSize = contentItem.config[ this._dimension ],
index = lm.utils.indexOf( contentItem, this.contentItems ),
splitterIndex = Math.max( index - 1, 0 ),
i,
childItem;
if( index === -1 ) {
throw new Error( 'Can\'t remove child. ContentItem is not child of this Row or Column' );
}
/**
* Remove the splitter before the item or after if the item happens
* to be the first in the row/column
*/
if( this._splitter[ splitterIndex ] ) {
this._splitter[ splitterIndex ]._$destroy();
this._splitter.splice( splitterIndex, 1 );
}
/**
* Allocate the space that the removed item occupied to the remaining items
*/
for( i = 0; i < this.contentItems.length; i++ ) {
if( this.contentItems[ i ] !== contentItem ) {
this.contentItems[ i ].config[ this._dimension ] += removedItemSize / ( this.contentItems.length - 1 );
}
}
lm.items.AbstractContentItem.prototype.removeChild.call( this, contentItem, keepChild );
if( this.contentItems.length === 1 && this.config.isClosable === true ) {
childItem = this.contentItems[ 0 ];
this.contentItems = [];
this.parent.replaceChild( this, childItem, true );
} else {
this.callDownwards( 'setSize' );
this.emitBubblingEvent( 'stateChanged' );
}
},
/**
* Replaces a child of this Row or Column with another contentItem
*
* @param {lm.items.AbstractContentItem} oldChild
* @param {lm.items.AbstractContentItem} newChild
*
* @returns {void}
*/
replaceChild: function( oldChild, newChild ) {
var size = oldChild.config[ this._dimension ];
lm.items.AbstractContentItem.prototype.replaceChild.call( this, oldChild, newChild );
newChild.config[ this._dimension ] = size;
this.callDownwards( 'setSize' );
this.emitBubblingEvent( 'stateChanged' );
},
/**
* Called whenever the dimensions of this item or one of its parents change
*
* @returns {void}
*/
setSize: function() {
if( this.contentItems.length > 0 ) {
this._calculateRelativeSizes();
this._setAbsoluteSizes();
}
this.emitBubblingEvent( 'stateChanged' );
this.emit( 'resize' );
},
/**
* Invoked recursively by the layout manager. AbstractContentItem.init appends
* the contentItem's DOM elements to the container, RowOrColumn init adds splitters
* in between them
*
* @package private
* @override AbstractContentItem._$init
* @returns {void}
*/
_$init: function() {
if( this.isInitialised === true ) return;
var i;
lm.items.AbstractContentItem.prototype._$init.call( this );
for( i = 0; i < this.contentItems.length - 1; i++ ) {
this.contentItems[ i ].element.after( this._createSplitter( i ).element );
}
},
/**
* Turns the relative sizes calculated by _calculateRelativeSizes into
* absolute pixel values and applies them to the children's DOM elements
*
* Assigns additional pixels to counteract Math.floor
*
* @private
* @returns {void}
*/
_setAbsoluteSizes: function() {
var i,
totalSplitterSize = ( this.contentItems.length - 1 ) * this._splitterSize,
totalWidth = this.element.width(),
totalHeight = this.element.height(),
totalAssigned = 0,
additionalPixel,
itemSize,
itemSizes = [];
if( this._isColumn ) {
totalHeight -= totalSplitterSize;
} else {
totalWidth -= totalSplitterSize;
}
for( i = 0; i < this.contentItems.length; i++ ) {
if( this._isColumn ) {
itemSize = Math.floor( totalHeight * ( this.contentItems[ i ].config.height / 100 ) );
} else {
itemSize = Math.floor( totalWidth * ( this.contentItems[ i ].config.width / 100 ) );
}
totalAssigned += itemSize;
itemSizes.push( itemSize );
}
additionalPixel = Math.floor( ( this._isColumn ? totalHeight : totalWidth ) - totalAssigned );
for( i = 0; i < this.contentItems.length; i++ ) {
if( additionalPixel - i > 0 ) {
itemSizes[ i ]++;
}
if( this._isColumn ) {
this.contentItems[ i ].element.width( totalWidth );
this.contentItems[ i ].element.height( itemSizes[ i ] );
} else {
this.contentItems[ i ].element.width( itemSizes[ i ] );
this.contentItems[ i ].element.height( totalHeight );
}
}
},
/**
* Calculates the relative sizes of all children of this Item. The logic
* is as follows:
*
* - Add up the total size of all items that have a configured size
*
* - If the total == 100 (check for floating point errors)
* Excellent, job done
*
* - If the total is > 100,
* set the size of items without set dimensions to 1/3 and add this to the total
* set the size off all items so that the total is hundred relative to their original size
*
* - If the total is < 100
* If there are items without set dimensions, distribute the remainder to 100 evenly between them
* If there are no items without set dimensions, increase all items sizes relative to
* their original size so that they add up to 100
*
* @private
* @returns {void}
*/
_calculateRelativeSizes: function() {
var i,
total = 0,
itemsWithoutSetDimension = [],
dimension = this._isColumn ? 'height' : 'width';
for( i = 0; i < this.contentItems.length; i++ ) {
if( this.contentItems[ i ].config[ dimension ] !== undefined ) {
total += this.contentItems[ i ].config[ dimension ];
} else {
itemsWithoutSetDimension.push( this.contentItems[ i ] );
}
}
/**
* Everything adds up to hundred, all good :-)
*/
if( Math.round( total ) === 100 ) {
return;
}
/**
* Allocate the remaining size to the items without a set dimension
*/
if( Math.round( total ) < 100 && itemsWithoutSetDimension.length > 0 ) {
for( i = 0; i < itemsWithoutSetDimension.length; i++ ) {
itemsWithoutSetDimension[ i ].config[ dimension ] = ( 100 - total ) / itemsWithoutSetDimension.length;
}
return;
}
/**
* If the total is > 100, but there are also items without a set dimension left, assing 50
* as their dimension and add it to the total
*
* This will be reset in the next step
*/
if( Math.round( total ) > 100 ) {
for( i = 0; i < itemsWithoutSetDimension.length; i++ ) {
itemsWithoutSetDimension[ i ].config[ dimension ] = 50;
total += 50;
}
}
/**
* Set every items size relative to 100 relative to its size to total
*/
for( i = 0; i < this.contentItems.length; i++ ) {
this.contentItems[ i ].config[ dimension ] = ( this.contentItems[ i ].config[ dimension ] / total ) * 100;
}
},
/**
* Instantiates a new lm.controls.Splitter, binds events to it and adds
* it to the array of splitters at the position specified as the index argument
*
* What it doesn't do though is append the splitter to the DOM
*
* @param {Int} index The position of the splitter
*
* @returns {lm.controls.Splitter}
*/
_createSplitter: function( index ) {
var splitter;
splitter = new lm.controls.Splitter( this._isColumn, this._splitterSize );
splitter.on( 'drag', lm.utils.fnBind( this._onSplitterDrag, this, [ splitter ] ), this );
splitter.on( 'dragStop', lm.utils.fnBind( this._onSplitterDragStop, this, [ splitter ] ), this );
splitter.on( 'dragStart', lm.utils.fnBind( this._onSplitterDragStart, this, [ splitter ] ), this );
this._splitter.splice( index, 0, splitter );
return splitter;
},
/**
* Locates the instance of lm.controls.Splitter in the array of
* registered splitters and returns a map containing the contentItem
* before and after the splitters, both of which are affected if the
* splitter is moved
*
* @param {lm.controls.Splitter} splitter
*
* @returns {Object} A map of contentItems that the splitter affects
*/
_getItemsForSplitter: function( splitter ) {
var index = lm.utils.indexOf( splitter, this._splitter );
return {
before: this.contentItems[ index ],
after: this.contentItems[ index + 1 ]
};
},
/**
* Gets the minimum dimensions for the given item configuration array
* @param item
* @private
*/
_getMinimumDimensions: function (arr) {
var minWidth = 0, minHeight = 0;
for (var i = 0; i < arr.length; ++i) {
minWidth = Math.max(arr[i].minWidth || 0, minWidth);
minHeight = Math.max(arr[i].minHeight || 0, minHeight);
}
return { horizontal: minWidth, vertical: minHeight };
},
/**
* Invoked when a splitter's dragListener fires dragStart. Calculates the splitters
* movement area once (so that it doesn't need calculating on every mousemove event)
*
* @param {lm.controls.Splitter} splitter
*
* @returns {void}
*/
_onSplitterDragStart: function( splitter ) {
var items = this._getItemsForSplitter( splitter ),
minSize = this.layoutManager.config.dimensions[ this._isColumn ? 'minItemHeight' : 'minItemWidth' ];
var beforeMinDim = this._getMinimumDimensions(items.before.config.content);
var beforeMinSize = this._isColumn ? beforeMinDim.vertical : beforeMinDim.horizontal;
var afterMinDim = this._getMinimumDimensions(items.after.config.content);
var afterMinSize = this._isColumn ? afterMinDim.vertical : afterMinDim.horizontal;
this._splitterPosition = 0;
this._splitterMinPosition = -1 * ( items.before.element[ this._dimension ]() - (beforeMinSize || minSize) );
this._splitterMaxPosition = items.after.element[ this._dimension ]() - (afterMinSize || minSize);
},
/**
* Invoked when a splitter's DragListener fires drag. Updates the splitters DOM position,
* but not the sizes of the elements the splitter controls in order to minimize resize events
*
* @param {lm.controls.Splitter} splitter
* @param {Int} offsetX Relative pixel values to the splitters original position. Can be negative
* @param {Int} offsetY Relative pixel values to the splitters original position. Can be negative
*
* @returns {void}
*/
_onSplitterDrag: function( splitter, offsetX, offsetY ) {
var offset = this._isColumn ? offsetY : offsetX;
if( offset > this._splitterMinPosition && offset < this._splitterMaxPosition ) {
this._splitterPosition = offset;
splitter.element.css( this._isColumn ? 'top' : 'left', offset );
}
},
/**
* Invoked when a splitter's DragListener fires dragStop. Resets the splitters DOM position,
* and applies the new sizes to the elements before and after the splitter and their children
* on the next animation frame
*
* @param {lm.controls.Splitter} splitter
*
* @returns {void}
*/
_onSplitterDragStop: function( splitter ) {
var items = this._getItemsForSplitter( splitter ),
sizeBefore = items.before.element[ this._dimension ](),
sizeAfter = items.after.element[ this._dimension ](),
splitterPositionInRange = ( this._splitterPosition + sizeBefore ) / ( sizeBefore + sizeAfter ),
totalRelativeSize = items.before.config[ this._dimension ] + items.after.config[ this._dimension ];
items.before.config[ this._dimension ] = splitterPositionInRange * totalRelativeSize;
items.after.config[ this._dimension ] = ( 1 - splitterPositionInRange ) * totalRelativeSize;
splitter.element.css({
'top': 0,
'left': 0
});
lm.utils.animFrame( lm.utils.fnBind( this.callDownwards, this, [ 'setSize' ] ) );
}
});
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;
this._dropZones = {};
this._dropSegment = null;
this._contentAreaDimensions = null;
this._dropIndex = null;
this.isStack = true;
this.childElementContainer = $( '<div class="lm_items"></div>' );
console.log('lm.items.Stack config', config);
console.log('lm.items.Stack parent', parent);
this.header = new lm.controls.Header( layoutManager, this, config );
if( layoutManager.config.settings.hasHeaders === true ) {
this.element.append( this.header.element );
}
this.element.append( this.childElementContainer );
this._$validateClosability();
};
lm.utils.extend( lm.items.Stack, lm.items.AbstractContentItem );
lm.utils.copy( lm.items.Stack.prototype, {
setSize: function() {
var i,
contentWidth = this.element.width(),
contentHeight = this.element.height() - this.layoutManager.config.dimensions.headerHeight;
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 ) {
throw new Error( 'Configured activeItemIndex out of bounds' );
}
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.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.contentItems.length > 0 ) {
this.setActiveContentItem( this.contentItems[ Math.max( index -1 , 0 ) ] );
} else {
this._activeContentItem = null;
}
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' }, 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( 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();
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 );
}
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();
},
_highlightBodyDropZone: function( segment ) {
var highlightArea = this._contentAreaDimensions[ segment ].highlightArea;
this.layoutManager.dropTargetIndicator.highlightArea( highlightArea );
this._dropSegment = segment;
}
});
lm.utils.BubblingEvent = function( name, origin ) {
this.name = name;
this.origin = origin;
this.isPropagationStopped = false;
};
lm.utils.BubblingEvent.prototype.stopPropagation = function() {
this.isPropagationStopped = true;
};
/**
* Minifies and unminifies configs by replacing frequent keys
* and values with one letter substitutes
*
* @constructor
*/
lm.utils.ConfigMinifier = function(){
this._keys = [
'settings',
'hasHeaders',
'constrainDragToContainer',
'selectionEnabled',
'dimensions',
'borderWidth',
'minItemHeight',
'minItemWidth',
'headerHeight',
'dragProxyWidth',
'dragProxyHeight',
'labels',
'close',
'maximise',
'minimise',
'popout',
'content',
'componentName',
'componentState',
'id',
'width',
'type',
'height',
'isClosable',
'title',
'popoutWholeStack',
'openPopouts',
'parentId',
'activeItemIndex',
'reorderEnabled'
//Maximum 36 entries, do not cross this line!
];
this._values = [
true,
false,
'row',
'column',
'stack',
'component',
'close',
'maximise',
'minimise',
'open in new window'
];
};
lm.utils.copy( lm.utils.ConfigMinifier.prototype, {
/**
* Takes a GoldenLayout configuration object and
* replaces its keys and values recursively with
* one letter counterparts
*
* @param {Object} config A GoldenLayout config object
*
* @returns {Object} minified config
*/
minifyConfig: function( config ) {
var min = {};
this._nextLevel( config, min, '_min' );
return min;
},
/**
* Takes a configuration Object that was previously minified
* using minifyConfig and returns its original version
*
* @param {Object} minifiedConfig
*
* @returns {Object} the original configuration
*/
unminifyConfig: function( minifiedConfig ) {
var orig = {};
this._nextLevel( minifiedConfig, orig, '_max' );
return orig;
},
/**
* Recursive function, called for every level of the config structure
*
* @param {Array|Object} orig
* @param {Array|Object} min
* @param {String} translationFn
*
* @returns {void}
*/
_nextLevel: function( from, to, translationFn ) {
var key, minKey;
for( key in from ) {
/**
* For in returns array indices as keys, so let's cast them to numbers
*/
if( from instanceof Array ) key = parseInt( key, 10 );
/**
* In case something has extended Object prototypes
*/
if( !from.hasOwnProperty( key ) ) continue;
/**
* Translate the key to a one letter substitute
*/
minKey = this[ translationFn ]( key, this._keys );
/**
* For Arrays and Objects, create a new Array/Object
* on the minified object and recurse into it
*/
if( typeof from[ key ] === 'object' ) {
to[ minKey ] = from[ key ] instanceof Array ? [] : {};
this._nextLevel( from[ key ], to[ minKey ], translationFn );
/**
* For primitive values (Strings, Numbers, Boolean etc.)
* minify the value
*/
} else {
to[ minKey ] = this[ translationFn ]( from[ key ], this._values );
}
}
},
/**
* Minifies value based on a dictionary
*
* @param {String|Boolean} value
* @param {Array<String|Boolean>} dictionary
*
* @returns {String} The minified version
*/
_min: function( value, dictionary ) {
/**
* If a value actually is a single character, prefix it
* with ___ to avoid mistaking it for a minification code
*/
if( typeof value === 'string' && value.length === 1 ) {
return '___' + value;
}
var index = lm.utils.indexOf( value, dictionary );
/**
* value not found in the dictionary, return it unmodified
*/
if( index === -1 ) {
return value;
/**
* value found in dictionary, return its base36 counterpart
*/
} else {
return index.toString( 36 );
}
},
_max: function( value, dictionary ) {
/**
* value is a single character. Assume that it's a translation
* and return the original value from the dictionary
*/
if( typeof value === 'string' && value.length === 1 ) {
return dictionary[ parseInt( value, 36 ) ];
}
/**
* value originally was a single character and was prefixed with ___
* to avoid mistaking it for a translation. Remove the prefix
* and return the original character
*/
if( typeof value === 'string' && value.substr( 0, 3 ) === '___' ) {
return value[ 3 ];
}
/**
* value was not minified
*/
return value;
}
});
/**
* An EventEmitter singleton that propagates events
* across multiple windows. This is a little bit trickier since
* windows are allowed to open childWindows in their own right
*
* This means that we deal with a tree of windows. Hence the rules for event propagation are:
*
* - Propagate events from this layout to both parents and children
* - Propagate events from parent to this and children
* - Propagate events from children to the other children (but not the emitting one) and the parent
*
* @constructor
*
* @param {lm.LayoutManager} layoutManager
*/
lm.utils.EventHub = function( layoutManager ) {
lm.utils.EventEmitter.call( this );
this._layoutManager = layoutManager;
this._dontPropagateToParent = null;
this._childEventSource = null;
this.on( lm.utils.EventEmitter.ALL_EVENT, lm.utils.fnBind( this._onEventFromThis, this ) );
this._boundOnEventFromChild = lm.utils.fnBind( this._onEventFromChild, this );
$(window).on( 'gl_child_event', this._boundOnEventFromChild );
};
/**
* Called on every event emitted on this eventHub, regardles of origin.
*
* @private
*
* @param {Mixed}
*
* @returns {void}
*/
lm.utils.EventHub.prototype._onEventFromThis = function() {
var args = Array.prototype.slice.call( arguments );
if( this._layoutManager.isSubWindow && args[ 0 ] !== this._dontPropagateToParent ) {
this._propagateToParent( args );
}
this._propagateToChildren( args );
//Reset
this._dontPropagateToParent = null;
this._childEventSource = null;
};
/**
* Called by the parent layout.
*
* @param {Array} args Event name + arguments
*
* @returns {void}
*/
lm.utils.EventHub.prototype._$onEventFromParent = function( args ) {
this._dontPropagateToParent = args[ 0 ];
this.emit.apply( this, args );
};
/**
* Callback for child events raised on the window
*
* @param {DOMEvent} event
* @private
*
* @returns {void}
*/
lm.utils.EventHub.prototype._onEventFromChild = function( event ) {
this._childEventSource = event.originalEvent.__gl;
this.emit.apply( this, event.originalEvent.__glArgs );
};
/**
* Propagates the event to the parent by emitting
* it on the parent's DOM window
*
* @param {Array} args Event name + arguments
* @private
*
* @returns {void}
*/
lm.utils.EventHub.prototype._propagateToParent = function( args ) {
var event,
eventName = 'gl_child_event';
if (document.createEvent) {
event = window.opener.document.createEvent( 'HTMLEvents' );
event.initEvent( eventName, true, true);
} else {
event = window.opener.document.createEventObject();
event.eventType = eventName;
}
event.eventName = eventName;
event.__glArgs = args;
event.__gl = this._layoutManager;
if (document.createEvent) {
window.opener.dispatchEvent(event);
} else {
window.opener.fireEvent( 'on' + event.eventType, event );
}
};
/**
* Propagate events to children
*
* @param {Array} args Event name + arguments
* @private
*
* @returns {void}
*/
lm.utils.EventHub.prototype._propagateToChildren = function( args ) {
var childGl, i;
for( i = 0; i < this._layoutManager.openPopouts.length; i++ ) {
childGl = this._layoutManager.openPopouts[ i ].getGlInstance();
if( childGl && childGl !== this._childEventSource ) {
childGl.eventHub._$onEventFromParent( args );
}
}
};
/**
* Destroys the EventHub
*
* @public
* @returns {void}
*/
lm.utils.EventHub.prototype.destroy = function() {
$(window).off( 'gl_child_event', this._boundOnEventFromChild );
};
/**
* A specialised GoldenLayout component that binds GoldenLayout container
* lifecycle events to react components
*
* @constructor
*
* @param {lm.container.ItemContainer} container
* @param {Object} state state is not required for react components
*/
lm.utils.ReactComponentHandler = function( container, state ) {
this._reactComponent = null;
this._originalComponentWillUpdate = null;
this._container = container;
this._initialState = state;
this._reactClass = this._getReactClass();
this._container.on( 'open', this._render, this );
this._container.on( 'destroy', this._destroy, this );
};
lm.utils.copy( lm.utils.ReactComponentHandler.prototype, {
/**
* Creates the react class and component and hydrates it with
* the initial state - if one is present
*
* By default, react's getInitialState will be used
*
* @private
* @returns {void}
*/
_render: function() {
this._reactComponent = ReactDOM.render( this._getReactComponent(), this._container.getElement()[ 0 ]);
this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function(){};
this._reactComponent.componentWillUpdate = this._onUpdate.bind( this );
if( this._container.getState() ) {
this._reactComponent.setState( this._container.getState() );
}
},
/**
* Removes the component from the DOM and thus invokes React's unmount lifecycle
*
* @private
* @returns {void}
*/
_destroy: function() {
ReactDOM.unmountComponentAtNode( this._container.getElement()[ 0 ]);
this._container.off( 'open', this._render, this );
this._container.off( 'destroy', this._destroy, this );
},
/**
* Hooks into React's state management and applies the componentstate
* to GoldenLayout
*
* @private
* @returns {void}
*/
_onUpdate: function( nextProps, nextState ) {
this._container.setState( nextState );
this._originalComponentWillUpdate.call( this._reactComponent, nextProps, nextState );
},
/**
* Retrieves the react class from GoldenLayout's registry
*
* @private
* @returns {React.Class}
*/
_getReactClass: function() {
var componentName = this._container._config.component;
var reactClass;
if( !componentName ) {
throw new Error( 'No react component name. type: react-component needs a field `component`' );
}
reactClass = this._container.layoutManager.getComponent( componentName );
if( !reactClass ) {
throw new Error( 'React component "' + componentName + '" not found. ' +
'Please register all components with GoldenLayout using `registerComponent(name, component)`' );
}
return reactClass;
},
/**
* Copies and extends the properties array and returns the React element
*
* @private
* @returns {React.Element}
*/
_getReactComponent: function() {
var defaultProps = {
glEventHub: this._container.layoutManager.eventHub,
glContainer: this._container,
};
var props = $.extend( defaultProps, this._container._config.props );
return React.createElement( this._reactClass, props );
}
});})(window.$);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment