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() {
// 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() {
// Create the player if missing
if (!players[index]) {
players[index] = new YT.Player(playerDiv.id, {
height: '270',
width: '480',
videoId: videoLists[index][currentVideoIndices[index]].videoId, // initial video for container
playerVars: { start: 0, rel: 0, modestbranding: 1 },
events: {
onReady: function(event) {
// Pause immediately to prevent autoplay
event.target.pauseVideo();
},
onStateChange: createOnStateChange(index)
}
});
}
// Create controls if missing
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 to avoid duplicates
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() {
document.querySelectorAll('.youtube-player-placeholder').forEach(function(p) {
var idx = parseInt(p.dataset.index, 10);
if (isNaN(idx)) return;
// update video list
var newList = parseVideosFromPlaceholder(p);
if (newList.length) videoLists[idx] = newList;
currentVideoIndices[idx] = 0;
// reset flags so observer can re-trigger if needed
delete p.dataset.ytLoaded;
// ensure being observed
window._ytObserver.observe(p);
// --- new: trigger player creation if visible ---
var rect = p.getBoundingClientRect();
var inViewport = rect.bottom > 0 && rect.top < (window.innerHeight || document.documentElement.clientHeight);
if (inViewport) {
ensurePlayerForIndex(idx);
}
});
};
// 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();
})();