MediaWiki:Gadget-floatingYouTubePlayer.js

From Angelina Jordan Wiki
Revision as of 21:03, 28 September 2025 by Most2dot0 (talk | contribs) (bugfix)

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);
            if (ytReady && !player && videoArray.length) createPlayer(videoArray[0]);
            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,
            _debug: function () { return { playlists: playlists, active: activePlaylist, player: !!player }; }
        };
    })();

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

    // ===== 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 || !videos.length) return;
            var pid = CorePlayer.registerPlaylist(ph, videos);
            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 });
            });
            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(); CorePlayer.setYTReady(); });

    // ===== AJAX navigation =====
    var API_URL = (window.mw && mw.config) ? (mw.config.get('wgScriptPath') + '/api.php') : '/api.php';

    function ajaxNavigate(href, isPop) {
        var title = extractTitleFromUrl(href);
        if (!title) { location.href = href; return; }
        var urlObj = new URL(href, location.href);
        if (location.pathname + location.search === urlObj.pathname + urlObj.search && location.hash !== urlObj.hash) { return; } // allow fragment
        fetch(API_URL + '?action=parse&format=json&prop=' + API_PROP + '&page=' + encodeURIComponent(title), { credentials: 'same-origin' })
            .then(res => res.ok ? res.json() : Promise.reject())
            .then(json => {
                if (json && json.parse && json.parse.text) {
                    var html = json.parse.text['*'];
                    var displayTitle = stripTags(json.parse.displaytitle || json.parse.title || title);
                    replaceContent(html, displayTitle, href, !isPop);
                } else location.href = href;
            }).catch(() => location.href = href);
    }

    function replaceContent(html, newTitle, href, pushState) {
        var container = document.querySelector(CONTENT_SELECTOR); if (!container) { location.href = href; return; }
        container.innerHTML = html;
        var heading = document.querySelector(HEADER_SELECTOR); if (heading) heading.innerHTML = newTitle;
        document.title = newTitle;
        if (pushState) history.pushState({ ajax: true, title: newTitle, html: html }, newTitle, href);
        attachPlaceholders(container); CorePlayer.cleanup();
        if (location.hash) { var target = document.getElementById(location.hash.slice(1)); if (target) target.scrollIntoView(); } else window.scrollTo(0, 0);
    }

    document.addEventListener('click', function (ev) {
        if (ev.defaultPrevented || ev.button !== 0 || (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey)) return;
        var anchor = ev.target.closest && ev.target.closest('a'); if (!anchor) return;
        var href = anchor.getAttribute('href') || ''; if (!href || href.indexOf('javascript:') === 0) return;
        var title = extractTitleFromUrl(href); if (!title) return;
        ev.preventDefault(); ev.stopPropagation(); ajaxNavigate(href);
    }, { capture: true });

    window.addEventListener('popstate', function (ev) {
        var state = ev.state;
        if (state && state.ajax && state.html) { replaceContent(state.html, state.title || document.title, location.href, false); }
        else ajaxNavigate(location.href, true);
    });

})();