├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── example ├── app.scss ├── components │ └── App.js ├── index.html ├── index.js ├── server.js ├── styles │ └── main.scss └── webpack.config.js ├── index.js ├── package.json ├── src ├── components │ ├── DragSpan.js │ ├── TabList.js │ ├── TabPanel.js │ └── workspace.js ├── index.js ├── manager.js ├── utils.js └── visibleArea.js ├── styles └── main.scss ├── test ├── build.spec.js ├── index.spec.js ├── manager.spec.js ├── mocha.opts └── utils │ └── document.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "targets": { "node": 6 }, "useBuiltIns": true }], 4 | "stage-1", 5 | "react" 6 | ], 7 | "plugins": ["add-module-exports", "transform-decorators-legacy"], 8 | "env": { 9 | "production": { 10 | "presets": ["react-optimize"], 11 | "plugins": ["babel-plugin-dev-expression"] 12 | }, 13 | "development": { 14 | "plugins": [ 15 | "transform-decorators-legacy", 16 | "transform-class-properties", 17 | "transform-es2015-classes", 18 | "tcomb" 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | **/node_modules 3 | **/webpack.config.js 4 | examples/**/server.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "react/jsx-uses-react": 2, 10 | "react/jsx-uses-vars": 2, 11 | "react/react-in-jsx-scope": 2 12 | }, 13 | "plugins": [ 14 | "react" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | dist 5 | lib 6 | coverage 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | src 4 | test 5 | examples 6 | coverage 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "iojs" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sean Dokko 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-workspace 2 | ========================= 3 | 4 | A fusion between [react-tabs][react-tabs] and [react-split-pane][react-split-pane]. 5 | 6 | ![apr-30-2017 19-29-47](https://cloud.githubusercontent.com/assets/1214868/25564006/da700b6c-2ddb-11e7-9ed2-2d4ecf2076ff.gif) 7 | 8 | 9 | ### How to use 10 | 11 | ```js 12 | // A representation of the panel structure 13 | const root = { 14 | axis: 'x', 15 | size: 50, 16 | children: [ 17 | { 18 | size: 50, 19 | axis: 'y', 20 | children: [ 21 | {}, 22 | {} 23 | ] 24 | }, 25 | {} 26 | ] 27 | }; 28 | 29 | 30 | const components = { 31 | green: ( 32 |
33 | ), 34 | red: ( 35 |
36 | ), 37 | yellow: ( 38 |
39 | ), 40 | blue: ( 41 |
42 | ), 43 | }; 44 | 45 | const tabs = { 46 | // keys are paths of root, 47 | // values are representations of tabs 48 | 'children[0].children[0]': ['green', 'red'], // if it is an array, then it will be a tab 49 | 'children[0].children[1]': 'blue', // if not, just render the component itself 50 | 'children[1]': ['yellow', 'red'] 51 | } 52 | 53 | const workspace = ( 54 | {}} root={root} tabs={tabs} components={components}/> 55 | ); 56 | 57 | ``` 58 | 59 | 60 | [react-split-pane]: https://github.com/tomkp/react-split-pane 61 | [react-tabs]: https://github.com/reactjs/react-tabs 62 | -------------------------------------------------------------------------------- /example/app.scss: -------------------------------------------------------------------------------- 1 | 2 | ul p { 3 | margin-top: 0; 4 | } -------------------------------------------------------------------------------- /example/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Workspace from 'react-pane'; 3 | import '../styles/main.scss'; 4 | import '../app.scss'; 5 | 6 | export default class App extends Component { 7 | onChange(root, tabs) { 8 | console.log('new root: ', root); 9 | console.log('tabs: ', tabs); 10 | } 11 | 12 | render() { 13 | const root = { 14 | axis: 'x', 15 | children: [ 16 | { 17 | axis: 'y', 18 | size: 50, 19 | children: [ 20 | { 21 | axis: 'x', 22 | size: 70, 23 | children: [ 24 | { 25 | size: 30, 26 | sidebar: true // sidebar 27 | }, 28 | { 29 | axis: 'y', 30 | size: 70, 31 | children: [ 32 | { 33 | editor: true // editor 34 | }, 35 | { 36 | block: true // block 37 | } 38 | ] 39 | } 40 | ] 41 | }, 42 | { 43 | size: 30, 44 | logs: true // logs 45 | } 46 | ] 47 | }, 48 | { 49 | size: 50, 50 | browser: true // browser 51 | } 52 | ] 53 | }; 54 | 55 | const components = { 56 | sidebar: ( 57 |
58 |
    59 |
  • 60 |

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quis dolor suscipit provident nobis, tempore, deleniti laboriosam tempora. Veritatis explicabo, corrupti. Maxime cupiditate vero quisquam ab dignissimos id voluptates sed magni sint culpa veniam eius in, inventore veritatis consequatur! Ipsam asperiores adipisci, consectetur quasi perspiciatis voluptates hic reprehenderit eligendi vitae quaerat.

    61 |
  • 62 |
  • 63 |

    Dolorum harum, quasi. A sunt neque ullam, veniam sit, maiores qui ad odit voluptatem fugiat laborum maxime blanditiis cupiditate beatae, libero cumque consequuntur rem? Architecto quo suscipit maxime! Quis similique eius obcaecati, vero sed facere voluptas dolores error, assumenda repellat excepturi eos amet! Beatae cum sed, nemo quisquam, blanditiis tempore.

    64 |
  • 65 |
  • 66 |

    Modi eligendi labore temporibus provident veniam saepe soluta, quisquam aut voluptatum omnis deleniti quaerat dignissimos fugiat, autem ut maiores sit maxime minima nihil corporis. Exercitationem quisquam dolorem doloremque tempore corporis dolorum atque impedit provident ab assumenda deserunt sapiente dolores, unde numquam temporibus obcaecati iure voluptatum doloribus nam, voluptatem fugiat labore.

    67 |
  • 68 |
  • 69 |

    Consequatur cupiditate veritatis sint saepe qui fugiat, quidem sunt voluptate placeat quas quasi quisquam animi earum atque aspernatur eum a dolore aperiam facilis, rem! Cupiditate doloribus maiores, repudiandae ut nobis distinctio amet totam quas accusantium soluta, inventore, quidem. Eaque minus eveniet quae, inventore ipsa reiciendis enim nobis! Error, perspiciatis, aut?

    70 |
  • 71 |
  • 72 |

    Soluta, et quisquam. Consequuntur temporibus voluptas sunt, sed ab dolorum magni ratione delectus eos harum adipisci, eaque expedita recusandae accusamus nostrum aperiam velit ea quisquam a porro vero ipsum rerum dolores molestias. Minus soluta excepturi amet ullam et sit impedit, similique facilis tenetur ad! Veritatis accusantium magnam quos saepe sint.

    73 |
  • 74 |
  • 75 |

    Provident vero quia nobis magnam esse fugiat numquam suscipit, dolorum voluptas delectus inventore, amet dolor hic odit iste, eos ad? Illum expedita, odio est porro, distinctio itaque vel quas aliquam nostrum maiores repellendus. Quas sequi eaque harum, sapiente ipsa maiores fugiat voluptatem repellendus beatae. Reprehenderit dolorum iste libero corporis sapiente.

    76 |
  • 77 |
  • 78 |

    Enim eum iusto dolore. Provident magni quas ipsam reprehenderit alias expedita obcaecati laboriosam iste recusandae saepe quam animi eaque autem nostrum velit, voluptate molestiae nihil amet earum nesciunt. Laborum a eius natus iusto voluptates. Natus distinctio repellat nobis? Obcaecati unde doloribus sapiente quibusdam fugiat consequatur doloremque tempore accusamus quia magnam.

    79 |
  • 80 |
  • 81 |

    Blanditiis corrupti, doloremque iure quibusdam quia sapiente aliquam, alias perferendis. Accusantium eum illo excepturi atque consectetur amet sapiente error, blanditiis impedit dolor, accusamus doloremque reprehenderit beatae magnam aliquid nisi unde sint. Quisquam possimus facere unde quaerat odio beatae modi? Eum qui, nemo numquam quam, cupiditate laudantium corrupti tempore. Recusandae, consequuntur.

    82 |
  • 83 |
  • 84 |

    Minima ex eum ipsum ratione error ullam consectetur enim rerum, quis sint veritatis dicta, est ipsam deserunt debitis nesciunt praesentium unde illo, necessitatibus quas distinctio! Quasi vel, eligendi voluptatum sapiente. Error id, laboriosam quo quis expedita inventore. Ratione recusandae autem, voluptatibus eaque aspernatur. Perferendis amet neque minus eius suscipit quisquam.

    85 |
  • 86 |
  • 87 |

    Rerum magnam possimus deleniti fugiat. Totam pariatur ipsum aspernatur doloremque repellendus aperiam ipsam distinctio quam obcaecati earum? Nihil, blanditiis, incidunt. Laborum veniam doloremque, cumque voluptatibus quibusdam dolore ut, ducimus quasi impedit soluta! Recusandae nesciunt, mollitia dignissimos molestiae vel velit inventore eos placeat itaque est esse dicta minima non harum ea.

    88 |
  • 89 |
  • 90 |

    Maxime mollitia recusandae corporis suscipit necessitatibus numquam dicta nisi, facilis placeat repellat nam quos officiis nesciunt, veritatis unde dolor. Adipisci debitis ipsa sed praesentium molestiae beatae saepe, sit neque fugit blanditiis ex corporis veniam expedita ad mollitia nulla nam id eaque a ducimus voluptatibus fuga maiores iure est? Doloribus, ducimus.

    91 |
  • 92 |
  • 93 |

    Eius distinctio corrupti numquam dolorem beatae soluta omnis temporibus atque sit, accusantium esse necessitatibus commodi officia ad. Possimus veniam enim, eveniet in distinctio voluptatem cumque. Voluptatum cum, hic atque. Consequatur a itaque sint sapiente dolores, nam dolore qui unde aspernatur consectetur, delectus quidem similique mollitia magni! Facere quo est atque?

    94 |
  • 95 |
  • 96 |

    Suscipit, dicta quas molestiae perspiciatis reiciendis, officia accusamus neque necessitatibus, magni tenetur omnis! Ratione in obcaecati, cumque nesciunt omnis vitae. Placeat dicta non excepturi expedita amet, libero nemo similique eos repellat officiis neque sunt iste. Reprehenderit veritatis illo minus atque dolores voluptatum a sunt porro molestiae corporis amet nisi, aut.

    97 |
  • 98 |
  • 99 |

    Doloribus tenetur vero asperiores facilis, quam nobis. Repellendus quas animi quisquam officia cupiditate commodi non cum vitae, eum provident quibusdam delectus. Cum pariatur explicabo quam architecto maxime ullam doloribus quia incidunt adipisci, libero esse dolores alias aliquid tempore praesentium! Tempora laudantium doloremque sequi facere nobis dolore dignissimos. Sint, dolor, deserunt?

    100 |
  • 101 |
  • 102 |

    Explicabo illo quod sapiente dolore totam quae eligendi nostrum iste quis voluptatibus quo a, odit aspernatur facere culpa quas incidunt, dolores repudiandae amet repellat officiis. Aut eum delectus, soluta facere quod veniam eligendi est a cumque, fuga doloribus, repudiandae sunt maxime expedita ea officia accusamus fugiat voluptates tempora. Qui, atque?

    103 |
  • 104 |
105 |
106 | ), 107 | editor: ( 108 |
109 | ), 110 | block: ( 111 |
112 | ), 113 | logs: ( 114 |
115 | ), 116 | configs: ( 117 |
118 | ), 119 | browser: ( 120 |
121 | ), 122 | }; 123 | 124 | const tabs = { 125 | 'children[0].children[0].children[0]': ['sidebar'], 126 | 'children[0].children[0].children[1].children[0]': ['editor'], 127 | 'children[0].children[0].children[1].children[1]': ['block'], 128 | 'children[0].children[1]': ['logs', 'configs'], 129 | 'children[1]': ['browser'], 130 | }; 131 | 132 | return ( 133 |
134 | 138 |
139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-workspace 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import App from './components/App'; 5 | 6 | render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true, 9 | stats: { 10 | colors: true 11 | } 12 | }).listen(3000, 'localhost', function (err) { 13 | if (err) { 14 | console.log(err); 15 | } 16 | 17 | console.log('Listening at localhost:3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /example/styles/main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | .workspace { 7 | width: 100%; 8 | height: 100%; 9 | position: fixed; 10 | } -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'webpack-dev-server/client?http://localhost:3000', 8 | 'webpack/hot/only-dev-server', 9 | 'babel-polyfill', 10 | './index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/static/' 16 | }, 17 | plugins: [ 18 | new webpack.HotModuleReplacementPlugin(), 19 | new webpack.NoErrorsPlugin() 20 | ], 21 | resolve: { 22 | alias: { 23 | 'react-pane': path.join(__dirname, '..', 'src') 24 | }, 25 | extensions: ['', '.js'] 26 | }, 27 | module: { 28 | loaders: [{ 29 | test: /\.js$/, 30 | loaders: ['react-hot', 'babel-loader'], 31 | exclude: /node_modules/, 32 | include: __dirname 33 | }, { 34 | test: /\.js$/, 35 | loaders: ['babel-loader'], 36 | include: path.join(__dirname, '..', 'src') 37 | }, 38 | { 39 | test: /\.scss$/, 40 | loaders: ["style-loader", "css-loader", "sass-loader"] 41 | } 42 | ] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dok/react-workspace/e50f6096d16f997cf61aa13e396f9f4879c64c95/index.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-workspaces", 3 | "version": "0.4.5", 4 | "description": "A component with a resizable and splittable workspace. A panel with draggable tabs.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib dist", 8 | "dev": "babel src --out-dir lib --watch", 9 | "build": "NODE_ENV=production babel src --out-dir lib", 10 | "build:umd": "webpack src/index.js dist/react-workspace.js && NODE_ENV=production webpack src/index.js dist/react-workspace.min.js", 11 | "lint": "eslint src test examples", 12 | "test": "NODE_ENV=test mocha --compilers js:babel-register --require ignore-styles test", 13 | "test:watch": "NODE_ENV=test mocha --watch --compilers js:babel-register --require ignore-styles test", 14 | "test:cov": "babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha", 15 | "example": "node example/server.js", 16 | "prepublish": "npm run build && npm run build:umd", 17 | "prepublish-example": "npm run lint && npm run test && npm run clean && npm run build && npm run build:umd" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/dok/react-workspace.git" 22 | }, 23 | "keywords": [ 24 | "panel", 25 | "react", 26 | "pane", 27 | "tabs", 28 | "split", 29 | "workspace" 30 | ], 31 | "author": "dok", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/dok/react-workspace/issues" 35 | }, 36 | "homepage": "https://github.com/dok/react-workspace", 37 | "peerDependencies": { 38 | "react": "^0.14 || ^15.0.0-rc || ^15.0", 39 | "react-dom": "^0.14 || ^15.0.0-rc || ^15.0" 40 | }, 41 | "devDependencies": { 42 | "babel-cli": "^6.24.1", 43 | "babel-core": "^6.22.1", 44 | "babel-eslint": "^7.1.1", 45 | "babel-loader": "^6.2.10", 46 | "babel-plugin-add-module-exports": "^0.2.1", 47 | "babel-plugin-dev-expression": "^0.2.1", 48 | "babel-plugin-minify-dead-code-elimination": "^0.1.4", 49 | "babel-plugin-minify-mangle-names": "0.0.8", 50 | "babel-plugin-minify-simplify": "0.0.8", 51 | "babel-plugin-tcomb": "^0.3.24", 52 | "babel-plugin-transform-class-properties": "^6.22.0", 53 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 54 | "babel-plugin-transform-es2015-classes": "^6.22.0", 55 | "babel-plugin-webpack-loaders": "^0.8.0", 56 | "babel-polyfill": "^6.23.0", 57 | "babel-preset-env": "^1.1.8", 58 | "babel-preset-react": "^6.22.0", 59 | "babel-preset-react-hmre": "^1.1.1", 60 | "babel-preset-react-optimize": "^1.0.1", 61 | "babel-preset-stage-0": "^6.22.0", 62 | "babel-register": "^6.22.0", 63 | "chai": "^3.5.0", 64 | "css-loader": "^0.28.0", 65 | "eslint": "^0.23", 66 | "eslint-config-airbnb": "0.0.6", 67 | "eslint-plugin-react": "^2.3.0", 68 | "expect": "^1.6.0", 69 | "ignore-styles": "^5.0.1", 70 | "isparta": "^3.0.3", 71 | "mocha": "^2.2.5", 72 | "node-libs-browser": "^0.5.2", 73 | "node-sass": "^4.5.2", 74 | "react": "^15.0", 75 | "react-dom": "^15.0", 76 | "react-hot-loader": "^1.2.7", 77 | "rimraf": "^2.3.4", 78 | "sass-loader": "^6.0.3", 79 | "style-loader": "^0.16.1", 80 | "webpack": "^1.14.0", 81 | "webpack-dev-server": "^1.16.5" 82 | }, 83 | "dependencies": { 84 | "classnames": "^2.2.5", 85 | "invariant": "^2.0.0", 86 | "lodash": "^4.17.4", 87 | "prop-types": "^15.5.8", 88 | "react-dnd": "^2.3.0", 89 | "react-dnd-html5-backend": "^2.3.0", 90 | "react-draggable": "^2.2.5", 91 | "react-redux": "^5.0.4", 92 | "react-resizable": "^1.6.0", 93 | "react-split-pane": "^0.1.63", 94 | "react-tabs": "^0.8.3" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/DragSpan.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 3 | import { mapDispatchToProps, mapStateToProps, dragSource, dragTarget } from '../utils'; 4 | import { DragDropContext, DragSource, DropTarget } from 'react-dnd'; 5 | import { connect } from 'react-redux'; 6 | 7 | @DragSource('TAB', dragSource, (connect, monitor) => ({ 8 | connectDragSource: connect.dragSource(), 9 | isDragging: monitor.isDragging(), 10 | })) 11 | export default class Comp extends Component { 12 | render() { 13 | const { connectDragSource, connectDropTarget } = this.props; 14 | 15 | return connectDragSource( 16 | 17 | {this.props.children} 18 | 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /src/components/TabList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { TabList } from 'react-tabs'; 3 | import { mapDispatchToProps, mapStateToProps, dragSource, dragTarget } from '../utils'; 4 | import { DragDropContext, DragSource, DropTarget } from 'react-dnd'; 5 | import { connect } from 'react-redux'; 6 | 7 | @DropTarget('TAB', dragTarget, connect => ({ 8 | connectDropTarget: connect.dropTarget(), 9 | })) 10 | export default class Comp extends Component { 11 | render() { 12 | const { connectDropTarget, path } = this.props; 13 | 14 | return connectDropTarget( 15 |
16 | 17 | {this.props.children} 18 | 19 |
20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /src/components/TabPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactDOM from 'react-dom'; 4 | import { TabPanel } from 'react-tabs'; 5 | import _ from 'lodash'; 6 | import elementResizeEvent, { unbind } from '../lib/element-resize-event'; 7 | import visibleArea from '../visibleArea'; 8 | 9 | const DEFAULT_CLASS = 'react-tabs__tab-panel'; 10 | 11 | // console.log(TabPanel.propTypes); 12 | 13 | class Comp extends TabPanel { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | onResize() { 19 | if(this.mounted) { 20 | const node = ReactDOM.findDOMNode(this); 21 | const parent = node.parentNode.parentNode; 22 | const prev = parseInt(parent.style.height, 10); 23 | const {height} = visibleArea(parent); 24 | 25 | if(prev === height || height === 0) { 26 | return; 27 | } 28 | if(height) { 29 | node.style.height = `${height - 32}px`; 30 | } 31 | } 32 | 33 | } 34 | 35 | componentDidMount() { 36 | if( typeof window !== 'undefined' ) { 37 | const fn = _.debounce(this.onResize.bind(this), 100); 38 | this.props.pubsub.on('resize', fn); 39 | // const elementResizeEvent = require('../lib/element-resize-event'); 40 | const node = ReactDOM.findDOMNode(this); 41 | // elementResizeEvent(node, this.onResize.bind(this)); 42 | elementResizeEvent(node, () => { 43 | this.props.pubsub.trigger('resize'); 44 | }); 45 | } 46 | this.mounted = true; 47 | } 48 | componentWillUnmount() { 49 | this.mounted = false; 50 | } 51 | } 52 | 53 | // Comp.propTypes = _.assign({ 54 | // key: PropTypes.number 55 | // }, TabPanel.propTypes); 56 | 57 | export default Comp; 58 | -------------------------------------------------------------------------------- /src/components/workspace.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import Manager from '../manager'; 5 | import { ResizableBox } from 'react-resizable'; 6 | import _ from 'lodash'; 7 | import Draggable from 'react-draggable'; 8 | import SplitPane from 'react-split-pane'; 9 | 10 | import { Tab, Tabs } from 'react-tabs'; 11 | 12 | import { dragSource, dragTarget } from '../utils'; 13 | import { DragDropContext, DragSource, DropTarget } from 'react-dnd'; 14 | import HTML5Backend from 'react-dnd-html5-backend'; 15 | 16 | import DragSpan from './DragSpan'; 17 | import TabList from './TabList'; 18 | import TabPanel from './TabPanel'; 19 | 20 | import '../../styles/main.scss'; 21 | 22 | class Events { 23 | constructor() { 24 | this.listeners = {}; 25 | } 26 | 27 | on(key, fn) { 28 | if(this.listeners[key]) { 29 | this.listeners[key].push(fn); 30 | } else { 31 | this.listeners[key] = [fn]; 32 | } 33 | } 34 | 35 | trigger(key) { 36 | _.each(this.listeners[key], (fn) => { 37 | fn(); 38 | }); 39 | } 40 | } 41 | 42 | @DragDropContext(HTML5Backend) 43 | class Workspace extends Component { 44 | constructor(props) { 45 | super(props); 46 | this.state = {...props}; 47 | this.pubsub = new Events(); 48 | } 49 | 50 | split(path, axis, multiplier) { 51 | const newRoot = Manager.split(this.state.root, path, axis, multiplier); 52 | this.setState({ 53 | root: newRoot 54 | }); 55 | } 56 | 57 | componentWillReceiveProps(nextProps) { 58 | this.setState({...nextProps}); 59 | } 60 | 61 | 62 | // { 63 | // x: [ 64 | // { 65 | // size: 50 66 | // }, 67 | // { 68 | // size: 50 69 | // } 70 | // ] 71 | // }; 72 | 73 | // const tabs = { 74 | // 'children[0].children[0]': ['green', 'red'], 75 | // 'children[0].children[1]': 'blue', 76 | // 'children[1]': ['yellow', 'red'] 77 | // } 78 | 79 | move(from, fromIndex, to, toIndex) { 80 | const newTabs = Manager.moveTab(this.state.tabs, from, fromIndex, to, toIndex); 81 | this.setState({ 82 | tabs: newTabs 83 | }); 84 | 85 | if(_.isFunction(this.props.onChange)) { 86 | this.props.onChange.call(this, this.state.root, newTabs); 87 | } 88 | 89 | } 90 | 91 | onResize() { 92 | this.pubsub.trigger('resize'); 93 | } 94 | 95 | renderTabs(components, path, index) { 96 | const tabs = this.state.tabs; 97 | const tabHeaders = _.map(components, (component, index) => { 98 | const tabName = tabs[path][index]; 99 | // const componentPath = `${path}[${index}]`; 100 | return ( 101 | 102 | 103 | {tabName} 104 | 105 | 106 | ); 107 | }); 108 | const tabPanels = _.map(components, (component, index) => { 109 | return ( 110 | 111 | {component} 112 | 113 | ); 114 | }); 115 | return ( 116 | 117 | 120 | {tabHeaders} 121 | 122 | {tabPanels} 123 | 124 | ); 125 | } 126 | 127 | renderNode(node, path='', index=0) { 128 | if(_.isArray(node.component)) { 129 | return this.renderTabs(node.component, path, index); 130 | } else if(node.component) { 131 | return node.component; 132 | } 133 | 134 | let children = null; 135 | const split = node.axis === 'x' ? 'vertical' : 'horizontal'; 136 | if(node.children) { 137 | children = _.map(node.children, (child, index) => { 138 | let childPath; 139 | if(path === '') { 140 | childPath = `children[${index}]`; 141 | } else { 142 | childPath = `${path}.children[${index}]`; 143 | } 144 | return this.renderNode(child, childPath, index); 145 | }); 146 | } 147 | 148 | const size = node.size ? `${node.size}%` : 200; 149 | 150 | return ( 151 | 152 | {children} 153 | 154 | ); 155 | } 156 | 157 | render() { 158 | const node = this.state.root; 159 | const axis = node.axis; 160 | 161 | // const root = this.renderNode(node, axis); 162 | // const newRoot = Manager.split(this.state.root, path, axis, multiplier); 163 | 164 | const tree = Manager.buildTree(node,this.state.components, this.state.tabs); 165 | const root = this.renderNode(tree); 166 | 167 | return ( 168 |
169 | {root} 170 |
171 | ); 172 | } 173 | 174 | } 175 | 176 | Workspace.propTypes = { 177 | root: PropTypes.object.isRequired, 178 | components: PropTypes.object.isRequired, 179 | onChange: PropTypes.func 180 | }; 181 | 182 | export default Workspace; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Workspace from './components/workspace'; 2 | 3 | export default Workspace; 4 | -------------------------------------------------------------------------------- /src/manager.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | 4 | export const MAX_DEPTH = 3; 5 | export const MSG = { 6 | IMPOSSIBLE_SPLIT: 'CANNOT SPLIT AT THIS PATH WITH AXIS', 7 | MAX_DEPTH: `CANNOT SPLIT BEYOND ${MAX_DEPTH} LAYERS`, 8 | INVALID_PATH: 'PANE VIA PATH NOT FOUND' 9 | }; 10 | 11 | class Manager { 12 | constructor() { 13 | } 14 | 15 | /** 16 | * Checks if current pane can be split in that axis 17 | * @return {Boolean} Returns true or false 18 | */ 19 | static validateSplit(state, path, axis, multiplier) { 20 | const depth = ''.split('.').length; 21 | let errors = []; 22 | 23 | if(depth > MAX_DEPTH) { 24 | errors.push(new Error(MSG.MAX_DEPTH)); 25 | } 26 | 27 | let root = _.cloneDeep(state); 28 | let pane; 29 | if(path === '') { 30 | pane = root; 31 | } else { 32 | pane = _.get(root, path); 33 | } 34 | 35 | if(!pane) { 36 | errors.push(new Error(MSG.NO_SUCH_PATH)); 37 | return errors; 38 | } 39 | 40 | // if clean slate 41 | if(!pane.children) { 42 | return errors; 43 | } 44 | 45 | if(pane.axis !== axis) { 46 | errors.push(new Error(MSG.IMPOSSIBLE_SPLIT)); 47 | } 48 | 49 | return errors; 50 | } 51 | 52 | /** 53 | * Creates a new state with the manipulation 54 | * @param {Object} state [description] 55 | * @param {String} path e.g. 'x[0].y[1]' 56 | * @param {String} axis 'x' or 'y' 57 | * @param {Number} muliplier How many times you want to split by 58 | * @return {Object} Returns a new state 59 | */ 60 | static split(state={}, path, axis, multiplier=2) { 61 | // needs to check if the current path can accept a split in the axis 62 | const errors = this.validateSplit(state, path, axis, multiplier); 63 | if(errors.length) { 64 | throw new Error(_.map(errors, _.identity)); 65 | } 66 | 67 | let root = _.cloneDeep(state); 68 | let currentPane; 69 | if(path === '') { 70 | currentPane = root; 71 | } else { 72 | currentPane = _.get(root, path); 73 | } 74 | 75 | const edited = this.splitPane(currentPane, axis, multiplier); 76 | return this.setPane(root, path, edited); 77 | } 78 | 79 | static setPane(root, path, pane) { 80 | if(path === '') { 81 | return pane; 82 | } else { 83 | return _.set(root, path, pane); 84 | } 85 | } 86 | 87 | 88 | /** 89 | * Split the panes at current position with axis 90 | */ 91 | static splitPane(pane, axis, multiplier) { 92 | const divider = _.round(100 / multiplier, 2); 93 | pane.axis = axis; 94 | 95 | // if pane doesn't have an existing setup 96 | if(!pane.children) { 97 | pane.children = [] 98 | for(var i = 0; i < multiplier; i++) { 99 | pane.children.push({ 100 | size: divider 101 | }); 102 | } 103 | } else { 104 | _.times(multiplier, (index) => { 105 | if(pane.children[index]) { 106 | pane.children[index].size = divider; 107 | } else { 108 | pane.children.push({ 109 | size: divider 110 | }); 111 | } 112 | }) 113 | } 114 | 115 | return pane; 116 | } 117 | 118 | static moveTab(tabs, from, fromIndex, to, toIndex) { 119 | // const tabs = { 120 | // 'children[0].children[0]': ['green', 'red'], 121 | // 'children[0].children[1]': 'blue', 122 | // 'children[1]': ['yellow', 'red'] 123 | // } 124 | if(!toIndex) { 125 | toIndex = tabs[to].length; 126 | } 127 | 128 | let newTabs = _.cloneDeep(tabs); 129 | const name = newTabs[from][fromIndex]; 130 | newTabs[from].splice(fromIndex, 1); 131 | newTabs[to].splice(toIndex, 0, name); 132 | 133 | return newTabs; 134 | } 135 | 136 | static buildTree(root, components, tabs) { 137 | function walk(node, path='') { 138 | if(tabs[path]) { 139 | if(_.isArray(tabs[path])) { 140 | node.component = _.map(tabs[path], 141 | (componentName) => components[componentName]); 142 | } else { 143 | node.component = components[tabs[path]]; 144 | } 145 | } 146 | 147 | if(node.children) { 148 | node.children = _.map(node.children, (child, index) => { 149 | let childPath; 150 | if(path === '') { 151 | childPath = `children[${index}]`; 152 | } else { 153 | childPath = `${path}.children[${index}]`; 154 | } 155 | return walk(child, childPath); 156 | }) 157 | } 158 | 159 | return node; 160 | } 161 | 162 | return walk(_.cloneDeep(root)); 163 | } 164 | } 165 | 166 | export default Manager; -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { findDOMNode } from 'react-dom'; 2 | 3 | const utils = { 4 | mapStateToProps: (state) => { 5 | return { 6 | // appState: state.get('app') 7 | }; 8 | }, 9 | mapDispatchToProps: (dispatch) => { 10 | // import * as AppActions from '../actions/app'; 11 | // const AppActions = require('../actions/app'); 12 | 13 | return bindActionCreators(null, dispatch); 14 | }, 15 | dragSource: { 16 | beginDrag(props) { 17 | console.log('props: ', props); 18 | return props; 19 | } 20 | }, 21 | dragTarget: { 22 | hover: (props, monitor, component) => { 23 | }, 24 | drop: (props, monitor, component) => { 25 | const item = monitor.getItem(); 26 | 27 | if(!item) { 28 | return; 29 | } 30 | 31 | const dragIndex = item.index; 32 | const hoverIndex = props.index; 33 | // Don't replace items with themselves 34 | // if (dragIndex === hoverIndex) { 35 | // return; 36 | // } 37 | 38 | // Determine rectangle on screen 39 | const node = findDOMNode(component); 40 | component.props.move(item.path, item.index, props.path); 41 | return; 42 | 43 | // const hoverBoundingRect = node.getBoundingClientRect(); 44 | 45 | // // Get vertical middle 46 | // const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; 47 | 48 | // // Determine mouse position 49 | // const clientOffset = monitor.getClientOffset(); 50 | 51 | // // Get pixels to the top 52 | // const hoverClientY = clientOffset.y - hoverBoundingRect.top; 53 | 54 | // // Only perform the move when the mouse has crossed half of the items height 55 | // // When dragging downwards, only move when the cursor is below 50% 56 | // // When dragging upwards, only move when the cursor is above 50% 57 | 58 | // // Dragging downwards 59 | // if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 60 | // return; 61 | // } 62 | 63 | // // Dragging upwards 64 | // if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 65 | // return; 66 | // } 67 | 68 | 69 | // console.log('moviddng: ', dragIndex, hoverIndex); 70 | // const ary = props.path.split(','); 71 | // props.move_item(ary.slice(0,ary.length - 1).join(','), dragIndex, hoverIndex); 72 | } 73 | } 74 | 75 | } 76 | 77 | export default utils; -------------------------------------------------------------------------------- /src/visibleArea.js: -------------------------------------------------------------------------------- 1 | // http://stackoverflow.com/questions/12868287/get-height-of-non-overflowed-portion-of-div 2 | 3 | export default function visibleArea(node){ 4 | var o = {height: node.offsetHeight, width: node.offsetWidth}, // size 5 | d = {y: (node.offsetTop || 0), x: (node.offsetLeft || 0), node: node.offsetParent}, // position 6 | css, y, x; 7 | while( null !== (node = node.parentNode) ){ // loop up through DOM 8 | css = window.getComputedStyle(node); 9 | if( css && css.overflow === 'hidden' ){ // if has style && overflow 10 | y = node.offsetHeight - d.y; // calculate visible y 11 | x = node.offsetWidth - d.x; // and x 12 | if( node !== d.node ){ 13 | y = y + (node.offsetTop || 0); // using || 0 in case it doesn't have an offsetParent 14 | x = x + (node.offsetLeft || 0); 15 | } 16 | if( y < o.height ) { 17 | if( y < 0 ) o.height = 0; 18 | else o.height = y; 19 | } 20 | if( x < o.width ) { 21 | if( x < 0 ) o.width = 0; 22 | else o.width = x; 23 | } 24 | return o; // return (modify if you want to loop up again) 25 | } 26 | if( node === d.node ){ // update offsets 27 | d.y = d.y + (node.offsetTop || 0); 28 | d.x = d.x + (node.offsetLeft || 0); 29 | d.node = node.offsetParent; 30 | } 31 | } 32 | return o; // return if no hidden 33 | } 34 | 35 | -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | .workspace { 2 | .react-tabs { 3 | [role=tablist] { 4 | height: 32px; 5 | margin: 0; 6 | } 7 | [role=tabpanel] { 8 | overflow: scroll; 9 | } 10 | width: 100%; 11 | height: 100%; 12 | } 13 | // box-sizing: border-box; 14 | // .pane { 15 | // display: flex; 16 | // flex-wrap: wrap; 17 | // flex: 0 auto; 18 | // border: 1px solid blue; 19 | // } 20 | // .pane-x { 21 | // // flex-direction: column ; 22 | // } 23 | // .pane-y { 24 | // // flex-direction: row; 25 | // } 26 | // * { 27 | // box-sizing: border-box; 28 | // } 29 | // .handle { 30 | // background: black; 31 | // } 32 | // .handle-axis-x { 33 | // height: 100%; 34 | // width: 2px; 35 | // cursor: col-resize; 36 | // } 37 | // .handle-axis-y { 38 | // width: 100%; 39 | // height: 2px; 40 | // cursor: row-resize; 41 | // } 42 | // .react-resizable { 43 | // position: relative; 44 | // } 45 | // .react-resizable-handle { 46 | // position: absolute; 47 | // width: 20px; 48 | // height: 20px; 49 | // bottom: 0; 50 | // right: 0; 51 | // background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); 52 | // background-position: bottom right; 53 | // padding: 0 3px 3px 0; 54 | // background-repeat: no-repeat; 55 | // background-origin: content-box; 56 | // box-sizing: border-box; 57 | // cursor: se-resize; 58 | // } 59 | 60 | .Resizer { 61 | background: #000; 62 | opacity: .2; 63 | z-index: 1; 64 | -moz-box-sizing: border-box; 65 | -webkit-box-sizing: border-box; 66 | box-sizing: border-box; 67 | -moz-background-clip: padding; 68 | -webkit-background-clip: padding; 69 | background-clip: padding-box; 70 | } 71 | 72 | .Resizer:hover { 73 | -webkit-transition: all 2s ease; 74 | transition: all 2s ease; 75 | } 76 | 77 | .Resizer.horizontal { 78 | height: 11px; 79 | margin: -5px 0; 80 | border-top: 5px solid rgba(255, 255, 255, 0); 81 | border-bottom: 5px solid rgba(255, 255, 255, 0); 82 | cursor: row-resize; 83 | width: 100%; 84 | } 85 | 86 | .Resizer.horizontal:hover { 87 | border-top: 5px solid rgba(0, 0, 0, 0.5); 88 | border-bottom: 5px solid rgba(0, 0, 0, 0.5); 89 | } 90 | 91 | .Resizer.vertical { 92 | width: 11px; 93 | margin: 0 -5px; 94 | border-left: 5px solid rgba(255, 255, 255, 0); 95 | border-right: 5px solid rgba(255, 255, 255, 0); 96 | cursor: col-resize; 97 | } 98 | 99 | .Resizer.vertical:hover { 100 | border-left: 5px solid rgba(0, 0, 0, 0.5); 101 | border-right: 5px solid rgba(0, 0, 0, 0.5); 102 | } 103 | Resizer.disabled { 104 | cursor: not-allowed; 105 | } 106 | Resizer.disabled:hover { 107 | border-color: transparent; 108 | } 109 | 110 | 111 | } -------------------------------------------------------------------------------- /test/build.spec.js: -------------------------------------------------------------------------------- 1 | const workspace = require('../lib/index.js'); 2 | import {expect} from 'chai'; 3 | 4 | 5 | describe('build', () => { 6 | it('should exist', () => { 7 | expect(workspace).to.exist; 8 | }) 9 | }) 10 | 11 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | // import { add } from '../src'; 3 | 4 | describe('workspace', () => { 5 | // it('should add 2 and 2', () => { 6 | // expect(add(2, 2)).toBe(4); 7 | // }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/manager.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import React from 'react'; 3 | 4 | import Manager from '../src/manager'; 5 | 6 | describe('manager', () => { 7 | it('should split a pane horizontally at root', () => { 8 | const state = Manager.split({}, '', 'x'); 9 | const expected = { 10 | axis: 'x', 11 | children: [ 12 | { 13 | size: 50 14 | }, 15 | { 16 | size: 50 17 | } 18 | ] 19 | }; 20 | expect(state).to.eql(expected); 21 | }); 22 | 23 | it('should split a pane horizontally at root with existing items', () => { 24 | const before = { 25 | axis: 'x', 26 | children: [ 27 | { 28 | size: 50, 29 | tabs: [{}] 30 | }, 31 | { 32 | size: 50, 33 | tabs: [{ 34 | name: 'configs' 35 | }] 36 | } 37 | ] 38 | }; 39 | const state = Manager.split(before, '', 'x', 3); 40 | const expected = { 41 | axis: 'x', 42 | children: [ 43 | { 44 | size: 33.33, 45 | tabs: [{}] 46 | }, 47 | { 48 | size: 33.33, 49 | tabs: [{ 50 | name: 'configs' 51 | }] 52 | }, 53 | { 54 | size: 33.33 55 | } 56 | ] 57 | }; 58 | expect(state).to.eql(expected); 59 | }); 60 | 61 | it('should throw an error if trying to split on both axis', () => { 62 | const before = { 63 | axis: 'x', 64 | children: [ 65 | { 66 | size: 50, 67 | tabs: [{}] 68 | }, 69 | { 70 | size: 50, 71 | tabs: [{ 72 | name: 'configs' 73 | }] 74 | } 75 | ] 76 | }; 77 | const fn = Manager.split.bind(Manager, before, '', 'y', 3); 78 | expect(fn).to.throw(Error); 79 | }); 80 | 81 | it('should throw an error if pane found via path doesn\'t exist', () => { 82 | const before = {}; 83 | const fn = Manager.split.bind(Manager, before, 'children[0]', 'y', 3); 84 | expect(fn).to.throw(Error); 85 | }); 86 | 87 | it('should throw an error if max depth is reached', () => { 88 | const before = { 89 | axis: 'x', 90 | children: [ 91 | { 92 | axis: 'y', 93 | children: [ 94 | { 95 | axis: 'x', 96 | children: [ 97 | ] 98 | } 99 | ] 100 | } 101 | ] 102 | }; 103 | const fn = Manager.split.bind(Manager, before, 'x[0].y[0].x[0].y[0]', 'y', 2); 104 | expect(fn).to.throw(Error); 105 | }); 106 | 107 | it('should split in a nested path', () => { 108 | const before = { 109 | axis: 'x', 110 | children: [ 111 | { 112 | size: 33 113 | }, 114 | { 115 | size: 33 116 | }, 117 | { 118 | size: 33 119 | }, 120 | ] 121 | }; 122 | const expected = { 123 | axis: 'x', 124 | children: [ 125 | { 126 | size: 33, 127 | axis: 'y', 128 | children: [ 129 | { 130 | size: 50 131 | }, 132 | { 133 | size: 50 134 | } 135 | ] 136 | }, 137 | { 138 | size: 33 139 | }, 140 | { 141 | size: 33 142 | }, 143 | ] 144 | }; 145 | const after = Manager.split(before, 'children[0]', 'y', 2); 146 | expect(after).to.eql(expected); 147 | }); 148 | 149 | it('should build a tree', () => { 150 | const root = { 151 | axis: 'x', 152 | children: [ 153 | { 154 | size: 50, 155 | axis: 'y', 156 | children: [ 157 | { 158 | size: 50 159 | }, 160 | { 161 | size: 50 162 | } 163 | ] 164 | }, 165 | { 166 | size: 50 167 | } 168 | ] 169 | }; 170 | const components = { 171 | green: ( 172 |
173 | ), 174 | red: ( 175 |
176 | ), 177 | yellow: ( 178 |
179 | ), 180 | }; 181 | 182 | const tabs = { 183 | 'children[0].children[0]': ['green', 'red'], 184 | 'children[0].children[1]': 'green', 185 | 'children[1]': 'yellow' 186 | }; 187 | 188 | const tree = Manager.buildTree(root, components, tabs); 189 | expect(tree).to.exist; 190 | // console.log(JSON.stringify(tree, null, 2)); 191 | 192 | }) 193 | 194 | it('should move a tab item', () => { 195 | const tabs = { 196 | 'children[0].children[0]': ['green', 'red'], 197 | 'children[0].children[1]': 'blue', 198 | 'children[1]': ['yellow', 'red'] 199 | } 200 | const expected = { 201 | 'children[0].children[0]': ['red'], 202 | 'children[0].children[1]': 'blue', 203 | 'children[1]': ['yellow', 'red', 'green'] 204 | } 205 | 206 | const after = Manager.moveTab(tabs, 'children[0].children[0]', 0, 'children[1]'); 207 | expect(after).to.eql(expected); 208 | }); 209 | 210 | }); -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel/register 2 | --recursive 3 | -------------------------------------------------------------------------------- /test/utils/document.js: -------------------------------------------------------------------------------- 1 | if (typeof document === 'undefined') { 2 | global.document = {}; 3 | } 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var plugins = [ 6 | new webpack.optimize.OccurenceOrderPlugin(), 7 | new webpack.DefinePlugin({ 8 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 9 | }) 10 | ]; 11 | 12 | // if (process.env.NODE_ENV === 'production') { 13 | // plugins.push( 14 | // new webpack.optimize.UglifyJsPlugin({ 15 | // compressor: { 16 | // screw_ie8: true, 17 | // warnings: false 18 | // } 19 | // }) 20 | // ); 21 | // } 22 | 23 | module.exports = { 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.js$/, 28 | loaders: ['babel-loader'], 29 | exclude: /node_modules/ 30 | }, 31 | { 32 | test: /\.scss$/, 33 | loaders: ["style-loader", "css-loader", "sass-loader"] 34 | }, 35 | ] 36 | }, 37 | output: { 38 | library: 'react-workspace', 39 | libraryTarget: 'umd' 40 | }, 41 | plugins: plugins, 42 | resolve: { 43 | extensions: ['', '.js'] 44 | } 45 | }; 46 | --------------------------------------------------------------------------------