aboutsummaryrefslogtreecommitdiffstats
path: root/assets/js/scroll-to-top.js
diff options
context:
space:
mode:
authorsillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
committersillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
commit898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch)
tree85c6ee5ad58b860144551184d4cf86b560c62b91 /assets/js/scroll-to-top.js
downloadwww-main.tar.gz
www-main.zip
import live www.sillylaird.ca webrootHEADmain
Diffstat (limited to 'assets/js/scroll-to-top.js')
-rw-r--r--assets/js/scroll-to-top.js180
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();
+ }
+})();