MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
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);
})();