MediaWiki:Gadget-embeddedYouTubePlayer.js

From Angelina Jordan Wiki
Revision as of 12:47, 8 November 2025 by Most2dot0 (talk | contribs) (bugfix)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// Embedded YouTube player implementation 
//
// Use like this, where "&t=" designates an optional start, and "&end=" an optional stop time:
//
// <div class="youtube-player-placeholder" data-videos="VIDEO_ID_1&t=45&end=135,VIDEO_ID_2,VIDEO_ID_3&end=20"></div>
//
// It can also be configured by enclosing text that contains YouTube video urls:
//
// <div class="youtube-player-placeholder" data-include-refs="true">
//   <ul>
//     <li><a url="https://www.youtube.com/watch?v=VIDEO_ID_1&t=45&end=135">Titel 1</a></li>
//     <li><a url="https://www.youtube.com/watch?v=VIDEO_ID_2">Titel 2</a></li>
//     <li><a url="https://www.youtube.com/watch?v=VIDEO_ID_3&end=20"></a></li>
//   </ul>
// </div>
//
// The data-include-refs="true" is opional; if it is set, YouTube videos in the references will also be included in the playlist. 
// But they can not directly choosen, as it is the case with the other YouTube URLS (click on the (Watch on) YouTube Button in 
// in the player if you want to go to watch on YouTube)
//
// There can be multiple players on a page, but only one will play at a time, the others will get stopped automatically

function insertYouTubePlayers() {
    var placeholders = document.querySelectorAll('.youtube-player-placeholder');

    // Load the YouTube IFrame API if not already loaded
    if (!document.getElementById('youtube-iframe-api')) {
        var tag = document.createElement('script');
        tag.src = "https://www.youtube.com/iframe_api";
        tag.id = 'youtube-iframe-api';
        var firstScriptTag = document.getElementsByTagName('script')[0];
        firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    }

	// Persistent IntersectionObserver for lazy-loading players
	if (!window._ytObserver) {
	    window._ytObserver = new IntersectionObserver(function(entries) {
	        entries.forEach(function(entry) {
	            if (entry.isIntersecting && !entry.target.dataset.loaded) {
	                entry.target.dataset.loaded = "true";
	                var index = entry.target.dataset.index;
	                if (index !== undefined) loadYouTubePlayer(entry.target, index);
	            }
	        });
	    }, { rootMargin: '200px' });
	}
	
    var videoLists = [];
    var currentVideoIndices = [];
    var checkIntervals = [];

    function parseVideosFromPlaceholder(placeholder) {
        var videoDataList = [];
        var links = placeholder.querySelectorAll('a');
        var includeRefs = placeholder.getAttribute('data-include-refs') === 'true';

        // Function to parse URLs
        function parseUrl(href) {
            var match = href.match(/\/\/(www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)([^\s]*)/);
            if (match) {
                var videoId = match[2];
                var urlParams = match[3];
                var params = new URLSearchParams(urlParams);
                var start = params.get('t') ? parseInt(params.get('t')) : 0;
                var end = params.get('end') ? parseInt(params.get('end')) : null;
                videoDataList.push({ videoId: videoId, start: start, end: end });
            }
        }

        // Parse URLs in normal links
        links.forEach(function(link) {
            var href = link.getAttribute('href');
            parseUrl(href);
        });

        // Parse URLs in <ref> tags if includeRefs is true
        if (includeRefs) {
            var refSupTags = placeholder.querySelectorAll('sup a[href^="#"]');
            refSupTags.forEach(function(supTag) {
                var refId = supTag.getAttribute('href').substring(1);
                var refListItem = document.getElementById(refId);
                if (refListItem) {
                    var refLinks = refListItem.querySelectorAll('a');
                    refLinks.forEach(function(link) {
                        var href = link.getAttribute('href');
                        parseUrl(href);
                    });
                }
            });
        }

        // If no URLs found, fall back to data-videos attribute
        if (videoDataList.length === 0) {
            var dataVideos = placeholder.getAttribute('data-videos').split(',');
            dataVideos.forEach(function(videoData) {
                var parts = videoData.split('&');
                var videoId = parts[0];
                var start = 0;
                var end = null;
                parts.forEach(function(part) {
                    if (part.startsWith('t=')) {
                        start = parseInt(part.substring(2)) || 0;
                    } else if (part.startsWith('end=')) {
                        end = parseInt(part.substring(4)) || null;
                    }
                });
                videoDataList.push({ videoId: videoId, start: start, end: end });
            });
        }

        return videoDataList;
    }

    function onYouTubeIframeAPIReady() {
		placeholders.forEach(function(placeholder, index) {
		    placeholder.setAttribute('data-index', index);
		    window._ytObserver.observe(placeholder);
		});
    }

    window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;

	function loadYouTubePlayer(placeholder, index) {
	    // Parse video list from placeholder
	    var videoDataList = parseVideosFromPlaceholder(placeholder);
	    if (!videoDataList || videoDataList.length === 0) return;
	
	    // Store/update video list and reset current index and interval
	    videoLists[index] = videoDataList;
	    currentVideoIndices[index] = 0;
	    checkIntervals[index] = null;
	
	    // Create player container if missing
	    var playerDiv = placeholder.querySelector('#youtube-player-' + index);
	    if (!playerDiv) {
	        playerDiv = document.createElement('div');
	        playerDiv.id = 'youtube-player-' + index;
	        placeholder.appendChild(playerDiv);
	    }
	
	    // Only create a new YT.Player if it doesn't exist yet
	    if (!window['youtube-player-' + index]) {
	        window['youtube-player-' + index] = new YT.Player(playerDiv.id, {
	            height: '270',
	            width: '480',
	            videoId: videoLists[index][currentVideoIndices[index]].videoId,
	            playerVars: {
	                start: videoLists[index][currentVideoIndices[index]].start
	            },
	            events: {
	                'onStateChange': onPlayerStateChange(index),
	                'onReady': onPlayerReady(index)
	            }
	        });
	    } else {
	        // If player exists, reload first video from updated list
	        window['youtube-player-' + index].loadVideoById({
	            videoId: videoLists[index][0].videoId,
	            startSeconds: videoLists[index][0].start
	        });
	    }
	
	    // Create controls div only if it doesn't already exist
	    var controlsDiv = placeholder.querySelector('.youtube-player-controls');
	    if (!controlsDiv) {
	        controlsDiv = document.createElement('div');
	        controlsDiv.className = 'youtube-player-controls';
	        controlsDiv.innerHTML = '<button class="prev-button" data-index="' + index + '">Previous</button>' +
	                                '<button class="next-button" data-index="' + index + '">Next</button>';
	        placeholder.appendChild(controlsDiv);
	    }
	
	    // Bind click handlers for all <a> links in placeholder (rebind after refresh)
	    var links = placeholder.querySelectorAll('a');
	    links.forEach(function(link) {
	        var href = link.getAttribute('href');
	        var match = href && href.match(/\/\/(www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)([^\s]*)/);
	        if (match) {
	            var videoId = match[2];
	            var urlParams = match[3];
	            var params = new URLSearchParams(urlParams);
	            var start = params.get('t') ? parseInt(params.get('t')) : 0;
	            var end = params.get('end') ? parseInt(params.get('end')) : null;
	
	            // Remove any previous click handlers to avoid duplicates
	            link.replaceWith(link.cloneNode(true));
	            link = placeholder.querySelector('a[href="' + href + '"]');
	
	            link.addEventListener('click', function(event) {
	                event.preventDefault();
	
	                var videoIndex = videoLists[index].findIndex(function(video) {
	                    return video.videoId === videoId &&
	                           video.start === start &&
	                           video.end === end;
	                });
	
	                if (videoIndex !== -1) {
	                    currentVideoIndices[index] = videoIndex;
	                    window['youtube-player-' + index].loadVideoById({
	                        videoId: videoLists[index][currentVideoIndices[index]].videoId,
	                        startSeconds: videoLists[index][currentVideoIndices[index]].start
	                    });
	                }
	            });
	        }
	    });
	}

    function onPlayerReady(index) {
        return function(event) {
            var placeholder = placeholders[index];
            var originalHTML = placeholder.querySelector('.youtube-placeholder-text');
            originalHTML.style.display = ''; // Ensure the original text is displayed correctly
        };
    }

    function onPlayerStateChange(index) {
        return function(event) {
            clearInterval(checkIntervals[index]);
            if (event.data == YT.PlayerState.PLAYING) {
                // Pause all other players except the current one
                for (var i = 0; i < placeholders.length; i++) {
                    if (i !== parseInt(index) && window['youtube-player-' + i] && window['youtube-player-' + i].getPlayerState() === YT.PlayerState.PLAYING) {
                        window['youtube-player-' + i].pauseVideo();
                    }
                }

                if (videoLists[index][currentVideoIndices[index]].end) {
                    checkIntervals[index] = setInterval(function() {
                        var currentTime = window['youtube-player-' + index].getCurrentTime();
                        if (currentTime >= videoLists[index][currentVideoIndices[index]].end) {
                            playVideo(index, 1); // Play next video
                        }
                    }, 1000); // Check every second
                }
            } else if (event.data == YT.PlayerState.ENDED) {
                playVideo(index, 1); // Play next video
            }
        };
    }

    function playVideo(index, direction) {
        clearInterval(checkIntervals[index]);
        currentVideoIndices[index] += direction;
        if (currentVideoIndices[index] >= videoLists[index].length) {
            currentVideoIndices[index] = 0;
        } else if (currentVideoIndices[index] < 0) {
            currentVideoIndices[index] = videoLists[index].length - 1;
        }
        window['youtube-player-' + index].loadVideoById({
            videoId: videoLists[index][currentVideoIndices[index]].videoId,
            startSeconds: videoLists[index][currentVideoIndices[index]].start
        });
    }

    document.addEventListener('click', function(event) {
        if (event.target.classList.contains('next-button')) {
            var index = event.target.getAttribute('data-index');
            playVideo(index, 1); // Next video
        } else if (event.target.classList.contains('prev-button')) {
            var index = event.target.getAttribute('data-index');
            playVideo(index, -1); // Previous video
        }
    });

	// to be called after AJAX updates
	window.refreshYouTubePlayers = function() {
	    var placeholders = document.querySelectorAll('.youtube-player-placeholder');
	    placeholders.forEach(function(placeholder, index) {
	        if (!placeholder.dataset.index) placeholder.dataset.index = index;
	
	        // Reset loaded flag so observer can trigger again
	        delete placeholder.dataset.loaded;
	
	        // Re-parse video list from updated content
	        var videoDataList = parseVideosFromPlaceholder(placeholder);
	        if (videoDataList.length) videoLists[index] = videoDataList;
	        currentVideoIndices[index] = 0;
	
	        // Re-observe for lazy loading
	        window._ytObserver.observe(placeholder);
	
	        // If currently visible, force initialization immediately
	        var rect = placeholder.getBoundingClientRect();
	        var isVisible = rect.bottom > 0 && rect.top < (window.innerHeight || document.documentElement.clientHeight);
	        if (isVisible) {
	            loadYouTubePlayer(placeholder, index);
	        }
	    });
	};
	
}

insertYouTubePlayers();