Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
6 changes: 0 additions & 6 deletions src/actions/focusable/event.js
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 11 additions & 5 deletions src/actions/focusable/focusable.js
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/actions/pausable/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './pausable'
30 changes: 30 additions & 0 deletions src/actions/pausable/pausable.js
Original file line number Diff line number Diff line change
@@ -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
})
}
})
}
50 changes: 33 additions & 17 deletions src/actions/swipeable/swipeable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
},
}
}
15 changes: 15 additions & 0 deletions src/actions/tappable/event.js
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions src/actions/tappable/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './tappable'
66 changes: 66 additions & 0 deletions src/actions/tappable/tappable.js
Original file line number Diff line number Diff line change
@@ -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)
},
}
}
41 changes: 28 additions & 13 deletions src/components/Carousel/Carousel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,7 +168,7 @@
children[pageIndex].style.maxWidth = `${pageWidth}px`
}

offsetPage(false)
offsetPage({ animated: false })
}

function addClones() {
Expand All @@ -190,7 +190,9 @@
return
}

autoplay && await autoplayDirectionFnDescription[autoplayDirection]()
if (autoplay) {
await autoplayDirectionFnDescription[autoplayDirection]()
}
}

let cleanupFns = []
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]()
}
Expand All @@ -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
}
</script>
Expand All @@ -322,16 +335,18 @@
<div
class="sc-carousel__pages-window"
bind:this={pageWindowElement}
use:focusable
on:focused={handleFocused}

use:pausable
on:pausedToggle={handlePausedToggle}
>
<div
class="sc-carousel__pages-container"
use:swipeable="{{ thresholdProvider: () => pageWidth/3 }}"
on:start={handleSwipeStart}
on:move={handleSwipeMove}
on:end={handleSwipeEnd}
on:threshold={handleThreshold}
on:swipeStart={handleSwipeStart}
on:swipeMove={handleSwipeMove}
on:swipeEnd={handleSwipeEnd}
on:swipeFailed={handleSwipeFailed}
on:swipeThresholdReached={handleSwipeThresholdReached}
style="
transform: translateX({offset}px);
transition-duration: {_duration}ms;
Expand Down
2 changes: 1 addition & 1 deletion src/docs/Carousel.svx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,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 |
Expand Down
5 changes: 5 additions & 0 deletions src/units.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const TAP_DURATION_MS = 110
export const TAP_MOVEMENT_PX = 9 // max movement during the tap, keep it small

export const SWIPE_MIN_DURATION_MS = 111
export const SWIPE_MIN_DISTANCE_PX = 20
Loading