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 = null;
var videoLists = []; // array of arrays {videoId,start,end}
var players = []; // YT player instances by index
var currentVideoIndices = [];
var checkIntervals = [];
var observer = null;
// 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 insertYouTubePlayers() {
placeholders = Array.prototype.slice.call(document.querySelectorAll('.youtube-player-placeholder'));
if (!placeholders.length) return;
// init arrays to match placeholders
placeholders.forEach(function(p, i) {
p.setAttribute('data-index', i);
if (!videoLists[i]) videoLists[i] = [];
if (!currentVideoIndices[i]) currentVideoIndices[i] = 0;
});
// install observer once
if (!observer) {
observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var idx = parseInt(entry.target.getAttribute('data-index'), 10);
if (!isNaN(idx)) ensurePlayerForIndex(idx);
}
});
}, { root: null, rootMargin: '200px', threshold: 0.01 });
}
placeholders.forEach(function(p) {
// mark as initialized and observe
if (!p.hasAttribute('data-initialized')) {
p.setAttribute('data-initialized', 'true');
observer.observe(p);
}
});
// if YT already loaded, try to ensure players for visible ones
whenYTReady(function() {
placeholders.forEach(function(p, i) {
// if placeholder already visible in viewport, create player
var rect = p.getBoundingClientRect();
if (rect.top < (window.innerHeight || document.documentElement.clientHeight) + 200 &&
rect.bottom > -200) {
ensurePlayerForIndex(i);
}
});
});
};
// Public: refresh existing players after AJAX changes inner HTML of placeholders.
// Call this after you update the placeholder content.
window.refreshYouTubePlayers = function refreshYouTubePlayers() {
// refresh placeholder node list in case structure changed
placeholders = Array.prototype.slice.call(document.querySelectorAll('.youtube-player-placeholder'));
placeholders.forEach(function(p, i) {
// keep data-index stable if already set; set if not
var indexAttr = p.getAttribute('data-index');
var idx = (indexAttr !== null) ? parseInt(indexAttr, 10) : i;
p.setAttribute('data-index', idx);
// reparse videos
var newList = parseVideosFromPlaceholder(p);
if (!newList || newList.length === 0) {
// if empty and there was a previous list, keep previous list (avoids clearing)
if (!videoLists[idx] || videoLists[idx].length === 0) return;
} else {
// replace list
videoLists[idx] = newList;
// reset current index if it would be out of range
if (!currentVideoIndices[idx] || currentVideoIndices[idx] >= videoLists[idx].length) {
currentVideoIndices[idx] = 0;
}
}
// If a player exists, load the new current video
if (players[idx]) {
var v = videoLists[idx][currentVideoIndices[idx]];
if (v) {
try {
players[idx].loadVideoById({ videoId: v.videoId, startSeconds: v.start || 0 });
} catch (e) {
// possibly player not ready; schedule load after YT ready
whenYTReady(function() {
try {
players[idx].loadVideoById({ videoId: v.videoId, startSeconds: v.start || 0 });
} catch (e) {}
});
}
}
} else {
// ensure a player gets created for this placeholder if it's visible
ensurePlayerForIndex(idx);
}
});
};
// wire click handlers for prev/next buttons at document level (works for dynamically added controls)
document.addEventListener('click', function(ev) {
var t = ev.target;
if (t.classList.contains('next-button')) {
var idx = parseInt(t.getAttribute('data-index'), 10);
playVideo(idx, 1);
} else if (t.classList.contains('prev-button')) {
var idx = parseInt(t.getAttribute('data-index'), 10);
playVideo(idx, -1);
} else {
// if a link to a known video is clicked inside a placeholder, intercept and load that video
var anchor = t.closest('a[href]');
if (!anchor) return;
var p = anchor.closest('.youtube-player-placeholder');
if (!p) return;
var idx = parseInt(p.getAttribute('data-index'), 10);
if (isNaN(idx)) return;
var parsed = parseUrlToVideoData(anchor.getAttribute('href'));
if (!parsed) return;
// prevent navigation and load the corresponding video if present in list
var foundIndex = (videoLists[idx] || []).findIndex(function(x) {
return x.videoId === parsed.videoId && (x.start || 0) === (parsed.start || 0) && (x.end || null) === (parsed.end || null);
});
if (foundIndex !== -1) {
ev.preventDefault();
currentVideoIndices[idx] = foundIndex;
if (players[idx]) {
try {
players[idx].loadVideoById({ videoId: parsed.videoId, startSeconds: parsed.start || 0 });
} catch (e) {}
} else {
ensurePlayerForIndex(idx);
whenYTReady(function() {
try { players[idx].loadVideoById({ videoId: parsed.videoId, startSeconds: parsed.start || 0 }); } catch(e) {}
});
}
}
}
}, true);
// initialize on script load
// If you want to defer, you can remove this and call insertYouTubePlayers() manually.
window.insertYouTubePlayers();
})();