MediaWiki:Common.js
MediaWiki interface page
More actions
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* 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);
})();
/* ============================================================================
* SV Definitions v1 — tooltips for .sv-def
* - Reads: data-sv-def-tip, data-sv-def-link
* - Works with spans output by Module:Definitions
* ========================================================================== */
(function (mw) {
"use strict";
if (!mw || window.SV_DEF_INIT) return;
window.SV_DEF_INIT = true;
var tipEl = null;
var tipInner = null;
var activeEl = null;
var pinned = false;
function qTipText(el) {
var s = (el && el.getAttribute("data-sv-def-tip")) || "";
return (s || "").trim();
}
function qLink(el) {
var s = (el && el.getAttribute("data-sv-def-link")) || "";
return (s || "").trim();
}
function ensureTip() {
if (tipEl) return;
tipEl = document.createElement("div");
tipEl.id = "sv-def-tip";
tipEl.className = "sv-def-tip";
tipEl.setAttribute("role", "tooltip");
tipEl.setAttribute("aria-hidden", "true");
// Minimal inline styling so it works even without CSS.
// You can override in CSS later using #sv-def-tip / .sv-def-tip.
tipEl.style.position = "fixed";
tipEl.style.zIndex = "99999";
tipEl.style.display = "none";
tipEl.style.maxWidth = "340px";
tipEl.style.padding = "8px 10px";
tipEl.style.borderRadius = "8px";
tipEl.style.background = "rgba(15, 18, 24, 0.96)";
tipEl.style.color = "#fff";
tipEl.style.fontSize = "13px";
tipEl.style.lineHeight = "1.25";
tipEl.style.boxShadow = "0 8px 24px rgba(0,0,0,0.35)";
tipEl.style.pointerEvents = "none";
tipInner = document.createElement("div");
tipInner.className = "sv-def-tip-inner";
tipEl.appendChild(tipInner);
document.body.appendChild(tipEl);
}
function suppressTitle(el) {
// Prevent double-tooltips (browser title tooltip + our custom tooltip).
if (!el) return;
var t = el.getAttribute("title");
if (t && !el.getAttribute("data-sv-def-title")) {
el.setAttribute("data-sv-def-title", t);
el.removeAttribute("title");
}
}
function restoreTitle(el) {
if (!el) return;
var t = el.getAttribute("data-sv-def-title");
if (t) {
el.setAttribute("title", t);
el.removeAttribute("data-sv-def-title");
}
}
function clamp(n, lo, hi) {
return Math.max(lo, Math.min(hi, n));
}
function positionTip(el) {
if (!tipEl || !el) return;
var r = el.getBoundingClientRect();
// Ensure we can measure tip size
tipEl.style.left = "0px";
tipEl.style.top = "0px";
tipEl.style.display = "block";
var tr = tipEl.getBoundingClientRect();
var pad = 8;
var gap = 8;
var x = r.left + (r.width / 2) - (tr.width / 2);
x = clamp(x, pad, window.innerWidth - tr.width - pad);
var y = r.bottom + gap;
if (y + tr.height > window.innerHeight - pad) {
y = r.top - tr.height - gap;
}
y = clamp(y, pad, window.innerHeight - tr.height - pad);
tipEl.style.left = Math.round(x) + "px";
tipEl.style.top = Math.round(y) + "px";
}
function show(el) {
ensureTip();
var txt = qTipText(el);
if (!txt) return;
activeEl = el;
tipInner.textContent = txt;
tipEl.setAttribute("aria-hidden", "false");
tipEl.style.display = "block";
suppressTitle(el);
positionTip(el);
}
function hide() {
if (!tipEl) return;
tipEl.style.display = "none";
tipEl.setAttribute("aria-hidden", "true");
if (activeEl) restoreTitle(activeEl);
activeEl = null;
pinned = false;
}
function isEntering(container, relatedTarget) {
return relatedTarget && container && container.contains(relatedTarget);
}
function closestDef(target) {
if (!target || !target.closest) return null;
return target.closest(".sv-def");
}
// Hover in/out (desktop)
document.addEventListener("mouseover", function (e) {
if (pinned) return;
var el = closestDef(e.target);
if (!el) return;
if (isEntering(el, e.relatedTarget)) return;
if (activeEl === el) return;
show(el);
}, true);
document.addEventListener("mouseout", function (e) {
if (pinned) return;
var el = closestDef(e.target);
if (!el) return;
if (isEntering(el, e.relatedTarget)) return;
if (activeEl === el) hide();
}, true);
// Tap/click behavior:
// - If data-sv-def-link is set, navigate (future-ready).
// - Otherwise toggle a pinned tooltip (mobile-friendly).
document.addEventListener("click", function (e) {
var el = closestDef(e.target);
// Click outside closes a pinned tooltip
if (!el) {
if (pinned) hide();
return;
}
var link = qLink(el);
if (link) {
// Let it act like a link to a page title
window.location.href = mw.util.getUrl(link);
return;
}
var txt = qTipText(el);
if (!txt) return;
// Toggle pin
e.preventDefault();
e.stopPropagation();
if (pinned && activeEl === el) {
hide();
return;
}
pinned = true;
show(el);
}, true);
// Close on Escape
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && (pinned || activeEl)) {
hide();
}
}, true);
// Reposition if visible
window.addEventListener("resize", function () {
if (activeEl && tipEl && tipEl.style.display === "block") {
positionTip(activeEl);
}
}, true);
// Capture scroll events from containers too
window.addEventListener("scroll", function () {
if (activeEl && tipEl && tipEl.style.display === "block") {
positionTip(activeEl);
}
}, true);
// Support dynamic page content (VisualEditor, etc.)
mw.hook("wikipage.content").add(function () {
// No-op: event delegation already covers new content.
});
})(window.mw);