presentation/components/diagram.component.ts

presentation/components/diagram.component.ts is a file in Bane's Lab Site. 68 lines of code and 15 definitions.

import {
    DIAGRAM_WALKED_CLASS,
    DIAGRAM_WALKING_CLASS,
    DIAGRAM_WALK_ATTRIBUTE,
    DIAGRAM_WALK_PAUSE_MS,
    DIAGRAM_WALK_SEPARATOR,
    DIAGRAM_WALK_STEP_MS,
    REDUCED_MOTION_QUERY,
} from "#configuration/constants/diagram.constants";
import type { Disposer } from "#types/base.types";

const ID_SELECTOR = "[id]";

const prefersStill = function prefersStill(): boolean {
    return typeof globalThis.matchMedia === "function" && globalThis.matchMedia(REDUCED_MOTION_QUERY).matches;
};

const stepsIn = function stepsIn(canvas: Element): readonly Element[] {
    const order = canvas.querySelector(`[${DIAGRAM_WALK_ATTRIBUTE}]`)?.getAttribute(DIAGRAM_WALK_ATTRIBUTE) ?? "";
    const byId = new Map([...canvas.querySelectorAll(ID_SELECTOR)].map((element) => [element.id, element]));
    return order.split(DIAGRAM_WALK_SEPARATOR).flatMap((id) => {
        const element = byId.get(id);
        return element === undefined ? [] : [element];
    });
};

export const attachDiagramWalk = function attachDiagramWalk(canvas: Element): Disposer {
    const steps = stepsIn(canvas);
    if (steps.length === 0 || prefersStill()) {
        return () => {};
    }
    canvas.classList.add(DIAGRAM_WALKING_CLASS);
    let head = 0;
    let visible = false;
    let timer: ReturnType<typeof setTimeout> | null = null;
    const clear = function clear(): void {
        for (const step of steps) {
            step.classList.remove(DIAGRAM_WALKED_CLASS);
        }
        head = 0;
    };
    const tick = function tick(): void {
        timer = null;
        if (!visible) {
            return;
        }
        const step = steps[head];
        if (step === undefined) {
            clear();
            timer = setTimeout(tick, DIAGRAM_WALK_STEP_MS);
            return;
        }
        step.classList.add(DIAGRAM_WALKED_CLASS);
        head += 1;
        timer = setTimeout(tick, head >= steps.length ? DIAGRAM_WALK_PAUSE_MS : DIAGRAM_WALK_STEP_MS);
    };
    const observer = new IntersectionObserver((entries) => {
        visible = entries.some((entry) => entry.isIntersecting);
        if (visible && timer === null) {
            tick();
        }
    });
    observer.observe(canvas);
    return () => {
        observer.disconnect();
        if (timer !== null) {
            clearTimeout(timer);
        }
        clear();
        canvas.classList.remove(DIAGRAM_WALKING_CLASS);
    };
};