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();
});
})();