MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/* | /* Common.js — Spirit Vale Wiki | ||
Standalone Module: Definitions (v1) | |||
- Event-delegated tooltips for .sv-def | |||
- Desktop: hover + follow cursor | |||
- Mobile/touch: tap to pin near tap point | |||
- Blank definitions do nothing (and will close any open tooltip) | |||
- Future-ready: data-sv-def-link navigation support | |||
Markup contract (produced by Module:Definitions / Template:def): | |||
<span class="sv-def" | |||
data-sv-def-domain="Stat" | |||
data-sv-def-key="Vit" | |||
data-sv-def-tip="Tooltip text (optional)" | |||
data-sv-def-link="Page Title (optional)"> | |||
... | |||
</span> | |||
*/ | |||
(function () { | (function () { | ||
if ( | // Namespace + per-module guard (no “41”). | ||
var SV = (window.SV = window.SV || {}); | |||
var COMMON = (SV.common = SV.common || {}); | |||
if (COMMON.definitionsInit) return; | |||
COMMON.definitionsInit = 1; | |||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Selectors | /* Selectors / Attributes */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
var | var DEF_SEL = ".sv-def"; | ||
var TIP_ATTR = "data-sv-def-tip"; | |||
var LINK_ATTR = "data-sv-def-link"; | |||
// | /* ------------------------------------------------------------------ */ | ||
/* Capability detection */ | |||
/* ------------------------------------------------------------------ */ | |||
var HOVER_CAPABLE = false; | |||
var | try { | ||
HOVER_CAPABLE = | |||
!!window.matchMedia && | |||
window.matchMedia("(hover:hover) and (pointer:fine)").matches; | |||
} catch (e) { | |||
HOVER_CAPABLE = false; | |||
} | |||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* | /* State */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
var | var tipEl = null; | ||
// | var state = { | ||
visible: false, | |||
pinned: false, | |||
anchor: null, // current .sv-def element | |||
lastX: 0, | |||
lastY: 0, | |||
suppressClickUntil: 0, // prevents synthetic click after touch pin | |||
}; | |||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Utilities | /* Utilities */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
function closest(el, sel) { | function closest(el, sel) { | ||
| Line 64: | Line 74: | ||
} | } | ||
function | function getTipText(defEl) { | ||
if (!defEl || !defEl.getAttribute) return ""; | |||
var t = defEl.getAttribute(TIP_ATTR); | |||
if (t == null) return ""; | |||
// Treat pure-whitespace as empty. | |||
t = String(t); | |||
if (!t.replace(/\s+/g, "")) return ""; | |||
return t; | |||
} | } | ||
function | function getLinkTitle(defEl) { | ||
if (! | if (!defEl || !defEl.getAttribute) return ""; | ||
var t = defEl.getAttribute(LINK_ATTR); | |||
if (t == null) return ""; | |||
t = String(t).trim(); | |||
return t; | |||
} | |||
function ensureTipEl() { | |||
if (tipEl) return tipEl; | |||
var | var el = document.createElement("div"); | ||
el.className = "sv-def-tip sv-hidden"; | |||
el.setAttribute("role", "tooltip"); | |||
el.setAttribute("aria-hidden", "true"); | |||
if | // Minimal inline safety styling (CSS can override via .sv-def-tip). | ||
// This is intentionally conservative so it still works even if site CSS | |||
// hasn’t loaded yet. | |||
el.style.position = "fixed"; | |||
el.style.zIndex = "99999"; | |||
el.style.maxWidth = "360px"; | |||
el.style.boxSizing = "border-box"; | |||
el.style.padding = "8px 10px"; | |||
el.style.borderRadius = "10px"; | |||
el.style.background = "rgba(10, 14, 22, 0.92)"; | |||
el.style.color = "#fff"; | |||
el.style.fontSize = "14px"; | |||
el.style.lineHeight = "1.25"; | |||
el.style.whiteSpace = "pre-line"; | |||
el.style.pointerEvents = "none"; // enabled only when pinned | |||
el.style.display = "none"; | |||
document.body.appendChild(el); | |||
tipEl = el; | |||
return tipEl; | |||
return | |||
} | } | ||
function | function setHidden(el, hidden) { | ||
if ( | if (!el) return; | ||
if ( | if (hidden) { | ||
el.classList.add("sv-hidden"); | |||
el.setAttribute("aria-hidden", "true"); | |||
el.style.display = "none"; | |||
} else { | |||
el.classList.remove("sv-hidden"); | |||
el.setAttribute("aria-hidden", "false"); | |||
el.style.display = "block"; | |||
} | |||
} | } | ||
function | function clamp(n, min, max) { | ||
return | if (n < min) return min; | ||
if (n > max) return max; | |||
return n; | |||
} | } | ||
function | function positionTipAt(clientX, clientY) { | ||
var el = ensureTipEl(); | |||
if (!el) return; | |||
// First, ensure it’s measurable. | |||
// (If we just turned it on, width/height might be 0 until next frame.) | |||
var vw = window.innerWidth || document.documentElement.clientWidth || 0; | |||
var vh = window.innerHeight || document.documentElement.clientHeight || 0; | |||
// Gap rules: | |||
// - hover: larger vertical gap so cursor doesn’t cover it | |||
// - pinned: tighter but still separated from the tap point | |||
var gapY = state.pinned ? 10 : 16; | |||
var gapX = 12; | |||
// Two-pass: set an initial position, then refine once we have rect. | |||
el.style.left = "0px"; | |||
el.style.top = "0px"; | |||
// Defer measuring to allow layout to settle. | |||
requestAnimationFrame(function () { | |||
if (!state.visible) return; | |||
var r = el.getBoundingClientRect(); | |||
var w = r.width || 240; | |||
var h = r.height || 60; | |||
// Prefer above cursor/tap. | |||
var left = clientX - w / 2; | |||
var top = clientY - h - gapY; | |||
// Clamp horizontally. | |||
left = clamp(left, gapX, Math.max(gapX, vw - w - gapX)); | |||
// If not enough space above, flip below. | |||
if (top < gapY) top = clientY + gapY; | |||
// Clamp vertically. | |||
top = clamp(top, gapY, Math.max(gapY, vh - h - gapY)); | |||
el.style.left = Math.round(left) + "px"; | |||
el.style.top = Math.round(top) + "px"; | |||
}); | |||
} | |||
function showTip(defEl, text, clientX, clientY, pinned) { | |||
var el = ensureTipEl(); | |||
if (!el) return; | |||
state.visible = true; | |||
state.pinned = !!pinned; | |||
state.anchor = defEl || null; | |||
state.lastX = clientX || 0; | |||
state.lastY = clientY || 0; | |||
// Text-only (safe). | |||
el.textContent = text; | |||
// Pointer events only when pinned (so pinned tooltip can be interacted with later if needed). | |||
el.style.pointerEvents = state.pinned ? "auto" : "none"; | |||
el.classList.toggle("sv-def-tip--pinned", state.pinned); | |||
setHidden(el, false); | |||
positionTipAt(state.lastX, state.lastY); | |||
} | } | ||
function | function hideTip() { | ||
if (!tipEl) return; | |||
state.visible = false; | |||
state.pinned = false; | |||
state.anchor = null; | |||
setHidden(tipEl, true); | |||
} | } | ||
function | function navigateToTitle(title) { | ||
if (!title) return; | |||
if ( | |||
// MediaWiki-friendly navigation if available. | |||
if (window.mw && mw.util && typeof mw.util.getUrl === "function") { | |||
if ( | window.location.href = mw.util.getUrl(title); | ||
return; | |||
} | |||
// Fallback: treat as relative. | |||
window.location.href = String(title); | |||
} | } | ||
function | function sameAnchor(el) { | ||
return !!(el && state.anchor && el === state.anchor); | |||
} | } | ||
function | function shouldSuppressClick() { | ||
return Date.now() < state.suppressClickUntil; | |||
} | } | ||
/* ------------------------------------------------------------------ */ | |||
/* Desktop hover behavior (only when hover-capable) */ | |||
/* ------------------------------------------------------------------ */ | |||
if (HOVER_CAPABLE) { | |||
document.addEventListener( | |||
"mouseover", | |||
function (e) { | |||
if (state.pinned) return; // pinned wins | |||
var defEl = closest(e.target, DEF_SEL); | |||
if (!defEl) { | |||
// If moving over non-def content, close hover tooltip (if any). | |||
if (state.visible && !state.pinned) hideTip(); | |||
return; | |||
} | |||
// Avoid re-triggering within the same element. | |||
var rel = e.relatedTarget; | |||
if (rel && defEl.contains(rel)) return; | |||
var text = getTipText(defEl); | |||
if (!text) { | |||
// Blank tip closes any active hover tooltip. | |||
if (state.visible && !state.pinned) hideTip(); | |||
return; | |||
} | |||
showTip(defEl, text, e.clientX, e.clientY, false); | |||
}, | |||
true | |||
); | |||
document.addEventListener( | |||
"mousemove", | |||
function (e) { | |||
if (!state.visible || state.pinned) return; | |||
// Only track if we’re still “on” the same anchor. | |||
var defEl = closest(e.target, DEF_SEL); | |||
if (!defEl || !sameAnchor(defEl)) return; | |||
state.lastX = e.clientX; | |||
state.lastY = e.clientY; | |||
positionTipAt(state.lastX, state.lastY); | |||
}, | |||
true | |||
); | |||
document.addEventListener( | |||
"mouseout", | |||
if ( | function (e) { | ||
if (!state.visible || state.pinned) return; | |||
var defEl = closest(e.target, DEF_SEL); | |||
if (!defEl || !sameAnchor(defEl)) return; | |||
var rel = e.relatedTarget; | |||
if (rel && defEl.contains(rel)) return; // still within the definition element | |||
hideTip(); | |||
}, | |||
true | |||
); | |||
} | |||
} | } | ||
// | /* ------------------------------------------------------------------ */ | ||
/* Touch / click behavior */ | |||
/* ------------------------------------------------------------------ */ | |||
// | // Pointerdown is the best universal “tap” detector. | ||
document.addEventListener( | document.addEventListener( | ||
"pointerdown", | "pointerdown", | ||
function (e) { | function (e) { | ||
var | var defEl = closest(e.target, DEF_SEL); | ||
if (!defEl) return; | |||
if (! | |||
var | var text = getTipText(defEl); | ||
if (! | if (!text) { | ||
// Blank def: close if open. | |||
if (state.visible) hideTip(); | |||
return; | |||
} | |||
e. | // Touch/pen: pin tooltip near tap. | ||
// Mouse: we don’t pin on pointerdown (let hover handle it); clicking can toggle pin. | |||
var isTouchLike = e.pointerType && e.pointerType !== "mouse"; | |||
if (!isTouchLike) return; | |||
// | // Prevent synthetic click immediately after a touch pin (avoids double-toggling). | ||
state.suppressClickUntil = Date.now() + 450; | |||
// If already pinned on same anchor and it has a link, a second tap navigates. | |||
var link = getLinkTitle(defEl); | |||
if (state.pinned && sameAnchor(defEl) && link) { | |||
e.preventDefault(); | |||
navigateToTitle(link); | |||
return; | |||
if ( | |||
} | } | ||
e.preventDefault(); | e.preventDefault(); | ||
showTip(defEl, text, e.clientX, e.clientY, true); | |||
}, | }, | ||
true | true | ||
); | ); | ||
// Click: desktop toggle pin (no link) OR navigate (has link). | |||
document.addEventListener( | document.addEventListener( | ||
" | "click", | ||
function (e) { | function (e) { | ||
if (shouldSuppressClick()) return; | |||
if ( | |||
var | var defEl = closest(e.target, DEF_SEL); | ||
if (! | if (!defEl) return; | ||
var | var text = getTipText(defEl); | ||
if (! | if (!text) { | ||
if (state.visible) hideTip(); | |||
return; | |||
} | |||
var | var link = getLinkTitle(defEl); | ||
// If link exists: navigate. | |||
if (link) { | |||
// If tooltip is pinned on this item, allow click to navigate (expected “future” behavior). | |||
if ( | e.preventDefault(); | ||
navigateToTitle(link); | |||
e. | |||
return; | return; | ||
} | } | ||
// No link: toggle pinned state on desktop. | |||
e.preventDefault(); | e.preventDefault(); | ||
if (state.pinned && sameAnchor(defEl)) { | |||
hideTip(); | |||
} else { | |||
showTip(defEl, text, e.clientX, e.clientY, true); | |||
} | |||
}, | }, | ||
true | true | ||
); | ); | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* | /* Dismissal behavior */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
// Click outside closes pinned tooltip. | |||
document.addEventListener( | document.addEventListener( | ||
"click", | "click", | ||
function (e) { | function (e) { | ||
if (!state.visible || !state.pinned) return; | |||
// Click inside tooltip should not close it. | |||
if ( | if (tipEl && (e.target === tipEl || (tipEl.contains && tipEl.contains(e.target)))) return; | ||
// Click on a definition is handled by the click handler above. | |||
if ( | if (closest(e.target, DEF_SEL)) return; | ||
hideTip(); | |||
}, | }, | ||
true | true | ||
); | ); | ||
/ | // Escape closes any tooltip (pinned or hover). | ||
document.addEventListener( | document.addEventListener( | ||
"keydown", | "keydown", | ||
function (e) { | function (e) { | ||
if | if (e.key !== "Escape") return; | ||
if (state.visible) hideTip(); | |||
if ( | |||
}, | }, | ||
true | true | ||
); | ); | ||
/ | // If the page scrolls while pinned, keep it near the last anchor/tap point. | ||
window.addEventListener( | |||
"scroll", | |||
function () { | |||
if (!state.visible) return; | |||
positionTipAt(state.lastX, state.lastY); | |||
" | |||
function ( | |||
if (! | |||
}, | }, | ||
true | true | ||
); | ); | ||
window.addEventListener( | |||
"resize", | |||
" | function () { | ||
function ( | if (!state.visible) return; | ||
positionTipAt(state.lastX, state.lastY); | |||
if (! | |||
}, | }, | ||
true | true | ||
); | ); | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Init hook */ | |||
/* Init | |||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
// This module is fully event-delegated; no per-node init required. | |||
// Still, we keep the MediaWiki hook so this file matches your site pattern. | |||
function initAll(root) { | function initAll(root) { | ||
// No-op on purpose (delegated listeners already installed once). | |||
// Placeholder for future expansions (e.g., prewarming tooltip element). | |||
void root; | |||
} | } | ||
Revision as of 19:05, 19 February 2026
/* Common.js — Spirit Vale Wiki
Standalone Module: Definitions (v1)
- Event-delegated tooltips for .sv-def
- Desktop: hover + follow cursor
- Mobile/touch: tap to pin near tap point
- Blank definitions do nothing (and will close any open tooltip)
- Future-ready: data-sv-def-link navigation support
Markup contract (produced by Module:Definitions / Template:def):
<span class="sv-def"
data-sv-def-domain="Stat"
data-sv-def-key="Vit"
data-sv-def-tip="Tooltip text (optional)"
data-sv-def-link="Page Title (optional)">
...
</span>
*/
(function () {
// Namespace + per-module guard (no “41”).
var SV = (window.SV = window.SV || {});
var COMMON = (SV.common = SV.common || {});
if (COMMON.definitionsInit) return;
COMMON.definitionsInit = 1;
/* ------------------------------------------------------------------ */
/* Selectors / Attributes */
/* ------------------------------------------------------------------ */
var DEF_SEL = ".sv-def";
var TIP_ATTR = "data-sv-def-tip";
var LINK_ATTR = "data-sv-def-link";
/* ------------------------------------------------------------------ */
/* Capability detection */
/* ------------------------------------------------------------------ */
var HOVER_CAPABLE = false;
try {
HOVER_CAPABLE =
!!window.matchMedia &&
window.matchMedia("(hover:hover) and (pointer:fine)").matches;
} catch (e) {
HOVER_CAPABLE = false;
}
/* ------------------------------------------------------------------ */
/* State */
/* ------------------------------------------------------------------ */
var tipEl = null;
var state = {
visible: false,
pinned: false,
anchor: null, // current .sv-def element
lastX: 0,
lastY: 0,
suppressClickUntil: 0, // prevents synthetic click after touch pin
};
/* ------------------------------------------------------------------ */
/* Utilities */
/* ------------------------------------------------------------------ */
function closest(el, sel) {
if (!el || el.nodeType !== 1) return null;
if (el.closest) return el.closest(sel);
while (el && el.nodeType === 1) {
if (el.matches && el.matches(sel)) return el;
el = el.parentElement;
}
return null;
}
function getTipText(defEl) {
if (!defEl || !defEl.getAttribute) return "";
var t = defEl.getAttribute(TIP_ATTR);
if (t == null) return "";
// Treat pure-whitespace as empty.
t = String(t);
if (!t.replace(/\s+/g, "")) return "";
return t;
}
function getLinkTitle(defEl) {
if (!defEl || !defEl.getAttribute) return "";
var t = defEl.getAttribute(LINK_ATTR);
if (t == null) return "";
t = String(t).trim();
return t;
}
function ensureTipEl() {
if (tipEl) return tipEl;
var el = document.createElement("div");
el.className = "sv-def-tip sv-hidden";
el.setAttribute("role", "tooltip");
el.setAttribute("aria-hidden", "true");
// Minimal inline safety styling (CSS can override via .sv-def-tip).
// This is intentionally conservative so it still works even if site CSS
// hasn’t loaded yet.
el.style.position = "fixed";
el.style.zIndex = "99999";
el.style.maxWidth = "360px";
el.style.boxSizing = "border-box";
el.style.padding = "8px 10px";
el.style.borderRadius = "10px";
el.style.background = "rgba(10, 14, 22, 0.92)";
el.style.color = "#fff";
el.style.fontSize = "14px";
el.style.lineHeight = "1.25";
el.style.whiteSpace = "pre-line";
el.style.pointerEvents = "none"; // enabled only when pinned
el.style.display = "none";
document.body.appendChild(el);
tipEl = el;
return tipEl;
}
function setHidden(el, hidden) {
if (!el) return;
if (hidden) {
el.classList.add("sv-hidden");
el.setAttribute("aria-hidden", "true");
el.style.display = "none";
} else {
el.classList.remove("sv-hidden");
el.setAttribute("aria-hidden", "false");
el.style.display = "block";
}
}
function clamp(n, min, max) {
if (n < min) return min;
if (n > max) return max;
return n;
}
function positionTipAt(clientX, clientY) {
var el = ensureTipEl();
if (!el) return;
// First, ensure it’s measurable.
// (If we just turned it on, width/height might be 0 until next frame.)
var vw = window.innerWidth || document.documentElement.clientWidth || 0;
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
// Gap rules:
// - hover: larger vertical gap so cursor doesn’t cover it
// - pinned: tighter but still separated from the tap point
var gapY = state.pinned ? 10 : 16;
var gapX = 12;
// Two-pass: set an initial position, then refine once we have rect.
el.style.left = "0px";
el.style.top = "0px";
// Defer measuring to allow layout to settle.
requestAnimationFrame(function () {
if (!state.visible) return;
var r = el.getBoundingClientRect();
var w = r.width || 240;
var h = r.height || 60;
// Prefer above cursor/tap.
var left = clientX - w / 2;
var top = clientY - h - gapY;
// Clamp horizontally.
left = clamp(left, gapX, Math.max(gapX, vw - w - gapX));
// If not enough space above, flip below.
if (top < gapY) top = clientY + gapY;
// Clamp vertically.
top = clamp(top, gapY, Math.max(gapY, vh - h - gapY));
el.style.left = Math.round(left) + "px";
el.style.top = Math.round(top) + "px";
});
}
function showTip(defEl, text, clientX, clientY, pinned) {
var el = ensureTipEl();
if (!el) return;
state.visible = true;
state.pinned = !!pinned;
state.anchor = defEl || null;
state.lastX = clientX || 0;
state.lastY = clientY || 0;
// Text-only (safe).
el.textContent = text;
// Pointer events only when pinned (so pinned tooltip can be interacted with later if needed).
el.style.pointerEvents = state.pinned ? "auto" : "none";
el.classList.toggle("sv-def-tip--pinned", state.pinned);
setHidden(el, false);
positionTipAt(state.lastX, state.lastY);
}
function hideTip() {
if (!tipEl) return;
state.visible = false;
state.pinned = false;
state.anchor = null;
setHidden(tipEl, true);
}
function navigateToTitle(title) {
if (!title) return;
// MediaWiki-friendly navigation if available.
if (window.mw && mw.util && typeof mw.util.getUrl === "function") {
window.location.href = mw.util.getUrl(title);
return;
}
// Fallback: treat as relative.
window.location.href = String(title);
}
function sameAnchor(el) {
return !!(el && state.anchor && el === state.anchor);
}
function shouldSuppressClick() {
return Date.now() < state.suppressClickUntil;
}
/* ------------------------------------------------------------------ */
/* Desktop hover behavior (only when hover-capable) */
/* ------------------------------------------------------------------ */
if (HOVER_CAPABLE) {
document.addEventListener(
"mouseover",
function (e) {
if (state.pinned) return; // pinned wins
var defEl = closest(e.target, DEF_SEL);
if (!defEl) {
// If moving over non-def content, close hover tooltip (if any).
if (state.visible && !state.pinned) hideTip();
return;
}
// Avoid re-triggering within the same element.
var rel = e.relatedTarget;
if (rel && defEl.contains(rel)) return;
var text = getTipText(defEl);
if (!text) {
// Blank tip closes any active hover tooltip.
if (state.visible && !state.pinned) hideTip();
return;
}
showTip(defEl, text, e.clientX, e.clientY, false);
},
true
);
document.addEventListener(
"mousemove",
function (e) {
if (!state.visible || state.pinned) return;
// Only track if we’re still “on” the same anchor.
var defEl = closest(e.target, DEF_SEL);
if (!defEl || !sameAnchor(defEl)) return;
state.lastX = e.clientX;
state.lastY = e.clientY;
positionTipAt(state.lastX, state.lastY);
},
true
);
document.addEventListener(
"mouseout",
function (e) {
if (!state.visible || state.pinned) return;
var defEl = closest(e.target, DEF_SEL);
if (!defEl || !sameAnchor(defEl)) return;
var rel = e.relatedTarget;
if (rel && defEl.contains(rel)) return; // still within the definition element
hideTip();
},
true
);
}
/* ------------------------------------------------------------------ */
/* Touch / click behavior */
/* ------------------------------------------------------------------ */
// Pointerdown is the best universal “tap” detector.
document.addEventListener(
"pointerdown",
function (e) {
var defEl = closest(e.target, DEF_SEL);
if (!defEl) return;
var text = getTipText(defEl);
if (!text) {
// Blank def: close if open.
if (state.visible) hideTip();
return;
}
// Touch/pen: pin tooltip near tap.
// Mouse: we don’t pin on pointerdown (let hover handle it); clicking can toggle pin.
var isTouchLike = e.pointerType && e.pointerType !== "mouse";
if (!isTouchLike) return;
// Prevent synthetic click immediately after a touch pin (avoids double-toggling).
state.suppressClickUntil = Date.now() + 450;
// If already pinned on same anchor and it has a link, a second tap navigates.
var link = getLinkTitle(defEl);
if (state.pinned && sameAnchor(defEl) && link) {
e.preventDefault();
navigateToTitle(link);
return;
}
e.preventDefault();
showTip(defEl, text, e.clientX, e.clientY, true);
},
true
);
// Click: desktop toggle pin (no link) OR navigate (has link).
document.addEventListener(
"click",
function (e) {
if (shouldSuppressClick()) return;
var defEl = closest(e.target, DEF_SEL);
if (!defEl) return;
var text = getTipText(defEl);
if (!text) {
if (state.visible) hideTip();
return;
}
var link = getLinkTitle(defEl);
// If link exists: navigate.
if (link) {
// If tooltip is pinned on this item, allow click to navigate (expected “future” behavior).
e.preventDefault();
navigateToTitle(link);
return;
}
// No link: toggle pinned state on desktop.
e.preventDefault();
if (state.pinned && sameAnchor(defEl)) {
hideTip();
} else {
showTip(defEl, text, e.clientX, e.clientY, true);
}
},
true
);
/* ------------------------------------------------------------------ */
/* Dismissal behavior */
/* ------------------------------------------------------------------ */
// Click outside closes pinned tooltip.
document.addEventListener(
"click",
function (e) {
if (!state.visible || !state.pinned) return;
// Click inside tooltip should not close it.
if (tipEl && (e.target === tipEl || (tipEl.contains && tipEl.contains(e.target)))) return;
// Click on a definition is handled by the click handler above.
if (closest(e.target, DEF_SEL)) return;
hideTip();
},
true
);
// Escape closes any tooltip (pinned or hover).
document.addEventListener(
"keydown",
function (e) {
if (e.key !== "Escape") return;
if (state.visible) hideTip();
},
true
);
// If the page scrolls while pinned, keep it near the last anchor/tap point.
window.addEventListener(
"scroll",
function () {
if (!state.visible) return;
positionTipAt(state.lastX, state.lastY);
},
true
);
window.addEventListener(
"resize",
function () {
if (!state.visible) return;
positionTipAt(state.lastX, state.lastY);
},
true
);
/* ------------------------------------------------------------------ */
/* Init hook */
/* ------------------------------------------------------------------ */
// This module is fully event-delegated; no per-node init required.
// Still, we keep the MediaWiki hook so this file matches your site pattern.
function initAll(root) {
// No-op on purpose (delegated listeners already installed once).
// Placeholder for future expansions (e.g., prewarming tooltip element).
void root;
}
if (window.mw && mw.hook) {
mw.hook("wikipage.content").add(function ($content) {
initAll($content && $content[0]);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
initAll(document);
});
} else {
initAll(document);
}
})();