drag.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. // drag
  2. ( function( window, factory ) {
  3. // universal module definition
  4. /* jshint strict: false */
  5. if ( typeof define == 'function' && define.amd ) {
  6. // AMD
  7. define( [
  8. './flickity',
  9. 'unidragger/unidragger',
  10. 'fizzy-ui-utils/utils'
  11. ], function( Flickity, Unidragger, utils ) {
  12. return factory( window, Flickity, Unidragger, utils );
  13. });
  14. } else if ( typeof module == 'object' && module.exports ) {
  15. // CommonJS
  16. module.exports = factory(
  17. window,
  18. require('./flickity'),
  19. require('unidragger'),
  20. require('fizzy-ui-utils')
  21. );
  22. } else {
  23. // browser global
  24. window.Flickity = factory(
  25. window,
  26. window.Flickity,
  27. window.Unidragger,
  28. window.fizzyUIUtils
  29. );
  30. }
  31. }( window, function factory( window, Flickity, Unidragger, utils ) {
  32. 'use strict';
  33. // ----- defaults ----- //
  34. utils.extend( Flickity.defaults, {
  35. draggable: '>1',
  36. dragThreshold: 3,
  37. });
  38. // ----- create ----- //
  39. Flickity.createMethods.push('_createDrag');
  40. // -------------------------- drag prototype -------------------------- //
  41. var proto = Flickity.prototype;
  42. utils.extend( proto, Unidragger.prototype );
  43. proto._touchActionValue = 'pan-y';
  44. // -------------------------- -------------------------- //
  45. var isTouch = 'createTouch' in document;
  46. var isTouchmoveScrollCanceled = false;
  47. proto._createDrag = function() {
  48. this.on( 'activate', this.onActivateDrag );
  49. this.on( 'uiChange', this._uiChangeDrag );
  50. this.on( 'deactivate', this.onDeactivateDrag );
  51. this.on( 'cellChange', this.updateDraggable );
  52. // TODO updateDraggable on resize? if groupCells & slides change
  53. // HACK - add seemingly innocuous handler to fix iOS 10 scroll behavior
  54. // #457, RubaXa/Sortable#973
  55. if ( isTouch && !isTouchmoveScrollCanceled ) {
  56. window.addEventListener( 'touchmove', function() {});
  57. isTouchmoveScrollCanceled = true;
  58. }
  59. };
  60. proto.onActivateDrag = function() {
  61. this.handles = [ this.viewport ];
  62. this.bindHandles();
  63. this.updateDraggable();
  64. };
  65. proto.onDeactivateDrag = function() {
  66. this.unbindHandles();
  67. this.element.classList.remove('is-draggable');
  68. };
  69. proto.updateDraggable = function() {
  70. // disable dragging if less than 2 slides. #278
  71. if ( this.options.draggable == '>1' ) {
  72. this.isDraggable = this.slides.length > 1;
  73. } else {
  74. this.isDraggable = this.options.draggable;
  75. }
  76. if ( this.isDraggable ) {
  77. this.element.classList.add('is-draggable');
  78. } else {
  79. this.element.classList.remove('is-draggable');
  80. }
  81. };
  82. // backwards compatibility
  83. proto.bindDrag = function() {
  84. this.options.draggable = true;
  85. this.updateDraggable();
  86. };
  87. proto.unbindDrag = function() {
  88. this.options.draggable = false;
  89. this.updateDraggable();
  90. };
  91. proto._uiChangeDrag = function() {
  92. delete this.isFreeScrolling;
  93. };
  94. // -------------------------- pointer events -------------------------- //
  95. proto.pointerDown = function( event, pointer ) {
  96. if ( !this.isDraggable ) {
  97. this._pointerDownDefault( event, pointer );
  98. return;
  99. }
  100. var isOkay = this.okayPointerDown( event );
  101. if ( !isOkay ) {
  102. return;
  103. }
  104. this._pointerDownPreventDefault( event );
  105. this.pointerDownFocus( event );
  106. // blur
  107. if ( document.activeElement != this.element ) {
  108. // do not blur if already focused
  109. this.pointerDownBlur();
  110. }
  111. // stop if it was moving
  112. this.dragX = this.x;
  113. this.viewport.classList.add('is-pointer-down');
  114. // track scrolling
  115. this.pointerDownScroll = getScrollPosition();
  116. window.addEventListener( 'scroll', this );
  117. this._pointerDownDefault( event, pointer );
  118. };
  119. // default pointerDown logic, used for staticClick
  120. proto._pointerDownDefault = function( event, pointer ) {
  121. // track start event position
  122. // Safari 9 overrides pageX and pageY. These values needs to be copied. #779
  123. this.pointerDownPointer = {
  124. pageX: pointer.pageX,
  125. pageY: pointer.pageY,
  126. };
  127. // bind move and end events
  128. this._bindPostStartEvents( event );
  129. this.dispatchEvent( 'pointerDown', event, [ pointer ] );
  130. };
  131. var focusNodes = {
  132. INPUT: true,
  133. TEXTAREA: true,
  134. SELECT: true,
  135. };
  136. proto.pointerDownFocus = function( event ) {
  137. var isFocusNode = focusNodes[ event.target.nodeName ];
  138. if ( !isFocusNode ) {
  139. this.focus();
  140. }
  141. };
  142. proto._pointerDownPreventDefault = function( event ) {
  143. var isTouchStart = event.type == 'touchstart';
  144. var isTouchPointer = event.pointerType == 'touch';
  145. var isFocusNode = focusNodes[ event.target.nodeName ];
  146. if ( !isTouchStart && !isTouchPointer && !isFocusNode ) {
  147. event.preventDefault();
  148. }
  149. };
  150. // ----- move ----- //
  151. proto.hasDragStarted = function( moveVector ) {
  152. return Math.abs( moveVector.x ) > this.options.dragThreshold;
  153. };
  154. // ----- up ----- //
  155. proto.pointerUp = function( event, pointer ) {
  156. delete this.isTouchScrolling;
  157. this.viewport.classList.remove('is-pointer-down');
  158. this.dispatchEvent( 'pointerUp', event, [ pointer ] );
  159. this._dragPointerUp( event, pointer );
  160. };
  161. proto.pointerDone = function() {
  162. window.removeEventListener( 'scroll', this );
  163. delete this.pointerDownScroll;
  164. };
  165. // -------------------------- dragging -------------------------- //
  166. proto.dragStart = function( event, pointer ) {
  167. if ( !this.isDraggable ) {
  168. return;
  169. }
  170. this.dragStartPosition = this.x;
  171. this.startAnimation();
  172. window.removeEventListener( 'scroll', this );
  173. this.dispatchEvent( 'dragStart', event, [ pointer ] );
  174. };
  175. proto.pointerMove = function( event, pointer ) {
  176. var moveVector = this._dragPointerMove( event, pointer );
  177. this.dispatchEvent( 'pointerMove', event, [ pointer, moveVector ] );
  178. this._dragMove( event, pointer, moveVector );
  179. };
  180. proto.dragMove = function( event, pointer, moveVector ) {
  181. if ( !this.isDraggable ) {
  182. return;
  183. }
  184. event.preventDefault();
  185. this.previousDragX = this.dragX;
  186. // reverse if right-to-left
  187. var direction = this.options.rightToLeft ? -1 : 1;
  188. if ( this.options.wrapAround ) {
  189. // wrap around move. #589
  190. moveVector.x = moveVector.x % this.slideableWidth;
  191. }
  192. var dragX = this.dragStartPosition + moveVector.x * direction;
  193. if ( !this.options.wrapAround && this.slides.length ) {
  194. // slow drag
  195. var originBound = Math.max( -this.slides[0].target, this.dragStartPosition );
  196. dragX = dragX > originBound ? ( dragX + originBound ) * 0.5 : dragX;
  197. var endBound = Math.min( -this.getLastSlide().target, this.dragStartPosition );
  198. dragX = dragX < endBound ? ( dragX + endBound ) * 0.5 : dragX;
  199. }
  200. this.dragX = dragX;
  201. this.dragMoveTime = new Date();
  202. this.dispatchEvent( 'dragMove', event, [ pointer, moveVector ] );
  203. };
  204. proto.dragEnd = function( event, pointer ) {
  205. if ( !this.isDraggable ) {
  206. return;
  207. }
  208. if ( this.options.freeScroll ) {
  209. this.isFreeScrolling = true;
  210. }
  211. // set selectedIndex based on where flick will end up
  212. var index = this.dragEndRestingSelect();
  213. if ( this.options.freeScroll && !this.options.wrapAround ) {
  214. // if free-scroll & not wrap around
  215. // do not free-scroll if going outside of bounding slides
  216. // so bounding slides can attract slider, and keep it in bounds
  217. var restingX = this.getRestingPosition();
  218. this.isFreeScrolling = -restingX > this.slides[0].target &&
  219. -restingX < this.getLastSlide().target;
  220. } else if ( !this.options.freeScroll && index == this.selectedIndex ) {
  221. // boost selection if selected index has not changed
  222. index += this.dragEndBoostSelect();
  223. }
  224. delete this.previousDragX;
  225. // apply selection
  226. // TODO refactor this, selecting here feels weird
  227. // HACK, set flag so dragging stays in correct direction
  228. this.isDragSelect = this.options.wrapAround;
  229. this.select( index );
  230. delete this.isDragSelect;
  231. this.dispatchEvent( 'dragEnd', event, [ pointer ] );
  232. };
  233. proto.dragEndRestingSelect = function() {
  234. var restingX = this.getRestingPosition();
  235. // how far away from selected slide
  236. var distance = Math.abs( this.getSlideDistance( -restingX, this.selectedIndex ) );
  237. // get closet resting going up and going down
  238. var positiveResting = this._getClosestResting( restingX, distance, 1 );
  239. var negativeResting = this._getClosestResting( restingX, distance, -1 );
  240. // use closer resting for wrap-around
  241. var index = positiveResting.distance < negativeResting.distance ?
  242. positiveResting.index : negativeResting.index;
  243. return index;
  244. };
  245. /**
  246. * given resting X and distance to selected cell
  247. * get the distance and index of the closest cell
  248. * @param {Number} restingX - estimated post-flick resting position
  249. * @param {Number} distance - distance to selected cell
  250. * @param {Integer} increment - +1 or -1, going up or down
  251. * @returns {Object} - { distance: {Number}, index: {Integer} }
  252. */
  253. proto._getClosestResting = function( restingX, distance, increment ) {
  254. var index = this.selectedIndex;
  255. var minDistance = Infinity;
  256. var condition = this.options.contain && !this.options.wrapAround ?
  257. // if contain, keep going if distance is equal to minDistance
  258. function( d, md ) { return d <= md; } : function( d, md ) { return d < md; };
  259. while ( condition( distance, minDistance ) ) {
  260. // measure distance to next cell
  261. index += increment;
  262. minDistance = distance;
  263. distance = this.getSlideDistance( -restingX, index );
  264. if ( distance === null ) {
  265. break;
  266. }
  267. distance = Math.abs( distance );
  268. }
  269. return {
  270. distance: minDistance,
  271. // selected was previous index
  272. index: index - increment
  273. };
  274. };
  275. /**
  276. * measure distance between x and a slide target
  277. * @param {Number} x
  278. * @param {Integer} index - slide index
  279. */
  280. proto.getSlideDistance = function( x, index ) {
  281. var len = this.slides.length;
  282. // wrap around if at least 2 slides
  283. var isWrapAround = this.options.wrapAround && len > 1;
  284. var slideIndex = isWrapAround ? utils.modulo( index, len ) : index;
  285. var slide = this.slides[ slideIndex ];
  286. if ( !slide ) {
  287. return null;
  288. }
  289. // add distance for wrap-around slides
  290. var wrap = isWrapAround ? this.slideableWidth * Math.floor( index / len ) : 0;
  291. return x - ( slide.target + wrap );
  292. };
  293. proto.dragEndBoostSelect = function() {
  294. // do not boost if no previousDragX or dragMoveTime
  295. if ( this.previousDragX === undefined || !this.dragMoveTime ||
  296. // or if drag was held for 100 ms
  297. new Date() - this.dragMoveTime > 100 ) {
  298. return 0;
  299. }
  300. var distance = this.getSlideDistance( -this.dragX, this.selectedIndex );
  301. var delta = this.previousDragX - this.dragX;
  302. if ( distance > 0 && delta > 0 ) {
  303. // boost to next if moving towards the right, and positive velocity
  304. return 1;
  305. } else if ( distance < 0 && delta < 0 ) {
  306. // boost to previous if moving towards the left, and negative velocity
  307. return -1;
  308. }
  309. return 0;
  310. };
  311. // ----- staticClick ----- //
  312. proto.staticClick = function( event, pointer ) {
  313. // get clickedCell, if cell was clicked
  314. var clickedCell = this.getParentCell( event.target );
  315. var cellElem = clickedCell && clickedCell.element;
  316. var cellIndex = clickedCell && this.cells.indexOf( clickedCell );
  317. this.dispatchEvent( 'staticClick', event, [ pointer, cellElem, cellIndex ] );
  318. };
  319. // ----- scroll ----- //
  320. proto.onscroll = function() {
  321. var scroll = getScrollPosition();
  322. var scrollMoveX = this.pointerDownScroll.x - scroll.x;
  323. var scrollMoveY = this.pointerDownScroll.y - scroll.y;
  324. // cancel click/tap if scroll is too much
  325. if ( Math.abs( scrollMoveX ) > 3 || Math.abs( scrollMoveY ) > 3 ) {
  326. this._pointerDone();
  327. }
  328. };
  329. // ----- utils ----- //
  330. function getScrollPosition() {
  331. return {
  332. x: window.pageXOffset,
  333. y: window.pageYOffset
  334. };
  335. }
  336. // ----- ----- //
  337. return Flickity;
  338. }));