embla-carousel-angular
Version:
Angular wrapper for Embla Carousel
177 lines (171 loc) • 7.62 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, NgZone, ElementRef, DestroyRef, input, output, signal, afterNextRender, effect, Directive } from '@angular/core';
import EmblaCarousel from 'embla-carousel';
import { areOptionsEqual, arePluginsEqual } from 'embla-carousel-reactive-utils';
import { map, scan, filter, Subject } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
const EMBLA_OPTIONS_TOKEN = new InjectionToken('embla global options', {
factory: () => undefined,
});
function canUseDOM() {
return typeof window !== 'undefined' && typeof document !== 'undefined';
}
function provideEmblaGlobalOptions(options) {
return [
{
provide: EMBLA_OPTIONS_TOKEN,
useValue: options,
},
];
}
// Solution from https://stackoverflow.com/a/53627167
function throttleDistinct(duration, equals = (a, b) => a === b) {
return (source) => {
return source
.pipe(map(x => {
return {
value: x,
time: Date.now(),
keep: true,
};
}), scan((acc, curr) => {
const diff = curr.time - acc.time;
const isSame = equals(curr.value, acc.value);
return diff > duration || (diff < duration && !isSame)
? { ...curr, keep: true }
: { ...acc, keep: false };
}), filter(x => x.keep), map(x => x.value));
};
}
class EmblaCarouselDirective {
_globalOptions = inject(EMBLA_OPTIONS_TOKEN);
_ngZone = inject(NgZone);
_elementRef = inject(ElementRef);
_destroyRef = inject(DestroyRef);
options = input({}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
plugins = input([], /* @ts-ignore */
...(ngDevMode ? [{ debugName: "plugins" }] : /* istanbul ignore next */ []));
subscribeToEvents = input([], /* @ts-ignore */
...(ngDevMode ? [{ debugName: "subscribeToEvents" }] : /* istanbul ignore next */ []));
eventsThrottleTime = input(100, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "eventsThrottleTime" }] : /* istanbul ignore next */ []));
emblaChange = output();
storedOptions = this.options();
storedPlugins = this.plugins();
_emblaApiSignal = signal(null, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "_emblaApiSignal" }] : /* istanbul ignore next */ []));
emblaApiSignal = this._emblaApiSignal.asReadonly();
get emblaApi() {
return this.emblaApiSignal();
}
constructor() {
if (this._globalOptions) {
EmblaCarousel.globalOptions = this._globalOptions;
}
// Init Embla Carousel
afterNextRender({
write: () => {
if (!canUseDOM())
return;
this._ngZone.runOutsideAngular(() => {
this._emblaApiSignal.set(EmblaCarousel(this._elementRef.nativeElement, this.storedOptions, this.storedPlugins));
this.listenEvents();
});
},
});
// Watch input changes
effect(() => {
const plugins = this.plugins();
const options = this.options();
let shouldReInit = false;
if (options && !areOptionsEqual(this.storedOptions, options)) {
this.storedOptions = options;
shouldReInit = true;
}
if (plugins && !arePluginsEqual(this.storedPlugins, plugins)) {
this.storedPlugins = plugins;
shouldReInit = true;
}
if (shouldReInit) {
this.reInit();
}
});
// Cleanup Embla Carousel
this._destroyRef.onDestroy(() => {
this.emblaApi?.destroy();
this._emblaApiSignal.set(null);
});
}
goTo(...args) {
this._ngZone.runOutsideAngular(() => this.emblaApi?.scrollTo(...args));
}
goToPrev(...args) {
this._ngZone.runOutsideAngular(() => this.emblaApi?.scrollPrev(...args));
}
goToNext(...args) {
this._ngZone.runOutsideAngular(() => this.emblaApi?.scrollNext(...args));
}
/**
* @deprecated Use `goTo` instead
*/
scrollTo(...args) {
this.goTo(...args);
}
/**
* @deprecated Use `goToPrev` instead
*/
scrollPrev(...args) {
this.goToPrev(...args);
}
/**
* @deprecated Use `goToNext` instead
*/
scrollNext(...args) {
this.goToNext(...args);
}
reInit() {
if (!this.emblaApi) {
return;
}
this._ngZone.runOutsideAngular(() => {
this.emblaApi?.reInit(this.storedOptions, this.storedPlugins);
});
}
/**
* `eventsThrottler$` Subject was made just because `scroll` event fires too often.
*/
listenEvents() {
if (this.subscribeToEvents().length === 0) {
return;
}
const eventsThrottler$ = new Subject();
eventsThrottler$
.pipe(throttleDistinct(this.eventsThrottleTime()), takeUntilDestroyed(this._destroyRef))
.subscribe(eventName => {
this._ngZone.run(() => this.emblaChange.emit(eventName));
});
this._ngZone.runOutsideAngular(() => {
this.subscribeToEvents().forEach(eventName => {
this.emblaApi.on(eventName, () => eventsThrottler$.next(eventName));
});
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: EmblaCarouselDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.0", type: EmblaCarouselDirective, isStandalone: true, selector: "[emblaCarousel]", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, plugins: { classPropertyName: "plugins", publicName: "plugins", isSignal: true, isRequired: false, transformFunction: null }, subscribeToEvents: { classPropertyName: "subscribeToEvents", publicName: "subscribeToEvents", isSignal: true, isRequired: false, transformFunction: null }, eventsThrottleTime: { classPropertyName: "eventsThrottleTime", publicName: "eventsThrottleTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { emblaChange: "emblaChange" }, exportAs: ["emblaCarousel"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: EmblaCarouselDirective, decorators: [{
type: Directive,
args: [{
selector: '[emblaCarousel]',
exportAs: 'emblaCarousel',
}]
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], plugins: [{ type: i0.Input, args: [{ isSignal: true, alias: "plugins", required: false }] }], subscribeToEvents: [{ type: i0.Input, args: [{ isSignal: true, alias: "subscribeToEvents", required: false }] }], eventsThrottleTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventsThrottleTime", required: false }] }], emblaChange: [{ type: i0.Output, args: ["emblaChange"] }] } });
/*
* Public API Surface of embla-carousel-angular
*/
/**
* Generated bundle index. Do not edit.
*/
export { EMBLA_OPTIONS_TOKEN, EmblaCarouselDirective, provideEmblaGlobalOptions };
//# sourceMappingURL=embla-carousel-angular.mjs.map