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.
```javascript
// Unified YouTube player gadget with safe SPA-style navigation
// Fixes section jump bug by ignoring hash-only navigation
function insertYouTubePlayers() {
var placeholders = document.querySelectorAll('.youtube-player-placeholder');
// Load the YouTube IFrame API if not already loaded
if (!document.getElementById('youtube-iframe-api')) {
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
tag.id = 'youtube-iframe-api';
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
}
var videoLists = [];
var currentVideoIndices = [];
var checkIntervals = [];
function parseVideosFromPlaceholder(placeholder) {
var videoDataList = [];
var links = placeholder.querySelectorAll('a');
var includeRefs = placeholder.getAttribute('data-include-refs') === 'true';
function parseUrl(href) {
var match = href && href.match(/\/\/(www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)([^\s]*)/);
if (match) {
var videoId = match[2];
var urlParams = match[3];
var params = new URLSearchParams(urlParams);
var start = params.get('t') ? parseInt(params.get('t')) : 0;
var end = params.get('end') ? parseInt(params.get('end')) : null;
videoDataList.push({ videoId: videoId, start: start, end: end });
}
}
links.forEach(function(link) {
parseUrl(link.getAttribute('href'));
});
if (includeRefs) {
var refSupTags = placeholder.querySelectorAll('sup a[href^="#"]');
refSupTags.forEach(function(supTag) {
var refId = supTag.getAttribute('href').substring(1);
var refListItem = document.getElementById(refId);
if (refListItem) {
var refLinks = refListItem.querySelectorAll('a');
refLinks.forEach(function(link) {
parseUrl(link.getAttribute('href'));
});
}
});
}
if (videoDataList.length === 0) {
var dataVideos = (placeholder.getAttribute('data-videos') || '').split(',');
dataVideos.forEach(function(videoData) {
if (!videoData) return;
var parts = videoData.split('&');
var videoId = parts[0];
var start = 0;
var end = null;
parts.forEach(function(part) {
if (part.startsWith('t=')) {
start = parseInt(part.substring(2)) || 0;
} else if (part.startsWith('end=')) {
end = parseInt(part.substring(4)) || null;
}
});
videoDataList.push({ videoId: videoId, start: start, end: end });
});
}
return videoDataList;
}
function onYouTubeIframeAPIReady() {
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var index = entry.target.getAttribute('data-index');
if (!entry.target.getAttribute('data-loaded')) {
entry.target.setAttribute('data-loaded', 'true');
loadYouTubePlayer(entry.target, index);
}
}
});
});
placeholders.forEach(function(placeholder, index) {
placeholder.setAttribute('data-index', index);
observer.observe(placeholder);
});
}
window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
function loadYouTubePlayer(placeholder, index) {
var videoDataList = parseVideosFromPlaceholder(placeholder);
videoLists[index] = videoDataList;
currentVideoIndices[index] = 0;
checkIntervals[index] = null;
var playerDiv = document.createElement('div');
playerDiv.id = 'youtube-player-' + index;
placeholder.appendChild(playerDiv);
var 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);
window['youtube-player-' + index] = new YT.Player('youtube-player-' + index, {
height: '270',
width: '480',
videoId: videoLists[index][currentVideoIndices[index]].videoId,
playerVars: {
start: videoLists[index][currentVideoIndices[index]].start
},
events: {
'onStateChange': onPlayerStateChange(index),
'onReady': onPlayerReady(index)
}
});
var links = placeholder.querySelectorAll('a');
links.forEach(function(link) {
var href = link.getAttribute('href');
var match = href && href.match(/\/\/(www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)([^\s]*)/);
if (match) {
var videoId = match[2];
var urlParams = match[3];
var params = new URLSearchParams(urlParams);
var start = params.get('t') ? parseInt(params.get('t')) : 0;
var end = params.get('end') ? parseInt(params.get('end')) : null;
link.addEventListener('click', function(event) {
event.preventDefault();
var videoIndex = videoLists[index].findIndex(function(video) {
return video.videoId === videoId && video.start === start && video.end === end;
});
if (videoIndex !== -1) {
currentVideoIndices[index] = videoIndex;
window['youtube-player-' + index].loadVideoById({
videoId: videoLists[index][currentVideoIndices[index]].videoId,
startSeconds: videoLists[index][currentVideoIndices[index]].start
});
}
});
}
});
}
function onPlayerReady(index) {
return function() {
var placeholder = placeholders[index];
var originalHTML = placeholder.querySelector('.youtube-placeholder-text');
if (originalHTML) {
originalHTML.style.display = '';
}
};
}
function onPlayerStateChange(index) {
return function(event) {
clearInterval(checkIntervals[index]);
if (event.data == YT.PlayerState.PLAYING) {
for (var i = 0; i < placeholders.length; i++) {
if (i !== parseInt(index) && window['youtube-player-' + i] &&
window['youtube-player-' + i].getPlayerState() === YT.PlayerState.PLAYING) {
window['youtube-player-' + i].pauseVideo();
}
}
if (videoLists[index][currentVideoIndices[index]].end) {
checkIntervals[index] = setInterval(function() {
var currentTime = window['youtube-player-' + index].getCurrentTime();
if (currentTime >= videoLists[index][currentVideoIndices[index]].end) {
playVideo(index, 1);
}
}, 1000);
}
} else if (event.data == YT.PlayerState.ENDED) {
playVideo(index, 1);
}
};
}
function playVideo(index, direction) {
clearInterval(checkIntervals[index]);
currentVideoIndices[index] += direction;
if (currentVideoIndices[index] >= videoLists[index].length) {
currentVideoIndices[index] = 0;
} else if (currentVideoIndices[index] < 0) {
currentVideoIndices[index] = videoLists[index].length - 1;
}
window['youtube-player-' + index].loadVideoById({
videoId: videoLists[index][currentVideoIndices[index]].videoId,
startSeconds: videoLists[index][currentVideoIndices[index]].start
});
}
document.addEventListener('click', function(event) {
if (event.target.classList.contains('next-button')) {
var index = event.target.getAttribute('data-index');
playVideo(index, 1);
} else if (event.target.classList.contains('prev-button')) {
var index = event.target.getAttribute('data-index');
playVideo(index, -1);
}
});
}
// ---- Safe SPA navigation wrapper ----
(function setupNavigation() {
// Normalize initial history state without hash
if (!history.state || !history.state.url) {
history.replaceState({ url: location.href.split('#')[0] }, '', location.href);
}
window.addEventListener('popstate', function() {
var baseNow = location.href.split('#')[0];
var baseState = history.state && history.state.url ? history.state.url : null;
if (baseState === baseNow) {
return; // hash-only change
}
loadArticleByUrl(location.href, { replaceState: true });
});
// Example shouldInterceptLink, skipping section jumps
function shouldInterceptLink(a) {
if (a.target === '_blank') return false;
if (a.hostname !== location.hostname) return false;
if (a.pathname === location.pathname && a.hash) return false;
return true;
}
document.addEventListener('click', function(ev) {
var a = ev.target.closest('a');
if (!a) return;
if (!shouldInterceptLink(a)) return;
ev.preventDefault();
loadArticleByUrl(a.href, { pushState: true });
});
function loadArticleByUrl(href, opts) {
var baseNow = location.href.split('#')[0];
var baseTarget = href.split('#')[0];
if (baseNow === baseTarget) {
return; // prevent reload on hash-only navigation
}
fetch(href, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function(resp) { return resp.text(); })
.then(function(html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var content = doc.querySelector('#content');
if (content) {
document.querySelector('#content').innerHTML = content.innerHTML;
}
if (opts.pushState) {
history.pushState({ url: href.split('#')[0] }, '', href);
} else if (opts.replaceState) {
history.replaceState({ url: href.split('#')[0] }, '', href);
}
insertYouTubePlayers();
});
}
})();
insertYouTubePlayers();
```