diff options
Diffstat (limited to '')
| -rw-r--r-- | assets/js/scroll-to-top.js | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/assets/js/scroll-to-top.js b/assets/js/scroll-to-top.js new file mode 100644 index 0000000..b2d6e67 --- /dev/null +++ b/assets/js/scroll-to-top.js @@ -0,0 +1,180 @@ +/* ============================================================================ + SillyLaird scroll-to-top (network-wide). + + Injected by nginx before </body> on HTML responses across *.sillylaird.ca + (see /etc/nginx/snippets/sillylaird-visit-counter.conf). Also loaded as a + fallback from site.js / nav.js. Runs once (window.__slScrollTopInit). + + Shows a fixed "▲ TOP" control after the page has been scrolled, and only + when the document is actually taller than the viewport. Works without the + host page loading site.css (styles come from scroll-to-top.css). + ============================================================================ */ +(function () { + "use strict"; + + if (window.__slScrollTopInit) return; + window.__slScrollTopInit = true; + + // Embedded forms / comments / changelog iframe: the parent page owns scroll. + try { + if (window.self !== window.top) return; + } catch (_) { + return; + } + + var path = location.pathname || "/"; + if (/\/(admin|api|login|logout|auth|captcha|relay|meta|submit)(\/|$|\.)/i.test(path)) { + return; + } + if (/\.(xml|json|m3u|pls|css|js|map|ico|png|jpe?g|gif|webp|svg|woff2?|mp3|flac|webm|mp4)(\?|$)/i.test(path)) { + return; + } + + var CSS_HREF = "https://www.sillylaird.ca/assets/css/scroll-to-top.css?v=20260821b"; + var THRESHOLD = 280; + var MIN_OVERFLOW = 240; + + function labelForLang() { + var lang = (document.documentElement.lang || "").toLowerCase(); + if (lang.indexOf("ja") === 0) return "先頭へ戻る"; + if (lang.indexOf("zh") === 0) return "回到顶部"; + return "Scroll to top"; + } + + function prefersReducedMotion() { + return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + } + + function scrollY() { + return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; + } + + function overflowPx() { + var el = document.scrollingElement || document.documentElement; + return (el ? el.scrollHeight : 0) - (window.innerHeight || 0); + } + + function ensureCss() { + var links = document.getElementsByTagName("link"); + var i; + for (i = 0; i < links.length; i++) { + if (links[i].rel === "stylesheet" && (links[i].href || "").indexOf("scroll-to-top.css") !== -1) { + return; + } + } + var l = document.createElement("link"); + l.rel = "stylesheet"; + l.href = CSS_HREF; + document.head.appendChild(l); + } + + function ensureTopTarget() { + if (document.getElementById("top")) return; + document.documentElement.id = "top"; + } + + function dodgeVisitCounter() { + var c = document.getElementById("site-visit-counter"); + if (!c) return; + var pos = (c.style && c.style.position) || ""; + var computed = window.getComputedStyle ? window.getComputedStyle(c).position : ""; + if (pos === "fixed" || computed === "fixed") { + c.setAttribute("data-stt-docked", "1"); + // Inline bottom from visit-counter.js wins over the stylesheet, so + // move it here or the two chips sit on top of each other. + c.style.bottom = "4.65rem"; + } + } + + function ensureButton() { + var existing = document.getElementById("scroll-to-top"); + if (existing) return existing; + + var a = document.createElement("a"); + a.id = "scroll-to-top"; + a.className = "scroll-to-top"; + a.href = "#top"; + var label = labelForLang(); + a.setAttribute("aria-label", label); + a.title = label; + + var caret = document.createElement("span"); + caret.className = "scroll-to-top-caret"; + caret.setAttribute("aria-hidden", "true"); + caret.appendChild(document.createTextNode("\u25B2")); + + var text = document.createElement("span"); + text.className = "scroll-to-top-label"; + text.setAttribute("aria-hidden", "true"); + text.appendChild(document.createTextNode("TOP")); + + a.appendChild(caret); + a.appendChild(text); + document.body.appendChild(a); + return a; + } + + function goTop(e) { + if (e) e.preventDefault(); + if (prefersReducedMotion() || typeof window.scrollTo !== "function") { + if (typeof window.scrollTo === "function") { + window.scrollTo(0, 0); + } else { + document.documentElement.scrollTop = 0; + document.body.scrollTop = 0; + } + return; + } + try { + window.scrollTo({ top: 0, behavior: "smooth" }); + } catch (_) { + window.scrollTo(0, 0); + } + } + + function init() { + ensureCss(); + ensureTopTarget(); + var btn = ensureButton(); + if (!btn) return; + + document.body.className = (document.body.className ? document.body.className + " " : "") + "has-scroll-to-top"; + dodgeVisitCounter(); + window.setTimeout(dodgeVisitCounter, 50); + window.setTimeout(dodgeVisitCounter, 400); + + var ticking = false; + function update() { + ticking = false; + var show = overflowPx() > MIN_OVERFLOW && scrollY() > THRESHOLD; + if (show) { + if (btn.className.indexOf("is-visible") === -1) { + btn.className += " is-visible"; + } + } else { + btn.className = btn.className.replace(/\s*is-visible\b/g, ""); + } + } + + function onScroll() { + if (ticking) return; + ticking = true; + if (window.requestAnimationFrame) { + window.requestAnimationFrame(update); + } else { + window.setTimeout(update, 16); + } + } + + window.addEventListener("scroll", onScroll, false); + window.addEventListener("resize", onScroll, false); + btn.addEventListener("click", goTop); + update(); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); |
