Files
svelte-carousel/src/components/Carousel/Carousel.svelte
2021-12-19 09:44:20 +03:00

372 lines
8.8 KiB
Svelte

<script>
import { onDestroy, onMount, tick, createEventDispatcher } from 'svelte'
import Dots from '../Dots/Dots.svelte'
import Arrow from '../Arrow/Arrow.svelte'
import Progress from '../Progress/Progress.svelte'
import { NEXT, PREV } from '../../direction'
import { swipeable } from '../../actions/swipeable'
import { hoverable } from '../../actions/hoverable'
import { tappable } from '../../actions/tappable'
import {
applyParticleSizes,
createResizeObserver,
} from '../../utils/page'
import {
getClones,
applyClones,
} from '../../utils/clones'
import { get, switcher } from '../../utils/object'
import createCarousel from './createCarousel'
// used for lazy loading images, preloaded only current, adjacent and cloanable images
let loaded = []
let currentPageIndex
let progressValue
let offset = 0
let durationMs = 0
let pagesCount = 1
const [{ data, progressManager }, methods, service] = createCarousel((key, value) => {
switcher({
'currentPageIndex': () => currentPageIndex = value,
'progressValue': () => progressValue = value,
'offset': () => offset = value,
'durationMs': () => durationMs = value,
'pagesCount': () => pagesCount = value,
'loaded': () => loaded = value,
})(key)
})
const dispatch = createEventDispatcher()
/**
* CSS animation timing function
* examples: 'linear', 'steps(5, end)', 'cubic-bezier(0.1, -0.6, 0.2, 0)'
*/
export let timingFunction = 'ease-in-out';
/**
* Enable Next/Prev arrows
*/
export let arrows = true
/**
* Infinite looping
*/
export let infinite = true
$: {
data.infinite = infinite
}
/**
* Page to start on
*/
export let initialPageIndex = 0
/**
* Transition duration (ms)
*/
export let duration = 500
$: {
data.durationMsInit = duration
}
/**
* Enables autoplay of pages
*/
export let autoplay = false
$: {
data.autoplay = autoplay
}
/**
* Autoplay change interval (ms)
*/
export let autoplayDuration = 3000
$: {
data.autoplayDuration = autoplayDuration
}
/**
* Autoplay change direction ('next', 'prev')
*/
export let autoplayDirection = NEXT
$: {
data.autoplayDirection = autoplayDirection
}
/**
* Pause autoplay on focus
*/
export let pauseOnFocus = false
$: {
data.pauseOnFocus = pauseOnFocus
}
/**
* Show autoplay duration progress indicator
*/
export let autoplayProgressVisible = false
/**
* Current page indicator dots
*/
export let dots = true
/**
* Enable swiping
*/
export let swiping = true
/**
* Number of particles to show
*/
export let particlesToShow = 1
$: {
data.particlesToShowInit = particlesToShow
}
/**
* Number of particles to scroll
*/
export let particlesToScroll = 1
$: {
data.particlesToScrollInit = particlesToScroll
}
export async function goTo(pageIndex, options) {
const animated = get(options, 'animated', true)
if (typeof pageIndex !== 'number') {
throw new Error('pageIndex should be a number')
}
await methods.showPage(pageIndex, { animated })
}
export async function goToPrev(options) {
const animated = get(options, 'animated', true)
await methods.showPrevPage({ animated })
}
export async function goToNext(options) {
const animated = get(options, 'animated', true)
await methods.showNextPage({ animated })
}
let pageWindowWidth = 0
let pageWindowElement
let particlesContainer
const pageWindowElementResizeObserver = createResizeObserver(({
width,
}) => {
pageWindowWidth = width
data.particleWidth = pageWindowWidth / data.particlesToShow
applyParticleSizes({
particlesContainerChildren: particlesContainer.children,
particleWidth: data.particleWidth,
})
methods.offsetPage({ animated: false })
})
function addClones() {
const {
clonesToAppend,
clonesToPrepend,
} = getClones({
clonesCountHead: data.clonesCountHead,
clonesCountTail: data.clonesCountTail,
particlesContainerChildren: particlesContainer.children,
})
applyClones({
particlesContainer,
clonesToAppend,
clonesToPrepend,
})
}
onMount(() => {
(async () => {
await tick()
if (particlesContainer && pageWindowElement) {
data.particlesCountWithoutClones = particlesContainer.children.length
await tick()
data.infinite && addClones()
// call after adding clones
data.particlesCount = particlesContainer.children.length
methods.showPage(initialPageIndex, { animated: false })
pageWindowElementResizeObserver.observe(pageWindowElement);
}
})()
})
onDestroy(() => {
pageWindowElementResizeObserver.disconnect()
progressManager.reset()
})
async function handlePageChange(pageIndex) {
await methods.showPage(pageIndex, { animated: true })
}
// gestures
function handleSwipeStart() {
if (!swiping) return
data.durationMs = 0
}
async function handleSwipeThresholdReached(event) {
if (!swiping) return
await switcher({
[NEXT]: methods.showNextPage,
[PREV]: methods.showPrevPage
})(event.detail.direction)
}
function handleSwipeMove(event) {
if (!swiping) return
data.offset += event.detail.dx
}
function handleSwipeEnd() {
if (!swiping) return
methods.showParticle(data.currentParticleIndex)
}
async function handleSwipeFailed() {
if (!swiping) return
await methods.offsetPage({ animated: true })
}
function handleHovered(event) {
data.focused = event.detail.value
}
function handleTapped() {
methods.toggleFocused()
}
function showPrevPage() {
methods.showPrevPage()
}
</script>
<div class="sc-carousel__carousel-container">
<div class="sc-carousel__content-container">
{#if arrows}
<slot name="prev" showPrevPage={methods.showPrevPage}>
<div class="sc-carousel__arrow-container">
<Arrow
direction="prev"
disabled={!infinite && currentPageIndex === 0}
on:click={showPrevPage}
/>
</div>
</slot>
{/if}
<div
class="sc-carousel__pages-window"
bind:this={pageWindowElement}
use:hoverable
on:hovered={handleHovered}
use:tappable
on:tapped={handleTapped}
>
<div
class="sc-carousel__pages-container"
use:swipeable="{{ thresholdProvider: () => pageWindowWidth/3 }}"
on:swipeStart={handleSwipeStart}
on:swipeMove={handleSwipeMove}
on:swipeEnd={handleSwipeEnd}
on:swipeFailed={handleSwipeFailed}
on:swipeThresholdReached={handleSwipeThresholdReached}
style="
transform: translateX({offset}px);
transition-duration: {durationMs}ms;
transition-timing-function: {timingFunction};
"
bind:this={particlesContainer}
>
<slot {loaded}></slot>
</div>
{#if autoplayProgressVisible}
<div class="sc-carousel-progress__container">
<Progress value={progressValue} />
</div>
{/if}
</div>
{#if arrows}
<slot name="next" showNextPage={methods.showNextPage}>
<div class="sc-carousel__arrow-container">
<Arrow
direction="next"
disabled={!infinite && currentPageIndex === pagesCount - 1}
on:click={methods.showNextPage}
/>
</div>
</slot>
{/if}
</div>
{#if dots}
<slot
name="dots"
currentPageIndex={currentPageIndex}
pagesCount={pagesCount}
showPage={handlePageChange}
>
<Dots
pagesCount={pagesCount}
currentPageIndex={currentPageIndex}
on:pageChange={event => handlePageChange(event.detail)}
></Dots>
</slot>
{/if}
</div>
<style>
:root {
--sc-color-rgb-light-50p: rgba(93, 93, 93, 0.5);
--sc-color-rgb-light: #5d5d5d;
--sc-color-hex-dark-50p: rgba(30, 30, 30, 0.5);
--sc-color-hex-dark: #1e1e1e;
}
.sc-carousel__carousel-container {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
}
.sc-carousel__content-container {
position: relative;
display: flex;
width: 100%;
}
.sc-carousel__pages-window {
flex: 1;
display: flex;
overflow: hidden;
box-sizing: border-box;
position: relative;
}
.sc-carousel__pages-container {
width: 100%;
display: flex; /* to put child elements in one row */
transition-property: transform;
}
.sc-carousel__arrow-container {
padding: 5px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
.sc-carousel-progress__container {
width: 100%;
height: 5px;
background-color: var(--sc-color-rgb-light-50p);
position: absolute;
bottom: 0;
}
</style>