MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/* Common.js — Spirit Vale Wiki | /* Common.js — Spirit Vale Wiki | ||
Phase 4.1 rebuild (incremental) | |||
Currently included (standalone modules): | |||
1) Definitions (v1) | |||
2) Level Selector / Descriptions (data-series) | |||
Not included yet: | |||
- Popups | |||
- Tabs | |||
*/ | */ | ||
(function () { | (function () { | ||
/* ================================================================== */ | |||
/* MODULE: Definitions (v1) */ | |||
/* ================================================================== */ | |||
// Namespace + per-module guard (no “41”). | // Namespace + per-module guard (no “41”). | ||
var SV = (window.SV = window.SV || {}); | var SV = (window.SV = window.SV || {}); | ||
| Line 78: | Line 76: | ||
var t = defEl.getAttribute(TIP_ATTR); | var t = defEl.getAttribute(TIP_ATTR); | ||
if (t == null) return ""; | if (t == null) return ""; | ||
t = String(t); | t = String(t); | ||
if (!t.replace(/\s+/g, "")) return ""; | if (!t.replace(/\s+/g, "")) return ""; | ||
| Line 100: | Line 97: | ||
el.setAttribute("aria-hidden", "true"); | el.setAttribute("aria-hidden", "true"); | ||
// | // Conservative inline styling (CSS can override via .sv-def-tip). | ||
el.style.position = "fixed"; | el.style.position = "fixed"; | ||
el.style.zIndex = "99999"; | el.style.zIndex = "99999"; | ||
| Line 145: | Line 140: | ||
if (!el) return; | if (!el) return; | ||
var vw = window.innerWidth || document.documentElement.clientWidth || 0; | var vw = window.innerWidth || document.documentElement.clientWidth || 0; | ||
var vh = window.innerHeight || document.documentElement.clientHeight || 0; | var vh = window.innerHeight || document.documentElement.clientHeight || 0; | ||
var gapY = state.pinned ? 10 : 16; | var gapY = state.pinned ? 10 : 16; | ||
var gapX = 12; | var gapX = 12; | ||
el.style.left = "0px"; | el.style.left = "0px"; | ||
el.style.top = "0px"; | el.style.top = "0px"; | ||
requestAnimationFrame(function () { | requestAnimationFrame(function () { | ||
if (!state.visible) return; | if (!state.visible) return; | ||
| Line 168: | Line 156: | ||
var h = r.height || 60; | var h = r.height || 60; | ||
var left = clientX - w / 2; | var left = clientX - w / 2; | ||
var top = clientY - h - gapY; | var top = clientY - h - gapY; | ||
left = clamp(left, gapX, Math.max(gapX, vw - w - gapX)); | left = clamp(left, gapX, Math.max(gapX, vw - w - gapX)); | ||
if (top < gapY) top = clientY + gapY; | if (top < gapY) top = clientY + gapY; | ||
top = clamp(top, gapY, Math.max(gapY, vh - h - gapY)); | top = clamp(top, gapY, Math.max(gapY, vh - h - gapY)); | ||
| Line 196: | Line 180: | ||
state.lastY = clientY || 0; | state.lastY = clientY || 0; | ||
el.textContent = text; | el.textContent = text; | ||
el.style.pointerEvents = state.pinned ? "auto" : "none"; | el.style.pointerEvents = state.pinned ? "auto" : "none"; | ||
el.classList.toggle("sv-def-tip--pinned", state.pinned); | el.classList.toggle("sv-def-tip--pinned", state.pinned); | ||
| Line 220: | Line 202: | ||
if (!title) return; | if (!title) return; | ||
if (window.mw && mw.util && typeof mw.util.getUrl === "function") { | if (window.mw && mw.util && typeof mw.util.getUrl === "function") { | ||
window.location.href = mw.util.getUrl(title); | window.location.href = mw.util.getUrl(title); | ||
| Line 226: | Line 207: | ||
} | } | ||
window.location.href = String(title); | window.location.href = String(title); | ||
} | } | ||
| Line 239: | Line 219: | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Desktop hover | /* Desktop hover */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
| Line 246: | Line 226: | ||
"mouseover", | "mouseover", | ||
function (e) { | function (e) { | ||
if (state.pinned) return; | if (state.pinned) return; | ||
var defEl = closest(e.target, DEF_SEL); | var defEl = closest(e.target, DEF_SEL); | ||
if (!defEl) { | if (!defEl) { | ||
if (state.visible && !state.pinned) hideTip(); | if (state.visible && !state.pinned) hideTip(); | ||
return; | return; | ||
} | } | ||
var rel = e.relatedTarget; | var rel = e.relatedTarget; | ||
if (rel && defEl.contains(rel)) return; | if (rel && defEl.contains(rel)) return; | ||
| Line 261: | Line 239: | ||
var text = getTipText(defEl); | var text = getTipText(defEl); | ||
if (!text) { | if (!text) { | ||
if (state.visible && !state.pinned) hideTip(); | if (state.visible && !state.pinned) hideTip(); | ||
return; | return; | ||
| Line 275: | Line 252: | ||
function (e) { | function (e) { | ||
if (!state.visible || state.pinned) return; | if (!state.visible || state.pinned) return; | ||
var defEl = closest(e.target, DEF_SEL); | var defEl = closest(e.target, DEF_SEL); | ||
if (!defEl || !sameAnchor(defEl)) return; | if (!defEl || !sameAnchor(defEl)) return; | ||
| Line 295: | Line 272: | ||
var rel = e.relatedTarget; | var rel = e.relatedTarget; | ||
if (rel && defEl.contains(rel)) return; | if (rel && defEl.contains(rel)) return; | ||
hideTip(); | hideTip(); | ||
| Line 304: | Line 281: | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Touch | /* Touch + click behavior */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
document.addEventListener( | document.addEventListener( | ||
"pointerdown", | "pointerdown", | ||
| Line 316: | Line 292: | ||
var text = getTipText(defEl); | var text = getTipText(defEl); | ||
if (!text) { | if (!text) { | ||
if (state.visible) hideTip(); | if (state.visible) hideTip(); | ||
return; | return; | ||
} | } | ||
var isTouchLike = e.pointerType && e.pointerType !== "mouse"; | var isTouchLike = e.pointerType && e.pointerType !== "mouse"; | ||
if (!isTouchLike) return; | if (!isTouchLike) return; | ||
state.suppressClickUntil = Date.now() + 450; | state.suppressClickUntil = Date.now() + 450; | ||
var link = getLinkTitle(defEl); | var link = getLinkTitle(defEl); | ||
if (state.pinned && sameAnchor(defEl) && link) { | if (state.pinned && sameAnchor(defEl) && link) { | ||
| Line 343: | Line 314: | ||
); | ); | ||
document.addEventListener( | document.addEventListener( | ||
"click", | "click", | ||
| Line 360: | Line 330: | ||
var link = getLinkTitle(defEl); | var link = getLinkTitle(defEl); | ||
if (link) { | if (link) { | ||
e.preventDefault(); | e.preventDefault(); | ||
navigateToTitle(link); | navigateToTitle(link); | ||
| Line 368: | Line 336: | ||
} | } | ||
e.preventDefault(); | e.preventDefault(); | ||
if (state.pinned && sameAnchor(defEl)) | if (state.pinned && sameAnchor(defEl)) hideTip(); | ||
else showTip(defEl, text, e.clientX, e.clientY, true); | |||
}, | }, | ||
true | true | ||
| Line 381: | Line 345: | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* Dismissal | /* Dismissal */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
document.addEventListener( | document.addEventListener( | ||
"click", | "click", | ||
| Line 390: | Line 353: | ||
if (!state.visible || !state.pinned) return; | if (!state.visible || !state.pinned) return; | ||
if (tipEl && (e.target === tipEl || (tipEl.contains && tipEl.contains(e.target)))) return; | if (tipEl && (e.target === tipEl || (tipEl.contains && tipEl.contains(e.target)))) return; | ||
if (closest(e.target, DEF_SEL)) return; | if (closest(e.target, DEF_SEL)) return; | ||
| Line 401: | Line 361: | ||
); | ); | ||
document.addEventListener( | document.addEventListener( | ||
"keydown", | "keydown", | ||
| Line 411: | Line 370: | ||
); | ); | ||
window.addEventListener( | window.addEventListener( | ||
"scroll", | "scroll", | ||
| Line 429: | Line 387: | ||
true | true | ||
); | ); | ||
// Fully delegated; hook kept for consistent pattern. | |||
function initAllDefs(root) { | |||
void root; | |||
} | |||
if (window.mw && mw.hook) { | |||
mw.hook("wikipage.content").add(function ($content) { | |||
initAllDefs($content && $content[0]); | |||
}); | |||
} | |||
if (document.readyState === "loading") { | |||
document.addEventListener("DOMContentLoaded", function () { | |||
initAllDefs(document); | |||
}); | |||
} else { | |||
initAllDefs(document); | |||
} | |||
})(); | |||
(function () { | |||
/* ================================================================== */ | |||
/* MODULE: Level Selector / Descriptions (data-series) */ | |||
/* ================================================================== */ | |||
// Namespace + per-module guard (standalone). | |||
var SV = (window.SV = window.SV || {}); | |||
var COMMON = (SV.common = SV.common || {}); | |||
if (COMMON.levelsInit) return; | |||
COMMON.levelsInit = 1; | |||
/* ------------------------------------------------------------------ */ | |||
/* Selectors */ | |||
/* ------------------------------------------------------------------ */ | |||
var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card"; | |||
// Legacy range input + custom slider | |||
var LEVEL_RANGE_SEL = | |||
"input.sv-level-range[type='range'], .sv-level-range--custom[data-sv-slider='1']"; | |||
var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level"; | |||
var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom"; | |||
var SERIES_SEL = "[data-series]"; | |||
/* ------------------------------------------------------------------ */ | |||
/* Internals */ | |||
/* ------------------------------------------------------------------ */ | |||
var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null; | |||
var _drag = null; | |||
/* ------------------------------------------------------------------ */ | |||
/* Utilities */ | |||
/* ------------------------------------------------------------------ */ | |||
function clampInt(value, min, max, fallback) { | |||
var n = parseInt(value, 10); | |||
if (isNaN(n)) n = fallback != null ? fallback : min; | |||
if (n < min) return min; | |||
if (n > max) return max; | |||
return n; | |||
} | |||
function closest(el, sel) { | |||
if (!el || el.nodeType !== 1) return null; | |||
if (el.closest) return el.closest(sel); | |||
while (el && el.nodeType === 1) { | |||
if (el.matches && el.matches(sel)) return el; | |||
el = el.parentElement; | |||
} | |||
return null; | |||
} | |||
function parseSeries(el) { | |||
if (!el) return null; | |||
if (_seriesCache) { | |||
if (_seriesCache.has(el)) return _seriesCache.get(el); | |||
} else if (el._svSeries !== undefined) { | |||
return el._svSeries; | |||
} | |||
var raw = el.getAttribute("data-series"); | |||
var parsed = null; | |||
if (raw != null && raw !== "") { | |||
try { | |||
parsed = JSON.parse(raw); | |||
if (!Array.isArray(parsed)) parsed = null; | |||
} catch (e) { | |||
parsed = null; | |||
} | |||
} | |||
if (_seriesCache) _seriesCache.set(el, parsed); | |||
else el._svSeries = parsed; | |||
return parsed; | |||
} | |||
function isAfter(boundaryNode, el) { | |||
if (!boundaryNode || !el) return true; | |||
if (boundaryNode === el) return false; | |||
if (!boundaryNode.compareDocumentPosition) return true; | |||
return (boundaryNode.compareDocumentPosition(el) & 4) !== 0; // following | |||
} | |||
function isRangeInput(el) { | |||
return !!(el && el.tagName === "INPUT" && el.type === "range"); | |||
} | |||
function isCustomSlider(el) { | |||
return !!(el && el.getAttribute && el.getAttribute("data-sv-slider") === "1"); | |||
} | |||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
/* | /* Core */ | ||
/* ------------------------------------------------------------------ */ | /* ------------------------------------------------------------------ */ | ||
// | function getCardMaxLevel(card) { | ||
// | return clampInt(card.getAttribute("data-max-level"), 1, 999, 1); | ||
function | } | ||
// | |||
// | function getCardLevel(card, maxLevel) { | ||
return clampInt(card.getAttribute("data-level"), 1, maxLevel, 1); | |||
} | |||
function getLevelSlider(card) { | |||
return card.querySelector(LEVEL_RANGE_SEL); | |||
} | |||
function getLevelBoundary(slider) { | |||
// boundary only matters when a slider exists | |||
if (!slider) return null; | |||
return closest(slider, LEVEL_BOUNDARY_SEL) || slider; | |||
} | |||
function getLevelScopeContainer(card, boundary) { | |||
var scope = card.querySelector(LEVEL_SCOPE_SEL) || card; | |||
if (boundary && scope !== card && !scope.contains(boundary)) return card; | |||
return scope; | |||
} | |||
function setLevelNumber(card, level) { | |||
var span = card.querySelector(".sv-level-num"); | |||
if (span) span.textContent = String(level); | |||
} | |||
function applySeriesToScope(scope, boundary, level) { | |||
if (!scope) return; | |||
var index = level - 1; | |||
var nodes = scope.querySelectorAll(SERIES_SEL); | |||
for (var i = 0; i < nodes.length; i++) { | |||
var el = nodes[i]; | |||
// Only update fields after boundary when boundary exists. | |||
if (boundary && !isAfter(boundary, el)) continue; | |||
var series = parseSeries(el); | |||
if (!series || series.length === 0) continue; | |||
var safeIndex = index; | |||
if (safeIndex >= series.length) safeIndex = series.length - 1; | |||
if (safeIndex < 0) safeIndex = 0; | |||
var value = series[safeIndex]; | |||
if (value == null) value = ""; | |||
el.textContent = String(value); | |||
} | |||
} | |||
/* ---------- Custom slider helpers --------------------------------- */ | |||
function getCustomMin(slider) { | |||
return clampInt( | |||
slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"), | |||
1, | |||
999, | |||
1 | |||
); | |||
} | |||
function getCustomMax(slider) { | |||
return clampInt( | |||
slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"), | |||
1, | |||
999, | |||
1 | |||
); | |||
} | |||
function getCustomValue(slider, fallback) { | |||
return clampInt( | |||
slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"), | |||
1, | |||
999, | |||
fallback != null ? fallback : 1 | |||
); | |||
} | |||
function setCustomBounds(slider, min, max) { | |||
slider.setAttribute("aria-valuemin", String(min)); | |||
slider.setAttribute("aria-valuemax", String(max)); | |||
slider.setAttribute("data-min", String(min)); | |||
slider.setAttribute("data-max", String(max)); | |||
} | |||
function updateCustomVisual(slider, value) { | |||
var min = getCustomMin(slider); | |||
var max = getCustomMax(slider); | |||
if (max < min) max = min; | |||
var pct = max === min ? 0 : (value - min) / (max - min); | |||
if (pct < 0) pct = 0; | |||
if (pct > 1) pct = 1; | |||
var left = pct * 100; | |||
var thumb = slider.querySelector(".sv-level-thumb"); | |||
var fill = slider.querySelector(".sv-level-fill"); | |||
if (thumb && thumb.style) thumb.style.left = left + "%"; | |||
if (fill && fill.style) fill.style.width = left + "%"; | |||
} | |||
function setCustomValue(slider, value) { | |||
slider.setAttribute("aria-valuenow", String(value)); | |||
slider.setAttribute("data-value", String(value)); | |||
updateCustomVisual(slider, value); | |||
} | |||
function valueFromClientX(slider, clientX) { | |||
var rect = slider.getBoundingClientRect(); | |||
var min = getCustomMin(slider); | |||
var max = getCustomMax(slider); | |||
if (max < min) max = min; | |||
var w = rect.width || 1; | |||
var x = (clientX - rect.left) / w; | |||
if (x < 0) x = 0; | |||
if (x > 1) x = 1; | |||
var raw = min + x * (max - min); | |||
var rounded = Math.round(raw); | |||
return clampInt(rounded, min, max, min); | |||
} | |||
function applyLevelToCard(card, rawLevel) { | |||
if (!card) return; | |||
var maxLevel = getCardMaxLevel(card); | |||
var slider = getLevelSlider(card); | |||
var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel)); | |||
// Store level on card for other systems / debugging. | |||
card.setAttribute("data-level", String(level)); | |||
setLevelNumber(card, level); | |||
// Keep slider in sync if one exists. | |||
if (slider && maxLevel > 1) { | |||
if (isRangeInput(slider)) { | |||
slider.setAttribute("min", "1"); | |||
slider.setAttribute("max", String(maxLevel)); | |||
if (String(slider.value) !== String(level)) slider.value = String(level); | |||
} else if (isCustomSlider(slider)) { | |||
setCustomBounds(slider, 1, maxLevel); | |||
setCustomValue(slider, level); | |||
} | |||
} | |||
// Apply series updates: | |||
// - If slider exists: honor boundary rule (only below boundary) | |||
// - If no slider: update all series in scope (boundary=null) | |||
var boundary = getLevelBoundary(slider); | |||
var scope = getLevelScopeContainer(card, boundary); | |||
applySeriesToScope(scope, boundary, level); | |||
} | |||
function initLevels(root) { | |||
var container = root || document; | |||
var cards = container.querySelectorAll(CARD_SEL); | |||
for (var i = 0; i < cards.length; i++) { | |||
var card = cards[i]; | |||
if (card.getAttribute("data-gi-level-init") === "1") continue; | |||
var maxLevel = getCardMaxLevel(card); | |||
var slider = getLevelSlider(card); | |||
var start = getCardLevel(card, maxLevel); | |||
if (slider && maxLevel > 1) { | |||
if (isRangeInput(slider)) start = slider.value; | |||
else if (isCustomSlider(slider)) start = getCustomValue(slider, start); | |||
} | |||
applyLevelToCard(card, start); | |||
card.setAttribute("data-gi-level-init", "1"); | |||
} | |||
} | |||
/* ------------------------------------------------------------------ */ | |||
/* Events */ | |||
/* ------------------------------------------------------------------ */ | |||
// Range input slider (delegated) | |||
document.addEventListener( | |||
"input", | |||
function (e) { | |||
var t = e.target; | |||
if (!t || t.nodeType !== 1) return; | |||
if (!t.matches || !t.matches("input.sv-level-range[type='range']")) return; | |||
var card = closest(t, CARD_SEL); | |||
if (!card) return; | |||
applyLevelToCard(card, t.value); | |||
}, | |||
true | |||
); | |||
// Custom slider pointer drag (delegated) | |||
document.addEventListener( | |||
"pointerdown", | |||
function (e) { | |||
var slider = | |||
e.target && e.target.closest | |||
? e.target.closest(".sv-level-range--custom[data-sv-slider='1']") | |||
: null; | |||
if (!slider) return; | |||
var card = closest(slider, CARD_SEL); | |||
if (!card) return; | |||
e.preventDefault(); | |||
if (slider.focus) slider.focus(); | |||
_drag = { slider: slider, card: card, pointerId: e.pointerId }; | |||
if (slider.setPointerCapture) { | |||
try { | |||
slider.setPointerCapture(e.pointerId); | |||
} catch (err) {} | |||
} | |||
applyLevelToCard(card, valueFromClientX(slider, e.clientX)); | |||
}, | |||
true | |||
); | |||
document.addEventListener( | |||
"pointermove", | |||
function (e) { | |||
if (!_drag) return; | |||
if (e.pointerId !== _drag.pointerId) return; | |||
e.preventDefault(); | |||
applyLevelToCard(_drag.card, valueFromClientX(_drag.slider, e.clientX)); | |||
}, | |||
true | |||
); | |||
function endDrag(e) { | |||
if (!_drag) return; | |||
if (e && e.pointerId != null && e.pointerId !== _drag.pointerId) return; | |||
if (_drag.slider && _drag.slider.releasePointerCapture) { | |||
try { | |||
_drag.slider.releasePointerCapture(_drag.pointerId); | |||
} catch (err) {} | |||
} | |||
_drag = null; | |||
} | |||
document.addEventListener("pointerup", endDrag, true); | |||
document.addEventListener("pointercancel", endDrag, true); | |||
// Custom slider keyboard support | |||
document.addEventListener( | |||
"keydown", | |||
function (e) { | |||
var el = document.activeElement; | |||
if (!el || !el.closest) return; | |||
var slider = el.closest(".sv-level-range--custom[data-sv-slider='1']"); | |||
if (!slider) return; | |||
var card = closest(slider, CARD_SEL); | |||
if (!card) return; | |||
var min = getCustomMin(slider); | |||
var max = getCustomMax(slider); | |||
if (max < min) max = min; | |||
var cur = getCustomValue(slider, getCardLevel(card, max)); | |||
var next = cur; | |||
if (e.key === "ArrowLeft" || e.key === "Left" || e.key === "ArrowDown") { | |||
next = cur - 1; | |||
} else if ( | |||
e.key === "ArrowRight" || | |||
e.key === "Right" || | |||
e.key === "ArrowUp" | |||
) { | |||
next = cur + 1; | |||
} else if (e.key === "Home") { | |||
next = min; | |||
} else if (e.key === "End") { | |||
next = max; | |||
} else if (e.key === "PageDown") { | |||
next = cur - 5; | |||
} else if (e.key === "PageUp") { | |||
next = cur + 5; | |||
} else { | |||
return; | |||
} | |||
e.preventDefault(); | |||
next = clampInt(next, min, max, cur); | |||
applyLevelToCard(card, next); | |||
}, | |||
true | |||
); | |||
/* ------------------------------------------------------------------ */ | |||
/* Init */ | |||
/* ------------------------------------------------------------------ */ | |||
function initAllLevels(root) { | |||
initLevels(root); | |||
} | } | ||
if (window.mw && mw.hook) { | if (window.mw && mw.hook) { | ||
mw.hook("wikipage.content").add(function ($content) { | mw.hook("wikipage.content").add(function ($content) { | ||
initAllLevels($content && $content[0]); | |||
}); | }); | ||
} | } | ||
| Line 450: | Line 833: | ||
if (document.readyState === "loading") { | if (document.readyState === "loading") { | ||
document.addEventListener("DOMContentLoaded", function () { | document.addEventListener("DOMContentLoaded", function () { | ||
initAllLevels(document); | |||
}); | }); | ||
} else { | } else { | ||
initAllLevels(document); | |||
} | } | ||
})(); | })(); | ||