MediaWiki:Organigramm.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
/**
* Organigramm-Werkzeuge für wikimitlinks069
*
* Macht die Seite „Organigramm Die Linke Frankfurt“ (und jede andere Seite
* mit <div id="lo-werkzeuge"></div>) interaktiver:
* - Suchfeld: zeigt nur passende Punkte, klappt sie auf und markiert die Treffer;
* Enter oder „Zum ersten Treffer“ springt zum ersten passenden Punkt
* - „Alle aufklappen“ / „Alle zuklappen“
* - Klick auf einen Link zu einem Punkt (#lo-…): klappt ihn auf, springt hin, hebt ihn kurz hervor
* - Klick auf die Titelzeile eines Punkts klappt ihn auf oder zu
* - „#“ hinter dem Titel kopiert einen Link direkt zu diesem Punkt
*
* Die Inhalte bleiben normaler Wikitext. Ein Punkt sieht so aus:
* <div class="mw-collapsible mw-collapsed" style="…">
* <span id="lo-eindeutige-id"></span>'''Titel''' – <span style="color:#6B5560">kurze Beschreibung</span>
* <div class="mw-collapsible-content">
* Text …
* </div>
* </div>
*
* Geladen über MediaWiki:Common.js, Gestaltung in MediaWiki:Organigramm.css.
* Stand: 20.10.2026
*/
( function () {
'use strict';
var MIN_ZEICHEN = 2;
function norm( text ) {
return ( text || '' )
.replace( //g, '' )
.replace( /\s+/g, ' ' )
.toLowerCase();
}
/* Punkte auf der Seite finden und ihren eigenen Text sammeln */
function sammlePunkte( root ) {
var punkte = [];
var nachElement = new Map();
root.querySelectorAll( 'span[id^="lo-"]' ).forEach( function ( anker ) {
var el = anker.closest( '.mw-collapsible' );
if ( !el || nachElement.has( el ) ) {
return;
}
// Der Anker muss in der Titelzeile stehen, nicht im aufklappbaren Inhalt
var inhalt = anker.closest( '.mw-collapsible-content' );
if ( inhalt && el.contains( inhalt ) ) {
return;
}
var kopf = anker.parentElement !== el ? anker.parentElement : null;
var punkt = {
el: el,
id: anker.id,
kopf: kopf,
titel: ( kopf ? ( kopf.querySelector( 'b' ) || kopf ) : anker ).textContent.trim(),
eltern: null,
textKnoten: [],
text: ''
};
el.classList.add( 'lo-knoten' );
if ( kopf ) {
kopf.classList.add( 'lo-kopf' );
}
punkte.push( punkt );
nachElement.set( el, punkt );
} );
punkte.forEach( function ( p ) {
var hoeher = p.el.parentElement && p.el.parentElement.closest( '.mw-collapsible' );
while ( hoeher && !nachElement.has( hoeher ) ) {
hoeher = hoeher.parentElement && hoeher.parentElement.closest( '.mw-collapsible' );
}
p.eltern = hoeher ? nachElement.get( hoeher ) : null;
// Eigener Text: alles im Punkt, aber ohne verschachtelte Punkte und ohne Bedienknöpfe
var walker = document.createTreeWalker( p.el, NodeFilter.SHOW_TEXT, {
acceptNode: function ( knoten ) {
var e = knoten.parentElement;
if ( !e ) {
return NodeFilter.FILTER_REJECT;
}
var naechster = e.closest( '.mw-collapsible' );
if ( naechster !== p.el ) {
return NodeFilter.FILTER_REJECT;
}
if ( e.closest( '.mw-collapsible-toggle, .lo-link' ) ) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
} );
var teile = [];
while ( walker.nextNode() ) {
p.textKnoten.push( walker.currentNode );
teile.push( walker.currentNode.data );
}
p.text = norm( teile.join( ' ' ) );
} );
return { liste: punkte, nachElement: nachElement };
}
function istZu( p ) {
return p.el.classList.contains( 'mw-collapsed' );
}
function aufklappen( p ) {
var api = $( p.el ).data( 'mw-collapsible' );
if ( api && istZu( p ) ) {
api.expand();
}
}
function zuklappen( p ) {
var api = $( p.el ).data( 'mw-collapsible' );
if ( api && !istZu( p ) ) {
api.collapse();
}
}
function aufblitzen( el ) {
el.classList.remove( 'lo-flash' );
// Neustart der Animation erzwingen
void el.offsetWidth;
el.classList.add( 'lo-flash' );
setTimeout( function () {
el.classList.remove( 'lo-flash' );
}, 2200 );
}
function linkKopieren( p ) {
var url = location.origin + mw.util.getUrl( mw.config.get( 'wgPageName' ) ) + '#' + p.id;
var fertig = function () {
mw.notify( 'Link zu „' + p.titel + '“ kopiert.', { tag: 'lo-link' } );
};
if ( navigator.clipboard && navigator.clipboard.writeText ) {
navigator.clipboard.writeText( url ).then( fertig, function () {
window.prompt( 'Link zum Kopieren:', url );
} );
} else {
window.prompt( 'Link zum Kopieren:', url );
}
}
function init( $content ) {
var root = $content[ 0 ];
var host = root && root.querySelector( '#lo-werkzeuge' );
if ( !host || host.getAttribute( 'data-lo-bereit' ) ) {
return;
}
host.setAttribute( 'data-lo-bereit', '1' );
var daten = sammlePunkte( root );
var punkte = daten.liste;
if ( !punkte.length ) {
return;
}
var nachId = new Map();
punkte.forEach( function ( p ) {
nachId.set( p.id, p );
} );
/* Bedienleiste */
host.innerHTML =
'<div class="lo-leiste" role="search">' +
'<label class="lo-suche">' +
'<span class="lo-sr">Im Organigramm suchen</span>' +
'<input type="search" autocomplete="off" spellcheck="false" ' +
'placeholder="Im Organigramm suchen, z. B. Ortsbeirat">' +
'</label>' +
'<button type="button" data-lo-aktion="auf">Alle aufklappen</button>' +
'<button type="button" data-lo-aktion="zu">Alle zuklappen</button>' +
'<div class="lo-status">' +
'<span class="lo-status-text" role="status" aria-live="polite"></span>' +
'<button type="button" class="lo-sprung" hidden>Zum ersten Treffer ↓</button>' +
'</div>' +
'</div>';
var eingabe = host.querySelector( 'input' );
var status = host.querySelector( '.lo-status-text' );
var sprungKnopf = host.querySelector( '.lo-sprung' );
var ersterTreffer = null;
/* „#“-Knopf und klickbare Titelzeile */
punkte.forEach( function ( p ) {
if ( !p.kopf ) {
return;
}
var knopf = document.createElement( 'button' );
knopf.type = 'button';
knopf.className = 'lo-link';
knopf.textContent = '#';
knopf.title = 'Link zu diesem Punkt kopieren';
knopf.setAttribute( 'aria-label', 'Link zu „' + p.titel + '“ kopieren' );
knopf.addEventListener( 'click', function ( e ) {
e.stopPropagation();
linkKopieren( p );
} );
var titel = p.kopf.querySelector( 'b' );
if ( titel && titel.parentNode === p.kopf ) {
titel.after( knopf );
} else {
p.kopf.appendChild( knopf );
}
p.kopf.addEventListener( 'click', function ( e ) {
if ( e.target.closest( 'a, button, input, label' ) ) {
return;
}
var auswahl = window.getSelection && window.getSelection().toString();
if ( auswahl ) {
return;
}
var api = $( p.el ).data( 'mw-collapsible' );
if ( api ) {
api.toggle();
}
} );
} );
/* Suche */
var vonSucheGeoeffnet = new Set();
var kannMarkieren = !!( window.CSS && CSS.highlights && window.Highlight );
function markierungenLoeschen() {
if ( kannMarkieren ) {
CSS.highlights.delete( 'lo-treffer' );
}
}
function markieren( begriffe, treffer ) {
if ( !kannMarkieren ) {
return;
}
var bereiche = [];
treffer.forEach( function ( p ) {
p.textKnoten.forEach( function ( tk ) {
var klein = tk.data.toLowerCase();
begriffe.forEach( function ( b ) {
var i = klein.indexOf( b );
while ( i !== -1 && bereiche.length < 600 ) {
var r = new Range();
r.setStart( tk, i );
r.setEnd( tk, i + b.length );
bereiche.push( r );
i = klein.indexOf( b, i + b.length );
}
} );
} );
} );
CSS.highlights.set( 'lo-treffer', new Highlight( ...bereiche ) );
}
function sucheZuruecksetzen() {
punkte.forEach( function ( p ) {
p.el.classList.remove( 'lo-hidden', 'lo-hit' );
} );
vonSucheGeoeffnet.forEach( zuklappen );
vonSucheGeoeffnet.clear();
markierungenLoeschen();
host.classList.remove( 'lo-sucht' );
ersterTreffer = null;
sprungKnopf.hidden = true;
}
function suchen( roh ) {
sucheZuruecksetzen();
var q = norm( roh ).trim();
if ( q.length < MIN_ZEICHEN ) {
status.textContent = '';
return [];
}
// Erst nach der genauen Wortfolge suchen, sonst nach allen Wörtern einzeln
var begriffe = [ q ];
var alsPhrase = punkte.some( function ( p ) {
return p.text.indexOf( q ) !== -1;
} );
if ( !alsPhrase ) {
begriffe = q.split( ' ' ).filter( Boolean );
}
punkte.forEach( function ( p ) {
p.selbst = begriffe.every( function ( b ) {
return p.text.indexOf( b ) !== -1;
} );
p.darunter = false;
p.darueber = false;
} );
punkte.forEach( function ( p ) {
if ( !p.selbst ) {
return;
}
for ( var e = p.eltern; e; e = e.eltern ) {
e.darunter = true;
}
} );
punkte.forEach( function ( p ) {
for ( var e = p.eltern; e; e = e.eltern ) {
if ( e.selbst ) {
p.darueber = true;
break;
}
}
} );
var treffer = [];
punkte.forEach( function ( p ) {
var sichtbar = p.selbst || p.darunter || p.darueber;
p.el.classList.toggle( 'lo-hidden', !sichtbar );
p.el.classList.toggle( 'lo-hit', !!p.selbst );
if ( p.selbst ) {
treffer.push( p );
}
if ( ( p.selbst || p.darunter ) && istZu( p ) ) {
aufklappen( p );
vonSucheGeoeffnet.add( p );
}
} );
host.classList.add( 'lo-sucht' );
markieren( begriffe, treffer );
status.textContent = treffer.length === 0 ?
'Kein Punkt gefunden.' :
( treffer.length === 1 ? '1 Punkt gefunden.' : treffer.length + ' Punkte gefunden.' );
ersterTreffer = treffer[ 0 ] || null;
sprungKnopf.hidden = !ersterTreffer;
return treffer;
}
var verzoegerung;
eingabe.addEventListener( 'input', function () {
clearTimeout( verzoegerung );
verzoegerung = setTimeout( function () {
suchen( eingabe.value );
}, 180 );
} );
function zumErstenTreffer() {
if ( ersterTreffer ) {
ersterTreffer.el.scrollIntoView( { behavior: 'smooth', block: 'start' } );
aufblitzen( ersterTreffer.el );
}
}
sprungKnopf.addEventListener( 'click', zumErstenTreffer );
eingabe.addEventListener( 'keydown', function ( e ) {
if ( e.key === 'Escape' ) {
eingabe.value = '';
suchen( '' );
} else if ( e.key === 'Enter' ) {
e.preventDefault();
clearTimeout( verzoegerung );
suchen( eingabe.value );
zumErstenTreffer();
}
} );
/* Alle auf- und zuklappen */
host.addEventListener( 'click', function ( e ) {
var knopf = e.target.closest( '[data-lo-aktion]' );
if ( !knopf ) {
return;
}
var auf = knopf.getAttribute( 'data-lo-aktion' ) === 'auf';
punkte.forEach( function ( p ) {
if ( p.el.classList.contains( 'lo-hidden' ) ) {
return;
}
if ( auf ) {
aufklappen( p );
} else {
zuklappen( p );
}
} );
status.textContent = auf ? 'Alle Punkte aufgeklappt.' : 'Alle Punkte zugeklappt.';
} );
/* Sprünge zu Punkten */
function springeZu( id, sanft ) {
var ziel = document.getElementById( id );
if ( !ziel || !root.contains( ziel ) ) {
return;
}
if ( ziel.closest( '.lo-hidden' ) ) {
eingabe.value = '';
suchen( '' );
}
$( ziel ).parents( '.mw-collapsible' ).each( function () {
var p = daten.nachElement.get( this );
if ( p ) {
aufklappen( p );
} else if ( this.classList.contains( 'mw-collapsed' ) && $( this ).data( 'mw-collapsible' ) ) {
$( this ).data( 'mw-collapsible' ).expand();
}
} );
ziel.scrollIntoView( { behavior: sanft ? 'smooth' : 'auto', block: 'start' } );
var punkt = nachId.get( id );
if ( punkt ) {
aufblitzen( punkt.el );
}
}
root.addEventListener( 'click', function ( e ) {
var a = e.target.closest( 'a[href^="#lo-"]' );
if ( !a || e.ctrlKey || e.metaKey || e.shiftKey || e.button !== 0 ) {
return;
}
var id = decodeURIComponent( a.getAttribute( 'href' ).slice( 1 ) );
if ( !document.getElementById( id ) ) {
return;
}
e.preventDefault();
if ( location.hash !== '#' + id ) {
history.pushState( null, '', '#' + id );
}
springeZu( id, true );
} );
window.addEventListener( 'popstate', function () {
var h = location.hash.slice( 1 );
if ( h.indexOf( 'lo-' ) === 0 ) {
springeZu( decodeURIComponent( h ), false );
}
} );
if ( location.hash.indexOf( '#lo-' ) === 0 ) {
springeZu( decodeURIComponent( location.hash.slice( 1 ) ), false );
}
}
mw.hook( 'wikipage.content' ).add( function ( $content ) {
if ( !$content.find( '#lo-werkzeuge' ).length ) {
return;
}
mw.loader.using( [ 'jquery.makeCollapsible', 'mediawiki.util' ] ).then( function () {
// Sicherstellen, dass die Punkte schon aufklappbar sind (macht nichts doppelt)
$content.find( '.mw-collapsible' ).makeCollapsible();
init( $content );
} );
} );
}() );