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