1/** 2 * Publishes each article embed's aspect ratio to CSS as --ar. 3 * 4 * An iframe has no intrinsic aspect ratio - unlike img and video, its width 5 * and height attributes do not produce one - so CSS cannot scale an embed 6 * down proportionally on its own. Capping the height alone letterboxes the 7 * player inside a full-width box. Handing the ratio to CSS lets 8 * article-nomin.css clamp the video to half the viewport height and shrink 9 * the width to match. 10 * 11 * The height cap in CSS applies from first paint, so this only ever narrows 12 * the box - it does not move the content below it. 13 */ 14( function() { 15 'use strict'; 16 17 // Only oEmbed video players. Ad creatives are iframes carrying width and 18 // height attributes too (sscats serves 168x48 and 970x200 among others), 19 // and sizing one of those to a video's rules clips the ad - so match on the 20 // provider host rather than on the attributes alone. The .vcont wrapper is 21 // not a sufficient test on its own: the theme only adds it on some posts. 22 var VIDEO_HOST = /^(https?:)?\/\/([a-z0-9-]+\.)*(youtube\.com|youtube-nocookie\.com|vimeo\.com|dailymotion\.com)\//i; 23 24 function isVideoEmbed( iframe ) { 25 return VIDEO_HOST.test( iframe.getAttribute( 'src' ) || '' ) || 26 !! ( iframe.parentElement && iframe.parentElement.classList.contains( 'vcont' ) ); 27 } 28 29 /* 30 * TikTok needs a different treatment entirely. 31 * 32 * TikTok ships a <blockquote class="tiktok-embed"> and its own embed.js 33 * swaps in an iframe with NO width/height attributes, sized by inline 34 * style at a fixed 740-745px. That height does not respond to width - 35 * narrowing the card to 300px leaves it at 740 - so there is no ratio to 36 * hand to CSS, and a max-height would clip the caption and music rows off 37 * the bottom of the card rather than shrink it. 38 * 39 * transform: scale() is the only thing that shrinks an iframe's rendered 40 * content. It does not affect layout though, so the wrapper has to carry 41 * the scaled height itself or the original height is left behind as a gap. 42 */ 43 var SCALE_SELECTOR = 'blockquote.tiktok-embed'; 44 45 /* 46 * Scaled cards get a taller allowance than the video cap in 47 * article-nomin.css (min(600px, 70svh)) on purpose. 48 * 49 * A video keeps its ratio, so trading height for width costs nothing. A 50 * TikTok card is a fixed 740px box, so the same 70svh forced it to 0.54 51 * scale on a 320px phone - only 175px wide, with the caption too small to 52 * read. 80svh keeps it noticeably larger while still leaving a fifth of 53 * the screen for surrounding text. 54 * 55 * Measured off a probe rather than computed, so the value always matches 56 * what CSS would resolve for the same expression. 57 */ 58 function maxCardHeight() { 59 var probe = document.createElement( 'div' ), 60 height; 61 62 probe.setAttribute( 'style', 'position:fixed;visibility:hidden;pointer-events:none;width:1px;height:620px;height:min(620px, 80svh);' ); 63 document.body.appendChild( probe ); 64 height = probe.getBoundingClientRect().height; 65 probe.parentNode.removeChild( probe ); 66 67 return height || 620; 68 } 69 70 function scaleEmbed( card ) { 71 var wrap = card.parentElement, 72 natW, natH, scale; 73 74 if ( ! card.querySelector( 'iframe' ) ) { 75 return; // provider script has not run yet 76 } 77 78 if ( ! wrap || ! wrap.classList.contains( 'sej-embed-scale' ) ) { 79 wrap = document.createElement( 'div' ); 80 wrap.className = 'sej-embed-scale'; 81 card.parentNode.insertBefore( wrap, card ); 82 wrap.appendChild( card ); 83 } 84 85 // Restore the provider's own inline style before measuring, so every 86 // pass sees the same natural box. Clearing only the properties we set 87 // is not enough: max-width:none would survive and the next pass would 88 // measure the full column instead of TikTok's 605px card. 89 if ( ! card.hasAttribute( 'data-sej-style' ) ) { 90 card.setAttribute( 'data-sej-style', card.getAttribute( 'style' ) || '' ); 91 } 92 93 card.setAttribute( 'style', card.getAttribute( 'data-sej-style' ) ); 94 wrap.style.width = ''; 95 wrap.style.height = ''; 96 97 // offsetWidth/offsetHeight are layout values, unaffected by transform. 98 natW = card.offsetWidth; 99 natH = card.offsetHeight; 100 101 if ( ! natW || ! natH ) { 102 return; 103 } 104 105 scale = Math.min( 1, maxCardHeight() / natH ); 106 107 if ( scale >= 1 ) { 108 return; // already short enough; leave it alone 109 } 110 111 // Pin the natural width so the narrowed wrapper cannot feed back into 112 // the card and compound the scaling. 113 card.style.width = natW + 'px'; 114 card.style.maxWidth = 'none'; 115 card.style.minWidth = '0'; 116 card.style.margin = '0'; 117 card.style.transformOrigin = 'top left'; 118 card.style.transform = 'scale(' + scale + ')'; 119 card.classList.add( 'sej-embed-scaled' ); 120 121 wrap.style.width = Math.round( natW * scale ) + 'px'; 122 wrap.style.height = Math.round( natH * scale ) + 'px'; 123 wrap.style.margin = '0 auto'; 124 // A transform shrinks what is painted, not the layout box: the card is 125 // still its full width underneath and would otherwise stick out of the 126 // wrapper and widen the page. TikTok's own min-width:325px makes this 127 // bite on a 320px phone. 128 wrap.style.overflow = 'hidden'; 129 } 130 131 /* 132 * Watch one card for the provider resizing its own iframe. 133 * 134 * TikTok sets the final height after we have already measured, so without 135 * this the card stays at its natural 740px. The observer is scoped to the
136 * card and to the iframe's style attribute - watching the whole article 137 * would fire on every ad slot restyling itself. Mutations on the card 138 * itself are ignored because those are ours, which is what stops a loop. 139 */ 140 function watchCard( card ) { 141 var pending = null; 142 143 if ( card.sejWatched || ! window.MutationObserver ) { 144 return; 145 } 146 147 card.sejWatched = true; 148 149 new MutationObserver( function( records ) { 150 var i; 151 152 for ( i = 0; i < records.length; i++ ) { 153 if ( records[ i ].target !== card ) { 154 clearTimeout( pending ); 155 pending = setTimeout( function() { 156 scaleEmbed( card ); 157 }, 150 ); 158 return; 159 } 160 } 161 } ).observe( card, { childList: true, subtree: true, attributes: true, attributeFilter: [ 'style' ] } ); 162 } 163 164 function scaleEmbeds( root ) { 165 var cards = ( root || document ).querySelectorAll( '.sej-article-content ' + SCALE_SELECTOR ); 166 167 Array.prototype.forEach.call( cards, function( card ) { 168 watchCard( card ); 169 scaleEmbed( card ); 170 } ); 171 } 172 173 function fitEmbeds( root ) { 174 var iframes = ( root || document ).querySelectorAll( 175 '.sej-article-content iframe[width][height]:not(.sej-iframe-auto-height):not(.sej-embed-fit)' 176 ); 177 178 Array.prototype.forEach.call( iframes, function( iframe ) { 179 var width, height; 180 181 if ( ! isVideoEmbed( iframe ) ) { 182 return; 183 } 184 185 width = parseInt( iframe.getAttribute( 'width' ), 10 ); 186 height = parseInt( iframe.getAttribute( 'height' ), 10 ); 187 188 if ( ! width || ! height ) { 189 return; 190 } 191 192 iframe.style.setProperty( '--ar', width + ' / ' + height ); 193 iframe.classList.add( 'sej-embed-fit' ); 194 195 // The parent theme wraps YouTube/Vimeo in p.vcont, a 16:9 196 // padding-bottom box holding an absolutely positioned iframe. 197 // That reserved height is wrong for any other ratio, so mark the 198 // wrapper and let the CSS hand sizing back to the iframe. 199 if ( iframe.parentElement && iframe.parentElement.classList.contains( 'vcont' ) ) { 200 iframe.parentElement.classList.add( 'sej-embed-fit-wrap' ); 201 } 202 } ); 203 } 204 205 if ( document.readyState === 'loading' ) { 206 document.addEventListener( 'DOMContentLoaded', function() { 207 fitEmbeds(); 208 scaleEmbeds(); 209 } ); 210 } else { 211 fitEmbeds(); 212 scaleEmbeds(); 213 } 214 215 // Embeds injected later: lazy-loaded article bodies, live blog updates, and 216 // provider scripts like TikTok's that swap their own iframe in after load. 217 if ( window.MutationObserver ) { 218 document.addEventListener( 'DOMContentLoaded', function() { 219 var content = document.querySelector( '.sej-article-content' ), 220 pending = null; 221 222 if ( ! content ) { 223 return; 224 } 225 226 // Watching style attributes is only worth it when a provider card 227 // is actually present - ad slots rewrite inline styles constantly, 228 // and on an article with no TikTok that is pure overhead. 229 var watchStyle = !! content.querySelector( SCALE_SELECTOR ); 230 231 new MutationObserver( function() { 232 fitEmbeds( content ); 233 234 if ( ! watchStyle && content.querySelector( SCALE_SELECTOR ) ) { 235 watchStyle = true; 236 } 237 238 if ( ! watchStyle ) { 239 return; 240 } 241 242 // Coalesce: our own transform writes back into this subtree, so 243 // reacting synchronously would loop. 244 clearTimeout( pending ); 245 pending = setTimeout( function() { 246 scaleEmbeds( content ); 247 }, 200 ); 248 } ).observe( content, { childList: true, subtree: true } ); 249 } ); 250 } 251 252 // The cap is viewport-relative, so a resize or rotation changes it. Only the 253 // scaled embeds need this - the rest are handled by CSS. 254 ( function() { 255 var pending = null; 256 257 window.addEventListener( 'resize', function() { 258 clearTimeout( pending ); 259 pending = setTimeout( scaleEmbeds, 250 ); 260 } ); 261 262 window.addEventListener( 'orientationchange', function() { 263 clearTimeout( pending ); 264 pending = setTimeout( scaleEmbeds, 250 ); 265 } ); 266 } )(); 267} )();
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.