159 lines
4.3 KiB
TypeScript
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();
|
|
}
|
|
}
|