diff --git a/_includes/default_head_tags.html b/_includes/default_head_tags.html
index 3d61dca..fe03894 100644
--- a/_includes/default_head_tags.html
+++ b/_includes/default_head_tags.html
@@ -66,13 +66,16 @@
{% unless jekyll.environment == "development" %}
+ async src="/assets/js/goat_count.js">
{% endunless %}
+
+
+
{% if page.mathjax %}
---
I pulled this dying LED downlight out of my kitchen ceiling.
diff --git a/assets/js/expand_img_tags.js b/assets/js/expand_img_tags.js
index cbb082f..704446c 100644
--- a/assets/js/expand_img_tags.js
+++ b/assets/js/expand_img_tags.js
@@ -1,14 +1,48 @@
-function isIOS() {
- return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
-}
+// Add this style block once to your document (or in a CSS file)
+const style = document.createElement("style");
+style.textContent = `
+ .fullscreen-overlay {
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s ease;
+ }
+ .fullscreen-overlay.active {
+ opacity: 1;
+ pointer-events: auto;
+ }
+`;
+document.head.appendChild(style);
+
+// Create and style fullscreen overlay container
+const fullscreenContainer = document.createElement("div");
+fullscreenContainer.className = "fullscreen-overlay";
+fullscreenContainer.style.position = "fixed";
+fullscreenContainer.style.top = 0;
+fullscreenContainer.style.left = 0;
+fullscreenContainer.style.width = "100vw";
+fullscreenContainer.style.height = "100vh";
+fullscreenContainer.style.background = "var(--theme-bg-color)";
+fullscreenContainer.style.display = "flex";
+fullscreenContainer.style.alignItems = "center";
+fullscreenContainer.style.justifyContent = "center";
+fullscreenContainer.style.zIndex = 9999;
+fullscreenContainer.style.cursor = "zoom-out";
+
+const fullscreenImage = document.createElement("img");
+fullscreenImage.style.maxWidth = "90vw";
+fullscreenImage.style.maxHeight = "90vh";
+fullscreenImage.style.boxShadow = "0 0 20px rgba(0,0,0,0.8)";
+fullscreenContainer.appendChild(fullscreenImage);
+
+document.body.appendChild(fullscreenContainer);
+
+// Fullscreen API helpers
function enterFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
- } else {
- console.warn("Fullscreen API not supported on this browser");
}
}
@@ -20,43 +54,19 @@ function exitFullscreen() {
}
}
-// Create and style fullscreen overlay container
-const fullscreenContainer = document.createElement("div");
-fullscreenContainer.style.position = "fixed";
-fullscreenContainer.style.top = 0;
-fullscreenContainer.style.left = 0;
-fullscreenContainer.style.width = "100vw";
-fullscreenContainer.style.height = "100vh";
-fullscreenContainer.style.background = "rgba(0, 0, 0, 0.95)";
-fullscreenContainer.style.display = "flex";
-fullscreenContainer.style.alignItems = "center";
-fullscreenContainer.style.justifyContent = "center";
-fullscreenContainer.style.zIndex = 9999;
-fullscreenContainer.style.cursor = "zoom-out";
-fullscreenContainer.style.visibility = "hidden";
-
-const fullscreenImage = document.createElement("img");
-fullscreenImage.style.maxWidth = "90vw";
-fullscreenImage.style.maxHeight = "90vh";
-fullscreenImage.style.boxShadow = "0 0 20px rgba(0,0,0,0.8)";
-fullscreenContainer.appendChild(fullscreenImage);
-
-document.body.appendChild(fullscreenContainer);
-
+// Attach listeners
document.querySelectorAll("img").forEach((img) => {
img.style.cursor = "zoom-in";
img.addEventListener("click", () => {
console.log("Image clicked:", img.src);
fullscreenImage.src = img.src;
- fullscreenContainer.style.visibility = "visible";
-
- // Only attempt fullscreen if supported
+ fullscreenContainer.classList.add("active");
enterFullscreen(fullscreenContainer);
});
});
fullscreenContainer.addEventListener("click", () => {
console.log("Exiting fullscreen");
- fullscreenContainer.style.visibility = "hidden";
+ fullscreenContainer.classList.remove("active");
exitFullscreen();
});
diff --git a/assets/js/goat_count.js b/assets/js/goat_count.js
new file mode 100644
index 0000000..2977137
--- /dev/null
+++ b/assets/js/goat_count.js
@@ -0,0 +1,323 @@
+// GoatCounter: https://www.goatcounter.com
+// This file is released under the ISC license: https://opensource.org/licenses/ISC
+(function () {
+ "use strict";
+
+ if (window.goatcounter && window.goatcounter.vars)
+ // Compatibility with very old version; do not use.
+ window.goatcounter = window.goatcounter.vars;
+ else window.goatcounter = window.goatcounter || {};
+
+ // Load settings from data-goatcounter-settings.
+ var s = document.querySelector("script[data-goatcounter]");
+ if (s && s.dataset.goatcounterSettings) {
+ try {
+ var set = JSON.parse(s.dataset.goatcounterSettings);
+ } catch (err) {
+ console.error("invalid JSON in data-goatcounter-settings: " + err);
+ }
+ for (var k in set)
+ if (
+ [
+ "no_onload",
+ "no_events",
+ "allow_local",
+ "allow_frame",
+ "path",
+ "title",
+ "referrer",
+ "event",
+ ].indexOf(k) > -1
+ )
+ window.goatcounter[k] = set[k];
+ }
+
+ var enc = encodeURIComponent;
+
+ // Get all data we're going to send off to the counter endpoint.
+ var get_data = function (vars) {
+ var data = {
+ p: vars.path === undefined ? goatcounter.path : vars.path,
+ r: vars.referrer === undefined ? goatcounter.referrer : vars.referrer,
+ t: vars.title === undefined ? goatcounter.title : vars.title,
+ e: !!(vars.event || goatcounter.event),
+ s: [
+ window.screen.width,
+ window.screen.height,
+ window.devicePixelRatio || 1,
+ ],
+ b: is_bot(),
+ q: location.search,
+ };
+
+ var rcb, pcb, tcb; // Save callbacks to apply later.
+ if (typeof data.r === "function") rcb = data.r;
+ if (typeof data.t === "function") tcb = data.t;
+ if (typeof data.p === "function") pcb = data.p;
+
+ if (is_empty(data.r)) data.r = document.referrer;
+ if (is_empty(data.t)) data.t = document.title;
+ if (is_empty(data.p)) data.p = get_path();
+
+ if (rcb) data.r = rcb(data.r);
+ if (tcb) data.t = tcb(data.t);
+ if (pcb) data.p = pcb(data.p);
+ return data;
+ };
+
+ // Check if a value is "empty" for the purpose of get_data().
+ var is_empty = function (v) {
+ return v === null || v === undefined || typeof v === "function";
+ };
+
+ // See if this looks like a bot; there is some additional filtering on the
+ // backend, but these properties can't be fetched from there.
+ var is_bot = function () {
+ // Headless browsers are probably a bot.
+ var w = window,
+ d = document;
+ if (w.callPhantom || w._phantom || w.phantom) return 150;
+ if (w.__nightmare) return 151;
+ if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
+ return 152;
+ if (navigator.webdriver) return 153;
+ return 0;
+ };
+
+ // Object to urlencoded string, starting with a ?.
+ var urlencode = function (obj) {
+ var p = [];
+ for (var k in obj)
+ if (
+ obj[k] !== "" &&
+ obj[k] !== null &&
+ obj[k] !== undefined &&
+ obj[k] !== false
+ )
+ p.push(enc(k) + "=" + enc(obj[k]));
+ return "?" + p.join("&");
+ };
+
+ // Show a warning in the console.
+ var warn = function (msg) {
+ if (console && "warn" in console) console.warn("goatcounter: " + msg);
+ };
+
+ // Get the endpoint to send requests to.
+ var get_endpoint = function () {
+ var s = document.querySelector("script[data-goatcounter]");
+ if (s && s.dataset.goatcounter) return s.dataset.goatcounter;
+ return goatcounter.endpoint || window.counter; // counter is for compat; don't use.
+ };
+
+ // Get current path.
+ var get_path = function () {
+ var loc = location,
+ c = document.querySelector('link[rel="canonical"][href]');
+ if (c) {
+ // May be relative or point to different domain.
+ var a = document.createElement("a");
+ a.href = c.href;
+ if (
+ a.hostname.replace(/^www\./, "") ===
+ location.hostname.replace(/^www\./, "")
+ )
+ loc = a;
+ }
+ return loc.pathname + loc.search || "/";
+ };
+
+ // Run function after DOM is loaded.
+ var on_load = function (f) {
+ if (document.body === null)
+ document.addEventListener(
+ "DOMContentLoaded",
+ function () {
+ f();
+ },
+ false
+ );
+ else f();
+ };
+
+ // Filter some requests that we (probably) don't want to count.
+ goatcounter.filter = function () {
+ if (
+ "visibilityState" in document &&
+ document.visibilityState === "prerender"
+ )
+ return "visibilityState";
+ if (!goatcounter.allow_frame && location !== parent.location)
+ return "frame";
+ if (
+ !goatcounter.allow_local &&
+ location.hostname.match(
+ /(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/
+ )
+ )
+ return "localhost";
+ if (!goatcounter.allow_local && location.protocol === "file:")
+ return "localfile";
+ if (localStorage && localStorage.getItem("skipgc") === "t")
+ return "disabled with #toggle-goatcounter";
+ return false;
+ };
+
+ // Get URL to send to GoatCounter.
+ window.goatcounter.url = function (vars) {
+ var data = get_data(vars || {});
+ if (data.p === null)
+ // null from user callback.
+ return;
+ data.rnd = Math.random().toString(36).substr(2, 5); // Browsers don't always listen to Cache-Control.
+
+ var endpoint = get_endpoint();
+ if (!endpoint) return warn("no endpoint found");
+
+ return endpoint + urlencode(data);
+ };
+
+ // Count a hit.
+ window.goatcounter.count = function (vars) {
+ var f = goatcounter.filter();
+ if (f) return warn("not counting because of: " + f);
+ var url = goatcounter.url(vars);
+ if (!url) return warn("not counting because path callback returned null");
+
+ if (!navigator.sendBeacon(url)) {
+ // This mostly fails due to being blocked by CSP; try again with an
+ // image-based fallback.
+ var img = document.createElement("img");
+ img.src = url;
+ img.style.position = "absolute"; // Affect layout less.
+ img.style.bottom = "0px";
+ img.style.width = "1px";
+ img.style.height = "1px";
+ img.loading = "eager";
+ img.setAttribute("alt", "");
+ img.setAttribute("aria-hidden", "true");
+
+ var rm = function () {
+ if (img && img.parentNode) img.parentNode.removeChild(img);
+ };
+ img.addEventListener("load", rm, false);
+ document.body.appendChild(img);
+ }
+ };
+
+ // Get a query parameter.
+ window.goatcounter.get_query = function (name) {
+ var s = location.search.substr(1).split("&");
+ for (var i = 0; i < s.length; i++)
+ if (s[i].toLowerCase().indexOf(name.toLowerCase() + "=") === 0)
+ return s[i].substr(name.length + 1);
+ };
+
+ // Track click events.
+ window.goatcounter.bind_events = function () {
+ if (!document.querySelectorAll)
+ // Just in case someone uses an ancient browser.
+ return;
+
+ var send = function (elem) {
+ return function () {
+ goatcounter.count({
+ event: true,
+ path: elem.dataset.goatcounterClick || elem.name || elem.id || "",
+ title:
+ elem.dataset.goatcounterTitle ||
+ elem.title ||
+ (elem.innerHTML || "").substr(0, 200) ||
+ "",
+ referrer:
+ elem.dataset.goatcounterReferrer ||
+ elem.dataset.goatcounterReferral ||
+ "",
+ });
+ };
+ };
+
+ Array.prototype.slice
+ .call(document.querySelectorAll("*[data-goatcounter-click]"))
+ .forEach(function (elem) {
+ if (elem.dataset.goatcounterBound) return;
+ var f = send(elem);
+ elem.addEventListener("click", f, false);
+ elem.addEventListener("auxclick", f, false); // Middle click.
+ elem.dataset.goatcounterBound = "true";
+ });
+ };
+
+ // Add a "visitor counter" frame or image.
+ window.goatcounter.visit_count = function (opt) {
+ on_load(function () {
+ opt = opt || {};
+ opt.type = opt.type || "html";
+ opt.append = opt.append || "body";
+ opt.path = opt.path || get_path();
+ opt.attr = opt.attr || {
+ width: "200",
+ height: opt.no_branding ? "60" : "80",
+ };
+
+ opt.attr["src"] =
+ get_endpoint() + "er/" + enc(opt.path) + "." + enc(opt.type) + "?";
+ if (opt.no_branding) opt.attr["src"] += "&no_branding=1";
+ if (opt.style) opt.attr["src"] += "&style=" + enc(opt.style);
+ if (opt.start) opt.attr["src"] += "&start=" + enc(opt.start);
+ if (opt.end) opt.attr["src"] += "&end=" + enc(opt.end);
+
+ var tag = { png: "img", svg: "img", html: "iframe" }[opt.type];
+ if (!tag) return warn("visit_count: unknown type: " + opt.type);
+
+ if (opt.type === "html") {
+ opt.attr["frameborder"] = "0";
+ opt.attr["scrolling"] = "no";
+ }
+
+ var d = document.createElement(tag);
+ for (var k in opt.attr) d.setAttribute(k, opt.attr[k]);
+
+ var p = document.querySelector(opt.append);
+ if (!p) return warn("visit_count: append not found: " + opt.append);
+ p.appendChild(d);
+ });
+ };
+
+ // Make it easy to skip your own views.
+ if (location.hash === "#toggle-goatcounter") {
+ if (localStorage.getItem("skipgc") === "t") {
+ localStorage.removeItem("skipgc", "t");
+ alert("GoatCounter tracking is now ENABLED in this browser.");
+ } else {
+ localStorage.setItem("skipgc", "t");
+ alert(
+ "GoatCounter tracking is now DISABLED in this browser until " +
+ location +
+ " is loaded again."
+ );
+ }
+ }
+
+ if (!goatcounter.no_onload)
+ on_load(function () {
+ // 1. Page is visible, count request.
+ // 2. Page is not yet visible; wait until it switches to 'visible' and count.
+ // See #487
+ if (
+ !("visibilityState" in document) ||
+ document.visibilityState === "visible"
+ )
+ goatcounter.count();
+ else {
+ var f = function (e) {
+ if (document.visibilityState !== "visible") return;
+ document.removeEventListener("visibilitychange", f);
+ goatcounter.count();
+ };
+ document.addEventListener("visibilitychange", f);
+ }
+
+ if (!goatcounter.no_events) goatcounter.bind_events();
+ });
+})();