Zum Inhalt springen

MediaWiki:Gadget-AufEinWortSeiteSitzung.js

aus AufEinWort, der freien Forschung

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);

  });
})();