Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Join the Playtest on Steam Now: SpiritVale

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
No edit summary
No edit summary
Tags: Mobile edit Mobile web edit
 
(22 intermediate revisions by the same user not shown)
Line 1: Line 1:
/* GameInfo JS (Phase 4.1 Refactor)
/* Common.js — Spirit Vale Wiki
   Shared behaviors for all GameInfo modules:
  Phase 4.1 — Core client scripts
   - Level selector (data-series swap)
 
   - Popups (sv-tip / sv-disclose using sv-hidden)
   Included (standalone modules):
   - Tabs (hidden attribute toggling + ARIA ids) */
   1) Level Selector / Descriptions (data-series)
   2) Universal Popups (tips, discloses, definitions)
   3) Tabs
*/


(function () {
(function () {
   if (window.SV_GAMEINFO_41_INIT) return;
  /* ================================================================== */
   window.SV_GAMEINFO_41_INIT = 1;
  /* 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.
   // Selectors
   var CARD_SEL =
  // ---------------------------------------------------------------------------
    "[data-sv-card='1'], .sv-gi-card, .sv-skill-card, .sv-passive-card";


   var CARD_SEL = ".sv-gi-card[data-gi='1'], .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_RANGE_SEL = "input.sv-level-range[type='range']";
   var LEVEL_BOUNDARY_SEL =
  var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
    "[data-sv-level-boundary='1'], .sv-skill-level, .sv-gi-level";
  var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom";
  var SERIES_SEL = "[data-series]";


   var POP_WRAP_SEL = ".sv-tip, .sv-disclose";
   var LEVEL_SCOPE_SEL =
  var POP_BTN_SEL = "[data-sv-toggle='1']";
    "[data-sv-level-scope='1'], .sv-gi-bottom, .sv-skill-bottom";
  var HIDDEN_CLASS = "sv-hidden";


   var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']";
   var SERIES_SEL = "[data-series]";
   var TAB_SEL = ".sv-tab[role='tab'], .sv-tab";
   var LEVEL_INIT_ATTR = "data-sv-level-init";
  var PANEL_SEL = ".sv-tabpanel";


  // Internals
   var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
   var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
   var _tabsUidCounter = 0;
   var _drag = null;
 
  // ---------------------------------------------------------------------------
  // Utils
  // ---------------------------------------------------------------------------


   function clampInt(value, min, max, fallback) {
   function clampInt(value, min, max, fallback) {
Line 85: Line 91:
     if (boundaryNode === el) return false;
     if (boundaryNode === el) return false;
     if (!boundaryNode.compareDocumentPosition) return true;
     if (!boundaryNode.compareDocumentPosition) return true;
     return (boundaryNode.compareDocumentPosition(el) & 4) !== 0; // following
     return (boundaryNode.compareDocumentPosition(el) & 4) !== 0;
  }
 
  function isRangeInput(el) {
    return !!(el && el.tagName === "INPUT" && el.type === "range");
   }
   }


   // ---------------------------------------------------------------------------
   function isCustomSlider(el) {
  // Level Selector
    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) {
   function getCardMaxLevel(card) {
     return clampInt(card.getAttribute("data-max-level"), 1, 999, 1);
    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, maxLevel) {
   function getCardLevel(card, minLevel, maxLevel) {
     return clampInt(card.getAttribute("data-level"), 1, maxLevel, 1);
     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;
   }
   }


Line 105: Line 167:


   function getLevelBoundary(slider) {
   function getLevelBoundary(slider) {
    if (!slider) return null;
     return closest(slider, LEVEL_BOUNDARY_SEL) || slider;
     return closest(slider, LEVEL_BOUNDARY_SEL) || slider;
   }
   }
Line 117: 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);
   }
   }


   function applySeriesToScope(scope, boundary, level) {
   function applySeriesToScope(scope, boundary, level) {
    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 125: Line 196:
     for (var i = 0; i < nodes.length; i++) {
     for (var i = 0; i < nodes.length; i++) {
       var el = nodes[i];
       var el = nodes[i];
 
       if (boundary && !isAfter(boundary, el)) continue;
      // Update only nodes below the level boundary.
       if (!isAfter(boundary, el)) continue;


       var series = parseSeries(el);
       var series = parseSeries(el);
Line 143: Line 212:
   }
   }


   function applyLevelToCard(card, rawLevel) {
   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;
     if (!card) return;


     var maxLevel = getCardMaxLevel(card);
     card._svLevelNext = rawLevel;
     if (maxLevel <= 1) {
    if (card._svLevelRaf) return;
       card.setAttribute("data-level", "1");
 
      setLevelNumber(card, 1);
     if (window.requestAnimationFrame) {
       card._svLevelRaf = requestAnimationFrame(function () {
        card._svLevelRaf = null;
        var v = card._svLevelNext;
        card._svLevelNext = null;
        applyLevelToCard(card, v);
      });
       return;
       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 slider = getLevelSlider(card);
     var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));
    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);
    setLevelMax(card, maxLevel);


     if (slider) {
     if (slider) {
       slider.setAttribute("min", "1");
       unhide(slider);
       slider.setAttribute("max", String(maxLevel));
       var wrap = closest(slider, ".sv-level-slider");
       if (String(slider.value) !== String(level)) slider.value = String(level);
       if (wrap) unhide(wrap);
     }
     }
    setLevelNumber(card, level);


     if (slider) {
     if (slider) {
       var boundary = getLevelBoundary(slider);
       if (isRangeInput(slider)) {
       var scope = getLevelScopeContainer(card, boundary);
        slider.setAttribute("min", String(minLevel));
      applySeriesToScope(scope, boundary, level);
        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);
   }
   }


Line 179: Line 479:
     for (var i = 0; i < cards.length; i++) {
     for (var i = 0; i < cards.length; i++) {
       var card = cards[i];
       var card = cards[i];
       if (card.getAttribute("data-gi-level-init") === "1") continue;
 
       var stamped = clampInt(card.getAttribute(LEVEL_INIT_ATTR), 0, 999, 0);
      if (stamped >= LEVELS_VERSION) continue;


       var maxLevel = getCardMaxLevel(card);
       var maxLevel = getCardMaxLevel(card);
       var slider = getLevelSlider(card);
       var slider = getLevelSlider(card);
      var minLevel = getCardMinLevel(card, slider, maxLevel);


       if (!slider || maxLevel <= 1) {
      var start = getCardLevel(card, minLevel, maxLevel);
         card.setAttribute("data-gi-level-init", "1");
 
         continue;
       if (slider && maxLevel > minLevel) {
         if (isRangeInput(slider)) start = clampInt(slider.value, minLevel, maxLevel, start);
         else if (isCustomSlider(slider)) start = getCustomValue(slider, minLevel, maxLevel, start);
       }
       }
      var start = slider.value;
      if (start == null || start === "") start = getCardLevel(card, maxLevel);


       applyLevelToCard(card, start);
       applyLevelToCard(card, start);
       card.setAttribute("data-gi-level-init", "1");
       card.setAttribute(LEVEL_INIT_ATTR, String(LEVELS_VERSION));
     }
     }
   }
   }


  /* ================================================================== */
  /* Events                                                              */
  /* ================================================================== */
  // Native range inputs
   document.addEventListener(
   document.addEventListener(
     "input",
     "input",
Line 202: Line 509:
       var t = e.target;
       var t = e.target;
       if (!t || t.nodeType !== 1) return;
       if (!t || t.nodeType !== 1) return;
       if (!t.matches || !t.matches(LEVEL_RANGE_SEL)) return;
       if (!t.matches || !t.matches("input.sv-level-range[type='range']")) return;


       var card = closest(t, CARD_SEL);
       var card = closest(t, CARD_SEL);
       if (!card) return;
       if (!card) return;


       applyLevelToCard(card, t.value);
       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
     true
   );
   );


   // ---------------------------------------------------------------------------
   document.addEventListener(
  // Popups (sv-hidden toggles)
    "pointermove",
  // ---------------------------------------------------------------------------
    function (e) {
      if (!_drag) return;
      if (e.pointerId !== _drag.pointerId) return;
      e.preventDefault();


   function getPopupPanel(btn) {
      showValueLabel(_drag.slider);
     if (!btn) return null;
      scheduleApply(_drag.card, valueFromClientX(_drag.slider, e.clientX));
     var id = btn.getAttribute("aria-controls");
    },
     if (!id) return null;
    true
     return document.getElementById(id);
  );
 
   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;
   }
   }


   function isPanelOpen(panel) {
   document.addEventListener("pointerup", endDrag, true);
    return panel && panel.classList && !panel.classList.contains(HIDDEN_CLASS);
  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);
   }
   }


   function setPanelOpen(btn, panel, open) {
   COMMON.initAllLevels = initAllLevels;
    if (!panel || !panel.classList) return;


    if (open) panel.classList.remove(HIDDEN_CLASS);
  if (window.mw && mw.hook) {
     else panel.classList.add(HIDDEN_CLASS);
     mw.hook("wikipage.content").add(function ($content) {
      initAllLevels($content && $content[0]);
    });
  }


    if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAllLevels(document);
    });
  } else {
    initAllLevels(document);
   }
   }
})();


  function closeAllPopups(scope) {
(function () {
    var root = scope || document;
  /* ================================================================== */
    var btns = root.querySelectorAll(POP_BTN_SEL);
  /* MODULE: Universal Popups                                            */
    for (var i = 0; i < btns.length; i++) {
  /* ================================================================== */
      var b = btns[i];
 
      var p = getPopupPanel(b);
  var SV = (window.SV = window.SV || {});
       if (p) setPanelOpen(b, p, false);
  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 closeOtherPopupsWithinCard(currentBtn) {
   function isInsidePopup(target) {
     var card = closest(currentBtn, CARD_SEL);
     return !!(popEl && (target === popEl || (popEl.contains && popEl.contains(target))));
    var scope = card || document;
  }


     var btns = scope.querySelectorAll(POP_BTN_SEL);
  function isSummaryTrigger(trigger) {
     for (var i = 0; i < btns.length; i++) {
    return !!(trigger && trigger.tagName === "SUMMARY");
      var b = btns[i];
  }
       if (b === currentBtn) continue;
 
       var p = getPopupPanel(b);
  function ensurePopEl() {
       if (p) setPanelOpen(b, p, false);
    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 togglePopup(btn) {
   function guessTitle(trigger, sourceNode) {
     var panel = getPopupPanel(btn);
     var pop = null;
     if (!panel) return;
    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;


     var open = !isPanelOpen(panel);
     return {
    if (open) closeOtherPopupsWithinCard(btn);
      pinned: mode === "click",
    setPanelOpen(btn, panel, open);
      size: size,
      title: guessTitle(trigger, source),
      titleLink: null,
      bodyNode: source,
      bodyPre: false,
      bodyText: null
    };
   }
   }


   // Click to toggle
   window.addEventListener(
   document.addEventListener(
    "pointerdown",
     "click",
    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) {
     function (e) {
       var btn = e.target && e.target.closest ? e.target.closest(POP_BTN_SEL) : null;
       if (!HOVER_CAPABLE) return;
       if (btn) {
      if (isInsidePopup(e.target)) return;
         togglePopup(btn);
 
        e.preventDefault();
      var trigger = findTrigger(e.target);
       if (!trigger) {
         if (state.open && !state.pinned) hidePopup();
         return;
         return;
       }
       }


       // Click outside closes all
       var opts = resolveOpts(trigger);
       if (!closest(e.target, POP_WRAP_SEL)) closeAllPopups(document);
       if (!opts || opts.pinned) return;
 
      state.anchorKind = "trigger";
 
      e.stopPropagation();
      clearTimeout(state.hideTimer);
      state.hideTimer = 0;
 
      openPopup(trigger, opts);
     },
     },
     true
     true
   );
   );


   // Keyboard toggle (Enter/Space on popup buttons)
   window.addEventListener(
   document.addEventListener(
    "mouseout",
     "keydown",
    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) {
     function (e) {
       var t = e.target;
       if (!state.open) return;
       if (!t || t.nodeType !== 1) return;
       if (isInsidePopup(e.target)) return;


       if (e.key === "Escape") {
       var trigger = findTrigger(e.target);
         closeAllPopups(document);
      if (trigger) {
         e.stopPropagation();
         return;
         return;
       }
       }


       var btn = t.closest ? t.closest(POP_BTN_SEL) : null;
       if (state.pinned || !HOVER_CAPABLE) {
       if (!btn) return;
        hidePopup();
        e.stopPropagation();
       }
    },
    true
  );


       if (e.key === "Enter" || e.key === " ") {
  window.addEventListener(
         togglePopup(btn);
    "keydown",
         e.preventDefault();
    function (e) {
       if (e.key !== "Escape") return;
      if (state.open) {
         hidePopup();
         e.stopPropagation();
       }
       }
     },
     },
Line 308: Line 1,251:
   );
   );


   // ---------------------------------------------------------------------------
  window.addEventListener(
   // Tabs (hidden attribute toggling + ARIA ids)
    "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) {
   function getTabs(root) {
Line 318: Line 1,322:
   function getPanels(root) {
   function getPanels(root) {
     return root.querySelectorAll(PANEL_SEL);
     return root.querySelectorAll(PANEL_SEL);
  }
  function getPanelByKey(root, key) {
    var panels = getPanels(root);
    for (var i = 0; i < panels.length; i++) {
      if (panels[i].getAttribute("data-panel") === key) return panels[i];
    }
    return null;
   }
   }


Line 324: Line 1,336:
     if (!uid) {
     if (!uid) {
       _tabsUidCounter++;
       _tabsUidCounter++;
       uid = (root.getAttribute("data-tabs-root") || "svgi-tabs") + "-" + _tabsUidCounter;
       uid = (root.getAttribute("data-tabs-root") || "sv-tabs") + "-" + _tabsUidCounter;
       root.setAttribute("data-tabs-uid", uid);
       root.setAttribute("data-tabs-uid", uid);
     }
     }


     var tabs = getTabs(root);
     var tabs = getTabs(root);
    var panels = getPanels(root);
     for (var i = 0; i < tabs.length; i++) {
    var n = Math.min(tabs.length, panels.length);
 
     for (var i = 0; i < n; i++) {
      var tabId = uid + "-tab-" + (i + 1);
      var panelId = uid + "-panel-" + (i + 1);
 
       var tab = tabs[i];
       var tab = tabs[i];
       var panel = panels[i];
      var key = tab.getAttribute("data-tab") || String(i + 1);
       var panel = getPanelByKey(root, key);


      var tabId = uid + "-tab-" + key;
       tab.id = tabId;
       tab.id = tabId;
      tab.setAttribute("aria-controls", panelId);


       panel.id = panelId;
       if (panel) {
      panel.setAttribute("aria-labelledby", tabId);
        var panelId = uid + "-panel-" + key;
        panel.id = panelId;
        tab.setAttribute("aria-controls", panelId);
        panel.setAttribute("aria-labelledby", tabId);
      }
     }
     }
  }
  function showPanel(panel) {
    if (!panel) return;
    panel.setAttribute("data-active", "1");
    if (panel.classList) panel.classList.remove("sv-hidden");
    panel.removeAttribute("hidden");
  }
  function hidePanel(panel) {
    if (!panel) return;
    panel.setAttribute("data-active", "0");
    if (panel.classList) panel.classList.add("sv-hidden");
    panel.setAttribute("hidden", "hidden");
   }
   }


Line 351: Line 1,376:


     var tabs = getTabs(root);
     var tabs = getTabs(root);
     var panels = getPanels(root);
     var key = btn.getAttribute("data-tab") || "";


     for (var i = 0; i < tabs.length; i++) {
     for (var i = 0; i < tabs.length; i++) {
Line 358: Line 1,383:
       t.setAttribute("aria-selected", active ? "true" : "false");
       t.setAttribute("aria-selected", active ? "true" : "false");
       t.setAttribute("tabindex", active ? "0" : "-1");
       t.setAttribute("tabindex", active ? "0" : "-1");
       if (t.classList) {
       if (t.classList) t.classList.toggle("sv-tab--active", active);
        if (active) t.classList.add("sv-tab--active");
        else t.classList.remove("sv-tab--active");
      }
     }
     }


     var targetId = btn.getAttribute("aria-controls");
     var panels = getPanels(root);
     for (var j = 0; j < panels.length; j++) {
     for (var j = 0; j < panels.length; j++) {
       var p = panels[j];
       var p = panels[j];
       var isTarget = p.id === targetId;
       var isTarget = p.getAttribute("data-panel") === key;
 
       if (isTarget) showPanel(p);
       if (isTarget) p.removeAttribute("hidden");
       else hidePanel(p);
       else p.setAttribute("hidden", "hidden");
     }
     }


     root.setAttribute("data-active-tab", targetId || "");
     root.setAttribute("data-active-tab", key);
   }
   }


Line 385: Line 1,406:


   function indexOfTab(tabs, btn) {
   function indexOfTab(tabs, btn) {
     for (var i = 0; i < tabs.length; i++) if (tabs[i] === btn) return i;
     for (var i = 0; i < tabs.length; i++) {
      if (tabs[i] === btn) return i;
    }
     return -1;
     return -1;
   }
   }
Line 421: Line 1,444:
   }
   }


  // Click to activate
   document.addEventListener(
   document.addEventListener("click", function (e) {
    "click",
    var btn = e.target && e.target.closest ? e.target.closest(TAB_SEL) : null;
    function (e) {
    if (!btn) return;
      var btn = e.target && e.target.closest ? e.target.closest(TAB_SEL) : null;
      if (!btn) return;


    var root = closest(btn, ".sv-tabs");
      var root = closest(btn, ".sv-tabs");
    if (!root || root.getAttribute("data-tabs") !== "1") return;
      if (!root || root.getAttribute("data-tabs") !== "1") return;


    activateTab(root, btn);
      e.preventDefault();
   });
      activateTab(root, btn);
    },
    true
   );


  // Keyboard navigation
   document.addEventListener(
   document.addEventListener("keydown", function (e) {
    "keydown",
    var btn =
    function (e) {
      document.activeElement && document.activeElement.closest
      var btn =
        ? document.activeElement.closest(TAB_SEL)
        document.activeElement && document.activeElement.closest
        : null;
          ? document.activeElement.closest(TAB_SEL)
    if (!btn) return;
          : null;
      if (!btn) return;


    var root = closest(btn, ".sv-tabs");
      var root = closest(btn, ".sv-tabs");
    if (!root || root.getAttribute("data-tabs") !== "1") return;
      if (!root || root.getAttribute("data-tabs") !== "1") return;


    var tabs = getTabs(root);
      var tabs = getTabs(root);
    if (!tabs || !tabs.length) return;
      if (!tabs || !tabs.length) return;


    var idx = indexOfTab(tabs, btn);
      var idx = indexOfTab(tabs, btn);
    if (idx < 0) return;
      if (idx < 0) return;


    if (e.key === "ArrowLeft" || e.key === "Left") {
      if (e.key === "ArrowLeft" || e.key === "Left") {
      e.preventDefault();
        e.preventDefault();
      activateTab(root, focusTab(tabs, idx - 1));
        activateTab(root, focusTab(tabs, idx - 1));
      return;
        return;
    }
      }
    if (e.key === "ArrowRight" || e.key === "Right") {
      if (e.key === "ArrowRight" || e.key === "Right") {
      e.preventDefault();
        e.preventDefault();
      activateTab(root, focusTab(tabs, idx + 1));
        activateTab(root, focusTab(tabs, idx + 1));
      return;
        return;
    }
      }
    if (e.key === "Home") {
      if (e.key === "Home") {
      e.preventDefault();
        e.preventDefault();
      activateTab(root, focusTab(tabs, 0));
        activateTab(root, focusTab(tabs, 0));
      return;
        return;
    }
      }
    if (e.key === "End") {
      if (e.key === "End") {
      e.preventDefault();
        e.preventDefault();
      activateTab(root, focusTab(tabs, tabs.length - 1));
        activateTab(root, focusTab(tabs, tabs.length - 1));
      return;
        return;
    }
      }
    if (e.key === "Enter" || e.key === " ") {
      if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
        e.preventDefault();
      activateTab(root, btn);
        activateTab(root, btn);
      return;
        return;
     }
      }
   });
     },
    true
   );


  // ---------------------------------------------------------------------------
   function initAllTabs(root) {
  // Init
  // ---------------------------------------------------------------------------
 
   function initAll(root) {
    initLevels(root);
     initTabs(root);
     initTabs(root);
   }
   }
Line 487: Line 1,512:
   if (window.mw && mw.hook) {
   if (window.mw && mw.hook) {
     mw.hook("wikipage.content").add(function ($content) {
     mw.hook("wikipage.content").add(function ($content) {
       initAll($content && $content[0]);
       initAllTabs($content && $content[0]);
     });
     });
   }
   }
Line 493: Line 1,518:
   if (document.readyState === "loading") {
   if (document.readyState === "loading") {
     document.addEventListener("DOMContentLoaded", function () {
     document.addEventListener("DOMContentLoaded", function () {
       initAll(document);
       initAllTabs(document);
     });
     });
   } else {
   } else {
     initAll(document);
     initAllTabs(document);
   }
   }
})();
})();

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);
  }
})();