MediaWiki:Common.js: Unterschied zwischen den Versionen

Aus Hist. Verein Herne / Wanne-Eickel
Inhalt gelöscht Inhalt hinzugefügt
KKeine Bearbeitungszusammenfassung
KKeine Bearbeitungszusammenfassung
 
(Eine dazwischenliegende Version desselben Benutzers wird nicht angezeigt)
Zeile 1: Zeile 1:
/* ============================================================
/* ============================================================
* Vorlage:Hauptseite/Neue Artikel
* Vorlage:Hauptseite/Neue Artikel
* Version 2 (Lookup per pageid statt per Titel — robust gegen
* zwischenzeitliche Umbenennungen / move-Operationen)
* Lädt die letzten 20 neu erstellten Seiten via MediaWiki-API
* Lädt die letzten 20 neu erstellten Seiten via MediaWiki-API
* und rendert pro Seite eine Kachel mit Bild, Titel, Auszug.
* und rendert pro Seite eine Kachel mit Bild, Titel, Auszug.
Zeile 39: Zeile 41:
container.innerHTML = '';
container.innerHTML = '';
ordered.forEach( function ( item ) {
ordered.forEach( function ( item ) {
var p = pageMap[ item.title ];
// Lookup per pageid robust gegen Umbenennungen.
var p = pageMap[ item.pageid ];
if ( !p || p.missing !== undefined ) { return; }
if ( !p || p.missing !== undefined ) { return; }


// Aktuellen Titel aus der Antwort verwenden, nicht den
var url = mw.util.getUrl( item.title );
// (eventuell veralteten) Titel aus recentchanges.
var titel = p.title;
var url = mw.util.getUrl( titel );


var card = document.createElement( 'div' );
var card = document.createElement( 'div' );
Zeile 51: Zeile 57:
imgWrap.className = 'hv-neue-artikel-bild';
imgWrap.className = 'hv-neue-artikel-bild';
imgWrap.href = url;
imgWrap.href = url;
imgWrap.title = item.title;
imgWrap.title = titel;
var img = document.createElement( 'img' );
var img = document.createElement( 'img' );
img.loading = 'lazy';
img.loading = 'lazy';
if ( p.thumbnail && p.thumbnail.source ) {
if ( p.thumbnail && p.thumbnail.source ) {
img.src = p.thumbnail.source;
img.src = p.thumbnail.source;
img.alt = item.title;
img.alt = titel;
imgWrap.appendChild( img );
imgWrap.appendChild( img );
} else if ( platzhalter ) {
} else if ( platzhalter ) {
// Kein Page-Image vorhanden -> Vereinslogo als Platzhalter
// Kein Page-Image vorhanden -> Platzhalter
img.src = platzhalter;
img.src = platzhalter;
img.alt = item.title;
img.alt = titel;
imgWrap.classList.add( 'hv-neue-artikel-bild-platzhalter' );
imgWrap.classList.add( 'hv-neue-artikel-bild-platzhalter' );
imgWrap.appendChild( img );
imgWrap.appendChild( img );
Zeile 69: Zeile 75:
card.appendChild( imgWrap );
card.appendChild( imgWrap );


// Titel
// Titel-Element
var titel = document.createElement( 'div' );
var titelDiv = document.createElement( 'div' );
titel.className = 'hv-neue-artikel-titel';
titelDiv.className = 'hv-neue-artikel-titel';
var a = document.createElement( 'a' );
var a = document.createElement( 'a' );
a.href = url;
a.href = url;
a.textContent = item.title;
a.textContent = titel;
titel.appendChild( a );
titelDiv.appendChild( a );
card.appendChild( titel );
card.appendChild( titelDiv );


// Auszug
// Auszug
Zeile 130: Zeile 136:
}
}


// 1) Liste neu erstellter Seiten holen
// 1) Liste neu erstellter Seiten holen — mit ids, damit wir
// im zweiten Schritt per pageid statt per Titel suchen
// können. Robust gegen Umbenennungen (move-Operationen),
// bei denen recentchanges weiterhin den alten Titel zeigt.
api.get( {
api.get( {
action: 'query',
action: 'query',
Zeile 137: Zeile 146:
rctype: 'new',
rctype: 'new',
rcshow: '!redirect',
rcshow: '!redirect',
rcprop: 'title|timestamp',
rcprop: 'title|timestamp|ids',
rclimit: limit,
rclimit: limit,
format: 'json',
format: 'json',
Zeile 147: Zeile 156:
return;
return;
}
}
// Doppelte Titel filtern (z. B. wenn zweimal in RC auftauchen)
// Doppelte pageids filtern und nur Einträge mit gültiger
// pageid übernehmen.
var seen = {};
var seen = {};
var ordered = [];
var ordered = [];
rc.forEach( function ( e ) {
rc.forEach( function ( e ) {
if ( !seen[ e.title ] ) {
if ( !e.pageid || seen[ e.pageid ] ) { return; }
seen[ e.title ] = true;
seen[ e.pageid ] = true;
ordered.push( { title: e.title, timestamp: e.timestamp } );
ordered.push( { pageid: e.pageid, timestamp: e.timestamp } );
}
} );
} );
if ( !ordered.length ) {
renderEmpty( container, 'Keine neuen Artikel gefunden.' );
return;
}


// 2) Bilder + Auszüge holen
// 2) Bilder + Auszüge holen — per pageids (NICHT titles),
// POST verwenden, weil die titles-Liste bei vielen
// damit wir auch zwischenzeitlich umbenannte Artikel
// Treffern lang werden kann und iOS Safari die GET-URL
// finden. POST aus iOS-Safari-Längen­gründen.
// dann abweist.
api.post( {
api.post( {
action: 'query',
action: 'query',
titles: ordered.map( function ( o ) { return o.title; } ).join( '|' ),
pageids: ordered.map( function ( o ) { return o.pageid; } ).join( '|' ),
prop: 'pageimages|extracts',
prop: 'pageimages|extracts',
pithumbsize: 320,
pithumbsize: 320,
Zeile 176: Zeile 188:
var pages = ( data2 && data2.query && data2.query.pages ) || [];
var pages = ( data2 && data2.query && data2.query.pages ) || [];
var pageMap = {};
var pageMap = {};
pages.forEach( function ( p ) { pageMap[ p.title ] = p; } );
pages.forEach( function ( p ) {
if ( p.pageid ) { pageMap[ p.pageid ] = p; }
} );
renderGrid( container, ordered, pageMap, platzhalterUrl );
renderGrid( container, ordered, pageMap, platzhalterUrl );
} ).fail( function () {
} ).fail( function () {
Zeile 210: Zeile 224:
/* ============================================================
/* ============================================================
* Vorlage:Zufallsbild – Bild-Karussell
* Vorlage:Zufallsbild – Bild-Karussell
* Version 3 (chunked imageinfo, POST, bessere Fehlermeldungen)
* Holt eine zufällige Stichprobe aus allen Wiki-Bildern via
* Holt eine zufällige Stichprobe aus allen Wiki-Bildern via
* MediaWiki-API, filtert kleine/dekorative Dateien heraus und
* MediaWiki-API, filtert kleine/dekorative Dateien heraus und
Zeile 337: Zeile 352:
}
}


// 2) Bildinfos holen (Größe, MIME, Thumb-URL).
// 2) Bildinfos in kleinen Häppchen holen.
// POST statt GET: bei einem Pool von 60 Dateititeln
// Statt einer einzigen großen Anfrage zerlegen wir die
// wird die GET-URL so lang, dass iOS Safari sie ablehnt.
// Titelliste in Chunks von 25 so kann weder die URL
api.post( {
// noch der Body zu groß werden, und im Fehlerfall sehen
action: 'query',
// wir genau, welcher Chunk gescheitert ist.
titles: kandidaten.join( '|' ),
holeBildInfosInChunks( api, kandidaten, thumbBreite, 25 )
prop: 'imageinfo',
.done( function ( allePages ) {
iiprop: 'size|url|mime|mediatype',
var valide = [];
iiurlwidth: thumbBreite,
allePages.forEach( function ( p ) {
format: 'json',
if ( !p.imageinfo || !p.imageinfo[ 0 ] ) { return; }
formatversion: 2
var i = p.imageinfo[ 0 ];
} ).done( function ( d2 ) {
if ( !i.mime || i.mime.indexOf( 'image/' ) !== 0 ) { return; }
var pages = ( d2 && d2.query && d2.query.pages ) || [];
if ( i.mediatype && i.mediatype !== 'BITMAP' ) { return; } // SVG raus
var valide = [];
if ( !i.width || !i.height ) { return; }
pages.forEach( function ( p ) {
if ( i.width < minB || i.height < minH ) { return; }
if ( !p.imageinfo || !p.imageinfo[ 0 ] ) { return; }
valide.push( { title: p.title, imageinfo: p.imageinfo } );
var i = p.imageinfo[ 0 ];
} );
if ( !i.mime || i.mime.indexOf( 'image/' ) !== 0 ) { return; }
if ( i.mediatype && i.mediatype !== 'BITMAP' ) { return; } // SVG raus
if ( !i.width || !i.height ) { return; }
if ( i.width < minB || i.height < minH ) { return; }
valide.push( { title: p.title, imageinfo: p.imageinfo } );
} );


if ( !valide.length ) {
if ( !valide.length ) {
container.innerHTML = '<div class="hv-karussell-leer"><em>Keine geeigneten Bilder gefunden.</em></div>';
container.innerHTML = '<div class="hv-karussell-leer"><em>Keine geeigneten Bilder gefunden.</em></div>';
return;
return;
}
}


// Mischen und auf gewünschte Anzahl kürzen
// Mischen und auf gewünschte Anzahl kürzen
valide = shuffle( valide ).slice( 0, anzahl );
valide = shuffle( valide ).slice( 0, anzahl );


container.innerHTML = '';
container.innerHTML = '';
container.classList.add( 'hv-karussell-aktiv' );
container.classList.add( 'hv-karussell-aktiv' );
var slides = valide.map( function ( v ) { return buildSlide( v, container ); } );
var slides = valide.map( function ( v ) { return buildSlide( v, container ); } );
slides[ 0 ].classList.add( 'hv-aktiv' );
slides[ 0 ].classList.add( 'hv-aktiv' );
startRotation( container, slides, intervall );
startRotation( container, slides, intervall );
} ).fail( function () {
} )
container.innerHTML = '<div class="hv-karussell-leer"><em>Fehler beim Laden der Bilddaten.</em></div>';
.fail( function ( code, info ) {
} );
var msg = 'Fehler beim Laden der Bilddaten';
} ).fail( function () {
if ( code ) { msg += ' (' + code + ')'; }
if ( info && info.error && info.error.info ) {
container.innerHTML = '<div class="hv-karussell-leer"><em>Fehler beim Abruf der API.</em></div>';
msg += ': ' + info.error.info;
}
container.innerHTML = '<div class="hv-karussell-leer"><em>' +
msg.replace( /[<>&]/g, function ( c ) { return { '<': '&lt;', '>': '&gt;', '&': '&amp;' }[ c ]; } ) +
'</em></div>';
} );
} ).fail( function ( code ) {
var msg = 'Fehler beim Abruf der API';
if ( code ) { msg += ' (' + code + ')'; }
container.innerHTML = '<div class="hv-karussell-leer"><em>' + msg + '</em></div>';
} );
} );
} );
} );
}

// Bildinfos in Häppchen holen, Ergebnisse zusammenfassen.
function holeBildInfosInChunks( api, titel, thumbBreite, chunkSize ) {
var alle = [];
function holeChunk( start ) {
var chunk = titel.slice( start, start + chunkSize );
if ( !chunk.length ) {
return $.Deferred().resolve( alle ).promise();
}
return api.post( {
action: 'query',
titles: chunk.join( '|' ),
prop: 'imageinfo',
iiprop: 'size|url|mime|mediatype',
iiurlwidth: thumbBreite,
format: 'json',
formatversion: 2
} ).then( function ( d ) {
var pages = ( d && d.query && d.query.pages ) || [];
pages.forEach( function ( p ) { alle.push( p ); } );
return holeChunk( start + chunkSize );
} );
}
return holeChunk( 0 );
}
}



Aktuelle Version vom 12. Mai 2026, 19:05 Uhr

/* ============================================================
 * Vorlage:Hauptseite/Neue Artikel
 * Version 2 (Lookup per pageid statt per Titel — robust gegen
 *           zwischenzeitliche Umbenennungen / move-Operationen)
 * Lädt die letzten 20 neu erstellten Seiten via MediaWiki-API
 * und rendert pro Seite eine Kachel mit Bild, Titel, Auszug.
 *
 * Voraussetzung: Erweiterungen PageImages und TextExtracts
 * sind aktiviert (im Wiki bereits der Fall).
 *
 * Bitte als Ergänzung in MediaWiki:Common.js einfügen.
 * ============================================================ */

( function () {
    'use strict';

    function escapeHtml( s ) {
        return String( s )
            .replace( /&/g, '&amp;' )
            .replace( /</g, '&lt;' )
            .replace( />/g, '&gt;' )
            .replace( /"/g, '&quot;' )
            .replace( /'/g, '&#39;' );
    }

    function formatDate( ts ) {
        if ( !ts ) { return ''; }
        var d = new Date( ts );
        if ( isNaN( d.getTime() ) ) { return ''; }
        var monate = [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
            'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ];
        return d.getDate() + '. ' + monate[ d.getMonth() ] + ' ' + d.getFullYear();
    }

    function renderEmpty( container, text ) {
        container.innerHTML = '<div class="hv-neue-artikel-leer"><em>' +
            escapeHtml( text ) + '</em></div>';
    }

    function renderGrid( container, ordered, pageMap, platzhalter ) {
        container.innerHTML = '';
        ordered.forEach( function ( item ) {
            // Lookup per pageid — robust gegen Umbenennungen.
            var p = pageMap[ item.pageid ];
            if ( !p || p.missing !== undefined ) { return; }

            // Aktuellen Titel aus der Antwort verwenden, nicht den
            // (eventuell veralteten) Titel aus recentchanges.
            var titel = p.title;
            var url = mw.util.getUrl( titel );

            var card = document.createElement( 'div' );
            card.className = 'hv-neue-artikel-kachel';

            // Bild (mit Link auf die Seite)
            var imgWrap = document.createElement( 'a' );
            imgWrap.className = 'hv-neue-artikel-bild';
            imgWrap.href = url;
            imgWrap.title = titel;
            var img = document.createElement( 'img' );
            img.loading = 'lazy';
            if ( p.thumbnail && p.thumbnail.source ) {
                img.src = p.thumbnail.source;
                img.alt = titel;
                imgWrap.appendChild( img );
            } else if ( platzhalter ) {
                // Kein Page-Image vorhanden -> Platzhalter
                img.src = platzhalter;
                img.alt = titel;
                imgWrap.classList.add( 'hv-neue-artikel-bild-platzhalter' );
                imgWrap.appendChild( img );
            } else {
                imgWrap.classList.add( 'hv-neue-artikel-bild-leer' );
            }
            card.appendChild( imgWrap );

            // Titel-Element
            var titelDiv = document.createElement( 'div' );
            titelDiv.className = 'hv-neue-artikel-titel';
            var a = document.createElement( 'a' );
            a.href = url;
            a.textContent = titel;
            titelDiv.appendChild( a );
            card.appendChild( titelDiv );

            // Auszug
            if ( p.extract ) {
                var ext = document.createElement( 'div' );
                ext.className = 'hv-neue-artikel-auszug';
                ext.textContent = p.extract;
                card.appendChild( ext );
            }

            // Datum
            if ( item.timestamp ) {
                var dt = document.createElement( 'div' );
                dt.className = 'hv-neue-artikel-datum';
                dt.textContent = formatDate( item.timestamp );
                card.appendChild( dt );
            }

            container.appendChild( card );
        } );

        if ( !container.children.length ) {
            renderEmpty( container, 'Keine neuen Artikel gefunden.' );
        }
    }

    function ladeNeueArtikel( container ) {
        var limit = parseInt( container.getAttribute( 'data-hv-limit' ) || '20', 10 );
        var ns = parseInt( container.getAttribute( 'data-hv-namespace' ) || '0', 10 );
        // Platzhalterbild für Artikel ohne eigenes Vorschaubild.
        // - Nicht gesetzt (Standard): Datei:Nochkeinbild.png
        // - Leerer String: kein Platzhalter (Streifenmuster).
        // - URL oder Pfad (enthält "/" oder beginnt mit "http"):
        //   wird direkt als Bild-URL verwendet.
        // - Sonst: als Wiki-Dateiname interpretiert und per
        //   Special:FilePath aufgelöst.
        var platzhalterAttr = container.getAttribute( 'data-hv-platzhalter' );
        if ( platzhalterAttr === null ) { platzhalterAttr = 'Nochkeinbild.png'; }
        if ( isNaN( limit ) || limit < 1 ) { limit = 20; }
        if ( limit > 50 ) { limit = 50; }

        mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).done( function () {
            var api = new mw.Api();
            var platzhalterUrl = null;
            if ( platzhalterAttr === '' ) {
                platzhalterUrl = null;
            } else if ( /^https?:\/\//.test( platzhalterAttr ) || platzhalterAttr.charAt( 0 ) === '/' ) {
                platzhalterUrl = platzhalterAttr;
            } else {
                platzhalterUrl = mw.util.getUrl(
                    'Special:FilePath/' + platzhalterAttr, { width: 320 }
                );
            }

            // 1) Liste neu erstellter Seiten holen — mit ids, damit wir
            //    im zweiten Schritt per pageid statt per Titel suchen
            //    können. Robust gegen Umbenennungen (move-Operationen),
            //    bei denen recentchanges weiterhin den alten Titel zeigt.
            api.get( {
                action: 'query',
                list: 'recentchanges',
                rcnamespace: ns,
                rctype: 'new',
                rcshow: '!redirect',
                rcprop: 'title|timestamp|ids',
                rclimit: limit,
                format: 'json',
                formatversion: 2
            } ).done( function ( data ) {
                var rc = ( data && data.query && data.query.recentchanges ) || [];
                if ( !rc.length ) {
                    renderEmpty( container, 'Keine neuen Artikel gefunden.' );
                    return;
                }
                // Doppelte pageids filtern und nur Einträge mit gültiger
                // pageid übernehmen.
                var seen = {};
                var ordered = [];
                rc.forEach( function ( e ) {
                    if ( !e.pageid || seen[ e.pageid ] ) { return; }
                    seen[ e.pageid ] = true;
                    ordered.push( { pageid: e.pageid, timestamp: e.timestamp } );
                } );
                if ( !ordered.length ) {
                    renderEmpty( container, 'Keine neuen Artikel gefunden.' );
                    return;
                }

                // 2) Bilder + Auszüge holen — per pageids (NICHT titles),
                //    damit wir auch zwischenzeitlich umbenannte Artikel
                //    finden. POST aus iOS-Safari-Längen­gründen.
                api.post( {
                    action: 'query',
                    pageids: ordered.map( function ( o ) { return o.pageid; } ).join( '|' ),
                    prop: 'pageimages|extracts',
                    pithumbsize: 320,
                    pilimit: 'max',
                    exintro: 1,
                    explaintext: 1,
                    exchars: 200,
                    exlimit: 'max',
                    format: 'json',
                    formatversion: 2
                } ).done( function ( data2 ) {
                    var pages = ( data2 && data2.query && data2.query.pages ) || [];
                    var pageMap = {};
                    pages.forEach( function ( p ) {
                        if ( p.pageid ) { pageMap[ p.pageid ] = p; }
                    } );
                    renderGrid( container, ordered, pageMap, platzhalterUrl );
                } ).fail( function () {
                    renderEmpty( container, 'Fehler beim Laden der Seitendaten.' );
                } );
            } ).fail( function () {
                renderEmpty( container, 'Fehler beim Abruf der API.' );
            } );
        } );
    }

    function initAll( $content ) {
        var root = ( $content && $content[ 0 ] ) || document;
        var nodes = root.querySelectorAll( '.hv-neue-artikel-grid' );
        Array.prototype.forEach.call( nodes, function ( node ) {
            if ( node.getAttribute( 'data-hv-loaded' ) === '1' ) { return; }
            node.setAttribute( 'data-hv-loaded', '1' );
            ladeNeueArtikel( node );
        } );
    }

    if ( window.mw && mw.hook ) {
        mw.hook( 'wikipage.content' ).add( initAll );
    } else if ( document.readyState !== 'loading' ) {
        initAll();
    } else {
        document.addEventListener( 'DOMContentLoaded', function () { initAll(); } );
    }

}() );


/* ============================================================
 * Vorlage:Zufallsbild – Bild-Karussell
 * Version 3 (chunked imageinfo, POST, bessere Fehlermeldungen)
 * Holt eine zufällige Stichprobe aus allen Wiki-Bildern via
 * MediaWiki-API, filtert kleine/dekorative Dateien heraus und
 * blendet die verbliebenen Bilder im 6-Sekunden-Takt durch.
 * Beim Hover pausiert das Karussell.
 * ============================================================ */

( function () {
    'use strict';

    // Dateinamen-Muster, die ausgeschlossen werden (Logos, Icons, Banner).
    // Bewusst konservativ; Hauptfilter ist ohnehin die Bildgröße.
    var AUSSCHLUSS_NAMEN = [
        /\blogo\b/i,
        /\bicon\b/i,
        /\bwappen\b/i,
        /\bsymbol\b/i,
        /_neu\.(png|svg|gif|jpe?g)$/i,   // typische Portal-Icons
        /werdejetzt/i,
        /herne[\s_-]mit[\s_-]respekt/i
    ];

    function istAusgeschlossen( title ) {
        var n = title.replace( /^Datei:|^File:|^Bild:/i, '' );
        for ( var i = 0; i < AUSSCHLUSS_NAMEN.length; i++ ) {
            if ( AUSSCHLUSS_NAMEN[ i ].test( n ) ) { return true; }
        }
        return false;
    }

    function shuffle( arr ) {
        for ( var i = arr.length - 1; i > 0; i-- ) {
            var j = Math.floor( Math.random() * ( i + 1 ) );
            var tmp = arr[ i ]; arr[ i ] = arr[ j ]; arr[ j ] = tmp;
        }
        return arr;
    }

    function buildSlide( p, container ) {
        var info = p.imageinfo[ 0 ];
        var a = document.createElement( 'a' );
        a.className = 'hv-karussell-bild';
        a.href = mw.util.getUrl( p.title );
        a.title = p.title.replace( /^Datei:/, '' );
        var img = document.createElement( 'img' );
        img.src = info.thumburl || info.url;
        img.alt = '';
        img.loading = 'lazy';
        a.appendChild( img );
        container.appendChild( a );
        return a;
    }

    function startRotation( container, slides, intervall ) {
        if ( slides.length < 2 ) { return; }
        slides[ 0 ].classList.add( 'hv-aktiv' );
        var aktiv = 0;
        var timer = null;

        function naechste() {
            slides[ aktiv ].classList.remove( 'hv-aktiv' );
            aktiv = ( aktiv + 1 ) % slides.length;
            slides[ aktiv ].classList.add( 'hv-aktiv' );
        }
        function start() { if ( !timer ) { timer = setInterval( naechste, intervall ); } }
        function stop() { if ( timer ) { clearInterval( timer ); timer = null; } }

        container.addEventListener( 'mouseenter', stop );
        container.addEventListener( 'mouseleave', start );
        // Bei Tab-Wechsel pausieren, um keine Zyklen im Hintergrund zu drehen
        document.addEventListener( 'visibilitychange', function () {
            if ( document.hidden ) { stop(); } else { start(); }
        } );
        start();
    }

    function ladeKarussell( container ) {
        var pool = parseInt( container.getAttribute( 'data-hv-pool' ) || '60', 10 );
        var anzahl = parseInt( container.getAttribute( 'data-hv-anzahl' ) || '10', 10 );
        var minB = parseInt( container.getAttribute( 'data-hv-min-breite' ) || '500', 10 );
        var minH = parseInt( container.getAttribute( 'data-hv-min-hoehe' ) || '400', 10 );
        var intervall = parseInt( container.getAttribute( 'data-hv-intervall' ) || '6000', 10 );
        var thumbBreite = parseInt( container.getAttribute( 'data-hv-thumb-breite' ) || '600', 10 );
        if ( pool < 10 ) { pool = 10; } if ( pool > 200 ) { pool = 200; }
        if ( anzahl < 1 ) { anzahl = 1; } if ( anzahl > 30 ) { anzahl = 30; }

        mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).done( function () {
            var api = new mw.Api();

            // 1) Pool zufälliger Datei-Titel holen.
            // list=random gibt max. 20 Einträge pro Aufruf zurück. Wir
            // sammeln in mehreren Aufrufen, falls pool > 20.
            var titel = [];
            function holePool( restzahl ) {
                if ( restzahl <= 0 ) { return $.Deferred().resolve().promise(); }
                var step = Math.min( restzahl, 20 );
                return api.get( {
                    action: 'query',
                    list: 'random',
                    rnnamespace: 6, // Datei-Namensraum
                    rnlimit: step,
                    format: 'json',
                    formatversion: 2
                } ).then( function ( d ) {
                    var rnd = ( d && d.query && d.query.random ) || [];
                    rnd.forEach( function ( e ) { titel.push( e.title ); } );
                    return holePool( restzahl - step );
                } );
            }

            holePool( pool ).done( function () {
                if ( !titel.length ) {
                    container.innerHTML = '<div class="hv-karussell-leer"><em>Keine Bilder gefunden.</em></div>';
                    return;
                }
                // Doppelte und ausgeschlossene direkt aussortieren
                var seen = {}, kandidaten = [];
                titel.forEach( function ( t ) {
                    if ( seen[ t ] ) { return; }
                    seen[ t ] = true;
                    if ( istAusgeschlossen( t ) ) { return; }
                    kandidaten.push( t );
                } );
                if ( !kandidaten.length ) {
                    container.innerHTML = '<div class="hv-karussell-leer"><em>Keine geeigneten Bilder gefunden.</em></div>';
                    return;
                }

                // 2) Bildinfos in kleinen Häppchen holen.
                // Statt einer einzigen großen Anfrage zerlegen wir die
                // Titelliste in Chunks von 25 — so kann weder die URL
                // noch der Body zu groß werden, und im Fehlerfall sehen
                // wir genau, welcher Chunk gescheitert ist.
                holeBildInfosInChunks( api, kandidaten, thumbBreite, 25 )
                    .done( function ( allePages ) {
                        var valide = [];
                        allePages.forEach( function ( p ) {
                            if ( !p.imageinfo || !p.imageinfo[ 0 ] ) { return; }
                            var i = p.imageinfo[ 0 ];
                            if ( !i.mime || i.mime.indexOf( 'image/' ) !== 0 ) { return; }
                            if ( i.mediatype && i.mediatype !== 'BITMAP' ) { return; } // SVG raus
                            if ( !i.width || !i.height ) { return; }
                            if ( i.width < minB || i.height < minH ) { return; }
                            valide.push( { title: p.title, imageinfo: p.imageinfo } );
                        } );

                        if ( !valide.length ) {
                            container.innerHTML = '<div class="hv-karussell-leer"><em>Keine geeigneten Bilder gefunden.</em></div>';
                            return;
                        }

                        // Mischen und auf gewünschte Anzahl kürzen
                        valide = shuffle( valide ).slice( 0, anzahl );

                        container.innerHTML = '';
                        container.classList.add( 'hv-karussell-aktiv' );
                        var slides = valide.map( function ( v ) { return buildSlide( v, container ); } );
                        slides[ 0 ].classList.add( 'hv-aktiv' );
                        startRotation( container, slides, intervall );
                    } )
                    .fail( function ( code, info ) {
                        var msg = 'Fehler beim Laden der Bilddaten';
                        if ( code ) { msg += ' (' + code + ')'; }
                        if ( info && info.error && info.error.info ) {
                            msg += ': ' + info.error.info;
                        }
                        container.innerHTML = '<div class="hv-karussell-leer"><em>' +
                            msg.replace( /[<>&]/g, function ( c ) { return { '<': '&lt;', '>': '&gt;', '&': '&amp;' }[ c ]; } ) +
                            '</em></div>';
                    } );
            } ).fail( function ( code ) {
                var msg = 'Fehler beim Abruf der API';
                if ( code ) { msg += ' (' + code + ')'; }
                container.innerHTML = '<div class="hv-karussell-leer"><em>' + msg + '</em></div>';
            } );
        } );
    }

    // Bildinfos in Häppchen holen, Ergebnisse zusammenfassen.
    function holeBildInfosInChunks( api, titel, thumbBreite, chunkSize ) {
        var alle = [];
        function holeChunk( start ) {
            var chunk = titel.slice( start, start + chunkSize );
            if ( !chunk.length ) {
                return $.Deferred().resolve( alle ).promise();
            }
            return api.post( {
                action: 'query',
                titles: chunk.join( '|' ),
                prop: 'imageinfo',
                iiprop: 'size|url|mime|mediatype',
                iiurlwidth: thumbBreite,
                format: 'json',
                formatversion: 2
            } ).then( function ( d ) {
                var pages = ( d && d.query && d.query.pages ) || [];
                pages.forEach( function ( p ) { alle.push( p ); } );
                return holeChunk( start + chunkSize );
            } );
        }
        return holeChunk( 0 );
    }

    function initKarussells( $content ) {
        var root = ( $content && $content[ 0 ] ) || document;
        var nodes = root.querySelectorAll( '.hv-bild-karussell' );
        Array.prototype.forEach.call( nodes, function ( node ) {
            if ( node.getAttribute( 'data-hv-loaded' ) === '1' ) { return; }
            node.setAttribute( 'data-hv-loaded', '1' );
            ladeKarussell( node );
        } );
    }

    if ( window.mw && mw.hook ) {
        mw.hook( 'wikipage.content' ).add( initKarussells );
    } else if ( document.readyState !== 'loading' ) {
        initKarussells();
    } else {
        document.addEventListener( 'DOMContentLoaded', function () { initKarussells(); } );
    }

}() );