|
<script type="application/javascript"> |
|
window.addEventListener("load", async () => { |
|
const placeholder = document.querySelector(".children-placeholder"); |
|
|
|
// Check if the placeholder element exists |
|
if (!placeholder) { |
|
console.log("🛈 No .children-placeholder found."); |
|
return; |
|
} |
|
|
|
// Read configuration from data attributes |
|
const limit = parseInt(placeholder.getAttribute("data-limit") || "100", 10); |
|
const maxDepth = parseInt(placeholder.getAttribute("data-depth") || "1", 10); |
|
const sortAttr = placeholder.getAttribute("data-sort") || "path:asc"; |
|
const debug = placeholder.getAttribute("data-debug") === "true"; |
|
|
|
const [sortField, sortDirection] = sortAttr.split(":"); |
|
const sortAsc = sortDirection !== "desc"; |
|
|
|
const log = (...args) => debug && console.log(...args); |
|
|
|
// Parse the URL path to determine the base path and locale |
|
let fullPath = window.location.pathname; |
|
let [, locale, ...pathParts] = fullPath.split("/"); |
|
locale = locale || "de"; |
|
let path = pathParts.join("/").replace(/^\/+|\/+$/g, ""); |
|
const basePath = path ? `${path}/` : ""; |
|
|
|
log("🌍 Locale:", locale); |
|
log("🔍 Searching for subpages of path:", basePath); |
|
|
|
// Show loading message |
|
placeholder.innerHTML = "Loading subpages…"; |
|
|
|
// GraphQL query to fetch pages |
|
const query = { |
|
query: ` |
|
query ($query: String!, $locale: String!) { |
|
pages { |
|
search(query: $query, locale: $locale) { |
|
results { |
|
title |
|
path |
|
description |
|
} |
|
} |
|
} |
|
} |
|
`, |
|
variables: { |
|
query: basePath, |
|
locale: locale |
|
} |
|
}; |
|
|
|
try { |
|
// Send GraphQL query to server |
|
const response = await fetch("/graphql", { |
|
method: "POST", |
|
headers: { "Content-Type": "application/json" }, |
|
body: JSON.stringify(query) |
|
}); |
|
|
|
const json = await response.json(); |
|
|
|
// Check for errors in response |
|
if (!response.ok || json.errors) { |
|
throw new Error("GraphQL error: " + JSON.stringify(json.errors)); |
|
} |
|
|
|
const results = json?.data?.pages?.search?.results ?? []; |
|
|
|
log("📄 Found pages:", results.map(p => p.path)); |
|
|
|
// Filter and sort child pages |
|
const children = results |
|
.filter(p => p.path !== path) |
|
.filter(p => p.path.startsWith(path + "/")) |
|
.sort((a, b) => { |
|
const aVal = a[sortField]?.toLowerCase?.() || ""; |
|
const bVal = b[sortField]?.toLowerCase?.() || ""; |
|
if (aVal < bVal) return sortAsc ? -1 : 1; |
|
if (aVal > bVal) return sortAsc ? 1 : -1; |
|
return 0; |
|
}) |
|
.slice(0, limit); |
|
|
|
log("✅ Filtered & sorted subpages:", children.map(p => p.path)); |
|
|
|
// Show message if no children were found |
|
if (children.length === 0) { |
|
placeholder.innerHTML = "<em>No subpages available.</em>"; |
|
return; |
|
} |
|
|
|
// Build a tree structure from the page paths |
|
const tree = {}; |
|
children.forEach(page => { |
|
const relPath = page.path.slice(basePath.length).replace(/^\/+|\/+$/g, ""); |
|
const parts = relPath.split("/"); |
|
let node = tree; |
|
parts.forEach((part, idx) => { |
|
if (!node[part]) { |
|
node[part] = { __meta: null, __children: {} }; |
|
} |
|
if (idx === parts.length - 1) { |
|
node[part].__meta = page; |
|
} |
|
node = node[part].__children; |
|
}); |
|
}); |
|
|
|
// Escape HTML to prevent XSS |
|
function escapeHtml(str) { |
|
return str.replace(/[&<>"']/g, (m) => |
|
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m]) |
|
); |
|
} |
|
|
|
// Recursively render the tree into a nested list |
|
function renderTree(treeObj, depth = 1) { |
|
if (depth > maxDepth) return null; |
|
const ul = document.createElement("ul"); |
|
ul.className = `children-tree level-${depth}`; |
|
|
|
for (const key of Object.keys(treeObj)) { |
|
const node = treeObj[key]; |
|
const hasChildren = Object.keys(node.__children).length > 0; |
|
const hasMeta = !!node.__meta; |
|
if (!hasMeta && !hasChildren) continue; |
|
|
|
const li = document.createElement("li"); |
|
li.className = "children-item"; |
|
|
|
if (hasMeta) { |
|
const p = node.__meta; |
|
li.innerHTML = `<a href="/${p.path}">${escapeHtml(p.title)}</a><br><small>${escapeHtml(p.description || "")}</small>`; |
|
} else { |
|
li.innerHTML = `<strong>${key}</strong>`; |
|
} |
|
|
|
const childList = renderTree(node.__children, depth + 1); |
|
if (childList) li.appendChild(childList); |
|
ul.appendChild(li); |
|
} |
|
|
|
return ul; |
|
} |
|
|
|
// Create the final HTML structure and replace the placeholder |
|
const wrapper = document.createElement("div"); |
|
wrapper.className = "children-list"; |
|
const treeHtml = renderTree(tree); |
|
if (treeHtml) wrapper.appendChild(treeHtml); |
|
|
|
placeholder.replaceWith(wrapper); |
|
log("🌲 Tree structure successfully rendered."); |
|
} catch (err) { |
|
console.error("❌ Error loading subpages:", err); |
|
placeholder.innerHTML = "<em>Error loading subpages.</em>"; |
|
} |
|
}); |
|
</script> |
I forked this and made some changes so that the script waits for Wiki.js to render the page before replacing the contents of the div (instead of replacing the entire div), which means additional text on the page doesn’t interfere with the scripts functionality.