# presentation/components/surface.component.ts

> 105 lines of code and 23 definitions.

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

Listed in [presentation/components](https://banes-lab.com/api/source/tree/presentation/components.md), after [presentation/components/step.component.ts](https://banes-lab.com/source/tree/presentation/components/step.component.ts.md) and before [presentation/components/tab.component.ts](https://banes-lab.com/source/tree/presentation/components/tab.component.ts.md).

## Definitions

- `constructor` (method_definition, line 52, exported)
- `lineRow` (lexical_declaration, line 30)
- `focus` (method_definition, line 104, exported)
- `HALF` (lexical_declaration, line 26)
- `SPACE` (lexical_declaration, line 28)
- `createPlaybackButton` (lexical_declaration, line 34, exported)
- `label` (lexical_declaration, line 35, exported)
- `button` (lexical_declaration, line 36, exported)
- `VenuePane` (class_declaration, line 45, exported)
- `element` (public_field_definition, line 46, exported)
- `viewport` (public_field_definition, line 47, exported)
- `content` (public_field_definition, line 48, exported)
- `rows` (public_field_definition, line 49, exported)
- `offset` (public_field_definition, line 50, exported)
- `show` (method_definition, line 64, exported)
- `insert` (method_definition, line 69, exported)
- `type` (method_definition, line 83, exported)
- `remove` (method_definition, line 92, exported)
- `mark` (method_definition, line 98, exported)
- `row` (lexical_declaration, line 105, exported)
- `room` (lexical_declaration, line 109, exported)
- `centered` (lexical_declaration, line 110, exported)
- `next` (lexical_declaration, line 111, 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)

## Source

```typescript
import {
    CAMERA_MS,
    SURFACE_CAMERA_PROPERTY,
    SURFACE_CHANGED_CLASS,
    SURFACE_DOCUMENT_CLASS,
    SURFACE_LINE_CLASS,
    SURFACE_NAME_CLASS,
    SURFACE_OFFSET_PROPERTY,
    SURFACE_PANE_CLASS,
    SURFACE_TOGGLE_CLASS,
    SURFACE_TYPING_CLASS,
    SURFACE_VENUE_CLASS,
    SURFACE_VENUE_ROWS_PROPERTY,
    SURFACE_VIEWPORT_CLASS,
    VENUE_ROWS,
} from "#configuration/constants/surface.constants";
import { ICON_BASE_CLASS, PAUSE_ICON, PLAY_ICON } from "#configuration/icons/element.icons";
import { PAUSE_LABEL, PLAY_LABEL, SURFACE_PANE_LABEL } from "#configuration/strings/surface.strings";
import { MILLISECONDS } from "#configuration/constants/reference.constants";
import { PIXEL } from "#configuration/constants/element.constants";
import type { SurfaceLine } from "#types/surface.types";
import { createElement } from "#core/factories/element.factory";
import { declareStyle } from "#core/registries/style.registry";
import { indentOf } from "#core/converters/surface.converter";

const HALF = 2;

const SPACE = " ";

const lineRow = function lineRow(text: string): HTMLElement {
    return createElement("span", { className: SURFACE_LINE_CLASS, text });
};

export const createPlaybackButton = function createPlaybackButton(paused: boolean, act: () => void): HTMLButtonElement {
    const label = paused ? PLAY_LABEL : PAUSE_LABEL;
    const button = createElement("button", {
        attributes: { "aria-label": label, title: label, type: "button" },
        children: [createElement("i", { className: `${ICON_BASE_CLASS} ${paused ? PLAY_ICON : PAUSE_ICON}` })],
        className: SURFACE_TOGGLE_CLASS,
    });
    button.addEventListener("click", act);
    return button;
};

export class VenuePane {
    public readonly element: HTMLElement;
    private readonly viewport: HTMLElement;
    private readonly content: HTMLElement;
    private rows: HTMLElement[] = [];
    private offset = 0;

    public constructor(surface: string) {
        this.content = createElement("code", { className: SURFACE_DOCUMENT_CLASS });
        this.viewport = createElement("pre", { children: [this.content], className: SURFACE_VIEWPORT_CLASS });
        this.element = createElement("section", {
            attributes: { "aria-label": SURFACE_PANE_LABEL },
            children: [createElement("p", { className: SURFACE_NAME_CLASS, text: surface }), this.viewport],
            className: `${SURFACE_PANE_CLASS} ${SURFACE_VENUE_CLASS}`,
        });
        declareStyle(this.element, SURFACE_VENUE_ROWS_PROPERTY, String(VENUE_ROWS));
        declareStyle(this.element, SURFACE_CAMERA_PROPERTY, String(CAMERA_MS) + MILLISECONDS);
    }

    public show(lines: readonly string[]): void {
        this.rows = lines.map(lineRow);
        this.content.replaceChildren(...this.rows);
    }

    public insert(at: number, line: SurfaceLine): void {
        const row = lineRow(SPACE.repeat(indentOf(line.text)));
        row.classList.add(SURFACE_TYPING_CLASS);
        row.classList.toggle(SURFACE_CHANGED_CLASS, line.changed);
        const next = this.rows[at];
        if (next === undefined) {
            this.content.append(row);
        } else {
            next.before(row);
        }
        this.rows.splice(at, 0, row);
        this.focus(at);
    }

    public type(at: number, text: string, done: boolean): void {
        const row = this.rows[at];
        if (row !== undefined) {
            row.textContent = text;
            row.classList.toggle(SURFACE_TYPING_CLASS, !done);
        }
        this.focus(at);
    }

    public remove(at: number): void {
        this.rows[at]?.remove();
        this.rows.splice(at, 1);
        this.focus(Math.max(0, at - 1));
    }

    public mark(lines: readonly SurfaceLine[]): void {
        for (const [index, row] of this.rows.entries()) {
            row.classList.toggle(SURFACE_CHANGED_CLASS, lines[index]?.changed === true);
        }
    }

    public focus(at: number): void {
        const row = this.rows[at];
        if (row === undefined) {
            return;
        }
        const room = this.viewport.clientHeight;
        const centered = row.offsetTop + row.offsetHeight / HALF - room / HALF;
        const next = Math.round(Math.min(Math.max(centered, 0), Math.max(this.content.offsetHeight - room, 0)));
        if (next !== this.offset) {
            this.offset = next;
            declareStyle(this.content, SURFACE_OFFSET_PROPERTY, String(-next) + PIXEL);
        }
    }
}
```
