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 (mixed suppression + popup-replace)
// Requires: mediawiki.api, jquery, mediawiki.util
mw.loader.using(['mediawiki.api', 'jquery', 'mediawiki.util']).then(function () {
var api = new mw.Api();
var timelineCache = null;
// Custom popup for links we fully suppress
var customPopup = $('<div>')
.attr('id', 'timeline-preview-popup')
.css({
position: 'absolute',
zIndex: 1100,
background: '#fff',
border: '1px solid #a2a9b1',
borderRadius: '2px',
padding: '0.75em 1em',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
maxWidth: '360px',
display: 'none'
})
.appendTo(document.body);
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 their text
clone.find('a').replaceWith(function () {
return $(this).text();
});
ddList.push(clone.html());
el = el.next();
}
timelineCache[id] = ddList.join('<br>');
});
});
}
function showCustomPopup($link, html) {
customPopup.html(html).show();
var offset = $link.offset();
customPopup.css({
top: offset.top + $link.outerHeight() + 6,
left: offset.left
});
}
function hideCustomPopup() { customPopup.hide(); }
// detect if element is effectively visible (not display:none and attached)
function isVisibleElement(el) {
return el && el.nodeType === 1 && el.offsetParent !== null;
}
// selector to find timeline links in a context
var timelineLinkSelector = 'a[href*="Timeline#"], a[href^="#"]';
// Replace the content inside a system popup element with our html
function replaceSystemPopupContent(popupNode, html) {
try {
var $p = $(popupNode);
// prefer known extract container classes if present
var extract = $p.find('.mwe-popups-extract, .mwe-popups-container, .popups-extract').first();
if (extract && extract.length) {
extract.html(html);
} else {
// fallback: replace popup inner content
$p.html(html);
}
} catch (e) {
// silent fail
}
}
// Observe newly added nodes briefly and replace the popup content when found.
// Returns a disconnectable observer handle.
function watchForSystemPopupOnce($link, html, timeoutMs) {
timeoutMs = timeoutMs || 1500;
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 || '';
// Heuristics: common PagePreview popup classes include "mwe-popups" or "popups" or "popups-container"
if (/\bmwe-popups\b/.test(cls) || /\bpopups\b/.test(cls) || /\bpopups-container\b/.test(cls)) {
replaceSystemPopupContent(node, html);
obs.disconnect();
return;
}
// also consider descendants
var $node = $(node);
var found = $node.find('.mwe-popups, .popups, .popups-container, .mwe-popups-extract, .popups-extract').first();
if (found && found.length) {
replaceSystemPopupContent(found.get(0), html);
obs.disconnect();
return;
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
// safety timeout
var to = setTimeout(function () { observer.disconnect(); }, timeoutMs);
return {
disconnect: function () { clearTimeout(to); observer.disconnect(); }
};
}
function onLinkMouseEnter(e) {
var $link = $(this);
var href = $link.attr('href') || '';
var match = href.match(/\/Timeline#(\d{4}-\d{2}-\d{2})$/) || href.match(/^#(\d{4}-\d{2}-\d{2})$/);
if (!match) return;
var id = match[1];
// If we bound this link as disabled earlier, show our custom popup
if ($link.data('timeline-disabled')) {
loadTimeline().then(function () {
var content = timelineCache && timelineCache[id];
if (content) showCustomPopup($link, content);
});
return;
}
// Otherwise this link was not disabled (likely initially hidden). Let system create popup,
// but watch for it and replace its content when it appears.
loadTimeline().then(function () {
var content = timelineCache && timelineCache[id];
if (!content) return;
// Start observing for a system popup
var handle = watchForSystemPopupOnce($link, content, 2000);
// Also hide our custom popup if it was visible by some chance
hideCustomPopup();
// When mouse leaves before popup appears, disconnect observer
$link.one('mouseleave.timelinePopupReplace', function () {
try { handle.disconnect(); } catch (err) {}
});
});
}
function onLinkMouseLeave(e) {
// hide custom popup if used
hideCustomPopup();
// mouseleave also remove one-time listener used for system popup replacement
$(this).off('mouseleave.timelinePopupReplace');
}
// Disable previews for links that are currently visible. For links that are hidden at bind time,
// leave them alone so their system popup can be used and later replaced on hover.
function bindTimelineLinks(context) {
$(context).find(timelineLinkSelector).each(function () {
var $link = $(this);
var href = $link.attr('href') || '';
if (!(href.match(/Timeline#\d{4}-\d{2}-\d{2}$/) || href.match(/^#\d{4}-\d{2}-\d{2}$/))) return;
// avoid double-binding
if ($link.data('timeline-bound')) return;
$link.data('timeline-bound', true);
// if link is visible now, disable built-in previews and use custom popup
if (isVisibleElement(this)) {
$link.attr('data-mw-previews-disable', 'true');
// Attempt to call ext.popups.disablePreview hook if available (best-effort)
if (mw.hook && mw.hook('ext.popups.disablePreview')) {
try { mw.hook('ext.popups.disablePreview').fire($link.get(0)); } catch (e) {}
}
$link.data('timeline-disabled', true);
$link.off('.timelinePreview')
.on('mouseenter.timelinePreview', onLinkMouseEnter)
.on('mouseleave.timelinePreview', onLinkMouseLeave);
} else {
// link not visible at bind time (collapsed). Do not disable preview.
$link.data('timeline-disabled', false);
$link.off('.timelinePreview')
.on('mouseenter.timelinePreview', onLinkMouseEnter)
.on('mouseleave.timelinePreview', onLinkMouseLeave);
}
});
}
// initial binding on content load
mw.hook('wikipage.content').add(function (context) { bindTimelineLinks(context); });
// Observe added nodes and bind links inside them (but respect visibility at bind time)
var observer = new MutationObserver(function (mutations) {
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) bindTimelineLinks(node);
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
});