|
<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> |
@mormorhaxa Nice - I took a look at it and will adopt it and embed something else as well.