1var mod_pagespeed_77Guht9mQf = "(function ($) {\n (\"use strict\");\n\n $(document).ready(function () {\n initYouTubeGallery();\n });\n\n function initYouTubeGallery() {\n // Helper for parsing JSON data\n function parseMaybeJSON(val, fallback) {\n if (typeof val === \"string\") {\n try {\n return JSON.parse(val);\n } catch (e) {\n return fallback;\n }\n }\n return typeof val === \"object\" && val !== null ? val : fallback;\n }\n\n // Helper to truncate text responsively\n function truncateText(text, length) {\n if (!text) return \"\";\n const width = $(window).width();\n let maxLength;\n if (width < 600) {\n maxLength = length.sm || length.lg;\n } else if (width < 900) {\n maxLength = length.md || length.lg;\n } else {\n maxLength = length.lg;\n }\n return text.length > maxLength\n ? text.substring(0, maxLength) + \"...\"\n : text;\n }\n\n // Helper to sort videos\n function sortVideos(videos, sortBy) {\n const sorted = [...videos];\n switch (sortBy) {\n case \"title\":\n return sorted.sort((a, b) =>\n a.title.localeCompare(b.title, undefined, {\n sensitivity: \"base\",\n })\n );\n case \"latest\":\n return sorted.sort(\n (a, b) =>\n new Date(b.publishedAt).getTime() -\n new Date(a.publishedAt).getTime()\n );\n case \"date\":\n return sorted.sort(\n (a, b) =>\n new Date(a.publishedAt).getTime() -\n new Date(b.publishedAt).getTime()\n );\n case \"popular\":\n return sorted.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));\n default:\n return videos;\n }\n }\n\n // Extract playlist ID from URL or direct ID, and handle channel URLs/handles\n function getPlaylistId(input) {\n if (!input) return \"\";\n try {\n const url = new URL(input);\n if (url.hostname === \"www.youtube.com\") {\n if (url.pathname.startsWith(\"/channel/\")) {\n return url.pathname.split(\"/channel/\")[1];\n } else if (url.pathname.startsWith(\"/@\")) {\n return url.pathname.substring(2); // Return handle for later resolution\n } else if (url.searchParams.get(\"list\")) {\n return url.searchParams.get(\"list\");\n }\n }\n return input;\n } catch (e) {\n return input;\n }\n }\n\n // Resolve handle to channel ID using YouTube API\n function resolveHandleToChannelId(handle, apiKey, callback) {\n $.get(\n `https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent(\n handle\n )}&type=channel&key=${apiKey}
1`\n )\n .done(function (data) {\n if (data.items && data.items.length > 0) {\n callback(data.items[0].snippet.channelId);\n } else {\n callback(null);\n }\n })\n .fail(function () {\n callback(null);\n });\n }\n\n // Helper function to generate video player iframe\n function generatePlayerIframe(videoId, config) {\n const params = [\n `autoplay=${config.autoplay ? \"1\" : \"0\"}`,\n `loop=${config.loop ? \"1\" : \"0\"}`,\n `mute=${config.mute ? \"1\" : \"0\"}`,\n `controls=${config.showPlayerControl ? \"1\" : \"0\"}`,\n `modestbranding=${config.hideYoutubeLogo ? \"1\" : \"0\"}`,\n config.loop ? `playlist=${videoId}` : null,\n ]\n .filter(Boolean)\n .join(\"&\");\n\n return `\n <div class=\"ultp-ytg-video-wrapper\">\n <iframe \n src=\"https://www.youtube.com/embed/${videoId}?${params}\"\n title=\"YouTube Video\"\n frameborder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowfullscreen\n ></iframe>\n </div>\n `;\n }\n\n // Generate text content (title and description)\n function getYoutubeTextContent(\n enableTitle,\n title,\n titleLength,\n enableDesc,\n description,\n descriptionLength,\n videoId\n ) {\n let html = '<div class=\"ultp-ytg-content\">';\n if (enableTitle) {\n html += `<div class=\"ultp-ytg-title\"><a href=\"https://www.youtube.com/watch?v=${videoId}\" target=\"_blank\" rel=\"noopener noreferrer\">${truncateText(\n title,\n titleLength\n )}</a></div>`;\n }\n if (enableDesc) {\n html += `<div class=\"ultp-ytg-description\">${truncateText(\n description,\n descriptionLength\n )}</div>`;\n }\n html += \"</div>\";\n return html;\n }\n\n // Initialize YouTube Gallery\n $(\".wp-block-ultimate-post-youtube-gallery\").each(function () {\n const $block = $(this);\n const $wrapper = $block.find(\".ultp-block-wrapper\");\n let $container = $block.find(\".ultp-ytg-view-grid, .ultp-ytg-container\");\n const $loadMoreBtn = $block.find(\".ultp-ytg-loadmore-btn\");\n\n // Get configuration from data attributes\n const config = {\n playlistIdOrUrl: $block.data(\"playlist\") || \"\",\n apiKey: $block.data(\"api-key\") || \"\",\n cacheDuration: parseInt($block.data(\"cache-duration\")) || 0,\n sortBy: $block.data(\"sort-by\") || \"date\",\n galleryLayout: $block.data(\"gallery-layout\") || \"grid\",\n videosPerPage: parseMaybeJSON($block.data(\"videos-per-page\"), {\n lg: 9,\n md: 6,\n sm: 3,\n }),\n showVideoTitle: $block.data(\"show-video-title\") == \"1\",\n videoTitleLength: parseMaybeJSON($block.data(\"video-title-length\"), {\n lg: 50,\n md: 50,\n sm: 50,\n }),\n loadMoreEnable: $block.data(\"load-more-enable\") == \"1\",\n moreButtonLabel: $block.data(\"more-button-label\") || \"More Videos\",\n autoplay: $block.data(\"autoplay\") == \"1\",\n loop: $block.data(\"loop\") == \"1\",\n mute: $block.data(\"mute\") == \"1\",\n showPlayerControl: $block.data(\"show-player-control\") == \"1\",\n hideYoutubeLogo: $block.data(\"hide-youtube-logo\") == \"1\",\n showDescription: $block.data(\"show-description\") == \"1\",\n videoDescriptionLength: parseMaybeJSON(\n $block.data(\"video-description-length\"),\n {\n lg: 100,\n md: 100,\n sm: 100,\n }\n ),\n imageHeightRatio: $block.data(\"image-height-ratio\") || \"16-9\",\n galleryColumn: parseMaybeJSON($block.data(\"gallery-column\"), {\n lg: 3,\n md: 2,\n sm: 1,\n }),\n displayType: $block.data(\"display-type\") || \"grid\",\n enableListView: $block.data(\"enable-list-view\") == \"1\",\n enableIconAnimation: $block.data(\"enable-icon-animation\") == \"1\",\n defaultYoutubeIcon: $block.data(\"enable-youtube-icon\") == \"1\",\n imgHeight: $block.data(\"img-height\"),\n };\n\n let playlistId = getPlaylistId(config.playlistIdOrUrl);\n\n // If it's a handle (starts with @), resolve to channel ID\n if (playlistId.startsWith(\"@\")) {\n const handle = playlistId.substring(1);\n resolveHandleToChannelId(handle, config.apiKey, function (channelId) {\n if (channelId) {\n playlistId = channelId;\n proceedWithPlaylist(playlistId);\n } else {\n $wrapper.html(\n '<p style=\"color:#888\">Invalid handle or API key.</p>'\n );\n }\n });\n } else {\n proceedWithPlaylist(playlistId);\n }\n\n function proceedWithPlaylist(playlistId) {\n // Convert channel ID to uploads playlist ID\n if (playlistId.startsWith(\"UC\")) {\n playlistId = \"UU\" + playlistId.substring(2);\n }\n\n if (!playlistId || !config.apiKey) {\n $wrapper.html(\n '<p style=\"color:#888\">Please provide both YouTube playlist ID/URL and API key.</p>'\n );\n return;\n }\n\n // State management\n let allVideos = [];\n let shownCount = config.videosPerPage.lg || 9;\n let activeVideo = null;\n let playingId = null;\n\n // Update responsive counts\n function updateResponsiveCounts() {\n const width = $(window).width();\n if (width < 600) {\n shownCount = Math.max(shownCount, config.videosPerPage.sm || 3);\n } else if (width < 900) {\n shownCount = Math.max(shownCount, config.videosPerPage.md || 6);\n } else {\n shownCount = Math.max(shownCount, config.videosPerPage.lg || 9);\n }\n }\n\n // Render videos for playlist layout\n function renderPlaylistLayout(videos) {\n if (!videos.length) {\n $container.html(\"<p>No videos found in this playlist.</p>\");\n return;\n }\n\n if (!activeVideo) {\n activeVideo = videos[0];\n }\n\n let html = '<div class=\"ultp-ytg-main\">';\n // Generate player iframe and place .ultp-ytg-content inside .ultp-ytg-video-wrapper\n const playerWrapper = `\n <div class=\"ultp-ytg-video-wrapper\">\n <iframe \n src=\"https://www.youtube.com/embed/${activeVideo.videoId}
1?${[\n `autoplay=${config.autoplay ? \"1\" : \"0\"}`,\n `loop=${config.loop ? \"1\" : \"0\"}`,\n `mute=${config.mute ? \"1\" : \"0\"}`,\n `controls=${config.showPlayerControl ? \"1\" : \"0\"}`,\n `modestbranding=${config.hideYoutubeLogo ? \"1\" : \"0\"}`,\n config.loop ? `playlist=${activeVideo.videoId}` : null,\n ]\n .filter(Boolean)\n .join(\"&\")}\"\n title=\"YouTube Video\"\n frameborder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowfullscreen\n ></iframe>\n ${getYoutubeTextContent(\n config.showVideoTitle,\n activeVideo.title,\n config.videoTitleLength,\n config.showDescription,\n activeVideo.description,\n config.videoDescriptionLength,\n activeVideo.videoId\n )}\n </div>\n `;\n html += playerWrapper;\n html += \"</div>\";\n\n html += '<div class=\"ultp-ytg-playlist-sidebar\">';\n html += '<div class=\"ultp-ytg-playlist-items\">';\n videos.forEach(function (video) {\n const isActive = video.videoId === activeVideo.videoId;\n html += `\n <div class=\"ultp-ytg-playlist-item ${\n isActive ? \"active\" : \"\"\n }\" data-video-id=\"${video.videoId}\">\n <img src=\"${video.thumbnail}\" alt=\"${video.title}\" loading=\"lazy\" />\n <div class=\"ultp-ytg-playlist-item-content\">\n <div class=\"ultp-ytg-playlist-item-title\">\n ${truncateText(video.title, config.videoTitleLength)}\n </div>\n </div>\n </div>\n `;\n });\n html += \"</div></div>\";\n\n $container.html(html);\n }\n\n // Render videos for grid/list layout\n function renderGridLayout(videos, count) {\n if (!videos.length) {\n $container.html(\"<p>No videos found in this playlist.</p>\");\n return;\n }\n\n const displayedVideos = videos.slice(0, count);\n let html = \"\";\n displayedVideos.forEach(function (video) {\n const isPlaying = playingId === video.videoId;\n html += `<div class=\"ultp-ytg-item${isPlaying ? \" active\" : \"\"}\">`;\n html += `<div class=\"ultp-ytg-video\">`;\n\n if (isPlaying) {\n html += generatePlayerIframe(video.videoId, config);\n } else {\n // enableIconAnimation\n const getSvgIcon = $(\".ultp-ytg-play__icon\").html();\n html += `\n <img src=\"${video.thumbnail}\" alt=\"${\n video.title\n }\" loading=\"lazy\" data-video-id=\"${\n video.videoId\n }\" style=\"cursor:pointer;\" />\n <div class=\"ultp-ytg-play__icon${\n config.enableIconAnimation ? \" ytg-icon-animation\" : \"\"\n }\">\n ${getSvgIcon}\n </div>\n `;\n }\n\n html += `</div>`;\n html += `<div class=\"ultp-ytg-inside\">`;\n html += getYoutubeTextContent(\n config.showVideoTitle,\n video.title,\n config.videoTitleLength,\n config.showDescription,\n video.description,\n config.videoDescriptionLength,\n video.videoId\n );\n html += `</div></div>`;\n });\n\n $container.html(html);\n\n // Update load more button visibility\n if (config.loadMoreEnable && count < videos.length) {\n $loadMoreBtn.show();\n } else {\n $loadMoreBtn.hide();\n }\n }\n\n // Render based on layout type\n function renderVideos(videos, count) {\n if (config.galleryLayout === \"playlist\") {\n renderPlaylistLayout(videos);\n } else {\n renderGridLayout(videos, count);\n }\n }\n\n // Fetch videos from YouTube API\n const cacheKey = `ultp_youtube_gallery_${playlistId}_${config.apiKey}_${config.sortBy}_${config.imgHeight}`;\n const duration = config.cacheDuration;\n let cached = null;\n\n try {\n cached = JSON.parse(localStorage.getItem(cacheKey));\n } catch (e) {\n cached = null;\n }\n\n const now = Date.now();\n if (\n cached &&\n cached.data &&\n cached.timestamp &&\n duration > 0 &&\n now - cached.timestamp < duration * 1000\n ) {\n allVideos = sortVideos(cached.data, config.sortBy);\n renderVideos(allVideos, shownCount);\n } else {\n // Only show loading skeleton for grid/list layouts, not for playlist layout\n if (config.galleryLayout !== \"playlist\") {\n $container.html(`\n <div class=\"ultp-ytg-loading gallery-postx gallery-active\">\n <div class=\"skeleton-box\"></div>\n <div class=\"skeleton-box\"></div>\n <div class=\"skeleton-box\"></div>\n <div class=\"skeleton-box\"></div>\n <div class=\"skeleton-box\"></div>\n <div class=\"skeleton-box\"></div>\n </div>\n `);\n } else {\n $container.html(`\n <div class=\"ultp-ytg-loading ultp-ytg-playlist-loading\">
1\n <div class=\"ytg-loader\"></div>\n </div>`);\n }\n\n $.get(\"https://www.googleapis.com/youtube/v3/playlistItems\", {\n part: \"snippet\",\n maxResults: 50,\n playlistId: playlistId,\n key: config.apiKey,\n })\n .done(function (data) {\n setTimeout(function () {\n $container.empty(); // Remove loading after 3s\n if (data.error) {\n $container.html(\n `<div class=\"ultp-ytg-error\">${\n data.error.message || \"Failed to fetch playlist.\"\n }</div>`\n );\n return;\n }\n\n const videos = (data.items || [])\n .filter(function (item) {\n return (\n item.snippet.title !== \"Private video\" &&\n item.snippet.title !== \"Deleted video\"\n );\n })\n .map(function (item) {\n return {\n videoId: item.snippet.resourceId.videoId,\n title: item.snippet.title,\n thumbnail:\n (item.snippet.thumbnails &&\n item.snippet.thumbnails[config.imgHeight] &&\n item.snippet.thumbnails[config.imgHeight].url) ||\n item.snippet.thumbnails[config.imgHeight].url ||\n item.snippet.thumbnails?.medium?.url ||\n \"\",\n publishedAt: item.snippet.publishedAt || \"\",\n description: item.snippet.description || \"\",\n viewCount: 0, // Initialize for popular sorting\n };\n }); // If sorting by popular, fetch view counts\n if (config.sortBy === \"popular\") {\n const videoIds = videos.map((v) => v.videoId).join(\",\");\n $.get(\n `https://www.googleapis.com/youtube/v3/videos?part=statistics&id=${videoIds}&key=${config.apiKey}`\n )\n .done(function (statsData) {\n if (statsData.items) {\n const statsMap = {};\n statsData.items.forEach(function (item) {\n statsMap[item.id] = item.statistics.viewCount;\n });\n videos.forEach(function (v) {\n v.viewCount = parseInt(statsMap[v.videoId] || 0);\n });\n }\n allVideos = sortVideos(videos, config.sortBy);\n if (duration > 0) {\n try {\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n data: videos,\n timestamp: now,\n })\n );\n } catch (e) {\n console.warn(\"Failed to cache videos:\", e);\n }\n }\n renderVideos(allVideos, shownCount);\n })\n .fail(function () {\n console.warn(\n \"Failed to fetch video statistics for popular sorting.\"\n );\n allVideos = sortVideos(videos, config.sortBy);\n renderVideos(allVideos, shownCount);\n });\n } else {\n allVideos = sortVideos(videos, config.sortBy);\n if (duration > 0) {\n try {\n localStorage.setItem(\n cacheKey,\n JSON.stringify({\n data: videos,\n timestamp: now,\n })\n );\n } catch (e) {\n console.warn(\"Failed to cache videos:\", e);\n }\n }\n renderVideos(allVideos, shownCount);\n }\n }, 2000);\n })\n .fail(function () {\n setTimeout(function () {\n $container.empty();\n $container.html(\n '<div class=\"ultp-ytg-error\">Failed to fetch videos. Please try again.</div>'\n );\n }, 3000);\n });\n }\n\n // Event handlers\n $block.on(\"click\", \".ultp-ytg-playlist-item\", function () {\n const videoId = $(this).data(\"video-id\");\n if (!videoId) return;\n\n activeVideo = allVideos.find(function (v) {\n return v.videoId === videoId;\n });\n\n if (activeVideo) {\n renderPlaylistLayout(allVideos);\n }\n });\n // Make clicking the play icon also start the video with loader fallback (per item)\n $block.on(\"click\", \".ultp-ytg-play__icon\", function () {\n // Find the previous img[data-vi
1deo-id] sibling to get the videoId\n const $img = $(this).siblings(\"img[data-video-id]\");\n const videoId = $img.data(\"video-id\");\n if (!videoId) return;\n // Find the closest .ultp-ytg-item and its .ultp-ytg-video\n const $item = $(this).closest(\".ultp-ytg-item\");\n const $videoDiv = $item.find(\".ultp-ytg-video\");\n // Show loader only in this video area\n $videoDiv.html(\n '<div class=\"ultp-ytg-loading\"><div class=\"ytg-loader\"></div></div>'\n );\n // Add active class to this item, remove from others\n $item\n .addClass(\"active\")\n .siblings(\".ultp-ytg-item\")\n .removeClass(\"active\");\n setTimeout(function () {\n playingId = videoId;\n renderGridLayout(allVideos, shownCount);\n }, 1000);\n });\n\n $block.on(\"click\", \".ultp-ytg-video img[data-video-id]\", function () {\n const videoId = $(this).data(\"video-id\");\n if (!videoId) return;\n\n playingId = videoId;\n renderGridLayout(allVideos, shownCount);\n });\n\n $loadMoreBtn.on(\"click\", function () {\n shownCount += config.videosPerPage.lg;\n renderGridLayout(allVideos, shownCount);\n });\n // Responsive handling\n $(window).on(\"resize\", function () {\n if (allVideos.length) {\n updateResponsiveCounts();\n renderVideos(allVideos, shownCount);\n }\n });\n }\n });\n }\n})(jQuery);\n"; 2var mod_pagespeed_h9MJ$SlQTp = "(function(){const siteNavigation=document.getElementById('site-navigation');if(!siteNavigation){return;}const button=siteNavigation.getElementsByTagName('button')[0];if('undefined'===typeof button){return;}const menu=siteNavigation.getElementsByTagName('ul')[0];if('undefined'===typeof menu){button.style.display='none';return;}if(!menu.classList.contains('nav-menu')){menu.classList.add('nav-menu');}button.addEventListener('click',function(){siteNavigation.classList.toggle('toggled');if(button.getAttribute('aria-expanded')==='true'){button.setAttribute('aria-expanded','false');}else{button.setAttribute('aria-expanded','true');}});let textnode=document.createTextNode('\\u2716');let linkItem=document.createElement('a');linkItem.href='#';linkItem.classList.add('dismiss-menu');linkItem.appendChild(textnode);linkItem.addEventListener('click',function(event){event.preventDefault;siteNavigation.classList.toggle('toggled');if(button.getAttribute('aria-expanded')==='true'){button.setAttribute('aria-expanded','false');}else{button.setAttribute('aria-expanded','true');}});let listItem=document.createElement('li');listItem.classList.add('dismiss-menu-item');listItem.appendChild(linkItem);const mainMenu=document.getElementById('primary-menu');mainMenu.prepend(listItem);document.addEventListener('click',function(event){const isClickInside=siteNavigation.contains(event.target);if(!isClickInside){siteNavigation.classList.remove('toggled');button.setAttribute('aria-expanded','false');}});const links=menu.getElementsByTagName('a');const linksWithChildren=menu.querySelectorAll('.menu-item-has-children > a, .page_item_has_children > a');for(const link of links){link.addEventListener('focus',toggleFocus,true);link.addEventListener('blur',toggleFocus,true);}for(const link of linksWithChildren){link.addEventListener('touchstart',toggleFocus,false);link.addEventListener('click',toggleFocus,false);}function toggleFocus(){if(event.type==='touchstart'||event.type==='click'){const menuItem=this.parentNode;if(siteNavigation.classList.contains('toggled')&&(!menuItem.classList.contains('focus')||this.getAttribute('href')==='#')){event.preventDefault();for(const link of menuItem.parentNode.children){if(menuItem!==link){link.classList.remove('focus');}}menuItem.classList.toggle('focus');}}}}());";
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.