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


         // Only pick cards we haven't initialized yet (data-sv-init is our marker).
         // 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])");
         var cards = container.querySelectorAll(
            ".sv-skill-card:not([data-sv-init]), .sv-passive-card:not([data-sv-init])"
        );


         cards.forEach(function (card) {
         cards.forEach(function (card) {
Line 186: Line 188:
         if (!kind) return null;
         if (!kind) return null;


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



Revision as of 20:48, 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]), .sv-passive-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-slot-card")
            || 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)}"]`);
    }

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