1/** 2 * Unified Ad Logic 3 * 4 * Combines: 5 * - Google Tag Display Controller 6 * - Google Tag Ad Slot Generator 7 * - Prestitial Ad Logic 8 * 9 * @package WTWHUniversalGAMPlugin 10 * @since 1.0.0 11 */ 12 13(function(window, document) { 14 'use strict'; 15 16 var dataLayer = window.dataLayer = window.dataLayer || []; 17 var googletag; 18 19 /** 20 * Check whether the provided googletag object has a usable command queue. 21 * @param {*} tagObject - Candidate googletag object. 22 * @returns {boolean} True when `cmd.push` is callable. 23 */ 24 function hasCommandQueue(tagObject) { 25 return !!(tagObject && tagObject.cmd && typeof tagObject.cmd.push === 'function'); 26 } 27 28 /** 29 * Normalize the active googletag object so command queue access is always safe. 30 * Preserves any queued callbacks if another script replaces `window.googletag`. 31 * @param {*} tagObject - Candidate googletag object. 32 * @returns {Object} Normalized googletag object. 33 */ 34 function normalizeGoogletag(tagObject) { 35 var next = tagObject && typeof tagObject === 'object' ? tagObject : {}; 36 var previousQueue = hasCommandQueue(googletag) ? googletag.cmd : null; 37 var nextQueue = hasCommandQueue(next) ? next.cmd : null; 38 39 if (!nextQueue) { 40 next.cmd = previousQueue || []; 41 nextQueue = next.cmd; 42 } else if (previousQueue && previousQueue !== nextQueue && Array.isArray(previousQueue) && previousQueue.length > 0) { 43 previousQueue.slice().forEach(function(command) { 44 nextQueue.push(command); 45 }); 46 previousQueue.length = 0; 47 } 48 49 googletag = next; 50 return next; 51 } 52 53 /** 54 * Queue a callback for GPT once the command queue is ready. 55 * @param {Function} callback - Function to execute when GPT is ready. 56 * @returns {*} Return value from the underlying `cmd.push`. 57 */ 58 function pushGoogletagCommand(callback) { 59 return normalizeGoogletag(window.googletag).cmd.push(callback); 60 } 61 62 googletag = normalizeGoogletag(window.googletag); 63 window.googletag = googletag; 64 65 /** 66 * Create a function wrapper that only executes once. 67 * Subsequent calls are no-ops and return `undefined`. 68 * @param {Function} fn - Function to execute a single time. 69 * @returns {Function} Wrapped function that runs at most once. 70 * @throws {Error} None. 71 */ 72 function once(fn) { 73 var called = false; 74 return function() { 75 if (called) return; 76 called = true; 77 return fn.apply(this, arguments); 78 }; 79 } 80 81 // --------------------------------------------------------------------- 82 // Google Tag Ad Slot Generator 83 // --------------------------------------------------------------------- 84 if (typeof window.googletagAdSlotGenerator === 'undefined') { 85 window.googletagAdSlotGenerator = (function() { 86 // Private variables 87 var slotsDefinedCount = 0; 88 var servicesEnabled = false; 89 var definedAdDivIds = []; 90 var legacySpecialDivIds = ['mlt-keyword-ad-container', 'search-keyword-ad-container']; 91 var prestitialRenderTimeoutMs = 10000; 92 const prestitialAutoCloseMs = 20000; 93 var prestitialState = { 94 status: 'idle', 95 timeoutId: null, 96 initialized: false, 97 markupInjected: false 98 }; 99 var displayState = { 100 originalDisplay: null, 101 isDisabled: false, 102 queuedCalls: [] 103 }; 104 var slotRenderLoggingBound = false; 105 var logBuffer = []; 106 107 /** 108 * Store a log entry for later inspection. 109 * Captures a timestamp and optional data payload. 110 * @param {string} message - Short message describing the event. 111 * @param {*} data - Optional contextual data to attach to the entry. 112 * @returns {void} No return value. 113 * @throws {Error} None. 114 */ 115 function log(message, data) { 116 var entry = { 117 ts: new Date().toISOString(), 118 message: message 119 }; 120 if (typeof data !== 'undefined') { 121 entry.data = data; 122 } 123 logBuffer.push(entry); 124 } 125 126 /** 127 * Resolve the active prestitial div id from localized config. 128 * @returns {string} Prestitial slot div id. 129 */ 130 function getPrestitialDivId() { 131 if (typeof PRESTITIALINFO !== 'undefined' && PRESTITIALINFO.divId) { 132 return PRESTITIALINFO.divId; 133 } 134 135 if (typeof wtwh_gam_ad_config !== 'undefined' && wtwh_gam_ad_config.prestitial && wtwh_gam_ad_config.prestitial.div_id) { 136 return wtwh_gam_ad_config.prestitial.div_id; 137 } 138 139 return 'gam-holder-prestitial-banner'; 140 } 141 142 /** 143 * Get div ids for deferred keyword-related and keyword-search slots. 144 * These slots may be rendered by page-specific markup after init, so they 145 * must not fail slot definition or be auto-displayed with
145normal slots. 146 * @returns {Array} Div ids treated as special/deferred containers. 147 */ 148 function getSpecialDivIds() { 149 var specialIds = legacySpecialDivIds.slice(); 150 151 if (typeof wtwh_gam_ad_config === 'undefined' || !Array.isArray(wtwh_gam_ad_config.slots)) { 152 return specialIds; 153 } 154 155 wtwh_gam_ad_config.slots.forEach(function(slot) { 156 if (!slot || typeof slot !== 'object') { 157 return; 158 } 159 160 if (slot.ad_type !== 'keyword_related' && slot.ad_type !== 'keyword_search') { 161 return; 162 } 163 164 if (typeof slot.div_id !== 'string' || !slot.div_id) { 165 return; 166 } 167 168 if (!specialIds.includes(slot.div_id)) { 169 specialIds.push(slot.div_id); 170 } 171 }); 172 173 return specialIds; 174 } 175 176 /** 177 * Output all stored log entries to the console. 178 * Uses a grouped console display for readability. 179 * @returns {void} No return value. 180 * @throws {Error} None. 181 */ 182 function dumpLog() { 183 if (logBuffer.length === 0) { 184 console.info('GAM Log: empty'); 185 return; 186 } 187 console.group('GAM Log'); 188 logBuffer.forEach(function(entry) { 189 if (entry.data !== undefined) { 190 console.log(entry.ts + ' - ' + entry.message, entry.data); 191 } else { 192 console.log(entry.ts + ' - ' + entry.message); 193 } 194 }); 195 console.groupEnd(); 196 } 197 198 /** 199 * Check whether the DOM is ready for queries and manipulation. 200 * @returns {boolean} True when ready, otherwise false. 201 * @throws {Error} None. 202 */ 203 function isDOMReady() { 204 return document.readyState === 'complete' || document.readyState === 'interactive'; 205 } 206 207 /** 208 * Invoke a callback once the DOM is ready. 209 * @param {Function} callback - Function to run after DOM is ready. 210 * @returns {void} No return value. 211 * @throws {Error} None. 212 */ 213 function waitForDOMReady(callback) { 214 if (isDOMReady()) { 215 callback(); 216 } else { 217 document.addEventListener('DOMContentLoaded', callback); 218 } 219 } 220 221 /** 222 * Parse an ad size string in the format "WIDTHxHEIGHT". 223 * @param {string} sizeStr - Size string such as "300x250". 224 * @returns {number[]} Width/height tuple; returns `[0, 0]` on invalid input. 225 * @throws {Error} None. 226 */ 227 function parseSizeString(sizeStr) { 228 if (typeof sizeStr !== 'string') { 229 log('size-parse-skip', {reason: 'not-string', value: sizeStr}); 230 return [0, 0]; 231 } 232 var parts = sizeStr.split('x'); 233 return [parseInt(parts[0], 10), parseInt(parts[1], 10)]; 234 } 235 236 /** 237 * Convert size strings into numeric pairs, preserving numeric pairs as-is. 238 * @param {Array} sizes - Array of strings like "300x250" or numeric pairs. 239 * @returns {Array} Array of size pairs; returns empty array on invalid input. 240 * @throws {Error} None. 241 */ 242 function convertSizes(sizes) { 243 if (!sizes || !Array.isArray(sizes)) { 244 log('sizes-convert-skip', {reason: 'invalid-sizes', value: sizes}); 245 return []; 246 } 247 return sizes.map(function(size) { 248 return typeof size === 'string' ? parseSizeString(size) : size; 249 }); 250 } 251 252 /** 253 * Merge desktop and mobile sizes into a unique list. 254 * @param {Array} desktopSizes - Desktop size list. 255 * @param {Array} mobileSizes - Mobile size list. 256 * @returns {Array} De-duplicated list of size pairs. 257 * @throws {Error} None. 258 */ 259 function getAllSizes(desktopSizes, mobileSizes) { 260 var allSizes = {}; 261 var result = []; 262
263 convertSizes(desktopSizes).forEach(function(size) { 264 var key = size[0] + 'x' + size[1]; 265 if (!allSizes[key]) { 266 allSizes[key] = true; 267 result.push(size); 268 } 269 }); 270 271 convertSizes(mobileSizes).forEach(function(size) { 272 var key = size[0] + 'x' + size[1]; 273 if (!allSizes[key]) { 274 allSizes[key] = true; 275 result.push(size); 276 } 277 }); 278 279 return result; 280 } 281 282 /** 283 * Build a GPT size mapping based on wallpaper rules and hostname. 284 * @param {boolean} isWallpaper - Whether the ad is a wallpaper unit. 285 * @param {Array} desktopSizes - Desktop size pairs. 286 * @param {Array} mobileSizes - Mobile size pairs. 287 * @returns {Object} GPT size mapping object. 288 * @throws {Error} None. 289 */ 290 function generateSizeMapping(isWallpaper, desktopSizes, mobileSizes) { 291 var hostname = (window.location && window.location.hostname) ? window.location.hostname.toLowerCase() : ''; 292 var isPMQ = hostname.indexOf('pmq') !== -1; 293 log('size-mapping', {isWallpaper: isWallpaper, isPMQ: isPMQ}); 294 295 if (!isWallpaper) { 296 return googletag 297 .sizeMapping() 298 .addSize([991, 0], desktopSizes) 299 .addSize([768, 0], desktopSizes) 300 .addSize([320, 0], mobileSizes) 301 .addSize([0, 0], mobileSizes) 302 .build(); 303 } 304 305 if (isPMQ) { 306 /*return googletag 307 .sizeMapping() 308 .addSize([1230, 0], desktopSizes) 309 .addSize([821, 0], [0, 0]) 310 .addSize([320, 0], mobileSizes) 311 .addSize([0, 0], mobileSizes) 312 .build();*/ 313 314 return googletag 315 .sizeMapping() 316 .addSize([1460, 0], desktopSizes) 317 .addSize([1230, 0], desktopSizes) 318 .addSize([821, 0], []) 319 .addSize([0, 0], mobileSizes) 320 .build(); 321 } 322 323 /*return googletag 324 .sizeMapping() 325 .addSize([1350, 0], desktopSizes) 326 .addSize([821, 0], [0, 0]) 327 .addSize([320, 0], mobileSizes) 328 .addSize([0, 0], mobileSizes) 329 .build();*/ 330 331 return googletag 332 .sizeMapping() 333 .addSize([1440, 0], desktopSizes) 334 .addSize([1350, 0], desktopSizes) 335 .addSize([821, 0], []) 336 .addSize([0, 0], mobileSizes) 337 .build(); 338 } 339 340 /** 341 * Disable googletag.display and optionally queue calls. 342 * @param {boolean} queueCalls - When true, store display calls for replay. 343 * @returns {boolean} True if disabled, false if already disabled or unavailable. 344 * @throws {Error} None. 345 */ 346 function disableDisplay(queueCalls) { 347 queueCalls = queueCalls || false; 348 349 if (displayState.isDisabled) { 350 log('display-disable-skipped', {reason: 'already-disabled'}); 351 return false; 352 } 353 354 if (typeof googletag === 'undefined' || typeof googletag.display !== 'function') { 355 log('display-disable-skipped', {reason: 'display-not-ready'}); 356 return false; 357 } 358 359 displayState.originalDisplay = googletag.display; 360 displayState.isDisabled = true; 361 362 googletag.display = function(slotId) { 363 if (queueCalls) { 364 displayState.queuedCalls.push(slotId); 365 log('display-queued', {slotId: slotId}); 366 } 367 }; 368 369 log('display-disabled', {queueCalls: queueCalls}); 370 return true; 371 } 372 373 /** 374 * Re-enable googletag.display and optionally replay queued calls. 375 * @param {boolean} executeQueued - When true, execute queued display calls. 376 * @returns {void} No return value. 377 * @throws {Error} None. 378 */ 379 function enableDisplay(executeQueued) { 380 executeQueued = executeQueued || false; 381 382 if (!displayState.isDisabled) { 383 log('display-enable-skipped', {reason: 'already-enabled'}); 384 return; 385 } 386 387 if (displayState.originalDisplay) { 388 googletag.display = displayState.originalDisplay; 389 } 390 displayState.isDisabled = false;
391 392 if (executeQueued && displayState.queuedCalls.length > 0) { 393 pushGoogletagCommand(function() { 394 displayState.queuedCalls.forEach(function(slotId) { 395 googletag.display(slotId); 396 }); 397 }); 398 displayState.queuedCalls = []; 399 log('display-enabled', {executeQueued: true}); 400 } else if (displayState.queuedCalls.length > 0) { 401 displayState.queuedCalls = []; 402 log('display-enabled', {executeQueued: false, discardedQueued: true}); 403 } else { 404 log('display-enabled', {executeQueued: false}); 405 } 406 } 407 408 /** 409 * Check whether display calls are currently disabled. 410 * @returns {boolean} True when disabled, otherwise false. 411 * @throws {Error} None. 412 */ 413 function isDisplayDisabled() { 414 return displayState.isDisabled; 415 } 416 417 /** 418 * Get the number of queued display calls. 419 * @returns {number} Count of queued display calls. 420 * @throws {Error} None. 421 */ 422 function getQueuedDisplayCount() { 423 return displayState.queuedCalls.length; 424 } 425 426 /** 427 * Bind Lytics targeting handler when `jstag` is available. 428 * Uses a setter hook to catch late `jstag` initialization. 429 * @returns {void} No return value. 430 * @throws {Error} None. 431 */ 432 function bindLyticsTargeting() { 433 if (window.jstag && window.jstag.call) { 434 log('lytics-bind', {mode: 'immediate'}); 435 window.jstag.call('entityReady', addLyticsProfileTargeting); 436 return; 437 } 438 439 try { 440 var current = window.jstag; 441 Object.defineProperty(window, 'jstag', { 442 configurable: true, 443 get: function() { 444 return current; 445 }, 446 set: function(value) { 447 current = value; 448 if (value && value.call) { 449 log('lytics-bind', {mode: 'late'}); 450 value.call('entityReady', addLyticsProfileTargeting); 451 } 452 } 453 }); 454 } catch (e) { 455 log('lytics-bind-skip', {reason: 'defineProperty-failed'}); 456 // If we can't observe late assignments, skip silently. 457 } 458 } 459 460 /** 461 * Add Lytics profile targeting to GAM setup 462 * 463 * @param {Object} profile - Lytics profile object 464 */ 465 /** 466 * Apply Lytics segments to targeting and refresh ads. 467 * @param {Object} profile - Lytics profile object containing segments. 468 * @returns {void} No return value. 469 * @throws {Error} None. 470 */ 471 function addLyticsProfileTargeting(profile) { 472 if (profile && profile.data && profile.data.user && profile.data.user.segments) { 473 log('lytics-targeting', {segments: profile.data.user.segments}); 474 pushGoogletagCommand(function() { 475 googletag.setConfig({targeting: {LyticsSegments: profile.data.user.segments}}); 476 //window.googletag.pubads().refresh(); 477 }); 478 } else { 479 log('lytics-targeting', {segments: []}); 480 //window.googletag.pubads().refresh(); 481 } 482 } 483 484 /** 485 * Resolve request metrics targeting from the latest `wtwhAdInfo` 486 * value in the data layer. 487 * @returns {Array|null} Targeting value, or null when unavailable. 488 * @throws {Error} None. 489 */ 490 function getRequestMetricsTargeting() { 491 const currentDataLayer = Array.isArray(window.dataLayer) ? window.dataLayer : []; 492 for (let i = currentDataLayer.length - 1; i >= 0; i--) { 493 const item = currentDataLayer[i]; 494 const requestStatus = item && item.wtwhAdInfo && item.wtwhAdInfo.request; 495 496 if (requestStatus === 'restricted' || requestStatus === 'unrestricted') { 497 return [requestStatus]; 498 } 499 } 500 501 return null; 502 } 503 504 /** 505 * Resolve traffic channel from the latest valid `wtwhAdInfo` value. 506 * @returns {Array|null} Targeting value, or null when unavailable. 507 */ 508 function getTrafficChannelTargeting() { 509 const currentDataLayer = Array.isArray(window.dataLayer) ? window.dataLayer : []; 510 for (let i = currentDataLayer.length - 1; i >= 0; i--) { 511 const item = currentDataLayer[i]; 512 const trafficChannel = item && item.wtwhAdInfo && item.wtwhAdInfo.traffic_channel; 513 514 if (typeof trafficChannel === 'string' && trafficChannel.trim() !== '') { 515 return [trafficChannel]; 516 } 517 } 518 519 return null; 520 } 521 522 /** 523 * Resolve traffic channel group from the latest valid `wtwhAdInfo` value. 524 * @returns {Array|null} Targeting value, or null when unavailable. 525 */ 526 function getTrafficChannelGroupTargeting() { 527 const currentDataLayer = Array.isArray(window.dataLayer) ? window.dataLayer : []; 528 for (let i = currentDataLayer.length - 1; i >= 0; i--) { 529 const item = currentDataLayer[i]; 530 const trafficChannelGroup = item && item.wtwhAdInfo && item.wtwhAdInfo.traffic_channel_group; 531 532 if (typeof trafficChannelGroup === 'string' && trafficChannelGroup.trim() !== '') { 533 return [trafficChannelGroup]; 534 } 535 } 536 537 return null; 538 } 539 540 /** 541 * Define a GPT ad slot from config and attach targeting. 542 * @param {Object} slot - Slot configuration object. 543 * @returns {boolean} True if defined, false if skipped or invalid. 544 * @throws {Error} None. 545 */ 546 function defineSlot(slot) { 547 if (!slot || typeof slot !== 'object' || slot.skip_ads) {
548 log('slot-skip', {reason: 'invalid-or-skip', slot: slot}); 549 return false; 550 } 551 552 var divElement = document.getElementById(slot.div_id); 553 if (!divElement && !getSpecialDivIds().includes(slot.div_id)) { 554 log('slot-skip', {reason: 'missing-div', divId: slot.div_id}); 555 return false; 556 } 557 558 definedAdDivIds.push(slot.div_id); 559 560 var adSlotPath = '/' + slot.account_id + '/' + slot.network_code + '/' + slot.ad_unit_code; 561 var desktopSizes = convertSizes(slot.desktop_sizes); 562 var mobileSizes = convertSizes(slot.mobile_sizes); 563 var allSizes = getAllSizes(slot.desktop_sizes, slot.mobile_sizes); 564 log('slot-define-start', {divId: slot.div_id, adSlotPath: adSlotPath}); 565 if (allSizes.length === 0) { 566 log('slot-define-warning', {reason: 'empty-sizes', divId: slot.div_id}); 567 } 568 569 var isWallpaper = slot.ad_type && 'wallpaper' === slot.ad_type ? true : false; 570 571 var adSlot = googletag.defineSlot(adSlotPath, allSizes, slot.div_id); 572 if (!adSlot) { 573 log('slot-define-failed', {divId: slot.div_id, adSlotPath: adSlotPath}); 574 return false; 575 } 576 577 var sizeMap = generateSizeMapping(isWallpaper, desktopSizes, mobileSizes); 578 adSlot.defineSizeMapping(sizeMap); 579 580 const targeting = {}; 581 if (slot.targeting && typeof slot.targeting === 'object') { 582 for (var key in slot.targeting) { 583 if (Object.prototype.hasOwnProperty.call(slot.targeting, key)) { 584 var value = slot.targeting[key]; 585 var targetingValue = Array.isArray(value) ? value : [value]; 586 targeting[key] = targetingValue; 587 } 588 } 589 } 590 591 var requestMetricsTargeting = getRequestMetricsTargeting(); 592 if (requestMetricsTargeting) { 593 targeting.requestMetrics = requestMetricsTargeting; 594 } 595 const trafficChannelTargeting = getTrafficChannelTargeting(); 596 if (trafficChannelTargeting) { 597 targeting.trafficChannel = trafficChannelTargeting; 598 } 599 const trafficChannelGroupTargeting = getTrafficChannelGroupTargeting(); 600 if (trafficChannelGroupTargeting) { 601 targeting.trafficChannelGroup = trafficChannelGroupTargeting; 602 } 603 604 adSlot.setConfig({targeting: targeting}); 605 adSlot.addService(googletag.pubads()); 606 slotsDefinedCount++; 607 log('slot-defined', { 608 divId: slot.div_id, 609 adSlotPath: adSlotPath, 610 sizeMap: sizeMap, 611 desktopSizes: desktopSizes, 612 mobileSizes: mobileSizes 613 }); 614 return true; 615 } 616 617 /** 618 * Define all slots from `wtwh_gam_ad_config.slots`. 619 * @returns {number} Number of newly defined slots. 620 * @throws {Error} None. 621 */ 622 function defineAllSlots() { 623 if (typeof wtwh_gam_ad_config === 'undefined' || !Array.isArray(wtwh_gam_ad_config.slots)) { 624 log('slots-skip', {reason: 'missing-config-or-invalid-slots'}); 625 return 0; 626 } 627 628 var initialCount = slotsDefinedCount; 629 630 wtwh_gam_ad_config.slots.forEach(function(slot) { 631 defineSlot(slot); 632 }); 633 634 var newlyDefined = slotsDefinedCount - initialCount; 635 log('slots-defined-batch', {count: newlyDefined}); 636 return newlyDefined; 637 } 638 639 /** 640 * Apply global GPT settings and enable services once. 641 * @returns {void} No return value. 642 * @throws {Error} None. 643 */ 644 function applyGlobalSettings() { 645 if (servicesEnabled || typeof wtwh_gam_ad_config === 'undefined') { 646 if (servicesEnabled) { 647 log('services-skip', {reason: 'already-enabled'}); 648 } else { 649 log('services-skip', {reason: 'missing-config'}); 650 } 651 return; 652 } 653 654 const cleanUrl = window.location.origin + window.location.pathname; 655 const pageSettings = {targeting: {page_url: cleanUrl}}; 656 657 if (wtwh_gam_ad_config.lazy_load_ads) { 658 pageSettings.lazyLoad = { 659 fetchMarginPercent: 500, // Fetch ads 5 viewports away 660 renderMarginPercent: 200, // Render ads 2 viewports away 661 mobileScaling: 2.0 // Double margins on mobile 662 }; 663 log('lazy-load-enabled'); 664 } 665 666 if (wtwh_gam_ad_config.enable_single_request) { 667 pageSettings.singleRequest = true; 668 } 669 670 if (wtwh_gam_ad_config.collapse_empty_divs) { 671 pageSettings.collapseDiv = 'ON_NO_FILL'; 672 } 673 674 googletag.setConfig(pageSettings); 675 676 googletag.enableServices(); 677 servicesEnabled = true; 678 log('services-enabled'); 679 } 680 681 /** 682 * Bind slot render logging once and report ad loaded status per defined slot. 683 * @returns {void} No return value. 684 * @throws {Error} None. 685 */ 686 function bindDefinedSlotRenderLogging() { 687 if (slotRenderLoggingBound || typeof googletag.pubads !== 'function') { 688 return; 689 } 690 691 googletag.pubads().addEventListener('slotRenderEnded', function(slotRenderEndedEvent) { 692 var divId = slotRenderEndedEvent && slotRenderEndedEvent.slot && typeof slotRenderEndedEvent.slot.getSlotElementId === 'function' 693 ? slotRenderEndedEvent.slot.getSlotElementId() 694 : ''; 695 696 if (!divId || !definedAdDivIds.includes(divId)) { 697 return; 698 } 699 700 log('slot-render-status', { 701 divId: divId, 702 loaded: !slotRenderEndedEvent.isEmpty, 703 isEmpty: slotRenderEndedEvent.isEmpty, 704 size: slotRenderEndedEvent.size || null 705 }); 706 }); 707 708 slotRenderLoggingBound = true; 709 log('slot-render-logging-bound'); 710 } 711 712 /** 713 * Reset the body wrapper opacity if it was forced to 0. 714 * @returns {void} No return value. 715 * @throws {Error} None. 716 */ 717 function resetBodyOpacity() { 718 var bodyDiv = document.getElementById('body-wrapper'); 719 if (!bodyDiv) return; 720 var computedOpacity = window.getComputedStyle ? window.getComputedStyle(bodyDiv).opacity : bodyDiv.style.opacity; 721 if (computedOpacity === '0') { 722 bodyDiv.style.opacity = ''; 723 } 724 } 725 726 /** 727 * Determine whether prestitial is enabled via config. 728 * @returns {boolean} True if enabled or unspecified, false if disabled. 729 * @throws {Error} None. 730 */ 731 function isPrestitialEnabled() { 732 if (typeof wtwh_gam_ad_config === 'undefined') return true; 733 if (typeof wtwh_gam_ad_config.enable_prestitial === 'undefined') return true; 734 return !!wtwh_gam_ad_config.enable_prestitial; 735 } 736 737 /** 738 * Clear the prestitial render timeout or automatic close timer. 739 * @returns {void} No return value. 740 */ 741 function clearPrestitialTimeout() {
742 if (prestitialState.timeoutId !== null) { 743 window.clearTimeout(prestitialState.timeoutId); 744 prestitialState.timeoutId = null; 745 } 746 } 747 748 /** 749 * Cancel pending rendering and remove the prestitial overlay. 750 * @returns {void} No return value. 751 * @throws {Error} None. 752 */ 753 function hidePrestitial() { 754 prestitialState.status = 'cancelled'; 755 clearPrestitialTimeout(); 756 resetBodyOpacity(); 757 // Restore page scrolling that showPrestitial() locked. 758 document.body.style.overflow = ''; 759 var prestitialEl = document.getElementById('prestitial'); 760 if (prestitialEl) { 761 prestitialEl.classList.add('prestitial-no-show'); 762 prestitialEl.remove(); 763 log('prestitial-hide'); 764 } 765 } 766 767 /** 768 * Show the prestitial overlay and ensure it is visible. 769 * @returns {boolean} True when the overlay and ad container exist and are shown. 770 * @throws {Error} None. 771 */ 772 function showPrestitial() { 773 var prestitialDiv = document.getElementById('prestitial'); 774 if (!prestitialDiv || !document.getElementById(getPrestitialDivId())) { 775 log('prestitial-show-skip', {reason: 'missing-element'}); 776 return false; 777 } 778 var overlay = document.getElementById('prestitial-overlay'); 779 if (overlay) { 780 overlay.style.display = 'block'; 781 } 782 prestitialDiv.classList.add('prestitial-active'); 783 prestitialDiv.style.removeProperty('opacity'); 784 // Lock the underlying page so the fixed overlay stays in the 785 // viewport until the user closes it. 786 document.body.style.overflow = 'hidden'; 787 788 // Safety net: if something external removes #prestitial after 789 // we lock scroll (another plugin, theme JS, etc.), restore 790 // overflow so the page is never permanently trapped. When 791 // hidePrestitial() runs normally it clears overflow first, so 792 // the callback below becomes a harmless no-op in that path. 793 if (typeof MutationObserver !== 'undefined') { 794 var removalObserver = new MutationObserver(function() { 795 if (!document.getElementById('prestitial')) { 796 removalObserver.disconnect(); 797 document.body.style.overflow = ''; 798 } 799 }); 800 removalObserver.observe(document.documentElement, {childList: true, subtree: true}); 801 } 802 803 log('prestitial-show'); 804 return true; 805 } 806 807 /** 808 * Check if the user arrived from the Jolt email campaign. 809 * @returns {boolean} True if UTM params match, otherwise false. 810 * @throws {Error} None. 811 */ 812 function fromEmail() { 813 var urlParams = new URLSearchParams(window.location.search); 814 return urlParams.get('utm_source') == 'jolt' && urlParams.get('utm_medium') == 'email'; 815 } 816 817 /** 818 * Check whether the current page is sponsored content. 819 * @returns {boolean} True when URL path contains sponsored content indicator. 820 * @throws {Error} None. 821 */ 822 function isSponsoredContent() { 823 var pathname = window.location.pathname; 824 return pathname.includes('sponsored_content'); 825 } 826 827 /** 828 * Register the prestitial as shown, start automatic close, and bind UI handlers. 829 * @returns {void} No return value. 830 * @throws {Error} None. 831 */ 832 function registerPrestitial() { 833 if (prestitialState.status !== 'pending') return; 834 if (!showPrestitial()) { 835 hidePrestitial(); 836 return; 837 } 838 839 prestitialState.status = 'rendered'; 840 clearPrestitialTimeout();
841 prestitialState.timeoutId = window.setTimeout(function() { 842 if (prestitialState.status !== 'rendered') return; 843 log('prestitial-auto-close', {timeoutMs: prestitialAutoCloseMs}); 844 hidePrestitial(); 845 }, prestitialAutoCloseMs); 846 var prestitialDiv = document.getElementById('prestitial'); 847 if (prestitialDiv) { 848 prestitialDiv.addEventListener('click', hidePrestitial); 849 } 850 try { 851 sessionStorage.setItem('prestitial_shown', true); 852 } catch (e) { 853 log('prestitial-session-storage-error', {error: e && e.message}); 854 } 855 log('prestitial-register'); 856 } 857 858 /** 859 * Inject prestitial markup if not already present. 860 * Builds logo, CTA, and ad container markup from `PRESTITIALINFO`. 861 * @returns {void} No return value. 862 * @throws {Error} None. 863 */ 864 function ensurePrestitialMarkup() { 865 if (prestitialState.markupInjected || typeof PRESTITIALINFO === 'undefined') { 866 if (prestitialState.markupInjected) { 867 log('prestitial-markup-skip', {reason: 'already-injected'}); 868 } else { 869 log('prestitial-markup-skip', {reason: 'missing-info'}); 870 } 871 return; 872 } 873 874 var grid = document.getElementById('prestitial-grid'); 875 if (!grid) { 876 log('prestitial-markup-skip', {reason: 'missing-grid'}); 877 return; 878 } 879 880 // Compute logo presentation and inline styles from PRESTITIALINFO. 881 var showLogo = true; 882 var logoStyle = ''; 883 if ('0' == PRESTITIALINFO.logoHeight || 0 == PRESTITIALINFO.logoWidth) { 884 showLogo = false; 885 } 886 887 if (showLogo) { 888 if ('svg' == PRESTITIALINFO.siteLogo) { 889 logoStyle = 'width:' + PRESTITIALINFO.logoWidth + '% !important;margin-top:-15% '; 890 } else { 891 logoStyle = 'max-height:' + PRESTITIALINFO.logoHeight + 'px;' + ('white' === PRESTITIALINFO.backgroundColor ? 'background-color:white;padding:5px;' : ''); 892 } 893 } else { 894 logoStyle = 'display:none;'; 895 } 896 897 var additionalLogoStyles = ''; 898 if ('svg' == PRESTITIALINFO.siteLogo) { 899 additionalLogoStyles = 'background-color: unset !important;border-bottom: unset !important;'; 900 } 901 902 var logoHTML = ''; 903 if ('svg' == PRESTITIALINFO.siteLogo) { 904 logoHTML = '<a href=\"' + PRESTITIALINFO.siteUrl + '\" style=\"' + logoStyle + '\">' + PRESTITIALINFO.siteName + '</a>'; 905 } else { 906 logoHTML = '<a href=\"' + PRESTITIALINFO.siteUrl + '\"><img style=\"' + logoStyle + '\" src=\"' + PRESTITIALINFO.siteLogo + '\"></a>'; 907 } 908 909 // Inject the prestitial header, CTA, and ad container markup. 910 grid.insertAdjacentHTML( 911 'beforeend', 912 '<div class=\"prestitial-logo' + ('svg' == PRESTITIALINFO.siteLogo ? ' site-header' : '') + '\" style=\"' + additionalLogoStyles + '\">' + 913 '<div class=\"title-area\">' + 914 '<h1 class=\"site-title\" style=\"margin: 0;\">' + 915 logoHTML + 916 '</h1>' + 917 '</div>' + 918 '</div>' + 919 '<div class=\"close-cta\">' + 920 'Continue to Site <i class=\"fa fa-arrow-right\"></i>' + 921 '</div>' + 922 '<div id=\"prestitial-banner\">' + 923 '<div id=\"' + getPrestitialDivId() + '\" class=\"gam-holder\" style=\"min-width: 300px; min-height: 225px;\"></div>' + 924 '</div>' 925 ); 926 927 prestitialState.markupInjected = true; 928 log('prestitial-markup-injected'); 929 } 930 931 /** 932 * Define and display the prestitial slot via GPT. 933 * @returns {void} No return value. 934 * @throws {Error} None. 935 */
936 function defineAndDisplayPrestitialSlot() { 937 if (!isPrestitialEnabled()) { 938 log('prestitial-define-skip', {reason: 'disabled-by-config'}); 939 hidePrestitial(); 940 return; 941 } 942 943 if (typeof wtwh_gam_ad_config === 'undefined' || !wtwh_gam_ad_config.prestitial) { 944 log('prestitial-define-skip', {reason: 'missing-config'}); 945 hidePrestitial(); 946 return; 947 } 948 949 pushGoogletagCommand(function() { 950 var cleanUrl = window.location.origin + window.location.pathname; 951 log('prestitial-define', {pageUrl: cleanUrl}); 952 953 var defineSlotAndDisplay = function() { 954 try { 955 if (sessionStorage.getItem('prestitial_shown')) { 956 log('prestitial-not-shown', {reason: 'previously-shown'}); 957 hidePrestitial(); 958 return; 959 } 960 } catch (e) { 961 log('prestitial-not-shown', {reason: 'session-storage-error', error: e && e.message}); 962 hidePrestitial(); 963 return; 964 } 965 966 // Define the prestitial slot and display it if valid. 967 var slotDefined = defineSlot(wtwh_gam_ad_config.prestitial); 968 if (!slotDefined) { 969 log('prestitial-define-failed'); 970 hidePrestitial(); 971 return; 972 } 973 974 applyGlobalSettings(); 975 // Allow GPT time to render independently of window.load. 976 prestitialState.status = 'pending'; 977 prestitialState.timeoutId = window.setTimeout(function() { 978 if (prestitialState.status !== 'pending') return; 979 log('prestitial-timeout-hide', {timeoutMs: prestitialRenderTimeoutMs}); 980 hidePrestitial(); 981 }, prestitialRenderTimeoutMs); 982 try { 983 googletag.display(getPrestitialDivId()); 984 log('prestitial-display'); 985 } catch (e) { 986 log('prestitial-display-failed', {error: e && e.message}); 987 hidePrestitial(); 988 } 989 }; 990 991 if (!isDOMReady()) { 992 log('prestitial-define-wait', {reason: 'dom-not-ready'}); 993 waitForDOMReady(defineSlotAndDisplay); 994 } else { 995 defineSlotAndDisplay(); 996 } 997 }); 998 } 999 1000 /** 1001 * Initialize prestitial behavior and render listeners. 1002 * @returns {void} No return value. 1003 * @throws {Error} None. 1004 */ 1005 function initPrestitial() { 1006 if (prestitialState.initialized) { 1007 log('prestitial-skip', {reason: 'already-initialized'}); 1008 return; 1009 } 1010 prestitialState.initialized = true; 1011 1012 var prestitialEl = document.getElementById('prestitial'); 1013 if (!prestitialEl) { 1014 log('prestitial-wait', {reason: 'missing-element'}); 1015 if (typeof MutationObserver !== 'undefined') { 1016 if (document.readyState === 'complete') { 1017 log('prestitial-wait', {reason: 'page-complete-no-element'}); 1018 return; 1019 } 1020 // Watch for late insertion of the prestitial element. 1021 var observer = new MutationObserver(function() { 1022 var lateEl = document.getElementById('prestitial'); 1023 if (lateEl) { 1024 observer.disconnect(); 1025 prestitialState.initialized = false;
1026 log('prestitial-found-late'); 1027 initPrestitial(); 1028 } 1029 }); 1030 observer.observe(document.documentElement || document.body, {childList: true, subtree: true}); 1031 window.addEventListener('load', function() { 1032 observer.disconnect(); 1033 }); 1034 } else { 1035 log('prestitial-wait', {reason: 'mutationobserver-unavailable'}); 1036 } 1037 return; 1038 } 1039 1040 try { 1041 if (fromEmail() || sessionStorage.getItem('prestitial_shown') || isSponsoredContent()) { 1042 log('prestitial-skip', {reason: 'blocked-by-rules'}); 1043 hidePrestitial(); 1044 return; 1045 } 1046 } catch (e) { 1047 log('prestitial-skip', {reason: 'exception', error: e && e.message}); 1048 hidePrestitial(); 1049 return; 1050 } 1051 1052 if (typeof wtwh_gam_ad_config === 'undefined' || !wtwh_gam_ad_config.prestitial) { 1053 log('prestitial-skip', {reason: 'missing-config'}); 1054 resetBodyOpacity(); 1055 hidePrestitial(); 1056 return; 1057 } 1058 1059 if (!isPrestitialEnabled()) { 1060 log('prestitial-skip', {reason: 'disabled-by-config'}); 1061 hidePrestitial(); 1062 return; 1063 } 1064 1065 ensurePrestitialMarkup(); 1066 1067 pushGoogletagCommand(function() { 1068 // Bind before display so even an immediate result is handled. 1069 googletag.pubads().addEventListener('slotRenderEnded', function(slotRenderEndedEvent) { 1070 if (slotRenderEndedEvent.slot.getSlotElementId() == getPrestitialDivId()) { 1071 if (prestitialState.status !== 'pending') return; 1072 if (!slotRenderEndedEvent.isEmpty) { 1073 log('prestitial-render', {isEmpty: false}); 1074 registerPrestitial(); 1075 } else { 1076 log('prestitial-render', {isEmpty: true}); 1077 hidePrestitial(); 1078 } 1079 } 1080 }); 1081 }); 1082 1083 defineAndDisplayPrestitialSlot(); 1084 log('prestitial-init'); 1085 } 1086 1087 /** 1088 * Initialize slot definition, services, and prestitial behavior. 1089 * @returns {void} No return value. 1090 * @throws {Error} None. 1091 */ 1092 function init() { 1093 pushGoogletagCommand(function() { 1094 if (typeof googletag.pubads !== 'function') { 1095 log('init-skip', {reason: 'pubads-not-ready'}); 1096 enableDisplay(true); 1097 return; 1098 } 1099 1100 var hasSlots = typeof wtwh_gam_ad_config !== 'undefined' && Array.isArray(wtwh_gam_ad_config.slots); 1101 1102 if (!hasSlots) { 1103 log('init', {hasSlots: false}); 1104 enableDisplay(true); 1105 initPrestitial(); 1106 return; 1107 } 1108 1109 waitForDOMReady(function() { 1110 // Define all slots, then apply global settings and display. 1111 var definedCount = defineAllSlots(); 1112 log('init', {definedCount: definedCount}); 1113 1114 if (definedCount > 0) { 1115 applyGlobalSettings(); 1116 bindDefinedSlotRenderLogging(); 1117 bindLyticsTargeting(); 1118 1119 enableDisplay(); 1120 log('display-enable-after-define', {definedCount: definedCount}); 1121 1122 if (wtwh_gam_ad_config.auto_display_ads && definedAdDivIds.length > 0) { 1123 definedAdDivIds.forEach(function(divId) { 1124 if (!getSpecialDivIds().includes(divId)) { 1125 googletag.display(divId); 1126 log('auto-display', {divId: divId}); 1127 } 1128 }); 1129 } 1130 } else { 1131 enableDisplay(true); 1132 log('display-enable-after-define', {definedCount: definedCount}); 1133 } 1134 1135 var eventName = 'wtwh_gam_slots_initialized'; 1136 var eventDetail = {definedCount: definedCount}; 1137 1138 try { 1139 window.dispatchEvent(new CustomEvent(eventName, {detail: eventDetail})); 1140 } catch (e) { 1141 var legacyEvent = document.createEvent('CustomEvent'); 1142 legacyEvent.initCustomEvent(eventName, false, false, eventDetail); 1143 window.dispatchEvent(legacyEvent); 1144 } 1145 1146 initPrestitial(); 1147 }); 1148 }); 1149 } 1150 1151 return { 1152 isDOMReady: isDOMReady, 1153 waitForDOMReady: waitForDOMReady, 1154 defineSlot: defineSlot, 1155 defineAllSlots: defineAllSlots, 1156 getRequestMetricsTargeting: getRequestMetricsTargeting, 1157 getTrafficChannelTargeting: getTrafficChannelTargeting, 1158 getTrafficChannelGroupTargeting: getTrafficChannelGroupTargeting, 1159 applyGlobalSettings: applyGlobalSettings, 1160 disableDisplay: disableDisplay, 1161 enableDisplay: enableDisplay, 1162 isDisplayDisabled: isDisplayDisabled, 1163 getQueuedDisplayCount: getQueuedDisplayCount, 1164 log: log, 1165 dumpLog: dumpLog, 1166 getSlotsDefinedCount: function() { 1167 return slotsDefinedCount; 1168 }, 1169 init: init, 1170 addLyticsProfileTargeting: addLyticsProfileTargeting, 1171 initPrestitial: initPrestitial 1172 }; 1173 })(); 1174 } 1175 1176 // --------------------------------------------------------------------- 1177 // Unified initialization flow 1178 // --------------------------------------------------------------------- 1179 /** 1180 * Initialize ads exactly once when GPT is ready. 1181 * Uses the `once` helper to avoid duplicate initialization. 1182 * @type {Function} 1183 */ 1184 var initAdsOnce = once(function() { 1185 if (window.googletagAdSlotGenerator) { 1186 window.googletagAdSlotGenerator.log('init-once'); 1187 window.googletagAdSlotGenerator.init(); 1188 } 1189 }); 1190 1191 /** 1192 * Check whether GPT is ready based on config or dataLayer. 1193 * @returns {boolean} True when GPT is ready, otherwise false. 1194 * @throws {Error} None. 1195 */ 1196 function hasGptReadyEvent() { 1197 if (typeof wtwh_gam_ad_config !== 'undefined' && wtwh_gam_ad_config.load_gpt_library) { 1198 if (window.googletagAdSlotGenerator) { 1199 window.googletagAdSlotGenerator.log('gpt-ready', {source: 'config'}); 1200 } 1201 return true; 1202 } 1203 var found = dataLayer.some(function(item) { 1204 return item && item.event === 'gpt_ready'; 1205 }); 1206 if (window.googletagAdSlotGenerator) { 1207 window.googletagAdSlotGenerator.log('gpt-ready', {source: 'dataLayer', found: found}); 1208 } 1209 return found; 1210 } 1211 1212 /** 1213 * Hook dataLayer.push to detect late GPT-ready events. 1214 * @returns {void} No return value. 1215 * @throws {Error} None. 1216 */ 1217 function hookDataLayerForGptReady() { 1218 if (window.googletagAdSlotGenerator) { 1219 window.googletagAdSlotGenerator.log('gpt-ready-hook'); 1220 } 1221 var originalPush = dataLayer.push; 1222 dataLayer.push = function() { 1223 for (var i = 0; i < arguments.length; i++) { 1224 var item = arguments[i]; 1225 if (item && item.event === 'gpt_ready') { 1226 if (window.googletagAdSlotGenerator) { 1227 window.googletagAdSlotGenerator.log('gpt-ready', {source: 'dataLayer-push'}); 1228 } 1229 initAdsOnce(); 1230 } 1231 } 1232 return originalPush.apply(dataLayer, arguments); 1233 }; 1234 } 1235 1236 if (window.googletagAdSlotGenerator) { 1237 window.googletagAdSlotGenerator.log('display-disable-init'); 1238 window.googletagAdSlotGenerator.disableDisplay(true); 1239 } 1240 1241 /** 1242 * Hook late GPT initialization to disable display once available. 1243 * This ensures queued display calls are captured even if GPT loads after this script. 1244 * @returns {void} No return value. 1245 * @throws {Error} None. 1246 */ 1247 function hookGoogletagDisplay() { 1248 if (!window.googletagAdSlotGenerator) { 1249 return; 1250 } 1251 1252 if (window.googletag && typeof window.googletag.display === 'function') { 1253 window.googletagAdSlotGenerator.log('display-disable-hook', {mode: 'immediate'}); 1254 window.googletagAdSlotGenerator.disableDisplay(true); 1255 return; 1256 } 1257 1258 try { 1259 var current = normalizeGoogletag(window.googletag); 1260 Object.defineProperty(window, 'googletag', { 1261 configurable: true, 1262 get: function() { 1263 return current; 1264 }, 1265 set: function(value) { 1266 current = normalizeGoogletag(value); 1267 if (current && typeof current.display === 'function') { 1268 window.googletagAdSlotGenerator.log('display-disable-hook', {mode: 'late'}); 1269 window.googletagAdSlotGenerator.disableDisplay(true); 1270 } 1271 } 1272 }); 1273 } catch (e) { 1274 window.googletagAdSlotGenerator.log('display-disable-hook-skip', {reason: 'defineProperty-failed'}); 1275 } 1276 } 1277 1278 hookGoogletagDisplay(); 1279 1280 pushGoogletagCommand(function() { 1281 if (window.googletagAdSlotGenerator) { 1282 window.googletagAdSlotGenerator.disableDisplay(true); 1283 } 1284 }); 1285 1286 if (hasGptReadyEvent()) { 1287 initAdsOnce(); 1288 } else { 1289 hookDataLayerForGptReady(); 1290 window.addEventListener('load', function() { 1291 if (window.googletagAdSlotGenerator) { 1292 window.googletagAdSlotGenerator.log('init-on-load'); 1293 } 1294 initAdsOnce(); 1295 }); 1296 } 1297 1298})(window, document);
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.