//#region ----------------- Initializations ----------------- let loadedURL = new URL(window.location.href); let absoluteBasePath = undefined; let relativeBasePath = undefined; let relativePathname = undefined; let webpageContainer; let documentContainer; let viewContent; let leftSidebar; let rightSidebar; let sidebarCollapseIcons; let sidebarGutters; let sidebars; let sidebarDefaultWidth; let sidebarTargetWidth; let contentTargetWidth; let themeToggle; let searchInput; let fileTree; let outlineTree; let fileTreeItems; let outlineTreeItems; let canvasWrapper; let canvas; let canvasNodes; let canvasBackground; let canvasBackgroundPattern; let focusedCanvasNode; let loadingIcon; let isOffline = false; let collapseIconUp = ["m7 15 5 5 5-5", "m7 9 5-5 5 5"]; // path 1, path 2 - svg paths let collapseIconDown = ["m7 20 5-5 5 5", "m7 4 5 5 5-5"]; // path 1, path 2 - svg paths let isTouchDevice = isTouchCapable(); let documentType; // "markdown" | "canvas" | "embed" | "custom" | "none" let embedType; // "img" | "video" | "audio" | "embed" | "none" let customType; // "kanban" | "excalidraw" | "none" let deviceSize; // "large-screen" | "small screen" | "tablet" | "phone" let fullyInitialized = false; async function initGlobalObjects() { if(window.location.protocol != "file:") { await loadIncludes(); } loadingIcon = document.createElement("div"); loadingIcon.classList.add("loading-icon"); document.body.appendChild(loadingIcon); loadingIcon.innerHTML = `
`; webpageContainer = document.querySelector(".webpage-container"); documentContainer = document.querySelector(".document-container"); leftSidebar = document.querySelector(".sidebar-left"); rightSidebar = document.querySelector(".sidebar-right"); fileTree = document.querySelector(".file-tree"); outlineTree = document.querySelector(".outline-tree"); fileTreeItems = Array.from(document.querySelectorAll(".tree-container.file-tree .tree-item")); sidebars = [] sidebarGutters = [] sidebarCollapseIcons = [] if (leftSidebar && rightSidebar) { sidebarCollapseIcons = Array.from(document.querySelectorAll(".sidebar-collapse-icon")); sidebarGutters = [sidebarCollapseIcons[0].parentElement, sidebarCollapseIcons[1].parentElement]; sidebars = [sidebarGutters[0].parentElement, sidebarGutters[1].parentElement]; } themeToggle = document.querySelector(".theme-toggle-input"); } async function initializePage() { focusedCanvasNode = null; canvasWrapper = document.querySelector(".canvas-wrapper") ?? canvasWrapper; canvas = document.querySelector(".canvas") ?? canvas; let canvasNodesTemp = document.querySelectorAll(".canvas-node"); canvasNodes = canvasNodesTemp.length > 0 ? canvasNodesTemp : canvasNodes; canvasBackground = document.querySelector(".canvas-background") ?? canvasBackground; canvasBackgroundPattern = document.querySelector(".canvas-background pattern") ?? canvasBackgroundPattern; viewContent = document.querySelector(".document-container > .view-content") ?? document.querySelector(".document-container > .markdown-preview-view") ?? viewContent; outlineTreeItems = Array.from(document.querySelectorAll(".tree-container.outline-tree .tree-item")); if(!fullyInitialized) { if (window.location.protocol == "file:") initializeForFileProtocol(); await initGlobalObjects(); initializeDocumentTypes(document); setupSidebars(); setupThemeToggle(); await setupSearch(); setupRootPath(document); sidebarDefaultWidth = await getComputedPixelValue("--sidebar-width"); contentTargetWidth = await getComputedPixelValue("--line-width") * 0.9; window.addEventListener('resize', () => onResize()); onResize(); } setTimeout(() => documentContainer.classList.remove("hide")); // hide the right sidebar when viewing specific file types if (rightSidebar && (embedType == "video" || embedType == "embed" || customType == "excalidraw" || customType == "kanban" || documentType == "canvas")) { if(!rightSidebar.collapsed) { rightSidebar.temporaryCollapse(); } } else { // if the right sidebar was temporarily collapsed and it is still collapsed, uncollapse it if (rightSidebar && rightSidebar.temporarilyCollapsed && rightSidebar.collapsed) { rightSidebar.collapse(false); rightSidebar.temporarilyCollapsed = false; } } parseURLParams(); relativePathname = getVaultRelativePath(loadedURL.href); } function initializePageEvents(setupOnNode) { if (!setupOnNode) return; setupHeaders(setupOnNode); setupTrees(setupOnNode); setupLists(setupOnNode); setupCallouts(setupOnNode); setupCheckboxes(setupOnNode); setupCanvas(setupOnNode); setupCodeblocks(setupOnNode); setupLinks(setupOnNode); setupScroll(setupOnNode); } function initializeDocumentTypes(fromDocument) { if (fromDocument.querySelector(".document-container > .markdown-preview-view")) documentType = "markdown"; else if (fromDocument.querySelector(".canvas-wrapper")) documentType = "canvas"; else { documentType = "custom"; if (fromDocument.querySelector(".kanban-plugin")) customType = "kanban"; else if (fromDocument.querySelector(".excalidraw-plugin")) customType = "excalidraw"; } } 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 } } window.onload = async function() { await initializePage(); initializePageEvents(document); setActiveDocument(loadedURL, true, false, false); fullyInitialized = true; }; window.onpopstate = function(event) { event.preventDefault(); event.stopPropagation(); if (document.body.classList.contains("floating-sidebars") && (!leftSidebar.collapsed || !rightSidebar.collapsed)) { leftSidebar.collapse(true); rightSidebar.collapse(true); return; } loadDocument(getURLPath(), false, true); console.log("Popped state: " + getURLPath()); } //#endregion //#region ----------------- Resize ----------------- function onEndResize() { document.body.classList.toggle("resizing", false); } function onStartResize() { document.body.classList.toggle("resizing", true); } let lastScreenWidth = undefined; let isResizing = false; let checkStillResizingTimeout = undefined; function onResize(isInitial = false) { if (!isResizing) { onStartResize(); isResizing = true; } function widthNowInRange(low, high) { let w = window.innerWidth; return (w > low && w < high && lastScreenWidth == undefined) || ((w > low && w < high) && (lastScreenWidth <= low || lastScreenWidth >= high)); } function widthNowGreaterThan(value) { let w = window.innerWidth; return (w > value && lastScreenWidth == undefined) || (w > value && lastScreenWidth < value); } function widthNowLessThan(value) { let w = window.innerWidth; return (w < value && lastScreenWidth == undefined) || (w < value && lastScreenWidth > value); } if (widthNowGreaterThan(contentTargetWidth + sidebarDefaultWidth * 2) || widthNowGreaterThan(1025)) { deviceSize = "large-screen"; document.body.classList.toggle("floating-sidebars", false); document.body.classList.toggle("is-large-screen", true); document.body.classList.toggle("is-small-screen", false); document.body.classList.toggle("is-tablet", false); document.body.classList.toggle("is-phone", false); sidebars.forEach(function (sidebar) { sidebar.collapse(false) }); sidebarGutters.forEach(function (gutter) { gutter.collapse(false) }); } else if (widthNowInRange((contentTargetWidth + sidebarDefaultWidth) * 1, contentTargetWidth + sidebarDefaultWidth * 2) || widthNowInRange(769, 1024)) { deviceSize = "small screen"; document.body.classList.toggle("floating-sidebars", false); document.body.classList.toggle("is-large-screen", false); document.body.classList.toggle("is-small-screen", true); document.body.classList.toggle("is-tablet", false); document.body.classList.toggle("is-phone", false); sidebarGutters.forEach(function (gutter) { gutter.collapse(false) }); if (leftSidebar && rightSidebar && !leftSidebar.collapsed) { rightSidebar.collapse(true); } } else if (widthNowInRange(sidebarDefaultWidth * 2, (contentTargetWidth + sidebarDefaultWidth) * 1) || widthNowInRange(481, 768)) { deviceSize = "tablet"; document.body.classList.toggle("floating-sidebars", true); document.body.classList.toggle("is-large-screen", false); document.body.classList.toggle("is-small-screen", false); document.body.classList.toggle("is-tablet", true); document.body.classList.toggle("is-phone", false); sidebarGutters.forEach(function (gutter) { gutter.collapse(false) }); if (leftSidebar && rightSidebar && !leftSidebar.collapsed) { rightSidebar.collapse(true); } if(leftSidebar && !fullyInitialized) leftSidebar.collapse(true); } else if (widthNowLessThan(sidebarDefaultWidth * 2) || widthNowLessThan(480)) { deviceSize = "phone"; document.body.classList.toggle("floating-sidebars", true); document.body.classList.toggle("is-large-screen", false); document.body.classList.toggle("is-small-screen", false); document.body.classList.toggle("is-tablet", false); document.body.classList.toggle("is-phone", true); sidebars.forEach(function (sidebar) { sidebar.collapse(true) }); sidebarGutters.forEach(function (gutter) { gutter.collapse(false) }); } lastScreenWidth = window.innerWidth; if (checkStillResizingTimeout != undefined) clearTimeout(checkStillResizingTimeout); // wait a little bit of time and if the width is still the same then we are done resizing let screenWidthSnapshot = window.innerWidth; checkStillResizingTimeout = setTimeout(function () { if (window.innerWidth == screenWidthSnapshot) { checkStillResizingTimeout = undefined; isResizing = false; onEndResize(); } }, 200); } // #endregion //#region ----------------- Helper Functions ----------------- function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } async function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function waitUntil(condition, interval = 100, timeout = 2000) { return new Promise(resolve => { let intervalId = 0; let timeoutId = setTimeout(() => { clearInterval(intervalId); resolve(); }, timeout); intervalId = setInterval(() => { if (condition()) { clearInterval(intervalId); clearTimeout(timeoutId); resolve(); } }, interval); }); } /**Gets the bounding rect of a given element*/ function getElBounds(El) { let elRect = El.getBoundingClientRect(); let x = elRect.x; let y = elRect.y; let width = elRect.width; let height = elRect.height; let centerX = elRect.x + elRect.width / 2; let centerY = elRect.y + elRect.height / 2; return { x: x, y: y, width: width, height: height, minX: x, minY: y, maxX: x + width, maxY: y + height, centerX: centerX, centerY: centerY }; } async function getComputedPixelValue(variableName) { const tempElement = document.createElement('div'); document.body.appendChild(tempElement); tempElement.style.position = 'absolute'; tempElement.style.width = `var(${variableName})`; await new Promise(resolve => setTimeout(resolve, 10)); const computedWidth = window.getComputedStyle(tempElement).width; tempElement.remove(); return parseFloat(computedWidth); } function getPointerPosition(event) { let touches = event.touches ? Array.from(event.touches) : []; let x = touches.length > 0 ? (touches.reduce((acc, cur) => acc + cur.clientX, 0) / event.touches.length) : event.clientX; let y = touches.length > 0 ? (touches.reduce((acc, cur) => acc + cur.clientY, 0) / event.touches.length) : event.clientY; return {x: x, y: y}; } function getTouchPosition(touch) { return {x: touch.clientX, y: touch.clientY}; } function getAllChildrenRecursive(element) { let children = []; for (let i = 0; i < element.children.length; i++) { const child = element.children[i]; children.push(child); children = children.concat(getAllChildrenRecursive(child)); } return children; } function isMobile() { let check = false; (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); return check; } function isTouchCapable() { return (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)); } function downloadBlob(blob, name = 'file.txt') { if ( window.navigator && window.navigator.msSaveOrOpenBlob ) return window.navigator.msSaveOrOpenBlob(blob); // For other browsers: // Create a link pointing to the ObjectURL containing the blob. const data = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = data; link.download = name; // this is necessary as link.click() does not work on the latest firefox link.dispatchEvent( new MouseEvent('click', { bubbles: true, cancelable: true, view: window }) ); setTimeout(() => { // For Firefox it is necessary to delay revoking the ObjectURL window.URL.revokeObjectURL(data); link.remove(); }, 100); } function extentionToTag(extention) { if (["png", "jpg", "jpeg", "svg", "gif", "bmp", "ico"].includes(extention)) return "img"; if (["mp4", "mov", "avi", "webm", "mpeg"].includes(extention)) return "video"; if (["mp3", "wav", "ogg", "aac"].includes(extention)) return "audio"; if (["pdf"].includes(extention)) return "embed"; return; } 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 => { if (!target) return; 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 => { if (!target) return; 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 => { if (!target) return; 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 => { if (!target) return; 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); } } function getURLExtention(url) { return url.split(".").pop().split("?")[0].split("#")[0].toLowerCase().trim(); } //#endregion //#region ----------------- Loading & Paths ----------------- let transferDocument = document.implementation.createHTMLDocument(); let loading = false; async function loadDocument(url, changeURL, showInTree) { url = decodeURI(url); if (loading) { console.log("Already loading document."); return; } loading = true; let newLoadedURL = new URL(url, absoluteBasePath); relativePathname = getVaultRelativePath(newLoadedURL.href); console.log("Loading document: ", newLoadedURL.pathname); if (newLoadedURL.pathname == loadedURL?.pathname ?? "") { console.log("Document already loaded."); loadedURL = newLoadedURL; setActiveDocument(loadedURL, false, false); await initializePage(); loading = false; return; } loadedURL = newLoadedURL; let pathname = loadedURL.pathname; await showLoading(true); let response; try { response = await fetch(pathname); } catch (error) { window.location.assign(pathname); loading = false; return; } if (response.ok) { setActiveDocument(loadedURL, showInTree, changeURL); let extention = getURLExtention(url); if (extention == "/") extention = "html"; // if no extention assume it is html documentType = "none"; embedType = "none"; customType = "none"; if(extention == "html") { let html = (await response.text()).replaceAll("", "").replaceAll("", "").replaceAll("", ""); transferDocument.write(html); setupRootPath(transferDocument); initializeDocumentTypes(transferDocument); // copy document content into DOM let newDocumentEl = transferDocument.querySelector(".document-container"); documentContainer.innerHTML = newDocumentEl.innerHTML; // copy the outline tree into the DOM let newOutline = transferDocument.querySelector(".outline-tree"); if (outlineTree && newOutline) outlineTree.innerHTML = newOutline.innerHTML; document.title = transferDocument.title; transferDocument.close(); } else { documentType = "embed"; embedType = extentionToTag(extention); if(embedType != undefined) { let media = document.createElement(embedType); media.controls = true; media.src = url; media.style.maxWidth = "100%"; if(embedType == "embed") { media.style.width = "100%"; media.style.height = "100%"; } media.style.objectFit = "contain"; viewContent.innerHTML = ""; viewContent.setAttribute("class", "view-content embed"); viewContent.appendChild(media); if (document.querySelector(".outline-tree")) document.querySelector(".outline-tree").innerHTML = ""; document.title = url.split("/").pop(); } else // just download the file { let blob = await response.blob(); downloadBlob(blob, url.split("/").pop()); } } await initializePage(); initializePageEvents(documentContainer); initializePageEvents(outlineTree); } else { pageNotFound(viewContent); } await showLoading(false); loading = false; return; } function setActiveDocument(url, showInTree, changeURL, animate = true) { let relativePath = getVaultRelativePath(url.href); let decodedRelativePath = decodeURI(relativePath); let searchlessHeaderlessPath = decodedRelativePath.split("#")[0].split("?")[0].replace("\"", "\\\"").replace("\'", "\\\'"); if (searchlessHeaderlessPath == "/" || searchlessHeaderlessPath == "") searchlessHeaderlessPath = "index.html"; // switch active file in file tree let oldActiveTreeItem = document.querySelector(".file-tree .tree-item.mod-active"); let newActiveTreeItem = document.querySelector(`.file-tree .tree-item:has(>.tree-link[href^="${searchlessHeaderlessPath}"])`); if(newActiveTreeItem && !newActiveTreeItem.isEqualNode(oldActiveTreeItem)) { oldActiveTreeItem?.classList.remove("mod-active"); newActiveTreeItem.classList.add("mod-active"); if(showInTree) scrollIntoView(newActiveTreeItem, {block: "center", inline: "nearest"}, animate); } // set the active file in the graph view if(typeof graphData != 'undefined' && window.graphRenderer) { let activeNode = graphData?.paths.findIndex(function(item) { return item.endsWith(searchlessHeaderlessPath); }) ?? -1; if(activeNode >= 0) { window.graphRenderer.activeNode = activeNode; } } console.log("Active document: " + changeURL); if(changeURL && window.location.protocol != "file:") { window.history.pushState({ path: relativePath }, '', relativePath); console.log("Pushed state: " + relativePath); } } function parseURLParams() { const highlightParam = loadedURL.searchParams.get('mark'); const searchParam = loadedURL.searchParams.get('query'); const hashParam = decodeURI(loadedURL.hash); if (highlightParam) { searchCurrentDocument(highlightParam); } if (searchParam) { search(searchParam); } if (hashParam) { const headingTarget = document.getElementById(hashParam.substring(1)); if (headingTarget) { scrollIntoView(headingTarget, { behavior: "smooth", block: "start"}); } else { console.log("Heading not found: " + hashParam); } } } async function showLoading(loading) { documentContainer.style.transitionDuration = ""; loadingIcon.classList.toggle("show", loading); documentContainer.classList.toggle("hide", loading); if(loading) { // position loading icon in the center of the screen let viewBounds = getViewBounds(); loadingIcon.style.left = (viewBounds.centerX - loadingIcon.offsetWidth / 2) + "px"; loadingIcon.style.top = (viewBounds.centerY - loadingIcon.offsetHeight / 2) + "px"; // hide the left sidebar if on phone if (deviceSize == "phone") leftSidebar.collapse(true); } await delay(200); } function pageNotFound(viewContent) { viewContent.innerHTML = `

Page Not Found

`; if (document.querySelector(".outline-tree")) document.querySelector(".outline-tree").innerHTML = ""; console.log("Page not found: " + absoluteBasePath + loadedURL.pathname); let newRootPath = getURLRootPath(absoluteBasePath + loadedURL.pathname); relativeBasePath = newRootPath; document.querySelector("base").href = newRootPath; document.title = "Page Not Found"; } function setupRootPath(fromDocument) { let rootEl = fromDocument.getElementById("root-path"); if (!rootEl) return; let basePath = rootEl.getAttribute("root-path"); let newBase = document.createElement("base"); newBase.href = basePath; console.log("Setting root path: " + basePath); document.querySelector("base").replaceWith(newBase); document.querySelector("#root-path").setAttribute("root-path", basePath); relativeBasePath = basePath; absoluteBasePath = new URL(basePath, window.location.href).href; } function getURLPath(url = window.location.pathname) { if (absoluteBasePath == undefined) setupRootPath(document); let pathname = url.replace(absoluteBasePath, ""); 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; } function getVaultRelativePath(absolutePath) { return absolutePath.replace(absoluteBasePath, "") } //#endregion //#region ----------------- Headers ----------------- function setupHeaders(setupOnNode) { setupOnNode.querySelectorAll(".heading-collapse-indicator").forEach(function (element) { element.addEventListener("click", function () { toggleTreeHeaderOpen(element.parentElement.parentElement, true); }); }); setupOnNode.querySelectorAll(".heading-wrapper").forEach(function (element) { element.collapsed = false; element.childrenContainer = element.querySelector(".heading-children"); element.parentHeader = element.parentElement.parentElement; element.headerElement = element.querySelector(".heading"); element.markdownPreviewSizer = getHeaderSizerEl(element); element.collapse = function (collapse, openParents = true, instant = false) { collapseHeader(element, collapse, openParents, instant) }; element.toggleCollapse = function (openParents = true) { toggleTreeHeaderOpen(element, openParents) }; element.hide = function () { hideHeader(element) }; element.show = function (parents = false, children = false, forceStay = false) { showHeader(element, parents, children, forceStay) }; }); setupOnNode.querySelectorAll(".heading").forEach(function (element) { element.headingWrapper = element.parentElement; }); } function isHeadingWrapper(headingWrapper) { if (!headingWrapper) return false; return headingWrapper.classList.contains("heading-wrapper"); } function getHeaderSizerEl(headingWrapper) { // go up the tree until we find a markdown-preview-sizer let parent = headingWrapper; while (parent && !parent.classList.contains("markdown-preview-sizer")) parent = parent.parentElement; if (parent) return parent; else return; } async function collapseHeader(headingWrapper, collapse, openParents = true, instant = false) { let collapseContainer = headingWrapper.childrenContainer; if (openParents && !collapse) { let parent = headingWrapper.parentHeader; if (isHeadingWrapper(parent)) parent.collapse(false, true, instant); } let needsChange = headingWrapper.classList.contains("is-collapsed") != collapse; if (!needsChange) { // if opening show the header if (!collapse && documentType == "canvas") headingWrapper.show(true); return; } if (headingWrapper.timeout) { clearTimeout(headingWrapper.timeout); collapseContainer.style.transitionDuration = ""; collapseContainer.style.height = ""; headingWrapper.classList.toggle("is-animating", false); } if (collapse) { headingWrapper.collapseHeight = collapseContainer.offsetHeight + parseFloat(collapseContainer.lastChild?.marginBottom || 0); // show all sibling headers after this one // this is so that when the header slides down you aren't left with a blank space let next = headingWrapper.nextElementSibling; while (next && documentType == "canvas") { let localNext = next; // force show the sibling header for 500ms while this one is collapsing if (isHeadingWrapper(localNext)) localNext.show(false, true, true); setTimeout(function() { localNext.forceShown = false; }, 500); next = next.nextElementSibling; } } let height = headingWrapper.collapseHeight; collapseContainer.style.height = height + "px"; // if opening show the header if (!collapse && documentType == "canvas") headingWrapper.show(true); headingWrapper.collapsed = collapse; function adjustSizerHeight(customHeight = undefined) { if (customHeight != undefined) headingWrapper.markdownPreviewSizer.style.minHeight = customHeight + "px"; else { let newTotalHeight = Array.from(headingWrapper.markdownPreviewSizer.children).reduce((acc, cur) => acc + cur.offsetHeight, 0); headingWrapper.markdownPreviewSizer.style.minHeight = newTotalHeight + "px"; } } if (instant) { collapseContainer.style.transitionDuration = "0s"; headingWrapper.classList.toggle("is-collapsed", collapse); collapseContainer.style.height = ""; collapseContainer.style.transitionDuration = ""; adjustSizerHeight() return; } // get the length of the height transition on heading container and wait for that time before not displaying the contents let transitionDuration = getComputedStyle(collapseContainer).transitionDuration; if (transitionDuration.endsWith("s")) transitionDuration = parseFloat(transitionDuration); else if (transitionDuration.endsWith("ms")) transitionDuration = parseFloat(transitionDuration) / 1000; else transitionDuration = 0; // multiply the duration by the height so that the transition is the same speed regardless of the height of the header let transitionDurationMod = Math.min(transitionDuration * Math.sqrt(height) / 16, 0.5); // longest transition is 0.5s collapseContainer.style.transitionDuration = `${transitionDurationMod}s`; if (collapse) collapseContainer.style.height = "0px"; else collapseContainer.style.height = height + "px"; headingWrapper.classList.toggle("is-animating", true); headingWrapper.classList.toggle("is-collapsed", collapse); if (headingWrapper.markdownPreviewSizer.closest(".markdown-embed")) // dont change the size of transcluded docments { adjustSizerHeight(collapse ? 0 : undefined); } setTimeout(function() { collapseContainer.style.transitionDuration = ""; if(!collapse) collapseContainer.style.height = ""; headingWrapper.classList.toggle("is-animating", false); adjustSizerHeight() }, transitionDurationMod * 1000); } function toggleTreeHeaderOpen(headingWrapper, openParents = true) { headingWrapper.collapse(!headingWrapper.collapsed, openParents); } /**Hides everything in a header and then makes the header div take up the same space as the header element */ function hideHeader(headingWrapper) { if(headingWrapper.forceShown) return; if(headingWrapper.classList.contains("is-hidden") || headingWrapper.classList.contains("is-collapsed")) return; if(getComputedStyle(headingWrapper).display == "none") return; let height = headingWrapper.offsetHeight; headingWrapper.classList.toggle("is-hidden", true); if (height != 0) headingWrapper.style.height = height + "px"; headingWrapper.style.visibility = "hidden"; } /**Restores a hidden header back to it's normal function */ function showHeader(headingWrapper, showParents = true, showChildren = false, forceStayShown = false) { if (forceStayShown) headingWrapper.forceShown = true; if (showParents) { let parent = headingWrapper.parentHeader; if (isHeadingWrapper(parent)) parent.show(true, false, forceStayShown); } if (showChildren) { let children = headingWrapper.querySelectorAll(".heading-wrapper"); children.forEach(function(child) { child.show(false, true, forceStayShown); }); } if(!headingWrapper.classList.contains("is-hidden") || headingWrapper.classList.contains("is-collapsed")) return; headingWrapper.classList.toggle("is-hidden", false); headingWrapper.style.height = ""; headingWrapper.style.visibility = ""; } //#endregion //#region ----------------- Trees ----------------- function setupTrees(setupOnNode) { setupOnNode.querySelectorAll(".collapse-tree-button").forEach(function(button) { button.treeRoot = button.closest(".tree-container"); button.icon = button.firstChild; button.icon.innerHTML = ""; button.setIcon = function(collapse) { button.icon.children[0].setAttribute("d", collapse ? collapseIconUp[0] : collapseIconDown[0]); button.icon.children[1].setAttribute("d", collapse ? collapseIconUp[1] : collapseIconDown[1]); } button.collapse = function(collapse) { let treeItems = button.treeRoot.classList.contains("file-tree") ? fileTreeItems : outlineTreeItems; setTreeCollapsedAll(treeItems, collapse); button.setIcon(collapse); button.collapsed = collapse; }; button.toggleCollapse = function() { button.collapse(!button.collapsed); }; button.toggleState = function(state) { if (state === undefined) state = !button.collapsed; button.collapsed = state; button.setIcon(state); }; button.addEventListener("click", function(event) { event.preventDefault(); event.stopPropagation(); button.toggleCollapse(); return false; }); // if any outline items are unncollapsed, toggle collapse all button state let treeItems = button.treeRoot.classList.contains("file-tree") ? fileTreeItems : outlineTreeItems; if (treeItems.some(item => !item.classList.contains("is-collapsed") && item.classList.contains("mod-collapsible"))) { button.toggleState(false); } }); let fileTreeClick = Array.from(setupOnNode.querySelectorAll(".tree-container.file-tree .tree-item:has(.collapse-icon) > .tree-link")); let outlineTreeClick = Array.from(setupOnNode.querySelectorAll(".tree-container.outline-tree .tree-item:has(.collapse-icon) > .tree-link .collapse-icon")); let collapsable = Array.from(fileTreeClick).concat(Array.from(outlineTreeClick)); for (let item of collapsable) { let closestItem = item?.closest(".tree-item"); if (closestItem && item) item?.addEventListener("click", function(event) { event.preventDefault(); event.stopPropagation(); toggleTreeCollapsed(closestItem); }); } } async function setTreeCollapsed(element, collapsed, animate = true, openParents = true) { if (!element.classList.contains("mod-collapsible")) element = element.closest(".mod-collapsible"); if (!element || !element.classList.contains("mod-collapsible")) { return; } if (element.classList.contains("is-collapsed") == collapsed) { return; } if (openParents) { let parent = element.parentElement.closest(".mod-collapsible"); if (parent) await setTreeCollapsed(parent, false, animate, openParents); } 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.display = ""; // make close all button collapse the tree instead of opening it if it's already open let treeContainer = element.closest(".tree-container"); if (treeContainer) { let collapseButton = treeContainer.querySelector(".collapse-tree-button"); if (collapseButton) collapseButton.toggleState(false); } } } 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) => { if(children) children.style.display = "none"; }); } else { if(animate) slideDownAll(childrenList, 100); else childrenList.forEach(async (children) => { if(children) children.style.display = ""; }); } } function toggleTreeCollapsed(element) { element = element.closest(".tree-item"); 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 getFileTreeItemFromPath(path) { return document.querySelector(`.file-tree .tree-item:has(> .tree-link[href^="${path}"])`); } // hide all files and folder except the ones in the list (show parents of shown files) async function filterFileTree(showPathList, hintLabelLists, query, openFileTree = true) { if (openFileTree) await setTreeCollapsedAll(fileTreeItems, false, false); // hide all files and folders let allItems = Array.from(document.querySelectorAll(".file-tree .tree-item:not(.filtered-out)")); for await (let item of allItems) { item.classList.add("filtered-out"); } await removeTreeHintLabels(); for (let i = 0; i < showPathList.length; i++) { let path = showPathList[i]; let hintLabels = hintLabelLists[i]; let treeItem = getFileTreeItemFromPath(path); if (treeItem) { // show the file and it's parent tree items treeItem.classList.remove("filtered-out"); let itemLink = treeItem.querySelector(".tree-link"); if(itemLink) itemLink.href = path + "?mark=" + query; let parent = treeItem.parentElement.closest(".tree-item"); while (parent) { parent.classList.remove("filtered-out"); parent = parent.parentElement.closest(".tree-item"); } if (hintLabels.length > 0) { let treeLink = treeItem.querySelector(".tree-link"); let hintContainer = treeLink.appendChild(document.createElement("div")); hintContainer.classList.add("tree-hint-container"); function createHintLabel(text, link) { text = text.trim(); if (text == "") return; let hintLabelEl = document.createElement("a"); hintLabelEl.classList.add("tree-hint-label"); hintLabelEl.classList.add("internal-link"); hintLabelEl.textContent = text; hintLabelEl.href = decodeURI(link).replaceAll(" ", "_"); hintContainer.appendChild(hintLabelEl); } // create the hint labels for (let label of hintLabels) { createHintLabel(label, path + "#" + label); } setupLinks(hintContainer); } } } } async function clearFileTreeFilter(closeFileTree = true) { await removeTreeHintLabels(); let filteredItems = document.querySelectorAll(".file-tree .filtered-out"); for await (let item of filteredItems) { item.classList.remove("filtered-out"); } let markItems = document.querySelectorAll(".file-tree .tree-link[href*='?mark=']"); for await (let item of markItems) { let href = item.href.split("?")[0]; href = getVaultRelativePath(href); item.href = href; } if (closeFileTree) await setTreeCollapsedAll(fileTreeItems, true, false); } async function removeTreeHintLabels() { let hintLabels = document.querySelectorAll(".tree-hint-container"); for await (let item of hintLabels) { item.remove(); } } function sortFileTreeDocuments(sortByFunction) { let treeItems = Array.from(document.querySelectorAll(".file-tree .tree-item.mod-tree-file:not(.filtered-out)")); treeItems.sort(sortByFunction); // sort the files within their parent folders for (let i = 1; i < treeItems.length; i++) { let item = treeItems[i]; let lastItem = treeItems[i - 1]; if (item.parentElement == lastItem.parentElement) { lastItem.after(item); } } // sort the folders using their contents let folders = Array.from(document.querySelectorAll(".file-tree .tree-item.mod-tree-folder:not(.filtered-out)")); folders.sort(function (a, b) { let aFirst = a.querySelector(".tree-item.mod-tree-file:not(.filtered-out)"); let bFirst = b.querySelector(".tree-item.mod-tree-file:not(.filtered-out)"); return treeItems.indexOf(aFirst) - treeItems.indexOf(bFirst); }); // sort the folders within their parent folders for (let i = 1; i < folders.length; i++) { let item = folders[i]; let foundPlace = false; // iterate backwards until we find an item with the same parent for (let j = i - 1; j >= 0; j--) { let lastItem = folders[j]; if (item.parentElement == lastItem.parentElement) { lastItem.after(item); foundPlace = true; break; } } // if we didn't find an item with the same parent, move it to the top if (!foundPlace) { item.parentElement.prepend(item); } } } function sortFileTree(sortByFunction) { let treeItems = Array.from(document.querySelectorAll(".file-tree .tree-item.mod-tree-file:not(.filtered-out)")); treeItems.sort(sortByFunction); // sort the files within their parent folders for (let i = 1; i < treeItems.length; i++) { let item = treeItems[i]; let lastItem = treeItems[i - 1]; if (item.parentElement == lastItem.parentElement) { lastItem.after(item); } } // sort the folders using their contents let folders = Array.from(document.querySelectorAll(".file-tree .tree-item.mod-tree-folder:not(.filtered-out)")); folders.sort(sortByFunction); // sort the folders within their parent folders for (let i = 1; i < folders.length; i++) { let item = folders[i]; let foundPlace = false; // iterate backwards until we find an item with the same parent for (let j = i - 1; j >= 0; j--) { let lastItem = folders[j]; if (item.parentElement == lastItem.parentElement) { lastItem.after(item); foundPlace = true; break; } } // if we didn't find an item with the same parent, move it to the top if (!foundPlace) { item.parentElement.prepend(item); } } } function sortFileTreeAlphabetically(reverse = false) { sortFileTree(function (a, b) { const aTitle = a.querySelector(".tree-item-title"); const bTitle = b.querySelector(".tree-item-title"); if (!aTitle || !bTitle) return 0; const aName = aTitle.textContent.toLowerCase(); const bName = bTitle.textContent.toLowerCase(); return aName.localeCompare(bName, undefined, { numeric: true }) * (reverse ? -1 : 1); }); } //#endregion //#region ----------------- Lists ----------------- function setupLists(setupOnNode) { let listCollpaseIcons = Array.from(setupOnNode.querySelectorAll(".list-collapse-indicator")); for (let i = 0; i < listCollpaseIcons.length; i++) { let icon = listCollpaseIcons[i]; icon.addEventListener("click", function (event) { let listItem = icon.closest("li"); if (listItem) { listItem.classList.toggle("is-collapsed"); icon.classList.toggle("is-collapsed"); } }); } } //#endregion //#region ----------------- Canvas ----------------- function setupCanvas(setupOnNode) { if(documentType != "canvas" || !setupOnNode.querySelector(".canvas-wrapper")) return; // initialize canvas tranformations setupOnNode.querySelector(".canvas")?.setAttribute("style", "translate: 0px 1px; scale: 1;"); let nodeBounds = getNodesBounds(); setViewCenter(nodeBounds.centerX, nodeBounds.centerY); // let nodes be focused when hovered over setupOnNode.querySelectorAll(".canvas-node-container").forEach(function (element) { var parent = element.parentElement; function onEnter(event) { parent.classList.toggle("is-focused"); if (focusedCanvasNode != null && focusedCanvasNode != parent) { focusedCanvasNode.classList.remove("is-focused"); focusedCanvasNode.querySelector(".canvas-node-container").style.display = ""; } focusedCanvasNode = parent; parent.addEventListener("mouseleave", onLeave); parent.addEventListener("touchend", onLeave); } function onLeave(event) { if (focusedCanvasNode) { focusedCanvasNode.classList.remove("is-focused"); focusedCanvasNode = null; } parent.removeEventListener("mouseleave", onLeave); parent.removeEventListener("touchend", onLeave); } element.addEventListener("mouseenter", onEnter); element.addEventListener("touchstart", onEnter); }); // make nodes fit to view when double clicked setupOnNode.querySelectorAll(".canvas-node").forEach(function (element) { element.addEventListener("dblclick", function (event) { fitViewToNode(element); }); }); // make whole canvas fit to view when double clicked on background setupOnNode.querySelectorAll(".canvas-background").forEach(function (element) { element.addEventListener("dblclick", function (event) { fitViewToCanvas(); }); }); // make canvas draggable / panable canvasWrapper.addEventListener("mousedown", canvasWrapperMouseDownHandler); canvasWrapper.addEventListener("touchstart", canvasWrapperMouseDownHandler); let scrollInterferance = false; function canvasWrapperMouseDownHandler(mouseDownEv) { let touchesDown = mouseDownEv.touches ?? []; scrollInterferance = false; // if there is already one tough down we don't want to start another mouse down event // extra fingers are already being handled in the move event below if (touchesDown.length > 1) return; if (mouseDownEv.button == 1 || mouseDownEv.button == 0 || touchesDown.length > 0) { let lastPointerPos = getPointerPosition(mouseDownEv); let startZoom = false; let lastDistance = 0; let lastTouchCount = touchesDown.length; let mouseMoveHandler = function (mouseMoveEv) { let touchesMove = mouseMoveEv.touches ?? []; let pointer = getPointerPosition(mouseMoveEv); if (lastTouchCount != touchesMove.length) { lastPointerPos = pointer; lastTouchCount = touchesMove.length; } let deltaX = pointer.x - lastPointerPos.x; let deltaY = pointer.y - lastPointerPos.y; if ((mouseDownEv.button == 1 || touchesMove.length == 1) && focusedCanvasNode) { let mouseHoriz = Math.abs(deltaX) > Math.abs(deltaY * 1.5); let mouseVert = Math.abs(deltaY) > Math.abs(deltaX * 1.5); // only skip if the focused node can be scrolled in the direction of mouse movement let sizer = focusedCanvasNode.querySelector(".markdown-preview-sizer"); if(sizer) { let scrollableVert = sizer.scrollHeight > sizer.parentElement.clientHeight + 1; let scrollableHoriz = sizer.scrollWidth > sizer.parentElement.clientWidth + 1; if (((mouseHoriz && scrollableHoriz) || (mouseVert && scrollableVert)) && (window?.navigator?.platform?.startsWith("Win") ?? true)) { scrollInterferance = true; } else { scrollInterferance = false; } } } if (mouseDownEv.button == 0 && focusedCanvasNode) { if (focusedCanvasNode.querySelector(".canvas-node-content").textContent.trim() != "") { scrollInterferance = true; } } if (!scrollInterferance) { translateCanvas(deltaX, deltaY); lastPointerPos = pointer; } if (touchesMove.length == 2) { let touchCenter = getPointerPosition(mouseMoveEv, false); let touch1 = getTouchPosition(mouseMoveEv.touches[0]); let touch2 = getTouchPosition(mouseMoveEv.touches[1]); let distance = Math.sqrt(Math.pow(touch1.x - touch2.x, 2) + Math.pow(touch1.y - touch2.y, 2)); if (!startZoom) { startZoom = true; lastDistance = distance; } let distanceDelta = distance - lastDistance; let scaleDelta = distanceDelta / lastDistance; scaleCanvasAroundPoint(1 + scaleDelta, touchCenter.x, touchCenter.y); lastDistance = distance; } }; let mouseUpHandler = function (mouseUpEv) { document.body.removeEventListener("mousemove", mouseMoveHandler); document.body.removeEventListener("mouseup", mouseUpHandler); document.body.removeEventListener("mouseenter", mouseEnterHandler); document.body.removeEventListener("touchmove", mouseMoveHandler); document.body.removeEventListener("touchend", mouseUpHandler); document.body.removeEventListener("touchcancel", mouseUpHandler); scrollInterferance = false; }; let mouseEnterHandler = function (mouseEnterEv) { if (mouseEnterEv.buttons == 1 || mouseEnterEv.buttons == 4) return; mouseUpHandler(mouseEnterEv); } document.body.addEventListener("mousemove", mouseMoveHandler); document.body.addEventListener("mouseup", mouseUpHandler); document.body.addEventListener("mouseenter", mouseEnterHandler); document.body.addEventListener("touchmove", mouseMoveHandler); document.body.addEventListener("touchend", mouseUpHandler); document.body.addEventListener("touchcancel", mouseUpHandler); } } // get mouse position on the canvas let mouseX = 0; let mouseY = 0; canvasWrapper.addEventListener("mousemove", function (event) { let pointer = getPointerPosition(event); mouseX = pointer.x; mouseY = pointer.y; }); let scale = 1; let speed = 0; let instant = false; // make canvas zoomable canvasWrapper.addEventListener("wheel", function (event) { if (focusedCanvasNode) { // only skip if the focused node can be scrolled let sizer = focusedCanvasNode.querySelector(".markdown-preview-sizer"); if(sizer && sizer.scrollHeight > sizer.parentElement.clientHeight) return; } event.preventDefault(); event.stopPropagation(); if(instant) { let scale = 1; scale -= event.deltaY / 700 * scale; scale = clamp(scale, 0.1, 10); let viewBounds = getViewBounds(); scaleCanvasAroundPoint(scale, viewBounds.centerX, viewBounds.centerY); } else { let isFirstFrame = speed == 0; speed -= (event.deltaY / 200); const maxSpeed = 0.14 * scale; speed = clamp(speed, -maxSpeed, maxSpeed); if (isFirstFrame) requestAnimationFrame(scrollAnimation); } }); let dt = 0; let lastTime = 0; let averageDt = 0; function scrollAnimation(currentTime) { dt = currentTime - lastTime; if (lastTime == 0) dt = 30; lastTime = currentTime; averageDt = dt * 0.05 + averageDt * 0.95; if (averageDt > 50) { console.log("Scrolling too slow, turning on instant scroll"); instant = true; return; } let oldScale = scale; scale += speed * (dt / 1000) * 30; scale = clamp(scale, 0.1, 10); let viewBounds = getViewBounds(); scaleCanvasAroundPoint(scale / oldScale, mouseX, mouseY); speed *= 0.4; if (Math.abs(speed) < 0.01) { speed = 0; lastTime = 0; } else requestAnimationFrame(scrollAnimation); } // fit all nodes to view on initialization after centering the camera // after any animations have possibly played setTimeout(fitViewToCanvas, 300); } /**Gets the bounding rect of the voew-content or markdown-preview-sizer*/ function getViewBounds() { let viewContentRect = viewContent.getBoundingClientRect(); let minX = viewContentRect.x; let minY = viewContentRect.y; let maxX = viewContentRect.x + viewContentRect.width; let maxY = viewContentRect.y + viewContentRect.height; let centerX = viewContentRect.x + viewContentRect.width / 2; let centerY = viewContentRect.y + viewContentRect.height / 2; return { x: minX, y: minY, width: maxX - minX, height: maxY - minY, minX: minX, minY: minY, maxX: maxX, maxY: maxY, centerX: centerX, centerY: centerY }; } /**Gets the bounding rect of all nodes in the canvas*/ function getNodesBounds() { let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; canvasNodes.forEach(function (node) { let nodeRect = node.getBoundingClientRect(); if (nodeRect.x < minX) minX = nodeRect.x; if (nodeRect.y < minY) minY = nodeRect.y; if (nodeRect.x + nodeRect.width > maxX) maxX = nodeRect.x + nodeRect.width; if (nodeRect.y + nodeRect.height > maxY) maxY = nodeRect.y + nodeRect.height; }); let x = minX; let y = minY; let width = maxX - minX; let height = maxY - minY; let centerX = minX + width / 2; let centerY = minY + height / 2; return { x: x, y: y, width: width, height: height, minX: minX, minY: minY, maxX: maxX, maxY: maxY, centerX: centerX, centerY: centerY }; } function getCanvasBounds() { let canvasRect = canvas.getBoundingClientRect(); let x = canvasRect.x; let y = canvasRect.y; let width = canvasRect.width; let height = canvasRect.height; let centerX = canvasRect.x + canvasRect.width / 2; let centerY = canvasRect.y + canvasRect.height / 2; return { x: x, y: y, width: width, height: height, minX: x, minY: y, maxX: x + width, maxY: y + height, centerX: centerX, centerY: centerY }; } /**Sets the relative scale of the canvas around a point*/ function scaleCanvasAroundPoint(scaleBy, aroundPointX, aroundPointY) { let canvasBounds = getCanvasBounds(); let xCenterToTarget = aroundPointX - canvasBounds.x; let yCenterToTarget = aroundPointY - canvasBounds.y; let xCenterPin = canvasBounds.x + xCenterToTarget * scaleBy; let yCenterPin = canvasBounds.y + yCenterToTarget * scaleBy; let offsetX = aroundPointX - xCenterPin; let offsetY = aroundPointY - yCenterPin; scaleCanvas(scaleBy); translateCanvas(offsetX, offsetY); return { x: offsetX, y: offsetY }; } /**Sets the relative scale of the canvas*/ function scaleCanvas(scaleBy) { let newScale = Math.max(scaleBy * canvas.style.scale, 0.001); canvas.style.scale = newScale; canvasWrapper.style.setProperty("--zoom-multiplier", (1/(Math.sqrt(newScale))) ); } /**Sets the relative translation of the canvas*/ function translateCanvas(x, y) { let translate = canvas.style.translate; let split = translate.split(" "); let translateX = split.length > 0 ? parseFloat(translate.split(" ")[0].trim()) : 0; let translateY = split.length > 1 ? parseFloat(translate.split(" ")[1].trim()) : translateX; canvas.style.translate = `${translateX + x}px ${translateY + y}px`; } /**Sets the absolute center of the view*/ function setViewCenter(x, y) { let viewContentRect = getViewBounds(); let deltaX = viewContentRect.centerX - x; let deltaY = viewContentRect.centerY - y; translateCanvas(deltaX, deltaY); } function getCanvasTranslation() { let translate = canvas.style.translate; let split = translate.split(" "); let translateX = split.length > 0 ? parseFloat(translate.split(" ")[0].trim()) : 0; let translateY = split.length > 1 ? parseFloat(translate.split(" ")[1].trim()) : translateX; return { x: translateX, y: translateY }; } /**Sets the absolute scale of the canvas background pattern*/ function scaleCanvasBackground(scaleBy) { let scaleX = scaleBy * canvasBackgroundPattern.getAttribute("width"); let scaleY = scaleBy * canvasBackgroundPattern.getAttribute("height"); canvasBackgroundPattern.setAttribute("width", scaleX); canvasBackgroundPattern.setAttribute("height", scaleY); } /**Sets the absolute translation of the canvas background pattern*/ function translateCanvasBackground(x, y) { canvasBackgroundPattern.setAttribute("x", x + canvasBackgroundPattern.getAttribute("x")); canvasBackgroundPattern.setAttribute("y", y + canvasBackgroundPattern.getAttribute("y")); } /**Fits the view to contain the given node*/ function fitViewToNode(node) { let nodeRect = getElBounds(node); let viewContentRect = getViewBounds(); let canvasBounds = getCanvasBounds(); let scale = 0.8 * Math.min(viewContentRect.width/nodeRect.width, viewContentRect.height/nodeRect.height); let canvasX = canvasBounds.x; let canvasY = canvasBounds.y; let canvToNodeX = (nodeRect.centerX - canvasX) * scale; let canvToNodeY = (nodeRect.centerY - canvasY) * scale; let newNodeX = canvasX + canvToNodeX; let newNodeY = canvasY + canvToNodeY; let deltaX = viewContentRect.centerX - newNodeX; let deltaY = viewContentRect.centerY - newNodeY; nodeRect = getElBounds(node); canvas.style.transition = "scale 0.5s cubic-bezier(0.5, -0.1, 0.5, 1.1), translate 0.5s cubic-bezier(0.5, -0.1, 0.5, 1.1)"; scaleCanvas(scale); translateCanvas(deltaX, deltaY); setTimeout(function() { canvas.style.transition = ""; }, 550); } /**Fits the view to contain all nodes in the graph*/ function fitViewToCanvas() { let nodesRect = getNodesBounds(); let viewContentRect = getViewBounds(); let canvasBounds = getCanvasBounds(); let scale = 0.8 * Math.min(viewContentRect.width/nodesRect.width, viewContentRect.height/nodesRect.height); let canvasX = canvasBounds.x; let canvasY = canvasBounds.y; let canvToNodeX = (nodesRect.centerX - canvasX) * scale; let canvToNodeY = (nodesRect.centerY - canvasY) * scale; let newNodeX = canvasX + canvToNodeX; let newNodeY = canvasY + canvToNodeY; let deltaX = viewContentRect.centerX - newNodeX; let deltaY = viewContentRect.centerY - newNodeY; canvas.style.transition = "scale 0.5s cubic-bezier(0.5, -0.1, 0.5, 1.1), translate 0.5s cubic-bezier(0.5, -0.1, 0.5, 1.1)"; scaleCanvas(scale); translateCanvas(deltaX, deltaY); setTimeout(function() { canvas.style.transition = ""; }, 550); } //#endregion //#region ----------------- Callouts ----------------- 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; parent.classList.toggle("is-collapsed"); element.querySelector(".callout-fold").classList.toggle("is-collapsed"); slideToggle(parent.querySelector(".callout-content"), 100); }); }); } //#endregion //#region ----------------- Checkboxes ----------------- 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; }); } //#endregion //#region ----------------- Code Blocks ----------------- 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); }); }); } //#endregion //#region ----------------- Links ----------------- function setupLinks(setupOnNode) { setupOnNode.querySelectorAll(".internal-link, a.tag, .tree-link, .footnote-link").forEach(function(link) { link.addEventListener("click", function(event) { let target = link.getAttribute("href"); event.preventDefault(); event.stopPropagation(); if(!target) { console.log("No target found for link"); return; } let relativePathnameStrip = relativePathname.split("#")[0].split("?")[0]; if(target.startsWith("#") || target.startsWith("?")) target = relativePathnameStrip + target; loadDocument(target, true, !link.classList.contains("tree-link")); }); }); } //#endregion //#region ----------------- Sidebars ----------------- function setupSidebars() { if (!rightSidebar || !leftSidebar) return; //#region sidebar object references sidebarCollapseIcons[0].otherIcon = sidebarCollapseIcons[1]; sidebarCollapseIcons[1].otherIcon = sidebarCollapseIcons[0]; sidebarCollapseIcons[0].gutter = sidebarGutters[0]; sidebarCollapseIcons[1].gutter = sidebarGutters[1]; sidebarCollapseIcons[0].sidebar = sidebars[0]; sidebarCollapseIcons[1].sidebar = sidebars[1]; sidebarGutters[0].otherGutter = sidebarGutters[1]; sidebarGutters[1].otherGutter = sidebarGutters[0]; sidebarGutters[0].collapseIcon = sidebarCollapseIcons[0]; sidebarGutters[1].collapseIcon = sidebarCollapseIcons[1]; sidebars[0].otherSidebar = sidebars[1]; sidebars[1].otherSidebar = sidebars[0]; sidebars[0].gutter = sidebarGutters[0]; sidebars[1].gutter = sidebarGutters[1]; //#endregion sidebars.forEach(function (sidebar) { sidebar.collapsed = sidebar.classList.contains("is-collapsed"); sidebar.collapse = function (collapsed = true) { if (!collapsed && this.temporarilyCollapsed && deviceSize == "large-screen") this.gutter.collapse(true); if (!collapsed && document.body.classList.contains("floating-sidebars")) { function clickOutsideCollapse(event) { // don't allow bubbling into sidebar if (event.target.closest(".sidebar")) return; sidebar.collapse(true); document.body.removeEventListener("click", clickOutsideCollapse); } document.body.addEventListener("click", clickOutsideCollapse); } // if there isn't enough space for both sidebars then close the other one if (deviceSize == "phone") { if (!collapsed) sidebar.otherSidebar.fullCollapse(true, true); if (collapsed) sidebar.gutter.otherGutter.collapse(false, true); } if (deviceSize == "tablet") { if (!collapsed) sidebar.otherSidebar.collapse(true); } this.classList.toggle("is-collapsed", collapsed); this.collapsed = collapsed; } sidebar.temporaryCollapse = function (collapsed = true) { this.temporarilyCollapsed = true; this.collapse(true); this.gutter.collapse(false); this.collapsed = collapsed; } sidebar.fullCollapse = function (collapsed = true, force = false) { this.collapse(collapsed); this.gutter.collapse(true, force); this.collapsed = collapsed; } sidebar.toggleCollapse = function () { this.collapse(!this.collapsed); } sidebar.toggleFullCollapse = function () { this.fullCollapse(!this.collapsed); } }); sidebarGutters.forEach(function (gutter) { gutter.collapsed = gutter.classList.contains("is-collapsed"); gutter.collapse = function (collapsed, force = false) { if(!force) return; this.classList.toggle("is-collapsed", collapsed); this.collapsed = collapsed; } gutter.toggleCollapse = function () { this.collapse(!this.collapsed); } }); sidebarCollapseIcons.forEach(function (icon) { icon.addEventListener("click", function (event) { event.stopPropagation(); icon.sidebar.toggleCollapse(); }); }); if (!isMobile()) setupSidebarResize(); } function setupSidebarResize() { let leftHandle = document.querySelector('.sidebar-left .sidebar-handle'); let rightHandle = document.querySelector('.sidebar-right .sidebar-handle'); if (!leftHandle || !rightHandle) return; let resizingSidebar = null; let minResizeWidth = parseFloat(getComputedStyle(leftHandle.parentElement).fontSize) * 15; let collapseWidth = minResizeWidth / 4.0; let rightWidth = localStorage.getItem('sidebar-right-width'); let leftWidth = localStorage.getItem('sidebar-left-width'); if (rightWidth) document.querySelector('.sidebar-right').style.setProperty('--sidebar-width', rightWidth); if (leftWidth) document.querySelector('.sidebar-left').style.setProperty('--sidebar-width', leftWidth); function resizeMove(e) { if (!resizingSidebar) return; let isLeft = resizingSidebar.classList.contains("sidebar-left"); var distance = isLeft ? e.clientX : window.innerWidth - e.clientX; var newWidth = `min(max(${distance}px, 15em), 40vw)`; // 15em is minResizeWidth if (distance < collapseWidth) { resizingSidebar.collapse(true); resizingSidebar.style.removeProperty('transition-duration'); } else { resizingSidebar.collapse(false); resizingSidebar.style.setProperty('--sidebar-width', newWidth); if (distance > minResizeWidth) resizingSidebar.style.transitionDuration = "0s"; } } function handleClick(e) { resizingSidebar = e.target.closest('.sidebar'); resizingSidebar.classList.add('is-resizing'); document.addEventListener('pointermove', resizeMove); document.addEventListener('pointerup', function () { document.removeEventListener('pointermove', resizeMove); var finalWidth = getComputedStyle(resizingSidebar).getPropertyValue('--sidebar-width'); let isLeft = resizingSidebar.classList.contains("sidebar-left"); localStorage.setItem(isLeft ? 'sidebar-left-width' : 'sidebar-right-width', finalWidth); resizingSidebar.classList.remove('is-resizing'); resizingSidebar.style.removeProperty('transition-duration'); }); } leftHandle.addEventListener('pointerdown', handleClick); rightHandle.addEventListener('pointerdown', handleClick); // reset sidebar width on double click function resetSidebarEvent(e) { let sidebar = e.target.closest('.sidebar'); if (sidebar) { sidebar.style.removeProperty('transition-duration'); sidebar.style.removeProperty('--sidebar-width'); let isLeft = sidebar.classList.contains("sidebar-left"); localStorage.removeItem(isLeft ? 'sidebar-left-width' : 'sidebar-right-width'); } } leftHandle.addEventListener('dblclick', resetSidebarEvent); rightHandle.addEventListener('dblclick', resetSidebarEvent); } /**Get the computed target sidebar width in px*/ function getSidebarWidthProp() { return getComputedPixelValue("--sidebar-width"); } //#endregion //#region ----------------- Theme ----------------- function setupThemeToggle() { if (!themeToggle) return; if (localStorage.getItem("theme") != null) { setThemeToggle(localStorage.getItem("theme") == "light"); } // set initial toggle state based on body theme class if (document.body.classList.contains("theme-light")) { setThemeToggle(true); } else { setThemeToggle(false); } function setThemeToggle(state, instant = false) { themeToggle.checked = state; if (instant) { var oldTransition = document.body.style.transition; document.body.style.transition = "none"; } if(!themeToggle.classList.contains("is-checked") && state) { themeToggle.classList.add("is-checked"); } else if (themeToggle.classList.contains("is-checked") && !state) { themeToggle.classList.remove("is-checked"); } if(!state) { 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"); } } else { 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"); } } if (instant) { setTimeout(function() { document.body.style.transition = oldTransition; }, 100); } localStorage.setItem("theme", state ? "light" : "dark"); } document.querySelector(".theme-toggle-input")?.addEventListener("change", event => { let newVal = !(localStorage.getItem("theme") == "light"); console.log("Theme toggle changed to: " + newVal); setThemeToggle(newVal); }); } //#endregion //#region ----------------- Scroll ----------------- let flashElement = null; let flashAnimation = null; function scrollIntoView(element, options, animate = true) { setTreeCollapsed(element, false, animate); const flashTiming = { duration: 1500, iterations: 1, delay: 300, }; const flashAnimationData = [ { opacity: 0 }, { opacity: 0.8 }, { opacity: 0.8 }, { opacity: 0.8 }, { opacity: 0.8 }, { opacity: 0.8 }, { opacity: 0 }, ]; if(flashElement) { flashElement.remove(); flashAnimation.cancel(); } flashElement = document.createElement("div"); flashElement.classList.add("scroll-highlight"); element.appendChild(flashElement); if(options) flashElement.scrollIntoView({ behavior: animate ? "smooth" : "auto", ...options }); else flashElement.scrollIntoView({ behavior: animate ? "smooth" : "auto" }); var savePos = element.style.position; element.style.position = "relative"; flashAnimation = flashElement.animate(flashAnimationData, flashTiming); flashAnimation.onfinish = function() { flashElement.remove(); element.style.position = savePos; } } function setupScroll(setupOnNode) { // hide elements clipped by scrollable areas in markdown-preview-view elements if(documentType != "canvas") return; let markdownViews = Array.from(setupOnNode.querySelectorAll(".markdown-preview-view")); let nextMarkdownViewId = 0; let marginMultiplier = 0.1; let maxMargin = 150; let margin = 0; markdownViews.forEach(async function (view) { console.log("Setting up markdown view"); let headers = Array.from(view.querySelectorAll(".heading-wrapper")); view.updateVisibleWindowMarkdown = function updateVisibleWindowMarkdown(allowVirtualization = true, allowDevirtualization = true) { let scrollBounds = view.getBoundingClientRect(); margin = Math.min(scrollBounds.height * marginMultiplier, maxMargin); let scrollBoundsTop = scrollBounds.top - margin; let scrollBoundsBottom = scrollBounds.bottom + margin; async function updateHeader(header) { let bounds = header?.getBoundingClientRect(); if (!bounds) return; let isClipped = (bounds.top < scrollBoundsTop && bounds.bottom < scrollBoundsTop) || (bounds.top > scrollBoundsBottom && bounds.bottom > scrollBoundsBottom); if (isClipped && allowVirtualization) { header.hide(); } else if (!isClipped && allowDevirtualization) { header.show(); } } for (let i = 0; i < headers.length; i++) { let h = headers[i]; if(h) updateHeader(h); } } let lastScrollTop = 0; view.addEventListener("scroll", function() { if (Math.abs(view.scrollTop - lastScrollTop) > margin / 3) { view.updateVisibleWindowMarkdown(false, true); } lastScrollTop = view.scrollTop; }); }); async function periodicUpdate() { if(markdownViews.length > 0) { markdownViews[nextMarkdownViewId].updateVisibleWindowMarkdown(); nextMarkdownViewId = (nextMarkdownViewId + 1) % markdownViews.length; } } setInterval(periodicUpdate, 200); } //#endregion //#region ----------------- Plugins ----------------- // Excalidraw function setupExcalidraw(setupOnNode) { setupOnNode.querySelectorAll(".excalidraw-svg svg").forEach(function (svg) { let isLight = svg.querySelector("rect").getAttribute("fill") > "#7F7F7F"; svg.classList.add(isLight ? "light" : "dark"); }); } //#endregion //#region ----------------- Search ----------------- // search box let index; let searchResults; async function setupSearch() { if (isFileProtocol) return; searchInput = document.querySelector('input[type="search"]'); if (!searchInput) return; const indexResp = await fetch('lib/search-index.json'); const indexJSON = await indexResp.text(); index = MiniSearch.loadJSON(indexJSON, { fields: ['title', 'path', 'tags', 'headers'] }); const inputClear = document.querySelector('.search-input-clear-button'); inputClear.addEventListener('click', (event) => { search(""); }); searchInput.addEventListener('input', (event) => { const query = event.target.value ?? ""; if (startsWithAny(query, ["#", "tag:", "title:", "name:", "header:", "H:"])) { searchInput.style.color = "var(--text-accent)"; } else { searchInput.style.color = ""; } search(query); }); searchResults = document.createElement('div'); searchResults.setAttribute('id', 'search-results'); } async function search(query) { searchInput.value = query; // parse special query filters let searchFields = ['title', 'content', 'tags', 'headers', 'path']; if (query.startsWith("#")) searchFields = ['tags', 'headers']; if (query.startsWith("tag:")) { query = query.substring(query.indexOf(":") + 1); searchFields = ['tags']; } if (startsWithAny(query, ["title:", "name:"])) { query = query.substring(query.indexOf(":") + 1); searchFields = ['title']; } if (startsWithAny(query, ["header:", "H:"])) { query = query.substring(query.indexOf(":") + 1); searchFields = ['headers']; } if (startsWithAny(query, ["path:"])) { query = query.substring(query.indexOf(":") + 1); searchFields = ['path']; } if (query.length >= 1) { const results = index.search(query, { prefix: true, fuzzy: 0.3, boost: { title: 4, headers: 3, tags: 2, path: 1 }, fields: searchFields }); // search through the file tree and hide documents that don't match the search let showPaths = []; let hintLabels = []; for (let result of results) { // only show the most relevant results if (((result.score < results[0].score * 0.33 || showPaths.length > 12) && showPaths.length > 3) || result.score < results[0].score * 0.1) break; showPaths.push(result.path); let hints = []; let breakEarly = false; for (match in result.match) { if (result.match[match].includes("headers")) { for (let header of result.headers) { if (header.toLowerCase().includes(match.toLowerCase())) { hints.push(header); if (query.toLowerCase() != match.toLowerCase()) { breakEarly = true; break; } } } } if (breakEarly) break; } hintLabels.push(hints); } let fileTree = document.querySelector(".file-tree"); if (fileTree) { // filter the file tree and sort it by the order of the search results filterFileTree(showPaths, hintLabels, query).then(() => sortFileTreeDocuments((a, b) => { if (!a || !b) return 0; let aPath = getVaultRelativePath(a.firstChild.href); let bPath = getVaultRelativePath(b.firstChild.href); return showPaths.findIndex((path) => aPath.startsWith(path)) - showPaths.findIndex((path) => bPath.startsWith(path)); })); } else { const list = document.createElement('div'); results.slice(0, 10).forEach(result => { const item = document.createElement('div'); item.classList.add('search-result'); const link = document.createElement('a'); link.classList.add('tree-link'); const searchURL = result.path + '?mark=' + encodeURIComponent(query); link.setAttribute('href', searchURL); link.appendChild(document.createTextNode(result.title)); item.appendChild(link); list.append(item); }); searchResults.replaceChildren(list); searchInput.parentElement.after(searchResults); initializePageEvents(searchResults); } } else { if (searchResults && searchResults.parentElement) searchResults.parentNode.removeChild(searchResults); clearCurrentDocumentSearch(); if (fileTree) clearFileTreeFilter().then(() => sortFileTreeAlphabetically()); } } function startsWithAny(string, prefixes) { for (let i = 0; i < prefixes.length; i++) { if (string.startsWith(prefixes[i])) return true; } return false; } async function searchCurrentDocument(query) { clearCurrentDocumentSearch(); const textNodes = getTextNodes(document.querySelector(".markdown-preview-sizer") ?? documentContainer); textNodes.forEach(async node => { const content = node.nodeValue; const newContent = content.replace(new RegExp(query, 'gi'), match => `${match}`); if (newContent !== content) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = newContent; const newNodes = Array.from(tempDiv.childNodes); newNodes.forEach(newNode => { if (newNode.nodeType != Node.TEXT_NODE) { newNode.setAttribute('class', 'search-mark'); } node.parentNode.insertBefore(newNode, node); }); node.parentNode.removeChild(node); } }); let firstMark = document.querySelector(".search-mark"); // wait for page to fade in setTimeout(() => { if(firstMark) scrollIntoView(firstMark, { behavior: "smooth", block: "start" }); }, 500); } function clearCurrentDocumentSearch() { document.querySelectorAll(".search-mark").forEach(node => { node.outerHTML = node.innerHTML; }); } function getTextNodes(element) { const textNodes = []; const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); let node; while (node = walker.nextNode()) { textNodes.push(node); } return textNodes; } //#endregion