Remove dead code and reorganize slightly

This commit is contained in:
Zack Rauen
2023-09-28 00:24:52 -04:00
parent a8bb2f110b
commit bdfe86ba1a
15 changed files with 12 additions and 305 deletions

View File

@@ -0,0 +1,53 @@
function anchorToId(anchor: HTMLAnchorElement) {
return anchor.href.replace("./", "");
}
const stored = localStorage.getItem("expanded") ?? "[]";
let parsed: string[];
try {
parsed = JSON.parse(stored) as string[];
}
catch (e) {
parsed = [];
}
const state = new Set(parsed);
const submenus = Array.from(document.querySelectorAll("#menu .submenu-item"));
for (const sub of submenus) {
try {
if (state.has(anchorToId(sub.children[0] as HTMLAnchorElement))) sub.classList.add("expanded");
}
catch (e) {
// TODO: create logger
console.warn("Could not restore expanded state"); // eslint-disable-line no-console
console.error(e); // eslint-disable-line no-console
}
}
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 id = anchorToId(ex.closest("a")!);
if (state.has(id)) state.delete(id);
else state.add(id);
localStorage.setItem("expanded", JSON.stringify([...state]));
});
}
// In case a linked article lead to a new tree
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.contains("expanded")) {
parent.classList.add("expanded");
}
parent = parent.parentElement;
}
}
}

View File

@@ -0,0 +1,69 @@
import {HLJSApi, HLJSPlugin} from "highlight.js";
declare const hljs: HLJSApi;
// Custom highlight.js plugin to highlight the `api` globals for Trilium
const highlightTriliumApi: HLJSPlugin = {
"after:highlight": (result) => {
result.value = result.value.replaceAll(/([^A-Za-z0-9])api\./g, function(match, prefix) {
return `${prefix}<span class="hljs-variable language_">api</span>.`;
});
}
};
// Custom highlight.js plugin to highlight JQuery function usage
const highlightJQuery: HLJSPlugin = {
"after:highlight": (result) => {
result.value = result.value.replaceAll(/([^A-Za-z0-9.])\$\((.+)\)/g, function(match, prefix, variable) {
return `${prefix}<span class="hljs-variable language_">$(</span>${variable}<span class="hljs-variable language_">)</span>`;
});
// TODO: add highlighting for static calls like $.ajax
}
};
/**
* Let's highlight some codeblocks!
*/
export default function addHljs() {
const codeblocks = document.querySelectorAll(`.ck-content pre`);
if (!codeblocks.length) return; // If there are none, don't add dependency
// Add the hightlight.js styles from the child note of this script
// TODO: make this a mapping
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "api/notes/cVaK9ZJwx5Hs/download";
document.head.append(link);
// Add the highlight.js script too
// TODO: make this a mappin as well
const script = document.createElement("script");
script.src = "api/notes/6PVElIem02b5/download";
script.addEventListener("load", () => {
// hljs.configure({languageDetectRe: /\blanguage-text-x-([\w-]+)\b/i});
const allLanguages = hljs.listLanguages().map(l => {
const definition = hljs.getLanguage(l);
if (definition?.aliases) return [l, ...definition.aliases];
return [l];
});
for (const langs of allLanguages) {
const lang = langs[0];
for (const l of langs) {
hljs.registerAliases(`text-x-${l}`, {languageName: lang});
}
}
// This registers the JS Frontend and JS Backend types as javascript aliases for highlighting purposes
hljs.registerAliases(["application-javascript-env-frontend", "application-javascript-env-backend"], {languageName: "javascript"});
// Add our custom plugins and highlight all on page
hljs.addPlugin(highlightTriliumApi);
hljs.addPlugin(highlightJQuery);
hljs.highlightAll();
});
document.head.append(script);
}

View File

@@ -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);
});
}

View File

@@ -0,0 +1,62 @@
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;
}
function buildResultItem(result: SearchResult) {
return `<a class="search-result-item" href="./${result.id}">
<div class="search-result-title">${result.title}</div>
<div class="search-result-note">${result.path || "Home"}</div>
</a>`;
}
export default function setupSearch() {
const searchInput: HTMLInputElement = document.querySelector(".search-input")!;
// TODO: move listener to another function
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 = [`<div class="search-results">`];
for (const result of results) {
lines.push(buildResultItem(result));
}
lines.push("</div>");
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); // TODO: consider updating existing container and never removing
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();
});
}

View File

@@ -0,0 +1,28 @@
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");
}
}
export default function setupThemeSelector() {
const themeSwitch: HTMLInputElement = document.querySelector(".theme-selection input")!;
// TODO: consolidate this with initialization (DRY)
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");
}
});
}

126
src/scripts/modules/toc.ts Normal file
View File

@@ -0,0 +1,126 @@
const slugify = (text: string) => text.toLowerCase().replace(/[^\w]/g, "-");
const getDepth = (el: Element) => parseInt(el.tagName.replace("H","").replace("h",""));
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);
anchor.textContent = "#";
const link = document.createElement("a");
link.setAttribute("href", `#${slug}`);
link.textContent = heading.textContent;
link.addEventListener("click", e => {
const target = document.querySelector(`#${slug}`);
if (!target) return;
e.preventDefault();
e.stopPropagation();
target.scrollIntoView({behavior: "smooth"});
});
heading.append(anchor);
const li = document.createElement("li");
li.append(link);
return li;
};
/**
* Generate a ToC from all heading elements in the main content area.
* This should go to full h6 depth and not be too opinionated. It
* does assume a "sensible" structure in that you don't go from
* h2 > h4 > h1 but rather h2 > h3 > h2 so you change by 1 and end
* up at the same level as before.
*/
export default function setupToC() {
// Get all headings from the page and map them to already built elements
const headings = Array.from(document.querySelectorAll("h1, h2, h3, h4, h5, h6"));
if (headings.length <= 1) return; // But if there are none, let's do nothing
const items = headings.map(h => buildItem(h));
// Setup the ToC list
const toc = document.createElement("ul");
toc.id = "toc";
// Get the depth of the first content heading on the page.
// This depth will be used as reference for all other headings.
// headings[0] === the <h1> from Trilium
const firstDepth = getDepth(headings[1]);
// Loop over ALL headings including the first
for (let h = 0; h < headings.length; h++) {
// Get current heading and determine depth
const current = headings[h];
const currentDepth = getDepth(current);
// If it's the same depth as our first heading, add to ToC
if (currentDepth === firstDepth) toc.append(items[h]);
// If this is the last element then it will have already
// been added as a child or as same depth as first
let nextIndex = h + 1;
if (nextIndex >= headings.length) continue;
// Time to find all children of this heading
const children = [];
const childDepth = currentDepth + 1;
let depthOfNext = getDepth(headings[nextIndex]);
while (depthOfNext > currentDepth) {
// If it's the expected depth, add as child
if (depthOfNext === childDepth) children.push(nextIndex);
nextIndex++;
// If the next index is valid, grab the depth for next loop
// TODO: could this be done cleaner with a for loop?
if (nextIndex < headings.length) depthOfNext = getDepth(headings[nextIndex]);
else depthOfNext = currentDepth; // If the index was invalid, break loop
}
// If this heading had children, add them as children
if (children.length) {
const ul = document.createElement("ul");
for (const c of children) ul.append(items[c]);
items[h].append(ul);
}
}
// Setup a moving "active" in the ToC that adjusts with the scroll state
const sections = headings.slice(1);
const links = toc.querySelectorAll("a");
function changeLinkState() {
let index = sections.length;
// Work backkwards to find the first matching section
while (--index && window.scrollY + 50 < (sections[index] as HTMLElement).offsetTop) {} // eslint-disable-line no-empty
// Update the "active" item in ToC
links.forEach((link) => link.classList.remove("active"));
links[index].classList.add("active");
}
// Initial render
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("#right-pane");
layout?.classList.add("toc");
layout?.append(pane);
}