// ============================================================================ // Stylescape | Content Revealer // ============================================================================ // Reveals content with fade-in effects after page load. // Supports data-ss-reveal attributes for declarative configuration. // ============================================================================ /** * Configuration options for ContentRevealer */ export interface ContentRevealerOptions { /** Delay before revealing (ms) */ delay?: number; /** Duration of fade animation (ms) */ duration?: number; /** Easing function */ easing?: string; /** Initial opacity */ initialOpacity?: number; /** Whether to use IntersectionObserver for scroll-triggered reveal */ onScroll?: boolean; /** Threshold for IntersectionObserver (0-1) */ threshold?: number; } /** * Reveals content with a fade-in effect. * * @example JavaScript * ```typescript * const revealer = new ContentRevealer(".hidden-content", { delay: 500 }) * ``` * * @example HTML with data-ss * ```html *
* Content to reveal *
* ``` */ export class ContentRevealer { private elements: HTMLElement[]; private options: Required; private observer: IntersectionObserver | null = null; constructor( selectorOrElements: | string | HTMLElement | HTMLElement[] | NodeListOf, options: ContentRevealerOptions = {}, ) { // Normalize input to array if (typeof selectorOrElements === "string") { this.elements = Array.from( document.querySelectorAll(selectorOrElements), ); } else if (selectorOrElements instanceof HTMLElement) { this.elements = [selectorOrElements]; } else { this.elements = Array.from(selectorOrElements); } this.options = { delay: options.delay ?? 0, duration: options.duration ?? 500, easing: options.easing ?? "ease", initialOpacity: options.initialOpacity ?? 0, onScroll: options.onScroll ?? false, threshold: options.threshold ?? 0.1, }; this.init(); } // ======================================================================== // Public Methods // ======================================================================== /** * Manually reveal all elements */ public revealAll(): void { this.elements.forEach((el) => this.reveal(el)); } /** * Reveal a specific element */ public reveal(element: HTMLElement): void { element.style.transition = `opacity ${this.options.duration}ms ${this.options.easing}`; element.style.opacity = "1"; element.classList.add("reveal--visible"); element.setAttribute("data-ss-reveal-revealed", "true"); } /** * Reset element to hidden state */ public hide(element: HTMLElement): void { element.style.opacity = String(this.options.initialOpacity); element.classList.remove("reveal--visible"); element.removeAttribute("data-ss-reveal-revealed"); } /** * Reset all elements to hidden state */ public hideAll(): void { this.elements.forEach((el) => this.hide(el)); } /** * Destroy the revealer instance */ public destroy(): void { if (this.observer) { this.observer.disconnect(); this.observer = null; } } // ======================================================================== // Private Methods // ======================================================================== private init(): void { // Set initial hidden state this.elements.forEach((el) => { el.style.opacity = String(this.options.initialOpacity); el.style.transition = `opacity ${this.options.duration}ms ${this.options.easing}`; }); if (this.options.onScroll) { this.initIntersectionObserver(); } else { this.initLoadReveal(); } } private initLoadReveal(): void { const reveal = () => { setTimeout(() => this.revealAll(), this.options.delay); }; if (document.readyState === "complete") { reveal(); } else { window.addEventListener("load", reveal); } } private initIntersectionObserver(): void { this.observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const el = entry.target as HTMLElement; setTimeout(() => this.reveal(el), this.options.delay); this.observer?.unobserve(el); } }); }, { threshold: this.options.threshold }, ); this.elements.forEach((el) => this.observer?.observe(el)); } } export default ContentRevealer;