import { DOCUMENT } from '@angular/common'; import { afterNextRender, DestroyRef, Directive, ElementRef, inject, Injector, input, } from '@angular/core'; import { isBrowserPlatform, prefersReducedMotion } from '../../core/platform/browser'; @Directive({ selector: '[appReveal]', }) export class RevealDirective { readonly appRevealThreshold = input(0.12); private readonly host = inject>(ElementRef); private readonly document = inject(DOCUMENT); private readonly injector = inject(Injector); private readonly destroyRef = inject(DestroyRef); private readonly isBrowser = isBrowserPlatform(); private readonly reducedMotion = prefersReducedMotion(); private observer: IntersectionObserver | null = null; constructor() { if (!this.isBrowser || this.reducedMotion || !this.canObserve()) { this.revealNow(); return; } afterNextRender(() => this.observe(), { injector: this.injector }); this.destroyRef.onDestroy(() => this.disconnect()); } private canObserve(): boolean { const view = this.document.defaultView; return !!view && typeof view.IntersectionObserver === 'function'; } private observe(): void { const view = this.document.defaultView; if (!view || typeof view.IntersectionObserver !== 'function') { this.revealNow(); return; } this.observer = new view.IntersectionObserver( (entries) => { if (entries.some((entry) => entry.isIntersecting)) { this.host.nativeElement.classList.remove('reveal-pending'); this.host.nativeElement.classList.add('is-revealed'); this.disconnect(); return; } this.host.nativeElement.classList.add('reveal-pending'); }, { rootMargin: '0px 0px -8% 0px', threshold: this.appRevealThreshold(), }, ); this.observer.observe(this.host.nativeElement); } private revealNow(): void { this.host.nativeElement.classList.add('is-revealed'); } private disconnect(): void { this.observer?.disconnect(); this.observer = null; } }