# presentation/components/folder.component.ts

> 107 lines of code and 30 definitions.

Tree: Site tree
Language: typescript
Layer: product
Canonical: https://banes-lab.com/anatomy/tree#file-presentation-components-folder-component-ts
Source text: https://banes-lab.com/assets/sources/source.14191d6c7f7f3903a0b0d4b666ef9e6fd88146b908ea3b10c9fd64364ed7ae03.generated.txt

## Definitions

- `treeLink` (lexical_declaration, line 65, exported)
- `isFolder` (lexical_declaration, line 43, exported)
- `markSelected` (lexical_declaration, line 108, exported)
- `renderFolderEntry` (lexical_declaration, line 80)
- `renderFileEntry` (lexical_declaration, line 76)
- `renderTree` (lexical_declaration, line 98, exported)
- `DETAILS_TAG` (lexical_declaration, line 13)
- `folderNode` (lexical_declaration, line 15)
- `files` (lexical_declaration, line 16)
- `buildTree` (lexical_declaration, line 25, exported)
- `[rootSection]` (lexical_declaration, line 26, exported)
- `root` (lexical_declaration, line 30, exported)
- `byPath` (lexical_declaration, line 31, exported)
- `path` (lexical_declaration, line 33, exported)
- `cut` (lexical_declaration, line 34, exported)
- `parent` (lexical_declaration, line 35, exported)
- `node` (lexical_declaration, line 36, exported)
- `ancestorsOf` (lexical_declaration, line 47, exported)
- `crumbs` (lexical_declaration, line 48, exported)
- `current` (lexical_declaration, line 49, exported)
- `next` (lexical_declaration, line 51, exported)
- `flatten` (lexical_declaration, line 61, exported)
- `folder` (lexical_declaration, line 66, exported)
- `link` (lexical_declaration, line 67, exported)
- `summary` (lexical_declaration, line 86)
- `details` (lexical_declaration, line 93)
- `list` (lexical_declaration, line 99, exported)
- `scroll` (lexical_declaration, line 101, exported)
- `selected` (lexical_declaration, line 112, exported)
- `ancestor` (lexical_declaration, line 113, exported)

## Uses

- [core/factories/element.factory.ts](https://banes-lab.com/source/tree/core/factories/element.factory.ts.md)

## Used by

- [presentation/renderers/anatomy.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/anatomy.renderer.ts.md)
- [presentation/renderers/folder.renderer.ts](https://banes-lab.com/source/tree/presentation/renderers/folder.renderer.ts.md)

## Source

```typescript
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 { ACTIVE_CLASS } from "#configuration/constants/element.constants";
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 { createLink } from "#presentation/components/link.component";
import { subsectionId } from "#presentation/renderers/section.renderer";

const DETAILS_TAG = "details";

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 };
};

export const buildTree = function buildTree(sections: readonly Section[]): FolderNode | null {
    const [rootSection] = sections;
    if (rootSection === undefined) {
        return null;
    }
    const root = folderNode(rootSection, "", ROOT_LABEL);
    const byPath = new Map<string, FolderNode>([["", root]]);
    for (const section of sections.slice(1)) {
        const path = section.title;
        const cut = path.lastIndexOf(PATH_SEPARATOR);
        const parent = byPath.get(cut === -1 ? "" : path.slice(0, cut)) ?? root;
        const node = folderNode(section, path, path.slice(cut + 1));
        byPath.set(path, node);
        parent.folders.push(node);
    }
    return root;
};

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 }),
    ]);
    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;
    }
};
```
