summaryrefslogtreecommitdiffstats
path: root/src/themes/serene/static/js/main.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/themes/serene/static/js/main.js259
1 files changed, 259 insertions, 0 deletions
diff --git a/src/themes/serene/static/js/main.js b/src/themes/serene/static/js/main.js
new file mode 100644
index 0000000..b0aee0c
--- /dev/null
+++ b/src/themes/serene/static/js/main.js
@@ -0,0 +1,259 @@
+function enableThemeToggle() {
+ const themeToggle = document.querySelector('#theme-toggle');
+ if (!themeToggle) return;
+ const hlLink = document.querySelector('link#hl');
+ const preferDark = window.matchMedia("(prefers-color-scheme: dark)");
+ function toggleTheme(theme) {
+ if (theme == "dark") document.body.classList.add('dark'); else document.body.classList.remove('dark');
+ if (hlLink) hlLink.href = `/hl-${theme}.css`;
+ sessionStorage.setItem("theme", theme);
+ toggleGiscusTheme(theme);
+ }
+ function toggleGiscusTheme(theme) {
+ const iframe = document.querySelector('iframe.giscus-frame');
+ if (iframe) iframe.contentWindow.postMessage({ giscus: { setConfig: { theme: `${location.origin}/giscus_${theme}.css` } } }, 'https://giscus.app');
+ }
+ function initGiscusTheme(evt) {
+ if (evt.origin !== 'https://giscus.app') return;
+ if (!(typeof evt.data === 'object' && evt.data.giscus)) return;
+ toggleGiscusTheme(sessionStorage.getItem("theme") || (preferDark.matches ? "dark" : "light"));
+ window.removeEventListener('message', initGiscusTheme);
+ }
+ window.addEventListener('message', initGiscusTheme);
+ themeToggle.addEventListener('click', () => toggleTheme(sessionStorage.getItem("theme") == "dark" ? "light" : "dark"));
+ preferDark.addEventListener("change", e => toggleTheme(e.matches ? "dark" : "light"));
+ if (!sessionStorage.getItem("theme") && preferDark.matches) toggleTheme("dark");
+ if (sessionStorage.getItem("theme") == "dark") toggleTheme("dark");
+}
+
+function enablePrerender() {
+ const prerender = (a) => {
+ if (!a.classList.contains('instant')) return;
+ const script = document.createElement('script');
+ script.type = 'speculationrules';
+ script.textContent = JSON.stringify({ prerender: [{ source: 'list', urls: [a.href] }] });
+ document.body.append(script);
+ a.classList.remove('instant');
+ }
+ const prefetch = (a) => {
+ if (!a.classList.contains('instant')) return;
+ const link = document.createElement('link');
+ link.rel = 'prefetch';
+ link.href = a.href;
+ document.head.append(link);
+ a.classList.remove('instant');
+ }
+ const support = HTMLScriptElement.supports && HTMLScriptElement.supports('speculationrules');
+ const handle = support ? prerender : prefetch;
+ document.querySelectorAll('a.instant').forEach(a => {
+ if (a.href.endsWith(window.location.pathname)) return;
+ let timer;
+ a.addEventListener('mouseenter', () => {
+ timer = setTimeout(() => handle(a), 50);
+ });
+ a.addEventListener('mouseleave', () => clearTimeout(timer));
+ a.addEventListener('touchstart', () => handle(a), { passive: true });
+ });
+}
+
+function enableRssMask() {
+ const rssBtn = document.querySelector('#rss-btn');
+ const mask = document.querySelector('#rss-mask');
+ const copyBtn = document.querySelector('#rss-mask button');
+ if (!rssBtn || !mask) return;
+ rssBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ mask.showModal();
+ });
+ const close = (e) => {
+ if (e.target == mask) mask.close();
+ };
+ mask.addEventListener('click', close);
+ const copy = () => {
+ navigator.clipboard.writeText(copyBtn.dataset.link).then(() => {
+ copyBtn.innerHTML = copyBtn.dataset.checkIcon;
+ copyBtn.classList.add('copied');
+ copyBtn.removeEventListener('click', copy);
+ setTimeout(() => {
+ mask.close();
+ copyBtn.innerHTML = copyBtn.dataset.copyIcon;
+ copyBtn.classList.remove('copied');
+ copyBtn.addEventListener('click', copy);
+ }, 400);
+ });
+ }
+ copyBtn.addEventListener('click', copy);
+}
+
+function enableOutdateAlert() {
+ const alert = document.querySelector('#outdate_alert');
+ if (!alert) return;
+ const publish = document.querySelector('#publish');
+ const updated = document.querySelector('#updated');
+ const updateDate = new Date(updated ? updated.textContent : publish.textContent);
+ const intervalDays = Math.floor((Date.now() - updateDate.getTime()) / (24 * 60 * 60 * 1000));
+ const alertDays = parseInt(alert.dataset.days);
+ if (intervalDays >= alertDays) {
+ const msg = alert.dataset.alertTextBefore + intervalDays + alert.dataset.alertTextAfter;
+ alert.querySelector('.content').textContent = msg;
+ alert.classList.remove('hidden');
+ }
+}
+
+function enableTocTooltip() {
+ const anchors = document.querySelectorAll('aside nav a');
+ if (anchors.length == 0) return;
+ const toggleTooltip = () => {
+ anchors.forEach(anchor => {
+ if (anchor.offsetWidth < anchor.scrollWidth) {
+ anchor.setAttribute('title', anchor.textContent);
+ } else {
+ anchor.removeAttribute('title');
+ }
+ });
+ };
+ window.addEventListener('resize', toggleTooltip);
+ toggleTooltip();
+}
+
+function addCopyBtns() {
+ const cfg = document.querySelector('#copy-cfg');
+ if (!cfg) return;
+ const copyIcon = cfg.dataset.copyIcon;
+ const checkIcon = cfg.dataset.checkIcon;
+ document.querySelectorAll('pre').forEach(block => {
+ if (block.classList.contains('mermaid')) return;
+ const wrapper = document.createElement('div');
+ wrapper.className = 'codeblock';
+ const btn = document.createElement('button');
+ btn.className = 'copy';
+ btn.ariaLabel = 'copy';
+ btn.innerHTML = copyIcon;
+ const copy = () => {
+ navigator.clipboard.writeText(block.textContent).then(() => {
+ btn.innerHTML = checkIcon;
+ btn.classList.add('copied');
+ btn.removeEventListener('click', copy);
+ setTimeout(() => {
+ btn.innerHTML = copyIcon;
+ btn.classList.remove('copied');
+ btn.addEventListener('click', copy);
+ }, 1500);
+ });
+ };
+ btn.addEventListener('click', copy);
+ wrapper.appendChild(block.cloneNode(true));
+ wrapper.appendChild(btn);
+ block.replaceWith(wrapper);
+ });
+}
+
+function addBackToTopBtn() {
+ const backBtn = document.querySelector('#back-to-top');
+ if (!backBtn) return;
+ const toTop = () => window.scrollTo({ top: 0 });
+ const toggle = () => {
+ const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
+ if (scrollTop > 200 && !backBtn.classList.contains('shown')) {
+ backBtn.classList.add('shown');
+ backBtn.setAttribute('tabindex', 0);
+ backBtn.addEventListener('click', toTop);
+ } else if (scrollTop <= 200 && backBtn.classList.contains('shown')) {
+ backBtn.classList.remove('shown');
+ backBtn.setAttribute('tabindex', -1);
+ backBtn.removeEventListener('click', toTop);
+ }
+ };
+ window.addEventListener('scroll', toggle);
+ toggle();
+}
+
+function addFootnoteBacklink() {
+ const footnotes = document.querySelectorAll('.footnote-definition');
+ footnotes.forEach(footnote => {
+ const backlink = document.createElement('button');
+ backlink.className = 'backlink';
+ backlink.ariaLabel = 'backlink';
+ backlink.innerHTML = '↩︎';
+ backlink.addEventListener('click', () => window.scrollTo({
+ top: document.querySelector(`.footnote-reference a[href="#${footnote.id}"]`).getBoundingClientRect().top + window.scrollY,
+ }));
+ const lastEl = footnote.lastElementChild || footnote;
+ lastEl.appendChild(backlink);
+ });
+}
+
+function enableImgLightense() {
+ window.addEventListener("load", () => Lightense(".prose img:not(.no-lightense)", { background: 'rgba(43, 43, 43, 0.19)' }));
+}
+
+function enableReaction() {
+ const container = document.querySelector('.reaction');
+ if (!container) return;
+ const endpoint = container.dataset.endpoint;
+ const slug = location.pathname.split('/').filter(Boolean).pop();
+ let state = { error: false, reaction: {} };
+ const render = () => {
+ const btns = Object.entries(state.reaction).map(([emoji, [count, reacted]])=> {
+ const span = document.createElement('span');
+ span.textContent = count;
+ const btn = document.createElement('button');
+ if (reacted) btn.classList.add('reacted');
+ btn.append(emoji, span);
+ btn.onclick = () => toggle(emoji);
+ return btn;
+ });
+ if (state.error) {
+ container.classList.add('error');
+ } else {
+ container.classList.remove('error');
+ }
+ container.replaceChildren(...btns);
+ };
+ const toggle = async (target) => {
+ const [count, reacted] = state.reaction[target];
+ state.reaction[target] = reacted ? [count - 1, false] : [count + 1, true];
+ render();
+ try {
+ const resp = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ slug, target, reacted: !reacted }),
+ });
+ if (resp.status === 200) {
+ error = false;
+ } else {
+ throw new Error();
+ }
+ } catch (err) {
+ state.error = true;
+ state.reaction[target] = [count, reacted];
+ render();
+ }
+ };
+ const init = async () => {
+ const resp = await fetch(`${endpoint}?slug=${slug}`);
+ if (resp.status === 200) {
+ state.reaction = await resp.json();
+ render();
+ }
+ };
+ init();
+}
+
+enableThemeToggle();
+enablePrerender();
+enableRssMask();
+if (document.body.classList.contains('post')) {
+ enableOutdateAlert();
+ addBackToTopBtn();
+ enableTocTooltip();
+}
+if (document.querySelector('.prose')) {
+ addCopyBtns();
+ addFootnoteBacklink();
+ enableImgLightense();
+ enableReaction();
+}