index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import * as React from "react";
  2. import { int, innerHeight, innerWidth, outerHeight, outerWidth, parseBounds, Bound } from '../utils'
  3. const doc = document
  4. interface DraggerProps {
  5. className?: string;
  6. /**
  7. * 给予元素一个x,y的初始位置,单位是px
  8. */
  9. x?: number,
  10. y?: number,
  11. /**
  12. * 拖动范围限制
  13. * 如果不规定范围,那么子元素就可以随意拖动不受限制
  14. * 1.可以提供自定义的范围限制
  15. * 2.也可以提供父类为边框的范围限制(string === parent)
  16. */
  17. bounds?: Bound | 'parent',
  18. /**
  19. * 以网格的方式移动,每次移动并不是平滑的移动
  20. * [20,30],鼠标x轴方向移动了20 px ,y方向移动了30 px,整个子元素才会移动
  21. */
  22. grid?: [number, number],
  23. /**只允许移动x轴 */
  24. /**只允许移动y轴 */
  25. allowX?: Boolean,
  26. allowY?: Boolean,
  27. /**
  28. * 是否由用户移动
  29. * 可能是通过外部props改变
  30. */
  31. isUserMove?: Boolean,
  32. /**
  33. * 生命周期回调
  34. */
  35. onDragStart?: (x: number, y: number) => void,
  36. onMove?: (event: MouseEvent | TouchEvent, x: number, y: number) => void,
  37. onDragEnd?: (event: MouseEvent | TouchEvent, x: number, y: number) => void,
  38. onResizeStart?: (event: any, x: number, y: number) => void,
  39. onResizing?: (event: MouseEvent | TouchEvent, x: number, y: number) => void
  40. onResizeEnd?: (event: MouseEvent | TouchEvent, x: number, y: number) => void
  41. style?: React.CSSProperties,
  42. w?: number,
  43. h?: number,
  44. handle?: Boolean;
  45. canDrag?: Boolean;
  46. canResize?: Boolean;
  47. children: (provided: any, resizeMix: any, dragMix: any) => any;
  48. }
  49. export class Dragger extends React.Component<DraggerProps, {}> {
  50. parent: any;
  51. self: any;
  52. Ref: any;
  53. constructor(props: DraggerProps) {
  54. super(props)
  55. // this.move = this.move.bind(this)
  56. // this.onDragEnd = this.onDragEnd.bind(this)
  57. this.parent = null;
  58. this.self = null;
  59. }
  60. /**
  61. * 初始变量设置
  62. */
  63. static defaultProps = {
  64. allowX: true,
  65. allowY: true,
  66. isUserMove: true
  67. }
  68. state = {
  69. /** x轴位移,单位是px */
  70. x: this.props.x || 0,
  71. /** y轴位移,单位是px */
  72. y: this.props.y || 0,
  73. /**鼠标点击元素的原始位置,单位是px */
  74. originX: 0,
  75. originY: 0,
  76. isUserMove: true,
  77. /**已经移动的位移,单位是px */
  78. lastX: 0,
  79. lastY: 0,
  80. /**堆叠的层级 */
  81. zIndex: 1,
  82. w: this.props.w || 0,
  83. h: this.props.h || 0,
  84. lastW: 0,
  85. lastH: 0
  86. }
  87. move = (event: any) => {
  88. let { lastX, lastY } = this.state;
  89. /* event.client - this.state.origin 表示的是移动的距离,
  90. * elX表示的是原来已经有的位移
  91. */
  92. let deltaX, deltaY;
  93. if (event.type.indexOf('mouse') >= 0) {
  94. deltaX = (event as MouseEvent).clientX - this.state.originX + lastX
  95. deltaY = (event as MouseEvent).clientY - this.state.originY + lastY
  96. } else {
  97. deltaX = (event as TouchEvent).touches[0].clientX - this.state.originX + lastX
  98. deltaY = (event as TouchEvent).touches[0].clientY - this.state.originY + lastY
  99. }
  100. const { bounds } = this.props
  101. if (bounds) {
  102. /**
  103. * 如果用户指定一个边界,那么在这里处理
  104. */
  105. let NewBounds = typeof bounds !== 'string' ? parseBounds(bounds) : bounds;
  106. /**
  107. * 网格式移动范围设定,永远移动 n 的倍数
  108. * 注意:设定移动范围的时候,一定要在判断bounds之前,否则会造成bounds不对齐
  109. */
  110. const { grid } = this.props
  111. if (Array.isArray(grid) && grid.length === 2) {
  112. deltaX = Math.round(deltaX / grid[0]) * grid[0]
  113. deltaY = Math.round(deltaY / grid[1]) * grid[1]
  114. }
  115. if (this.props.bounds === 'parent') {
  116. NewBounds = {
  117. left: int(this.parent.style.paddingLeft) + int(this.self.style.marginLeft) - this.self.offsetLeft,
  118. top: int(this.parent.style.paddingTop) + int(this.self.style.marginTop) - this.self.offsetTop,
  119. right: innerWidth(this.parent) - outerWidth(this.self) - this.self.offsetLeft +
  120. int(this.parent.style.paddingRight) - int(this.self.style.marginRight),
  121. bottom: innerHeight(this.parent) - outerHeight(this.self) - this.self.offsetTop +
  122. int(this.parent.style.paddingBottom) - int(this.self.style.marginBottom)
  123. }
  124. }
  125. /**
  126. * 保证不超出右边界和底部
  127. * keep element right and bot can not cross the bounds
  128. */
  129. if (NewBounds !== 'parent') deltaX = Math.min(deltaX, NewBounds.right)
  130. if (NewBounds !== 'parent') deltaY = Math.min(deltaY, NewBounds.bottom)
  131. /**
  132. * 保证不超出左边和上边
  133. * keep element left and top can not cross the bounds
  134. */
  135. if (NewBounds !== 'parent') deltaX = Math.max(deltaX, NewBounds.left)
  136. if (NewBounds !== 'parent') deltaY = Math.max(deltaY, NewBounds.top)
  137. }
  138. /**如果设置了x,y限制 */
  139. deltaX = this.props.allowX ? deltaX : 0
  140. deltaY = this.props.allowY ? deltaY : 0
  141. /**
  142. * 调整手感
  143. * 无论是向上移动还是向下移动,全部都是根据重力中心
  144. * */
  145. const height = (this.Ref as HTMLDivElement).getClientRects()[0].height;
  146. const upNdown = this.state.y - deltaY;
  147. const fixY = deltaY + (upNdown >= 0 ? 0 : height / 2);
  148. /**移动时回调,用于外部控制 */
  149. if (this.props.onMove) this.props.onMove(event, deltaX, fixY)
  150. this.setState({
  151. x: deltaX,
  152. y: deltaY
  153. })
  154. }
  155. onDragStart = (event: any) => {
  156. /** 保证用户在移动元素的时候不会选择到元素内部的东西 */
  157. doc.body.style.userSelect = 'none'
  158. // if (event.target.id !== 'dragact-handle') return
  159. /**
  160. * 把监听事件的回掉函数,绑定在document上
  161. * 当设置边界的时候,用户鼠标会离开元素的范围
  162. * 绑定在document上可以使得其依旧能够监听
  163. * 如果绑定在元素上,则鼠标离开元素,就不会再被监听了
  164. */
  165. if (event.type.indexOf('mouse') >= 0) {
  166. doc.addEventListener('mousemove', this.move);
  167. doc.addEventListener('mouseup', this.onDragEnd);
  168. } else {
  169. doc.addEventListener('touchmove', this.move)
  170. doc.addEventListener('touchend', this.onDragEnd)
  171. }
  172. if (this.props.bounds === 'parent' &&
  173. /**为了让 这段代码不会重复执行 */
  174. (typeof this.parent === 'undefined' || this.parent === null)) {
  175. /**
  176. * 在这里我们将父节点缓存下来,保证当用户鼠标离开拖拽区域时,我们仍然能获取到父节点
  177. * what we do here is
  178. * making sure that we still can retrieve our parent when user's mouse left this node.
  179. */
  180. this.parent = (event as any).currentTarget.offsetParent //todo
  181. /**
  182. * 我们自己
  183. * ourself
  184. */
  185. this.self = event.currentTarget
  186. }
  187. this.props.onDragStart && this.props.onDragStart(this.state.x, this.state.y)
  188. let originX, originY;
  189. if (event.type.indexOf('mouse') >= 0) {
  190. originX = (event as MouseEvent).clientX
  191. originY = (event as MouseEvent).clientY
  192. } else {
  193. originX = (event as TouchEvent).touches[0].clientX
  194. originY = (event as TouchEvent).touches[0].clientY
  195. }
  196. this.setState({
  197. originX: originX,
  198. originY: originY,
  199. lastX: this.state.x,
  200. lastY: this.state.y,
  201. zIndex: 10
  202. })
  203. }
  204. onDragEnd = (event: any) => {
  205. /** 取消用户选择限制,用户可以重新选择 */
  206. doc.body.style.userSelect = ''
  207. this.parent = null
  208. this.self = null
  209. if (event.type.indexOf('mouse') >= 0) {
  210. doc.removeEventListener('mousemove', this.move)
  211. doc.removeEventListener('mouseup', this.onDragEnd)
  212. } else {
  213. doc.removeEventListener('touchmove', this.move)
  214. doc.removeEventListener('touchend', this.onDragEnd)
  215. }
  216. this.setState({
  217. zIndex: 1
  218. })
  219. this.props.onDragEnd && this.props.onDragEnd(event, this.state.x, this.state.y)
  220. }
  221. onResizeStart = (event: React.MouseEvent<HTMLSpanElement>) => {
  222. /** 保证用户在移动元素的时候不会选择到元素内部的东西 */
  223. doc.body.style.userSelect = 'none';
  224. doc.addEventListener('mouseup', this.onResizeEnd);
  225. doc.addEventListener('mousemove', this.onResizing);
  226. let originX, originY;
  227. originX = event.clientX
  228. originY = event.clientY
  229. this.props.onResizeStart && this.props.onResizeStart(event, this.state.w, this.state.h);
  230. this.setState({
  231. originX: originX,
  232. originY: originY,
  233. zIndex: 2,
  234. lastW: this.state.w,
  235. lastH: this.state.h
  236. })
  237. event.stopPropagation();
  238. }
  239. onResizing = (event: any) => {
  240. /* event.client - this.state.origin 表示的是移动的距离,
  241. * elX表示的是原来已经有的位移
  242. */
  243. let deltaX, deltaY;
  244. if (event.type.indexOf('mouse') >= 0) {
  245. deltaX = (event as MouseEvent).clientX - this.state.originX
  246. deltaY = (event as MouseEvent).clientY - this.state.originY
  247. } else {
  248. deltaX = (event as TouchEvent).touches[0].clientX - this.state.originX
  249. deltaY = (event as TouchEvent).touches[0].clientY - this.state.originY
  250. }
  251. /**移动时回调,用于外部控制 */
  252. this.props.onResizing && this.props.onResizing(event, this.state.w, this.state.h);
  253. this.setState({
  254. w: deltaX + this.state.lastW,
  255. h: deltaY + this.state.lastH
  256. })
  257. }
  258. onResizeEnd = (event: any) => {
  259. doc.body.style.userSelect = '';
  260. doc.removeEventListener('mousemove', this.onResizing)
  261. doc.removeEventListener('mouseup', this.onResizeEnd)
  262. this.props.onResizeEnd && this.props.onResizeEnd(event, this.state.w, this.state.h);
  263. }
  264. componentDidMount() {
  265. /**
  266. * 这个函数只会调用一次
  267. * 这个只是一个临时的解决方案,因为这样会使得元素进行两次刷新
  268. */
  269. // if (typeof this.props.x === 'number' &&
  270. // typeof this.props.y === 'number') {
  271. // this.setState({
  272. // x: this.props.x,
  273. // y: this.props.y
  274. // })
  275. // }
  276. }
  277. componentWillReceiveProps(nextProps: DraggerProps) {
  278. /**
  279. * 外部props 改变的时候更新元素的内部位置
  280. * 这个api设计其实很不好
  281. * 以后可能会修改掉
  282. */
  283. const { isUserMove } = nextProps
  284. if (!isUserMove) {
  285. if (typeof nextProps.x === 'number' &&
  286. typeof nextProps.y === 'number') {
  287. this.setState({
  288. x: nextProps.x,
  289. y: nextProps.y,
  290. lastX: nextProps.x,
  291. lastY: nextProps.y,
  292. w: nextProps.w,
  293. h: nextProps.h
  294. })
  295. }
  296. }
  297. }
  298. mixin = () => {
  299. var dragMix = {
  300. onMouseDown: this.onDragStart,
  301. onTouchStart: this.onDragStart,
  302. onTouchEnd: this.onDragEnd,
  303. onMouseUp: this.onDragEnd
  304. };
  305. var resizeMix = {
  306. onMouseDown: this.onResizeStart,
  307. onMouseUp: this.onResizeEnd
  308. }
  309. return {
  310. dragMix, resizeMix
  311. };
  312. }
  313. render() {
  314. var { x, y, w, h } = this.state
  315. var { style, className, canResize } = this.props
  316. if (!this.props.isUserMove) {
  317. /**当外部设置其props的x,y初始属性的时候,我们在这里设置元素的初始位移 */
  318. x = this.props.x ? this.props.x : 0;
  319. y = this.props.y ? this.props.y : 0;
  320. if (style) {
  321. w = style.width ? style.width : w;
  322. h = style.height ? style.height : h;
  323. }
  324. }
  325. if (style) {
  326. //使得初始化的时候,不会有从0-1缩放动画
  327. w = w === 0 ? style.width : w;
  328. h = h === 0 ? style.height : h;
  329. }
  330. const { dragMix, resizeMix } = this.mixin();
  331. /**主要是为了让用户定义自己的className去修改css */
  332. // const fixedClassName = typeof className === 'undefined' ? '' : className + ' '
  333. resizeMix;
  334. canResize;
  335. className;
  336. const provided = {
  337. style: {
  338. ...style,
  339. touchAction: 'none!important',
  340. transform: `translate(${x}px,${y}px)`,
  341. width: w,
  342. height: h,
  343. },
  344. ref: (node: any) => this.Ref = node
  345. }
  346. return this.props.children(provided, dragMix, resizeMix)
  347. }
  348. }
  349. // return (
  350. // <div className={`${fixedClassName}WrapDragger`}
  351. // ref={'dragger'}
  352. // >
  353. // {this.props.children(provided)}
  354. // {canResize !== false ?
  355. // <span
  356. // {...resizeMix}
  357. // style={{
  358. // position: 'absolute',
  359. // width: 10, height: 10, right: 2, bottom: 2, cursor: 'se-resize',
  360. // borderRight: '2px solid rgba(15,15,15,0.2)',
  361. // borderBottom: '2px solid rgba(15,15,15,0.2)'
  362. // }}
  363. // /> : null}
  364. // </div>
  365. // )