//#region Helpers
function getAbsoluteRootPath()
{
if (typeof rootPath == 'undefined') setupRootPath(document);
return new URL(window.location.href + "/../" + rootPath).pathname;
}
function getURLPath(url = window.location.pathname)
{
let absoluteRoot = getAbsoluteRootPath();
let pathname = url.substring(absoluteRoot.length);
return pathname;
}
function getURLRootPath(url = window.location.pathname)
{
let path = getURLPath(url);
let splitPath = path.split("/");
let rootPath = "";
for (let i = 0; i < splitPath.length - 1; i++)
{
rootPath += "../";
}
return rootPath;
}
async function setTreeCollapsed(element, collapsed, animate = true)
{
if (!element || !element.classList.contains("mod-collapsible")) return;
let children = element.querySelector(".tree-item-children");
if (collapsed)
{
element.classList.add("is-collapsed");
if(animate) slideUp(children, 100);
else children.style.display = "none";
}
else
{
element.classList.remove("is-collapsed");
if(animate) slideDown(children, 100);
else children.style.removeProperty("display");
}
}
async function setTreeCollapsedAll(elements, collapsed, animate = true)
{
let childrenList = [];
elements.forEach(async element =>
{
if (!element || !element.classList.contains("mod-collapsible")) return;
let children = element.querySelector(".tree-item-children");
if (collapsed)
{
element.classList.add("is-collapsed");
}
else
{
element.classList.remove("is-collapsed");
}
childrenList.push(children);
});
if (collapsed)
{
if(animate) slideUpAll(childrenList, 100);
else childrenList.forEach(async children => children.style.display = "none");
}
else
{
if(animate) slideDownAll(childrenList, 100);
else childrenList.forEach(async children => children.style.removeProperty("display"));
}
}
function toggleTreeCollapsed(element)
{
if (!element) return;
setTreeCollapsed(element, !element.classList.contains("is-collapsed"));
}
function toggleTreeCollapsedAll(elements)
{
if (!elements) return;
setTreeCollapsedAll(elements, !elements[0].classList.contains("is-collapsed"));
}
function getHeaderEl(headerDiv)
{
let possibleChildHeader = headerDiv.firstChild;
let isHeader = false;
while (possibleChildHeader != null)
{
isHeader = possibleChildHeader ? /[Hh][1-6]/g.test(possibleChildHeader.tagName) : false;
if (isHeader) break;
possibleChildHeader = possibleChildHeader.nextElementSibling;
}
return possibleChildHeader;
}
function getPreviousHeader(headerDiv)
{
let possibleParent = headerDiv.previousElementSibling;
let isHeader = false;
while (possibleParent != null)
{
let possibleChildHeader = getHeaderEl(possibleParent);
isHeader = possibleChildHeader ? /[Hh][1-6]/g.test(possibleChildHeader.tagName) : false;
if (isHeader) break;
possibleParent = possibleParent.previousElementSibling;
}
return possibleParent;
}
function setHeaderOpen(headerDiv, open, openParents = true)
{
if(headerDiv.tagName != "DIV" || !getHeaderEl(headerDiv))
{
console.error("setHeaderOpen() must be called with a header div");
return;
}
// let selector = getHeadingContentsSelector(header);
if (open)
{
headerDiv.classList.remove("is-collapsed");
headerDiv.style.display = "";
}
if (!open)
{
headerDiv.classList.add("is-collapsed");
}
let headerEl = getHeaderEl(headerDiv);
let childHeaders = [];
let possibleChild = headerDiv.nextElementSibling;
// loop through next siblings showing/ hiding children until we reach a header of the same or lower level
while (possibleChild != null)
{
let possibleChildHeader = getHeaderEl(possibleChild);
if(possibleChildHeader)
{
// if header is a sibling of this header then break
if (possibleChildHeader.tagName <= headerEl.tagName) break;
// save child headers to be re closed afterwards
childHeaders.push(possibleChild);
}
if (!open) possibleChild.style.display = "none";
else possibleChild.style.display = "";
possibleChild = possibleChild.nextElementSibling;
}
if(open)
{
// if we are opening the header then we need to make sure that all closed child headers stay closed
childHeaders.forEach(function(item)
{
if (item.classList.contains("is-collapsed"))
{
setHeaderOpen(item, false);
}
});
// if we are opening the header then we need to make sure that all parent headers are open
if (openParents)
{
let previousHeader = getPreviousHeader(headerDiv);
while (previousHeader != null)
{
let previousHeaderEl = getHeaderEl(previousHeader);
if (previousHeaderEl.tagName < headerEl.tagName)
{
// if header is a parent of this header then unhide
setHeaderOpen(previousHeader, true);
break;
}
previousHeader = getPreviousHeader(previousHeader);
}
}
}
}
let slideUp = (target, duration=500) => {
target.style.transitionProperty = 'height, margin, padding';
target.style.transitionDuration = duration + 'ms';
target.style.boxSizing = 'border-box';
target.style.height = target.offsetHeight + 'px';
target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = 0;
target.style.paddingTop = 0;
target.style.paddingBottom = 0;
target.style.marginTop = 0;
target.style.marginBottom = 0;
window.setTimeout(async () => {
target.style.display = 'none';
target.style.removeProperty('height');
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
}, duration);
}
let slideUpAll = (targets, duration=500) => {
targets.forEach(async target => {
target.style.transitionProperty = 'height, margin, padding';
target.style.transitionDuration = duration + 'ms';
target.style.boxSizing = 'border-box';
target.style.height = target.offsetHeight + 'px';
target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = 0;
target.style.paddingTop = 0;
target.style.paddingBottom = 0;
target.style.marginTop = 0;
target.style.marginBottom = 0;
});
window.setTimeout(async () => {
targets.forEach(async target => {
target.style.display = 'none';
target.style.removeProperty('height');
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
});
}, duration);
}
let slideDown = (target, duration=500) => {
target.style.removeProperty('display');
let display = window.getComputedStyle(target).display;
if (display === 'none') display = 'block';
target.style.display = display;
let height = target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = 0;
target.style.paddingTop = 0;
target.style.paddingBottom = 0;
target.style.marginTop = 0;
target.style.marginBottom = 0;
target.offsetHeight;
target.style.boxSizing = 'border-box';
target.style.transitionProperty = "height, margin, padding";
target.style.transitionDuration = duration + 'ms';
target.style.height = height + 'px';
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
window.setTimeout(async () => {
target.style.removeProperty('height');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
}, duration);
}
let slideDownAll = (targets, duration=500) => {
targets.forEach(async target => {
target.style.removeProperty('display');
let display = window.getComputedStyle(target).display;
if (display === 'none') display = 'block';
target.style.display = display;
let height = target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = 0;
target.style.paddingTop = 0;
target.style.paddingBottom = 0;
target.style.marginTop = 0;
target.style.marginBottom = 0;
target.offsetHeight;
target.style.boxSizing = 'border-box';
target.style.transitionProperty = "height, margin, padding";
target.style.transitionDuration = duration + 'ms';
target.style.height = height + 'px';
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
});
window.setTimeout( async () => {
targets.forEach(async target => {
target.style.removeProperty('height');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
});
}, duration);
}
var slideToggle = (target, duration = 500) => {
if (window.getComputedStyle(target).display === 'none') {
return slideDown(target, duration);
} else {
return slideUp(target, duration);
}
}
var slideToggleAll = (targets, duration = 500) => {
if (window.getComputedStyle(targets[0]).display === 'none') {
return slideDownAll(targets, duration);
} else {
return slideUpAll(targets, duration);
}
}
//#endregion
async function loadDocument(url, pushHistory = true, scrollTo = true)
{
console.log("Loading document: " + url);
// change the active file
setActiveDocument(url, scrollTo, pushHistory);
let response;
// if(typeof embeddedDocuments == 'undefined')
// {
try
{
response = await fetch(url);
}
catch (error)
{
console.log("Cannot use fetch API (likely due to CORS), just loading the page normally.");
window.location.assign(url);
return;
}
// }
// else
// {
// response = new Response(embeddedDocuments[url], {status: 200, statusText: "OK"});
// }
let doc = document.implementation.createHTMLDocument();
if (response.ok)
{
let html = (await response.text()).replaceAll("", "").replaceAll("", "").replaceAll("", "");
doc.documentElement.innerHTML = html;
// copy document content and outline tree
document.querySelector(".document-container").innerHTML = doc.querySelector(".document-container").innerHTML;
document.querySelector(".outline-tree").innerHTML = doc.querySelector(".outline-tree").innerHTML;
// if the url has a heading, scroll to it
let splitURL = url.split("#");
let pathnameTarget = splitURL[0] ?? url;
let headingTarget = splitURL.length > 1 ? splitURL[1] : null;
if (headingTarget) document.getElementById(headingTarget).scrollIntoView();
// Change the root path to match the match from the new page
setupRootPath(doc);
// initialize events on the new page
initializePage(document.querySelector(".document-container"));
initializePage(document.querySelector(".outline-tree"));
document.title = doc.title;
}
else
{
// if the page is not able to load instead add a header saying the page doesn't exist
document.querySelector(".markdown-preview-view").innerHTML =
`
Page Not Found
`;
document.querySelector(".outline-tree").innerHTML = "";
console.log("Page not found: " + getAbsoluteRootPath() + url);
let newRootPath = getURLRootPath(getAbsoluteRootPath() + url);
rootPath = newRootPath;
document.querySelector("base").href = newRootPath;
document.title = "Page Not Found";
}
return doc;
}
function setActiveDocument(url, scrollTo = true, pushHistory = true)
{
let pathnameTarget = url.split("#")[0] ?? url; // path with no header
// switch active file in file tree
document.querySelector(".tree-item.mod-active")?.classList.remove("mod-active");
let treeItems = Array.from(document.querySelectorAll(".tree-item > .tree-item-contents > .tree-item-link"));
let treeItem = undefined;
for (let item of treeItems)
{
if (item.getAttribute("href") == url)
{
let parent = item.parentElement.parentElement;
parent.classList.add("mod-active");
treeItem = parent;
while (parent.hasAttribute("data-depth"))
{
setTreeCollapsed(parent, false, false);
parent = parent.parentElement.parentElement;
}
continue;
}
}
if(scrollTo) treeItem?.scrollIntoView({block: "center", inline: "nearest"});
// set the active file in th graph view
if(typeof nodes != 'undefined' && window.renderWorker)
{
let activeNode = nodes?.paths.findIndex(function(item) { return item.endsWith(pathnameTarget); }) ?? -1;
if(activeNode >= 0)
{
window.renderWorker.activeNode = activeNode;
}
}
if(pushHistory && window.location.protocol != "file:") window.history.pushState({ path: pathnameTarget }, '', pathnameTarget);
}
//#region Initialization
function setupThemeToggle(setupOnNode)
{
if (localStorage.getItem("theme_toggle") != null)
{
setThemeToggle(localStorage.getItem("theme_toggle") == "true");
}
var lastScheme = "theme-dark";
// change theme to match current system theme
if (localStorage.getItem("theme_toggle") == null && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
{
setThemeToggle(true);
lastScheme = "theme-dark";
}
if (localStorage.getItem("theme_toggle") == null && window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches)
{
setThemeToggle(true);
lastScheme = "theme-light";
}
// set initial toggle state based on body theme class
if (document.body.classList.contains("theme-light"))
{
setThemeToggle(false);
}
else
{
setThemeToggle(true);
}
function setThemeToggle(state, instant = false)
{
let toggle = document.querySelector(".theme-toggle-input");
toggle.checked = state;
if (instant)
{
var oldTransition = document.body.style.transition;
document.body.style.transition = "none";
}
if(!toggle.classList.contains("is-checked") && state)
{
toggle.classList.add("is-checked");
}
else if (toggle.classList.contains("is-checked") && !state)
{
toggle.classList.remove("is-checked");
}
if(!state)
{
if (document.body.classList.contains("theme-dark"))
{
document.body.classList.remove("theme-dark");
}
if (!document.body.classList.contains("theme-light"))
{
document.body.classList.add("theme-light");
}
}
else
{
if (document.body.classList.contains("theme-light"))
{
document.body.classList.remove("theme-light");
}
if (!document.body.classList.contains("theme-dark"))
{
document.body.classList.add("theme-dark");
}
}
if (instant)
{
setTimeout(function()
{
document.body.style.transition = oldTransition;
}, 100);
}
localStorage.setItem("theme_toggle", state ? "true" : "false");
}
setupOnNode.querySelector(".theme-toggle-input")?.addEventListener("change", event =>
{
setThemeToggle(!(localStorage.getItem("theme_toggle") == "true"));
});
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event =>
{
// return if we are printing
if (window.matchMedia('print').matches)
{
printing = true;
return;
}
let newColorScheme = event.matches ? "theme-dark" : "theme-light";
if (newColorScheme == lastScheme) return;
if (newColorScheme == "theme-dark")
{
setThemeToggle(true);
}
if (newColorScheme == "theme-light")
{
setThemeToggle(false);
}
lastScheme = newColorScheme;
});
}
function setupHeaders(setupOnNode)
{
// MAKE HEADERS COLLAPSIBLE
setupOnNode.querySelectorAll(".heading-collapse-indicator").forEach(function (element)
{
element.addEventListener("click", function ()
{
var isOpen = !this.parentElement.parentElement.classList.contains("is-collapsed");
setHeaderOpen(this.parentElement.parentElement, !isOpen);
});
});
// unfold header when an internal link that points to that header is clicked
setupOnNode.querySelectorAll("a.internal-link, a.tree-item-link").forEach(function (element)
{
element.addEventListener("click", function (event)
{
event.preventDefault();
let target = this.getAttribute("href");
// if the target is a header uncollapse it
if (target.startsWith("#"))
{
console.log("Uncollapsing header: " + target);
let header = document.getElementById(target.substring(1));
setHeaderOpen(header.parentElement, true);
}
});
});
}
function setupTrees(setupOnNode)
{
const fileTreeItems = Array.from(setupOnNode.querySelectorAll(".tree-container.file-tree .tree-item"));
setupOnNode.querySelectorAll(".tree-item-contents > .collapse-icon").forEach(function(item)
{
item.addEventListener("click", function()
{
toggleTreeCollapsed(item.parentElement.parentElement);
});
});
let fileTreeCollapse = setupOnNode.querySelector(".tree-container.file-tree .collapse-tree-button");
if (fileTreeCollapse) fileTreeCollapse.addEventListener("click", async function()
{
let fileTreeIsCollapsed = fileTreeCollapse.classList.contains("is-collapsed");
setTreeCollapsedAll(fileTreeItems, !fileTreeIsCollapsed, fileTreeItems.length < 100);
fileTreeCollapse.classList.toggle("is-collapsed");
fileTreeCollapse.querySelector("iconify-icon").setAttribute("icon", fileTreeIsCollapsed ? "ph:arrows-out-line-horizontal-bold" : "ph:arrows-in-line-horizontal-bold");
});
let outlineTreeCollapse = setupOnNode.querySelector(".tree-container.outline-tree .collapse-tree-button");
if(outlineTreeCollapse) outlineTreeCollapse.addEventListener("click", async function()
{
let outlineTreeIsCollapsed = outlineTreeCollapse.classList.contains("is-collapsed");
let items = Array.from(outlineTreeCollapse.parentElement.parentElement.querySelectorAll(".tree-item"));
setTreeCollapsedAll(items, !outlineTreeIsCollapsed, items.length < 100);
outlineTreeCollapse.classList.toggle("is-collapsed");
outlineTreeCollapse.querySelector("iconify-icon").setAttribute("icon", outlineTreeIsCollapsed ? "ph:arrows-out-line-horizontal-bold" : "ph:arrows-in-line-horizontal-bold");
});
// start with all closed
setupOnNode.querySelectorAll(".tree-container .tree-item").forEach(function(item)
{
if (item.classList.contains("is-collapsed")) setTreeCollapsed(item, true, false);
});
// make sure the icons match their starting collaped state
setupOnNode.querySelectorAll(".tree-container > .tree-header > .collapse-tree-button").forEach(function(item)
{
if (item.classList.contains("is-collapsed"))
{
item.querySelector("iconify-icon").setAttribute("icon", "ph:arrows-out-line-horizontal-bold");
}
else
{
item.querySelector("iconify-icon").setAttribute("icon", "ph:arrows-in-line-horizontal-bold");
}
});
}
function setupCallouts(setupOnNode)
{
// MAKE CALLOUTS COLLAPSIBLE
// if the callout title is clicked, toggle the display of .callout-content
setupOnNode.querySelectorAll(".callout.is-collapsible .callout-title").forEach(function (element)
{
element.addEventListener("click", function ()
{
var parent = this.parentElement;
var isCollapsed = parent.classList.contains("is-collapsed");
parent.classList.toggle("is-collapsed");
element.querySelector(".callout-fold").classList.toggle("is-collapsed");
slideToggle(parent.querySelector(".callout-content"), 100);
});
});
}
function setupCheckboxes(setupOnNode)
{
// Fix checkboxed toggling .is-checked
setupOnNode.querySelectorAll(".task-list-item-checkbox").forEach(function (element)
{
element.addEventListener("click", function ()
{
var parent = this.parentElement;
parent.classList.toggle("is-checked");
parent.setAttribute("data-task", parent.classList.contains("is-checked") ? "x" : " ");
});
});
setupOnNode.querySelectorAll(`.plugin-tasks-list-item input[type="checkbox"]`).forEach(function(checkbox)
{
checkbox.checked = checkbox.parentElement.classList.contains("is-checked");
});
setupOnNode.querySelectorAll('.kanban-plugin__item.is-complete').forEach(function(checkbox)
{
checkbox.querySelector('input[type="checkbox"]').checked = true;
});
}
function setupCanvas(setupOnNode)
{
let focusedNode = null;
// make canvas nodes selectable
setupOnNode.querySelectorAll(".canvas-node-content-blocker").forEach(function (element)
{
element.addEventListener("click", function ()
{
var parent = this.parentElement.parentElement;
parent.classList.toggle("is-focused");
this.style.display = "none";
if (focusedNode)
{
focusedNode.classList.remove("is-focused");
focusedNode.querySelector(".canvas-node-content-blocker").style.display = "";
}
focusedNode = parent;
});
});
// make canvas node deselect when clicking outside
// document.addEventListener("click", function (event)
// {
// if (!event.target.closest(".canvas-node"))
// {
// document.querySelectorAll(".canvas-node").forEach(function (node)
// {
// node.classList.remove("is-focused");
// node.querySelector(".canvas-node-content-blocker").style.display = "";
// });
// }
// });
}
function setupCodeblocks(setupOnNode)
{
// make code snippet block copy button copy the code to the clipboard
setupOnNode.querySelectorAll(".copy-code-button").forEach(function (element)
{
element.addEventListener("click", function ()
{
var code = this.parentElement.querySelector("code").textContent;
navigator.clipboard.writeText(code);
this.textContent = "Copied!";
// set a timeout to change the text back
setTimeout(function ()
{
setupOnNode.querySelectorAll(".copy-code-button").forEach(function (button)
{
button.textContent = "Copy";
});
}, 2000);
});
});
}
function setupLinks(setupOnNode)
{
setupOnNode.querySelectorAll(".internal-link, .footnote-link, .tree-item-link").forEach(function(link)
{
link.addEventListener("click", function(event)
{
let target = link.getAttribute("href");
event.preventDefault();
// this is linking to a different page
if (!target.startsWith("#"))
{
// load doc, if it is a tree link then don't scroll to the active doc in the file tree
loadDocument(target, true, !link.classList.contains("tree-item-link"));
return;
}
else
{
let headerTarget = document.getElementById(target.substring(1));
setHeaderOpen(headerTarget.parentElement, true);
headerTarget.scrollIntoView();
}
});
});
window.onpopstate = function(event)
{
loadDocument(getURLPath(), false);
}
}
// What the FUCK is this abomination??? - Endermanch, 2023
/* let sidebarWidth = undefined;
let lineWidth = undefined;
function setupResize(setupOnNode)
{
if (setupOnNode != document) return;
function updateSidebars()
{
let rightSidebar = document.querySelector(".sidebar-right");
let leftSidebar = document.querySelector(".sidebar-left");
let sidebarCount = (rightSidebar ? 1 : 0) + (leftSidebar ? 1 : 0);
if (sidebarCount == 0) return;
if(!sidebarWidth) sidebarWidth = Math.max(rightSidebar?.clientWidth, leftSidebar?.clientWidth);
if (!lineWidth)
{
let docWidthTestEl = document.createElement("div");
document.querySelector(".markdown-preview-view").appendChild(docWidthTestEl);
docWidthTestEl.style.width = "var(--line-width)";
docWidthTestEl.style.minWidth = "var(--line-width)";
docWidthTestEl.style.maxWidth = "var(--line-width)";
lineWidth = docWidthTestEl.clientWidth;
docWidthTestEl.remove();
}
let letHideRightThreshold = sidebarWidth * sidebarCount + lineWidth / 2;
if (window.innerWidth < letHideRightThreshold)
{
rightSidebar.style.display = "none";
}
else
{
rightSidebar.style.display = "";
}
let letHideLeftThreshold = lineWidth / 2 + sidebarWidth;
if (window.innerWidth < letHideLeftThreshold)
{
leftSidebar.style.display = "none";
}
else
{
leftSidebar.style.display = "";
}
}
window.addEventListener("resize", function()
{
updateSidebars();
});
updateSidebars();
}
*/
function setupRootPath(fromDocument)
{
let basePath = fromDocument.querySelector("#root-path").getAttribute("root-path");
document.querySelector("base").href = basePath;
document.querySelector("#root-path").setAttribute("root-path", basePath);
rootPath = basePath;
}
let touchDrag = false;
function initializePage(setupOnNode)
{
setupThemeToggle(setupOnNode);
setupHeaders(setupOnNode);
setupTrees(setupOnNode);
setupCallouts(setupOnNode);
setupCheckboxes(setupOnNode);
setupCanvas(setupOnNode);
setupCodeblocks(setupOnNode);
setupLinks(setupOnNode);
setupOnNode.querySelectorAll("*").forEach(function(element)
{
element.addEventListener("touchend", function(event)
{
if (touchDrag)
{
touchDrag = false;
event.stopPropagation();
return;
}
if (element instanceof HTMLElement) element.click();
});
});
if(setupOnNode == document)
{
document.body.addEventListener("touchmove", function(event)
{
event.stopImmediatePropagation();
touchDrag = true;
});
setupRootPath(document);
setActiveDocument(getURLPath());
}
}
function initializeForFileProtocol()
{
let graphEl = document.querySelector(".graph-view-placeholder");
if(graphEl)
{
console.log("Running locally, skipping graph view initialization and hiding graph.");
graphEl.style.display = "none";
graphEl.previousElementSibling.style.display = "none"; // hide the graph's header
}
}
//#endregion
window.onload = function()
{
if (window.location.protocol == "file:") initializeForFileProtocol();
initializePage(document);
}