Add 'packages/share-theme/' from commit '2cdd2a0a543f0bced8284ca55bc94efadbc7c91f'

git-subtree-dir: packages/share-theme
git-subtree-mainline: d8f0709bce
git-subtree-split: 2cdd2a0a54
This commit is contained in:
Elian Doran
2025-06-08 22:06:45 +03:00
38 changed files with 4617 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
export default function debounce<T extends (...args: unknown[]) => unknown>(executor: T, delay: number) {
let timeout: NodeJS.Timeout | null;
return function(...args: Parameters<T>): void {
const callback = () => {
timeout = null;
Reflect.apply(executor, null, args);
};
if (timeout) clearTimeout(timeout);
timeout = setTimeout(callback, delay);
};
}

View File

@@ -0,0 +1,7 @@
export default function parents<T extends HTMLElement>(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;
}

View File

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

View File

@@ -0,0 +1,23 @@
import highlight from "./modules/highlight";
import setupToC from "./modules/toc";
import setupExpanders from "./modules/expanders";
import setupMobileMenu from "./modules/mobile";
import setupSearch from "./modules/search";
import setupThemeSelector from "./modules/theme";
function $try<T extends (...a: unknown[]) => unknown>(func: T, ...args: Parameters<T>) {
try {
func.apply(func, args);
}
catch (e) {
console.error(e); // eslint-disable-line no-console
}
}
$try(setupThemeSelector);
$try(setupToC);
$try(highlight);
$try(setupExpanders);
$try(setupMobileMenu);
$try(setupSearch);

View File

@@ -0,0 +1,27 @@
// 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;
// }
// }
export default function setupExpanders() {
const expanders = Array.from(document.querySelectorAll("#menu .submenu-item"));
for (const ex of expanders) {
ex.addEventListener("click", e => {
if ((e.target as Element).closest(".submenu-item,.item") !== ex) return;
e.preventDefault();
e.stopPropagation();
const ul = ex.querySelector("ul")!;
ul.style.height = `${ul.scrollHeight}px`;
setTimeout(() => ex.classList.toggle("expanded"), 1);
setTimeout(() => ul.style.height = ``, 200);
});
}
}

View File

@@ -0,0 +1,66 @@
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>`;
});
}
};
/**
* 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
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "api/notes/cVaK9ZJwx5Hs/download";
document.head.append(link);
// Add the highlight.js script too
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,61 @@
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")!;
searchInput.addEventListener("keyup", debounce(async () => {
// console.log("CHANGE EVENT");
const ancestor = document.body.dataset.ancestorNoteId;
const query = searchInput.value;
if (query.length < 3) return;
const resp = await fetch(`api/notes?search=${query}&ancestorNoteId=${ancestor}`);
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);
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,35 @@
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")!;
if (preference) {
const themeSelection: HTMLDivElement = document.querySelector(".theme-selection")!;
themeSelection.classList.add("no-transition");
themeSwitch.checked = preference === "dark";
setTimeout(() => themeSelection.classList.remove("no-transition"), 400);
}
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");
}
});
}

View File

@@ -0,0 +1,46 @@
/**
* The ToC is now generated in the page template so
* it even exists for users without client-side js
* and that means it loads with the page so it avoids
* all potential reshuffling or layout recalculations.
*
* So, all this function needs to do is make the links
* perform smooth animation, and adjust the "active"
* entry as the user scrolls.
*/
export default function setupToC() {
const toc = document.getElementById("toc");
if (!toc) return;
// Get all relevant elements
const sections = document.getElementById("content")!.querySelectorAll("h2, h3, h4, h5, h6");
const links = toc.querySelectorAll("a");
// Setup smooth scroll on click
for (const link of links) {
link.addEventListener("click", e => {
const target = document.querySelector(link.getAttribute("href")!);
if (!target) return;
e.preventDefault();
e.stopPropagation();
target.scrollIntoView({behavior: "smooth"});
});
}
// Setup a moving "active" in the ToC that adjusts with the scroll state
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);
}

File diff suppressed because one or more lines are too long