www-from-model/static/filetree.js
Stephan Heunis d2c93fd8cf
All checks were successful
Deploy on webserver / Build site and deploy on success (push) Successful in 2m16s
Change distribution selector from tabs to dropdown
2026-06-17 20:44:30 +02:00

191 lines
No EOL
6 KiB
JavaScript

// Keep track of first click on a given distribution tab
const distributionsRendered = [];
// Add dropdown select event listerners
document.getElementById("distribution-select")
.addEventListener("change", (e) => {
const target = e.target.value;
if (target) onTabClick(target);
});
async function onTabClick(targetId) {
// Only show selected pane
document.querySelectorAll(".tab-pane").forEach(p => {
p.style.display = "none";
});
const pane = document.getElementById(targetId);
if (pane) pane.style.display = "block";
// Only render new root for the first click
if (!distributionsRendered.includes(targetId)) {
distributionsRendered.push(targetId)
await renderDistributionRoot(targetId);
}
}
// Render the root and children for the first time,
// after fetching and decompressing the source json
// and then building the file tree hierarchy
async function renderDistributionRoot(distributionId) {
const tree = document.getElementById(`filetree${distributionId}`);
const download = await fetchJsonGzFileContent(`${distributionId}.json.gz`);
const treeData = buildTreeFromDataDownload(download);
renderChildren(treeData.children, tree);
}
// Fetch and stream gzipped json file
async function fetchJsonGzFileContent(url) {
const response = await fetch(url, { cache: 'no-cache' });
if (!response.ok) {
configError.value = true;
throw new Error(
`Error fetching config file: ${response.statusText}`
);
}
const ds = new DecompressionStream("gzip");
const decompressed = response.body.pipeThrough(ds);
const json = await new Response(decompressed).json();
return json
}
function buildTreeFromDataDownload(download) {
// A DataDownload can have many children DataDownloads via the hasPart array,
// each with the `position` indicating its path relative to its parent.
// We need to process every path and create a hierarchical structure.
// For now we assume no recursion.
const root = {
type: "directory",
name: download.prefLabel || download["@id"] || "distribution",
children: []
};
if (!download.hasPart) {
return root;
}
for (const part of download.hasPart) {
insertPart(root, part);
}
sortTree(root)
return root;
}
function insertPart(parent, part) {
const path = part.position.split("/");
let current = parent;
for (let i = 0; i < path.length - 1; i++) {
const dirname = path[i];
let child = current.children.find(
x => x.type === "directory"
&& x.name === dirname
);
if (!child) {
child = {
type: "directory",
name: dirname,
children: []
};
current.children.push(child);
}
current = child;
}
const filename = path[path.length - 1];
current.children.push({
type: "file",
name: filename,
contentSize: part.contentSize,
contentUrl: part.contentUrl,
schema: part
});
}
// Sort children of a directory alphanumerically
function sortTree(node) {
if (!node.children) {
return;
}
node.children.sort((a, b) => {
if (a.type === "directory" && b.type !== "directory") {
return -1;
}
if (a.type !== "directory" && b.type === "directory") {
return 1;
}
return a.name.localeCompare(
b.name,
undefined,
{
numeric: true,
sensitivity: "base"
}
);
});
node.children.forEach(sortTree);
}
// Create elements for a directory node
// Uses details-summary element for expand/collapse functionality
// (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details)
function createDirectoryNode(node) {
const li = document.createElement("li");
const details = document.createElement("details");
const summary = document.createElement("summary");
details.classList.add("tree-directory");
// Open/closed folder icon depending on expand/collapse state
summary.innerHTML = `
<span class="icon icon-closed">${HUGO_ICONS.folder}</span>
<span class="icon icon-open">${HUGO_ICONS.folderOpen}</span>
${node.name}
`;
details.appendChild(summary);
const childList = document.createElement("ul");
details.appendChild(childList);
let rendered = false;
// Render directory children when toggled to open state
details.addEventListener( "toggle",() => {
if (!details.open || rendered) {
return;
}
renderChildren(node.children, childList);
rendered = true;
});
li.appendChild(details);
return li;
}
// Render children of an open directory
function renderChildren(children, parentElement) {
children.forEach(child => {
const node =
child.type === "directory"
? createDirectoryNode(child)
: createFileNode(child);
parentElement.appendChild(node);
});
}
// File node with file icon
// TODO: file size
function createFileNode(node) {
const li = document.createElement("li");
li.classList.add("file-node");
const span = document.createElement("span");
span.innerHTML = `
<span class="icon">${HUGO_ICONS.file}</span>
${node.name}
`;
li.appendChild(span);
if (node.contentbytesize) {
const size = document.createElement("span");
size.textContent = ` (${formatBytes(node.contentbytesize)})`;
size.classList.add("file-size");
li.appendChild(size);
}
return li;
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}