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.