Skip to content

Support for resize handles in multiple locations #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

A simple widget that can be resized via a handle.
A simple widget that can be resized via one or more handles.

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

Expand Down Expand Up @@ -59,6 +59,7 @@ These props apply to both `<Resizable>` and `<ResizableBox>`.
onResizeStop?: ?(e: SyntheticEvent, data: ResizeCallbackData) => any,
onResizeStart?: ?(e: SyntheticEvent, data: ResizeCallbackData) => any,
onResize?: ?(e: SyntheticEvent, data: ResizeCallbackData) => any,
draggableOpts?: ?Object
draggableOpts?: ?Object,
resizeHandles?: ?Array<'s' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne'> = ['se']
};
```
58 changes: 53 additions & 5 deletions css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,61 @@
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+');
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+');
background-position: bottom right;
padding: 0 3px 3px 0;
}
.react-resizable-handle-sw {
bottom: 0;
left: 0;
cursor: sw-resize;
transform: rotate(90deg);
}
.react-resizable-handle-se {
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-resizable-handle-nw {
top: 0;
left: 0;
cursor: nw-resize;
transform: rotate(180deg);
}
.react-resizable-handle-ne {
top: 0;
right: 0;
cursor: ne-resize;
transform: rotate(270deg);
}
.react-resizable-handle-w,
.react-resizable-handle-e {
top: 50%;
margin-top: -10px;
cursor: ew-resize;
}
.react-resizable-handle-w {
left: 0;
transform: rotate(135deg);
}
.react-resizable-handle-e {
right: 0;
transform: rotate(315deg);
}
.react-resizable-handle-n,
.react-resizable-handle-s {
left: 50%;
margin-left: -10px;
cursor: ns-resize;
}
.react-resizable-handle-n {
top: 0;
transform: rotate(225deg);
}
.react-resizable-handle-s {
bottom: 0;
transform: rotate(45deg);
}
77 changes: 57 additions & 20 deletions lib/Resizable.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import cloneElement from './cloneElement';
import type {Element as ReactElement, Node as ReactNode} from 'react';

type Axis = 'both' | 'x' | 'y' | 'none';
type ResizeHandle = 's' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne';
type State = {
resizing: boolean,
width: number, height: number,
Expand All @@ -19,14 +20,15 @@ type DragCallbackData = {
};
export type ResizeCallbackData = {
node: HTMLElement,
size: {width: number, height: number}
size: {width: number, height: number},
handle: ResizeHandle
};
export type Props = {
children: ReactElement<any>,
className?: ?string,
width: number,
height: number,
handle: ReactElement<any>,
handle: ReactElement<any> | (resizeHandle: ResizeHandle) => ReactElement<any>,
handleSize: [number, number],
lockAspectRatio: boolean,
axis: Axis,
Expand All @@ -35,7 +37,8 @@ export type Props = {
onResizeStop?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any,
onResizeStart?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any,
onResize?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any,
draggableOpts?: ?Object
draggableOpts?: ?Object,
resizeHandles?: ?ResizeHandle[]
};

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

// Defines which resize handles should be rendered (default: 'se')
// Allows for any combination of:
// 's' - South handle (bottom-center)
// 'w' - West handle (left-center)
// 'e' - East handle (right-center)
// 'n' - North handle (top-center)
// 'sw' - Southwest handle (bottom-left)
// 'nw' - Northwest handle (top-left)
// 'se' - Southeast handle (bottom-right)
// 'ne' - Northeast handle (top-center)
resizeHandles: PropTypes.arrayOf(PropTypes.oneOf(['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'])),

// If true, will only allow width/height to move in lockstep
lockAspectRatio: PropTypes.bool,

Expand Down Expand Up @@ -89,7 +104,8 @@ export default class Resizable extends React.Component<Props, State> {
lockAspectRatio: false,
axis: 'both',
minConstraints: [20, 20],
maxConstraints: [Infinity, Infinity]
maxConstraints: [Infinity, Infinity],
resizeHandles: ['se']
};

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

// Axis restrictions
const canDragX = this.props.axis === 'both' || this.props.axis === 'x';
const canDragY = this.props.axis === 'both' || this.props.axis === 'y';
const canDragX = (this.props.axis === 'both' || this.props.axis === 'x') && ['n', 's'].indexOf(axis) === -1;
const canDragY = (this.props.axis === 'both' || this.props.axis === 'y') && ['e', 'w'].indexOf(axis) === -1;

// reverse delta if using top or left drag handles
if (canDragX && axis[axis.length - 1] === 'w') {
deltaX = -deltaX;
}
if (canDragY && axis[0] === 'n') {
deltaY = -deltaY;
}

// Update w/h
let width = this.state.width + (canDragX ? deltaX : 0);
Expand Down Expand Up @@ -195,18 +219,29 @@ export default class Resizable extends React.Component<Props, State> {
const hasCb = typeof this.props[handlerName] === 'function';
if (hasCb) {
if (typeof e.persist === 'function') e.persist();
this.setState(newState, () => this.props[handlerName](e, {node, size: {width, height}}));
this.setState(newState, () => this.props[handlerName](e, {node, size: {width, height}, handle: axis}));
} else {
this.setState(newState);
}
};
}

renderResizeHandle(resizeHandle: ResizeHandle): ReactNode {
const {handle} = this.props;
if (handle) {
if (typeof handle === 'function') {
return handle(resizeHandle);
}
return handle;
}
return <span className={`react-resizable-handle react-resizable-handle-${resizeHandle}`} />;
}

render(): ReactNode {
// eslint-disable-next-line no-unused-vars
const {children, draggableOpts, width, height, handle, handleSize,
const {children, draggableOpts, width, height, handleSize,
lockAspectRatio, axis, minConstraints, maxConstraints, onResize,
onResizeStop, onResizeStart, ...p} = this.props;
onResizeStop, onResizeStart, resizeHandles, ...p} = this.props;

const className = p.className ?
`${p.className} react-resizable`:
Expand All @@ -215,21 +250,23 @@ export default class Resizable extends React.Component<Props, State> {
// What we're doing here is getting the child of this element, and cloning it with this element's props.
// We are then defining its children as:
// Its original children (resizable's child's children), and
// A draggable handle.
// One or more draggable handles.
return cloneElement(children, {
...p,
className,
children: [
children.props.children,
<DraggableCore
{...draggableOpts}
key="resizableHandle"
onStop={this.resizeHandler('onResizeStop')}
onStart={this.resizeHandler('onResizeStart')}
onDrag={this.resizeHandler('onResize')}
>
{handle || <span className="react-resizable-handle" />}
</DraggableCore>
resizeHandles.map(h => (
<DraggableCore
{...draggableOpts}
key={`resizableHandle-${h}`}
onStop={this.resizeHandler('onResizeStop', h)}
onStart={this.resizeHandler('onResizeStart', h)}
onDrag={this.resizeHandler('onResize', h)}
>
{this.renderResizeHandle(h)}
</DraggableCore>
))
]
});
}
Expand Down
5 changes: 3 additions & 2 deletions lib/ResizableBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export default class ResizableBox extends React.Component<ResizableProps, State>
// Basic wrapper around a Resizable instance.
// If you use Resizable directly, you are responsible for updating the child component
// with a new width and height.
const {handle, handleSize, onResize, onResizeStart, onResizeStop, draggableOpts,
minConstraints, maxConstraints, lockAspectRatio, axis, width, height, ...props} = this.props;
const {handle, handleSize, onResize, onResizeStart, onResizeStop, draggableOpts, minConstraints,
maxConstraints, lockAspectRatio, axis, width, height, resizeHandles, ...props} = this.props;
return (
<Resizable
handle={handle}
Expand All @@ -64,6 +64,7 @@ export default class ResizableBox extends React.Component<ResizableProps, State>
maxConstraints={maxConstraints}
lockAspectRatio={lockAspectRatio}
axis={axis}
resizeHandles={resizeHandles}
>
<div style={{width: this.state.width + 'px', height: this.state.height + 'px'}} {...props} />
</Resizable>
Expand Down
25 changes: 22 additions & 3 deletions test/TestLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import Resizable from '../lib/Resizable';
import ResizableBox from '../lib/ResizableBox';
import 'style-loader!css-loader!../css/styles.css';
import 'style-loader!css-loader!./test.css';

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

onResize = (event, {element, size}) => {
onResize = (event, {element, size, handle}) => {
console.log(handle);
this.setState({width: size.width, height: size.height});
};

Expand All @@ -19,14 +21,31 @@ export default class TestLayout extends React.Component<{}, {width: number, heig
<div>
<button onClick={this.onClick} style={{'marginBottom': '10px'}}>Reset first element's width/height</button>
<div className="layoutRoot">
<Resizable className="box" height={this.state.height} width={this.state.width} onResize={this.onResize}>
<Resizable className="box" height={this.state.height} width={this.state.width} onResize={this.onResize} resizeHandles={['sw', 'se', 'nw', 'ne', 'w', 'e', 'n', 's']}>
<div className="box" style={{width: this.state.width + 'px', height: this.state.height + 'px'}}>
<span className="text">{"Raw use of <Resizable> element. 200x200, no constraints."}</span>
<span className="text">{"Raw use of <Resizable> element. 200x200, all Resize Handles."}</span>
</div>
</Resizable>
<ResizableBox className="box" width={200} height={200}>
<span className="text">{"<ResizableBox>, same as above."}</span>
</ResizableBox>
<ResizableBox
className="custom-box box"
width={200}
height={200}
handle={<span className="custom-handle custom-handle-se" />}
handleSize={[8, 8]}>
<span className="text">{"<ResizableBox> with custom handle in SE corner."}</span>
</ResizableBox>
<ResizableBox
className="custom-box box"
width={200}
height={200}
handle={(h) => <span className={`custom-handle custom-handle-${h}`} />}
handleSize={[8, 8]}
resizeHandles={['sw', 'se', 'nw', 'ne', 'w', 'e', 'n', 's']}>
<span className="text">{"<ResizableBox> with custom handles in all locations."}</span>
</ResizableBox>
<ResizableBox className="box" width={200} height={200} draggableOpts={{grid: [25, 25]}}>
<span className="text">Resizable box that snaps to even intervals of 25px.</span>
</ResizableBox>
Expand Down
55 changes: 55 additions & 0 deletions test/test.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.custom-box {
overflow: visible;
}
.custom-handle {
position: absolute;
width: 8px;
height: 8px;
background-color: #1153aa;
opacity: 0.75;
border-radius: 4px;
}
.custom-handle-sw {
bottom: -4px;
left: -4px;
cursor: sw-resize;
}
.custom-handle-se {
bottom: -4px;
right: -4px;
cursor: se-resize;
}
.custom-handle-nw {
top: -4px;
left: -4px;
cursor: nw-resize;
}
.custom-handle-ne {
top: -4px;
right: -4px;
cursor: ne-resize;
}
.custom-handle-w,
.custom-handle-e {
top: 50%;
margin-top: -4px;
cursor: ew-resize;
}
.custom-handle-w {
left: -4px;
}
.custom-handle-e {
right: -4px;
}
.custom-handle-n,
.custom-handle-s {
left: 50%;
margin-left: -4px;
cursor: ns-resize;
}
.custom-handle-n {
top: -4px;
}
.custom-handle-s {
bottom: -4px;
}