PageSourceSearch

https://cdn.searchenginejournal.com/wp-content/plugins/abm-sej/includes/js/embed-fit.js?ver=1788441617

js searchenginejournal.com collected 2026-09-24 06:23:37 UTC 9,191 bytes, 267 lines download raw bytes

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.