1+ // GoatCounter: https://www.goatcounter.com
2+ // This file is released under the ISC license: https://opensource.org/licenses/ISC
3+ ; ( function ( ) {
4+ 'use strict' ;
5+
6+ if ( window . goatcounter && window . goatcounter . vars ) // Compatibility with very old version; do not use.
7+ window . goatcounter = window . goatcounter . vars
8+ else
9+ window . goatcounter = window . goatcounter || { }
10+
11+ // Load settings from data-goatcounter-settings.
12+ var s = document . querySelector ( 'script[data-goatcounter]' )
13+ if ( s && s . dataset . goatcounterSettings ) {
14+ try { var set = JSON . parse ( s . dataset . goatcounterSettings ) }
15+ catch ( err ) { console . error ( 'invalid JSON in data-goatcounter-settings: ' + err ) }
16+ for ( var k in set )
17+ if ( [ 'no_onload' , 'no_events' , 'allow_local' , 'allow_frame' , 'path' , 'title' , 'referrer' , 'event' ] . indexOf ( k ) > - 1 )
18+ window . goatcounter [ k ] = set [ k ]
19+ }
20+
21+ var enc = encodeURIComponent
22+
23+ // Get all data we're going to send off to the counter endpoint.
24+ var get_data = function ( vars ) {
25+ var data = {
26+ p : ( vars . path === undefined ? goatcounter . path : vars . path ) ,
27+ r : ( vars . referrer === undefined ? goatcounter . referrer : vars . referrer ) ,
28+ t : ( vars . title === undefined ? goatcounter . title : vars . title ) ,
29+ e : ! ! ( vars . event || goatcounter . event ) ,
30+ s : [ window . screen . width , window . screen . height , ( window . devicePixelRatio || 1 ) ] ,
31+ b : is_bot ( ) ,
32+ q : location . search ,
33+ }
34+
35+ var rcb , pcb , tcb // Save callbacks to apply later.
36+ if ( typeof ( data . r ) === 'function' ) rcb = data . r
37+ if ( typeof ( data . t ) === 'function' ) tcb = data . t
38+ if ( typeof ( data . p ) === 'function' ) pcb = data . p
39+
40+ if ( is_empty ( data . r ) ) data . r = document . referrer
41+ if ( is_empty ( data . t ) ) data . t = document . title
42+ if ( is_empty ( data . p ) ) data . p = get_path ( )
43+
44+ if ( rcb ) data . r = rcb ( data . r )
45+ if ( tcb ) data . t = tcb ( data . t )
46+ if ( pcb ) data . p = pcb ( data . p )
47+ return data
48+ }
49+
50+ // Check if a value is "empty" for the purpose of get_data().
51+ var is_empty = function ( v ) { return v === null || v === undefined || typeof ( v ) === 'function' }
52+
53+ // See if this looks like a bot; there is some additional filtering on the
54+ // backend, but these properties can't be fetched from there.
55+ var is_bot = function ( ) {
56+ // Headless browsers are probably a bot.
57+ var w = window , d = document
58+ if ( w . callPhantom || w . _phantom || w . phantom )
59+ return 150
60+ if ( w . __nightmare )
61+ return 151
62+ if ( d . __selenium_unwrapped || d . __webdriver_evaluate || d . __driver_evaluate )
63+ return 152
64+ if ( navigator . webdriver )
65+ return 153
66+ return 0
67+ }
68+
69+ // Object to urlencoded string, starting with a ?.
70+ var urlencode = function ( obj ) {
71+ var p = [ ]
72+ for ( var k in obj )
73+ if ( obj [ k ] !== '' && obj [ k ] !== null && obj [ k ] !== undefined && obj [ k ] !== false )
74+ p . push ( enc ( k ) + '=' + enc ( obj [ k ] ) )
75+ return '?' + p . join ( '&' )
76+ }
77+
78+ // Show a warning in the console.
79+ var warn = function ( msg ) {
80+ if ( console && 'warn' in console )
81+ console . warn ( 'goatcounter: ' + msg )
82+ }
83+
84+ // Get the endpoint to send requests to.
85+ var get_endpoint = function ( ) {
86+ var s = document . querySelector ( 'script[data-goatcounter]' )
87+ if ( s && s . dataset . goatcounter )
88+ return s . dataset . goatcounter
89+ return ( goatcounter . endpoint || window . counter ) // counter is for compat; don't use.
90+ }
91+
92+ // Get current path.
93+ var get_path = function ( ) {
94+ var loc = location ,
95+ c = document . querySelector ( 'link[rel="canonical"][href]' )
96+ if ( c ) { // May be relative or point to different domain.
97+ var a = document . createElement ( 'a' )
98+ a . href = c . href
99+ if ( a . hostname . replace ( / ^ w w w \. / , '' ) === location . hostname . replace ( / ^ w w w \. / , '' ) )
100+ loc = a
101+ }
102+ return ( loc . pathname + loc . search ) || '/'
103+ }
104+
105+ // Run function after DOM is loaded.
106+ var on_load = function ( f ) {
107+ if ( document . body === null )
108+ document . addEventListener ( 'DOMContentLoaded' , function ( ) { f ( ) } , false )
109+ else
110+ f ( )
111+ }
112+
113+ // Filter some requests that we (probably) don't want to count.
114+ goatcounter . filter = function ( ) {
115+ if ( 'visibilityState' in document && document . visibilityState === 'prerender' )
116+ return 'visibilityState'
117+ if ( ! goatcounter . allow_frame && location !== parent . location )
118+ return 'frame'
119+ if ( ! goatcounter . allow_local && location . hostname . match ( / ( l o c a l h o s t $ | ^ 1 2 7 \. | ^ 1 0 \. | ^ 1 7 2 \. ( 1 [ 6 - 9 ] | 2 [ 0 - 9 ] | 3 [ 0 - 1 ] ) \. | ^ 1 9 2 \. 1 6 8 \. | ^ 0 \. 0 \. 0 \. 0 $ ) / ) )
120+ return 'localhost'
121+ if ( ! goatcounter . allow_local && location . protocol === 'file:' )
122+ return 'localfile'
123+ if ( localStorage && localStorage . getItem ( 'skipgc' ) === 't' )
124+ return 'disabled with #toggle-goatcounter'
125+ return false
126+ }
127+
128+ // Get URL to send to GoatCounter.
129+ window . goatcounter . url = function ( vars ) {
130+ var data = get_data ( vars || { } )
131+ if ( data . p === null ) // null from user callback.
132+ return
133+ data . rnd = Math . random ( ) . toString ( 36 ) . substr ( 2 , 5 ) // Browsers don't always listen to Cache-Control.
134+
135+ var endpoint = get_endpoint ( )
136+ if ( ! endpoint )
137+ return warn ( 'no endpoint found' )
138+
139+ return endpoint + urlencode ( data )
140+ }
141+
142+ // Count a hit.
143+ window . goatcounter . count = function ( vars ) {
144+ var f = goatcounter . filter ( )
145+ if ( f )
146+ return warn ( 'not counting because of: ' + f )
147+ var url = goatcounter . url ( vars )
148+ if ( ! url )
149+ return warn ( 'not counting because path callback returned null' )
150+
151+ if ( ! navigator . sendBeacon ( url ) ) {
152+ // This mostly fails due to being blocked by CSP; try again with an
153+ // image-based fallback.
154+ var img = document . createElement ( 'img' )
155+ img . src = url
156+ img . style . position = 'absolute' // Affect layout less.
157+ img . style . bottom = '0px'
158+ img . style . width = '1px'
159+ img . style . height = '1px'
160+ img . loading = 'eager'
161+ img . setAttribute ( 'alt' , '' )
162+ img . setAttribute ( 'aria-hidden' , 'true' )
163+
164+ var rm = function ( ) { if ( img && img . parentNode ) img . parentNode . removeChild ( img ) }
165+ img . addEventListener ( 'load' , rm , false )
166+ document . body . appendChild ( img )
167+ }
168+ }
169+
170+ // Get a query parameter.
171+ window . goatcounter . get_query = function ( name ) {
172+ var s = location . search . substr ( 1 ) . split ( '&' )
173+ for ( var i = 0 ; i < s . length ; i ++ )
174+ if ( s [ i ] . toLowerCase ( ) . indexOf ( name . toLowerCase ( ) + '=' ) === 0 )
175+ return s [ i ] . substr ( name . length + 1 )
176+ }
177+
178+ // Track click events.
179+ window . goatcounter . bind_events = function ( ) {
180+ if ( ! document . querySelectorAll ) // Just in case someone uses an ancient browser.
181+ return
182+
183+ var send = function ( elem ) {
184+ return function ( ) {
185+ goatcounter . count ( {
186+ event : true ,
187+ path : ( elem . dataset . goatcounterClick || elem . name || elem . id || '' ) ,
188+ title : ( elem . dataset . goatcounterTitle || elem . title || ( elem . innerHTML || '' ) . substr ( 0 , 200 ) || '' ) ,
189+ referrer : ( elem . dataset . goatcounterReferrer || elem . dataset . goatcounterReferral || '' ) ,
190+ } )
191+ }
192+ }
193+
194+ Array . prototype . slice . call ( document . querySelectorAll ( "*[data-goatcounter-click]" ) ) . forEach ( function ( elem ) {
195+ if ( elem . dataset . goatcounterBound )
196+ return
197+ var f = send ( elem )
198+ elem . addEventListener ( 'click' , f , false )
199+ elem . addEventListener ( 'auxclick' , f , false ) // Middle click.
200+ elem . dataset . goatcounterBound = 'true'
201+ } )
202+ }
203+
204+ // Add a "visitor counter" frame or image.
205+ window . goatcounter . visit_count = function ( opt ) {
206+ on_load ( function ( ) {
207+ opt = opt || { }
208+ opt . type = opt . type || 'html'
209+ opt . append = opt . append || 'body'
210+ opt . path = opt . path || get_path ( )
211+ opt . attr = opt . attr || { width : '200' , height : ( opt . no_branding ? '60' : '80' ) }
212+
213+ opt . attr [ 'src' ] = get_endpoint ( ) + 'er/' + enc ( opt . path ) + '.' + enc ( opt . type ) + '?'
214+ if ( opt . no_branding ) opt . attr [ 'src' ] += '&no_branding=1'
215+ if ( opt . style ) opt . attr [ 'src' ] += '&style=' + enc ( opt . style )
216+ if ( opt . start ) opt . attr [ 'src' ] += '&start=' + enc ( opt . start )
217+ if ( opt . end ) opt . attr [ 'src' ] += '&end=' + enc ( opt . end )
218+
219+ var tag = { png : 'img' , svg : 'img' , html : 'iframe' } [ opt . type ]
220+ if ( ! tag )
221+ return warn ( 'visit_count: unknown type: ' + opt . type )
222+
223+ if ( opt . type === 'html' ) {
224+ opt . attr [ 'frameborder' ] = '0'
225+ opt . attr [ 'scrolling' ] = 'no'
226+ }
227+
228+ var d = document . createElement ( tag )
229+ for ( var k in opt . attr )
230+ d . setAttribute ( k , opt . attr [ k ] )
231+
232+ var p = document . querySelector ( opt . append )
233+ if ( ! p )
234+ return warn ( 'visit_count: append not found: ' + opt . append )
235+ p . appendChild ( d )
236+ } )
237+ }
238+
239+ // Make it easy to skip your own views.
240+ if ( location . hash === '#toggle-goatcounter' ) {
241+ if ( localStorage . getItem ( 'skipgc' ) === 't' ) {
242+ localStorage . removeItem ( 'skipgc' , 't' )
243+ alert ( 'GoatCounter tracking is now ENABLED in this browser.' )
244+ }
245+ else {
246+ localStorage . setItem ( 'skipgc' , 't' )
247+ alert ( 'GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.' )
248+ }
249+ }
250+
251+ if ( ! goatcounter . no_onload )
252+ on_load ( function ( ) {
253+ // 1. Page is visible, count request.
254+ // 2. Page is not yet visible; wait until it switches to 'visible' and count.
255+ // See #487
256+ if ( ! ( 'visibilityState' in document ) || document . visibilityState === 'visible' )
257+ goatcounter . count ( )
258+ else {
259+ var f = function ( e ) {
260+ if ( document . visibilityState !== 'visible' )
261+ return
262+ document . removeEventListener ( 'visibilitychange' , f )
263+ goatcounter . count ( )
264+ }
265+ document . addEventListener ( 'visibilitychange' , f )
266+ }
267+
268+ if ( ! goatcounter . no_events )
269+ goatcounter . bind_events ( )
270+ } )
271+ } ) ( ) ;
0 commit comments