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