271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
import BlogPost from "./BlogPost";
|
|
import Author from "./Author";
|
|
import TimeUtility from "./TimeUtility";
|
|
|
|
declare const bootstrap: any;
|
|
declare const katex: any;
|
|
declare const Prism: any;
|
|
|
|
class UI {
|
|
public static get blogPost(): HTMLDivElement {
|
|
return document.querySelector("article[data-blog-post='true']");
|
|
}
|
|
|
|
public static get blogPostContainer(): HTMLDivElement {
|
|
return document.querySelector("#all-blog-posts");
|
|
}
|
|
|
|
public static get blogPostTemplate(): HTMLDivElement {
|
|
return document.querySelector("#blog-post-template");
|
|
}
|
|
|
|
/**
|
|
* Creates a <script> element that loads the Disqus comment counter.
|
|
*/
|
|
public static createDisqusCounterScript(): HTMLScriptElement {
|
|
const script = document.createElement("script");
|
|
script.id = "dsq-count-scr";
|
|
script.src = "https://oliverbooth-dev.disqus.com/count.js";
|
|
script.async = true;
|
|
return script;
|
|
}
|
|
|
|
/**
|
|
* Creates a blog post card from the given template, post, and author.
|
|
* @param template The Handlebars template to use.
|
|
* @param post The blog post to use.
|
|
* @param author The author of the blog post.
|
|
*/
|
|
public static createBlogPostCard(template: any, post: BlogPost, author: Author): HTMLDivElement {
|
|
const card = document.createElement("div") as HTMLDivElement;
|
|
card.classList.add("card");
|
|
card.classList.add("blog-card");
|
|
card.classList.add("animate__animated");
|
|
card.classList.add("animate__fadeIn");
|
|
card.style.marginBottom = "50px";
|
|
|
|
const body = template({
|
|
post: {
|
|
title: post.title,
|
|
excerpt: post.excerpt,
|
|
url: `/blog/${post.url.year}/${post.url.month}/${post.url.day}/${post.url.slug}`,
|
|
date: TimeUtility.formatRelativeTimestamp(post.published),
|
|
formattedDate: post.updated ? post.formattedUpdateDate : post.formattedPublishDate,
|
|
date_humanized: `${post.updated ? "Updated" : "Published"} ${post.humanizedTimestamp}`,
|
|
enable_comments: post.commentsEnabled,
|
|
trimmed: post.trimmed,
|
|
tags: post.tags
|
|
},
|
|
author: {
|
|
name: author.name,
|
|
avatar: author.avatarUrl
|
|
}
|
|
});
|
|
card.innerHTML = body.trim();
|
|
return card;
|
|
}
|
|
|
|
/**
|
|
* Forces all UI elements under the given element to update their rendering.
|
|
* @param element The element to search for UI elements in.
|
|
*/
|
|
public static updateUI(element?: Element) {
|
|
element = element || document.body;
|
|
UI.unescapeMarkTags(element);
|
|
UI.addLineNumbers(element);
|
|
UI.addHighlighting(element);
|
|
UI.addBootstrapTooltips(element);
|
|
UI.renderSpoilers(element);
|
|
UI.renderTabs(element);
|
|
UI.renderTeX(element);
|
|
UI.renderTimestamps(element);
|
|
UI.updateProjectCards(element);
|
|
}
|
|
|
|
/**
|
|
* Adds Bootstrap tooltips to all elements with a title attribute.
|
|
* @param element The element to search for elements with a title attribute in.
|
|
*/
|
|
public static addBootstrapTooltips(element?: Element) {
|
|
element = element || document.body;
|
|
|
|
const list = element.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
list.forEach((el: Element) => new bootstrap.Tooltip(el));
|
|
|
|
element.querySelectorAll("[title]").forEach((el) => {
|
|
el.setAttribute("data-bs-toggle", "tooltip");
|
|
el.setAttribute("data-bs-placement", "bottom");
|
|
el.setAttribute("data-bs-html", "true");
|
|
el.setAttribute("data-bs-title", el.getAttribute("title"));
|
|
|
|
new bootstrap.Tooltip(el);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds line numbers to all <pre> <code> blocks that have more than one line.
|
|
* @param element The element to search for <pre> <code> blocks in.
|
|
*/
|
|
public static addLineNumbers(element?: Element) {
|
|
element = element || document.body;
|
|
element.querySelectorAll("pre code").forEach((block) => {
|
|
if (block.className.indexOf("|nolinenumbers") > 0) {
|
|
block.className = block.className.replaceAll("|nolinenumbers", "");
|
|
return;
|
|
}
|
|
|
|
let content = block.textContent;
|
|
if (content.trim().split("\n").length > 1) {
|
|
block.parentElement.classList.add("line-numbers");
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds syntax highlighting to all <pre> <code> blocks in the element.
|
|
* @param element The element to search for <pre> <code> blocks in.
|
|
*/
|
|
public static addHighlighting(element?: Element) {
|
|
element = element || document.body;
|
|
element.querySelectorAll("pre code").forEach((block) => {
|
|
Prism.highlightAllUnder(block.parentElement);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Renders all spoilers in the document.
|
|
* @param element The element to search for spoilers in.
|
|
*/
|
|
public static renderSpoilers(element?: Element) {
|
|
element = element || document.body;
|
|
const spoilers = element.querySelectorAll(".spoiler");
|
|
spoilers.forEach((spoiler) => {
|
|
spoiler.addEventListener("click", () => {
|
|
spoiler.classList.add("spoiler-revealed");
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Renders tabs in the document.
|
|
* @param element The element to search for tabs in.
|
|
*/
|
|
public static renderTabs(element?: Element) {
|
|
element = element || document.body;
|
|
element.querySelectorAll("[role=\"tablist\"]").forEach(function (tabList: HTMLElement) {
|
|
const identifier = tabList.dataset.identifier;
|
|
const tabLinks = tabList.querySelectorAll(".nav-link");
|
|
const tabPanes = element.querySelectorAll(`.tab-pane[data-identifier="${identifier}"]`);
|
|
|
|
tabLinks.forEach(function (tabLink: Element) {
|
|
tabLink.addEventListener("click", (ev: Event) => {
|
|
ev.preventDefault();
|
|
|
|
const controls = document.getElementById(tabLink.getAttribute("aria-controls"));
|
|
|
|
// switch "active" tab link
|
|
tabLinks.forEach(e => e.classList.remove("active"));
|
|
tabLink.classList.add("active");
|
|
|
|
// switch active tab itself
|
|
tabPanes.forEach(e => e.classList.remove("show", "active"));
|
|
controls.classList.add("show", "active");
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Renders all TeX in the document.
|
|
* @param element The element to search for TeX in.
|
|
*/
|
|
public static renderTeX(element?: Element) {
|
|
element = element || document.body;
|
|
const tex = element.getElementsByClassName("math");
|
|
Array.from(tex).forEach(function (el: Element) {
|
|
let content = el.textContent.trim();
|
|
if (content.startsWith("\\[")) content = content.slice(2);
|
|
if (content.endsWith("\\]")) content = content.slice(0, -2);
|
|
|
|
katex.render(content, el);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Renders Discord-style <t:timestamp:format> tags.
|
|
* @param element The element to search for timestamps in.
|
|
*/
|
|
public static renderTimestamps(element?: Element) {
|
|
element = element || document.body;
|
|
const timestamps = element.querySelectorAll("span[data-timestamp][data-format]");
|
|
timestamps.forEach((timestamp) => {
|
|
const seconds = parseInt(timestamp.getAttribute("data-timestamp"));
|
|
const format = timestamp.getAttribute("data-format");
|
|
const date = new Date(seconds * 1000);
|
|
|
|
const shortTimeString = date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
|
|
const shortDateString = date.toLocaleDateString([], {day: "2-digit", month: "2-digit", year: "numeric"});
|
|
const longTimeString = date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
|
const longDateString = date.toLocaleDateString([], {day: "numeric", month: "long", year: "numeric"});
|
|
const weekday = date.toLocaleString([], {weekday: "long"});
|
|
timestamp.setAttribute("title", `${weekday}, ${longDateString} ${shortTimeString}`);
|
|
|
|
switch (format) {
|
|
case "t":
|
|
timestamp.textContent = shortTimeString;
|
|
break;
|
|
|
|
case "T":
|
|
timestamp.textContent = longTimeString;
|
|
break;
|
|
|
|
case "d":
|
|
timestamp.textContent = shortDateString;
|
|
break;
|
|
|
|
case "D":
|
|
timestamp.textContent = longDateString;
|
|
break;
|
|
|
|
case "f":
|
|
timestamp.textContent = `${longDateString} at ${shortTimeString}`
|
|
break;
|
|
|
|
case "F":
|
|
timestamp.textContent = `${weekday}, ${longDateString} at ${shortTimeString}`
|
|
break;
|
|
|
|
case "R":
|
|
setInterval(() => {
|
|
timestamp.textContent = TimeUtility.formatRelativeTimestamp(date);
|
|
}, 1000);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Unescapes all <mark> tags in <pre> <code> blocks.
|
|
* @param element The element to search for <pre> <code> blocks in.
|
|
*/
|
|
public static unescapeMarkTags(element?: Element) {
|
|
element = element || document.body;
|
|
element.querySelectorAll("pre code").forEach((block) => {
|
|
let content = block.innerHTML;
|
|
|
|
// but ugly fucking hack. I hate this
|
|
content = content.replaceAll(/<mark(.*?)>/g, "<mark$1>");
|
|
content = content.replaceAll("</mark>", "</mark>");
|
|
block.innerHTML = content;
|
|
});
|
|
}
|
|
|
|
private static updateProjectCards(element?: Element) {
|
|
element = element || document.body;
|
|
element.querySelectorAll(".project-card .card-body p").forEach((p: HTMLParagraphElement) => {
|
|
p.classList.add("card-text");
|
|
});
|
|
}
|
|
}
|
|
|
|
export default UI; |