MediaWiki:Gadget-embeddedYouTubePlayer.js

From Angelina Jordan Wiki

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() {
function insertYouTubePlayers() {
    var placeholders = document.querySelectorAll('.youtube-player-placeholder');
    var videoLists = [];
    var currentVideoIndices = [];
    var checkIntervals = [];

    // --- Load YouTube API if needed ---
    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);
    }

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

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

        links.forEach(l => parseUrl(l.getAttribute('href')));

        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) {
                        parseUrl(link.getAttribute('href'));
                    });
                }
            });
        }

        if (videoDataList.length === 0) {
            var dataVideos = placeholder.getAttribute('data-videos');
            if (!dataVideos) return [];
            dataVideos.split(',').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, start, end });
            });
        }
        return videoDataList;
    }

    // --- Safe observer setup ---
    function observeOnce(el) {
        if (!window._ytObserved) window._ytObserved = new WeakSet();
        if (!window._ytObserver) return;
        if (window._ytObserved.has(el)) return;
        window._ytObserved.add(el);
        window._ytObserver.observe(el);
    }

    // --- Player creation ---
    function ensurePlayerForIndex(index) {
        var placeholder = placeholders[index];
        if (!placeholder || placeholder.dataset.ytLoaded) return;
        placeholder.dataset.ytLoaded = 'true';

        // Defensive cleanup
        if (window['youtube-player-' + index]) return;

        var videoDataList = parseVideosFromPlaceholder(placeholder);
        if (!videoDataList.length) return;

        videoLists[index] = videoDataList;
        currentVideoIndices[index] = 0;
        checkIntervals[index] = null;

        var playerDiv = document.createElement('div');
        playerDiv.id = 'youtube-player-' + index;
        placeholder.appendChild(playerDiv);

        var 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);

        // Create player
        window['youtube-player-' + index] = new YT.Player('youtube-player-' + index, {
            height: '270',
            width: '480',
            videoId: videoLists[index][0].videoId,
            playerVars: { start: videoLists[index][0].start },
            events: {
                'onStateChange': onPlayerStateChange(index),
                'onReady': onPlayerReady(index)
            }
        });

        // Deduplicate and re-bind link handlers
        placeholder.querySelectorAll('a[href]').forEach(function(a) {
            var clone = a.cloneNode(true);
            a.parentNode.replaceChild(clone, a);
            var href = clone.getAttribute('href');
            var match = href.match(/\/\/(www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)([^\s]*)/);
            if (!match) return;
            var videoId = match[2];
            var params = new URLSearchParams(match[3]);
            var start = params.get('t') ? parseInt(params.get('t')) : 0;
            var end = params.get('end') ? parseInt(params.get('end')) : null;
            clone.addEventListener('click', function(event) {
                event.preventDefault();
                var vIndex = videoLists[index].findIndex(v =>
                    v.videoId === videoId && v.start === start && v.end === end
                );
                if (vIndex !== -1) {
                    currentVideoIndices[index] = vIndex;
                    window['youtube-player-' + index].loadVideoById({
                        videoId,
                        startSeconds: start
                    });
                }
            });
        });
    }

    // --- Player ready and state change ---
    function onPlayerReady(index) {
        return function() {
            var p = placeholders[index];
            if (!p) return;
            var t = p.querySelector('.youtube-placeholder-text');
            if (t) t.style.display = '';
        };
    }

    function onPlayerStateChange(index) {
        return function(event) {
            clearInterval(checkIntervals[index]);
            if (event.data === YT.PlayerState.PLAYING) {
                for (var i = 0; i < placeholders.length; i++) {
                    if (i !== index && window['youtube-player-' + i]) {
                        if (window['youtube-player-' + i].getPlayerState() === YT.PlayerState.PLAYING) {
                            window['youtube-player-' + i].pauseVideo();
                        }
                    }
                }
                var end = videoLists[index][currentVideoIndices[index]].end;
                if (end) {
                    checkIntervals[index] = setInterval(function() {
                        var t = window['youtube-player-' + index].getCurrentTime();
                        if (t >= end) playVideo(index, 1);
                    }, 1000);
                }
            } else if (event.data === YT.PlayerState.ENDED) {
                playVideo(index, 1);
            }
        };
    }

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

    document.addEventListener('click', function(e) {
        if (e.target.classList.contains('next-button')) playVideo(e.target.dataset.index, 1);
        else if (e.target.classList.contains('prev-button')) playVideo(e.target.dataset.index, -1);
    });

    // --- Global observer setup ---
    window._ytObserver = new IntersectionObserver(function(entries) {
        entries.forEach(function(entry) {
            if (entry.isIntersecting) {
                var i = parseInt(entry.target.dataset.index, 10);
                ensurePlayerForIndex(i);
            }
        });
    });

    // --- Initialization after API load ---
    window.onYouTubeIframeAPIReady = function() {
        placeholders.forEach(function(p, i) {
            p.dataset.index = i;
            observeOnce(p);
        });
        window.refreshYouTubePlayers = function() {
            document.querySelectorAll('.youtube-player-placeholder').forEach(function(p) {
                var idx = parseInt(p.dataset.index, 10);
                if (isNaN(idx)) return;
                var newList = parseVideosFromPlaceholder(p);
                if (newList.length) videoLists[idx] = newList;
                currentVideoIndices[idx] = 0;
                delete p.dataset.ytLoaded;
                observeOnce(p);
                var rect = p.getBoundingClientRect();
                var inViewport = rect.bottom > 0 && rect.top < (window.innerHeight || document.documentElement.clientHeight);
                if (inViewport) ensurePlayerForIndex(idx);
            });
        };
    };
}

insertYouTubePlayers();
})();