class/Slide.js

  1. /**
  2. * 封装鼠标触摸事件。
  3. * @example
  4. * const el = document.querySelector('#container')
  5. * new Slide(el, 200, 200)
  6. * el.addEventListener('slidemove', (e) => {
  7. * console.log(e.detail)
  8. * // {
  9. * // // 滑动开始的点
  10. * // startx: ,
  11. * // starty: ,
  12. * // // 滑动过程的点
  13. * // endx: ,
  14. * // endy: ,
  15. * // // 滑动过程中与上一个点的距离
  16. * // dx: ,
  17. * // dy: ,
  18. * // // 滑动过程中与开始点的距离
  19. * // offsetx: ,
  20. * // offsety:
  21. * // }
  22. * })
  23. */
  24. export class Slide {
  25. // dom节点
  26. el = null
  27. // 手指移动过程中上一个坐标
  28. prePoint = {
  29. x: 0,
  30. y: 0
  31. }
  32. // 自定义用户数据
  33. customData = {
  34. // 滑动开始的点
  35. startx: 0,
  36. starty: 0,
  37. // 滑动过程的点
  38. endx: 0,
  39. endy: 0,
  40. // 滑动过程中与上一个点的距离
  41. dx: 0,
  42. dy: 0,
  43. // 滑动过程中与开始点的距离
  44. offsetx: 0,
  45. offsety: 0
  46. }
  47. /**
  48. * @param {HTMLElement} el - html 节点。
  49. * @param {Number} maxSlideDx - x 方向最大移动距离。
  50. * @param {Number} maxSlideDy - y 方向最大移动距离。
  51. * @param {Boolean} [limitArea=false] - 是否限制区域。
  52. * 如果为true,当滑动超过 maxSlideDx 限定的区域内,
  53. * 获取到的 dx 为 0,offsetx 为 ±maxSlideDx。
  54. * 如果为false,当滑动超过 maxSlideDx 限定的区域内,
  55. * 获取到的 dx 为 正常值,offsetx 为 ±maxSlideDx。
  56. */
  57. constructor (
  58. el,
  59. options = {
  60. limitArea: false
  61. }
  62. ) {
  63. this.el = el
  64. // 单次的最大横向滑动距离
  65. this.maxSlideDx = options.maxSlideDx
  66. this.maxSlideDy = options.maxSlideDy
  67. this.limitArea = options.limitArea
  68. this._init()
  69. }
  70. _init () {
  71. // 注册滑动开始、过程、结束事件
  72. this.slidestart = new CustomEvent('slidestart', {
  73. detail: this.customData,
  74. bubbles: true,
  75. cancelable: true
  76. })
  77. this.slidemove = new CustomEvent('slidemove', {
  78. detail: this.customData,
  79. bubbles: true,
  80. cancelable: true
  81. })
  82. this.slideend = new CustomEvent('slideend', {
  83. detail: this.customData,
  84. bubbles: true,
  85. cancelable: true
  86. })
  87. // 监听原生触摸事件
  88. this.setSupportsPassive()
  89. if ('ontouchstart' in window) {
  90. this._on(this.el, 'touchstart', this._start)
  91. this._on(this.el, 'touchmove', this._move)
  92. this._on(this.el, 'touchend', this._end)
  93. } else {
  94. this._on(window, 'mousedown', this._start)
  95. this._on(window, 'mousemove', this._move)
  96. this._on(window, 'mouseup', this._end)
  97. }
  98. }
  99. _on (el, event, fn) {
  100. el.addEventListener(
  101. event,
  102. fn,
  103. this.supportsPassive
  104. ? {
  105. capture: false,
  106. passive: false
  107. }
  108. : false
  109. )
  110. }
  111. _off (el, event, fn) {
  112. el.removeEventListener(
  113. event,
  114. fn,
  115. this.supportsPassive
  116. ? {
  117. capture: false,
  118. passive: false
  119. }
  120. : false
  121. )
  122. }
  123. setSupportsPassive () {
  124. try {
  125. const opts = Object.defineProperty({}, 'passive', {
  126. get: () => {
  127. this.supportsPassive = true
  128. return true
  129. }
  130. })
  131. window.addEventListener('testPassive', null, opts)
  132. window.removeEventListener('testPassive', null, opts)
  133. } catch (e) {
  134. console.error(e)
  135. }
  136. }
  137. checkNode (node) {
  138. if (!node) {
  139. return false
  140. }
  141. if (node === this.el) {
  142. return true
  143. }
  144. return this.checkNode(node.parentNode)
  145. }
  146. _start = (e) => {
  147. e.preventDefault()
  148. let startx = 0
  149. let starty = 0
  150. if (e.type === 'mousedown') {
  151. if (e.button !== 0) {
  152. return
  153. }
  154. if (this.checkNode(e.target)) {
  155. this.canSlide = true
  156. } else {
  157. this.canSlide = false
  158. return
  159. }
  160. startx = e.pageX
  161. starty = e.pageY
  162. } else {
  163. e.preventDefault()
  164. startx = e.targetTouches[0].pageX
  165. starty = e.targetTouches[0].pageY
  166. }
  167. // 初始化data
  168. Object.assign(this.customData, {
  169. startx,
  170. starty,
  171. endx: startx,
  172. endy: starty,
  173. dx: 0,
  174. dy: 0,
  175. offsetx: 0,
  176. offsety: 0
  177. })
  178. this.prePoint = {
  179. x: startx,
  180. y: starty
  181. }
  182. this.el.dispatchEvent(this.slidestart)
  183. }
  184. _move = (e) => {
  185. let endx = 0
  186. let endy = 0
  187. if (e.type === 'mousemove') {
  188. if (!this.canSlide) {
  189. return
  190. }
  191. endx = e.pageX
  192. endy = e.pageY
  193. } else {
  194. endx = e.targetTouches[0].pageX
  195. endy = e.targetTouches[0].pageY
  196. }
  197. // 相较于上一次touchmove点的距离
  198. let dx = endx - this.prePoint.x
  199. let dy = endy - this.prePoint.y
  200. let offsetx = 0
  201. let offsety = 0
  202. // 单次滑动过程不能超过设定maxSlideDx值
  203. if (this.maxSlideDx) {
  204. if (this.customData.offsetx + dx >= this.maxSlideDx) {
  205. dx = this.maxSlideDx - this.customData.offsetx
  206. offsetx = this.maxSlideDx
  207. } else if (this.customData.offsetx + dx <= -this.maxSlideDx) {
  208. dx = -this.maxSlideDx - this.customData.offsetx
  209. offsetx = -this.maxSlideDx
  210. } else {
  211. if (!this.limitArea) {
  212. offsetx = this.customData.offsetx + dx
  213. } else {
  214. const x = endx - this.customData.startx
  215. // 鼠标在界限范内
  216. if (x > -this.maxSlideDx && x < this.maxSlideDx) {
  217. // 处理边界问题
  218. // 如果是上次鼠标位置在左边界外面,然后移动到里面
  219. // 修正 dx 的值
  220. if (this.prePoint.x - this.customData.startx < -this.maxSlideDx) {
  221. dx = x - -this.maxSlideDx
  222. } else if (
  223. this.prePoint.x - this.customData.startx >
  224. this.maxSlideDx
  225. ) {
  226. dx = x - this.maxSlideDx
  227. }
  228. offsetx = x
  229. } else {
  230. offsetx = this.customData.offsetx
  231. dx = 0
  232. }
  233. }
  234. }
  235. } else {
  236. offsetx = endx - this.customData.startx
  237. }
  238. // 单次滑动过程不能超过设定maxSlideDy值
  239. if (this.customData.offsety + dy > this.maxSlideDy) {
  240. dy = this.maxSlideDy - this.customData.offsety
  241. offsety = this.maxSlideDy
  242. } else if (this.customData.offsety + dy < -this.maxSlideDy) {
  243. dy = -this.maxSlideDy - this.customData.offsety
  244. offsety = -this.maxSlideDy
  245. } else {
  246. offsety = endy - this.customData.starty
  247. }
  248. Object.assign(this.customData, {
  249. endx,
  250. endy,
  251. dx,
  252. dy,
  253. offsetx,
  254. offsety
  255. })
  256. this.prePoint = {
  257. x: endx,
  258. y: endy
  259. }
  260. this.el.dispatchEvent(this.slidemove)
  261. }
  262. _end = (e) => {
  263. let endx = 0
  264. let endy = 0
  265. if (e.type === 'mouseup') {
  266. if (!this.canSlide) {
  267. return
  268. }
  269. endx = e.pageX
  270. endy = e.pageY
  271. this.canSlide = false
  272. } else {
  273. endx = e.changedTouches[0].pageX
  274. endy = e.changedTouches[0].pageY
  275. }
  276. Object.assign(this.customData, {
  277. endx,
  278. endy
  279. })
  280. this.el.dispatchEvent(this.slideend)
  281. }
  282. setMaxSlideDx (ds) {
  283. this.maxSlideDx = ds
  284. }
  285. setMaxSlideDy (ds) {
  286. this.maxSlideDy = ds
  287. }
  288. // 销毁自定义事件
  289. destroy () {
  290. this._off(this.el, 'touchstart', this._start)
  291. this._off(this.el, 'touchmove', this._move)
  292. this._off(this.el, 'touchend', this._end)
  293. this._off(window, 'mousedown', this._start)
  294. this._off(window, 'mousemove', this._move)
  295. this._off(window, 'mouseup', this._end)
  296. }
  297. }