react-resizable-area
Version:
Create a resizable component with percentage width/height support
357 lines (343 loc) • 10.3 kB
JavaScript
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export class ResizableArea extends Component {
static propTypes = {
id: PropTypes.string,
className: PropTypes.string,
children: PropTypes.node,
disable: PropTypes.shape({
width: PropTypes.bool,
height: PropTypes.bool,
}),
usePercentageResize: PropTypes.shape({
width: PropTypes.bool,
height: PropTypes.bool,
}),
parentContainer: PropTypes.node,
minimumWidth: PropTypes.shape({
px: PropTypes.number,
percent: PropTypes.number,
}),
minimumHeight: PropTypes.shape({
px: PropTypes.number,
percent: PropTypes.number,
}),
maximumWidth: PropTypes.shape({
px: PropTypes.number,
percent: PropTypes.number,
}),
maximumHeight: PropTypes.shape({
px: PropTypes.number,
percent: PropTypes.number,
}),
initWidth: PropTypes.shape({
px: PropTypes.number,
percent: PropTypes.number,
}),
initHeight: PropTypes.shape({
px: PropTypes.number,
percent: PropTypes.number,
}),
defaultWidth: PropTypes.shape({
px: PropTypes.number,
percent: PropTypes.number,
}),
defaultHeight: PropTypes.shape({
px: PropTypes.number,
percent: PropTypes.number,
}),
onResizing: PropTypes.func,
onResized: PropTypes.func,
}
static defaultProps = {
disable: {
width: false,
height: false,
},
usePercentageResize: {
width: true,
height: true,
},
parentContainer: document.body,
minimumWidth: {
px: 150,
percent: 0,
},
minimumHeight: {
px: 350,
percent: 0,
},
maximumWidth: {
px: 0,
percent: 100,
},
maximumHeight: {
px: 0,
percent: 100,
},
initWidth: {
px: 0,
percent: 30,
},
initHeight: {
px: 0,
percent: 100,
},
defaultWidth: {
px: 0,
percent: 30,
},
defaultHeight: {
px: 0,
percent: 100,
},
onResizing: e => null,
onResized: e => null,
}
state = {
width: this.props.initWidth,
height: this.props.initHeight,
}
setSize = ({ width, height }) => {
this.setState({
width,
height,
})
}
mapSizeOptionToCSS = ({ px, percent }) => `calc(${(px || 0)}px ${percent < 0 ? '-' : '+'} ${Math.abs(percent || 0)}%)`
calcCurrentSize = ({ width: curWidth, height: curHeight }) => {
const {
defaultWidth,
defaultHeight,
maximumWidth,
minimumWidth,
maximumHeight,
minimumHeight,
usePercentageResize,
} = this.props
const { parentWidth, parentHeight } = this
const maxWidth = usePercentageResize.width ? {
px: defaultWidth.px,
percent: (maximumWidth.px - defaultWidth.px) / parentWidth * 100 + maximumWidth.percent,
} : {
px: (maximumWidth.percent - defaultWidth.percent) / 100 * parentWidth + maximumWidth.px,
percent: defaultWidth.percent,
}
const minWidth = usePercentageResize.width ? {
px: defaultWidth.px,
percent: (minimumWidth.px - defaultWidth.px) / parentWidth * 100 + minimumWidth.percent,
} : {
px: (minimumWidth.percent - defaultWidth.percent) / 100 * parentWidth + minimumWidth.px,
percent: defaultWidth.percent,
}
const maxHeight = usePercentageResize.height ? {
px: defaultHeight.px,
percent: (maximumHeight.px - defaultHeight.px) / parentHeight * 100 + maximumHeight.percent,
} : {
px: (maximumHeight.percent - defaultHeight.percent) / 100 * parentHeight + maximumHeight.px,
percent: defaultHeight.percent,
}
const minHeight = usePercentageResize.height ? {
px: defaultHeight.px,
percent: (minimumHeight.px - defaultHeight.px) / parentHeight * 100 + minimumHeight.percent,
} : {
px: (minimumHeight.percent - defaultHeight.percent) / 100 * parentHeight + minimumHeight.px,
percent: defaultHeight.percent,
}
const maxWidthPx = parentWidth * maxWidth.percent / 100 + maxWidth.px
const minWidthPx = parentWidth * minWidth.percent / 100 + minWidth.px
const curWidthPx = parentWidth * curWidth.percent / 100 + curWidth.px
const maxHeightPx = parentHeight * maxHeight.percent / 100 + maxHeight.px
const minHeightPx = parentHeight * minHeight.percent / 100 + minHeight.px
const curHeightPx = parentHeight * curHeight.percent / 100 + curHeight.px
const ret = { width: curWidth, height: curHeight }
if (curWidthPx > maxWidthPx) {
ret.width = maxWidth
}
if (curWidthPx < minWidthPx) {
ret.width = minWidth
}
if (curHeightPx > maxHeightPx) {
ret.height = maxHeight
}
if (curHeightPx < minHeightPx) {
ret.height = minHeight
}
return ret
}
handleDoubleClickFactory = handler => e => {
e.preventDefault()
this.removeDragEventListener()
const { disable, defaultWidth, defaultHeight } = this.props
const masked = {
width: !disable.width && handler.width,
height: !disable.height && handler.height,
}
if (!masked.width && !masked.height) return
let state = {}
if (masked.width) {
state = {
...state,
width: {
...defaultWidth,
},
}
}
if (masked.height) {
state = {
...state,
height: {
...defaultHeight,
},
}
}
this.curState = state
requestAnimationFrame(this.tick)
this.props.onResized(state)
}
handleDragStartFactory = handler => e => {
e.preventDefault()
const { disable, parentContainer } = this.props
this.handler = {
width: !disable.width && handler.width,
height: !disable.height && handler.height,
}
if (!this.handler.width && !this.handler.height) return
this.startX = e.clientX
this.startY = e.clientY
const { width: parentWidth, height: parentHeight } = parentContainer.getBoundingClientRect()
this.parentWidth = parentWidth
this.parentHeight = parentHeight
const size = this.calcCurrentSize(this.state)
this.curState = size
this.orgState = size
this.dragUpdate = true
requestAnimationFrame(this.tick)
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
}
handleMouseMove = e => {
e.preventDefault()
const deltaX = e.clientX - this.startX
const deltaY = e.clientY - this.startY
let { width: curWidth, height: curHeight } = { ...this.state }
const { width: orgWidth, height: orgHeight } = this.orgState
const { usePercentageResize } = this.props
if (this.handler.width) {
if (usePercentageResize.width) {
curWidth = {
...curWidth,
percent: orgWidth.percent + deltaX / this.parentWidth * 100,
}
} else {
curWidth = {
...curWidth,
px: orgWidth.px + deltaX,
}
}
}
if (this.handler.height) {
if (usePercentageResize.height) {
curHeight = {
...curHeight,
percent: orgHeight.percent + deltaY / this.parentHeight * 100,
}
} else {
curHeight = {
...curHeight,
px: orgHeight.px + deltaY,
}
}
}
this.curState = { width: curWidth, height: curHeight }
this.props.onResizing(this.curState)
}
handleMouseUp = e => {
e.preventDefault()
const size = this.calcCurrentSize(this.curState)
this.curState = {
...this.state,
...size,
}
this.props.onResized(this.curState)
this.removeDragEventListener()
requestAnimationFrame(this.tick)
}
removeDragEventListener = () => {
document.removeEventListener('mousemove', this.handleMouseMove)
document.removeEventListener('mouseup', this.handleMouseUp)
this.dragUpdate = false
}
tick = ts => {
this.setState(this.curState)
if (this.dragUpdate) {
requestAnimationFrame(this.tick)
}
}
render () {
const { disable, className, id, children } = this.props
const containerStyle = {
width: this.mapSizeOptionToCSS(this.state.width),
height: this.mapSizeOptionToCSS(this.state.height),
maxWidth: this.mapSizeOptionToCSS(this.props.maximumWidth),
minWidth: this.mapSizeOptionToCSS(this.props.minimumWidth),
maxHeight: this.mapSizeOptionToCSS(this.props.maximumHeight),
minHeight: this.mapSizeOptionToCSS(this.props.minimumHeight),
position: 'relative',
}
const resizeHandleRightStyle = {
position: 'absolute',
right: -5,
top: 0,
height: '100%',
width: 10,
cursor: !disable.width ? 'ew-resize' : 'auto',
}
const resizeHandleBottomStyle = {
position: 'absolute',
left: 0,
bottom: -5,
height: 10,
width: '100%',
cursor: !disable.height ? 'ns-resize' : 'auto',
}
const resizeHandleCornerStyle = {
position: 'absolute',
right: -5,
bottom: -5,
height: 10,
width: 10,
cursor: !disable.width && !disable.height
? 'nwse-resize' : !disable.width
? 'ew-resize' : !disable.height
? 'ns-resize' : 'auto',
}
return (
<div
id={id}
className={`resizable-component${className != null ? ' ' + className : ''}`}
style={containerStyle}
>
{children}
<div
onMouseDown={this.handleDragStartFactory({ width: true, height: false })}
onDoubleClick={this.handleDoubleClickFactory({ width: true, height: false })}
className='resize-handle-right'
style={resizeHandleRightStyle}
/>
<div
onMouseDown={this.handleDragStartFactory({ width: false, height: true })}
onDoubleClick={this.handleDoubleClickFactory({ width: false, height: true })}
className='resize-handle-bottom'
style={resizeHandleBottomStyle}
/>
<div
onMouseDown={this.handleDragStartFactory({ width: true, height: true })}
onDoubleClick={this.handleDoubleClickFactory({ width: true, height: true })}
className='resize-handle-corner'
style={resizeHandleCornerStyle}
/>
</div>
)
}
}