├── example ├── .gitignore ├── javascripts │ ├── components │ │ ├── NavLink.jsx │ │ ├── StandardVerticalLayout.jsx │ │ ├── PercentageLayout.jsx │ │ ├── StandardHorizontalLayout.jsx │ │ ├── App.jsx │ │ ├── LayoutWithMinimalSize.jsx │ │ ├── TogglableSidebarLayout.jsx │ │ ├── HorizontalLayoutWithIFrame.jsx │ │ ├── HorizontalLayoutWithEvents.jsx │ │ ├── NestedLayout.jsx │ │ └── Lorem.jsx │ └── index.jsx ├── index.html ├── webpack.config.js └── stylesheets │ └── index.css ├── .babelrc ├── index.js ├── .gitignore ├── .editorconfig ├── .travis.yml ├── .eslintrc.js ├── test ├── setup.js ├── Pane.spec.jsx └── SplitterLayout.spec.jsx ├── webpack.config.js ├── src ├── components │ ├── Pane.jsx │ └── SplitterLayout.jsx └── stylesheets │ └── index.css ├── LICENSE ├── package.json └── README.md /example/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import SplitterLayout from './src/components/SplitterLayout'; 2 | 3 | export default SplitterLayout; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # IDE 5 | .idea/ 6 | .vscode/ 7 | 8 | # Build Output 9 | build/ 10 | coverage/ 11 | lib/ 12 | .nyc_output/ 13 | 14 | # NPM 15 | node_modules/ 16 | npm-debug.log* 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | [*] 3 | charset = utf-8 4 | end_of_line = lf 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "8" 5 | env: 6 | - NODE_ENV=TEST 7 | install: 8 | - npm install 9 | script: 10 | - npm run lint 11 | - npm run build 12 | after_success: 13 | - npm run coverage 14 | - npm run coveralls 15 | -------------------------------------------------------------------------------- /example/javascripts/components/NavLink.jsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: [0] */ 2 | import React from 'react'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | export default function(props) { 6 | return ( 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Splitter Layout 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | Fork me on GitHub 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': 'airbnb', 3 | 'env': { 4 | 'browser': true, 5 | 'jest': true 6 | }, 7 | 'rules': { 8 | 'class-methods-use-this': [0], 9 | 'comma-dangle': [2, 'never'], 10 | "function-paren-newline": [2, 'consistent'], 11 | 'max-len': [2, 120, 2, { 12 | 'ignoreUrls': true 13 | }], 14 | "no-confusing-arrow": [2, { "allowParens": true }], 15 | 'no-multiple-empty-lines': [1, { 'max': 1 }], 16 | 'no-plusplus': [0], 17 | 'object-curly-newline': [0], 18 | 'operator-linebreak': [2, 'after'], 19 | 'space-before-function-paren': [2, 'never'], 20 | 'react/destructuring-assignment': [0], 21 | 'react/no-did-mount-set-state': [0], 22 | 'react/jsx-one-expression-per-line': [0], 23 | 'jsx-a11y/no-noninteractive-element-interactions': [0] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: [ 6 | './javascripts/index.jsx' 7 | ], 8 | resolve: { 9 | extensions: ['.js', '.jsx', '.css'] 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | use: [ 17 | { 18 | loader: 'babel-loader', 19 | options: { 20 | presets: [ 21 | '@babel/env', 22 | '@babel/react' 23 | ] 24 | } 25 | } 26 | ] 27 | }, { 28 | test: /\.css$/, 29 | use: [ 30 | 'style-loader', 31 | 'css-loader' 32 | ] 33 | } 34 | ] 35 | }, 36 | output: { 37 | path: __dirname, 38 | filename: 'bundle.js' 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | 3 | const jsdom = new JSDOM(''); 4 | global.window = jsdom.window; 5 | global.document = window.document; 6 | 7 | document.simulateMouseUp = () => { 8 | document.dispatchEvent(new MouseEvent('mouseup')); 9 | }; 10 | 11 | document.simulateMouseMove = (clientX, clientY) => { 12 | document.dispatchEvent(new MouseEvent('mousemove', { clientX, clientY })); 13 | }; 14 | 15 | document.simulateTouchEnd = () => { 16 | document.dispatchEvent(new TouchEvent('touchend')); 17 | }; 18 | 19 | document.simulateTouchMove = (clientX, clientY) => { 20 | document.dispatchEvent(new TouchEvent('touchmove', { changedTouches: [{ clientX, clientY }] })); 21 | }; 22 | 23 | window.resizeTo = (width, height) => { 24 | window.innerWidth = width; 25 | window.innerHeight = height; 26 | window.dispatchEvent(new Event('resize')); 27 | }; 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const webpack = require('webpack'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'production', 7 | entry: [ 8 | './index.js' 9 | ], 10 | resolve: { 11 | extensions: ['.js', '.jsx'] 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | exclude: /node_modules/, 18 | use: 'babel-loader' 19 | } 20 | ] 21 | }, 22 | plugins: [ 23 | new CopyWebpackPlugin([ 24 | { 25 | from: 'src/stylesheets/*', 26 | flatten: true 27 | } 28 | ]) 29 | ], 30 | output: { 31 | path: resolve(__dirname, 'lib'), 32 | filename: 'index.js', 33 | library: 'react-splitter-layout', 34 | libraryTarget: 'umd' 35 | }, 36 | externals: { 37 | react: 'react', 38 | 'prop-types': 'prop-types' 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Pane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | function Pane(props) { 5 | const size = props.size || 0; 6 | const unit = props.percentage ? '%' : 'px'; 7 | let classes = 'layout-pane'; 8 | const style = {}; 9 | if (!props.primary) { 10 | if (props.vertical) { 11 | style.height = `${size}${unit}`; 12 | } else { 13 | style.width = `${size}${unit}`; 14 | } 15 | } else { 16 | classes += ' layout-pane-primary'; 17 | } 18 | return ( 19 |
{props.children}
20 | ); 21 | } 22 | 23 | Pane.propTypes = { 24 | vertical: PropTypes.bool, 25 | primary: PropTypes.bool, 26 | size: PropTypes.number, 27 | percentage: PropTypes.bool, 28 | children: PropTypes.oneOfType([ 29 | PropTypes.arrayOf(PropTypes.node), 30 | PropTypes.node 31 | ]) 32 | }; 33 | 34 | Pane.defaultProps = { 35 | vertical: false, 36 | primary: false, 37 | size: 0, 38 | percentage: false, 39 | children: [] 40 | }; 41 | 42 | export default Pane; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Yang Liu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | .splitter-layout { 2 | position: absolute; 3 | display: flex; 4 | flex-direction: row; 5 | width: 100%; 6 | height: 100%; 7 | overflow: hidden; 8 | } 9 | 10 | .splitter-layout .layout-pane { 11 | position: relative; 12 | flex: 0 0 auto; 13 | overflow: auto; 14 | } 15 | 16 | .splitter-layout .layout-pane.layout-pane-primary { 17 | flex: 1 1 auto; 18 | } 19 | 20 | .splitter-layout > .layout-splitter { 21 | flex: 0 0 auto; 22 | width: 4px; 23 | height: 100%; 24 | cursor: col-resize; 25 | background-color: #ccc; 26 | } 27 | 28 | .splitter-layout .layout-splitter:hover { 29 | background-color: #bbb; 30 | } 31 | 32 | .splitter-layout.layout-changing { 33 | cursor: col-resize; 34 | } 35 | 36 | .splitter-layout.layout-changing > .layout-splitter { 37 | background-color: #aaa; 38 | } 39 | 40 | .splitter-layout.splitter-layout-vertical { 41 | flex-direction: column; 42 | } 43 | 44 | .splitter-layout.splitter-layout-vertical.layout-changing { 45 | cursor: row-resize; 46 | } 47 | 48 | .splitter-layout.splitter-layout-vertical > .layout-splitter { 49 | width: 100%; 50 | height: 4px; 51 | cursor: row-resize; 52 | } 53 | -------------------------------------------------------------------------------- /example/javascripts/components/StandardVerticalLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SplitterLayout from '../../../index'; 3 | import Lorem from './Lorem'; 4 | 5 | export default function() { 6 | return ( 7 | 8 |
9 |

1st Pane

10 |

This is the 1st pane, and this is the primary pane by default.

11 |
12 |           <SplitterLayout primaryIndex={'{0}'}>{'\n'}
13 |             <div>1st</div>{'\n'}
14 |             <div>2nd</div>{'\n'}
15 |           </SplitterLayout>
16 |         
17 | 18 |
19 |
20 |

2nd Pane

21 |

This is the 2nd pane, and this is the secondary pane by default.

22 |
23 |           <SplitterLayout primaryIndex={'{0}'}>{'\n'}
24 |             <div>1st</div>{'\n'}
25 |             <div>2nd</div>{'\n'}
26 |           </SplitterLayout>
27 |         
28 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /example/javascripts/components/PercentageLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SplitterLayout from '../../../index'; 3 | import Lorem from './Lorem'; 4 | 5 | export default function() { 6 | return ( 7 | 8 |
9 |

1st Pane

10 |

This is the 1st pane, and this is the primary pane by default.

11 |

Try to resize the window and see how size changes.

12 |
13 |           <SplitterLayout primaryIndex={'{0}'} percentage>{'\n'}
14 |             <div>1st</div>{'\n'}
15 |             <div>2nd</div>{'\n'}
16 |           </SplitterLayout>
17 |         
18 | 19 |
20 |
21 |

2nd Pane

22 |

This is the 2nd pane, and this is the secondary pane by default.

23 |

Try to resize the window and see how size changes.

24 |
25 |           <SplitterLayout primaryIndex={'{0}'} percentage>{'\n'}
26 |             <div>1st</div>{'\n'}
27 |             <div>2nd</div>{'\n'}
28 |           </SplitterLayout>
29 |         
30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /example/javascripts/components/StandardHorizontalLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SplitterLayout from '../../../index'; 3 | import Lorem from './Lorem'; 4 | 5 | export default function() { 6 | return ( 7 | 8 |
9 |

1st Pane

10 |

This is the 1st pane, and this is the primary pane by default.

11 |

Try to resize the window and see how secondary pane's size keeps.

12 |
13 |           <SplitterLayout primaryIndex={'{0}'}>{'\n'}
14 |             <div>1st</div>{'\n'}
15 |             <div>2nd</div>{'\n'}
16 |           </SplitterLayout>
17 |         
18 | 19 |
20 |
21 |

2nd Pane

22 |

This is the 2nd pane, and this is the secondary pane by default.

23 |

Try to resize the window and see how secondary pane's size keeps.

24 |
25 |           <SplitterLayout primaryIndex={'{0}'}>{'\n'}
26 |             <div>1st</div>{'\n'}
27 |             <div>2nd</div>{'\n'}
28 |           </SplitterLayout>
29 |         
30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /example/javascripts/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import NavLink from './NavLink'; 4 | 5 | function constructLinks() { 6 | return ( 7 | 17 | ); 18 | } 19 | 20 | function App(props) { 21 | return ( 22 |
23 |
24 |

React Splitter Layout

25 |

A split layout for React and modern browsers.

26 |
27 |
28 | 31 |
32 | {props.children} 33 |
34 |
35 |
Licensed under MIT
36 |
37 | ); 38 | } 39 | 40 | App.propTypes = { 41 | children: PropTypes.element.isRequired 42 | }; 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /test/Pane.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ShallowRenderer from 'react-test-renderer/shallow'; 3 | import Pane from '../src/components/Pane'; 4 | 5 | function render(content, props = {}) { 6 | const renderer = new ShallowRenderer(); 7 | renderer.render({content}); 8 | return renderer.getRenderOutput(); 9 | } 10 | 11 | describe('Pane', () => { 12 | it('should render a Pane correctly', () => { 13 | const output = render('test pane'); 14 | expect(output.type).toBe('div'); 15 | expect(output.props.className).toBe('layout-pane'); 16 | expect(output.props.style).toEqual({ width: '0px' }); 17 | expect(output.props.children).toBe('test pane'); 18 | }); 19 | 20 | it('should render properties of a Pane correctly if requested', () => { 21 | const output = render('test pane', { vertical: true, size: 2, percentage: true }); 22 | expect(output.type).toBe('div'); 23 | expect(output.props.className).toBe('layout-pane'); 24 | expect(output.props.style).toEqual({ height: '2%' }); 25 | expect(output.props.children).toBe('test pane'); 26 | }); 27 | 28 | it('should render a primary Pane correctly if requested', () => { 29 | const output = render('test pane', { primary: true, vertical: true, size: 2, percentage: true }); 30 | expect(output.type).toBe('div'); 31 | expect(output.props.className).toBe('layout-pane layout-pane-primary'); 32 | expect(output.props.style).toEqual({}); 33 | expect(output.props.children).toBe('test pane'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /example/javascripts/components/LayoutWithMinimalSize.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SplitterLayout from '../../../index'; 3 | import Lorem from './Lorem'; 4 | 5 | export default function() { 6 | return ( 7 | 8 |
9 |

1st Pane

10 |

This is the 1st pane, and this is the primary pane by default.

11 |

12 | Layout will try to ensure this pane's width to be larger than 400px. 13 | When total width is not enough, this pane's (primary pane's) minimal width has priority. 14 |

15 |
16 |           <SplitterLayout primaryIndex={'{0}'} primaryMinSize={'{400}'} secondaryMinSize={'{200}'}>{'\n'}
17 |             <div>1st</div>{'\n'}
18 |             <div>2nd</div>{'\n'}
19 |           </SplitterLayout>
20 |         
21 | 22 |
23 |
24 |

2nd Pane

25 |

This is the 2nd pane, and this is the secondary pane by default.

26 |

27 | Layout will try to ensure this pane's width to be larger than 200px. 28 | When total width is not enough, the opposite pane's (primary pane's) minimal width has priority. 29 |

30 |
31 |           <SplitterLayout primaryIndex={'{0}'} primaryMinSize={'{400}'} secondaryMinSize={'{200}'}>{'\n'}
32 |             <div>1st</div>{'\n'}
33 |             <div>2nd</div>{'\n'}
34 |           </SplitterLayout>
35 |         
36 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /example/javascripts/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: [0] */ 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { HashRouter, Route, Switch } from 'react-router-dom'; 5 | import App from './components/App'; 6 | import StandardHorizontalLayout from './components/StandardHorizontalLayout'; 7 | import StandardVerticalLayout from './components/StandardVerticalLayout'; 8 | import LayoutWithMinimalSize from './components/LayoutWithMinimalSize'; 9 | import PercentageLayout from './components/PercentageLayout'; 10 | import NestedLayout from './components/NestedLayout'; 11 | import TogglableSidebarLayout from './components/TogglableSidebarLayout'; 12 | import HorizontalLayoutWithEvents from './components/HorizontalLayoutWithEvents'; 13 | import HorizontalLayoutWithIFrame from './components/HorizontalLayoutWithIFrame'; 14 | import '../../lib/index.css'; 15 | import '../stylesheets/index.css'; 16 | 17 | function NoMatch() { 18 | return ( 19 |
20 |

Not Found

21 |

Please one of links on the left.

22 |
23 | ); 24 | } 25 | 26 | render( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | , 43 | document.getElementById('root') 44 | ); 45 | -------------------------------------------------------------------------------- /example/javascripts/components/TogglableSidebarLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SplitterLayout from '../../../index'; 3 | import Lorem from './Lorem'; 4 | 5 | export default class TogglableSidebarLayout extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.toggleSidebar = this.toggleSidebar.bind(this); 9 | this.state = { 10 | sidebarVisible: true 11 | }; 12 | } 13 | 14 | toggleSidebar() { 15 | this.setState(state => ({ sidebarVisible: !state.sidebarVisible })); 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 |
22 |

1st Pane

23 |

This is the 1st pane, and this is the primary pane by default.

24 | 28 |
29 |             <SplitterLayout primaryIndex={'{0}'}>{'\n'}
30 |               <div>1st</div>{'\n'}
31 |             {this.state.sidebarVisible && '  
2nd
\n'} 32 | </SplitterLayout> 33 |
34 | 35 |
36 | {this.state.sidebarVisible && 37 | ( 38 |
39 |

2nd Pane

40 |

This is the 2nd pane, considered as a sidebar.

41 |
42 |                 <SplitterLayout primaryIndex={'{0}'}>{'\n'}
43 |                   <div>1st</div>{'\n'}
44 |                   <div>2nd</div>{'\n'}
45 |                 </SplitterLayout>
46 |               
47 | 48 |
49 | ) 50 | } 51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/javascripts/components/HorizontalLayoutWithIFrame.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SplitterLayout from '../../../index'; 3 | 4 | export default class HorizontalLayoutWithIFrame extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.onDragStart = this.onDragStart.bind(this); 8 | this.onDragEnd = this.onDragEnd.bind(this); 9 | this.state = { 10 | dragging: false 11 | }; 12 | } 13 | 14 | onDragStart() { 15 | this.setState({ dragging: true }); 16 | } 17 | 18 | onDragEnd() { 19 | this.setState({ dragging: false }); 20 | } 21 | 22 | renderDetailLinks() { 23 | return ( 24 |

25 | Refer to the following pages for details: 26 |

40 |

41 | ); 42 | } 43 | 44 | render() { 45 | return ( 46 | 47 |
48 |

1st Pane

49 |

50 | This is the 1st pane, and this is the primary pane by default. 51 | The 2nd pane on the right contains an iframe from https://example.com. 52 | A simple hack is used so that dragging is not interfered. 53 |

54 | {this.renderDetailLinks()} 55 |
56 |
57 | {this.state.dragging &&
} 58 |