MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary |
No edit summary |
||
| Line 5: | Line 5: | ||
if (window.SV_GAMEINFO_41_INIT) return; | if (window.SV_GAMEINFO_41_INIT) return; | ||
window.SV_GAMEINFO_41_INIT = 1; | window.SV_GAMEINFO_41_INIT = 1; | ||
/* ------------------------------------------------------------------ */ | |||
/* Selectors */ | |||
/* ------------------------------------------------------------------ */ | |||
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card"; | |||
// Level | |||
var LEVEL_RANGE_SEL = "input.sv-level-range[type='range']"; | |||
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]"; | |||
// Popups (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"; | |||
// Tabs | |||
var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']"; | |||
var TAB_SEL = ".sv-tab"; | |||
var PANEL_SEL = ".sv-tabpanel"; | |||
/* ------------------------------------------------------------------ */ | |||
/* Internals */ | |||
/* ------------------------------------------------------------------ */ | |||
var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null; | |||
var _tabsUidCounter = 0; | |||
/* ------------------------------------------------------------------ */ | |||
/* Utilities */ | |||
/* ------------------------------------------------------------------ */ | |||
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 hasClass(el, cls) { | |||
return !!(el && el.classList && el.classList.contains(cls)); | |||
} | |||
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; // following | |||
} | |||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
| Line 138: | Line 230: | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Popup | /* Popup (Phase 4.1: span toggles + legacy <details> support) */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
function hidePop(pop) { | |||
if (!pop) return; | |||
if (pop.classList) pop.classList.add(POPUP_HIDDEN_CLASS); | |||
// Also set [hidden] so it hides even if CSS is missing/late. | |||
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 scoping to the wrapper to avoid cross-card collisions. | |||
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]); | |||
} | |||
} | |||
/* ---------- Legacy <details> helpers (safe to keep) ---------------- */ | |||
function closeDetails(d) { | function closeDetails(d) { | ||
| Line 145: | Line 307: | ||
} | } | ||
function | function closeAllDetails(scope) { | ||
var root = scope || document; | var root = scope || document; | ||
var openOnes = root.querySelectorAll(POPUP_DETAILS_SEL + "[open]"); | var openOnes = root.querySelectorAll(POPUP_DETAILS_SEL + "[open]"); | ||
| Line 151: | Line 313: | ||
} | } | ||
/* ---------- Optional: keep aria-controls for legacy <details> ------ */ | |||
function syncPopupAria(detailsEl) { | function syncPopupAria(detailsEl) { | ||
| Line 175: | Line 328: | ||
} | } | ||
// details toggle (delegated) | /* ---------- Normalize popups on load ------------------------------- */ | ||
function normalizeBtnPopups(container) { | |||
var root = container || document; | |||
// Ensure popup divs that carry sv-hidden are also [hidden] so they start hidden. | |||
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); | |||
} | |||
} | |||
/* ---------- Events -------------------------------------------------- */ | |||
// Click toggle (delegated) | |||
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 | |||
); | |||
// Keyboard toggle (Enter/Space) (delegated) | |||
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 | |||
); | |||
// Legacy <details> toggle (delegated) | |||
document.addEventListener( | document.addEventListener( | ||
"toggle", | "toggle", | ||
| Line 183: | Line 409: | ||
if (!d.matches || !d.matches(POPUP_DETAILS_SEL)) return; | if (!d.matches || !d.matches(POPUP_DETAILS_SEL)) return; | ||
if (d.open) | // Close other legacy details within the same card. | ||
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); | syncPopupAria(d); | ||
}, | }, | ||
| Line 195: | Line 431: | ||
var t = e.target; | var t = e.target; | ||
if (!t) return; | if (!t) return; | ||
if (closest(t, | |||
// If click is inside any popup wrapper, do nothing. | |||
if (closest(t, POPUP_WRAP_SEL)) return; | |||
closeAllBtnPopups(document); | |||
closeAllDetails(document); | |||
}, | }, | ||
true | true | ||
| Line 206: | Line 446: | ||
function (e) { | function (e) { | ||
if (e.key !== "Escape") return; | if (e.key !== "Escape") return; | ||
closeAllBtnPopups(document); | |||
closeAllDetails(document); | |||
}, | }, | ||
true | true | ||
| Line 212: | Line 453: | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Tabs | /* Tabs (Phase 4.1: key-based, uses .sv-hidden + [hidden]) */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
| Line 221: | Line 462: | ||
function getPanels(root) { | function getPanels(root) { | ||
return root.querySelectorAll(PANEL_SEL); | return root.querySelectorAll(PANEL_SEL); | ||
} | |||
function getPanelByKey(root, key) { | |||
var panels = getPanels(root); | |||
for (var i = 0; i < panels.length; i++) { | |||
if (panels[i].getAttribute("data-panel") === key) return panels[i]; | |||
} | |||
return null; | |||
} | } | ||
| Line 228: | Line 477: | ||
if (!uid) { | if (!uid) { | ||
_tabsUidCounter++; | _tabsUidCounter++; | ||
uid = | uid = (root.getAttribute("data-tabs-root") || "svgi-tabs") + "-" + _tabsUidCounter; | ||
root.setAttribute("data-tabs-uid", uid); | root.setAttribute("data-tabs-uid", uid); | ||
} | } | ||
// Pair by | // Pair by key (stable for generated markup). | ||
var tabs = getTabs(root); | var tabs = getTabs(root); | ||
for (var i = 0; i < tabs.length; i++) { | |||
for (var i = 0; i < | |||
var tab = tabs[i]; | var tab = tabs[i]; | ||
var panel = | var key = tab.getAttribute("data-tab") || String(i + 1); | ||
var panel = getPanelByKey(root, key); | |||
var tabId = uid + "-tab-" + key; | |||
tab.id = tabId; | tab.id = tabId; | ||
panel.id = panelId; | if (panel) { | ||
var panelId = uid + "-panel-" + key; | |||
panel.id = panelId; | |||
tab.setAttribute("aria-controls", panelId); | |||
panel.setAttribute("aria-labelledby", tabId); | |||
} | |||
} | } | ||
} | |||
function showPanel(panel) { | |||
if (!panel) return; | |||
panel.setAttribute("data-active", "1"); | |||
if (panel.classList) panel.classList.remove("sv-hidden"); | |||
panel.removeAttribute("hidden"); | |||
} | |||
function hidePanel(panel) { | |||
if (!panel) return; | |||
panel.setAttribute("data-active", "0"); | |||
if (panel.classList) panel.classList.add("sv-hidden"); | |||
panel.setAttribute("hidden", "hidden"); | |||
} | } | ||
| Line 257: | Line 518: | ||
var tabs = getTabs(root); | var tabs = getTabs(root); | ||
var | var key = btn.getAttribute("data-tab") || ""; | ||
// Tabs | |||
for (var i = 0; i < tabs.length; i++) { | for (var i = 0; i < tabs.length; i++) { | ||
var t = tabs[i]; | var t = tabs[i]; | ||
| Line 264: | Line 526: | ||
t.setAttribute("aria-selected", active ? "true" : "false"); | t.setAttribute("aria-selected", active ? "true" : "false"); | ||
t.setAttribute("tabindex", active ? "0" : "-1"); | t.setAttribute("tabindex", active ? "0" : "-1"); | ||
if (t.classList) t.classList.toggle("sv-tab--active", active); | |||
} | } | ||
var | // Panels | ||
var panels = getPanels(root); | |||
for (var j = 0; j < panels.length; j++) { | for (var j = 0; j < panels.length; j++) { | ||
var p = panels[j]; | var p = panels[j]; | ||
var isTarget = p. | var isTarget = p.getAttribute("data-panel") === key; | ||
if (isTarget) showPanel(p); | |||
if (isTarget) p | else hidePanel(p); | ||
else p | |||
} | } | ||
root.setAttribute("data-active-tab", | root.setAttribute("data-active-tab", key); | ||
} | } | ||
| Line 306: | Line 569: | ||
} | } | ||
// Determine active tab | |||
var activeBtn = null; | var activeBtn = null; | ||
for (var i = 0; i < tabs.length; i++) { | for (var i = 0; i < tabs.length; i++) { | ||
| Line 381: | Line 645: | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* | /* Init */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
function initAll(root) { | function initAll(root) { | ||
| Line 456: | Line 653: | ||
var container = root || document; | var container = root || document; | ||
// Normalize Phase 4.1 span-popups | |||
normalizeBtnPopups(container); | |||
// Keep legacy <details> aria in sync if any remain | |||
var ds = container.querySelectorAll(POPUP_DETAILS_SEL); | var ds = container.querySelectorAll(POPUP_DETAILS_SEL); | ||
for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]); | for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]); | ||