MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary |
No edit summary |
||
| Line 2: | Line 2: | ||
Phase 4.1 rebuild (incremental) | Phase 4.1 rebuild (incremental) | ||
Included (standalone modules): | |||
1) Definitions (v1) | 1) Definitions (v1) | ||
2) Level Selector / Descriptions (data-series) | 2) Level Selector / Descriptions (data-series) | ||
3) Popups (Phase 4.1 span toggles + legacy <details> support) | |||
Not included yet: | Not included yet: | ||
- Tabs | - Tabs | ||
*/ | */ | ||
| Line 16: | Line 16: | ||
/* ================================================================== */ | /* ================================================================== */ | ||
var SV = (window.SV = window.SV || {}); | var SV = (window.SV = window.SV || {}); | ||
var COMMON = (SV.common = SV.common || {}); | var COMMON = (SV.common = SV.common || {}); | ||
if (COMMON.definitionsInit) return; | if (COMMON.definitionsInit) return; | ||
COMMON.definitionsInit = 1; | COMMON.definitionsInit = 1; | ||
var DEF_SEL = ".sv-def"; | var DEF_SEL = ".sv-def"; | ||
var TIP_ATTR = "data-sv-def-tip"; | var TIP_ATTR = "data-sv-def-tip"; | ||
var LINK_ATTR = "data-sv-def-link"; | var LINK_ATTR = "data-sv-def-link"; | ||
var HOVER_CAPABLE = false; | var HOVER_CAPABLE = false; | ||
| Line 42: | Line 33: | ||
HOVER_CAPABLE = false; | HOVER_CAPABLE = false; | ||
} | } | ||
var tipEl = null; | var tipEl = null; | ||
| Line 52: | Line 39: | ||
visible: false, | visible: false, | ||
pinned: false, | pinned: false, | ||
anchor: null, | anchor: null, | ||
lastX: 0, | lastX: 0, | ||
lastY: 0, | lastY: 0, | ||
suppressClickUntil: 0, | suppressClickUntil: 0, | ||
}; | }; | ||
function closest(el, sel) { | function closest(el, sel) { | ||
| Line 85: | Line 68: | ||
var t = defEl.getAttribute(LINK_ATTR); | var t = defEl.getAttribute(LINK_ATTR); | ||
if (t == null) return ""; | if (t == null) return ""; | ||
return String(t).trim(); | |||
} | } | ||
| Line 97: | Line 79: | ||
el.setAttribute("aria-hidden", "true"); | el.setAttribute("aria-hidden", "true"); | ||
el.style.position = "fixed"; | el.style.position = "fixed"; | ||
el.style.zIndex = "99999"; | el.style.zIndex = "99999"; | ||
| Line 109: | Line 90: | ||
el.style.lineHeight = "1.25"; | el.style.lineHeight = "1.25"; | ||
el.style.whiteSpace = "pre-line"; | el.style.whiteSpace = "pre-line"; | ||
el.style.pointerEvents = "none"; | el.style.pointerEvents = "none"; | ||
el.style.display = "none"; | el.style.display = "none"; | ||
| Line 160: | Line 141: | ||
left = clamp(left, gapX, Math.max(gapX, vw - w - gapX)); | left = clamp(left, gapX, Math.max(gapX, vw - w - gapX)); | ||
if (top < gapY) top = clientY + gapY; | if (top < gapY) top = clientY + gapY; | ||
top = clamp(top, gapY, Math.max(gapY, vh - h - gapY)); | top = clamp(top, gapY, Math.max(gapY, vh - h - gapY)); | ||
| Line 191: | Line 170: | ||
function hideTip() { | function hideTip() { | ||
if (!tipEl) return; | if (!tipEl) return; | ||
state.visible = false; | state.visible = false; | ||
state.pinned = false; | state.pinned = false; | ||
state.anchor = null; | state.anchor = null; | ||
setHidden(tipEl, true); | setHidden(tipEl, true); | ||
} | } | ||
| Line 201: | Line 178: | ||
function navigateToTitle(title) { | function navigateToTitle(title) { | ||
if (!title) return; | if (!title) return; | ||
if (window.mw && mw.util && typeof mw.util.getUrl === "function") { | if (window.mw && mw.util && typeof mw.util.getUrl === "function") { | ||
window.location.href = mw.util.getUrl(title); | window.location.href = mw.util.getUrl(title); | ||
return; | return; | ||
} | } | ||
window.location.href = String(title); | window.location.href = String(title); | ||
} | } | ||
| Line 217: | Line 192: | ||
return Date.now() < state.suppressClickUntil; | return Date.now() < state.suppressClickUntil; | ||
} | } | ||
if (HOVER_CAPABLE) { | if (HOVER_CAPABLE) { | ||
| Line 252: | Line 223: | ||
function (e) { | function (e) { | ||
if (!state.visible || state.pinned) return; | if (!state.visible || state.pinned) return; | ||
var defEl = closest(e.target, DEF_SEL); | var defEl = closest(e.target, DEF_SEL); | ||
if (!defEl || !sameAnchor(defEl)) return; | if (!defEl || !sameAnchor(defEl)) return; | ||
| Line 279: | Line 249: | ||
); | ); | ||
} | } | ||
document.addEventListener( | document.addEventListener( | ||
| Line 329: | Line 295: | ||
var link = getLinkTitle(defEl); | var link = getLinkTitle(defEl); | ||
if (link) { | if (link) { | ||
e.preventDefault(); | e.preventDefault(); | ||
| Line 337: | Line 302: | ||
e.preventDefault(); | e.preventDefault(); | ||
if (state.pinned && sameAnchor(defEl)) hideTip(); | if (state.pinned && sameAnchor(defEl)) hideTip(); | ||
else showTip(defEl, text, e.clientX, e.clientY, true); | else showTip(defEl, text, e.clientX, e.clientY, true); | ||
| Line 343: | Line 307: | ||
true | true | ||
); | ); | ||
document.addEventListener( | document.addEventListener( | ||
| Line 353: | Line 313: | ||
if (!state.visible || !state.pinned) return; | if (!state.visible || !state.pinned) return; | ||
if (tipEl && (e.target === tipEl || (tipEl.contains && tipEl.contains(e.target)))) return; | if ( | ||
tipEl && | |||
(e.target === tipEl || (tipEl.contains && tipEl.contains(e.target))) | |||
) | |||
return; | |||
if (closest(e.target, DEF_SEL)) return; | if (closest(e.target, DEF_SEL)) return; | ||
| Line 388: | Line 353: | ||
); | ); | ||
function initAllDefs(root) { | function initAllDefs(root) { | ||
void root; | void root; | ||
| Line 413: | Line 377: | ||
/* ================================================================== */ | /* ================================================================== */ | ||
var SV = (window.SV = window.SV || {}); | var SV = (window.SV = window.SV || {}); | ||
var COMMON = (SV.common = SV.common || {}); | var COMMON = (SV.common = SV.common || {}); | ||
if (COMMON.levelsInit) return; | if (COMMON.levelsInit) return; | ||
COMMON.levelsInit = 1; | COMMON.levelsInit = 1; | ||
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card"; | var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card"; | ||
var LEVEL_RANGE_SEL = | var LEVEL_RANGE_SEL = | ||
"input.sv-level-range[type='range'], .sv-level-range--custom[data-sv-slider='1']"; | "input.sv-level-range[type='range'], .sv-level-range--custom[data-sv-slider='1']"; | ||
var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level"; | var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level"; | ||
var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom"; | var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom"; | ||
var SERIES_SEL = "[data-series]"; | var SERIES_SEL = "[data-series]"; | ||
var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null; | var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null; | ||
var _drag = null; | var _drag = null; | ||
function clampInt(value, min, max, fallback) { | function clampInt(value, min, max, fallback) { | ||
| Line 493: | Line 441: | ||
if (boundaryNode === el) return false; | if (boundaryNode === el) return false; | ||
if (!boundaryNode.compareDocumentPosition) return true; | if (!boundaryNode.compareDocumentPosition) return true; | ||
return (boundaryNode.compareDocumentPosition(el) & 4) !== 0; | return (boundaryNode.compareDocumentPosition(el) & 4) !== 0; | ||
} | } | ||
| Line 503: | Line 451: | ||
return !!(el && el.getAttribute && el.getAttribute("data-sv-slider") === "1"); | return !!(el && el.getAttribute && el.getAttribute("data-sv-slider") === "1"); | ||
} | } | ||
function getCardMaxLevel(card) { | function getCardMaxLevel(card) { | ||
| Line 521: | Line 465: | ||
function getLevelBoundary(slider) { | function getLevelBoundary(slider) { | ||
if (!slider) return null; | if (!slider) return null; | ||
return closest(slider, LEVEL_BOUNDARY_SEL) || slider; | return closest(slider, LEVEL_BOUNDARY_SEL) || slider; | ||
| Line 545: | Line 488: | ||
for (var i = 0; i < nodes.length; i++) { | for (var i = 0; i < nodes.length; i++) { | ||
var el = nodes[i]; | var el = nodes[i]; | ||
if (boundary && !isAfter(boundary, el)) continue; | if (boundary && !isAfter(boundary, el)) continue; | ||
| Line 562: | Line 503: | ||
} | } | ||
} | } | ||
function getCustomMin(slider) { | function getCustomMin(slider) { | ||
| Line 644: | Line 583: | ||
var maxLevel = getCardMaxLevel(card); | var maxLevel = getCardMaxLevel(card); | ||
var slider = getLevelSlider(card); | var slider = getLevelSlider(card); | ||
var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel)); | var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel)); | ||
card.setAttribute("data-level", String(level)); | card.setAttribute("data-level", String(level)); | ||
setLevelNumber(card, level); | setLevelNumber(card, level); | ||
if (slider && maxLevel > 1) { | if (slider && maxLevel > 1) { | ||
if (isRangeInput(slider)) { | if (isRangeInput(slider)) { | ||
| Line 663: | Line 599: | ||
} | } | ||
var boundary = getLevelBoundary(slider); | var boundary = getLevelBoundary(slider); | ||
var scope = getLevelScopeContainer(card, boundary); | var scope = getLevelScopeContainer(card, boundary); | ||
| Line 683: | Line 616: | ||
var start = getCardLevel(card, maxLevel); | var start = getCardLevel(card, maxLevel); | ||
if (slider && maxLevel > 1) { | if (slider && maxLevel > 1) { | ||
if (isRangeInput(slider)) start = slider.value; | if (isRangeInput(slider)) start = slider.value; | ||
| Line 694: | Line 626: | ||
} | } | ||
document.addEventListener( | document.addEventListener( | ||
"input", | "input", | ||
| Line 714: | Line 641: | ||
); | ); | ||
document.addEventListener( | document.addEventListener( | ||
"pointerdown", | "pointerdown", | ||
| Line 728: | Line 654: | ||
e.preventDefault(); | e.preventDefault(); | ||
if (slider.focus) slider.focus(); | if (slider.focus) slider.focus(); | ||
| Line 770: | Line 695: | ||
document.addEventListener("pointercancel", endDrag, true); | document.addEventListener("pointercancel", endDrag, true); | ||
document.addEventListener( | document.addEventListener( | ||
"keydown", | "keydown", | ||
| Line 816: | Line 740: | ||
true | true | ||
); | ); | ||
function initAllLevels(root) { | function initAllLevels(root) { | ||
| Line 837: | Line 757: | ||
} else { | } else { | ||
initAllLevels(document); | initAllLevels(document); | ||
} | |||
})(); | |||
(function () { | |||
/* ================================================================== */ | |||
/* MODULE: Popups */ | |||
/* ================================================================== */ | |||
var SV = (window.SV = window.SV || {}); | |||
var COMMON = (SV.common = SV.common || {}); | |||
if (COMMON.popupsInit) return; | |||
COMMON.popupsInit = 1; | |||
/* ------------------------------------------------------------------ */ | |||
/* Selectors */ | |||
/* ------------------------------------------------------------------ */ | |||
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card"; | |||
// Phase 4.1 markup: span toggles + div popovers; legacy <details> supported | |||
var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose"; // legacy | |||
var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose"; | |||
var POPUP_BTN_SEL = | |||
".sv-tip-btn[data-sv-toggle='1'], .sv-disclose-btn[data-sv-toggle='1']"; | |||
var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop"; | |||
var POPUP_HIDDEN_CLASS = "sv-hidden"; | |||
/* ------------------------------------------------------------------ */ | |||
/* 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 hasClass(el, cls) { | |||
return !!(el && el.classList && el.classList.contains(cls)); | |||
} | |||
/* ------------------------------------------------------------------ */ | |||
/* Phase 4.1 button popovers */ | |||
/* ------------------------------------------------------------------ */ | |||
function hidePop(pop) { | |||
if (!pop) return; | |||
if (pop.classList) pop.classList.add(POPUP_HIDDEN_CLASS); | |||
pop.setAttribute("hidden", "hidden"); | |||
} | |||
function showPop(pop) { | |||
if (!pop) return; | |||
if (pop.classList) pop.classList.remove(POPUP_HIDDEN_CLASS); | |||
pop.removeAttribute("hidden"); | |||
} | |||
function getPopFromBtn(btn) { | |||
if (!btn) return null; | |||
var id = btn.getAttribute("aria-controls"); | |||
if (!id) return null; | |||
// Prefer local scope so ids never collide across cards. | |||
var wrap = closest(btn, POPUP_WRAP_SEL); | |||
if (wrap) { | |||
var local = wrap.querySelector("#" + id); | |||
if (local) return local; | |||
} | |||
return document.getElementById(id); | |||
} | |||
function isBtnExpanded(btn) { | |||
return btn && btn.getAttribute("aria-expanded") === "true"; | |||
} | |||
function setBtnExpanded(btn, expanded) { | |||
if (!btn) return; | |||
btn.setAttribute("aria-expanded", expanded ? "true" : "false"); | |||
} | |||
function closeBtnPopup(btn) { | |||
var pop = getPopFromBtn(btn); | |||
hidePop(pop); | |||
setBtnExpanded(btn, false); | |||
} | |||
function openBtnPopup(btn) { | |||
var pop = getPopFromBtn(btn); | |||
showPop(pop); | |||
setBtnExpanded(btn, true); | |||
} | |||
function closeAllBtnPopups(scope) { | |||
var root = scope || document; | |||
var openBtns = root.querySelectorAll( | |||
POPUP_BTN_SEL + "[aria-expanded='true']" | |||
); | |||
for (var i = 0; i < openBtns.length; i++) closeBtnPopup(openBtns[i]); | |||
} | |||
function closeOtherBtnPopupsWithinCard(btn) { | |||
var card = closest(btn, CARD_SEL); | |||
var scope = card || document; | |||
var openBtns = scope.querySelectorAll( | |||
POPUP_BTN_SEL + "[aria-expanded='true']" | |||
); | |||
for (var i = 0; i < openBtns.length; i++) { | |||
if (openBtns[i] !== btn) closeBtnPopup(openBtns[i]); | |||
} | |||
} | |||
function normalizeBtnPopups(container) { | |||
var root = container || document; | |||
// Ensure .sv-hidden popovers also carry [hidden] for safety. | |||
var pops = root.querySelectorAll(POPUP_POP_SEL); | |||
for (var i = 0; i < pops.length; i++) { | |||
var p = pops[i]; | |||
if (hasClass(p, POPUP_HIDDEN_CLASS) && !p.hasAttribute("hidden")) { | |||
p.setAttribute("hidden", "hidden"); | |||
} | |||
} | |||
// Ensure aria-expanded matches actual visibility. | |||
var btns = root.querySelectorAll(POPUP_BTN_SEL); | |||
for (var j = 0; j < btns.length; j++) { | |||
var b = btns[j]; | |||
var pop = getPopFromBtn(b); | |||
var expanded = pop | |||
? !hasClass(pop, POPUP_HIDDEN_CLASS) && !pop.hasAttribute("hidden") | |||
: false; | |||
setBtnExpanded(b, expanded); | |||
} | |||
} | |||
/* ------------------------------------------------------------------ */ | |||
/* Legacy <details> support */ | |||
/* ------------------------------------------------------------------ */ | |||
function closeDetails(d) { | |||
if (d && d.open) d.open = false; | |||
} | |||
function closeAllDetails(scope) { | |||
var root = scope || document; | |||
var openOnes = root.querySelectorAll(POPUP_DETAILS_SEL + "[open]"); | |||
for (var i = 0; i < openOnes.length; i++) closeDetails(openOnes[i]); | |||
} | |||
function syncPopupAria(detailsEl) { | |||
var summary = detailsEl.querySelector("summary"); | |||
if (!summary) return; | |||
summary.setAttribute("aria-expanded", detailsEl.open ? "true" : "false"); | |||
var pop = detailsEl.querySelector(".sv-tip-pop, .sv-disclose-pop"); | |||
if (pop) { | |||
if (!pop.id) pop.id = "sv-pop-" + Math.random().toString(36).slice(2); | |||
summary.setAttribute("aria-controls", pop.id); | |||
} | |||
} | |||
/* ------------------------------------------------------------------ */ | |||
/* Click toggle (buttons) */ | |||
/* ------------------------------------------------------------------ */ | |||
document.addEventListener( | |||
"click", | |||
function (e) { | |||
var btn = | |||
e.target && e.target.closest ? e.target.closest(POPUP_BTN_SEL) : null; | |||
if (!btn) return; | |||
e.preventDefault(); | |||
if (isBtnExpanded(btn)) { | |||
closeBtnPopup(btn); | |||
return; | |||
} | |||
closeOtherBtnPopupsWithinCard(btn); | |||
openBtnPopup(btn); | |||
}, | |||
true | |||
); | |||
/* Close on popover head click (touch-friendly) */ | |||
document.addEventListener( | |||
"click", | |||
function (e) { | |||
var head = | |||
e.target && e.target.closest | |||
? e.target.closest(".sv-tip-pop-head, .sv-disclose-pop-head") | |||
: null; | |||
if (!head) return; | |||
var wrap = closest(head, POPUP_WRAP_SEL); | |||
if (!wrap) return; | |||
var btn = wrap.querySelector(POPUP_BTN_SEL); | |||
if (!btn) return; | |||
e.preventDefault(); | |||
closeBtnPopup(btn); | |||
}, | |||
true | |||
); | |||
/* Keyboard toggle (Enter/Space) */ | |||
document.addEventListener( | |||
"keydown", | |||
function (e) { | |||
if (!(e.key === "Enter" || e.key === " " || e.key === "Spacebar")) return; | |||
var btn = | |||
document.activeElement && document.activeElement.closest | |||
? document.activeElement.closest(POPUP_BTN_SEL) | |||
: null; | |||
if (!btn) return; | |||
e.preventDefault(); | |||
if (isBtnExpanded(btn)) closeBtnPopup(btn); | |||
else { | |||
closeOtherBtnPopupsWithinCard(btn); | |||
openBtnPopup(btn); | |||
} | |||
}, | |||
true | |||
); | |||
/* ------------------------------------------------------------------ */ | |||
/* Hover / rollover (desktop only) */ | |||
/* ------------------------------------------------------------------ */ | |||
var _hoverCapable = false; | |||
try { | |||
_hoverCapable = | |||
window.matchMedia && | |||
window.matchMedia("(hover:hover) and (pointer:fine)").matches; | |||
} catch (e) { | |||
_hoverCapable = false; | |||
} | |||
function _wrapFromTarget(t) { | |||
return t && t.closest ? t.closest(POPUP_WRAP_SEL) : null; | |||
} | |||
function _btnFromWrap(wrap) { | |||
return wrap ? wrap.querySelector(POPUP_BTN_SEL) : null; | |||
} | |||
if (_hoverCapable) { | |||
document.addEventListener( | |||
"mouseover", | |||
function (e) { | |||
var wrap = _wrapFromTarget(e.target); | |||
if (!wrap) return; | |||
var rel = e.relatedTarget; | |||
if (rel && wrap.contains(rel)) return; | |||
var btn = _btnFromWrap(wrap); | |||
if (!btn) return; | |||
if (!isBtnExpanded(btn)) { | |||
closeOtherBtnPopupsWithinCard(btn); | |||
openBtnPopup(btn); | |||
} | |||
}, | |||
true | |||
); | |||
document.addEventListener( | |||
"mouseout", | |||
function (e) { | |||
var wrap = _wrapFromTarget(e.target); | |||
if (!wrap) return; | |||
var rel = e.relatedTarget; | |||
if (rel && wrap.contains(rel)) return; | |||
var btn = _btnFromWrap(wrap); | |||
if (!btn) return; | |||
if (isBtnExpanded(btn)) closeBtnPopup(btn); | |||
}, | |||
true | |||
); | |||
} | |||
/* ------------------------------------------------------------------ */ | |||
/* Legacy <details> toggle (delegated) */ | |||
/* ------------------------------------------------------------------ */ | |||
document.addEventListener( | |||
"toggle", | |||
function (e) { | |||
var d = e.target; | |||
if (!d || d.nodeType !== 1) return; | |||
if (!d.matches || !d.matches(POPUP_DETAILS_SEL)) return; | |||
if (d.open) { | |||
var card = closest(d, CARD_SEL); | |||
var scope = card || document; | |||
var openOnes = scope.querySelectorAll(POPUP_DETAILS_SEL + "[open]"); | |||
for (var i = 0; i < openOnes.length; i++) { | |||
if (openOnes[i] !== d) closeDetails(openOnes[i]); | |||
} | |||
} | |||
syncPopupAria(d); | |||
}, | |||
true | |||
); | |||
/* ------------------------------------------------------------------ */ | |||
/* Global close rules */ | |||
/* ------------------------------------------------------------------ */ | |||
document.addEventListener( | |||
"click", | |||
function (e) { | |||
var t = e.target; | |||
if (!t) return; | |||
// click inside any popup wrapper => don’t close | |||
if (closest(t, POPUP_WRAP_SEL)) return; | |||
closeAllBtnPopups(document); | |||
closeAllDetails(document); | |||
}, | |||
true | |||
); | |||
document.addEventListener( | |||
"keydown", | |||
function (e) { | |||
if (e.key !== "Escape") return; | |||
closeAllBtnPopups(document); | |||
closeAllDetails(document); | |||
}, | |||
true | |||
); | |||
/* ------------------------------------------------------------------ */ | |||
/* Init (normalize aria/hidden) */ | |||
/* ------------------------------------------------------------------ */ | |||
function initAllPopups(root) { | |||
var container = root || document; | |||
normalizeBtnPopups(container); | |||
var ds = container.querySelectorAll(POPUP_DETAILS_SEL); | |||
for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]); | |||
} | |||
if (window.mw && mw.hook) { | |||
mw.hook("wikipage.content").add(function ($content) { | |||
initAllPopups($content && $content[0]); | |||
}); | |||
} | |||
if (document.readyState === "loading") { | |||
document.addEventListener("DOMContentLoaded", function () { | |||
initAllPopups(document); | |||
}); | |||
} else { | |||
initAllPopups(document); | |||
} | } | ||
})(); | })(); | ||
Revision as of 19:25, 19 February 2026
/* Common.js — Spirit Vale Wiki
Phase 4.1 rebuild (incremental)
Included (standalone modules):
1) Definitions (v1)
2) Level Selector / Descriptions (data-series)
3) Popups (Phase 4.1 span toggles + legacy <details> support)
Not included yet:
- Tabs
*/
(function () {
/* ================================================================== */
/* MODULE: Definitions (v1) */
/* ================================================================== */
var SV = (window.SV = window.SV || {});
var COMMON = (SV.common = SV.common || {});
if (COMMON.definitionsInit) return;
COMMON.definitionsInit = 1;
var DEF_SEL = ".sv-def";
var TIP_ATTR = "data-sv-def-tip";
var LINK_ATTR = "data-sv-def-link";
var HOVER_CAPABLE = false;
try {
HOVER_CAPABLE =
!!window.matchMedia &&
window.matchMedia("(hover:hover) and (pointer:fine)").matches;
} catch (e) {
HOVER_CAPABLE = false;
}
var tipEl = null;
var state = {
visible: false,
pinned: false,
anchor: null,
lastX: 0,
lastY: 0,
suppressClickUntil: 0,
};
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 "";
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 "";
return String(t).trim();
}
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");
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";
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;
var vw = window.innerWidth || document.documentElement.clientWidth || 0;
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
var gapY = state.pinned ? 10 : 16;
var gapX = 12;
el.style.left = "0px";
el.style.top = "0px";
requestAnimationFrame(function () {
if (!state.visible) return;
var r = el.getBoundingClientRect();
var w = r.width || 240;
var h = r.height || 60;
var left = clientX - w / 2;
var top = clientY - h - gapY;
left = clamp(left, gapX, Math.max(gapX, vw - w - gapX));
if (top < gapY) top = clientY + gapY;
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;
el.textContent = text;
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;
if (window.mw && mw.util && typeof mw.util.getUrl === "function") {
window.location.href = mw.util.getUrl(title);
return;
}
window.location.href = String(title);
}
function sameAnchor(el) {
return !!(el && state.anchor && el === state.anchor);
}
function shouldSuppressClick() {
return Date.now() < state.suppressClickUntil;
}
if (HOVER_CAPABLE) {
document.addEventListener(
"mouseover",
function (e) {
if (state.pinned) return;
var defEl = closest(e.target, DEF_SEL);
if (!defEl) {
if (state.visible && !state.pinned) hideTip();
return;
}
var rel = e.relatedTarget;
if (rel && defEl.contains(rel)) return;
var text = getTipText(defEl);
if (!text) {
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;
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;
hideTip();
},
true
);
}
document.addEventListener(
"pointerdown",
function (e) {
var defEl = closest(e.target, DEF_SEL);
if (!defEl) return;
var text = getTipText(defEl);
if (!text) {
if (state.visible) hideTip();
return;
}
var isTouchLike = e.pointerType && e.pointerType !== "mouse";
if (!isTouchLike) return;
state.suppressClickUntil = Date.now() + 450;
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
);
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) {
e.preventDefault();
navigateToTitle(link);
return;
}
e.preventDefault();
if (state.pinned && sameAnchor(defEl)) hideTip();
else showTip(defEl, text, e.clientX, e.clientY, true);
},
true
);
document.addEventListener(
"click",
function (e) {
if (!state.visible || !state.pinned) return;
if (
tipEl &&
(e.target === tipEl || (tipEl.contains && tipEl.contains(e.target)))
)
return;
if (closest(e.target, DEF_SEL)) return;
hideTip();
},
true
);
document.addEventListener(
"keydown",
function (e) {
if (e.key !== "Escape") return;
if (state.visible) hideTip();
},
true
);
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
);
function initAllDefs(root) {
void root;
}
if (window.mw && mw.hook) {
mw.hook("wikipage.content").add(function ($content) {
initAllDefs($content && $content[0]);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
initAllDefs(document);
});
} else {
initAllDefs(document);
}
})();
(function () {
/* ================================================================== */
/* MODULE: Level Selector / Descriptions (data-series) */
/* ================================================================== */
var SV = (window.SV = window.SV || {});
var COMMON = (SV.common = SV.common || {});
if (COMMON.levelsInit) return;
COMMON.levelsInit = 1;
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
var LEVEL_RANGE_SEL =
"input.sv-level-range[type='range'], .sv-level-range--custom[data-sv-slider='1']";
var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom";
var SERIES_SEL = "[data-series]";
var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
var _drag = null;
function clampInt(value, min, max, fallback) {
var n = parseInt(value, 10);
if (isNaN(n)) n = fallback != null ? fallback : min;
if (n < min) return min;
if (n > max) return max;
return n;
}
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 parseSeries(el) {
if (!el) return null;
if (_seriesCache) {
if (_seriesCache.has(el)) return _seriesCache.get(el);
} else if (el._svSeries !== undefined) {
return el._svSeries;
}
var raw = el.getAttribute("data-series");
var parsed = null;
if (raw != null && raw !== "") {
try {
parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) parsed = null;
} catch (e) {
parsed = null;
}
}
if (_seriesCache) _seriesCache.set(el, parsed);
else el._svSeries = parsed;
return parsed;
}
function isAfter(boundaryNode, el) {
if (!boundaryNode || !el) return true;
if (boundaryNode === el) return false;
if (!boundaryNode.compareDocumentPosition) return true;
return (boundaryNode.compareDocumentPosition(el) & 4) !== 0;
}
function isRangeInput(el) {
return !!(el && el.tagName === "INPUT" && el.type === "range");
}
function isCustomSlider(el) {
return !!(el && el.getAttribute && el.getAttribute("data-sv-slider") === "1");
}
function getCardMaxLevel(card) {
return clampInt(card.getAttribute("data-max-level"), 1, 999, 1);
}
function getCardLevel(card, maxLevel) {
return clampInt(card.getAttribute("data-level"), 1, maxLevel, 1);
}
function getLevelSlider(card) {
return card.querySelector(LEVEL_RANGE_SEL);
}
function getLevelBoundary(slider) {
if (!slider) return null;
return closest(slider, LEVEL_BOUNDARY_SEL) || slider;
}
function getLevelScopeContainer(card, boundary) {
var scope = card.querySelector(LEVEL_SCOPE_SEL) || card;
if (boundary && scope !== card && !scope.contains(boundary)) return card;
return scope;
}
function setLevelNumber(card, level) {
var span = card.querySelector(".sv-level-num");
if (span) span.textContent = String(level);
}
function applySeriesToScope(scope, boundary, level) {
if (!scope) return;
var index = level - 1;
var nodes = scope.querySelectorAll(SERIES_SEL);
for (var i = 0; i < nodes.length; i++) {
var el = nodes[i];
if (boundary && !isAfter(boundary, el)) continue;
var series = parseSeries(el);
if (!series || series.length === 0) continue;
var safeIndex = index;
if (safeIndex >= series.length) safeIndex = series.length - 1;
if (safeIndex < 0) safeIndex = 0;
var value = series[safeIndex];
if (value == null) value = "";
el.textContent = String(value);
}
}
function getCustomMin(slider) {
return clampInt(
slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"),
1,
999,
1
);
}
function getCustomMax(slider) {
return clampInt(
slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"),
1,
999,
1
);
}
function getCustomValue(slider, fallback) {
return clampInt(
slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"),
1,
999,
fallback != null ? fallback : 1
);
}
function setCustomBounds(slider, min, max) {
slider.setAttribute("aria-valuemin", String(min));
slider.setAttribute("aria-valuemax", String(max));
slider.setAttribute("data-min", String(min));
slider.setAttribute("data-max", String(max));
}
function updateCustomVisual(slider, value) {
var min = getCustomMin(slider);
var max = getCustomMax(slider);
if (max < min) max = min;
var pct = max === min ? 0 : (value - min) / (max - min);
if (pct < 0) pct = 0;
if (pct > 1) pct = 1;
var left = pct * 100;
var thumb = slider.querySelector(".sv-level-thumb");
var fill = slider.querySelector(".sv-level-fill");
if (thumb && thumb.style) thumb.style.left = left + "%";
if (fill && fill.style) fill.style.width = left + "%";
}
function setCustomValue(slider, value) {
slider.setAttribute("aria-valuenow", String(value));
slider.setAttribute("data-value", String(value));
updateCustomVisual(slider, value);
}
function valueFromClientX(slider, clientX) {
var rect = slider.getBoundingClientRect();
var min = getCustomMin(slider);
var max = getCustomMax(slider);
if (max < min) max = min;
var w = rect.width || 1;
var x = (clientX - rect.left) / w;
if (x < 0) x = 0;
if (x > 1) x = 1;
var raw = min + x * (max - min);
var rounded = Math.round(raw);
return clampInt(rounded, min, max, min);
}
function applyLevelToCard(card, rawLevel) {
if (!card) return;
var maxLevel = getCardMaxLevel(card);
var slider = getLevelSlider(card);
var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));
card.setAttribute("data-level", String(level));
setLevelNumber(card, level);
if (slider && maxLevel > 1) {
if (isRangeInput(slider)) {
slider.setAttribute("min", "1");
slider.setAttribute("max", String(maxLevel));
if (String(slider.value) !== String(level)) slider.value = String(level);
} else if (isCustomSlider(slider)) {
setCustomBounds(slider, 1, maxLevel);
setCustomValue(slider, level);
}
}
var boundary = getLevelBoundary(slider);
var scope = getLevelScopeContainer(card, boundary);
applySeriesToScope(scope, boundary, level);
}
function initLevels(root) {
var container = root || document;
var cards = container.querySelectorAll(CARD_SEL);
for (var i = 0; i < cards.length; i++) {
var card = cards[i];
if (card.getAttribute("data-gi-level-init") === "1") continue;
var maxLevel = getCardMaxLevel(card);
var slider = getLevelSlider(card);
var start = getCardLevel(card, maxLevel);
if (slider && maxLevel > 1) {
if (isRangeInput(slider)) start = slider.value;
else if (isCustomSlider(slider)) start = getCustomValue(slider, start);
}
applyLevelToCard(card, start);
card.setAttribute("data-gi-level-init", "1");
}
}
document.addEventListener(
"input",
function (e) {
var t = e.target;
if (!t || t.nodeType !== 1) return;
if (!t.matches || !t.matches("input.sv-level-range[type='range']")) return;
var card = closest(t, CARD_SEL);
if (!card) return;
applyLevelToCard(card, t.value);
},
true
);
document.addEventListener(
"pointerdown",
function (e) {
var slider =
e.target && e.target.closest
? e.target.closest(".sv-level-range--custom[data-sv-slider='1']")
: null;
if (!slider) return;
var card = closest(slider, CARD_SEL);
if (!card) return;
e.preventDefault();
if (slider.focus) slider.focus();
_drag = { slider: slider, card: card, pointerId: e.pointerId };
if (slider.setPointerCapture) {
try {
slider.setPointerCapture(e.pointerId);
} catch (err) {}
}
applyLevelToCard(card, valueFromClientX(slider, e.clientX));
},
true
);
document.addEventListener(
"pointermove",
function (e) {
if (!_drag) return;
if (e.pointerId !== _drag.pointerId) return;
e.preventDefault();
applyLevelToCard(_drag.card, valueFromClientX(_drag.slider, e.clientX));
},
true
);
function endDrag(e) {
if (!_drag) return;
if (e && e.pointerId != null && e.pointerId !== _drag.pointerId) return;
if (_drag.slider && _drag.slider.releasePointerCapture) {
try {
_drag.slider.releasePointerCapture(_drag.pointerId);
} catch (err) {}
}
_drag = null;
}
document.addEventListener("pointerup", endDrag, true);
document.addEventListener("pointercancel", endDrag, true);
document.addEventListener(
"keydown",
function (e) {
var el = document.activeElement;
if (!el || !el.closest) return;
var slider = el.closest(".sv-level-range--custom[data-sv-slider='1']");
if (!slider) return;
var card = closest(slider, CARD_SEL);
if (!card) return;
var min = getCustomMin(slider);
var max = getCustomMax(slider);
if (max < min) max = min;
var cur = getCustomValue(slider, getCardLevel(card, max));
var next = cur;
if (e.key === "ArrowLeft" || e.key === "Left" || e.key === "ArrowDown") {
next = cur - 1;
} else if (
e.key === "ArrowRight" ||
e.key === "Right" ||
e.key === "ArrowUp"
) {
next = cur + 1;
} else if (e.key === "Home") {
next = min;
} else if (e.key === "End") {
next = max;
} else if (e.key === "PageDown") {
next = cur - 5;
} else if (e.key === "PageUp") {
next = cur + 5;
} else {
return;
}
e.preventDefault();
next = clampInt(next, min, max, cur);
applyLevelToCard(card, next);
},
true
);
function initAllLevels(root) {
initLevels(root);
}
if (window.mw && mw.hook) {
mw.hook("wikipage.content").add(function ($content) {
initAllLevels($content && $content[0]);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
initAllLevels(document);
});
} else {
initAllLevels(document);
}
})();
(function () {
/* ================================================================== */
/* MODULE: Popups */
/* ================================================================== */
var SV = (window.SV = window.SV || {});
var COMMON = (SV.common = SV.common || {});
if (COMMON.popupsInit) return;
COMMON.popupsInit = 1;
/* ------------------------------------------------------------------ */
/* Selectors */
/* ------------------------------------------------------------------ */
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
// Phase 4.1 markup: span toggles + div popovers; legacy <details> supported
var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose"; // legacy
var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
var POPUP_BTN_SEL =
".sv-tip-btn[data-sv-toggle='1'], .sv-disclose-btn[data-sv-toggle='1']";
var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
var POPUP_HIDDEN_CLASS = "sv-hidden";
/* ------------------------------------------------------------------ */
/* 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 hasClass(el, cls) {
return !!(el && el.classList && el.classList.contains(cls));
}
/* ------------------------------------------------------------------ */
/* Phase 4.1 button popovers */
/* ------------------------------------------------------------------ */
function hidePop(pop) {
if (!pop) return;
if (pop.classList) pop.classList.add(POPUP_HIDDEN_CLASS);
pop.setAttribute("hidden", "hidden");
}
function showPop(pop) {
if (!pop) return;
if (pop.classList) pop.classList.remove(POPUP_HIDDEN_CLASS);
pop.removeAttribute("hidden");
}
function getPopFromBtn(btn) {
if (!btn) return null;
var id = btn.getAttribute("aria-controls");
if (!id) return null;
// Prefer local scope so ids never collide across cards.
var wrap = closest(btn, POPUP_WRAP_SEL);
if (wrap) {
var local = wrap.querySelector("#" + id);
if (local) return local;
}
return document.getElementById(id);
}
function isBtnExpanded(btn) {
return btn && btn.getAttribute("aria-expanded") === "true";
}
function setBtnExpanded(btn, expanded) {
if (!btn) return;
btn.setAttribute("aria-expanded", expanded ? "true" : "false");
}
function closeBtnPopup(btn) {
var pop = getPopFromBtn(btn);
hidePop(pop);
setBtnExpanded(btn, false);
}
function openBtnPopup(btn) {
var pop = getPopFromBtn(btn);
showPop(pop);
setBtnExpanded(btn, true);
}
function closeAllBtnPopups(scope) {
var root = scope || document;
var openBtns = root.querySelectorAll(
POPUP_BTN_SEL + "[aria-expanded='true']"
);
for (var i = 0; i < openBtns.length; i++) closeBtnPopup(openBtns[i]);
}
function closeOtherBtnPopupsWithinCard(btn) {
var card = closest(btn, CARD_SEL);
var scope = card || document;
var openBtns = scope.querySelectorAll(
POPUP_BTN_SEL + "[aria-expanded='true']"
);
for (var i = 0; i < openBtns.length; i++) {
if (openBtns[i] !== btn) closeBtnPopup(openBtns[i]);
}
}
function normalizeBtnPopups(container) {
var root = container || document;
// Ensure .sv-hidden popovers also carry [hidden] for safety.
var pops = root.querySelectorAll(POPUP_POP_SEL);
for (var i = 0; i < pops.length; i++) {
var p = pops[i];
if (hasClass(p, POPUP_HIDDEN_CLASS) && !p.hasAttribute("hidden")) {
p.setAttribute("hidden", "hidden");
}
}
// Ensure aria-expanded matches actual visibility.
var btns = root.querySelectorAll(POPUP_BTN_SEL);
for (var j = 0; j < btns.length; j++) {
var b = btns[j];
var pop = getPopFromBtn(b);
var expanded = pop
? !hasClass(pop, POPUP_HIDDEN_CLASS) && !pop.hasAttribute("hidden")
: false;
setBtnExpanded(b, expanded);
}
}
/* ------------------------------------------------------------------ */
/* Legacy <details> support */
/* ------------------------------------------------------------------ */
function closeDetails(d) {
if (d && d.open) d.open = false;
}
function closeAllDetails(scope) {
var root = scope || document;
var openOnes = root.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
for (var i = 0; i < openOnes.length; i++) closeDetails(openOnes[i]);
}
function syncPopupAria(detailsEl) {
var summary = detailsEl.querySelector("summary");
if (!summary) return;
summary.setAttribute("aria-expanded", detailsEl.open ? "true" : "false");
var pop = detailsEl.querySelector(".sv-tip-pop, .sv-disclose-pop");
if (pop) {
if (!pop.id) pop.id = "sv-pop-" + Math.random().toString(36).slice(2);
summary.setAttribute("aria-controls", pop.id);
}
}
/* ------------------------------------------------------------------ */
/* Click toggle (buttons) */
/* ------------------------------------------------------------------ */
document.addEventListener(
"click",
function (e) {
var btn =
e.target && e.target.closest ? e.target.closest(POPUP_BTN_SEL) : null;
if (!btn) return;
e.preventDefault();
if (isBtnExpanded(btn)) {
closeBtnPopup(btn);
return;
}
closeOtherBtnPopupsWithinCard(btn);
openBtnPopup(btn);
},
true
);
/* Close on popover head click (touch-friendly) */
document.addEventListener(
"click",
function (e) {
var head =
e.target && e.target.closest
? e.target.closest(".sv-tip-pop-head, .sv-disclose-pop-head")
: null;
if (!head) return;
var wrap = closest(head, POPUP_WRAP_SEL);
if (!wrap) return;
var btn = wrap.querySelector(POPUP_BTN_SEL);
if (!btn) return;
e.preventDefault();
closeBtnPopup(btn);
},
true
);
/* Keyboard toggle (Enter/Space) */
document.addEventListener(
"keydown",
function (e) {
if (!(e.key === "Enter" || e.key === " " || e.key === "Spacebar")) return;
var btn =
document.activeElement && document.activeElement.closest
? document.activeElement.closest(POPUP_BTN_SEL)
: null;
if (!btn) return;
e.preventDefault();
if (isBtnExpanded(btn)) closeBtnPopup(btn);
else {
closeOtherBtnPopupsWithinCard(btn);
openBtnPopup(btn);
}
},
true
);
/* ------------------------------------------------------------------ */
/* Hover / rollover (desktop only) */
/* ------------------------------------------------------------------ */
var _hoverCapable = false;
try {
_hoverCapable =
window.matchMedia &&
window.matchMedia("(hover:hover) and (pointer:fine)").matches;
} catch (e) {
_hoverCapable = false;
}
function _wrapFromTarget(t) {
return t && t.closest ? t.closest(POPUP_WRAP_SEL) : null;
}
function _btnFromWrap(wrap) {
return wrap ? wrap.querySelector(POPUP_BTN_SEL) : null;
}
if (_hoverCapable) {
document.addEventListener(
"mouseover",
function (e) {
var wrap = _wrapFromTarget(e.target);
if (!wrap) return;
var rel = e.relatedTarget;
if (rel && wrap.contains(rel)) return;
var btn = _btnFromWrap(wrap);
if (!btn) return;
if (!isBtnExpanded(btn)) {
closeOtherBtnPopupsWithinCard(btn);
openBtnPopup(btn);
}
},
true
);
document.addEventListener(
"mouseout",
function (e) {
var wrap = _wrapFromTarget(e.target);
if (!wrap) return;
var rel = e.relatedTarget;
if (rel && wrap.contains(rel)) return;
var btn = _btnFromWrap(wrap);
if (!btn) return;
if (isBtnExpanded(btn)) closeBtnPopup(btn);
},
true
);
}
/* ------------------------------------------------------------------ */
/* Legacy <details> toggle (delegated) */
/* ------------------------------------------------------------------ */
document.addEventListener(
"toggle",
function (e) {
var d = e.target;
if (!d || d.nodeType !== 1) return;
if (!d.matches || !d.matches(POPUP_DETAILS_SEL)) return;
if (d.open) {
var card = closest(d, CARD_SEL);
var scope = card || document;
var openOnes = scope.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
for (var i = 0; i < openOnes.length; i++) {
if (openOnes[i] !== d) closeDetails(openOnes[i]);
}
}
syncPopupAria(d);
},
true
);
/* ------------------------------------------------------------------ */
/* Global close rules */
/* ------------------------------------------------------------------ */
document.addEventListener(
"click",
function (e) {
var t = e.target;
if (!t) return;
// click inside any popup wrapper => don’t close
if (closest(t, POPUP_WRAP_SEL)) return;
closeAllBtnPopups(document);
closeAllDetails(document);
},
true
);
document.addEventListener(
"keydown",
function (e) {
if (e.key !== "Escape") return;
closeAllBtnPopups(document);
closeAllDetails(document);
},
true
);
/* ------------------------------------------------------------------ */
/* Init (normalize aria/hidden) */
/* ------------------------------------------------------------------ */
function initAllPopups(root) {
var container = root || document;
normalizeBtnPopups(container);
var ds = container.querySelectorAll(POPUP_DETAILS_SEL);
for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]);
}
if (window.mw && mw.hook) {
mw.hook("wikipage.content").add(function ($content) {
initAllPopups($content && $content[0]);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
initAllPopups(document);
});
} else {
initAllPopups(document);
}
})();