import { CHAPTER_KIND_LABEL, REFERENCE_ARROW, RELATION_LABELS } from "#configuration/strings/reference.strings"; import type { ChapterReference, ReferenceRecord, ReferenceRelation } from "#types/reference.types"; import { FIELD_SEPARATOR, LABEL_SEPARATOR, LIST_SEPARATOR, PRINCIPLE_LABEL, } from "#configuration/strings/ontology.strings"; 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 { PIXEL } from "#configuration/constants/element.constants"; import { createLink } from "#presentation/components/link.component"; import { hrefOf } from "#domain/converters/ontology.converter"; const NO_CLASS = ""; const HYPHEN = "-"; const SPACE = " "; 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); panel.style.left = String(left) + PIXEL; panel.style.top = 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), ); panel.style.left = String(left) + PIXEL; panel.style.top = 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 labelledRow = function labelledRow(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 humanised = function humanised(id: string): string { const words = id.split(HYPHEN).join(SPACE); return words.charAt(0).toUpperCase() + words.slice(1); }; const relationRow = function relationRow(relation: ReferenceRelation): HTMLElement { return labelledRow(RELATION_LABELS.get(relation.relation) ?? humanised(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 })]; }; 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), ]; }; export const createChapterPanel = function createChapterPanel(chapter: ChapterReference): readonly HTMLElement[] { const principle = chapter.principle === null ? [] : [labelledRow(PRINCIPLE_LABEL, [chapter.principle])]; return [ titleRow(chapter.title, null), metaRow(CHAPTER_KIND_LABEL, null), ...summaryRow(chapter.intro), ...principle, ...chapter.relations.map(relationRow), ]; };