PageSourceSearch

https://www.zkhiphani.co.za/wp-content/plugins/light-views-counter/public/js/lvc-tracker.js?ver=1.3.0

js zkhiphani.co.za collected 2026-10-02 13:02:18 UTC 11,375 bytes, 476 lines download raw bytes

1/**
2 * Light Views Counter - Tracker Script
3 *
4 * Smart view counting with scroll detection and localStorage caching.
5 *
6 * @package Light_Views_Counter
7 * @since 1.0.0
8 */
9
10/* global lightvcData */
11
12(function () {
13	'use strict';
14
15	// Check if lightvcData is available
16	if ('undefined' === typeof lightvcData) {
17		return;
18	}
19
20	const config = {
21		postId: lightvcData.postId,
22		ajaxUrl: lightvcData.ajaxUrl,
23		scrollThreshold:
24			lightvcData.scrollThreshold !== undefined
25				? parseInt(lightvcData.scrollThreshold, 10)
26				: 50,
27		timeWindow: lightvcData.timeWindow !== undefined ? parseInt(lightvcData.timeWindow, 10) : 1800,
28		fastMode: lightvcData.fastMode || false,
29		excludeBots: lightvcData.excludeBots !== false,
30	};
31
32	const STORAGE_KEY = 'lightvc_viewed_posts';
33	let hasScrolled = false;
34	let hasCounted = false;
35	let localStorageAvailable = false;
36
37	/**
38	 * Check if localStorage is available and enabled.
39	 *
40	 * @return {boolean} True if localStorage is available.
41	 */
42	function isLocalStorageAvailable() {
43		try {
44			const test = '__lightvc_test__';
45			localStorage.setItem(test, test);
46			localStorage.removeItem(test);
47			return true;
48		} catch (e) {
49			return false;
50		}
51	}
52
53	/**
54	 * Detect if the current visitor is a bot.
55	 *
56	 * This function checks multiple factors to identify bots:
57	 * - User agent string patterns
58	 * - Web driver properties (automated browsers)
59	 * - Missing browser features
60	 * - Headless browser detection
61	 *
62	 * @return {boolean} True if visitor is likely a bot.
63	 */
64	function isBot() {
65		// If bot exclusion is disabled, always return false
66		if (!config.excludeBots) {
67			return false;
68		}
69
70		// Check user agent for known bot patterns
71		const userAgent = navigator.userAgent.toLowerCase();
72		const botPatterns = [
73			'googlebot',
74			'bingbot',
75			'slurp',
76			'duckduckbot',
77			'baiduspider',
78			'yandexbot',
79			'sogou',
80			'exabot',
81			'facebot',
82			'ia_archiver',
83			'crawler',
84			'spider',
85			'bot',
86			'facebookexternalhit',
87			'twitterbot',
88			'rogerbot',
89			'linkedinbot',
90			'embedly',
91			'quora link preview',
92			'showyoubot',
93			'outbrain',
94			'pinterest',
95			'developers.google.com/+/web/snippet',
96			'slackbot',
97			'vkshare',
98			'w3c_validator',
99			'redditbot',
100			'applebot',
101			'whatsapp',
102			'flipboard',
103			'tumblr',
104			'bitlybot',
105			'skypeuripreview',
106			'nuzzel',
107			'discordbot',
108			'qwantify',
109			'pinterestbot',
110			'bitrix',
111			'headlesschrome',
112			'phantomjs',
113			'slimerjs',
114			'ahrefsbot',
115			'semrushbot',
116			'dotbot',
117			'mj12bot',
118			'petalbot',
119			'screaming frog',
120			'sitebulb',
121			'lighthouse',
122		];
123
124		// Check if user agent matches any bot pattern
125		for (let i = 0; i < botPatterns.length; i++) {
126			if (userAgent.indexOf(botPatterns[i]) !== -1) {
127				return true;
128			}
129		}
130
131		// Check for web driver (Selenium, Puppeteer, etc.)
132		if (navigator.webdriver) {
133			return true;
134		}
135
136		// Check for headless browser indicators
137		if (window.navigator.plugins && window.navigator.plugins.length === 0) {
138			// Most headless browsers have no plugins
139			// But this alone isn't conclusive, so combine with other checks
140			if (
141				!window.chrome ||
142				typeof window.chrome.runtime === 'undefined'
143			) {
144				// Not Chrome or missing Chrome runtime - might be headless
145				if (
146					userAgent.indexOf('chrome') !== -1 ||
147					userAgent.indexOf('chromium') !== -1
148				) {
149					return true; // Claims to be Chrome but doesn't have Chrome properties
150				}
151			}
152		}
153
154		// Check for missing expected browser features
155		if (
156			typeof navigator.languages === 'undefined' ||
157			!navigator.languages.length
158		) {
159			return true; // Real browsers always have languages
160		}
161
162		// Check for automation tools
163		if (window.document.documentElement.getAttribute('webdriver')) {
164			return true;
165		}
166
167		// Check for PhantomJS
168		if (window._phantom || window.callPhantom) {
169			return true;
170		}
171
172		// Check for Nightmare.js
173		if (window.__nightmare) {
174			return true;
175		}
176
177		// Check for Selenium
178		if (
179			window.document.$cdc_asdjflasutopfhvcZLmcfl_ ||
180			window.document.documentElement.getAttribute('selenium')
181		) {
182			return true;
183		}
184
185		// Check Chrome driver
186		if (window.document.documentElement.getAttribute('driver')) {
187			return true;
188		}
189
190		// All checks passed - likely a real user
191		return false;
192	}
193
194	/**
195	 * Get viewed posts from localStorage.
196	 *
197	 * @return {Object} Object containing post IDs and timestamps.
198	 */
199	function getViewedPosts() {
200		if (!localStorageAvailable) {
201			return {};
202		}
203
204		try {
205			const data = localStorage.getItem(STORAGE_KEY);
206			return data ? JSON.parse(data) : {};
207		} catch (e) {
208			console.error('LVC: localStorage error', e);
209			return {};
210		}
211	}
212
213	/**
214	 * Save viewed post to localStorage.
215	 *
216	 * @param {number} postId Post ID.
217	 */
218	function saveViewedPost(postId) {
219		if (!localStorageAvailable) {
220			return;
221		}
222
223		try {
224			const viewedPosts = getViewedPosts();
225			viewedPosts[postId] = Date.now();
226
227			// Clean up old entries (older than 7 days)
228			const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
229			Object.keys(viewedPosts).forEach(function (id) {
230				if (viewedPosts[id] < sevenDaysAgo) {
231					delete viewedPosts[id];
232				}
233			});
234
235			localStorage.setItem(STORAGE_KEY, JSON.stringify(viewedPosts));
236		} catch (e) {
237			console.error('LVC: localStorage save error', e);
238		}
239	}
240
241	/**
242	 * Check if post was recently viewed.
243	 *
244	 * @param {number} postId Post ID.
245	 * @return {boolean} True if recently viewed.
246	 */
247	function wasRecentlyViewed(postId) {
248		// If localStorage not available, always allow counting
249		if (!localStorageAvailable) {
250			return false;
251		}
252
253		const viewedPosts = getViewedPosts();
254		const lastViewed = viewedPosts[postId];
255
256		if (!lastViewed) {
257			return false;
258		}
259
260		const timeWindow = config.timeWindow * 1000; // Convert seconds to milliseconds
261		const timeSinceView = Date.now() - lastViewed;
262
263		return timeSinceView < timeWindow;
264	}
265
266	/**
267	 * Get scroll percentage.
268	 *
269	 * Handles edge case where content is shorter than viewport.
270	 *
271	 * @return {number} Scroll percentage (0-100).
272	 */
273	function getScrollPercentage() {
274		const windowHeight = window.innerHeight;
275		const documentHeight = document.documentElement.scrollHeight;
276		const scrollTop =
277			window.pageYOffset || document.documentElement.scrollTop;
278
279		const trackLength = documentHeight - windowHeight;
280
281		// If content fits in viewport (no scrolling possible), return 100%
282		if (trackLength <= 0) {
283			return 100;
284		}
285
286		const percentScrolled = Math.floor((scrollTop / trackLength) * 100);
287
288		return Math.min(percentScrolled, 100);
289	}
290
291	/**
292	 * Send view count to server.
293	 */
294	function countView() {
295		if (hasCounted) {
296			return;
297		}
298
299		hasCounted = true;
300
301		const postData = JSON.stringify({
302			post_id: config.postId,
303		});
304
305		// Fast mode: Use sendBeacon API (fire-and-forget, no response)
306		if (config.fastMode && typeof navigator.sendBeacon !== 'undefined') {
307			const blob = new Blob([postData], { type: 'application/json' });
308			const sent = navigator.sendBeacon(config.ajaxUrl, blob);
309
310			if (sent) {
311				// Assume success and save to localStorage
312				saveViewedPost(config.postId);
313
314				// Trigger custom event (no view count available in fast mode)
315				if (typeof CustomEvent !== 'undefined') {
316					const event = new CustomEvent('lvcViewCounted', {
317						detail: {
318							postId: config.postId,
319							fastMode: true,
320						},
321					});
322					document.dispatchEvent(event);
323				}
324			}
325			return;
326		}
327
328		// Standard mode: Use fetch API for modern browsers, fallback to XMLHttpRequest
329		if (typeof fetch !== 'undefined') {
330			fetch(config.ajaxUrl, {
331				method: 'POST',
332				headers: {
333					'Content-Type': 'application/json',
334				},
335				body: postData,
336			})
337				.then(function (response) {
338					return response.json();
339				})
340				.then(function (data) {
341					if (data.success) {
342						saveViewedPost(config.postId);
343
344						// Trigger custom event
345						if (typeof CustomEvent !== 'undefined') {
346							const event = new CustomEvent('lvcViewCounted', {
347								detail: {
348									postId: config.postId,
349									viewCount: data.view_count,
350								},
351							});
352							document.dispatchEvent(event);
353						}
354					}
355				})
356				.catch(function (error) {
357					console.error('LVC: Count failed', error);
358					hasCounted = false; // Allow retry
359				});
360		} else {
361			// Fallback for older browsers
362			const xhr = new XMLHttpRequest();
363			xhr.open('POST', config.ajaxUrl, true);
364			xhr.setRequestHeader('Content-Type', 'application/json');
365
366			xhr.onload = function () {
367				if (xhr.status >= 200 && xhr.status < 400) {
368					try {
369						const data = JSON.parse(xhr.responseText);
370						if (data.success) {
371							saveViewedPost(config.postId);
372						}
373					} catch (e) {
374						console.error('LVC: Parse error', e);
375					}
376				}
377			};
378
379			xhr.onerror = function () {
380				console.error('LVC: Request failed');
381				hasCounted = false; // Allow retry
382			};
383
384			xhr.send(postData);
385		}
386	}
387
388	/**
389	 * Handle scroll event.
390	 */
391	function handleScroll() {
392		if (hasScrolled) {
393			return;
394		}
395
396		const scrollPercentage = getScrollPercentage();
397
398		if (scrollPercentage >= config.scrollThreshold) {
399			hasScrolled = true;
400
401			// Remove scroll listener
402			window.removeEventListener('scroll', handleScroll);
403
404			// Count the view
405			countView();
406		}
407	}
408
409	/**
410	 * Check if content is short enough to count immediately.
411	 *
412	 * @return {boolean} True if content fits in viewport.
413	 */
414	function isContentShort() {
415		const windowHeight = window.innerHeight;
416		const documentHeight = document.documentElement.scrollHeight;
417		return documentHeight <= windowHeight;
418	}
419
420	/**
421	 * Initialize the tracker.
422	 */
423	function init() {
424		// Check if visitor is a bot - exit early if true
425		if (isBot()) {
426			return;
427		}
428
429		// Check localStorage availability first
430		localStorageAvailable = isLocalStorageAvailable();
431
432		// Check if this post was recently viewed
433		if (wasRecentlyViewed(config.postId)) {
434			return;
435		}
436
437		// If scroll threshold is 0, count immediately
438		if (config.scrollThreshold === 0) {
439			countView();
440			return;
441		}
442
443		// Wait for page to fully load before checking
444		if (document.readyState === 'loading') {
445			document.addEventListener('DOMContentLoaded', function () {
446				initializeScrollTracking();
447			});
448		} else {
449			initializeScrollTracking();
450		}
451	}
452
453	/**
454	 * Initialize scroll tracking after page is ready.
455	 */
456	function initializeScrollTracking() {
457		// Check if content is too short to require scrolling
458		if (isContentShort()) {
459			// Content fits in viewport - count view after short delay
460			// This ensures user actually sees the content
461			setTimeout(function () {
462				countView();
463			}, 1000);
464			return;
465		}
466
467		// Attach scroll listener for longer content
468		window.addEventListener('scroll', handleScroll, { passive: true });
469
470		// Check initial scroll position (in case page loads scrolled)
471		setTimeout(handleScroll, 500);
472	}
473
474	// Start the tracker
475	init();
476})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.