MediaWiki:Common.js: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung Markierung: Zurückgesetzt |
Keine Bearbeitungszusammenfassung Markierung: Zurückgesetzt |
||
| Zeile 209: | Zeile 209: | ||
})(); | })(); | ||
/* TabberNeue: Anker-Scroll unter Vector-Header + Freeze | /* TabberNeue: Anker-Scroll unter Vector-Header + Freeze + Pinning */ | ||
(function () { | (function () { | ||
'use strict'; | 'use strict'; | ||
/* | /* --- Sticky-Offset (+0.5rem Luft) --- */ | ||
function stickyOffsetPx() { | function stickyOffsetPx() { | ||
var hdr = document.querySelector('.vector-sticky-header-container'); | var hdr = document.querySelector('.vector-sticky-header-container'); | ||
| Zeile 221: | Zeile 221: | ||
} | } | ||
/* --- | /* --- oberste Tabber-Leiste zu einem Panel --- */ | ||
function headerForPanel(panel) { | function headerForPanel(panel) { | ||
if (!panel) return null; | if (!panel) return null; | ||
var top = panel.closest('.tabber'); | var top = panel.closest('.tabber'); | ||
for (var | for (var cur = top; cur && cur.parentElement; ) { | ||
up = cur.parentElement.closest('.tabber'); | var up = cur.parentElement.closest('.tabber'); | ||
if (!up) break; | if (!up) break; | ||
top = up; cur = up; | top = up; cur = up; | ||
| Zeile 233: | Zeile 233: | ||
} | } | ||
/* -------- | /* --- hartes Pinning: hält ScrollY auf gewünschtem Wert --- */ | ||
function startPin(targetY) { | |||
var on = true; | |||
function lock() { | |||
if (!on) return; | |||
if (Math.abs(window.scrollY - targetY) > 1) window.scrollTo(0, targetY); | |||
requestAnimationFrame(lock); | |||
} | |||
document.documentElement.classList.add('fs-scroll-now'); | |||
requestAnimationFrame(lock); | |||
return function stopPin(){ on = false; document.documentElement.classList.remove('fs-scroll-now'); }; | |||
} | |||
/* --- Freeze: min-height stabil + Scroll-Anchor neutralisieren --- */ | |||
function freezeTabber(tabber) { | function freezeTabber(tabber) { | ||
var section = tabber && tabber.querySelector(':scope > .tabber__section'); | var section = tabber && tabber.querySelector(':scope > .tabber__section'); | ||
if (!section) return function(){}; | if (!section) return function(){}; | ||
var h = Math.max(0, Math.round(section.getBoundingClientRect().height)); | var h = Math.max(0, Math.round(section.getBoundingClientRect().height)); | ||
tabber.style.setProperty('--tabber-freeze-h', h + 'px'); | tabber.style.setProperty('--tabber-freeze-h', h + 'px'); | ||
tabber.classList.add('tabber-is-switching'); | tabber.classList.add('tabber-is-switching'); | ||
var oldOA = section.style.overflowAnchor; | |||
var | section.style.overflowAnchor = 'none'; | ||
return function unfreeze(){ | |||
return function unfreeze() { | |||
tabber.classList.remove('tabber-is-switching'); | tabber.classList.remove('tabber-is-switching'); | ||
tabber.style.removeProperty('--tabber-freeze-h'); | tabber.style.removeProperty('--tabber-freeze-h'); | ||
section.style.overflowAnchor = oldOA || ''; | |||
}; | }; | ||
} | } | ||
/* -------- | /* --- Warte bis Panel „ruhig“ ist (keine Mutationen + Höhe stabil) --- */ | ||
function waitStable(panel) { | |||
var section = panel && panel.closest('.tabber')?.querySelector(':scope > .tabber__section'); | |||
if (!section) return Promise.resolve(); | |||
var lastH = Math.round(section.getBoundingClientRect().height); | |||
var stableFrames = 0, lastMutation = performance.now(); | |||
var mo = new MutationObserver(function(){ lastMutation = performance.now(); }); | |||
mo.observe(section, {subtree:true, childList:true, attributes:true, characterData:true}); | |||
return new Promise(function(resolve){ | |||
(function loop(){ | |||
var h = Math.round(section.getBoundingClientRect().height); | |||
stableFrames = (Math.abs(h - lastH) < 1) ? (stableFrames + 1) : 0; | |||
lastH = h; | |||
var quiet = (performance.now() - lastMutation) > 300; // 300ms ohne Mutation | |||
if (stableFrames >= 12 && quiet) { // ~200ms höhenstabil | |||
mo.disconnect(); resolve(); return; | |||
} | |||
requestAnimationFrame(loop); | |||
})(); | |||
}); | |||
} | |||
/* --- Tabber internes scrollIntoView neutralisieren --- */ | |||
var _siv = Element.prototype.scrollIntoView; | var _siv = Element.prototype.scrollIntoView; | ||
Element.prototype.scrollIntoView = function(arg){ | Element.prototype.scrollIntoView = function(arg){ | ||
try { if (this.closest && this.closest('.tabber')) return; } catch(e){} | try{ if (this.closest && this.closest('.tabber')) return; }catch(e){} | ||
return _siv.call(this, arg); | return _siv.call(this, arg); | ||
}; | }; | ||
/* --- | /* --- Zielposition berechnen + springen --- */ | ||
function jumpTo(id, behavior) { | function jumpTo(id, behavior) { | ||
var name = (id || '').replace(/^#/, ''); | var name = (id || '').replace(/^#/, ''); | ||
if (!/^tabber-/.test(name)) return; | if (!/^tabber-/.test(name)) return; | ||
var panel | var panel = document.getElementById(name); if (!panel) return; | ||
var | var head = headerForPanel(panel) || panel; | ||
var y = | var y = head.getBoundingClientRect().top + window.scrollY - stickyOffsetPx(); | ||
window.scrollTo({ top: y, behavior: behavior || 'auto' }); | window.scrollTo({ top: y, behavior: behavior || 'auto' }); | ||
return { panel: panel, y: y }; | |||
} | } | ||
/* --- | /* --- Hauptlogik: Freeze + Pinning bis Stabilität, dann finaler Feinsprung --- */ | ||
function smartJump(id) { | async function smartJump(id) { | ||
var | var j = jumpTo(id, 'auto'); if (!j) return; | ||
var tabber = j.panel.closest('.tabber'); | |||
var tabber = panel.closest('.tabber'); | |||
var unfreeze = tabber ? freezeTabber(tabber) : function(){}; | var unfreeze = tabber ? freezeTabber(tabber) : function(){}; | ||
var stop = startPin(j.y); | |||
// | // solange DOM wackelt, gepinnt lassen | ||
await waitStable(j.panel); | |||
// final: Pin lösen, Freeze lösen, feiner Smooth-Jump | |||
stop(); unfreeze(); | |||
jumpTo(id, 'smooth'); | |||
} | } | ||
/* | /* --- Hash beim Laden --- */ | ||
if (/^#tabber-/.test(location.hash)) | if (/^#tabber-/.test(location.hash)) setTimeout(function(){ smartJump(location.hash); }, 0); | ||
/* --- | /* --- Hashwechsel --- */ | ||
window.addEventListener('hashchange', function () { | window.addEventListener('hashchange', function () { | ||
if (/^#tabber-/.test(location.hash)) smartJump(location.hash); | if (/^#tabber-/.test(location.hash)) smartJump(location.hash); | ||
}); | }); | ||
/* | /* --- Klicks auf Tabs (Capture, ohne preventDefault) --- */ | ||
document.addEventListener('click', function (e) { | document.addEventListener('click', function (e) { | ||
var a = e.target && e.target.closest && e.target.closest('a[href^="#tabber-"], button[aria-controls^="tabber-"]'); | var a = e.target && e.target.closest && e.target.closest('a[href^="#tabber-"], button[aria-controls^="tabber-"]'); | ||
if (!a) return; | if (!a) return; | ||
var id = a.getAttribute('href') || ('#' + a.getAttribute('aria-controls')); | var id = a.getAttribute('href') || ('#' + a.getAttribute('aria-controls')); | ||
smartJump(id); | smartJump(id); | ||
}, true); | }, true); | ||
})(); | })(); | ||
Version vom 30. Oktober 2025, 03:42 Uhr
/* Das folgende JavaScript wird für alle Benutzer geladen. */
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 & Drawer) ==== */
(function () {
'use strict';
if ( mw.config.get('skin') !== 'vector-2022' ) return;
/* ---------- Persistenz ---------- */
var STORAGE_KEY = 'kr-sb-state:v3:' + ( mw.config.get('wgUserName') || 'anon' );
function loadState() {
try { return JSON.parse( localStorage.getItem(STORAGE_KEY) || '{}' ); }
catch (e) { return {}; }
}
function saveState(state) {
try { localStorage.setItem( STORAGE_KEY, JSON.stringify(state) ); }
catch (e) {}
}
var STATE = loadState();
/* ---------- Utils ---------- */
function extractDirective(a) {
if (!a) return null;
var href = a.getAttribute('href') || '';
try { href = new URL(href, location.href).hash || ''; } catch (e) {}
var s = (href || '').trim().toLowerCase();
if (s.includes('#group:')) return { kind: 'group', value: decodeURIComponent(s.split('#group:').pop()) };
if (s.includes('#link:')) return { kind: 'link', value: decodeURIComponent(s.split('#link:').pop()) };
return null;
}
function stripDirectiveHash(a) {
if (!a) return;
a.setAttribute('href', (a.getAttribute('href') || '').replace(/#(?:link|group):[^#]*$/i, '') );
}
function hasRealHref(a) {
if (!a) return false;
return !/^#/.test( a.getAttribute('href') || '' );
}
function normId(s) {
return String(s || '').trim().toLowerCase()
.replace(/\s+/g, '-').replace(/[^\w\-:.|]/g, '');
}
function toggleSet(li, arrow, expanded) {
if (expanded) { li.classList.remove('is-collapsed'); arrow.setAttribute('aria-expanded','true'); }
else { li.classList.add('is-collapsed'); arrow.setAttribute('aria-expanded','false'); }
}
/* ---------- Kopf + ID ---------- */
function makeHead(li, keepLink, containerUL, labelOverride) {
var a = li.querySelector(':scope > a');
var head = document.createElement('div');
head.className = 'kr-head has-toggle'; // ganze Kopfzeile darf toggeln (außer Link)
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'kr-arrow';
btn.setAttribute('aria-expanded', 'false');
head.appendChild(btn);
if (keepLink && a) { stripDirectiveHash(a); head.appendChild(a); }
else {
var span = document.createElement('span');
span.className = 'kr-title';
span.textContent = labelOverride || (a && a.textContent) || '';
head.appendChild(span);
if (a) a.remove();
}
li.insertBefore(head, li.firstChild);
// stabile ID für Persistenz
var portlet = containerUL.closest('.vector-menu');
var pid = (portlet && (portlet.id || portlet.getAttribute('id'))) || 'portlet';
var labelText = (labelOverride || (head.querySelector('a, .kr-title') || {}).textContent || '').trim();
li.dataset.krId = normId( pid + '::' + labelText );
// Default zu; gespeicherten Zustand anwenden
toggleSet(li, btn, false);
var st = STATE[ li.dataset.krId ];
if (st === 1) toggleSet(li, btn, true);
return head;
}
/* ---------- Toggle-Handler ---------- */
function handleArrowClick(btn) {
var li = btn.closest('li.kr-group, li.kr-top');
if (!li) return;
var willExpand = li.classList.contains('is-collapsed');
toggleSet(li, btn, willExpand);
var id = li.dataset.krId;
if (id) {
STATE[id] = willExpand ? 1 : 0;
saveState(STATE);
}
}
/* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */
document.addEventListener('click', function (e) {
var arrow = e.target && e.target.closest && e.target.closest('.kr-arrow');
if (arrow) {
e.preventDefault();
handleArrowClick(arrow);
return;
}
var head = e.target && e.target.closest && e.target.closest('.kr-head');
if (head && !e.target.closest('.kr-head a')) {
e.preventDefault(); // freie Fläche der Kopfzeile
var btn = head.querySelector('.kr-arrow');
if (btn) handleArrowClick(btn);
}
}, true); // capture=true, damit nichts dazwischenfunkt
/* ---------- Aufbau der Gruppen ---------- */
function buildGroups(ul) {
var lis = Array.from( ul.querySelectorAll(':scope > li.mw-list-item') );
var current = null;
lis.forEach(function (li) {
var a = li.querySelector(':scope > a');
var d = extractDirective(a);
if (d && d.kind === 'group') {
var clickable = hasRealHref(a);
li.classList.add('kr-group');
makeHead(li, clickable, ul, (a && a.textContent) || d.value || '');
if (!clickable && a) a.remove(); else if (clickable) stripDirectiveHash(a);
var sub = document.createElement('ul');
sub.className = 'kr-sub';
li.appendChild(sub);
current = sub;
return;
}
if (current && !(d && (d.kind === 'group' || d.kind === 'link'))) {
current.appendChild(li);
}
});
}
/* ---------- Portlets verarbeiten ---------- */
function buildPortlets(root) {
var lists = root.querySelectorAll('.vector-menu .vector-menu-content-list');
lists.forEach(function (ul) {
if (ul.dataset.krSbDone) return;
ul.dataset.krSbDone = '1';
var items = Array.from( ul.querySelectorAll(':scope > li.mw-list-item') );
var topIdx = items.findIndex(function (li) {
var a = li.querySelector(':scope > a');
var d = extractDirective(a);
return d && d.kind === 'link';
});
if (topIdx >= 0) {
var top = items[topIdx];
top.classList.add('kr-top');
makeHead(top, true, ul);
stripDirectiveHash( top.querySelector(':scope > a') );
var sub = document.createElement('ul');
sub.className = 'kr-sub';
top.appendChild(sub);
for (var i = topIdx + 1; i < items.length; i++) sub.appendChild(items[i]);
buildGroups(sub);
} else {
buildGroups(ul);
}
// Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden
ul.querySelectorAll('a[href*="#link:"], a[href*="#group:"]').forEach(stripDirectiveHash);
var portlet = ul.closest('.vector-menu');
var heading = portlet && portlet.querySelector(':scope > .vector-menu-heading');
if (heading) heading.remove();
});
}
/* ---------- Bootstrapping ---------- */
function processRoot(root) {
if (!root || root.dataset.krSbRootDone) return;
root.dataset.krSbRootDone = '1';
root.classList.add('kr-sb');
buildPortlets(root);
}
function init() {
document.querySelectorAll('#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer')
.forEach(processRoot);
}
init();
mw.hook('wikipage.content').add(init);
// Drawer/DOM-Änderungen beobachten
var mo = new MutationObserver(function (muts) {
muts.forEach(function (m) {
m.addedNodes && m.addedNodes.forEach(function (n) {
if (!(n instanceof Element)) return;
if (n.matches('#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel')) processRoot(n);
n.querySelectorAll && n.querySelectorAll('#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel')
.forEach(processRoot);
});
});
});
mo.observe(document.body, { childList: true, subtree: true });
})();
/* TabberNeue: Anker-Scroll unter Vector-Header + Freeze + Pinning */
(function () {
'use strict';
/* --- Sticky-Offset (+0.5rem Luft) --- */
function stickyOffsetPx() {
var hdr = document.querySelector('.vector-sticky-header-container');
var h = hdr ? Math.round(hdr.getBoundingClientRect().height) : 56;
var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);
return h + air;
}
/* --- oberste Tabber-Leiste zu einem Panel --- */
function headerForPanel(panel) {
if (!panel) return null;
var top = panel.closest('.tabber');
for (var cur = top; cur && cur.parentElement; ) {
var up = cur.parentElement.closest('.tabber');
if (!up) break;
top = up; cur = up;
}
return top ? top.querySelector(':scope > .tabber__header') : null;
}
/* --- hartes Pinning: hält ScrollY auf gewünschtem Wert --- */
function startPin(targetY) {
var on = true;
function lock() {
if (!on) return;
if (Math.abs(window.scrollY - targetY) > 1) window.scrollTo(0, targetY);
requestAnimationFrame(lock);
}
document.documentElement.classList.add('fs-scroll-now');
requestAnimationFrame(lock);
return function stopPin(){ on = false; document.documentElement.classList.remove('fs-scroll-now'); };
}
/* --- Freeze: min-height stabil + Scroll-Anchor neutralisieren --- */
function freezeTabber(tabber) {
var section = tabber && tabber.querySelector(':scope > .tabber__section');
if (!section) return function(){};
var h = Math.max(0, Math.round(section.getBoundingClientRect().height));
tabber.style.setProperty('--tabber-freeze-h', h + 'px');
tabber.classList.add('tabber-is-switching');
var oldOA = section.style.overflowAnchor;
section.style.overflowAnchor = 'none';
return function unfreeze(){
tabber.classList.remove('tabber-is-switching');
tabber.style.removeProperty('--tabber-freeze-h');
section.style.overflowAnchor = oldOA || '';
};
}
/* --- Warte bis Panel „ruhig“ ist (keine Mutationen + Höhe stabil) --- */
function waitStable(panel) {
var section = panel && panel.closest('.tabber')?.querySelector(':scope > .tabber__section');
if (!section) return Promise.resolve();
var lastH = Math.round(section.getBoundingClientRect().height);
var stableFrames = 0, lastMutation = performance.now();
var mo = new MutationObserver(function(){ lastMutation = performance.now(); });
mo.observe(section, {subtree:true, childList:true, attributes:true, characterData:true});
return new Promise(function(resolve){
(function loop(){
var h = Math.round(section.getBoundingClientRect().height);
stableFrames = (Math.abs(h - lastH) < 1) ? (stableFrames + 1) : 0;
lastH = h;
var quiet = (performance.now() - lastMutation) > 300; // 300ms ohne Mutation
if (stableFrames >= 12 && quiet) { // ~200ms höhenstabil
mo.disconnect(); resolve(); return;
}
requestAnimationFrame(loop);
})();
});
}
/* --- Tabber internes scrollIntoView neutralisieren --- */
var _siv = Element.prototype.scrollIntoView;
Element.prototype.scrollIntoView = function(arg){
try{ if (this.closest && this.closest('.tabber')) return; }catch(e){}
return _siv.call(this, arg);
};
/* --- Zielposition berechnen + springen --- */
function jumpTo(id, behavior) {
var name = (id || '').replace(/^#/, '');
if (!/^tabber-/.test(name)) return;
var panel = document.getElementById(name); if (!panel) return;
var head = headerForPanel(panel) || panel;
var y = head.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();
window.scrollTo({ top: y, behavior: behavior || 'auto' });
return { panel: panel, y: y };
}
/* --- Hauptlogik: Freeze + Pinning bis Stabilität, dann finaler Feinsprung --- */
async function smartJump(id) {
var j = jumpTo(id, 'auto'); if (!j) return;
var tabber = j.panel.closest('.tabber');
var unfreeze = tabber ? freezeTabber(tabber) : function(){};
var stop = startPin(j.y);
// solange DOM wackelt, gepinnt lassen
await waitStable(j.panel);
// final: Pin lösen, Freeze lösen, feiner Smooth-Jump
stop(); unfreeze();
jumpTo(id, 'smooth');
}
/* --- Hash beim Laden --- */
if (/^#tabber-/.test(location.hash)) setTimeout(function(){ smartJump(location.hash); }, 0);
/* --- Hashwechsel --- */
window.addEventListener('hashchange', function () {
if (/^#tabber-/.test(location.hash)) smartJump(location.hash);
});
/* --- Klicks auf Tabs (Capture, ohne preventDefault) --- */
document.addEventListener('click', function (e) {
var a = e.target && e.target.closest && e.target.closest('a[href^="#tabber-"], button[aria-controls^="tabber-"]');
if (!a) return;
var id = a.getAttribute('href') || ('#' + a.getAttribute('aria-controls'));
smartJump(id);
}, true);
})();