MediaWiki:Common.js: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung |
Keine Bearbeitungszusammenfassung |
||
| (15 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
| Zeile 2: | Zeile 2: | ||
/* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 & Drawer) ==== */ | /* ==== Sidebar: klickbare Kopfzeile + Gruppen mit Auf/Zu + Persistenz (Vector-2022 & Drawer) ==== */ | ||
(function () { | (function () { | ||
if (mw.config.get('skin') !== 'vector-2022') return; | 'use strict'; | ||
if ( mw.config.get('skin') !== 'vector-2022' ) return; | |||
var STORAGE_KEY = 'kr-sb-state:v3:' + (mw.config.get('wgUserName') || 'anon'); | /* ---------- 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(); | var STATE = loadState(); | ||
/* ---------- Utils ---------- */ | |||
function extractDirective(a) { | |||
function extractDirective(a){ | |||
if (!a) return null; | if (!a) return null; | ||
var href = a.getAttribute('href') || ''; | var href = a.getAttribute('href') || ''; | ||
try { href = new URL(href, location.href).hash || ''; } catch(e){} | try { href = new URL(href, location.href).hash || ''; } catch (e) {} | ||
var s = (href || '').trim().toLowerCase(); | var s = (href || '').trim().toLowerCase(); | ||
if (s.includes('#group:')) return { kind:'group', value: decodeURIComponent(s.split('#group:').pop()) }; | 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()) }; | if (s.includes('#link:')) return { kind: 'link', value: decodeURIComponent(s.split('#link:').pop()) }; | ||
return null; | return null; | ||
} | } | ||
function stripDirectiveHash(a){ | function stripDirectiveHash(a) { | ||
if (!a) return; | if (!a) return; | ||
a.setAttribute('href', (a.getAttribute('href') || '').replace(/#(?:link|group):[^#]*$/i, '')); | a.setAttribute('href', (a.getAttribute('href') || '').replace(/#(?:link|group):[^#]*$/i, '') ); | ||
} | } | ||
function hasRealHref(a){ | function hasRealHref(a) { | ||
if (!a) return false; | if (!a) return false; | ||
return !/^#/.test( a.getAttribute('href') || '' ); | |||
return | } | ||
function normId(s) { | |||
return String(s || '').trim().toLowerCase() | |||
.replace(/\s+/g, '-').replace(/[^\w\-:.|]/g, ''); | |||
} | } | ||
function toggleSet(li, arrow, expanded) { | |||
function toggleSet(li, arrow, expanded){ | if (expanded) { li.classList.remove('is-collapsed'); arrow.setAttribute('aria-expanded','true'); } | ||
if (expanded){ li.classList.remove('is-collapsed'); arrow.setAttribute('aria-expanded','true'); } | else { li.classList.add('is-collapsed'); arrow.setAttribute('aria-expanded','false'); } | ||
else { li.classList.add('is-collapsed'); arrow.setAttribute('aria-expanded','false'); } | |||
} | } | ||
function makeHead(li, keepLink, containerUL, labelOverride){ | /* ---------- Kopf + ID ---------- */ | ||
function makeHead(li, keepLink, containerUL, labelOverride) { | |||
var a = li.querySelector(':scope > a'); | var a = li.querySelector(':scope > a'); | ||
var head = document.createElement('div'); head.className = 'kr-head'; | |||
var btn | 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); | head.appendChild(btn); | ||
if (keepLink && a){ stripDirectiveHash(a); head.appendChild(a); } | if (keepLink && a) { stripDirectiveHash(a); head.appendChild(a); } | ||
else { | else { | ||
var span = document.createElement('span'); span.className='kr-title'; | var span = document.createElement('span'); | ||
span.className = 'kr-title'; | |||
span.textContent = labelOverride || (a && a.textContent) || ''; | span.textContent = labelOverride || (a && a.textContent) || ''; | ||
head.appendChild(span); if (a) a.remove(); | head.appendChild(span); | ||
if (a) a.remove(); | |||
} | } | ||
li.insertBefore(head, li.firstChild); | li.insertBefore(head, li.firstChild); | ||
// stabile ID für Persistenz | |||
var portlet = containerUL.closest('.vector-menu'); | var portlet = containerUL.closest('.vector-menu'); | ||
var pid = (portlet && (portlet.id || portlet.getAttribute('id'))) || 'portlet'; | var pid = (portlet && (portlet.id || portlet.getAttribute('id'))) || 'portlet'; | ||
var labelText = (labelOverride || (head.querySelector('a, .kr-title') || {}).textContent || '').trim(); | var labelText = (labelOverride || (head.querySelector('a, .kr-title') || {}).textContent || '').trim(); | ||
li.dataset.krId = normId(pid + '::' + labelText); | li.dataset.krId = normId( pid + '::' + labelText ); | ||
// Default | // Default zu; gespeicherten Zustand anwenden | ||
toggleSet(li, btn, false); | toggleSet(li, btn, false); | ||
var st = STATE[li.dataset.krId]; | var st = STATE[ li.dataset.krId ]; | ||
if (st === 1) toggleSet(li, btn, true); | if (st === 1) toggleSet(li, btn, true); | ||
| Zeile 62: | Zeile 82: | ||
} | } | ||
function handleArrowClick(btn){ | /* ---------- Toggle-Handler ---------- */ | ||
var li = btn.closest('li.kr-group, li.kr-top'); if (!li) return; | function handleArrowClick(btn) { | ||
var li = btn.closest('li.kr-group, li.kr-top'); | |||
if (!li) return; | |||
var willExpand = li.classList.contains('is-collapsed'); | var willExpand = li.classList.contains('is-collapsed'); | ||
toggleSet(li, btn, willExpand); | toggleSet(li, btn, willExpand); | ||
var id = li.dataset.krId; | var id = li.dataset.krId; | ||
if (id){ | if (id) { | ||
STATE[id] = willExpand ? 1 : 0; | STATE[id] = willExpand ? 1 : 0; | ||
saveState(); | saveState(STATE); | ||
} | } | ||
} | } | ||
// | /* Globale Delegation: Pfeil klickt immer; Kopfzeile klickt nur, wenn NICHT im Link */ | ||
document.addEventListener('click', function(e){ | document.addEventListener('click', function (e) { | ||
var | var arrow = e.target && e.target.closest && e.target.closest('.kr-arrow'); | ||
if ( | if (arrow) { | ||
e.preventDefault(); | e.preventDefault(); | ||
handleArrowClick(arrow); | |||
}, true); | 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 | |||
function buildGroups(ul){ | /* ---------- Aufbau der Gruppen ---------- */ | ||
var lis = Array.from(ul.querySelectorAll(':scope > li.mw-list-item')); | function buildGroups(ul) { | ||
var lis = Array.from( ul.querySelectorAll(':scope > li.mw-list-item') ); | |||
var current = null; | var current = null; | ||
lis.forEach(function (li){ | lis.forEach(function (li) { | ||
var a = li.querySelector(':scope > a'); | var a = li.querySelector(':scope > a'); | ||
var d = extractDirective(a); | var d = extractDirective(a); | ||
if (d && d.kind === 'group'){ | if (d && d.kind === 'group') { | ||
var clickable = hasRealHref(a); | var clickable = hasRealHref(a); | ||
li.classList.add('kr-group'); | li.classList.add('kr-group'); | ||
| Zeile 96: | Zeile 128: | ||
if (!clickable && a) a.remove(); else if (clickable) stripDirectiveHash(a); | if (!clickable && a) a.remove(); else if (clickable) stripDirectiveHash(a); | ||
var sub = document.createElement('ul'); sub.className='kr-sub'; li.appendChild(sub); | var sub = document.createElement('ul'); | ||
sub.className = 'kr-sub'; | |||
li.appendChild(sub); | |||
current = sub; | current = sub; | ||
return; | return; | ||
} | } | ||
if (current && !(d && (d.kind === 'group' || d.kind === 'link'))) current.appendChild(li); | if (current && !(d && (d.kind === 'group' || d.kind === 'link'))) { | ||
current.appendChild(li); | |||
} | |||
}); | }); | ||
} | } | ||
function buildPortlets(root){ | /* ---------- Portlets verarbeiten ---------- */ | ||
function buildPortlets(root) { | |||
var lists = root.querySelectorAll('.vector-menu .vector-menu-content-list'); | var lists = root.querySelectorAll('.vector-menu .vector-menu-content-list'); | ||
lists.forEach(function (ul){ | lists.forEach(function (ul) { | ||
if (ul.dataset.krSbDone) return; | if (ul.dataset.krSbDone) return; | ||
ul.dataset.krSbDone = '1'; | ul.dataset.krSbDone = '1'; | ||
var items = Array.from(ul.querySelectorAll(':scope > li.mw-list-item')); | var items = Array.from( ul.querySelectorAll(':scope > li.mw-list-item') ); | ||
var topIdx = items.findIndex(function(li){ | var topIdx = items.findIndex(function (li) { | ||
var a = li.querySelector(':scope > a'); var d = extractDirective(a); | var a = li.querySelector(':scope > a'); | ||
var d = extractDirective(a); | |||
return d && d.kind === 'link'; | return d && d.kind === 'link'; | ||
}); | }); | ||
if (topIdx >= 0){ | if (topIdx >= 0) { | ||
var top = items[topIdx]; | var top = items[topIdx]; | ||
top.classList.add('kr-top'); | top.classList.add('kr-top'); | ||
makeHead(top, true, ul); | makeHead(top, true, ul); | ||
stripDirectiveHash(top.querySelector(':scope > a')); | 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]); | for (var i = topIdx + 1; i < items.length; i++) sub.appendChild(items[i]); | ||
buildGroups(sub); | buildGroups(sub); | ||
| Zeile 130: | Zeile 171: | ||
} | } | ||
// | // Direktiven aus hrefs entfernen; Vector-Portlet-Heading ausblenden | ||
ul.querySelectorAll('a[href*="#link:"], a[href*="#group:"]').forEach(stripDirectiveHash); | ul.querySelectorAll('a[href*="#link:"], a[href*="#group:"]').forEach(stripDirectiveHash); | ||
var portlet = ul.closest('.vector-menu'); | var portlet = ul.closest('.vector-menu'); | ||
| Zeile 138: | Zeile 179: | ||
} | } | ||
function processRoot(root){ | /* ---------- Bootstrapping ---------- */ | ||
function processRoot(root) { | |||
if (!root || root.dataset.krSbRootDone) return; | if (!root || root.dataset.krSbRootDone) return; | ||
root.dataset.krSbRootDone = '1'; | root.dataset.krSbRootDone = '1'; | ||
| Zeile 145: | Zeile 187: | ||
} | } | ||
function init(){ | function init() { | ||
document.querySelectorAll('#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer') | document.querySelectorAll('#mw-panel, #vector-main-menu, #vector-main-menu-pinned, .vector-drawer') | ||
.forEach(processRoot); | .forEach(processRoot); | ||
| Zeile 153: | Zeile 195: | ||
mw.hook('wikipage.content').add(init); | mw.hook('wikipage.content').add(init); | ||
// Drawer | // Drawer/DOM-Änderungen beobachten | ||
var mo = new MutationObserver(function(muts){ | var mo = new MutationObserver(function (muts) { | ||
muts.forEach(function(m){ | muts.forEach(function (m) { | ||
m.addedNodes && m.addedNodes.forEach(function(n){ | m.addedNodes && m.addedNodes.forEach(function (n) { | ||
if (!(n instanceof Element)) return; | if (!(n instanceof Element)) return; | ||
if (n.matches('#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel')) processRoot(n); | if (n.matches('#vector-main-menu, #vector-main-menu-pinned, .vector-drawer, #mw-panel')) processRoot(n); | ||
| Zeile 164: | Zeile 206: | ||
}); | }); | ||
}); | }); | ||
mo.observe(document.body, { childList:true, subtree:true }); | mo.observe(document.body, { childList: true, subtree: true }); | ||
})(); | |||
/* TabberNeue: Anker-Scroll unter Vector-Header + robustes „Pinning“ */ | |||
(function () { | |||
/* reale Sticky-Höhe (+0.5rem Luft) – korrekt für Gäste */ | |||
function stickyOffsetPx() { | |||
var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5); | |||
// eingeloggt zuverlässig erkennen (Logout-Link existiert nur dann) | |||
var isLoggedIn = !!document.getElementById('pt-logout'); | |||
// Vector 2022 Header-Container | |||
var hdr = document.querySelector('.vector-sticky-header-container, #vector-sticky-header'); | |||
if (!isLoggedIn || !hdr) return air; // Gäste: nur Luft | |||
// nur zählen, wenn der Header wirklich sichtbar/aktiv ist | |||
var cs = getComputedStyle(hdr); | |||
var op = parseFloat(cs.opacity || '1'); | |||
var r = hdr.getBoundingClientRect(); | |||
var visible = op > 0.01 && r.top <= 0 && r.bottom > 0; | |||
var h = visible ? Math.round(r.height) : 0; | |||
if (!h && isLoggedIn) h = 50; // letzte Rettung nur für eingeloggte | |||
return h + air; | |||
} | |||
/* oberste Tabber-Leiste finden */ | |||
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; | |||
} | |||
/* Panel/Tabber, die wir beobachten wollen */ | |||
function getObserved(tabber) { | |||
var section = tabber && tabber.querySelector(':scope > .tabber__section'); | |||
return { tabber: tabber, section: section }; | |||
} | |||
/* „Pinne“ den Viewport hart auf header/panel; löse erst, wenn stabil */ | |||
function pinScrollToTarget(panelOrHeader) { | |||
var target = panelOrHeader; | |||
if (!target) return; | |||
var tabber = target.closest('.tabber'); | |||
var obs = getObserved(tabber); | |||
/* Freeze: Höhe zum Start lesen und auf dem Container festhalten */ | |||
if (tabber && obs.section) { | |||
var h0 = Math.ceil(obs.section.getBoundingClientRect().height); | |||
tabber.style.setProperty('--tabber-freeze-h', h0 + 'px'); | |||
tabber.classList.add('tabber-is-switching'); | |||
} | |||
/* Smooth-Scrolling global aus, damit nichts „nachfedert“ */ | |||
document.documentElement.classList.add('fs-scroll-now'); | |||
/* Fremde scrollIntoView()-Aufrufe währenddessen blocken */ | |||
var origScrollIntoView = Element.prototype.scrollIntoView; | |||
Element.prototype.scrollIntoView = function(){ /* noop while pinned */ }; | |||
/* Beobachte Größenänderungen → Stability Counter zurücksetzen */ | |||
var stable = 0, lastH = -1, raf = 0; | |||
var ro = null; | |||
if (obs.section && window.ResizeObserver) { | |||
ro = new ResizeObserver(function(){ stable = 0; }); | |||
try { ro.observe(obs.section); } catch(e){} | |||
} | |||
var MAX_MS = 3000; | |||
var t0 = performance.now(); | |||
function wantY() { | |||
return target.getBoundingClientRect().top + window.scrollY - stickyOffsetPx(); | |||
} | |||
function release() { | |||
if (ro) ro.disconnect(); | |||
Element.prototype.scrollIntoView = origScrollIntoView; | |||
document.documentElement.classList.remove('fs-scroll-now'); | |||
if (tabber) { | |||
tabber.classList.remove('tabber-is-switching'); | |||
tabber.style.removeProperty('--tabber-freeze-h'); | |||
} | |||
cancelAnimationFrame(raf); | |||
} | |||
function tick() { | |||
var y = wantY(); | |||
if (Math.abs(window.scrollY - y) > 1) { | |||
/* Hart auf die Ziel-Position setzen */ | |||
window.scrollTo(0, y); | |||
stable = 0; /* wir mussten korrigieren → noch nicht stabil */ | |||
} | |||
/* aktuelles Maß nehmen (Section bevorzugt, sonst Tabber) */ | |||
var hNow = 0; | |||
if (obs.section) hNow = obs.section.getBoundingClientRect().height; | |||
else if (tabber) hNow = tabber.getBoundingClientRect().height; | |||
if (Math.abs(hNow - lastH) < 1 && Math.abs(window.scrollY - y) <= 1) { | |||
stable++; | |||
} else { | |||
stable = 0; | |||
lastH = hNow; | |||
} | |||
/* 12 stabile Frames ≈ ~200ms bei 60fps */ | |||
if (stable >= 12 || (performance.now() - t0) > MAX_MS) { | |||
release(); | |||
return; | |||
} | |||
raf = requestAnimationFrame(tick); | |||
} | |||
/* sofort starten */ | |||
raf = requestAnimationFrame(tick); | |||
} | |||
function scrollToTab(hashOrId) { | |||
var name = (hashOrId || '').replace(/^#/, ''); | |||
if (!/^tabber-/.test(name)) return; | |||
var panel = document.getElementById(name); | |||
if (!panel) return; | |||
var header = headerForPanel(panel) || panel; | |||
pinScrollToTarget(header); | |||
} | |||
/* 1) Direkt mit Hash geladen */ | |||
if (/^#tabber-/.test(location.hash)) { | |||
setTimeout(function(){ scrollToTab(location.hash); }, 60); | |||
} | |||
/* 2) Hashwechsel */ | |||
window.addEventListener('hashchange', function () { | |||
if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash); | |||
}); | |||
/* 3) Klicks (TabberNeue nutzt History API, verhindert default) */ | |||
document.addEventListener('click', function (e) { | |||
var a = e.target && e.target.closest('a[href^="#tabber-"], button[aria-controls^="tabber-"]'); | |||
if (!a) return; | |||
var id = a.getAttribute('href') || ('#' + a.getAttribute('aria-controls')); | |||
setTimeout(function(){ scrollToTab(id); }, 60); | |||
}, true); | |||
})(); | })(); | ||
Aktuelle Version vom 30. Oktober 2025, 12:08 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 + robustes „Pinning“ */
(function () {
/* reale Sticky-Höhe (+0.5rem Luft) – korrekt für Gäste */
function stickyOffsetPx() {
var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);
// eingeloggt zuverlässig erkennen (Logout-Link existiert nur dann)
var isLoggedIn = !!document.getElementById('pt-logout');
// Vector 2022 Header-Container
var hdr = document.querySelector('.vector-sticky-header-container, #vector-sticky-header');
if (!isLoggedIn || !hdr) return air; // Gäste: nur Luft
// nur zählen, wenn der Header wirklich sichtbar/aktiv ist
var cs = getComputedStyle(hdr);
var op = parseFloat(cs.opacity || '1');
var r = hdr.getBoundingClientRect();
var visible = op > 0.01 && r.top <= 0 && r.bottom > 0;
var h = visible ? Math.round(r.height) : 0;
if (!h && isLoggedIn) h = 50; // letzte Rettung nur für eingeloggte
return h + air;
}
/* oberste Tabber-Leiste finden */
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;
}
/* Panel/Tabber, die wir beobachten wollen */
function getObserved(tabber) {
var section = tabber && tabber.querySelector(':scope > .tabber__section');
return { tabber: tabber, section: section };
}
/* „Pinne“ den Viewport hart auf header/panel; löse erst, wenn stabil */
function pinScrollToTarget(panelOrHeader) {
var target = panelOrHeader;
if (!target) return;
var tabber = target.closest('.tabber');
var obs = getObserved(tabber);
/* Freeze: Höhe zum Start lesen und auf dem Container festhalten */
if (tabber && obs.section) {
var h0 = Math.ceil(obs.section.getBoundingClientRect().height);
tabber.style.setProperty('--tabber-freeze-h', h0 + 'px');
tabber.classList.add('tabber-is-switching');
}
/* Smooth-Scrolling global aus, damit nichts „nachfedert“ */
document.documentElement.classList.add('fs-scroll-now');
/* Fremde scrollIntoView()-Aufrufe währenddessen blocken */
var origScrollIntoView = Element.prototype.scrollIntoView;
Element.prototype.scrollIntoView = function(){ /* noop while pinned */ };
/* Beobachte Größenänderungen → Stability Counter zurücksetzen */
var stable = 0, lastH = -1, raf = 0;
var ro = null;
if (obs.section && window.ResizeObserver) {
ro = new ResizeObserver(function(){ stable = 0; });
try { ro.observe(obs.section); } catch(e){}
}
var MAX_MS = 3000;
var t0 = performance.now();
function wantY() {
return target.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();
}
function release() {
if (ro) ro.disconnect();
Element.prototype.scrollIntoView = origScrollIntoView;
document.documentElement.classList.remove('fs-scroll-now');
if (tabber) {
tabber.classList.remove('tabber-is-switching');
tabber.style.removeProperty('--tabber-freeze-h');
}
cancelAnimationFrame(raf);
}
function tick() {
var y = wantY();
if (Math.abs(window.scrollY - y) > 1) {
/* Hart auf die Ziel-Position setzen */
window.scrollTo(0, y);
stable = 0; /* wir mussten korrigieren → noch nicht stabil */
}
/* aktuelles Maß nehmen (Section bevorzugt, sonst Tabber) */
var hNow = 0;
if (obs.section) hNow = obs.section.getBoundingClientRect().height;
else if (tabber) hNow = tabber.getBoundingClientRect().height;
if (Math.abs(hNow - lastH) < 1 && Math.abs(window.scrollY - y) <= 1) {
stable++;
} else {
stable = 0;
lastH = hNow;
}
/* 12 stabile Frames ≈ ~200ms bei 60fps */
if (stable >= 12 || (performance.now() - t0) > MAX_MS) {
release();
return;
}
raf = requestAnimationFrame(tick);
}
/* sofort starten */
raf = requestAnimationFrame(tick);
}
function scrollToTab(hashOrId) {
var name = (hashOrId || '').replace(/^#/, '');
if (!/^tabber-/.test(name)) return;
var panel = document.getElementById(name);
if (!panel) return;
var header = headerForPanel(panel) || panel;
pinScrollToTarget(header);
}
/* 1) Direkt mit Hash geladen */
if (/^#tabber-/.test(location.hash)) {
setTimeout(function(){ scrollToTab(location.hash); }, 60);
}
/* 2) Hashwechsel */
window.addEventListener('hashchange', function () {
if (/^#tabber-/.test(location.hash)) scrollToTab(location.hash);
});
/* 3) Klicks (TabberNeue nutzt History API, verhindert default) */
document.addEventListener('click', function (e) {
var a = e.target && e.target.closest('a[href^="#tabber-"], button[aria-controls^="tabber-"]');
if (!a) return;
var id = a.getAttribute('href') || ('#' + a.getAttribute('aria-controls'));
setTimeout(function(){ scrollToTab(id); }, 60);
}, true);
})();