├── .npmignore ├── .gitignore ├── .travis.yml ├── src ├── js │ ├── config │ │ ├── ItemDefaultConfig.js │ │ └── defaultConfig.js │ ├── utils │ │ ├── BubblingEvent.js │ │ ├── ReactComponentHandler.js │ │ ├── DragListener.js │ │ ├── EventEmitter.js │ │ ├── EventHub.js │ │ ├── ConfigMinifier.js │ │ └── utils.js │ ├── errors │ │ └── ConfigurationError.js │ ├── controls │ │ ├── HeaderButton.js │ │ ├── DropTargetIndicator.js │ │ ├── Splitter.js │ │ ├── DragSource.js │ │ ├── TransitionIndicator.js │ │ ├── Tab.js │ │ ├── DragProxy.js │ │ └── BrowserPopout.js │ ├── items │ │ ├── Component.js │ │ └── Root.js │ └── container │ │ └── ItemContainer.js ├── css │ ├── README.md │ ├── goldenlayout-translucent-theme.css.map │ ├── goldenlayout-soda-theme.css.map │ ├── goldenlayout-light-theme.css.map │ ├── goldenlayout-dark-theme.css.map │ ├── goldenlayout-base.css.map │ ├── goldenlayout-translucent-theme.css │ ├── default-theme.css │ ├── goldenlayout-dark-theme.css │ ├── goldenlayout-light-theme.css │ ├── goldenlayout-soda-theme.css │ └── goldenlayout-base.css └── less │ ├── goldenlayout-dark-theme.less │ ├── goldenlayout-translucent-theme.less │ ├── goldenlayout-light-theme.less │ ├── goldenlayout-soda-theme.less │ └── goldenlayout-base.less ├── tsconfig.json ├── .github └── ISSUE_TEMPLATE.MD ├── test ├── create-config.tests.js ├── initialisation-tests.js ├── tab-tests.js ├── component-creation-events-tests.js ├── empty-item-tests.js ├── xss_tests.js ├── test-tools.js ├── drag-tests.js ├── deferred-create-drag-tests.js ├── component-state-save-tests.js ├── disabled-selection-tests.js ├── popout-tests.js ├── tree-manipulation-tests.js ├── item-creation-events-tests.js ├── title-tests.js ├── id-tests.js ├── selector-tests.js ├── enabled-selection-tests.js ├── event-bubble-tests.js ├── minifier-tests.js ├── create-from-config-tests.js └── event-emitter-tests.js ├── bower.json ├── test.css ├── README.md ├── index.hbs ├── LICENSE ├── gulpfile.js ├── karma.conf.js ├── .jshintrc ├── tslint.json ├── package.json ├── index.html ├── Gruntfile.js └── start.js /.npmignore: -------------------------------------------------------------------------------- 1 | typings 2 | npm-debug.log* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | typings 3 | .idea 4 | npm-debug.log 5 | lib/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.2' 4 | before_script: 5 | - 'npm install -g grunt-cli' 6 | -------------------------------------------------------------------------------- /src/js/config/ItemDefaultConfig.js: -------------------------------------------------------------------------------- 1 | lm.config.itemDefaultConfig = { 2 | isClosable: true, 3 | reorderEnabled: true, 4 | title: '' 5 | }; -------------------------------------------------------------------------------- /src/css/README.md: -------------------------------------------------------------------------------- 1 | # Beware! 2 | 3 | All these files are generated automatically from the `less` templates in `../src/less` 4 | and are created by the `grunt less` command. 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node" 5 | }, 6 | "exclude": [ 7 | "node_modules" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/js/utils/BubblingEvent.js: -------------------------------------------------------------------------------- 1 | lm.utils.BubblingEvent = function( name, origin ) { 2 | this.name = name; 3 | this.origin = origin; 4 | this.isPropagationStopped = false; 5 | }; 6 | 7 | lm.utils.BubblingEvent.prototype.stopPropagation = function() { 8 | this.isPropagationStopped = true; 9 | }; -------------------------------------------------------------------------------- /src/js/errors/ConfigurationError.js: -------------------------------------------------------------------------------- 1 | lm.errors.ConfigurationError = function( message, node ) { 2 | Error.call( this ); 3 | 4 | this.name = 'Configuration Error'; 5 | this.message = message; 6 | this.node = node; 7 | }; 8 | 9 | lm.errors.ConfigurationError.prototype = new Error(); 10 | -------------------------------------------------------------------------------- /src/js/controls/HeaderButton.js: -------------------------------------------------------------------------------- 1 | lm.controls.HeaderButton = function( header, label, cssClass, action ) { 2 | this._header = header; 3 | this.element = $( '
  • ' ); 4 | this._header.on( 'destroy', this._$destroy, this ); 5 | this._action = action; 6 | this.element.on( 'click touchstart', this._action ); 7 | this._header.controlsContainer.append( this.element ); 8 | }; 9 | 10 | lm.utils.copy( lm.controls.HeaderButton.prototype, { 11 | _$destroy: function() { 12 | this.element.off(); 13 | this.element.remove(); 14 | } 15 | } ); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.MD: -------------------------------------------------------------------------------- 1 | 5 | 6 | **Current behavior** 7 | 8 | 9 | **Expected behavior:** 10 | 11 | 12 | **Codepen example:** 13 | 16 | -------------------------------------------------------------------------------- /test/create-config.tests.js: -------------------------------------------------------------------------------- 1 | describe('It creates and extends config segments correctly', function(){ 2 | 3 | it( 'doesn\'t change the default config when calling extend', function(){ 4 | var createConfig = window.GoldenLayout.prototype._createConfig; 5 | 6 | expect( createConfig({}).dimensions.borderWidth ).toBe( 5 ); 7 | 8 | var myConfig = createConfig({ 9 | dimensions:{ 10 | borderWidth: 10 11 | } 12 | }); 13 | 14 | expect( myConfig ).not.toEqual( createConfig({}) ); 15 | expect( createConfig({}).dimensions.borderWidth ).toBe( 5 ); 16 | expect( myConfig.dimensions.borderWidth ).toBe( 10 ); 17 | 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "golden-layout", 3 | "version": "1.5.9", 4 | "homepage": "https://golden-layout.com", 5 | "authors": [ 6 | "deepstreamHub GmbH" 7 | ], 8 | "description": "a multi-screen/multi-window javascript layout manager", 9 | "main": "./dist/goldenlayout.min.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals" 13 | ], 14 | "keywords": [ 15 | "layoutmanager", 16 | "layout manager", 17 | "html5", 18 | "javascript", 19 | "layout", 20 | "docker", 21 | "popup" 22 | ], 23 | "dependencies": { 24 | "jquery": "*" 25 | }, 26 | "license": "MIT", 27 | "ignore": [ 28 | "**/.*", 29 | "node_modules", 30 | "bower_components", 31 | "test", 32 | "lib" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test.css: -------------------------------------------------------------------------------- 1 | h2{ 2 | font: 14px Arial, sans-serif; 3 | color:#fff; 4 | padding: 10px; 5 | } 6 | 7 | .lm_content{ 8 | text-align: center; 9 | color: white; 10 | } 11 | 12 | body { 13 | height: 100%; 14 | width: 100%; 15 | position: absolute; 16 | transition: all 0.5s ease; 17 | } 18 | 19 | #menuContainer { 20 | list-style: none; 21 | margin: 10px; 22 | padding: 0; 23 | } 24 | 25 | #menuContainer:after { 26 | content: ""; 27 | display: table; 28 | clear: both; 29 | } 30 | 31 | #menuContainer li { 32 | float: left; 33 | margin-right: 10px; 34 | } 35 | 36 | #menuContainer li a { 37 | background-color: black; 38 | color: white; 39 | padding: 5px; 40 | text-decoration: none; 41 | font-family: Arial, sans-serif; 42 | font-size: 12px; 43 | } 44 | -------------------------------------------------------------------------------- /src/js/controls/DropTargetIndicator.js: -------------------------------------------------------------------------------- 1 | lm.controls.DropTargetIndicator = function() { 2 | this.element = $( lm.controls.DropTargetIndicator._template ); 3 | $( document.body ).append( this.element ); 4 | }; 5 | 6 | lm.controls.DropTargetIndicator._template = '
    '; 7 | 8 | lm.utils.copy( lm.controls.DropTargetIndicator.prototype, { 9 | destroy: function() { 10 | this.element.remove(); 11 | }, 12 | 13 | highlight: function( x1, y1, x2, y2 ) { 14 | this.highlightArea( { x1: x1, y1: y1, x2: x2, y2: y2 } ); 15 | }, 16 | 17 | highlightArea: function( area ) { 18 | this.element.css( { 19 | left: area.x1, 20 | top: area.y1, 21 | width: area.x2 - area.x1, 22 | height: area.y2 - area.y1 23 | } ).show(); 24 | }, 25 | 26 | hide: function() { 27 | this.element.hide(); 28 | } 29 | } ); -------------------------------------------------------------------------------- /src/css/goldenlayout-translucent-theme.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["src/less/goldenlayout-translucent-theme.less"],"names":[],"mappings":"AAMA,iBACE,sBAAA,CACA,WAAY,4DAId,YACE,gCAAA,CACA,uCAAA,CACA,iBAIF,aACE,aACE,uCAKJ,wBACE,+CAAA,CACA,0BAAA,CACA,UAAA,CACA,0BAWF,aACE,kBAAA,CACA,YAAA,CACA,8BAEA,YAAC,OACD,YAAC,aACC,kBAAA,CACA,WAKJ,WACE,YAGA,UAAC,eACC,eALJ,UASE,SACE,4BAAA,CACA,cAAA,CACA,aAAA,CACA,gCAAA,CACA,gBAAA,CACA,mBAfJ,UASE,QASE,eACE,UAAA,CACA,WAAA,CACA,gOAAA,CACA,iCAAA,CACA,2BAAA,CACA,SAAA,CACA,OAAA,CACA,WAEA,UAnBJ,QASE,cAUG,OACC,UAKJ,UAzBF,QAyBG,WACC,kBAAA,CACA,4CAAA,CACA,mBAHF,UAzBF,QAyBG,UAKC,eACE,UASJ,aAHS,UAEX,WAAW,QACR,WAAD,SAFK,UACP,WAAW,QACR,WACC,4CAeJ,OAAC,OACD,OAAC,WAEC,gCAAA,CACA,cAaJ,YAEE,IACE,iBAAA,CACA,iCAAA,CACA,2BAAA,CACA,UAAA,CACA,8BAEA,YAPF,GAOG,OACC,UAVN,YAeE,YACE,6MAhBJ,YAoBE,cACE,iLArBJ,YAyBE,WACE,iNAmCJ,UACE,eADF,SAaE,UACE,gPAAA,CACA,iCAAA,CACA,2BAAA,CACA,WAGF,SAAC,MACC,UACE,UAON,SACE"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Golden Layout](https://golden-layout.com/) [![NPM version](https://badge.fury.io/js/golden-layout.svg)](http://badge.fury.io/js/golden-layout) [![Build Status](https://travis-ci.org/deepstreamIO/golden-layout.svg?branch=master)](https://travis-ci.org/deepstreamIO/golden-layout) 2 | 3 | ![Screenshot](https://cloud.githubusercontent.com/assets/512416/4584449/e6c154a0-4ffa-11e4-81a8-a7e5f8689dc5.PNG) 4 | 5 | # [https://golden-layout.com/](https://golden-layout.com/) 6 | 7 | ## Installation 8 | 9 | Add `golden-layout` to your bower.json, or [download](https://golden-layout.com/download/) the source. 10 | 11 | ## Features 12 | 13 | * Native popup windows 14 | * Completely themeable 15 | * Comprehensive API 16 | * Powerful persistence 17 | * Works in IE8+, Firefox, Chrome 18 | * Reponsive design 19 | 20 | 21 | ## [Examples](https://golden-layout.com/examples/) 22 | 23 | ## License 24 | MIT 25 | -------------------------------------------------------------------------------- /test/initialisation-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'Can initialise the layoutmanager', function() { 2 | 3 | var myLayout; 4 | 5 | it( 'Finds the layoutmanager on the global namespace', function() { 6 | expect( window.GoldenLayout ).toBeDefined(); 7 | }); 8 | 9 | it( 'Can create a most basic layout', function() { 10 | myLayout = new window.GoldenLayout({ 11 | content: [{ 12 | type: 'component', 13 | componentName: 'testComponent' 14 | }] 15 | }); 16 | 17 | myLayout.registerComponent( 'testComponent', function( container ){ 18 | container.getElement().html( 'that worked' ); 19 | }); 20 | 21 | myLayout.init(); 22 | expect( $( '.lm_goldenlayout' ).length ).toBe( 1 ); 23 | testTools.verifyPath( 'stack.0.component', myLayout, expect ); 24 | }); 25 | 26 | it( 'Destroys the layout', function(){ 27 | myLayout.destroy(); 28 | expect( myLayout.root.contentItems.length ).toBe( 0 ); 29 | }); 30 | }); -------------------------------------------------------------------------------- /src/css/goldenlayout-soda-theme.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["src/less/goldenlayout-soda-theme.less"],"names":[],"mappings":"AAcA,iBACE,kBAAA,CACA,WAAY,iCAAZ,CACA,yBAIF,YACE,mBAIF,aACE,aACE,uCAKJ,wBACE,iCAAA,CACA,0BAAA,CACA,0BAHF,uBAME,WACE,kBAAA,CACA,WAKJ,aACE,kBAAA,CACA,YAAA,CACA,8BAEA,YAAC,OACD,YAAC,aACC,kBAAA,CACA,UAKJ,WACE,0LAAA,CACA,YAGA,UAAC,eACC,eANJ,UAUE,SACE,4BAAA,CACA,cAAA,CACA,0LAAA,CACA,aAAA,CACA,QAAA,CACA,mBAhBJ,UAUE,QAgBE,eACE,UAAA,CACA,WAAA,CACA,gOAAA,CACA,iCAAA,CACA,2BAAA,CACA,SAAA,CACA,OAAA,CACA,WAEA,UA1BJ,QAgBE,cAUG,OACC,UAKJ,UAhCF,QAgCG,WACC,kBAAA,CACA,mBAFF,UAhCF,QAgCG,UAIC,eACE,UAKR,SAAS,QAEP,YADF,SAAS,SACP,YACE,2OAIJ,YACE,YACE,yBAKF,OAAC,OACD,OAAC,WAEC,0KAAA,CACA,cAKJ,UAAW,aAAa,gBAAe,QACrC,cAIF,YAEE,IACE,iBAAA,CACA,iCAAA,CACA,2BAAA,CACA,UAAA,CACA,8BAEA,YAPF,GAOG,OACC,UAVN,YAeE,YACE,6MAhBJ,YAoBE,cACE,iLArBJ,YAyBE,WACE,iNAKJ,aAEE,YACE,yBAHJ,aAOE,aACE,cACE,6KAKN,yBACE,wBAAA,CACA,0BAIF,UACE,eADF,SAIE,QACE,kBAAA,CACA,WANJ,SAUE,UACE,gPAAA,CACA,iCAAA,CACA,2BAAA,CACA,WAGF,SAAC,MACC,UACE"} -------------------------------------------------------------------------------- /src/js/config/defaultConfig.js: -------------------------------------------------------------------------------- 1 | lm.config.defaultConfig = { 2 | openPopouts: [], 3 | settings: { 4 | hasHeaders: true, 5 | constrainDragToContainer: true, 6 | reorderEnabled: true, 7 | selectionEnabled: false, 8 | popoutWholeStack: false, 9 | blockedPopoutsThrowError: true, 10 | closePopoutsOnUnload: true, 11 | showPopoutIcon: true, 12 | showMaximiseIcon: true, 13 | showCloseIcon: true, 14 | responsiveMode: 'onload', // Can be onload, always, or none. 15 | tabOverlapAllowance: 0, // maximum pixel overlap per tab 16 | reorderOnTabMenuClick: true, 17 | tabControlOffset: 10 18 | }, 19 | dimensions: { 20 | borderWidth: 5, 21 | borderGrabWidth: 15, 22 | minItemHeight: 10, 23 | minItemWidth: 10, 24 | headerHeight: 20, 25 | dragProxyWidth: 300, 26 | dragProxyHeight: 200 27 | }, 28 | labels: { 29 | close: 'close', 30 | maximise: 'maximise', 31 | minimise: 'minimise', 32 | popout: 'open in new window', 33 | popin: 'pop in', 34 | tabDropdown: 'additional tabs' 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/css/goldenlayout-light-theme.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["src/less/goldenlayout-light-theme.less"],"names":[],"mappings":"AAiBA,iBAEE,+aAIF,YACE,kBAAA,CACA,yBAIF,aACE,aACE,sCAAA,CACA,sBAKJ,wBACE,yCAAA,CACA,0BAAA,CACA,UAAA,CACA,0BAJF,uBAOE,WACE,kBAAA,CACA,WAKJ,aACE,kBAAA,CACA,YAAA,CACA,8BAEA,YAAC,OACD,YAAC,aACC,kBAAA,CACA,UAKJ,WACE,YAGA,UAAC,eACC,eALJ,UASE,SACE,4BAAA,CACA,cAAA,CACA,aAAA,CACA,kBAAA,CACA,gBAAA,CACA,kBAAA,CACA,wBAAA,CACA,mBAjBJ,UASE,QAUE,WACE,gBApBN,UASE,QAeE,eACE,UAAA,CACA,WAAA,CACA,wKAAA,CACA,iCAAA,CACA,2BAAA,CACA,SAAA,CACA,OAAA,CACA,WAEA,UAzBJ,QAeE,cAUG,OACC,UAKJ,UA/BF,QA+BG,WACC,kBAAA,CACA,4CAAA,CACA,mBAHF,UA/BF,QA+BG,UAKC,eACE,UASJ,aAHS,UAEX,WAAW,QACR,WAAD,SAFK,UACP,WAAW,QACR,WACC,4CAMN,YACE,YACE,yBAKF,OAAC,OACD,OAAC,WAEC,kBAAA,CACA,cAKJ,UAAW,aAAa,gBAAe,QACrC,cAIF,YAEE,IACE,iBAAA,CACA,iCAAA,CACA,2BAAA,CACA,UAAA,CACA,8BAEA,YAPF,GAOG,OACC,UAVN,YAeE,YACE,iMAhBJ,YAoBE,cACE,yKArBJ,YAyBE,WACE,iLAKJ,aAEE,YACE,yBAHJ,aAOE,aACE,cACE,6KAKN,yBACE,wBAAA,CACA,0BAIF,UACE,eADF,SAIE,QACE,kBAAA,CACA,WANJ,SAUE,UACE,gPAAA,CACA,iCAAA,CACA,2BAAA,CACA,WAGF,SAAC,MACC,UACE"} -------------------------------------------------------------------------------- /index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layout Manager 6 | 7 | 8 | 9 | 10 | 11 | 14 | {{#each files}} 15 | 16 | {{/each}} 17 | 18 | 19 | 20 | 21 | 22 |
    23 | 28 |
    29 |
    30 | 31 | -------------------------------------------------------------------------------- /src/css/goldenlayout-dark-theme.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["src/less/goldenlayout-dark-theme.less"],"names":[],"mappings":"AAiBA,iBACE,mBAIF,YACE,mBAIF,aACE,aACE,uCAKJ,wBACE,iCAAA,CACA,0BAAA,CACA,0BAHF,uBAME,WACE,kBAAA,CACA,WAKJ,aACE,kBAAA,CACA,YAAA,CACA,8BAEA,YAAC,OACD,YAAC,aACC,kBAAA,CACA,UAKJ,WACE,WAAA,CACA,iBAEA,UAAC,eACC,eALJ,UASE,SACE,4BAAA,CACA,cAAA,CACA,aAAA,CACA,kBAAA,CACA,uCAAA,CACA,gBAAA,CACA,kBAAA,CACA,gBAjBJ,UASE,QAgBE,eACE,UAAA,CACA,WAAA,CACA,gOAAA,CACA,iCAAA,CACA,2BAAA,CACA,OAAA,CACA,SAAA,CACA,WAEA,UA1BJ,QAgBE,cAUG,OACC,UAKJ,UAhCF,QAgCG,WACC,kBAAA,CACA,6BAAA,CACA,mBAHF,UAhCF,QAgCG,UAKC,eACE,UAMR,aAAa,UAEX,WAAW,SADb,SAAS,UACP,WAAW,SACT,uCACA,aAJS,UAEX,WAAW,QAER,WAAD,SAHK,UACP,WAAW,QAER,WACC,6BAMN,YACE,YACE,yBAKF,OAAC,OACD,OAAC,WAEC,kBAAA,CACA,cAKJ,UAAW,aAAa,gBAAe,QACrC,cAIF,YAEE,IACE,iBAAA,CACA,iCAAA,CACA,2BAAA,CACA,UAAA,CACA,8BAEA,YAPF,GAOG,OACC,UAVN,YAeE,YACE,6MAhBJ,YAoBE,cACE,iLArBJ,YAyBE,WACE,iNAKJ,aAEE,YACE,yBAHJ,aAOE,aACE,cACE,6KAKN,yBACE,wBAAA,CACA,0BAIF,UACE,eADF,SAIE,QACE,kBAAA,CACA,WANJ,SAUE,UACE,gPAAA,CACA,iCAAA,CACA,2BAAA,CACA,6BAAA,CACA,4BAAA,CACA,WAGF,SAAC,MACC,UACE"} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 deepstream.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Deprecated but left here for now; use the gulp tasks in the Gruntfile. 2 | var gulp = require('gulp'); 3 | var concat = require('gulp-concat'); 4 | var cConcat = require('gulp-continuous-concat'); 5 | var uglify = require('gulp-uglify'); 6 | var insert = require('gulp-insert'); 7 | var watch = require('gulp-watch'); 8 | 9 | gulp.task( 'dev', function() { 10 | return gulp 11 | .src([ 12 | './build/ns.js', 13 | './src/js/utils/utils.js', 14 | './src/js/utils/EventEmitter.js', 15 | './src/js/utils/DragListener.js', 16 | './src/js/**' 17 | ]) 18 | .pipe(watch('./src/js/**')) 19 | .pipe(cConcat('goldenlayout.js')) 20 | .pipe(insert.wrap('(function($){', '})(window.$);' )) 21 | .pipe(gulp.dest('./dist')); 22 | }); 23 | 24 | gulp.task( 'build', function() { 25 | return gulp 26 | .src([ 27 | './build/ns.js', 28 | './src/js/utils/utils.js', 29 | './src/js/utils/EventEmitter.js', 30 | './src/js/utils/DragListener.js', 31 | './src/js/**' 32 | ]) 33 | .pipe(concat('goldenlayout.js')) 34 | .pipe(insert.wrap('(function($){', '})(window.$);' )) 35 | .pipe(gulp.dest('./dist')) 36 | .pipe(uglify()) 37 | .pipe(concat('goldenlayout.min.js')) 38 | .pipe(gulp.dest('./dist')); 39 | }); 40 | -------------------------------------------------------------------------------- /test/tab-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'tabs apply their configuration', function(){ 2 | var layout; 3 | 4 | it( 'creates a layout', function(){ 5 | layout = testTools.createLayout({ 6 | content: [{ 7 | type: 'stack', 8 | content: [{ 9 | type: 'component', 10 | componentName: 'testComponent' 11 | }, 12 | { 13 | type: 'component', 14 | componentName: 'testComponent', 15 | reorderEnabled: false 16 | }] 17 | }] 18 | }); 19 | 20 | expect( layout.isInitialised ).toBe( true ); 21 | }); 22 | 23 | it( 'attached a drag listener to the first tab', function(){ 24 | 25 | 26 | var item1 = layout.root.contentItems[ 0 ].contentItems[ 0 ], 27 | item2 = layout.root.contentItems[ 0 ].contentItems[ 1 ], 28 | header = layout.root.contentItems[ 0 ].header; 29 | 30 | expect( header.tabs.length ).toBe( 2 ); 31 | 32 | expect( item1.type ).toBe( 'component' ); 33 | expect( item1.config.reorderEnabled ).toBe( true ); 34 | expect( header.tabs[ 0 ]._dragListener ).toBeDefined(); 35 | 36 | expect( item2.type ).toBe( 'component' ); 37 | expect( item2.config.reorderEnabled ).toBe( false ); 38 | expect( header.tabs[ 1 ]._dragListener ).not.toBeDefined(); 39 | }); 40 | 41 | it( 'destroys the layout', function(){ 42 | layout.destroy(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/component-creation-events-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'emits events when components are created', function() { 2 | 3 | var layout, eventListener = window.jasmine.createSpyObj( 'eventListener', [ 4 | 'show', 5 | 'shown' 6 | ] ); 7 | 8 | it( 'creates a layout', function() { 9 | layout = new window.GoldenLayout( { 10 | content: [ { 11 | type: 'stack', 12 | content: [ { 13 | type: 'column', 14 | content: [ { 15 | type: 'component', 16 | componentName: 'testComponent' 17 | } ] 18 | } ] 19 | } ] 20 | } ); 21 | 22 | function Recorder( container ) { 23 | container.getElement().html( 'that worked' ); 24 | container.on( 'show', eventListener.show ); 25 | container.on( 'shown', eventListener.shown ); 26 | } 27 | 28 | layout.registerComponent( 'testComponent', Recorder ); 29 | } ); 30 | 31 | it( 'registers listeners', function() { 32 | expect( eventListener.show ).not.toHaveBeenCalled(); 33 | expect( eventListener.shown ).not.toHaveBeenCalled(); 34 | 35 | layout.init(); 36 | } ); 37 | 38 | it( 'has called listeners', function() { 39 | expect( eventListener.show.calls.length ).toBe( 1 ); 40 | expect( eventListener.shown.calls.length ).toBe( 1 ); 41 | } ); 42 | 43 | it( 'destroys the layout', function() { 44 | layout.destroy(); 45 | } ); 46 | } ); -------------------------------------------------------------------------------- /src/js/controls/Splitter.js: -------------------------------------------------------------------------------- 1 | lm.controls.Splitter = function( isVertical, size, grabSize ) { 2 | this._isVertical = isVertical; 3 | this._size = size; 4 | this._grabSize = grabSize < size ? size : grabSize; 5 | 6 | this.element = this._createElement(); 7 | this._dragListener = new lm.utils.DragListener( this.element ); 8 | }; 9 | 10 | lm.utils.copy( lm.controls.Splitter.prototype, { 11 | on: function( event, callback, context ) { 12 | this._dragListener.on( event, callback, context ); 13 | }, 14 | 15 | _$destroy: function() { 16 | this.element.remove(); 17 | }, 18 | 19 | _createElement: function() { 20 | var dragHandle = $( '
    ' ); 21 | var element = $( '
    ' ); 22 | element.append(dragHandle); 23 | 24 | var handleExcessSize = this._grabSize - this._size; 25 | var handleExcessPos = handleExcessSize / 2; 26 | 27 | if( this._isVertical ) { 28 | dragHandle.css( 'top', -handleExcessPos ); 29 | dragHandle.css( 'height', this._size + handleExcessSize ); 30 | element.addClass( 'lm_vertical' ); 31 | element[ 'height' ]( this._size ); 32 | } else { 33 | dragHandle.css( 'left', -handleExcessPos ); 34 | dragHandle.css( 'width', this._size + handleExcessSize ); 35 | element.addClass( 'lm_horizontal' ); 36 | element[ 'width' ]( this._size ); 37 | } 38 | 39 | return element; 40 | } 41 | } ); 42 | -------------------------------------------------------------------------------- /test/empty-item-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'The layout can handle empty stacks', function(){ 2 | 3 | var myLayout; 4 | 5 | it('Creates an initial layout', function(){ 6 | myLayout = testTools.createLayout({ 7 | content: [{ 8 | type: 'row', 9 | content: [{ 10 | type:'component', 11 | componentName: 'testComponent', 12 | componentState: { text: 'Component 1' } 13 | },{ 14 | type:'component', 15 | componentName: 'testComponent', 16 | componentState: { text: 'Component 2' } 17 | },{ 18 | isClosable: false, 19 | type: 'stack', 20 | content: [] 21 | }] 22 | }] 23 | }); 24 | }); 25 | 26 | it( 'can manipulate the layout tree with an empty item present', function(){ 27 | var row = myLayout.root.contentItems[ 0 ]; 28 | expect( row.isRow ).toBe( true ); 29 | 30 | row.addChild({ 31 | type:'component', 32 | componentName: 'testComponent' 33 | }); 34 | }); 35 | 36 | it( 'can add children to the empty stack', function(){ 37 | var stack = myLayout.root.contentItems[ 0 ].contentItems[ 2 ]; 38 | expect( stack.isStack ).toBe( true ); 39 | expect( stack.contentItems.length ).toBe( 0 ); 40 | 41 | stack.addChild({ 42 | type:'component', 43 | componentName: 'testComponent' 44 | }); 45 | 46 | expect( stack.contentItems.length ).toBe( 1 ); 47 | }); 48 | 49 | it( 'destroys the layout', function(){ 50 | myLayout.destroy(); 51 | }); 52 | }); -------------------------------------------------------------------------------- /test/xss_tests.js: -------------------------------------------------------------------------------- 1 | describe( 'Basic XSS filtering is applied', function(){ 2 | var filterFn = window.GoldenLayout.__lm.utils.filterXss; 3 | 4 | it( 'escapes tags', function(){ 5 | var escapedString = filterFn( '>\'>">' ); 6 | expect( escapedString ).toBe( '>\'>"><img src=x onerror=alert(0)>' ); 7 | }); 8 | 9 | it( 'escapes javascript urls', function(){ 10 | var escapedString = filterFn( 'javascript:alert("hi")' ); // jshint ignore:line 11 | expect( escapedString ).toBe( 'javascript:alert("hi")' ); 12 | }); 13 | 14 | it( 'escapes expression statements', function(){ 15 | var escapedString = filterFn( 'expression:alert("hi")' ); // jshint ignore:line 16 | expect( escapedString ).toBe( 'expression:alert("hi")' ); 17 | }); 18 | 19 | it( 'escapes onload statements', function(){ 20 | var escapedString = filterFn( 'onload=alert("hi")' ); // jshint ignore:line 21 | expect( escapedString ).toBe( 'onload=alert("hi")' ); 22 | 23 | escapedString = filterFn( 'onLoad=alert("hi")' ); // jshint ignore:line 24 | expect( escapedString ).toBe( 'onload=alert("hi")' ); 25 | }); 26 | 27 | it( 'escapes onerror statements', function(){ 28 | var escapedString = filterFn( 'onerror=alert("hi")' ); // jshint ignore:line 29 | expect( escapedString ).toBe( 'onerror=alert("hi")' ); 30 | 31 | escapedString = filterFn( 'onError=alert("hi")' ); // jshint ignore:line 32 | expect( escapedString ).toBe( 'onerror=alert("hi")' ); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/test-tools.js: -------------------------------------------------------------------------------- 1 | 2 | testTools = {}; 3 | 4 | testTools.createLayout = function( config ) { 5 | var myLayout = new window.GoldenLayout( config ); 6 | 7 | myLayout.registerComponent( 'testComponent', testTools.TestComponent ); 8 | 9 | myLayout.init(); 10 | 11 | 12 | waitsFor(function(){ 13 | return myLayout.isInitialised; 14 | }); 15 | 16 | return myLayout; 17 | }; 18 | 19 | testTools.TestComponent = function( container, state ){ 20 | if ( state === undefined ) { 21 | container.getElement().html( 'that worked' ); 22 | } else { 23 | container.getElement().html( state.html ); 24 | } 25 | this.isTestComponentInstance = true; 26 | }; 27 | 28 | /** 29 | * Takes a path of type.index.type.index, e.g. 30 | * 31 | * 'row.0.stack.1.component' 32 | * 33 | * and resolves it to an element 34 | * 35 | * @param {String} path 36 | * @param {GoldenLayout} layout 37 | * @param {Function} expect Jasmine expect function 38 | * 39 | * @returns {AbstractContentItem} 40 | */ 41 | testTools.verifyPath = function( path, layout, expect ) { 42 | expect( layout.root ).toBeDefined(); 43 | expect( layout.root.contentItems.length ).toBe( 1 ); 44 | 45 | var pathSegments = path.split( '.' ), 46 | node = layout.root.contentItems[ 0 ], 47 | i; 48 | 49 | for( i = 0; i < pathSegments.length; i++ ) { 50 | 51 | if( isNaN( pathSegments[ i ] ) ) { 52 | expect( node.type ).toBe( pathSegments[ i ] ); 53 | } else { 54 | node = node.contentItems[ parseInt( pathSegments[ i ], 10 ) ]; 55 | 56 | expect( node ).toBeDefined(); 57 | 58 | if( node === undefined ) { 59 | return null; 60 | } 61 | } 62 | } 63 | 64 | return node; 65 | }; -------------------------------------------------------------------------------- /test/drag-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'supports drag creation', function() { 2 | 3 | var layout, dragSrc; 4 | 5 | it( 'creates a layout', function() { 6 | layout = testTools.createLayout( { 7 | content: [ { 8 | type: 'stack', 9 | content: [ { 10 | type: 'component', 11 | componentState: { html: '
    ' }, 12 | componentName: 'testComponent' 13 | } ] 14 | } ] 15 | } ); 16 | 17 | expect( layout.isInitialised ).toBe( true ); 18 | } ); 19 | 20 | it( 'creates a drag source', function() { 21 | dragSrc = layout.root.contentItems[ 0 ].element.find( '#dragsource' ); 22 | expect( dragSrc.length ).toBe( 1 ); 23 | 24 | layout.createDragSource( dragSrc, { 25 | type: 'component', 26 | componentState: { html: '
    ' }, 27 | componentName: 'testComponent' 28 | } 29 | ); 30 | } ); 31 | 32 | it( 'creates a new components if dragged', function() { 33 | expect( $( '.dragged' ).length ).toBe( 0 ); 34 | 35 | var mouse = $.Event( 'mousedown' ); 36 | mouse.pageX = dragSrc.position().left; 37 | mouse.pageY = dragSrc.position().top; 38 | mouse.button = 0; 39 | dragSrc.trigger( mouse ); 40 | 41 | mouse = $.Event( 'mousemove' ); 42 | mouse.pageX = dragSrc.position().left + 50; 43 | mouse.pageY = dragSrc.position().top + 50; 44 | dragSrc.trigger( mouse ); 45 | 46 | dragSrc.trigger( 'mouseup' ); 47 | 48 | expect( $( '.dragged' ).length ).toBe( 1 ); 49 | var node = testTools.verifyPath( "row.0", layout, expect ); 50 | expect( node.element.find( ".dragged" ).length ).toBe( 1 ); 51 | } ); 52 | 53 | it( 'destroys the layout', function() { 54 | layout.destroy(); 55 | } ); 56 | } ); -------------------------------------------------------------------------------- /test/deferred-create-drag-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'supports drag creation with deferred content', function() { 2 | 3 | var layout, dragSrc; 4 | 5 | it( 'creates a layout', function() { 6 | layout = testTools.createLayout( { 7 | content: [ { 8 | type: 'stack', 9 | content: [ { 10 | type: 'component', 11 | componentState: { html: '
    ' }, 12 | componentName: 'testComponent' 13 | } ] 14 | } ] 15 | } ); 16 | 17 | expect( layout.isInitialised ).toBe( true ); 18 | } ); 19 | 20 | it( 'creates a drag source', function() { 21 | dragSrc = layout.root.contentItems[ 0 ].element.find( '#dragsource' ); 22 | expect( dragSrc.length ).toBe( 1 ); 23 | 24 | layout.createDragSource( dragSrc, function() { 25 | return { 26 | type: 'component', 27 | componentState: { html: '
    ' }, 28 | componentName: 'testComponent' 29 | }; 30 | } 31 | ); 32 | } ); 33 | 34 | it( 'creates a new components if dragged', function() { 35 | expect( $( '.dragged' ).length ).toBe( 0 ); 36 | 37 | var mouse = $.Event( 'mousedown' ); 38 | mouse.pageX = dragSrc.position().left; 39 | mouse.pageY = dragSrc.position().top; 40 | mouse.button = 0; 41 | dragSrc.trigger( mouse ); 42 | 43 | mouse = $.Event( 'mousemove' ); 44 | mouse.pageX = dragSrc.position().left + 50; 45 | mouse.pageY = dragSrc.position().top + 50; 46 | dragSrc.trigger( mouse ); 47 | 48 | dragSrc.trigger( 'mouseup' ); 49 | expect( $( '.dragged' ).length ).toBe( 1 ); 50 | var node = testTools.verifyPath( "row.0", layout, expect ); 51 | expect( node.element.find( ".dragged" ).length ).toBe( 1 ); 52 | } ); 53 | 54 | it( 'destroys the layout', function() { 55 | layout.destroy(); 56 | } ); 57 | } ); -------------------------------------------------------------------------------- /test/component-state-save-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'Sets and retrieves a component\'s state', function() { 2 | 3 | var myLayout, myComponent; 4 | 5 | it( 'Can create a most basic layout', function() { 6 | runs(function(){ 7 | myLayout = new window.GoldenLayout({ 8 | content: [{ 9 | type: 'component', 10 | componentName: 'testComponent', 11 | componentState: { testValue: 'initial' } 12 | }] 13 | }); 14 | 15 | 16 | myLayout.registerComponent( 'testComponent', function( container, state ){ 17 | this.container = container; 18 | this.state = state; 19 | myComponent = this; 20 | }); 21 | 22 | myLayout.init(); 23 | }); 24 | 25 | waitsFor(function(){ 26 | return myLayout.isInitialised; 27 | }); 28 | 29 | runs(function(){ 30 | expect( myComponent.state.testValue ).toBe( 'initial' ); 31 | }); 32 | }); 33 | 34 | it( 'returns the initial state', function(){ 35 | var config = myLayout.toConfig(); 36 | expect( config.content[ 0 ].content[ 0 ].componentState.testValue ).toBe( 'initial' ); 37 | }); 38 | 39 | it( 'emits stateChanged when a component updates its state', function(){ 40 | var stateChanges = 0; 41 | 42 | myLayout.on( 'stateChanged', function(){ 43 | stateChanges++; 44 | }); 45 | 46 | runs(function(){ 47 | myComponent.container.setState({ testValue: 'updated' }); 48 | }); 49 | 50 | waitsFor(function(){ 51 | return stateChanges !== 0; 52 | }); 53 | 54 | runs(function(){ 55 | expect( stateChanges ).toBe( 1 ); 56 | }); 57 | }); 58 | 59 | it( 'returns the updated state', function(){ 60 | var config = myLayout.toConfig(); 61 | expect( config.content[ 0 ].content[ 0 ].componentState.testValue ).toBe( 'updated' ); 62 | }); 63 | 64 | it( 'Destroys the layout', function(){ 65 | myLayout.destroy(); 66 | expect( myLayout.root.contentItems.length ).toBe( 0 ); 67 | }); 68 | }); -------------------------------------------------------------------------------- /src/js/controls/DragSource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows for any DOM item to create a component on drag 3 | * start tobe dragged into the Layout 4 | * 5 | * @param {jQuery element} element 6 | * @param {Object} itemConfig the configuration for the contentItem that will be created 7 | * @param {LayoutManager} layoutManager 8 | * 9 | * @constructor 10 | */ 11 | lm.controls.DragSource = function( element, itemConfig, layoutManager ) { 12 | this._element = element; 13 | this._itemConfig = itemConfig; 14 | this._layoutManager = layoutManager; 15 | this._dragListener = null; 16 | 17 | this._createDragListener(); 18 | }; 19 | 20 | lm.utils.copy( lm.controls.DragSource.prototype, { 21 | 22 | /** 23 | * Called initially and after every drag 24 | * 25 | * @returns {void} 26 | */ 27 | _createDragListener: function() { 28 | if( this._dragListener !== null ) { 29 | this._dragListener.destroy(); 30 | } 31 | 32 | this._dragListener = new lm.utils.DragListener( this._element ); 33 | this._dragListener.on( 'dragStart', this._onDragStart, this ); 34 | this._dragListener.on( 'dragStop', this._createDragListener, this ); 35 | }, 36 | 37 | /** 38 | * Callback for the DragListener's dragStart event 39 | * 40 | * @param {int} x the x position of the mouse on dragStart 41 | * @param {int} y the x position of the mouse on dragStart 42 | * 43 | * @returns {void} 44 | */ 45 | _onDragStart: function( x, y ) { 46 | var itemConfig = this._itemConfig; 47 | if( lm.utils.isFunction( itemConfig ) ) { 48 | itemConfig = itemConfig(); 49 | } 50 | var contentItem = this._layoutManager._$normalizeContentItem( $.extend( true, {}, itemConfig ) ), 51 | dragProxy = new lm.controls.DragProxy( x, y, this._dragListener, this._layoutManager, contentItem, null ); 52 | 53 | this._layoutManager.transitionIndicator.transitionElements( this._element, dragProxy.element ); 54 | } 55 | } ); 56 | -------------------------------------------------------------------------------- /src/js/controls/TransitionIndicator.js: -------------------------------------------------------------------------------- 1 | lm.controls.TransitionIndicator = function() { 2 | this._element = $( '
    ' ); 3 | $( document.body ).append( this._element ); 4 | 5 | this._toElement = null; 6 | this._fromDimensions = null; 7 | this._totalAnimationDuration = 200; 8 | this._animationStartTime = null; 9 | }; 10 | 11 | lm.utils.copy( lm.controls.TransitionIndicator.prototype, { 12 | destroy: function() { 13 | this._element.remove(); 14 | }, 15 | 16 | transitionElements: function( fromElement, toElement ) { 17 | /** 18 | * TODO - This is not quite as cool as expected. Review. 19 | */ 20 | return; 21 | this._toElement = toElement; 22 | this._animationStartTime = lm.utils.now(); 23 | this._fromDimensions = this._measure( fromElement ); 24 | this._fromDimensions.opacity = 0.8; 25 | this._element.show().css( this._fromDimensions ); 26 | lm.utils.animFrame( lm.utils.fnBind( this._nextAnimationFrame, this ) ); 27 | }, 28 | 29 | _nextAnimationFrame: function() { 30 | var toDimensions = this._measure( this._toElement ), 31 | animationProgress = ( lm.utils.now() - this._animationStartTime ) / this._totalAnimationDuration, 32 | currentFrameStyles = {}, 33 | cssProperty; 34 | 35 | if( animationProgress >= 1 ) { 36 | this._element.hide(); 37 | return; 38 | } 39 | 40 | toDimensions.opacity = 0; 41 | 42 | for( cssProperty in this._fromDimensions ) { 43 | currentFrameStyles[ cssProperty ] = this._fromDimensions[ cssProperty ] + 44 | ( toDimensions[ cssProperty ] - this._fromDimensions[ cssProperty ] ) * 45 | animationProgress; 46 | } 47 | 48 | this._element.css( currentFrameStyles ); 49 | lm.utils.animFrame( lm.utils.fnBind( this._nextAnimationFrame, this ) ); 50 | }, 51 | 52 | _measure: function( element ) { 53 | var offset = element.offset(); 54 | 55 | return { 56 | left: offset.left, 57 | top: offset.top, 58 | width: element.outerWidth(), 59 | height: element.outerHeight() 60 | }; 61 | } 62 | } ); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Jun 18 2014 07:15:37 GMT+0100 (GMT Summer Time) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | './lib/jquery.js', 19 | './build/ns.js', 20 | './src/js/utils/utils.js', 21 | './src/js/**', 22 | './test/**' 23 | ], 24 | 25 | 26 | // list of files to exclude 27 | exclude: [ 28 | 29 | ], 30 | 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | // '../src/**': 'coverage' 36 | }, 37 | 38 | 39 | // test results reporter to use 40 | // possible values: 'dots', 'progress' 41 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 42 | reporters: ['progress'], 43 | 44 | 45 | // web server port 46 | port: 9876, 47 | 48 | 49 | // enable / disable colors in the output (reporters and logs) 50 | colors: true, 51 | 52 | 53 | // level of logging 54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 55 | logLevel: config.LOG_INFO, 56 | 57 | 58 | // enable / disable watching file and executing tests whenever any file changes 59 | autoWatch: true, 60 | 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['Chrome'/*, 'IE'*/], 65 | 66 | 67 | // Continuous Integration mode 68 | // if true, Karma captures browsers, runs the tests and exits 69 | singleRun: false 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /test/disabled-selection-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'content items are abled to to emit events that bubble up the tree', function(){ 2 | 3 | var layout, selectionChangedSpy, stackA, stackB; 4 | 5 | it( 'creates a layout', function(){ 6 | layout = testTools.createLayout({ 7 | content: [{ 8 | type: 'stack', 9 | content: [{ 10 | type: 'column', 11 | content:[{ 12 | type: 'component', 13 | componentName: 'testComponent', 14 | id: 'test' 15 | }, 16 | { 17 | type: 'component', 18 | componentName: 'testComponent', 19 | id: 'test' 20 | }] 21 | },{ 22 | type: 'row' 23 | }] 24 | }] 25 | }); 26 | expect( layout.isInitialised ).toBe( true ); 27 | testTools.verifyPath( 'stack.0.column.0.stack.0.component', layout, expect ); 28 | testTools.verifyPath( 'stack.1.row', layout, expect ); 29 | }); 30 | 31 | it( 'attaches event listeners and retrieves stacks', function(){ 32 | 33 | var components = layout.root.getItemsById( 'test' ); 34 | 35 | expect( components.length ).toBe( 2 ); 36 | 37 | stackA = components[ 0 ].parent; 38 | stackB = components[ 1 ].parent; 39 | 40 | expect( stackA.type ).toBe( 'stack' ); 41 | expect( stackB.type ).toBe( 'stack' ); 42 | 43 | selectionChangedSpy = window.jasmine.createSpyObj( 'selectionChanged', ['onselectionChanged'] ); 44 | 45 | layout.on( 'selectionChanged', selectionChangedSpy.onselectionChanged ); 46 | }); 47 | 48 | it( 'clicks a header, but nothing happens since enableSelection == false', function(){ 49 | var headerElement = stackA.element.find( '.lm_header' ); 50 | expect( headerElement.length ).toBe( 1 ); 51 | expect( selectionChangedSpy.onselectionChanged.calls.length ).toBe( 0 ); 52 | expect( layout.selectedItem ).toBe( null ); 53 | expect( headerElement.hasClass( 'lm_selectable' ) ).toBe( false ); 54 | headerElement.trigger( 'click' ); 55 | expect( selectionChangedSpy.onselectionChanged.calls.length ).toBe( 0 ); 56 | expect( layout.selectedItem ).toBe( null ); 57 | }); 58 | 59 | it( 'destroys the layout', function(){ 60 | layout.destroy(); 61 | }); 62 | }); -------------------------------------------------------------------------------- /test/popout-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'it can popout components into browserwindows', function(){ 2 | 3 | var layout, browserPopout; 4 | 5 | it( 'creates a layout', function(){ 6 | layout = testTools.createLayout({ 7 | content: [{ 8 | type: 'stack', 9 | content: [{ 10 | type: 'component', 11 | componentName: 'testComponent', 12 | id: 'componentA' 13 | }, 14 | { 15 | type: 'component', 16 | componentName: 'testComponent', 17 | id: 'componentB' 18 | }] 19 | }] 20 | }); 21 | 22 | expect( layout.isInitialised ).toBe( true ); 23 | }); 24 | 25 | it( 'opens testComponent in a new window', function(){ 26 | expect( layout.openPopouts.length ).toBe( 0 ); 27 | var component = layout.root.getItemsById( 'componentA' )[ 0 ]; 28 | browserPopout = component.popout(); 29 | 30 | expect( browserPopout.getWindow().closed ).toBe( false ); 31 | expect( layout.openPopouts.length ).toBe( 1 ); 32 | }); 33 | 34 | /** 35 | * TODO This test doens't run since karma injects 36 | * all sorts of stuff into the new window which throws errors 37 | * before GoldenLayout can initialise... 38 | */ 39 | /* global xit */ 40 | xit( 'serialises the new window', function(){ 41 | expect( layout.openPopouts.length ).toBe( 1 ); 42 | 43 | waitsFor(function(){ 44 | return layout.openPopouts[ 0 ].isInitialised; 45 | }); 46 | 47 | runs(function(){ 48 | var config = layout.toConfig(); 49 | expect( config.openPopouts.length ).toBe( 1 ); 50 | expect( typeof config.openPopouts[ 0 ].left ).toBe( 'number'); 51 | expect( typeof config.openPopouts[ 0 ].top ).toBe( 'number'); 52 | expect( config.openPopouts[ 0 ].width > 0 ).toBe( true ); 53 | expect( config.openPopouts[ 0 ].height > 0 ).toBe( true ); 54 | expect( config.openPopouts[ 0 ].config.content[ 0 ].type ).toBe( 'component' ); 55 | }); 56 | }); 57 | 58 | xit( 'closes the open window', function(){ 59 | runs(function(){ 60 | browserPopout.close(); 61 | }); 62 | 63 | waitsFor(function(){ 64 | return browserPopout.getWindow().closed && 65 | layout.openPopouts.length === 0; 66 | }); 67 | }); 68 | 69 | it( 'destroys the layout', function(){ 70 | layout.destroy(); 71 | }); 72 | }); -------------------------------------------------------------------------------- /test/tree-manipulation-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'The layout can be manipulated at runtime', function(){ 2 | 3 | var myLayout; 4 | 5 | it('Creates an initial layout', function(){ 6 | myLayout = testTools.createLayout({ 7 | content: [{ 8 | type: 'component', 9 | componentName: 'testComponent' 10 | }] 11 | }); 12 | }); 13 | 14 | it( 'has the right initial structure', function(){ 15 | testTools.verifyPath( 'stack.0.component', myLayout, expect ); 16 | }); 17 | 18 | it( 'adds a child to the stack', function(){ 19 | myLayout.root.contentItems[ 0 ].addChild({ 20 | type: 'component', 21 | componentName: 'testComponent' 22 | }); 23 | 24 | expect( myLayout.root.contentItems[ 0 ].contentItems.length ).toBe( 2 ); 25 | testTools.verifyPath( 'stack.1.component', myLayout, expect ); 26 | }); 27 | 28 | it( 'replaces a component with a row of components', function(){ 29 | 30 | var oldChild = myLayout.root.contentItems[ 0 ].contentItems[ 1 ]; 31 | var newChild = { 32 | type: 'row', 33 | content: [{ 34 | type: 'component', 35 | componentName: 'testComponent' 36 | }, 37 | { 38 | type: 'component', 39 | componentName: 'testComponent' 40 | }] 41 | }; 42 | 43 | myLayout.root.contentItems[ 0 ].replaceChild( oldChild, newChild ); 44 | 45 | testTools.verifyPath( 'stack.1.row.0.stack.0.component', myLayout, expect ); 46 | testTools.verifyPath( 'stack.1.row.1.stack.0.component', myLayout, expect ); 47 | }); 48 | 49 | it( 'Has setup parents correctly', function(){ 50 | var component = testTools.verifyPath( 'stack.1.row.1.stack.0.component', myLayout, expect ); 51 | expect( component.isComponent ).toBe( true ); 52 | expect( component.parent.isStack ).toBe( true ); 53 | expect( component.parent.parent.isRow ).toBe( true ); 54 | expect( component.parent.parent.parent.isStack ).toBe( true ); 55 | expect( component.parent.parent.parent.parent.isRoot ).toBe( true ); 56 | }); 57 | 58 | it( 'Destroys a component and its parent', function(){ 59 | var stack = testTools.verifyPath( 'stack.1.row.1.stack', myLayout, expect ); 60 | expect( stack.contentItems.length ).toBe( 1 ); 61 | stack.contentItems[ 0 ].remove(); 62 | expect( stack.contentItems.length ).toBe( 0 ); 63 | }); 64 | }); -------------------------------------------------------------------------------- /src/js/items/Component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {[type]} layoutManager [description] 3 | * @param {[type]} config [description] 4 | * @param {[type]} parent [description] 5 | */ 6 | lm.items.Component = function( layoutManager, config, parent ) { 7 | lm.items.AbstractContentItem.call( this, layoutManager, config, parent ); 8 | 9 | var ComponentConstructor = layoutManager.getComponent( this.config.componentName ), 10 | componentConfig = $.extend( true, {}, this.config.componentState || {} ); 11 | 12 | componentConfig.componentName = this.config.componentName; 13 | this.componentName = this.config.componentName; 14 | 15 | if( this.config.title === '' ) { 16 | this.config.title = this.config.componentName; 17 | } 18 | 19 | this.isComponent = true; 20 | this.container = new lm.container.ItemContainer( this.config, this, layoutManager ); 21 | this.instance = new ComponentConstructor( this.container, componentConfig ); 22 | this.element = this.container._element; 23 | }; 24 | 25 | lm.utils.extend( lm.items.Component, lm.items.AbstractContentItem ); 26 | 27 | lm.utils.copy( lm.items.Component.prototype, { 28 | 29 | close: function() { 30 | this.parent.removeChild( this ); 31 | }, 32 | 33 | setSize: function() { 34 | if( this.element.is( ':visible' ) ) { 35 | // Do not update size of hidden components to prevent unwanted reflows 36 | this.container._$setSize( this.element.width(), this.element.height() ); 37 | } 38 | }, 39 | 40 | _$init: function() { 41 | lm.items.AbstractContentItem.prototype._$init.call( this ); 42 | this.container.emit( 'open' ); 43 | }, 44 | 45 | _$hide: function() { 46 | this.container.hide(); 47 | lm.items.AbstractContentItem.prototype._$hide.call( this ); 48 | }, 49 | 50 | _$show: function() { 51 | this.container.show(); 52 | lm.items.AbstractContentItem.prototype._$show.call( this ); 53 | }, 54 | 55 | _$shown: function() { 56 | this.container.shown(); 57 | lm.items.AbstractContentItem.prototype._$shown.call( this ); 58 | }, 59 | 60 | _$destroy: function() { 61 | this.container.emit( 'destroy', this ); 62 | lm.items.AbstractContentItem.prototype._$destroy.call( this ); 63 | }, 64 | 65 | /** 66 | * Dragging onto a component directly is not an option 67 | * 68 | * @returns null 69 | */ 70 | _$getArea: function() { 71 | return null; 72 | } 73 | } ); 74 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | /** 2 | * Airbnb JSHint settings for use with SublimeLinter and Sublime Text 2. 3 | * 4 | * 1. Install SublimeLinter at https://github.com/SublimeLinter/SublimeLinter 5 | * 2. Open user preferences for the SublimeLinter package in Sublime Text 2 6 | * * For Mac OS X go to _Sublime Text 2_ > _Preferences_ > _Package Settings_ > _SublimeLinter_ > _Settings - User_ 7 | * 3. Paste the contents of this file into your settings file 8 | * 4. Save the settings file 9 | * 10 | * @version 0.3.0 11 | * @see https://github.com/SublimeLinter/SublimeLinter 12 | * @see http://www.jshint.com/docs/ 13 | */ 14 | { 15 | /* 16 | * ENVIRONMENTS 17 | * ================= 18 | */ 19 | 20 | // Define globals exposed by modern browsers. 21 | "browser": true, 22 | 23 | // Define globals exposed by jQuery. 24 | "jquery": true, 25 | 26 | // Define globals exposed by Node.js. 27 | "node": true, 28 | 29 | "predef": [ "lm" ], 30 | 31 | "globals" : { 32 | /* MOCHA */ 33 | "ko" : false, 34 | "describe" : false, 35 | "it" : false, 36 | "before" : false, 37 | "beforeEach" : false, 38 | "after" : false, 39 | "afterEach" : false, 40 | "expect" : false, 41 | "runs" : false, 42 | "waitsFor" : false, 43 | "testTools" : false, 44 | "spyOn" : false 45 | }, 46 | 47 | /* 48 | * ENFORCING OPTIONS 49 | * ================= 50 | */ 51 | 52 | // Force all variable names to use either camelCase style or UPPER_CASE 53 | // with underscores. 54 | "camelcase": true, 55 | 56 | // Prohibit use of == and != in favor of === and !==. 57 | "eqeqeq": true, 58 | 59 | // Suppress warnings about == null comparisons. 60 | "eqnull": true, 61 | 62 | 63 | // Prohibit use of a variable before it is defined. 64 | "latedef": true, 65 | 66 | // Require capitalized names for constructor functions. 67 | "newcap": true, 68 | 69 | // Enforce use of single quotation marks for strings. 70 | "quotmark": "single", 71 | 72 | // Prohibit trailing whitespace. 73 | "trailing": true, 74 | 75 | // Prohibit use of explicitly undeclared variables. 76 | "undef": true, 77 | 78 | // Warn when variables are defined but never used. 79 | "unused": true, 80 | 81 | // Enforce line length to 80 characters 82 | "maxlen": 120 83 | } -------------------------------------------------------------------------------- /test/item-creation-events-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'emits events when items are created', function(){ 2 | 3 | var layout, eventListener = window.jasmine.createSpyObj( 'eventListener', [ 4 | 'onItemCreated', 5 | 'onStackCreated', 6 | 'onComponentCreated', 7 | 'onRowCreated', 8 | 'onColumnCreated', 9 | ]); 10 | 11 | it( 'creates a layout', function(){ 12 | layout = new window.GoldenLayout({ 13 | content: [{ 14 | type: 'stack', 15 | content: [{ 16 | type: 'column', 17 | content:[{ 18 | type: 'component', 19 | componentName: 'testComponent' 20 | }] 21 | },{ 22 | type: 'row' 23 | }] 24 | }] 25 | }); 26 | 27 | layout.registerComponent( 'testComponent', testTools.TestComponent ); 28 | }); 29 | 30 | it( 'registeres listeners', function(){ 31 | expect( eventListener.onItemCreated ).not.toHaveBeenCalled(); 32 | expect( eventListener.onStackCreated ).not.toHaveBeenCalled(); 33 | expect( eventListener.onRowCreated ).not.toHaveBeenCalled(); 34 | expect( eventListener.onColumnCreated ).not.toHaveBeenCalled(); 35 | expect( eventListener.onComponentCreated ).not.toHaveBeenCalled(); 36 | 37 | layout.on( 'itemCreated', eventListener.onItemCreated ); 38 | layout.on( 'stackCreated', eventListener.onStackCreated ); 39 | layout.on( 'rowCreated', eventListener.onRowCreated ); 40 | layout.on( 'columnCreated', eventListener.onColumnCreated ); 41 | layout.on( 'componentCreated', eventListener.onComponentCreated ); 42 | 43 | layout.init(); 44 | }); 45 | 46 | it( 'has called listeners', function(){ 47 | expect( eventListener.onItemCreated.calls.length ).toBe( 6 ); 48 | expect( eventListener.onStackCreated.calls.length ).toBe( 2 ); 49 | expect( eventListener.onRowCreated.calls.length ).toBe( 1 ); 50 | expect( eventListener.onColumnCreated.calls.length ).toBe( 1 ); 51 | expect( eventListener.onComponentCreated.calls.length ).toBe( 1 ); 52 | }); 53 | 54 | it( 'provided the right arguments', function(){ 55 | expect( eventListener.onComponentCreated.mostRecentCall.args[0].type ).toEqual( 'component' ); 56 | expect( eventListener.onStackCreated.mostRecentCall.args[0].type ).toEqual( 'stack' ); 57 | expect( eventListener.onColumnCreated.mostRecentCall.args[0].type ).toEqual( 'column' ); 58 | expect( eventListener.onRowCreated.mostRecentCall.args[0].type ).toEqual( 'row' ); 59 | }); 60 | 61 | it( 'destroys the layout', function(){ 62 | layout.destroy(); 63 | }); 64 | }); -------------------------------------------------------------------------------- /src/css/goldenlayout-base.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["src/less/goldenlayout-base.less"],"names":[],"mappings":"AAkBA,SACE,kBAGF,OAAQ,UACN,WAIF,YACE,eAAA,CACA,kBAIF,aACA,YAAa,GACX,WAAA,YACA,iBAIF,cACE,iBAAA,CACA,KAAA,CACA,MAAA,CACA,WAGF,yBACE,aAIF,aACE,iBAAA,CACA,WAEA,YAAC,OACD,YAAC,aACC,kBAGF,YAAC,YACC,iBACE,UAAA,CACA,iBAAA,CACA,iBAIJ,YAAC,eACC,UAAA,CACA,YAFF,YAAC,cAIC,iBACE,WAAA,CACA,iBAAA,CACA,iBAMN,WACE,gBAAA,CACA,iBAAA,CACA,UAHF,UAKE,cACE,sBAAA,YANJ,UAUE,cACE,iBAAA,CACA,UAZJ,UAUE,aAIE,IACE,cAAA,CACA,UAAA,CACA,UAAA,CACA,WAAA,CACA,kBAnBN,UAuBE,IACE,QAAA,CACA,SAAA,CACA,qBA1BJ,UA6BE,UACE,kBA9BJ,UAkCE,SACE,cAAA,CACA,UAAA,CACA,WAAA,CACA,cAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAzCJ,UAkCE,QASE,GACE,SAAA,CACA,WAAA,CACA,kBAEA,UAdJ,QASE,EAKG,SACC,KAAA,CACA,UAGF,UAnBJ,QASE,EAUG,UACC,KAAA,CACA,WAvDR,UAkCE,QAyBE,WACE,oBAAA,CACA,eAAA,CACA,uBA9DN,UAkCE,QAgCE,eACE,UAAA,CACA,WAAA,CACA,iBAAA,CACA,KAAA,CACA,OAAA,CACA,kBAMN,SAAS,QAEP,YADF,SAAS,SACP,YACE,YAIJ,aAAa,QAIX,YAHF,aAAa,SAGX,YAFF,SAAS,QAEP,YADF,SAAS,SACP,YACE,UAAA,CACA,UAAA,CACA,mBAPJ,aAAa,QAIX,WAIE,UAPJ,aAAa,SAGX,WAIE,UANJ,SAAS,QAEP,WAIE,UALJ,SAAS,SACP,WAIE,UACE,yBAAA,CACA,KAAA,CACA,aAXN,aAAa,QAIX,WASE,cAZJ,aAAa,SAGX,WASE,cAXJ,SAAS,QAEP,WASE,cAVJ,SAAS,SACP,WASE,cACE,SAdN,aAAa,QAiBX,WAhBF,aAAa,SAgBX,WAfF,SAAS,QAeP,WAdF,SAAS,SAcP,WACE,WAIJ,aAAa,QAEX,WACE,UAFJ,SAAS,QACP,WACE,UACE,UAAW,eAAe,UAA1B,CACA,OALN,aAAa,QAEX,WACE,SAGE,SALN,SAAS,QACP,WACE,SAGE,SACE,UAAW,UAAX,CACA,eARR,aAAa,QAEX,WASE,sBAVJ,SAAS,QACP,WASE,sBACE,WAAA,CACA,aAAA,CACA,UAKN,aAAa,SAAU,aACrB,WAGF,aAAa,SAEX,WACE,UAFJ,SAAS,SACP,WACE,UACE,UAAW,cAAc,SAAzB,CACA,SAAA,CACA,cANN,aAAa,SAEX,WAME,cAPJ,SAAS,SACP,WAME,cACE,SATN,aAAa,SAEX,WASE,sBAVJ,SAAS,SACP,WASE,sBACE,WAAA,CACA,WAKN,aAAa,UAEX,WACE,SAFJ,SAAS,UACP,WACE,SACE,YAAA,CACA,gBALN,aAAa,UAEX,WAKE,cANJ,SAAS,UACP,WAKE,cACE,QARN,aAAa,UAEX,WAQE,sBATJ,SAAS,UACP,WAQE,sBACE,WAAA,CACA,YAKN,yBACE,UAAA,CACA,WAAA,CACA,WAAA,CACA,kBAIF,UACE,aAAa,gBAAe,QAC1B,QAAS,EAAT,CACA,OAAA,CACA,QAAA,CACA,qBAAA,CACA,oBAAA,CACA,qBAAA,CACA,kCAAA,CACA,iCAAA,CACA,YAVJ,UAaE,sBACE,iBAAA,CACA,QAAA,CACA,OAAA,CACA,SAAA,CACA,gBAlBJ,UAaE,qBAOE,SACE,UAAA,CACA,kBAAA,CACA,SAvBN,UAaE,qBAOE,QAKE,WACE,YA1BR,UAaE,qBAiBE,eACE,YAAA,YAUN,cACE,iBAAA,CACA,KAAA,CACA,MAAA,CACA,WAJF,aAME,YACE,uBAPJ,aAUE,aACE,eAAA,CACA,gBAKJ,wBACE,YAAA,CACA,iBAAA,CACA,WAHF,uBAME,WACE,UAAA,CACA,WAAA,CACA,iBAAA,CACA,KAAA,CACA,OAIJ,yBACE,YAAA,CACA,UAAA,CACA,WAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,WAIF,UACE,UAAA,CACA,WAAA,CACA,iBAAA,CACA,QAAA,CACA,OAAA,CACA,aANF,SAQE,GACE,UAAA,CACA,WAAA,CACA,iBAAA,CACA,KAAA,CACA,OAbJ,SAgBE,QACE,WAjBJ,SAoBE,UACE"} -------------------------------------------------------------------------------- /test/title-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'content items are abled to to emit events that bubble up the tree', function(){ 2 | 3 | var layout, itemWithTitle, itemWithoutTitle, stack; 4 | 5 | it( 'creates a layout', function(){ 6 | layout = testTools.createLayout({ 7 | content: [{ 8 | type: 'stack', 9 | content: [{ 10 | type: 'component', 11 | componentName: 'testComponent', 12 | title: 'First Title', 13 | id: 'hasTitle' 14 | }, 15 | { 16 | type: 'component', 17 | componentName: 'testComponent', 18 | id: 'noTitle' 19 | }] 20 | }] 21 | }); 22 | 23 | expect( layout.isInitialised ).toBe( true ); 24 | }); 25 | 26 | it( 'applies titles from configuration', function(){ 27 | itemWithTitle = layout.root.getItemsById( 'hasTitle' )[ 0 ]; 28 | itemWithoutTitle = layout.root.getItemsById( 'noTitle' )[ 0 ]; 29 | 30 | expect( itemWithTitle.config.title ).toBe( 'First Title' ); 31 | expect( itemWithoutTitle.config.title ).toBe( 'testComponent' ); 32 | }); 33 | 34 | it( 'displays the title on the tab', function() { 35 | stack = layout.root.getItemsByType( 'stack' )[ 0 ]; 36 | expect( stack.header.tabs.length ).toBe( 2 ); 37 | expect( stack.header.tabs[ 0 ].element.find( '.lm_title' ).html() ).toBe( 'First Title' ); 38 | expect( stack.header.tabs[ 1 ].element.find( '.lm_title' ).html() ).toBe( 'testComponent' ); 39 | }); 40 | 41 | it( 'updates the title when calling setTitle on the item', function() { 42 | itemWithTitle.setTitle( 'Second Title' ); 43 | expect( stack.header.tabs[ 0 ].element.find( '.lm_title' ).html() ).toBe( 'Second Title' ); 44 | }); 45 | 46 | it( 'updates the title when calling setTitle from the container', function() { 47 | itemWithTitle.container.setTitle( 'Third Title' ); 48 | expect( stack.header.tabs[ 0 ].element.find( '.lm_title' ).html() ).toBe( 'Third Title' ); 49 | }); 50 | 51 | it( 'Persists the title', function() { 52 | expect( layout.toConfig().content[ 0 ].content[ 0 ].title ).toBe( 'Third Title' ); 53 | }); 54 | 55 | it( 'supports html in title', function() { 56 | itemWithTitle.container.setTitle( 'title with html' ); 57 | expect( stack.header.tabs[ 0 ].element.find( '.lm_title' ).html() ).toBe( 'title with html' ); 58 | expect( stack.header.tabs[ 0 ].element.find( '.lm_title' ).text() ).toBe( 'title with html' ); 59 | expect( stack.header.tabs[ 0 ].element.attr( 'title' ) ).toBe( 'title with html' ); 60 | }); 61 | 62 | it( 'destroys the layout', function(){ 63 | layout.destroy(); 64 | }); 65 | }); -------------------------------------------------------------------------------- /test/id-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'Dynamic ids work properly', function(){ 2 | var layout, item; 3 | 4 | it( 'creates a layout', function(){ 5 | layout = testTools.createLayout({ 6 | content: [{ 7 | type: 'component', 8 | componentName: 'testComponent' 9 | }] 10 | }); 11 | }); 12 | 13 | it( 'finds the item', function(){ 14 | item = layout.root.contentItems[ 0 ].contentItems[ 0 ]; 15 | expect( item.isComponent ).toBe( true ); 16 | }); 17 | 18 | it( 'has no id initially', function(){ 19 | expect( item.config.id ).toBe( undefined ); 20 | expect( item.hasId( 'id_1' ) ).toBe( false ); 21 | expect( item.hasId( 'id_2' ) ).toBe( false ); 22 | }); 23 | 24 | it( 'adds the first id as a string', function(){ 25 | item.addId( 'id_1' ); 26 | expect( item.hasId( 'id_1' ) ).toBe( true ); 27 | expect( item.hasId( 'id_2' ) ).toBe( false ); 28 | expect( item.config.id ).toBe( 'id_1' ); 29 | expect( layout.root.getItemsById( 'id_1' )[ 0 ] ).toBe( item ); 30 | }); 31 | 32 | it( 'adds the second id to an array', function(){ 33 | item.addId( 'id_2' ); 34 | expect( item.config.id instanceof Array ).toBe( true ); 35 | expect( item.config.id.length ).toBe( 2 ); 36 | expect( item.config.id[ 0 ] ).toBe( 'id_1' ); 37 | expect( item.config.id[ 1 ] ).toBe( 'id_2' ); 38 | expect( item.hasId( 'id_1' ) ).toBe( true ); 39 | expect( item.hasId( 'id_2' ) ).toBe( true ); 40 | expect( layout.root.getItemsById( 'id_1' )[ 0 ] ).toBe( item ); 41 | expect( layout.root.getItemsById( 'id_2' )[ 0 ] ).toBe( item ); 42 | }); 43 | 44 | it( 'doesn\t add duplicated ids', function(){ 45 | item.addId( 'id_2' ); 46 | expect( item.config.id instanceof Array ).toBe( true ); 47 | expect( item.config.id.length ).toBe( 2 ); 48 | expect( item.config.id[ 0 ] ).toBe( 'id_1' ); 49 | expect( item.config.id[ 1 ] ).toBe( 'id_2' ); 50 | expect( layout.root.getItemsById( 'id_1' )[ 0 ] ).toBe( item ); 51 | expect( layout.root.getItemsById( 'id_2' )[ 0 ] ).toBe( item ); 52 | }); 53 | 54 | it( 'removes ids', function(){ 55 | item.removeId( 'id_2' ); 56 | expect( item.hasId( 'id_1' ) ).toBe( true ); 57 | expect( item.hasId( 'id_2' ) ).toBe( false ); 58 | expect( item.config.id.length ).toBe( 1 ); 59 | }); 60 | 61 | it( 'throws error when trying to remove a non-existant id', function(){ 62 | var error; 63 | 64 | try{ 65 | item.removeId( 'id_2' ); 66 | } catch( e ) { 67 | error = e; 68 | } 69 | 70 | expect( error ).toBeDefined(); 71 | }); 72 | 73 | it( 'destroys the layout', function(){ 74 | layout.destroy(); 75 | }); 76 | }); -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "statements" 6 | ], 7 | "class-name": false, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": false, 15 | "indent": [ 16 | true, 17 | "spaces" 18 | ], 19 | "interface-name": [true, "never-prefix"], 20 | "jsdoc-format": true, 21 | "label-position": true, 22 | "label-undefined": true, 23 | "member-access": false, 24 | "member-ordering": [false], 25 | "no-any": false, 26 | "no-arg": false, 27 | "no-bitwise": false, 28 | "no-conditional-assignment": true, 29 | "no-consecutive-blank-lines": false, 30 | "no-console": [ 31 | true, 32 | "assert", 33 | "count", 34 | "log", 35 | "warn", 36 | "trace", 37 | "error", 38 | "debug" 39 | ], 40 | "no-construct": true, 41 | "no-constructor-vars": false, 42 | "no-debugger": true, 43 | "no-duplicate-variable": true, 44 | "no-empty": false, 45 | "no-eval": true, 46 | "no-inferrable-types": false, 47 | "no-internal-module": true, 48 | "no-null-keyword": false, 49 | "no-require-imports": false, 50 | "no-shadowed-variable": true, 51 | "no-string-literal": true, 52 | "no-switch-case-fall-through": true, 53 | "no-trailing-whitespace": true, 54 | "no-unreachable": true, 55 | "no-unused-expression": false, 56 | "no-unused-variable": true, 57 | "no-use-before-declare": false, 58 | "no-var-keyword": true, 59 | "no-var-requires": true, 60 | "object-literal-sort-keys": false, 61 | "one-line": [ 62 | true, 63 | "check-open-brace", 64 | "check-whitespace" 65 | ], 66 | "quotemark": [ 67 | true, 68 | "single", 69 | "avoid-escape" 70 | ], 71 | "radix": false, 72 | "semicolon": [ 73 | true, 74 | "always" 75 | ], 76 | "switch-default": true, 77 | "trailing-comma": [ 78 | true, 79 | { 80 | "singleline": "never" 81 | } 82 | ], 83 | "triple-equals": [ 84 | true, 85 | "allow-null-check" 86 | ], 87 | "typedef": [ 88 | false 89 | ], 90 | "typedef-whitespace": [ 91 | false 92 | ], 93 | "use-strict": [false], 94 | "variable-name": [ 95 | true, 96 | "check-format", 97 | "allow-leading-underscore", 98 | "ban-keywords" 99 | ], 100 | "whitespace": [ 101 | false 102 | ] 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "golden-layout", 3 | "version": "1.5.9", 4 | "author": "deepstreamHub GmbH", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@types/zepto": "^1.0.29", 8 | "grunt": "~0.4.2", 9 | "grunt-contrib-less": "^1.4.1", 10 | "grunt-contrib-watch": "0.5.3", 11 | "grunt-gulp": "^1.0.1", 12 | "grunt-karma": "^2.0.0", 13 | "grunt-release": "0.13.*", 14 | "gulp": "^3.9.1", 15 | "gulp-concat": "^2.6.0", 16 | "gulp-continuous-concat": "^0.1.1", 17 | "gulp-insert": "^0.5.0", 18 | "gulp-uglify": "^1.5.3", 19 | "gulp-watch": "^4.3.5", 20 | "handlebars": "2.0.0-alpha.2", 21 | "karma": "0.13.*", 22 | "karma-chrome-launcher": "~0.1.4", 23 | "karma-coverage": "0.2.4", 24 | "karma-firefox-launcher": "^1.0.0", 25 | "karma-ie-launcher": "~0.1.5", 26 | "karma-jasmine": "~0.1.5", 27 | "karma-phantomjs-launcher": "^1.0.2", 28 | "tslint": "^5.7.0", 29 | "typescript": "^2.5.3", 30 | "walker": "1.0.6" 31 | }, 32 | "description": "A multi-screen javascript Layout manager \r https://golden-layout.com", 33 | "main": "./dist/goldenlayout.js", 34 | "types": "./index.d.ts", 35 | "directories": { 36 | "test": "test" 37 | }, 38 | "dependencies": { 39 | "zepto": "*" 40 | }, 41 | "scripts": { 42 | "test": "grunt test && npm run tslint", 43 | "tslint": "tslint --fix \"**/*.ts\" -e \"node_modules/**\"", 44 | "publish": "npm -s run tslint" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/deepstreamIO/golden-layout.git" 49 | }, 50 | "keywords": [ 51 | "layout manager", 52 | "javascript", 53 | "docker", 54 | "layout", 55 | "popouts" 56 | ], 57 | "bugs": { 58 | "url": "https://github.com/deepstreamIO/golden-layout/issues" 59 | }, 60 | "homepage": "https://github.com/deepstreamIO/golden-layout", 61 | "npmName": "golden-layout", 62 | "npmFileMap": [ 63 | { 64 | "basePath": "/dist/", 65 | "files": [ 66 | "goldenlayout.js", 67 | "goldenlayout.min.js" 68 | ] 69 | }, 70 | { 71 | "basePath": "/src/css/", 72 | "files": [ 73 | "goldenlayout.base.css", 74 | "goldenlayout-dark-theme.css", 75 | "goldenlayout-light-theme.css" 76 | ] 77 | }, 78 | { 79 | "basePath": "/", 80 | "files": [ 81 | "typings.json" 82 | ] 83 | } 84 | ], 85 | "jspm": { 86 | "main": "dist/goldenlayout", 87 | "format": "global", 88 | "registry": "jspm", 89 | "dependencies": { 90 | "jquery": "^2.1.0" 91 | }, 92 | "shim": { 93 | "dist/goldenlayout": [ 94 | "jquery" 95 | ] 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/selector-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'it is possible to select elements from the tree using selectors', function(){ 2 | 3 | var layout; 4 | 5 | it( 'creates a layout with elements that have ids', function(){ 6 | var config = { 7 | content: [ 8 | { 9 | type: 'column', 10 | content:[ 11 | { 12 | type: 'component', 13 | id: 'simpleStringId', 14 | componentName: 'testComponent' 15 | }, 16 | { 17 | type: 'column', 18 | id: [ 'outerColumn', 'groupA', 'groupB' ], 19 | content: [ 20 | { 21 | type: 'column', 22 | id: [ 'groupB' ] 23 | } 24 | ] 25 | } 26 | ] 27 | } 28 | ] 29 | }; 30 | layout = testTools.createLayout( config ); 31 | testTools.verifyPath( 'column.0.stack.0.component', layout, expect ); 32 | testTools.verifyPath( 'column.1.column.0.column', layout, expect ); 33 | }); 34 | 35 | it( 'finds an item by string id', function(){ 36 | expect( layout.isInitialised ).toBe( true ); 37 | var items = layout.root.getItemsById( 'simpleStringId' ); 38 | expect( items.length ).toBe( 1 ); 39 | expect( items[ 0 ].isComponent ).toBe( true ); 40 | }); 41 | 42 | it( 'returns an empty array if no item was found for id', function(){ 43 | var items = layout.root.getItemsById( 'doesNotExist' ); 44 | expect( items instanceof Array ).toBe( true ); 45 | expect( items.length ).toBe( 0 ); 46 | }); 47 | 48 | it( 'finds items by an id from an array', function(){ 49 | var items = layout.root.getItemsById( 'groupB' ); 50 | expect( items.length ).toBe( 2 ); 51 | 52 | items = layout.root.getItemsById( 'groupA' ); 53 | expect( items.length ).toBe( 1 ); 54 | }); 55 | 56 | it( 'finds items by type', function(){ 57 | var items = layout.root.getItemsByType( 'column' ); 58 | expect( items.length ).toBe( 3 ); 59 | expect( items[ 0 ].type ).toBe( 'column' ); 60 | expect( items[ 1 ].type ).toBe( 'column' ); 61 | }); 62 | 63 | it( 'returns an empty array if no item was found for type', function(){ 64 | var items = layout.root.getItemsByType( 'row' ); 65 | expect( items instanceof Array ).toBe( true ); 66 | expect( items.length ).toBe( 0 ); 67 | }); 68 | 69 | it( 'finds the component instance by name', function(){ 70 | var components = layout.root.getComponentsByName( 'testComponent' ); 71 | expect( components.length ).toBe( 1 ); 72 | expect( components[ 0 ].isTestComponentInstance ).toBe( true ); 73 | }); 74 | 75 | it( 'allows for chaining', function(){ 76 | var innerColumns = layout.root.getItemsById( 'outerColumn' )[ 0 ] 77 | .getItemsByType( 'column' ); 78 | 79 | expect( innerColumns.length ).toBe( 1 ); 80 | expect( innerColumns[ 0 ].type ).toBe( 'column' ); 81 | }); 82 | }); -------------------------------------------------------------------------------- /src/css/goldenlayout-translucent-theme.css: -------------------------------------------------------------------------------- 1 | .lm_goldenlayout{background:#dodgerblue;background:linear-gradient(to right bottom, dodgerblue, palevioletred)}.lm_content{background:rgba(255,255,255,0.1);box-shadow:0 0 15px 2px rgba(0,0,0,0.1);color:whitesmoke}.lm_dragProxy .lm_content{box-shadow:2px 2px 4px rgba(0,0,0,0.9)}.lm_dropTargetIndicator{box-shadow:inset 0 0 20px rgba(255,255,255,0.5);outline:1px dashed #ffffff;margin:1px;transition:all 200ms ease}.lm_splitter{background:#ffffff;opacity:.001;transition:opacity 200ms ease}.lm_splitter:hover,.lm_splitter.lm_dragging{background:#ffffff;opacity:.4}.lm_header{height:20px}.lm_header.lm_selectable{cursor:pointer}.lm_header .lm_tab{font-family:Arial,sans-serif;font-size:13px;color:#ffffff;background:rgba(255,255,255,0.1);margin-right:2px;padding-bottom:4px}.lm_header .lm_tab .lm_close_tab{width:11px;height:11px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAATElEQVR4nG3OwQ0DMQwDwZGRBtR/j1YJzMc5+IDoR+yCVO29g+pu981MFgqZmRdAfU7+CYWcbF11LwALjpBL0N0qybNx/RPU+gOeiS/+XCRwDlTgkQAAAABJRU5ErkJggg==);background-position:center center;background-repeat:no-repeat;right:6px;top:4px;opacity:.4}.lm_header .lm_tab .lm_close_tab:hover{opacity:1}.lm_header .lm_tab.lm_active{border-bottom:none;box-shadow:2px -2px 2px -2px rgba(0,0,0,0.2);padding-bottom:5px}.lm_header .lm_tab.lm_active .lm_close_tab{opacity:1}.lm_dragProxy.lm_bottom .lm_header .lm_tab.lm_active,.lm_stack.lm_bottom .lm_header .lm_tab.lm_active{box-shadow:2px 2px 2px -2px rgba(0,0,0,0.2)}.lm_tab:hover,.lm_tab.lm_active{background:rgba(255,255,255,0.3);color:#ffffff}.lm_controls>li{position:relative;background-position:center center;background-repeat:no-repeat;opacity:.4;transition:opacity 300ms ease}.lm_controls>li:hover{opacity:1}.lm_controls .lm_popout{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=)}.lm_controls .lm_maximise{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==)}.lm_controls .lm_close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=)}.lm_popin{cursor:pointer}.lm_popin .lm_icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC);background-position:center center;background-repeat:no-repeat;opacity:.7}.lm_popin:hover .lm_icon{opacity:1}.lm_item{box-shadow:2px 2px 2px rgba(0,0,0,0.1)}/*# sourceMappingURL=goldenlayout-translucent-theme.css.map */ -------------------------------------------------------------------------------- /src/css/default-theme.css: -------------------------------------------------------------------------------- 1 | 2 | .lm_header, 3 | .lm_header .lm_tab{ 4 | background: -webkit-linear-gradient(#dadada, #f6f6f6); 5 | /*background: url(http://subtlepatterns.com/patterns/p2.png);*/ 6 | } 7 | 8 | .lm_splitter{ 9 | background: -webkit-linear-gradient(#fff, #eee); 10 | } 11 | 12 | .lm_header{ 13 | border-bottom: 1px solid #ccc; 14 | height: 19px !important; 15 | } 16 | 17 | 18 | 19 | 20 | /*.lm_splitter.lm_vertical{ 21 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAFCAIAAAAL5hHIAAAAHElEQVR4nGNYtGgJk7a2JtPPnz+ZGBgYmFhYWABG7AVnGY/wNAAAAABJRU5ErkJggg==); 22 | background-repeat: repeat-x; 23 | } 24 | */ 25 | 26 | .lm_header .lm_tab{ 27 | color: #4c4c51; 28 | font-size: 13px; 29 | font-weight: bold; 30 | border-bottom: 1px solid #ccc; 31 | border-right: 1px solid #ccc; 32 | padding-bottom: 4px; 33 | font-family: Arial, sans-serif; 34 | } 35 | 36 | .lm_header .lm_tab i.lm_left{ 37 | left: -2px; 38 | top: 0; 39 | } 40 | 41 | .lm_header .lm_tab i.lm_right{ 42 | right: -2px; 43 | top: 0; 44 | width: 0; 45 | z-index: 1; 46 | 47 | } 48 | 49 | .lm_header .lm_tab.active i.lm_right{ 50 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAABCAYAAAD5PA/NAAAAGUlEQVR4nGNkYGAwZ2BgeMXAwPCBgYHhCwAQywMH5I3pAgAAAABJRU5ErkJggg==); 51 | background-repeat: repeat-y; 52 | width: 4px; 53 | right: -5px; 54 | border:none; 55 | } 56 | 57 | .lm_header .lm_tab.active i.lm_left{ 58 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAABCAYAAAD5PA/NAAAAGUlEQVR4nGNkYGBgZWBg4GFgYBBgYGAQAwABNQA5SO3S/AAAAABJRU5ErkJggg==); 59 | background-repeat: y; 60 | z-index: 1; 61 | width: 4px; 62 | left: -5px; 63 | } 64 | 65 | .lm_header .lm_tab.active, 66 | .lm_header .lm_tab:hover{ 67 | color: #18181a; 68 | 69 | } 70 | 71 | .lm_header .lm_tab:hover{ 72 | background: #ccc; 73 | } 74 | 75 | .lm_header .lm_tab.active{ 76 | padding-bottom: 5px; 77 | border-bottom: none; 78 | } 79 | .lm_controls .lm_close, 80 | .lm_header .lm_tab .lm_close_tab{ 81 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAyUlEQVR4nH3QXYrCMBDA8X+bWfQ2CsruJRrQwt5CT6OncCEKc4M+SC3ri7dZ1zK+JCWoOBBCmN98kKKqlmOgBaY8hwBnYKIaEOAUE51q+EzK+/oD+AVwzl2ApgQSGHlfdy9gfzj8NMCqqKolwAjoHneI8AisAEoA1fCXTUjwlsMBv4q+7y2HAG/XAG6qYZZ3HqCIXFVDA8xjXryvzzke4H6/a4G1avjPCxaL7wuwLYEvESHB1CUWzJxzRfo6zAwz28T78Tgz26b3Hdv9aHMjH0aYAAAAAElFTkSuQmCC); 82 | } 83 | 84 | .lm_header .lm_tab .lm_close_tab{ 85 | width: 11px; 86 | height: 11px; 87 | right: 6px; 88 | top: 4px; 89 | background-repeat: no-repeat; 90 | } 91 | 92 | .lm_header .lm_tab.active .lm_close_tab, 93 | .lm_controls .lm_close:hover{ 94 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAfUlEQVR4nHWR0Q2AIAxED6Y46xAu5jhu5AQMQTrG+WEgCPUSAvRdaC9AEkgrpEHSskaWt20veNX2roldGcARwM/Zvd4AzkQa3OsHjmpGAEiSoraLEQBy9Nqgc7x0czTGXMtRGPcahk6kLakjo3u9QRpIK5Kuv09prBVD48weClyE4HksR0kAAAAASUVORK5CYII=); 95 | } 96 | 97 | .lm_controls .lm_close{ 98 | background-position: center center; 99 | background-repeat: no-repeat; 100 | position: relative; 101 | } 102 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layout Manager 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
    73 | 78 |
    79 |
    80 | 81 | -------------------------------------------------------------------------------- /src/js/items/Root.js: -------------------------------------------------------------------------------- 1 | lm.items.Root = function( layoutManager, config, containerElement ) { 2 | lm.items.AbstractContentItem.call( this, layoutManager, config, null ); 3 | this.isRoot = true; 4 | this.type = 'root'; 5 | this.element = $( '
    ' ); 6 | this.childElementContainer = this.element; 7 | this._containerElement = containerElement; 8 | this._containerElement.append( this.element ); 9 | }; 10 | 11 | lm.utils.extend( lm.items.Root, lm.items.AbstractContentItem ); 12 | 13 | lm.utils.copy( lm.items.Root.prototype, { 14 | addChild: function( contentItem ) { 15 | if( this.contentItems.length > 0 ) { 16 | throw new Error( 'Root node can only have a single child' ); 17 | } 18 | 19 | contentItem = this.layoutManager._$normalizeContentItem( contentItem, this ); 20 | this.childElementContainer.append( contentItem.element ); 21 | lm.items.AbstractContentItem.prototype.addChild.call( this, contentItem ); 22 | 23 | this.callDownwards( 'setSize' ); 24 | this.emitBubblingEvent( 'stateChanged' ); 25 | }, 26 | 27 | setSize: function( width, height ) { 28 | width = (typeof width === 'undefined') ? this._containerElement.width() : width; 29 | height = (typeof height === 'undefined') ? this._containerElement.height() : height; 30 | 31 | this.element.width( width ); 32 | this.element.height( height ); 33 | 34 | /* 35 | * Root can be empty 36 | */ 37 | if( this.contentItems[ 0 ] ) { 38 | this.contentItems[ 0 ].element.width( width ); 39 | this.contentItems[ 0 ].element.height( height ); 40 | } 41 | }, 42 | _$highlightDropZone: function( x, y, area ) { 43 | this.layoutManager.tabDropPlaceholder.remove(); 44 | lm.items.AbstractContentItem.prototype._$highlightDropZone.apply( this, arguments ); 45 | }, 46 | 47 | _$onDrop: function( contentItem, area ) { 48 | var stack; 49 | 50 | if( contentItem.isComponent ) { 51 | stack = this.layoutManager.createContentItem( { 52 | type: 'stack', 53 | header: contentItem.config.header || {} 54 | }, this ); 55 | stack._$init(); 56 | stack.addChild( contentItem ); 57 | contentItem = stack; 58 | } 59 | 60 | if( !this.contentItems.length ) { 61 | this.addChild( contentItem ); 62 | } else { 63 | var type = area.side[ 0 ] == 'x' ? 'row' : 'column'; 64 | var dimension = area.side[ 0 ] == 'x' ? 'width' : 'height'; 65 | var insertBefore = area.side[ 1 ] == '2'; 66 | var column = this.contentItems[ 0 ]; 67 | if( !column instanceof lm.items.RowOrColumn || column.type != type ) { 68 | var rowOrColumn = this.layoutManager.createContentItem( { type: type }, this ); 69 | this.replaceChild( column, rowOrColumn ); 70 | rowOrColumn.addChild( contentItem, insertBefore ? 0 : undefined, true ); 71 | rowOrColumn.addChild( column, insertBefore ? undefined : 0, true ); 72 | column.config[ dimension ] = 50; 73 | contentItem.config[ dimension ] = 50; 74 | rowOrColumn.callDownwards( 'setSize' ); 75 | } else { 76 | var sibbling = column.contentItems[ insertBefore ? 0 : column.contentItems.length - 1 ] 77 | column.addChild( contentItem, insertBefore ? 0 : undefined, true ); 78 | sibbling.config[ dimension ] *= 0.5; 79 | contentItem.config[ dimension ] = sibbling.config[ dimension ]; 80 | column.callDownwards( 'setSize' ); 81 | } 82 | } 83 | } 84 | } ); 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/css/goldenlayout-dark-theme.css: -------------------------------------------------------------------------------- 1 | .lm_goldenlayout{background:#000000}.lm_content{background:#222222}.lm_dragProxy .lm_content{box-shadow:2px 2px 4px rgba(0,0,0,0.9)}.lm_dropTargetIndicator{box-shadow:inset 0 0 30px #000000;outline:1px dashed #cccccc;transition:all 200ms ease}.lm_dropTargetIndicator .lm_inner{background:#000000;opacity:.2}.lm_splitter{background:#000000;opacity:.001;transition:opacity 200ms ease}.lm_splitter:hover,.lm_splitter.lm_dragging{background:#444444;opacity:1}.lm_header{height:20px;user-select:none}.lm_header.lm_selectable{cursor:pointer}.lm_header .lm_tab{font-family:Arial,sans-serif;font-size:12px;color:#999999;background:#111111;box-shadow:2px -2px 2px rgba(0,0,0,0.3);margin-right:2px;padding-bottom:2px;padding-top:2px}.lm_header .lm_tab .lm_close_tab{width:11px;height:11px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAATElEQVR4nG3OwQ0DMQwDwZGRBtR/j1YJzMc5+IDoR+yCVO29g+pu981MFgqZmRdAfU7+CYWcbF11LwALjpBL0N0qybNx/RPU+gOeiS/+XCRwDlTgkQAAAABJRU5ErkJggg==);background-position:center center;background-repeat:no-repeat;top:4px;right:6px;opacity:.4}.lm_header .lm_tab .lm_close_tab:hover{opacity:1}.lm_header .lm_tab.lm_active{border-bottom:none;box-shadow:0 -2px 2px #000000;padding-bottom:3px}.lm_header .lm_tab.lm_active .lm_close_tab{opacity:1}.lm_dragProxy.lm_bottom .lm_header .lm_tab,.lm_stack.lm_bottom .lm_header .lm_tab{box-shadow:2px 2px 2px rgba(0,0,0,0.3)}.lm_dragProxy.lm_bottom .lm_header .lm_tab.lm_active,.lm_stack.lm_bottom .lm_header .lm_tab.lm_active{box-shadow:0 2px 2px #000000}.lm_selected .lm_header{background-color:#452500}.lm_tab:hover,.lm_tab.lm_active{background:#222222;color:#dddddd}.lm_header .lm_controls .lm_tabdropdown:before{color:#ffffff}.lm_controls>li{position:relative;background-position:center center;background-repeat:no-repeat;opacity:.4;transition:opacity 300ms ease}.lm_controls>li:hover{opacity:1}.lm_controls .lm_popout{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=)}.lm_controls .lm_maximise{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==)}.lm_controls .lm_close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=)}.lm_maximised .lm_header{background-color:#000000}.lm_maximised .lm_controls .lm_maximise{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVR4nGP8//8/AzGAiShVI1YhCwMDA8OsWbPwBmZaWhoj0SYCAN1lBxMAX4n0AAAAAElFTkSuQmCC)}.lm_transition_indicator{background-color:#000000;border:1px dashed #555555}.lm_popin{cursor:pointer}.lm_popin .lm_bg{background:#ffffff;opacity:.3}.lm_popin .lm_icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC);background-position:center center;background-repeat:no-repeat;border-left:1px solid #eeeeee;border-top:1px solid #eeeeee;opacity:.7}.lm_popin:hover .lm_icon{opacity:1}/*# sourceMappingURL=goldenlayout-dark-theme.css.map */ -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require( 'gulp' ); 2 | var concat = require( 'gulp-concat' ); 3 | var uglify = require( 'gulp-uglify' ); 4 | var insert = require( 'gulp-insert' ); 5 | var watch = require( 'gulp-watch' ); 6 | 7 | /* global require */ 8 | module.exports = function( grunt ) { 9 | grunt.registerTask( 'build', require( './build/task' ) ); 10 | 11 | var sources = [ 12 | './build/ns.js', 13 | './src/js/utils/utils.js', 14 | './src/js/utils/EventEmitter.js', 15 | './src/js/utils/DragListener.js', 16 | './src/js/**' 17 | ]; 18 | 19 | var basicGulpStream = function( stream ) { 20 | return stream 21 | .pipe( concat( 'goldenlayout.js' ) ) 22 | .pipe( insert.wrap( '(function($){', '})(window.$);' ) ); 23 | }; 24 | 25 | // Project configuration. 26 | grunt.initConfig( { 27 | pkg: grunt.file.readJSON( 'package.json' ), 28 | 29 | /*********************** 30 | * WATCH 31 | ***********************/ 32 | watch: { 33 | tasks: [ 'dist', 'test' ], 34 | files: [ './src/**', './test/**' ], 35 | options: { livereload: 5051 }, 36 | }, 37 | 38 | /*********************** 39 | * RELEASE 40 | ***********************/ 41 | release: { 42 | options: { 43 | additionalFiles: [ 'bower.json' ], 44 | beforeRelease: [ 'less', 'gulp:gl', 'gulp:glmin' ], 45 | tagName: 'v<%= version %>', 46 | github: { 47 | repo: 'deepstreamIO/golden-layout', 48 | accessTokenVar: 'GITHUB_ACCESS_TOKEN' 49 | } 50 | } 51 | }, 52 | /*********************** 53 | * GULP 54 | ***********************/ 55 | gulp: { 56 | gl: { 57 | options: { 58 | tasks: basicGulpStream 59 | }, 60 | src: sources, 61 | dest: 'dist/goldenlayout.js' 62 | }, 63 | glmin: { 64 | options: { 65 | tasks: function( stream ) { 66 | return basicGulpStream( stream ) 67 | .pipe( uglify() ) 68 | .pipe( concat( 'goldenlayout.min.js' ) ); 69 | } 70 | }, 71 | src: sources, 72 | dest: 'dist/goldenlayout.min.js' 73 | } 74 | }, 75 | 76 | /*********************** 77 | * KARMA 78 | ***********************/ 79 | karma: { 80 | unit: { 81 | configFile: 'karma.conf.js', 82 | background: true, 83 | singleRun: false 84 | } 85 | , 86 | travis: { 87 | configFile: 'karma.conf.js', 88 | singleRun: true, 89 | browsers: [ 'PhantomJS' ] 90 | } 91 | }, 92 | 93 | less: { 94 | development: { 95 | options: { 96 | compress: true, 97 | optimization: 2, 98 | sourceMap: true 99 | }, 100 | files: [ { 101 | expand: true, 102 | flatten: true, 103 | src: "src/less/*.less", 104 | ext: ".css", 105 | dest: "src/css/" 106 | } ] 107 | } 108 | } 109 | } 110 | ); 111 | 112 | grunt.loadNpmTasks( 'grunt-contrib-less' ); 113 | grunt.loadNpmTasks( 'grunt-contrib-watch' ); 114 | grunt.loadNpmTasks( 'grunt-release' ); 115 | grunt.loadNpmTasks( 'grunt-karma' ); 116 | grunt.loadNpmTasks( 'grunt-gulp' ); 117 | 118 | // Default task(s). 119 | grunt.registerTask( 'default', [ 'watch' ] ); 120 | 121 | // travis support 122 | grunt.registerTask( 'test', [ 'karma:travis' ] ); 123 | 124 | // distribution support 125 | grunt.registerTask( 'dist', [ 'build', 'less', 'gulp' ] ); 126 | }; 127 | -------------------------------------------------------------------------------- /src/js/utils/ReactComponentHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A specialised GoldenLayout component that binds GoldenLayout container 3 | * lifecycle events to react components 4 | * 5 | * @constructor 6 | * 7 | * @param {lm.container.ItemContainer} container 8 | * @param {Object} state state is not required for react components 9 | */ 10 | lm.utils.ReactComponentHandler = function( container, state ) { 11 | this._reactComponent = null; 12 | this._originalComponentWillUpdate = null; 13 | this._container = container; 14 | this._initialState = state; 15 | this._reactClass = this._getReactClass(); 16 | this._container.on( 'open', this._render, this ); 17 | this._container.on( 'destroy', this._destroy, this ); 18 | }; 19 | 20 | lm.utils.copy( lm.utils.ReactComponentHandler.prototype, { 21 | 22 | /** 23 | * Creates the react class and component and hydrates it with 24 | * the initial state - if one is present 25 | * 26 | * By default, react's getInitialState will be used 27 | * 28 | * @private 29 | * @returns {void} 30 | */ 31 | _render: function() { 32 | this._reactComponent = ReactDOM.render( this._getReactComponent(), this._container.getElement()[ 0 ] ); 33 | this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function() { 34 | }; 35 | this._reactComponent.componentWillUpdate = this._onUpdate.bind( this ); 36 | if( this._container.getState() ) { 37 | this._reactComponent.setState( this._container.getState() ); 38 | } 39 | }, 40 | 41 | /** 42 | * Removes the component from the DOM and thus invokes React's unmount lifecycle 43 | * 44 | * @private 45 | * @returns {void} 46 | */ 47 | _destroy: function() { 48 | ReactDOM.unmountComponentAtNode( this._container.getElement()[ 0 ] ); 49 | this._container.off( 'open', this._render, this ); 50 | this._container.off( 'destroy', this._destroy, this ); 51 | }, 52 | 53 | /** 54 | * Hooks into React's state management and applies the componentstate 55 | * to GoldenLayout 56 | * 57 | * @private 58 | * @returns {void} 59 | */ 60 | _onUpdate: function( nextProps, nextState ) { 61 | this._container.setState( nextState ); 62 | this._originalComponentWillUpdate.call( this._reactComponent, nextProps, nextState ); 63 | }, 64 | 65 | /** 66 | * Retrieves the react class from GoldenLayout's registry 67 | * 68 | * @private 69 | * @returns {React.Class} 70 | */ 71 | _getReactClass: function() { 72 | var componentName = this._container._config.component; 73 | var reactClass; 74 | 75 | if( !componentName ) { 76 | throw new Error( 'No react component name. type: react-component needs a field `component`' ); 77 | } 78 | 79 | reactClass = this._container.layoutManager.getComponent( componentName ); 80 | 81 | if( !reactClass ) { 82 | throw new Error( 'React component "' + componentName + '" not found. ' + 83 | 'Please register all components with GoldenLayout using `registerComponent(name, component)`' ); 84 | } 85 | 86 | return reactClass; 87 | }, 88 | 89 | /** 90 | * Copies and extends the properties array and returns the React element 91 | * 92 | * @private 93 | * @returns {React.Element} 94 | */ 95 | _getReactComponent: function() { 96 | var defaultProps = { 97 | glEventHub: this._container.layoutManager.eventHub, 98 | glContainer: this._container, 99 | }; 100 | var props = $.extend( defaultProps, this._container._config.props ); 101 | return React.createElement( this._reactClass, props ); 102 | } 103 | } ); -------------------------------------------------------------------------------- /src/css/goldenlayout-light-theme.css: -------------------------------------------------------------------------------- 1 | .lm_goldenlayout{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAGFBMVEX29vb19fXw8PDy8vL09PTz8/Pv7+/x8fGKuegbAAAAyUlEQVR42pXRQQ7CMBRDwST9pfe/MahEmgURbt7WmpVb6+vG0dd9REnn66xRy/qXiCgmEIIJhGACIZhACCYQgvlDCDFIEAwSBIMEwSBBMEgQDBIEgwTBIEEwCJEMQiSDENFMQmQzCZEbNyGemd6KeGZ6u4hnXe2qbdLHFjhf1XqNLXHev4wdMd9nspiEiWISJgqECQJhgkCYIBAmCIQJAmGCQJggECYJhAkCEUMEwhCBMEQgDJEIQ2RSg0iEIRJhiB/S+rrjqvXQ3paIJUgPBXxiAAAAAElFTkSuQmCC)}.lm_content{background:#e1e1e1;border:1px solid #cccccc}.lm_dragProxy .lm_content{box-shadow:2px 2px 4px rgba(0,0,0,0.2);box-sizing:border-box}.lm_dropTargetIndicator{box-shadow:inset 0 0 30px rgba(0,0,0,0.4);outline:1px dashed #cccccc;margin:1px;transition:all 200ms ease}.lm_dropTargetIndicator .lm_inner{background:#000000;opacity:.1}.lm_splitter{background:#999999;opacity:.001;transition:opacity 200ms ease}.lm_splitter:hover,.lm_splitter.lm_dragging{background:#bbbbbb;opacity:1}.lm_header{height:20px}.lm_header.lm_selectable{cursor:pointer}.lm_header .lm_tab{font-family:Arial,sans-serif;font-size:12px;color:#888888;background:#fafafa;margin-right:2px;padding-bottom:4px;border:1px solid #cccccc;border-bottom:none}.lm_header .lm_tab .lm_title{padding-top:1px}.lm_header .lm_tab .lm_close_tab{width:11px;height:11px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAIklEQVR4nGNgYGD4z4Ad/Mdg4ODDBXCZRFgCp5EEHQMXBwAQAgz0SVCcggAAAABJRU5ErkJggg==);background-position:center center;background-repeat:no-repeat;right:6px;top:4px;opacity:.4}.lm_header .lm_tab .lm_close_tab:hover{opacity:1}.lm_header .lm_tab.lm_active{border-bottom:none;box-shadow:2px -2px 2px -2px rgba(0,0,0,0.2);padding-bottom:5px}.lm_header .lm_tab.lm_active .lm_close_tab{opacity:1}.lm_dragProxy.lm_bottom .lm_header .lm_tab.lm_active,.lm_stack.lm_bottom .lm_header .lm_tab.lm_active{box-shadow:2px 2px 2px -2px rgba(0,0,0,0.2)}.lm_selected .lm_header{background-color:#452500}.lm_tab:hover,.lm_tab.lm_active{background:#e1e1e1;color:#777777}.lm_header .lm_controls .lm_tabdropdown:before{color:#000000}.lm_controls>li{position:relative;background-position:center center;background-repeat:no-repeat;opacity:.4;transition:opacity 300ms ease}.lm_controls>li:hover{opacity:1}.lm_controls .lm_popout{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAANUlEQVR4nI2QMQoAMAwCz5L/f9mOzZIaN0E9UDyZhaaQz6atgBHgambEJ5wBKoS0WaIvfT+6K2MIECN19MAAAAAASUVORK5CYII=)}.lm_controls .lm_maximise{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAIklEQVR4nGNkYGD4z0AAMBFSAAOETPpPlEmDUREjAxHhBABPvAQLFv3qngAAAABJRU5ErkJggg==)}.lm_controls .lm_close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKUlEQVR4nGNgYGD4z4Af/Mdg4FKASwCnDf8JKSBoAtEmEXQTQd8RDCcA6+4Q8OvIgasAAAAASUVORK5CYII=)}.lm_maximised .lm_header{background-color:#ffffff}.lm_maximised .lm_controls .lm_maximise{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAJklEQVR4nGP8//8/AyHARFDFUFbEwsDAwMDIyIgzHP7//89IlEkApSkHEScJTKoAAAAASUVORK5CYII=)}.lm_transition_indicator{background-color:#000000;border:1px dashed #555555}.lm_popin{cursor:pointer}.lm_popin .lm_bg{background:#000000;opacity:.7}.lm_popin .lm_icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC);background-position:center center;background-repeat:no-repeat;opacity:.7}.lm_popin:hover .lm_icon{opacity:1}/*# sourceMappingURL=goldenlayout-light-theme.css.map */ -------------------------------------------------------------------------------- /src/js/utils/DragListener.js: -------------------------------------------------------------------------------- 1 | lm.utils.DragListener = function( eElement, nButtonCode ) { 2 | lm.utils.EventEmitter.call( this ); 3 | 4 | this._eElement = $( eElement ); 5 | this._oDocument = $( document ); 6 | this._eBody = $( document.body ); 7 | this._nButtonCode = nButtonCode || 0; 8 | 9 | /** 10 | * The delay after which to start the drag in milliseconds 11 | */ 12 | this._nDelay = 200; 13 | 14 | /** 15 | * The distance the mouse needs to be moved to qualify as a drag 16 | */ 17 | this._nDistance = 10;//TODO - works better with delay only 18 | 19 | this._nX = 0; 20 | this._nY = 0; 21 | 22 | this._nOriginalX = 0; 23 | this._nOriginalY = 0; 24 | 25 | this._bDragging = false; 26 | 27 | this._fMove = lm.utils.fnBind( this.onMouseMove, this ); 28 | this._fUp = lm.utils.fnBind( this.onMouseUp, this ); 29 | this._fDown = lm.utils.fnBind( this.onMouseDown, this ); 30 | 31 | 32 | this._eElement.on( 'mousedown touchstart', this._fDown ); 33 | }; 34 | 35 | lm.utils.DragListener.timeout = null; 36 | 37 | lm.utils.copy( lm.utils.DragListener.prototype, { 38 | destroy: function() { 39 | this._eElement.unbind( 'mousedown touchstart', this._fDown ); 40 | this._oDocument.unbind( 'mouseup touchend', this._fUp ); 41 | this._eElement = null; 42 | this._oDocument = null; 43 | this._eBody = null; 44 | }, 45 | 46 | onMouseDown: function( oEvent ) { 47 | oEvent.preventDefault(); 48 | 49 | if( oEvent.button == 0 || oEvent.type === "touchstart" ) { 50 | var coordinates = this._getCoordinates( oEvent ); 51 | 52 | this._nOriginalX = coordinates.x; 53 | this._nOriginalY = coordinates.y; 54 | 55 | this._oDocument.on( 'mousemove touchmove', this._fMove ); 56 | this._oDocument.one( 'mouseup touchend', this._fUp ); 57 | 58 | this._timeout = setTimeout( lm.utils.fnBind( this._startDrag, this ), this._nDelay ); 59 | } 60 | }, 61 | 62 | onMouseMove: function( oEvent ) { 63 | if( this._timeout != null ) { 64 | oEvent.preventDefault(); 65 | 66 | var coordinates = this._getCoordinates( oEvent ); 67 | 68 | this._nX = coordinates.x - this._nOriginalX; 69 | this._nY = coordinates.y - this._nOriginalY; 70 | 71 | if( this._bDragging === false ) { 72 | if( 73 | Math.abs( this._nX ) > this._nDistance || 74 | Math.abs( this._nY ) > this._nDistance 75 | ) { 76 | clearTimeout( this._timeout ); 77 | this._startDrag(); 78 | } 79 | } 80 | 81 | if( this._bDragging ) { 82 | this.emit( 'drag', this._nX, this._nY, oEvent ); 83 | } 84 | } 85 | }, 86 | 87 | onMouseUp: function( oEvent ) { 88 | if( this._timeout != null ) { 89 | clearTimeout( this._timeout ); 90 | this._eBody.removeClass( 'lm_dragging' ); 91 | this._eElement.removeClass( 'lm_dragging' ); 92 | this._oDocument.find( 'iframe' ).css( 'pointer-events', '' ); 93 | this._oDocument.unbind( 'mousemove touchmove', this._fMove ); 94 | this._oDocument.unbind( 'mouseup touchend', this._fUp ); 95 | 96 | if( this._bDragging === true ) { 97 | this._bDragging = false; 98 | this.emit( 'dragStop', oEvent, this._nOriginalX + this._nX ); 99 | } 100 | } 101 | }, 102 | 103 | _startDrag: function() { 104 | this._bDragging = true; 105 | this._eBody.addClass( 'lm_dragging' ); 106 | this._eElement.addClass( 'lm_dragging' ); 107 | this._oDocument.find( 'iframe' ).css( 'pointer-events', 'none' ); 108 | this.emit( 'dragStart', this._nOriginalX, this._nOriginalY ); 109 | }, 110 | 111 | _getCoordinates: function( event ) { 112 | event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[ 0 ] : event; 113 | return { 114 | x: event.pageX, 115 | y: event.pageY 116 | }; 117 | } 118 | } ); -------------------------------------------------------------------------------- /src/css/goldenlayout-soda-theme.css: -------------------------------------------------------------------------------- 1 | .lm_goldenlayout{background:#000000;background:linear-gradient(#000000, #eeeeee);background-repeat:repeat}.lm_content{background:#272822}.lm_dragProxy .lm_content{box-shadow:2px 2px 4px rgba(0,0,0,0.9)}.lm_dropTargetIndicator{box-shadow:inset 0 0 30px #000000;outline:1px dashed #cccccc;transition:all 200ms ease}.lm_dropTargetIndicator .lm_inner{background:#000000;opacity:.2}.lm_splitter{background:#000000;opacity:.001;transition:opacity 200ms ease}.lm_splitter:hover,.lm_splitter.lm_dragging{background:#444444;opacity:1}.lm_header{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcCAIAAAAvP0KbAAAANElEQVR4nH2IsQ0AMAyDHM5J/v8qD3ixulWdOiAQmhkAquoi6frt33udBEnYprvZXZJg+wAKcQ/o96fYNQAAAABJRU5ErkJggg==);height:28px}.lm_header.lm_selectable{cursor:pointer}.lm_header .lm_tab{font-family:Arial,sans-serif;font-size:13px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcCAIAAAAvP0KbAAAANklEQVR4nHXGsQ0AMAgDQcuFh2EC9p+HhpIGaCMlKV5/cHdKoiQC+DYzl8+/nJk0M0YEu5tVtXqyIehfJSkOAAAAAElFTkSuQmCC);color:#999999;margin:0;padding-bottom:4px}.lm_header .lm_tab .lm_close_tab{width:11px;height:11px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAATElEQVR4nG3OwQ0DMQwDwZGRBtR/j1YJzMc5+IDoR+yCVO29g+pu981MFgqZmRdAfU7+CYWcbF11LwALjpBL0N0qybNx/RPU+gOeiS/+XCRwDlTgkQAAAABJRU5ErkJggg==);background-position:center center;background-repeat:no-repeat;right:6px;top:4px;opacity:.4}.lm_header .lm_tab .lm_close_tab:hover{opacity:1}.lm_header .lm_tab.lm_active{border-bottom:none;padding-bottom:5px}.lm_header .lm_tab.lm_active .lm_close_tab{opacity:1}.lm_stack.lm_left .lm_header,.lm_stack.lm_right .lm_header{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAABCAIAAABCJ1mGAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QQHEjUmFgXMqwAAADBJREFUCNdth7ENADAMwkjOIf9/xQMsqEPVTPVg2TUz3V0PANcb310nAWCbpKQktg/HHA+z1P+XmwAAAABJRU5ErkJggg==)}.lm_selected .lm_header{background-color:#452500}.lm_tab:hover,.lm_tab.lm_active{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcCAIAAAAvP0KbAAAAKUlEQVR4nGPw8vJi4ubmZmJgYGD6//8/nEZnY+MTUoPM/vfvH9PPnz8BJQc56Apw2moAAAAASUVORK5CYII=);color:#eeeeee}.lm_header .lm_controls .lm_tabdropdown:before{color:#eeeeee}.lm_controls>li{position:relative;background-position:center center;background-repeat:no-repeat;opacity:.4;transition:opacity 300ms ease}.lm_controls>li:hover{opacity:1}.lm_controls .lm_popout{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=)}.lm_controls .lm_maximise{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==)}.lm_controls .lm_close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=)}.lm_maximised .lm_header{background-color:#000000}.lm_maximised .lm_controls .lm_maximise{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAJklEQVR4nGP8//8/AyHARFDFUFbEwsDAwMDIyIgzHP7//89IlEkApSkHEScJTKoAAAAASUVORK5CYII=)}.lm_transition_indicator{background-color:#000000;border:1px dashed #555555}.lm_popin{cursor:pointer}.lm_popin .lm_bg{background:#eeeeee;opacity:.7}.lm_popin .lm_icon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC);background-position:center center;background-repeat:no-repeat;opacity:.7}.lm_popin:hover .lm_icon{opacity:1}/*# sourceMappingURL=goldenlayout-soda-theme.css.map */ -------------------------------------------------------------------------------- /src/js/utils/EventEmitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A generic and very fast EventEmitter 3 | * implementation. On top of emitting the 4 | * actual event it emits an 5 | * 6 | * lm.utils.EventEmitter.ALL_EVENT 7 | * 8 | * event for every event triggered. This allows 9 | * to hook into it and proxy events forwards 10 | * 11 | * @constructor 12 | */ 13 | lm.utils.EventEmitter = function() { 14 | this._mSubscriptions = {}; 15 | this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ] = []; 16 | 17 | /** 18 | * Listen for events 19 | * 20 | * @param {String} sEvent The name of the event to listen to 21 | * @param {Function} fCallback The callback to execute when the event occurs 22 | * @param {[Object]} oContext The value of the this pointer within the callback function 23 | * 24 | * @returns {void} 25 | */ 26 | this.on = function( sEvent, fCallback, oContext ) { 27 | if( !lm.utils.isFunction( fCallback ) ) { 28 | throw new Error( 'Tried to listen to event ' + sEvent + ' with non-function callback ' + fCallback ); 29 | } 30 | 31 | if( !this._mSubscriptions[ sEvent ] ) { 32 | this._mSubscriptions[ sEvent ] = []; 33 | } 34 | 35 | this._mSubscriptions[ sEvent ].push( { fn: fCallback, ctx: oContext } ); 36 | }; 37 | 38 | /** 39 | * Emit an event and notify listeners 40 | * 41 | * @param {String} sEvent The name of the event 42 | * @param {Mixed} various additional arguments that will be passed to the listener 43 | * 44 | * @returns {void} 45 | */ 46 | this.emit = function( sEvent ) { 47 | var i, ctx, args; 48 | 49 | args = Array.prototype.slice.call( arguments, 1 ); 50 | 51 | var subs = this._mSubscriptions[ sEvent ]; 52 | 53 | if( subs ) { 54 | subs = subs.slice(); 55 | for( i = 0; i < subs.length; i++ ) { 56 | ctx = subs[ i ].ctx || {}; 57 | subs[ i ].fn.apply( ctx, args ); 58 | } 59 | } 60 | 61 | args.unshift( sEvent ); 62 | 63 | var allEventSubs = this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ].slice() 64 | 65 | for( i = 0; i .lm_item{float:left}.lm_content{overflow:hidden;position:relative}.lm_dragging,.lm_dragging *{cursor:move !important;user-select:none}.lm_maximised{position:absolute;top:0;left:0;z-index:40}.lm_maximise_placeholder{display:none}.lm_splitter{position:relative;z-index:20}.lm_splitter:hover,.lm_splitter.lm_dragging{background:orange}.lm_splitter.lm_vertical .lm_drag_handle{width:100%;position:absolute;cursor:ns-resize}.lm_splitter.lm_horizontal{float:left;height:100%}.lm_splitter.lm_horizontal .lm_drag_handle{height:100%;position:absolute;cursor:ew-resize}.lm_header{overflow:visible;position:relative;z-index:1}.lm_header [class^=lm_]{box-sizing:content-box !important}.lm_header .lm_controls{position:absolute;right:3px}.lm_header .lm_controls>li{cursor:pointer;float:left;width:18px;height:18px;text-align:center}.lm_header ul{margin:0;padding:0;list-style-type:none}.lm_header .lm_tabs{position:absolute}.lm_header .lm_tab{cursor:pointer;float:left;height:14px;margin-top:1px;padding:0 10px 5px;padding-right:25px;position:relative}.lm_header .lm_tab i{width:2px;height:19px;position:absolute}.lm_header .lm_tab i.lm_left{top:0;left:-2px}.lm_header .lm_tab i.lm_right{top:0;right:-2px}.lm_header .lm_tab .lm_title{display:inline-block;overflow:hidden;text-overflow:ellipsis}.lm_header .lm_tab .lm_close_tab{width:14px;height:14px;position:absolute;top:0;right:0;text-align:center}.lm_stack.lm_left .lm_header,.lm_stack.lm_right .lm_header{height:100%}.lm_dragProxy.lm_left .lm_header,.lm_dragProxy.lm_right .lm_header,.lm_stack.lm_left .lm_header,.lm_stack.lm_right .lm_header{width:20px;float:left;vertical-align:top}.lm_dragProxy.lm_left .lm_header .lm_tabs,.lm_dragProxy.lm_right .lm_header .lm_tabs,.lm_stack.lm_left .lm_header .lm_tabs,.lm_stack.lm_right .lm_header .lm_tabs{transform-origin:left top;top:0;width:1000px}.lm_dragProxy.lm_left .lm_header .lm_controls,.lm_dragProxy.lm_right .lm_header .lm_controls,.lm_stack.lm_left .lm_header .lm_controls,.lm_stack.lm_right .lm_header .lm_controls{bottom:0}.lm_dragProxy.lm_left .lm_items,.lm_dragProxy.lm_right .lm_items,.lm_stack.lm_left .lm_items,.lm_stack.lm_right .lm_items{float:left}.lm_dragProxy.lm_left .lm_header .lm_tabs,.lm_stack.lm_left .lm_header .lm_tabs{transform:rotate(-90deg) scaleX(-1);left:0}.lm_dragProxy.lm_left .lm_header .lm_tabs .lm_tab,.lm_stack.lm_left .lm_header .lm_tabs .lm_tab{transform:scaleX(-1);margin-top:1px}.lm_dragProxy.lm_left .lm_header .lm_tabdropdown_list,.lm_stack.lm_left .lm_header .lm_tabdropdown_list{top:initial;right:initial;left:20px}.lm_dragProxy.lm_right .lm_content{float:left}.lm_dragProxy.lm_right .lm_header .lm_tabs,.lm_stack.lm_right .lm_header .lm_tabs{transform:rotate(90deg) scaleX(1);left:100%;margin-left:0}.lm_dragProxy.lm_right .lm_header .lm_controls,.lm_stack.lm_right .lm_header .lm_controls{left:3px}.lm_dragProxy.lm_right .lm_header .lm_tabdropdown_list,.lm_stack.lm_right .lm_header .lm_tabdropdown_list{top:initial;right:20px}.lm_dragProxy.lm_bottom .lm_header .lm_tab,.lm_stack.lm_bottom .lm_header .lm_tab{margin-top:0;border-top:none}.lm_dragProxy.lm_bottom .lm_header .lm_controls,.lm_stack.lm_bottom .lm_header .lm_controls{top:3px}.lm_dragProxy.lm_bottom .lm_header .lm_tabdropdown_list,.lm_stack.lm_bottom .lm_header .lm_tabdropdown_list{top:initial;bottom:20px}.lm_drop_tab_placeholder{float:left;width:100px;height:10px;visibility:hidden}.lm_header .lm_controls .lm_tabdropdown:before{content:'';width:0;height:0;vertical-align:middle;display:inline-block;border-top:5px dashed;border-right:5px solid transparent;border-left:5px solid transparent;color:white}.lm_header .lm_tabdropdown_list{position:absolute;top:20px;right:0;z-index:5;overflow:hidden}.lm_header .lm_tabdropdown_list .lm_tab{clear:both;padding-right:10px;margin:0}.lm_header .lm_tabdropdown_list .lm_tab .lm_title{width:100px}.lm_header .lm_tabdropdown_list .lm_close_tab{display:none !important}.lm_dragProxy{position:absolute;top:0;left:0;z-index:30}.lm_dragProxy .lm_header{background:transparent}.lm_dragProxy .lm_content{border-top:none;overflow:hidden}.lm_dropTargetIndicator{display:none;position:absolute;z-index:20}.lm_dropTargetIndicator .lm_inner{width:100%;height:100%;position:relative;top:0;left:0}.lm_transition_indicator{display:none;width:20px;height:20px;position:absolute;top:0;left:0;z-index:20}.lm_popin{width:20px;height:20px;position:absolute;bottom:0;right:0;z-index:9999}.lm_popin>*{width:100%;height:100%;position:absolute;top:0;left:0}.lm_popin>.lm_bg{z-index:10}.lm_popin>.lm_icon{z-index:20}/*# sourceMappingURL=goldenlayout-base.css.map */ -------------------------------------------------------------------------------- /test/create-from-config-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'Creates the right structure based on the provided config', function() { 2 | 3 | var createLayout = function( config ) { 4 | var myLayout = new window.GoldenLayout( config ); 5 | 6 | myLayout.registerComponent( 'testComponent', function( container ){ 7 | container.getElement().html( 'that worked' ); 8 | }); 9 | 10 | myLayout.init(); 11 | 12 | return myLayout; 13 | }; 14 | 15 | 16 | it( 'creates the right primitive types: component only', function() { 17 | var layout; 18 | 19 | runs(function(){ 20 | layout = createLayout({ 21 | content: [{ 22 | type: 'component', 23 | componentName: 'testComponent' 24 | }] 25 | }); 26 | }); 27 | 28 | waitsFor(function(){ 29 | return layout.isInitialised; 30 | }); 31 | 32 | runs(function(){ 33 | expect( layout.isInitialised ).toBe( true ); 34 | expect( layout.root.isRoot ).toBe( true ); 35 | expect( layout.root.contentItems.length ).toBe( 1 ); 36 | expect( layout.root.contentItems[ 0 ].isStack ).toBe( true ); 37 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].isComponent ).toBe( true ); 38 | }); 39 | 40 | runs(function(){ 41 | layout.destroy(); 42 | }); 43 | }); 44 | 45 | it( 'creates the right primitive types: stack and component', function() { 46 | var layout; 47 | 48 | runs(function(){ 49 | layout = createLayout({ 50 | content: [{ 51 | type: 'stack', 52 | content: [{ 53 | type: 'component', 54 | componentName: 'testComponent' 55 | }] 56 | }] 57 | }); 58 | }); 59 | 60 | waitsFor(function(){ 61 | return layout.isInitialised; 62 | }); 63 | 64 | runs(function(){ 65 | expect( layout.isInitialised ).toBe( true ); 66 | expect( layout.root.isRoot ).toBe( true ); 67 | expect( layout.root.contentItems.length ).toBe( 1 ); 68 | expect( layout.root.contentItems[ 0 ].isStack ).toBe( true ); 69 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].isComponent ).toBe( true ); 70 | }); 71 | 72 | runs(function(){ 73 | layout.destroy(); 74 | }); 75 | }); 76 | 77 | it( 'creates the right primitive types: row and two component', function() { 78 | var layout; 79 | 80 | runs(function(){ 81 | layout = createLayout({ 82 | content: [{ 83 | type: 'row', 84 | content: [{ 85 | type: 'component', 86 | componentName: 'testComponent' 87 | }, 88 | { 89 | type: 'component', 90 | componentName: 'testComponent' 91 | }] 92 | }] 93 | }); 94 | }); 95 | 96 | waitsFor(function(){ 97 | return layout.isInitialised; 98 | }); 99 | 100 | runs(function(){ 101 | expect( layout.isInitialised ).toBe( true ); 102 | expect( layout.root.contentItems.length ).toBe( 1 ); 103 | expect( layout.root.contentItems[ 0 ].isRow ).toBe( true ); 104 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].isStack ).toBe( true ); 105 | expect( layout.root.contentItems[ 0 ].contentItems[ 1 ].isStack ).toBe( true ); 106 | expect( layout.root.contentItems[ 0 ].contentItems.length ).toBe( 2 ); 107 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].contentItems[ 0 ].isComponent ).toBe( true ); 108 | expect( layout.root.contentItems[ 0 ].contentItems[ 1 ].contentItems[ 0 ].isComponent ).toBe( true ); 109 | }); 110 | 111 | runs(function(){ 112 | layout.destroy(); 113 | }); 114 | }); 115 | 116 | 117 | it( 'creates the right primitive types: stack -> column -> component', function() { 118 | var layout; 119 | 120 | runs(function(){ 121 | layout = createLayout({ 122 | content: [{ 123 | type: 'stack', 124 | content: [{ 125 | type: 'column', 126 | content:[{ 127 | type: 'component', 128 | componentName: 'testComponent' 129 | }] 130 | }] 131 | }] 132 | }); 133 | }); 134 | 135 | waitsFor(function(){ 136 | return layout.isInitialised; 137 | }); 138 | 139 | runs(function(){ 140 | expect( layout.isInitialised ).toBe( true ); 141 | 142 | expect( layout.root.contentItems.length ).toBe( 1 ); 143 | expect( layout.root.contentItems[ 0 ].isStack ).toBe( true ); 144 | 145 | expect( layout.root.contentItems[ 0 ].contentItems.length ).toBe( 1 ); 146 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].isColumn ).toBe( true ); 147 | 148 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].contentItems.length ).toBe( 1 ); 149 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].contentItems[ 0 ].isStack ).toBe( true ); 150 | 151 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].contentItems[ 0 ].contentItems.length ).toBe( 1 ); 152 | expect( layout.root.contentItems[ 0 ].contentItems[ 0 ].contentItems[ 0 ].contentItems[ 0 ].isComponent ).toBe( true ); 153 | }); 154 | 155 | runs(function(){ 156 | layout.destroy(); 157 | }); 158 | }); 159 | }); -------------------------------------------------------------------------------- /src/js/utils/ConfigMinifier.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minifies and unminifies configs by replacing frequent keys 3 | * and values with one letter substitutes. Config options must 4 | * retain array position/index, add new options at the end. 5 | * 6 | * @constructor 7 | */ 8 | lm.utils.ConfigMinifier = function() { 9 | this._keys = [ 10 | 'settings', 11 | 'hasHeaders', 12 | 'constrainDragToContainer', 13 | 'selectionEnabled', 14 | 'dimensions', 15 | 'borderWidth', 16 | 'minItemHeight', 17 | 'minItemWidth', 18 | 'headerHeight', 19 | 'dragProxyWidth', 20 | 'dragProxyHeight', 21 | 'labels', 22 | 'close', 23 | 'maximise', 24 | 'minimise', 25 | 'popout', 26 | 'content', 27 | 'componentName', 28 | 'componentState', 29 | 'id', 30 | 'width', 31 | 'type', 32 | 'height', 33 | 'isClosable', 34 | 'title', 35 | 'popoutWholeStack', 36 | 'openPopouts', 37 | 'parentId', 38 | 'activeItemIndex', 39 | 'reorderEnabled', 40 | 'borderGrabWidth', 41 | 42 | 43 | 44 | 45 | //Maximum 36 entries, do not cross this line! 46 | ]; 47 | if( this._keys.length > 36 ) { 48 | throw new Error( 'Too many keys in config minifier map' ); 49 | } 50 | 51 | this._values = [ 52 | true, 53 | false, 54 | 'row', 55 | 'column', 56 | 'stack', 57 | 'component', 58 | 'close', 59 | 'maximise', 60 | 'minimise', 61 | 'open in new window' 62 | ]; 63 | }; 64 | 65 | lm.utils.copy( lm.utils.ConfigMinifier.prototype, { 66 | 67 | /** 68 | * Takes a GoldenLayout configuration object and 69 | * replaces its keys and values recursively with 70 | * one letter counterparts 71 | * 72 | * @param {Object} config A GoldenLayout config object 73 | * 74 | * @returns {Object} minified config 75 | */ 76 | minifyConfig: function( config ) { 77 | var min = {}; 78 | this._nextLevel( config, min, '_min' ); 79 | return min; 80 | }, 81 | 82 | /** 83 | * Takes a configuration Object that was previously minified 84 | * using minifyConfig and returns its original version 85 | * 86 | * @param {Object} minifiedConfig 87 | * 88 | * @returns {Object} the original configuration 89 | */ 90 | unminifyConfig: function( minifiedConfig ) { 91 | var orig = {}; 92 | this._nextLevel( minifiedConfig, orig, '_max' ); 93 | return orig; 94 | }, 95 | 96 | /** 97 | * Recursive function, called for every level of the config structure 98 | * 99 | * @param {Array|Object} orig 100 | * @param {Array|Object} min 101 | * @param {String} translationFn 102 | * 103 | * @returns {void} 104 | */ 105 | _nextLevel: function( from, to, translationFn ) { 106 | var key, minKey; 107 | 108 | for( key in from ) { 109 | 110 | /** 111 | * For in returns array indices as keys, so let's cast them to numbers 112 | */ 113 | if( from instanceof Array ) key = parseInt( key, 10 ); 114 | 115 | /** 116 | * In case something has extended Object prototypes 117 | */ 118 | if( !from.hasOwnProperty( key ) ) continue; 119 | 120 | /** 121 | * Translate the key to a one letter substitute 122 | */ 123 | minKey = this[ translationFn ]( key, this._keys ); 124 | 125 | /** 126 | * For Arrays and Objects, create a new Array/Object 127 | * on the minified object and recurse into it 128 | */ 129 | if( typeof from[ key ] === 'object' ) { 130 | to[ minKey ] = from[ key ] instanceof Array ? [] : {}; 131 | this._nextLevel( from[ key ], to[ minKey ], translationFn ); 132 | 133 | /** 134 | * For primitive values (Strings, Numbers, Boolean etc.) 135 | * minify the value 136 | */ 137 | } else { 138 | to[ minKey ] = this[ translationFn ]( from[ key ], this._values ); 139 | } 140 | } 141 | }, 142 | 143 | /** 144 | * Minifies value based on a dictionary 145 | * 146 | * @param {String|Boolean} value 147 | * @param {Array} dictionary 148 | * 149 | * @returns {String} The minified version 150 | */ 151 | _min: function( value, dictionary ) { 152 | /** 153 | * If a value actually is a single character, prefix it 154 | * with ___ to avoid mistaking it for a minification code 155 | */ 156 | if( typeof value === 'string' && value.length === 1 ) { 157 | return '___' + value; 158 | } 159 | 160 | var index = lm.utils.indexOf( value, dictionary ); 161 | 162 | /** 163 | * value not found in the dictionary, return it unmodified 164 | */ 165 | if( index === -1 ) { 166 | return value; 167 | 168 | /** 169 | * value found in dictionary, return its base36 counterpart 170 | */ 171 | } else { 172 | return index.toString( 36 ); 173 | } 174 | }, 175 | 176 | _max: function( value, dictionary ) { 177 | /** 178 | * value is a single character. Assume that it's a translation 179 | * and return the original value from the dictionary 180 | */ 181 | if( typeof value === 'string' && value.length === 1 ) { 182 | return dictionary[ parseInt( value, 36 ) ]; 183 | } 184 | 185 | /** 186 | * value originally was a single character and was prefixed with ___ 187 | * to avoid mistaking it for a translation. Remove the prefix 188 | * and return the original character 189 | */ 190 | if( typeof value === 'string' && value.substr( 0, 3 ) === '___' ) { 191 | return value[ 3 ]; 192 | } 193 | /** 194 | * value was not minified 195 | */ 196 | return value; 197 | } 198 | } ); 199 | -------------------------------------------------------------------------------- /src/js/container/ItemContainer.js: -------------------------------------------------------------------------------- 1 | lm.container.ItemContainer = function( config, parent, layoutManager ) { 2 | lm.utils.EventEmitter.call( this ); 3 | 4 | this.width = null; 5 | this.height = null; 6 | this.title = config.componentName; 7 | this.parent = parent; 8 | this.layoutManager = layoutManager; 9 | this.isHidden = false; 10 | 11 | this._config = config; 12 | this._element = $( [ 13 | '
    ', 14 | '
    ', 15 | '
    ' 16 | ].join( '' ) ); 17 | 18 | this._contentElement = this._element.find( '.lm_content' ); 19 | }; 20 | 21 | lm.utils.copy( lm.container.ItemContainer.prototype, { 22 | 23 | /** 24 | * Get the inner DOM element the container's content 25 | * is intended to live in 26 | * 27 | * @returns {DOM element} 28 | */ 29 | getElement: function() { 30 | return this._contentElement; 31 | }, 32 | 33 | /** 34 | * Hide the container. Notifies the containers content first 35 | * and then hides the DOM node. If the container is already hidden 36 | * this should have no effect 37 | * 38 | * @returns {void} 39 | */ 40 | hide: function() { 41 | this.emit( 'hide' ); 42 | this.isHidden = true; 43 | this._element.hide(); 44 | }, 45 | 46 | /** 47 | * Shows a previously hidden container. Notifies the 48 | * containers content first and then shows the DOM element. 49 | * If the container is already visible this has no effect. 50 | * 51 | * @returns {void} 52 | */ 53 | show: function() { 54 | this.emit( 'show' ); 55 | this.isHidden = false; 56 | this._element.show(); 57 | // call shown only if the container has a valid size 58 | if( this.height != 0 || this.width != 0 ) { 59 | this.emit( 'shown' ); 60 | } 61 | }, 62 | 63 | /** 64 | * Set the size from within the container. Traverses up 65 | * the item tree until it finds a row or column element 66 | * and resizes its items accordingly. 67 | * 68 | * If this container isn't a descendant of a row or column 69 | * it returns false 70 | * @todo Rework!!! 71 | * @param {Number} width The new width in pixel 72 | * @param {Number} height The new height in pixel 73 | * 74 | * @returns {Boolean} resizeSuccesful 75 | */ 76 | setSize: function( width, height ) { 77 | var rowOrColumn = this.parent, 78 | rowOrColumnChild = this, 79 | totalPixel, 80 | percentage, 81 | direction, 82 | newSize, 83 | delta, 84 | i; 85 | 86 | while( !rowOrColumn.isColumn && !rowOrColumn.isRow ) { 87 | rowOrColumnChild = rowOrColumn; 88 | rowOrColumn = rowOrColumn.parent; 89 | 90 | 91 | /** 92 | * No row or column has been found 93 | */ 94 | if( rowOrColumn.isRoot ) { 95 | return false; 96 | } 97 | } 98 | 99 | direction = rowOrColumn.isColumn ? "height" : "width"; 100 | newSize = direction === "height" ? height : width; 101 | 102 | totalPixel = this[ direction ] * ( 1 / ( rowOrColumnChild.config[ direction ] / 100 ) ); 103 | percentage = ( newSize / totalPixel ) * 100; 104 | delta = ( rowOrColumnChild.config[ direction ] - percentage ) / (rowOrColumn.contentItems.length - 1); 105 | 106 | for( i = 0; i < rowOrColumn.contentItems.length; i++ ) { 107 | if( rowOrColumn.contentItems[ i ] === rowOrColumnChild ) { 108 | rowOrColumn.contentItems[ i ].config[ direction ] = percentage; 109 | } else { 110 | rowOrColumn.contentItems[ i ].config[ direction ] += delta; 111 | } 112 | } 113 | 114 | rowOrColumn.callDownwards( 'setSize' ); 115 | 116 | return true; 117 | }, 118 | 119 | /** 120 | * Closes the container if it is closable. Can be called by 121 | * both the component within at as well as the contentItem containing 122 | * it. Emits a close event before the container itself is closed. 123 | * 124 | * @returns {void} 125 | */ 126 | close: function() { 127 | if( this._config.isClosable ) { 128 | this.emit( 'close' ); 129 | this.parent.close(); 130 | } 131 | }, 132 | 133 | /** 134 | * Returns the current state object 135 | * 136 | * @returns {Object} state 137 | */ 138 | getState: function() { 139 | return this._config.componentState; 140 | }, 141 | 142 | /** 143 | * Merges the provided state into the current one 144 | * 145 | * @param {Object} state 146 | * 147 | * @returns {void} 148 | */ 149 | extendState: function( state ) { 150 | this.setState( $.extend( true, this.getState(), state ) ); 151 | }, 152 | 153 | /** 154 | * Notifies the layout manager of a stateupdate 155 | * 156 | * @param {serialisable} state 157 | */ 158 | setState: function( state ) { 159 | this._config.componentState = state; 160 | this.parent.emitBubblingEvent( 'stateChanged' ); 161 | }, 162 | 163 | /** 164 | * Set's the components title 165 | * 166 | * @param {String} title 167 | */ 168 | setTitle: function( title ) { 169 | this.parent.setTitle( title ); 170 | }, 171 | 172 | /** 173 | * Set's the containers size. Called by the container's component. 174 | * To set the size programmatically from within the container please 175 | * use the public setSize method 176 | * 177 | * @param {[Int]} width in px 178 | * @param {[Int]} height in px 179 | * 180 | * @returns {void} 181 | */ 182 | _$setSize: function( width, height ) { 183 | if( width !== this.width || height !== this.height ) { 184 | this.width = width; 185 | this.height = height; 186 | var cl = this._contentElement[0]; 187 | var hdelta = cl.offsetWidth - cl.clientWidth; 188 | var vdelta = cl.offsetHeight - cl.clientHeight; 189 | this._contentElement.width( this.width-hdelta ) 190 | .height( this.height-vdelta ); 191 | this.emit( 'resize' ); 192 | } 193 | } 194 | } ); 195 | -------------------------------------------------------------------------------- /test/event-emitter-tests.js: -------------------------------------------------------------------------------- 1 | describe( 'the EventEmitter works', function(){ 2 | var EmitterImplementor = function() { 3 | lm.utils.EventEmitter.call( this ); 4 | }; 5 | 6 | it( 'is possible to inherit from EventEmitter', function(){ 7 | var myObject = new EmitterImplementor(); 8 | expect( typeof myObject.on ).toBe( 'function' ); 9 | expect( typeof myObject.unbind ).toBe( 'function' ); 10 | expect( typeof myObject.trigger ).toBe( 'function' ); 11 | }); 12 | 13 | it( 'notifies callbacks', function(){ 14 | var myObject = new EmitterImplementor(); 15 | var myListener = { callback: function(){} }; 16 | spyOn( myListener, 'callback' ); 17 | expect( myListener.callback ).not.toHaveBeenCalled(); 18 | myObject.on( 'someEvent', myListener.callback ); 19 | expect( myListener.callback ).not.toHaveBeenCalled(); 20 | myObject.emit( 'someEvent', 'Good', 'Morning' ); 21 | expect( myListener.callback ).toHaveBeenCalledWith( 'Good', 'Morning' ); 22 | expect(myListener.callback.calls.length).toEqual(1); 23 | }); 24 | 25 | it( 'triggers an \'all\' event', function(){ 26 | var myObject = new EmitterImplementor(); 27 | var myListener = { callback: function(){}, allCallback: function(){} }; 28 | spyOn( myListener, 'callback' ); 29 | spyOn( myListener, 'allCallback' ); 30 | 31 | myObject.on( 'someEvent', myListener.callback ); 32 | myObject.on( lm.utils.EventEmitter.ALL_EVENT, myListener.allCallback ); 33 | 34 | 35 | expect( myListener.callback ).not.toHaveBeenCalled(); 36 | expect( myListener.allCallback ).not.toHaveBeenCalled(); 37 | myObject.emit( 'someEvent', 'Good', 'Morning' ); 38 | expect( myListener.callback ).toHaveBeenCalledWith( 'Good', 'Morning' ); 39 | expect(myListener.callback.calls.length).toEqual(1); 40 | expect( myListener.allCallback ).toHaveBeenCalledWith( 'someEvent', 'Good', 'Morning' ); 41 | expect(myListener.allCallback.calls.length).toEqual(1); 42 | 43 | myObject.emit( 'someOtherEvent', 123 ); 44 | expect(myListener.callback.calls.length).toEqual(1); 45 | expect(myListener.allCallback ).toHaveBeenCalledWith( 'someOtherEvent', 123 ); 46 | expect(myListener.allCallback.calls.length).toEqual(2); 47 | }); 48 | 49 | it( 'triggers sets the right context', function(){ 50 | var myObject = new EmitterImplementor(); 51 | var context = null; 52 | var myListener = { callback: function(){ 53 | context = this; 54 | }}; 55 | 56 | myObject.on( 'someEvent', myListener.callback, {some: 'thing' } ); 57 | expect( context ).toBe( null ); 58 | myObject.emit( 'someEvent' ); 59 | expect( context.some ).toBe( 'thing' ); 60 | }); 61 | 62 | it( 'unbinds events', function(){ 63 | var myObject = new EmitterImplementor(); 64 | var myListener = { callback: function(){}}; 65 | spyOn( myListener, 'callback' ); 66 | myObject.on( 'someEvent', myListener.callback ); 67 | expect(myListener.callback.calls.length).toEqual(0); 68 | myObject.emit( 'someEvent' ); 69 | expect(myListener.callback.calls.length).toEqual(1); 70 | myObject.unbind( 'someEvent', myListener.callback ); 71 | myObject.emit( 'someEvent' ); 72 | expect(myListener.callback.calls.length).toEqual(1); 73 | }); 74 | 75 | it( 'unbinds all events if no context is provided', function(){ 76 | var myObject = new EmitterImplementor(); 77 | var myListener = { callback: function(){}}; 78 | spyOn( myListener, 'callback' ); 79 | myObject.on( 'someEvent', myListener.callback ); 80 | expect(myListener.callback.calls.length).toEqual(0); 81 | myObject.emit( 'someEvent' ); 82 | expect(myListener.callback.calls.length).toEqual(1); 83 | myObject.unbind( 'someEvent' ); 84 | myObject.emit( 'someEvent' ); 85 | expect(myListener.callback.calls.length).toEqual(1); 86 | }); 87 | 88 | it( 'unbinds events for a specific context only', function(){ 89 | var myObject = new EmitterImplementor(); 90 | var myListener = { callback: function(){}}; 91 | var contextA = { name: 'a' }; 92 | var contextB = { name: 'b' }; 93 | spyOn( myListener, 'callback' ); 94 | myObject.on( 'someEvent', myListener.callback, contextA ); 95 | myObject.on( 'someEvent', myListener.callback, contextB ); 96 | expect(myListener.callback.calls.length).toEqual(0); 97 | myObject.emit( 'someEvent' ); 98 | expect(myListener.callback.calls.length).toEqual(2); 99 | myObject.unbind( 'someEvent', myListener.callback, contextA ); 100 | myObject.emit( 'someEvent' ); 101 | expect(myListener.callback.calls.length).toEqual(3); 102 | myObject.unbind( 'someEvent', myListener.callback, contextB ); 103 | myObject.emit( 'someEvent' ); 104 | expect(myListener.callback.calls.length).toEqual(3); 105 | }); 106 | 107 | it( 'throws an exception when trying to unsubscribe for a non existing method', function(){ 108 | var myObject = new EmitterImplementor(); 109 | var myListener = { callback: function(){}}; 110 | 111 | myObject.on( 'someEvent', myListener.callback ); 112 | 113 | expect(function(){ 114 | myObject.unbind( 'someEvent', function(){} ); 115 | }).toThrow(); 116 | 117 | expect(function(){ 118 | myObject.unbind( 'doesNotExist', myListener.callback ); 119 | }).toThrow(); 120 | 121 | expect(function(){ 122 | myObject.unbind( 'someEvent', myListener.callback ); 123 | }).not.toThrow(); 124 | }); 125 | 126 | it( 'throws an exception when attempting to bind a non-function', function() { 127 | var myObject = new EmitterImplementor(); 128 | 129 | expect(function(){ 130 | myObject.on( 'someEvent', 1 ); 131 | }).toThrow(); 132 | 133 | expect(function(){ 134 | myObject.on( 'someEvent', undefined ); 135 | }).toThrow(); 136 | 137 | expect(function(){ 138 | myObject.on( 'someEvent', {} ); 139 | }).toThrow(); 140 | }); 141 | }); -------------------------------------------------------------------------------- /src/js/controls/Tab.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an individual tab within a Stack's header 3 | * 4 | * @param {lm.controls.Header} header 5 | * @param {lm.items.AbstractContentItem} contentItem 6 | * 7 | * @constructor 8 | */ 9 | lm.controls.Tab = function( header, contentItem ) { 10 | this.header = header; 11 | this.contentItem = contentItem; 12 | this.element = $( lm.controls.Tab._template ); 13 | this.titleElement = this.element.find( '.lm_title' ); 14 | this.closeElement = this.element.find( '.lm_close_tab' ); 15 | this.closeElement[ contentItem.config.isClosable ? 'show' : 'hide' ](); 16 | this.isActive = false; 17 | 18 | this.setTitle( contentItem.config.title ); 19 | this.contentItem.on( 'titleChanged', this.setTitle, this ); 20 | 21 | this._layoutManager = this.contentItem.layoutManager; 22 | 23 | if( 24 | this._layoutManager.config.settings.reorderEnabled === true && 25 | contentItem.config.reorderEnabled === true 26 | ) { 27 | this._dragListener = new lm.utils.DragListener( this.element ); 28 | this._dragListener.on( 'dragStart', this._onDragStart, this ); 29 | this.contentItem.on( 'destroy', this._dragListener.destroy, this._dragListener ); 30 | } 31 | 32 | this._onTabClickFn = lm.utils.fnBind( this._onTabClick, this ); 33 | this._onCloseClickFn = lm.utils.fnBind( this._onCloseClick, this ); 34 | 35 | this.element.on( 'mousedown touchstart', this._onTabClickFn ); 36 | 37 | if( this.contentItem.config.isClosable ) { 38 | this.closeElement.on( 'click touchstart', this._onCloseClickFn ); 39 | this.closeElement.on('mousedown', this._onCloseMousedown); 40 | } else { 41 | this.closeElement.remove(); 42 | } 43 | 44 | this.contentItem.tab = this; 45 | this.contentItem.emit( 'tab', this ); 46 | this.contentItem.layoutManager.emit( 'tabCreated', this ); 47 | 48 | if( this.contentItem.isComponent ) { 49 | this.contentItem.container.tab = this; 50 | this.contentItem.container.emit( 'tab', this ); 51 | } 52 | }; 53 | 54 | /** 55 | * The tab's html template 56 | * 57 | * @type {String} 58 | */ 59 | lm.controls.Tab._template = '
  • ' + 60 | '
    ' + 61 | '
  • '; 62 | 63 | lm.utils.copy( lm.controls.Tab.prototype, { 64 | 65 | /** 66 | * Sets the tab's title to the provided string and sets 67 | * its title attribute to a pure text representation (without 68 | * html tags) of the same string. 69 | * 70 | * @public 71 | * @param {String} title can contain html 72 | */ 73 | setTitle: function( title ) { 74 | this.element.attr( 'title', lm.utils.stripTags( title ) ); 75 | this.titleElement.html( title ); 76 | }, 77 | 78 | /** 79 | * Sets this tab's active state. To programmatically 80 | * switch tabs, use header.setActiveContentItem( item ) instead. 81 | * 82 | * @public 83 | * @param {Boolean} isActive 84 | */ 85 | setActive: function( isActive ) { 86 | if( isActive === this.isActive ) { 87 | return; 88 | } 89 | this.isActive = isActive; 90 | 91 | if( isActive ) { 92 | this.element.addClass( 'lm_active' ); 93 | } else { 94 | this.element.removeClass( 'lm_active' ); 95 | } 96 | }, 97 | 98 | /** 99 | * Destroys the tab 100 | * 101 | * @private 102 | * @returns {void} 103 | */ 104 | _$destroy: function() { 105 | this.element.off( 'mousedown touchstart', this._onTabClickFn ); 106 | this.closeElement.off( 'click touchstart', this._onCloseClickFn ); 107 | if( this._dragListener ) { 108 | this.contentItem.off( 'destroy', this._dragListener.destroy, this._dragListener ); 109 | this._dragListener.off( 'dragStart', this._onDragStart ); 110 | this._dragListener = null; 111 | } 112 | this.element.remove(); 113 | }, 114 | 115 | /** 116 | * Callback for the DragListener 117 | * 118 | * @param {Number} x The tabs absolute x position 119 | * @param {Number} y The tabs absolute y position 120 | * 121 | * @private 122 | * @returns {void} 123 | */ 124 | _onDragStart: function( x, y ) { 125 | if( this.contentItem.parent.isMaximised === true ) { 126 | this.contentItem.parent.toggleMaximise(); 127 | } 128 | new lm.controls.DragProxy( 129 | x, 130 | y, 131 | this._dragListener, 132 | this._layoutManager, 133 | this.contentItem, 134 | this.header.parent 135 | ); 136 | }, 137 | 138 | /** 139 | * Callback when the tab is clicked 140 | * 141 | * @param {jQuery DOM event} event 142 | * 143 | * @private 144 | * @returns {void} 145 | */ 146 | _onTabClick: function( event ) { 147 | // left mouse button or tap 148 | if( event.button === 0 || event.type === 'touchstart' ) { 149 | var activeContentItem = this.header.parent.getActiveContentItem(); 150 | if( this.contentItem !== activeContentItem ) { 151 | this.header.parent.setActiveContentItem( this.contentItem ); 152 | } 153 | 154 | // middle mouse button 155 | } else if( event.button === 1 && this.contentItem.config.isClosable ) { 156 | this._onCloseClick( event ); 157 | } 158 | }, 159 | 160 | /** 161 | * Callback when the tab's close button is 162 | * clicked 163 | * 164 | * @param {jQuery DOM event} event 165 | * 166 | * @private 167 | * @returns {void} 168 | */ 169 | _onCloseClick: function( event ) { 170 | event.stopPropagation(); 171 | this.header.parent.removeChild( this.contentItem ); 172 | }, 173 | 174 | 175 | /** 176 | * Callback to capture tab close button mousedown 177 | * to prevent tab from activating. 178 | * 179 | * @param (jQuery DOM event) event 180 | * 181 | * @private 182 | * @returns {void} 183 | */ 184 | _onCloseMousedown: function(event) { 185 | event.stopPropagation(); 186 | } 187 | } ); 188 | -------------------------------------------------------------------------------- /src/js/utils/utils.js: -------------------------------------------------------------------------------- 1 | lm.utils.F = function() { 2 | }; 3 | 4 | lm.utils.extend = function( subClass, superClass ) { 5 | subClass.prototype = lm.utils.createObject( superClass.prototype ); 6 | subClass.prototype.contructor = subClass; 7 | }; 8 | 9 | lm.utils.createObject = function( prototype ) { 10 | if( typeof Object.create === 'function' ) { 11 | return Object.create( prototype ); 12 | } else { 13 | lm.utils.F.prototype = prototype; 14 | return new lm.utils.F(); 15 | } 16 | }; 17 | 18 | lm.utils.objectKeys = function( object ) { 19 | var keys, key; 20 | 21 | if( typeof Object.keys === 'function' ) { 22 | return Object.keys( object ); 23 | } else { 24 | keys = []; 25 | for( key in object ) { 26 | keys.push( key ); 27 | } 28 | return keys; 29 | } 30 | }; 31 | 32 | lm.utils.getHashValue = function( key ) { 33 | var matches = location.hash.match( new RegExp( key + '=([^&]*)' ) ); 34 | return matches ? matches[ 1 ] : null; 35 | }; 36 | 37 | lm.utils.getQueryStringParam = function( param ) { 38 | if( window.location.hash ) { 39 | return lm.utils.getHashValue( param ); 40 | } else if( !window.location.search ) { 41 | return null; 42 | } 43 | 44 | var keyValuePairs = window.location.search.substr( 1 ).split( '&' ), 45 | params = {}, 46 | pair, 47 | i; 48 | 49 | for( i = 0; i < keyValuePairs.length; i++ ) { 50 | pair = keyValuePairs[ i ].split( '=' ); 51 | params[ pair[ 0 ] ] = pair[ 1 ]; 52 | } 53 | 54 | return params[ param ] || null; 55 | }; 56 | 57 | lm.utils.copy = function( target, source ) { 58 | for( var key in source ) { 59 | target[ key ] = source[ key ]; 60 | } 61 | return target; 62 | }; 63 | 64 | /** 65 | * This is based on Paul Irish's shim, but looks quite odd in comparison. Why? 66 | * Because 67 | * a) it shouldn't affect the global requestAnimationFrame function 68 | * b) it shouldn't pass on the time that has passed 69 | * 70 | * @param {Function} fn 71 | * 72 | * @returns {void} 73 | */ 74 | lm.utils.animFrame = function( fn ) { 75 | return ( window.requestAnimationFrame || 76 | window.webkitRequestAnimationFrame || 77 | window.mozRequestAnimationFrame || 78 | function( callback ) { 79 | window.setTimeout( callback, 1000 / 60 ); 80 | })( function() { 81 | fn(); 82 | } ); 83 | }; 84 | 85 | lm.utils.indexOf = function( needle, haystack ) { 86 | if( !( haystack instanceof Array ) ) { 87 | throw new Error( 'Haystack is not an Array' ); 88 | } 89 | 90 | if( haystack.indexOf ) { 91 | return haystack.indexOf( needle ); 92 | } else { 93 | for( var i = 0; i < haystack.length; i++ ) { 94 | if( haystack[ i ] === needle ) { 95 | return i; 96 | } 97 | } 98 | return -1; 99 | } 100 | }; 101 | 102 | if( typeof /./ != 'function' && typeof Int8Array != 'object' ) { 103 | lm.utils.isFunction = function( obj ) { 104 | return typeof obj == 'function' || false; 105 | }; 106 | } else { 107 | lm.utils.isFunction = function( obj ) { 108 | return toString.call( obj ) === '[object Function]'; 109 | }; 110 | } 111 | 112 | lm.utils.fnBind = function( fn, context, boundArgs ) { 113 | 114 | if( Function.prototype.bind !== undefined ) { 115 | return Function.prototype.bind.apply( fn, [ context ].concat( boundArgs || [] ) ); 116 | } 117 | 118 | var bound = function() { 119 | 120 | // Join the already applied arguments to the now called ones (after converting to an array again). 121 | var args = ( boundArgs || [] ).concat( Array.prototype.slice.call( arguments, 0 ) ); 122 | 123 | // If not being called as a constructor 124 | if( !(this instanceof bound) ) { 125 | // return the result of the function called bound to target and partially applied. 126 | return fn.apply( context, args ); 127 | } 128 | // If being called as a constructor, apply the function bound to self. 129 | fn.apply( this, args ); 130 | }; 131 | // Attach the prototype of the function to our newly created function. 132 | bound.prototype = fn.prototype; 133 | return bound; 134 | }; 135 | 136 | lm.utils.removeFromArray = function( item, array ) { 137 | var index = lm.utils.indexOf( item, array ); 138 | 139 | if( index === -1 ) { 140 | throw new Error( 'Can\'t remove item from array. Item is not in the array' ); 141 | } 142 | 143 | array.splice( index, 1 ); 144 | }; 145 | 146 | lm.utils.now = function() { 147 | if( typeof Date.now === 'function' ) { 148 | return Date.now(); 149 | } else { 150 | return ( new Date() ).getTime(); 151 | } 152 | }; 153 | 154 | lm.utils.getUniqueId = function() { 155 | return ( Math.random() * 1000000000000000 ) 156 | .toString( 36 ) 157 | .replace( '.', '' ); 158 | }; 159 | 160 | /** 161 | * A basic XSS filter. It is ultimately up to the 162 | * implementing developer to make sure their particular 163 | * applications and usecases are save from cross site scripting attacks 164 | * 165 | * @param {String} input 166 | * @param {Boolean} keepTags 167 | * 168 | * @returns {String} filtered input 169 | */ 170 | lm.utils.filterXss = function( input, keepTags ) { 171 | 172 | var output = input 173 | .replace( /javascript/gi, 'javascript' ) 174 | .replace( /expression/gi, 'expression' ) 175 | .replace( /onload/gi, 'onload' ) 176 | .replace( /script/gi, 'script' ) 177 | .replace( /onerror/gi, 'onerror' ); 178 | 179 | if( keepTags === true ) { 180 | return output; 181 | } else { 182 | return output 183 | .replace( />/g, '>' ) 184 | .replace( /]+)>)/ig, '' ) ); 197 | }; -------------------------------------------------------------------------------- /src/js/controls/DragProxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class creates a temporary container 3 | * for the component whilst it is being dragged 4 | * and handles drag events 5 | * 6 | * @constructor 7 | * @private 8 | * 9 | * @param {Number} x The initial x position 10 | * @param {Number} y The initial y position 11 | * @param {lm.utils.DragListener} dragListener 12 | * @param {lm.LayoutManager} layoutManager 13 | * @param {lm.item.AbstractContentItem} contentItem 14 | * @param {lm.item.AbstractContentItem} originalParent 15 | */ 16 | lm.controls.DragProxy = function( x, y, dragListener, layoutManager, contentItem, originalParent ) { 17 | 18 | lm.utils.EventEmitter.call( this ); 19 | 20 | this._dragListener = dragListener; 21 | this._layoutManager = layoutManager; 22 | this._contentItem = contentItem; 23 | this._originalParent = originalParent; 24 | 25 | this._area = null; 26 | this._lastValidArea = null; 27 | 28 | this._dragListener.on( 'drag', this._onDrag, this ); 29 | this._dragListener.on( 'dragStop', this._onDrop, this ); 30 | 31 | this.element = $( lm.controls.DragProxy._template ); 32 | if( originalParent && originalParent._side ) { 33 | this._sided = originalParent._sided; 34 | this.element.addClass( 'lm_' + originalParent._side ); 35 | if( [ 'right', 'bottom' ].indexOf( originalParent._side ) >= 0 ) 36 | this.element.find( '.lm_content' ).after( this.element.find( '.lm_header' ) ); 37 | } 38 | this.element.css( { left: x, top: y } ); 39 | this.element.find( '.lm_tab' ).attr( 'title', lm.utils.stripTags( this._contentItem.config.title ) ); 40 | this.element.find( '.lm_title' ).html( this._contentItem.config.title ); 41 | this.childElementContainer = this.element.find( '.lm_content' ); 42 | this.childElementContainer.append( contentItem.element ); 43 | 44 | this._updateTree(); 45 | this._layoutManager._$calculateItemAreas(); 46 | this._setDimensions(); 47 | 48 | $( document.body ).append( this.element ); 49 | 50 | var offset = this._layoutManager.container.offset(); 51 | 52 | this._minX = offset.left; 53 | this._minY = offset.top; 54 | this._maxX = this._layoutManager.container.width() + this._minX; 55 | this._maxY = this._layoutManager.container.height() + this._minY; 56 | this._width = this.element.width(); 57 | this._height = this.element.height(); 58 | 59 | this._setDropPosition( x, y ); 60 | }; 61 | 62 | lm.controls.DragProxy._template = '
    ' + 63 | '
    ' + 64 | '
      ' + 65 | '
    • ' + 66 | '' + 67 | '
    • ' + 68 | '
    ' + 69 | '
    ' + 70 | '
    ' + 71 | '
    '; 72 | 73 | lm.utils.copy( lm.controls.DragProxy.prototype, { 74 | 75 | /** 76 | * Callback on every mouseMove event during a drag. Determines if the drag is 77 | * still within the valid drag area and calls the layoutManager to highlight the 78 | * current drop area 79 | * 80 | * @param {Number} offsetX The difference from the original x position in px 81 | * @param {Number} offsetY The difference from the original y position in px 82 | * @param {jQuery DOM event} event 83 | * 84 | * @private 85 | * 86 | * @returns {void} 87 | */ 88 | _onDrag: function( offsetX, offsetY, event ) { 89 | 90 | event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[ 0 ] : event; 91 | 92 | var x = event.pageX, 93 | y = event.pageY, 94 | isWithinContainer = x > this._minX && x < this._maxX && y > this._minY && y < this._maxY; 95 | 96 | if( !isWithinContainer && this._layoutManager.config.settings.constrainDragToContainer === true ) { 97 | return; 98 | } 99 | 100 | this._setDropPosition( x, y ); 101 | }, 102 | 103 | /** 104 | * Sets the target position, highlighting the appropriate area 105 | * 106 | * @param {Number} x The x position in px 107 | * @param {Number} y The y position in px 108 | * 109 | * @private 110 | * 111 | * @returns {void} 112 | */ 113 | _setDropPosition: function( x, y ) { 114 | this.element.css( { left: x, top: y } ); 115 | this._area = this._layoutManager._$getArea( x, y ); 116 | 117 | if( this._area !== null ) { 118 | this._lastValidArea = this._area; 119 | this._area.contentItem._$highlightDropZone( x, y, this._area ); 120 | } 121 | }, 122 | 123 | /** 124 | * Callback when the drag has finished. Determines the drop area 125 | * and adds the child to it 126 | * 127 | * @private 128 | * 129 | * @returns {void} 130 | */ 131 | _onDrop: function() { 132 | this._layoutManager.dropTargetIndicator.hide(); 133 | 134 | /* 135 | * Valid drop area found 136 | */ 137 | if( this._area !== null ) { 138 | this._area.contentItem._$onDrop( this._contentItem, this._area ); 139 | 140 | /** 141 | * No valid drop area available at present, but one has been found before. 142 | * Use it 143 | */ 144 | } else if( this._lastValidArea !== null ) { 145 | this._lastValidArea.contentItem._$onDrop( this._contentItem, this._lastValidArea ); 146 | 147 | /** 148 | * No valid drop area found during the duration of the drag. Return 149 | * content item to its original position if a original parent is provided. 150 | * (Which is not the case if the drag had been initiated by createDragSource) 151 | */ 152 | } else if( this._originalParent ) { 153 | this._originalParent.addChild( this._contentItem ); 154 | 155 | /** 156 | * The drag didn't ultimately end up with adding the content item to 157 | * any container. In order to ensure clean up happens, destroy the 158 | * content item. 159 | */ 160 | } else { 161 | this._contentItem._$destroy(); 162 | } 163 | 164 | this.element.remove(); 165 | 166 | this._layoutManager.emit( 'itemDropped', this._contentItem ); 167 | }, 168 | 169 | /** 170 | * Removes the item from its original position within the tree 171 | * 172 | * @private 173 | * 174 | * @returns {void} 175 | */ 176 | _updateTree: function() { 177 | 178 | /** 179 | * parent is null if the drag had been initiated by a external drag source 180 | */ 181 | if( this._contentItem.parent ) { 182 | this._contentItem.parent.removeChild( this._contentItem, true ); 183 | } 184 | 185 | this._contentItem._$setParent( this ); 186 | }, 187 | 188 | /** 189 | * Updates the Drag Proxie's dimensions 190 | * 191 | * @private 192 | * 193 | * @returns {void} 194 | */ 195 | _setDimensions: function() { 196 | var dimensions = this._layoutManager.config.dimensions, 197 | width = dimensions.dragProxyWidth, 198 | height = dimensions.dragProxyHeight; 199 | 200 | this.element.width( width ); 201 | this.element.height( height ); 202 | width -= ( this._sided ? dimensions.headerHeight : 0 ); 203 | height -= ( !this._sided ? dimensions.headerHeight : 0 ); 204 | this.childElementContainer.width( width ); 205 | this.childElementContainer.height( height ); 206 | this._contentItem.element.width( width ); 207 | this._contentItem.element.height( height ); 208 | this._contentItem.callDownwards( '_$show' ); 209 | this._contentItem.callDownwards( 'setSize' ); 210 | } 211 | } ); 212 | -------------------------------------------------------------------------------- /src/less/goldenlayout-dark-theme.less: -------------------------------------------------------------------------------- 1 | // Color variables (appears count calculates by raw css) 2 | @color0: #000000; // Appears 7 times 3 | @color1: #222222; // Appears 3 times 4 | @color2: #eeeeee; // Appears 2 times 5 | @color3: #dddddd; // Appears 2 times 6 | 7 | @color4: #cccccc; // Appears 1 time 8 | @color5: #444444; // Appears 1 time 9 | @color6: #999999; // Appears 1 time 10 | @color7: #111111; // Appears 1 time 11 | @color8: #452500; // Appears 1 time 12 | @color9: #555555; // Appears 1 time 13 | @color10: #ffffff; // Appears 2 time 14 | 15 | // ".lm_dragging" is applied to BODY tag during Drag and is also directly applied to the root of the object being dragged 16 | 17 | // Entire GoldenLayout Container, if a background is set, it is visible as color of "pane header" and "splitters" (if these latest has opacity very low) 18 | .lm_goldenlayout { 19 | background: @color0; 20 | } 21 | 22 | // Single Pane content (area in which final dragged content is contained) 23 | .lm_content { 24 | background: @color1; 25 | } 26 | 27 | // Single Pane content during Drag (style of moving window following mouse) 28 | .lm_dragProxy { 29 | .lm_content { 30 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9); 31 | } 32 | } 33 | 34 | // Placeholder Container of target position 35 | .lm_dropTargetIndicator { 36 | box-shadow: inset 0 0 30px @color0; 37 | outline: 1px dashed @color4; 38 | transition: all 200ms ease; 39 | 40 | // Inner Placeholder 41 | .lm_inner { 42 | background: @color0; 43 | opacity: 0.2; 44 | } 45 | } 46 | 47 | // Separator line (handle to change pane size) 48 | .lm_splitter { 49 | background: @color0; 50 | opacity: 0.001; 51 | transition: opacity 200ms ease; 52 | 53 | &:hover, // When hovered by mouse... 54 | &.lm_dragging { 55 | background: @color5; 56 | opacity: 1; 57 | } 58 | } 59 | 60 | // Pane Header (container of Tabs for each pane) 61 | .lm_header { 62 | height: 20px; 63 | user-select: none; 64 | 65 | &.lm_selectable { 66 | cursor: pointer; 67 | } 68 | 69 | // Single Tab container. A single Tab is set for each pane, a group of Tabs are contained in ".lm_header" 70 | .lm_tab { 71 | font-family: Arial, sans-serif; 72 | font-size: 12px; 73 | color: @color6; 74 | background: @color7; 75 | box-shadow: 2px -2px 2px rgba(0, 0, 0, 0.3); 76 | margin-right: 2px; 77 | padding-bottom: 2px; 78 | padding-top: 2px; 79 | 80 | /*.lm_title // Present in LIGHT Theme 81 | { 82 | padding-top:1px; 83 | }*/ 84 | 85 | // Close Tab Icon 86 | .lm_close_tab { 87 | width: 11px; 88 | height: 11px; 89 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAATElEQVR4nG3OwQ0DMQwDwZGRBtR/j1YJzMc5+IDoR+yCVO29g+pu981MFgqZmRdAfU7+CYWcbF11LwALjpBL0N0qybNx/RPU+gOeiS/+XCRwDlTgkQAAAABJRU5ErkJggg==); 90 | background-position: center center; 91 | background-repeat: no-repeat; 92 | top: 4px; 93 | right: 6px; 94 | opacity: 0.4; 95 | 96 | &:hover { 97 | opacity: 1; 98 | } 99 | } 100 | 101 | // If Tab is active, so if it's in foreground 102 | &.lm_active { 103 | border-bottom: none; 104 | box-shadow: 0 -2px 2px @color0; 105 | padding-bottom: 3px; 106 | 107 | .lm_close_tab { 108 | opacity: 1; 109 | } 110 | } 111 | } 112 | } 113 | 114 | .lm_dragProxy.lm_bottom, 115 | .lm_stack.lm_bottom { 116 | .lm_header .lm_tab { 117 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3); 118 | &.lm_active { 119 | box-shadow: 0 2px 2px @color0; 120 | } 121 | } 122 | } 123 | 124 | // If Pane Header (container of Tabs for each pane) is selected (used only if addition of new Contents is made "by selection" and not "by drag") 125 | .lm_selected { 126 | .lm_header { 127 | background-color: @color8; 128 | } 129 | } 130 | 131 | .lm_tab { 132 | &:hover, // If Tab is hovered 133 | &.lm_active // If Tab is active, so if it's in foreground 134 | { 135 | background: @color1; 136 | color: @color3; 137 | } 138 | } 139 | 140 | // Dropdown arrow for additional tabs when too many to be displayed 141 | .lm_header .lm_controls .lm_tabdropdown:before { 142 | color: @color10; 143 | } 144 | 145 | // Pane controls (popout, maximize, minimize, close) 146 | .lm_controls { 147 | // All Pane controls shares these 148 | > li { 149 | position: relative; 150 | background-position: center center; 151 | background-repeat: no-repeat; 152 | opacity: 0.4; 153 | transition: opacity 300ms ease; 154 | 155 | &:hover { 156 | opacity: 1; 157 | } 158 | } 159 | 160 | // Icon to PopOut Pane, so move it to a different Browser Window 161 | .lm_popout { 162 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=); 163 | } 164 | 165 | // Icon to Maximize Pane, so it will fill the entire GoldenLayout Container 166 | .lm_maximise { 167 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==); 168 | } 169 | 170 | // Icon to Close Pane and so remove it from GoldenLayout Container 171 | .lm_close { 172 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=); 173 | } 174 | } 175 | 176 | // If a specific Pane is maximized 177 | .lm_maximised { 178 | // Pane Header (container of Tabs for each pane) can have different style when is Maximized 179 | .lm_header { 180 | background-color: @color0; 181 | } 182 | 183 | // Pane controls are different in Maximized Mode, especially the old Icon "Maximise" that now has a different meaning, so "Minimize" (even if CSS Class did not change) 184 | .lm_controls { 185 | .lm_maximise { 186 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVR4nGP8//8/AzGAiShVI1YhCwMDA8OsWbPwBmZaWhoj0SYCAN1lBxMAX4n0AAAAAElFTkSuQmCC); 187 | } 188 | } 189 | } 190 | 191 | .lm_transition_indicator { 192 | background-color: @color0; 193 | border: 1px dashed @color9; 194 | } 195 | 196 | // If a specific Pane is Popped Out, so move it to a different Browser Window, Icon to restore original position is: 197 | .lm_popin { 198 | cursor: pointer; 199 | 200 | // Background of Icon 201 | .lm_bg { 202 | background: @color10; 203 | opacity: 0.3; 204 | } 205 | 206 | // Icon to Restore original position in Golden Layout Container 207 | .lm_icon { 208 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC); 209 | background-position: center center; 210 | background-repeat: no-repeat; 211 | border-left: 1px solid @color2; 212 | border-top: 1px solid @color2; 213 | opacity: 0.7; 214 | } 215 | 216 | &:hover { 217 | .lm_icon { 218 | opacity: 1; 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/less/goldenlayout-translucent-theme.less: -------------------------------------------------------------------------------- 1 | // Color variables (appears count calculates by raw css) 2 | @color0: #ffffff; // Appears 7 times 3 | 4 | // ".lm_dragging" is applied to BODY tag during Drag and is also directly applied to the root of the object being dragged 5 | 6 | // Entire GoldenLayout Container, if a background is set, it is visible as color of "pane header" and "splitters" (if these latest has opacity very low) 7 | .lm_goldenlayout { 8 | background: #dodgerblue; 9 | background: linear-gradient(to right bottom, dodgerblue, palevioletred); 10 | } 11 | 12 | // Single Pane content (area in which final dragged content is contained) 13 | .lm_content { 14 | background: rgba(255, 255, 255, 0.1); 15 | box-shadow: 0 0 15px 2px rgba(0, 0, 0, 0.1); 16 | color: whitesmoke; 17 | } 18 | 19 | // Single Pane content during Drag (style of moving window following mouse) 20 | .lm_dragProxy { 21 | .lm_content { 22 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9); 23 | } 24 | } 25 | 26 | // Placeholder Container of target position 27 | .lm_dropTargetIndicator { 28 | box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.5); 29 | outline: 1px dashed @color0; 30 | margin: 1px; 31 | transition: all 200ms ease; 32 | 33 | // Inner Placeholder (Used in other Themes but actually not here) 34 | /*.lm_inner 35 | { 36 | background:@color0; 37 | opacity:0.1; 38 | }*/ 39 | } 40 | 41 | // Separator line (handle to change pane size) 42 | .lm_splitter { 43 | background: @color0; 44 | opacity: 0.001; 45 | transition: opacity 200ms ease; 46 | 47 | &:hover, // When hovered by mouse... 48 | &.lm_dragging { 49 | background: @color0; 50 | opacity: 0.4; 51 | } 52 | } 53 | 54 | // Pane Header (container of Tabs for each pane) 55 | .lm_header { 56 | height: 20px; 57 | //user-select:none; // Present in DARK Theme 58 | 59 | &.lm_selectable { 60 | cursor: pointer; 61 | } 62 | 63 | // Single Tab container. A single Tab is set for each pane, a group of Tabs are contained in ".lm_header" 64 | .lm_tab { 65 | font-family: Arial, sans-serif; 66 | font-size: 13px; 67 | color: @color0; 68 | background: rgba(255, 255, 255, 0.1); 69 | margin-right: 2px; 70 | padding-bottom: 4px; 71 | 72 | // Close Tab Icon 73 | .lm_close_tab { 74 | width: 11px; 75 | height: 11px; 76 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAATElEQVR4nG3OwQ0DMQwDwZGRBtR/j1YJzMc5+IDoR+yCVO29g+pu981MFgqZmRdAfU7+CYWcbF11LwALjpBL0N0qybNx/RPU+gOeiS/+XCRwDlTgkQAAAABJRU5ErkJggg==); 77 | background-position: center center; 78 | background-repeat: no-repeat; 79 | right: 6px; 80 | top: 4px; 81 | opacity: 0.4; 82 | 83 | &:hover { 84 | opacity: 1; 85 | } 86 | } 87 | 88 | // If Tab is active, so if it's in foreground 89 | &.lm_active { 90 | border-bottom: none; 91 | box-shadow: 2px -2px 2px -2px rgba(0, 0, 0, 0.2); 92 | padding-bottom: 5px; 93 | 94 | .lm_close_tab { 95 | opacity: 1; 96 | } 97 | } 98 | } 99 | } 100 | 101 | .lm_dragProxy.lm_bottom, 102 | .lm_stack.lm_bottom { 103 | .lm_header .lm_tab { 104 | &.lm_active { 105 | box-shadow: 2px 2px 2px -2px rgba(0, 0, 0, 0.2); 106 | } 107 | } 108 | } 109 | 110 | // If Pane Header (container of Tabs for each pane) is selected (used only if addition of new Contents is made "by selection" and not "by drag") 111 | .lm_selected { 112 | // (Used in other Themes but actually not here) 113 | /*.lm_header 114 | { 115 | background-color:@color6; 116 | }*/ 117 | } 118 | 119 | .lm_tab { 120 | &:hover, // If Tab is hovered 121 | &.lm_active // If Tab is active, so if it's in foreground 122 | { 123 | background: rgba(255, 255, 255, 0.3); 124 | color: @color0; 125 | } 126 | } 127 | 128 | // Dropdown arrow for additional tabs when too many to be displayed 129 | // (Used in other Themes but actually not here) 130 | /* 131 | .lm_header .lm_controls .lm_tabdropdown:before 132 | { 133 | color:@color1; 134 | }*/ 135 | 136 | // Pane controls (popout, maximize, minimize, close) 137 | .lm_controls { 138 | // All Pane controls shares these 139 | > li { 140 | position: relative; 141 | background-position: center center; 142 | background-repeat: no-repeat; 143 | opacity: 0.4; 144 | transition: opacity 300ms ease; 145 | 146 | &:hover { 147 | opacity: 1; 148 | } 149 | } 150 | 151 | // Icon to PopOut Pane, so move it to a different Browser Window 152 | .lm_popout { 153 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=); 154 | } 155 | 156 | // Icon to Maximize Pane, so it will fill the entire GoldenLayout Container 157 | .lm_maximise { 158 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==); 159 | } 160 | 161 | // Icon to Close Pane and so remove it from GoldenLayout Container 162 | .lm_close { 163 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=); 164 | } 165 | } 166 | 167 | // If a specific Pane is maximized 168 | // (Used in other Themes but actually not here) 169 | /* 170 | .lm_maximised 171 | { 172 | // Pane Header (container of Tabs for each pane) can have different style when is Maximized 173 | .lm_header 174 | { 175 | background-color:@color4; 176 | } 177 | 178 | // Pane controls are different in Maximized Mode, especially the old Icon "Maximise" that now has a different meaning, so "Minimize" (even if CSS Class did not change) 179 | .lm_controls 180 | { 181 | .lm_maximise 182 | { 183 | background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAJklEQVR4nGP8//8/AyHARFDFUFbEwsDAwMDIyIgzHP7//89IlEkApSkHEScJTKoAAAAASUVORK5CYII=); 184 | } 185 | } 186 | } 187 | */ 188 | 189 | // (Used in other Themes but actually not here) 190 | /* 191 | .lm_transition_indicator 192 | { 193 | background-color:@color1; 194 | border:1px dashed @color5; 195 | }*/ 196 | 197 | // If a specific Pane is Popped Out, so move it to a different Browser Window, Icon to restore original position is: 198 | .lm_popin { 199 | cursor: pointer; 200 | 201 | // Background of Icon 202 | // (Used in other Themes but actually not here) 203 | /* 204 | .lm_bg 205 | { 206 | background:@color1; 207 | opacity:0.7; 208 | }*/ 209 | 210 | // Icon to Restore original position in Golden Layout Container 211 | .lm_icon { 212 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC); 213 | background-position: center center; 214 | background-repeat: no-repeat; 215 | opacity: 0.7; 216 | } 217 | 218 | &:hover { 219 | .lm_icon { 220 | opacity: 1; 221 | } 222 | } 223 | } 224 | 225 | // Present only in this Theme 226 | 227 | .lm_item { 228 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1); 229 | } 230 | -------------------------------------------------------------------------------- /src/less/goldenlayout-light-theme.less: -------------------------------------------------------------------------------- 1 | // Color variables (appears count calculates by raw css) 2 | @color0: #e1e1e1; // Appears 3 times 3 | @color1: #000000; // Appears 4 times 4 | @color2: #cccccc; // Appears 3 times 5 | @color3: #777777; // Appears 2 times 6 | 7 | @color4: #ffffff; // Appears 1 time 8 | @color5: #555555; // Appears 1 time 9 | @color6: #452500; // Appears 1 time 10 | @color7: #fafafa; // Appears 1 time 11 | @color8: #999999; // Appears 1 time 12 | @color9: #bbbbbb; // Appears 1 time 13 | @color10: #888888; // Appears 1 time 14 | 15 | // ".lm_dragging" is applied to BODY tag during Drag and is also directly applied to the root of the object being dragged 16 | 17 | // Entire GoldenLayout Container, if a background is set, it is visible as color of "pane header" and "splitters" (if these latest has opacity very low) 18 | .lm_goldenlayout { 19 | //background:@color0; 20 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAGFBMVEX29vb19fXw8PDy8vL09PTz8/Pv7+/x8fGKuegbAAAAyUlEQVR42pXRQQ7CMBRDwST9pfe/MahEmgURbt7WmpVb6+vG0dd9REnn66xRy/qXiCgmEIIJhGACIZhACCYQgvlDCDFIEAwSBIMEwSBBMEgQDBIEgwTBIEEwCJEMQiSDENFMQmQzCZEbNyGemd6KeGZ6u4hnXe2qbdLHFjhf1XqNLXHev4wdMd9nspiEiWISJgqECQJhgkCYIBAmCIQJAmGCQJggECYJhAkCEUMEwhCBMEQgDJEIQ2RSg0iEIRJhiB/S+rrjqvXQ3paIJUgPBXxiAAAAAElFTkSuQmCC); 21 | } 22 | 23 | // Single Pane content (area in which final dragged content is contained) 24 | .lm_content { 25 | background: @color0; 26 | border: 1px solid @color2; 27 | } 28 | 29 | // Single Pane content during Drag (style of moving window following mouse) 30 | .lm_dragProxy { 31 | .lm_content { 32 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 33 | box-sizing: border-box; 34 | } 35 | } 36 | 37 | // Placeholder Container of target position 38 | .lm_dropTargetIndicator { 39 | box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.4); 40 | outline: 1px dashed @color2; 41 | margin: 1px; 42 | transition: all 200ms ease; 43 | 44 | // Inner Placeholder 45 | .lm_inner { 46 | background: @color1; 47 | opacity: 0.1; 48 | } 49 | } 50 | 51 | // Separator line (handle to change pane size) 52 | .lm_splitter { 53 | background: @color8; 54 | opacity: 0.001; 55 | transition: opacity 200ms ease; 56 | 57 | &:hover, // When hovered by mouse... 58 | &.lm_dragging { 59 | background: @color9; 60 | opacity: 1; 61 | } 62 | } 63 | 64 | // Pane Header (container of Tabs for each pane) 65 | .lm_header { 66 | height: 20px; 67 | //user-select:none; // Present in DARK Theme 68 | 69 | &.lm_selectable { 70 | cursor: pointer; 71 | } 72 | 73 | // Single Tab container. A single Tab is set for each pane, a group of Tabs are contained in ".lm_header" 74 | .lm_tab { 75 | font-family: Arial, sans-serif; 76 | font-size: 12px; 77 | color: @color10; 78 | background: @color7; 79 | margin-right: 2px; 80 | padding-bottom: 4px; 81 | border: 1px solid @color2; 82 | border-bottom: none; 83 | 84 | .lm_title { 85 | padding-top: 1px; 86 | } 87 | 88 | // Close Tab Icon 89 | .lm_close_tab { 90 | width: 11px; 91 | height: 11px; 92 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAIklEQVR4nGNgYGD4z4Ad/Mdg4ODDBXCZRFgCp5EEHQMXBwAQAgz0SVCcggAAAABJRU5ErkJggg==); 93 | background-position: center center; 94 | background-repeat: no-repeat; 95 | right: 6px; 96 | top: 4px; 97 | opacity: 0.4; 98 | 99 | &:hover { 100 | opacity: 1; 101 | } 102 | } 103 | 104 | // If Tab is active, so if it's in foreground 105 | &.lm_active { 106 | border-bottom: none; 107 | box-shadow: 2px -2px 2px -2px rgba(0, 0, 0, 0.2); 108 | padding-bottom: 5px; 109 | 110 | .lm_close_tab { 111 | opacity: 1; 112 | } 113 | } 114 | } 115 | } 116 | 117 | .lm_dragProxy.lm_bottom, 118 | .lm_stack.lm_bottom { 119 | .lm_header .lm_tab { 120 | &.lm_active { 121 | box-shadow: 2px 2px 2px -2px rgba(0, 0, 0, 0.2); 122 | } 123 | } 124 | } 125 | 126 | // If Pane Header (container of Tabs for each pane) is selected (used only if addition of new Contents is made "by selection" and not "by drag") 127 | .lm_selected { 128 | .lm_header { 129 | background-color: @color6; 130 | } 131 | } 132 | 133 | .lm_tab { 134 | &:hover, // If Tab is hovered 135 | &.lm_active // If Tab is active, so if it's in foreground 136 | { 137 | background: @color0; 138 | color: @color3; 139 | } 140 | } 141 | 142 | // Dropdown arrow for additional tabs when too many to be displayed 143 | .lm_header .lm_controls .lm_tabdropdown:before { 144 | color: @color1; 145 | } 146 | 147 | // Pane controls (popout, maximize, minimize, close) 148 | .lm_controls { 149 | // All Pane controls shares these 150 | > li { 151 | position: relative; 152 | background-position: center center; 153 | background-repeat: no-repeat; 154 | opacity: 0.4; 155 | transition: opacity 300ms ease; 156 | 157 | &:hover { 158 | opacity: 1; 159 | } 160 | } 161 | 162 | // Icon to PopOut Pane, so move it to a different Browser Window 163 | .lm_popout { 164 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAANUlEQVR4nI2QMQoAMAwCz5L/f9mOzZIaN0E9UDyZhaaQz6atgBHgambEJ5wBKoS0WaIvfT+6K2MIECN19MAAAAAASUVORK5CYII=); 165 | } 166 | 167 | // Icon to Maximize Pane, so it will fill the entire GoldenLayout Container 168 | .lm_maximise { 169 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAIklEQVR4nGNkYGD4z0AAMBFSAAOETPpPlEmDUREjAxHhBABPvAQLFv3qngAAAABJRU5ErkJggg==); 170 | } 171 | 172 | // Icon to Close Pane and so remove it from GoldenLayout Container 173 | .lm_close { 174 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKUlEQVR4nGNgYGD4z4Af/Mdg4FKASwCnDf8JKSBoAtEmEXQTQd8RDCcA6+4Q8OvIgasAAAAASUVORK5CYII=); 175 | } 176 | } 177 | 178 | // If a specific Pane is maximized 179 | .lm_maximised { 180 | // Pane Header (container of Tabs for each pane) can have different style when is Maximized 181 | .lm_header { 182 | background-color: @color4; 183 | } 184 | 185 | // Pane controls are different in Maximized Mode, especially the old Icon "Maximise" that now has a different meaning, so "Minimize" (even if CSS Class did not change) 186 | .lm_controls { 187 | .lm_maximise { 188 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAJklEQVR4nGP8//8/AyHARFDFUFbEwsDAwMDIyIgzHP7//89IlEkApSkHEScJTKoAAAAASUVORK5CYII=); 189 | } 190 | } 191 | } 192 | 193 | .lm_transition_indicator { 194 | background-color: @color1; 195 | border: 1px dashed @color5; 196 | } 197 | 198 | // If a specific Pane is Popped Out, so move it to a different Browser Window, Icon to restore original position is: 199 | .lm_popin { 200 | cursor: pointer; 201 | 202 | // Background of Icon 203 | .lm_bg { 204 | background: @color1; 205 | opacity: 0.7; 206 | } 207 | 208 | // Icon to Restore original position in Golden Layout Container 209 | .lm_icon { 210 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC); 211 | background-position: center center; 212 | background-repeat: no-repeat; 213 | opacity: 0.7; 214 | } 215 | 216 | &:hover { 217 | .lm_icon { 218 | opacity: 1; 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/less/goldenlayout-soda-theme.less: -------------------------------------------------------------------------------- 1 | // Color variables (appears count calculates by raw css) 2 | @color0: #000000; // Appears 7 times 3 | @color1: #eeeeee; // Appears 3 times 4 | @color2: #444444; // Appears 2 times 5 | @color3: #222222; // Appears 1 time 6 | @color4: #555555; // Appears 1 time 7 | @color5: #452500; // Appears 1 time 8 | @color6: #999999; // Appears 1 time 9 | @color7: #272822; // Appears 1 time 10 | @color8: #cccccc; // Appears 1 time 11 | 12 | // ".lm_dragging" is applied to BODY tag during Drag and is also directly applied to the root of the object being dragged 13 | 14 | // Entire GoldenLayout Container, if a background is set, it is visible as color of "pane header" and "splitters" (if these latest has opacity very low) 15 | .lm_goldenlayout { 16 | background: @color0; 17 | background: linear-gradient(@color0, @color1); 18 | background-repeat: repeat; 19 | } 20 | 21 | // Single Pane content (area in which final dragged content is contained) 22 | .lm_content { 23 | background: @color7; 24 | } 25 | 26 | // Single Pane content during Drag (style of moving window following mouse) 27 | .lm_dragProxy { 28 | .lm_content { 29 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9); 30 | } 31 | } 32 | 33 | // Placeholder Container of target position 34 | .lm_dropTargetIndicator { 35 | box-shadow: inset 0 0 30px @color0; 36 | outline: 1px dashed @color8; 37 | transition: all 200ms ease; 38 | 39 | // Inner Placeholder 40 | .lm_inner { 41 | background: @color0; 42 | opacity: 0.2; 43 | } 44 | } 45 | 46 | // Separator line (handle to change pane size) 47 | .lm_splitter { 48 | background: @color0; 49 | opacity: 0.001; 50 | transition: opacity 200ms ease; 51 | 52 | &:hover, // When hovered by mouse... 53 | &.lm_dragging { 54 | background: @color2; 55 | opacity: 1; 56 | } 57 | } 58 | 59 | // Pane Header (container of Tabs for each pane) 60 | .lm_header { 61 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcCAIAAAAvP0KbAAAANElEQVR4nH2IsQ0AMAyDHM5J/v8qD3ixulWdOiAQmhkAquoi6frt33udBEnYprvZXZJg+wAKcQ/o96fYNQAAAABJRU5ErkJggg==); 62 | height: 28px; 63 | //user-select:none; // Present in DARK Theme 64 | 65 | &.lm_selectable { 66 | cursor: pointer; 67 | } 68 | 69 | // Single Tab container. A single Tab is set for each pane, a group of Tabs are contained in ".lm_header" 70 | .lm_tab { 71 | font-family: Arial, sans-serif; 72 | font-size: 13px; 73 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcCAIAAAAvP0KbAAAANklEQVR4nHXGsQ0AMAgDQcuFh2EC9p+HhpIGaCMlKV5/cHdKoiQC+DYzl8+/nJk0M0YEu5tVtXqyIehfJSkOAAAAAElFTkSuQmCC); 74 | color: @color6; 75 | margin: 0; 76 | padding-bottom: 4px; 77 | 78 | // (Used in other Themes but actually not here) 79 | /* 80 | .lm_title 81 | { 82 | padding-top:1px; 83 | }*/ 84 | 85 | // Close Tab Icon 86 | .lm_close_tab { 87 | width: 11px; 88 | height: 11px; 89 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAATElEQVR4nG3OwQ0DMQwDwZGRBtR/j1YJzMc5+IDoR+yCVO29g+pu981MFgqZmRdAfU7+CYWcbF11LwALjpBL0N0qybNx/RPU+gOeiS/+XCRwDlTgkQAAAABJRU5ErkJggg==); 90 | background-position: center center; 91 | background-repeat: no-repeat; 92 | right: 6px; 93 | top: 4px; 94 | opacity: 0.4; 95 | 96 | &:hover { 97 | opacity: 1; 98 | } 99 | } 100 | 101 | // If Tab is active, so if it's in foreground 102 | &.lm_active { 103 | border-bottom: none; 104 | padding-bottom: 5px; 105 | 106 | .lm_close_tab { 107 | opacity: 1; 108 | } 109 | } 110 | } 111 | } 112 | .lm_stack.lm_left, 113 | .lm_stack.lm_right { 114 | .lm_header { 115 | background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAABCAIAAABCJ1mGAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QQHEjUmFgXMqwAAADBJREFUCNdth7ENADAMwkjOIf9/xQMsqEPVTPVg2TUz3V0PANcb310nAWCbpKQktg/HHA+z1P+XmwAAAABJRU5ErkJggg==); 116 | } 117 | } 118 | // If Pane Header (container of Tabs for each pane) is selected (used only if addition of new Contents is made "by selection" and not "by drag") 119 | .lm_selected { 120 | .lm_header { 121 | background-color: @color5; 122 | } 123 | } 124 | 125 | .lm_tab { 126 | &:hover, // If Tab is hovered 127 | &.lm_active // If Tab is active, so if it's in foreground 128 | { 129 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcCAIAAAAvP0KbAAAAKUlEQVR4nGPw8vJi4ubmZmJgYGD6//8/nEZnY+MTUoPM/vfvH9PPnz8BJQc56Apw2moAAAAASUVORK5CYII=); 130 | color: @color1; 131 | } 132 | } 133 | 134 | // Dropdown arrow for additional tabs when too many to be displayed 135 | .lm_header .lm_controls .lm_tabdropdown:before { 136 | color: @color1; 137 | } 138 | 139 | // Pane controls (popout, maximize, minimize, close) 140 | .lm_controls { 141 | // All Pane controls shares these 142 | > li { 143 | position: relative; 144 | background-position: center center; 145 | background-repeat: no-repeat; 146 | opacity: 0.4; 147 | transition: opacity 300ms ease; 148 | 149 | &:hover { 150 | opacity: 1; 151 | } 152 | } 153 | 154 | // Icon to PopOut Pane, so move it to a different Browser Window 155 | .lm_popout { 156 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=); 157 | } 158 | 159 | // Icon to Maximize Pane, so it will fill the entire GoldenLayout Container 160 | .lm_maximise { 161 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==); 162 | } 163 | 164 | // Icon to Close Pane and so remove it from GoldenLayout Container 165 | .lm_close { 166 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=); 167 | } 168 | } 169 | 170 | // If a specific Pane is maximized 171 | .lm_maximised { 172 | // Pane Header (container of Tabs for each pane) can have different style when is Maximized 173 | .lm_header { 174 | background-color: @color0; 175 | } 176 | 177 | // Pane controls are different in Maximized Mode, especially the old Icon "Maximise" that now has a different meaning, so "Minimize" (even if CSS Class did not change) 178 | .lm_controls { 179 | .lm_maximise { 180 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAJklEQVR4nGP8//8/AyHARFDFUFbEwsDAwMDIyIgzHP7//89IlEkApSkHEScJTKoAAAAASUVORK5CYII=); 181 | } 182 | } 183 | } 184 | 185 | .lm_transition_indicator { 186 | background-color: @color0; 187 | border: 1px dashed @color4; 188 | } 189 | 190 | // If a specific Pane is Popped Out, so move it to a different Browser Window, Icon to restore original position is: 191 | .lm_popin { 192 | cursor: pointer; 193 | 194 | // Background of Icon 195 | .lm_bg { 196 | background: @color1; 197 | opacity: 0.7; 198 | } 199 | 200 | // Icon to Restore original position in Golden Layout Container 201 | .lm_icon { 202 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC); 203 | background-position: center center; 204 | background-repeat: no-repeat; 205 | opacity: 0.7; 206 | } 207 | 208 | &:hover { 209 | .lm_icon { 210 | opacity: 1; 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/less/goldenlayout-base.less: -------------------------------------------------------------------------------- 1 | // Width variables (appears count calculates by raw css) 2 | @width0: 100%; // Appears 3 times 3 | @width1: 20px; // Appears 2 times 4 | @width2: 100px; // Appears 1 time 5 | @width3: 14px; // Appears 1 time 6 | @width4: 18px; // Appears 1 time 7 | @width5: 15px; // Appears 1 time 8 | @width6: 2px; // Appears 1 time 9 | 10 | // Height variables (appears count calculates by raw css) 11 | @height0: 100%; // Appears 4 times 12 | @height1: 20px; // Appears 2 times 13 | @height2: 14px; // Appears 2 times 14 | @height3: 10px; // Appears 1 time 15 | @height4: 19px; // Appears 1 time 16 | @height5: 18px; // Appears 1 time 17 | @height6: 15px; // Appears 1 time 18 | 19 | .lm_root { 20 | position: relative; 21 | } 22 | 23 | .lm_row > .lm_item { 24 | float: left; 25 | } 26 | 27 | // Single Pane content (area in which final dragged content is contained) 28 | .lm_content { 29 | overflow: hidden; 30 | position: relative; 31 | } 32 | 33 | // ".lm_dragging" is applied to BODY tag during Drag and is also directly applied to the root of the object being dragged 34 | .lm_dragging, 35 | .lm_dragging * { 36 | cursor: move !important; 37 | user-select: none; 38 | } 39 | 40 | // If a specific Pane is maximized 41 | .lm_maximised { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | z-index: 40; 46 | } 47 | 48 | .lm_maximise_placeholder { 49 | display: none; 50 | } 51 | 52 | // Separator line (handle to change pane size) 53 | .lm_splitter { 54 | position: relative; 55 | z-index: 20; 56 | 57 | &:hover, // When hovered by mouse... 58 | &.lm_dragging { 59 | background: orange; 60 | } 61 | 62 | &.lm_vertical { 63 | .lm_drag_handle { 64 | width: @width0; 65 | position: absolute; 66 | cursor: ns-resize; 67 | } 68 | } 69 | 70 | &.lm_horizontal { 71 | float: left; 72 | height: @height0; 73 | 74 | .lm_drag_handle { 75 | height: @height0; 76 | position: absolute; 77 | cursor: ew-resize; 78 | } 79 | } 80 | } 81 | 82 | // Pane Header (container of Tabs for each pane) 83 | .lm_header { 84 | overflow: visible; 85 | position: relative; 86 | z-index: 1; 87 | 88 | [class^=lm_] { 89 | box-sizing: content-box !important; 90 | } 91 | 92 | // Pane controls (popout, maximize, minimize, close) 93 | .lm_controls { 94 | position: absolute; 95 | right: 3px; 96 | 97 | > li { 98 | cursor: pointer; 99 | float: left; 100 | width: @width4; 101 | height: @height5; 102 | text-align: center; 103 | } 104 | } 105 | 106 | ul { 107 | margin: 0; 108 | padding: 0; 109 | list-style-type: none; 110 | } 111 | 112 | .lm_tabs { 113 | position: absolute; 114 | } 115 | 116 | // Single Tab container. A single Tab is set for each pane, a group of Tabs are contained in ".lm_header" 117 | .lm_tab { 118 | cursor: pointer; 119 | float: left; 120 | height: @height2; 121 | margin-top: 1px; 122 | padding: 0px 10px 5px; 123 | padding-right: 25px; 124 | position: relative; 125 | 126 | i { 127 | width: @width6; 128 | height: @height4; 129 | position: absolute; 130 | 131 | &.lm_left { 132 | top: 0; 133 | left: -2px; 134 | } 135 | 136 | &.lm_right { 137 | top: 0; 138 | right: -2px; 139 | } 140 | } 141 | 142 | .lm_title { 143 | display: inline-block; 144 | overflow: hidden; 145 | text-overflow: ellipsis; 146 | } 147 | 148 | // Close Tab Icon 149 | .lm_close_tab { 150 | width: @width3; 151 | height: @height2; 152 | position: absolute; 153 | top: 0; 154 | right: 0; 155 | text-align: center; 156 | } 157 | } 158 | } 159 | 160 | // Headers positions 161 | .lm_stack.lm_left, 162 | .lm_stack.lm_right { 163 | .lm_header { 164 | height: 100%; 165 | } 166 | } 167 | 168 | .lm_dragProxy.lm_left, 169 | .lm_dragProxy.lm_right, 170 | .lm_stack.lm_left, 171 | .lm_stack.lm_right { 172 | .lm_header { 173 | width: 20px; 174 | float: left; 175 | vertical-align: top; 176 | .lm_tabs { 177 | transform-origin: left top; 178 | top: 0; 179 | width:1000px; /*hack*/ 180 | } 181 | .lm_controls { 182 | bottom:0; 183 | } 184 | } 185 | .lm_items { 186 | float:left; 187 | } 188 | } 189 | 190 | .lm_dragProxy.lm_left, 191 | .lm_stack.lm_left { 192 | .lm_header { 193 | .lm_tabs { 194 | transform: rotate(-90deg) scaleX(-1); 195 | left: 0; 196 | .lm_tab { 197 | transform: scaleX(-1); 198 | margin-top: 1px; 199 | } 200 | } 201 | .lm_tabdropdown_list { 202 | top:initial; 203 | right:initial; 204 | left:20px; 205 | } 206 | } 207 | } 208 | 209 | .lm_dragProxy.lm_right .lm_content { 210 | float: left; 211 | } 212 | 213 | .lm_dragProxy.lm_right, 214 | .lm_stack.lm_right { 215 | .lm_header { 216 | .lm_tabs { 217 | transform: rotate(90deg) scaleX(1); 218 | left: 100%; 219 | margin-left: 0; 220 | } 221 | .lm_controls { 222 | left: 3px; 223 | } 224 | .lm_tabdropdown_list { 225 | top:initial; 226 | right:20px; 227 | } 228 | } 229 | } 230 | 231 | .lm_dragProxy.lm_bottom, 232 | .lm_stack.lm_bottom { 233 | .lm_header { 234 | .lm_tab { 235 | margin-top:0; 236 | border-top: none; 237 | } 238 | .lm_controls { 239 | top: 3px; 240 | } 241 | .lm_tabdropdown_list { 242 | top:initial; 243 | bottom:20px; 244 | } 245 | } 246 | } 247 | 248 | .lm_drop_tab_placeholder { 249 | float: left; 250 | width: @width2; 251 | height: @height3; 252 | visibility: hidden; 253 | } 254 | 255 | // Dropdown arrow for additional tabs when too many to be displayed 256 | .lm_header { 257 | .lm_controls .lm_tabdropdown:before { 258 | content: ''; 259 | width: 0; 260 | height: 0; 261 | vertical-align: middle; 262 | display: inline-block; 263 | border-top: 5px dashed; 264 | border-right: 5px solid transparent; 265 | border-left: 5px solid transparent; 266 | color: white; // Overriden in specific Themes 267 | } 268 | 269 | .lm_tabdropdown_list { 270 | position: absolute; 271 | top: 20px; 272 | right: 0; 273 | z-index: 5; 274 | overflow: hidden; 275 | 276 | .lm_tab { 277 | clear: both; 278 | padding-right: 10px; 279 | margin: 0; 280 | 281 | .lm_title { 282 | width: 100px; 283 | } 284 | } 285 | 286 | .lm_close_tab { 287 | display: none !important; 288 | } 289 | } 290 | } 291 | 292 | /*********************************** 293 | * Drag Proxy 294 | ***********************************/ 295 | 296 | // Single Pane content during Drag (style of moving window following mouse) 297 | .lm_dragProxy { 298 | position: absolute; 299 | top: 0; 300 | left: 0; 301 | z-index: 30; 302 | 303 | .lm_header { 304 | background: transparent; 305 | } 306 | 307 | .lm_content { 308 | border-top: none; 309 | overflow: hidden; 310 | } 311 | } 312 | 313 | // Placeholder Container of target position 314 | .lm_dropTargetIndicator { 315 | display: none; 316 | position: absolute; 317 | z-index: 20; 318 | 319 | // Inner Placeholder 320 | .lm_inner { 321 | width: @width0; 322 | height: @height0; 323 | position: relative; 324 | top: 0; 325 | left: 0; 326 | } 327 | } 328 | 329 | .lm_transition_indicator { 330 | display: none; 331 | width: @width1; 332 | height: @height1; 333 | position: absolute; 334 | top: 0; 335 | left: 0; 336 | z-index: 20; 337 | } 338 | 339 | // If a specific Pane is Popped Out, so move it to a different Browser Window, Icon to restore original position is: 340 | .lm_popin { 341 | width: @width1; 342 | height: @height1; 343 | position: absolute; 344 | bottom: 0; 345 | right: 0; 346 | z-index: 9999; 347 | 348 | > * { 349 | width: @width0; 350 | height: @height0; 351 | position: absolute; 352 | top: 0; 353 | left: 0; 354 | } 355 | 356 | > .lm_bg { 357 | z-index: 10; 358 | } 359 | 360 | > .lm_icon { 361 | z-index: 20; 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/js/controls/BrowserPopout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pops a content item out into a new browser window. 3 | * This is achieved by 4 | * 5 | * - Creating a new configuration with the content item as root element 6 | * - Serializing and minifying the configuration 7 | * - Opening the current window's URL with the configuration as a GET parameter 8 | * - GoldenLayout when opened in the new window will look for the GET parameter 9 | * and use it instead of the provided configuration 10 | * 11 | * @param {Object} config GoldenLayout item config 12 | * @param {Object} dimensions A map with width, height, top and left 13 | * @param {String} parentId The id of the element the item will be appended to on popIn 14 | * @param {Number} indexInParent The position of this element within its parent 15 | * @param {lm.LayoutManager} layoutManager 16 | */ 17 | lm.controls.BrowserPopout = function( config, dimensions, parentId, indexInParent, layoutManager ) { 18 | lm.utils.EventEmitter.call( this ); 19 | this.isInitialised = false; 20 | 21 | this._config = config; 22 | this._dimensions = dimensions; 23 | this._parentId = parentId; 24 | this._indexInParent = indexInParent; 25 | this._layoutManager = layoutManager; 26 | this._popoutWindow = null; 27 | this._id = null; 28 | this._createWindow(); 29 | }; 30 | 31 | lm.utils.copy( lm.controls.BrowserPopout.prototype, { 32 | 33 | toConfig: function() { 34 | if( this.isInitialised === false ) { 35 | throw new Error( 'Can\'t create config, layout not yet initialised' ); 36 | return; 37 | } 38 | return { 39 | dimensions: { 40 | width: this.getGlInstance().width, 41 | height: this.getGlInstance().height, 42 | left: this._popoutWindow.screenX || this._popoutWindow.screenLeft, 43 | top: this._popoutWindow.screenY || this._popoutWindow.screenTop 44 | }, 45 | content: this.getGlInstance().toConfig().content, 46 | parentId: this._parentId, 47 | indexInParent: this._indexInParent 48 | }; 49 | }, 50 | 51 | getGlInstance: function() { 52 | return this._popoutWindow.__glInstance; 53 | }, 54 | 55 | getWindow: function() { 56 | return this._popoutWindow; 57 | }, 58 | 59 | close: function() { 60 | if( this.getGlInstance() ) { 61 | this.getGlInstance()._$closeWindow(); 62 | } else { 63 | try { 64 | this.getWindow().close(); 65 | } catch( e ) { 66 | } 67 | } 68 | }, 69 | 70 | /** 71 | * Returns the popped out item to its original position. If the original 72 | * parent isn't available anymore it falls back to the layout's topmost element 73 | */ 74 | popIn: function() { 75 | var childConfig, 76 | parentItem, 77 | index = this._indexInParent; 78 | 79 | if( this._parentId ) { 80 | 81 | /* 82 | * The $.extend call seems a bit pointless, but it's crucial to 83 | * copy the config returned by this.getGlInstance().toConfig() 84 | * onto a new object. Internet Explorer keeps the references 85 | * to objects on the child window, resulting in the following error 86 | * once the child window is closed: 87 | * 88 | * The callee (server [not server application]) is not available and disappeared 89 | */ 90 | childConfig = $.extend( true, {}, this.getGlInstance().toConfig() ).content[ 0 ]; 91 | parentItem = this._layoutManager.root.getItemsById( this._parentId )[ 0 ]; 92 | 93 | /* 94 | * Fallback if parentItem is not available. Either add it to the topmost 95 | * item or make it the topmost item if the layout is empty 96 | */ 97 | if( !parentItem ) { 98 | if( this._layoutManager.root.contentItems.length > 0 ) { 99 | parentItem = this._layoutManager.root.contentItems[ 0 ]; 100 | } else { 101 | parentItem = this._layoutManager.root; 102 | } 103 | index = 0; 104 | } 105 | } 106 | 107 | parentItem.addChild( childConfig, this._indexInParent ); 108 | this.close(); 109 | }, 110 | 111 | /** 112 | * Creates the URL and window parameter 113 | * and opens a new window 114 | * 115 | * @private 116 | * 117 | * @returns {void} 118 | */ 119 | _createWindow: function() { 120 | var checkReadyInterval, 121 | url = this._createUrl(), 122 | 123 | /** 124 | * Bogus title to prevent re-usage of existing window with the 125 | * same title. The actual title will be set by the new window's 126 | * GoldenLayout instance if it detects that it is in subWindowMode 127 | */ 128 | title = Math.floor( Math.random() * 1000000 ).toString( 36 ), 129 | 130 | /** 131 | * The options as used in the window.open string 132 | */ 133 | options = this._serializeWindowOptions( { 134 | width: this._dimensions.width, 135 | height: this._dimensions.height, 136 | innerWidth: this._dimensions.width, 137 | innerHeight: this._dimensions.height, 138 | menubar: 'no', 139 | toolbar: 'no', 140 | location: 'no', 141 | personalbar: 'no', 142 | resizable: 'yes', 143 | scrollbars: 'no', 144 | status: 'no' 145 | } ); 146 | 147 | this._popoutWindow = window.open( url, title, options ); 148 | 149 | if( !this._popoutWindow ) { 150 | if( this._layoutManager.config.settings.blockedPopoutsThrowError === true ) { 151 | var error = new Error( 'Popout blocked' ); 152 | error.type = 'popoutBlocked'; 153 | throw error; 154 | } else { 155 | return; 156 | } 157 | } 158 | 159 | $( this._popoutWindow ) 160 | .on( 'load', lm.utils.fnBind( this._positionWindow, this ) ) 161 | .on( 'unload beforeunload', lm.utils.fnBind( this._onClose, this ) ); 162 | 163 | /** 164 | * Polling the childwindow to find out if GoldenLayout has been initialised 165 | * doesn't seem optimal, but the alternatives - adding a callback to the parent 166 | * window or raising an event on the window object - both would introduce knowledge 167 | * about the parent to the child window which we'd rather avoid 168 | */ 169 | checkReadyInterval = setInterval( lm.utils.fnBind( function() { 170 | if( this._popoutWindow.__glInstance && this._popoutWindow.__glInstance.isInitialised ) { 171 | this._onInitialised(); 172 | clearInterval( checkReadyInterval ); 173 | } 174 | }, this ), 10 ); 175 | }, 176 | 177 | /** 178 | * Serialises a map of key:values to a window options string 179 | * 180 | * @param {Object} windowOptions 181 | * 182 | * @returns {String} serialised window options 183 | */ 184 | _serializeWindowOptions: function( windowOptions ) { 185 | var windowOptionsString = [], key; 186 | 187 | for( key in windowOptions ) { 188 | windowOptionsString.push( key + '=' + windowOptions[ key ] ); 189 | } 190 | 191 | return windowOptionsString.join( ',' ); 192 | }, 193 | 194 | /** 195 | * Creates the URL for the new window, including the 196 | * config GET parameter 197 | * 198 | * @returns {String} URL 199 | */ 200 | _createUrl: function() { 201 | var config = { content: this._config }, 202 | storageKey = 'gl-window-config-' + lm.utils.getUniqueId(), 203 | urlParts; 204 | 205 | config = ( new lm.utils.ConfigMinifier() ).minifyConfig( config ); 206 | 207 | try { 208 | localStorage.setItem( storageKey, JSON.stringify( config ) ); 209 | } catch( e ) { 210 | throw new Error( 'Error while writing to localStorage ' + e.toString() ); 211 | } 212 | 213 | urlParts = document.location.href.split( '?' ); 214 | 215 | // URL doesn't contain GET-parameters 216 | if( urlParts.length === 1 ) { 217 | return urlParts[ 0 ] + '?gl-window=' + storageKey; 218 | 219 | // URL contains GET-parameters 220 | } else { 221 | return document.location.href + '&gl-window=' + storageKey; 222 | } 223 | }, 224 | 225 | /** 226 | * Move the newly created window roughly to 227 | * where the component used to be. 228 | * 229 | * @private 230 | * 231 | * @returns {void} 232 | */ 233 | _positionWindow: function() { 234 | this._popoutWindow.moveTo( this._dimensions.left, this._dimensions.top ); 235 | this._popoutWindow.focus(); 236 | }, 237 | 238 | /** 239 | * Callback when the new window is opened and the GoldenLayout instance 240 | * within it is initialised 241 | * 242 | * @returns {void} 243 | */ 244 | _onInitialised: function() { 245 | this.isInitialised = true; 246 | this.getGlInstance().on( 'popIn', this.popIn, this ); 247 | this.emit( 'initialised' ); 248 | }, 249 | 250 | /** 251 | * Invoked 50ms after the window unload event 252 | * 253 | * @private 254 | * 255 | * @returns {void} 256 | */ 257 | _onClose: function() { 258 | setTimeout( lm.utils.fnBind( this.emit, this, [ 'closed' ] ), 50 ); 259 | } 260 | } ); -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | $( function() { 2 | var queryParams = getQueryParams(); 3 | var layout = queryParams.layout || ''; 4 | var config = null; 5 | switch( layout.toLowerCase() ) { 6 | case 'responsive': 7 | config = createResponsiveConfig(); 8 | break; 9 | case 'tab-dropdown': 10 | config = createTabDropdownConfig(); 11 | break; 12 | default: 13 | config = createStandardConfig(); 14 | break; 15 | } 16 | 17 | window.myLayout = new GoldenLayout( config ); 18 | 19 | var rotate = function( container ) { 20 | if( !container ) return; 21 | while( container.parent && container.type != 'stack' ) 22 | container = container.parent; 23 | if( container.parent ) { 24 | var p = container.header.position(); 25 | var sides = [ 'top', 'right', 'bottom', 'left', false ]; 26 | var n = sides[ ( sides.indexOf( p ) + 1 ) % sides.length ]; 27 | container.header.position( n ); 28 | } 29 | } 30 | var nexttheme = function() { 31 | var link = $( 'link[href*=theme]' ), href = link.attr( 'href' ).split( '-' ); 32 | var themes = [ 'dark', 'light', 'soda', 'translucent' ]; 33 | href[ 1 ] = themes[ ( themes.indexOf( href[ 1 ] ) + 1 ) % themes.length ]; 34 | link.attr( 'href', href.join( '-' ) ); 35 | } 36 | 37 | myLayout.registerComponent( 'html', function( container, state ) { 38 | container 39 | .getElement() 40 | .html( state.html ? state.html.join( '\n' ) : '

    ' + container._config.title + '

    ' ); 41 | 42 | if( state.style ) { 43 | $( 'head' ).append( '' ); 44 | } 45 | 46 | if( state.className ) { 47 | container.getElement().addClass( state.className ); 48 | } 49 | 50 | if( state.bg ) { 51 | container 52 | .getElement() 53 | .text( 'hey' ) 54 | .append( '
    ' ) 55 | .append( $( '