MediaWiki:Gadget-TimelinePreview.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.
// Gadget: Timeline hover previews (no suppression; system-popup replace + fallback custom popup)
// Styled like MediaWiki popups (outer shell, no inner gray frame)
// Requires: mediawiki.api, jquery, mediawiki.util

mw.loader.using(['mediawiki.api', 'jquery', 'mediawiki.util']).then(function () {
    var api = new mw.Api();
    var timelineCache = null;

	function capitalizeFirst(str) {
	    return str.charAt(0).toUpperCase() + str.slice(1);
	}

    // Custom popup (fallback) — uses MediaWiki popup classes for authentic shape
    var customPopup = $('<div>')
        .addClass('mwe-popups mwe-popups-type-page')
        .attr('id', 'timeline-preview-fallback')
        .css({
            position: 'absolute',
            zIndex: 1100,
            display: 'none'
        })
        .append(
            $('<div>')
                .addClass('mwe-popups-container')
                .append(
                    $('<div>')
                        .addClass('mwe-popups-extract')
                        .css({
                            padding: '0.0em 0.0em',
                            background: '#fff',
                            border: 'none',       // remove inner gray frame
                            borderRadius: '0',    // remove rounded corners inside
                            boxShadow: 'none',    // remove inner drop shadow
                            fontWeight: '460',
                            fontSize: '0.95em',
                            maxWidth: '360px'
                        })
                )
        )
        .appendTo(document.body);

    function setPopupContent(html) {
        customPopup.find('.mwe-popups-extract').html(html);
    }

    function loadTimeline() {
        if (timelineCache) return $.Deferred().resolve();
        return api.get({
            action: 'parse',
            page: 'Timeline',
            prop: 'text',
            formatversion: 2
        }).then(function (data) {
            var html = $('<div>').html(data.parse.text);
            timelineCache = {};
            html.find('dt').each(function () {
                var dt = $(this);
                var id = dt.find('span[id]').attr('id');
                if (!id) return;
                var ddList = [];
                var el = dt.next();
                while (el.length && el.prop('tagName') === 'DD') {
                    var clone = el.clone();
                    // strip citation superscripts
                    clone.find('sup.reference').remove();
                    // replace links with inner HTML (preserve <b>, <i>, etc.)
                    clone.find('a').replaceWith(function () {
                        return $(this).html();
                    });
                    ddList.push(clone.html());
                    el = el.next();
                }
                timelineCache[id] = capitalizeFirst(ddList.join('<br>').trim());
            });
        });
    }

    // Replace system popup content (with padding and consistent font weight)
	function replaceSystemPopupContent(popupNode, html) {
	    try {
	        var $p = $(popupNode);
	        var extract = $p.find('.mwe-popups-extract, .popups-extract, .mwe-popups-container').first();

	        var contentWithPadding = $('<div>')
	            .css({ padding: '1.5em', fontSize: '0.95em', fontWeight: '460' })
	            .html(html);

	        if (extract && extract.length) {
	            extract.html(contentWithPadding);
	        } else {
	            var inner = $p.find('*').first();
	            if (inner && inner.length) inner.html(contentWithPadding);
	            else $p.html(contentWithPadding);
	        }
	    } catch (e) {
	        // ignore
	    }
	}

    // Observe for system popups appearing; replace content when found
    function watchForSystemPopupOnce(html, timeoutMs, onFound) {
        timeoutMs = timeoutMs || 1500;
        var found = false;
        var observer = new MutationObserver(function (mutations, obs) {
            for (var i = 0; i < mutations.length; i++) {
                var m = mutations[i];
                for (var j = 0; j < m.addedNodes.length; j++) {
                    var node = m.addedNodes[j];
                    if (node.nodeType !== 1) continue;
                    var cls = node.className || '';
                    if (/\bmwe-popups\b/.test(cls) || /\bpopups\b/.test(cls) || /\bpopups-container\b/.test(cls)) {
                        found = true;
                        replaceSystemPopupContent(node, html);
                        if (typeof onFound === 'function') onFound(node);
                        obs.disconnect();
                        return;
                    }
                    var $node = $(node);
                    var foundEl = $node.find('.mwe-popups, .popups, .popups-container, .mwe-popups-extract, .popups-extract').first();
                    if (foundEl && foundEl.length) {
                        found = true;
                        replaceSystemPopupContent(foundEl.get(0), html);
                        if (typeof onFound === 'function') onFound(foundEl.get(0));
                        obs.disconnect();
                        return;
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        var to = setTimeout(function () {
            if (!found) {
                try { observer.disconnect(); } catch (e) {}
            }
        }, timeoutMs);
        return {
            disconnect: function () { clearTimeout(to); try { observer.disconnect(); } catch (e) {} }
        };
    }

    function showFallbackPopup($link, html) {
        setPopupContent(html);
        var offset = $link.offset();
        customPopup
            .css({
                top: offset.top + $link.outerHeight() + 6,
                left: offset.left
            })
            .show();
    }
    function hideFallbackPopup() { customPopup.hide(); }

    function onLinkEnter() {
        var $link = $(this);
        var href = $link.attr('href') || '';
        var match = href.match(/(?:https?:\/\/[^\/]+)?\/wiki\/Timeline#(\d{4}-\d{2}-\d{2})$/)
          || href.match(/\/Timeline#(\d{4}-\d{2}-\d{2})$/)
          || href.match(/^#(\d{4}-\d{2}-\d{2})$/);
        if (!match) return;
        var id = match[1];

        loadTimeline().then(function () {
            var content = timelineCache && timelineCache[id];
            if (!content) return;

            // Watch for a system popup to replace; if found, cancel fallback
            var handle = watchForSystemPopupOnce(content, 2000, function () {
                clearTimeout(fallbackTimer);
                hideFallbackPopup();
            });

            // Fallback: show our custom popup after short delay if system popup not found
            var isTimelinePage = mw.config.get('wgPageName') === 'Timeline';
            var fallbackDelay = isTimelinePage ? 100 : 1000;

            var fallbackTimer = setTimeout(function () {
                showFallbackPopup($link, content);
            }, fallbackDelay);

            // Cleanup when mouse leaves
            $link.one('mouseleave.timelineCleanup', function () {
                clearTimeout(fallbackTimer);
                try { handle.disconnect(); } catch (e) {}
                hideFallbackPopup();
                $link.off('mouseleave.timelineCleanup');
            });
        });
    }

    function onLinkLeave() {
        hideFallbackPopup();
        $(this).off('mouseleave.timelineCleanup');
    }

	var timelineLinkSelector = 'a[href*="Timeline#"], a[href^="#"]';

	// Delegated binding for all present and future links
	$(document)
	    .on('mouseenter.timelinePreview', timelineLinkSelector, onLinkEnter)
	    .on('mouseleave.timelinePreview', timelineLinkSelector, onLinkLeave);
});