diff --git a/src/core/events/onResize.js b/src/core/events/onResize.js index 5f1e80c0b..1437fcfb9 100644 --- a/src/core/events/onResize.js +++ b/src/core/events/onResize.js @@ -1,3 +1,4 @@ +let timeout; export default function onResize() { const swiper = this; @@ -37,7 +38,10 @@ export default function onResize() { } if (swiper.autoplay && swiper.autoplay.running && swiper.autoplay.paused) { - swiper.autoplay.run(); + clearTimeout(timeout); + timeout = setTimeout(() => { + swiper.autoplay.resume(); + }, 500); } // Return locks after resize swiper.allowSlidePrev = allowSlidePrev; diff --git a/src/modules/autoplay/autoplay.js b/src/modules/autoplay/autoplay.js index c20264a21..e0fd64631 100644 --- a/src/modules/autoplay/autoplay.js +++ b/src/modules/autoplay/autoplay.js @@ -1,14 +1,12 @@ /* eslint no-underscore-dangle: "off" */ /* eslint no-use-before-define: "off" */ import { getDocument } from 'ssr-window'; -import { nextTick } from '../../shared/utils.js'; - -export default function Autoplay({ swiper, extendParams, on, emit }) { - let timeout; +export default function Autoplay({ swiper, extendParams, on, emit, params }) { swiper.autoplay = { running: false, paused: false, + timeLeft: 0, }; extendParams({ @@ -19,198 +17,281 @@ export default function Autoplay({ swiper, extendParams, on, emit }) { disableOnInteraction: true, stopOnLastSlide: false, reverseDirection: false, - pauseOnMouseEnter: false, + pauseOnPointerEnter: true, }, }); + let timeout; + let raf; + let autoplayDelay = params && params.autoplay ? params.autoplay.delay : 3000; + let autoplayTimeLeft; + let autoplayStartTime = new Date().getTime; + let isTouched; + let pausedByTouch; + let touchStartTimeout; + let slideChanged; + let pausedByInteraction; - function run() { - if (!swiper.size) { - swiper.autoplay.running = false; - swiper.autoplay.paused = false; - return; - } - const $activeSlideEl = swiper.slides.eq(swiper.activeIndex); - let delay = swiper.params.autoplay.delay; - if ($activeSlideEl.attr('data-swiper-autoplay')) { - delay = $activeSlideEl.attr('data-swiper-autoplay') || swiper.params.autoplay.delay; + function onTransitionEnd(e) { + if (!swiper || swiper.destroyed || !swiper.$wrapperEl) return; + if (e.target !== swiper.$wrapperEl[0]) return; + swiper.$wrapperEl[0].removeEventListener('transitionend', onTransitionEnd); + resume(); + } + + const calcTimeLeft = () => { + if (swiper.destroyed || !swiper.autoplay.running) return; + const timeLeft = swiper.autoplay.paused + ? autoplayTimeLeft + : autoplayStartTime + autoplayDelay - new Date().getTime(); + swiper.autoplay.timeLeft = timeLeft; + emit('autoplayTimeLeft', timeLeft, timeLeft / autoplayDelay); + raf = requestAnimationFrame(() => { + calcTimeLeft(); + }); + }; + + const getSlideDelay = () => { + const currentSlideDelay = parseInt( + swiper.slides[swiper.activeIndex].getAttribute('data-swiper-autoplay'), + 10, + ); + return currentSlideDelay; + }; + + const run = (delayForce) => { + if (swiper.destroyed || !swiper.autoplay.running) return; + cancelAnimationFrame(raf); + calcTimeLeft(); + + let delay = typeof delayForce === 'undefined' ? swiper.params.autoplay.delay : delayForce; + autoplayDelay = swiper.params.autoplay.delay; + const currentSlideDelay = getSlideDelay(); + if ( + !Number.isNaN(currentSlideDelay) && + currentSlideDelay > 0 && + typeof delayForce === 'undefined' + ) { + delay = currentSlideDelay; + autoplayDelay = currentSlideDelay; } - clearTimeout(timeout); - timeout = nextTick(() => { - let autoplayResult; + autoplayTimeLeft = delay; + + const speed = swiper.params.speed; + const proceed = () => { if (swiper.params.autoplay.reverseDirection) { - if (swiper.params.loop) { - swiper.loopFix(); - autoplayResult = swiper.slidePrev(swiper.params.speed, true, true); - emit('autoplay'); - } else if (!swiper.isBeginning) { - autoplayResult = swiper.slidePrev(swiper.params.speed, true, true); + if (!swiper.isBeginning || swiper.params.loop || swiper.params.rewind) { + swiper.slidePrev(speed, true, true); emit('autoplay'); } else if (!swiper.params.autoplay.stopOnLastSlide) { - autoplayResult = swiper.slideTo( - swiper.slides.length - 1, - swiper.params.speed, - true, - true, - ); + swiper.slideTo(swiper.slides.length - 1, speed, true, true); emit('autoplay'); - } else { - stop(); } - } else if (swiper.params.loop) { - swiper.loopFix(); - autoplayResult = swiper.slideNext(swiper.params.speed, true, true); - emit('autoplay'); - } else if (!swiper.isEnd) { - autoplayResult = swiper.slideNext(swiper.params.speed, true, true); - emit('autoplay'); - } else if (!swiper.params.autoplay.stopOnLastSlide) { - autoplayResult = swiper.slideTo(0, swiper.params.speed, true, true); - emit('autoplay'); } else { - stop(); + if (!swiper.isEnd || swiper.params.loop || swiper.params.rewind) { + swiper.slideNext(speed, true, true); + emit('autoplay'); + } else if (!swiper.params.autoplay.stopOnLastSlide) { + swiper.slideTo(0, speed, true, true); + emit('autoplay'); + } } - if (swiper.params.cssMode && swiper.autoplay.running) run(); - else if (autoplayResult === false) { - run(); + if (swiper.params.cssMode) { + autoplayStartTime = new Date().getTime(); + requestAnimationFrame(() => { + run(); + }); } - }, delay); - } - function start() { - if (typeof timeout !== 'undefined') return false; - if (swiper.autoplay.running) return false; + }; + if (delay > 0) { + clearTimeout(timeout); + timeout = setTimeout(() => { + proceed(); + }, delay); + } else { + requestAnimationFrame(() => { + proceed(); + }); + } + + // eslint-disable-next-line + return delay; + }; + + const start = () => { swiper.autoplay.running = true; - emit('autoplayStart'); run(); - return true; - } - function stop() { - if (!swiper.autoplay.running) return false; - if (typeof timeout === 'undefined') return false; + emit('autoplayStart'); + }; - if (timeout) { - clearTimeout(timeout); - timeout = undefined; - } + const stop = () => { swiper.autoplay.running = false; + clearTimeout(timeout); + cancelAnimationFrame(raf); emit('autoplayStop'); - return true; - } - function pause(speed) { - if (!swiper.autoplay.running) return; - if (swiper.autoplay.paused) return; - if (timeout) clearTimeout(timeout); - swiper.autoplay.paused = true; - if (speed === 0 || !swiper.params.autoplay.waitForTransition) { - swiper.autoplay.paused = false; - run(); - } else { - ['transitionend', 'webkitTransitionEnd'].forEach((event) => { - swiper.$wrapperEl[0].addEventListener(event, onTransitionEnd); - }); + }; + const pause = (internal, reset) => { + if (swiper.destroyed || !swiper.autoplay.running) return; + clearTimeout(timeout); + if (!internal) { + pausedByInteraction = true; } - } - function onVisibilityChange() { - const document = getDocument(); - if (document.visibilityState === 'hidden' && swiper.autoplay.running) { - pause(); + + const proceed = () => { + emit('autoplayPause'); + if (swiper.params.autoplay.waitForTransition) { + swiper.$wrapperEl[0].addEventListener('transitionend', onTransitionEnd); + } else { + resume(); + } + }; + + swiper.autoplay.paused = true; + if (reset) { + if (slideChanged) { + autoplayTimeLeft = swiper.params.autoplay.delay; + } + slideChanged = false; + proceed(); + return; } - if (document.visibilityState === 'visible' && swiper.autoplay.paused) { + const delay = autoplayTimeLeft || swiper.params.autoplay.delay; + autoplayTimeLeft = delay - (new Date().getTime() - autoplayStartTime); + if (swiper.isEnd && autoplayTimeLeft < 0 && !swiper.params.loop) return; + if (autoplayTimeLeft < 0) autoplayTimeLeft = 0; + proceed(); + }; + + const resume = () => { + if ( + (swiper.isEnd && autoplayTimeLeft < 0 && !swiper.params.loop) || + swiper.destroyed || + !swiper.autoplay.running + ) + return; + autoplayStartTime = new Date().getTime(); + if (pausedByInteraction) { + pausedByInteraction = false; + run(autoplayTimeLeft); + } else { run(); - swiper.autoplay.paused = false; } - } - function onTransitionEnd(e) { - if (!swiper || swiper.destroyed || !swiper.$wrapperEl) return; - if (e.target !== swiper.$wrapperEl[0]) return; - ['transitionend', 'webkitTransitionEnd'].forEach((event) => { - swiper.$wrapperEl[0].removeEventListener(event, onTransitionEnd); - }); swiper.autoplay.paused = false; - if (!swiper.autoplay.running) { - stop(); - } else { - run(); + emit('autoplayResume'); + }; + + const onVisibilityChange = () => { + if (swiper.destroyed || !swiper.autoplay.running) return; + const document = getDocument(); + if (document.visibilityState === 'hidden') { + pausedByInteraction = true; + pause(true); } - } - function onMouseEnter() { - if (swiper.params.autoplay.disableOnInteraction) { - stop(); - } else { - emit('autoplayPause'); - pause(); + if (document.visibilityState === 'visible') { + resume(); } + }; - ['transitionend', 'webkitTransitionEnd'].forEach((event) => { - swiper.$wrapperEl[0].removeEventListener(event, onTransitionEnd); - }); - } - function onMouseLeave() { - if (swiper.params.autoplay.disableOnInteraction) { - return; + const onPointerEnter = () => { + pausedByInteraction = true; + pause(true); + }; + + const onPointerLeave = () => { + if (swiper.autoplay.paused) { + resume(); } - swiper.autoplay.paused = false; - emit('autoplayResume'); - run(); - } - function attachMouseEvents() { - if (swiper.params.autoplay.pauseOnMouseEnter) { - swiper.$el.on('mouseenter', onMouseEnter); - swiper.$el.on('mouseleave', onMouseLeave); + }; + + const attachMouseEvents = () => { + if (swiper.params.autoplay.pauseOnPointerEnter) { + swiper.$el.on('pointerenter', onPointerEnter); + swiper.$el.on('pointerleave', onPointerLeave); } - } - function detachMouseEvents() { - swiper.$el.off('mouseenter', onMouseEnter); - swiper.$el.off('mouseleave', onMouseLeave); - } + }; + + const detachMouseEvents = () => { + swiper.$el.off('pointerenter', onPointerEnter); + swiper.$el.off('pointerleave', onPointerLeave); + }; + + const attachDocumentEvents = () => { + const document = getDocument(); + document.addEventListener('visibilitychange', onVisibilityChange); + }; + + const detachDocumentEvents = () => { + const document = getDocument(); + document.removeEventListener('visibilitychange', onVisibilityChange); + }; on('init', () => { if (swiper.params.autoplay.enabled) { - start(); - const document = getDocument(); - document.addEventListener('visibilitychange', onVisibilityChange); attachMouseEvents(); + attachDocumentEvents(); + autoplayStartTime = new Date().getTime(); + start(); } }); - on('beforeTransitionStart', (_s, speed, internal) => { + + on('destroy', () => { + detachMouseEvents(); + detachDocumentEvents(); if (swiper.autoplay.running) { - if (internal || !swiper.params.autoplay.disableOnInteraction) { - swiper.autoplay.pause(speed); - } else { - stop(); - } + stop(); + } + }); + + on('beforeTransitionStart', (_s, speed, internal) => { + if (swiper.destroyed || !swiper.autoplay.running) return; + if (internal || !swiper.params.autoplay.disableOnInteraction) { + pause(true, true); + } else { + stop(); } }); + on('sliderFirstMove', () => { - if (swiper.autoplay.running) { - if (swiper.params.autoplay.disableOnInteraction) { - stop(); - } else { - pause(); - } + if (swiper.destroyed || !swiper.autoplay.running) return; + + if (swiper.params.autoplay.disableOnInteraction) { + stop(); + return; } + isTouched = true; + pausedByTouch = false; + pausedByInteraction = false; + touchStartTimeout = setTimeout(() => { + pausedByInteraction = true; + pausedByTouch = true; + pause(true); + }, 200); }); + on('touchEnd', () => { - if ( - swiper.params.cssMode && - swiper.autoplay.paused && - !swiper.params.autoplay.disableOnInteraction - ) { - run(); + if (swiper.destroyed || !swiper.autoplay.running) return; + clearTimeout(touchStartTimeout); + clearTimeout(timeout); + + if (!isTouched || swiper.params.autoplay.disableOnInteraction) { + pausedByTouch = false; + isTouched = false; + return; } + + if (pausedByTouch && swiper.params.cssMode) resume(); + pausedByTouch = false; + isTouched = false; }); - on('destroy', () => { - detachMouseEvents(); - if (swiper.autoplay.running) { - stop(); - } - const document = getDocument(); - document.removeEventListener('visibilitychange', onVisibilityChange); + + on('slideChange', () => { + if (swiper.destroyed || !swiper.autoplay.running) return; + slideChanged = true; }); Object.assign(swiper.autoplay, { - pause, - run, start, stop, + pause, + resume, }); } diff --git a/src/types/modules/autoplay.d.ts b/src/types/modules/autoplay.d.ts index 1f96c9fd9..acd84ded8 100644 --- a/src/types/modules/autoplay.d.ts +++ b/src/types/modules/autoplay.d.ts @@ -11,15 +11,20 @@ export interface AutoplayMethods { */ paused: boolean; + /** + * If autoplay is paused, it contains time left (in ms) before transition to next slide + */ + timeLeft: number; + /** * Pause autoplay */ - pause(speed?: number): void; + pause(): void; /** - * Run the autoplay logic + * Resume autoplay */ - run(): void; + resume(): void; /** * Start autoplay @@ -42,13 +47,17 @@ export interface AutoplayEvents { */ autoplayStop: (swiper: Swiper) => void; /** - * Event will be fired on autoplay pause (on mouse/pointer enter), when `pauseOnMouseEnter` enabled + * Event will be fired on autoplay pause */ autoplayPause: (swiper: Swiper) => void; /** - * Event will be fired on autoplay resume (on mouse/pointer leave), when `pauseOnMouseEnter` enabled + * Event will be fired on autoplay resume */ autoplayResume: (swiper: Swiper) => void; + /** + * Event triggers continuously while autoplay is enabled. It contains time left (in ms) before transition to next slide and percentage of that time related to autoplay delay + */ + autoplayTimeLeft: (swiper: Swiper, timeLeft: number, percentage: number) => void; /** * Event will be fired when slide changed with autoplay */ @@ -116,9 +125,9 @@ export interface AutoplayOptions { waitForTransition?: boolean; /** - * When enabled autoplay will be paused on mouse enter over Swiper container. If `disableOnInteraction` is also enabled, it will stop autoplay instead of pause + * When enabled autoplay will be paused on pointer (mouse) enter over Swiper container. * * @default false */ - pauseOnMouseEnter?: boolean; + pauseOnPointerEnter?: boolean; } diff --git a/src/types/swiper-class.d.ts b/src/types/swiper-class.d.ts index b2c20bb22..9464ffd27 100644 --- a/src/types/swiper-class.d.ts +++ b/src/types/swiper-class.d.ts @@ -103,12 +103,12 @@ interface Swiper extends SwiperClass { /** * Index number of currently active slide * - * @note Note, that in loop mode active index value will be always shifted on a number of looped/duplicated slides + * @note Note, that in loop mode active index value will be always shifted on a number of looped slides */ activeIndex: number; /** - * Index number of currently active slide considering duplicated slides in loop mode + * Index number of currently active slide considering rearranged slides in loop mode */ realIndex: number;