MediaWiki:Common.js: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung Markierung: Zurückgesetzt  | 
				Keine Bearbeitungszusammenfassung Markierung: Zurückgesetzt  | 
				||
| Zeile 209: | Zeile 209: | ||
})();  | })();  | ||
/* TabberNeue: Anker-  | /* TabberNeue: Anchor-Lock wie früher #TabberStart (ohne echtes Anker-Element) */  | ||
(function () {  | (function () {  | ||
   function stickyOffsetPx() {  |    function stickyOffsetPx() {  | ||
     var hdr = document.querySelector('.vector-sticky-header-container');  |      var hdr = document.querySelector('.vector-sticky-header-container');  | ||
| Zeile 219: | Zeile 218: | ||
   }  |    }  | ||
   function headerForPanel(panel) {  |    function headerForPanel(panel) {  | ||
     if (!panel) return null;  |      if (!panel) return null;  | ||
| Zeile 231: | Zeile 229: | ||
   }  |    }  | ||
   //   |    // Scroll an Y „anklammern“ und für ms Millisekunden festhalten  | ||
   function   |    function lockScroll(y, ms) {  | ||
     var   |      var until = performance.now() + (ms || 900);  | ||
     var active = true;  | |||
     function clamp() {  | |||
      if (!active) return;  | |||
      window.scrollTo(0, y);  | |||
      if (performance.now() < until) requestAnimationFrame(clamp);  | |||
     }  | |||
    function onScroll() {  | |||
      if (!active) return;  | |||
      if (Math.abs(window.scrollY - y) > 1) window.scrollTo(0, y);  | |||
    }  | |||
    window.addEventListener('scroll', onScroll, { passive: true });  | |||
    requestAnimationFrame(clamp);  | |||
    return function release() {  | |||
      active = false;  | |||
      window.removeEventListener('scroll', onScroll);  | |||
    };  | |||
  }  | |||
  // Warten bis die Tabber-Höhe einige Frames stabil bleibt  | |||
  function waitStableHeight(tabber, frames) {  | |||
    var sec = tabber && tabber.querySelector(':scope > .tabber__section');  | |||
    if (!sec) return Promise.resolve();  | |||
    var last = sec.getBoundingClientRect().height, ok = 0, need = frames || 8;  | |||
     return new Promise(function (resolve) {  |      return new Promise(function (resolve) {  | ||
       function step() {  | |||
       function   |          var h = sec.getBoundingClientRect().height;  | ||
         var   |          ok = (Math.abs(h - last) < 1) ? ok + 1 : 0;  | ||
        last = h;  | |||
         if (  |          if (ok >= need) return resolve();  | ||
         requestAnimationFrame(step);  | |||
         requestAnimationFrame(  | |||
       }  |        }  | ||
       requestAnimationFrame(  |        requestAnimationFrame(step);  | ||
     });  |      });  | ||
   }  |    }  | ||
   function   |    function jumpToTab(id) {  | ||
     var name = (id || '').replace(/^#/, '');  |      var name = (id || '').replace(/^#/, '');  | ||
     if (!/^tabber-/.test(name)) return;  |      if (!/^tabber-/.test(name)) return;  | ||
     var panel  = document.getElementById(name);  |      var panel  = document.getElementById(name);  | ||
     if (!panel) return;  |      if (!panel) return;  | ||
     var   |      var tabber = panel.closest('.tabber');  | ||
    var header = headerForPanel(panel);  | |||
    var anchor = header || panel;  | |||
     var   |      var y = anchor.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();  | ||
     // 1) Sofortiger Sprung, dann Scroll „anklammern“  | |||
    window.scrollTo(0, y);  | |||
    var release = lockScroll(y, 1100);            // ggf. 900–1500 ms anpassen  | |||
    if (tabber) tabber.classList.add('tabber-is-switching'); // nutzt deine vorhandene CSS-Regel  | |||
    // 2) Nach Stabilisierung lösen + fein nachjustieren  | |||
    waitStableHeight(tabber).then(function () {  | |||
      release();  | |||
      if (tabber) tabber.classList.remove('tabber-is-switching');  | |||
       var y2 = (header || panel).getBoundingClientRect().top + window.scrollY - stickyOffsetPx();  |        var y2 = (header || panel).getBoundingClientRect().top + window.scrollY - stickyOffsetPx();  | ||
       if (Math.abs(y2 - y) > 1) window.scrollTo({ top: y2, behavior: 'smooth' });  | |||
       if (Math.abs(y2 -   | |||
     });  |      });  | ||
   }  |    }  | ||
   //   |    // Direktaufruf mit Hash  | ||
   if (/^#tabber-/.test(location.hash)) {  |    if (/^#tabber-/.test(location.hash)) {  | ||
     setTimeout(function () {   |      setTimeout(function () { jumpToTab(location.hash); }, 60);  | ||
   }  |    }  | ||
   // Hashwechsel (falls gesetzt)  | |||
   //   | |||
   window.addEventListener('hashchange', function () {  |    window.addEventListener('hashchange', function () {  | ||
     if (/^#tabber-/.test(location.hash))   |      if (/^#tabber-/.test(location.hash)) jumpToTab(location.hash);  | ||
   });  |    });  | ||
   // Tab-Klicks (TabberNeue nutzt History-API → kurzer Delay)  | |||
   //   | |||
   document.addEventListener('click', function (e) {  |    document.addEventListener('click', function (e) {  | ||
     var a = e.target && e.target.closest('a[href^="#tabber-"], button[aria-controls^="tabber-"]');  |      var a = e.target && 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'));  | ||
     setTimeout(function () { jumpToTab(id); }, 60);  | |||
     setTimeout(function () {   | |||
   }, true);  |    }, true);  | ||
})();  | })();  | ||
Version vom 30. Oktober 2025, 03:03 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: Anchor-Lock wie früher #TabberStart (ohne echtes Anker-Element) */
(function () {
  function stickyOffsetPx() {
    var hdr = document.querySelector('.vector-sticky-header-container');
    var h = hdr ? Math.round(hdr.getBoundingClientRect().height) : 0;
    var air = Math.round(parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.5);
    return h + air;
  }
  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;
  }
  // Scroll an Y „anklammern“ und für ms Millisekunden festhalten
  function lockScroll(y, ms) {
    var until = performance.now() + (ms || 900);
    var active = true;
    function clamp() {
      if (!active) return;
      window.scrollTo(0, y);
      if (performance.now() < until) requestAnimationFrame(clamp);
    }
    function onScroll() {
      if (!active) return;
      if (Math.abs(window.scrollY - y) > 1) window.scrollTo(0, y);
    }
    window.addEventListener('scroll', onScroll, { passive: true });
    requestAnimationFrame(clamp);
    return function release() {
      active = false;
      window.removeEventListener('scroll', onScroll);
    };
  }
  // Warten bis die Tabber-Höhe einige Frames stabil bleibt
  function waitStableHeight(tabber, frames) {
    var sec = tabber && tabber.querySelector(':scope > .tabber__section');
    if (!sec) return Promise.resolve();
    var last = sec.getBoundingClientRect().height, ok = 0, need = frames || 8;
    return new Promise(function (resolve) {
      function step() {
        var h = sec.getBoundingClientRect().height;
        ok = (Math.abs(h - last) < 1) ? ok + 1 : 0;
        last = h;
        if (ok >= need) return resolve();
        requestAnimationFrame(step);
      }
      requestAnimationFrame(step);
    });
  }
  function jumpToTab(id) {
    var name = (id || '').replace(/^#/, '');
    if (!/^tabber-/.test(name)) return;
    var panel  = document.getElementById(name);
    if (!panel) return;
    var tabber = panel.closest('.tabber');
    var header = headerForPanel(panel);
    var anchor = header || panel;
    var y = anchor.getBoundingClientRect().top + window.scrollY - stickyOffsetPx();
    // 1) Sofortiger Sprung, dann Scroll „anklammern“
    window.scrollTo(0, y);
    var release = lockScroll(y, 1100);            // ggf. 900–1500 ms anpassen
    if (tabber) tabber.classList.add('tabber-is-switching'); // nutzt deine vorhandene CSS-Regel
    // 2) Nach Stabilisierung lösen + fein nachjustieren
    waitStableHeight(tabber).then(function () {
      release();
      if (tabber) tabber.classList.remove('tabber-is-switching');
      var y2 = (header || panel).getBoundingClientRect().top + window.scrollY - stickyOffsetPx();
      if (Math.abs(y2 - y) > 1) window.scrollTo({ top: y2, behavior: 'smooth' });
    });
  }
  // Direktaufruf mit Hash
  if (/^#tabber-/.test(location.hash)) {
    setTimeout(function () { jumpToTab(location.hash); }, 60);
  }
  // Hashwechsel (falls gesetzt)
  window.addEventListener('hashchange', function () {
    if (/^#tabber-/.test(location.hash)) jumpToTab(location.hash);
  });
  // Tab-Klicks (TabberNeue nutzt History-API → kurzer Delay)
  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 () { jumpToTab(id); }, 60);
  }, true);
})();