presentation/components/reference.component.ts

presentation/components/reference.component.ts is a file in Bane's Lab Site. 131 lines of code and 26 definitions.

import { CHAPTER_KIND_LABEL, REFERENCE_ARROW, relationLabelOf } from "#configuration/strings/reference.strings";
import type { ChapterReference, RecordCheckView, ReferenceRecord, ReferenceRelation } from "#types/reference.types";
import {
    FIELD_SEPARATOR,
    LABEL_SEPARATOR,
    LIST_SEPARATOR,
    PRINCIPLE_LABEL,
} from "#configuration/strings/ontology.strings";
import { NO_CLASS, PIXEL, TOP_PROPERTY } from "#configuration/constants/element.constants";
import {
    REFERENCE_ABOVE_CLASS,
    REFERENCE_ARROW_CLASS,
    REFERENCE_CODE_CLASS,
    REFERENCE_DIAL_CLASS,
    REFERENCE_DIAL_MARKUP,
    REFERENCE_EDGES_CLASS,
    REFERENCE_GAP_PX,
    REFERENCE_LABEL_CLASS,
    REFERENCE_META_CLASS,
    REFERENCE_PENDING_CLASS,
    REFERENCE_RELATION_CLASS,
    REFERENCE_SUMMARY_CLASS,
    REFERENCE_TITLE_CLASS,
} from "#configuration/constants/reference.constants";
import { createElement, createVectorElement } from "#core/factories/element.factory";
import type { Disposer } from "#types/base.types";
import type { EdgeRef } from "#types/link.types";
import type { ElementChild } from "#types/element.types";
import { checkRowsOf } from "#domain/converters/check.converter";
import { createLink } from "#presentation/components/link.component";
import { declareStyle } from "#core/registries/style.registry";
import { hrefOf } from "#domain/converters/ontology.converter";

const LEFT_PROPERTY = "left";

const clampedTop = function clampedTop(top: number, height: number): number {
    return Math.max(REFERENCE_GAP_PX, Math.min(top, window.innerHeight - height - REFERENCE_GAP_PX));
};

export const placePanel = function placePanel(panel: HTMLElement, anchor: Element): void {
    const rect = anchor.getBoundingClientRect();
    const width = panel.offsetWidth;
    const height = panel.offsetHeight;
    const below = rect.bottom + REFERENCE_GAP_PX + height <= window.innerHeight;
    const left = Math.max(REFERENCE_GAP_PX, Math.min(rect.left, window.innerWidth - width - REFERENCE_GAP_PX));
    const top = below ? rect.bottom + REFERENCE_GAP_PX : clampedTop(rect.top - REFERENCE_GAP_PX - height, height);
    panel.classList.toggle(REFERENCE_ABOVE_CLASS, !below);
    declareStyle(panel, LEFT_PROPERTY, String(left) + PIXEL);
    declareStyle(panel, TOP_PROPERTY, String(top) + PIXEL);
};

export const clampPanel = function clampPanel(panel: HTMLElement): void {
    const left = Math.max(
        REFERENCE_GAP_PX,
        Math.min(panel.offsetLeft, window.innerWidth - panel.offsetWidth - REFERENCE_GAP_PX),
    );
    declareStyle(panel, LEFT_PROPERTY, String(left) + PIXEL);
    declareStyle(panel, TOP_PROPERTY, String(clampedTop(panel.offsetTop, panel.offsetHeight)) + PIXEL);
};

export const armLink = function armLink(anchor: HTMLAnchorElement): Disposer {
    const dial = createElement("span", {
        children: [createVectorElement(REFERENCE_DIAL_MARKUP)],
        className: REFERENCE_DIAL_CLASS,
    });
    const arrow = createElement("span", { className: REFERENCE_ARROW_CLASS, text: REFERENCE_ARROW });
    anchor.classList.add(REFERENCE_PENDING_CLASS);
    anchor.append(dial, arrow);
    return () => {
        anchor.classList.remove(REFERENCE_PENDING_CLASS);
        dial.remove();
        arrow.remove();
    };
};

const edgeNode = function edgeNode(edge: EdgeRef): ElementChild {
    const href = edge.ref === null ? null : hrefOf(edge.ref);
    return href === null ? edge.label : createLink(href, NO_CLASS, [edge.label]);
};

const joined = function joined(edges: readonly EdgeRef[]): readonly ElementChild[] {
    return edges.flatMap((edge, at) => (at === 0 ? [edgeNode(edge)] : [LIST_SEPARATOR, edgeNode(edge)]));
};

const labeledRow = function labeledRow(label: string, value: readonly ElementChild[]): HTMLElement {
    return createElement("div", {
        children: [
            createElement("span", { className: REFERENCE_LABEL_CLASS, text: label + LABEL_SEPARATOR }),
            createElement("span", { children: value, className: REFERENCE_EDGES_CLASS }),
        ],
        className: REFERENCE_RELATION_CLASS,
    });
};

const relationRow = function relationRow(relation: ReferenceRelation): HTMLElement {
    return labeledRow(relationLabelOf(relation.relation), joined(relation.edges));
};

const titleRow = function titleRow(name: string, code: string | null): HTMLElement {
    const children: ElementChild[] = [name];
    if (code !== null) {
        children.push(FIELD_SEPARATOR, createElement("code", { className: REFERENCE_CODE_CLASS, text: code }));
    }
    return createElement("div", { children, className: REFERENCE_TITLE_CLASS });
};

const metaRow = function metaRow(kind: string, layer: EdgeRef | null): HTMLElement {
    const children: ElementChild[] = [kind];
    if (layer !== null) {
        children.push(FIELD_SEPARATOR, edgeNode(layer));
    }
    return createElement("div", { children, className: REFERENCE_META_CLASS });
};

const summaryRow = function summaryRow(text: string | null): readonly HTMLElement[] {
    return text === null ? [] : [createElement("p", { className: REFERENCE_SUMMARY_CLASS, text })];
};

const checkRows = function checkRows(check: RecordCheckView | undefined): readonly HTMLElement[] {
    return check === undefined
        ? []
        : checkRowsOf(check).map((entry) =>
              labeledRow(entry.label, entry.edges === null ? [entry.text] : joined(entry.edges)),
          );
};

export const createRecordPanel = function createRecordPanel(record: ReferenceRecord): readonly HTMLElement[] {
    return [
        titleRow(record.name, record.code),
        metaRow(record.kind, record.layer),
        ...summaryRow(record.summary),
        ...record.relations.map(relationRow),
        ...checkRows(record.check),
    ];
};

export const createChapterPanel = function createChapterPanel(chapter: ChapterReference): readonly HTMLElement[] {
    const principle = chapter.principle === null ? [] : [labeledRow(PRINCIPLE_LABEL, [chapter.principle])];
    return [
        titleRow(chapter.title, null),
        metaRow(CHAPTER_KIND_LABEL, null),
        ...summaryRow(chapter.intro),
        ...principle,
        ...chapter.relations.map(relationRow),
    ];
};