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 352: Line 352:


   function initAllDefs(root) {
   function initAllDefs(root) {
    // Disable the legacy/native tooltip mechanism (usually `title=""`) when JS is
    // running, so only the JS tooltip shows. If JS fails to load, this never runs,
    // so the `title` attribute remains as the no-JS fallback.
     var container = root || document;
     var container = root || document;
     var defs = container.querySelectorAll(DEF_SEL + "[title]");
     var defs = container.querySelectorAll(DEF_SEL + "[title]");
Line 384: Line 381:
   var SV = (window.SV = window.SV || {});
   var SV = (window.SV = window.SV || {});
   var COMMON = (SV.common = SV.common || {});
   var COMMON = (SV.common = SV.common || {});
   if (COMMON.levelsInit) return;
 
   COMMON.levelsInit = 1;
  // --------- VERSIONED INIT (fixes "old script blocks new script") -----
  var LEVELS_VERSION = 2;
  // If an older Common.js set levelsInit=1, we still want THIS one to run.
   if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return;
   COMMON.levelsInit = LEVELS_VERSION;


   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
  // Be a bit forgiving: accept the custom slider even if the data attr changes.
   var LEVEL_RANGE_SEL =
   var LEVEL_RANGE_SEL =
     "input.sv-level-range[type='range'], .sv-level-range--custom[data-sv-slider='1']";
     "input.sv-level-range[type='range'], .sv-level-range--custom, .sv-level-range[data-sv-slider='1']";
 
   var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
   var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
   var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom";
   var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom";
   var SERIES_SEL = "[data-series]";
   var SERIES_SEL = "[data-series]";
  var LEVEL_INIT_ATTR = "data-gi-level-init";


   var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
   var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
Line 454: Line 459:


   function isCustomSlider(el) {
   function isCustomSlider(el) {
     return !!(el && el.getAttribute && el.getAttribute("data-sv-slider") === "1");
     if (!el || !el.getAttribute) return false;
    if (el.getAttribute("data-sv-slider") === "1") return true;
    // fallback: class-based detection
    return !!(el.classList && el.classList.contains("sv-level-range--custom"));
  }
 
  function parseMaxFromUi(card) {
    // fallback if attributes drift: parse "... / 10" out of the UI text
    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 getCardLevel(card, maxLevel) {
   function getCardLevel(card, 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, 1, maxLevel, 1);
   }
   }


Line 581: Line 611:
     var rounded = Math.round(raw);
     var rounded = Math.round(raw);
     return clampInt(rounded, min, max, min);
     return clampInt(rounded, min, max, min);
  }
  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 = "";
   }
   }


Line 592: Line 629:
     card.setAttribute("data-level", String(level));
     card.setAttribute("data-level", String(level));
     setLevelNumber(card, level);
     setLevelNumber(card, level);
    if (slider) {
      unhide(slider);
      var wrap = closest(slider, ".sv-level-slider");
      if (wrap) unhide(wrap);
    }


     if (slider && maxLevel > 1) {
     if (slider && maxLevel > 1) {
Line 602: Line 645:
         setCustomValue(slider, level);
         setCustomValue(slider, level);
       }
       }
    } else if (slider && isCustomSlider(slider)) {
      // Even if max==1, keep the visual consistent
      setCustomBounds(slider, 1, maxLevel);
      setCustomValue(slider, level);
     }
     }


Line 615: Line 662:
     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;
 
       // Versioned per-card init (fixes "old init stamp blocks new logic")
      var stamped = clampInt(card.getAttribute(LEVEL_INIT_ATTR), 0, 999, 0);
      if (stamped >= LEVELS_VERSION) continue;


       var maxLevel = getCardMaxLevel(card);
       var maxLevel = getCardMaxLevel(card);
Line 627: Line 677:


       applyLevelToCard(card, start);
       applyLevelToCard(card, start);
       card.setAttribute("data-gi-level-init", "1");
       card.setAttribute(LEVEL_INIT_ATTR, String(LEVELS_VERSION));
     }
     }
   }
   }
Line 651: Line 701:
       var slider =
       var slider =
         e.target && e.target.closest
         e.target && e.target.closest
           ? e.target.closest(".sv-level-range--custom[data-sv-slider='1']")
           ? e.target.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']")
           : null;
           : null;
       if (!slider) return;
       if (!slider) return;
Line 706: Line 756:
       if (!el || !el.closest) return;
       if (!el || !el.closest) return;


       var slider = el.closest(".sv-level-range--custom[data-sv-slider='1']");
       var slider = el.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
       if (!slider) return;
       if (!slider) return;


Line 749: Line 799:
     initLevels(root);
     initLevels(root);
   }
   }
  // Expose for debugging / manual re-init if needed
  COMMON.initAllLevels = initAllLevels;


   if (window.mw && mw.hook) {
   if (window.mw && mw.hook) {
Line 780: Line 833:
   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";


  // Modern triggers (Phase 4.1 span toggles)
   var POPUP_BTN_SEL =
   var POPUP_BTN_SEL =
     ".sv-tip-btn[data-sv-toggle='1'], .sv-disclose-btn[data-sv-toggle='1']";
     ".sv-tip-btn[data-sv-toggle='1'], .sv-disclose-btn[data-sv-toggle='1']";


  // Legacy triggers (no-JS fallback)
   var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
   var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
   var POPUP_SUMMARY_SEL = "details.sv-tip > summary, details.sv-disclose > summary";
   var POPUP_SUMMARY_SEL = "details.sv-tip > summary, details.sv-disclose > summary";


  // Content nodes (usually hidden in-page; we clone into a floating pop)
   var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
   var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
   var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
   var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
Line 807: Line 857:
     visible: false,
     visible: false,
     pinned: false,
     pinned: false,
     anchor: null, // trigger element (btn or summary)
     anchor: null,
     lastX: 0,
     lastX: 0,
     lastY: 0,
     lastY: 0,
Line 882: Line 932:


   function safeIdSelector(id) {
   function safeIdSelector(id) {
    // Minimal escaping for querySelector("#id") without relying on CSS.escape.
    // Covers common special chars that would break selectors.
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
   }
   }
Line 890: Line 938:
     if (!trigger) return null;
     if (!trigger) return null;


    // Legacy details: summary inside details
     if (trigger.tagName === "SUMMARY") {
     if (trigger.tagName === "SUMMARY") {
       var det = closest(trigger, POPUP_DETAILS_SEL);
       var det = closest(trigger, POPUP_DETAILS_SEL);
Line 902: Line 949:
     }
     }


    // Modern button: aria-controls points to a hidden pop node.
     var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
     var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
     if (id) {
     if (id) {
Line 923: Line 969:
     }
     }


    // Last resort: find a pop node in the same wrapper.
     var w = closest(trigger, POPUP_WRAP_SEL);
     var w = closest(trigger, POPUP_WRAP_SEL);
     if (w) {
     if (w) {
Line 939: Line 984:
     if (!node) return true;
     if (!node) return true;


    // Consider "blank" when text is only whitespace / dashes.
     var t = node.textContent;
     var t = node.textContent;
     if (t == null) return true;
     if (t == null) return true;
Line 964: Line 1,008:
     if (root.classList) root.classList.remove("sv-hidden");
     if (root.classList) root.classList.remove("sv-hidden");


    // Avoid carrying display:none into the floating pop.
     if (root.style && root.style.display === "none") root.style.display = "";
     if (root.style && root.style.display === "none") root.style.display = "";


Line 975: Line 1,018:
     if (!el) return;
     if (!el) return;


    // Clear previous content
     while (el.firstChild) el.removeChild(el.firstChild);
     while (el.firstChild) el.removeChild(el.firstChild);


    // Clone for safety (don't mutate page DOM)
     var clone = sourceNode.cloneNode(true);
     var clone = sourceNode.cloneNode(true);
     stripIds(clone);
     stripIds(clone);
     unhideTree(clone);
     unhideTree(clone);


    // If clone is a wrapper body, append its children to avoid double wrappers.
    // Otherwise append as-is.
     var isBody =
     var isBody =
       clone.classList &&
       clone.classList &&
Line 1,034: Line 1,073:
     setExpanded(state.anchor, false);
     setExpanded(state.anchor, false);


    // Keep legacy details closed when JS is active.
     if (state.anchor && state.anchor.tagName === "SUMMARY") {
     if (state.anchor && state.anchor.tagName === "SUMMARY") {
       var det = closest(state.anchor, POPUP_DETAILS_SEL);
       var det = closest(state.anchor, POPUP_DETAILS_SEL);
Line 1,051: Line 1,089:
     if (!el) return;
     if (!el) return;


    // Close previous (so popups don't stack)
     if (state.visible && state.anchor && state.anchor !== trigger) hidePop();
     if (state.visible && state.anchor && state.anchor !== trigger) hidePop();


Line 1,062: Line 1,099:
     setPopContentFromNode(sourceNode);
     setPopContentFromNode(sourceNode);


    // Interactivity: pinned pop allows clicking links; hover pop doesn't.
     el.style.pointerEvents = state.pinned ? "auto" : "none";
     el.style.pointerEvents = state.pinned ? "auto" : "none";
     el.classList.toggle("sv-pop-tip--pinned", state.pinned);
     el.classList.toggle("sv-pop-tip--pinned", state.pinned);
Line 1,068: Line 1,104:
     setExpanded(trigger, true);
     setExpanded(trigger, true);


    // Keep details closed; details is no-JS fallback.
     if (trigger && trigger.tagName === "SUMMARY") {
     if (trigger && trigger.tagName === "SUMMARY") {
       var det = closest(trigger, POPUP_DETAILS_SEL);
       var det = closest(trigger, POPUP_DETAILS_SEL);
Line 1,086: Line 1,121:
     return !!(e && e.pointerType && e.pointerType !== "mouse");
     return !!(e && e.pointerType && e.pointerType !== "mouse");
   }
   }
  /* --------------------------- Hover (desktop) ---------------------- */


   if (HOVER_CAPABLE) {
   if (HOVER_CAPABLE) {
Line 1,146: Line 1,179:
     );
     );
   }
   }
  /* --------------------------- Touch pin ---------------------------- */


   document.addEventListener(
   document.addEventListener(
Line 1,163: Line 1,194:
       if (!isTouchLikeEvent(e)) return;
       if (!isTouchLikeEvent(e)) return;


      // Stop <details> from opening when JS is present.
       e.preventDefault();
       e.preventDefault();


Line 1,171: Line 1,201:
     true
     true
   );
   );
  /* --------------------------- Click toggle ------------------------- */


   document.addEventListener(
   document.addEventListener(
Line 1,188: Line 1,216:
       }
       }


      // Prevent <details> default toggle + prevent navigation for span buttons.
       e.preventDefault();
       e.preventDefault();


Line 1,196: Line 1,223:
     true
     true
   );
   );
  /* --------------------------- Keyboard (Enter/Space) ---------------- */


   document.addEventListener(
   document.addEventListener(
Line 1,216: Line 1,241:
       e.preventDefault();
       e.preventDefault();


      // Keep details closed; use JS popup.
       if (state.pinned && sameAnchor(trigger)) hidePop();
       if (state.pinned && sameAnchor(trigger)) hidePop();
       else showPop(trigger, source, state.lastX || 0, state.lastY || 0, true);
       else showPop(trigger, source, state.lastX || 0, state.lastY || 0, true);
Line 1,222: Line 1,246:
     true
     true
   );
   );
  /* --------------------------- Close conditions ---------------------- */


   document.addEventListener(
   document.addEventListener(
Line 1,230: Line 1,252:
       if (!state.visible || !state.pinned) return;
       if (!state.visible || !state.pinned) return;


      // Click inside the floating pop -> keep open
       if (popEl && (e.target === popEl || (popEl.contains && popEl.contains(e.target))))
       if (popEl && (e.target === popEl || (popEl.contains && popEl.contains(e.target))))
         return;
         return;


      // Click on a trigger -> handled by toggle handler
       if (findTriggerFromEventTarget(e.target)) return;
       if (findTriggerFromEventTarget(e.target)) return;


Line 1,268: Line 1,288:
     true
     true
   );
   );
  /* --------------------------- Init / pruning ------------------------ */


   function hideTrigger(el) {
   function hideTrigger(el) {
Line 1,281: Line 1,299:
     var container = root || document;
     var container = root || document;


    // Prune modern buttons that have no content.
     var btns = container.querySelectorAll(POPUP_BTN_SEL);
     var btns = container.querySelectorAll(POPUP_BTN_SEL);
     for (var i = 0; i < btns.length; i++) {
     for (var i = 0; i < btns.length; i++) {
Line 1,290: Line 1,307:
     }
     }


    // Prune legacy details that have no content (keeps no-JS cleaner when possible).
     var ds = container.querySelectorAll(POPUP_DETAILS_SEL);
     var ds = container.querySelectorAll(POPUP_DETAILS_SEL);
     for (var j = 0; j < ds.length; j++) {
     for (var j = 0; j < ds.length; j++) {
Line 1,300: Line 1,316:


       if (isBlankContent(node2)) {
       if (isBlankContent(node2)) {
        // Hide the entire details block (no content to show).
         hideTrigger(d);
         hideTrigger(d);
       } else {
       } else {
        // Ensure closed when JS is active; details is fallback only.
         if (d.open) d.open = false;
         if (d.open) d.open = false;
         if (s) setExpanded(s, false);
         if (s) setExpanded(s, false);