<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://en.battlestarwiki.org/MediaWiki:Common.portal.js/history?feed=atom</id>
	<title>MediaWiki:Common.portal.js - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://en.battlestarwiki.org/MediaWiki:Common.portal.js/history?feed=atom"/>
	<link rel="alternate" type="text/html" href="https://en.battlestarwiki.org/MediaWiki:Common.portal.js/history"/>
	<updated>2026-04-13T18:32:34Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.45.1</generator>
	<entry>
		<id>https://en.battlestarwiki.org/w/index.php?title=MediaWiki:Common.portal.js&amp;diff=262267&amp;oldid=prev</id>
		<title>Joe Beaudoin Jr.: Created page with &quot;/* Portal image slider — MediaWiki:Common.js  *  * Wires up any .portal-slider-wrapper on the page.  * The Lua module (Module:Portal|randomImage) renders the first image  * server-side; the JS fetches the full ImageList sub-page via the API  * and pre-loads remaining images for the slider.  *  * Falls back gracefully: if JS is disabled, the first random image  * is still shown (rendered server-side by Lua).  */ ( function ( mw, $ ) {     &#039;use strict&#039;;      /* ── Co...&quot;</title>
		<link rel="alternate" type="text/html" href="https://en.battlestarwiki.org/w/index.php?title=MediaWiki:Common.portal.js&amp;diff=262267&amp;oldid=prev"/>
		<updated>2026-04-13T14:26:27Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;&lt;span class=&quot;autocomment&quot;&gt;Portal image slider — MediaWiki:Common.js  *  * Wires up any .portal-slider-wrapper on the page.  * The Lua module (Module:Portal|randomImage) renders the first image  * server-side; the JS fetches the full ImageList sub-page via the API  * and pre-loads remaining images for the slider.  *  * Falls back gracefully: if JS is disabled, the first random image  * is still shown (rendered server-side by Lua).: &lt;/span&gt; ( function ( mw, $ ) {     &amp;#039;use strict&amp;#039;;      /* ── Co...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;/* Portal image slider — MediaWiki:Common.js&lt;br /&gt;
 *&lt;br /&gt;
 * Wires up any .portal-slider-wrapper on the page.&lt;br /&gt;
 * The Lua module (Module:Portal|randomImage) renders the first image&lt;br /&gt;
 * server-side; the JS fetches the full ImageList sub-page via the API&lt;br /&gt;
 * and pre-loads remaining images for the slider.&lt;br /&gt;
 *&lt;br /&gt;
 * Falls back gracefully: if JS is disabled, the first random image&lt;br /&gt;
 * is still shown (rendered server-side by Lua).&lt;br /&gt;
 */&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
    &amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
    /* ── Configuration ─────────────────────────────────── */&lt;br /&gt;
    var SLIDE_INTERVAL_MS = 5000;   // auto-advance interval&lt;br /&gt;
    var IMAGE_WIDTH_PX    = 600;    // thumbnail width requested from API&lt;br /&gt;
&lt;br /&gt;
    /* ── Bootstrap: run after DOM ready ────────────────── */&lt;br /&gt;
    mw.hook( &amp;#039;wikipage.content&amp;#039; ).add( function ( $content ) {&lt;br /&gt;
        $content.find( &amp;#039;.portal-slider-wrapper&amp;#039; ).each( function () {&lt;br /&gt;
            initSlider( $( this ) );&lt;br /&gt;
        } );&lt;br /&gt;
    } );&lt;br /&gt;
&lt;br /&gt;
    /* ── Per-slider initialisation ─────────────────────── */&lt;br /&gt;
    function initSlider( $wrapper ) {&lt;br /&gt;
        var $nav      = $wrapper.find( &amp;#039;.portal-slider-nav&amp;#039; );&lt;br /&gt;
        var category  = $nav.data( &amp;#039;category&amp;#039; );   // set by template&lt;br /&gt;
        var $prevBtn  = $nav.find( &amp;#039;.portal-slider-prev&amp;#039; );&lt;br /&gt;
        var $nextBtn  = $nav.find( &amp;#039;.portal-slider-next&amp;#039; );&lt;br /&gt;
        var $dots     = $nav.find( &amp;#039;.portal-slider-dots&amp;#039; );&lt;br /&gt;
        var $counter  = $nav.find( &amp;#039;.portal-slider-counter&amp;#039; );&lt;br /&gt;
&lt;br /&gt;
        // The server already rendered the first image inside $wrapper&lt;br /&gt;
        // as a plain &amp;lt;a&amp;gt;&amp;lt;img&amp;gt;&amp;lt;/a&amp;gt; block (from wikitext [[File:...]]).&lt;br /&gt;
        var $firstImg = $wrapper.find( &amp;#039;a.image, img&amp;#039; ).first();&lt;br /&gt;
&lt;br /&gt;
        var slides  = [];   // { src, caption, pageTitle }&lt;br /&gt;
        var current = 0;&lt;br /&gt;
        var timer   = null;&lt;br /&gt;
        var $display;       // the &amp;lt;img&amp;gt; we&amp;#039;ll swap&lt;br /&gt;
&lt;br /&gt;
        /* ── Step 1: wrap the server-rendered image ──── */&lt;br /&gt;
        if ( $firstImg.length ) {&lt;br /&gt;
            // Build a display container around it&lt;br /&gt;
            $display = $( &amp;#039;&amp;lt;img&amp;gt;&amp;#039; )&lt;br /&gt;
                .addClass( &amp;#039;portal-slider-image&amp;#039; )&lt;br /&gt;
                .attr( &amp;#039;alt&amp;#039;, &amp;#039;&amp;#039; )&lt;br /&gt;
                .css( { width: &amp;#039;100%&amp;#039;, borderRadius: &amp;#039;6px&amp;#039;,&lt;br /&gt;
                        maxHeight: &amp;#039;260px&amp;#039;, objectFit: &amp;#039;cover&amp;#039; } );&lt;br /&gt;
&lt;br /&gt;
            var $caption = $( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).addClass( &amp;#039;portal-slider-caption&amp;#039; );&lt;br /&gt;
            var $frame   = $( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).addClass( &amp;#039;portal-slider-frame&amp;#039; )&lt;br /&gt;
                .append( $display, $caption );&lt;br /&gt;
&lt;br /&gt;
            // Replace whatever MW rendered with our controlled frame&lt;br /&gt;
            $firstImg.closest( &amp;#039;a.image, p&amp;#039; ).first().replaceWith( $frame );&lt;br /&gt;
        } else {&lt;br /&gt;
            // Placeholder already in DOM from Lua; just ensure nav shows&lt;br /&gt;
            $prevBtn.hide();&lt;br /&gt;
            $nextBtn.hide();&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        /* ── Step 2: fetch ImageList sub-page ────────── */&lt;br /&gt;
        if ( !category ) return;&lt;br /&gt;
&lt;br /&gt;
        var listPage = mw.config.get( &amp;#039;wgPageName&amp;#039; ) + &amp;#039;/ImageList&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
        mw.api = mw.api || new mw.Api();&lt;br /&gt;
        ( new mw.Api() ).get( {&lt;br /&gt;
            action:  &amp;#039;query&amp;#039;,&lt;br /&gt;
            titles:  listPage,&lt;br /&gt;
            prop:    &amp;#039;revisions&amp;#039;,&lt;br /&gt;
            rvprop:  &amp;#039;content&amp;#039;,&lt;br /&gt;
            rvslots: &amp;#039;main&amp;#039;,&lt;br /&gt;
            formatversion: 2&lt;br /&gt;
        } ).then( function ( data ) {&lt;br /&gt;
            var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
            if ( !pages || !pages[0] ) return;&lt;br /&gt;
&lt;br /&gt;
            var page = pages[0];&lt;br /&gt;
            if ( page.missing ) {&lt;br /&gt;
                // No ImageList — fall back to random category query&lt;br /&gt;
                fetchRandomFromCategory( category );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var content = page.revisions &amp;amp;&amp;amp;&lt;br /&gt;
                          page.revisions[0] &amp;amp;&amp;amp;&lt;br /&gt;
                          page.revisions[0].slots &amp;amp;&amp;amp;&lt;br /&gt;
                          page.revisions[0].slots.main &amp;amp;&amp;amp;&lt;br /&gt;
                          page.revisions[0].slots.main.content;&lt;br /&gt;
            if ( !content ) return;&lt;br /&gt;
&lt;br /&gt;
            var fileNames = [];&lt;br /&gt;
            content.split( &amp;#039;\n&amp;#039; ).forEach( function ( line ) {&lt;br /&gt;
                var trimmed = line.replace( /^\s+|\s+$/g, &amp;#039;&amp;#039; );&lt;br /&gt;
                if ( trimmed ) {&lt;br /&gt;
                    // Strip leading &amp;quot;File:&amp;quot; if present&lt;br /&gt;
                    var fname = trimmed.replace( /^[Ff]ile:/, &amp;#039;&amp;#039; )&lt;br /&gt;
                                      .split( &amp;#039;|&amp;#039; )[0]&lt;br /&gt;
                                      .replace( /^\s+|\s+$/g, &amp;#039;&amp;#039; );&lt;br /&gt;
                    if ( fname ) fileNames.push( &amp;#039;File:&amp;#039; + fname );&lt;br /&gt;
                }&lt;br /&gt;
            } );&lt;br /&gt;
&lt;br /&gt;
            if ( fileNames.length === 0 ) return;&lt;br /&gt;
&lt;br /&gt;
            // Shuffle using a date-seeded PRNG so order is stable all day&lt;br /&gt;
            fileNames = seededShuffle( fileNames );&lt;br /&gt;
            fetchThumbnails( fileNames );&lt;br /&gt;
&lt;br /&gt;
        } ).catch( function () {&lt;br /&gt;
            // Silent fail — first image still visible&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        /* ── Fetch thumbnails for a list of file titles ─ */&lt;br /&gt;
        function fetchThumbnails( fileNames ) {&lt;br /&gt;
            ( new mw.Api() ).get( {&lt;br /&gt;
                action:   &amp;#039;query&amp;#039;,&lt;br /&gt;
                titles:   fileNames.slice( 0, 20 ).join( &amp;#039;|&amp;#039; ),&lt;br /&gt;
                prop:     &amp;#039;imageinfo&amp;#039;,&lt;br /&gt;
                iiprop:   &amp;#039;url|extmetadata&amp;#039;,&lt;br /&gt;
                iiurlwidth: IMAGE_WIDTH_PX,&lt;br /&gt;
                formatversion: 2&lt;br /&gt;
            } ).then( function ( data ) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if ( !pages ) return;&lt;br /&gt;
&lt;br /&gt;
                pages.forEach( function ( page ) {&lt;br /&gt;
                    var ii = page.imageinfo &amp;amp;&amp;amp; page.imageinfo[0];&lt;br /&gt;
                    if ( !ii || !ii.thumburl ) return;&lt;br /&gt;
                    var caption = &amp;#039;&amp;#039;;&lt;br /&gt;
                    if ( ii.extmetadata ) {&lt;br /&gt;
                        caption = ( ii.extmetadata.ImageDescription &amp;amp;&amp;amp;&lt;br /&gt;
                                    ii.extmetadata.ImageDescription.value ) || &amp;#039;&amp;#039;;&lt;br /&gt;
                        // Strip HTML tags&lt;br /&gt;
                        caption = caption.replace( /&amp;lt;[^&amp;gt;]+&amp;gt;/g, &amp;#039;&amp;#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                    slides.push( {&lt;br /&gt;
                        src:       ii.thumburl,&lt;br /&gt;
                        caption:   caption || page.title.replace( /^File:/, &amp;#039;&amp;#039; )&lt;br /&gt;
                                                         .replace( /\.[^.]+$/, &amp;#039;&amp;#039; ),&lt;br /&gt;
                        pageTitle: page.title&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
&lt;br /&gt;
                if ( slides.length === 0 ) return;&lt;br /&gt;
&lt;br /&gt;
                // Set the first slide immediately&lt;br /&gt;
                showSlide( 0 );&lt;br /&gt;
                buildDots();&lt;br /&gt;
                startAutoplay();&lt;br /&gt;
&lt;br /&gt;
                $prevBtn.on( &amp;#039;click&amp;#039;, function () { advance( -1 ); } );&lt;br /&gt;
                $nextBtn.on( &amp;#039;click&amp;#039;, function () { advance(  1 ); } );&lt;br /&gt;
                $wrapper.on( &amp;#039;mouseenter&amp;#039;, stopAutoplay )&lt;br /&gt;
                        .on( &amp;#039;mouseleave&amp;#039;, startAutoplay );&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        /* ── Fetch random images directly from category ─ */&lt;br /&gt;
        function fetchRandomFromCategory( cat ) {&lt;br /&gt;
            ( new mw.Api() ).get( {&lt;br /&gt;
                action:     &amp;#039;query&amp;#039;,&lt;br /&gt;
                list:       &amp;#039;categorymembers&amp;#039;,&lt;br /&gt;
                cmtitle:    &amp;#039;Category:&amp;#039; + cat,&lt;br /&gt;
                cmtype:     &amp;#039;file&amp;#039;,&lt;br /&gt;
                cmlimit:    20,&lt;br /&gt;
                cmsort:     &amp;#039;sortkey&amp;#039;,&lt;br /&gt;
                formatversion: 2&lt;br /&gt;
            } ).then( function ( data ) {&lt;br /&gt;
                var members = data.query &amp;amp;&amp;amp; data.query.categorymembers;&lt;br /&gt;
                if ( !members || members.length === 0 ) return;&lt;br /&gt;
&lt;br /&gt;
                var fileNames = members.map( function ( m ) { return m.title; } );&lt;br /&gt;
                fileNames = seededShuffle( fileNames );&lt;br /&gt;
                fetchThumbnails( fileNames );&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        /* ── Slide display ──────────────────────────────── */&lt;br /&gt;
        function showSlide( idx ) {&lt;br /&gt;
            current = ( idx + slides.length ) % slides.length;&lt;br /&gt;
            var slide = slides[current];&lt;br /&gt;
&lt;br /&gt;
            $display.attr( &amp;#039;src&amp;#039;, slide.src ).attr( &amp;#039;alt&amp;#039;, slide.caption );&lt;br /&gt;
            $wrapper.find( &amp;#039;.portal-slider-caption&amp;#039; )&lt;br /&gt;
                    .text( slide.caption );&lt;br /&gt;
&lt;br /&gt;
            $dots.find( &amp;#039;.portal-slider-dot&amp;#039; ).removeClass( &amp;#039;is-active&amp;#039; )&lt;br /&gt;
                 .eq( current ).addClass( &amp;#039;is-active&amp;#039; );&lt;br /&gt;
&lt;br /&gt;
            $counter.text( ( current + 1 ) + &amp;#039; / &amp;#039; + slides.length );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        function advance( dir ) {&lt;br /&gt;
            stopAutoplay();&lt;br /&gt;
            showSlide( current + dir );&lt;br /&gt;
            startAutoplay();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        /* ── Dots ───────────────────────────────────────── */&lt;br /&gt;
        function buildDots() {&lt;br /&gt;
            $dots.empty();&lt;br /&gt;
            for ( var i = 0; i &amp;lt; slides.length; i++ ) {&lt;br /&gt;
                $( &amp;#039;&amp;lt;span&amp;gt;&amp;#039; ).addClass( &amp;#039;portal-slider-dot&amp;#039; )&lt;br /&gt;
                    .toggleClass( &amp;#039;is-active&amp;#039;, i === 0 )&lt;br /&gt;
                    .data( &amp;#039;idx&amp;#039;, i )&lt;br /&gt;
                    .on( &amp;#039;click&amp;#039;, function () { showSlide( $( this ).data( &amp;#039;idx&amp;#039; ) ); } )&lt;br /&gt;
                    .appendTo( $dots );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        /* ── Auto-play ──────────────────────────────────── */&lt;br /&gt;
        function startAutoplay() {&lt;br /&gt;
            stopAutoplay();&lt;br /&gt;
            timer = setInterval( function () { advance( 1 ); }, SLIDE_INTERVAL_MS );&lt;br /&gt;
        }&lt;br /&gt;
        function stopAutoplay() {&lt;br /&gt;
            if ( timer ) { clearInterval( timer ); timer = null; }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* ── Seeded shuffle (date-based, stable all day) ──── */&lt;br /&gt;
    function seededShuffle( arr ) {&lt;br /&gt;
        var date   = new Date();&lt;br /&gt;
        var seed   = date.getFullYear() * 10000 +&lt;br /&gt;
                     ( date.getMonth() + 1 ) * 100 +&lt;br /&gt;
                     date.getDate();&lt;br /&gt;
        var state  = seed;&lt;br /&gt;
&lt;br /&gt;
        function rand() {&lt;br /&gt;
            // Xorshift32&lt;br /&gt;
            state ^= state &amp;lt;&amp;lt; 13;&lt;br /&gt;
            state ^= state &amp;gt;&amp;gt; 17;&lt;br /&gt;
            state ^= state &amp;lt;&amp;lt; 5;&lt;br /&gt;
            return ( state &amp;gt;&amp;gt;&amp;gt; 0 ) / 0xFFFFFFFF;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var a = arr.slice();&lt;br /&gt;
        for ( var i = a.length - 1; i &amp;gt; 0; i-- ) {&lt;br /&gt;
            var j = Math.floor( rand() * ( i + 1 ) );&lt;br /&gt;
            var t = a[i]; a[i] = a[j]; a[j] = t;&lt;br /&gt;
        }&lt;br /&gt;
        return a;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}( mediaWiki, jQuery ) );&lt;/div&gt;</summary>
		<author><name>Joe Beaudoin Jr.</name></author>
	</entry>
</feed>