MediaWiki:Gadget-embeddedYouTubePlayer.js

From Angelina Jordan Wiki
Revision as of 12:57, 8 November 2025 by Most2dot0 (talk | contribs) (Another complete rewrite)

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() {
    // Shared state
    var placeholders = [];
    var videoLists = [];            // array of arrays {videoId,start,end}
    var players = [];               // YT player instances by index
    var currentVideoIndices = [];
    var checkIntervals = [];

    // IntersectionObserver for lazy loading
    if (!window._ytObserver) {
        window._ytObserver = new IntersectionObserver(function(entries) {
            entries.forEach(function(entry) {
                if (entry.isIntersecting && !entry.target.dataset.ytLoaded) {
                    var idx = parseInt(entry.target.dataset.index, 10);
                    if (!isNaN(idx)) {
                        entry.target.dataset.ytLoaded = "1";
                        ensurePlayerForIndex(idx);
                    }
                }
            });
        }, { rootMargin: "200px" });
    }

    // Utility: parse YouTube URLs to video data
    function parseUrlToVideoData(href) {
        if (!href) return null;
        try {
            if (href.indexOf('//') === 0) href = 'https:' + href;
            var url = new URL(href, window.location.href);
            var vid = null, start = 0, end = null;

            if (url.hostname.match(/(^|\.)youtube\.com$/)) {
                vid = url.searchParams.get('v') || (url.pathname.startsWith('/embed/') ? url.pathname.split('/embed/')[1] : null);
                start = parseInt(url.searchParams.get('t')) || 0;
                end = url.searchParams.get('end') ? parseInt(url.searchParams.get('end')) : null;
            } else if (url.hostname === 'youtu.be') {
                vid = url.pathname.substring(1);
                start = parseInt(url.searchParams.get('t')) || 0;
                end = url.searchParams.get('end') ? parseInt(url.searchParams.get('end')) : null;
            }

            if (vid) return { videoId: vid, start: start, end: end };
        } catch (e) {}
        return null;
    }

    // Parse videos from a placeholder
    function parseVideosFromPlaceholder(placeholder) {
        var list = [];

        // Links inside placeholder
        placeholder.querySelectorAll('a[href]').forEach(function(a) {
            var parsed = parseUrlToVideoData(a.getAttribute('href'));
            if (parsed) list.push(parsed);
        });

        // Include refs in <sup> if configured
        if (placeholder.getAttribute('data-include-refs') === 'true') {
            placeholder.querySelectorAll('sup a[href^="#"]').forEach(function(sup) {
                var refId = sup.getAttribute('href').substring(1);
                var el = document.getElementById(refId);
                if (el) {
                    el.querySelectorAll('a[href]').forEach(function(a) {
                        var parsed = parseUrlToVideoData(a.getAttribute('href'));
                        if (parsed) list.push(parsed);
                    });
                }
            });
        }

        // Fallback to data-videos attribute
        if (list.length === 0) {
            var dataVideos = placeholder.getAttribute('data-videos') || '';
            if (dataVideos.trim()) {
                dataVideos.split(',').forEach(function(item) {
                    var parts = item.split('&');
                    var vid = parts[0].trim();
                    if (!vid) return;
                    var start = 0, end = null;
                    parts.forEach(function(p) {
                        if (p.indexOf('t=') === 0) start = parseInt(p.substring(2)) || 0;
                        if (p.indexOf('end=') === 0) end = parseInt(p.substring(4)) || null;
                    });
                    list.push({ videoId: vid, start: start, end: end });
                });
            }
        }

        return list;
    }

    // YT API readiness helper
    var ytReady = !!(window.YT && window.YT.Player);
    var ytReadyQueue = [];
    function whenYTReady(fn) {
        if (ytReady) return fn();
        ytReadyQueue.push(fn);
        if (!window._youtube_api_loader_started) {
            var tag = document.createElement('script');
            tag.src = "https://www.youtube.com/iframe_api";
            tag.id = 'youtube-iframe-api';
            document.head.appendChild(tag);
            window._youtube_api_loader_started = true;
        }
        if (!window._onYouTubeIframeAPIReady_wrapped) {
            window.onYouTubeIframeAPIReady = function() {
                ytReady = true;
                ytReadyQueue.forEach(function(cb){ try { cb(); } catch(e){} });
                ytReadyQueue = [];
            };
            window._onYouTubeIframeAPIReady_wrapped = true;
        }
    }

    // Create or update a player for a placeholder index
    function ensurePlayerForIndex(index) {
        var placeholder = placeholders[index];
        if (!placeholder) return;

        // Build default list if missing
        if (!videoLists[index] || !videoLists[index].length) {
            videoLists[index] = parseVideosFromPlaceholder(placeholder);
        }
        if (!videoLists[index] || !videoLists[index].length) return;

        currentVideoIndices[index] = currentVideoIndices[index] || 0;
        checkIntervals[index] = null;

        // Create container if missing
        var playerDiv = placeholder.querySelector('.youtube-player-container');
        if (!playerDiv) {
            playerDiv = document.createElement('div');
            playerDiv.className = 'youtube-player-container';
            playerDiv.id = 'youtube-player-' + index;
            placeholder.appendChild(playerDiv);
        }

        // Only create player if missing
        whenYTReady(function() {
            if (!players[index]) {
                players[index] = new YT.Player(playerDiv.id, {
                    height: '270',
                    width: '480',
                    videoId: videoLists[index][currentVideoIndices[index]].videoId,
                    playerVars: { start: videoLists[index][currentVideoIndices[index]].start || 0, rel: 0, modestbranding: 1 },
                    events: { onReady: function(){}, onStateChange: createOnStateChange(index) }
                });
            } else {
                // Reload first video if player exists
                players[index].loadVideoById({
                    videoId: videoLists[index][0].videoId,
                    startSeconds: videoLists[index][0].start || 0
                });
            }

            // Create prev/next controls only once
            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 links
            placeholder.querySelectorAll('a[href]').forEach(function(link){
                var href = link.getAttribute('href');
                var parsed = parseUrlToVideoData(href);
                if (!parsed) return;

                // Remove previous handlers by cloning
                var newLink = link.cloneNode(true);
                link.replaceWith(newLink);
                newLink.addEventListener('click', function(e){
                    e.preventDefault();
                    var videoIndex = videoLists[index].findIndex(function(v){
                        return v.videoId === parsed.videoId && v.start === parsed.start && v.end === parsed.end;
                    });
                    if (videoIndex !== -1) {
                        currentVideoIndices[index] = videoIndex;
                        players[index].loadVideoById({ videoId: parsed.videoId, startSeconds: parsed.start });
                    }
                });
            });
        });
    }

    function createOnStateChange(index) {
        return function(event) {
            clearInterval(checkIntervals[index]);
            if (event.data == YT.PlayerState.PLAYING) {
                // Pause other players
                for (var i=0; i<players.length; i++) {
                    if (i!==index && players[i] && typeof players[i].getPlayerState==='function') {
                        try { if(players[i].getPlayerState()===YT.PlayerState.PLAYING) players[i].pauseVideo(); } catch(e){}
                    }
                }
                var currentVideo = videoLists[index][currentVideoIndices[index]];
                if (currentVideo && currentVideo.end) {
                    checkIntervals[index] = setInterval(function(){
                        try{
                            if(players[index].getCurrentTime()>=currentVideo.end) playVideo(index,1);
                        } catch(e){}
                    },500);
                }
            } else if(event.data==YT.PlayerState.ENDED) playVideo(index,1);
        };
    }

    function playVideo(index,direction){
        clearInterval(checkIntervals[index]);
        if(!videoLists[index]||videoLists[index].length===0) return;
        currentVideoIndices[index] = (currentVideoIndices[index]||0)+direction;
        if(currentVideoIndices[index]>=videoLists[index].length) currentVideoIndices[index]=0;
        if(currentVideoIndices[index]<0) currentVideoIndices[index]=videoLists[index].length-1;

        var v = videoLists[index][currentVideoIndices[index]];
        if(!players[index]){
            ensurePlayerForIndex(index);
            whenYTReady(function(){ try{ players[index].loadVideoById({videoId:v.videoId,startSeconds:v.start||0}); }catch(e){} });
            return;
        }
        try{ players[index].loadVideoById({videoId:v.videoId,startSeconds:v.start||0}); }catch(e){}
    }

    // Initial scan and lazy init
    window.insertYouTubePlayers = function(){
        placeholders = Array.from(document.querySelectorAll('.youtube-player-placeholder'));
        placeholders.forEach(function(p,i){
            if(!p.dataset.index) p.dataset.index = i;
            videoLists[i] = parseVideosFromPlaceholder(p);
            currentVideoIndices[i]=0;
            window._ytObserver.observe(p);
        });

        // Initialize placeholders that are already visible
        placeholders.forEach(function(p,i){
            var rect = p.getBoundingClientRect();
            if(rect.bottom>0 && rect.top < (window.innerHeight || document.documentElement.clientHeight)){
                function tryLoad(){ if(window.YT && window.YT.Player){ ensurePlayerForIndex(i); } else setTimeout(tryLoad,100); }
                tryLoad();
            }
        });
    };

    // Refresh after AJAX updates
    window.refreshYouTubePlayers = function(){
        placeholders.forEach(function(p,i){
            delete p.dataset.ytLoaded;
            videoLists[i] = parseVideosFromPlaceholder(p);
            currentVideoIndices[i] = 0;
            window._ytObserver.observe(p);

            // Initialize if visible
            var rect = p.getBoundingClientRect();
            if(rect.bottom>0 && rect.top < (window.innerHeight || document.documentElement.clientHeight)){
                function tryLoad(){ if(window.YT && window.YT.Player){ ensurePlayerForIndex(i); } else setTimeout(tryLoad,100); }
                tryLoad();
            }
        });
    };

    // Global click handlers for prev/next buttons
    document.addEventListener('click',function(event){
        var idx = parseInt(event.target.dataset.index,10);
        if(event.target.classList.contains('next-button')) playVideo(idx,1);
        else if(event.target.classList.contains('prev-button')) playVideo(idx,-1);
    });

    // Start initialization
    window.insertYouTubePlayers();
})();