mousewheel.js 14 KB

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