Skip to content

Commit a18a8c6

Browse files
authored
Merge pull request #101 from RoccoC/resize-handles
Support for resize handles in multiple locations
2 parents 4ce15ee + 2a2e587 commit a18a8c6

File tree

6 files changed

+193
-32
lines changed

6 files changed

+193
-32
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[View the Demo](https://strml.github.io/react-resizable/examples/1.html)
44

5-
A simple widget that can be resized via a handle.
5+
A simple widget that can be resized via one or more handles.
66

77
You can either use the `<Resizable>` element directly, or use the much simpler `<ResizableBox>` element.
88

@@ -59,6 +59,7 @@ These props apply to both `<Resizable>` and `<ResizableBox>`.
5959
onResizeStop?: ?(e: SyntheticEvent, data: ResizeCallbackData) => any,
6060
onResizeStart?: ?(e: SyntheticEvent, data: ResizeCallbackData) => any,
6161
onResize?: ?(e: SyntheticEvent, data: ResizeCallbackData) => any,
62-
draggableOpts?: ?Object
62+
draggableOpts?: ?Object,
63+
resizeHandles?: ?Array<'s' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne'> = ['se']
6364
};
6465
```

css/styles.css

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,61 @@
55
position: absolute;
66
width: 20px;
77
height: 20px;
8-
bottom: 0;
9-
right: 0;
10-
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+');
11-
background-position: bottom right;
12-
padding: 0 3px 3px 0;
138
background-repeat: no-repeat;
149
background-origin: content-box;
1510
box-sizing: border-box;
11+
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+');
12+
background-position: bottom right;
13+
padding: 0 3px 3px 0;
14+
}
15+
.react-resizable-handle-sw {
16+
bottom: 0;
17+
left: 0;
18+
cursor: sw-resize;
19+
transform: rotate(90deg);
20+
}
21+
.react-resizable-handle-se {
22+
bottom: 0;
23+
right: 0;
1624
cursor: se-resize;
1725
}
26+
.react-resizable-handle-nw {
27+
top: 0;
28+
left: 0;
29+
cursor: nw-resize;
30+
transform: rotate(180deg);
31+
}
32+
.react-resizable-handle-ne {
33+
top: 0;
34+
right: 0;
35+
cursor: ne-resize;
36+
transform: rotate(270deg);
37+
}
38+
.react-resizable-handle-w,
39+
.react-resizable-handle-e {
40+
top: 50%;
41+
margin-top: -10px;
42+
cursor: ew-resize;
43+
}
44+
.react-resizable-handle-w {
45+
left: 0;
46+
transform: rotate(135deg);
47+
}
48+
.react-resizable-handle-e {
49+
right: 0;
50+
transform: rotate(315deg);
51+
}
52+
.react-resizable-handle-n,
53+
.react-resizable-handle-s {
54+
left: 50%;
55+
margin-left: -10px;
56+
cursor: ns-resize;
57+
}
58+
.react-resizable-handle-n {
59+
top: 0;
60+
transform: rotate(225deg);
61+
}
62+
.react-resizable-handle-s {
63+
bottom: 0;
64+
transform: rotate(45deg);
65+
}

lib/Resizable.js

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import cloneElement from './cloneElement';
66
import type {Element as ReactElement, Node as ReactNode} from 'react';
77

88
type Axis = 'both' | 'x' | 'y' | 'none';
9+
type ResizeHandle = 's' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne';
910
type State = {
1011
resizing: boolean,
1112
width: number, height: number,
@@ -19,14 +20,15 @@ type DragCallbackData = {
1920
};
2021
export type ResizeCallbackData = {
2122
node: HTMLElement,
22-
size: {width: number, height: number}
23+
size: {width: number, height: number},
24+
handle: ResizeHandle
2325
};
2426
export type Props = {
2527
children: ReactElement<any>,
2628
className?: ?string,
2729
width: number,
2830
height: number,
29-
handle: ReactElement<any>,
31+
handle: ReactElement<any> | (resizeHandle: ResizeHandle) => ReactElement<any>,
3032
handleSize: [number, number],
3133
lockAspectRatio: boolean,
3234
axis: Axis,
@@ -35,7 +37,8 @@ export type Props = {
3537
onResizeStop?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any,
3638
onResizeStart?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any,
3739
onResize?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any,
38-
draggableOpts?: ?Object
40+
draggableOpts?: ?Object,
41+
resizeHandles?: ?ResizeHandle[]
3942
};
4043

4144
export default class Resizable extends React.Component<Props, State> {
@@ -61,6 +64,18 @@ export default class Resizable extends React.Component<Props, State> {
6164
// If you change this, be sure to update your css
6265
handleSize: PropTypes.array,
6366

67+
// Defines which resize handles should be rendered (default: 'se')
68+
// Allows for any combination of:
69+
// 's' - South handle (bottom-center)
70+
// 'w' - West handle (left-center)
71+
// 'e' - East handle (right-center)
72+
// 'n' - North handle (top-center)
73+
// 'sw' - Southwest handle (bottom-left)
74+
// 'nw' - Northwest handle (top-left)
75+
// 'se' - Southeast handle (bottom-right)
76+
// 'ne' - Northeast handle (top-center)
77+
resizeHandles: PropTypes.arrayOf(PropTypes.oneOf(['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'])),
78+
6479
// If true, will only allow width/height to move in lockstep
6580
lockAspectRatio: PropTypes.bool,
6681

@@ -89,7 +104,8 @@ export default class Resizable extends React.Component<Props, State> {
89104
lockAspectRatio: false,
90105
axis: 'both',
91106
minConstraints: [20, 20],
92-
maxConstraints: [Infinity, Infinity]
107+
maxConstraints: [Infinity, Infinity],
108+
resizeHandles: ['se']
93109
};
94110

95111
state: State = {
@@ -161,12 +177,20 @@ export default class Resizable extends React.Component<Props, State> {
161177
* @param {String} handlerName Handler name to wrap.
162178
* @return {Function} Handler function.
163179
*/
164-
resizeHandler(handlerName: string): Function {
180+
resizeHandler(handlerName: string, axis: ResizeHandle): Function {
165181
return (e: SyntheticEvent<> | MouseEvent, {node, deltaX, deltaY}: DragCallbackData) => {
166182

167183
// Axis restrictions
168-
const canDragX = this.props.axis === 'both' || this.props.axis === 'x';
169-
const canDragY = this.props.axis === 'both' || this.props.axis === 'y';
184+
const canDragX = (this.props.axis === 'both' || this.props.axis === 'x') && ['n', 's'].indexOf(axis) === -1;
185+
const canDragY = (this.props.axis === 'both' || this.props.axis === 'y') && ['e', 'w'].indexOf(axis) === -1;
186+
187+
// reverse delta if using top or left drag handles
188+
if (canDragX && axis[axis.length - 1] === 'w') {
189+
deltaX = -deltaX;
190+
}
191+
if (canDragY && axis[0] === 'n') {
192+
deltaY = -deltaY;
193+
}
170194

171195
// Update w/h
172196
let width = this.state.width + (canDragX ? deltaX : 0);
@@ -195,18 +219,29 @@ export default class Resizable extends React.Component<Props, State> {
195219
const hasCb = typeof this.props[handlerName] === 'function';
196220
if (hasCb) {
197221
if (typeof e.persist === 'function') e.persist();
198-
this.setState(newState, () => this.props[handlerName](e, {node, size: {width, height}}));
222+
this.setState(newState, () => this.props[handlerName](e, {node, size: {width, height}, handle: axis}));
199223
} else {
200224
this.setState(newState);
201225
}
202226
};
203227
}
204228

229+
renderResizeHandle(resizeHandle: ResizeHandle): ReactNode {
230+
const {handle} = this.props;
231+
if (handle) {
232+
if (typeof handle === 'function') {
233+
return handle(resizeHandle);
234+
}
235+
return handle;
236+
}
237+
return <span className={`react-resizable-handle react-resizable-handle-${resizeHandle}`} />;
238+
}
239+
205240
render(): ReactNode {
206241
// eslint-disable-next-line no-unused-vars
207-
const {children, draggableOpts, width, height, handle, handleSize,
242+
const {children, draggableOpts, width, height, handleSize,
208243
lockAspectRatio, axis, minConstraints, maxConstraints, onResize,
209-
onResizeStop, onResizeStart, ...p} = this.props;
244+
onResizeStop, onResizeStart, resizeHandles, ...p} = this.props;
210245

211246
const className = p.className ?
212247
`${p.className} react-resizable`:
@@ -215,21 +250,23 @@ export default class Resizable extends React.Component<Props, State> {
215250
// What we're doing here is getting the child of this element, and cloning it with this element's props.
216251
// We are then defining its children as:
217252
// Its original children (resizable's child's children), and
218-
// A draggable handle.
253+
// One or more draggable handles.
219254
return cloneElement(children, {
220255
...p,
221256
className,
222257
children: [
223258
children.props.children,
224-
<DraggableCore
225-
{...draggableOpts}
226-
key="resizableHandle"
227-
onStop={this.resizeHandler('onResizeStop')}
228-
onStart={this.resizeHandler('onResizeStart')}
229-
onDrag={this.resizeHandler('onResize')}
230-
>
231-
{handle || <span className="react-resizable-handle" />}
232-
</DraggableCore>
259+
resizeHandles.map(h => (
260+
<DraggableCore
261+
{...draggableOpts}
262+
key={`resizableHandle-${h}`}
263+
onStop={this.resizeHandler('onResizeStop', h)}
264+
onStart={this.resizeHandler('onResizeStart', h)}
265+
onDrag={this.resizeHandler('onResize', h)}
266+
>
267+
{this.renderResizeHandle(h)}
268+
</DraggableCore>
269+
))
233270
]
234271
});
235272
}

lib/ResizableBox.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export default class ResizableBox extends React.Component<ResizableProps, State>
4848
// Basic wrapper around a Resizable instance.
4949
// If you use Resizable directly, you are responsible for updating the child component
5050
// with a new width and height.
51-
const {handle, handleSize, onResize, onResizeStart, onResizeStop, draggableOpts,
52-
minConstraints, maxConstraints, lockAspectRatio, axis, width, height, ...props} = this.props;
51+
const {handle, handleSize, onResize, onResizeStart, onResizeStop, draggableOpts, minConstraints,
52+
maxConstraints, lockAspectRatio, axis, width, height, resizeHandles, ...props} = this.props;
5353
return (
5454
<Resizable
5555
handle={handle}
@@ -64,6 +64,7 @@ export default class ResizableBox extends React.Component<ResizableProps, State>
6464
maxConstraints={maxConstraints}
6565
lockAspectRatio={lockAspectRatio}
6666
axis={axis}
67+
resizeHandles={resizeHandles}
6768
>
6869
<div style={{width: this.state.width + 'px', height: this.state.height + 'px'}} {...props} />
6970
</Resizable>

test/TestLayout.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import Resizable from '../lib/Resizable';
33
import ResizableBox from '../lib/ResizableBox';
44
import 'style-loader!css-loader!../css/styles.css';
5+
import 'style-loader!css-loader!./test.css';
56

67
export default class TestLayout extends React.Component<{}, {width: number, height: number}> {
78
state = {width: 200, height: 200};
@@ -10,7 +11,8 @@ export default class TestLayout extends React.Component<{}, {width: number, heig
1011
this.setState({width: 200, height: 200});
1112
};
1213

13-
onResize = (event, {element, size}) => {
14+
onResize = (event, {element, size, handle}) => {
15+
console.log(handle);
1416
this.setState({width: size.width, height: size.height});
1517
};
1618

@@ -19,14 +21,31 @@ export default class TestLayout extends React.Component<{}, {width: number, heig
1921
<div>
2022
<button onClick={this.onClick} style={{'marginBottom': '10px'}}>Reset first element's width/height</button>
2123
<div className="layoutRoot">
22-
<Resizable className="box" height={this.state.height} width={this.state.width} onResize={this.onResize}>
24+
<Resizable className="box" height={this.state.height} width={this.state.width} onResize={this.onResize} resizeHandles={['sw', 'se', 'nw', 'ne', 'w', 'e', 'n', 's']}>
2325
<div className="box" style={{width: this.state.width + 'px', height: this.state.height + 'px'}}>
24-
<span className="text">{"Raw use of <Resizable> element. 200x200, no constraints."}</span>
26+
<span className="text">{"Raw use of <Resizable> element. 200x200, all Resize Handles."}</span>
2527
</div>
2628
</Resizable>
2729
<ResizableBox className="box" width={200} height={200}>
2830
<span className="text">{"<ResizableBox>, same as above."}</span>
2931
</ResizableBox>
32+
<ResizableBox
33+
className="custom-box box"
34+
width={200}
35+
height={200}
36+
handle={<span className="custom-handle custom-handle-se" />}
37+
handleSize={[8, 8]}>
38+
<span className="text">{"<ResizableBox> with custom handle in SE corner."}</span>
39+
</ResizableBox>
40+
<ResizableBox
41+
className="custom-box box"
42+
width={200}
43+
height={200}
44+
handle={(h) => <span className={`custom-handle custom-handle-${h}`} />}
45+
handleSize={[8, 8]}
46+
resizeHandles={['sw', 'se', 'nw', 'ne', 'w', 'e', 'n', 's']}>
47+
<span className="text">{"<ResizableBox> with custom handles in all locations."}</span>
48+
</ResizableBox>
3049
<ResizableBox className="box" width={200} height={200} draggableOpts={{grid: [25, 25]}}>
3150
<span className="text">Resizable box that snaps to even intervals of 25px.</span>
3251
</ResizableBox>

test/test.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
.custom-box {
2+
overflow: visible;
3+
}
4+
.custom-handle {
5+
position: absolute;
6+
width: 8px;
7+
height: 8px;
8+
background-color: #1153aa;
9+
opacity: 0.75;
10+
border-radius: 4px;
11+
}
12+
.custom-handle-sw {
13+
bottom: -4px;
14+
left: -4px;
15+
cursor: sw-resize;
16+
}
17+
.custom-handle-se {
18+
bottom: -4px;
19+
right: -4px;
20+
cursor: se-resize;
21+
}
22+
.custom-handle-nw {
23+
top: -4px;
24+
left: -4px;
25+
cursor: nw-resize;
26+
}
27+
.custom-handle-ne {
28+
top: -4px;
29+
right: -4px;
30+
cursor: ne-resize;
31+
}
32+
.custom-handle-w,
33+
.custom-handle-e {
34+
top: 50%;
35+
margin-top: -4px;
36+
cursor: ew-resize;
37+
}
38+
.custom-handle-w {
39+
left: -4px;
40+
}
41+
.custom-handle-e {
42+
right: -4px;
43+
}
44+
.custom-handle-n,
45+
.custom-handle-s {
46+
left: 50%;
47+
margin-left: -4px;
48+
cursor: ns-resize;
49+
}
50+
.custom-handle-n {
51+
top: -4px;
52+
}
53+
.custom-handle-s {
54+
bottom: -4px;
55+
}

0 commit comments

Comments
 (0)