core/observers/element.observer.ts

core/observers/element.observer.ts is a file in Bane's Lab Site. 108 lines of code and 29 definitions.

import { ANIMATE_ATTRIBUTE, ANIMATE_DELAY_ATTRIBUTE, RADIX } from "#configuration/constants/element.constants";
import type { Disposer } from "#types/base.types";
import { declareStyle } from "#core/registries/style.registry";

const STAGGER_ATTRIBUTE = "data-animate-stagger";
const TRIGGER_ATTRIBUTE = "data-animate-trigger";
const ANIMATED_CLASS = "animated";
const DELAY_PROPERTY = "--animate-delay";
const ROOT_MARGIN = "50px 0px 0px 0px";
const THRESHOLD = 0;
const LOAD_TRIGGER = "load";
const TRIGGER_EVENTS: ReadonlyMap<string, string> = new Map([
    ["hover", "mouseenter"],
    ["click", "click"],
]);

const animatedSelector = `[${ANIMATE_ATTRIBUTE}]`;

const staggerDelay = function staggerDelay(element: Element): number | null {
    const parent = element.closest(`[${STAGGER_ATTRIBUTE}]`);
    if (parent === null) {
        return null;
    }
    const step = Number.parseInt(parent.getAttribute(STAGGER_ATTRIBUTE) ?? "", RADIX);
    const index = [...parent.querySelectorAll(animatedSelector)].indexOf(element);
    return index > 0 && Number.isFinite(step) ? index * step : null;
};

const animate = function animate(element: HTMLElement): void {
    const delay = element.getAttribute(ANIMATE_DELAY_ATTRIBUTE);
    if (delay !== null) {
        declareStyle(element, DELAY_PROPERTY, `${delay}ms`);
    }
    const stagger = staggerDelay(element);
    if (stagger !== null) {
        declareStyle(element, DELAY_PROPERTY, `${stagger}ms`);
    }
    element.classList.add(ANIMATED_CLASS);
};

const replay = function replay(element: HTMLElement): void {
    element.classList.remove(ANIMATED_CLASS);
    element.getBoundingClientRect();
    element.classList.add(ANIMATED_CLASS);
};

const primeReplay = function primeReplay(element: HTMLElement, eventName: string): void {
    element.classList.add(ANIMATED_CLASS);
    element.addEventListener(eventName, () => {
        replay(element);
    });
};

type Tracker = (element: Element) => void;

const trackElement = function trackElement(track: Tracker, element: HTMLElement): void {
    if (element.classList.contains(ANIMATED_CLASS)) {
        return;
    }
    const trigger = element.getAttribute(TRIGGER_ATTRIBUTE) ?? "";
    const eventName = TRIGGER_EVENTS.get(trigger);
    if (eventName !== undefined) {
        primeReplay(element, eventName);
        return;
    }
    if (trigger === LOAD_TRIGGER) {
        animate(element);
        return;
    }
    track(element);
};

const scan = function scan(track: Tracker, root: Element): void {
    for (const element of root.querySelectorAll<HTMLElement>(animatedSelector)) {
        trackElement(track, element);
    }
};

const onIntersect = function onIntersect(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void {
    for (const entry of entries) {
        if (entry.isIntersecting && entry.target instanceof HTMLElement) {
            animate(entry.target);
            observer.unobserve(entry.target);
        }
    }
};

const onMutate = function onMutate(track: Tracker, mutations: MutationRecord[]): void {
    for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
            if (!(node instanceof HTMLElement)) {
                continue;
            }
            if (node.hasAttribute(ANIMATE_ATTRIBUTE)) {
                trackElement(track, node);
            }
            scan(track, node);
        }
    }
};

export const observeAnimations = function observeAnimations(root: Element): Disposer {
    const observer = new IntersectionObserver(onIntersect, {
        root: null,
        rootMargin: ROOT_MARGIN,
        threshold: THRESHOLD,
    });
    const track = (target: Element): void => {
        observer.observe(target);
    };
    const mutations = new MutationObserver((records) => {
        onMutate(track, records);
    });
    scan(track, root);
    mutations.observe(root, { childList: true, subtree: true });
    return () => {
        observer.disconnect();
        mutations.disconnect();
    };
};