MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary Tags: Mobile edit Mobile web edit |
No edit summary Tags: Mobile edit Mobile web edit |
||
| Line 17: | Line 17: | ||
// Versioned init guard (allows safe updates without stale blocks) | // Versioned init guard (allows safe updates without stale blocks) | ||
var LEVELS_VERSION = | var LEVELS_VERSION = 3; // bumped: min-level correctness + value label + rAF drag | ||
if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return; | if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return; | ||
COMMON.levelsInit = LEVELS_VERSION; | COMMON.levelsInit = LEVELS_VERSION; | ||
| Line 115: | Line 115: | ||
} | } | ||
function getCardLevel(card, maxLevel) { | function getCardMinLevel(card, slider, maxLevel) { | ||
// Prefer explicit attributes if ever present (future-proof), then slider attrs. | |||
var raw = | |||
card.getAttribute("data-min-level") || | |||
card.getAttribute("data-minLevel") || | |||
card.getAttribute("data-level-min"); | |||
if (raw == null || raw === "") { | |||
if (slider) { | |||
if (isRangeInput(slider)) raw = slider.getAttribute("min"); | |||
else if (isCustomSlider(slider)) raw = slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"); | |||
} | |||
} | |||
var min = clampInt(raw, 1, maxLevel, 1); | |||
if (min > maxLevel) min = maxLevel; | |||
return min; | |||
} | |||
function getCardLevel(card, minLevel, maxLevel) { | |||
var raw = | var raw = | ||
card.getAttribute("data-level") || | card.getAttribute("data-level") || | ||
| Line 123: | Line 142: | ||
: null); | : null); | ||
return clampInt(raw, 1, | return clampInt(raw, minLevel, maxLevel, minLevel); | ||
} | |||
function getStep(slider) { | |||
// Default = 1. Support future: data-step, or <input step=""> | |||
var raw = null; | |||
if (slider) { | |||
raw = slider.getAttribute("data-step") || slider.getAttribute("step"); | |||
} | |||
var s = parseInt(raw, 10); | |||
if (!s || isNaN(s) || s < 1) s = 1; | |||
return s; | |||
} | } | ||
| Line 144: | Line 174: | ||
var span = card.querySelector(".sv-level-num"); | var span = card.querySelector(".sv-level-num"); | ||
if (span) span.textContent = String(level); | if (span) span.textContent = String(level); | ||
} | |||
function setLevelMax(card, maxLevel) { | |||
var span = card.querySelector(".sv-level-max"); | |||
if (span) span.textContent = String(maxLevel); | |||
} | } | ||
| Line 149: | Line 184: | ||
if (!scope) return; | if (!scope) return; | ||
// JS arrays are 0-based; Lua uses 1-based "level". | |||
var index = level - 1; | var index = level - 1; | ||
var nodes = scope.querySelectorAll(SERIES_SEL); | var nodes = scope.querySelectorAll(SERIES_SEL); | ||
| Line 170: | Line 206: | ||
} | } | ||
function getCustomMin(slider) { | function getCustomMin(slider, fallback) { | ||
return clampInt( | return clampInt( | ||
slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"), | slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"), | ||
1, | 1, | ||
999, | 999, | ||
1 | fallback != null ? fallback : 1 | ||
); | ); | ||
} | } | ||
function getCustomMax(slider) { | function getCustomMax(slider, fallback) { | ||
return clampInt( | return clampInt( | ||
slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"), | slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"), | ||
1, | 1, | ||
999, | 999, | ||
1 | fallback != null ? fallback : 1 | ||
); | ); | ||
} | } | ||
function getCustomValue(slider, fallback) { | function getCustomValue(slider, min, max, fallback) { | ||
return clampInt( | return clampInt( | ||
slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"), | slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"), | ||
min, | |||
max, | |||
fallback != null ? fallback : | fallback != null ? fallback : min | ||
); | ); | ||
} | } | ||
| Line 202: | Line 238: | ||
slider.setAttribute("data-min", String(min)); | slider.setAttribute("data-min", String(min)); | ||
slider.setAttribute("data-max", String(max)); | slider.setAttribute("data-max", String(max)); | ||
} | |||
function ensureValueLabel(slider) { | |||
if (!slider || !slider.querySelector || !isCustomSlider(slider)) return null; | |||
var bubble = slider.querySelector(".sv-level-bubble"); | |||
if (bubble) return bubble; | |||
bubble = document.createElement("span"); | |||
bubble.className = "sv-level-bubble sv-hidden"; | |||
bubble.setAttribute("hidden", "hidden"); | |||
bubble.setAttribute("aria-hidden", "true"); | |||
// Minimal layout-only styling (no color system changes yet). | |||
// We'll fully theme this in CSS next. | |||
bubble.style.position = "absolute"; | |||
bubble.style.top = "-28px"; | |||
bubble.style.left = "0%"; | |||
bubble.style.transform = "translateX(-50%)"; | |||
bubble.style.whiteSpace = "nowrap"; | |||
bubble.style.fontWeight = "900"; | |||
bubble.style.fontSize = "12px"; | |||
bubble.style.lineHeight = "1"; | |||
bubble.style.pointerEvents = "none"; | |||
slider.appendChild(bubble); | |||
return bubble; | |||
} | |||
function unhide(el) { | |||
if (!el) return; | |||
if (el.classList) el.classList.remove("sv-hidden"); | |||
if (el.removeAttribute) el.removeAttribute("hidden"); | |||
if (el.style && el.style.display === "none") el.style.display = ""; | |||
} | |||
function hide(el) { | |||
if (!el) return; | |||
if (el.classList) el.classList.add("sv-hidden"); | |||
if (el.setAttribute) el.setAttribute("hidden", "hidden"); | |||
} | |||
function setValueLabel(slider, value) { | |||
var bubble = ensureValueLabel(slider); | |||
if (!bubble) return; | |||
var min = getCustomMin(slider, 1); | |||
var max = getCustomMax(slider, min); | |||
if (max < min) max = min; | |||
var pct = max === min ? 0 : (value - min) / (max - min); | |||
if (pct < 0) pct = 0; | |||
if (pct > 1) pct = 1; | |||
bubble.textContent = String(value); | |||
bubble.style.left = pct * 100 + "%"; | |||
} | |||
function showValueLabel(slider) { | |||
if (!slider) return; | |||
var bubble = ensureValueLabel(slider); | |||
if (!bubble) return; | |||
// Cancel any pending hide timers. | |||
if (slider._svBubbleTimer) { | |||
clearTimeout(slider._svBubbleTimer); | |||
slider._svBubbleTimer = null; | |||
} | |||
unhide(bubble); | |||
} | |||
function hideValueLabelSoon(slider, delayMs) { | |||
if (!slider) return; | |||
var bubble = slider.querySelector ? slider.querySelector(".sv-level-bubble") : null; | |||
if (!bubble) return; | |||
if (slider._svBubbleTimer) clearTimeout(slider._svBubbleTimer); | |||
slider._svBubbleTimer = setTimeout(function () { | |||
hide(bubble); | |||
slider._svBubbleTimer = null; | |||
}, delayMs != null ? delayMs : 450); | |||
} | } | ||
function updateCustomVisual(slider, value) { | function updateCustomVisual(slider, value) { | ||
var min = getCustomMin(slider); | var min = getCustomMin(slider, 1); | ||
var max = getCustomMax(slider); | var max = getCustomMax(slider, min); | ||
if (max < min) max = min; | if (max < min) max = min; | ||
| Line 220: | Line 338: | ||
if (thumb && thumb.style) thumb.style.left = left + "%"; | if (thumb && thumb.style) thumb.style.left = left + "%"; | ||
if (fill && fill.style) fill.style.width = left + "%"; | if (fill && fill.style) fill.style.width = left + "%"; | ||
// Keep the value label aligned, if present/visible. | |||
setValueLabel(slider, value); | |||
} | } | ||
function setCustomValue(slider, value) { | function setCustomValue(slider, value, min, max) { | ||
slider.setAttribute("aria-valuenow", String(value)); | slider.setAttribute("aria-valuenow", String(value)); | ||
slider.setAttribute("data-value", String(value)); | slider.setAttribute("data-value", String(value)); | ||
// Helpful SR text (doesn't affect visuals) | |||
slider.setAttribute("aria-valuetext", "Level " + String(value) + " of " + String(max)); | |||
updateCustomVisual(slider, value); | updateCustomVisual(slider, value); | ||
} | |||
function snapToStep(raw, min, max, step) { | |||
if (step < 1) step = 1; | |||
var x = raw; | |||
if (x < min) x = min; | |||
if (x > max) x = max; | |||
var snapped = min + Math.round((x - min) / step) * step; | |||
if (snapped < min) snapped = min; | |||
if (snapped > max) snapped = max; | |||
return snapped; | |||
} | } | ||
function valueFromClientX(slider, clientX) { | function valueFromClientX(slider, clientX) { | ||
var rect = slider.getBoundingClientRect(); | var rect = slider.getBoundingClientRect(); | ||
var min = getCustomMin(slider); | var min = getCustomMin(slider, 1); | ||
var max = getCustomMax(slider); | var max = getCustomMax(slider, min); | ||
if (max < min) max = min; | if (max < min) max = min; | ||
| Line 240: | Line 377: | ||
var raw = min + x * (max - min); | var raw = min + x * (max - min); | ||
var | var step = getStep(slider); | ||
return | return snapToStep(raw, min, max, step); | ||
} | } | ||
function | function setTickLabels(boundary, min, max) { | ||
if (! | if (!boundary || !boundary.querySelector) return; | ||
if ( | var ticks = boundary.querySelector(".sv-level-ticklabels"); | ||
if ( | if (!ticks) return; | ||
// Always show min/max (lightweight + useful). We can enhance into real ticks later. | |||
// Only rebuild if needed to avoid layout churn. | |||
var curKey = ticks.getAttribute("data-sv-ticks") || ""; | |||
var nextKey = String(min) + "/" + String(max); | |||
if (curKey === nextKey) return; | |||
ticks.setAttribute("data-sv-ticks", nextKey); | |||
while (ticks.firstChild) ticks.removeChild(ticks.firstChild); | |||
var a = document.createElement("span"); | |||
a.className = "sv-level-tick sv-level-tick--min"; | |||
a.textContent = String(min); | |||
var b = document.createElement("span"); | |||
b.className = "sv-level-tick sv-level-tick--max"; | |||
b.textContent = String(max); | |||
ticks.appendChild(a); | |||
ticks.appendChild(b); | |||
} | |||
function scheduleApply(card, rawLevel) { | |||
if (!card) return; | |||
card._svLevelNext = rawLevel; | |||
if (card._svLevelRaf) return; | |||
card._svLevelRaf = window.requestAnimationFrame | |||
? requestAnimationFrame(function () { | |||
card._svLevelRaf = null; | |||
var v = card._svLevelNext; | |||
card._svLevelNext = null; | |||
applyLevelToCard(card, v); | |||
}) | |||
: (function () { | |||
card._svLevelRaf = null; | |||
var v = card._svLevelNext; | |||
card._svLevelNext = null; | |||
applyLevelToCard(card, v); | |||
})(); | |||
} | } | ||
| Line 256: | Line 433: | ||
var maxLevel = getCardMaxLevel(card); | var maxLevel = getCardMaxLevel(card); | ||
var slider = getLevelSlider(card); | var slider = getLevelSlider(card); | ||
var level = clampInt(rawLevel, | var minLevel = getCardMinLevel(card, slider, maxLevel); | ||
var level = clampInt(rawLevel, minLevel, maxLevel, getCardLevel(card, minLevel, maxLevel)); | |||
card.setAttribute("data-level", String(level)); | card.setAttribute("data-level", String(level)); | ||
setLevelNumber(card, level); | setLevelNumber(card, level); | ||
setLevelMax(card, maxLevel); | |||
if (slider) { | if (slider) { | ||
| Line 267: | Line 447: | ||
} | } | ||
if (slider | if (slider) { | ||
if (isRangeInput(slider)) { | if (isRangeInput(slider)) { | ||
slider.setAttribute("min", | slider.setAttribute("min", String(minLevel)); | ||
slider.setAttribute("max", String(maxLevel)); | slider.setAttribute("max", String(maxLevel)); | ||
slider.setAttribute("step", String(getStep(slider))); | |||
slider.setAttribute("aria-valuetext", "Level " + String(level) + " of " + String(maxLevel)); | |||
if (String(slider.value) !== String(level)) slider.value = String(level); | if (String(slider.value) !== String(level)) slider.value = String(level); | ||
} else if (isCustomSlider(slider)) { | } else if (isCustomSlider(slider)) { | ||
setCustomBounds(slider, | setCustomBounds(slider, minLevel, maxLevel); | ||
setCustomValue(slider, level); | setCustomValue(slider, level, minLevel, maxLevel); | ||
} | } | ||
} | } | ||
var boundary = getLevelBoundary(slider); | var boundary = getLevelBoundary(slider); | ||
setTickLabels(boundary, minLevel, maxLevel); | |||
var scope = getLevelScopeContainer(card, boundary); | var scope = getLevelScopeContainer(card, boundary); | ||
applySeriesToScope(scope, boundary, level); | applySeriesToScope(scope, boundary, level); | ||
| Line 298: | Line 479: | ||
var maxLevel = getCardMaxLevel(card); | var maxLevel = getCardMaxLevel(card); | ||
var slider = getLevelSlider(card); | var slider = getLevelSlider(card); | ||
var minLevel = getCardMinLevel(card, slider, maxLevel); | |||
var start = getCardLevel(card, minLevel, maxLevel); | |||
if (slider && maxLevel > minLevel) { | |||
if (slider && maxLevel > | if (isRangeInput(slider)) start = clampInt(slider.value, minLevel, maxLevel, start); | ||
if (isRangeInput(slider)) start = slider.value; | else if (isCustomSlider(slider)) start = getCustomValue(slider, minLevel, maxLevel, start); | ||
else if (isCustomSlider(slider)) start = getCustomValue(slider, start); | |||
} | } | ||
| Line 310: | Line 493: | ||
} | } | ||
// Native range inputs | |||
document.addEventListener( | document.addEventListener( | ||
"input", | "input", | ||
| Line 320: | Line 504: | ||
if (!card) return; | if (!card) return; | ||
scheduleApply(card, t.value); | |||
}, | }, | ||
true | true | ||
); | ); | ||
// Custom slider pointer interaction (click anywhere + drag) | |||
document.addEventListener( | document.addEventListener( | ||
"pointerdown", | "pointerdown", | ||
function (e) { | function (e) { | ||
if (e.button != null && e.button !== 0) return; // primary only | |||
var slider = | var slider = | ||
e.target && e.target.closest | e.target && e.target.closest | ||
| Line 348: | Line 535: | ||
} | } | ||
// Show value label while interacting | |||
showValueLabel(slider); | |||
scheduleApply(card, valueFromClientX(slider, e.clientX)); | |||
}, | }, | ||
true | true | ||
| Line 359: | Line 549: | ||
if (e.pointerId !== _drag.pointerId) return; | if (e.pointerId !== _drag.pointerId) return; | ||
e.preventDefault(); | e.preventDefault(); | ||
showValueLabel(_drag.slider); | |||
scheduleApply(_drag.card, valueFromClientX(_drag.slider, e.clientX)); | |||
}, | }, | ||
true | true | ||
| Line 373: | Line 565: | ||
} catch (err) {} | } catch (err) {} | ||
} | } | ||
// Let the value label linger briefly, then hide. | |||
hideValueLabelSoon(_drag.slider, 650); | |||
_drag = null; | _drag = null; | ||
} | } | ||
| Line 379: | Line 575: | ||
document.addEventListener("pointercancel", endDrag, true); | document.addEventListener("pointercancel", endDrag, true); | ||
// Keyboard support for custom slider | |||
document.addEventListener( | document.addEventListener( | ||
"keydown", | "keydown", | ||
| Line 391: | Line 588: | ||
if (!card) return; | if (!card) return; | ||
var | var maxLevel = getCardMaxLevel(card); | ||
var | var minLevel = getCardMinLevel(card, slider, maxLevel); | ||
var step = getStep(slider); | |||
var cur = getCustomValue(slider, getCardLevel(card, | var cur = getCustomValue(slider, minLevel, maxLevel, getCardLevel(card, minLevel, maxLevel)); | ||
var next = cur; | var next = cur; | ||
// Shift+Arrow: faster adjustments | |||
var arrowStep = step; | |||
if (e.shiftKey) arrowStep = step * 5; | |||
if (e.key === "ArrowLeft" || e.key === "Left" || e.key === "ArrowDown") { | if (e.key === "ArrowLeft" || e.key === "Left" || e.key === "ArrowDown") { | ||
next = cur - | next = cur - arrowStep; | ||
} else if (e.key === "ArrowRight" || e.key === "Right" || e.key === "ArrowUp") { | } else if (e.key === "ArrowRight" || e.key === "Right" || e.key === "ArrowUp") { | ||
next = cur + | next = cur + arrowStep; | ||
} else if (e.key === "Home") { | } else if (e.key === "Home") { | ||
next = | next = minLevel; | ||
} else if (e.key === "End") { | } else if (e.key === "End") { | ||
next = | next = maxLevel; | ||
} else if (e.key === "PageDown") { | } else if (e.key === "PageDown") { | ||
next = cur - 5; | next = cur - step * 5; | ||
} else if (e.key === "PageUp") { | } else if (e.key === "PageUp") { | ||
next = cur + 5; | next = cur + step * 5; | ||
} else { | } else { | ||
return; | return; | ||
| Line 415: | Line 616: | ||
e.preventDefault(); | e.preventDefault(); | ||
next = clampInt(next, | next = clampInt(next, minLevel, maxLevel, cur); | ||
showValueLabel(slider); | |||
scheduleApply(card, next); | |||
hideValueLabelSoon(slider, 900); | |||
}, | |||
true | |||
); | |||
// Show label on focus (keyboard discoverability), hide on blur | |||
document.addEventListener( | |||
"focusin", | |||
function () { | |||
var el = document.activeElement; | |||
if (!el || !el.closest) return; | |||
var slider = el.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']"); | |||
if (!slider) return; | |||
var max = getCustomMax(slider, 1); | |||
var min = getCustomMin(slider, 1); | |||
var v = getCustomValue(slider, min, max, min); | |||
setValueLabel(slider, v); | |||
showValueLabel(slider); | |||
}, | |||
true | |||
); | |||
document.addEventListener( | |||
"focusout", | |||
function (e) { | |||
var t = e.target; | |||
if (!t || !t.closest) return; | |||
var slider = t.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']"); | |||
if (!slider) return; | |||
hideValueLabelSoon(slider, 300); | |||
}, | }, | ||
true | true | ||