presentation/renderers/walk.renderer.ts

presentation/renderers/walk.renderer.ts is a file in Bane's Lab Site. 120 lines of code and 15 definitions.

import { REFERENCE_HIDE_MS, REFERENCE_SHOW_MS } from "#configuration/constants/reference.constants";
import { STEPS_LABEL, VIEW_FULL_WALK_TITLE, WALK_MISSING } from "#configuration/strings/walk.strings";
import { createElement, createVectorElement } from "#core/factories/element.factory";
import { createStatus, runGhost } from "#presentation/components/walk.component";
import { DIAGRAM_FILLED } from "#core/ids/diagram.ids";
import { DRAG_TOLERANCE_PX } from "#configuration/constants/anatomy.constants";
import { ESCAPE_KEY } from "#configuration/constants/overlay.constants";
import type { PanelHandle } from "#types/panel.types";
import type { WalkBlock } from "#types/block.types";
import type { WalkCellView } from "#types/anatomy.types";
import { WalkInspector } from "#presentation/components/step.component";
import { createExpandButton } from "#presentation/components/overlay.component";
import { createPanel } from "#presentation/components/panel.component";
import { isStaticRender } from "#core/predicates/document.predicate";
import { loadWalk } from "#core/loaders/walk.loader";

const bindPointer = function bindPointer(element: HTMLElement, inspector: WalkInspector): void {
    let pressed: { readonly x: number; readonly y: number } | null = null;
    element.addEventListener("pointerdown", (event) => {
        pressed = { x: event.clientX, y: event.clientY };
    });
    element.addEventListener("pointerup", (event) => {
        const start = pressed;
        pressed = null;
        if (start === null || Math.hypot(event.clientX - start.x, event.clientY - start.y) > DRAG_TOLERANCE_PX) {
            return;
        }
        const index = inspector.indexAt(document.elementFromPoint(event.clientX, event.clientY));
        if (index !== -1) {
            inspector.show(index, true);
        }
    });
};

const bindHover = function bindHover(element: HTMLElement, inspector: WalkInspector): void {
    element.addEventListener("mouseover", (event) => {
        const index = inspector.indexAt(event.target);
        if (index !== -1) {
            inspector.schedule(() => {
                inspector.show(index, true);
            }, REFERENCE_SHOW_MS);
        }
    });
    element.addEventListener("mouseout", (event) => {
        if (inspector.indexAt(event.target) === -1 || inspector.holds(event.relatedTarget)) {
            return;
        }
        inspector.schedule(() => {
            inspector.hide();
        }, REFERENCE_HIDE_MS);
    });
    document.addEventListener("mouseover", (event) => {
        if (inspector.isOpen() && inspector.holds(event.target)) {
            inspector.cancel();
        }
    });
    document.addEventListener("mouseout", (event) => {
        const leaving = inspector.isOpen() && inspector.holds(event.target) && !inspector.holds(event.relatedTarget);
        if (leaving && inspector.indexAt(event.relatedTarget) === -1) {
            inspector.schedule(() => {
                inspector.hide();
            }, REFERENCE_HIDE_MS);
        }
    });
    document.addEventListener("keydown", (event) => {
        if (event.key === ESCAPE_KEY) {
            inspector.hide();
        }
    });
};

export const enableInspector = function enableInspector(
    element: HTMLElement,
    status: HTMLElement,
    cells: readonly WalkCellView[],
): void {
    const inspector = new WalkInspector(element, status, cells);
    void runGhost(element);
    bindHover(element, inspector);
    bindPointer(element, inspector);
};

const createCanvas = function createCanvas(
    block: WalkBlock,
    className: string,
    status: HTMLElement | null,
): HTMLElement {
    const holder = createElement("div");
    const panel: PanelHandle = createPanel(holder, className, "fill");
    if (isStaticRender()) {
        return panel.element;
    }
    void loadWalk(block.walk).then((walk) => {
        if (walk === null) {
            holder.replaceChildren(createElement("p", { className: "diagram-missing", text: WALK_MISSING }));
        } else {
            holder.replaceChildren(createVectorElement(walk.markup));
            if (status !== null) {
                enableInspector(panel.element, status, walk.cells);
            }
        }
        panel.reset();
        panel.element.dispatchEvent(new Event(DIAGRAM_FILLED));
    });
    return panel.element;
};

export const renderWalk = function renderWalk(block: WalkBlock): Element {
    const status = createStatus();
    const expand = createExpandButton("diagram-expand", VIEW_FULL_WALK_TITLE, block.caption, () =>
        createCanvas(block, "diagram-full", null),
    );
    const header = createElement("figcaption", {
        children: [
            createElement("span", { className: "diagram-caption", text: block.caption }),
            createElement("span", { className: "walk-steps", text: String(block.walk.steps) + STEPS_LABEL }),
            expand,
        ],
        className: "diagram-header",
    });
    return createElement("figure", {
        children: [header, createCanvas(block, "diagram-canvas", status), status],
        className: "diagram-figure walk-figure",
    });
};