From 6acf4ab874c58ee14f35da671029e56972745ce6 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Fri, 25 Apr 2025 12:09:21 +0200 Subject: feat(treewide): Migrate to zola --- src/themes/serene/static/js/main.js | 259 ++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 src/themes/serene/static/js/main.js (limited to 'src/themes/serene/static/js/main.js') 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(); +} -- cgit 1.4.1