From 08ab5606eb6bfc16e511e63d3a96d3ef77b87242 Mon Sep 17 00:00:00 2001 From: pfych Date: Sat, 18 Oct 2025 14:57:19 +1100 Subject: [PATCH] Update count.js --- .../assets/custom/scripts/external/count.js | 569 ++++++++++-------- 1 file changed, 318 insertions(+), 251 deletions(-) diff --git a/packages/site/assets/custom/scripts/external/count.js b/packages/site/assets/custom/scripts/external/count.js index e85f0bc..a29e0c8 100644 --- a/packages/site/assets/custom/scripts/external/count.js +++ b/packages/site/assets/custom/scripts/external/count.js @@ -1,254 +1,321 @@ // 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') - navigator.sendBeacon(url) - } - - // 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() - }) +(function () { + 'use strict'; + + 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. + window.goatcounter.get_data = function (vars) { + vars = 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, + 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]'); + return s && s.dataset.goatcounter + ? s.dataset.goatcounter + : goatcounter.endpoint; + }; + + // 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. + window.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 = window.goatcounter.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: element to append to 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(); + }); })(); -- 2.51.2