# core/observers/element.observer.ts

> 108 lines of code and 31 definitions.

Tree: Site tree
Language: typescript
Layer: application
Canonical: https://banes-lab.com/anatomy/tree#file-core-observers-element-observer-ts
Source text: https://banes-lab.com/assets/sources/source.22ef698e2501eccb82eedb0175defb579de58f62efb8ac08691a57d92acdaf1b.generated.txt

## Definitions

- `trackElement` (lexical_declaration, line 57)
- `animate` (lexical_declaration, line 30)
- `scan` (lexical_declaration, line 74)
- `primeReplay` (lexical_declaration, line 48)
- `replay` (lexical_declaration, line 42)
- `track` (lexical_declaration, line 109, exported)
- `trackTree` (lexical_declaration, line 113, exported)
- `onMutate` (lexical_declaration, line 89)
- `observeAnimations` (lexical_declaration, line 103, exported)
- `onIntersect` (lexical_declaration, line 80)
- `STAGGER_ATTRIBUTE` (lexical_declaration, line 5)
- `TRIGGER_ATTRIBUTE` (lexical_declaration, line 6)
- `ANIMATED_CLASS` (lexical_declaration, line 7)
- `DELAY_PROPERTY` (lexical_declaration, line 8)
- `ROOT_MARGIN` (lexical_declaration, line 9)
- `THRESHOLD` (lexical_declaration, line 10)
- `RADIX` (lexical_declaration, line 11)
- `LOAD_TRIGGER` (lexical_declaration, line 12)
- `TRIGGER_EVENTS` (lexical_declaration, line 13)
- `animatedSelector` (lexical_declaration, line 18)
- `staggerDelay` (lexical_declaration, line 20)
- `parent` (lexical_declaration, line 21)
- `step` (lexical_declaration, line 25)
- `index` (lexical_declaration, line 26)
- `delay` (lexical_declaration, line 31)
- `stagger` (lexical_declaration, line 35)
- `Tracker` (type_alias_declaration, line 55)
- `trigger` (lexical_declaration, line 61)
- `eventName` (lexical_declaration, line 62)
- `observer` (lexical_declaration, line 104, exported)
- `mutations` (lexical_declaration, line 110, exported)

## Uses

- [core/registries/style.registry.ts](https://banes-lab.com/source/tree/core/registries/style.registry.ts.md)

## Source

```typescript
import { ANIMATE_ATTRIBUTE, ANIMATE_DELAY_ATTRIBUTE } 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 RADIX = 10;
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 = observer.observe.bind(observer);
    const mutations = new MutationObserver((records) => {
        onMutate(track, records);
    });
    const trackTree = mutations.observe.bind(mutations);
    scan(track, root);
    trackTree(root, { childList: true, subtree: true });
    return () => {
        observer.disconnect();
        mutations.disconnect();
    };
};
```
