MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
Created page with "→Any JavaScript here will be loaded for all users on every page load.: →SpiritVale Skill Slider What this does: - Finds every skill card: <div class="sv-skill-card" data-max-level="10" data-level="10"> - Inserts a slider into: <div class="sv-level-slider"></div> - Updates any element inside the card that has: data-series='["Lv1 text","Lv2 text", ...]' so that it shows the correct value for the chosen level.: (function () {..." |
No edit summary Tags: Mobile edit Mobile web edit |
||
| (33 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/* | /* 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 () { | (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 < min) return min; | ||
if (n > max) return max; | if (n > max) return max; | ||
| Line 28: | Line 50: | ||
} | } | ||
// | function closest(el, sel) { | ||
// | if (!el || el.nodeType !== 1) return null; | ||
function | 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 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 | |||
var maxLevel = getCardMaxLevel(card); | |||
if (maxLevel | 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); | ||
var | card.setAttribute(LEVEL_INIT_ATTR, String(LEVELS_VERSION)); | ||
if ( | } | ||
} | |||
/* ================================================================== */ | |||
/* 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)); | |||
if (! | }, | ||
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; | 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") : ""; | |||
var | if (id) { | ||
if ( | 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) { | if (window.mw && mw.hook) { | ||
mw.hook("wikipage.content").add(function ($content) { | mw.hook("wikipage.content").add(function ($content) { | ||
stripDefTitles($content && $content[0]); | |||
}); | }); | ||
} | } | ||
if (document.readyState === "loading") { | if (document.readyState === "loading") { | ||
document.addEventListener("DOMContentLoaded", function () { | document.addEventListener("DOMContentLoaded", function () { | ||
stripDefTitles(document); | |||
}); | }); | ||
} else { | } 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); | |||
} | |||
})(); | })(); | ||