PageSourceSearch

https://www.windpowerengineering.com/wp-content/plugins/wtwh-unive…/unified-ad-logic.js?ver=1790240599

js windpowerengineering.com collected 2026-09-24 09:17:50 UTC 54,695 bytes, 1,298 lines download raw bytes

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.