MediaWiki:Gadget-embeddedYouTubePlayer.js

From Angelina Jordan Wiki
Revision as of 11:49, 8 November 2025 by Most2dot0 (talk | contribs) (bugfixes)

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

    if (!window._ytObserver) {
      window._ytObserver = new IntersectionObserver(function(entries) {
        entries.forEach(function(entry) {
            if (entry.isIntersecting && !entry.target.dataset.ytLoaded) {
                entry.target.dataset.ytLoaded = "1";
                var idx = parseInt(entry.target.dataset.index, 10);
                if (!isNaN(idx)) ensurePlayerForIndex(idx);
            }
        });
      }, { rootMargin: "200px" });
    }
    // Utility: robust URL parser for youtube watch and youtu.be short links
    function parseUrlToVideoData(href) {
        if (!href) return null;
        try {
            // Normalize protocol-less URLs
            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$/)) {
                // watch?v=VIDEO_ID
                vid = url.searchParams.get('v');
                if (!vid && url.pathname.startsWith('/embed/')) {
                    vid = url.pathname.split('/embed/')[1];
                }
                // t and end may be present
                if (url.searchParams.get('t')) start = parseInt(url.searchParams.get('t')) || 0;
                if (url.searchParams.get('end')) end = parseInt(url.searchParams.get('end')) || null;
            } else if (url.hostname === 'youtu.be') {
                vid = url.pathname.substring(1);
                if (url.searchParams.get('t')) start = parseInt(url.searchParams.get('t')) || 0;
                if (url.searchParams.get('end')) end = parseInt(url.searchParams.get('end')) || null;
            }

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

    function parseVideosFromPlaceholder(placeholder) {
        var list = [];

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

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

        // fallback to data-videos attribute if nothing found
        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) {
            // load script once
            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;
        }
        // set global callback once
        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;

        // create container for player 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;
            // insert at top
            placeholder.insertBefore(playerDiv, placeholder.firstChild);
        }

        // if player exists return
        if (players[index]) return;

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

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

        // create the YouTube player once API is ready
        whenYTReady(function() {
            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() { /* no-op for now */ },
                    onStateChange: createOnStateChange(index)
                }
            });
        });
    }

    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) continue;
                    if (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 {
                            var t = players[index].getCurrentTime();
                            if (t >= 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);
            // when player becomes ready it will play first item. Try to load when ready.
            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) {}
    }

    // Public: initial scan and lazy init using IntersectionObserver
    window.insertYouTubePlayers = function() {
      document.querySelectorAll('.youtube-player-placeholder').forEach(function(p, i) {
        if (!p.dataset.index) p.dataset.index = i;
        if (!videoLists[i] || !videoLists[i].length) {
            videoLists[i] = parseVideosFromPlaceholder(p);
        }
        window._ytObserver.observe(p);
      });
    };

    // Public: refresh existing players after AJAX changes inner HTML of placeholders.
    // Call this after you update the placeholder content.
    window.refreshYouTubePlayers = function() {
      document.querySelectorAll('.youtube-player-placeholder').forEach(function(p) {
        var idx = parseInt(p.dataset.index, 10);
        if (isNaN(idx)) return;

        // update only if inner HTML changed
        var newList = parseVideosFromPlaceholder(p);
        if (newList.length) videoLists[idx] = newList;

        // ensure it’s being observed again
        window._ytObserver.observe(p);

        // reset flag so observer can re-trigger if element was replaced
        delete p.dataset.ytLoaded;
      });
};

    // initialize on script load
    // If you want to defer, you can remove this and call insertYouTubePlayers() manually.
    window.insertYouTubePlayers();
})();