MediaWiki:Gadget-AufEinWortSeiteSitzung.js
Erscheinungsbild
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/*
* HxKipperSeite – vereintes Gadget für einklappbare Überschriften + Seitenzustand
* MediaWiki 1.44+, Vector-2022 (skin-neutral)
* - Funktioniert in allen Namensräumen
* - Unterstützt h1–h3 (CONFIG.levels, leicht auf h6 erweiterbar)
* - Merkt pro Überschrift (id/Text+Level) den Zustand (offen/zu)
* - Merkt letzte Scroll-Position (pro Seite)
*
* WICHTIG (MW 1.44): Überschriften erscheinen teils als Wrapper .mw-headingN
* mit einem inneren <hN>. Für eine saubere Abschnittsgrenze muss der
* Traversal vom Wrapper-Block ausgehen, nicht vom inneren <hN> – sonst
* würde nur der Edit-Bereich getoggelt. Diese Datei behandelt das.
*/
(function () {
'use strict';
mw.loader.using(['mediawiki.util', 'mediawiki.storage']).then(function () {
// ===================== Konfiguration =====================
var CONFIG = {
levels: [1, 2, 3, 4, 5], // für h4–h6 einfach erweitern: [1,2,3,4,5,6]
defaultCollapsedGeneral: false, // normale Seiten: offen
defaultCollapsedCategories: false, // Kategorieseiten: offen
defaultCollapsedSpecialpages: true, // Spezial:Spezialseiten-Liste: zu
useClickOnHeading: true,
applyDelayMs: 250, // kleine Wartezeit für DOM-Stabilität
restoreWindowMs: 900 // Delay für Scroll-Restore nach Aufbau
};
// ===================== Storage-Keys =====================
function pageKey() {
var id = mw.config.get('wgArticleId');
var name = mw.config.get('wgPageName') || 'unknown';
return String((id != null ? id : '0') + '|' + name);
}
function keyState(id) { return 'aew:hx:v1:' + pageKey() + ':' + id; }
function keyScroll() { return 'aew:hx:scroll:v1:' + pageKey(); }
// ===================== Headings erkennen (H-Tags + Wrapper) =====================
function headingLevelFrom(el) {
if (!el || el.nodeType !== 1) return 99;
var m = /^H([1-6])$/i.exec(el.tagName);
if (m) return parseInt(m[1], 10);
for (var i = 1; i <= 6; i++) {
if (el.classList.contains('mw-heading' + i)) return i;
}
return 99;
}
function isHeadingElement(el) {
if (!el || el.nodeType !== 1) return false;
if (/^H[1-6]$/i.test(el.tagName)) return true;
for (var i = 1; i <= 6; i++) {
if (el.classList.contains('mw-heading' + i)) return true;
}
return false;
}
// Liefert den „Heading-Block“ (Wrapper, falls vorhanden) + das kanonische <hN>
function getHeadingBlock(el) {
var level = headingLevelFrom(el);
var block = el;
var canonical = el;
// Fall A: Wrapper .mw-headingN -> inneres <hN> als canonical; Block = Wrapper
if (el.classList && el.classList.contains('mw-heading' + level)) {
var inner = el.querySelector('h' + level);
if (inner) {
block = el;
canonical = inner;
}
}
// Fall B: echtes <hN>, aber Parent ist .mw-headingN -> Block = Parent
if (/^H[1-6]$/i.test(el.tagName)) {
var p = el.parentElement;
if (p && p.classList && p.classList.contains('mw-heading' + level)) {
block = p;
canonical = el;
}
}
return { block: block, canonical: canonical, level: level };
}
function extractHeadlineInfo(headingEl) {
var headline = headingEl.querySelector('.mw-headline');
var idPart = headline && headline.id ? headline.id : null;
var textPart = (headline ? headline.textContent : headingEl.textContent || '')
.trim().replace(/\s+/g, ' ').slice(0, 120);
return { idPart: idPart, textPart: textPart };
}
// ===================== Abschnitt sammeln =====================
// Wichtig: Start am BLOCK (Wrapper), nicht nur am inneren <hN>.
// Wir sammeln alle Geschwister des Blocks bis zur nächsten Überschrift gleicher/höherer Ebene.
function collectSectionNodesFromBlock(blockEl, level) {
var nodes = [];
for (var n = blockEl.nextSibling; n; n = n.nextSibling) {
if (n.nodeType === 1 && isHeadingElement(n)) {
var nxt = headingLevelFrom(n);
if (nxt <= level) break;
}
nodes.push(n);
}
return nodes;
}
// ===================== Toggle / UI =====================
function makeSectionCollapsible(blockEl, canonicalHeadingEl, contentNodes, storageKey, startCollapsed) {
if (!blockEl || !canonicalHeadingEl || !contentNodes || !contentNodes.length) return;
if (blockEl.classList.contains('aew-collapsible-bound')) return;
blockEl.classList.add('aew-collapsible-bound');
// Wrapper für Inhalt direkt NACH dem Block einsetzen
var wrapper = document.createElement('div');
wrapper.className = 'aew-collapsible-content';
var wrapperId = 'aew-collapsible-' + Math.random().toString(36).slice(2, 9);
wrapper.id = wrapperId;
contentNodes.forEach(function (n) { wrapper.appendChild(n); });
blockEl.parentNode.insertBefore(wrapper, blockEl.nextSibling);
// Toggle-Button erzeugen
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'aew-collapsible-toggle';
btn.setAttribute('aria-controls', wrapperId);
btn.setAttribute('aria-expanded', 'true');
btn.title = 'ein-/ausklappen';
btn.textContent = '⎇';
// Button möglichst vor den Edit-Links positionieren – innerhalb des BLOCKS
var edit = blockEl.querySelector('.mw-editsection');
if (edit) {
blockEl.insertBefore(btn, edit);
} else {
// falls kein Edit-Bereich: an das Ende des Canonical-Headings
canonicalHeadingEl.appendChild(btn);
}
function setState(collapsed) {
if (collapsed) {
wrapper.style.display = 'none';
btn.textContent = '⎇';
btn.setAttribute('aria-expanded', 'false');
mw.storage.set(storageKey, '1');
} else {
wrapper.style.display = '';
btn.textContent = '⌥';
btn.setAttribute('aria-expanded', 'true');
mw.storage.set(storageKey, '0');
}
}
setState(!!startCollapsed);
function toggle() {
var isOpen = wrapper.style.display !== 'none';
setState(isOpen);
var info = extractHeadlineInfo(canonicalHeadingEl);
if (info.idPart) {
try { localStorage.setItem(keyScroll(), JSON.stringify({ id: info.idPart, t: Date.now() })); } catch (e) {}
}
}
btn.addEventListener('click', function (e) { e.preventDefault(); toggle(); });
if (CONFIG.useClickOnHeading) {
canonicalHeadingEl.addEventListener('click', function (e) {
if (e.target === btn || e.target.closest('a, button, .mw-editsection')) return;
toggle();
});
}
}
// ===================== Kategorien & Spezialseiten =====================
function enhanceCategory(sectionId, defaultCollapsed) {
var sec = document.getElementById(sectionId);
if (!sec) return;
var block = sec.querySelector('.mw-heading2') || sec.querySelector('h2');
if (!block) return;
var hb = getHeadingBlock(block);
var nodes = collectSectionNodesFromBlock(hb.block, hb.level);
if (!nodes.length) return;
var key = 'aew:cat:' + mw.config.get('wgPageName') + ':' + sectionId;
var stored = mw.storage.get(key);
var startCollapsed = (stored === '1') || (stored === null && !!defaultCollapsed);
makeSectionCollapsible(hb.block, hb.canonical, nodes, key, startCollapsed);
}
function enhanceSpecialpages(defaultCollapsed) {
var groups = document.querySelectorAll('h2.mw-specialpagesgroup, .mw-body .mw-heading2.mw-specialpagesgroup');
if (!groups.length) return;
groups.forEach(function (el) {
var hb = getHeadingBlock(el);
var nodes = collectSectionNodesFromBlock(hb.block, hb.level);
if (!nodes.length) return;
var title = (hb.canonical.textContent || '').trim();
var key = 'aew:spc:' + mw.config.get('wgPageName') + ':' + title;
var stored = mw.storage.get(key);
var startCollapsed = (stored === '1') || (stored === null && !!defaultCollapsed);
makeSectionCollapsible(hb.block, hb.canonical, nodes, key, startCollapsed);
});
}
// ===================== Generischer Teil – alle Namensräume =====================
function enhanceGeneral() {
var root = document.getElementById('mw-content-text') || document.querySelector('.mw-body');
if (!root) return;
// breit sammeln
var sels = [];
CONFIG.levels.forEach(function (L) { sels.push('h' + L); sels.push('.mw-heading' + L); });
var raw = Array.from(root.querySelectorAll(sels.join(',')));
// deduplizieren & kanonisieren
var seen = new WeakSet();
var list = [];
raw.forEach(function (el) {
var hb = getHeadingBlock(el);
if (CONFIG.levels.indexOf(hb.level) === -1) return;
if (hb.canonical.id === 'firstHeading') return;
if (seen.has(hb.block)) return; // pro Block nur einmal
seen.add(hb.block);
list.push(hb);
});
var ns = mw.config.get('wgNamespaceNumber');
var canonicalSp = mw.config.get('wgCanonicalSpecialPageName');
var defaultCollapsed = (ns === 14) ? CONFIG.defaultCollapsedCategories :
((ns === -1 && canonicalSp === 'Specialpages') ? CONFIG.defaultCollapsedSpecialpages : CONFIG.defaultCollapsedGeneral);
list.forEach(function (hb) {
var nodes = collectSectionNodesFromBlock(hb.block, hb.level);
if (!nodes.length) return;
var info = extractHeadlineInfo(hb.canonical);
var key = 'aew:gen:' + mw.config.get('wgPageName') + ':' + (info.idPart || (info.textPart + '|L' + hb.level));
var stored = mw.storage.get(key);
var startCollapsed = (stored === '1') || (stored === null && !!defaultCollapsed);
makeSectionCollapsible(hb.block, hb.canonical, nodes, key, startCollapsed);
});
}
// ===================== Scroll-Wiederherstellung =====================
function restoreScrollPosition() {
var raw = null;
try { raw = localStorage.getItem(keyScroll()); } catch (e) { raw = null; }
if (!raw) return;
try {
var data = JSON.parse(raw);
if (!data || !data.id) return;
var el = document.getElementById(data.id);
if (!el) return;
setTimeout(function () { try { el.scrollIntoView({ behavior: 'auto', block: 'start' }); } catch (e) {} }, 300);
} catch (e) {}
}
// ===================== Laufzeitsteuerung =====================
function run() {
var action = mw.config.get('wgAction');
if (action && !/^(view|submit)$/.test(action)) return;
setTimeout(function () {
var ns = mw.config.get('wgNamespaceNumber');
var canonical = mw.config.get('wgCanonicalSpecialPageName');
if (ns === 14) {
enhanceCategory('mw-subcategories', CONFIG.defaultCollapsedCategories);
enhanceCategory('mw-pages', CONFIG.defaultCollapsedCategories);
} else if (ns === -1 && canonical === 'Specialpages') {
enhanceSpecialpages(CONFIG.defaultCollapsedSpecialpages);
}
enhanceGeneral();
setTimeout(restoreScrollPosition, CONFIG.restoreWindowMs);
}, CONFIG.applyDelayMs);
}
mw.hook('wikipage.content').add(run);
if (document.readyState !== 'loading') run(); else document.addEventListener('DOMContentLoaded', run);
});
})();