Skip to content

Commit

Permalink
Implement #1060.
Browse files Browse the repository at this point in the history
  • Loading branch information
davidjerleke committed Nov 16, 2024
1 parent cbf0e66 commit b219b68
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 42 deletions.
81 changes: 62 additions & 19 deletions packages/embla-carousel-class-names/src/components/ClassNames.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defaultOptions, OptionsType } from './Options'
import { nodeListToArray, addClass, removeClass } from './utils'
import { defaultOptions, OptionsType, ClassNamesListType } from './Options'
import { addClass, normalizeClassNames, removeClass } from './utils'
import {
CreatePluginType,
OptionsHandlerType,
Expand All @@ -22,9 +22,19 @@ function ClassNames(userOptions: ClassNamesOptionsType = {}): ClassNamesType {
let emblaApi: EmblaCarouselType
let root: HTMLElement
let slides: HTMLElement[]
let snappedIndexes: number[] = []
let inViewIndexes: number[] = []

const selectedEvents: EmblaEventType[] = ['select']
const draggingEvents: EmblaEventType[] = ['pointerDown', 'pointerUp']
const inViewEvents: EmblaEventType[] = ['slidesInView']
const classNames: ClassNamesListType = {
snapped: [],
inView: [],
draggable: [],
dragging: [],
loop: []
}

function init(
emblaApiInstance: EmblaCarouselType,
Expand All @@ -39,58 +49,91 @@ function ClassNames(userOptions: ClassNamesOptionsType = {}): ClassNamesType {

root = emblaApi.rootNode()
slides = emblaApi.slideNodes()
const isDraggable = !!emblaApi.internalEngine().options.watchDrag

if (isDraggable) {
addClass(root, options.draggable)
const { watchDrag, loop } = emblaApi.internalEngine().options
const isDraggable = !!watchDrag

if (options.loop && loop) {
classNames.loop = normalizeClassNames(options.loop)
addClass(root, classNames.loop)
}

if (options.draggable && isDraggable) {
classNames.draggable = normalizeClassNames(options.draggable)
addClass(root, classNames.draggable)
}

if (options.dragging) {
classNames.dragging = normalizeClassNames(options.dragging)
draggingEvents.forEach((evt) => emblaApi.on(evt, toggleDraggingClass))
}

if (options.snapped) {
classNames.snapped = normalizeClassNames(options.snapped)
selectedEvents.forEach((evt) => emblaApi.on(evt, toggleSnappedClasses))
toggleSnappedClasses()
}

if (options.inView) {
classNames.inView = normalizeClassNames(options.inView)
inViewEvents.forEach((evt) => emblaApi.on(evt, toggleInViewClasses))
toggleInViewClasses()
}
}

function destroy(): void {
removeClass(root, options.draggable)
draggingEvents.forEach((evt) => emblaApi.off(evt, toggleDraggingClass))
selectedEvents.forEach((evt) => emblaApi.off(evt, toggleSnappedClasses))
inViewEvents.forEach((evt) => emblaApi.off(evt, toggleInViewClasses))
slides.forEach((slide) => removeClass(slide, options.snapped))

removeClass(root, classNames.loop)
removeClass(root, classNames.draggable)
removeClass(root, classNames.dragging)
toggleSlideClasses([], snappedIndexes, classNames.snapped)
toggleSlideClasses([], inViewIndexes, classNames.inView)
}

function toggleDraggingClass(
_: EmblaCarouselType,
evt: EmblaEventType
): void {
if (evt === 'pointerDown') addClass(root, options.dragging)
else removeClass(root, options.dragging)
const toggleClass = evt === 'pointerDown' ? addClass : removeClass
toggleClass(root, classNames.dragging)
}

function toggleSlideClasses(slideIndexes: number[], className: string): void {
const container = emblaApi.containerNode()
const slideNodeList = container.querySelectorAll(`.${className}`)
const removeClassSlides = nodeListToArray(slideNodeList)
function toggleSlideClasses(
addClassIndexes: number[] = [],
removeClassIndexes: number[] = [],
classNames: string[]
): number[] {
const removeClassSlides = removeClassIndexes.map((index) => slides[index])
const addClassSlides = addClassIndexes.map((index) => slides[index])

removeClassSlides.forEach((slide) => removeClass(slide, className))
slideIndexes.forEach((index) => addClass(slides[index], className))
removeClassSlides.forEach((slide) => removeClass(slide, classNames))
addClassSlides.forEach((slide) => addClass(slide, classNames))

return addClassIndexes
}

function toggleSnappedClasses(): void {
const { slideRegistry } = emblaApi.internalEngine()
const slideIndexes = slideRegistry[emblaApi.selectedScrollSnap()]
toggleSlideClasses(slideIndexes, options.snapped)
const newSnappedIndexes = slideRegistry[emblaApi.selectedScrollSnap()]

snappedIndexes = toggleSlideClasses(
newSnappedIndexes,
snappedIndexes,
classNames.snapped
)
}

function toggleInViewClasses(): void {
const slideIndexes = emblaApi.slidesInView()
toggleSlideClasses(slideIndexes, options.inView)
const newInViewIndexes = emblaApi.slidesInView()

inViewIndexes = toggleSlideClasses(
newInViewIndexes,
inViewIndexes,
classNames.inView
)
}

const self: ClassNamesType = {
Expand Down
18 changes: 13 additions & 5 deletions packages/embla-carousel-class-names/src/components/Options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { CreateOptionsType } from 'embla-carousel'

export type ClassNameOptionType = string | string[]

export type ClassNamesListType = {
[Key in keyof Omit<OptionsType, 'active' | 'breakpoints'>]: string[]
}

export type OptionsType = CreateOptionsType<{
snapped: string
inView: string
draggable: string
dragging: string
snapped: ClassNameOptionType
inView: ClassNameOptionType
draggable: ClassNameOptionType
dragging: ClassNameOptionType
loop: ClassNameOptionType
}>

export const defaultOptions: OptionsType = {
Expand All @@ -13,5 +20,6 @@ export const defaultOptions: OptionsType = {
snapped: 'is-snapped',
inView: 'is-in-view',
draggable: 'is-draggable',
dragging: 'is-dragging'
dragging: 'is-dragging',
loop: 'is-loop'
}
21 changes: 11 additions & 10 deletions packages/embla-carousel-class-names/src/components/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
export function removeClass(node: HTMLElement, className: string): void {
if (!node || !className) return
const { classList } = node
if (classList.contains(className)) classList.remove(className)
import { ClassNameOptionType } from './Options'

export function normalizeClassNames(classNames: ClassNameOptionType): string[] {
const normalized = Array.isArray(classNames) ? classNames : [classNames]
return normalized.filter(Boolean)
}

export function addClass(node: HTMLElement, className: string): void {
if (!node || !className) return
const { classList } = node
if (!classList.contains(className)) classList.add(className)
export function removeClass(node: HTMLElement, classNames: string[]): void {
if (!node || !classNames.length) return
node.classList.remove(...classNames)
}

export function nodeListToArray(nodeList: NodeListOf<Element>): HTMLElement[] {
return <HTMLElement[]>Array.from(nodeList)
export function addClass(node: HTMLElement, classNames: string[]): void {
if (!node || !classNames.length) return
node.classList.add(...classNames)
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ Below follows an exhaustive **list of all** `Class Names` **options** and their

### snapped

Type: <BrandPrimaryText>`string`</BrandPrimaryText>
Type: <BrandPrimaryText>`string | string[]`</BrandPrimaryText>
Default: <BrandSecondaryText>`is-snapped`</BrandSecondaryText>

Choose a classname that will be applied to the snapped slides. Pass an empty string to opt-out.
Choose a class name that will be applied to the **snapped slides**. It's also possible to pass an array of class names. Pass an empty string to opt-out.

---

### inView

Type: <BrandPrimaryText>`string`</BrandPrimaryText>
Type: <BrandPrimaryText>`string | string[]`</BrandPrimaryText>
Default: <BrandSecondaryText>`is-in-view`</BrandSecondaryText>

Choose a classname that will be applied to slides in view. Pass an empty string to opt-out.
Choose a class name that will be applied to **slides in view**. It's also possible to pass an array of class names. Pass an empty string to opt-out.

<Admonition type="note">
This feature will honor the [inViewThreshold](/api/options/#inviewthreshold)
Expand All @@ -87,18 +87,27 @@ Choose a classname that will be applied to slides in view. Pass an empty string

### draggable

Type: <BrandPrimaryText>`string`</BrandPrimaryText>
Type: <BrandPrimaryText>`string | string[]`</BrandPrimaryText>
Default: <BrandSecondaryText>`is-draggable`</BrandSecondaryText>

Choose a classname that will be applied to a draggable carousel container. Pass an empty string to opt-out.
Choose a class name that will be applied to a **draggable carousel**. It's also possible to pass an array of class names. Pass an empty string to opt-out.

---

### dragging

Type: <BrandPrimaryText>`string`</BrandPrimaryText>
Type: <BrandPrimaryText>`string | string[]`</BrandPrimaryText>
Default: <BrandSecondaryText>`is-dragging`</BrandSecondaryText>

Choose a classname that will be applied to the container when dragging. Pass an empty string to opt-out.
Choose a class name that will be applied to the container **when dragging**. It's also possible to pass an array of class names. Pass an empty string to opt-out.

---

### loop

Type: <BrandPrimaryText>`string | string[]`</BrandPrimaryText>
Default: <BrandSecondaryText>`is-loop`</BrandSecondaryText>

Choose a class name that will be applied to a carousel with **loop activated**. It's also possible to pass an array of class names. Pass an empty string to opt-out.

---

0 comments on commit b219b68

Please sign in to comment.