MediaWiki:Common.portal.js
MediaWiki interface page
More actions
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* 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, $ ) {
'use strict';
/* ── Configuration ─────────────────────────────────── */
var SLIDE_INTERVAL_MS = 5000; // auto-advance interval
var IMAGE_WIDTH_PX = 600; // thumbnail width requested from API
/* ── Bootstrap: run after DOM ready ────────────────── */
mw.hook( 'wikipage.content' ).add( function ( $content ) {
$content.find( '.portal-slider-wrapper' ).each( function () {
initSlider( $( this ) );
} );
} );
/* ── Per-slider initialisation ─────────────────────── */
function initSlider( $wrapper ) {
var $nav = $wrapper.find( '.portal-slider-nav' );
var category = $nav.data( 'category' ); // set by template
var $prevBtn = $nav.find( '.portal-slider-prev' );
var $nextBtn = $nav.find( '.portal-slider-next' );
var $dots = $nav.find( '.portal-slider-dots' );
var $counter = $nav.find( '.portal-slider-counter' );
// The server already rendered the first image inside $wrapper
// as a plain <a><img></a> block (from wikitext [[File:...]]).
var $firstImg = $wrapper.find( 'a.image, img' ).first();
var slides = []; // { src, caption, pageTitle }
var current = 0;
var timer = null;
var $display; // the <img> we'll swap
/* ── Step 1: wrap the server-rendered image ──── */
if ( $firstImg.length ) {
// Build a display container around it
$display = $( '<img>' )
.addClass( 'portal-slider-image' )
.attr( 'alt', '' )
.css( { width: '100%', borderRadius: '6px',
maxHeight: '260px', objectFit: 'cover' } );
var $caption = $( '<div>' ).addClass( 'portal-slider-caption' );
var $frame = $( '<div>' ).addClass( 'portal-slider-frame' )
.append( $display, $caption );
// Replace whatever MW rendered with our controlled frame
$firstImg.closest( 'a.image, p' ).first().replaceWith( $frame );
} else {
// Placeholder already in DOM from Lua; just ensure nav shows
$prevBtn.hide();
$nextBtn.hide();
return;
}
/* ── Step 2: fetch ImageList sub-page ────────── */
if ( !category ) return;
var listPage = mw.config.get( 'wgPageName' ) + '/ImageList';
mw.api = mw.api || new mw.Api();
( new mw.Api() ).get( {
action: 'query',
titles: listPage,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
} ).then( function ( data ) {
var pages = data.query && data.query.pages;
if ( !pages || !pages[0] ) return;
var page = pages[0];
if ( page.missing ) {
// No ImageList — fall back to random category query
fetchRandomFromCategory( category );
return;
}
var content = page.revisions &&
page.revisions[0] &&
page.revisions[0].slots &&
page.revisions[0].slots.main &&
page.revisions[0].slots.main.content;
if ( !content ) return;
var fileNames = [];
content.split( '\n' ).forEach( function ( line ) {
var trimmed = line.replace( /^\s+|\s+$/g, '' );
if ( trimmed ) {
// Strip leading "File:" if present
var fname = trimmed.replace( /^[Ff]ile:/, '' )
.split( '|' )[0]
.replace( /^\s+|\s+$/g, '' );
if ( fname ) fileNames.push( 'File:' + fname );
}
} );
if ( fileNames.length === 0 ) return;
// Shuffle using a date-seeded PRNG so order is stable all day
fileNames = seededShuffle( fileNames );
fetchThumbnails( fileNames );
} ).catch( function () {
// Silent fail — first image still visible
} );
/* ── Fetch thumbnails for a list of file titles ─ */
function fetchThumbnails( fileNames ) {
( new mw.Api() ).get( {
action: 'query',
titles: fileNames.slice( 0, 20 ).join( '|' ),
prop: 'imageinfo',
iiprop: 'url|extmetadata',
iiurlwidth: IMAGE_WIDTH_PX,
formatversion: 2
} ).then( function ( data ) {
var pages = data.query && data.query.pages;
if ( !pages ) return;
pages.forEach( function ( page ) {
var ii = page.imageinfo && page.imageinfo[0];
if ( !ii || !ii.thumburl ) return;
var caption = '';
if ( ii.extmetadata ) {
caption = ( ii.extmetadata.ImageDescription &&
ii.extmetadata.ImageDescription.value ) || '';
// Strip HTML tags
caption = caption.replace( /<[^>]+>/g, '' );
}
slides.push( {
src: ii.thumburl,
caption: caption || page.title.replace( /^File:/, '' )
.replace( /\.[^.]+$/, '' ),
pageTitle: page.title
} );
} );
if ( slides.length === 0 ) return;
// Set the first slide immediately
showSlide( 0 );
buildDots();
startAutoplay();
$prevBtn.on( 'click', function () { advance( -1 ); } );
$nextBtn.on( 'click', function () { advance( 1 ); } );
$wrapper.on( 'mouseenter', stopAutoplay )
.on( 'mouseleave', startAutoplay );
} );
}
/* ── Fetch random images directly from category ─ */
function fetchRandomFromCategory( cat ) {
( new mw.Api() ).get( {
action: 'query',
list: 'categorymembers',
cmtitle: 'Category:' + cat,
cmtype: 'file',
cmlimit: 20,
cmsort: 'sortkey',
formatversion: 2
} ).then( function ( data ) {
var members = data.query && data.query.categorymembers;
if ( !members || members.length === 0 ) return;
var fileNames = members.map( function ( m ) { return m.title; } );
fileNames = seededShuffle( fileNames );
fetchThumbnails( fileNames );
} );
}
/* ── Slide display ──────────────────────────────── */
function showSlide( idx ) {
current = ( idx + slides.length ) % slides.length;
var slide = slides[current];
$display.attr( 'src', slide.src ).attr( 'alt', slide.caption );
$wrapper.find( '.portal-slider-caption' )
.text( slide.caption );
$dots.find( '.portal-slider-dot' ).removeClass( 'is-active' )
.eq( current ).addClass( 'is-active' );
$counter.text( ( current + 1 ) + ' / ' + slides.length );
}
function advance( dir ) {
stopAutoplay();
showSlide( current + dir );
startAutoplay();
}
/* ── Dots ───────────────────────────────────────── */
function buildDots() {
$dots.empty();
for ( var i = 0; i < slides.length; i++ ) {
$( '<span>' ).addClass( 'portal-slider-dot' )
.toggleClass( 'is-active', i === 0 )
.data( 'idx', i )
.on( 'click', function () { showSlide( $( this ).data( 'idx' ) ); } )
.appendTo( $dots );
}
}
/* ── Auto-play ──────────────────────────────────── */
function startAutoplay() {
stopAutoplay();
timer = setInterval( function () { advance( 1 ); }, SLIDE_INTERVAL_MS );
}
function stopAutoplay() {
if ( timer ) { clearInterval( timer ); timer = null; }
}
}
/* ── Seeded shuffle (date-based, stable all day) ──── */
function seededShuffle( arr ) {
var date = new Date();
var seed = date.getFullYear() * 10000 +
( date.getMonth() + 1 ) * 100 +
date.getDate();
var state = seed;
function rand() {
// Xorshift32
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return ( state >>> 0 ) / 0xFFFFFFFF;
}
var a = arr.slice();
for ( var i = a.length - 1; i > 0; i-- ) {
var j = Math.floor( rand() * ( i + 1 ) );
var t = a[i]; a[i] = a[j]; a[j] = t;
}
return a;
}
}( mediaWiki, jQuery ) );