├── .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 | 
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 |
--------------------------------------------------------------------------------