mirror of
https://github.com/zadam/trilium.git
synced 2025-12-21 15:49:56 +01:00
Add 'packages/share-theme/' from commit '2cdd2a0a543f0bced8284ca55bc94efadbc7c91f'
git-subtree-dir: packages/share-theme git-subtree-mainline:d8f0709bcegit-subtree-split:2cdd2a0a54
This commit is contained in:
11
packages/share-theme/src/scripts/common/debounce.ts
Normal file
11
packages/share-theme/src/scripts/common/debounce.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
7
packages/share-theme/src/scripts/common/parents.ts
Normal file
7
packages/share-theme/src/scripts/common/parents.ts
Normal 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;
|
||||
}
|
||||
7
packages/share-theme/src/scripts/common/parsehtml.ts
Normal file
7
packages/share-theme/src/scripts/common/parsehtml.ts
Normal 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];
|
||||
}
|
||||
23
packages/share-theme/src/scripts/index.ts
Normal file
23
packages/share-theme/src/scripts/index.ts
Normal 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);
|
||||
27
packages/share-theme/src/scripts/modules/expanders.ts
Normal file
27
packages/share-theme/src/scripts/modules/expanders.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
66
packages/share-theme/src/scripts/modules/highlight.ts
Normal file
66
packages/share-theme/src/scripts/modules/highlight.ts
Normal 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);
|
||||
}
|
||||
25
packages/share-theme/src/scripts/modules/mobile.ts
Normal file
25
packages/share-theme/src/scripts/modules/mobile.ts
Normal 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);
|
||||
});
|
||||
|
||||
}
|
||||
61
packages/share-theme/src/scripts/modules/search.ts
Normal file
61
packages/share-theme/src/scripts/modules/search.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
35
packages/share-theme/src/scripts/modules/theme.ts
Normal file
35
packages/share-theme/src/scripts/modules/theme.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
}
|
||||
46
packages/share-theme/src/scripts/modules/toc.ts
Normal file
46
packages/share-theme/src/scripts/modules/toc.ts
Normal 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);
|
||||
}
|
||||
78
packages/share-theme/src/scripts/test.ts
Normal file
78
packages/share-theme/src/scripts/test.ts
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user