PageSourceSearch

https://viacampesina.org/es/wp-content/plugins/ultimate-post/asset…/ultp-youtube-gallery-block.js?ver=5.0.41

js viacampesina.org collected 2026-09-24 08:46:23 UTC 18,267 bytes, 604 lines download raw bytes

1(function ($) {
2	("use strict");
3
4	$(document).ready(function () {
5		initYouTubeGallery();
6	});
7
8	function initYouTubeGallery() {
9		// Helper for parsing JSON data
10		function parseMaybeJSON(val, fallback) {
11			if (typeof val === "string") {
12				try {
13					return JSON.parse(val);
14				} catch (e) {
15					return fallback;
16				}
17			}
18			return typeof val === "object" && val !== null ? val : fallback;
19		}
20
21		// Helper to truncate text responsively
22		function truncateText(text, length) {
23			if (!text) return "";
24			const width = $(window).width();
25			let maxLength;
26			if (width < 600) {
27				maxLength = length.sm || length.lg;
28			} else if (width < 900) {
29				maxLength = length.md || length.lg;
30			} else {
31				maxLength = length.lg;
32			}
33			return text.length > maxLength
34				? text.substring(0, maxLength) + "..."
35				: text;
36		}
37
38		// Helper to sort videos
39		function sortVideos(videos, sortBy) {
40			const sorted = [...videos];
41			switch (sortBy) {
42				case "title":
43					return sorted.sort((a, b) =>
44						a.title.localeCompare(b.title, undefined, {
45							sensitivity: "base",
46						})
47					);
48				case "latest":
49					return sorted.sort(
50						(a, b) =>
51							new Date(b.publishedAt).getTime() -
52							new Date(a.publishedAt).getTime()
53					);
54				case "date":
55					return sorted.sort(
56						(a, b) =>
57							new Date(a.publishedAt).getTime() -
58							new Date(b.publishedAt).getTime()
59					);
60				case "popular":
61					return sorted.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
62				default:
63					return videos;
64			}
65		}
66
67		// Extract playlist ID from URL or direct ID, and handle channel URLs/handles
68		function getPlaylistId(input) {
69			if (!input) return "";
70			try {
71				const url = new URL(input);
72				if (url.hostname === "www.youtube.com") {
73					if (url.pathname.startsWith("/channel/")) {
74						return url.pathname.split("/channel/")[1];
75					} else if (url.pathname.startsWith("/@")) {
76						return url.pathname.substring(2); // Return handle for later resolution
77					} else if (url.searchParams.get("list")) {
78						return url.searchParams.get("list");
79					}
80				}
81				return input;
82			} catch (e) {
83				return input;
84			}
85		}
86
87		// Resolve handle to channel ID using YouTube API
88		function resolveHandleToChannelId(handle, apiKey, callback) {
89			$.get(
90				`https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent(
91					handle
92				)}&type=channel&key=${apiKey}`
93			)
94				.done(function (data) {
95					if (data.items && data.items.length > 0) {
96						callback(data.items[0].snippet.channelId);
97					} else {
98						callback(null);
99					}
100				})
101				.fail(function () {
102					callback(null);
103				});
104		}
105
106		// Helper function to generate video player iframe
107		function generatePlayerIframe(videoId, config) {
108			const params = [
109				`autoplay=${config.autoplay ? "1" : "0"}`,
110				`loop=${config.loop ? "1" : "0"}`,
111				`mute=${config.mute ? "1" : "0"}`,
112				`controls=${config.showPlayerControl ? "1" : "0"}`,
113				`modestbranding=${config.hideYoutubeLogo ? "1" : "0"}`,
114				config.loop ? `playlist=${videoId}` : null,
115			]
116				.filter(Boolean)
117				.join("&");
118
119			return `
120				<div class="ultp-ytg-video-wrapper">
121					<iframe 
122						src="https://www.youtube.com/embed/${videoId}?${params}"
123						title="YouTube Video"
124						frameborder="0"
125						allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
126						allowfullscreen
127					></iframe>
128				</div>
129			`;
130		}
131
132		// Generate text content (title and description)
133		function getYoutubeTextContent(
134			enableTitle,
135			title,
136			titleLength,
137			enableDesc,
138			description,
139			descriptionLength,
140			videoId
141		) {
142			let html = '<div class="ultp-ytg-content">';
143			if (enableTitle) {
144				html += `<div class="ultp-ytg-title"><a href="https://www.youtube.com/watch?v=${videoId}" target="_blank" rel="noopener noreferrer">${truncateText(
145					title,
146					titleLength
147				)}</a></div>`;
148			}
149			if (enableDesc) {
150				html += `<div class="ultp-ytg-description">${truncateText(
151					description,
152					descriptionLength
153				)}</div>`;
154			}
155			html += "</div>";
156			return html;
157		}
158
159		// Initialize YouTube Gallery
160		$(".wp-block-ultimate-post-youtube-gallery").each(function () {
161			const $block = $(this);
162			const $wrapper = $block.find(".ultp-block-wrapper");
163			let $container = $block.find(".ultp-ytg-view-grid, .ultp-ytg-container");
164			const $loadMoreBtn = $block.find(".ultp-ytg-loadmore-btn");
165
166			// Get configuration from data attributes
167			const config = {
168				playlistIdOrUrl: $block.data("playlist") || "",
169				apiKey: $block.data("api-key") || "",
170				cacheDuration: parseInt($block.data("cache-duration")) || 0,
171				sortBy: $block.data("sort-by") || "date",
172				galleryLayout: $block.data("gallery-layout") || "grid",
173				videosPerPage: parseMaybeJSON($block.data("videos-per-page"), {
174					lg: 9,
175					md: 6,
176					sm: 3,
177				}),
178				showVideoTitle: $block.data("show-video-title") == "1",
179				videoTitleLength: parseMaybeJSON($block.data("video-title-length"), {
180					lg: 50,
181					md: 50,
182					sm: 50,
183				}),
184				loadMoreEnable: $block.data("load-more-enable") == "1",
185				moreButtonLabel: $block.data("more-button-label") || "More Videos",
186				autoplay: $block.data("autoplay") == "1",
187				loop: $block.data("loop") == "1",
188				mute: $block.data("mute") == "1",
189				showPlayerControl: $block.data("show-player-control") == "1",
190				hideYoutubeLogo: $block.data("hide-youtube-logo") == "1",
191				showDescription: $block.data("show-description") == "1",
192				videoDescriptionLength: parseMaybeJSON(
193					$block.data("video-description-length"),
194					{
195						lg: 100,
196						md: 100,
197						sm: 100,
198					}
199				),
200				imageHeightRatio: $block.data("image-height-ratio") || "16-9",
201				galleryColumn: parseMaybeJSON($block.data("gallery-column"), {
202					lg: 3,
203					md: 2,
204					sm: 1,
205				}),
206				displayType: $block.data("display-type") || "grid",
207				enableListView: $block.data("enable-list-view") == "1",
208				enableIconAnimation: $block.data("enable-icon-animation") == "1",
209				defaultYoutubeIcon: $block.data("enable-youtube-icon") == "1",
210				imgHeight: $block.data("img-height"),
211			};
212
213			let playlistId = getPlaylistId(config.playlistIdOrUrl);
214
215			// If it's a handle (starts with @), resolve to channel ID
216			if (playlistId.startsWith("@")) {
217				const handle = playlistId.substring(1);
218				resolveHandleToChannelId(handle, config.apiKey, function (channelId) {
219					if (channelId) {
220						playlistId = channelId;
221						proceedWithPlaylist(playlistId);
222					} else {
223						$wrapper.html(
224							'<p style="color:#888">Invalid handle or API key.</p>'
225						);
226					}
227				});
228			} else {
229				proceedWithPlaylist(playlistId);
230			}
231
232			function proceedWithPlaylist(playlistId) {
233				// Convert channel ID to uploads playlist ID
234				if (playlistId.startsWith("UC")) {
235					playlistId = "UU" + playlistId.substring(2);
236				}
237
238				if (!playlistId || !config.apiKey) {
239					$wrapper.html(
240						'<p style="color:#888">Please provide both YouTube playlist ID/URL and API key.</p>'
241					);
242					return;
243				}
244
245				// State management
246				let allVideos = [];
247				let shownCount = config.videosPerPage.lg || 9;
248				let activeVideo = null;
249				let playingId = null;
250
251				// Update responsive counts
252				function updateResponsiveCounts() {
253					const width = $(window).width();
254					if (width < 600) {
255						shownCount = Math.max(shownCount, config.videosPerPage.sm || 3);
256					} else if (width < 900) {
257						shownCount = Math.max(shownCount, config.videosPerPage.md || 6);
258					} else {
259						shownCount = Math.max(shownCount, config.videosPerPage.lg || 9);
260					}
261				}
262
263				// Render videos for playlist layout
264				function renderPlaylistLayout(videos) {
265					if (!videos.length) {
266						$container.html("<p>No videos found in this playlist.</p>");
267						return;
268					}
269
270					if (!activeVideo) {
271						activeVideo = videos[0];
272					}
273
274					let html = '<div class="ultp-ytg-main">';
275					// Generate player iframe and place .ultp-ytg-content inside .ultp-ytg-video-wrapper
276					const playerWrapper = `
277						<div class="ultp-ytg-video-wrapper">
278							<iframe 
279								src="https://www.youtube.com/embed/${activeVideo.videoId}?${[
280						`autoplay=${config.autoplay ? "1" : "0"}`,
281						`loop=${config.loop ? "1" : "0"}`,
282						`mute=${config.mute ? "1" : "0"}`,
283						`controls=${config.showPlayerControl ? "1" : "0"}`,
284						`modestbranding=${config.hideYoutubeLogo ? "1" : "0"}`,
285						config.loop ? `playlist=${activeVideo.videoId}` : null,
286					]
287						.filter(Boolean)
288						.join("&")}"
289								title="YouTube Video"
290								frameborder="0"
291								allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
292								allowfullscreen
293							></iframe>
294							${getYoutubeTextContent(
295								config.showVideoTitle,
296								activeVideo.title,
297								config.videoTitleLength,
298								config.showDescription,
299								activeVideo.description,
300								config.videoDescriptionLength,
301								activeVideo.videoId
302							)}
303						</div>
304					`;
305					html += playerWrapper;
306					html += "</div>";
307
308					html += '<div class="ultp-ytg-playlist-sidebar">';
309					html += '<div class="ultp-ytg-playlist-items">';
310					videos.forEach(function (video) {
311						const isActive = video.videoId === activeVideo.videoId;
312						html += `
313							<div class="ultp-ytg-playlist-item ${
314								isActive ? "active" : ""
315							}" data-video-id="${video.videoId}">
316								<img src="${video.thumbnail}" alt="${video.title}" loading="lazy" />
317								<div class="ultp-ytg-playlist-item-content">
318									<div class="ultp-ytg-playlist-item-title">
319										${truncateText(video.title, config.videoTitleLength)}
320									</div>
321								</div>
322							</div>
323						`;
324					});
325					html += "</div></div>";
326
327					$container.html(html);
328				}
329
330				// Render videos for grid/list layout
331				function renderGridLayout(videos, count) {
332					if (!videos.length) {
333						$container.html("<p>No videos found in this playlist.</p>");
334						return;
335					}
336
337					const displayedVideos = videos.slice(0, count);
338					let html = "";
339					displayedVideos.forEach(function (video) {
340						const isPlaying = playingId === video.videoId;
341						html += `<div class="ultp-ytg-item${isPlaying ? " active" : ""}">`;
342						html += `<div class="ultp-ytg-video">`;
343
344						if (isPlaying) {
345							html += generatePlayerIframe(video.videoId, config);
346						} else {
347							// enableIconAnimation
348							const getSvgIcon = $(".ultp-ytg-play__icon").html();
349							html += `
350								<img src="${video.thumbnail}" alt="${
351								video.title
352							}" loading="lazy" data-video-id="${
353								video.videoId
354							}" style="cursor:pointer;" />
355								<div class="ultp-ytg-play__icon${
356									config.enableIconAnimation ? " ytg-icon-animation" : ""
357								}">
358									${getSvgIcon}
359								</div>
360							`;
361						}
362
363						html += `</div>`;
364						html += `<div class="ultp-ytg-inside">`;
365						html += getYoutubeTextContent(
366							config.showVideoTitle,
367							video.title,
368							config.videoTitleLength,
369							config.showDescription,
370							video.description,
371							config.videoDescriptionLength,
372							video.videoId
373						);
374						html += `</div></div>`;
375					});
376
377					$container.html(html);
378
379					// Update load more button visibility
380					if (config.loadMoreEnable && count < videos.length) {
381						$loadMoreBtn.show();
382					} else {
383						$loadMoreBtn.hide();
384					}
385				}
386
387				// Render based on layout type
388				function renderVideos(videos, count) {
389					if (config.galleryLayout === "playlist") {
390						renderPlaylistLayout(videos);
391					} else {
392						renderGridLayout(videos, count);
393					}
394				}
395
396				// Fetch videos from YouTube API
397				const cacheKey = `ultp_youtube_gallery_${playlistId}_${config.apiKey}_${config.sortBy}_${config.imgHeight}`;
398				const duration = config.cacheDuration;
399				let cached = null;
400
401				try {
402					cached = JSON.parse(localStorage.getItem(cacheKey));
403				} catch (e) {
404					cached = null;
405				}
406
407				const now = Date.now();
408				if (
409					cached &&
410					cached.data &&
411					cached.timestamp &&
412					duration > 0 &&
413					now - cached.timestamp < duration * 1000
414				) {
415					allVideos = sortVideos(cached.data, config.sortBy);
416					renderVideos(allVideos, shownCount);
417				} else {
418					// Only show loading skeleton for grid/list layouts, not for playlist layout
419					if (config.galleryLayout !== "playlist") {
420						$container.html(`
421							<div class="ultp-ytg-loading gallery-postx gallery-active">
422								<div class="skeleton-box"></div>
423								<div class="skeleton-box"></div>
424								<div class="skeleton-box"></div>
425								<div class="skeleton-box"></div>
426								<div class="skeleton-box"></div>
427								<div class="skeleton-box"></div>
428							</div>
429						`);
430					} else {
431						$container.html(`
432							<div class="ultp-ytg-loading ultp-ytg-playlist-loading">
433								<div class="ytg-loader"></div>
434							</div>`);
435					}
436
437					$.get("https://www.googleapis.com/youtube/v3/playlistItems", {
438						part: "snippet",
439						maxResults: 50,
440						playlistId: playlistId,
441						key: config.apiKey,
442					})
443						.done(function (data) {
444							setTimeout(function () {
445								$container.empty(); // Remove loading after 3s
446								if (data.error) {
447									$container.html(
448										`<div class="ultp-ytg-error">${
449											data.error.message || "Failed to fetch playlist."
450										}</div>`
451									);
452									return;
453								}
454
455								const videos = (data.items || [])
456									.filter(function (item) {
457										return (
458											item.snippet.title !== "Private video" &&
459											item.snippet.title !== "Deleted video"
460										);
461									})
462									.map(function (item) {
463										return {
464											videoId: item.snippet.resourceId.videoId,
465											title: item.snippet.title,
466											thumbnail:
467												(item.snippet.thumbnails &&
468													item.snippet.thumbnails[config.imgHeight] &&
469													item.snippet.thumbnails[config.imgHeight].url) ||
470												item.snippet.thumbnails[config.imgHeight].url ||
471												item.snippet.thumbnails?.medium?.url ||
472												"",
473											publishedAt: item.snippet.publishedAt || "",
474											description: item.snippet.description || "",
475											viewCount: 0, // Initialize for popular sorting
476										};
477									}); // If sorting by popular, fetch view counts
478								if (config.sortBy === "popular") {
479									const videoIds = videos.map((v) => v.videoId).join(",");
480									$.get(
481										`https://www.googleapis.com/youtube/v3/videos?part=statistics&id=${videoIds}&key=${config.apiKey}`
482									)
483										.done(function (statsData) {
484											if (statsData.items) {
485												const statsMap = {};
486												statsData.items.forEach(function (item) {
487													statsMap[item.id] = item.statistics.viewCount;
488												});
489												videos.forEach(function (v) {
490													v.viewCount = parseInt(statsMap[v.videoId] || 0);
491												});
492											}
493											allVideos = sortVideos(videos, config.sortBy);
494											if (duration > 0) {
495												try {
496													localStorage.setItem(
497														cacheKey,
498														JSON.stringify({
499															data: videos,
500															timestamp: now,
501														})
502													);
503												} catch (e) {
504													console.warn("Failed to cache videos:", e);
505												}
506											}
507											renderVideos(allVideos, shownCount);
508										})
509										.fail(function () {
510											console.warn(
511												"Failed to fetch video statistics for popular sorting."
512											);
513											allVideos = sortVideos(videos, config.sortBy);
514											renderVideos(allVideos, shownCount);
515										});
516								} else {
517									allVideos = sortVideos(videos, config.sortBy);
518									if (duration > 0) {
519										try {
520											localStorage.setItem(
521												cacheKey,
522												JSON.stringify({
523													data: videos,
524													timestamp: now,
525												})
526											);
527										} catch (e) {
528											console.warn("Failed to cache videos:", e);
529										}
530									}
531									renderVideos(allVideos, shownCount);
532								}
533							}, 2000);
534						})
535						.fail(function () {
536							setTimeout(function () {
537								$container.empty();
538								$container.html(
539									'<div class="ultp-ytg-error">Failed to fetch videos. Please try again.</div>'
540								);
541							}, 3000);
542						});
543				}
544
545				// Event handlers
546				$block.on("click", ".ultp-ytg-playlist-item", function () {
547					const videoId = $(this).data("video-id");
548					if (!videoId) return;
549
550					activeVideo = allVideos.find(function (v) {
551						return v.videoId === videoId;
552					});
553
554					if (activeVideo) {
555						renderPlaylistLayout(allVideos);
556					}
557				});
558				// Make clicking the play icon also start the video with loader fallback (per item)
559				$block.on("click", ".ultp-ytg-play__icon", function () {
560					// Find the previous img[data-video-id] sibling to get the videoId
561					const $img = $(this).siblings("img[data-video-id]");
562					const videoId = $img.data("video-id");
563					if (!videoId) return;
564					// Find the closest .ultp-ytg-item and its .ultp-ytg-video
565					const $item = $(this).closest(".ultp-ytg-item");
566					const $videoDiv = $item.find(".ultp-ytg-video");
567					// Show loader only in this video area
568					$videoDiv.html(
569						'<div class="ultp-ytg-loading"><div class="ytg-loader"></div></div>'
570					);
571					// Add active class to this item, remove from others
572					$item
573						.addClass("active")
574						.siblings(".ultp-ytg-item")
575						.removeClass("active");
576					setTimeout(function () {
577						playingId = videoId;
578						renderGridLayout(allVideos, shownCount);
579					}, 1000);
580				});
581
582				$block.on("click", ".ultp-ytg-video img[data-video-id]", function () {
583					const videoId = $(this).data("video-id");
584					if (!videoId) return;
585
586					playingId = videoId;
587					renderGridLayout(allVideos, shownCount);
588				});
589
590				$loadMoreBtn.on("click", function () {
591					shownCount += config.videosPerPage.lg;
592					renderGridLayout(allVideos, shownCount);
593				});
594				// Responsive handling
595				$(window).on("resize", function () {
596					if (allVideos.length) {
597						updateResponsiveCounts();
598						renderVideos(allVideos, shownCount);
599					}
600				});
601			}
602		});
603	}
604})(jQuery);

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.