presentation/components/surface.component.ts
presentation/components/surface.component.ts is a file in Bane's Lab Site. 104 lines of code and 21 definitions.
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 { HALF, PIXEL } from "#configuration/constants/element.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 { SPACE } from "#configuration/constants/document.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 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);
}
}
}