From 4147f2b8d881aaa446256f1762eb70611faa7213 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Wed, 27 Sep 2023 23:18:03 -0400 Subject: [PATCH] Switch scripts to redesign --- .eslintrc | 3 ++ src/scripts/common/debounce.ts | 11 ++++++ src/scripts/common/parents.ts | 7 ++++ src/scripts/common/parsehtml.ts | 7 ++++ src/scripts/expanders.ts | 23 +++++++++++++ src/scripts/index.ts | 61 +++++++++++++++++++-------------- src/scripts/mobile.ts | 25 ++++++++++++++ src/scripts/navigation/toc.ts | 15 ++++++-- src/scripts/search.ts | 53 ++++++++++++++++++++++++++++ src/scripts/theme.ts | 27 +++++++++++++++ 10 files changed, 204 insertions(+), 28 deletions(-) create mode 100644 src/scripts/common/debounce.ts create mode 100644 src/scripts/common/parents.ts create mode 100644 src/scripts/common/parsehtml.ts create mode 100644 src/scripts/expanders.ts create mode 100644 src/scripts/mobile.ts create mode 100644 src/scripts/search.ts create mode 100644 src/scripts/theme.ts diff --git a/.eslintrc b/.eslintrc index 23653b106..19abc5113 100644 --- a/.eslintrc +++ b/.eslintrc @@ -87,5 +87,8 @@ "wrap-iife": ["error", "inside"], "yield-star-spacing": "error", "yoda": "error" + }, + "globals": { + "NodeJS": "readonly" } } \ No newline at end of file diff --git a/src/scripts/common/debounce.ts b/src/scripts/common/debounce.ts new file mode 100644 index 000000000..a4537d963 --- /dev/null +++ b/src/scripts/common/debounce.ts @@ -0,0 +1,11 @@ +export default function debounce unknown>(executor: T, delay: number) { + let timeout: NodeJS.Timeout | null; + return function(...args: Parameters): void { + const callback = () => { + timeout = null; + Reflect.apply(executor, null, args); + }; + if (timeout) clearTimeout(timeout); + timeout = setTimeout(callback, delay); + }; +} \ No newline at end of file diff --git a/src/scripts/common/parents.ts b/src/scripts/common/parents.ts new file mode 100644 index 000000000..cfe0a7fce --- /dev/null +++ b/src/scripts/common/parents.ts @@ -0,0 +1,7 @@ +export default function parents(el: T, selector: string) { + const result = []; + for (let p = el && el.parentElement; p; p = p.parentElement) { + if (p.matches(selector)) result.push(p); + } + return result; +} \ No newline at end of file diff --git a/src/scripts/common/parsehtml.ts b/src/scripts/common/parsehtml.ts new file mode 100644 index 000000000..2563e1c87 --- /dev/null +++ b/src/scripts/common/parsehtml.ts @@ -0,0 +1,7 @@ +export default function parseHTML(html: string, fragment = false) { + const template = document.createElement("template"); + template.innerHTML = html; + const node = template.content.cloneNode(true); + if (fragment) return node; + return node.childNodes.length > 1 ? node.childNodes : node.childNodes[0]; +} \ No newline at end of file diff --git a/src/scripts/expanders.ts b/src/scripts/expanders.ts new file mode 100644 index 000000000..6749b6852 --- /dev/null +++ b/src/scripts/expanders.ts @@ -0,0 +1,23 @@ +export default function setupExpanders() { + const expanders = Array.from(document.querySelectorAll("#menu .collapse-button")); + for (const ex of expanders) { + ex.addEventListener("click", e => { + e.preventDefault(); + e.stopPropagation(); + // ex.parentElement.parentElement.classList.toggle("expanded"); + ex.closest(".submenu-item")?.classList.toggle("expanded"); + }); + } + + const activeLink = document.querySelector("#menu a.active"); + if (activeLink) { + let parent = activeLink.parentElement; + const mainMenu = document.getElementById("#menu"); + while (parent && parent !== mainMenu) { + if (parent.matches(".submenu-item")) { + parent.classList.add("expanded"); + } + parent = parent.parentElement; + } + } +} \ No newline at end of file diff --git a/src/scripts/index.ts b/src/scripts/index.ts index 88879347b..d95f5f124 100644 --- a/src/scripts/index.ts +++ b/src/scripts/index.ts @@ -1,25 +1,29 @@ -import fixActiveLink from "./fixes/activelink"; -import fixTableHeaders from "./fixes/tableheaders"; +// import fixActiveLink from "./fixes/activelink"; +// import fixTableHeaders from "./fixes/tableheaders"; import highlight from "./other/highlight"; -import buildSidenav from "./navigation/sidenav"; -import buildBreadcrumbs from "./navigation/breadcrumbs"; -import fixSubMenus from "./fixes/submenu"; +// import buildSidenav from "./navigation/sidenav"; +// import buildBreadcrumbs from "./navigation/breadcrumbs"; +// import fixSubMenus from "./fixes/submenu"; import generateTOC from "./navigation/toc"; -import addExternalLinks from "./fixes/externallinks"; -import injectSwagger from "./other/swagger"; -import makeMobileMenu from "./other/mobile"; +// import addExternalLinks from "./fixes/externallinks"; +// import injectSwagger from "./other/swagger"; +// import makeMobileMenu from "./other/mobile"; +import setupExpanders from "./expanders"; +import setupMobileMenu from "./mobile"; +import setupSearch from "./search"; +import setupThemeSelector from "./theme"; -const ETAPI_REF_NOTE_ID = "pPIXi0uwF5GX"; -const HIDDEN_SUBMENUS = ["blog"]; -const EXTERNAL_LINKS = { - EGFtX8Uw96FQ: "https://github.com/zadam/trilium", - dXAKFE0fJtom: "https://discord.gg/eTaTXUgcBr" -}; -const ALIASES = { - WqBnya4Ye8rS: "", - ZapIU17QNEyU: "blog" -}; +// const ETAPI_REF_NOTE_ID = "pPIXi0uwF5GX"; +// const HIDDEN_SUBMENUS = ["blog"]; +// const EXTERNAL_LINKS = { +// EGFtX8Uw96FQ: "https://github.com/zadam/trilium", +// dXAKFE0fJtom: "https://discord.gg/eTaTXUgcBr" +// }; +// const ALIASES = { +// WqBnya4Ye8rS: "", +// ZapIU17QNEyU: "blog" +// }; function $try unknown>(func: T, ...args: Parameters) { try { @@ -44,20 +48,25 @@ function $try unknown>(func: T, ...args: Paramete */ // Perform fixes first -$try(fixActiveLink, ALIASES); -$try(fixTableHeaders); -$try(fixSubMenus, HIDDEN_SUBMENUS); -$try(addExternalLinks, EXTERNAL_LINKS); +// $try(fixActiveLink, ALIASES); +// $try(fixTableHeaders); +// $try(fixSubMenus, HIDDEN_SUBMENUS); +// $try(addExternalLinks, EXTERNAL_LINKS); // Now layout changes -$try(buildBreadcrumbs); -$try(buildSidenav); +// $try(buildBreadcrumbs); +// $try(buildSidenav); $try(generateTOC); // Finally, other features $try(highlight); -$try(injectSwagger, ETAPI_REF_NOTE_ID); -$try(makeMobileMenu); +// $try(injectSwagger, ETAPI_REF_NOTE_ID); + +$try(setupExpanders); +$try(setupMobileMenu); +$try(setupSearch); +$try(setupThemeSelector); +// $try(makeMobileMenu); /** * This was removed because both the title change and the opengraph diff --git a/src/scripts/mobile.ts b/src/scripts/mobile.ts new file mode 100644 index 000000000..721e5946d --- /dev/null +++ b/src/scripts/mobile.ts @@ -0,0 +1,25 @@ +import parents from "./common/parents"; + + +export default function setupMobileMenu() { + function toggleMobileMenu(event: MouseEvent) { + event.stopPropagation(); // Don't prevent default for links + + const isOpen = document.body.classList.contains("menu-open"); + if (isOpen) return document.body.classList.remove("menu-open"); + return document.body.classList.add("menu-open"); + } + + const showMenuButton = document.getElementById("show-menu-button"); + showMenuButton?.addEventListener("click", toggleMobileMenu); + + window.addEventListener("click", e => { + const isOpen = document.body.classList.contains("menu-open"); + if (!isOpen) return; // This listener is only to close + + // If the click was anywhere in the mobile nav, don't close + if (parents(e.target as HTMLElement, "#left-pane").length) return; + return toggleMobileMenu(e); + }); + +} \ No newline at end of file diff --git a/src/scripts/navigation/toc.ts b/src/scripts/navigation/toc.ts index 052da9583..f9dbb830f 100644 --- a/src/scripts/navigation/toc.ts +++ b/src/scripts/navigation/toc.ts @@ -6,6 +6,7 @@ const buildItem = (heading: Element) => { const slug = slugify(heading.textContent ?? ""); const anchor = document.createElement("a"); + anchor.className = "toc-anchor"; anchor.setAttribute("href", `#${slug}`); anchor.setAttribute("name", slug); anchor.setAttribute("id", slug); @@ -108,8 +109,18 @@ export default function generateTOC() { changeLinkState(); window.addEventListener("scroll", changeLinkState); + // Create the toc wrapper + const pane = document.createElement("div"); + pane.id = "toc-pane"; + + // Create the header + const header = document.createElement("h3"); + header.textContent = "On This Page"; + pane.append(header); + pane.append(toc); + // Finally, add the ToC to the end of layout. Give the layout a class for adjusting widths. - const layout = document.querySelector("#layout"); + const layout = document.querySelector("#right-pane"); layout?.classList.add("toc"); - layout?.append(toc); + layout?.append(pane); } \ No newline at end of file diff --git a/src/scripts/search.ts b/src/scripts/search.ts new file mode 100644 index 000000000..d992b430e --- /dev/null +++ b/src/scripts/search.ts @@ -0,0 +1,53 @@ +import debounce from "./common/debounce"; +import parents from "./common/parents"; +import parseHTML from "./common/parsehtml"; + + +interface SearchResults { + results: SearchResult[]; +} + +interface SearchResult { + id: string; + title: string; + score: number; + path: string; +} + + +export default function setupSearch() { + const searchInput: HTMLInputElement = document.querySelector(".search-input")!; + searchInput.addEventListener("keyup", debounce(async () => { + // console.log("CHANGE EVENT"); + const current = document.body.dataset.noteId; + const query = searchInput.value; + if (query.length < 3) return; + const resp = await fetch(`api/search/${current}?query=${query}`); + const json = await resp.json() as SearchResults; + const results = json.results.slice(0, 5); + const lines = [`
`]; + for (const result of results) { + lines.push(`
${result.title}
${result.path || "Home"}
`); + } + lines.push("
"); + + const container = parseHTML(lines.join("")) as HTMLDivElement; + // console.log(container, lines); + const rect = searchInput.getBoundingClientRect(); + container.style.top = `${rect.bottom}px`; + container.style.left = `${rect.left}px`; + container.style.minWidth = `${rect.width}px`; + + const existing = document.querySelector(".search-results"); + if (existing) existing.replaceWith(container); + else document.body.append(container); + }, 500)); + + window.addEventListener("click", e => { + const existing = document.querySelector(".search-results"); + if (!existing) return; + // If the click was anywhere search components ignore it + if (parents(e.target as HTMLElement, ".search-results,.search-item").length) return; + if (existing) existing.remove(); + }); +} \ No newline at end of file diff --git a/src/scripts/theme.ts b/src/scripts/theme.ts new file mode 100644 index 000000000..06aec7edb --- /dev/null +++ b/src/scripts/theme.ts @@ -0,0 +1,27 @@ +export default function setupThemeSelector() { + const themeSwitch: HTMLInputElement = document.querySelector(".theme-selection input")!; + themeSwitch?.addEventListener("change", () => { + if (themeSwitch.checked) { + document.body.classList.add("theme-dark"); + document.body.classList.remove("theme-light"); + localStorage.setItem("theme", "dark"); + } + else { + document.body.classList.remove("theme-dark"); + document.body.classList.add("theme-light"); + localStorage.setItem("theme", "light"); + } + }); + + const preference = localStorage.getItem("theme"); + if (preference) { + if (preference === "dark") { + document.body.classList.add("theme-dark"); + document.body.classList.remove("theme-light"); + } + else { + document.body.classList.remove("theme-dark"); + document.body.classList.add("theme-light"); + } + } +} \ No newline at end of file