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", }); };