embla-carousel-angular
Version:
Angular wrapper for Embla Carousel
146 lines (140 loc) • 6.11 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, NgZone, ElementRef, DestroyRef, input, output, afterNextRender, effect, Directive } from '@angular/core';
import EmblaCarousel from 'embla-carousel';
import { canUseDOM, 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 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 {
constructor() {
this._globalOptions = inject(EMBLA_OPTIONS_TOKEN);
this._ngZone = inject(NgZone);
this._elementRef = inject(ElementRef);
this._destroyRef = inject(DestroyRef);
this.options = input({});
this.plugins = input([]);
this.subscribeToEvents = input([]);
this.eventsThrottleTime = input(100);
this.emblaChange = output();
this._destroy$ = new Subject();
this.storedOptions = this.options();
this.storedPlugins = this.plugins();
if (this._globalOptions) {
EmblaCarousel.globalOptions = this._globalOptions;
}
// Init Embla Carousel
afterNextRender({
write: () => {
if (!canUseDOM())
return;
this._ngZone.runOutsideAngular(() => {
this.emblaApi = 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();
});
}
scrollTo(...args) {
this._ngZone.runOutsideAngular(() => this.emblaApi?.scrollTo(...args));
}
scrollPrev(...args) {
this._ngZone.runOutsideAngular(() => this.emblaApi?.scrollPrev(...args));
}
scrollNext(...args) {
this._ngZone.runOutsideAngular(() => this.emblaApi?.scrollNext(...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 { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: EmblaCarouselDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.0.1", 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: "19.0.1", ngImport: i0, type: EmblaCarouselDirective, decorators: [{
type: Directive,
args: [{
selector: '[emblaCarousel]',
exportAs: 'emblaCarousel',
}]
}], ctorParameters: () => [] });
/*
* 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