presentation/components/folder.component.ts

presentation/components/folder.component.ts is a file in Bane's Lab Site. 120 lines of code and 30 definitions.

import { ACTIVE_CLASS, DETAILS_TAG } from "#configuration/constants/element.constants";
import type { FileNode, FolderNode, TreeNode } from "#types/folder.types";
import { NODE_ATTRIBUTE, PATH_SEPARATOR, TREE_FOLDER_CLASS } from "#configuration/constants/anatomy.constants";
import { OPEN_FILE_TITLE, OPEN_FOLDER_TITLE, ROOT_LABEL, TREE_NAV_LABEL } from "#configuration/strings/folder.strings";
import { TREE_CHEVRON_ICON, TREE_FILE_ICON, TREE_FOLDER_ICON } from "#configuration/icons/anatomy.icons";
import { appendChildren, createElement } from "#core/factories/element.factory";
import { ANCHOR_PREFIX } from "#configuration/constants/document.constants";
import { ICON_BASE_CLASS } from "#configuration/icons/element.icons";
import type { Section } from "#types/document.types";
import { contextOf } from "#presentation/renderers/element.renderer";
import { createLink } from "#presentation/components/link.component";
import { isGenericLabel } from "#core/predicates/link.predicate";
import { subsectionId } from "#presentation/renderers/section.renderer";

const folderNode = function folderNode(section: Section, path: string, name: string): FolderNode {
    const files = section.subsections.map((sub, index) => ({
        id: subsectionId(section.id, index, sub.id),
        name: sub.title,
        section,
        sub,
    }));
    return { files, folders: [], id: section.id, name, path, section };
};

const withChildren = function withChildren(
    node: FolderNode,
    sections: ReadonlyMap<string, Section>,
    folders: Readonly<Record<string, readonly string[]>>,
): FolderNode {
    const ids = Object.hasOwn(folders, node.id) ? (folders[node.id] ?? []) : [];
    const children = ids.flatMap((id) => {
        const section = sections.get(id);
        if (section === undefined) {
            return [];
        }
        const path = section.title;
        const child = folderNode(section, path, path.slice(path.lastIndexOf(PATH_SEPARATOR) + 1));
        return [withChildren(child, sections, folders)];
    });
    return { ...node, folders: children };
};

export const treeNodesOf = function treeNodesOf(
    sections: readonly Section[],
    folders: Readonly<Record<string, readonly string[]>>,
): FolderNode | null {
    const [rootSection] = sections;
    if (rootSection === undefined) {
        return null;
    }
    const byId = new Map(sections.map((section) => [section.id, section] as const));
    return withChildren(folderNode(rootSection, "", ROOT_LABEL), byId, folders);
};

export const isFolder = function isFolder(node: TreeNode): node is FolderNode {
    return "folders" in node;
};

export const ancestorsOf = function ancestorsOf(root: FolderNode, path: string): readonly FolderNode[] {
    const crumbs: FolderNode[] = [root];
    let current = root;
    for (const segment of path.length === 0 ? [] : path.split(PATH_SEPARATOR)) {
        const next = current.folders.find((folder) => folder.name === segment);
        if (next === undefined) {
            break;
        }
        crumbs.push(next);
        current = next;
    }
    return crumbs;
};

export const flatten = function flatten(root: FolderNode): readonly TreeNode[] {
    return [root, ...root.files, ...root.folders.flatMap(flatten)];
};

export const treeLink = function treeLink(node: TreeNode, className: string): HTMLAnchorElement {
    const folder = isFolder(node);
    const link = createLink(ANCHOR_PREFIX + node.id, className, [
        createElement("i", { className: `${ICON_BASE_CLASS} ${folder ? TREE_FOLDER_ICON : TREE_FILE_ICON} tree-icon` }),
        createElement("span", { className: "tree-name", text: node.name }),
        ...(folder && isGenericLabel(node.name) ? [contextOf(node.path)] : []),
    ]);
    link.setAttribute(NODE_ATTRIBUTE, node.id);
    link.title = folder ? OPEN_FOLDER_TITLE : OPEN_FILE_TITLE;
    return link;
};

const renderFileEntry = function renderFileEntry(file: FileNode): Element {
    return createElement("li", { children: [treeLink(file, "tree-link tree-file")] });
};

const renderFolderEntry = function renderFolderEntry(node: FolderNode, open: boolean): Element {
    const list = createElement("ul", { className: "tree-list" });
    appendChildren(list, [
        ...node.folders.map((child) => renderFolderEntry(child, false)),
        ...node.files.map(renderFileEntry),
    ]);
    const summary = createElement("summary", {
        children: [
            createElement("i", { className: `${ICON_BASE_CLASS} ${TREE_CHEVRON_ICON} tree-chevron` }),
            treeLink(node, "tree-link"),
        ],
        className: "tree-summary",
    });
    const details = createElement(DETAILS_TAG, { children: [summary, list], className: TREE_FOLDER_CLASS });
    details.open = open;
    return createElement("li", { children: [details] });
};

export const renderTree = function renderTree(root: FolderNode): Element {
    const list = createElement("ul", { className: "tree-list tree-root" });
    list.append(renderFolderEntry(root, true));
    const scroll = createElement("div", {
        children: [createElement("h2", { className: "tree-title", text: TREE_NAV_LABEL }), list],
        className: "tree-scroll",
    });
    return createElement("nav", { children: [scroll], className: "filesystem-tree" });
};

export const markSelected = function markSelected(tree: Element, id: string): void {
    for (const link of tree.querySelectorAll<HTMLAnchorElement>(`[${NODE_ATTRIBUTE}]`)) {
        link.classList.toggle(ACTIVE_CLASS, link.getAttribute(NODE_ATTRIBUTE) === id);
    }
    const selected = tree.querySelector<HTMLAnchorElement>(`[${NODE_ATTRIBUTE}="${id}"]`);
    let ancestor = selected?.closest<HTMLDetailsElement>(DETAILS_TAG) ?? null;
    while (ancestor !== null) {
        ancestor.open = true;
        ancestor = ancestor.parentElement?.closest<HTMLDetailsElement>(DETAILS_TAG) ?? null;
    }
};