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.
Nothing/src/app/carousel/carousel.component.ts

247 lines
7.0 KiB

import { animate, keyframes, state, style, transition, trigger } from '@angular/animations';
import { NgStyle } from '@angular/common';
import { AfterViewInit, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { Subject, debounceTime } from 'rxjs';
@Component({
selector: 'carousel',
templateUrl: './carousel.component.html',
styleUrls: ['./carousel.component.scss'],
standalone: true,
imports: [
BrowserModule
],
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 {
@Output() afterInitSelf: EventEmitter<CarouselComponent<T>> = new EventEmitter<CarouselComponent<T>>();
@Output() changedIndex: EventEmitter<number> = new EventEmitter<number>();
@Input() slides?: T[];
@ContentChild(TemplateRef) template?: TemplateRef<any>;
@ViewChild('carouselWindow') window!: ElementRef;
@ViewChildren('slideElement') slideElements!: QueryList<ElementRef>;
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;
private debouncedOnchange: Subject<void> = new Subject<void>();
constructor(private cdr: ChangeDetectorRef) {
this.visibleSlides = [];
this.debouncedOnchange.pipe(debounceTime(500)).subscribe(() => this.realOnChange());
}
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) => {
return s.index == entryIndex;
});
if (!entryIntersectingSlide) {
return;
}
entryIntersectingSlide.currentlyIntersecting = entry.isIntersecting;
if (entryIntersectingSlide.hasBeenVisible && !entry.isIntersecting) {
this.visibleSlides = this.visibleSlides?.filter(e => e.index != entryIndex);
this.intersectionObserver?.unobserve(entry.target);
}
};
runIntersectionHandling();
this.onChange();
})
})
}
numExtraNext() {
return Math.floor((this.numVisible - 1) / 2);
}
numExtraPrev() {
return Math.ceil((this.numVisible - 1) / 2);
}
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,
hasBeenVisible: false,
currentlyIntersecting: false,
})
}
}
handleNewDomSlide(ref: ElementRef) {
this.intersectionObserver?.observe(ref.nativeElement);
}
next(): void {
this.increasing = true;
if (this.slides && this.current + 1 < this.slides?.length) {
this.current += 1;
this.changedIndex.emit(this.current);
console.log("numextra", this.numExtraNext());
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(),
hasBeenVisible: false,
currentlyIntersecting: false,
});
}
}
this.onChange();
}
}
prev(): void {
this.increasing = false;
if (this.current - 1 >= 0) {
this.current -= 1;
this.changedIndex.emit(this.current);
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(),
hasBeenVisible: false,
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);
const lowestIntersecting = intersecting![0];
this.visibleSlides = this.visibleSlides?.filter(e => e.index + safetyFactor >= lowestIntersecting!.index && e.index >= this.current - this.numExtraPrev());
}
{
const intersecting = this.visibleSlides?.filter(e => e.currentlyIntersecting).sort((e1, e2) => e1.index - e2.index).reverse();
const highestIntersecting = intersecting![0]
this.visibleSlides = this.visibleSlides?.filter(e => e.index - safetyFactor <= highestIntersecting!.index && e.index <= this.current + this.numExtraNext());
}
}
translation() {
let num = (this.current - this.numExtraNext() - (this.numVisible % 2 == 0 ? 1 : 0)) * (-1 / this.numVisible) * 100;
if (this.vertical) {
return `0 ${num}%`
} else {
return `${num}% 0`
}
}
templateValue() {
const len = this.slides?.length;
return `repeat(${len}, minmax(calc(100% / ${this.numVisible}), 1fr))`;
}
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';
}
}
interface IntersectingSlide {
index: number;
hasBeenVisible: boolean;
currentlyIntersecting: boolean;
}