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
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users on every page load. */
/* GameInfo JS (Phase 4.1)
 
  Shared, reusable behaviors for all GameInfo modules. */
/*
  SpiritVale Skill Slider
 
  What this does:
  - Finds every skill card:        <div class="sv-skill-card" data-max-level="10" data-level="10">
  - Inserts a slider into:        <div class="sv-level-slider"></div>
  - Updates any element inside the card that has:
        data-series='["Lv1 text","Lv2 text", ...]'
    so that it shows the correct value for the chosen level.
*/


(function () {
(function () {
  if (window.SV_GAMEINFO_41_INIT) return;
  window.SV_GAMEINFO_41_INIT = 1;


    // Ensure a number stays between a minimum and maximum.
  /* ------------------------------------------------------------------ */
    function clampNumber(value, min, max) {
  /* Level Selector                                                    */
        var n = parseInt(value, 10);
  /* ------------------------------------------------------------------ */


        // If value is not a valid number, fall back to min.
  function getCardMaxLevel(card) {
        if (isNaN(n)) {
    return clampInt(card.getAttribute("data-max-level"), 1, 999, 1);
            return min;
  }
        }


        if (n < min) return min;
  function getCardLevel(card, maxLevel) {
        if (n > max) return max;
    return clampInt(card.getAttribute("data-level"), 1, maxLevel, 1);
        return n;
  }
    }


    // Initialize skill cards found under `root`.
  function getLevelSlider(card) {
     // `root` is usually the page, but MediaWiki sometimes gives us just a section of HTML.
     return card.querySelector(LEVEL_RANGE_SEL);
    function initSkillCards(root) {
  }
        var container = root || document;


        // Only pick cards we haven't initialized yet (data-sv-init is our marker).
  function getLevelBoundary(slider) {
        var cards = container.querySelectorAll(
    return closest(slider, LEVEL_BOUNDARY_SEL) || slider;
            ".sv-skill-card:not([data-sv-init]), .sv-passive-card:not([data-sv-init])"
  }
        );


        cards.forEach(function (card) {
  function getLevelScopeContainer(card, boundary) {
            // Read max level from HTML attribute (default 1 if missing).
    var scope = card.querySelector(LEVEL_SCOPE_SEL) || card;
            var maxLevel = clampNumber(card.getAttribute("data-max-level"), 1, 999);
    if (boundary && scope !== card && !scope.contains(boundary)) return card;
    return scope;
  }


            // If max level is 1, there's no reason to create a slider.
  function setLevelNumber(card, level) {
            if (maxLevel <= 1) {
    var span = card.querySelector(".sv-level-num");
                card.setAttribute("data-sv-init", "1");
    if (span) span.textContent = String(level);
                return;
  }
            }


            // Read the starting level; if missing, default to max level.
  function applySeriesToScope(scope, boundary, level) {
            var startLevel = card.getAttribute("data-level");
    var index = level - 1;
            if (startLevel == null || startLevel === "") {
    var nodes = scope.querySelectorAll(SERIES_SEL);
                startLevel = maxLevel;
            }
            startLevel = clampNumber(startLevel, 1, maxLevel);


            // Find where we should place the slider.
    for (var i = 0; i < nodes.length; i++) {
            var sliderHost = card.querySelector(".sv-level-slider");
      var el = nodes[i];
            if (!sliderHost) {
                // No placeholder = nothing to do.
                card.setAttribute("data-sv-init", "1");
                return;
            }


            // Optional: element that shows the current level number in text.
      // Phase 4.1 rule: only update fields below the level boundary.
            var levelNumberSpan = card.querySelector(".sv-level-num");
      if (!isAfter(boundary, el)) continue;


            // Find all dynamic elements that contain a data-series list.
      var series = parseSeries(el);
            var dynamicElements = card.querySelectorAll("[data-series]");
      if (!series || series.length === 0) continue;


            // Parse the JSON data-series once and store it on the element for reuse.
      // Clamp if series shorter than expected.
            dynamicElements.forEach(function (el) {
      var safeIndex = index;
                var raw = el.getAttribute("data-series");
      if (safeIndex >= series.length) safeIndex = series.length - 1;
                try {
      if (safeIndex < 0) safeIndex = 0;
                    el._svSeries = JSON.parse(raw);
                } catch (e) {
                    el._svSeries = null;
                }
            });


            // Create the slider element.
      var value = series[safeIndex];
            var slider = document.createElement("input");
      if (value == null) value = "";
            slider.type = "range";
            slider.min = "1";
            slider.max = String(maxLevel);
            slider.value = String(startLevel);
            slider.className = "sv-level-range";


            // Clear any existing content in the host, then add the slider.
      // Text-only (no HTML mode).
            sliderHost.textContent = "";
      el.textContent = String(value);
            sliderHost.appendChild(slider);
    }
  }


            // Apply a given level to this card:
  function applyLevelToCard(card, rawLevel) {
            // - update the level number text
    if (!card) return;
            // - update all dynamic elements to show the correct series value
            function applyLevel(level) {
                var lv = clampNumber(level, 1, maxLevel);


                // Store the chosen level on the card (optional, but useful for debugging).
    var maxLevel = getCardMaxLevel(card);
                card.setAttribute("data-level", String(lv));
    if (maxLevel <= 1) {
 
      card.setAttribute("data-level", "1");
                // Update the visible "current level" number if present.
      setLevelNumber(card, 1);
                if (levelNumberSpan) {
      return;
                    levelNumberSpan.textContent = String(lv);
    }
                }
 
                // Update each dynamic element:
                // Level 1 => index 0, Level 2 => index 1, etc.
                var index = lv - 1;
 
                dynamicElements.forEach(function (el) {
                    var series = el._svSeries;


                    // If series is missing or not a proper list, skip it.
    var slider = getLevelSlider(card);
                    if (!series || !Array.isArray(series) || series.length === 0) {
    var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));
                        return;
                    }


                    // If the series is shorter than maxLevel, clamp index to last element.
    // Store on card for other systems / debugging.
                    var safeIndex = index;
    card.setAttribute("data-level", String(level));
                    if (safeIndex >= series.length) {
                        safeIndex = series.length - 1;
                    }


                    var value = series[safeIndex];
    // Keep slider in sync.
 
    if (slider) {
                    // Put the chosen value into the element.
      slider.setAttribute("min", "1");
                    // Use an empty string if it's null/undefined.
      slider.setAttribute("max", String(maxLevel));
                    if (value == null) {
      if (String(slider.value) !== String(level)) slider.value = String(level);
                        el.textContent = "";
                    } else {
                        el.textContent = String(value);
                    }
                });
            }
 
            // When the slider moves, update the card live.
            slider.addEventListener("input", function () {
                applyLevel(slider.value);
            });
 
            // Apply the starting level right away.
            applyLevel(startLevel);
 
            // Mark this card as initialized so we don't do it twice.
            card.setAttribute("data-sv-init", "1");
        });
     }
     }


     // MediaWiki hook:
     setLevelNumber(card, level);
    // Runs when page content is ready (and also after AJAX updates).
    if (window.mw && mw.hook) {
        mw.hook("wikipage.content").add(function ($content) {
            // $content is usually a jQuery object; $content[0] is the real DOM node.
            initSkillCards($content && $content[0]);
        });
    }


     // Also run on normal page load as a fallback.
     // Apply dynamic series values (scoped below boundary).
     if (document.readyState === "loading") {
     if (slider) {
        document.addEventListener("DOMContentLoaded", function () {
      var boundary = getLevelBoundary(slider);
            initSkillCards(document);
      var scope = getLevelScopeContainer(card, boundary);
        });
      applySeriesToScope(scope, boundary, level);
    } else {
        initSkillCards(document);
     }
     }
  }


})();
  function initLevels(root) {
    var container = root || document;
    var cards = container.querySelectorAll(CARD_SEL);


(function () {
    for (var i = 0; i < cards.length; i++) {
    let pop = null;
      var card = cards[i];
    let activeBtn = null;
      if (card.getAttribute("data-gi-level-init") === "1") continue;
    let openMode = null; // "hover" | "click"


    function ensurePop() {
      var maxLevel = getCardMaxLevel(card);
        if (pop) return pop;
      var slider = getLevelSlider(card);
        pop = document.createElement("div");
        pop.className = "sv-tip-pop";
        pop.setAttribute("role", "dialog");
        pop.setAttribute("id", "sv-tip-pop");
        pop.style.display = "none";
        document.body.appendChild(pop);
        return pop;
    }


    function findContent(btn) {
      // No slider (or max <= 1) => this card doesn't participate.
         const kind = btn.getAttribute("data-sv-tip");
      if (!slider || maxLevel <= 1) {
         if (!kind) return null;
         card.setAttribute("data-gi-level-init", "1");
         continue;
      }


        const scope = btn.closest(".sv-tip-scope")
      var start = slider.value;
            || btn.closest(".sv-slot-card")
      if (start == null || start === "") start = getCardLevel(card, maxLevel);
            || btn.closest(".sv-skill-card")
            || btn.closest(".sv-passive-card")
            || btn.parentElement;
        if (!scope) return null;


        return scope.querySelector(`.sv-tip-content[data-sv-tip-content="${CSS.escape(kind)}"]`);
      applyLevelToCard(card, start);
      card.setAttribute("data-gi-level-init", "1");
     }
     }
  }


     function setExpanded(btn, expanded) {
  // Slider input (delegated)
        if (!btn) return;
  document.addEventListener(
        btn.setAttribute("aria-expanded", expanded ? "true" : "false");
    "input",
    }
     function (e) {
      var t = e.target;
      if (!t || t.nodeType !== 1) return;
      if (!t.matches || !t.matches(LEVEL_RANGE_SEL)) return;


    function positionPop(btn) {
      var card = closest(t, CARD_SEL);
        if (!pop || !btn) return;
      if (!card) return;


        const r = btn.getBoundingClientRect();
      applyLevelToCard(card, t.value);
        const margin = 8;
    },
    true
  );


        const pw = pop.offsetWidth;
  /* ------------------------------------------------------------------ */
        const ph = pop.offsetHeight;
  /* Popup                                                              */
  /* ------------------------------------------------------------------ */


        let left = r.left + (r.width / 2) - (pw / 2);
  function closeDetails(d) {
        left = Math.max(margin, Math.min(left, window.innerWidth - pw - margin));
    if (d && d.open) d.open = false;
  }


        let top = r.bottom + margin;
  function closeAllPopups(scope) {
        if (top + ph > window.innerHeight - margin) {
    var root = scope || document;
            top = r.top - ph - margin;
    var openOnes = root.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
        }
    for (var i = 0; i < openOnes.length; i++) closeDetails(openOnes[i]);
  }


        pop.style.left = `${Math.round(left)}px`;
  function closeOtherPopupsWithinCard(currentDetails) {
        pop.style.top  = `${Math.round(top)}px`;
    var card = closest(currentDetails, CARD_SEL);
     }
     var scope = card || document;


     function closeTip() {
     var openOnes = scope.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
        if (!pop) return;
    for (var i = 0; i < openOnes.length; i++) {
        setExpanded(activeBtn, false);
      var d = openOnes[i];
        activeBtn = null;
      if (d !== currentDetails) closeDetails(d);
        openMode = null;
        pop.style.display = "none";
        pop.innerHTML = "";
     }
     }
  }


    function openTip(btn, mode) {
  function syncPopupAria(detailsEl) {
        const content = findContent(btn);
    var summary = detailsEl.querySelector("summary");
        if (!content) {
    if (!summary) return;
            closeTip();
            return;
        }


        ensurePop();
    summary.setAttribute("aria-expanded", detailsEl.open ? "true" : "false");


        if (activeBtn === btn && pop.style.display !== "none") {
    var pop = detailsEl.querySelector(".sv-tip-pop, .sv-disclose-pop");
            openMode = mode || openMode;
    if (pop) {
            positionPop(btn);
      if (!pop.id) pop.id = "svgi-pop-" + Math.random().toString(36).slice(2);
            return;
      summary.setAttribute("aria-controls", pop.id);
        }
 
        document.querySelectorAll(".sv-tip-btn[aria-expanded='true']").forEach(function (b) { setExpanded(b, false); });
 
        activeBtn = btn;
        openMode = mode || null;
        btn.setAttribute("aria-controls", "sv-tip-pop");
        setExpanded(btn, true);
 
        pop.innerHTML = content.innerHTML;
        pop.style.display = "block";
        positionPop(btn);
     }
     }
  }


    document.addEventListener("click", function (e) {
  // details toggle (delegated)
        const btn = e.target.closest(".sv-tip-btn");
  document.addEventListener(
        if (btn) {
    "toggle",
            e.preventDefault();
    function (e) {
            if (activeBtn === btn && pop && pop.style.display !== "none") {
      var d = e.target;
                if (openMode === "click") {
      if (!d || d.nodeType !== 1) return;
                    closeTip();
      if (!d.matches || !d.matches(POPUP_DETAILS_SEL)) return;
                } else {
                    openTip(btn, "click");
                }
                return;
            }
 
            openTip(btn, "click");
            return;
        }


        if (pop && pop.style.display !== "none") {
      if (d.open) closeOtherPopupsWithinCard(d);
            if (!e.target.closest(".sv-tip-pop")) closeTip();
      syncPopupAria(d);
        }
    },
     });
     true
  );


    const hoverCapable = window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches;
  // Click outside closes all popups
    if (hoverCapable) {
  document.addEventListener(
        document.addEventListener("mouseover", function (e) {
    "click",
            const btn = e.target.closest(".sv-tip-btn");
    function (e) {
            if (!btn) return;
      var t = e.target;
            if (openMode === "click") return;
      if (!t) return;
            openTip(btn, "hover");
      if (closest(t, POPUP_DETAILS_SEL)) return;
        });
      closeAllPopups(document);
    },
    true
  );


        document.addEventListener("mouseout", function (e) {
  // Escape closes all popups
            if (openMode !== "hover" || !activeBtn || !pop || pop.style.display === "none") return;
  document.addEventListener(
    "keydown",
    function (e) {
      if (e.key !== "Escape") return;
      closeAllPopups(document);
    },
    true
  );


            const from = e.target;
  /* ------------------------------------------------------------------ */
            if (!(activeBtn.contains(from) || pop.contains(from))) return;
  /* Tabs                                                              */
  /* ------------------------------------------------------------------ */


            const to = e.relatedTarget;
  function getTabs(root) {
            if (to && (activeBtn.contains(to) || pop.contains(to))) return;
    return root.querySelectorAll(TAB_SEL);
  }


            const toBtn = to && to.closest ? to.closest(".sv-tip-btn") : null;
  function getPanels(root) {
            if (toBtn && toBtn !== activeBtn) return;
    return root.querySelectorAll(PANEL_SEL);
  }


            closeTip();
  function ensureUniqueTabIds(root) {
         });
    // Per-root uid so multiple transclusions don't collide.
    var uid = root.getAttribute("data-tabs-uid");
    if (!uid) {
      _tabsUidCounter++;
      uid =
         (root.getAttribute("data-tabs-root") || "svgi-tabs") + "-" + _tabsUidCounter;
      root.setAttribute("data-tabs-uid", uid);
     }
     }


     document.addEventListener("keydown", function (e) {
     // Pair by index (stable for generated markup).
        if (e.key === "Escape") {
    var tabs = getTabs(root);
            closeTip();
    var panels = getPanels(root);
            return;
    var n = Math.min(tabs.length, panels.length);
        }


        const btn = document.activeElement && document.activeElement.closest
    for (var i = 0; i < n; i++) {
            ? document.activeElement.closest(".sv-tip-btn")
      var tabId = uid + "-tab-" + (i + 1);
            : null;
      var panelId = uid + "-panel-" + (i + 1);


        if (!btn) return;
      var tab = tabs[i];
      var panel = panels[i];


        if (e.key === "Enter" || e.key === " ") {
      tab.id = tabId;
            e.preventDefault();
      tab.setAttribute("aria-controls", panelId);
            if (activeBtn === btn && pop && pop.style.display !== "none") {
                if (openMode === "click") {
                    closeTip();
                } else {
                    openTip(btn, "click");
                }
                return;
            }


            openTip(btn, "click");
      panel.id = panelId;
        }
      panel.setAttribute("aria-labelledby", tabId);
    });
 
    if (window.mw && mw.hook) {
        mw.hook("wikipage.content").add(function () {
            closeTip();
        });
     }
     }
    window.addEventListener("resize", function () { if (activeBtn) positionPop(activeBtn); });
    window.addEventListener("scroll",  function () { if (activeBtn) positionPop(activeBtn); }, true);
})();
/* ============================================================================
* SV Definitions v1 — tooltips for .sv-def
* - Reads: data-sv-def-tip, data-sv-def-link
* - Only activates when data-sv-def-tip is non-empty
* ========================================================================== */
(function (mw) {
  "use strict";
  if (!mw || window.SV_DEF_INIT) return;
  window.SV_DEF_INIT = true;
  var tipEl = null;
  var tipInner = null;
  var activeEl = null;
  var pinned = false;
  var lastPoint = null; // {x,y}
  var lastTouchTime = 0;
  function qTipText(el) {
    var s = (el && el.getAttribute("data-sv-def-tip")) || "";
    return (s || "").trim();
   }
   }


   function qLink(el) {
   function activateTab(root, btn) {
     var s = (el && el.getAttribute("data-sv-def-link")) || "";
     if (!root || !btn) return;
    return (s || "").trim();
  }


  function ensureTip() {
    var tabs = getTabs(root);
     if (tipEl) return;
     var panels = getPanels(root);


     tipEl = document.createElement("div");
     for (var i = 0; i < tabs.length; i++) {
    tipEl.id = "sv-def-tip";
      var t = tabs[i];
    tipEl.className = "sv-def-tip";
      var active = t === btn;
    tipEl.setAttribute("role", "tooltip");
      t.setAttribute("aria-selected", active ? "true" : "false");
    tipEl.setAttribute("aria-hidden", "true");
      t.setAttribute("tabindex", active ? "0" : "-1");
    }


     tipEl.style.position = "fixed";
     var targetId = btn.getAttribute("aria-controls");
     tipEl.style.zIndex = "99999";
     for (var j = 0; j < panels.length; j++) {
    tipEl.style.display = "none";
      var p = panels[j];
    tipEl.style.maxWidth = "340px";
      var isTarget = p.id === targetId;
    tipEl.style.padding = "8px 10px";
      p.setAttribute("data-active", isTarget ? "1" : "0");
    tipEl.style.borderRadius = "8px";
      if (isTarget) p.removeAttribute("hidden");
    tipEl.style.background = "rgba(15, 18, 24, 0.96)";
      else p.setAttribute("hidden", "hidden");
    tipEl.style.color = "#fff";
     }
    tipEl.style.fontSize = "13px";
    tipEl.style.lineHeight = "1.25";
    tipEl.style.boxShadow = "0 8px 24px rgba(0,0,0,0.35)";
     tipEl.style.pointerEvents = "none";


     tipInner = document.createElement("div");
     root.setAttribute("data-active-tab", targetId || "");
    tipInner.className = "sv-def-tip-inner";
    tipEl.appendChild(tipInner);
 
    document.body.appendChild(tipEl);
   }
   }


   function suppressTitle(el) {
   function focusTab(tabs, idx) {
     if (!el) return;
     if (idx < 0) idx = 0;
     var t = el.getAttribute("title");
    if (idx >= tabs.length) idx = tabs.length - 1;
     if (t && !el.getAttribute("data-sv-def-title")) {
     var t = tabs[idx];
      el.setAttribute("data-sv-def-title", t);
     if (t && t.focus) t.focus();
      el.removeAttribute("title");
     return t;
     }
   }
   }


   function restoreTitle(el) {
   function indexOfTab(tabs, btn) {
     if (!el) return;
     for (var i = 0; i < tabs.length; i++) {
    var t = el.getAttribute("data-sv-def-title");
       if (tabs[i] === btn) return i;
    if (t) {
       el.setAttribute("title", t);
      el.removeAttribute("data-sv-def-title");
     }
     }
    return -1;
   }
   }


   function clamp(n, lo, hi) {
   function normalizeTabsRoot(root) {
     return Math.max(lo, Math.min(hi, n));
     if (!root) return;
  }
    if (root.getAttribute("data-gi-tabs-init") === "1") return;


  // Cursor/finger anchored positioning; prefers ABOVE the point.
     ensureUniqueTabIds(root);
  function positionTipPoint(clientX, clientY) {
     if (!tipEl) return;


     tipEl.style.left = "0px";
     var tabs = getTabs(root);
     tipEl.style.top = "0px";
     var panels = getPanels(root);
     tipEl.style.display = "block";
     if (!tabs.length || !panels.length) {
    tipEl.style.pointerEvents = pinned ? "auto" : "none";
      root.setAttribute("data-gi-tabs-init", "1");
      return;
    }


     var tr = tipEl.getBoundingClientRect();
     var activeBtn = null;
     var pad = 8;
    for (var i = 0; i < tabs.length; i++) {
     var gap = pinned ? 14 : 18; // larger gap on hover so cursor doesn't cover text
      if (tabs[i].getAttribute("aria-selected") === "true") {
        activeBtn = tabs[i];
        break;
      }
     }
     if (!activeBtn) activeBtn = tabs[0];


     var x = clientX - tr.width / 2;
     activateTab(root, activeBtn);
    x = clamp(x, pad, window.innerWidth - tr.width - pad);
     root.setAttribute("data-gi-tabs-init", "1");
 
     var y = clientY - tr.height - gap; // ABOVE by default
    if (y < pad) y = clientY + gap;    // flip below if needed
    y = clamp(y, pad, window.innerHeight - tr.height - pad);
 
    tipEl.style.left = Math.round(x) + "px";
    tipEl.style.top = Math.round(y) + "px";
 
    lastPoint = { x: clientX, y: clientY };
   }
   }


   function show(el) {
   function initTabs(root) {
     ensureTip();
     var container = root || document;
 
     var roots = container.querySelectorAll(TABS_ROOT_SEL);
     var txt = qTipText(el);
     for (var i = 0; i < roots.length; i++) normalizeTabsRoot(roots[i]);
     if (!txt) return false;
 
    activeEl = el;
    tipInner.textContent = txt;
    tipEl.setAttribute("aria-hidden", "false");
    tipEl.style.display = "block";
 
    suppressTitle(el);
    return true;
   }
   }


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


     tipEl.style.display = "none";
     var root = closest(btn, ".sv-tabs");
     tipEl.setAttribute("aria-hidden", "true");
     if (!root || root.getAttribute("data-tabs") !== "1") return;


     if (activeEl) restoreTitle(activeEl);
     activateTab(root, btn);
  });


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


  function isEntering(container, relatedTarget) {
    var root = closest(btn, ".sv-tabs");
     return relatedTarget && container && container.contains(relatedTarget);
     if (!root || root.getAttribute("data-tabs") !== "1") return;
  }


  function closestDef(target) {
    var tabs = getTabs(root);
     if (!target || !target.closest) return null;
     if (!tabs || !tabs.length) return;
    return target.closest(".sv-def");
  }


  // Desktop hover
    var idx = indexOfTab(tabs, btn);
  document.addEventListener("mouseover", function (e) {
     if (idx < 0) return;
     if (pinned) return;


     var el = closestDef(e.target);
     if (e.key === "ArrowLeft" || e.key === "Left") {
 
      e.preventDefault();
     if (!el) {
      activateTab(root, focusTab(tabs, idx - 1));
       if (activeEl) hide();
      return;
    }
     if (e.key === "ArrowRight" || e.key === "Right") {
       e.preventDefault();
      activateTab(root, focusTab(tabs, idx + 1));
       return;
       return;
     }
     }
 
     if (e.key === "Home") {
     if (isEntering(el, e.relatedTarget)) return;
      e.preventDefault();
    if (activeEl === el) return;
       activateTab(root, focusTab(tabs, 0));
 
    // Blank definition disables rollover AND clears any previous tooltip
    if (!qTipText(el)) {
       if (activeEl) hide();
       return;
       return;
     }
     }
 
     if (e.key === "End") {
     if (show(el)) positionTipPoint(e.clientX, e.clientY);
      e.preventDefault();
  }, true);
      activateTab(root, focusTab(tabs, tabs.length - 1));
 
  document.addEventListener("mousemove", function (e) {
    if (pinned) return;
 
    var el = closestDef(e.target);
    if (!el) {
      if (activeEl) hide();
       return;
       return;
     }
     }
 
     if (e.key === "Enter" || e.key === " ") {
     if (activeEl && el !== activeEl) return;
      e.preventDefault();
 
       activateTab(root, btn);
    if (!qTipText(el)) {
       if (activeEl) hide();
       return;
       return;
     }
     }
  });


    if (activeEl) positionTipPoint(e.clientX, e.clientY);
  /* ------------------------------------------------------------------ */
   }, true);
  /* Definitions                                                        */
   /* ------------------------------------------------------------------ */


   document.addEventListener("mouseout", function (e) {
   // Selectors
    if (pinned) return;
  var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
    var el = closestDef(e.target);
  var LEVEL_RANGE_SEL = "input.sv-level-range[type='range']";
    if (!el) return;
  var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
    if (isEntering(el, e.relatedTarget)) return;
  var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom";
    if (activeEl === el) hide();
  var SERIES_SEL = "[data-series]";
   }, true);
  var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
  var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']";
  var TAB_SEL = ".sv-tab";
   var PANEL_SEL = ".sv-tabpanel";


   // Touch pin (positions above finger)
   // Internals
   document.addEventListener("pointerdown", function (e) {
   var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
    var el = closestDef(e.target);
  var _tabsUidCounter = 0;
    if (!el) return;


     if (e.pointerType !== "touch") return;
  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;
  }


    // Link = allow navigation
  function closest(el, sel) {
     if (qLink(el)) return;
     if (!el || el.nodeType !== 1) return null;
 
     if (el.closest) return el.closest(sel);
     // Blank tip = do nothing (and close any pinned tooltip)
     while (el && el.nodeType === 1) {
     if (!qTipText(el)) {
       if (el.matches && el.matches(sel)) return el;
       if (pinned) hide();
       el = el.parentElement;
       return;
     }
     }
    return null;
  }


    lastTouchTime = Date.now();
  function parseSeries(el) {
     pinned = true;
     if (!el) return null;


     if (show(el)) positionTipPoint(e.clientX, e.clientY);
     if (_seriesCache) {
 
      if (_seriesCache.has(el)) return _seriesCache.get(el);
    e.preventDefault();
     } else if (el._svSeries !== undefined) {
    e.stopPropagation();
       return el._svSeries;
  }, true);
 
  // Click behavior:
  // - link -> navigate
  // - tip -> toggle pinned
  // - blank tip -> close pinned
  document.addEventListener("click", function (e) {
    // Ignore the synthetic click right after a touch pin
    if (Date.now() - lastTouchTime < 500) return;
 
    var el = closestDef(e.target);
 
     if (!el) {
      if (pinned) hide();
       return;
     }
     }


     var link = qLink(el);
     var raw = el.getAttribute("data-series");
     if (link) {
     var parsed = null;
      window.location.href = mw.util.getUrl(link);
      return;
    }


     if (!qTipText(el)) {
     if (raw != null && raw !== "") {
       if (pinned) hide();
       try {
       return;
        parsed = JSON.parse(raw);
        if (!Array.isArray(parsed)) parsed = null;
      } catch (e) {
        parsed = null;
       }
     }
     }


     e.preventDefault();
     if (_seriesCache) _seriesCache.set(el, parsed);
     e.stopPropagation();
     else el._svSeries = parsed;


     if (pinned && activeEl === el) {
     return parsed;
      hide();
  }
      return;
    }


     pinned = true;
  function isAfter(boundaryNode, el) {
     if (show(el)) positionTipPoint(e.clientX, e.clientY);
     if (!boundaryNode || !el) return true;
   }, true);
     if (boundaryNode === el) return false;
    if (!boundaryNode.compareDocumentPosition) return true;
    return (boundaryNode.compareDocumentPosition(el) & 4) !== 0; // following
   }


   document.addEventListener("keydown", function (e) {
   function initAll(root) {
     if (e.key === "Escape" && (pinned || activeEl)) hide();
     initLevels(root);
  }, true);
    initTabs(root);


  window.addEventListener("resize", function () {
    var container = root || document;
     if (tipEl && tipEl.style.display === "block" && lastPoint) {
    var ds = container.querySelectorAll(POPUP_DETAILS_SEL);
      positionTipPoint(lastPoint.x, lastPoint.y);
     for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]);
    }
   }
   }, true);


   window.addEventListener("scroll", function () {
   if (window.mw && mw.hook) {
     if (tipEl && tipEl.style.display === "block" && lastPoint) {
     mw.hook("wikipage.content").add(function ($content) {
       positionTipPoint(lastPoint.x, lastPoint.y);
       initAll($content && $content[0]);
     }
     });
   }, true);
   }


})(window.mw);
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAll(document);
    });
  } else {
    initAll(document);
  }
})();

Revision as of 17:05, 19 February 2026

/* GameInfo JS (Phase 4.1)
   Shared, reusable behaviors for all GameInfo modules. */

(function () {
  if (window.SV_GAMEINFO_41_INIT) return;
  window.SV_GAMEINFO_41_INIT = 1;

  /* ------------------------------------------------------------------ */
  /* Level Selector                                                     */
  /* ------------------------------------------------------------------ */

  function getCardMaxLevel(card) {
    return clampInt(card.getAttribute("data-max-level"), 1, 999, 1);
  }

  function getCardLevel(card, maxLevel) {
    return clampInt(card.getAttribute("data-level"), 1, maxLevel, 1);
  }

  function getLevelSlider(card) {
    return card.querySelector(LEVEL_RANGE_SEL);
  }

  function getLevelBoundary(slider) {
    return closest(slider, LEVEL_BOUNDARY_SEL) || slider;
  }

  function getLevelScopeContainer(card, boundary) {
    var scope = card.querySelector(LEVEL_SCOPE_SEL) || card;
    if (boundary && scope !== card && !scope.contains(boundary)) return card;
    return scope;
  }

  function setLevelNumber(card, level) {
    var span = card.querySelector(".sv-level-num");
    if (span) span.textContent = String(level);
  }

  function applySeriesToScope(scope, boundary, level) {
    var index = level - 1;
    var nodes = scope.querySelectorAll(SERIES_SEL);

    for (var i = 0; i < nodes.length; i++) {
      var el = nodes[i];

      // Phase 4.1 rule: only update fields below the level boundary.
      if (!isAfter(boundary, el)) continue;

      var series = parseSeries(el);
      if (!series || series.length === 0) continue;

      // Clamp if series shorter than expected.
      var safeIndex = index;
      if (safeIndex >= series.length) safeIndex = series.length - 1;
      if (safeIndex < 0) safeIndex = 0;

      var value = series[safeIndex];
      if (value == null) value = "";

      // Text-only (no HTML mode).
      el.textContent = String(value);
    }
  }

  function applyLevelToCard(card, rawLevel) {
    if (!card) return;

    var maxLevel = getCardMaxLevel(card);
    if (maxLevel <= 1) {
      card.setAttribute("data-level", "1");
      setLevelNumber(card, 1);
      return;
    }

    var slider = getLevelSlider(card);
    var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));

    // Store on card for other systems / debugging.
    card.setAttribute("data-level", String(level));

    // Keep slider in sync.
    if (slider) {
      slider.setAttribute("min", "1");
      slider.setAttribute("max", String(maxLevel));
      if (String(slider.value) !== String(level)) slider.value = String(level);
    }

    setLevelNumber(card, level);

    // Apply dynamic series values (scoped below boundary).
    if (slider) {
      var boundary = getLevelBoundary(slider);
      var scope = getLevelScopeContainer(card, boundary);
      applySeriesToScope(scope, boundary, level);
    }
  }

  function initLevels(root) {
    var container = root || document;
    var cards = container.querySelectorAll(CARD_SEL);

    for (var i = 0; i < cards.length; i++) {
      var card = cards[i];
      if (card.getAttribute("data-gi-level-init") === "1") continue;

      var maxLevel = getCardMaxLevel(card);
      var slider = getLevelSlider(card);

      // No slider (or max <= 1) => this card doesn't participate.
      if (!slider || maxLevel <= 1) {
        card.setAttribute("data-gi-level-init", "1");
        continue;
      }

      var start = slider.value;
      if (start == null || start === "") start = getCardLevel(card, maxLevel);

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

  // Slider input (delegated)
  document.addEventListener(
    "input",
    function (e) {
      var t = e.target;
      if (!t || t.nodeType !== 1) return;
      if (!t.matches || !t.matches(LEVEL_RANGE_SEL)) return;

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

      applyLevelToCard(card, t.value);
    },
    true
  );

  /* ------------------------------------------------------------------ */
  /* Popup                                                              */
  /* ------------------------------------------------------------------ */

  function closeDetails(d) {
    if (d && d.open) d.open = false;
  }

  function closeAllPopups(scope) {
    var root = scope || document;
    var openOnes = root.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
    for (var i = 0; i < openOnes.length; i++) closeDetails(openOnes[i]);
  }

  function closeOtherPopupsWithinCard(currentDetails) {
    var card = closest(currentDetails, CARD_SEL);
    var scope = card || document;

    var openOnes = scope.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
    for (var i = 0; i < openOnes.length; i++) {
      var d = openOnes[i];
      if (d !== currentDetails) closeDetails(d);
    }
  }

  function syncPopupAria(detailsEl) {
    var summary = detailsEl.querySelector("summary");
    if (!summary) return;

    summary.setAttribute("aria-expanded", detailsEl.open ? "true" : "false");

    var pop = detailsEl.querySelector(".sv-tip-pop, .sv-disclose-pop");
    if (pop) {
      if (!pop.id) pop.id = "svgi-pop-" + Math.random().toString(36).slice(2);
      summary.setAttribute("aria-controls", pop.id);
    }
  }

  // details toggle (delegated)
  document.addEventListener(
    "toggle",
    function (e) {
      var d = e.target;
      if (!d || d.nodeType !== 1) return;
      if (!d.matches || !d.matches(POPUP_DETAILS_SEL)) return;

      if (d.open) closeOtherPopupsWithinCard(d);
      syncPopupAria(d);
    },
    true
  );

  // Click outside closes all popups
  document.addEventListener(
    "click",
    function (e) {
      var t = e.target;
      if (!t) return;
      if (closest(t, POPUP_DETAILS_SEL)) return;
      closeAllPopups(document);
    },
    true
  );

  // Escape closes all popups
  document.addEventListener(
    "keydown",
    function (e) {
      if (e.key !== "Escape") return;
      closeAllPopups(document);
    },
    true
  );

  /* ------------------------------------------------------------------ */
  /* Tabs                                                               */
  /* ------------------------------------------------------------------ */

  function getTabs(root) {
    return root.querySelectorAll(TAB_SEL);
  }

  function getPanels(root) {
    return root.querySelectorAll(PANEL_SEL);
  }

  function ensureUniqueTabIds(root) {
    // Per-root uid so multiple transclusions don't collide.
    var uid = root.getAttribute("data-tabs-uid");
    if (!uid) {
      _tabsUidCounter++;
      uid =
        (root.getAttribute("data-tabs-root") || "svgi-tabs") + "-" + _tabsUidCounter;
      root.setAttribute("data-tabs-uid", uid);
    }

    // Pair by index (stable for generated markup).
    var tabs = getTabs(root);
    var panels = getPanels(root);
    var n = Math.min(tabs.length, panels.length);

    for (var i = 0; i < n; i++) {
      var tabId = uid + "-tab-" + (i + 1);
      var panelId = uid + "-panel-" + (i + 1);

      var tab = tabs[i];
      var panel = panels[i];

      tab.id = tabId;
      tab.setAttribute("aria-controls", panelId);

      panel.id = panelId;
      panel.setAttribute("aria-labelledby", tabId);
    }
  }

  function activateTab(root, btn) {
    if (!root || !btn) return;

    var tabs = getTabs(root);
    var panels = getPanels(root);

    for (var i = 0; i < tabs.length; i++) {
      var t = tabs[i];
      var active = t === btn;
      t.setAttribute("aria-selected", active ? "true" : "false");
      t.setAttribute("tabindex", active ? "0" : "-1");
    }

    var targetId = btn.getAttribute("aria-controls");
    for (var j = 0; j < panels.length; j++) {
      var p = panels[j];
      var isTarget = p.id === targetId;
      p.setAttribute("data-active", isTarget ? "1" : "0");
      if (isTarget) p.removeAttribute("hidden");
      else p.setAttribute("hidden", "hidden");
    }

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

  function focusTab(tabs, idx) {
    if (idx < 0) idx = 0;
    if (idx >= tabs.length) idx = tabs.length - 1;
    var t = tabs[idx];
    if (t && t.focus) t.focus();
    return t;
  }

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

  function normalizeTabsRoot(root) {
    if (!root) return;
    if (root.getAttribute("data-gi-tabs-init") === "1") return;

    ensureUniqueTabIds(root);

    var tabs = getTabs(root);
    var panels = getPanels(root);
    if (!tabs.length || !panels.length) {
      root.setAttribute("data-gi-tabs-init", "1");
      return;
    }

    var activeBtn = null;
    for (var i = 0; i < tabs.length; i++) {
      if (tabs[i].getAttribute("aria-selected") === "true") {
        activeBtn = tabs[i];
        break;
      }
    }
    if (!activeBtn) activeBtn = tabs[0];

    activateTab(root, activeBtn);
    root.setAttribute("data-gi-tabs-init", "1");
  }

  function initTabs(root) {
    var container = root || document;
    var roots = container.querySelectorAll(TABS_ROOT_SEL);
    for (var i = 0; i < roots.length; i++) normalizeTabsRoot(roots[i]);
  }

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

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

    activateTab(root, btn);
  });

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

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

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

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

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

  /* ------------------------------------------------------------------ */
  /* Definitions                                                        */
  /* ------------------------------------------------------------------ */

  // Selectors
  var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
  var LEVEL_RANGE_SEL = "input.sv-level-range[type='range']";
  var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
  var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom";
  var SERIES_SEL = "[data-series]";
  var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
  var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']";
  var TAB_SEL = ".sv-tab";
  var PANEL_SEL = ".sv-tabpanel";

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

  function clampInt(value, min, max, fallback) {
    var n = parseInt(value, 10);
    if (isNaN(n)) n = fallback != null ? fallback : min;
    if (n < min) return min;
    if (n > max) return max;
    return n;
  }

  function closest(el, sel) {
    if (!el || el.nodeType !== 1) return null;
    if (el.closest) return el.closest(sel);
    while (el && el.nodeType === 1) {
      if (el.matches && el.matches(sel)) return el;
      el = el.parentElement;
    }
    return null;
  }

  function parseSeries(el) {
    if (!el) return null;

    if (_seriesCache) {
      if (_seriesCache.has(el)) return _seriesCache.get(el);
    } else if (el._svSeries !== undefined) {
      return el._svSeries;
    }

    var raw = el.getAttribute("data-series");
    var parsed = null;

    if (raw != null && raw !== "") {
      try {
        parsed = JSON.parse(raw);
        if (!Array.isArray(parsed)) parsed = null;
      } catch (e) {
        parsed = null;
      }
    }

    if (_seriesCache) _seriesCache.set(el, parsed);
    else el._svSeries = parsed;

    return parsed;
  }

  function isAfter(boundaryNode, el) {
    if (!boundaryNode || !el) return true;
    if (boundaryNode === el) return false;
    if (!boundaryNode.compareDocumentPosition) return true;
    return (boundaryNode.compareDocumentPosition(el) & 4) !== 0; // following
  }

  function initAll(root) {
    initLevels(root);
    initTabs(root);

    var container = root || document;
    var ds = container.querySelectorAll(POPUP_DETAILS_SEL);
    for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]);
  }

  if (window.mw && mw.hook) {
    mw.hook("wikipage.content").add(function ($content) {
      initAll($content && $content[0]);
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAll(document);
    });
  } else {
    initAll(document);
  }
})();