Resizable.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import React, { ReactNode, SyntheticEvent } from 'react'
  2. import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
  3. import { cloneElement } from './utils'
  4. import { ResizableProps, ResizeHandle } from './types'
  5. type ResizableState = {
  6. slackW: number
  7. slackH: number
  8. }
  9. export default class Resizable extends React.Component<
  10. ResizableProps,
  11. ResizableState
  12. > {
  13. public static defaultProps: Partial<ResizableProps> = {
  14. handleSize: [20, 20],
  15. lockAspectRatio: false,
  16. axis: 'both',
  17. minConstraints: [20, 20],
  18. maxConstraints: [Infinity, Infinity],
  19. resizeHandles: ['se']
  20. }
  21. public state: Readonly<ResizableState> = {
  22. slackW: 0,
  23. slackH: 0
  24. }
  25. public lockAspectRatio(
  26. width: number,
  27. height: number,
  28. aspectRatio: number
  29. ): [number, number] {
  30. height = width / aspectRatio
  31. width = height * aspectRatio
  32. return [width, height]
  33. }
  34. // If you do this, be careful of constraints
  35. private runConstraints(width: number, height: number): [number, number] {
  36. const [min, max] = [this.props.minConstraints, this.props.maxConstraints]
  37. if (!min && !max) return [width, height]
  38. // Fit width & height to aspect ratio
  39. if (this.props.lockAspectRatio) {
  40. if (height === this.props.height) {
  41. const ratio = this.props.width / this.props.height
  42. height = width / ratio
  43. width = height * ratio
  44. } else {
  45. // Take into account vertical resize with N/S handles on locked aspect
  46. // ratio. Calculate the change height-first, instead of width-first
  47. const ratio = this.props.height / this.props.width
  48. width = height / ratio
  49. height = width * ratio
  50. }
  51. }
  52. const [oldW, oldH] = [width, height]
  53. // Add slack to the values used to calculate bound position. This will ensure that if
  54. // we start removing slack, the element won't react to it right away until it's been
  55. // completely removed.
  56. let { slackW, slackH } = this.state
  57. width += slackW
  58. height += slackH
  59. if (min) {
  60. width = Math.max(min[0], width)
  61. height = Math.max(min[1], height)
  62. }
  63. if (max) {
  64. width = Math.min(max[0], width)
  65. height = Math.min(max[1], height)
  66. }
  67. // If the numbers changed, we must have introduced some slack. Record it for the next iteration.
  68. slackW += oldW - width
  69. slackH += oldH - height
  70. if (slackW !== this.state.slackW || slackH !== this.state.slackH) {
  71. this.setState({ slackW, slackH })
  72. }
  73. return [width, height]
  74. }
  75. /**
  76. * Wrapper around drag events to provide more useful data.
  77. *
  78. * @private
  79. * @param {string} handlerName Handler name to wrap.
  80. * @param {ResizeHandle} axis
  81. * @returns Handler function.
  82. * @memberof Resizable
  83. */
  84. private resizeHandler(handlerName: string, axis: ResizeHandle) {
  85. return (
  86. e: DraggableEvent,
  87. { node, deltaX, deltaY }: DraggableData
  88. ) => {
  89. deltaX = Math.round(deltaX)
  90. deltaY = Math.round(deltaY)
  91. // Axis restrictions
  92. const canDragX =
  93. (this.props.axis === 'both' || this.props.axis === 'x') &&
  94. ['n', 's'].indexOf(axis) === -1
  95. const canDragY =
  96. (this.props.axis === 'both' || this.props.axis === 'y') &&
  97. ['e', 'w'].indexOf(axis) === -1
  98. // reverse delta if using top or left drag handles
  99. if (canDragX && axis[axis.length - 1] === 'w') {
  100. deltaX = -deltaX
  101. }
  102. if (canDragY && axis[0] === 'n') {
  103. deltaY = -deltaY
  104. }
  105. // Update w/h
  106. let width = this.props.width + (canDragX ? deltaX : 0)
  107. let height = this.props.height + (canDragY ? deltaY : 0)
  108. // Early return if no change
  109. const widthChanged = width !== this.props.width
  110. const heightChanged = height !== this.props.height
  111. if (handlerName === 'onResize' && !widthChanged && !heightChanged) return
  112. ;[width, height] = this.runConstraints(width, height)
  113. // Set the appropriate state for this handler.
  114. const { slackW, slackH } = this.state
  115. const newState: ResizableState = { slackW, slackH }
  116. if (handlerName === 'onResizeStart') {
  117. // nothing
  118. } else if (handlerName === 'onResizeStop') {
  119. newState.slackW = newState.slackH = 0
  120. } else {
  121. // Early return if no change after constraints
  122. if (width === this.props.width && height === this.props.height) return
  123. }
  124. const hasCb = typeof this.props[handlerName] === 'function'
  125. if (hasCb) {
  126. if (typeof (e as SyntheticEvent).persist === 'function')
  127. (e as SyntheticEvent).persist()
  128. this.setState(newState, () =>
  129. this.props[handlerName](e, {
  130. node,
  131. size: { width, height },
  132. handle: axis
  133. })
  134. )
  135. } else {
  136. this.setState(newState)
  137. }
  138. }
  139. }
  140. renderResizeHandle(resizeHandle: ResizeHandle): ReactNode {
  141. const { handle } = this.props
  142. if (handle) {
  143. if (typeof handle === 'function') {
  144. return handle(resizeHandle)
  145. }
  146. return handle
  147. }
  148. return (
  149. <span
  150. className={`react-resizable-handle react-resizable-handle-${resizeHandle}`}
  151. />
  152. )
  153. }
  154. public render() {
  155. // eslint-disable-next-line no-unused-vars
  156. const {
  157. children,
  158. draggableOpts,
  159. width,
  160. height,
  161. handle,
  162. handleSize,
  163. lockAspectRatio,
  164. axis,
  165. minConstraints,
  166. maxConstraints,
  167. onResize,
  168. onResizeStop,
  169. onResizeStart,
  170. resizeHandles,
  171. scale,
  172. ...p
  173. } = this.props
  174. const className = p.className
  175. ? `${p.className} react-resizable`
  176. : 'react-resizable'
  177. // What we're doing here is getting the child of this element, and cloning it with this element's props.
  178. // We are then defining its children as:
  179. // Its original children (resizable's child's children), and
  180. // A draggable handle.
  181. return cloneElement(children, {
  182. ...p,
  183. className,
  184. children: [
  185. children.props.children,
  186. resizeHandles.map((h) => (
  187. <DraggableCore
  188. {...draggableOpts}
  189. scale={scale}
  190. key={`resizableHandle-${h}`}
  191. onStop={this.resizeHandler('onResizeStop', h)}
  192. onStart={this.resizeHandler('onResizeStart', h)}
  193. onDrag={this.resizeHandler('onResize', h)}
  194. >
  195. {this.renderResizeHandle(h)}
  196. </DraggableCore>
  197. ))
  198. ]
  199. })
  200. }
  201. }