7 |
{props.title}
8 |
9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
10 | Etiam a orci ac diam pharetra viverra sit amet eget arcu.
11 | Ut sit amet efficitur elit. Suspendisse pulvinar pulvinar lectus ac malesuada.
12 | Curabitur ac dictum elit. Nam lectus ex, suscipit nec aliquet vel, molestie sed justo.
13 | Nullam commodo dui viverra sem hendrerit, eget iaculis nisi commodo.
14 | Aliquam at mauris porta libero mattis convallis et posuere leo.
15 | Vivamus vehicula libero lacus, ac molestie mi varius eu.
16 | Nam vel libero odio. Nulla sollicitudin dignissim pellentesque.
17 | Praesent eleifend tortor ac dapibus efficitur. Nunc eleifend augue vel laoreet rutrum.
18 |
19 |
{props.title}
20 |
21 | Mauris eros lorem, tempus et dapibus sed, auctor sit amet turpis.
22 | Aenean in ex quis nisl dignissim consectetur.
23 | Vestibulum tristique, risus sed luctus egestas, nibh purus fringilla orci, at molestie sem dui a urna.
24 | Mauris eu risus gravida, pellentesque est vitae, finibus urna.
25 | Nulla non posuere leo. Curabitur nec feugiat nunc. In elementum sem tellus, eget tempus sapien imperdiet ac.
26 | Proin ullamcorper nunc elementum ex fermentum, in faucibus lectus mollis.
27 | Suspendisse ullamcorper sollicitudin arcu, eu viverra ex tincidunt ut.
28 | Suspendisse tortor eros, interdum eu fermentum sed, ultrices id enim.
29 | Nam fermentum mattis ligula a lobortis. Nunc eros enim, cursus sit amet mi id, vehicula laoreet erat.
30 | Etiam vel iaculis felis. Donec nec ultricies lacus.
31 |
32 |
{props.title}
33 |
34 | Suspendisse elementum vitae leo nec vehicula. Nam fringilla consequat nisi convallis congue.
35 | Donec non metus nec massa ultricies consequat sed non lorem. Duis aliquet nec orci nec dapibus.
36 | Nam eget lobortis nisl, nec dapibus ligula.
37 | Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
38 | Ut dolor urna, sodales nec fringilla nec, consectetur vel nisl. Integer nec aliquet risus.
39 | Aenean nec scelerisque turpis.
40 |
41 |
{props.title}
42 |
43 | In hac habitasse platea dictumst.
44 | Proin viverra, turpis quis imperdiet maximus, tellus elit pellentesque libero, in aliquet dolor quam nec ex.
45 | Duis sed tempus ipsum. Maecenas sollicitudin neque id scelerisque porta.
46 | Nunc vestibulum lacus in justo viverra porttitor. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
47 | Praesent eu vulputate odio. In nec est sapien.
48 | Phasellus pellentesque, nisl in bibendum eleifend, mauris dolor feugiat sapien, quis auctor dui diam quis est.
49 | Integer porta massa ut ultricies euismod.
50 |
51 |
{props.title}
52 |
53 | Morbi id lacinia lorem. Donec at eros ultrices urna pulvinar lobortis vitae lacinia nisl.
54 | Quisque tempus, lorem sit amet luctus molestie, lacus metus ultricies ipsum,
55 | sit amet commodo dui lorem eget magna. Suspendisse eget placerat ante, in blandit ante.
56 | Suspendisse potenti. Etiam id metus orci. Mauris euismod blandit commodo.
57 | Aenean sed aliquam purus, ac pretium enim. Ut at elementum ligula.
58 | Donec venenatis est in turpis pharetra, ac laoreet diam finibus. Morbi volutpat sit amet magna a facilisis.
59 | Sed vitae dolor in justo bibendum euismod eget at justo. Praesent faucibus justo quis tristique porta.
60 | Pellentesque nulla diam, aliquet vel metus nec, commodo malesuada ante.
61 | Fusce vel augue eu risus ullamcorper accumsan. Morbi aliquet ipsum lorem, nec finibus risus luctus ac.
62 |
63 |
{props.title}
64 |
65 | Integer ut laoreet lectus, sit amet bibendum justo. Aenean convallis ac nisi id viverra.
66 | Ut eu velit eget dui efficitur facilisis. Sed tincidunt est quis libero congue, vel volutpat magna finibus.
67 | Vivamus sodales neque ornare, tempor erat a, congue est. Vestibulum velit orci, rutrum a libero ac,
68 | venenatis condimentum velit. Proin nec nunc vel diam accumsan fringilla. Fusce et diam sem.
69 | Quisque tincidunt risus eu venenatis ultrices. Mauris vel quam ut erat commodo porta.
70 | Quisque a neque at leo placerat porta a et dui.
71 |
72 |
{props.title}
73 |
74 | Vestibulum commodo ante non mi dignissim, et fermentum magna interdum. Suspendisse dui augue,
75 | porta eget auctor at, auctor at ligula. Aenean accumsan ex turpis, vitae rhoncus felis bibendum in.
76 | Suspendisse sed justo a massa ullamcorper consectetur sed vitae erat. Proin id magna tellus.
77 | Proin odio nulla, mattis in auctor id, sollicitudin sit amet odio. In dapibus nibh lectus,
78 | vitae fringilla nisi posuere ac. Class aptent taciti sociosqu ad litora torquent per conubia nostra,
79 | per inceptos himenaeos. Pellentesque mollis massa a fermentum mattis.
80 | Vestibulum cursus nunc nec tortor tempus, ut condimentum dolor eleifend. Quisque eu libero orci.
81 | Pellentesque quis risus eros.
82 |
83 |
{props.title}
84 |
85 | Donec eget convallis arcu. Quisque mauris nulla, consectetur at accumsan sed, commodo eu orci.
86 | Nulla facilisi. Duis vel hendrerit leo. Duis gravida orci nec arcu sollicitudin eleifend.
87 | Aenean ac erat a urna rutrum lacinia. Pellentesque luctus libero a dui bibendum, non vulputate purus tempor.
88 | Ut at tortor pharetra, pharetra lorem eu, ullamcorper tortor. Curabitur cursus porttitor ex,
89 | sed tempus sapien mattis sodales. Donec nisi quam, luctus sed sodales eget, scelerisque eget justo.
90 | In neque mi, commodo ac arcu a, condimentum finibus leo. Pellentesque aliquam hendrerit felis,
91 | a hendrerit tellus aliquet iaculis. Curabitur id ex at magna fringilla tristique eget eu ante.
92 | Cras mollis euismod dolor dignissim dictum.
93 |
94 |
{props.title}
95 |
96 | Curabitur at consequat nisl. Proin egestas enim vel venenatis sodales.
97 | In lorem dui, tristique non eros in, pharetra posuere nisi. Sed non risus bibendum, lacinia nulla posuere,
98 | ultrices arcu. Sed tincidunt molestie quam et lobortis. Ut sit amet dolor porta, rutrum quam nec,
99 | dapibus velit. Etiam consequat porta leo non dignissim. Praesent et pharetra nunc.
100 |
101 |
{props.title}
102 |
103 | Nunc imperdiet nunc sit amet justo viverra ultricies. Proin vestibulum iaculis neque nec sagittis.
104 | Nulla facilisis eget eros nec iaculis. Fusce sagittis vel mauris eu consequat. Aenean diam elit,
105 | lobortis in malesuada a, dictum nec purus. Cras in tristique metus, ut eleifend risus.
106 | Praesent vel diam sit amet lacus pharetra scelerisque. Pellentesque vitae elit mauris.
107 | Cras sed interdum lacus. Fusce porta dui vestibulum, hendrerit lectus sed, eleifend quam.
108 | Vestibulum nec placerat augue. Praesent lacinia porta sem, vitae imperdiet magna iaculis non.
109 | Phasellus venenatis eget odio a ultrices. Aliquam scelerisque dolor in tristique elementum.
110 | Aenean suscipit neque porttitor, ullamcorper justo ac, venenatis mauris.
111 |
112 |
113 | );
114 | }
115 |
116 | Lorem.propTypes = {
117 | title: PropTypes.string
118 | };
119 |
120 | Lorem.defaultProps = {
121 | title: ''
122 | };
123 |
124 | export default Lorem;
125 |
--------------------------------------------------------------------------------
/src/components/SplitterLayout.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Pane from './Pane';
4 |
5 | function clearSelection() {
6 | if (document.body.createTextRange) {
7 | // https://github.com/zesik/react-splitter-layout/issues/16
8 | // https://stackoverflow.com/questions/22914075/#37580789
9 | const range = document.body.createTextRange();
10 | range.collapse();
11 | range.select();
12 | } else if (window.getSelection) {
13 | if (window.getSelection().empty) {
14 | window.getSelection().empty();
15 | } else if (window.getSelection().removeAllRanges) {
16 | window.getSelection().removeAllRanges();
17 | }
18 | } else if (document.selection) {
19 | document.selection.empty();
20 | }
21 | }
22 |
23 | const DEFAULT_SPLITTER_SIZE = 4;
24 |
25 | class SplitterLayout extends React.Component {
26 | constructor(props) {
27 | super(props);
28 | this.handleResize = this.handleResize.bind(this);
29 | this.handleMouseMove = this.handleMouseMove.bind(this);
30 | this.handleMouseUp = this.handleMouseUp.bind(this);
31 | this.handleTouchMove = this.handleTouchMove.bind(this);
32 | this.handleSplitterMouseDown = this.handleSplitterMouseDown.bind(this);
33 | this.state = {
34 | secondaryPaneSize: 0,
35 | resizing: false
36 | };
37 | }
38 |
39 | componentDidMount() {
40 | window.addEventListener('resize', this.handleResize);
41 | document.addEventListener('mouseup', this.handleMouseUp);
42 | document.addEventListener('mousemove', this.handleMouseMove);
43 | document.addEventListener('touchend', this.handleMouseUp);
44 | document.addEventListener('touchmove', this.handleTouchMove);
45 |
46 | let secondaryPaneSize;
47 | if (typeof this.props.secondaryInitialSize !== 'undefined') {
48 | secondaryPaneSize = this.props.secondaryInitialSize;
49 | } else {
50 | const containerRect = this.container.getBoundingClientRect();
51 | let splitterRect;
52 | if (this.splitter) {
53 | splitterRect = this.splitter.getBoundingClientRect();
54 | } else {
55 | // Simulate a splitter
56 | splitterRect = { width: DEFAULT_SPLITTER_SIZE, height: DEFAULT_SPLITTER_SIZE };
57 | }
58 | secondaryPaneSize = this.getSecondaryPaneSize(containerRect, splitterRect, {
59 | left: containerRect.left + ((containerRect.width - splitterRect.width) / 2),
60 | top: containerRect.top + ((containerRect.height - splitterRect.height) / 2)
61 | }, false);
62 | }
63 | this.setState({ secondaryPaneSize });
64 | }
65 |
66 | componentDidUpdate(prevProps, prevState) {
67 | if (prevState.secondaryPaneSize !== this.state.secondaryPaneSize && this.props.onSecondaryPaneSizeChange) {
68 | this.props.onSecondaryPaneSizeChange(this.state.secondaryPaneSize);
69 | }
70 | if (prevState.resizing !== this.state.resizing) {
71 | if (this.state.resizing) {
72 | if (this.props.onDragStart) {
73 | this.props.onDragStart();
74 | }
75 | } else if (this.props.onDragEnd) {
76 | this.props.onDragEnd();
77 | }
78 | }
79 | }
80 |
81 | componentWillUnmount() {
82 | window.removeEventListener('resize', this.handleResize);
83 | document.removeEventListener('mouseup', this.handleMouseUp);
84 | document.removeEventListener('mousemove', this.handleMouseMove);
85 | document.removeEventListener('touchend', this.handleMouseUp);
86 | document.removeEventListener('touchmove', this.handleTouchMove);
87 | }
88 |
89 | getSecondaryPaneSize(containerRect, splitterRect, clientPosition, offsetMouse) {
90 | let totalSize;
91 | let splitterSize;
92 | let offset;
93 | if (this.props.vertical) {
94 | totalSize = containerRect.height;
95 | splitterSize = splitterRect.height;
96 | offset = clientPosition.top - containerRect.top;
97 | } else {
98 | totalSize = containerRect.width;
99 | splitterSize = splitterRect.width;
100 | offset = clientPosition.left - containerRect.left;
101 | }
102 | if (offsetMouse) {
103 | offset -= splitterSize / 2;
104 | }
105 | if (offset < 0) {
106 | offset = 0;
107 | } else if (offset > totalSize - splitterSize) {
108 | offset = totalSize - splitterSize;
109 | }
110 |
111 | let secondaryPaneSize;
112 | if (this.props.primaryIndex === 1) {
113 | secondaryPaneSize = offset;
114 | } else {
115 | secondaryPaneSize = totalSize - splitterSize - offset;
116 | }
117 | let primaryPaneSize = totalSize - splitterSize - secondaryPaneSize;
118 | if (this.props.percentage) {
119 | secondaryPaneSize = (secondaryPaneSize * 100) / totalSize;
120 | primaryPaneSize = (primaryPaneSize * 100) / totalSize;
121 | splitterSize = (splitterSize * 100) / totalSize;
122 | totalSize = 100;
123 | }
124 |
125 | if (primaryPaneSize < this.props.primaryMinSize) {
126 | secondaryPaneSize = Math.max(secondaryPaneSize - (this.props.primaryMinSize - primaryPaneSize), 0);
127 | } else if (secondaryPaneSize < this.props.secondaryMinSize) {
128 | secondaryPaneSize = Math.min(totalSize - splitterSize - this.props.primaryMinSize, this.props.secondaryMinSize);
129 | }
130 |
131 | return secondaryPaneSize;
132 | }
133 |
134 | handleResize() {
135 | if (this.splitter && !this.props.percentage) {
136 | const containerRect = this.container.getBoundingClientRect();
137 | const splitterRect = this.splitter.getBoundingClientRect();
138 | const secondaryPaneSize = this.getSecondaryPaneSize(containerRect, splitterRect, {
139 | left: splitterRect.left,
140 | top: splitterRect.top
141 | }, false);
142 | this.setState({ secondaryPaneSize });
143 | }
144 | }
145 |
146 | handleMouseMove(e) {
147 | if (this.state.resizing) {
148 | const containerRect = this.container.getBoundingClientRect();
149 | const splitterRect = this.splitter.getBoundingClientRect();
150 | const secondaryPaneSize = this.getSecondaryPaneSize(containerRect, splitterRect, {
151 | left: e.clientX,
152 | top: e.clientY
153 | }, true);
154 | clearSelection();
155 | this.setState({ secondaryPaneSize });
156 | }
157 | }
158 |
159 | handleTouchMove(e) {
160 | this.handleMouseMove(e.changedTouches[0]);
161 | }
162 |
163 | handleSplitterMouseDown() {
164 | clearSelection();
165 | this.setState({ resizing: true });
166 | }
167 |
168 | handleMouseUp() {
169 | this.setState(prevState => (prevState.resizing ? { resizing: false } : null));
170 | }
171 |
172 | render() {
173 | let containerClasses = 'splitter-layout';
174 | if (this.props.customClassName) {
175 | containerClasses += ` ${this.props.customClassName}`;
176 | }
177 | if (this.props.vertical) {
178 | containerClasses += ' splitter-layout-vertical';
179 | }
180 | if (this.state.resizing) {
181 | containerClasses += ' layout-changing';
182 | }
183 |
184 | const children = React.Children.toArray(this.props.children).slice(0, 2);
185 | if (children.length === 0) {
186 | children.push( { this.container = c; }}>
206 | {wrappedChildren[0]}
207 | {wrappedChildren.length > 1 &&
208 | (
209 |
{ this.splitter = c; }}
213 | onMouseDown={this.handleSplitterMouseDown}
214 | onTouchStart={this.handleSplitterMouseDown}
215 | />
216 | )
217 | }
218 | {wrappedChildren.length > 1 && wrappedChildren[1]}
219 |
220 | );
221 | }
222 | }
223 |
224 | SplitterLayout.propTypes = {
225 | customClassName: PropTypes.string,
226 | vertical: PropTypes.bool,
227 | percentage: PropTypes.bool,
228 | primaryIndex: PropTypes.number,
229 | primaryMinSize: PropTypes.number,
230 | secondaryInitialSize: PropTypes.number,
231 | secondaryMinSize: PropTypes.number,
232 | onDragStart: PropTypes.func,
233 | onDragEnd: PropTypes.func,
234 | onSecondaryPaneSizeChange: PropTypes.func,
235 | children: PropTypes.arrayOf(PropTypes.node)
236 | };
237 |
238 | SplitterLayout.defaultProps = {
239 | customClassName: '',
240 | vertical: false,
241 | percentage: false,
242 | primaryIndex: 0,
243 | primaryMinSize: 0,
244 | secondaryInitialSize: undefined,
245 | secondaryMinSize: 0,
246 | onDragStart: null,
247 | onDragEnd: null,
248 | onSecondaryPaneSizeChange: null,
249 | children: []
250 | };
251 |
252 | export default SplitterLayout;
253 |
--------------------------------------------------------------------------------
/test/SplitterLayout.spec.jsx:
--------------------------------------------------------------------------------
1 | /* eslint prefer-spread: [0], react/no-array-index-key: [0], react/no-find-dom-node: [0] */
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import ReactTestUtils from 'react-dom/test-utils';
5 | import ShallowRenderer from 'react-test-renderer/shallow';
6 | import SplitterLayout from '../src/components/SplitterLayout';
7 | import Pane from '../src/components/Pane';
8 |
9 | function render(length, props = {}) {
10 | const children = Array.apply(null, { length }).map((_, i) =>
Child #{i}
);
11 | const renderer = new ShallowRenderer();
12 | renderer.render(
{children});
13 | return renderer.getRenderOutput();
14 | }
15 |
16 | function renderIntoDocument(length, props) {
17 | const children = Array.apply(null, { length }).map((_, i) =>
Child #{i}
);
18 | const component = ReactTestUtils.renderIntoDocument(
{children});
19 | return component;
20 | }
21 |
22 | describe('SplitterLayout', () => {
23 | describe('rendering', () => {
24 | it('should render correctly when 2 children provided', () => {
25 | const output = render(2);
26 | expect(output.type).toBe('div');
27 | expect(output.props.className).toBe('splitter-layout');
28 | expect(output.props.children.length).toBe(3);
29 | expect(output.props.children[0].type).toBe(Pane);
30 | expect(output.props.children[0].props.vertical).toBe(false);
31 | expect(output.props.children[0].props.primary).toBe(true);
32 | expect(output.props.children[0].props.percentage).toBe(false);
33 | expect(output.props.children[1].type).toBe('div');
34 | expect(output.props.children[1].props.className).toBe('layout-splitter');
35 | expect(output.props.children[2].type).toBe(Pane);
36 | expect(output.props.children[2].props.vertical).toBe(false);
37 | expect(output.props.children[2].props.primary).toBe(false);
38 | expect(output.props.children[2].props.percentage).toBe(false);
39 | });
40 |
41 | it('should render properties correctly if requested', () => {
42 | const output = render(2, {
43 | customClassName: 'custom-class',
44 | vertical: true,
45 | percentage: true,
46 | primaryIndex: 1
47 | });
48 | expect(output.type).toBe('div');
49 | expect(output.props.className).toBe('splitter-layout custom-class splitter-layout-vertical');
50 | expect(output.props.children.length).toBe(3);
51 | expect(output.props.children[0].type).toBe(Pane);
52 | expect(output.props.children[0].props.vertical).toBe(true);
53 | expect(output.props.children[0].props.primary).toBe(false);
54 | expect(output.props.children[0].props.percentage).toBe(true);
55 | expect(output.props.children[1].type).toBe('div');
56 | expect(output.props.children[1].props.className).toBe('layout-splitter');
57 | expect(output.props.children[2].type).toBe(Pane);
58 | expect(output.props.children[2].props.vertical).toBe(true);
59 | expect(output.props.children[2].props.primary).toBe(true);
60 | expect(output.props.children[2].props.percentage).toBe(true);
61 | });
62 |
63 | it('should set the first children as primary if invalid primary index is provided', () => {
64 | const output = render(2, {
65 | primaryIndex: 5
66 | });
67 | expect(output.type).toBe('div');
68 | expect(output.props.className).toBe('splitter-layout');
69 | expect(output.props.children.length).toBe(3);
70 | expect(output.props.children[0].type).toBe(Pane);
71 | expect(output.props.children[0].props.vertical).toBe(false);
72 | expect(output.props.children[0].props.primary).toBe(true);
73 | expect(output.props.children[0].props.percentage).toBe(false);
74 | expect(output.props.children[1].type).toBe('div');
75 | expect(output.props.children[1].props.className).toBe('layout-splitter');
76 | expect(output.props.children[2].type).toBe(Pane);
77 | expect(output.props.children[2].props.vertical).toBe(false);
78 | expect(output.props.children[2].props.primary).toBe(false);
79 | expect(output.props.children[2].props.percentage).toBe(false);
80 | });
81 |
82 | it('should render one child when nothing provided', () => {
83 | const output = render(0);
84 | expect(output.type).toBe('div');
85 | expect(output.props.className).toBe('splitter-layout');
86 | expect(output.props.children.length).toBe(3);
87 | expect(output.props.children[0].type).toBe(Pane);
88 | expect(output.props.children[0].props.vertical).toBe(false);
89 | expect(output.props.children[0].props.primary).toBe(true);
90 | expect(output.props.children[0].props.percentage).toBe(false);
91 | expect(output.props.children[1]).toBe(false);
92 | expect(output.props.children[2]).toBe(false);
93 | });
94 |
95 | it('should render one child when only 1 child provided', () => {
96 | const output = render(1);
97 | expect(output.type).toBe('div');
98 | expect(output.props.className).toBe('splitter-layout');
99 | expect(output.props.children.length).toBe(3);
100 | expect(output.props.children[0].type).toBe(Pane);
101 | expect(output.props.children[0].props.vertical).toBe(false);
102 | expect(output.props.children[0].props.primary).toBe(true);
103 | expect(output.props.children[0].props.percentage).toBe(false);
104 | expect(output.props.children[1]).toBe(false);
105 | expect(output.props.children[2]).toBe(false);
106 | });
107 |
108 | it('should render 2 children when more than 2 children provided', () => {
109 | const output = render(5);
110 | expect(output.type).toBe('div');
111 | expect(output.props.className).toBe('splitter-layout');
112 | expect(output.props.children.length).toBe(3);
113 | expect(output.props.children[0].type).toBe(Pane);
114 | expect(output.props.children[0].props.vertical).toBe(false);
115 | expect(output.props.children[0].props.primary).toBe(true);
116 | expect(output.props.children[0].props.percentage).toBe(false);
117 | expect(output.props.children[1].type).toBe('div');
118 | expect(output.props.children[1].props.className).toBe('layout-splitter');
119 | expect(output.props.children[2].type).toBe(Pane);
120 | expect(output.props.children[2].props.vertical).toBe(false);
121 | expect(output.props.children[2].props.primary).toBe(false);
122 | expect(output.props.children[2].props.percentage).toBe(false);
123 | });
124 | });
125 |
126 | describe('sizing', () => {
127 | it('should get correct secondary pane size when horizontal, pixel sizing and first child as primary', () => {
128 | const component = {
129 | props: {
130 | vertical: false,
131 | percentage: false,
132 | primaryIndex: 0,
133 | primaryMinSize: 0,
134 | secondaryMinSize: 0
135 | }
136 | };
137 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
138 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
139 | const splitterRect = { top: 0, left: 40, width: 4, height: 512 };
140 | const position = { left: 50, top: 200 };
141 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(972);
142 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(970);
143 | });
144 |
145 | it('should get correct secondary pane size when vertical, pixel sizing and first child as primary', () => {
146 | const component = {
147 | props: {
148 | vertical: true,
149 | percentage: false,
150 | primaryIndex: 0,
151 | primaryMinSize: 0,
152 | secondaryMinSize: 0
153 | }
154 | };
155 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
156 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
157 | const splitterRect = { top: 40, left: 0, width: 1024, height: 4 };
158 | const position = { left: 50, top: 200 };
159 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(310);
160 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(308);
161 | });
162 |
163 | it('should get correct secondary pane size when horizontal, percentage sizing and first child as primary', () => {
164 | const component = {
165 | props: {
166 | vertical: false,
167 | percentage: true,
168 | primaryIndex: 0,
169 | primaryMinSize: 0,
170 | secondaryMinSize: 0
171 | }
172 | };
173 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
174 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
175 | const splitterRect = { top: 0, left: 40, width: 4, height: 512 };
176 | const position = { left: 512, top: 128 };
177 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(49.8046875);
178 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(49.609375);
179 | });
180 |
181 | it('should get correct secondary pane size when vertical, percentage sizing and first child as primary', () => {
182 | const component = {
183 | props: {
184 | vertical: true,
185 | percentage: true,
186 | primaryIndex: 0,
187 | primaryMinSize: 0,
188 | secondaryMinSize: 0
189 | }
190 | };
191 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
192 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
193 | const splitterRect = { top: 0, left: 40, width: 512, height: 4 };
194 | const position = { left: 512, top: 128 };
195 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(74.609375);
196 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(74.21875);
197 | });
198 |
199 | it('should get correct secondary pane size when horizontal, pixel sizing and second child as primary', () => {
200 | const component = {
201 | props: {
202 | vertical: false,
203 | percentage: false,
204 | primaryIndex: 1,
205 | primaryMinSize: 0,
206 | secondaryMinSize: 0
207 | }
208 | };
209 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
210 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
211 | const splitterRect = { top: 0, left: 40, width: 4, height: 512 };
212 | const position = { left: 50, top: 200 };
213 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(48);
214 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(50);
215 | });
216 |
217 | it('should get correct secondary pane size when vertical, pixel sizing and second child as primary', () => {
218 | const component = {
219 | props: {
220 | vertical: true,
221 | percentage: false,
222 | primaryIndex: 1,
223 | primaryMinSize: 0,
224 | secondaryMinSize: 0
225 | }
226 | };
227 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
228 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
229 | const splitterRect = { top: 40, left: 0, width: 1024, height: 4 };
230 | const position = { left: 50, top: 200 };
231 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(198);
232 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(200);
233 | });
234 |
235 | it('should get correct secondary pane size when horizontal, percentage sizing and second child as primary', () => {
236 | const component = {
237 | props: {
238 | vertical: false,
239 | percentage: true,
240 | primaryIndex: 1,
241 | primaryMinSize: 0,
242 | secondaryMinSize: 0
243 | }
244 | };
245 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
246 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
247 | const splitterRect = { top: 0, left: 40, width: 4, height: 512 };
248 | const position = { left: 512, top: 128 };
249 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(49.8046875);
250 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(50);
251 | });
252 |
253 | it('should get correct secondary pane size when vertical, percentage sizing and second child as primary', () => {
254 | const component = {
255 | props: {
256 | vertical: true,
257 | percentage: true,
258 | primaryIndex: 1,
259 | primaryMinSize: 0,
260 | secondaryMinSize: 0
261 | }
262 | };
263 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
264 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
265 | const splitterRect = { top: 0, left: 40, width: 512, height: 4 };
266 | const position = { left: 512, top: 128 };
267 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(24.609375);
268 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(25);
269 | });
270 |
271 | it('should adjust the pane size when exceeds limit', () => {
272 | const component = {
273 | props: {
274 | vertical: false,
275 | percentage: false,
276 | primaryIndex: 0,
277 | primaryMinSize: 0,
278 | secondaryMinSize: 0
279 | }
280 | };
281 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
282 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
283 | const splitterRect = { top: 0, left: 40, width: 4, height: 512 };
284 | expect(getSecondaryPaneSize(containerRect, splitterRect, { left: -10, top: 200 }, true)).toBe(1020);
285 | expect(getSecondaryPaneSize(containerRect, splitterRect, { left: -10, top: 200 }, false)).toBe(1020);
286 | expect(getSecondaryPaneSize(containerRect, splitterRect, { left: 1050, top: 200 }, true)).toBe(0);
287 | expect(getSecondaryPaneSize(containerRect, splitterRect, { left: 1050, top: 200 }, false)).toBe(0);
288 | });
289 |
290 | it('should respect user setting of secondary pane minimal size', () => {
291 | const component = {
292 | props: {
293 | vertical: false,
294 | percentage: false,
295 | primaryIndex: 0,
296 | primaryMinSize: 0,
297 | secondaryMinSize: 200
298 | }
299 | };
300 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
301 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
302 | const splitterRect = { top: 0, left: 40, width: 4, height: 512 };
303 | const position = { left: 1024, top: 200 };
304 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(200);
305 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(200);
306 | });
307 |
308 | it('should respect primary pane minimal size over secondary pane minimal size', () => {
309 | const component = {
310 | props: {
311 | vertical: false,
312 | percentage: false,
313 | primaryIndex: 0,
314 | primaryMinSize: 600,
315 | secondaryMinSize: 600
316 | }
317 | };
318 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
319 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
320 | const splitterRect = { top: 0, left: 40, width: 4, height: 512 };
321 | expect(getSecondaryPaneSize(containerRect, splitterRect, { left: 500, top: 200 }, true)).toBe(420);
322 | expect(getSecondaryPaneSize(containerRect, splitterRect, { left: 500, top: 200 }, false)).toBe(420);
323 | expect(getSecondaryPaneSize(containerRect, splitterRect, { left: 900, top: 200 }, true)).toBe(420);
324 | expect(getSecondaryPaneSize(containerRect, splitterRect, { left: 900, top: 200 }, false)).toBe(420);
325 | });
326 |
327 | it('should respect primary pane minimal size over secondary pane minimal size when width is not enough', () => {
328 | const component = {
329 | props: {
330 | vertical: false,
331 | percentage: false,
332 | primaryIndex: 0,
333 | primaryMinSize: 1200,
334 | secondaryMinSize: 200
335 | }
336 | };
337 | const getSecondaryPaneSize = SplitterLayout.prototype.getSecondaryPaneSize.bind(component);
338 | const containerRect = { top: 0, left: 0, width: 1024, height: 512 };
339 | const splitterRect = { top: 0, left: 40, width: 4, height: 512 };
340 | const position = { left: 200, top: 200 };
341 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, true)).toBe(0);
342 | expect(getSecondaryPaneSize(containerRect, splitterRect, position, false)).toBe(0);
343 | });
344 | });
345 |
346 | describe('DOM', () => {
347 | afterEach(() => {
348 | document.body.createTextRange = undefined;
349 | window.getSelection = undefined;
350 | document.selection = undefined;
351 | });
352 |
353 | it('should add DOM event listeners when mounted', () => {
354 | const windowSpy = jest.spyOn(window, 'addEventListener');
355 | const documentSpy = jest.spyOn(document, 'addEventListener');
356 | const component = renderIntoDocument(2);
357 | expect(windowSpy).toBeCalledWith('resize', component.handleResize);
358 | expect(documentSpy).toBeCalledWith('mouseup', component.handleMouseUp);
359 | expect(documentSpy).toBeCalledWith('mousemove', component.handleMouseMove);
360 | windowSpy.mockRestore();
361 | documentSpy.mockRestore();
362 | });
363 |
364 | it('should remove DOM event listeners when unmounted', () => {
365 | const component = renderIntoDocument(2);
366 | const windowSpy = jest.spyOn(window, 'removeEventListener');
367 | const documentSpy = jest.spyOn(document, 'removeEventListener');
368 | ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(component).parentNode);
369 | expect(windowSpy).toBeCalledWith('resize', component.handleResize);
370 | expect(documentSpy).toBeCalledWith('mouseup', component.handleMouseUp);
371 | expect(documentSpy).toBeCalledWith('mousemove', component.handleMouseMove);
372 | windowSpy.mockRestore();
373 | documentSpy.mockRestore();
374 | });
375 |
376 | it('should set splitter reference when it is rendered', () => {
377 | const component = renderIntoDocument(2);
378 | expect(component.container).toBeTruthy();
379 | expect(component.splitter).toBeTruthy();
380 | });
381 |
382 | it('should not set splitter reference when it is not rendered', () => {
383 | const component = renderIntoDocument(1);
384 | expect(component.container).toBeTruthy();
385 | expect(component.splitter).toBeFalsy();
386 | });
387 |
388 | it('should set resizing state when dragging splitter', () => {
389 | const component = renderIntoDocument(2);
390 | expect(component.state.resizing).toBe(false);
391 | ReactTestUtils.Simulate.mouseDown(component.splitter);
392 | expect(component.state.resizing).toBe(true);
393 | document.simulateMouseUp();
394 | expect(component.state.resizing).toBe(false);
395 | });
396 |
397 | it('should set pane size when dragging splitter', () => {
398 | const component = renderIntoDocument(2);
399 | const fn = component.container.getBoundingClientRect;
400 | component.container.getBoundingClientRect = () => ({ left: 0, top: 0, width: 200, height: 300 });
401 | ReactTestUtils.Simulate.mouseDown(component.splitter);
402 | document.simulateMouseMove(25, 30);
403 | expect(component.state.secondaryPaneSize).toBe(175);
404 | component.container.getBoundingClientRect = fn;
405 | });
406 |
407 | it('should keep secondary pane size when resizing', () => {
408 | const component = renderIntoDocument(2);
409 | const containerRectFn = component.container.getBoundingClientRect;
410 | const splitterRectFn = component.splitter.getBoundingClientRect;
411 | component.container.getBoundingClientRect = () => ({ left: 0, top: 0, width: 200, height: 300 });
412 | component.splitter.getBoundingClientRect = () => ({ left: 100, top: 0, width: 4, height: 300 });
413 | window.resizeTo(200, 300);
414 | expect(component.state.secondaryPaneSize).toBe(96);
415 | component.container.getBoundingClientRect = containerRectFn;
416 | component.splitter.getBoundingClientRect = splitterRectFn;
417 | });
418 |
419 | it('should choose createTextRange() if available to clear selection when dragging requested', () => {
420 | const component = renderIntoDocument(2);
421 | const collapseFn = jest.fn();
422 | const selectFn = jest.fn();
423 | const emptyFn = jest.fn();
424 | const removeAllRangesFn = jest.fn();
425 | const selectionEmptyFn = jest.fn();
426 |
427 | document.body.createTextRange = () => ({ collapse: collapseFn, select: selectFn });
428 | window.getSelection = () => ({ empty: emptyFn, removeAllRanges: removeAllRangesFn });
429 | document.selection = { empty: selectionEmptyFn };
430 |
431 | ReactTestUtils.Simulate.mouseDown(component.splitter);
432 | expect(collapseFn).toHaveBeenCalledTimes(1);
433 | expect(selectFn).toHaveBeenCalledTimes(1);
434 | expect(emptyFn).not.toHaveBeenCalled();
435 | expect(removeAllRangesFn).not.toHaveBeenCalled();
436 | expect(selectionEmptyFn).not.toHaveBeenCalled();
437 | });
438 |
439 | it('should choose getSelection().empty() if available to clear selection when dragging requested', () => {
440 | const component = renderIntoDocument(2);
441 | const emptyFn = jest.fn();
442 | const removeAllRangesFn = jest.fn();
443 | const selectionEmptyFn = jest.fn();
444 |
445 | window.getSelection = () => ({ empty: emptyFn, removeAllRanges: removeAllRangesFn });
446 | document.selection = { empty: selectionEmptyFn };
447 |
448 | ReactTestUtils.Simulate.mouseDown(component.splitter);
449 | expect(emptyFn).toHaveBeenCalledTimes(1);
450 | expect(removeAllRangesFn).not.toHaveBeenCalled();
451 | expect(selectionEmptyFn).not.toHaveBeenCalled();
452 | });
453 |
454 | it('should choose getSelection().removeAllRanges() if available to clear selection when dragging requested', () => {
455 | const component = renderIntoDocument(2);
456 | const removeAllRangesFn = jest.fn();
457 | const selectionEmptyFn = jest.fn();
458 |
459 | window.getSelection = () => ({ removeAllRanges: removeAllRangesFn });
460 | document.selection = { empty: selectionEmptyFn };
461 |
462 | ReactTestUtils.Simulate.mouseDown(component.splitter);
463 | expect(removeAllRangesFn).toHaveBeenCalledTimes(1);
464 | expect(selectionEmptyFn).not.toHaveBeenCalled();
465 | });
466 |
467 | it('should choose getSelection() if available to clear selection when dragging requested', () => {
468 | const component = renderIntoDocument(2);
469 | const selectionEmptyFn = jest.fn();
470 |
471 | window.getSelection = () => ({});
472 | document.selection = { empty: selectionEmptyFn };
473 |
474 | ReactTestUtils.Simulate.mouseDown(component.splitter);
475 | expect(selectionEmptyFn).not.toHaveBeenCalled();
476 | });
477 |
478 | it('should choose selection.empty() if available to clear selection when dragging requested', () => {
479 | const component = renderIntoDocument(2);
480 | const selectionEmptyFn = jest.fn();
481 |
482 | document.selection = { empty: selectionEmptyFn };
483 |
484 | ReactTestUtils.Simulate.mouseDown(component.splitter);
485 | expect(selectionEmptyFn).toHaveBeenCalledTimes(1);
486 | });
487 |
488 | it('should trigger drag events when dragging starts and finishes', () => {
489 | const startFn = jest.fn();
490 | const endFn = jest.fn();
491 | const component = renderIntoDocument(2, { onDragStart: startFn, onDragEnd: endFn });
492 | expect(startFn).not.toHaveBeenCalled();
493 | expect(endFn).not.toHaveBeenCalled();
494 | ReactTestUtils.Simulate.mouseDown(component.splitter);
495 | expect(startFn).toHaveBeenCalledTimes(1);
496 | expect(endFn).not.toHaveBeenCalled();
497 | document.simulateMouseUp();
498 | expect(startFn).toHaveBeenCalledTimes(1);
499 | expect(endFn).toHaveBeenCalledTimes(1);
500 | });
501 |
502 | it('should trigger size change events when secondary pane size has been changed', () => {
503 | const fn = jest.fn();
504 | const component = renderIntoDocument(2, { secondaryInitialSize: 20, onSecondaryPaneSizeChange: fn });
505 | expect(fn).toHaveBeenCalledTimes(1);
506 | expect(fn).toHaveBeenCalledWith(20);
507 | const rectFn = component.container.getBoundingClientRect;
508 | component.container.getBoundingClientRect = () => ({ left: 0, top: 0, width: 200, height: 300 });
509 | ReactTestUtils.Simulate.mouseDown(component.splitter);
510 | document.simulateMouseMove(25, 30);
511 | expect(fn).toHaveBeenCalledTimes(2);
512 | expect(fn).toHaveBeenCalledWith(175);
513 | component.container.getBoundingClientRect = rectFn;
514 | });
515 |
516 | it('should trigger drag events when touching starts and finishes', () => {
517 | const startFn = jest.fn();
518 | const endFn = jest.fn();
519 | const component = renderIntoDocument(2, { onDragStart: startFn, onDragEnd: endFn });
520 | expect(startFn).not.toHaveBeenCalled();
521 | expect(endFn).not.toHaveBeenCalled();
522 | ReactTestUtils.Simulate.touchStart(component.splitter);
523 | expect(startFn).toHaveBeenCalledTimes(1);
524 | expect(endFn).not.toHaveBeenCalled();
525 | document.simulateTouchEnd();
526 | expect(startFn).toHaveBeenCalledTimes(1);
527 | expect(endFn).toHaveBeenCalledTimes(1);
528 | });
529 |
530 | it('should trigger size change events when touching moves', () => {
531 | const fn = jest.fn();
532 | const component = renderIntoDocument(2, { secondaryInitialSize: 20, onSecondaryPaneSizeChange: fn });
533 | expect(fn).toHaveBeenCalledTimes(1);
534 | expect(fn).toHaveBeenCalledWith(20);
535 | const rectFn = component.container.getBoundingClientRect;
536 | component.container.getBoundingClientRect = () => ({ left: 0, top: 0, width: 200, height: 300 });
537 | ReactTestUtils.Simulate.touchStart(component.splitter);
538 | document.simulateTouchMove(25, 30);
539 | expect(fn).toHaveBeenCalledTimes(2);
540 | expect(fn).toHaveBeenCalledWith(175);
541 | component.container.getBoundingClientRect = rectFn;
542 | });
543 |
544 | it('should initialize horizontal secondary size if requested even when splitter is not rendered', () => {
545 | const component = renderIntoDocument(2, { secondaryInitialSize: 20 });
546 | expect(component.state.secondaryPaneSize).toBe(20);
547 | });
548 |
549 | it('should initialize vertical secondary size if requested even when splitter is not rendered', () => {
550 | const component = renderIntoDocument(2, { secondaryInitialSize: 20, vertical: true });
551 | expect(component.state.secondaryPaneSize).toBe(20);
552 | });
553 | });
554 | });
555 |
--------------------------------------------------------------------------------