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 5: Line 5:
   if (window.SV_GAMEINFO_41_INIT) return;
   if (window.SV_GAMEINFO_41_INIT) return;
   window.SV_GAMEINFO_41_INIT = 1;
   window.SV_GAMEINFO_41_INIT = 1;
  /* ------------------------------------------------------------------ */
  /* Selectors                                                          */
  /* ------------------------------------------------------------------ */
  var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
  // Level
  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]";
  // Popups (Phase 4.1 markup: span toggles + div popovers; legacy <details> supported)
  var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose"; // legacy
  var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
  var POPUP_BTN_SEL =
    ".sv-tip-btn[data-sv-toggle='1'], .sv-disclose-btn[data-sv-toggle='1']";
  var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
  var POPUP_HIDDEN_CLASS = "sv-hidden";
  // Tabs
  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;
  /* ------------------------------------------------------------------ */
  /* Utilities                                                          */
  /* ------------------------------------------------------------------ */
  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 hasClass(el, cls) {
    return !!(el && el.classList && el.classList.contains(cls));
  }
  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
  }


   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */
Line 138: Line 230:


   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */
   /* Popup                                                             */
   /* Popup (Phase 4.1: span toggles + legacy <details> support)          */
   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */
  function hidePop(pop) {
    if (!pop) return;
    if (pop.classList) pop.classList.add(POPUP_HIDDEN_CLASS);
    // Also set [hidden] so it hides even if CSS is missing/late.
    pop.setAttribute("hidden", "hidden");
  }
  function showPop(pop) {
    if (!pop) return;
    if (pop.classList) pop.classList.remove(POPUP_HIDDEN_CLASS);
    pop.removeAttribute("hidden");
  }
  function getPopFromBtn(btn) {
    if (!btn) return null;
    var id = btn.getAttribute("aria-controls");
    if (!id) return null;
    // Prefer scoping to the wrapper to avoid cross-card collisions.
    var wrap = closest(btn, POPUP_WRAP_SEL);
    if (wrap) {
      var local = wrap.querySelector("#" + id);
      if (local) return local;
    }
    return document.getElementById(id);
  }
  function isBtnExpanded(btn) {
    return btn && btn.getAttribute("aria-expanded") === "true";
  }
  function setBtnExpanded(btn, expanded) {
    if (!btn) return;
    btn.setAttribute("aria-expanded", expanded ? "true" : "false");
  }
  function closeBtnPopup(btn) {
    var pop = getPopFromBtn(btn);
    hidePop(pop);
    setBtnExpanded(btn, false);
  }
  function openBtnPopup(btn) {
    var pop = getPopFromBtn(btn);
    showPop(pop);
    setBtnExpanded(btn, true);
  }
  function closeAllBtnPopups(scope) {
    var root = scope || document;
    var openBtns = root.querySelectorAll(
      POPUP_BTN_SEL + "[aria-expanded='true']"
    );
    for (var i = 0; i < openBtns.length; i++) closeBtnPopup(openBtns[i]);
  }
  function closeOtherBtnPopupsWithinCard(btn) {
    var card = closest(btn, CARD_SEL);
    var scope = card || document;
    var openBtns = scope.querySelectorAll(
      POPUP_BTN_SEL + "[aria-expanded='true']"
    );
    for (var i = 0; i < openBtns.length; i++) {
      if (openBtns[i] !== btn) closeBtnPopup(openBtns[i]);
    }
  }
  /* ---------- Legacy <details> helpers (safe to keep) ---------------- */


   function closeDetails(d) {
   function closeDetails(d) {
Line 145: Line 307:
   }
   }


   function closeAllPopups(scope) {
   function closeAllDetails(scope) {
     var root = scope || document;
     var root = scope || document;
     var openOnes = root.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
     var openOnes = root.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
Line 151: Line 313:
   }
   }


   function closeOtherPopupsWithinCard(currentDetails) {
   /* ---------- Optional: keep aria-controls for legacy <details> ------ */
    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) {
   function syncPopupAria(detailsEl) {
Line 175: Line 328:
   }
   }


   // details toggle (delegated)
   /* ---------- Normalize popups on load ------------------------------- */
 
  function normalizeBtnPopups(container) {
    var root = container || document;
 
    // Ensure popup divs that carry sv-hidden are also [hidden] so they start hidden.
    var pops = root.querySelectorAll(POPUP_POP_SEL);
    for (var i = 0; i < pops.length; i++) {
      var p = pops[i];
      if (hasClass(p, POPUP_HIDDEN_CLASS) && !p.hasAttribute("hidden")) {
        p.setAttribute("hidden", "hidden");
      }
    }
 
    // Ensure aria-expanded matches actual visibility.
    var btns = root.querySelectorAll(POPUP_BTN_SEL);
    for (var j = 0; j < btns.length; j++) {
      var b = btns[j];
      var pop = getPopFromBtn(b);
      var expanded = pop
        ? !hasClass(pop, POPUP_HIDDEN_CLASS) && !pop.hasAttribute("hidden")
        : false;
      setBtnExpanded(b, expanded);
    }
  }
 
  /* ---------- Events -------------------------------------------------- */
 
  // Click toggle (delegated)
  document.addEventListener(
    "click",
    function (e) {
      var btn =
        e.target && e.target.closest ? e.target.closest(POPUP_BTN_SEL) : null;
      if (!btn) return;
 
      e.preventDefault();
 
      if (isBtnExpanded(btn)) {
        closeBtnPopup(btn);
        return;
      }
 
      closeOtherBtnPopupsWithinCard(btn);
      openBtnPopup(btn);
    },
    true
  );
 
  // Keyboard toggle (Enter/Space) (delegated)
  document.addEventListener(
    "keydown",
    function (e) {
      if (!(e.key === "Enter" || e.key === " " || e.key === "Spacebar"))
        return;
 
      var btn =
        document.activeElement && document.activeElement.closest
          ? document.activeElement.closest(POPUP_BTN_SEL)
          : null;
      if (!btn) return;
 
      e.preventDefault();
 
      if (isBtnExpanded(btn)) closeBtnPopup(btn);
      else {
        closeOtherBtnPopupsWithinCard(btn);
        openBtnPopup(btn);
      }
    },
    true
  );
 
  // Legacy <details> toggle (delegated)
   document.addEventListener(
   document.addEventListener(
     "toggle",
     "toggle",
Line 183: Line 409:
       if (!d.matches || !d.matches(POPUP_DETAILS_SEL)) return;
       if (!d.matches || !d.matches(POPUP_DETAILS_SEL)) return;


       if (d.open) closeOtherPopupsWithinCard(d);
      // Close other legacy details within the same card.
       if (d.open) {
        var card = closest(d, CARD_SEL);
        var scope = card || document;
 
        var openOnes = scope.querySelectorAll(POPUP_DETAILS_SEL + "[open]");
        for (var i = 0; i < openOnes.length; i++) {
          if (openOnes[i] !== d) closeDetails(openOnes[i]);
        }
      }
 
       syncPopupAria(d);
       syncPopupAria(d);
     },
     },
Line 195: Line 431:
       var t = e.target;
       var t = e.target;
       if (!t) return;
       if (!t) return;
       if (closest(t, POPUP_DETAILS_SEL)) return;
 
       closeAllPopups(document);
      // If click is inside any popup wrapper, do nothing.
       if (closest(t, POPUP_WRAP_SEL)) return;
 
      closeAllBtnPopups(document);
       closeAllDetails(document);
     },
     },
     true
     true
Line 206: Line 446:
     function (e) {
     function (e) {
       if (e.key !== "Escape") return;
       if (e.key !== "Escape") return;
       closeAllPopups(document);
       closeAllBtnPopups(document);
      closeAllDetails(document);
     },
     },
     true
     true
Line 212: Line 453:


   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */
   /* Tabs                                                               */
   /* Tabs (Phase 4.1: key-based, uses .sv-hidden + [hidden])            */
   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */


Line 221: Line 462:
   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 228: Line 477:
     if (!uid) {
     if (!uid) {
       _tabsUidCounter++;
       _tabsUidCounter++;
       uid =
       uid = (root.getAttribute("data-tabs-root") || "svgi-tabs") + "-" + _tabsUidCounter;
        (root.getAttribute("data-tabs-root") || "svgi-tabs") + "-" + _tabsUidCounter;
       root.setAttribute("data-tabs-uid", uid);
       root.setAttribute("data-tabs-uid", uid);
     }
     }


     // Pair by index (stable for generated markup).
     // Pair by key (stable for generated markup).
     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 257: Line 518:


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


    // Tabs
     for (var i = 0; i < tabs.length; i++) {
     for (var i = 0; i < tabs.length; i++) {
       var t = tabs[i];
       var t = tabs[i];
Line 264: Line 526:
       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) t.classList.toggle("sv-tab--active", active);
     }
     }


     var targetId = btn.getAttribute("aria-controls");
    // Panels
     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;
      p.setAttribute("data-active", isTarget ? "1" : "0");
       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 306: Line 569:
     }
     }


    // Determine active tab
     var activeBtn = null;
     var activeBtn = null;
     for (var i = 0; i < tabs.length; i++) {
     for (var i = 0; i < tabs.length; i++) {
Line 381: Line 645:


   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */
   /* Definitions                                                        */
   /* Init                                                              */
   /* ------------------------------------------------------------------ */
   /* ------------------------------------------------------------------ */
  // 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) {
   function initAll(root) {
Line 456: Line 653:


     var container = root || document;
     var container = root || document;
    // Normalize Phase 4.1 span-popups
    normalizeBtnPopups(container);
    // Keep legacy <details> aria in sync if any remain
     var ds = container.querySelectorAll(POPUP_DETAILS_SEL);
     var ds = container.querySelectorAll(POPUP_DETAILS_SEL);
     for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]);
     for (var i = 0; i < ds.length; i++) syncPopupAria(ds[i]);