mousewheel.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /* eslint-disable consistent-return */
  2. import { getWindow } from 'ssr-window';
  3. import $ from '../../shared/dom.js';
  4. import { now, nextTick } from '../../shared/utils.js';
  5. export default function Mousewheel(_ref) {
  6. let {
  7. swiper,
  8. extendParams,
  9. on,
  10. emit
  11. } = _ref;
  12. const window = getWindow();
  13. extendParams({
  14. mousewheel: {
  15. enabled: false,
  16. releaseOnEdges: false,
  17. invert: false,
  18. forceToAxis: false,
  19. sensitivity: 1,
  20. eventsTarget: 'container',
  21. thresholdDelta: null,
  22. thresholdTime: null
  23. }
  24. });
  25. swiper.mousewheel = {
  26. enabled: false
  27. };
  28. let timeout;
  29. let lastScrollTime = now();
  30. let lastEventBeforeSnap;
  31. const recentWheelEvents = [];
  32. function normalize(e) {
  33. // Reasonable defaults
  34. const PIXEL_STEP = 10;
  35. const LINE_HEIGHT = 40;
  36. const PAGE_HEIGHT = 800;
  37. let sX = 0;
  38. let sY = 0; // spinX, spinY
  39. let pX = 0;
  40. let pY = 0; // pixelX, pixelY
  41. // Legacy
  42. if ('detail' in e) {
  43. sY = e.detail;
  44. }
  45. if ('wheelDelta' in e) {
  46. sY = -e.wheelDelta / 120;
  47. }
  48. if ('wheelDeltaY' in e) {
  49. sY = -e.wheelDeltaY / 120;
  50. }
  51. if ('wheelDeltaX' in e) {
  52. sX = -e.wheelDeltaX / 120;
  53. } // side scrolling on FF with DOMMouseScroll
  54. if ('axis' in e && e.axis === e.HORIZONTAL_AXIS) {
  55. sX = sY;
  56. sY = 0;
  57. }
  58. pX = sX * PIXEL_STEP;
  59. pY = sY * PIXEL_STEP;
  60. if ('deltaY' in e) {
  61. pY = e.deltaY;
  62. }
  63. if ('deltaX' in e) {
  64. pX = e.deltaX;
  65. }
  66. if (e.shiftKey && !pX) {
  67. // if user scrolls with shift he wants horizontal scroll
  68. pX = pY;
  69. pY = 0;
  70. }
  71. if ((pX || pY) && e.deltaMode) {
  72. if (e.deltaMode === 1) {
  73. // delta in LINE units
  74. pX *= LINE_HEIGHT;
  75. pY *= LINE_HEIGHT;
  76. } else {
  77. // delta in PAGE units
  78. pX *= PAGE_HEIGHT;
  79. pY *= PAGE_HEIGHT;
  80. }
  81. } // Fall-back if spin cannot be determined
  82. if (pX && !sX) {
  83. sX = pX < 1 ? -1 : 1;
  84. }
  85. if (pY && !sY) {
  86. sY = pY < 1 ? -1 : 1;
  87. }
  88. return {
  89. spinX: sX,
  90. spinY: sY,
  91. pixelX: pX,
  92. pixelY: pY
  93. };
  94. }
  95. function handleMouseEnter() {
  96. if (!swiper.enabled) return;
  97. swiper.mouseEntered = true;
  98. }
  99. function handleMouseLeave() {
  100. if (!swiper.enabled) return;
  101. swiper.mouseEntered = false;
  102. }
  103. function animateSlider(newEvent) {
  104. if (swiper.params.mousewheel.thresholdDelta && newEvent.delta < swiper.params.mousewheel.thresholdDelta) {
  105. // Prevent if delta of wheel scroll delta is below configured threshold
  106. return false;
  107. }
  108. if (swiper.params.mousewheel.thresholdTime && now() - lastScrollTime < swiper.params.mousewheel.thresholdTime) {
  109. // Prevent if time between scrolls is below configured threshold
  110. return false;
  111. } // If the movement is NOT big enough and
  112. // if the last time the user scrolled was too close to the current one (avoid continuously triggering the slider):
  113. // Don't go any further (avoid insignificant scroll movement).
  114. if (newEvent.delta >= 6 && now() - lastScrollTime < 60) {
  115. // Return false as a default
  116. return true;
  117. } // If user is scrolling towards the end:
  118. // If the slider hasn't hit the latest slide or
  119. // if the slider is a loop and
  120. // if the slider isn't moving right now:
  121. // Go to next slide and
  122. // emit a scroll event.
  123. // Else (the user is scrolling towards the beginning) and
  124. // if the slider hasn't hit the first slide or
  125. // if the slider is a loop and
  126. // if the slider isn't moving right now:
  127. // Go to prev slide and
  128. // emit a scroll event.
  129. if (newEvent.direction < 0) {
  130. if ((!swiper.isEnd || swiper.params.loop) && !swiper.animating) {
  131. swiper.slideNext();
  132. emit('scroll', newEvent.raw);
  133. }
  134. } else if ((!swiper.isBeginning || swiper.params.loop) && !swiper.animating) {
  135. swiper.slidePrev();
  136. emit('scroll', newEvent.raw);
  137. } // If you got here is because an animation has been triggered so store the current time
  138. lastScrollTime = new window.Date().getTime(); // Return false as a default
  139. return false;
  140. }
  141. function releaseScroll(newEvent) {
  142. const params = swiper.params.mousewheel;
  143. if (newEvent.direction < 0) {
  144. if (swiper.isEnd && !swiper.params.loop && params.releaseOnEdges) {
  145. // Return true to animate scroll on edges
  146. return true;
  147. }
  148. } else if (swiper.isBeginning && !swiper.params.loop && params.releaseOnEdges) {
  149. // Return true to animate scroll on edges
  150. return true;
  151. }
  152. return false;
  153. }
  154. function handle(event) {
  155. let e = event;
  156. let disableParentSwiper = true;
  157. if (!swiper.enabled) return;
  158. const params = swiper.params.mousewheel;
  159. if (swiper.params.cssMode) {
  160. e.preventDefault();
  161. }
  162. let target = swiper.$el;
  163. if (swiper.params.mousewheel.eventsTarget !== 'container') {
  164. target = $(swiper.params.mousewheel.eventsTarget);
  165. }
  166. if (!swiper.mouseEntered && !target[0].contains(e.target) && !params.releaseOnEdges) return true;
  167. if (e.originalEvent) e = e.originalEvent; // jquery fix
  168. let delta = 0;
  169. const rtlFactor = swiper.rtlTranslate ? -1 : 1;
  170. const data = normalize(e);
  171. if (params.forceToAxis) {
  172. if (swiper.isHorizontal()) {
  173. if (Math.abs(data.pixelX) > Math.abs(data.pixelY)) delta = -data.pixelX * rtlFactor;else return true;
  174. } else if (Math.abs(data.pixelY) > Math.abs(data.pixelX)) delta = -data.pixelY;else return true;
  175. } else {
  176. delta = Math.abs(data.pixelX) > Math.abs(data.pixelY) ? -data.pixelX * rtlFactor : -data.pixelY;
  177. }
  178. if (delta === 0) return true;
  179. if (params.invert) delta = -delta; // Get the scroll positions
  180. let positions = swiper.getTranslate() + delta * params.sensitivity;
  181. if (positions >= swiper.minTranslate()) positions = swiper.minTranslate();
  182. if (positions <= swiper.maxTranslate()) positions = swiper.maxTranslate(); // When loop is true:
  183. // the disableParentSwiper will be true.
  184. // When loop is false:
  185. // if the scroll positions is not on edge,
  186. // then the disableParentSwiper will be true.
  187. // if the scroll on edge positions,
  188. // then the disableParentSwiper will be false.
  189. disableParentSwiper = swiper.params.loop ? true : !(positions === swiper.minTranslate() || positions === swiper.maxTranslate());
  190. if (disableParentSwiper && swiper.params.nested) e.stopPropagation();
  191. if (!swiper.params.freeMode || !swiper.params.freeMode.enabled) {
  192. // Register the new event in a variable which stores the relevant data
  193. const newEvent = {
  194. time: now(),
  195. delta: Math.abs(delta),
  196. direction: Math.sign(delta),
  197. raw: event
  198. }; // Keep the most recent events
  199. if (recentWheelEvents.length >= 2) {
  200. recentWheelEvents.shift(); // only store the last N events
  201. }
  202. const prevEvent = recentWheelEvents.length ? recentWheelEvents[recentWheelEvents.length - 1] : undefined;
  203. recentWheelEvents.push(newEvent); // If there is at least one previous recorded event:
  204. // If direction has changed or
  205. // if the scroll is quicker than the previous one:
  206. // Animate the slider.
  207. // Else (this is the first time the wheel is moved):
  208. // Animate the slider.
  209. if (prevEvent) {
  210. if (newEvent.direction !== prevEvent.direction || newEvent.delta > prevEvent.delta || newEvent.time > prevEvent.time + 150) {
  211. animateSlider(newEvent);
  212. }
  213. } else {
  214. animateSlider(newEvent);
  215. } // If it's time to release the scroll:
  216. // Return now so you don't hit the preventDefault.
  217. if (releaseScroll(newEvent)) {
  218. return true;
  219. }
  220. } else {
  221. // Freemode or scrollContainer:
  222. // If we recently snapped after a momentum scroll, then ignore wheel events
  223. // to give time for the deceleration to finish. Stop ignoring after 500 msecs
  224. // or if it's a new scroll (larger delta or inverse sign as last event before
  225. // an end-of-momentum snap).
  226. const newEvent = {
  227. time: now(),
  228. delta: Math.abs(delta),
  229. direction: Math.sign(delta)
  230. };
  231. const ignoreWheelEvents = lastEventBeforeSnap && newEvent.time < lastEventBeforeSnap.time + 500 && newEvent.delta <= lastEventBeforeSnap.delta && newEvent.direction === lastEventBeforeSnap.direction;
  232. if (!ignoreWheelEvents) {
  233. lastEventBeforeSnap = undefined;
  234. if (swiper.params.loop) {
  235. swiper.loopFix();
  236. }
  237. let position = swiper.getTranslate() + delta * params.sensitivity;
  238. const wasBeginning = swiper.isBeginning;
  239. const wasEnd = swiper.isEnd;
  240. if (position >= swiper.minTranslate()) position = swiper.minTranslate();
  241. if (position <= swiper.maxTranslate()) position = swiper.maxTranslate();
  242. swiper.setTransition(0);
  243. swiper.setTranslate(position);
  244. swiper.updateProgress();
  245. swiper.updateActiveIndex();
  246. swiper.updateSlidesClasses();
  247. if (!wasBeginning && swiper.isBeginning || !wasEnd && swiper.isEnd) {
  248. swiper.updateSlidesClasses();
  249. }
  250. if (swiper.params.freeMode.sticky) {
  251. // When wheel scrolling starts with sticky (aka snap) enabled, then detect
  252. // the end of a momentum scroll by storing recent (N=15?) wheel events.
  253. // 1. do all N events have decreasing or same (absolute value) delta?
  254. // 2. did all N events arrive in the last M (M=500?) msecs?
  255. // 3. does the earliest event have an (absolute value) delta that's
  256. // at least P (P=1?) larger than the most recent event's delta?
  257. // 4. does the latest event have a delta that's smaller than Q (Q=6?) pixels?
  258. // If 1-4 are "yes" then we're near the end of a momentum scroll deceleration.
  259. // Snap immediately and ignore remaining wheel events in this scroll.
  260. // See comment above for "remaining wheel events in this scroll" determination.
  261. // If 1-4 aren't satisfied, then wait to snap until 500ms after the last event.
  262. clearTimeout(timeout);
  263. timeout = undefined;
  264. if (recentWheelEvents.length >= 15) {
  265. recentWheelEvents.shift(); // only store the last N events
  266. }
  267. const prevEvent = recentWheelEvents.length ? recentWheelEvents[recentWheelEvents.length - 1] : undefined;
  268. const firstEvent = recentWheelEvents[0];
  269. recentWheelEvents.push(newEvent);
  270. if (prevEvent && (newEvent.delta > prevEvent.delta || newEvent.direction !== prevEvent.direction)) {
  271. // Increasing or reverse-sign delta means the user started scrolling again. Clear the wheel event log.
  272. recentWheelEvents.splice(0);
  273. } else if (recentWheelEvents.length >= 15 && newEvent.time - firstEvent.time < 500 && firstEvent.delta - newEvent.delta >= 1 && newEvent.delta <= 6) {
  274. // We're at the end of the deceleration of a momentum scroll, so there's no need
  275. // to wait for more events. Snap ASAP on the next tick.
  276. // Also, because there's some remaining momentum we'll bias the snap in the
  277. // direction of the ongoing scroll because it's better UX for the scroll to snap
  278. // in the same direction as the scroll instead of reversing to snap. Therefore,
  279. // if it's already scrolled more than 20% in the current direction, keep going.
  280. const snapToThreshold = delta > 0 ? 0.8 : 0.2;
  281. lastEventBeforeSnap = newEvent;
  282. recentWheelEvents.splice(0);
  283. timeout = nextTick(() => {
  284. swiper.slideToClosest(swiper.params.speed, true, undefined, snapToThreshold);
  285. }, 0); // no delay; move on next tick
  286. }
  287. if (!timeout) {
  288. // if we get here, then we haven't detected the end of a momentum scroll, so
  289. // we'll consider a scroll "complete" when there haven't been any wheel events
  290. // for 500ms.
  291. timeout = nextTick(() => {
  292. const snapToThreshold = 0.5;
  293. lastEventBeforeSnap = newEvent;
  294. recentWheelEvents.splice(0);
  295. swiper.slideToClosest(swiper.params.speed, true, undefined, snapToThreshold);
  296. }, 500);
  297. }
  298. } // Emit event
  299. if (!ignoreWheelEvents) emit('scroll', e); // Stop autoplay
  300. if (swiper.params.autoplay && swiper.params.autoplayDisableOnInteraction) swiper.autoplay.stop(); // Return page scroll on edge positions
  301. if (position === swiper.minTranslate() || position === swiper.maxTranslate()) return true;
  302. }
  303. }
  304. if (e.preventDefault) e.preventDefault();else e.returnValue = false;
  305. return false;
  306. }
  307. function events(method) {
  308. let target = swiper.$el;
  309. if (swiper.params.mousewheel.eventsTarget !== 'container') {
  310. target = $(swiper.params.mousewheel.eventsTarget);
  311. }
  312. target[method]('mouseenter', handleMouseEnter);
  313. target[method]('mouseleave', handleMouseLeave);
  314. target[method]('wheel', handle);
  315. }
  316. function enable() {
  317. if (swiper.params.cssMode) {
  318. swiper.wrapperEl.removeEventListener('wheel', handle);
  319. return true;
  320. }
  321. if (swiper.mousewheel.enabled) return false;
  322. events('on');
  323. swiper.mousewheel.enabled = true;
  324. return true;
  325. }
  326. function disable() {
  327. if (swiper.params.cssMode) {
  328. swiper.wrapperEl.addEventListener(event, handle);
  329. return true;
  330. }
  331. if (!swiper.mousewheel.enabled) return false;
  332. events('off');
  333. swiper.mousewheel.enabled = false;
  334. return true;
  335. }
  336. on('init', () => {
  337. if (!swiper.params.mousewheel.enabled && swiper.params.cssMode) {
  338. disable();
  339. }
  340. if (swiper.params.mousewheel.enabled) enable();
  341. });
  342. on('destroy', () => {
  343. if (swiper.params.cssMode) {
  344. enable();
  345. }
  346. if (swiper.mousewheel.enabled) disable();
  347. });
  348. Object.assign(swiper.mousewheel, {
  349. enable,
  350. disable
  351. });
  352. }