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();
};
};