# presentation/components/step.component.ts

> 143 lines of code and 26 definitions.

Tree: Site tree
Language: typescript
Layer: product
Canonical: https://banes-lab.com/anatomy/tree#file-presentation-components-step-component-ts
Source text: https://banes-lab.com/assets/sources/source.c13e4a68413b5fbbfd006de969b503f09a8466986a14427da5c9d5101135cf04.generated.txt

## Definitions

- `show` (method_definition, line 76, exported)
- `stepRow` (method_definition, line 139, exported)
- `sharedPanel` (lexical_declaration, line 23)
- `hide` (method_definition, line 103, exported)
- `stepLink` (method_definition, line 119, exported)
- `existing` (lexical_declaration, line 24)
- `WalkInspector` (class_declaration, line 34, exported)
- `panel` (public_field_definition, line 35, exported)
- `polygons` (public_field_definition, line 36, exported)
- `cells` (public_field_definition, line 37, exported)
- `status` (public_field_definition, line 38, exported)
- `current` (public_field_definition, line 39, exported)
- `timer` (public_field_definition, line 40, exported)
- `disarm` (public_field_definition, line 41, exported)
- `constructor` (method_definition, line 43, exported)
- `indexAt` (method_definition, line 54, exported)
- `ref` (lexical_declaration, line 56, exported)
- `schedule` (method_definition, line 60, exported)
- `cancel` (method_definition, line 65, exported)
- `cell` (lexical_declaration, line 77, exported)
- `polygon` (lexical_declaration, line 78, exported)
- `step` (lexical_declaration, line 88, exported)
- `isOpen` (method_definition, line 111, exported)
- `holds` (method_definition, line 115, exported)
- `link` (lexical_declaration, line 120, exported)
- `edges` (lexical_declaration, line 140, exported)

## Uses

- [core/factories/element.factory.ts](https://banes-lab.com/source/tree/core/factories/element.factory.ts.md)
- [core/registries/style.registry.ts](https://banes-lab.com/source/tree/core/registries/style.registry.ts.md)
- [presentation/components/reference.component.ts](https://banes-lab.com/source/tree/presentation/components/reference.component.ts.md)
- [presentation/components/walk.component.ts](https://banes-lab.com/source/tree/presentation/components/walk.component.ts.md)

## Source

```typescript
import { LABEL_SEPARATOR, LIST_SEPARATOR } from "#configuration/strings/ontology.strings";
import {
    MILLISECONDS,
    REFERENCE_CHAIN_MS,
    REFERENCE_CHAIN_PROPERTY,
    REFERENCE_CLASS,
    REFERENCE_EDGES_CLASS,
    REFERENCE_LABEL_CLASS,
    REFERENCE_RELATION_CLASS,
} from "#configuration/constants/reference.constants";
import { NEXT_STEP_LABEL, OF_STEPS_LABEL, PREVIOUS_STEP_LABEL, STEP_LABEL } from "#configuration/strings/walk.strings";
import { WALK_CELL_SELECTOR, WALK_CURRENT_CLASS, WALK_REF_ATTRIBUTE } from "#configuration/constants/anatomy.constants";
import { armLink, clampPanel, createRecordPanel, placePanel } from "#presentation/components/reference.component";
import { cellRecord, cellText, loadSnippet } from "#presentation/components/walk.component";
import { clearChildren, createElement } from "#core/factories/element.factory";
import { ANCHOR_PREFIX } from "#configuration/constants/document.constants";
import type { Disposer } from "#types/base.types";
import { OPEN_CLASS } from "#configuration/constants/element.constants";
import { WALK_PANEL_ID } from "#core/ids/anatomy.ids";
import type { WalkCellView } from "#types/anatomy.types";
import { declareStyle } from "#core/registries/style.registry";

const sharedPanel = function sharedPanel(): HTMLElement {
    const existing = document.getElementById(WALK_PANEL_ID);
    if (existing !== null) {
        return existing;
    }
    const panel = createElement("div", { attributes: { id: WALK_PANEL_ID }, className: REFERENCE_CLASS });
    declareStyle(panel, REFERENCE_CHAIN_PROPERTY, String(REFERENCE_CHAIN_MS) + MILLISECONDS);
    document.body.append(panel);
    return panel;
};

export class WalkInspector {
    private readonly panel = sharedPanel();
    private readonly polygons: ReadonlyMap<string, Element>;
    private readonly cells: readonly WalkCellView[];
    private readonly status: HTMLElement;
    private current = -1;
    private timer: ReturnType<typeof setTimeout> | null = null;
    private disarm: Disposer | null = null;

    public constructor(element: HTMLElement, status: HTMLElement, cells: readonly WalkCellView[]) {
        this.cells = cells;
        this.status = status;
        this.polygons = new Map(
            [...element.querySelectorAll(WALK_CELL_SELECTOR)].map((polygon) => [
                polygon.getAttribute(WALK_REF_ATTRIBUTE) ?? "",
                polygon,
            ]),
        );
    }

    public indexAt(target: EventTarget | null): number {
        const polygon = target instanceof Element ? target.closest(WALK_CELL_SELECTOR) : null;
        const ref = polygon?.getAttribute(WALK_REF_ATTRIBUTE) ?? null;
        return ref === null ? -1 : this.cells.findIndex((cell) => cell.ref === ref);
    }

    public schedule(action: () => void, delay: number): void {
        this.cancel();
        this.timer = setTimeout(action, delay);
    }

    public cancel(): void {
        if (this.timer !== null) {
            clearTimeout(this.timer);
            this.timer = null;
        }
        if (this.disarm !== null) {
            this.disarm();
            this.disarm = null;
        }
    }

    public show(index: number, replace: boolean): void {
        const cell = this.cells[index];
        const polygon = this.polygons.get(cell?.ref ?? "");
        if (cell === undefined || polygon === undefined) {
            return;
        }
        this.cancel();
        this.polygons.get(this.cells[this.current]?.ref ?? "")?.classList.remove(WALK_CURRENT_CLASS);
        this.current = index;
        polygon.classList.add(WALK_CURRENT_CLASS);
        this.status.textContent = cellText(cell);
        clearChildren(this.panel);
        const step = this.stepRow(index);
        this.panel.append(...createRecordPanel(cellRecord(cell)), step);
        this.panel.classList.add(OPEN_CLASS);
        if (replace) {
            placePanel(this.panel, polygon);
        }
        clampPanel(this.panel);
        void loadSnippet(cell).then((snippet) => {
            if (snippet !== null && this.current === index) {
                step.before(snippet);
                clampPanel(this.panel);
            }
        });
    }

    public hide(): void {
        this.cancel();
        this.polygons.get(this.cells[this.current]?.ref ?? "")?.classList.remove(WALK_CURRENT_CLASS);
        this.current = -1;
        this.panel.classList.remove(OPEN_CLASS);
        clearChildren(this.panel);
    }

    public isOpen(): boolean {
        return this.current !== -1;
    }

    public holds(node: EventTarget | null): boolean {
        return node instanceof Node && this.panel.contains(node);
    }

    private stepLink(label: string, index: number): HTMLAnchorElement {
        const link = createElement("a", { attributes: { href: ANCHOR_PREFIX + WALK_PANEL_ID }, text: label });
        link.addEventListener("click", (event) => {
            event.preventDefault();
            event.stopPropagation();
            this.show(index, false);
        });
        link.addEventListener("mouseenter", () => {
            this.cancel();
            this.disarm = armLink(link);
            this.timer = setTimeout(() => {
                this.show(index, false);
            }, REFERENCE_CHAIN_MS);
        });
        link.addEventListener("mouseleave", () => {
            this.cancel();
        });
        return link;
    }

    private stepRow(index: number): HTMLElement {
        const edges: (HTMLElement | string)[] = [String(index + 1) + OF_STEPS_LABEL + String(this.cells.length)];
        if (index > 0) {
            edges.push(LIST_SEPARATOR, this.stepLink(PREVIOUS_STEP_LABEL, index - 1));
        }
        if (index < this.cells.length - 1) {
            edges.push(LIST_SEPARATOR, this.stepLink(NEXT_STEP_LABEL, index + 1));
        }
        return createElement("div", {
            children: [
                createElement("span", { className: REFERENCE_LABEL_CLASS, text: STEP_LABEL + LABEL_SEPARATOR }),
                createElement("span", { children: edges, className: REFERENCE_EDGES_CLASS }),
            ],
            className: REFERENCE_RELATION_CLASS,
        });
    }
}
```
