UNPKG

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