MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary Tags: Mobile edit Mobile web edit |
No edit summary Tags: Mobile edit Mobile web edit |
||
| (4 intermediate revisions by the same user not shown) | |||
| Line 17: | Line 17: | ||
// Versioned init guard (allows safe updates without stale blocks) | // Versioned init guard (allows safe updates without stale blocks) | ||
var LEVELS_VERSION = | var LEVELS_VERSION = 6; // neutral hooks + bubble styling owned by CSS + no tick/check row | ||
if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return; | if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return; | ||
COMMON.levelsInit = LEVELS_VERSION; | COMMON.levelsInit = LEVELS_VERSION; | ||
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card"; | // Prefer neutral shared hooks; keep legacy class fallbacks for compatibility. | ||
var CARD_SEL = | |||
"[data-sv-card='1'], .sv-gi-card, .sv-skill-card, .sv-passive-card"; | |||
// Accept the native range input and the custom slider variants. | // Accept the native range input and the custom slider variants. | ||
| Line 27: | Line 30: | ||
"input.sv-level-range[type='range'], .sv-level-range--custom, .sv-level-range[data-sv-slider='1']"; | "input.sv-level-range[type='range'], .sv-level-range--custom, .sv-level-range[data-sv-slider='1']"; | ||
var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level"; | var LEVEL_BOUNDARY_SEL = | ||
var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom"; | "[data-sv-level-boundary='1'], .sv-skill-level, .sv-gi-level"; | ||
var LEVEL_SCOPE_SEL = | |||
"[data-sv-level-scope='1'], .sv-gi-bottom, .sv-skill-bottom"; | |||
var SERIES_SEL = "[data-series]"; | var SERIES_SEL = "[data-series]"; | ||
var LEVEL_INIT_ATTR = "data- | var LEVEL_INIT_ATTR = "data-sv-level-init"; | ||
var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null; | var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null; | ||
| Line 115: | Line 122: | ||
} | } | ||
function getCardLevel(card, maxLevel) { | function getCardMinLevel(card, slider, maxLevel) { | ||
// Prefer explicit neutral hooks, then legacy attrs, then slider attrs. | |||
var raw = | |||
card.getAttribute("data-min-level") || | |||
card.getAttribute("data-minLevel") || | |||
card.getAttribute("data-level-min"); | |||
if (raw == null || raw === "") { | |||
if (slider) { | |||
if (isRangeInput(slider)) raw = slider.getAttribute("min"); | |||
else if (isCustomSlider(slider)) | |||
raw = slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"); | |||
} | |||
} | |||
var min = clampInt(raw, 1, maxLevel, 1); | |||
if (min > maxLevel) min = maxLevel; | |||
return min; | |||
} | |||
function getCardLevel(card, minLevel, maxLevel) { | |||
var raw = | var raw = | ||
card.getAttribute("data-level") || | card.getAttribute("data-level") || | ||
| Line 123: | Line 150: | ||
: null); | : null); | ||
return clampInt(raw, 1, | return clampInt(raw, minLevel, maxLevel, minLevel); | ||
} | |||
function getStep(slider) { | |||
// Default = 1. Support future: data-step, or <input step=""> | |||
var raw = null; | |||
if (slider) raw = slider.getAttribute("data-step") || slider.getAttribute("step"); | |||
var s = parseInt(raw, 10); | |||
if (!s || isNaN(s) || s < 1) s = 1; | |||
return s; | |||
} | } | ||
| Line 144: | Line 180: | ||
var span = card.querySelector(".sv-level-num"); | var span = card.querySelector(".sv-level-num"); | ||
if (span) span.textContent = String(level); | if (span) span.textContent = String(level); | ||
} | |||
function setLevelMax(card, maxLevel) { | |||
var span = card.querySelector(".sv-level-max"); | |||
if (span) span.textContent = String(maxLevel); | |||
} | } | ||
| Line 149: | Line 190: | ||
if (!scope) return; | if (!scope) return; | ||
// JS arrays are 0-based; Lua uses 1-based "level". | |||
var index = level - 1; | var index = level - 1; | ||
var nodes = scope.querySelectorAll(SERIES_SEL); | var nodes = scope.querySelectorAll(SERIES_SEL); | ||
| Line 170: | Line 212: | ||
} | } | ||
function getCustomMin(slider) { | function unhide(el) { | ||
if (!el) return; | |||
if (el.classList) el.classList.remove("sv-hidden"); | |||
if (el.removeAttribute) el.removeAttribute("hidden"); | |||
if (el.style && el.style.display === "none") el.style.display = ""; | |||
} | |||
function hide(el) { | |||
if (!el) return; | |||
if (el.classList) el.classList.add("sv-hidden"); | |||
if (el.setAttribute) el.setAttribute("hidden", "hidden"); | |||
} | |||
/* ================================================================== */ | |||
/* Custom slider helpers (bounds/percent) */ | |||
/* ================================================================== */ | |||
function getCustomMin(slider, fallback) { | |||
return clampInt( | return clampInt( | ||
slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"), | slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"), | ||
1, | 1, | ||
999, | 999, | ||
1 | fallback != null ? fallback : 1 | ||
); | ); | ||
} | } | ||
function getCustomMax(slider) { | function getCustomMax(slider, fallback) { | ||
return clampInt( | return clampInt( | ||
slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"), | slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"), | ||
1, | 1, | ||
999, | 999, | ||
1 | fallback != null ? fallback : 1 | ||
); | ); | ||
} | } | ||
function getCustomValue(slider, fallback) { | function getCustomBounds(slider, fallbackMin) { | ||
var min = getCustomMin(slider, fallbackMin != null ? fallbackMin : 1); | |||
var max = getCustomMax(slider, min); | |||
if (max < min) max = min; | |||
return { min: min, max: max }; | |||
} | |||
function getCustomValue(slider, min, max, fallback) { | |||
return clampInt( | return clampInt( | ||
slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"), | slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"), | ||
min, | |||
max, | |||
fallback != null ? fallback : | fallback != null ? fallback : min | ||
); | ); | ||
} | |||
function pctFromValue(min, max, value) { | |||
if (max === min) return 0; | |||
var pct = (value - min) / (max - min); | |||
if (pct < 0) pct = 0; | |||
if (pct > 1) pct = 1; | |||
return pct; | |||
} | } | ||
| Line 204: | Line 278: | ||
} | } | ||
function | /* ================================================================== */ | ||
var | /* Custom slider value bubble */ | ||
var max = | /* - JS owns structure + text/position only */ | ||
if ( | /* - CSS owns visuals */ | ||
/* ================================================================== */ | |||
function ensureValueLabel(slider) { | |||
if (!slider || !slider.querySelector || !isCustomSlider(slider)) return null; | |||
var bubble = slider.querySelector(".sv-level-bubble"); | |||
if (bubble) return bubble; | |||
bubble = document.createElement("span"); | |||
bubble.className = "sv-level-bubble sv-hidden"; | |||
bubble.setAttribute("hidden", "hidden"); | |||
bubble.setAttribute("aria-hidden", "true"); | |||
slider.appendChild(bubble); | |||
return bubble; | |||
} | |||
function setValueLabel(slider, value) { | |||
var bubble = ensureValueLabel(slider); | |||
if (!bubble) return; | |||
var b = getCustomBounds(slider, 1); | |||
bubble.textContent = String(value); | |||
bubble.style.left = pctFromValue(b.min, b.max, value) * 100 + "%"; | |||
} | |||
function showValueLabel(slider) { | |||
if (!slider) return; | |||
var bubble = ensureValueLabel(slider); | |||
if (!bubble) return; | |||
if (slider._svBubbleTimer) { | |||
clearTimeout(slider._svBubbleTimer); | |||
slider._svBubbleTimer = null; | |||
} | |||
unhide(bubble); | |||
} | |||
function hideValueLabelSoon(slider, delayMs) { | |||
if (!slider) return; | |||
var bubble = slider.querySelector ? slider.querySelector(".sv-level-bubble") : null; | |||
if (!bubble) return; | |||
if (slider._svBubbleTimer) clearTimeout(slider._svBubbleTimer); | |||
slider._svBubbleTimer = setTimeout(function () { | |||
hide(bubble); | |||
slider._svBubbleTimer = null; | |||
}, delayMs != null ? delayMs : 450); | |||
} | |||
var left = | function updateCustomVisual(slider, value) { | ||
var b = getCustomBounds(slider, 1); | |||
var left = pctFromValue(b.min, b.max, value) * 100; | |||
var thumb = slider.querySelector(".sv-level-thumb"); | var thumb = slider.querySelector(".sv-level-thumb"); | ||
| Line 220: | Line 342: | ||
if (thumb && thumb.style) thumb.style.left = left + "%"; | if (thumb && thumb.style) thumb.style.left = left + "%"; | ||
if (fill && fill.style) fill.style.width = left + "%"; | if (fill && fill.style) fill.style.width = left + "%"; | ||
setValueLabel(slider, value); | |||
} | } | ||
function setCustomValue(slider, value) { | function setCustomValue(slider, value, min, max) { | ||
slider.setAttribute("aria-valuenow", String(value)); | slider.setAttribute("aria-valuenow", String(value)); | ||
slider.setAttribute("data-value", String(value)); | slider.setAttribute("data-value", String(value)); | ||
// Helpful SR text (doesn't affect visuals) | |||
slider.setAttribute("aria-valuetext", "Level " + String(value) + " of " + String(max)); | |||
updateCustomVisual(slider, value); | updateCustomVisual(slider, value); | ||
} | |||
function snapToStep(raw, min, max, step) { | |||
if (step < 1) step = 1; | |||
var x = raw; | |||
if (x < min) x = min; | |||
if (x > max) x = max; | |||
var snapped = min + Math.round((x - min) / step) * step; | |||
if (snapped < min) snapped = min; | |||
if (snapped > max) snapped = max; | |||
return snapped; | |||
} | } | ||
function valueFromClientX(slider, clientX) { | function valueFromClientX(slider, clientX) { | ||
var rect = slider.getBoundingClientRect(); | var rect = slider.getBoundingClientRect(); | ||
var | var b = getCustomBounds(slider, 1); | ||
var w = rect.width || 1; | var w = rect.width || 1; | ||
| Line 239: | Line 377: | ||
if (x > 1) x = 1; | if (x > 1) x = 1; | ||
var raw = min + x * (max - min); | var raw = b.min + x * (b.max - b.min); | ||
return snapToStep(raw, b.min, b.max, getStep(slider)); | |||
} | } | ||
function | /* ================================================================== */ | ||
if ( | /* End-of-bar value label (right-side value) */ | ||
if ( | /* ================================================================== */ | ||
if ( | |||
if ( | function ensureEndValueLabel(card, slider) { | ||
var wrap = null; | |||
if (slider) wrap = closest(slider, ".sv-level-slider"); | |||
if (!wrap && card && card.querySelector) wrap = card.querySelector(".sv-level-slider"); | |||
if (!wrap) return null; | |||
var out = wrap.querySelector(".sv-level-endvalue"); | |||
if (out) return out; | |||
out = document.createElement("span"); | |||
out.className = "sv-level-endvalue"; | |||
out.setAttribute("aria-hidden", "true"); // slider already exposes aria-valuetext | |||
wrap.appendChild(out); | |||
return out; | |||
} | |||
function setEndValueLabel(card, slider, value) { | |||
var out = ensureEndValueLabel(card, slider); | |||
if (out) out.textContent = String(value); | |||
} | |||
/* ================================================================== */ | |||
/* Core apply / init */ | |||
/* ================================================================== */ | |||
function scheduleApply(card, rawLevel) { | |||
if (!card) return; | |||
card._svLevelNext = rawLevel; | |||
if (card._svLevelRaf) return; | |||
if (window.requestAnimationFrame) { | |||
card._svLevelRaf = requestAnimationFrame(function () { | |||
card._svLevelRaf = null; | |||
var v = card._svLevelNext; | |||
card._svLevelNext = null; | |||
applyLevelToCard(card, v); | |||
}); | |||
return; | |||
} | |||
card._svLevelRaf = setTimeout(function () { | |||
card._svLevelRaf = null; | |||
var v = card._svLevelNext; | |||
card._svLevelNext = null; | |||
applyLevelToCard(card, v); | |||
}, 0); | |||
} | } | ||
| Line 256: | Line 439: | ||
var maxLevel = getCardMaxLevel(card); | var maxLevel = getCardMaxLevel(card); | ||
var slider = getLevelSlider(card); | var slider = getLevelSlider(card); | ||
var level = clampInt(rawLevel, | var minLevel = getCardMinLevel(card, slider, maxLevel); | ||
var level = clampInt(rawLevel, minLevel, maxLevel, getCardLevel(card, minLevel, maxLevel)); | |||
card.setAttribute("data-level", String(level)); | card.setAttribute("data-level", String(level)); | ||
setLevelNumber(card, level); | setLevelNumber(card, level); | ||
setLevelMax(card, maxLevel); | |||
if (slider) { | if (slider) { | ||
| Line 267: | Line 453: | ||
} | } | ||
if (slider | if (slider) { | ||
if (isRangeInput(slider)) { | if (isRangeInput(slider)) { | ||
slider.setAttribute("min", | slider.setAttribute("min", String(minLevel)); | ||
slider.setAttribute("max", String(maxLevel)); | slider.setAttribute("max", String(maxLevel)); | ||
slider.setAttribute("step", String(getStep(slider))); | |||
slider.setAttribute("aria-valuetext", "Level " + String(level) + " of " + String(maxLevel)); | |||
if (String(slider.value) !== String(level)) slider.value = String(level); | if (String(slider.value) !== String(level)) slider.value = String(level); | ||
} else if (isCustomSlider(slider)) { | } else if (isCustomSlider(slider)) { | ||
setCustomBounds(slider, | setCustomBounds(slider, minLevel, maxLevel); | ||
setCustomValue(slider, level); | setCustomValue(slider, level, minLevel, maxLevel); | ||
} | } | ||
} | } | ||
setEndValueLabel(card, slider, level); | |||
var boundary = getLevelBoundary(slider); | var boundary = getLevelBoundary(slider); | ||
| Line 298: | Line 485: | ||
var maxLevel = getCardMaxLevel(card); | var maxLevel = getCardMaxLevel(card); | ||
var slider = getLevelSlider(card); | var slider = getLevelSlider(card); | ||
var minLevel = getCardMinLevel(card, slider, maxLevel); | |||
var start = getCardLevel(card, minLevel, maxLevel); | |||
if (slider && maxLevel > minLevel) { | |||
if (slider && maxLevel > | if (isRangeInput(slider)) start = clampInt(slider.value, minLevel, maxLevel, start); | ||
if (isRangeInput(slider)) start = slider.value; | else if (isCustomSlider(slider)) start = getCustomValue(slider, minLevel, maxLevel, start); | ||
else if (isCustomSlider(slider)) start = getCustomValue(slider, start); | |||
} | } | ||
| Line 310: | Line 499: | ||
} | } | ||
/* ================================================================== */ | |||
/* Events */ | |||
/* ================================================================== */ | |||
// Native range inputs | |||
document.addEventListener( | document.addEventListener( | ||
"input", | "input", | ||
| Line 320: | Line 514: | ||
if (!card) return; | if (!card) return; | ||
scheduleApply(card, t.value); | |||
}, | }, | ||
true | true | ||
); | ); | ||
// Custom slider pointer interaction (click anywhere + drag) | |||
document.addEventListener( | document.addEventListener( | ||
"pointerdown", | "pointerdown", | ||
function (e) { | function (e) { | ||
if (e.button != null && e.button !== 0) return; // primary only | |||
var slider = | var slider = | ||
e.target && e.target.closest | e.target && e.target.closest | ||
| Line 348: | Line 545: | ||
} | } | ||
showValueLabel(slider); | |||
scheduleApply(card, valueFromClientX(slider, e.clientX)); | |||
}, | }, | ||
true | true | ||
| Line 359: | Line 557: | ||
if (e.pointerId !== _drag.pointerId) return; | if (e.pointerId !== _drag.pointerId) return; | ||
e.preventDefault(); | e.preventDefault(); | ||
showValueLabel(_drag.slider); | |||
scheduleApply(_drag.card, valueFromClientX(_drag.slider, e.clientX)); | |||
}, | }, | ||
true | true | ||
| Line 373: | Line 573: | ||
} catch (err) {} | } catch (err) {} | ||
} | } | ||
hideValueLabelSoon(_drag.slider, 650); | |||
_drag = null; | _drag = null; | ||
} | } | ||
| Line 378: | Line 580: | ||
document.addEventListener("pointerup", endDrag, true); | document.addEventListener("pointerup", endDrag, true); | ||
document.addEventListener("pointercancel", endDrag, true); | document.addEventListener("pointercancel", endDrag, true); | ||
// Mouse-only slider interaction: | |||
// - Remove keyboard-driven adjustments on custom sliders | |||
// - Block native <input type="range"> from changing via keyboard (if present) | |||
var _SV_BLOCK_KEYS = { | |||
ArrowLeft: 1, | |||
ArrowRight: 1, | |||
ArrowUp: 1, | |||
ArrowDown: 1, | |||
Left: 1, | |||
Right: 1, | |||
Up: 1, | |||
Down: 1, | |||
Home: 1, | |||
End: 1, | |||
PageUp: 1, | |||
PageDown: 1 | |||
}; | |||
document.addEventListener( | document.addEventListener( | ||
"keydown", | "keydown", | ||
function (e) { | function (e) { | ||
var key = e && e.key; | |||
if (!_SV_BLOCK_KEYS[key]) return; | |||
var el = document.activeElement; | |||
if (!el) return; | |||
// Native range inputs (keep mouse drag, disable keyboard nudges) | |||
if (isRangeInput(el) && el.classList && el.classList.contains("sv-level-range")) { | |||
e.preventDefault(); | |||
return; | |||
} | |||
// Custom sliders (we keep focus behaviors, but no keyboard changes) | |||
var slider = closest(el, ".sv-level-range--custom, .sv-level-range[data-sv-slider='1']"); | |||
if (slider) { | |||
e.preventDefault(); | |||
return; | |||
} | |||
}, | |||
true | |||
); | |||
// Show bubble on focus (discoverability), hide on blur | |||
document.addEventListener( | |||
"focusin", | |||
function () { | |||
var el = document.activeElement; | var el = document.activeElement; | ||
if (!el || !el.closest) return; | if (!el || !el.closest) return; | ||
var slider = el.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']"); | var slider = el.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']"); | ||
if (!slider) return; | if (!slider) return; | ||
var | var b = getCustomBounds(slider, 1); | ||
var v = getCustomValue(slider, b.min, b.max, b.min); | |||
setValueLabel(slider, v); | |||
showValueLabel(slider); | |||
}, | |||
true | |||
); | |||
document.addEventListener( | |||
var | "focusout", | ||
function (e) { | |||
if ( | var t = e.target; | ||
if (!t || !t.closest) return; | |||
var slider = t.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']"); | |||
if (!slider) return; | |||
hideValueLabelSoon(slider, 300); | |||
}, | }, | ||
true | true | ||
| Line 450: | Line 684: | ||
var COMMON = (SV.common = SV.common || {}); | var COMMON = (SV.common = SV.common || {}); | ||
var UPOP_VERSION = | // Bumped only because we changed positioning margins (safe cache-bust). | ||
var UPOP_VERSION = 32; // Universal Popups v3.2 | |||
if (typeof COMMON.universalPopupsInit === "number" && COMMON.universalPopupsInit >= UPOP_VERSION) return; | if (typeof COMMON.universalPopupsInit === "number" && COMMON.universalPopupsInit >= UPOP_VERSION) return; | ||
COMMON.universalPopupsInit = UPOP_VERSION; | COMMON.universalPopupsInit = UPOP_VERSION; | ||
| Line 749: | Line 984: | ||
var vw = window.innerWidth || document.documentElement.clientWidth || 0; | var vw = window.innerWidth || document.documentElement.clientWidth || 0; | ||
var vh = window.innerHeight || document.documentElement.clientHeight || 0; | var vh = window.innerHeight || document.documentElement.clientHeight || 0; | ||
var margin = 10; | |||
var gap = 12; | var tight = vw <= 520; | ||
var margin = tight ? 18 : 10; | |||
var gap = tight ? 14 : 12; | |||
var pr = el.getBoundingClientRect(); | var pr = el.getBoundingClientRect(); | ||
Latest revision as of 00:31, 5 March 2026
/* Common.js — Spirit Vale Wiki
Phase 4.1 — Core client scripts
Included (standalone modules):
1) Level Selector / Descriptions (data-series)
2) Universal Popups (tips, discloses, definitions)
3) Tabs
*/
(function () {
/* ================================================================== */
/* MODULE: Level Selector / Descriptions (data-series) */
/* ================================================================== */
var SV = (window.SV = window.SV || {});
var COMMON = (SV.common = SV.common || {});
// Versioned init guard (allows safe updates without stale blocks)
var LEVELS_VERSION = 6; // neutral hooks + bubble styling owned by CSS + no tick/check row
if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return;
COMMON.levelsInit = LEVELS_VERSION;
// Prefer neutral shared hooks; keep legacy class fallbacks for compatibility.
var CARD_SEL =
"[data-sv-card='1'], .sv-gi-card, .sv-skill-card, .sv-passive-card";
// Accept the native range input and the custom slider variants.
var LEVEL_RANGE_SEL =
"input.sv-level-range[type='range'], .sv-level-range--custom, .sv-level-range[data-sv-slider='1']";
var LEVEL_BOUNDARY_SEL =
"[data-sv-level-boundary='1'], .sv-skill-level, .sv-gi-level";
var LEVEL_SCOPE_SEL =
"[data-sv-level-scope='1'], .sv-gi-bottom, .sv-skill-bottom";
var SERIES_SEL = "[data-series]";
var LEVEL_INIT_ATTR = "data-sv-level-init";
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) {
if (!el || !el.getAttribute) return false;
if (el.getAttribute("data-sv-slider") === "1") return true;
return !!(el.classList && el.classList.contains("sv-level-range--custom"));
}
function parseMaxFromUi(card) {
var ui = card && card.querySelector ? card.querySelector(".sv-level-ui") : null;
if (!ui) return null;
var m = String(ui.textContent || "").match(/\/\s*(\d+)/);
return m ? m[1] : null;
}
function getCardMaxLevel(card) {
var raw =
card.getAttribute("data-max-level") ||
card.getAttribute("data-maxLevel") ||
card.getAttribute("data-max") ||
card.getAttribute("data-level-max") ||
parseMaxFromUi(card);
return clampInt(raw, 1, 999, 1);
}
function getCardMinLevel(card, slider, maxLevel) {
// Prefer explicit neutral hooks, then legacy attrs, then slider attrs.
var raw =
card.getAttribute("data-min-level") ||
card.getAttribute("data-minLevel") ||
card.getAttribute("data-level-min");
if (raw == null || raw === "") {
if (slider) {
if (isRangeInput(slider)) raw = slider.getAttribute("min");
else if (isCustomSlider(slider))
raw = slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min");
}
}
var min = clampInt(raw, 1, maxLevel, 1);
if (min > maxLevel) min = maxLevel;
return min;
}
function getCardLevel(card, minLevel, maxLevel) {
var raw =
card.getAttribute("data-level") ||
card.getAttribute("data-gi-level") ||
(card.querySelector && card.querySelector(".sv-level-num")
? card.querySelector(".sv-level-num").textContent
: null);
return clampInt(raw, minLevel, maxLevel, minLevel);
}
function getStep(slider) {
// Default = 1. Support future: data-step, or <input step="">
var raw = null;
if (slider) raw = slider.getAttribute("data-step") || slider.getAttribute("step");
var s = parseInt(raw, 10);
if (!s || isNaN(s) || s < 1) s = 1;
return s;
}
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 setLevelMax(card, maxLevel) {
var span = card.querySelector(".sv-level-max");
if (span) span.textContent = String(maxLevel);
}
function applySeriesToScope(scope, boundary, level) {
if (!scope) return;
// JS arrays are 0-based; Lua uses 1-based "level".
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 unhide(el) {
if (!el) return;
if (el.classList) el.classList.remove("sv-hidden");
if (el.removeAttribute) el.removeAttribute("hidden");
if (el.style && el.style.display === "none") el.style.display = "";
}
function hide(el) {
if (!el) return;
if (el.classList) el.classList.add("sv-hidden");
if (el.setAttribute) el.setAttribute("hidden", "hidden");
}
/* ================================================================== */
/* Custom slider helpers (bounds/percent) */
/* ================================================================== */
function getCustomMin(slider, fallback) {
return clampInt(
slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"),
1,
999,
fallback != null ? fallback : 1
);
}
function getCustomMax(slider, fallback) {
return clampInt(
slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"),
1,
999,
fallback != null ? fallback : 1
);
}
function getCustomBounds(slider, fallbackMin) {
var min = getCustomMin(slider, fallbackMin != null ? fallbackMin : 1);
var max = getCustomMax(slider, min);
if (max < min) max = min;
return { min: min, max: max };
}
function getCustomValue(slider, min, max, fallback) {
return clampInt(
slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"),
min,
max,
fallback != null ? fallback : min
);
}
function pctFromValue(min, max, value) {
if (max === min) return 0;
var pct = (value - min) / (max - min);
if (pct < 0) pct = 0;
if (pct > 1) pct = 1;
return pct;
}
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));
}
/* ================================================================== */
/* Custom slider value bubble */
/* - JS owns structure + text/position only */
/* - CSS owns visuals */
/* ================================================================== */
function ensureValueLabel(slider) {
if (!slider || !slider.querySelector || !isCustomSlider(slider)) return null;
var bubble = slider.querySelector(".sv-level-bubble");
if (bubble) return bubble;
bubble = document.createElement("span");
bubble.className = "sv-level-bubble sv-hidden";
bubble.setAttribute("hidden", "hidden");
bubble.setAttribute("aria-hidden", "true");
slider.appendChild(bubble);
return bubble;
}
function setValueLabel(slider, value) {
var bubble = ensureValueLabel(slider);
if (!bubble) return;
var b = getCustomBounds(slider, 1);
bubble.textContent = String(value);
bubble.style.left = pctFromValue(b.min, b.max, value) * 100 + "%";
}
function showValueLabel(slider) {
if (!slider) return;
var bubble = ensureValueLabel(slider);
if (!bubble) return;
if (slider._svBubbleTimer) {
clearTimeout(slider._svBubbleTimer);
slider._svBubbleTimer = null;
}
unhide(bubble);
}
function hideValueLabelSoon(slider, delayMs) {
if (!slider) return;
var bubble = slider.querySelector ? slider.querySelector(".sv-level-bubble") : null;
if (!bubble) return;
if (slider._svBubbleTimer) clearTimeout(slider._svBubbleTimer);
slider._svBubbleTimer = setTimeout(function () {
hide(bubble);
slider._svBubbleTimer = null;
}, delayMs != null ? delayMs : 450);
}
function updateCustomVisual(slider, value) {
var b = getCustomBounds(slider, 1);
var left = pctFromValue(b.min, b.max, value) * 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 + "%";
setValueLabel(slider, value);
}
function setCustomValue(slider, value, min, max) {
slider.setAttribute("aria-valuenow", String(value));
slider.setAttribute("data-value", String(value));
// Helpful SR text (doesn't affect visuals)
slider.setAttribute("aria-valuetext", "Level " + String(value) + " of " + String(max));
updateCustomVisual(slider, value);
}
function snapToStep(raw, min, max, step) {
if (step < 1) step = 1;
var x = raw;
if (x < min) x = min;
if (x > max) x = max;
var snapped = min + Math.round((x - min) / step) * step;
if (snapped < min) snapped = min;
if (snapped > max) snapped = max;
return snapped;
}
function valueFromClientX(slider, clientX) {
var rect = slider.getBoundingClientRect();
var b = getCustomBounds(slider, 1);
var w = rect.width || 1;
var x = (clientX - rect.left) / w;
if (x < 0) x = 0;
if (x > 1) x = 1;
var raw = b.min + x * (b.max - b.min);
return snapToStep(raw, b.min, b.max, getStep(slider));
}
/* ================================================================== */
/* End-of-bar value label (right-side value) */
/* ================================================================== */
function ensureEndValueLabel(card, slider) {
var wrap = null;
if (slider) wrap = closest(slider, ".sv-level-slider");
if (!wrap && card && card.querySelector) wrap = card.querySelector(".sv-level-slider");
if (!wrap) return null;
var out = wrap.querySelector(".sv-level-endvalue");
if (out) return out;
out = document.createElement("span");
out.className = "sv-level-endvalue";
out.setAttribute("aria-hidden", "true"); // slider already exposes aria-valuetext
wrap.appendChild(out);
return out;
}
function setEndValueLabel(card, slider, value) {
var out = ensureEndValueLabel(card, slider);
if (out) out.textContent = String(value);
}
/* ================================================================== */
/* Core apply / init */
/* ================================================================== */
function scheduleApply(card, rawLevel) {
if (!card) return;
card._svLevelNext = rawLevel;
if (card._svLevelRaf) return;
if (window.requestAnimationFrame) {
card._svLevelRaf = requestAnimationFrame(function () {
card._svLevelRaf = null;
var v = card._svLevelNext;
card._svLevelNext = null;
applyLevelToCard(card, v);
});
return;
}
card._svLevelRaf = setTimeout(function () {
card._svLevelRaf = null;
var v = card._svLevelNext;
card._svLevelNext = null;
applyLevelToCard(card, v);
}, 0);
}
function applyLevelToCard(card, rawLevel) {
if (!card) return;
var maxLevel = getCardMaxLevel(card);
var slider = getLevelSlider(card);
var minLevel = getCardMinLevel(card, slider, maxLevel);
var level = clampInt(rawLevel, minLevel, maxLevel, getCardLevel(card, minLevel, maxLevel));
card.setAttribute("data-level", String(level));
setLevelNumber(card, level);
setLevelMax(card, maxLevel);
if (slider) {
unhide(slider);
var wrap = closest(slider, ".sv-level-slider");
if (wrap) unhide(wrap);
}
if (slider) {
if (isRangeInput(slider)) {
slider.setAttribute("min", String(minLevel));
slider.setAttribute("max", String(maxLevel));
slider.setAttribute("step", String(getStep(slider)));
slider.setAttribute("aria-valuetext", "Level " + String(level) + " of " + String(maxLevel));
if (String(slider.value) !== String(level)) slider.value = String(level);
} else if (isCustomSlider(slider)) {
setCustomBounds(slider, minLevel, maxLevel);
setCustomValue(slider, level, minLevel, maxLevel);
}
}
setEndValueLabel(card, 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];
var stamped = clampInt(card.getAttribute(LEVEL_INIT_ATTR), 0, 999, 0);
if (stamped >= LEVELS_VERSION) continue;
var maxLevel = getCardMaxLevel(card);
var slider = getLevelSlider(card);
var minLevel = getCardMinLevel(card, slider, maxLevel);
var start = getCardLevel(card, minLevel, maxLevel);
if (slider && maxLevel > minLevel) {
if (isRangeInput(slider)) start = clampInt(slider.value, minLevel, maxLevel, start);
else if (isCustomSlider(slider)) start = getCustomValue(slider, minLevel, maxLevel, start);
}
applyLevelToCard(card, start);
card.setAttribute(LEVEL_INIT_ATTR, String(LEVELS_VERSION));
}
}
/* ================================================================== */
/* Events */
/* ================================================================== */
// Native range inputs
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;
scheduleApply(card, t.value);
},
true
);
// Custom slider pointer interaction (click anywhere + drag)
document.addEventListener(
"pointerdown",
function (e) {
if (e.button != null && e.button !== 0) return; // primary only
var slider =
e.target && e.target.closest
? e.target.closest(".sv-level-range--custom, .sv-level-range[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) {}
}
showValueLabel(slider);
scheduleApply(card, valueFromClientX(slider, e.clientX));
},
true
);
document.addEventListener(
"pointermove",
function (e) {
if (!_drag) return;
if (e.pointerId !== _drag.pointerId) return;
e.preventDefault();
showValueLabel(_drag.slider);
scheduleApply(_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) {}
}
hideValueLabelSoon(_drag.slider, 650);
_drag = null;
}
document.addEventListener("pointerup", endDrag, true);
document.addEventListener("pointercancel", endDrag, true);
// Mouse-only slider interaction:
// - Remove keyboard-driven adjustments on custom sliders
// - Block native <input type="range"> from changing via keyboard (if present)
var _SV_BLOCK_KEYS = {
ArrowLeft: 1,
ArrowRight: 1,
ArrowUp: 1,
ArrowDown: 1,
Left: 1,
Right: 1,
Up: 1,
Down: 1,
Home: 1,
End: 1,
PageUp: 1,
PageDown: 1
};
document.addEventListener(
"keydown",
function (e) {
var key = e && e.key;
if (!_SV_BLOCK_KEYS[key]) return;
var el = document.activeElement;
if (!el) return;
// Native range inputs (keep mouse drag, disable keyboard nudges)
if (isRangeInput(el) && el.classList && el.classList.contains("sv-level-range")) {
e.preventDefault();
return;
}
// Custom sliders (we keep focus behaviors, but no keyboard changes)
var slider = closest(el, ".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
if (slider) {
e.preventDefault();
return;
}
},
true
);
// Show bubble on focus (discoverability), hide on blur
document.addEventListener(
"focusin",
function () {
var el = document.activeElement;
if (!el || !el.closest) return;
var slider = el.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
if (!slider) return;
var b = getCustomBounds(slider, 1);
var v = getCustomValue(slider, b.min, b.max, b.min);
setValueLabel(slider, v);
showValueLabel(slider);
},
true
);
document.addEventListener(
"focusout",
function (e) {
var t = e.target;
if (!t || !t.closest) return;
var slider = t.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
if (!slider) return;
hideValueLabelSoon(slider, 300);
},
true
);
function initAllLevels(root) {
initLevels(root);
}
COMMON.initAllLevels = initAllLevels;
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: Universal Popups */
/* ================================================================== */
var SV = (window.SV = window.SV || {});
var COMMON = (SV.common = SV.common || {});
// Bumped only because we changed positioning margins (safe cache-bust).
var UPOP_VERSION = 32; // Universal Popups v3.2
if (typeof COMMON.universalPopupsInit === "number" && COMMON.universalPopupsInit >= UPOP_VERSION) return;
COMMON.universalPopupsInit = UPOP_VERSION;
try { document.documentElement.classList.add("sv-uipop-v3"); } catch (e) {}
var DEF_SEL = ".sv-def";
var DEF_TIP_ATTR = "data-sv-def-tip";
var DEF_LINK_ATTR = "data-sv-def-link";
var TIP_BTN_SEL = ".sv-tip-btn[data-sv-toggle='1'], details.sv-tip > summary";
var DISCLOSE_BTN_SEL = ".sv-disclose-btn[data-sv-toggle='1'], details.sv-disclose > summary";
var WRAP_SEL = ".sv-tip, .sv-disclose";
var POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
var HOVER_CAPABLE = false;
try {
HOVER_CAPABLE = !!window.matchMedia && window.matchMedia("(hover:hover) and (pointer:fine)").matches;
} catch (e) { HOVER_CAPABLE = false; }
var popEl = null;
var state = {
open: false,
pinned: false,
trigger: null,
hideTimer: 0,
anchorKind: "trigger", // trigger | finger | center
anchorX: 0,
anchorY: 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 clamp(n, min, max) {
if (n < min) return min;
if (n > max) return max;
return n;
}
function safeIdSelector(id) {
return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
}
function isInsidePopup(target) {
return !!(popEl && (target === popEl || (popEl.contains && popEl.contains(target))));
}
function isSummaryTrigger(trigger) {
return !!(trigger && trigger.tagName === "SUMMARY");
}
function ensurePopEl() {
if (popEl) return popEl;
var el = document.createElement("div");
el.className = "sv-uipop sv-uipop--sm";
el.setAttribute("role", "dialog");
el.setAttribute("aria-hidden", "true");
document.body.appendChild(el);
el.addEventListener("mouseenter", function () {
if (!HOVER_CAPABLE) return;
if (!state.open || state.pinned) return;
clearTimeout(state.hideTimer);
state.hideTimer = 0;
});
el.addEventListener("mouseleave", function () {
if (!HOVER_CAPABLE) return;
if (!state.open || state.pinned) return;
clearTimeout(state.hideTimer);
state.hideTimer = setTimeout(function () {
if (state.open && !state.pinned) hidePopup();
}, 80);
});
popEl = el;
return popEl;
}
function setHidden(el, hidden) {
if (!el) return;
el.setAttribute("aria-hidden", hidden ? "true" : "false");
}
function stripIds(root) {
if (!root || root.nodeType !== 1) return;
if (root.hasAttribute && root.hasAttribute("id")) root.removeAttribute("id");
var nodes = root.querySelectorAll ? root.querySelectorAll("[id]") : [];
for (var i = 0; i < nodes.length; i++) nodes[i].removeAttribute("id");
}
function unhideTree(root) {
if (!root || root.nodeType !== 1) return;
if (root.hasAttribute && root.hasAttribute("hidden")) root.removeAttribute("hidden");
if (root.classList) root.classList.remove("sv-hidden");
if (root.style && root.style.display === "none") root.style.display = "";
var kids = root.children || [];
for (var i = 0; i < kids.length; i++) unhideTree(kids[i]);
}
function isBlankContent(node) {
if (!node) return true;
var t = node.textContent;
if (t == null) return true;
var compact = String(t).replace(/\s+/g, "").replace(/[—–-]+/g, "").trim();
return !compact;
}
function getDefTipText(defEl) {
if (!defEl || !defEl.getAttribute) return "";
var t = defEl.getAttribute(DEF_TIP_ATTR);
if (t == null) return "";
t = String(t);
t = t.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n");
if (!t.replace(/\s+/g, "")) return "";
return t;
}
function getDefLinkTitle(defEl) {
if (!defEl || !defEl.getAttribute) return "";
var t = defEl.getAttribute(DEF_LINK_ATTR);
if (t == null) return "";
return String(t).trim();
}
function getContentNodeFromTrigger(trigger) {
if (!trigger) return null;
if (trigger.tagName === "SUMMARY") {
var det = closest(trigger, "details.sv-tip, details.sv-disclose");
if (!det) return null;
var pop0 = det.querySelector(POP_SEL);
if (!pop0) return null;
return (
pop0.querySelector(".sv-tip-pop-body") ||
pop0.querySelector(".sv-disclose-pop-body") ||
pop0.querySelector(".sv-disclose-list") ||
pop0
);
}
var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
if (id) {
var wrap = closest(trigger, WRAP_SEL);
if (wrap) {
try {
var local = wrap.querySelector(safeIdSelector(id));
if (local) {
return (
local.querySelector(".sv-tip-pop-body") ||
local.querySelector(".sv-disclose-pop-body") ||
local.querySelector(".sv-disclose-list") ||
local
);
}
} catch (e) {}
}
var global = document.getElementById(id);
if (global) {
return (
global.querySelector(".sv-tip-pop-body") ||
global.querySelector(".sv-disclose-pop-body") ||
global.querySelector(".sv-disclose-list") ||
global
);
}
}
var w = closest(trigger, WRAP_SEL);
if (w) {
var pop = w.querySelector(POP_SEL);
if (pop) {
return (
pop.querySelector(".sv-tip-pop-body") ||
pop.querySelector(".sv-disclose-pop-body") ||
pop.querySelector(".sv-disclose-list") ||
pop
);
}
}
return null;
}
function guessTitle(trigger, sourceNode) {
var pop = null;
if (sourceNode && sourceNode.nodeType === 1) pop = closest(sourceNode, POP_SEL);
if (pop) {
var t = pop.querySelector(".sv-tip-pop-title") || pop.querySelector(".sv-disclose-pop-title");
if (t && t.textContent) return String(t.textContent).trim();
}
var aria = trigger && trigger.getAttribute ? trigger.getAttribute("aria-label") : "";
if (aria) return String(aria).trim();
var txt = trigger && trigger.textContent ? String(trigger.textContent).trim() : "";
if (txt) return txt.replace(/\s+/g, " ").trim();
return "Info";
}
function buildHeaderLink(linkTitle, displayText) {
if (!linkTitle) return null;
var a = document.createElement("a");
a.className = "sv-uipop-title-link";
a.textContent = displayText || String(linkTitle);
if (window.mw && mw.util && typeof mw.util.getUrl === "function") a.href = mw.util.getUrl(linkTitle);
else a.href = String(linkTitle);
a.addEventListener("click", function (e) { e.stopPropagation(); });
return a;
}
function fireContentHook(el) {
if (!el) return;
if (window.mw && mw.hook && window.jQuery) {
try { mw.hook("wikipage.content").fire(window.jQuery(el)); } catch (e) {}
}
}
function renderPopup(opts) {
var el = ensurePopEl();
if (!el) return;
el.classList.remove("sv-uipop--sm", "sv-uipop--lg");
el.classList.add(opts.size === "lg" ? "sv-uipop--lg" : "sv-uipop--sm");
while (el.firstChild) el.removeChild(el.firstChild);
var head = document.createElement("div");
head.className = "sv-uipop-head" + (opts.pinned ? " sv-uipop-head--clickable" : "");
var title = document.createElement("div");
title.className = "sv-uipop-title";
if (opts.titleLink) title.appendChild(opts.titleLink);
else title.textContent = opts.title || "Info";
head.appendChild(title);
el.appendChild(head);
var body = document.createElement("div");
body.className = "sv-uipop-body";
if (opts.bodyPre) body.classList.add("sv-uipop-body--pre");
if (opts.bodyNode) {
var clone = opts.bodyNode.cloneNode(true);
stripIds(clone);
unhideTree(clone);
body.appendChild(clone);
} else if (opts.bodyText != null) {
body.textContent = String(opts.bodyText);
}
el.appendChild(body);
if (opts.pinned) {
head.addEventListener(
"click",
function (e) {
e.preventDefault();
hidePopup();
},
{ once: true }
);
}
fireContentHook(el);
}
function positionPopup(trigger) {
var el = ensurePopEl();
if (!el) return;
el.style.left = "0px";
el.style.top = "0px";
setHidden(el, false);
requestAnimationFrame(function () {
if (!state.open) return;
var vw = window.innerWidth || document.documentElement.clientWidth || 0;
var vh = window.innerHeight || document.documentElement.clientHeight || 0;
var tight = vw <= 520;
var margin = tight ? 18 : 10;
var gap = tight ? 14 : 12;
var pr = el.getBoundingClientRect();
var w = pr.width || 240;
var h = pr.height || 180;
var left = margin;
var top = margin;
if (state.anchorKind === "center") {
left = (vw - w) / 2;
top = (vh - h) / 2;
} else if (state.anchorKind === "finger") {
left = (vw - w) / 2;
var y = state.anchorY || vh / 2;
top = y - h - gap;
if (top < margin) top = y + gap;
} else {
if (!trigger || !trigger.getBoundingClientRect) return;
var tr = trigger.getBoundingClientRect();
var isIconish = (tr.width || 0) <= 40;
left = isIconish ? tr.right - w : tr.left;
top = tr.bottom + gap;
left = clamp(left, margin, Math.max(margin, vw - w - margin));
if (top + h + margin > vh) top = tr.top - h - gap;
}
left = clamp(left, margin, Math.max(margin, vw - w - margin));
top = clamp(top, margin, Math.max(margin, vh - h - margin));
el.style.left = Math.round(left) + "px";
el.style.top = Math.round(top) + "px";
});
}
function setExpanded(trigger, expanded) {
if (!trigger || !trigger.setAttribute) return;
trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
}
function hidePopup() {
if (!state.open) return;
clearTimeout(state.hideTimer);
state.hideTimer = 0;
setExpanded(state.trigger, false);
state.open = false;
state.pinned = false;
state.trigger = null;
state.anchorKind = "trigger";
setHidden(ensurePopEl(), true);
}
function openPopup(trigger, opts) {
if (!trigger) return;
if (state.open && state.trigger && state.trigger !== trigger) hidePopup();
state.open = true;
state.pinned = !!opts.pinned;
state.trigger = trigger;
setExpanded(trigger, true);
renderPopup(opts);
setHidden(popEl, false);
positionPopup(trigger);
}
function findTrigger(t) {
if (!t) return null;
if (isInsidePopup(t)) return null;
var defEl = closest(t, DEF_SEL);
if (defEl && defEl.getAttribute && defEl.getAttribute(DEF_TIP_ATTR) != null) return defEl;
var tip = closest(t, TIP_BTN_SEL);
if (tip) return tip;
var dis = closest(t, DISCLOSE_BTN_SEL);
if (dis) return dis;
return null;
}
function resolveOpts(trigger) {
var isDef = trigger && trigger.matches && trigger.matches(DEF_SEL);
var isTip = trigger && trigger.matches && trigger.matches(TIP_BTN_SEL);
var isDisclose = trigger && trigger.matches && trigger.matches(DISCLOSE_BTN_SEL);
var mode = "hover";
var size = "sm";
var attrMode = trigger.getAttribute && trigger.getAttribute("data-sv-pop");
if (attrMode === "hover" || attrMode === "click") mode = attrMode;
var attrSize = trigger.getAttribute && trigger.getAttribute("data-sv-pop-size");
if (attrSize === "sm" || attrSize === "lg") size = attrSize;
if (!attrMode) {
if (isDisclose) mode = "click";
else if (isTip) mode = "hover";
else if (isDef) mode = getDefLinkTitle(trigger) ? "click" : "hover";
}
if (!attrSize) size = mode === "click" ? "lg" : "sm";
if (!HOVER_CAPABLE && mode === "hover") {
mode = "click";
size = "sm";
}
if (isDef) {
var tipText = getDefTipText(trigger);
if (!tipText) return null;
var linkTitle = getDefLinkTitle(trigger);
var label = trigger.querySelector ? trigger.querySelector(".sv-def-text") : null;
var titleText = label ? String(label.textContent || "").trim() : String(trigger.textContent || "").trim();
if (!titleText) titleText = "Definition";
return {
pinned: mode === "click",
size: size,
title: titleText,
titleLink: linkTitle ? buildHeaderLink(linkTitle, titleText) : null,
bodyText: tipText,
bodyPre: true,
bodyNode: null
};
}
var source = getContentNodeFromTrigger(trigger);
if (isBlankContent(source)) return null;
return {
pinned: mode === "click",
size: size,
title: guessTitle(trigger, source),
titleLink: null,
bodyNode: source,
bodyPre: false,
bodyText: null
};
}
window.addEventListener(
"pointerdown",
function (e) {
if (!e) return;
if (isInsidePopup(e.target)) return;
var trigger = findTrigger(e.target);
if (!trigger) return;
var opts = resolveOpts(trigger);
if (!opts) return;
if (isSummaryTrigger(trigger)) e.preventDefault();
state.anchorKind = !HOVER_CAPABLE || (e.pointerType && e.pointerType !== "mouse") ? "finger" : "trigger";
state.anchorX = typeof e.clientX === "number" ? e.clientX : 0;
state.anchorY = typeof e.clientY === "number" ? e.clientY : 0;
if (HOVER_CAPABLE && !opts.pinned && (e.pointerType === "mouse" || !e.pointerType)) {
e.stopPropagation();
return;
}
e.preventDefault();
e.stopPropagation();
if (state.open && state.pinned && state.trigger === trigger) {
hidePopup();
return;
}
openPopup(trigger, opts);
},
true
);
window.addEventListener(
"mouseover",
function (e) {
if (!HOVER_CAPABLE) return;
if (isInsidePopup(e.target)) return;
var trigger = findTrigger(e.target);
if (!trigger) {
if (state.open && !state.pinned) hidePopup();
return;
}
var opts = resolveOpts(trigger);
if (!opts || opts.pinned) return;
state.anchorKind = "trigger";
e.stopPropagation();
clearTimeout(state.hideTimer);
state.hideTimer = 0;
openPopup(trigger, opts);
},
true
);
window.addEventListener(
"mouseout",
function (e) {
if (!HOVER_CAPABLE) return;
if (!state.open || state.pinned) return;
var trigger = findTrigger(e.target);
if (!trigger || trigger !== state.trigger) return;
e.stopPropagation();
clearTimeout(state.hideTimer);
state.hideTimer = setTimeout(function () {
if (state.open && !state.pinned) hidePopup();
}, 80);
},
true
);
window.addEventListener(
"click",
function (e) {
if (!state.open) return;
if (isInsidePopup(e.target)) return;
var trigger = findTrigger(e.target);
if (trigger) {
e.stopPropagation();
return;
}
if (state.pinned || !HOVER_CAPABLE) {
hidePopup();
e.stopPropagation();
}
},
true
);
window.addEventListener(
"keydown",
function (e) {
if (e.key !== "Escape") return;
if (state.open) {
hidePopup();
e.stopPropagation();
}
},
true
);
window.addEventListener(
"scroll",
function () {
if (!state.open || !state.trigger) return;
positionPopup(state.trigger);
},
true
);
window.addEventListener(
"resize",
function () {
if (!state.open || !state.trigger) return;
positionPopup(state.trigger);
},
true
);
function stripDefTitles(root) {
var container = root || document;
var defs = container.querySelectorAll(DEF_SEL + "[title]");
for (var i = 0; i < defs.length; i++) defs[i].removeAttribute("title");
}
if (window.mw && mw.hook) {
mw.hook("wikipage.content").add(function ($content) {
stripDefTitles($content && $content[0]);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
stripDefTitles(document);
});
} else {
stripDefTitles(document);
}
})();
(function () {
/* ================================================================== */
/* MODULE: Tabs */
/* ================================================================== */
var SV = (window.SV = window.SV || {});
var COMMON = (SV.common = SV.common || {});
if (COMMON.tabsInit) return;
COMMON.tabsInit = 1;
var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']";
var TAB_SEL = ".sv-tab";
var PANEL_SEL = ".sv-tabpanel";
var _tabsUidCounter = 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 getTabs(root) {
return root.querySelectorAll(TAB_SEL);
}
function getPanels(root) {
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;
}
function ensureUniqueTabIds(root) {
var uid = root.getAttribute("data-tabs-uid");
if (!uid) {
_tabsUidCounter++;
uid = (root.getAttribute("data-tabs-root") || "sv-tabs") + "-" + _tabsUidCounter;
root.setAttribute("data-tabs-uid", uid);
}
var tabs = getTabs(root);
for (var i = 0; i < tabs.length; i++) {
var tab = tabs[i];
var key = tab.getAttribute("data-tab") || String(i + 1);
var panel = getPanelByKey(root, key);
var tabId = uid + "-tab-" + key;
tab.id = tabId;
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");
}
function activateTab(root, btn) {
if (!root || !btn) return;
var tabs = getTabs(root);
var key = btn.getAttribute("data-tab") || "";
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");
if (t.classList) t.classList.toggle("sv-tab--active", active);
}
var panels = getPanels(root);
for (var j = 0; j < panels.length; j++) {
var p = panels[j];
var isTarget = p.getAttribute("data-panel") === key;
if (isTarget) showPanel(p);
else hidePanel(p);
}
root.setAttribute("data-active-tab", key);
}
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]);
}
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;
e.preventDefault();
activateTab(root, btn);
},
true
);
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;
}
},
true
);
function initAllTabs(root) {
initTabs(root);
}
if (window.mw && mw.hook) {
mw.hook("wikipage.content").add(function ($content) {
initAllTabs($content && $content[0]);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
initAllTabs(document);
});
} else {
initAllTabs(document);
}
})();