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:
/* GameInfo JS (Phase 4.1)
/* GameInfo JS (Phase 4.1 Refactor)
   Shared, reusable behaviors for all GameInfo modules. */
   Shared behaviors for all GameInfo modules:
  - Level selector (data-series swap)
  - Popups (sv-tip / sv-disclose using sv-hidden)
  - Tabs (hidden attribute toggling + ARIA ids) */


(function () {
(function () {
Line 6: Line 9:
   window.SV_GAMEINFO_41_INIT = 1;
   window.SV_GAMEINFO_41_INIT = 1;


   /* ------------------------------------------------------------------ */
   // ---------------------------------------------------------------------------
   /* Selectors                                                         */
   // Selectors
   /* ------------------------------------------------------------------ */
   // ---------------------------------------------------------------------------


   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
   var CARD_SEL = ".sv-gi-card[data-gi='1'], .sv-skill-card, .sv-passive-card";


  // Level
   var LEVEL_RANGE_SEL = "input.sv-level-range[type='range']";
   var LEVEL_RANGE_SEL = "input.sv-level-range[type='range']";
   var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
   var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
Line 18: Line 20:
   var SERIES_SEL = "[data-series]";
   var SERIES_SEL = "[data-series]";


  // Popups (Phase 4.1 markup: span toggles + div popovers)
   var POP_WRAP_SEL = ".sv-tip, .sv-disclose";
   var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
   var POP_BTN_SEL = "[data-sv-toggle='1']";
   var POPUP_BTN_SEL =
   var HIDDEN_CLASS = "sv-hidden";
    ".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_HEAD_SEL = ".sv-tip-pop-head, .sv-disclose-pop-head";
   var POPUP_HIDDEN_CLASS = "sv-hidden";


  // Tabs
   var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']";
   var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']";
   var TAB_SEL = ".sv-tab";
   var TAB_SEL = ".sv-tab[role='tab'], .sv-tab";
   var PANEL_SEL = ".sv-tabpanel";
   var PANEL_SEL = ".sv-tabpanel";


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


   /* ------------------------------------------------------------------ */
   // ---------------------------------------------------------------------------
   /* Utilities                                                          */
   // Utils
   /* ------------------------------------------------------------------ */
   // ---------------------------------------------------------------------------


   function clampInt(value, min, max, fallback) {
   function clampInt(value, min, max, fallback) {
Line 58: Line 52:
     }
     }
     return null;
     return null;
  }
  function hasClass(el, cls) {
    return !!(el && el.classList && el.classList.contains(cls));
   }
   }


Line 98: Line 88:
   }
   }


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


   function getCardMaxLevel(card) {
   function getCardMaxLevel(card) {
Line 136: Line 126:
       var el = nodes[i];
       var el = nodes[i];


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


Line 142: Line 132:
       if (!series || series.length === 0) continue;
       if (!series || series.length === 0) continue;


      // Clamp if series shorter than expected.
       var safeIndex = index;
       var safeIndex = index;
       if (safeIndex >= series.length) safeIndex = series.length - 1;
       if (safeIndex >= series.length) safeIndex = series.length - 1;
Line 150: Line 139:
       if (value == null) value = "";
       if (value == null) value = "";


      // Text-only (no HTML mode).
       el.textContent = String(value);
       el.textContent = String(value);
     }
     }
Line 168: Line 156:
     var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));
     var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));


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


    // Keep slider in sync.
     if (slider) {
     if (slider) {
       slider.setAttribute("min", "1");
       slider.setAttribute("min", "1");
Line 180: Line 166:
     setLevelNumber(card, level);
     setLevelNumber(card, level);


    // Apply dynamic series values (scoped below boundary).
     if (slider) {
     if (slider) {
       var boundary = getLevelBoundary(slider);
       var boundary = getLevelBoundary(slider);
Line 199: Line 184:
       var slider = getLevelSlider(card);
       var slider = getLevelSlider(card);


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


  // Slider input (delegated)
   document.addEventListener(
   document.addEventListener(
     "input",
     "input",
Line 229: Line 212:
   );
   );


   /* ------------------------------------------------------------------ */
   // ---------------------------------------------------------------------------
   /* Popup (Phase 4.1: span toggles + div popovers)                     */
   // Popups (sv-hidden toggles)
   /* ------------------------------------------------------------------ */
   // ---------------------------------------------------------------------------
 
  function hidePop(pop) {
    if (!pop) return;
    if (pop.classList) pop.classList.add(POPUP_HIDDEN_CLASS);
    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) {
   function getPopupPanel(btn) {
     if (!btn) return null;
     if (!btn) return null;
     var id = btn.getAttribute("aria-controls");
     var id = btn.getAttribute("aria-controls");
     if (!id) return null;
     if (!id) return null;
 
     return document.getElementById(id);
     var pop = document.getElementById(id);
    if (!pop) return null;
 
    // Prefer scoping to wrapper; prevents collisions if IDs ever repeat.
    var wrap = closest(btn, POPUP_WRAP_SEL);
    if (wrap && !wrap.contains(pop)) return null;
 
    return pop;
   }
   }


   function isBtnExpanded(btn) {
   function isPanelOpen(panel) {
     return btn && btn.getAttribute("aria-expanded") === "true";
     return panel && panel.classList && !panel.classList.contains(HIDDEN_CLASS);
   }
   }


   function setBtnExpanded(btn, expanded) {
   function setPanelOpen(btn, panel, open) {
     if (!btn) return;
     if (!panel || !panel.classList) return;
    btn.setAttribute("aria-expanded", expanded ? "true" : "false");
  }


  function closeBtnPopup(btn) {
     if (open) panel.classList.remove(HIDDEN_CLASS);
     var pop = getPopFromBtn(btn);
     else panel.classList.add(HIDDEN_CLASS);
    hidePop(pop);
     setBtnExpanded(btn, false);
  }


  function openBtnPopup(btn) {
    if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
    var pop = getPopFromBtn(btn);
    showPop(pop);
    setBtnExpanded(btn, true);
   }
   }


   function closeAllBtnPopups(scope) {
   function closeAllPopups(scope) {
     var root = scope || document;
     var root = scope || document;
     var openBtns = root.querySelectorAll(
     var btns = root.querySelectorAll(POP_BTN_SEL);
      POPUP_BTN_SEL + "[aria-expanded='true']"
     for (var i = 0; i < btns.length; i++) {
    );
      var b = btns[i];
     for (var i = 0; i < openBtns.length; i++) closeBtnPopup(openBtns[i]);
      var p = getPopupPanel(b);
      if (p) setPanelOpen(b, p, false);
    }
   }
   }


   function closeOtherBtnPopupsWithinCard(btn) {
   function closeOtherPopupsWithinCard(currentBtn) {
     var card = closest(btn, CARD_SEL);
     var card = closest(currentBtn, CARD_SEL);
     var scope = card || document;
     var scope = card || document;
     var openBtns = scope.querySelectorAll(
 
      POPUP_BTN_SEL + "[aria-expanded='true']"
     var btns = scope.querySelectorAll(POP_BTN_SEL);
    );
     for (var i = 0; i < btns.length; i++) {
     for (var i = 0; i < openBtns.length; i++) {
       var b = btns[i];
       if (openBtns[i] !== btn) closeBtnPopup(openBtns[i]);
      if (b === currentBtn) continue;
      var p = getPopupPanel(b);
      if (p) setPanelOpen(b, p, false);
     }
     }
   }
   }


   function normalizeBtnPopups(container) {
   function togglePopup(btn) {
     var root = container || document;
     var panel = getPopupPanel(btn);
 
     if (!panel) return;
    // Ensure pop divs with 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 open = !isPanelOpen(panel);
     var btns = root.querySelectorAll(POPUP_BTN_SEL);
     if (open) closeOtherPopupsWithinCard(btn);
     for (var j = 0; j < btns.length; j++) {
    setPanelOpen(btn, panel, open);
      var b = btns[j];
      var pop = getPopFromBtn(b);
      var expanded = pop
        ? !hasClass(pop, POPUP_HIDDEN_CLASS) && !pop.hasAttribute("hidden")
        : false;
      setBtnExpanded(b, expanded);
    }
   }
   }


   // Click handling (delegated):
   // Click to toggle
  // - clicking popup header closes
  // - clicking toggle button toggles
  // - clicking outside any wrapper closes all
   document.addEventListener(
   document.addEventListener(
     "click",
     "click",
     function (e) {
     function (e) {
       var t = e.target;
       var btn = e.target && e.target.closest ? e.target.closest(POP_BTN_SEL) : null;
      if (!t) return;
 
      // 1) Header click closes its popup
      var head = t.closest ? t.closest(POPUP_HEAD_SEL) : null;
      if (head) {
        var wrap = closest(head, POPUP_WRAP_SEL);
        if (wrap) {
          var btn = wrap.querySelector(POPUP_BTN_SEL);
          if (btn) closeBtnPopup(btn);
        }
        e.preventDefault();
        return;
      }
 
      // 2) Button click toggles
      var btn =
        t.closest && t.closest(POPUP_BTN_SEL) ? t.closest(POPUP_BTN_SEL) : null;
       if (btn) {
       if (btn) {
        togglePopup(btn);
         e.preventDefault();
         e.preventDefault();
        if (isBtnExpanded(btn)) {
          closeBtnPopup(btn);
          return;
        }
        closeOtherBtnPopupsWithinCard(btn);
        openBtnPopup(btn);
         return;
         return;
       }
       }


       // 3) Click outside wrappers closes all
       // Click outside closes all
       if (!closest(t, POPUP_WRAP_SEL)) {
       if (!closest(e.target, POP_WRAP_SEL)) closeAllPopups(document);
        closeAllBtnPopups(document);
      }
     },
     },
     true
     true
   );
   );


   // Keyboard toggle (Enter/Space) + Escape closes all
   // Keyboard toggle (Enter/Space on popup buttons)
   document.addEventListener(
   document.addEventListener(
     "keydown",
     "keydown",
     function (e) {
     function (e) {
      var t = e.target;
      if (!t || t.nodeType !== 1) return;
       if (e.key === "Escape") {
       if (e.key === "Escape") {
         closeAllBtnPopups(document);
         closeAllPopups(document);
         return;
         return;
       }
       }


      if (!(e.key === "Enter" || e.key === " " || e.key === "Spacebar"))
       var btn = t.closest ? t.closest(POP_BTN_SEL) : null;
        return;
 
       var btn =
        document.activeElement && document.activeElement.closest
          ? document.activeElement.closest(POPUP_BTN_SEL)
          : null;
       if (!btn) return;
       if (!btn) return;


       e.preventDefault();
       if (e.key === "Enter" || e.key === " ") {
 
         togglePopup(btn);
      if (isBtnExpanded(btn)) closeBtnPopup(btn);
         e.preventDefault();
      else {
         closeOtherBtnPopupsWithinCard(btn);
         openBtnPopup(btn);
       }
       }
     },
     },
Line 399: Line 308:
   );
   );


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


   function getTabs(root) {
   function getTabs(root) {
Line 409: Line 318:
   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;
   }
   }


   function ensureUniqueTabIds(root) {
   function ensureUniqueTabIds(root) {
    // Per-root uid so multiple transclusions don't collide.
     var uid = root.getAttribute("data-tabs-uid");
     var uid = root.getAttribute("data-tabs-uid");
     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 key (stable for generated markup).
     var tabs = getTabs(root);
     var tabs = getTabs(root);
     for (var i = 0; i < tabs.length; i++) {
    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 tab = tabs[i];
       var key = tab.getAttribute("data-tab") || String(i + 1);
       var panel = panels[i];
      var panel = getPanelByKey(root, key);


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


       if (panel) {
       panel.id = panelId;
        var panelId = uid + "-panel-" + key;
      panel.setAttribute("aria-labelledby", tabId);
        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 468: Line 351:


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


    // 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 476: Line 358:
       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);
       if (t.classList) {
        if (active) t.classList.add("sv-tab--active");
        else t.classList.remove("sv-tab--active");
      }
     }
     }


    // Panels
     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.getAttribute("data-panel") === key;
       var isTarget = p.id === targetId;
       if (isTarget) showPanel(p);
 
       else hidePanel(p);
       if (isTarget) p.removeAttribute("hidden");
       else p.setAttribute("hidden", "hidden");
     }
     }


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


Line 500: Line 385:


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


    // 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 539: Line 421:
   }
   }


   // Click to activate (delegated)
   // Click to activate
   document.addEventListener("click", function (e) {
   document.addEventListener("click", function (e) {
     var btn = e.target && e.target.closest ? e.target.closest(TAB_SEL) : null;
     var btn = e.target && e.target.closest ? e.target.closest(TAB_SEL) : null;
Line 550: Line 432:
   });
   });


   // Keyboard navigation (delegated)
   // Keyboard navigation
   document.addEventListener("keydown", function (e) {
   document.addEventListener("keydown", function (e) {
     var btn =
     var btn =
Line 594: Line 476:
   });
   });


   /* ------------------------------------------------------------------ */
   // ---------------------------------------------------------------------------
   /* Init                                                               */
   // Init
   /* ------------------------------------------------------------------ */
   // ---------------------------------------------------------------------------


   function initAll(root) {
   function initAll(root) {
     initLevels(root);
     initLevels(root);
     initTabs(root);
     initTabs(root);
    // Normalize Phase 4.1 span-popups
    normalizeBtnPopups(root || document);
   }
   }