presentation/components/walk.component.ts
presentation/components/walk.component.ts is a file in Bane's Lab Site. 152 lines of code and 32 definitions.
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 } 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_BREAK } from "#configuration/constants/code.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();
}
});
observer.observe(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);
}
};
};