Drag.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import { EVENT_DRAG, EVENT_DRAGGED, EVENT_DRAGGING } from '../../constants/events';
  2. import { FADE, LOOP, SLIDE } from '../../constants/types';
  3. import { EventInterface } from '../../constructors';
  4. import { Splide } from '../../core/Splide/Splide';
  5. import { BaseComponent, Components, Options } from '../../types';
  6. import { abs, clamp, min, prevent, sign } from '../../utils';
  7. import { FRICTION, POINTER_DOWN_EVENTS, POINTER_MOVE_EVENTS, POINTER_UP_EVENTS, SAMPLING_INTERVAL } from './constants';
  8. /**
  9. * The interface for the Drag component.
  10. *
  11. * @since 3.0.0
  12. */
  13. export interface DragComponent extends BaseComponent {
  14. }
  15. /**
  16. * The component for dragging the slider.
  17. *
  18. * @since 3.0.0
  19. *
  20. * @param Splide - A Splide instance.
  21. * @param Components - A collection of components.
  22. * @param options - Options.
  23. *
  24. * @return A Drag component object.
  25. */
  26. export function Drag( Splide: Splide, Components: Components, options: Options ): DragComponent {
  27. const { emit, bind, unbind } = EventInterface( Splide );
  28. const { track } = Components.Elements;
  29. const { resolve, orient } = Components.Direction;
  30. const { listSize } = Components.Layout;
  31. const { go, getEnd } = Components.Controller;
  32. const { Move, Scroll } = Components;
  33. const { translate, toIndex, getPosition, isExceeded } = Move;
  34. const isSlide = Splide.is( SLIDE );
  35. const isFade = Splide.is( FADE );
  36. const isFree = options.drag === 'free';
  37. /**
  38. * The coord where a pointer becomes active.
  39. */
  40. let startCoord: number;
  41. /**
  42. * Keeps the last time when the component detects dragging.
  43. */
  44. let lastTime: number;
  45. /**
  46. * The base slider position where the diff of coords is applied.
  47. */
  48. let basePosition: number;
  49. /**
  50. * The base coord to calculate the diff of coords.
  51. */
  52. let baseCoord: number;
  53. /**
  54. * The base time when the base position and the base coord are saved.
  55. */
  56. let baseTime: number;
  57. /**
  58. * Keeps the last TouchEvent/MouseEvent object.
  59. */
  60. let lastEvent: TouchEvent | MouseEvent;
  61. /**
  62. * Indicates whether the user is dragging the slider or not.
  63. */
  64. let moving: boolean;
  65. /**
  66. * Indicates whether the user drags the slider by the mouse or not.
  67. */
  68. let isMouse: boolean;
  69. /**
  70. * The target element to attach listeners.
  71. */
  72. let target: Window | HTMLElement;
  73. /**
  74. * Indicates whether the slider exceeds borders or not.
  75. */
  76. let exceeded: boolean;
  77. /**
  78. * Called when the component is mounted.
  79. */
  80. function mount(): void {
  81. if ( options.drag ) {
  82. bind( track, POINTER_DOWN_EVENTS, onPointerDown );
  83. }
  84. }
  85. /**
  86. * Called when the user clicks or touches the slider.
  87. *
  88. * @param e - A TouchEvent or MouseEvent object
  89. */
  90. function onPointerDown( e: TouchEvent | MouseEvent ): void {
  91. isMouse = e.type === 'mousedown';
  92. target = isMouse ? window : track;
  93. if ( ! ( isMouse && ( e as MouseEvent ).button ) ) {
  94. if ( ! Move.isBusy() ) {
  95. bind( target, POINTER_MOVE_EVENTS, onPointerMove );
  96. bind( target, POINTER_UP_EVENTS, onPointerUp );
  97. Move.cancel();
  98. Scroll.cancel();
  99. startCoord = getCoord( e );
  100. } else {
  101. prevent( e );
  102. }
  103. }
  104. }
  105. /**
  106. * Called while the user moves the pointer on the slider.
  107. *
  108. * @param e - A TouchEvent or MouseEvent object
  109. */
  110. function onPointerMove( e: TouchEvent | MouseEvent ): void {
  111. if ( e.cancelable ) {
  112. const min = options.dragMinThreshold || 15;
  113. if ( isMouse || abs( getCoord( e ) - startCoord ) > min ) {
  114. moving = true;
  115. onDrag();
  116. }
  117. if ( moving ) {
  118. onDragging( e );
  119. prevent( e, true );
  120. }
  121. } else {
  122. onPointerUp( e );
  123. }
  124. }
  125. /**
  126. * Called when the user releases pointing devices.
  127. * Be aware that the TouchEvent object provided by the `touchend` does not contain `Touch` objects,
  128. * which means the last touch position is not available.
  129. *
  130. * @param e - A TouchEvent or MouseEvent object
  131. */
  132. function onPointerUp( e: TouchEvent | MouseEvent ): void {
  133. unbind( target, `${ POINTER_MOVE_EVENTS } ${ POINTER_UP_EVENTS }` );
  134. moving = false;
  135. if ( lastEvent ) {
  136. onDragged( e );
  137. lastEvent = null;
  138. }
  139. }
  140. /**
  141. * Called when the user starts dragging the slider.
  142. */
  143. function onDrag(): void {
  144. bind( track, 'click', e => {
  145. unbind( track, 'click' );
  146. prevent( e, true );
  147. }, { capture: true } );
  148. emit( EVENT_DRAG );
  149. }
  150. /**
  151. * Called while the user is dragging the slider.
  152. *
  153. * @param e - A TouchEvent or MouseEvent object
  154. */
  155. function onDragging( e: TouchEvent | MouseEvent ): void {
  156. const { timeStamp } = e;
  157. const expired = ! lastTime || ( timeStamp - lastTime > SAMPLING_INTERVAL );
  158. if ( expired || isExceeded() !== exceeded ) {
  159. basePosition = getPosition();
  160. baseCoord = getCoord( e );
  161. baseTime = timeStamp;
  162. }
  163. exceeded = isExceeded();
  164. lastTime = timeStamp;
  165. lastEvent = e;
  166. if ( ! isFade ) {
  167. translate( basePosition + constrain( getCoord( e ) - baseCoord ) );
  168. }
  169. emit( EVENT_DRAGGING );
  170. }
  171. /**
  172. * Called when the user finishes dragging.
  173. *
  174. * @param e - A TouchEvent or MouseEvent object
  175. */
  176. function onDragged( e: TouchEvent | MouseEvent ): void {
  177. const velocity = computeVelocity( e );
  178. if ( isFade ) {
  179. go( Splide.index + orient( sign( velocity ) ) );
  180. } else {
  181. const destination = computeDestination( velocity );
  182. if ( isFree ) {
  183. Scroll.scroll( destination );
  184. } else {
  185. go( computeIndex( destination ), true );
  186. }
  187. }
  188. lastTime = 0;
  189. emit( EVENT_DRAGGED );
  190. }
  191. /**
  192. * Computes the drag velocity.
  193. *
  194. * @param e - A TouchEvent or MouseEvent object
  195. *
  196. * @return The drag velocity.
  197. */
  198. function computeVelocity( e: TouchEvent | MouseEvent ): number {
  199. if ( Splide.is( LOOP ) || ! isExceeded() ) {
  200. const diffCoord = getCoord( lastEvent ) - baseCoord;
  201. const diffTime = lastEvent.timeStamp - baseTime;
  202. const isFlick = e.timeStamp - lastTime < SAMPLING_INTERVAL;
  203. if ( diffTime && isFlick ) {
  204. return diffCoord / diffTime;
  205. }
  206. }
  207. return 0;
  208. }
  209. /**
  210. * Computes the destination by the velocity and the `flickPower` option.
  211. *
  212. * @param velocity - The drag velocity.
  213. *
  214. * @return The destination.
  215. */
  216. function computeDestination( velocity: number ): number {
  217. const flickPower = options.flickPower || 600;
  218. return getPosition() + sign( velocity ) * min(
  219. abs( velocity ) * flickPower,
  220. isFree ? Infinity : listSize() * ( options.flickMaxPages || 1 )
  221. );
  222. }
  223. /**
  224. * Converts the destination to the slide index.
  225. *
  226. * @param destination - The target destination.
  227. *
  228. * @return The destination index.
  229. */
  230. function computeIndex( destination: number ): number {
  231. const dest = toIndex( destination );
  232. return isSlide ? clamp( dest, 0, getEnd() ) : dest;
  233. }
  234. /**
  235. * Returns the `pageX` and `pageY` coordinates provided by the event.
  236. * Be aware that IE does not support both TouchEvent and MouseEvent constructors.
  237. *
  238. * @param e - A TouchEvent or MouseEvent object.
  239. *
  240. * @return A pageX or pageY coordinate.
  241. */
  242. function getCoord( e: TouchEvent | MouseEvent ): number {
  243. return ( isMouse ? e : ( e as TouchEvent ).touches[ 0 ] )[ resolve( 'pageX' ) ];
  244. }
  245. /**
  246. * Reduces the distance to move by the predefined friction.
  247. * This does nothing when the slider type is not `slide`, or the position is inside borders.
  248. *
  249. * @param diff - Diff to constrain.
  250. *
  251. * @return The constrained diff.
  252. */
  253. function constrain( diff: number ): number {
  254. return diff / ( exceeded && isSlide ? FRICTION : 1 );
  255. }
  256. return {
  257. mount,
  258. };
  259. }