Files
shopit-front/src/app/shared/components/main-carousel/main-carousel.component.ts

159 lines
4.3 KiB
TypeScript

import {
ChangeDetectionStrategy,
Component,
PLATFORM_ID,
WritableSignal,
computed,
effect,
inject,
input,
output,
signal,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-main-carousel',
templateUrl: './main-carousel.component.html',
styleUrl: './main-carousel.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MainCarouselComponent {
private readonly platformId = inject(PLATFORM_ID);
private readonly requestedIndex = signal(0);
private readonly paused = signal(false);
private readonly loadedIndexes = signal<ReadonlySet<number>>(new Set());
private readonly settledIndexes = signal<ReadonlySet<number>>(new Set());
private firstImageReadyEmitted = false;
readonly images = input.required<readonly string[]>();
readonly imageAlts = input<readonly string[]>([]);
readonly autoSlideInterval = input(5000);
readonly ariaLabel = input('Imágenes destacadas');
readonly firstImageReady = output<void>();
protected readonly activeIndex = computed(() => {
const imageCount = this.images().length;
return imageCount ? this.requestedIndex() % imageCount : 0;
});
protected readonly hasMultipleImages = computed(() => this.images().length > 1);
protected readonly sequentialLoadIndex = computed(() => {
const settledIndexes = this.settledIndexes();
for (let index = 0; index < this.images().length; index += 1) {
if (!settledIndexes.has(index)) {
return index;
}
}
return -1;
});
constructor() {
effect(() => {
this.images();
this.requestedIndex.set(0);
this.loadedIndexes.set(new Set());
this.settledIndexes.set(new Set());
this.firstImageReadyEmitted = false;
});
effect(() => {
const imageCount = this.images().length;
const interval = Math.max(this.autoSlideInterval(), 1000);
if (!isPlatformBrowser(this.platformId) || imageCount < 2) {
return;
}
const timer = window.setInterval(() => {
if (!this.paused()) {
this.requestedIndex.update((index) => {
const nextIndex = (index + 1) % imageCount;
return this.loadedIndexes().has(nextIndex) ? nextIndex : index;
});
}
}, interval);
return () => window.clearInterval(timer);
});
}
protected selectImage(index: number): void {
if (index >= 0 && index < this.images().length) {
this.requestedIndex.set(index);
}
}
protected setPaused(paused: boolean): void {
this.paused.set(paused);
}
protected imageSource(image: string, index: number): string | null {
const shouldLoad =
this.loadedIndexes().has(index) ||
index === this.sequentialLoadIndex() ||
index === this.activeIndex();
return shouldLoad ? image : null;
}
protected handleImageLoad(index: number): void {
this.updateIndexSet(this.loadedIndexes, index, true);
this.updateIndexSet(this.settledIndexes, index, true);
this.notifyFirstImageReady(index);
}
protected handleImageError(index: number): void {
this.updateIndexSet(this.loadedIndexes, index, false);
this.updateIndexSet(this.settledIndexes, index, true);
this.notifyFirstImageReady(index);
}
protected handleKeydown(event: KeyboardEvent): void {
if (!this.hasMultipleImages()) {
return;
}
if (event.key === 'ArrowLeft') {
event.preventDefault();
this.requestedIndex.update(
(index) => (index - 1 + this.images().length) % this.images().length,
);
}
if (event.key === 'ArrowRight') {
event.preventDefault();
this.requestedIndex.update((index) => (index + 1) % this.images().length);
}
}
protected imageAlt(index: number): string {
return this.imageAlts()[index] || `Imagen destacada ${index + 1}`;
}
private updateIndexSet(
target: WritableSignal<ReadonlySet<number>>,
index: number,
present: boolean,
): void {
target.update((current) => {
const updated = new Set(current);
present ? updated.add(index) : updated.delete(index);
return updated;
});
}
private notifyFirstImageReady(index: number): void {
if (index !== 0 || this.firstImageReadyEmitted) {
return;
}
this.firstImageReadyEmitted = true;
this.firstImageReady.emit();
}
}