MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/* | /* GameInfo JS (Phase 4.1) | ||
Shared, reusable behaviors for all GameInfo modules. */ | |||
*/ | |||
(function () { | (function () { | ||
if (window.SV_GAMEINFO_41_INIT) return; | |||
window.SV_GAMEINFO_41_INIT = 1; | |||
/* ------------------------------------------------------------------ */ | |||
/* Level Selector */ | |||
/* ------------------------------------------------------------------ */ | |||
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) { | |||
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) { | |||
var index = level - 1; | |||
var nodes = scope.querySelectorAll(SERIES_SEL); | |||
for (var i = 0; i < nodes.length; i++) { | |||
var el = nodes[i]; | |||
// Phase 4.1 rule: only update fields below the level boundary. | |||
if (!isAfter(boundary, el)) continue; | |||
var series = parseSeries(el); | |||
if (!series || series.length === 0) continue; | |||
// Clamp if series shorter than expected. | |||
var safeIndex = index; | |||
if (safeIndex >= series.length) safeIndex = series.length - 1; | |||
if (safeIndex < 0) safeIndex = 0; | |||
var value = series[safeIndex]; | |||
if (value == null) value = ""; | |||
// Text-only (no HTML mode). | |||
el.textContent = String(value); | |||
} | |||
} | |||
function applyLevelToCard(card, rawLevel) { | |||
if (!card) return; | |||
var maxLevel = getCardMaxLevel(card); | |||
if (maxLevel <= 1) { | |||
card.setAttribute("data-level", "1"); | |||
setLevelNumber(card, 1); | |||
return; | |||
} | |||
var slider = getLevelSlider(card); | |||
var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel)); | |||
// Store on card for other systems / debugging. | |||
card.setAttribute("data-level", String(level)); | |||
// Keep slider in sync. | |||
if (slider) { | |||
slider.setAttribute("min", "1"); | |||
slider.setAttribute("max", String(maxLevel)); | |||
if (String(slider.value) !== String(level)) slider.value = String(level); | |||
} | } | ||
setLevelNumber(card, level); | |||
// | // Apply dynamic series values (scoped below boundary). | ||
if ( | if (slider) { | ||
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); | |||
// No slider (or max <= 1) => this card doesn't participate. | |||
if (!slider || maxLevel <= 1) { | |||
card.setAttribute("data-gi-level-init", "1"); | |||
continue; | |||
} | |||
var start = slider.value; | |||
if (start == null || start === "") start = getCardLevel(card, maxLevel); | |||
applyLevelToCard(card, start); | |||
card.setAttribute("data-gi-level-init", "1"); | |||
} | } | ||
} | |||
function | // Slider input (delegated) | ||
document.addEventListener( | |||
"input", | |||
function (e) { | |||
var t = e.target; | |||
if (!t || t.nodeType !== 1) return; | |||
if (!t.matches || !t.matches(LEVEL_RANGE_SEL)) return; | |||
var card = closest(t, CARD_SEL); | |||
if (!card) return; | |||
applyLevelToCard(card, t.value); | |||
}, | |||
true | |||
); | |||
/* ------------------------------------------------------------------ */ | |||
/* Popup */ | |||
/* ------------------------------------------------------------------ */ | |||
function closeDetails(d) { | |||
if (d && d.open) d.open = false; | |||
} | |||
function closeAllPopups(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 closeOtherPopupsWithinCard(currentDetails) { | |||
var card = closest(currentDetails, CARD_SEL); | |||
var scope = card || document; | |||
var openOnes = scope.querySelectorAll(POPUP_DETAILS_SEL + "[open]"); | |||
for (var i = 0; i < openOnes.length; i++) { | |||
var d = openOnes[i]; | |||
if (d !== currentDetails) closeDetails(d); | |||
} | } | ||
} | |||
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 = "svgi-pop-" + Math.random().toString(36).slice(2); | |||
summary.setAttribute("aria-controls", pop.id); | |||
} | } | ||
} | |||
// 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) closeOtherPopupsWithinCard(d); | |||
syncPopupAria(d); | |||
}, | |||
true | |||
); | |||
// Click outside closes all popups | |||
document.addEventListener( | |||
"click", | |||
function (e) { | |||
var t = e.target; | |||
if (!t) return; | |||
if (closest(t, POPUP_DETAILS_SEL)) return; | |||
closeAllPopups(document); | |||
}, | |||
true | |||
); | |||
// Escape closes all popups | |||
document.addEventListener( | |||
"keydown", | |||
function (e) { | |||
if (e.key !== "Escape") return; | |||
closeAllPopups(document); | |||
}, | |||
true | |||
); | |||
/* ------------------------------------------------------------------ */ | |||
/* Tabs */ | |||
/* ------------------------------------------------------------------ */ | |||
function getTabs(root) { | |||
return root.querySelectorAll(TAB_SEL); | |||
} | |||
function getPanels(root) { | |||
return root.querySelectorAll(PANEL_SEL); | |||
} | |||
function ensureUniqueTabIds(root) { | |||
// Per-root uid so multiple transclusions don't collide. | |||
var uid = root.getAttribute("data-tabs-uid"); | |||
if (!uid) { | |||
_tabsUidCounter++; | |||
uid = | |||
(root.getAttribute("data-tabs-root") || "svgi-tabs") + "-" + _tabsUidCounter; | |||
root.setAttribute("data-tabs-uid", uid); | |||
} | } | ||
// Pair by index (stable for generated markup). | |||
var tabs = getTabs(root); | |||
var panels = getPanels(root); | |||
var n = Math.min(tabs.length, panels.length); | |||
for (var i = 0; i < n; i++) { | |||
var tabId = uid + "-tab-" + (i + 1); | |||
var panelId = uid + "-panel-" + (i + 1); | |||
var tab = tabs[i]; | |||
var panel = panels[i]; | |||
tab.id = tabId; | |||
tab.setAttribute("aria-controls", panelId); | |||
panel.id = panelId; | |||
panel.setAttribute("aria-labelledby", tabId); | |||
} | } | ||
} | } | ||
function | function activateTab(root, btn) { | ||
if (!root || !btn) return; | |||
var tabs = getTabs(root); | |||
var panels = getPanels(root); | |||
for (var i = 0; i < tabs.length; i++) { | |||
var t = tabs[i]; | |||
var active = t === btn; | |||
t.setAttribute("aria-selected", active ? "true" : "false"); | |||
t.setAttribute("tabindex", active ? "0" : "-1"); | |||
} | |||
var targetId = btn.getAttribute("aria-controls"); | |||
for (var j = 0; j < panels.length; j++) { | |||
var p = panels[j]; | |||
var isTarget = p.id === targetId; | |||
p.setAttribute("data-active", isTarget ? "1" : "0"); | |||
if (isTarget) p.removeAttribute("hidden"); | |||
else p.setAttribute("hidden", "hidden"); | |||
} | |||
root.setAttribute("data-active-tab", targetId || ""); | |||
} | } | ||
function | function focusTab(tabs, idx) { | ||
if ( | if (idx < 0) idx = 0; | ||
var t = | if (idx >= tabs.length) idx = tabs.length - 1; | ||
if (t && | var t = tabs[idx]; | ||
if (t && t.focus) t.focus(); | |||
return t; | |||
} | } | ||
function | function indexOfTab(tabs, btn) { | ||
for (var i = 0; i < tabs.length; i++) { | |||
if (tabs[i] === btn) return i; | |||
} | } | ||
return -1; | |||
} | } | ||
function | function normalizeTabsRoot(root) { | ||
return | if (!root) return; | ||
if (root.getAttribute("data-gi-tabs-init") === "1") return; | |||
ensureUniqueTabIds(root); | |||
var tabs = getTabs(root); | |||
var panels = getPanels(root); | |||
if (!tabs.length || !panels.length) { | |||
root.setAttribute("data-gi-tabs-init", "1"); | |||
return; | |||
} | |||
var | var activeBtn = null; | ||
for (var i = 0; i < tabs.length; i++) { | |||
if (tabs[i].getAttribute("aria-selected") === "true") { | |||
activeBtn = tabs[i]; | |||
break; | |||
} | |||
} | |||
if (!activeBtn) activeBtn = tabs[0]; | |||
activateTab(root, activeBtn); | |||
root.setAttribute("data-gi-tabs-init", "1"); | |||
} | } | ||
function | function initTabs(root) { | ||
var container = root || document; | |||
var roots = container.querySelectorAll(TABS_ROOT_SEL); | |||
var | for (var i = 0; i < roots.length; i++) normalizeTabsRoot(roots[i]); | ||
} | } | ||
function | // Click to activate (delegated) | ||
if (! | document.addEventListener("click", function (e) { | ||
var btn = e.target && e.target.closest ? e.target.closest(TAB_SEL) : null; | |||
if (!btn) return; | |||
var root = closest(btn, ".sv-tabs"); | |||
if (!root || root.getAttribute("data-tabs") !== "1") return; | |||
activateTab(root, btn); | |||
}); | |||
// Keyboard navigation (delegated) | |||
document.addEventListener("keydown", function (e) { | |||
var btn = | |||
document.activeElement && document.activeElement.closest | |||
? document.activeElement.closest(TAB_SEL) | |||
: null; | |||
if (!btn) return; | |||
var root = closest(btn, ".sv-tabs"); | |||
if (!root || root.getAttribute("data-tabs") !== "1") return; | |||
var tabs = getTabs(root); | |||
if (! | if (!tabs || !tabs.length) return; | ||
var idx = indexOfTab(tabs, btn); | |||
if (idx < 0) return; | |||
if ( | |||
if (e.key === "ArrowLeft" || e.key === "Left") { | |||
e.preventDefault(); | |||
if ( | activateTab(root, focusTab(tabs, idx - 1)); | ||
return; | |||
} | |||
if (e.key === "ArrowRight" || e.key === "Right") { | |||
e.preventDefault(); | |||
activateTab(root, focusTab(tabs, idx + 1)); | |||
return; | return; | ||
} | } | ||
if (e.key === "Home") { | |||
if ( | e.preventDefault(); | ||
activateTab(root, focusTab(tabs, 0)); | |||
return; | return; | ||
} | } | ||
if (e.key === "End") { | |||
if | e.preventDefault(); | ||
activateTab(root, focusTab(tabs, tabs.length - 1)); | |||
return; | return; | ||
} | } | ||
if (e.key === "Enter" || e.key === " ") { | |||
if ( | e.preventDefault(); | ||
activateTab(root, btn); | |||
return; | return; | ||
} | } | ||
}); | |||
/* ------------------------------------------------------------------ */ | |||
/* Definitions */ | |||
/* ------------------------------------------------------------------ */ | |||
// Selectors | |||
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card"; | |||
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]"; | |||
var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose"; | |||
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; | |||
if ( | 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 ( | 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; | |||
if ( | el = el.parentElement; | ||
} | } | ||
return null; | |||
} | |||
function parseSeries(el) { | |||
if (!el) return null; | |||
if ( | if (_seriesCache) { | ||
if (_seriesCache.has(el)) return _seriesCache.get(el); | |||
} else if (el._svSeries !== undefined) { | |||
return el._svSeries; | |||
if (! | |||
return; | |||
} | } | ||
var | var raw = el.getAttribute("data-series"); | ||
var parsed = null; | |||
if (! | if (raw != null && raw !== "") { | ||
if ( | 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 ( | if (!boundaryNode || !el) return true; | ||
} | if (boundaryNode === el) return false; | ||
if (!boundaryNode.compareDocumentPosition) return true; | |||
return (boundaryNode.compareDocumentPosition(el) & 4) !== 0; // following | |||
} | |||
function initAll(root) { | |||
initLevels(root); | |||
initTabs(root); | |||
var container = root || document; | |||
var ds = container.querySelectorAll(POPUP_DETAILS_SEL); | |||
for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]); | |||
} | |||
} | |||
window. | 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); | |||
} | |||
})(); | |||
Revision as of 17:05, 19 February 2026
/* GameInfo JS (Phase 4.1)
Shared, reusable behaviors for all GameInfo modules. */
(function () {
if (window.SV_GAMEINFO_41_INIT) return;
window.SV_GAMEINFO_41_INIT = 1;
/* ------------------------------------------------------------------ */
/* Level Selector */
/* ------------------------------------------------------------------ */
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) {
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) {
var index = level - 1;
var nodes = scope.querySelectorAll(SERIES_SEL);
for (var i = 0; i < nodes.length; i++) {
var el = nodes[i];
// Phase 4.1 rule: only update fields below the level boundary.
if (!isAfter(boundary, el)) continue;
var series = parseSeries(el);
if (!series || series.length === 0) continue;
// Clamp if series shorter than expected.
var safeIndex = index;
if (safeIndex >= series.length) safeIndex = series.length - 1;
if (safeIndex < 0) safeIndex = 0;
var value = series[safeIndex];
if (value == null) value = "";
// Text-only (no HTML mode).
el.textContent = String(value);
}
}
function applyLevelToCard(card, rawLevel) {
if (!card) return;
var maxLevel = getCardMaxLevel(card);
if (maxLevel <= 1) {
card.setAttribute("data-level", "1");
setLevelNumber(card, 1);
return;
}
var slider = getLevelSlider(card);
var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));
// Store on card for other systems / debugging.
card.setAttribute("data-level", String(level));
// Keep slider in sync.
if (slider) {
slider.setAttribute("min", "1");
slider.setAttribute("max", String(maxLevel));
if (String(slider.value) !== String(level)) slider.value = String(level);
}
setLevelNumber(card, level);
// Apply dynamic series values (scoped below boundary).
if (slider) {
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);
// No slider (or max <= 1) => this card doesn't participate.
if (!slider || maxLevel <= 1) {
card.setAttribute("data-gi-level-init", "1");
continue;
}
var start = slider.value;
if (start == null || start === "") start = getCardLevel(card, maxLevel);
applyLevelToCard(card, start);
card.setAttribute("data-gi-level-init", "1");
}
}
// Slider input (delegated)
document.addEventListener(
"input",
function (e) {
var t = e.target;
if (!t || t.nodeType !== 1) return;
if (!t.matches || !t.matches(LEVEL_RANGE_SEL)) return;
var card = closest(t, CARD_SEL);
if (!card) return;
applyLevelToCard(card, t.value);
},
true
);
/* ------------------------------------------------------------------ */
/* Popup */
/* ------------------------------------------------------------------ */
function closeDetails(d) {
if (d && d.open) d.open = false;
}
function closeAllPopups(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 closeOtherPopupsWithinCard(currentDetails) {
var card = closest(currentDetails, CARD_SEL);
var scope = card || document;
var openOnes = scope.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
for (var i = 0; i < openOnes.length; i++) {
var d = openOnes[i];
if (d !== currentDetails) closeDetails(d);
}
}
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 = "svgi-pop-" + Math.random().toString(36).slice(2);
summary.setAttribute("aria-controls", pop.id);
}
}
// 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) closeOtherPopupsWithinCard(d);
syncPopupAria(d);
},
true
);
// Click outside closes all popups
document.addEventListener(
"click",
function (e) {
var t = e.target;
if (!t) return;
if (closest(t, POPUP_DETAILS_SEL)) return;
closeAllPopups(document);
},
true
);
// Escape closes all popups
document.addEventListener(
"keydown",
function (e) {
if (e.key !== "Escape") return;
closeAllPopups(document);
},
true
);
/* ------------------------------------------------------------------ */
/* Tabs */
/* ------------------------------------------------------------------ */
function getTabs(root) {
return root.querySelectorAll(TAB_SEL);
}
function getPanels(root) {
return root.querySelectorAll(PANEL_SEL);
}
function ensureUniqueTabIds(root) {
// Per-root uid so multiple transclusions don't collide.
var uid = root.getAttribute("data-tabs-uid");
if (!uid) {
_tabsUidCounter++;
uid =
(root.getAttribute("data-tabs-root") || "svgi-tabs") + "-" + _tabsUidCounter;
root.setAttribute("data-tabs-uid", uid);
}
// Pair by index (stable for generated markup).
var tabs = getTabs(root);
var panels = getPanels(root);
var n = Math.min(tabs.length, panels.length);
for (var i = 0; i < n; i++) {
var tabId = uid + "-tab-" + (i + 1);
var panelId = uid + "-panel-" + (i + 1);
var tab = tabs[i];
var panel = panels[i];
tab.id = tabId;
tab.setAttribute("aria-controls", panelId);
panel.id = panelId;
panel.setAttribute("aria-labelledby", tabId);
}
}
function activateTab(root, btn) {
if (!root || !btn) return;
var tabs = getTabs(root);
var panels = getPanels(root);
for (var i = 0; i < tabs.length; i++) {
var t = tabs[i];
var active = t === btn;
t.setAttribute("aria-selected", active ? "true" : "false");
t.setAttribute("tabindex", active ? "0" : "-1");
}
var targetId = btn.getAttribute("aria-controls");
for (var j = 0; j < panels.length; j++) {
var p = panels[j];
var isTarget = p.id === targetId;
p.setAttribute("data-active", isTarget ? "1" : "0");
if (isTarget) p.removeAttribute("hidden");
else p.setAttribute("hidden", "hidden");
}
root.setAttribute("data-active-tab", targetId || "");
}
function focusTab(tabs, idx) {
if (idx < 0) idx = 0;
if (idx >= tabs.length) idx = tabs.length - 1;
var t = tabs[idx];
if (t && t.focus) t.focus();
return t;
}
function indexOfTab(tabs, btn) {
for (var i = 0; i < tabs.length; i++) {
if (tabs[i] === btn) return i;
}
return -1;
}
function normalizeTabsRoot(root) {
if (!root) return;
if (root.getAttribute("data-gi-tabs-init") === "1") return;
ensureUniqueTabIds(root);
var tabs = getTabs(root);
var panels = getPanels(root);
if (!tabs.length || !panels.length) {
root.setAttribute("data-gi-tabs-init", "1");
return;
}
var activeBtn = null;
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].getAttribute("aria-selected") === "true") {
activeBtn = tabs[i];
break;
}
}
if (!activeBtn) activeBtn = tabs[0];
activateTab(root, activeBtn);
root.setAttribute("data-gi-tabs-init", "1");
}
function initTabs(root) {
var container = root || document;
var roots = container.querySelectorAll(TABS_ROOT_SEL);
for (var i = 0; i < roots.length; i++) normalizeTabsRoot(roots[i]);
}
// Click to activate (delegated)
document.addEventListener("click", function (e) {
var btn = e.target && e.target.closest ? e.target.closest(TAB_SEL) : null;
if (!btn) return;
var root = closest(btn, ".sv-tabs");
if (!root || root.getAttribute("data-tabs") !== "1") return;
activateTab(root, btn);
});
// Keyboard navigation (delegated)
document.addEventListener("keydown", function (e) {
var btn =
document.activeElement && document.activeElement.closest
? document.activeElement.closest(TAB_SEL)
: null;
if (!btn) return;
var root = closest(btn, ".sv-tabs");
if (!root || root.getAttribute("data-tabs") !== "1") return;
var tabs = getTabs(root);
if (!tabs || !tabs.length) return;
var idx = indexOfTab(tabs, btn);
if (idx < 0) return;
if (e.key === "ArrowLeft" || e.key === "Left") {
e.preventDefault();
activateTab(root, focusTab(tabs, idx - 1));
return;
}
if (e.key === "ArrowRight" || e.key === "Right") {
e.preventDefault();
activateTab(root, focusTab(tabs, idx + 1));
return;
}
if (e.key === "Home") {
e.preventDefault();
activateTab(root, focusTab(tabs, 0));
return;
}
if (e.key === "End") {
e.preventDefault();
activateTab(root, focusTab(tabs, tabs.length - 1));
return;
}
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
activateTab(root, btn);
return;
}
});
/* ------------------------------------------------------------------ */
/* Definitions */
/* ------------------------------------------------------------------ */
// Selectors
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
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]";
var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
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;
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; // following
}
function initAll(root) {
initLevels(root);
initTabs(root);
var container = root || document;
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) {
initAll($content && $content[0]);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
initAll(document);
});
} else {
initAll(document);
}
})();