MediaWiki:Gadget-floatingYouTubePlayer.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.
(function () {
    'use strict';

    var CORE_PLAYER_ID = 'core-youtube-player';
    var YT_SCRIPT_ID = 'youtube-iframe-api';
    var CONTENT_SELECTOR = '#mw-content-text';
    var HEADER_SELECTOR = '#firstHeading';

    // --------------------------
    // Utility Functions
    // --------------------------
    function parseTimeParam(val) {
        if (!val) return 0;
        if (/^\d+$/.test(val)) return parseInt(val, 10);
        var m = val.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
        if (m) return (parseInt(m[1]||0)*3600) + (parseInt(m[2]||0)*60) + parseInt(m[3]||0);
        var digits = val.replace(/\D/g,''); return digits ? parseInt(digits,10) : 0;
    }

    function parseYouTubeFromHref(href) {
        if (!href) return null;
        var idMatch = href.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{5,})/);
        if (!idMatch) return null;
        var videoId = idMatch[1];
        var urlObj;
        try { urlObj = new URL(href, location.href); } catch(e){ urlObj = { search: '' }; }
        var params = new URLSearchParams(urlObj.search);
        var start = parseTimeParam(params.get('t') || params.get('start'));
        var end = params.get('end') ? parseTimeParam(params.get('end')) : null;
        return { videoId: videoId, start: start, end: end };
    }

    // --------------------------
    // CorePlayer: persistent floating player
    // --------------------------
    var CorePlayer = (function(){
        var playlists=[], player=null, ytReady=false, active=null, interval=null;

        function ensureContainer(){
            var c=document.getElementById(CORE_PLAYER_ID);
            if(!c){
                c=document.createElement('div');
                c.id=CORE_PLAYER_ID;
                c.style.position='fixed';
                c.style.bottom='12px';
                c.style.right='12px';
                c.style.zIndex=9999;
                c.style.background='#000';
                c.style.padding='6px';
                c.style.borderRadius='6px';
                c.style.boxShadow='0 2px 8px rgba(0,0,0,0.35)';
                document.body.appendChild(c);
            }
            return c;
        }

        function createPlayer(video){
            var c=ensureContainer();
            if(player || !ytReady || !video) return;
            c.innerHTML='<div id="core-youtube-player-frame"></div>';
            player=new YT.Player('core-youtube-player-frame',{
                height:'270', width:'480',
                videoId: video.videoId,
                playerVars:{ autoplay:0, rel:0, start: video.start },
                events:{ onStateChange:onStateChange }
            });
        }

        function onStateChange(e){
            if(active===null) return;
            clearIntervalSafe();
            var pl=playlists[active], v=pl.videos[pl.current];
            if(e.data===YT.PlayerState.PLAYING && v.end!==null){
                interval=setInterval(function(){
                    try{ if(player.getCurrentTime()>=v.end) playNext(active); }
                    catch(e){ clearIntervalSafe(); }
                },500);
            } else if(e.data===YT.PlayerState.ENDED) playNext(active);
        }

        function clearIntervalSafe(){ if(interval){ clearInterval(interval); interval=null; } }

        function registerPlaylist(el,videos){
            var pid=playlists.length;
            playlists.push({el:el, videos:videos, current:0});
            el.setAttribute('data-core-playlist-id', pid);
            return pid;
        }

        function gotoVideo(pid, idx){
            var pl=playlists[pid]; if(!pl || idx<0 || idx>=pl.videos.length) return;
            pl.current=idx; active=pid; clearIntervalSafe();
            var v=pl.videos[idx];
            if(!ytReady){ loadYouTubeApi(); setTimeout(function(){ createPlayer(v); try{ player.loadVideoById({videoId:v.videoId,startSeconds:v.start}); }catch(e){} },300); return; }
            if(!player){ createPlayer(v); setTimeout(function(){ try{ player.loadVideoById({videoId:v.videoId,startSeconds:v.start}); }catch(e){} },200); return; }
            try{ player.loadVideoById({videoId:v.videoId,startSeconds:v.start }); } catch(e){ try{ player.cueVideoById({videoId:v.videoId,startSeconds:v.start}); } catch(e2){} }
        }

        function playNext(pid){ var pl=playlists[pid]; if(!pl) return; pl.current=(pl.current+1)%pl.videos.length; gotoVideo(pid,pl.current); }
        function playPrev(pid){ var pl=playlists[pid]; if(!pl) return; pl.current=(pl.current-1+pl.videos.length)%pl.videos.length; gotoVideo(pid,pl.current); }

        return { setYTReady:function(){ytReady=true;}, registerPlaylist:registerPlaylist, gotoVideo:gotoVideo, next:playNext, prev:playPrev };
    })();

    // --------------------------
    // YouTube API loader
    // --------------------------
    function loadYouTubeApi(){
        if(document.getElementById(YT_SCRIPT_ID)) return;
        var s=document.createElement('script'); s.id=YT_SCRIPT_ID; s.src='https://www.youtube.com/iframe_api';
        document.head.appendChild(s);
    }

    window.onYouTubeIframeAPIReady=function(){
        CorePlayer.setYTReady();
        var phs=document.querySelectorAll('.youtube-player-placeholder');
        for(var i=0;i<phs.length;i++){
            var vids=parseVideos(phs[i]);
            if(vids.length){ var pid=CorePlayer.registerPlaylist(phs[i],vids); CorePlayer.gotoVideo(pid,0); break; }
        }
    };

    // --------------------------
    // Placeholder parser
    // --------------------------
    function parseVideos(ph){
        var vids=[], links=ph.querySelectorAll('a'), includeRefs=ph.getAttribute('data-include-refs')==='true';
        function addHref(h){ var info=parseYouTubeFromHref(h); if(info) vids.push({videoId:info.videoId,start:info.start,end:info.end}); }
        links.forEach(l=>addHref(l.getAttribute('href')||l.getAttribute('url')||''));
        if(includeRefs){ ph.querySelectorAll('sup a[href^="#"]').forEach(sup=>{
            var id=sup.getAttribute('href').substring(1), el=document.getElementById(id);
            if(el) el.querySelectorAll('a').forEach(l=>addHref(l.getAttribute('href')||'')); }); }
        if(!vids.length && ph.getAttribute('data-videos')){ ph.getAttribute('data-videos').split(',').forEach(v=>{
            var parts=v.split('&'), vid=parts[0].trim(), start=0, end=null;
            parts.forEach(p=>{p=p.trim(); if(p.startsWith('t=')) start=parseTimeParam(p.substring(2)); else if(p.startsWith('end=')) end=parseTimeParam(p.substring(4)); });
            if(vid) vids.push({videoId:vid,start:start,end:end});
        }); }
        return vids;
    }

    function attachPlaceholders(root){
        root=root||document;
        var phs=root.querySelectorAll('.youtube-player-placeholder');
        phs.forEach(function(ph){
            if(ph.getAttribute('data-core-processed')==='true') return;
            var vids=parseVideos(ph); if(!vids.length) return;
            var pid=CorePlayer.registerPlaylist(ph,vids);

            // clickable links
            ph.querySelectorAll('a').forEach(function(a){
                var info=parseYouTubeFromHref(a.getAttribute('href')||a.getAttribute('url')||'');
                if(!info) return;
                a.addEventListener('click',function(e){
                    e.preventDefault(); e.stopPropagation();
                    var idx=vids.findIndex(v=>v.videoId===info.videoId && v.start===info.start && (v.end===info.end));
                    if(idx===-1) idx=0;
                    CorePlayer.gotoVideo(pid,idx);
                },{passive:false});
            });

            // controls
            if(!ph.querySelector('.youtube-player-controls')){
                var c=document.createElement('div'); c.className='youtube-player-controls';
                c.innerHTML='<button class="prev-button" data-core-playlist-id="'+pid+'">Previous</button><button class="next-button" data-core-playlist-id="'+pid+'">Next</button>';
                ph.appendChild(c);
            }
            ph.querySelectorAll('.prev-button,.next-button').forEach(function(btn){
                btn.addEventListener('click',function(e){
                    e.preventDefault();
                    var id=btn.getAttribute('data-core-playlist-id');
                    if(btn.classList.contains('prev-button')) CorePlayer.prev(id); else CorePlayer.next(id);
                });
            });
            ph.setAttribute('data-core-processed','true');
        });
    }

    // --------------------------
    // AJAX Page Loader
    // --------------------------
    function loadPage(url, addHistory){
        fetch(url, { credentials:'same-origin' })
        .then(resp=>resp.text())
        .then(function(html){
            var parser=new DOMParser();
            var doc=parser.parseFromString(html,'text/html');
            // replace heading
            var newHeader=doc.querySelector(HEADER_SELECTOR);
            if(newHeader){
                var header=document.querySelector(HEADER_SELECTOR);
                if(header) header.innerHTML=newHeader.innerHTML;
            }
            // replace content
            var newContent=doc.querySelector(CONTENT_SELECTOR);
            if(newContent){
                var content=document.querySelector(CONTENT_SELECTOR);
                if(content) content.innerHTML=newContent.innerHTML;
                attachPlaceholders(content);
            }
            // update title
            if(doc.title) document.title=doc.title;
            // update history
            if(addHistory) history.pushState({url:url},'',url);
        });
    }

    // --------------------------
    // Internal link interception
    // --------------------------
    document.addEventListener('click',function(e){
        var a=e.target.closest('a');
        if(!a) return;
        if(a.href.indexOf(location.origin+'/wiki/')!==0) return;
        if(a.hash && a.pathname===location.pathname) return; // allow in-page anchor jump
        e.preventDefault();
        var url=a.href;
        loadPage(url,true);
    });

    window.addEventListener('popstate', function(e){
        if(e.state && e.state.url) loadPage(e.state.url,false);
    });

    document.addEventListener('DOMContentLoaded',function(){
        attachPlaceholders(document);
        loadYouTubeApi();
    });

})();