1/** 2 * Fathom Analytics Auto-Fill Handler 3 * Handles automated Fathom site ID creation and auto-filling 4 */ 5 6(function() { 7 'use strict'; 8 9 // Configuration 10 const FATHOM_FIELD_SELECTORS = [ 11 '#fathom_site_id', 12 'input[name="fathom_site_id"]', 13 'input[name="fathom_analytics_site_id"]', 14 'input[name="_fathom_analytics_site_id"]', 15 'input[name="analytics_fathom_site_id"]', 16 'input[name="fathom_tracking_code"]', 17 'input[name="fathom_code"]', 18 'input[name="fathom_tracking_id"]', 19 'input[name="fathom_site_code"]', 20 '[data-fathom-field]' 21 ]; 22 23 const API_ENDPOINT = '/wp-json/tbs-child/v1/fathom/site'; 24 const DEBOUNCE_DELAY = 500; 25 const CACHE_DURATION = 300000; // 5 minutes 26 27 // Global state 28 let isRequestInFlight = false; 29 let debounceTimer = null; 30 let fathomCache = { 31 id: null, 32 domain: null, 33 timestamp: null 34 }; 35 36 // Clear any invalid cached IDs on page load 37 function clearInvalidCache() { 38 if (fathomCache.id && !isValidFathomId(fathomCache.id)) {
39 console.log('Clearing invalid cached Fathom ID:', fathomCache.id); 40 fathomCache = { id: null, domain: null, timestamp: null }; 41 } 42 } 43 44 /** 45 * Initialize the Fathom auto-fill system 46 */ 47 function initFathomAutoFill() { 48 // Wait for DOM to be ready 49 if (document.readyState === 'loading') { 50 document.addEventListener('DOMContentLoaded', setupFathomAutoFill); 51 } else { 52 setupFathomAutoFill(); 53 } 54 } 55 56 /** 57 * Setup Fathom auto-fill functionality 58 */ 59 function setupFathomAutoFill() { 60 // Clear any invalid cached IDs first 61 clearInvalidCache(); 62 63 const fathomField = findFathomField(); 64 65 if (!fathomField) { 66 console.log('TBS Fathom Auto-fill: No Fathom field found'); 67 return; 68 } 69 70 console.log('TBS Fathom Auto-fill: Found Fathom field', fathomField); 71 72 // Add status indicator 73 addStatusIndicator(fathomField); 74 75 // Auto-fill on page load if field is empty 76 if (!fathomField.value.trim()) { 77 autoFillFathomId(fathomField); 78 } else { 79 // Field already has value, show connected status 80 showStatus('connected', 'Connected â (' + fathomField.value.substr(-6) + ')'); 81 } 82 83 // Add refresh button 84 addRefreshButton(fathomField); 85 86 87 // Monitor field changes to prevent overwrites 88 monitorFieldChanges(fathomField); 89 } 90 91 /** 92 * Find the Fathom field in the DOM 93 */ 94 function findFathomField() { 95 for (const selector of FATHOM_FIELD_SELECTORS) { 96 const field = document.querySelector(selector); 97 if (field) { 98 return field; 99 } 100 } 101 return null; 102 } 103 104 /** 105 * Auto-fill Fathom ID with proper guards 106 */ 107 function autoFillFathomId(field, force = false) { 108 // Check if request is already in flight 109 if (isRequestInFlight && !force) { 110 console.log('TBS Fathom Auto-fill: Request already in flight, skipping'); 111 return Promise.resolve(); 112 } 113 114 // Check if field already has a valid ID and we're not forcing 115 if (!force && field.value.trim() && isValidFathomId(field.value.trim())) { 116 console.log('TBS Fathom Auto-fill: Field already has valid ID'); 117 showStatus('connected', 'Connected â (' + field.value.substr(-6) + ')'); 118 return Promise.resolve(); 119 } 120 121 // Check cache first 122 const cachedId = getCachedFathomId(); 123 if (cachedId && !force) { 124 console.log('TBS Fathom Auto-fill: Using cached ID'); 125 setFieldValue(field, cachedId); 126 showStatus('connected', 'Connected â (' + cachedId.substr(-6) + ')'); 127 return Promise.resolve(); 128 } 129 130 // Clear any existing debounce timer 131 if (debounceTimer) { 132 clearTimeout(debounceTimer); 133 } 134 135 // Debounce the request 136 return new Promise((resolve, reject) => { 137 debounceTimer = setTimeout(() => { 138 fetchFathomId(field).then(resolve).catch(reject); 139 }, DEBOUNCE_DELAY); 140 }); 141 } 142 143 /** 144 * Fetch Fathom ID from the API 145 */ 146 async function fetchFathomId(field) { 147 if (isRequestInFlight) { 148 console.log('TBS Fathom Auto-fill: Request already in flight'); 149 return; 150 } 151 152 isRequestInFlight = true; 153 showStatus('creating', 'Creating...'); 154 155 try { 156 const domain = getCurrentDomain(); 157 158 const response = await fetch(API_ENDPOINT, { 159 method: 'POST', 160 headers: { 161 'Content-Type': 'application/json', 162 'X-WP-Nonce': getFathomNonce() 163 }, 164 body: JSON.stringify({ domain }) 165 }); 166 167 if (!response.ok) { 168 throw new Error(`HTTP ${response.status}: ${response.statusText}`); 169 } 170 171 const data = await response.json(); 172 173 if (data.success && data.id) { 174 // Cache the result 175 cacheFathomId(data.id, domain); 176 177 // Set the field value 178 setFieldValue(field, data.id); 179 180 // Show success status 181 const statusText = 'Connected â (' + data.id.substr(-6) + ')'; 182 showStatus('connected', statusText); 183 184 console.log('TBS Fathom Auto-fill: Success', data); 185 186 } else { 187 // Don't auto-fill if no ID found - let user create via CRM 188 showStatus('info', 'Create via CRM'); 189 console.log('TBS Fathom Auto-fill: No Fathom ID found, create via CRM'); 190 } 191 192 } catch (error) { 193 console.error('TBS Fathom Auto-fill: Error', error); 194 showStatus('error', 'Retry'); 195 196 // Don't clear existing value on error 197 if (!field.value.trim()) { 198 showToast('Couldn\'t fetch Fathom ID. Try again.', 'error'); 199 } 200 201 } finally { 202 isRequestInFlight = false;
203 } 204 } 205 206 /** 207 * Set field value and trigger events 208 */ 209 function setFieldValue(field, value) { 210 if (field.value === value) { 211 return; // No change needed 212 } 213 214 field.value = value; 215 216 // Mark field as changed/dirty 217 field.setAttribute('data-fathom-autofilled', 'true'); 218 219 // Trigger change events for form frameworks 220 const events = ['input', 'change', 'blur']; 221 events.forEach(eventType => { 222 const event = new Event(eventType, { bubbles: true, cancelable: true }); 223 field.dispatchEvent(event); 224 }); 225 226 // Trigger jQuery events if jQuery is available 227 if (window.jQuery && window.jQuery(field).length) { 228 window.jQuery(field).trigger('change').trigger('input'); 229 } 230 231 console.log('TBS Fathom Auto-fill: Field value set and events triggered', value); 232 } 233 234 /** 235 * Add status indicator next to the field 236 */ 237 function addStatusIndicator(field) { 238 // Check if status indicator already exists 239 if (field.parentNode.querySelector('.fathom-status')) { 240 return; 241 } 242 243 const status = document.createElement('span'); 244 status.className = 'fathom-status'; 245 status.style.cssText = ` 246 margin-left: 8px; 247 padding: 4px 8px; 248 border-radius: 4px; 249 font-size: 12px; 250 font-weight: 500; 251 display: inline-block; 252 min-width: 60px; 253 text-align: center; 254 `; 255 256 // Insert after the field 257 field.parentNode.insertBefore(status, field.nextSibling); 258 } 259 260 /** 261 * Add refresh button 262 */ 263 function addRefreshButton(field) { 264 // Check if refresh button already exists 265 if (field.parentNode.querySelector('.fathom-refresh')) { 266 return; 267 } 268 269 const button = document.createElement('button'); 270 button.type = 'button'; 271 button.className = 'fathom-refresh button button-secondary'; 272 button.textContent = 'Get Fathom ID'; 273 button.style.cssText = ` 274 margin-left: 8px; 275 padding: 4px 12px; 276 font-size: 12px; 277 height: auto; 278 `; 279 280 button.addEventListener('click', (e) => { 281 e.preventDefault(); 282 autoFillFathomId(field, true); // Force refresh 283 }); 284 285 // Insert after status or field 286 const status = field.parentNode.querySelector('.fathom-status'); 287 const insertAfter = status || field; 288 insertAfter.parentNode.insertBefore(button, insertAfter.nextSibling); 289 } 290 291 292 /** 293 * Show status with appropriate styling 294 */ 295 function showStatus(type, text) { 296 const status = document.querySelector('.fathom-status'); 297 if (!status) return; 298 299 status.textContent = text; 300 status.className = 'fathom-status fathom-status-' + type; 301 302 // Apply type-specific styles 303 const baseStyle = ` 304 margin-left: 8px; 305 padding: 4px 8px; 306 border-radius: 4px; 307 font-size: 12px; 308 font-weight: 500; 309 display: inline-block; 310 min-width: 60px; 311 text-align: center; 312 `; 313 314 const typeStyles = { 315 creating: 'background: #f0f0f1; color: #646970;', 316 connected: 'background: #d1e7dd; color: #0f5132;', 317 error: 'background: #f8d7da; color: #721c24; cursor: pointer;' 318 }; 319 320 status.style.cssText = baseStyle + (typeStyles[type] || ''); 321 322 // Make error status clickable to retry 323 if (type === 'error') { 324 status.style.cursor = 'pointer'; 325 status.title = 'Click to retry'; 326 status.onclick = () => { 327 const field = findFathomField(); 328 if (field) { 329 autoFillFathomId(field, true); 330 } 331 }; 332 } else { 333 status.onclick = null; 334 status.title = ''; 335 } 336 } 337 338 /**
339 * Monitor field changes to prevent overwrites 340 */ 341 function monitorFieldChanges(field) { 342 let lastValue = field.value; 343 344 const checkChanges = () => { 345 if (field.value !== lastValue) { 346 lastValue = field.value; 347 348 if (field.value.trim() && isValidFathomId(field.value.trim())) { 349 showStatus('connected', 'Connected â (' + field.value.substr(-6) + ')'); 350 } else if (!field.value.trim()) { 351 showStatus('', ''); 352 } 353 } 354 }; 355 356 field.addEventListener('input', checkChanges); 357 field.addEventListener('change', checkChanges); 358 field.addEventListener('paste', () => setTimeout(checkChanges, 10)); 359 } 360 361 /** 362 * Validate Fathom ID format 363 */ 364 function isValidFathomId(id) { 365 // Basic format validation 366 if (!/^[A-Z0-9]{8}$/.test(id)) { 367 return false; 368 } 369 370 // Must contain both letters and numbers 371 if (!/[A-Z]/.test(id) || !/[0-9]/.test(id)) { 372 return false; 373 } 374 375 // Check against configurable invalid patterns 376 const invalidPatterns = getInvalidFathomPatterns(); 377 for (const pattern of invalidPatterns) { 378 if (new RegExp(pattern.replace(/\//g, '')).test(id)) { 379 return false; 380 } 381 } 382 383 return true; 384 } 385 386 /** 387 * Get invalid Fathom ID patterns (configurable) 388 */ 389 function getInvalidFathomPatterns() { 390 // Default patterns that are unlikely to be valid Fathom IDs 391 const defaultPatterns = [ 392 '^[0-9]{8}$', // All digits 393 '^[A-Z]{8}$', // All letters 394 '^(TEST|DEMO|FAKE|NULL|VOID)', // Test values 395 '^MGCGAZMU$', // Block old hardcoded ID 396 '^3X8IDTFI$', // Block current fake ID 397 ]; 398 399 // Allow customization via window object 400 if (window.tbsInvalidFathomPatterns && Array.isArray(window.tbsInvalidFathomPatterns)) { 401 return [...defaultPatterns, ...window.tbsInvalidFathomPatterns]; 402 } 403 404 return defaultPatterns; 405 } 406 407 /** 408 * Get current domain for API request 409 */ 410 function getCurrentDomain() { 411 return window.location.hostname; 412 } 413 414 /** 415 * Get WordPress nonce for API requests 416 */ 417 function getFathomNonce() { 418 // Try to get nonce from various sources 419 const sources = [ 420 () => window.fathomNonce, 421 () => window.tbsAjax?.nonce, 422 () => window.pbnAjax?.nonce, 423 () => document.querySelector('meta[name="wp-nonce"]')?.content, 424 () => document.querySelector('#_wpnonce')?.value 425 ]; 426 427 for (const source of sources) { 428 try { 429 const nonce = source(); 430 if (nonce) return nonce; 431 } catch (e) { 432 // Continue to next source 433 } 434 } 435 436 return ''; // Endpoint doesn't require nonce 437 } 438 439 /** 440 * Cache Fathom ID 441 */ 442 function cacheFathomId(id, domain) { 443 fathomCache = { 444 id, 445 domain, 446 timestamp: Date.now() 447 }; 448 } 449 450 /** 451 * Get cached Fathom ID if valid 452 */ 453 function getCachedFathomId() { 454 const now = Date.now(); 455 const domain = getCurrentDomain(); 456 457 if (fathomCache.id && 458 fathomCache.domain === domain && 459 fathomCache.timestamp && 460 (now - fathomCache.timestamp) < CACHE_DURATION) { 461 return fathomCache.id; 462 } 463 464 return null; 465 } 466 467 /** 468 * Show toast notification 469 */ 470 function showToast(message, type = 'info') { 471 // Create toast element 472 const toast = document.createElement('div'); 473 toast.className = 'fathom-toast fathom-toast-' + type; 474 toast.textContent = message; 475 toast.style.cssText = `
476 position: fixed; 477 top: 20px; 478 right: 20px; 479 padding: 12px 16px; 480 border-radius: 4px; 481 font-size: 14px; 482 font-weight: 500; 483 z-index: 999999; 484 box-shadow: 0 2px 8px rgba(0,0,0,0.15); 485 ${type === 'error' ? 'background: #dc3545; color: white;' : 'background: #17a2b8; color: white;'} 486 `; 487 488 document.body.appendChild(toast); 489 490 // Auto-remove after 3 seconds 491 setTimeout(() => { 492 if (toast.parentNode) { 493 toast.parentNode.removeChild(toast); 494 } 495 }, 3000); 496 } 497 498 // Initialize when script loads 499 initFathomAutoFill(); 500 501 // Export for manual triggering if needed 502 window.TBSFathomAutoFill = { 503 init: initFathomAutoFill, 504 refresh: () => { 505 const field = findFathomField(); 506 if (field) { 507 autoFillFathomId(field, true); 508 } 509 } 510 }; 511 512})();
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.