├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── .gitignore ├── index.html ├── javascripts │ ├── components │ │ ├── App.jsx │ │ ├── HorizontalLayoutWithEvents.jsx │ │ ├── HorizontalLayoutWithIFrame.jsx │ │ ├── LayoutWithMinimalSize.jsx │ │ ├── Lorem.jsx │ │ ├── NavLink.jsx │ │ ├── NestedLayout.jsx │ │ ├── PercentageLayout.jsx │ │ ├── StandardHorizontalLayout.jsx │ │ ├── StandardVerticalLayout.jsx │ │ └── TogglableSidebarLayout.jsx │ └── index.jsx ├── stylesheets │ └── index.css └── webpack.config.js ├── index.js ├── package.json ├── src ├── components │ ├── Pane.jsx │ └── SplitterLayout.jsx └── stylesheets │ └── index.css ├── test ├── Pane.spec.jsx ├── SplitterLayout.spec.jsx └── setup.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-splitter-layout 2 | 3 | [![Travis](https://img.shields.io/travis/zesik/react-splitter-layout.svg)](https://travis-ci.org/zesik/react-splitter-layout) 4 | [![Coveralls](https://img.shields.io/coveralls/zesik/react-splitter-layout.svg)](https://coveralls.io/github/zesik/react-splitter-layout) 5 | [![npm](https://img.shields.io/npm/v/react-splitter-layout.svg)](https://www.npmjs.com/package/react-splitter-layout) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/zesik/react-splitter-layout/master/LICENSE) 7 | [![David devDependencies](https://img.shields.io/david/dev/zesik/react-splitter-layout.svg)](https://david-dm.org/zesik/react-splitter-layout?type=dev) 8 | [![David peerDependencies](https://img.shields.io/david/peer/zesik/react-splitter-layout.svg)](https://david-dm.org/zesik/react-splitter-layout?type=peer) 9 | 10 | A simple split layout for React and modern browsers. 11 | 12 | [Demo](https://zesik.com/react-splitter-layout/) 13 | 14 | ## Dependencies 15 | 16 | React-splitter-layout depends on React and prop-types. See [package.json](package.json#L50-L53) for more details. 17 | 18 | ## Installation 19 | 20 | ```sh 21 | $ npm install --save react-splitter-layout 22 | ``` 23 | 24 | ## Testing 25 | 26 | To start example server, execute `example` script with `npm`. 27 | 28 | ```sh 29 | $ npm run example 30 | ``` 31 | 32 | To run tests, execute `test` command with `npm`. 33 | 34 | ```sh 35 | $ npm test 36 | ``` 37 | 38 | To run coverage tests, execute `coverage` script with `npm`. 39 | 40 | ```sh 41 | $ npm run coverage 42 | ``` 43 | 44 | ## Integration 45 | 46 | 1. Add `react-splitter-layout` dependency to your code. 47 | 48 | ```sh 49 | $ npm install --save react-splitter-layout 50 | ``` 51 | 52 | 2. Include the library into your code and use it. 53 | 54 | ```javascript 55 | import React from 'react'; 56 | import SplitterLayout from 'react-splitter-layout'; 57 | import 'react-splitter-layout/lib/index.css'; 58 | 59 | class YourComponent extends React.Component { 60 | render() { 61 | return ( 62 | 63 |
Pane 1
64 |
Pane 2
65 |
66 | ); 67 | } 68 | } 69 | 70 | export default YourComponent; 71 | ``` 72 | 73 | *Note: From version 4.0.0, you need to import CSS files or handle it in your favorite way explicitly.* 74 | 75 | ## Usage 76 | 77 | Write two parts of the layout as direct children of your `SplitterLayout` element. 78 | `SplitterLayout` renders the first 2 direct children only if it has more than 2 direct children. 79 | `SplitterLayout` does not render splitter when it has only 1 direct children, 80 | and the only direct children occupies all available space. 81 | 82 | The `SplitterLayout` component supports the following props. 83 | 84 | * `customClassName: PropTypes.string` 85 | 86 | Custom CSS class name applied to the layout `div`. You can use this to customize layout style. 87 | Refers to the [original stylesheet](src/stylesheets/index.css) to see what you can customize. 88 | 89 | * `vertical: PropTypes.bool` 90 | 91 | Determine whether the layout should be a horizontal split or a vertical split. The default value is `false`. 92 | 93 | * `percentage: PropTypes.bool` 94 | 95 | Determine whether the width of each pane should be calculated in percentage or by pixels. 96 | The default value is `false`, which means width is calculated in pixels. 97 | 98 | * `primaryIndex: PropTypes.number` 99 | 100 | Index of the *primary pane*. Since `SplitterLayout` supports at most 2 children, only `0` or `1` is allowed. 101 | The default value is `0`. 102 | 103 | A *primary pane* is used to show users primary content, while a *secondary pane* is the other pane. 104 | When window size changes and `percentage` is set to `false`, 105 | primary pane's size is flexible and secondary pane's size is kept unchanged. 106 | However, when the window size is not enough for showing both minimal primary pane and minimal secondary pane, 107 | the primary pane's size is served first. 108 | 109 | * `primaryMinSize: PropTypes.number` 110 | 111 | Minimal size of primary pane. The default value is 0. 112 | 113 | When `percentage` is set to `false`, this value is pixel size (25 means 25px). 114 | When `percentage` is set to `true`, this value is percentage (25 means 25%). 115 | 116 | * `secondaryMinSize: PropTypes.number` 117 | 118 | Minimal size of secondary pane. 119 | 120 | * `secondaryInitialSize: PropTypes.number` 121 | 122 | Initial size of secondary pane when page loads. 123 | 124 | If this prop is not defined, `SplitterLayout` tries to split the layout with equal sizes. 125 | (Note: equal size may not apply when there are nested layouts.) 126 | 127 | * `onDragStart: PropTypes.func` 128 | 129 | Called when dragging is started. 130 | 131 | No parameter will be passed to event handlers. 132 | 133 | * `onDragEnd: PropTypes.func` 134 | 135 | Called when dragging finishes. 136 | 137 | No parameter will be passed to event handlers. 138 | 139 | * `onSecondaryPaneSizeChange: PropTypes.func` 140 | 141 | Called when the size of secondary pane is changed. 142 | 143 | Event handlers will be passed with a single parameter of `number` type representing new size of secondary pane. 144 | When `percentage` is set to `false`, the value is in pixel size. 145 | When `percentage` is set to `true`, the value is in percentage. 146 | 147 | ## Release History 148 | 149 | * 4.0.0 150 | * Stylesheets are no longer integrated by default. It has to be handled in your favorite way explicitly. 151 | * 3.0.0 152 | * Add dragging and size change events. 153 | * Drop support of React earlier than 15.5.0. 154 | * 100% code coverage test! 155 | * 0.2.1 156 | * Fix an incorrect layout when nesting a horizontal splitter inside a vertical one, 157 | and now root element of the splitter is absolutely positioned. 158 | * 0.1.0 159 | * First proper release. 160 | 161 | ## License 162 | 163 | [MIT](LICENSE) 164 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/javascripts/components/HorizontalLayoutWithEvents.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-void: [0] */ 2 | import React from 'react'; 3 | import SplitterLayout from '../../../index'; 4 | 5 | export default class HorizontalLayoutWithEvents extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.onDragStart = this.onDragStart.bind(this); 9 | this.onDragEnd = this.onDragEnd.bind(this); 10 | this.onSecondaryPaneSizeChange = this.onSecondaryPaneSizeChange.bind(this); 11 | this.state = { 12 | secondaryPaneSize: -1, 13 | dragging: false 14 | }; 15 | } 16 | 17 | componentDidUpdate(prevProps, prevState) { 18 | if (prevState.dragging !== this.state.dragging && this.draggingEl1 && this.draggingEl2) { 19 | this.draggingEl1.classList.add('highlight'); 20 | this.draggingEl2.classList.add('highlight'); 21 | void this.draggingEl1.offsetWidth; 22 | void this.draggingEl2.offsetWidth; 23 | this.draggingEl1.classList.remove('highlight'); 24 | this.draggingEl2.classList.remove('highlight'); 25 | } 26 | if (prevState.secondaryPaneSize !== this.state.secondaryPaneSize && this.sizeEl1 && this.sizeEl2) { 27 | this.sizeEl1.classList.add('highlight'); 28 | this.sizeEl2.classList.add('highlight'); 29 | void this.sizeEl1.offsetWidth; 30 | void this.sizeEl2.offsetWidth; 31 | this.sizeEl1.classList.remove('highlight'); 32 | this.sizeEl2.classList.remove('highlight'); 33 | } 34 | } 35 | 36 | onDragStart() { 37 | this.setState({ dragging: true }); 38 | } 39 | 40 | onDragEnd() { 41 | this.setState({ dragging: false }); 42 | } 43 | 44 | onSecondaryPaneSizeChange(secondaryPaneSize) { 45 | this.setState({ secondaryPaneSize }); 46 | } 47 | 48 | render() { 49 | return ( 50 | 55 |
56 |

1st Pane

57 |

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

58 |

Dragging: 59 | { this.draggingEl1 = e; }}> 60 | {this.state.dragging ? 'Yes' : 'No'} 61 | 62 |

63 |

Size of the 2nd pane: 64 | { this.sizeEl1 = e; }}> 65 | {this.state.secondaryPaneSize} 66 | 67 |

68 |
69 |
70 |

2nd Pane

71 |

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

72 |

Dragging: 73 | { this.draggingEl2 = e; }}> 74 | {this.state.dragging ? 'Yes' : 'No'} 75 | 76 |

77 |

Size of this pane: 78 | { this.sizeEl2 = e; }}> 79 | {this.state.secondaryPaneSize} 80 | 81 |

82 |
83 |
84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /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 |