Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]: ClassNames - Add possibility to pass an array of strings and add a loop option #1061

Merged
merged 1 commit into from
Nov 17, 2024
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
86 changes: 67 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,96 @@ 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)

Object.keys(classNames).forEach((classNameKey) => {
const key = <keyof ClassNamesListType>classNameKey
classNames[key] = []
})
}

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 = {
snapped: string[]
inView: string[]
draggable: string[]
dragging: string[]
loop: string[]
}

export type OptionsType = CreateOptionsType<{
snapped: string
inView: string
draggable: string
dragging: string
[Key in keyof ClassNamesListType]: 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.

---
Loading