PageSourceSearch

https://hothouseflowers.com/wp-content/plugins/jetpack/modules/likes/queuehandler.js?ver=16.2-beta

js hothouseflowers.com collected 2026-09-24 22:53:30 UTC 16,941 bytes, 526 lines download raw bytes

1/* global wpcom_reblog */
2
3var jetpackLikesWidgetBatch = [];
4var jetpackLikesMasterReady = false;
5
6// Due to performance problems on pages with a large number of widget iframes that need to be loaded,
7// we are limiting the processing at any instant to unloaded widgets that are currently in viewport,
8// plus this constant that will allow processing of widgets above and bellow the current fold.
9// This aim of it is to improve the UX and hide the transition from unloaded to loaded state from users.
10var jetpackLikesLookAhead = 2000; // pixels
11
12// Keeps track of loaded comment likes widget so we can unload them when they are scrolled out of view.
13var jetpackCommentLikesLoadedWidgets = [];
14
15var jetpackLikesDocReadyPromise = new Promise( resolve => {
16	if ( document.readyState !== 'loading' ) {
17		resolve();
18	} else {
19		window.addEventListener( 'DOMContentLoaded', () => resolve() );
20	}
21} );
22
23function JetpackLikesPostMessage( message, target ) {
24	if ( typeof message === 'string' ) {
25		try {
26			message = JSON.parse( message );
27		} catch {
28			return;
29		}
30	}
31
32	if ( target && typeof target.postMessage === 'function' ) {
33		try {
34			target.postMessage(
35				JSON.stringify( {
36					type: 'likesMessage',
37					data: message,
38				} ),
39				'*'
40			);
41		} catch {
42			// Ignore error
43		}
44	}
45}
46
47function JetpackLikesBatchHandler() {
48	const requests = [];
49	document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' ).forEach( widget => {
50		if ( jetpackLikesWidgetBatch.indexOf( widget.id ) > -1 ) {
51			return;
52		}
53
54		if ( ! jetpackIsScrolledIntoView( widget ) ) {
55			return;
56		}
57
58		jetpackLikesWidgetBatch.push( widget.id );
59
60		var regex = /like-(post|comment)-wrapper-(\d+)-(\d+)-(\w+)/,
61			match = regex.exec( widget.id ),
62			info;
63
64		if ( ! match || match.length !== 5 ) {
65			return;
66		}
67
68		info = {
69			blog_id: match[ 2 ],
70			width: widget.width,
71		};
72
73		if ( 'post' === match[ 1 ] ) {
74			info.post_id = match[ 3 ];
75		} else if ( 'comment' === match[ 1 ] ) {
76			info.comment_id = match[ 3 ];
77		}
78
79		info.obj_id = match[ 4 ];
80
81		requests.push( info );
82	} );
83
84	if ( requests.length > 0 ) {
85		JetpackLikesPostMessage(
86			{ event: 'initialBatch', requests: requests },
87			window.frames[ 'likes-master' ]
88		);
89	}
90}
91
92function JetpackLikesMessageListener( event ) {
93	let message = event && event.data;
94	if ( typeof message === 'string' ) {
95		try {
96			message = JSON.parse( message );
97		} catch {
98			return;
99		}
100	}
101
102	const type = message && message.type;
103	const data = message && message.data;
104
105	if ( type !== 'likesMessage' || typeof data.event === 'undefined' ) {
106		return;
107	}
108
109	// We only allow messages from one origin
110	const allowedOrigin = 'https://widgets.wp.com';
111	if ( allowedOrigin !== event.origin ) {
112		return;
113	}
114
115	switch ( data.event ) {
116		case 'masterReady':
117			jetpackLikesDocReadyPromise.then( () => {
118				jetpackLikesMasterReady = true;
119
120				const stylesData = {
121					event: 'injectStyles',
122				};
123				const sdTextColor = document.querySelector( '.sd-text-color' );
124				const sdLinkColor = document.querySelector( '.sd-link-color' );
125				const sdTextColorStyles = ( sdTextColor && getComputedStyle( sdTextColor ) ) || {};
126				const sdLinkColorStyles = ( sdLinkColor && getComputedStyle( sdLinkColor ) ) || {};
127
128				// enable reblogs if we're on a single post page
129				if ( document.body.classList.contains( 'single' ) ) {
130					JetpackLikesPostMessage( { event: 'reblogsEnabled' }, window.frames[ 'likes-master' ] );
131				}
132
133				stylesData.textStyles = {
134					color: sdTextColorStyles.color,
135					fontFamily: sdTextColorStyles[ 'font-family' ],
136					fontSize: sdTextColorStyles[ 'font-size' ],
137					direction: sdTextColorStyles.direction,
138					fontWeight: sdTextColorStyles[ 'font-weight' ],
139					fontStyle: sdTextColorStyles[ 'font-style' ],
140					textDecoration: sdTextColorStyles[ 'text-decoration' ],
141				};
142
143				stylesData.linkStyles = {
144					color: sdLinkColorStyles.color,
145					fontFamily: sdLinkColorStyles[ 'font-family' ],
146					fontSize: sdLinkColorStyles[ 'font-size' ],
147					textDecoration: sdLinkColorStyles[ 'text-decoration' ],
148					fontWeight: sdLinkColorStyles[ 'font-weight' ],
149					fontStyle: sdLinkColorStyles[ 'font-style' ],
150				};
151
152				JetpackLikesPostMessage( stylesData, window.frames[ 'likes-master' ] );
153
154				JetpackLikesBatchHandler();
155			} );
156
157			break;
158
159		// We're keeping this for planned future follow ups.
160		// @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815
161		case 'showLikeWidget':
162			break;
163
164		// We're keeping this for planned future follow ups.
165		// @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815
166		case 'showCommentLikeWidget':
167			break;
168
169		case 'killCommentLikes':
170			// If kill switch for comment likes is enabled remove all widgets wrappers and `Loading...` placeholders.
171			document
172				.querySelectorAll( '.jetpack-comment-likes-widget-wrapper' )
173				.forEach( wrapper => wrapper.remove() );
174			break;
175
176		case 'clickReblogFlair':
177			if ( wpcom_reblog && typeof wpcom_reblog.toggle_reblog_box_flair === 'function' ) {
178				wpcom_reblog.toggle_reblog_box_flair( data.obj_id );
179			}
180			break;
181
182		case 'hideOtherGravatars': {
183			hideLikersPopover();
184			break;
185		}
186
187		case 'showOtherGravatars': {
188			const container = document.querySelector( '#likes-other-gravatars' );
189
190			if ( ! container ) {
191				break;
192			}
193
194			const list = container.querySelector( 'ul' );
195
196			container.style.display = 'none';
197			list.innerHTML = '';
198
199			container
200				.querySelectorAll( '.likes-text span' )
201				.forEach( item => ( item.textContent = data.totalLikesLabel ) );
202
203			( data.likers || [] ).forEach( async ( liker, index ) => {
204				if ( liker.profile_URL.substr( 0, 4 ) !== 'http' ) {
205					// We only display gravatars with http or https schema
206					return;
207				}
208
209				const element = document.createElement( 'li' );
210				list.append( element );
211
212				const profileLink = encodeURI( liker.profile_URL );
213				const avatarLink = encodeURI( liker.avatar_URL );
214				element.innerHTML = `<a href="${ profileLink }" rel="nofollow" target="_parent" class="wpl-liker">
215						<img src="${ avatarLink }"
216							alt=""
217							style="width: 28px; height: 28px;" />
218						<span></span>
219					</a>`;
220
221				// Add some extra attributes through native methods, to ensure strings are sanitized.
222				element.classList.add( liker.css_class );
223				element.querySelector( 'img' ).alt = data.avatarAltTitle.replace( '%s', liker.name );
224				element.querySelector( 'span' ).innerText = liker.name;
225
226				if ( index === data.likers.length - 1 ) {
227					element.addEventListener( 'keydown', e => {
228						if ( e.key === 'Tab' && ! e.shiftKey ) {
229							e.preventDefault();
230							hideLikersPopover();
231
232							JetpackLikesPostMessage(
233								{ event: 'focusLikesCount', parent: data.parent },
234								window.frames[ 'likes-master' ]
235							);
236						}
237					} );
238				}
239			} );
240
241			const positionPopup = function () {
242				const containerStyle = getComputedStyle( container );
243				const isRtl = containerStyle.direction === 'rtl';
244
245				const el = document.querySelector( `*[name='${ data.parent }']` );
246				const rect = el.getBoundingClientRect();
247				const win = el.ownerDocument.defaultView;
248				const offset = {
249					top: rect.top + win.pageYOffset,
250					left: rect.left + win.pageXOffset,
251				};
252
253				let containerLeft;
254				container.style.top = offset.top + data.position.top - 1 + 'px';
255
256				if ( isRtl ) {
257					const visibleAvatarsCount = data && data.likers ? Math.min( data.likers.length, 5 ) : 0;
258					// 24px is the width of the avatar + 4px is the padding between avatars
259					containerLeft = offset.left + data.position.left + 24 * visibleAvatarsCount + 4;
260					container.style.transform = 'translateX(-100%)';
261				} else {
262					containerLeft = offset.left + data.position.left;
263				}
264				container.style.left = containerLeft + 'px';
265
266				// If the popup overflows viewport width, we should show it on the next line.
267				// Push it offscreen to calculated rendered width.
268				container.style.left = '-9999px';
269				container.style.display = 'block';
270
271				// If the popup exceeds the viewport width,
272				// flip the position of the popup.
273				const containerWidth = container.offsetWidth;
274				const containerRight = containerLeft + containerWidth;
275				if ( containerRight > win.innerWidth ) {
276					containerLeft = rect.right - containerWidth;
277				}
278
279				// Set the container left
280				container.style.left = containerLeft + 'px';
281				container.setAttribute( 'aria-hidden', 'false' );
282			};
283
284			positionPopup();
285			container.focus();
286
287			const debounce = function ( func, wait ) {
288				var timeout;
289				return function () {
290					var context = this;
291					var args = arguments;
292					clearTimeout( timeout );
293					timeout = setTimeout( function () {
294						func.apply( context, args );
295					}, wait );
296				};
297			};
298
299			const debouncedPositionPopup = debounce( positionPopup, 100 );
300
301			// Keep a reference of this function in the element itself
302			// so that we can destroy it later
303			container.__resizeHandler = debouncedPositionPopup;
304
305			// When window is resized, resize the popup.
306			window.addEventListener( 'resize', debouncedPositionPopup );
307
308			container.focus();
309		}
310	}
311}
312
313window.addEventListener( 'message', JetpackLikesMessageListener );
314
315function hideLikersPopover() {
316	const container = document.querySelector( '#likes-other-gravatars' );
317
318	if ( container ) {
319		container.style.display = 'none';
320		container.setAttribute( 'aria-hidden', 'true' );
321
322		// Remove the resize event listener and cleanup.
323		const resizeHandler = container.__resizeHandler;
324		if ( resizeHandler ) {
325			window.removeEventListener( 'resize', resizeHandler );
326			delete container.__resizeHandler;
327		}
328	}
329}
330
331document.addEventListener( 'click', hideLikersPopover );
332
333function JetpackLikesWidgetQueueHandler() {
334	var wrapperID;
335
336	if ( ! jetpackLikesMasterReady ) {
337		// The master iframe emits `masterReady` a single time, when it finishes loading. On
338		// script-heavy pages it can finish loading — and emit — before this script attaches its
339		// message listener above, so that one event is missed and the queue never starts. Ping the
340		// master iframe while we wait so it re-emits `masterReady` once both it and our listener
341		// are ready.
342		JetpackLikesPostMessage( { event: 'queryMasterReady' }, window.frames[ 'likes-master' ] );
343		setTimeout( JetpackLikesWidgetQueueHandler, 500 );
344		return;
345	}
346
347	// Restore widgets to initial unloaded state when they are scrolled out of view.
348	jetpackUnloadScrolledOutWidgets();
349
350	jetpackObserveUnloadedWidgets();
351
352	var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
353
354	if ( unloadedWidgetsInView.length > 0 ) {
355		// Grab any unloaded widgets for a batch request
356		JetpackLikesBatchHandler();
357	}
358
359	for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) {
360		wrapperID = unloadedWidgetsInView[ i ].id;
361
362		if ( ! wrapperID ) {
363			continue;
364		}
365
366		jetpackLoadLikeWidgetIframe( wrapperID );
367	}
368}
369
370function jetpackLoadLikeWidgetIframe( wrapperID ) {
371	if ( typeof wrapperID === 'undefined' ) {
372		return;
373	}
374
375	const wrapper = document.querySelector( '#' + wrapperID );
376	wrapper.querySelectorAll( 'iframe' ).forEach( iFrame => iFrame.remove() );
377
378	const placeholder = wrapper.querySelector( '.likes-widget-placeholder' );
379
380	// Post like iframe
381	if ( placeholder && placeholder.classList.contains( 'post-likes-widget-placeholder' ) ) {
382		const postLikesFrame = document.createElement( 'iframe' );
383
384		postLikesFrame.classList.add( 'post-likes-widget', 'jetpack-likes-widget' );
385		postLikesFrame.name = wrapper.dataset.name;
386		postLikesFrame.src = wrapper.dataset.src;
387		postLikesFrame.height = '55px';
388		postLikesFrame.width = '100%';
389		postLikesFrame.frameBorder = '0';
390		postLikesFrame.scrolling = 'no';
391		postLikesFrame.title = wrapper.dataset.title;
392
393		placeholder.after( postLikesFrame );
394	}
395
396	// Comment like iframe
397	if ( placeholder.classList.contains( 'comment-likes-widget-placeholder' ) ) {
398		const commentLikesFrame = document.createElement( 'iframe' );
399
400		commentLikesFrame.class = 'comment-likes-widget-frame jetpack-likes-widget-frame';
401		commentLikesFrame.name = wrapper.dataset.name;
402		commentLikesFrame.src = wrapper.dataset.src;
403		commentLikesFrame.height = '18px';
404		commentLikesFrame.width = '100%';
405		commentLikesFrame.frameBorder = '0';
406		commentLikesFrame.scrolling = 'no';
407
408		wrapper.querySelector( '.comment-like-feedback' ).after( commentLikesFrame );
409
410		jetpackCommentLikesLoadedWidgets.push( commentLikesFrame );
411	}
412
413	wrapper.classList.remove( 'jetpack-likes-widget-unloaded' );
414	wrapper.classList.add( 'jetpack-likes-widget-loading' );
415
416	wrapper.querySelector( 'iframe' ).addEventListener( 'load', e => {
417		// A widget scrolled out of view mid-load has its iframe dropped, and a reloaded one
418		// replaces it, either of which leaves this closure holding a detached iframe. Acting on
419		// it would hide a placeholder that is now the only thing the widget has left to show.
420		if ( ! wrapper.contains( e.target ) ) {
421			return;
422		}
423
424		JetpackLikesPostMessage(
425			{ event: 'loadLikeWidget', name: e.target.name, width: e.target.width },
426			window.frames[ 'likes-master' ]
427		);
428
429		// The stylesheet is otherwise the only thing that hides the placeholder, so one that never
430		// applies leaves "Loading…" sitting over a widget that loaded fine.
431		if ( placeholder ) {
432			placeholder.style.display = 'none';
433		}
434
435		wrapper.classList.remove( 'jetpack-likes-widget-loading' );
436		wrapper.classList.add( 'jetpack-likes-widget-loaded' );
437	} );
438}
439
440function jetpackGetUnloadedWidgetsInView() {
441	const unloadedWidgets = document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' );
442
443	return [ ...unloadedWidgets ].filter( item => jetpackIsScrolledIntoView( item ) );
444}
445
446function jetpackIsScrolledIntoView( element ) {
447	const top = element.getBoundingClientRect().top;
448	const bottom = element.getBoundingClientRect().bottom;
449
450	// Allow some slack above and bellow the fold with jetpackLikesLookAhead,
451	// with the aim of hiding the transition from unloaded to loaded widget from users.
452	return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead;
453}
454
455function jetpackUnloadScrolledOutWidgets() {
456	for ( let i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) {
457		const currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ];
458
459		if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) {
460			const widgetWrapper =
461				currentWidgetIframe &&
462				currentWidgetIframe.parentElement &&
463				currentWidgetIframe.parentElement.parentElement;
464
465			// Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed.
466			widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' );
467			widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' );
468			widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' );
469
470			// An empty string removes the inline declaration rather than setting one, handing the
471			// placeholder back to the stylesheet. The `display: none` set on load would otherwise
472			// outrank it, leaving an unloaded widget with neither an iframe nor a placeholder.
473			const placeholder = widgetWrapper.querySelector( '.likes-widget-placeholder' );
474			if ( placeholder ) {
475				placeholder.style.display = '';
476			}
477
478			// Remove it from the list of loaded widgets.
479			jetpackCommentLikesLoadedWidgets.splice( i, 1 );
480
481			// Remove comment like widget iFrame.
482			currentWidgetIframe.remove();
483		}
484	}
485}
486
487var jetpackWidgetsDelayedExec = function ( after, fn ) {
488	var timer;
489	return function () {
490		clearTimeout( timer );
491		timer = setTimeout( fn, after );
492	};
493};
494
495var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler );
496
497// Scrolling is not the only thing that brings a widget into range. A stylesheet that lands late
498// reflows the page without firing a scroll event, and the queue would never look at that widget
499// again, leaving it on "Loading…" until the reader happens to scroll.
500var jetpackLikesWidgetObserver =
501	typeof IntersectionObserver === 'function'
502		? new IntersectionObserver( jetpackOnScrollStopped, {
503				rootMargin: `${ jetpackLikesLookAhead }px`,
504				// jetpackIsScrolledIntoView() wants the widget fully inside the band, so ask to be
505				// told when it gets there - crossing into partial overlap alone would not load it.
506				threshold: [ 0, 1 ],
507			} )
508		: null;
509
510// Observing an element twice is a no-op, so every queue pass can call this to pick up widgets
511// added after load.
512function jetpackObserveUnloadedWidgets() {
513	if ( ! jetpackLikesWidgetObserver ) {
514		return;
515	}
516
517	document
518		.querySelectorAll( 'div.jetpack-likes-widget-unloaded' )
519		.forEach( widget => jetpackLikesWidgetObserver.observe( widget ) );
520}
521
522// Load initial batch of widgets, prior to any scrolling events.
523JetpackLikesWidgetQueueHandler();
524
525// Add event listener to execute queue handler after scroll.
526window.addEventListener( 'scroll', jetpackOnScrollStopped, true );

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.