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';
// ===== CONFIG =====
var CONTENT_SELECTOR = '#mw-content-text';
var HEADER_SELECTOR = '#firstHeading';
var CORE_PLAYER_ID = 'core-youtube-player';
var YT_SCRIPT_ID = 'youtube-iframe-api';
var API_PROP = 'text|displaytitle|title';
function stripTags(s) {
var d = document.createElement('div');
d.innerHTML = s || '';
return d.textContent || d.innerText || '';
}
function parseTimeParam(val) {
if (!val) return 0;
val = String(val);
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, 10) * 3600) + (parseInt(m[2] || 0, 10) * 60) + (parseInt(m[3] || 0, 10));
var digits = val.replace(/\D/g, '');
return digits ? parseInt(digits, 10) : 0;
}
function extractTitleFromUrl(href) {
try {
var url = new URL(href, location.href);
if (url.origin !== location.origin) return null;
var path = url.pathname;
var m = path.match(/^\/wiki\/(.+)/);
if (m) return decodeURIComponent(m[1]).replace(/_/g, ' ').split(/[?#]/)[0];
if (path.indexOf('/index.php') !== -1 || path.indexOf('/w/index.php') !== -1) {
var t = url.searchParams.get('title');
if (t) return decodeURIComponent(t).replace(/_/g, ' ').split(/[?#]/)[0];
}
return null;
} catch (e) { return null; }
}
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 q = '';
try { q = (new URL(href, location.href)).search; } catch (e) { var parts = href.split('?'); q = parts[1] ? '?' + parts[1] : ''; }
var params = new URLSearchParams(q);
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 =====
var CorePlayer = (function () {
var playlists = [], player = null, ytReady = false, activePlaylist = null, activeCheckInterval = 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(initialVideo) {
var container = ensureContainer();
if (!ytReady) return;
if (player) return;
container.innerHTML = '<div id="core-youtube-player-frame"></div>';
var opts = {
height: '270',
width: '480',
playerVars: { autoplay: 0, rel: 0 },
events: {
onReady: function () { },
onStateChange: onPlayerStateChange
}
};
if (initialVideo) opts.videoId = initialVideo.videoId;
player = new YT.Player('core-youtube-player-frame', opts);
}
function onPlayerStateChange(ev) {
if (activePlaylist === null) return;
clearActiveInterval();
if (ev.data === YT.PlayerState.PLAYING) {
var cur = playlists[activePlaylist];
var curVideo = cur && cur.videos[cur.currentIndex];
if (curVideo && curVideo.end != null) {
activeCheckInterval = setInterval(function () {
try {
var ct = player.getCurrentTime();
if (ct >= curVideo.end) { clearActiveInterval(); playNext(activePlaylist); }
} catch (e) { clearActiveInterval(); }
}, 500);
}
} else if (ev.data === YT.PlayerState.ENDED) {
playNext(activePlaylist);
}
}
function clearActiveInterval() { if (activeCheckInterval) { clearInterval(activeCheckInterval); activeCheckInterval = null; } }
function registerPlaylist(placeholderEl, videoArray) {
var pid = playlists.length;
playlists.push({ placeholderEl: placeholderEl, videos: videoArray.slice(), currentIndex: 0 });
placeholderEl.setAttribute('data-core-playlist-id', pid);
return pid;
}
function cleanup() {
for (var i = playlists.length - 1; i >= 0; i--) {
if (!playlists[i].placeholderEl || !document.body.contains(playlists[i].placeholderEl)) {
if (i === activePlaylist) { activePlaylist = null; clearActiveInterval(); }
playlists.splice(i, 1);
}
}
playlists.forEach(function (pl, idx) {
if (pl.placeholderEl) pl.placeholderEl.setAttribute('data-core-playlist-id', idx);
});
}
function gotoVideo(playlistId, idx) {
playlistId = Number(playlistId); if (!playlists[playlistId]) return;
var pl = playlists[playlistId]; if (idx < 0 || idx >= pl.videos.length) return;
pl.currentIndex = idx; var v = pl.videos[idx]; activePlaylist = playlistId; clearActiveInterval();
if (!ytReady) { loadYouTubeApi(); setTimeout(function () { if (!player && ytReady) 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(playlistId) { playlistId = Number(playlistId); var pl = playlists[playlistId]; if (!pl) return; pl.currentIndex++; if (pl.currentIndex >= pl.videos.length) pl.currentIndex = 0; gotoVideo(playlistId, pl.currentIndex); }
function playPrev(playlistId) { playlistId = Number(playlistId); var pl = playlists[playlistId]; if (!pl) return; pl.currentIndex--; if (pl.currentIndex < 0) pl.currentIndex = pl.videos.length - 1; gotoVideo(playlistId, pl.currentIndex); }
function hasAnyPlaylist() { return playlists.length > 0; }
return {
setYTReady: function () { ytReady = true; },
registerPlaylist: registerPlaylist,
gotoVideo: gotoVideo,
next: playNext,
prev: playPrev,
cleanup: cleanup,
hasAnyPlaylist: hasAnyPlaylist
};
})();
// ===== Load YouTube API =====
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();
// Auto-load first video from first available playlist
var placeholders = document.querySelectorAll('.youtube-player-placeholder');
for (var i = 0; i < placeholders.length; i++) {
var videos = parseVideosFromPlaceholder(placeholders[i]);
if (videos.length) {
CorePlayer.registerPlaylist(placeholders[i], videos);
CorePlayer.gotoVideo(i, 0);
break;
}
}
};
// ===== Placeholder Parsing =====
function parseVideosFromPlaceholder(placeholder) {
var videoDataList = [], links = placeholder.querySelectorAll('a'), includeRefs = placeholder.getAttribute('data-include-refs') === 'true';
function tryParseHref(href) { var info = parseYouTubeFromHref(href); if (info) videoDataList.push({ videoId: info.videoId, start: info.start || 0, end: (info.end != null ? info.end : null) }); }
links.forEach(function (link) { tryParseHref(link.getAttribute('href') || link.getAttribute('url') || ''); });
if (includeRefs) {
var refSupTags = placeholder.querySelectorAll('sup a[href^="#"]');
refSupTags.forEach(function (sup) {
var refId = sup.getAttribute('href').substring(1);
var refListItem = document.getElementById(refId);
if (refListItem) {
var rlinks = refListItem.querySelectorAll('a');
rlinks.forEach(function (l) { tryParseHref(l.getAttribute('href') || ''); });
}
});
}
if (videoDataList.length === 0) {
var dataAttr = placeholder.getAttribute('data-videos') || '';
if (dataAttr) { dataAttr.split(',').forEach(function (v) { var parts = v.split('&'), vid = parts[0].trim(), start = 0, end = null; parts.forEach(function (p) { p = p.trim(); if (p.startsWith('t=')) start = parseTimeParam(p.substring(2)); else if (p.startsWith('start=')) start = parseTimeParam(p.substring(6)); else if (p.startsWith('end=')) end = parseTimeParam(p.substring(4)); }); if (vid) videoDataList.push({ videoId: vid, start: start, end: end }); }); }
}
return videoDataList;
}
function attachPlaceholders(root) {
root = root || document;
var placeholders = root.querySelectorAll('.youtube-player-placeholder');
placeholders.forEach(function (ph) {
if (ph.getAttribute('data-core-processed') === 'true') return;
var videos = parseVideosFromPlaceholder(ph);
if (!videos.length) return;
var pid = CorePlayer.registerPlaylist(ph, videos);
// clickable links
var links = ph.querySelectorAll('a');
links.forEach(function (link) {
var info = parseYouTubeFromHref(link.getAttribute('href') || link.getAttribute('url') || '');
if (!info) return;
link.addEventListener('click', function (ev) {
ev.preventDefault(); ev.stopPropagation();
var idx = videos.findIndex(function (v) { return 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 controls = document.createElement('div'); controls.className = 'youtube-player-controls';
controls.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(controls);
}
ph.querySelectorAll('.prev-button,.next-button').forEach(function (btn) {
btn.addEventListener('click', function (ev) { ev.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');
});
}
document.addEventListener('DOMContentLoaded', function () { attachPlaceholders(document); loadYouTubeApi(); });
})();