File size: 4,850 Bytes
1b72d7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { useRef, useEffect, useState } from 'react'
/**
 * 可拖拽组件
 */

export const Draggable = (props) => {
  const { children } = props
  const draggableRef = useRef(null)
  const rafRef = useRef(null)
  const [moving, setMoving] = useState(false)
  let currentObj, offsetX, offsetY

  useEffect(() => {
    const draggableElements = document.getElementsByClassName('draggable')

    // 标准化鼠标事件对象
    function e(event) { // 定义事件对象标准化函数
      if (!event) { // 兼容IE浏览器
        event = window.event
        event.target = event.srcElement
        event.layerX = event.offsetX
        event.layerY = event.offsetY
      }
      // 移动端
      if (event.type === 'touchstart' || event.type === 'touchmove') {
        event.clientX = event.touches[0].clientX
        event.clientY = event.touches[0].clientY
      }

      event.mx = event.pageX || event.clientX + document.body.scrollLeft
      // 计算鼠标指针的x轴距离
      event.my = event.pageY || event.clientY + document.body.scrollTop
      // 计算鼠标指针的y轴距离

      return event // 返回标准化的事件对象
    }

    // 定义鼠标事件处理函数
    // document.pointerdown = start
    document.onmousedown = start
    document.ontouchstart = start

    function start (event) { // 按下鼠标时,初始化处理
      if (!draggableElements) return
      event = e(event)// 获取标准事件对象

      for (const drag of draggableElements) {
        // 判断鼠标点击的区域是否是拖拽框内
        if (inDragBox(event, drag)) {
          currentObj = drag.firstElementChild
        }
      }
      if (currentObj) {
        if (event.type === 'touchstart') {
          event.preventDefault() // 阻止默认的滚动行为
          document.documentElement.style.overflow = 'hidden' // 防止页面一起滚动
        }

        setMoving(true)
        offsetX = event.mx - currentObj.offsetLeft
        offsetY = event.my - currentObj.offsetTop

        document.onmousemove = move// 注册鼠标移动事件处理函数
        document.ontouchmove = move
        document.onmouseup = stop// 注册松开鼠标事件处理函数
        document.ontouchend = stop
      }
    }

    function move(event) { // 鼠标移动处理函数
      event = e(event)
      rafRef.current = requestAnimationFrame(() => updatePosition(event))
    }

    const stop = (event) => {
      event = e(event)
      document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
      cancelAnimationFrame(rafRef.current)
      setMoving(false)
      currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
    }

    const updatePosition = (event) => {
      if (currentObj) {
        const left = event.mx - offsetX
        const top = event.my - offsetY
        currentObj.style.left = left + 'px'
        currentObj.style.top = top + 'px'
        checkInWindow()
      }
    }

    /**
     * 鼠标是否在可拖拽区域内
     * @param {*} event
     * @returns
     */
    function inDragBox(event, drag) {
      const { clientX, clientY } = event // 鼠标位置
      const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild // 窗口位置
      const horizontal = clientX > offsetLeft && clientX < offsetLeft + offsetWidth
      const vertical = clientY > offsetTop && clientY < offsetTop + offsetHeight

      if (horizontal && vertical) {
        return true
      }

      return false
    }

    /**
     * 若超出窗口则吸附。
     */
    function checkInWindow() {
      // 检查是否悬浮在窗口内
      for (const drag of draggableElements) {
        // 判断鼠标点击的区域是否是拖拽框内
        const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild
        const { clientHeight, clientWidth } = document.documentElement
        if (offsetTop < 0) {
          drag.firstElementChild.style.top = 0
        }
        if (offsetTop > (clientHeight - offsetHeight)) {
          drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
        }
        if (offsetLeft < 0) {
          drag.firstElementChild.style.left = 0
        }
        if (offsetLeft > (clientWidth - offsetWidth)) {
          drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
        }
      }
    }

    window.addEventListener('resize', checkInWindow)

    return () => {
      return () => {
        window.removeEventListener('resize', checkInWindow)
        cancelAnimationFrame(rafRef.current)
      }
    }
  }, [])

  return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
     {children}
  </div>
}

Draggable.defaultProps = { left: 0, top: 0 }