1/** 2 * Removes a Dianomi slot's loading placeholder once the ad is on screen, or 3 * once the vendor answers with nothing. 4 * 5 * Enqueued by Pymnts_Ads_Frontend::skeleton_markup(), so every placement that 6 * prints a placeholder gets this watcher, whoever renders the slot. 7 * 8 * ES6, no jQuery. 9 */ 10( () => { 11 'use strict'; 12 13 // contextfeed.js sets data-dianomi-complete="1" as soon as it has an 14 // answer, which for a filled slot is still before the creative renders. 15 // Measured on an article: complete at ~100ms, creative at ~1000ms. On a tag 16 // archive the gap is longer, complete at 1400ms and the creative at 2600ms, 17 // and giving up early costs a collapse followed by the ad pushing back in. 18 const GRACE_MS = 2500; 19 20 // A height within this multiple of the slot's reserved height is the ad's 21 // real size and is shown at once. Anything beyond it is the vendor's first 22 // pass: measured 0 -> 1300px -> 275px over 800ms on an article, against a 23 // slot reserving 250px. 24 const SANE_FACTOR = 2; 25 26 // How long to sit on a height beyond that before believing it anyway, so a 27 // genuinely tall unit is not hidden forever. Must outlast the gap between 28 // the vendor's two resizes, measured at 800ms. 29 const STABLE_MS = 2000; 30 31 // Past contextfeed.js's normal response on a slow connection. Without it a 32 // no-fill the vendor never reports would shimmer forever. 33 const TIMEOUT_MS = 8000; 34 35 // That timeout only means anything once the vendor has been asked, which 36 // with dianomi-lazy.js is whenever a slot is approached rather than at page 37 // load. Slots wait here until it signals; where nothing lazy-loads, the 38 // request is already in flight by the load event and they arm then. 39 let asked = false; 40 const waiting = new Set(); 41 42 const armAll = () => { 43 asked = true; 44 waiting.forEach( ( arm ) => arm() ); 45 waiting.clear(); 46 }; 47 48 document.addEventListener( 'pymnts-dianomi-loading', armAll ); 49 window.addEventListener( 'load', () => { 50 if ( ! window.pymntsDianomiLazy ) { 51 armAll(); 52 } 53 }, { once: true } ); 54 55 const watch = ( slot ) => { 56 const skeleton = slot.querySelector( ':scope > .pymnts-ad-skeleton' ); 57 if ( ! skeleton || slot.dataset.pymntsSkeletonWatched === '1' || window.pymntsAdsHoldSkeleton ) { 58 return; 59 } 60 slot.dataset.pymntsSkeletonWatched = '1'; 61 62 let done = false; 63 let detached = false; 64 let released = false; 65 let graceTimer = 0; 66 let stableTimer = 0; 67 let timer = 0; 68 let lastHeight = -1; 69 let reserved = 0; 70 let watchedFrame = null; 71 72 const frame = () => slot.querySelector( 'iframe' ); 73 74 // Hold the space the placeholder is occupying. This runs before the 75 // vendor script does anything, because contextfeed.js puts its own 76 // height on the container: measured on a tag archive, height:0px at 77 // 1400ms, then auto and a 420px ad at 2600ms. Waiting for its iframe to 78 // appear means measuring a container it has already collapsed. 79 const pin = () => { 80 if ( reserved > 0 ) { 81 return; 82 } 83 reserved = Math.round( slot.getBoundingClientRect().height ); 84 if ( reserved > 0 ) { 85 slot.style.minHeight = reserved + 'px'; 86 slot.style.maxHeight = reserved + 'px'; 87 slot.style.overflow = 'hidden'; 88 } 89 }; 90 91 // Once the vendor has an iframe in the slot, the placeholder stops 92 // taking space and covers it instead. In flow the two stack, so the 93 // container is briefly placeholder-plus-ad tall and everything below it 94 // moves down and then snaps back. 95 const detach = () => { 96 if ( detached ) { 97 return; 98 } 99 detached = true; 100 pin(); 101 slot.classList.add( 'pymnts-ad--filling' ); 102 }; 103 104 // Let the slot take the ad's real height, or collapse if there is none. 105 // Watching stops here rather than when the placeholder goes, because the 106 // vendor's second resize lands well after the ad is first visible. 107 const release = () => { 108 if ( released ) { 109 return; 110 } 111 released = true; 112 window.clearTimeout( stableTimer ); 113 obs.disconnect(); 114 if ( resize ) { 115 resize.disconnect(); 116 } 117 slot.style.minHeight = ''; 118 slot.style.maxHeight = ''; 119 slot.style.overflow = ''; 120 }; 121 122 // The vendor appends an empty iframe within milliseconds and fills it 123 // much later, so height is the only signal that anything is on screen. 124 const adVisible = () => { 125 const el = frame(); 126 return !! el && el.getBoundingClientRect().height > 0; 127 }; 128
129 const resize = 'ResizeObserver' in window ? new ResizeObserver( () => check() ) : null; 130 const obs = new MutationObserver( () => check() ); 131 132 const finish = () => { 133 if ( done ) { 134 return; 135 } 136 done = true; 137 window.clearTimeout( graceTimer ); 138 window.clearTimeout( timer ); 139 skeleton.remove(); 140 slot.classList.remove( 'pymnts-ad--filling' ); 141 }; 142 143 // Show the ad: lift the clamp and drop the placeholder together, so a 144 // half-sized ad is never on screen behind a clipped container. 145 const settle = () => { 146 finish(); 147 release(); 148 }; 149 150 function check() { 151 const el = frame(); 152 153 if ( el ) { 154 detach(); 155 if ( resize && watchedFrame !== el ) { 156 watchedFrame = el; 157 resize.observe( el ); 158 } 159 160 const height = Math.round( el.getBoundingClientRect().height ); 161 if ( height !== lastHeight ) { 162 lastHeight = height; 163 window.clearTimeout( stableTimer ); 164 165 if ( height > 0 ) { 166 if ( ! reserved || height <= reserved * SANE_FACTOR ) { 167 settle(); 168 return; 169 } 170 // The vendor's first pass. Keep the placeholder over it 171 // until it corrects itself, or believe it if it does not. 172 stableTimer = window.setTimeout( settle, STABLE_MS ); 173 } 174 } 175 } 176 177 if ( done ) { 178 return; 179 } 180 181 if ( '1' === slot.getAttribute( 'data-dianomi-complete' ) && ! graceTimer ) { 182 graceTimer = window.setTimeout( () => { 183 if ( adVisible() ) { 184 return; 185 } 186 // Nothing is coming: drop the placeholder and let the slot 187 // collapse. 188 settle(); 189 }, GRACE_MS ); 190 } 191 } 192 193 obs.observe( slot, { 194 childList: true, 195 attributes: true, 196 attributeFilter: [ 'data-dianomi-complete', 'data-dianomi-loading', 'style' ], 197 } ); 198 199 const armTimeout = () => { 200 if ( timer || done ) { 201 return; 202 } 203 timer = window.setTimeout( () => { 204 finish(); 205 release(); 206 }, TIMEOUT_MS ); 207 }; 208 209 if ( asked ) { 210 armTimeout(); 211 } else { 212 waiting.add( armTimeout ); 213 } 214 215 // Claim the space first, then start from the slot's current state rather 216 // than waiting for a change: the vendor can act before this script runs. 217 pin(); 218 check(); 219 }; 220 221 const scan = ( root ) => { 222 ( root || document ).querySelectorAll( '.dianomi_context' ).forEach( watch ); 223 }; 224 225 // Loaders that inject slots after page load (News & Trends, the 226 // headlines-grid "Load more") call scan() on the container they filled. 227 window.pymntsDianomiSkeleton = { watch, scan }; 228 229 if ( document.readyState === 'loading' ) { 230 document.addEventListener( 'DOMContentLoaded', () => scan() ); 231 } else { 232 scan(); 233 } 234} )();
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.