├── .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/) [](http://badge.fury.io/js/golden-layout) [](https://travis-ci.org/deepstreamIO/golden-layout)
2 |
3 | 
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 |
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 |
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 | ''
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 | '' +
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( $( '