MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary |
No edit summary |
||
| Line 343: | Line 343: | ||
window.addEventListener("scroll", function () { if (activeBtn) positionPop(activeBtn); }, true); | window.addEventListener("scroll", function () { if (activeBtn) positionPop(activeBtn); }, true); | ||
})(); | })(); | ||
/* ============================================================================ | |||
* SV Definitions v1 — tooltips for .sv-def | |||
* - Reads: data-sv-def-tip, data-sv-def-link | |||
* - Works with spans output by Module:Definitions | |||
* ========================================================================== */ | |||
(function (mw) { | |||
"use strict"; | |||
if (!mw || window.SV_DEF_INIT) return; | |||
window.SV_DEF_INIT = true; | |||
var tipEl = null; | |||
var tipInner = null; | |||
var activeEl = null; | |||
var pinned = false; | |||
function qTipText(el) { | |||
var s = (el && el.getAttribute("data-sv-def-tip")) || ""; | |||
return (s || "").trim(); | |||
} | |||
function qLink(el) { | |||
var s = (el && el.getAttribute("data-sv-def-link")) || ""; | |||
return (s || "").trim(); | |||
} | |||
function ensureTip() { | |||
if (tipEl) return; | |||
tipEl = document.createElement("div"); | |||
tipEl.id = "sv-def-tip"; | |||
tipEl.className = "sv-def-tip"; | |||
tipEl.setAttribute("role", "tooltip"); | |||
tipEl.setAttribute("aria-hidden", "true"); | |||
// Minimal inline styling so it works even without CSS. | |||
// You can override in CSS later using #sv-def-tip / .sv-def-tip. | |||
tipEl.style.position = "fixed"; | |||
tipEl.style.zIndex = "99999"; | |||
tipEl.style.display = "none"; | |||
tipEl.style.maxWidth = "340px"; | |||
tipEl.style.padding = "8px 10px"; | |||
tipEl.style.borderRadius = "8px"; | |||
tipEl.style.background = "rgba(15, 18, 24, 0.96)"; | |||
tipEl.style.color = "#fff"; | |||
tipEl.style.fontSize = "13px"; | |||
tipEl.style.lineHeight = "1.25"; | |||
tipEl.style.boxShadow = "0 8px 24px rgba(0,0,0,0.35)"; | |||
tipEl.style.pointerEvents = "none"; | |||
tipInner = document.createElement("div"); | |||
tipInner.className = "sv-def-tip-inner"; | |||
tipEl.appendChild(tipInner); | |||
document.body.appendChild(tipEl); | |||
} | |||
function suppressTitle(el) { | |||
// Prevent double-tooltips (browser title tooltip + our custom tooltip). | |||
if (!el) return; | |||
var t = el.getAttribute("title"); | |||
if (t && !el.getAttribute("data-sv-def-title")) { | |||
el.setAttribute("data-sv-def-title", t); | |||
el.removeAttribute("title"); | |||
} | |||
} | |||
function restoreTitle(el) { | |||
if (!el) return; | |||
var t = el.getAttribute("data-sv-def-title"); | |||
if (t) { | |||
el.setAttribute("title", t); | |||
el.removeAttribute("data-sv-def-title"); | |||
} | |||
} | |||
function clamp(n, lo, hi) { | |||
return Math.max(lo, Math.min(hi, n)); | |||
} | |||
function positionTip(el) { | |||
if (!tipEl || !el) return; | |||
var r = el.getBoundingClientRect(); | |||
// Ensure we can measure tip size | |||
tipEl.style.left = "0px"; | |||
tipEl.style.top = "0px"; | |||
tipEl.style.display = "block"; | |||
var tr = tipEl.getBoundingClientRect(); | |||
var pad = 8; | |||
var gap = 8; | |||
var x = r.left + (r.width / 2) - (tr.width / 2); | |||
x = clamp(x, pad, window.innerWidth - tr.width - pad); | |||
var y = r.bottom + gap; | |||
if (y + tr.height > window.innerHeight - pad) { | |||
y = r.top - tr.height - gap; | |||
} | |||
y = clamp(y, pad, window.innerHeight - tr.height - pad); | |||
tipEl.style.left = Math.round(x) + "px"; | |||
tipEl.style.top = Math.round(y) + "px"; | |||
} | |||
function show(el) { | |||
ensureTip(); | |||
var txt = qTipText(el); | |||
if (!txt) return; | |||
activeEl = el; | |||
tipInner.textContent = txt; | |||
tipEl.setAttribute("aria-hidden", "false"); | |||
tipEl.style.display = "block"; | |||
suppressTitle(el); | |||
positionTip(el); | |||
} | |||
function hide() { | |||
if (!tipEl) return; | |||
tipEl.style.display = "none"; | |||
tipEl.setAttribute("aria-hidden", "true"); | |||
if (activeEl) restoreTitle(activeEl); | |||
activeEl = null; | |||
pinned = false; | |||
} | |||
function isEntering(container, relatedTarget) { | |||
return relatedTarget && container && container.contains(relatedTarget); | |||
} | |||
function closestDef(target) { | |||
if (!target || !target.closest) return null; | |||
return target.closest(".sv-def"); | |||
} | |||
// Hover in/out (desktop) | |||
document.addEventListener("mouseover", function (e) { | |||
if (pinned) return; | |||
var el = closestDef(e.target); | |||
if (!el) return; | |||
if (isEntering(el, e.relatedTarget)) return; | |||
if (activeEl === el) return; | |||
show(el); | |||
}, true); | |||
document.addEventListener("mouseout", function (e) { | |||
if (pinned) return; | |||
var el = closestDef(e.target); | |||
if (!el) return; | |||
if (isEntering(el, e.relatedTarget)) return; | |||
if (activeEl === el) hide(); | |||
}, true); | |||
// Tap/click behavior: | |||
// - If data-sv-def-link is set, navigate (future-ready). | |||
// - Otherwise toggle a pinned tooltip (mobile-friendly). | |||
document.addEventListener("click", function (e) { | |||
var el = closestDef(e.target); | |||
// Click outside closes a pinned tooltip | |||
if (!el) { | |||
if (pinned) hide(); | |||
return; | |||
} | |||
var link = qLink(el); | |||
if (link) { | |||
// Let it act like a link to a page title | |||
window.location.href = mw.util.getUrl(link); | |||
return; | |||
} | |||
var txt = qTipText(el); | |||
if (!txt) return; | |||
// Toggle pin | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
if (pinned && activeEl === el) { | |||
hide(); | |||
return; | |||
} | |||
pinned = true; | |||
show(el); | |||
}, true); | |||
// Close on Escape | |||
document.addEventListener("keydown", function (e) { | |||
if (e.key === "Escape" && (pinned || activeEl)) { | |||
hide(); | |||
} | |||
}, true); | |||
// Reposition if visible | |||
window.addEventListener("resize", function () { | |||
if (activeEl && tipEl && tipEl.style.display === "block") { | |||
positionTip(activeEl); | |||
} | |||
}, true); | |||
// Capture scroll events from containers too | |||
window.addEventListener("scroll", function () { | |||
if (activeEl && tipEl && tipEl.style.display === "block") { | |||
positionTip(activeEl); | |||
} | |||
}, true); | |||
// Support dynamic page content (VisualEditor, etc.) | |||
mw.hook("wikipage.content").add(function () { | |||
// No-op: event delegation already covers new content. | |||
}); | |||
})(window.mw); | |||