You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
7.5 KiB
266 lines
7.5 KiB
import { animate, keyframes, state, style, transition, trigger } from '@angular/animations';
|
|
import { NgStyle } from '@angular/common';
|
|
import { AfterViewInit, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren } from '@angular/core';
|
|
import { BrowserModule } from '@angular/platform-browser';
|
|
import { AngularResizeEventModule, ResizedEvent } from 'angular-resize-event';
|
|
import { Subject, debounceTime } from 'rxjs';
|
|
|
|
@Component({
|
|
selector: 'carousel',
|
|
templateUrl: './carousel.component.html',
|
|
styleUrls: ['./carousel.component.scss'],
|
|
standalone: true,
|
|
imports: [
|
|
BrowserModule,
|
|
AngularResizeEventModule
|
|
],
|
|
animations: [
|
|
trigger('direction', [
|
|
|
|
state('vertical', style({})),
|
|
state('horizontal', style({})),
|
|
|
|
state('hidden', style({
|
|
opacity: 0,
|
|
})),
|
|
state('unhidden', style({
|
|
opacity: 1,
|
|
})),
|
|
|
|
transition('* => hidden', animate(`{{directionTime}}ms`)),
|
|
transition('hidden => unhidden', animate(`{{directionTime}}ms`)),
|
|
transition('unhidden => *', animate('10ms')),
|
|
])
|
|
]
|
|
})
|
|
export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
|
|
|
|
@Output() afterInitSelf: EventEmitter<CarouselComponent<T>> = new EventEmitter<CarouselComponent<T>>();
|
|
@Input() slides?: T[];
|
|
@ContentChild(TemplateRef) template?: TemplateRef<any>;
|
|
@ViewChild('carouselWindow') window!: ElementRef;
|
|
@ViewChildren('slideElement') slideElements!: QueryList<ElementRef>;
|
|
|
|
@Input() initIndex?: number;
|
|
|
|
current: number = 0;
|
|
|
|
intersectionObserver?: IntersectionObserver;
|
|
visibleSlides?: IntersectingSlide[];
|
|
increasing: boolean = true;
|
|
vertical: boolean = true;
|
|
angularAnimating: boolean = false;
|
|
animation: string = 'vertical';
|
|
directionTime: number = 0;
|
|
@Input() numVisible: number = 1;
|
|
@Input() offset: number = 0;
|
|
containerDirectionLength: number = 0;
|
|
private debouncedOnchange: Subject<void> = new Subject<void>();
|
|
|
|
constructor(private cdr: ChangeDetectorRef) {
|
|
this.visibleSlides = [];
|
|
this.debouncedOnchange.pipe(debounceTime(500)).subscribe(() => this.realOnChange());
|
|
if (this.initIndex) {
|
|
this.current = this.initIndex;
|
|
}
|
|
}
|
|
|
|
|
|
ngOnInit(): void {
|
|
this.afterInitSelf.next(this);
|
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(entry => {
|
|
const runIntersectionHandling = () => {
|
|
const entryIndex = parseInt(entry.target.getAttribute('data-slideIndex')!);
|
|
if (!entryIndex && entryIndex != 0) {
|
|
return;
|
|
}
|
|
|
|
const entryIntersectingSlide = this.visibleSlides?.find(s => s.index == entryIndex);
|
|
if (!entryIntersectingSlide) {
|
|
return;
|
|
}
|
|
|
|
entryIntersectingSlide.currentlyIntersecting = entry.isIntersecting;
|
|
};
|
|
runIntersectionHandling();
|
|
this.onChange();
|
|
})
|
|
})
|
|
}
|
|
|
|
numExtraNext() {
|
|
return this.numVisible + this.offset - 1;
|
|
}
|
|
|
|
numExtraPrev() {
|
|
return 0 + this.offset;
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges): void {
|
|
if(changes['numVisible'] || changes['offset']) {
|
|
this.reinitializeVisibleSlides();
|
|
}
|
|
}
|
|
|
|
ngAfterViewInit(): void {
|
|
this.slideElements.changes.subscribe((comps: QueryList<ElementRef>) => {
|
|
comps.forEach((comp) => this.handleNewDomSlide(comp))
|
|
});
|
|
|
|
setTimeout(() => {
|
|
this.initialize();
|
|
this.cdr.detectChanges();
|
|
}, 0);
|
|
}
|
|
|
|
initialize() {
|
|
for (let i = 0; i <= this.numExtraNext() && i < this.slides!.length; i++) {
|
|
this.visibleSlides?.push({
|
|
index: i,
|
|
currentlyIntersecting: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
handleNewDomSlide(ref: ElementRef) {
|
|
this.intersectionObserver?.observe(ref.nativeElement);
|
|
}
|
|
|
|
setIndex(slideIndex: number) {
|
|
this.current = slideIndex;
|
|
this.reinitializeVisibleSlides();
|
|
this.onChange();
|
|
}
|
|
|
|
reinitializeVisibleSlides() {
|
|
this.visibleSlides!.length = 0;
|
|
const start = Math.max(0, this.current - this.numExtraPrev());
|
|
const end = Math.min(this.current + this.numExtraNext(), this.slides!.length - 1);
|
|
for (let i = start; i <= end; i++) {
|
|
this.visibleSlides?.push({
|
|
index: i,
|
|
currentlyIntersecting: false,
|
|
});
|
|
}
|
|
|
|
this.onChange();
|
|
}
|
|
|
|
next(): void {
|
|
this.increasing = true;
|
|
if (this.slides && this.current + 1 < this.slides?.length) {
|
|
this.current += 1;
|
|
if (this.current + this.numExtraNext() < this.slides.length) {
|
|
if (!this.visibleSlides?.find(e => e.index == this.current + this.numExtraNext())) {
|
|
this.visibleSlides?.push({
|
|
index: this.current + this.numExtraNext(),
|
|
currentlyIntersecting: false,
|
|
});
|
|
}
|
|
}
|
|
this.onChange();
|
|
}
|
|
}
|
|
|
|
prev(): void {
|
|
this.increasing = false;
|
|
if (this.current - 1 >= 0) {
|
|
this.current -= 1;
|
|
if (this.current - this.numExtraPrev() >= 0) {
|
|
if (!this.visibleSlides?.find(e => e.index == this.current - this.numExtraPrev())) {
|
|
this.visibleSlides?.push({
|
|
index: this.current - this.numExtraPrev(),
|
|
currentlyIntersecting: false,
|
|
});
|
|
}
|
|
}
|
|
this.onChange();
|
|
}
|
|
}
|
|
|
|
onChange() {
|
|
this.debouncedOnchange.next();
|
|
}
|
|
|
|
realOnChange() {
|
|
const safetyFactor = (this.numVisible == 1 ? 1 : 2);
|
|
const intersecting = this.visibleSlides?.filter(e => e.currentlyIntersecting).sort((e1, e2) => e1.index - e2.index);
|
|
if (intersecting && intersecting.length > 0) {
|
|
const lowestIntersecting = intersecting![0];
|
|
const highestIntersecting = intersecting![intersecting!.length - 1];
|
|
|
|
const min = Math.min(lowestIntersecting.index - safetyFactor, this.current - this.numExtraPrev());
|
|
const max = Math.max(highestIntersecting.index + safetyFactor, this.current + this.numExtraNext());
|
|
|
|
this.visibleSlides = this.visibleSlides?.filter(e => e.index >= min && e.index <= max);
|
|
}
|
|
}
|
|
|
|
translation() {
|
|
const step = this.containerDirectionLength / this.numVisible;
|
|
const translation = -((this.current - this.offset) * step);
|
|
if (this.vertical) {
|
|
return `0 ${translation}px`
|
|
} else {
|
|
return `${translation}px 0`
|
|
}
|
|
}
|
|
|
|
templateValue() {
|
|
const len = this.slides?.length;
|
|
return `repeat(${len}, ${this.containerDirectionLength / this.numVisible}px)`;
|
|
}
|
|
|
|
style() {
|
|
let style: any = {};
|
|
|
|
if (this.vertical) {
|
|
style['grid-template-rows'] = this.templateValue();
|
|
style['grid-auto-flow'] = 'column';
|
|
|
|
} else {
|
|
style['grid-template-columns'] = this.templateValue();
|
|
style['grid-auto-flow'] = 'row';
|
|
}
|
|
|
|
if (!this.angularAnimating) {
|
|
style.transition = '500ms';
|
|
}
|
|
|
|
return style;
|
|
}
|
|
|
|
onAnimationEnd(event: any) {
|
|
if (event.toState == 'hidden') {
|
|
this.vertical = !this.vertical;
|
|
this.animation = 'unhidden';
|
|
}
|
|
else if (event.toState == 'unhidden') {
|
|
this.animation = this.vertical ? 'vertical' : 'horizontal';
|
|
} else {
|
|
this.angularAnimating = false;
|
|
}
|
|
}
|
|
|
|
onAnimationStart(event: any) {
|
|
this.angularAnimating = true;
|
|
}
|
|
|
|
changeDirection() {
|
|
this.animation = 'hidden';
|
|
}
|
|
|
|
carouselResize(event: ResizedEvent) {
|
|
if (this.vertical) {
|
|
this.containerDirectionLength = event.newRect.height;
|
|
} else {
|
|
this.containerDirectionLength = event.newRect.width;
|
|
}
|
|
}
|
|
}
|
|
|
|
interface IntersectingSlide {
|
|
index: number;
|
|
currentlyIntersecting: boolean;
|
|
} |