diff --git a/src/components/Carousel/Carousel.svelte b/src/components/Carousel/Carousel.svelte index 21d0393..32b25dc 100644 --- a/src/components/Carousel/Carousel.svelte +++ b/src/components/Carousel/Carousel.svelte @@ -13,30 +13,19 @@ } from '../../utils/event' import { getAdjacentIndexes } from '../../utils/page' import { get } from '../../utils/object' - import { ProgressManager } from '../../utils/ProgressManager.js' + import { ProgressManager } from '../../utils/ProgressManager' + import { wait } from '../../utils/interval' const dispatch = createEventDispatcher() const autoplayDirectionFnDescription = { - [NEXT]: () => { - progressManager.start(() => { - showNextPage() - }) - }, - [PREV]: () => { - progressManager.start(() => { - showPrevPage() - }) - } + [NEXT]: async () => await progressManager.start(showNextPage), + [PREV]: async () => await progressManager.start(showPrevPage) } const directionFnDescription = { - [NEXT]: () => { - showNextPage() - }, - [PREV]: () => { - showPrevPage() - } + [NEXT]: showNextPage, + [PREV]: showPrevPage } /** @@ -70,6 +59,9 @@ * Enables autoplay of pages */ export let autoplay = false + $: { + applyAutoplayIfNeeded(autoplay) + } /** * Autoplay change interval (ms) @@ -96,31 +88,42 @@ */ export let dots = true - export function goTo(pageIndex, options) { + export async function goTo(pageIndex, options) { const animated = get(options, 'animated', true) if (typeof pageIndex !== 'number') { throw new Error('pageIndex should be a number') } - showPage(pageIndex + Number(infinite), { animated }) + await showPage(pageIndex + Number(infinite), { animated }) } - export function goToPrev(options) { + export async function goToPrev(options) { const animated = get(options, 'animated', true) - showPrevPage({ animated }) + await showPrevPage({ animated }) } - export function goToNext(options) { + export async function goToNext(options) { const animated = get(options, 'animated', true) - showNextPage({ animated }) + await showNextPage({ animated }) } let store = createStore() let currentPageIndex = 0 - $: originalCurrentPageIndex = currentPageIndex - Number(infinite); + $: originalCurrentPageIndex = getOriginalCurrentPageIndex(currentPageIndex, pagesCount, infinite) // index without cloenes $: dispatch('pageChange', originalCurrentPageIndex) let pagesCount = 0 $: originalPagesCount = Math.max(pagesCount - (infinite ? 2 : 0), 1) // without clones + + function getOriginalCurrentPageIndex(currentPageIndex, pagesCount, infinite) { + if (infinite) { + const CLONES_COUNT = 2 + if (currentPageIndex === pagesCount - 1) return 0 + if (currentPageIndex === 0) return (pagesCount - CLONES_COUNT) - 1 + return currentPageIndex - 1 + } + return currentPageIndex + } + let pageWidth = 0 let offset = 0 let pageWindowElement @@ -169,7 +172,7 @@ pagesElement.append(first.cloneNode(true)) } - function applyAutoplayIfNeeded(options) { + async function applyAutoplayIfNeeded(autoplay) { // prevent progress change if not infinite for first and last page if ( !infinite && ( @@ -180,16 +183,8 @@ progressManager.reset() return } - if (autoplay) { - const delayMs = get(options, 'delayMs', 0) - if (delayMs) { - setTimeout(() => { - autoplayDirectionFnDescription[autoplayDirection]() - }, delayMs) - } else { - autoplayDirectionFnDescription[autoplayDirection]() - } - } + + autoplay && await autoplayDirectionFnDescription[autoplayDirection]() } let cleanupFns = [] @@ -211,8 +206,6 @@ applyPageSizes() } - applyAutoplayIfNeeded() - addResizeEventListener(applyPageSizes) })() }) @@ -222,26 +215,30 @@ cleanupFns.filter(fn => fn && typeof fn === 'function').forEach(fn => fn()) }) - function handlePageChange(pageIndex) { - showPage(pageIndex + Number(infinite)) + async function handlePageChange(pageIndex) { + await showPage(pageIndex + Number(infinite)) } function offsetPage(animated) { - // _duration is an offset animation time - _duration = animated ? duration : 0 - offset = -currentPageIndex * pageWidth + return new Promise((resolve) => { + // _duration is an offset animation time + _duration = animated ? duration : 0 + offset = -currentPageIndex * pageWidth + setTimeout(() => { + resolve() + }, _duration) + }) } // makes delayed jump to 1st or last element - function jumpIfNeeded() { + async function jumpIfNeeded() { let jumped = false if (infinite) { if (currentPageIndex === 0) { - // offsetDelayMs should depend on _duration, as it wait when offset finishes - showPage(pagesCount - 2, { offsetDelayMs: _duration, animated: false }) + await showPage(pagesCount - 2, { animated: false }) jumped = true } else if (currentPageIndex === pagesCount - 1) { - showPage(1, { offsetDelayMs: _duration, animated: false }) + await showPage(1, { animated: false }) jumped = true } } @@ -250,54 +247,43 @@ // Disable page change while animation is in progress let disabled = false - function safeChangePage(cb, options) { - const animated = get(options, 'animated', true) + async function changePage(updateStoreFn, options) { if (disabled) return - cb() disabled = true - setTimeout(() => { - disabled = false - }, animated ? duration : 0) + + updateStoreFn() + await offsetPage(get(options, 'animated', true)) + disabled = false + + const jumped = await jumpIfNeeded() + !jumped && applyAutoplayIfNeeded(autoplay) // no need to wait it finishes } - function showPage(pageIndex, options) { - const animated = get(options, 'animated', true) - const offsetDelayMs = get(options, 'offsetDelayMs', 0) - safeChangePage(() => { - store.moveToPage({ pageIndex, pagesCount }) - // delayed page transition, used for infinite autoplay to jump to real page - setTimeout(() => { - offsetPage(animated) - const jumped = jumpIfNeeded() - !jumped && applyAutoplayIfNeeded({ delayMs: _duration }) // while offset animation is in progress (delayMs = _duration ms) wait for it - }, offsetDelayMs) - }, { animated }) + async function showPage(pageIndex, options) { + await changePage( + () => store.moveToPage({ pageIndex, pagesCount }), + options + ) } - function showPrevPage(options) { - const animated = get(options, 'animated', true) - safeChangePage(() => { - store.prev({ infinite, pagesCount }) - offsetPage(animated) - const jumped = jumpIfNeeded() - !jumped && applyAutoplayIfNeeded({ delayMs: _duration }) - }, { animated }) + async function showPrevPage(options) { + await changePage( + () => store.prev({ infinite, pagesCount }), + options + ) } - function showNextPage(options) { - const animated = get(options, 'animated', true) - safeChangePage(() => { - store.next({ infinite, pagesCount }) - offsetPage(animated) - const jumped = jumpIfNeeded() - !jumped && applyAutoplayIfNeeded({ delayMs: _duration }) - }, { animated }) + async function showNextPage(options) { + await changePage( + () => store.next({ infinite, pagesCount }), + options + ) } // gestures function handleSwipeStart() { _duration = 0 } - function handleThreshold(event) { - directionFnDescription[event.detail.direction]() + async function handleThreshold(event) { + await directionFnDescription[event.detail.direction]() } function handleSwipeMove(event) { offset += event.detail.dx diff --git a/src/utils/ProgressManager.js b/src/utils/ProgressManager.js index 457b333..68b127e 100644 --- a/src/utils/ProgressManager.js +++ b/src/utils/ProgressManager.js @@ -1,6 +1,8 @@ import { setIntervalImmediate } from './interval' const STEP_MS = 35 +const MAX_VALUE = 1 + export class ProgressManager { #autoplayDuration #onProgressValueChange @@ -17,25 +19,28 @@ export class ProgressManager { } start(onFinish) { - this.reset() + return new Promise((resolve) => { + this.reset() - const stepMs = Math.min(STEP_MS, this.#autoplayDuration) - let progress = -stepMs - - this.#interval = setIntervalImmediate(() => { - if (this.#paused) { - return - } - progress += stepMs - - const value = progress / this.#autoplayDuration - this.#onProgressValueChange(value) - - if (value > 1) { - this.reset() - onFinish() - } - }, stepMs) + const stepMs = Math.min(STEP_MS, this.#autoplayDuration) + let progress = -stepMs + + this.#interval = setIntervalImmediate(async () => { + if (this.#paused) { + return + } + progress += stepMs + + const value = progress / this.#autoplayDuration + this.#onProgressValueChange(value) + + if (value > MAX_VALUE) { + this.reset() + await onFinish() + resolve() + } + }, stepMs) + }) } pause() { @@ -48,5 +53,6 @@ export class ProgressManager { reset() { clearInterval(this.#interval) + this.#onProgressValueChange(MAX_VALUE) } } diff --git a/src/utils/interval.js b/src/utils/interval.js index 49a9978..7ae9127 100644 --- a/src/utils/interval.js +++ b/src/utils/interval.js @@ -2,3 +2,11 @@ export const setIntervalImmediate = (fn, ms) => { fn(); return setInterval(fn, ms); } + +export const wait = (ms) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, ms) + }) +} diff --git a/src/utils/interval.test.js b/src/utils/interval.test.js index 9aa209f..2840443 100644 --- a/src/utils/interval.test.js +++ b/src/utils/interval.test.js @@ -1,5 +1,6 @@ import { setIntervalImmediate, + wait } from './interval.js' describe('setIntervalImmediate', () => { @@ -28,3 +29,18 @@ describe('setIntervalImmediate', () => { expect(clearInterval).toHaveBeenCalledWith(interval) }) }) + +describe('wait', () => { + beforeEach(() => { + jest.useFakeTimers(); + }) + + it('wait n ms', () => { + const ms = 1000 + + wait(ms) + jest.runAllTimers() + + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), ms) + }) +})