presentation/components/step.component.ts
presentation/components/step.component.ts is a file in Bane's Lab Site. 143 lines of code and 26 definitions.
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,
});
}
}