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
Line 14: Line 14:
(function () {
(function () {


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


    // If value is not a valid number, fall back to min.
        // If value is not a valid number, fall back to min.
    if (isNaN(n)) {
        if (isNaN(n)) {
      return min;
            return min;
        }
 
        if (n < min) return min;
        if (n > max) return max;
        return n;
     }
     }


     if (n < min) return min;
     // Initialize skill cards found under `root`.
    if (n > max) return max;
    // `root` is usually the page, but MediaWiki sometimes gives us just a section of HTML.
    return n;
    function initSkillCards(root) {
  }
        var container = root || document;
 
        // Only pick cards we haven't initialized yet (data-sv-init is our marker).
        var cards = container.querySelectorAll(".sv-skill-card:not([data-sv-init])");
 
        cards.forEach(function (card) {
            // Read max level from HTML attribute (default 1 if missing).
            var maxLevel = clampNumber(card.getAttribute("data-max-level"), 1, 999);
 
            // If max level is 1, there's no reason to create a slider.
            if (maxLevel <= 1) {
                card.setAttribute("data-sv-init", "1");
                return;
            }
 
            // Read the starting level; if missing, default to max level.
            var startLevel = card.getAttribute("data-level");
            if (startLevel == null || startLevel === "") {
                startLevel = maxLevel;
            }
            startLevel = clampNumber(startLevel, 1, maxLevel);
 
            // Find where we should place the slider.
            var sliderHost = card.querySelector(".sv-level-slider");
            if (!sliderHost) {
                // No placeholder = nothing to do.
                card.setAttribute("data-sv-init", "1");
                return;
            }
 
            // Optional: element that shows the current level number in text.
            var levelNumberSpan = card.querySelector(".sv-level-num");
 
            // Find all dynamic elements that contain a data-series list.
            var dynamicElements = card.querySelectorAll("[data-series]");
 
            // Parse the JSON data-series once and store it on the element for reuse.
            dynamicElements.forEach(function (el) {
                var raw = el.getAttribute("data-series");
                try {
                    el._svSeries = JSON.parse(raw);
                } catch (e) {
                    el._svSeries = null;
                }
            });
 
            // Create the slider element.
            var slider = document.createElement("input");
            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.
            sliderHost.textContent = "";
            sliderHost.appendChild(slider);
 
            // Apply a given level to this card:
            // - update the level number text
            // - 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).
                card.setAttribute("data-level", String(lv));
 
                // Update the visible "current level" number if present.
                if (levelNumberSpan) {
                    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.
                    if (!series || !Array.isArray(series) || series.length === 0) {
                        return;
                    }
 
                    // If the series is shorter than maxLevel, clamp index to last element.
                    var safeIndex = index;
                    if (safeIndex >= series.length) {
                        safeIndex = series.length - 1;
                    }
 
                    var value = series[safeIndex];
 
                    // Put the chosen value into the element.
                    // Use an empty string if it's null/undefined.
                    if (value == null) {
                        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");
        });
    }


  // Initialize skill cards found under `root`.
    // MediaWiki hook:
  // `root` is usually the page, but MediaWiki sometimes gives us just a section of HTML.
    // Runs when page content is ready (and also after AJAX updates).
  function initSkillCards(root) {
    if (window.mw && mw.hook) {
     var container = root || document;
        mw.hook("wikipage.content").add(function ($content) {
            // $content is usually a jQuery object; $content[0] is the real DOM node.
            initSkillCards($content && $content[0]);
        });
     }


     // Only pick cards we haven't initialized yet (data-sv-init is our marker).
     // Also run on normal page load as a fallback.
     var cards = container.querySelectorAll(".sv-skill-card:not([data-sv-init])");
     if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", function () {
            initSkillCards(document);
        });
    } else {
        initSkillCards(document);
    }


    cards.forEach(function (card) {
})();
      // Read max level from HTML attribute (default 1 if missing).
      var maxLevel = clampNumber(card.getAttribute("data-max-level"), 1, 999);


      // If max level is 1, there's no reason to create a slider.
(function () {
      if (maxLevel <= 1) {
    let pop = null;
        card.setAttribute("data-sv-init", "1");
    let activeBtn = null;
        return;
    let openMode = null; // "hover" | "click"
      }


      // Read the starting level; if missing, default to max level.
    function ensurePop() {
      var startLevel = card.getAttribute("data-level");
        if (pop) return pop;
      if (startLevel == null || startLevel === "") {
        pop = document.createElement("div");
         startLevel = maxLevel;
        pop.className = "sv-tip-pop";
      }
        pop.setAttribute("role", "dialog");
      startLevel = clampNumber(startLevel, 1, maxLevel);
        pop.setAttribute("id", "sv-tip-pop");
         pop.style.display = "none";
        document.body.appendChild(pop);
        return pop;
    }


      // Find where we should place the slider.
    function findContent(btn) {
      var sliderHost = card.querySelector(".sv-level-slider");
         const kind = btn.getAttribute("data-sv-tip");
      if (!sliderHost) {
         if (!kind) return null;
         // No placeholder = nothing to do.
        card.setAttribute("data-sv-init", "1");
         return;
      }


      // Optional: element that shows the current level number in text.
        const scope = btn.closest(".sv-tip-scope") || btn.closest(".sv-skill-card") || btn.parentElement;
      var levelNumberSpan = card.querySelector(".sv-level-num");
        if (!scope) return null;


      // Find all dynamic elements that contain a data-series list.
        return scope.querySelector(`.sv-tip-content[data-sv-tip-content="${CSS.escape(kind)}"]`);
      var dynamicElements = card.querySelectorAll("[data-series]");
    }


      // Parse the JSON data-series once and store it on the element for reuse.
    function setExpanded(btn, expanded) {
      dynamicElements.forEach(function (el) {
        if (!btn) return;
         var raw = el.getAttribute("data-series");
        btn.setAttribute("aria-expanded", expanded ? "true" : "false");
         try {
    }
          el._svSeries = JSON.parse(raw);
 
         } catch (e) {
    function positionPop(btn) {
          el._svSeries = null;
         if (!pop || !btn) return;
 
        const r = btn.getBoundingClientRect();
         const margin = 8;
 
        const pw = pop.offsetWidth;
        const ph = pop.offsetHeight;
 
        let left = r.left + (r.width / 2) - (pw / 2);
         left = Math.max(margin, Math.min(left, window.innerWidth - pw - margin));
 
        let top = r.bottom + margin;
        if (top + ph > window.innerHeight - margin) {
            top = r.top - ph - margin;
         }
         }
      });


      // Create the slider element.
        pop.style.left = `${Math.round(left)}px`;
      var slider = document.createElement("input");
        pop.style.top  = `${Math.round(top)}px`;
      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.
    function closeTip() {
      sliderHost.textContent = "";
        if (!pop) return;
      sliderHost.appendChild(slider);
        setExpanded(activeBtn, false);
        activeBtn = null;
        openMode = null;
        pop.style.display = "none";
        pop.innerHTML = "";
    }


      // Apply a given level to this card:
    function openTip(btn, mode) {
      // - update the level number text
         const content = findContent(btn);
      // - update all dynamic elements to show the correct series value
        if (!content) {
      function applyLevel(level) {
            closeTip();
         var lv = clampNumber(level, 1, maxLevel);
            return;
        }


         // Store the chosen level on the card (optional, but useful for debugging).
         ensurePop();
        card.setAttribute("data-level", String(lv));


         // Update the visible "current level" number if present.
         if (activeBtn === btn && pop.style.display !== "none") {
        if (levelNumberSpan) {
            openMode = mode || openMode;
          levelNumberSpan.textContent = String(lv);
            positionPop(btn);
            return;
         }
         }


         // Update each dynamic element:
         document.querySelectorAll(".sv-tip-btn[aria-expanded='true']").forEach(function (b) { setExpanded(b, false); });
         // Level 1 => index 0, Level 2 => index 1, etc.
 
         var index = lv - 1;
        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);
    }


         dynamicElements.forEach(function (el) {
    document.addEventListener("click", function (e) {
          var series = el._svSeries;
         const btn = e.target.closest(".sv-tip-btn");
        if (btn) {
            e.preventDefault();
            if (activeBtn === btn && pop && pop.style.display !== "none") {
                if (openMode === "click") {
                    closeTip();
                } else {
                    openTip(btn, "click");
                }
                return;
            }


          // If series is missing or not a proper list, skip it.
            openTip(btn, "click");
          if (!series || !Array.isArray(series) || series.length === 0) {
             return;
             return;
          }
        }


          // If the series is shorter than maxLevel, clamp index to last element.
        if (pop && pop.style.display !== "none") {
          var safeIndex = index;
            if (!e.target.closest(".sv-tip-pop")) closeTip();
          if (safeIndex >= series.length) {
        }
             safeIndex = series.length - 1;
    });
          }
 
    const hoverCapable = window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches;
    if (hoverCapable) {
        document.addEventListener("mouseover", function (e) {
             const btn = e.target.closest(".sv-tip-btn");
            if (!btn) return;
            if (openMode === "click") return;
            openTip(btn, "hover");
        });
 
        document.addEventListener("mouseout", function (e) {
            if (openMode !== "hover" || !activeBtn || !pop || pop.style.display === "none") return;
 
            const from = e.target;
            if (!(activeBtn.contains(from) || pop.contains(from))) return;
 
            const to = e.relatedTarget;
            if (to && (activeBtn.contains(to) || pop.contains(to))) return;


          var value = series[safeIndex];
            const toBtn = to && to.closest ? to.closest(".sv-tip-btn") : null;
            if (toBtn && toBtn !== activeBtn) return;


          // Put the chosen value into the element.
             closeTip();
          // Use an empty string if it's null/undefined.
          if (value == null) {
            el.textContent = "";
          } else {
             el.textContent = String(value);
          }
         });
         });
      }
    }


      // When the slider moves, update the card live.
    document.addEventListener("keydown", function (e) {
      slider.addEventListener("input", function () {
         if (e.key === "Escape") {
         applyLevel(slider.value);
            closeTip();
      });
            return;
        }
 
        const btn = document.activeElement && document.activeElement.closest
            ? document.activeElement.closest(".sv-tip-btn")
            : null;


      // Apply the starting level right away.
        if (!btn) return;
      applyLevel(startLevel);


      // Mark this card as initialized so we don't do it twice.
        if (e.key === "Enter" || e.key === " ") {
      card.setAttribute("data-sv-init", "1");
            e.preventDefault();
    });
            if (activeBtn === btn && pop && pop.style.display !== "none") {
  }
                if (openMode === "click") {
                    closeTip();
                } else {
                    openTip(btn, "click");
                }
                return;
            }


  // MediaWiki hook:
            openTip(btn, "click");
  // 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.
    if (window.mw && mw.hook) {
  if (document.readyState === "loading") {
        mw.hook("wikipage.content").add(function () {
    document.addEventListener("DOMContentLoaded", function () {
            closeTip();
      initSkillCards(document);
        });
    });
     }
  } else {
     initSkillCards(document);
  }


    window.addEventListener("resize", function () { if (activeBtn) positionPop(activeBtn); });
    window.addEventListener("scroll",  function () { if (activeBtn) positionPop(activeBtn); }, true);
})();
})();

Revision as of 08:04, 21 December 2025

/* Any JavaScript here will be loaded for all users on every page load. */

/*
  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 () {

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

        // If value is not a valid number, fall back to min.
        if (isNaN(n)) {
            return min;
        }

        if (n < min) return min;
        if (n > max) return max;
        return n;
    }

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

        // Only pick cards we haven't initialized yet (data-sv-init is our marker).
        var cards = container.querySelectorAll(".sv-skill-card:not([data-sv-init])");

        cards.forEach(function (card) {
            // Read max level from HTML attribute (default 1 if missing).
            var maxLevel = clampNumber(card.getAttribute("data-max-level"), 1, 999);

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

            // Read the starting level; if missing, default to max level.
            var startLevel = card.getAttribute("data-level");
            if (startLevel == null || startLevel === "") {
                startLevel = maxLevel;
            }
            startLevel = clampNumber(startLevel, 1, maxLevel);

            // Find where we should place the slider.
            var sliderHost = card.querySelector(".sv-level-slider");
            if (!sliderHost) {
                // No placeholder = nothing to do.
                card.setAttribute("data-sv-init", "1");
                return;
            }

            // Optional: element that shows the current level number in text.
            var levelNumberSpan = card.querySelector(".sv-level-num");

            // Find all dynamic elements that contain a data-series list.
            var dynamicElements = card.querySelectorAll("[data-series]");

            // Parse the JSON data-series once and store it on the element for reuse.
            dynamicElements.forEach(function (el) {
                var raw = el.getAttribute("data-series");
                try {
                    el._svSeries = JSON.parse(raw);
                } catch (e) {
                    el._svSeries = null;
                }
            });

            // Create the slider element.
            var slider = document.createElement("input");
            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.
            sliderHost.textContent = "";
            sliderHost.appendChild(slider);

            // Apply a given level to this card:
            // - update the level number text
            // - 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).
                card.setAttribute("data-level", String(lv));

                // Update the visible "current level" number if present.
                if (levelNumberSpan) {
                    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.
                    if (!series || !Array.isArray(series) || series.length === 0) {
                        return;
                    }

                    // If the series is shorter than maxLevel, clamp index to last element.
                    var safeIndex = index;
                    if (safeIndex >= series.length) {
                        safeIndex = series.length - 1;
                    }

                    var value = series[safeIndex];

                    // Put the chosen value into the element.
                    // Use an empty string if it's null/undefined.
                    if (value == null) {
                        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:
    // 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.
    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", function () {
            initSkillCards(document);
        });
    } else {
        initSkillCards(document);
    }

})();

(function () {
    let pop = null;
    let activeBtn = null;
    let openMode = null; // "hover" | "click"

    function ensurePop() {
        if (pop) return pop;
        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) {
        const kind = btn.getAttribute("data-sv-tip");
        if (!kind) return null;

        const scope = btn.closest(".sv-tip-scope") || btn.closest(".sv-skill-card") || btn.parentElement;
        if (!scope) return null;

        return scope.querySelector(`.sv-tip-content[data-sv-tip-content="${CSS.escape(kind)}"]`);
    }

    function setExpanded(btn, expanded) {
        if (!btn) return;
        btn.setAttribute("aria-expanded", expanded ? "true" : "false");
    }

    function positionPop(btn) {
        if (!pop || !btn) return;

        const r = btn.getBoundingClientRect();
        const margin = 8;

        const pw = pop.offsetWidth;
        const ph = pop.offsetHeight;

        let left = r.left + (r.width / 2) - (pw / 2);
        left = Math.max(margin, Math.min(left, window.innerWidth - pw - margin));

        let top = r.bottom + margin;
        if (top + ph > window.innerHeight - margin) {
            top = r.top - ph - margin;
        }

        pop.style.left = `${Math.round(left)}px`;
        pop.style.top  = `${Math.round(top)}px`;
    }

    function closeTip() {
        if (!pop) return;
        setExpanded(activeBtn, false);
        activeBtn = null;
        openMode = null;
        pop.style.display = "none";
        pop.innerHTML = "";
    }

    function openTip(btn, mode) {
        const content = findContent(btn);
        if (!content) {
            closeTip();
            return;
        }

        ensurePop();

        if (activeBtn === btn && pop.style.display !== "none") {
            openMode = mode || openMode;
            positionPop(btn);
            return;
        }

        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) {
        const btn = e.target.closest(".sv-tip-btn");
        if (btn) {
            e.preventDefault();
            if (activeBtn === btn && pop && pop.style.display !== "none") {
                if (openMode === "click") {
                    closeTip();
                } else {
                    openTip(btn, "click");
                }
                return;
            }

            openTip(btn, "click");
            return;
        }

        if (pop && pop.style.display !== "none") {
            if (!e.target.closest(".sv-tip-pop")) closeTip();
        }
    });

    const hoverCapable = window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches;
    if (hoverCapable) {
        document.addEventListener("mouseover", function (e) {
            const btn = e.target.closest(".sv-tip-btn");
            if (!btn) return;
            if (openMode === "click") return;
            openTip(btn, "hover");
        });

        document.addEventListener("mouseout", function (e) {
            if (openMode !== "hover" || !activeBtn || !pop || pop.style.display === "none") return;

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

            const to = e.relatedTarget;
            if (to && (activeBtn.contains(to) || pop.contains(to))) return;

            const toBtn = to && to.closest ? to.closest(".sv-tip-btn") : null;
            if (toBtn && toBtn !== activeBtn) return;

            closeTip();
        });
    }

    document.addEventListener("keydown", function (e) {
        if (e.key === "Escape") {
            closeTip();
            return;
        }

        const btn = document.activeElement && document.activeElement.closest
            ? document.activeElement.closest(".sv-tip-btn")
            : null;

        if (!btn) return;

        if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            if (activeBtn === btn && pop && pop.style.display !== "none") {
                if (openMode === "click") {
                    closeTip();
                } else {
                    openTip(btn, "click");
                }
                return;
            }

            openTip(btn, "click");
        }
    });

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