Skip to content

Commit

Permalink
Implement #619, #599.
Browse files Browse the repository at this point in the history
  • Loading branch information
davidjerleke committed Nov 10, 2023
1 parent 939d798 commit 43a738a
Show file tree
Hide file tree
Showing 19 changed files with 268 additions and 53 deletions.
117 changes: 80 additions & 37 deletions packages/embla-carousel-autoplay/src/components/Autoplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ declare module 'embla-carousel/components/Plugins' {
}
}

declare module 'embla-carousel/components/EventHandler' {
interface EmblaEventListType {
autoplayPlay: 'autoplay:play'
autoplayStop: 'autoplay:stop'
}
}

export type AutoplayType = CreatePluginType<
{
play: (jump?: boolean) => void
stop: () => void
reset: () => void
isPlaying: () => boolean
},
OptionsType
>
Expand All @@ -23,84 +31,118 @@ export type AutoplayOptionsType = AutoplayType['options']
function Autoplay(userOptions: AutoplayOptionsType = {}): AutoplayType {
let options: OptionsType
let emblaApi: EmblaCarouselType
let interaction: () => void
let timer = 0
let destroyed: boolean
let playing = false
let wasPlaying = false
let jump = false
let animationFrame = 0
let timer = 0

function init(
emblaApiInstance: EmblaCarouselType,
optionsHandler: OptionsHandlerType
): void {
emblaApi = emblaApiInstance
if (emblaApi.scrollSnapList().length <= 1) return

const { mergeOptions, optionsAtMedia } = optionsHandler
const optionsBase = mergeOptions(defaultOptions, Autoplay.globalOptions)
const allOptions = mergeOptions(optionsBase, userOptions)
options = optionsAtMedia(allOptions)

jump = options.jump
interaction = options.stopOnInteraction ? destroy : stop
destroyed = false

const { eventStore, ownerDocument, ownerWindow } = emblaApi.internalEngine()
const { eventStore, ownerDocument } = emblaApi.internalEngine()
const emblaRoot = emblaApi.rootNode()
const root = (options.rootNode && options.rootNode(emblaRoot)) || emblaRoot

emblaApi.on('pointerDown', interaction)
if (!options.stopOnInteraction) emblaApi.on('pointerUp', reset)
emblaApi.on('pointerDown', clearTimer)
if (!options.stopOnInteraction) emblaApi.on('pointerUp', startTimer)

if (options.stopOnMouseEnter) {
eventStore.add(root, 'mouseenter', interaction)
if (!options.stopOnInteraction) eventStore.add(root, 'mouseleave', reset)
eventStore.add(root, 'mouseenter', clearTimer)

if (!options.stopOnInteraction) {
eventStore.add(root, 'mouseleave', startTimer)
}
}

eventStore.add(ownerDocument, 'visibilitychange', () => {
if (ownerDocument.visibilityState === 'hidden') return stop()
reset()
})
eventStore.add(ownerWindow, 'pagehide', (event: PageTransitionEvent) => {
if (event.persisted) stop()
if (ownerDocument.visibilityState === 'hidden') {
wasPlaying = playing
return clearTimer()
}

if (wasPlaying) startTimer()
})

if (options.playOnInit) play()
if (options.playOnInit) {
emblaApi.on('init', startTimer).on('reInit', startTimer)
}
}

function destroy(): void {
emblaApi.off('pointerDown', interaction)
if (!options.stopOnInteraction) emblaApi.off('pointerUp', reset)
stop()
destroyed = true
playing = false
emblaApi.off('init', startTimer).off('reInit', startTimer)
emblaApi.off('pointerDown', clearTimer)
if (!options.stopOnInteraction) emblaApi.off('pointerUp', startTimer)
clearTimer()
cancelAnimationFrame(animationFrame)
animationFrame = 0
}

function startTimer(): void {
if (destroyed) return
if (!playing) emblaApi.emit('autoplay:play')
const { ownerWindow } = emblaApi.internalEngine()
ownerWindow.clearInterval(timer)
timer = ownerWindow.setInterval(next, options.delay)
playing = true
}

function clearTimer(): void {
if (destroyed) return
if (playing) emblaApi.emit('autoplay:stop')
const { ownerWindow } = emblaApi.internalEngine()
ownerWindow.clearInterval(timer)
timer = 0
playing = false
}

function play(jumpOverride?: boolean): void {
stop()
if (typeof jumpOverride !== 'undefined') jump = jumpOverride
timer = window.setTimeout(next, options.delay)
startTimer()
}

function stop(): void {
if (!timer) return
window.clearTimeout(timer)
if (playing) clearTimer()
}

function reset(): void {
if (!timer) return
stop()
play()
if (playing) play()
}

function next(): void {
const { index } = emblaApi.internalEngine()
const lastIndex = emblaApi.scrollSnapList().length - 1
const kill = options.stopOnLastSnap && index.get() === lastIndex

if (kill) return destroy()
function isPlaying(): boolean {
return playing
}

if (emblaApi.canScrollNext()) {
emblaApi.scrollNext(jump)
} else {
emblaApi.scrollTo(0, jump)
}
play()
function next(): void {
animationFrame = requestAnimationFrame(() => {
const { index } = emblaApi.internalEngine()
const nextIndex = index.clone().add(1).get()
const lastIndex = emblaApi.scrollSnapList().length - 1
const kill = options.stopOnLastSnap && nextIndex === lastIndex

if (kill) clearTimer()

if (emblaApi.canScrollNext()) {
emblaApi.scrollNext(jump)
} else {
emblaApi.scrollTo(0, jump)
}
})
}

const self: AutoplayType = {
Expand All @@ -110,7 +152,8 @@ function Autoplay(userOptions: AutoplayOptionsType = {}): AutoplayType {
destroy,
play,
stop,
reset
reset,
isPlaying
}
return self
}
Expand Down
2 changes: 2 additions & 0 deletions packages/embla-carousel-docs/src/content/pages/api/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { TabsItem } from 'components/Tabs/TabsItem'

Embla Carousel exposes **custom events** that can be hooked on to. Listening to events allows for extending the carousel.

---

## Usage

You need an **initialized carousel** in order to **make use of events**. Events will only be fired during the lifecycle of a carousel and added event listeners will persist even when you hard reset the carousel with the [reInit](/api/methods/#reinit) method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { TabsItem } from 'components/Tabs/TabsItem'

Once a carousel is up and running, Embla Carousel exposes a set of **useful methods** which makes it very **extensible**.

---

## Usage

You need an **initialized carousel** in order to **make use of methods**. They can be accessed during the lifecycle of a carousel and won't do anything after a carousel instance has been destroyed with the [destroy](/api/methods/#destroy) method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { TabsItem } from 'components/Tabs/TabsItem'

Embla Carousel takes various **options** in order to customize how the carousel works. You can provide options in two different ways.

---

## Usage

You can customize Embla with the [constructor options](/api/options/#constructor-options) and/or [global options](/api/options/#global-options). If both are provided, they will be merged, and if any options are in conflict, the **constructor option** has precedence and will **override global options**.
Expand Down
81 changes: 81 additions & 0 deletions packages/embla-carousel-docs/src/content/pages/api/plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { TabsItem } from 'components/Tabs/TabsItem'

It's possible to **extend** Embla carousel with additional features using **plugins**. The complete list of official plugins can be found [here](/plugins/).

---

## Usage

The Embla Carousel **constructor** accepts an **array of plugins**. Each plugin has its own [options](/api/plugins/#constructor-options) and [methods](/api/plugins/#calling-methods).
Expand Down Expand Up @@ -243,3 +245,82 @@ export const EmblaCarousel = () => {

</TabsItem>
</Tabs>

### Adding event listeners

Some plugins fire their own **events**. Plugin events are structured as follows `<plugin-name>:eventname`. [Adding](/api/events/#adding-event-listeners) and [removing](/api/events/#removing-event-listeners) plugin event listeners is done the same way as native Embla events. Here's an example where an event is added to the autoplay plugin:

<Tabs groupId="library">
<TabsItem label="Vanilla" value="vanilla">

```js{10}
import EmblaCarousel from 'embla-carousel'
import Autoplay from 'embla-carousel-autoplay'
const emblaApi = EmblaCarousel(emblaNode, { loop: true }, [Autoplay()])
function logPluginEvent(emblaApi, eventName) {
console.log('Autoplay plugin stopped playing!')
}
emblaApi.on('autoplay:stop', logPluginEvent)
```

</TabsItem>
<TabsItem label="React" value="react">

```jsx{15}
import { useEffect, useCallback } from 'react'
import useEmblaCarousel from 'embla-carousel-react'
import Autoplay from 'embla-carousel-autoplay'
export function EmblaCarousel() {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [Autoplay()])
const logAutoplayStopEvent = useCallback(() => {
console.log('Autoplay plugin stopped playing!')
}, [])
useEffect(() => {
if (!emblaApi) return
emblaApi.on('autoplay:stop', logAutoplayStopEvent) // add
}, [emblaApi, logAutoplayStopEvent])
// ...
}
```

</TabsItem>
<TabsItem label="Vue" value="vue">

```html{18}
<script>
import { watchEffect, onBeforeUnmount } from 'vue'
import emblaCarouselVue from 'embla-carousel-vue'
import Autoplay from 'embla-carousel-autoplay'
export default {
setup() {
const [emblaNode, emblaApi] = emblaCarouselVue({ loop: true }, [
Autoplay()
])
function logAutoplayStopEvent() {
console.log('Autoplay plugin stopped playing!')
}
watchEffect(() => {
if (emblaApi.value) {
emblaApi.value.on('autoplay:stop', logAutoplayStopEvent) // add
}
})
// ...
}
}
</script>
```

</TabsItem>
</Tabs>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Join the <LinkContent to={`${URLS.GITHUB_DISCUSSIONS}/513`}>discussion here</Lin

</Admonition>

---

## Choose framework

<CarouselGeneratorFrameworkSettings />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ import {

Get started instantly with pre-made CodeSandboxes. Do you want to customize your carousel more? Try the [carousel generator](/examples/generator).

---

## Basic Examples

Basic carousel setups that require minimal effort to get started with.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Start by including the Embla Carousel script from a CDN with a `script` tag:
<script src="https://unpkg.com/embla-carousel/embla-carousel.umd.js"></script>
```

---

## The HTML structure

A minimal setup requires an **overflow wrapper** and a **scroll container**. Start by adding the following **HTML** structure to your carousel:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Start by installing the **npm package** and save it to your dependencies:
</TabsItem>
</Tabs>

---

## The HTML structure

A minimal setup requires an **overflow wrapper** and a **scroll container**. Start by adding the following **HTML** structure to your carousel:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Start by installing the Embla Carousel **npm package** and add it to your depend
</TabsItem>
</Tabs>

---

## The component structure

Embla Carousel provides the handy `useEmblaCarousel` hook for seamless integration with React. A minimal setup requires an **overflow wrapper** and a **scroll container**. Start by adding the following structure to your carousel:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Start by installing the Embla Carousel **npm package** and add it to your depend
</TabsItem>
</Tabs>

---

## The component structure

Embla Carousel provides the handy `emblaCarouselSvelte` action for seamless integration with Svelte. A minimal setup requires an **overflow wrapper** and a **scroll container**. Start by adding the following structure to your carousel:
Expand Down
Loading

0 comments on commit 43a738a

Please sign in to comment.