// @flow import React from 'react'; import PropTypes from 'prop-types'; import {DraggableCore} from 'react-draggable'; import cloneElement from './cloneElement'; import type {Element as ReactElement, Node as ReactNode} from 'react'; type Axis = 'both' | 'x' | 'y' | 'none'; type State = { resizing: boolean, width: number, height: number, slackW: number, slackH: number }; type DragCallbackData = { node: HTMLElement, x: number, y: number, deltaX: number, deltaY: number, lastX: number, lastY: number }; export type ResizeCallbackData = { node: HTMLElement, size: {width: number, height: number} }; export type Props = { children: ReactElement, className?: ?string, width: number, height: number, handleSize: [number, number], lockAspectRatio: boolean, axis: Axis, minConstraints: [number, number], maxConstraints: [number, number], onResizeStop?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any, onResizeStart?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any, onResize?: ?(e: SyntheticEvent<>, data: ResizeCallbackData) => any, draggableOpts?: ?Object }; export default class Resizable extends React.Component { static propTypes = { // // Required Props // // Require that one and only one child be present. children: PropTypes.element.isRequired, // Initial w/h width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, // // Optional props // // If you change this, be sure to update your css handleSize: PropTypes.array, // If true, will only allow width/height to move in lockstep lockAspectRatio: PropTypes.bool, // Restricts resizing to a particular axis (default: 'both') // 'both' - allows resizing by width or height // 'x' - only allows the width to be changed // 'y' - only allows the height to be changed // 'none' - disables resizing altogether axis: PropTypes.oneOf(['both', 'x', 'y', 'none']), // Min/max size minConstraints: PropTypes.arrayOf(PropTypes.number), maxConstraints: PropTypes.arrayOf(PropTypes.number), // Callbacks onResizeStop: PropTypes.func, onResizeStart: PropTypes.func, onResize: PropTypes.func, // These will be passed wholesale to react-draggable's DraggableCore draggableOpts: PropTypes.object }; static defaultProps = { handleSize: [20, 20], lockAspectRatio: false, axis: 'both', minConstraints: [20, 20], maxConstraints: [Infinity, Infinity] }; state: State = { resizing: false, width: this.props.width, height: this.props.height, slackW: 0, slackH: 0 }; componentWillReceiveProps(nextProps: Object) { // If parent changes height/width, set that in our state. if (!this.state.resizing && (nextProps.width !== this.props.width || nextProps.height !== this.props.height)) { this.setState({ width: nextProps.width, height: nextProps.height }); } } lockAspectRatio(width: number, height: number, aspectRatio: number): [number, number] { height = width / aspectRatio; width = height * aspectRatio; return [width, height]; } // If you do this, be careful of constraints runConstraints(width: number, height: number): [number, number] { const [min, max] = [this.props.minConstraints, this.props.maxConstraints]; if (this.props.lockAspectRatio) { const ratio = this.state.width / this.state.height; height = width / ratio; width = height * ratio; } if (!min && !max) return [width, height]; const [oldW, oldH] = [width, height]; // Add slack to the values used to calculate bound position. This will ensure that if // we start removing slack, the element won't react to it right away until it's been // completely removed. let {slackW, slackH} = this.state; width += slackW; height += slackH; if (min) { width = Math.max(min[0], width); height = Math.max(min[1], height); } if (max) { width = Math.min(max[0], width); height = Math.min(max[1], height); } // If the numbers changed, we must have introduced some slack. Record it for the next iteration. slackW += (oldW - width); slackH += (oldH - height); if (slackW !== this.state.slackW || slackH !== this.state.slackH) { this.setState({slackW, slackH}); } return [width, height]; } /** * Wrapper around drag events to provide more useful data. * * @param {String} handlerName Handler name to wrap. * @return {Function} Handler function. */ resizeHandler(handlerName: string): 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'; // Update w/h let width = this.state.width + (canDragX ? deltaX : 0); let height = this.state.height + (canDragY ? deltaY : 0); // Early return if no change const widthChanged = width !== this.state.width, heightChanged = height !== this.state.height; if (handlerName === 'onResize' && !widthChanged && !heightChanged) return; [width, height] = this.runConstraints(width, height); // Set the appropriate state for this handler. const newState = {}; if (handlerName === 'onResizeStart') { newState.resizing = true; } else if (handlerName === 'onResizeStop') { newState.resizing = false; newState.slackW = newState.slackH = 0; } else { // Early return if no change after constraints if (width === this.state.width && height === this.state.height) return; newState.width = width; newState.height = height; } 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}})); } else { this.setState(newState); } }; } render(): ReactNode { // eslint-disable-next-line no-unused-vars const {children, draggableOpts, width, height, handleSize, lockAspectRatio, axis, minConstraints, maxConstraints, onResize, onResizeStop, onResizeStart, ...p} = this.props; const className = p.className ? `${p.className} react-resizable`: 'react-resizable'; // 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. return cloneElement(children, { ...p, className, children: [ children.props.children, ] }); } }