diff --git a/README.md b/README.md index 2dc47d1..46af224 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Import component | `autoplay` | `boolean` | `false` | Enables auto play of pages | | `autoplayDuration` | `number` | `3000` | Autoplay change interval (ms) | | `autoplayDirection` | `string` | `'next'` | Autoplay change direction (`next` or `prev`) | -| `pauseOnFocus` | `boolean` | `false` | Pause autoplay on focus | +| `pauseOnFocus` | `boolean` | `false` | Pauses autoplay on focus (for desktop - hover on the carousel to toggle the autoplay, for touchable devices - tap the carousel to toggle the autoplay) | | `autoplayProgressVisible` | `boolean` | `false` | Show autoplay duration progress indicator | | `dots` | `boolean` | `true` | Current page indicator dots | | `timingFunction` | `string` | `'ease-in-out'` | CSS animation timing function | diff --git a/src/actions/focusable/event.js b/src/actions/focusable/event.js index a52ddaf..7bd13f4 100644 --- a/src/actions/focusable/event.js +++ b/src/actions/focusable/event.js @@ -1,21 +1,15 @@ // focusin event export function addFocusinEventListener(source, cb) { source.addEventListener('mouseenter', cb) - source.addEventListener('touchstart', cb) } export function removeFocusinEventListener(source, cb) { source.removeEventListener('mouseenter', cb) - source.removeEventListener('touchstart', cb) } // focusout event export function addFocusoutEventListener(source, cb) { source.addEventListener('mouseleave', cb) - source.addEventListener('touchend', cb) - source.addEventListener('touchcancel', cb) } export function removeFocusoutEventListener(source, cb) { source.removeEventListener('mouseleave', cb) - source.removeEventListener('touchend', cb) - source.removeEventListener('touchcancel', cb) } diff --git a/src/actions/focusable/focusable.js b/src/actions/focusable/focusable.js index 3e13deb..e0f2107 100644 --- a/src/actions/focusable/focusable.js +++ b/src/actions/focusable/focusable.js @@ -1,25 +1,31 @@ import { createDispatcher } from '../../utils/event' +import { get } from '../../utils/object' import { addFocusinEventListener, removeFocusinEventListener, addFocusoutEventListener, - removeFocusoutEventListener, + removeFocusoutEventListener } from './event' -export function focusable(node) { - const dispatch = createDispatcher(node) +/** + * focusable events are for mouse events only + */ +export function focusable(node, options) { + // pass custom dispatch fn in order to re-translate dispatched event + const dispatch = get(options, 'dispatch', createDispatcher(node)) function handleFocusin() { + addFocusoutEventListener(node, handleFocusout) dispatch('focused', { value: true }) } function handleFocusout() { dispatch('focused', { value: false }) + removeFocusoutEventListener(node, handleFocusout) } addFocusinEventListener(node, handleFocusin) - addFocusoutEventListener(node, handleFocusout) - + return { destroy() { removeFocusinEventListener(node, handleFocusin) diff --git a/src/actions/pausable/index.js b/src/actions/pausable/index.js new file mode 100644 index 0000000..f721bcc --- /dev/null +++ b/src/actions/pausable/index.js @@ -0,0 +1 @@ +export * from './pausable' diff --git a/src/actions/pausable/pausable.js b/src/actions/pausable/pausable.js new file mode 100644 index 0000000..fdfd5d2 --- /dev/null +++ b/src/actions/pausable/pausable.js @@ -0,0 +1,30 @@ +import { + createDispatcher, + getIsTouchable, +} from '../../utils/event' + +import { focusable } from '../focusable' +import { tappable } from '../tappable' + +export function pausable(node) { + const dispatch = createDispatcher(node) + + if (getIsTouchable()) { + return tappable(node, { + dispatch: (_, payload) => { + dispatch('pausedToggle', { + isTouchable: true, + ...payload + }) + } + }) + } + return focusable(node, { + dispatch: (_, payload) => { + dispatch('pausedToggle', { + isTouchable: false, + ...payload + }) + } + }) +} diff --git a/src/actions/swipeable/swipeable.js b/src/actions/swipeable/swipeable.js index 1f70f2e..d4f8bf8 100644 --- a/src/actions/swipeable/swipeable.js +++ b/src/actions/swipeable/swipeable.js @@ -8,6 +8,7 @@ import { removeEndEventListener, } from './event' import { createDispatcher } from '../../utils/event' +import { SWIPE_MIN_DURATION_MS, SWIPE_MIN_DISTANCE_PX } from '../../units' function getCoords(event) { if ('TouchEvent' in window && event instanceof TouchEvent) { @@ -28,49 +29,64 @@ export function swipeable(node, { thresholdProvider }) { let x let y let moved = 0 + let swipeStartedAt + let isTouching = false - function handleMousedown(event) { + function isValidSwipe() { + const swipeDurationMs = Date.now() - swipeStartedAt + return swipeDurationMs >= SWIPE_MIN_DURATION_MS && Math.abs(moved) >= SWIPE_MIN_DISTANCE_PX + } + + function handleDown(event) { + swipeStartedAt = Date.now() moved = 0 + isTouching = true const coords = getCoords(event) x = coords.x y = coords.y - dispatch('start', { x, y }) - addMoveEventListener(window, handleMousemove) - addEndEventListener(window, handleMouseup) + dispatch('swipeStart', { x, y }) + addMoveEventListener(window, handleMove) + addEndEventListener(window, handleUp) } - function handleMousemove(event) { + function handleMove(event) { + if (!isTouching) return const coords = getCoords(event) const dx = coords.x - x const dy = coords.y - y x = coords.x y = coords.y - dispatch('move', { x, y, dx, dy }) + dispatch('swipeMove', { x, y, dx, dy }) if (dx !== 0 && Math.sign(dx) !== Math.sign(moved)) { moved = 0 } moved += dx if (Math.abs(moved) > thresholdProvider()) { - dispatch('threshold', { direction: moved > 0 ? PREV : NEXT }) - removeEndEventListener(window, handleMouseup) - removeMoveEventListener(window, handleMousemove) + dispatch('swipeThresholdReached', { direction: moved > 0 ? PREV : NEXT }) + removeEndEventListener(window, handleUp) + removeMoveEventListener(window, handleMove) } } - function handleMouseup(event) { + function handleUp(event) { + removeEndEventListener(window, handleUp) + removeMoveEventListener(window, handleMove) + + isTouching = false + + if (!isValidSwipe()) { + dispatch('swipeFailed') + return + } const coords = getCoords(event) - x = coords.x - y = coords.y - dispatch('end', { x, y }) - removeEndEventListener(window, handleMouseup) - removeMoveEventListener(window, handleMousemove) + dispatch('swipeEnd', { x: coords.x, y: coords.y }) } - addStartEventListener(node, handleMousedown) + addStartEventListener(node, handleDown) return { destroy() { - removeStartEventListener(node, handleMousedown) + removeStartEventListener(node, handleDown) }, } } diff --git a/src/actions/tappable/event.js b/src/actions/tappable/event.js new file mode 100644 index 0000000..855e11d --- /dev/null +++ b/src/actions/tappable/event.js @@ -0,0 +1,15 @@ +// tap start event +export function addFocusinEventListener(source, cb) { + source.addEventListener('touchstart', cb) +} +export function removeFocusinEventListener(source, cb) { + source.removeEventListener('touchstart', cb) +} + +// tap end event +export function addFocusoutEventListener(source, cb) { + source.addEventListener('touchend', cb) +} +export function removeFocusoutEventListener(source, cb) { + source.removeEventListener('touchend', cb) +} diff --git a/src/actions/tappable/index.js b/src/actions/tappable/index.js new file mode 100644 index 0000000..807b7a4 --- /dev/null +++ b/src/actions/tappable/index.js @@ -0,0 +1 @@ +export * from './tappable' diff --git a/src/actions/tappable/tappable.js b/src/actions/tappable/tappable.js new file mode 100644 index 0000000..dc983f8 --- /dev/null +++ b/src/actions/tappable/tappable.js @@ -0,0 +1,66 @@ +import { createDispatcher } from '../../utils/event' +import { get } from '../../utils/object' +import { getDistance } from '../../utils/math' +import { + addFocusinEventListener, + removeFocusinEventListener, + addFocusoutEventListener, + removeFocusoutEventListener, +} from './event' +import { + TAP_DURATION_MS, + TAP_MOVEMENT_PX, +} from '../../units' + +/** + * tappable events are for touchable devices only + */ +export function tappable(node, options) { + // pass custom dispatch fn in order to re-translate dispatched event + const dispatch = get(options, 'dispatch', createDispatcher(node)) + + let tapStartedAt = 0 + let tapStartPos = { x: 0, y: 0 } + + function getIsValidTap({ + tapEndedAt, + tapEndedPos + }) { + const tapTime = tapEndedAt - tapStartedAt + const tapDist = getDistance(tapStartPos, tapEndedPos) + return ( + tapTime <= TAP_DURATION_MS && + tapDist <= TAP_MOVEMENT_PX + ) + } + + function handleTapstart(event) { + tapStartedAt = Date.now() + + const touch = event.touches[0] + tapStartPos = { x: touch.clientX, y: touch.clientY } + + addFocusoutEventListener(node, handleTapend) + } + + function handleTapend(event) { + removeFocusoutEventListener(node, handleTapend) + + const touch = event.changedTouches[0] + if (getIsValidTap({ + tapEndedAt: Date.now(), + tapEndedPos: { x: touch.clientX, y: touch.clientY } + })) { + dispatch('tapped') + } + } + + addFocusinEventListener(node, handleTapstart) + + return { + destroy() { + removeFocusinEventListener(node, handleTapstart) + removeFocusoutEventListener(node, handleTapend) + }, + } +} diff --git a/src/components/Carousel/Carousel.svelte b/src/components/Carousel/Carousel.svelte index 43c35af..459bcb6 100644 --- a/src/components/Carousel/Carousel.svelte +++ b/src/components/Carousel/Carousel.svelte @@ -6,7 +6,7 @@ import Progress from '../Progress/Progress.svelte' import { NEXT, PREV } from '../../direction' import { swipeable } from '../../actions/swipeable' - import { focusable } from '../../actions/focusable' + import { pausable } from '../../actions/pausable' import { addResizeEventListener, removeResizeEventListener @@ -168,7 +168,7 @@ children[pageIndex].style.maxWidth = `${pageWidth}px` } - offsetPage(false) + offsetPage({ animated: false }) } function addClones() { @@ -190,7 +190,9 @@ return } - autoplay && await autoplayDirectionFnDescription[autoplayDirection]() + if (autoplay) { + await autoplayDirectionFnDescription[autoplayDirection]() + } } let cleanupFns = [] @@ -225,7 +227,8 @@ await showPage(pageIndex + Number(infinite)) } - function offsetPage(animated) { + function offsetPage(options) { + const animated = get(options, 'animated', true) return new Promise((resolve) => { // _duration is an offset animation time _duration = animated ? duration : 0 @@ -254,11 +257,12 @@ // Disable page change while animation is in progress let disabled = false async function changePage(updateStoreFn, options) { + progressManager.reset() if (disabled) return disabled = true updateStoreFn() - await offsetPage(get(options, 'animated', true)) + await offsetPage({ animated: get(options, 'animated', true) }) disabled = false const jumped = await jumpIfNeeded() @@ -289,7 +293,7 @@ if (!swiping) return _duration = 0 } - async function handleThreshold(event) { + async function handleSwipeThresholdReached(event) { if (!swiping) return await directionFnDescription[event.detail.direction]() } @@ -301,7 +305,16 @@ if (!swiping) return showPage(currentPageIndex) } - function handleFocused(event) { + async function handleSwipeFailed() { + if (!swiping) return + await offsetPage({ animated: true }) + } + + function handlePausedToggle(event) { + if (event.detail.isTouchable) { + focused = !focused + return + } focused = event.detail.value } @@ -322,16 +335,18 @@