mirror of
https://github.com/zadam/trilium.git
synced 2025-12-22 16:20:08 +01:00
Remove dead code and reorganize slightly
This commit is contained in:
53
src/scripts/modules/expanders.ts
Normal file
53
src/scripts/modules/expanders.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/scripts/modules/highlight.ts
Normal file
69
src/scripts/modules/highlight.ts
Normal 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);
|
||||
}
|
||||
25
src/scripts/modules/mobile.ts
Normal file
25
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);
|
||||
});
|
||||
|
||||
}
|
||||
62
src/scripts/modules/search.ts
Normal file
62
src/scripts/modules/search.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
28
src/scripts/modules/theme.ts
Normal file
28
src/scripts/modules/theme.ts
Normal 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
126
src/scripts/modules/toc.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user