# presentation/components/walk.component.ts

> 152 lines of code and 33 definitions.

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

## Definitions

- `locationOf` (lexical_declaration, line 37)
- `advanceGhost` (lexical_declaration, line 97)
- `loadSnippet` (lexical_declaration, line 63, exported)
- `cellRecord` (lexical_declaration, line 69, exported)
- `runGhost` (lexical_declaration, line 131, exported)
- `cellText` (lexical_declaration, line 43, exported)
- `fileRef` (lexical_declaration, line 47)
- `snippetOf` (lexical_declaration, line 54)
- `stepGhost` (lexical_declaration, line 123)
- `track` (lexical_declaration, line 153, exported)
- `tick` (lexical_declaration, line 138, exported)
- `createStatus` (lexical_declaration, line 87, exported)
- `TRANSITION_END` (lexical_declaration, line 35)
- `lines` (lexical_declaration, line 55)
- `first` (lexical_declaration, line 56)
- `last` (lexical_declaration, line 57)
- `code` (lexical_declaration, line 58)
- `text` (lexical_declaration, line 65, exported)
- `name` (lexical_declaration, line 70, exported)
- `release` (lexical_declaration, line 91)
- `previous` (lexical_declaration, line 98)
- `current` (lexical_declaration, line 104)
- `ghostPace` (lexical_declaration, line 113, exported)
- `step` (lexical_declaration, line 118, exported)
- `stride` (lexical_declaration, line 119, exported)
- `next` (lexical_declaration, line 124)
- `count` (lexical_declaration, line 125)
- `polygons` (lexical_declaration, line 132, exported)
- `pace` (lexical_declaration, line 133, exported)
- `head` (lexical_declaration, line 135, exported)
- `timer` (lexical_declaration, line 136, exported)
- `visible` (lexical_declaration, line 137, exported)
- `observer` (lexical_declaration, line 147, 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)
- [domain/converters/source.converter.ts](https://banes-lab.com/source/tree/domain/converters/source.converter.ts.md)

## Used by

- [presentation/components/step.component.ts](https://banes-lab.com/source/tree/presentation/components/step.component.ts.md)
- [presentation/renderers/walk.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/walk.renderer.ts.md)

## Source

```typescript
import {
    CELL_TEXT_SEPARATOR,
    GHOST_DECAY_PROPERTY,
    GHOST_LOOP_PAUSE_MS,
    GHOST_REFERENCE_CELLS,
    GHOST_STEP_MS,
    GHOST_TAIL_SHARE,
    SNIPPET_RADIUS,
    WALK_CELL_SELECTOR,
    WALK_GHOST_CLASS,
    WALK_STATUS_CLASS,
    WALK_TRAIL_CLASS,
} from "#configuration/constants/anatomy.constants";
import { CODE_LINE_ATTRIBUTE, CODE_LINE_MARKED_CLASS, LINE_BREAK } from "#configuration/constants/syntax.constants";
import {
    DEPTH_LABEL,
    DEPTH_RELATION,
    LOCATION_RELATION,
    SEVERITY_RELATION,
    WALK_HINT,
} from "#configuration/strings/walk.strings";
import { MILLISECONDS, REFERENCE_SNIPPET_CLASS } from "#configuration/constants/reference.constants";
import { languageOf, nodeHref } from "#domain/converters/source.converter";
import { loadSource, sourceNameOf } from "#core/loaders/walk.loader";
import { CHAPTER_FACE } from "#configuration/constants/vocabulary.constants";
import type { Disposer } from "#types/base.types";
import { FACE_SEPARATOR } from "@govlab/constants";
import { LINE_MARK } from "#configuration/strings/folder.strings";
import type { ReferenceRecord } from "#types/reference.types";
import type { WalkCellView } from "#types/anatomy.types";
import { createElement } from "#core/factories/element.factory";
import { declareStyle } from "#core/registries/style.registry";
import { renderSyntax } from "#presentation/renderers/syntax.renderer";

const TRANSITION_END = "transitionend";

const locationOf = function locationOf(cell: WalkCellView): string {
    return cell.file === "" || cell.line === null
        ? DEPTH_LABEL + String(cell.depth)
        : cell.file + LINE_MARK + String(cell.line);
};

export const cellText = function cellText(cell: WalkCellView): string {
    return [locationOf(cell), cell.label, cell.text].join(CELL_TEXT_SEPARATOR);
};

const fileRef = function fileRef(cell: WalkCellView): string | null {
    if (cell.file === "") {
        return null;
    }
    return CHAPTER_FACE + FACE_SEPARATOR + nodeHref(cell.file, cell.line);
};

const snippetOf = function snippetOf(text: string, line: number, language: string): HTMLElement {
    const lines = text.split(LINE_BREAK);
    const first = Math.max(1, line - SNIPPET_RADIUS);
    const last = Math.min(lines.length, line + SNIPPET_RADIUS);
    const code = renderSyntax(lines.slice(first - 1, last).join(LINE_BREAK), language);
    code.querySelector(`[${CODE_LINE_ATTRIBUTE}="${String(line - first + 1)}"]`)?.classList.add(CODE_LINE_MARKED_CLASS);
    return createElement("pre", { children: [code], className: REFERENCE_SNIPPET_CLASS });
};

export const loadSnippet = async function loadSnippet(cell: WalkCellView): Promise<HTMLElement | null> {
    const name = cell.line === null ? null : sourceNameOf(cell.file);
    const text = name === null ? null : await loadSource(name);
    return text === null || cell.line === null ? null : snippetOf(text, cell.line, languageOf(cell.file));
};

export const cellRecord = function cellRecord(cell: WalkCellView): ReferenceRecord {
    const name = cell.name.length > 0 ? cell.name : cell.label;
    return {
        code: cell.state,
        kind: cell.label,
        layer: null,
        name,
        relations: [
            { edges: [{ label: locationOf(cell), ref: fileRef(cell) }], relation: LOCATION_RELATION },
            { edges: [{ label: String(cell.depth), ref: null }], relation: DEPTH_RELATION },
            ...(cell.severity === null
                ? []
                : [{ edges: [{ label: cell.severity, ref: null }], relation: SEVERITY_RELATION }]),
        ],
        summary: cell.text === name ? null : cell.text,
    };
};

export const createStatus = function createStatus(): HTMLElement {
    return createElement("p", { className: WALK_STATUS_CLASS, text: WALK_HINT });
};

const release = function release(event: Event): void {
    if (event.target instanceof Element) {
        event.target.classList.remove(WALK_TRAIL_CLASS);
    }
};

const advanceGhost = function advanceGhost(polygons: readonly Element[], head: number): number {
    const previous = polygons[head];
    if (previous !== undefined) {
        previous.classList.remove(WALK_GHOST_CLASS);
        previous.classList.add(WALK_TRAIL_CLASS);
    }
    const next = head + 1;
    const current = polygons[next];
    if (current === undefined) {
        return -1;
    }
    current.classList.remove(WALK_TRAIL_CLASS);
    current.classList.add(WALK_GHOST_CLASS);
    return next;
};

export const ghostPace = function ghostPace(cells: number): {
    readonly decay: number;
    readonly step: number;
    readonly stride: number;
} {
    const step = Math.max(GHOST_STEP_MS, (GHOST_STEP_MS * GHOST_REFERENCE_CELLS) / Math.max(cells, 1));
    const stride = Math.max(1, Math.round(cells / GHOST_REFERENCE_CELLS));
    return { decay: Math.round((step * Math.max(1, cells / GHOST_TAIL_SHARE)) / stride), step, stride };
};

const stepGhost = function stepGhost(polygons: readonly Element[], head: number, stride: number): number {
    let next = head;
    for (let count = 0; count < stride && next !== -1; count += 1) {
        next = advanceGhost(polygons, next);
    }
    return next;
};

export const runGhost = function runGhost(element: HTMLElement): Disposer {
    const polygons = [...element.querySelectorAll(WALK_CELL_SELECTOR)];
    const pace = ghostPace(polygons.length);
    declareStyle(element, GHOST_DECAY_PROPERTY, String(pace.decay) + MILLISECONDS);
    let head = -1;
    let timer: ReturnType<typeof setTimeout> | null = null;
    let visible = false;
    const tick = function tick(): void {
        timer = null;
        if (!visible) {
            return;
        }
        head = head === -1 ? advanceGhost(polygons, head) : stepGhost(polygons, head, pace.stride);
        timer = setTimeout(tick, head === -1 ? GHOST_LOOP_PAUSE_MS : pace.step);
    };
    element.addEventListener(TRANSITION_END, release);
    const observer = new IntersectionObserver((entries) => {
        visible = entries.some((entry) => entry.isIntersecting);
        if (visible && timer === null) {
            tick();
        }
    });
    const track = observer.observe.bind(observer);
    track(element);
    return () => {
        observer.disconnect();
        element.removeEventListener(TRANSITION_END, release);
        if (timer !== null) {
            clearTimeout(timer);
        }
        for (const polygon of polygons) {
            polygon.classList.remove(WALK_GHOST_CLASS, WALK_TRAIL_CLASS);
        }
    };
};
```
