|
<script type="application/javascript"> |
|
(async function() { |
|
// Wait until Wiki.js finishes rendering the page content |
|
function waitForContent(timeout = 10000) { |
|
return new Promise((resolve) => { |
|
const start = Date.now(); |
|
const check = () => { |
|
const el = document.querySelector(".children-placeholder"); |
|
if (el) return resolve(el); |
|
if (Date.now() - start > timeout) return resolve(null); |
|
requestAnimationFrame(check); |
|
}; |
|
check(); |
|
}); |
|
} |
|
|
|
const placeholder = await waitForContent(); |
|
if (!placeholder) { |
|
console.log("🛈 No .children-placeholder found (timeout reached)."); |
|
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 targetPath = placeholder.getAttribute("data-target-path"); |
|
|
|
// Validate sort field against allowed values |
|
const ALLOWED_SORT_FIELDS = ["path", "title", "description"]; |
|
const [rawSortField, sortDirection] = sortAttr.split(":"); |
|
const sortField = ALLOWED_SORT_FIELDS.includes(rawSortField) ? rawSortField : "path"; |
|
const sortAsc = sortDirection !== "desc"; |
|
|
|
const log = (...args) => debug && console.log(...args); |
|
|
|
if (sortField !== rawSortField) { |
|
log("⚠️ Invalid sort field:", rawSortField, "– falling back to 'path'"); |
|
} |
|
|
|
// Parse current URL for locale and path |
|
let fullPath = window.location.pathname; |
|
let [, rawLocale, ...pathParts] = fullPath.split("/"); |
|
|
|
// Validate locale: must be 2-3 letter language code (e.g. "en", "de", "deu") |
|
const locale = /^[a-z]{2,3}$/.test(rawLocale) ? rawLocale : "en"; |
|
|
|
if (locale !== rawLocale) { |
|
log("⚠️ Invalid locale detected:", rawLocale, "– falling back to 'en'"); |
|
} |
|
|
|
// Use targetPath if specified, otherwise use current path |
|
let path; |
|
if (targetPath) { |
|
path = targetPath.replace(/^\/+|\/+$/g, ""); |
|
// Strip locale prefix if present (e.g. "de" → "" or "de/projekte/dana" → "projekte/dana") |
|
if (path === locale) { |
|
path = ""; |
|
} else if (path.startsWith(locale + "/")) { |
|
path = path.slice(locale.length + 1); |
|
} |
|
log("🎯 Using target path:", path || "(root)"); |
|
} else { |
|
path = pathParts.join("/").replace(/^\/+|\/+$/g, ""); |
|
log("📍 Using current path:", path); |
|
} |
|
|
|
const isRoot = path === ""; |
|
const basePath = isRoot ? "" : `${path}/`; |
|
|
|
log("🌍 Locale:", locale); |
|
log("🔍 Searching for subpages of path:", isRoot ? "(root)" : basePath); |
|
|
|
placeholder.textContent = "Loading subpages…"; |
|
|
|
// Build GraphQL query: use pages.list for root level, pages.search for subpaths |
|
let gqlQuery; |
|
if (isRoot) { |
|
gqlQuery = { |
|
query: ` |
|
query { |
|
pages { |
|
list(orderBy: PATH) { |
|
path |
|
title |
|
description |
|
} |
|
} |
|
} |
|
` |
|
}; |
|
log("📋 Using pages.list (root level)"); |
|
} else { |
|
gqlQuery = { |
|
query: ` |
|
query ($query: String!, $locale: String!) { |
|
pages { |
|
search(query: $query, locale: $locale) { |
|
results { |
|
title |
|
path |
|
description |
|
} |
|
} |
|
} |
|
} |
|
`, |
|
variables: { query: basePath, locale: locale } |
|
}; |
|
log("🔎 Using pages.search for:", basePath); |
|
} |
|
|
|
try { |
|
const response = await fetch("/graphql", { |
|
method: "POST", |
|
headers: { "Content-Type": "application/json" }, |
|
body: JSON.stringify(gqlQuery) |
|
}); |
|
|
|
const json = await response.json(); |
|
|
|
if (!response.ok || json.errors) { |
|
throw new Error("GraphQL error: " + JSON.stringify(json.errors)); |
|
} |
|
|
|
// Extract results depending on which query was used |
|
const results = isRoot |
|
? (json?.data?.pages?.list ?? []) |
|
: (json?.data?.pages?.search?.results ?? []); |
|
|
|
log("📄 Found pages:", results.map(p => p.path)); |
|
|
|
// Filter for children |
|
const children = results |
|
.filter(p => p.path !== path) |
|
.filter(p => isRoot || 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)); |
|
|
|
if (children.length === 0) { |
|
placeholder.innerHTML = "<em>No subpages available.</em>"; |
|
return; |
|
} |
|
|
|
// Build a tree |
|
const tree = {}; |
|
children.forEach(page => { |
|
const relPath = isRoot |
|
? page.path.replace(/^\/+|\/+$/g, "") |
|
: 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; |
|
}); |
|
}); |
|
|
|
function escapeHtml(str) { |
|
return str.replace(/[&<>"']/g, m => |
|
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m]) |
|
); |
|
} |
|
|
|
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="/${locale}/${p.path}">${escapeHtml(p.title)}</a> |
|
<br><small>${escapeHtml(p.description || "")}</small> |
|
`; |
|
} else { |
|
li.innerHTML = `<strong>${escapeHtml(key)}</strong>`; |
|
} |
|
|
|
const childList = renderTree(node.__children, depth + 1); |
|
if (childList) li.appendChild(childList); |
|
ul.appendChild(li); |
|
} |
|
|
|
return ul; |
|
} |
|
|
|
// Render final tree |
|
const wrapper = document.createElement("div"); |
|
wrapper.className = "children-list"; |
|
const treeHtml = renderTree(tree); |
|
if (treeHtml) wrapper.appendChild(treeHtml); |
|
|
|
// Safely replace content inside placeholder |
|
placeholder.innerHTML = ""; |
|
placeholder.appendChild(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.