Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
Shane (talk | contribs)
New page: /** Interwiki links to featured articles *************************************** * * Description: Highlights interwiki links to featured articles (or * equivalents...
 
Joe Beaudoin Jr. (talk | contribs)
No edit summary
 
(35 intermediate revisions by 2 users not shown)
Line 5: Line 5:
   *              equivalents) by changing the bullet before the interwiki link
   *              equivalents) by changing the bullet before the interwiki link
   *              into a star.
   *              into a star.
   *  Maintainers: [[Wikipedia:User:R. Koot|User:R. Koot]]
   *  Maintainers: [[Wikipedia:User:R. Koot|R. Koot]]
   */
   */
   
   
function LinkFA()  
function LinkFA()  
{
{
     if ( document.getElementById( "p-lang" ) ) {
     if ( document.getElementById( "p-lang" ) ) {
         var InterwikiLinks = document.getElementById( "p-lang" ).getElementsByTagName( "li" );
         var InterwikiLinks = document.getElementById( "p-lang" ).getElementsByTagName( "li" );
Line 20: Line 20:
         }
         }
     }
     }
  }
}
   
addOnloadHook( LinkFA );
   
   
addOnloadHook( LinkFA );
/** Collapsible tables *********************************************************
 
/** Collapsible tables *********************************************************
   *
   *
   *  Description: Allows tables to be collapsed, showing only the header. See
   *  Description: Allows tables to be collapsed, showing only the header. See
   *              [[Wikipedia:Wikipedia:NavFrame|Wikipedia:NavFrame]].
   *              [[Wikipedia:Wikipedia:NavFrame|Wikipedia:NavFrame]].
   *  Maintainers: [[Wikipedia:User:R. Koot|User:R. Koot]]
   *  Maintainers: [[Wikipedia:User:R. Koot|R. Koot]]
   */
   */
   
   
var autoCollapse = 2;
var autoCollapse = 2;
var collapseCaption = "hide";
var collapseCaption = "hide";
var expandCaption = "show";
var expandCaption = "show";
   
   
function collapseTable( tableIndex )
  function collapseTable( tableIndex )
{
{
     var Button = document.getElementById( "collapseButton" + tableIndex );
     var Button = document.getElementById( "collapseButton" + tableIndex );
     var Table = document.getElementById( "collapsibleTable" + tableIndex );
     var Table = document.getElementById( "collapsibleTable" + tableIndex );
Line 48: Line 48:
     if ( Button.firstChild.data == collapseCaption ) {
     if ( Button.firstChild.data == collapseCaption ) {
         for ( var i = 1; i < Rows.length; i++ ) {
         for ( var i = 1; i < Rows.length; i++ ) {
             Rows[i].style.display = "none";
             if(Rows[i].parentNode.parentNode.id == ("collapsibleTable" + tableIndex))
            {
                Rows[i].style.display = "none";
            }
         }
         }
         Button.firstChild.data = expandCaption;
         Button.firstChild.data = expandCaption;
     } else {
     } else {
         for ( var i = 1; i < Rows.length; i++ ) {
         for ( var i = 1; i < Rows.length; i++ ) {
             Rows[i].style.display = Rows[0].style.display;
             if(Rows[i].parentNode.parentNode.id == ("collapsibleTable" + tableIndex))
            {
                Rows[i].style.display = Rows[0].style.display;
            }
         }
         }
         Button.firstChild.data = collapseCaption;
         Button.firstChild.data = collapseCaption;
     }
     }
}
}
   
   
function createCollapseButtons()
function createCollapseButtons()
{
{
     var tableIndex = 0;
     var tableIndex = 0;
     var NavigationBoxes = new Object();
     var NavigationBoxes = new Object();
Line 102: Line 108:
         }
         }
     }
     }
}
}
   
   
addOnloadHook( createCollapseButtons );
addOnloadHook( createCollapseButtons );


  /** Dynamic Navigation Bars (experimental) *************************************
  /** Dynamic Navigation Bars (experimental) *************************************
Line 115: Line 121:
   var NavigationBarHide = '[' + collapseCaption + ']';
   var NavigationBarHide = '[' + collapseCaption + ']';
   var NavigationBarShow = '[' + expandCaption + ']';
   var NavigationBarShow = '[' + expandCaption + ']';
 
  // set up max count of Navigation Bars on page,
  // if there are more, all will be hidden
  // NavigationBarShowDefault = 0; // all bars will be hidden
  // NavigationBarShowDefault = 1; // on pages with more than 1 bar all bars will be hidden
  var NavigationBarShowDefault = autoCollapse;
 
 
   // shows and hides content and picture (if available) of navigation bars
   // shows and hides content and picture (if available) of navigation bars
   // Parameters:
   // Parameters:
Line 130: Line 129:
     var NavToggle = document.getElementById("NavToggle" + indexNavigationBar);
     var NavToggle = document.getElementById("NavToggle" + indexNavigationBar);
     var NavFrame = document.getElementById("NavFrame" + indexNavigationBar);
     var NavFrame = document.getElementById("NavFrame" + indexNavigationBar);
 
     if (!NavFrame || !NavToggle) {
     if (!NavFrame || !NavToggle) {
         return false;
         return false;
     }
     }
 
     // if shown now
     // if shown now
     if (NavToggle.firstChild.data == NavigationBarHide) {
     if (NavToggle.firstChild.data == NavigationBarHide) {
Line 150: Line 149:
         }
         }
     NavToggle.firstChild.data = NavigationBarShow;
     NavToggle.firstChild.data = NavigationBarShow;
 
     // if hidden now
     // if hidden now
     } else if (NavToggle.firstChild.data == NavigationBarShow) {
     } else if (NavToggle.firstChild.data == NavigationBarShow) {
Line 168: Line 167:
     }
     }
   }
   }
 
   // adds show/hide-button to navigation bars
   // adds show/hide-button to navigation bars
   function createNavigationBarToggleButton()
   function createNavigationBarToggleButton()
Line 182: Line 181:
         // if found a navigation bar
         // if found a navigation bar
         if (hasClass(NavFrame, "NavFrame")) {
         if (hasClass(NavFrame, "NavFrame")) {
 
             indexNavigationBar++;
             indexNavigationBar++;
             var NavToggle = document.createElement("a");
             var NavToggle = document.createElement("a");
Line 188: Line 187:
             NavToggle.setAttribute('id', 'NavToggle' + indexNavigationBar);
             NavToggle.setAttribute('id', 'NavToggle' + indexNavigationBar);
             NavToggle.setAttribute('href', 'javascript:toggleNavigationBar(' + indexNavigationBar + ');');
             NavToggle.setAttribute('href', 'javascript:toggleNavigationBar(' + indexNavigationBar + ');');
           
             var NavToggleText = document.createTextNode(NavigationBarHide);
             var NavToggleText = document.createTextNode(NavigationBarHide);
            for (
                  var NavChild = NavFrame.firstChild;
                  NavChild != null;
                  NavChild = NavChild.nextSibling
                ) {
                if ( hasClass( NavChild, 'NavPic' ) || hasClass( NavChild, 'NavContent' ) ) {
                    if (NavChild.style.display == 'none') {
                        NavToggleText = document.createTextNode(NavigationBarShow);
                        break;
                    }
                }
            }
             NavToggle.appendChild(NavToggleText);
             NavToggle.appendChild(NavToggleText);
             // Find the NavHead and attach the toggle link (Must be this complicated because Moz's firstChild handling is borked)
             // Find the NavHead and attach the toggle link (Must be this complicated because Moz's firstChild handling is borked)
Line 204: Line 216:
         }
         }
     }
     }
    // if more Navigation Bars found than Default: hide all
    if (NavigationBarShowDefault < indexNavigationBar) {
        for(
                var i=1;
                i<=indexNavigationBar;
                i++
        ) {
            toggleNavigationBar(i);
        }
    }
 
   }
   }
 
   addOnloadHook( createNavigationBarToggleButton );
   addOnloadHook( createNavigationBarToggleButton );


/* Test if an element has a certain class **************************************
/* Test if an element has a certain class **************************************
   *
   *
   * Description: Uses regular expressions and caching for better performance.
   * Description: Uses regular expressions and caching for better performance.
   * Maintainers: [[Wikipedia:User:Mike Dillon|User:Mike Dillon]], [[Wikipedia:User:R. Koot|User:R. Koot]], [[Wikipedia:User:SG|User:SG]]
   * Maintainers: [[Wikipedia:User:Mike Dillon|Mike Dillon]], [[Wikipedia:User:R. Koot|R. Koot]], [[Wikipedia:User:SG|SG]]
   */
   */
   
   
var hasClass = (function () {
var hasClass = (function () {
     var reCache = {};
     var reCache = {};
     return function (element, className) {
     return function (element, className) {
         return (reCache[className] ? reCache[className] : (reCache[className] = new RegExp("(?:\\s|^)" + className + "(?:\\s|$)"))).test(element.className);
         return (reCache[className] ? reCache[className] : (reCache[className] = new RegExp("(?:\\s|^)" + className + "(?:\\s|$)"))).test(element.className);
     };
     };
})();
})();
 
/* Any JavaScript here will be loaded for all users on every page load. */


function hideMainPageTitle(e)
/** includePage ************
* force the loading of another JavaScript file
*
* Maintainer: [[Commons:User:Dschwen]]
*/
function includePage( name )
{
{
    e = (e) ? e : event;
document.write('<script type="text/javascript" src="' + wgScript + '?title='
     var mainPageTitle = "Main Page";
  + name
     var headings = document.getElementsByTagName("h1");
  + '&action=raw&ctype=text/javascript"><\/script>'
    var i, done = false;
);
    for (i = 1; ((!done) && (i <= headings.length)); i++)
}
     {
/* End of includePage */
         if (headings[i - 1].className == "firstHeading")
 
         {
/**
             done = true;
* BattlestarWiki — Main Page JavaScript
            if ((headings[i - 1].innerHTML == mainPageTitle) && (document.getElementById("contentSub").innerHTML == ""))
* Append to MediaWiki:Common.js
             {
*
                 headings[i - 1].style.display = "none";
* Handles:
                 document.getElementById("siteSub").style.display = "none";
*  1. Hero slideshow auto-rotation + article image fetching
                 document.getElementById("contentSub").style.display = "none";
*  2. Featured Article of the Day — daily deterministic pick,
*      excluding Category:Stub_Pages, with extract + thumbnail
*  3. Media wiki file count (from media.battlestarwiki.org)
*  4. Recent changes tab switching (EN / DE / Media wikis)
*  5. Red-link card filtering
*
* All logic is scoped to .bsw-main-page; safe to load globally.
*/
 
( function () {
    'use strict';
 
    var API      = 'https://en.battlestarwiki.org/w/api.php';
     var MEDIA_API = 'https://media.battlestarwiki.org/w/api.php';
     var DE_API    = 'https://de.battlestarwiki.org/w/api.php';
 
    /* ── Shared utilities ─────────────────────────────────────────── */
 
    function dailySeed( dateStr ) {
        var d;
        if ( dateStr ) {
            var p = dateStr.split( '-' );
            d = Date.UTC( +p[0], +p[1] - 1, +p[2] );
        } else {
            var now = new Date();
            d = Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() );
        }
        return Math.floor( d / 86400000 );
    }
 
    function esc( s ) {
        return String( s || '' )
            .replace( /&/g, '&amp;' )
            .replace( /</g, '&lt;' )
            .replace( />/g, '&gt;' )
            .replace( /"/g, '&quot;' );
    }
 
    function apiGetFrom( baseUrl, params ) {
        params.format = 'json';
        params.origin = '*';
        var qs = Object.keys( params )
            .map( function ( k ) { return encodeURIComponent( k ) + '=' + encodeURIComponent( params[k] ); } )
            .join( '&' );
        return fetch( baseUrl + '?' + qs, { headers: { Accept: 'application/json' } } )
            .then( function ( r ) {
                if ( !r.ok ) throw new Error( 'API HTTP ' + r.status );
                return r.json();
            } );
     }
 
    function apiGet( params ) {
         return apiGetFrom( API, params );
    }
 
    /* ════════════════════════════════════════════════════════════════
      1. HERO SLIDESHOW
      Each .bsw-slide carries a data-article attribute with the
      linked article title. JS batch-fetches the page image for
      all slides in one API call and sets it as the slide
      background. Falls back gracefully to the dark overlay if
      no image is found for a given article.
      ════════════════════════════════════════════════════════════════ */
 
    function initHero() {
        var slides = document.querySelectorAll( '.bsw-slide' );
        var dots  = document.querySelectorAll( '.bsw-hero-dot' );
        if ( !slides.length ) return;
 
        var current = 0;
        var total  = slides.length;
        var timer;
 
        function goTo( n ) {
            slides[ current ].classList.remove( 'bsw-active' );
            dots[ current ] && dots[ current ].classList.remove( 'bsw-active' );
            current = ( n + total ) % total;
            slides[ current ].classList.add( 'bsw-active' );
            dots[ current ] && dots[ current ].classList.add( 'bsw-active' );
            resetTimer();
        }
 
        function resetTimer() {
            clearInterval( timer );
            timer = setInterval( function () { goTo( current + 1 ); }, 6000 );
        }
 
        /* Expose globally for inline onclick on span[role=button] elements */
        window.bswGoSlide  = function ( n ) { goTo( n ); };
        window.bswPrevSlide = function () { goTo( current - 1 ); };
        window.bswNextSlide = function () { goTo( current + 1 ); };
 
         /* Also wire by event listener as fallback */
        dots.forEach( function ( dot, i ) {
             dot.addEventListener( 'click', function () { goTo( i ); } );
        } );
 
        var prevBtn = document.getElementById( 'bsw-hero-prev' );
        var nextBtn = document.getElementById( 'bsw-hero-next' );
        if ( prevBtn ) prevBtn.addEventListener( 'click', function () { goTo( current - 1 ); } );
        if ( nextBtn ) nextBtn.addEventListener( 'click', function () { goTo( current + 1 ); } );
 
        /* pause on hover */
        var hero = document.querySelector( '.bsw-hero' );
        if ( hero ) {
            hero.addEventListener( 'mouseenter', function () { clearInterval( timer ); } );
            hero.addEventListener( 'mouseleave', resetTimer );
        }
 
        /* ── Fetch article images for all slides ── */
        var titles = [];
        slides.forEach( function ( slide ) {
            var t = slide.dataset.article;
            if ( t ) titles.push( t );
        } );
 
        if ( titles.length ) {
            apiGet( {
                action:      'query',
                titles:      titles.join( '|' ),
                prop:        'pageimages',
                piprop:      'thumbnail|original',
                pithumbsize: '1200'  /* large enough for a hero background */
            } ).then( function ( data ) {
                var pages = data.query.pages;
 
                /* Build a map of title → image URL.
                  Prefer the full original; fall back to thumbnail. */
                var imageMap = {};
                Object.values( pages ).forEach( function ( page ) {
                    if ( !page.title ) return;
                    var img = ( page.original && page.original.source ) ||
                              ( page.thumbnail && page.thumbnail.source ) ||
                              '';
                    if ( img ) imageMap[ page.title ] = img;
                } );
 
                /* Apply images to slides */
                slides.forEach( function ( slide ) {
                    var articleTitle = slide.dataset.article;
                    var imgUrl = articleTitle && imageMap[ articleTitle ];
                    if ( !imgUrl ) return;
 
                    /* Insert an <img> as the slide background layer,
                      behind the existing .bsw-slide-overlay and content.
                      CSS handles object-fit: cover sizing. */
                    var bg = slide.querySelector( '.bsw-slide-bg' );
                    if ( bg ) {
                        var img = document.createElement( 'img' );
                        img.src = imgUrl;
                        img.alt = '';
                        img.className = 'bsw-slide-bg-img';
                        bg.appendChild( img );
                    }
                } );
            } ).catch( function () {
                /* Silently fail — dark gradient fallback is already in CSS */
            } );
        }
 
        resetTimer();
    }
 
    /* ════════════════════════════════════════════════════════════════
      2. FEATURED ARTICLE OF THE DAY
      Daily deterministic pick from all mainspace articles,
      excluding Category:Stub_Pages.
      ════════════════════════════════════════════════════════════════ */
 
    function initFeatured() {
        var container = document.getElementById( 'bsw-featured-inner' );
        if ( !container ) return;
 
        var seed = dailySeed();
 
        /* We fetch batches of 20 random articles, check for stubs,
          and pick the first clean one. Two batches covers ~99.99%
          of cases given only ~9% of articles are stubs. */
        fetchCandidateBatch( seed )
            .then( function ( pages ) {
                var clean = pages.filter( function ( p ) {
                    /* If categories array is present and contains Stub_Pages,
                      exclude. Absence of categories key means not a stub. */
                    if ( !p.categories ) return true;
                    return !p.categories.some( function ( c ) {
                        return c.title === 'Category:Stub_Pages';
                    } );
                } );
 
                if ( !clean.length ) {
                    /* Extremely unlikely — fall back to second batch */
                    return fetchCandidateBatch( seed + 1 );
                }
                return clean;
             } )
            .then( function ( pages ) {
                 /* Pick deterministically from the clean pool */
                var pick = pages[ seed % pages.length ];
                renderFeatured( container, pick );
            } )
            .catch( function ( e ) {
                container.innerHTML =
                    '<div class="bsw-error">Could not load featured article. ' + esc( e.message ) + '</div>';
            } );
    }
 
    /**
    * Fetch 20 random mainspace articles with extract, thumbnail,
    * and stub-category membership check in a single API call.
    * Uses rnstart derived from seed for determinism.
    */
    function fetchCandidateBatch( seed ) {
        /* Step 1: get 20 random page IDs */
        return apiGet( {
            action:      'query',
            list:        'random',
            rnnamespace: '0',
            rnlimit:    '20'
        } ).then( function ( data ) {
            var ids = data.query.random.map( function ( p ) { return p.id; } ).join( '|' );
 
            /* Step 2: batch fetch extract + thumbnail + stub check */
            return apiGet( {
                 action:        'query',
                pageids:      ids,
                prop:          'extracts|pageimages|categories',
                exintro:      '1',
                exchars:      '600',
                exsectionformat: 'plain',
                piprop:        'thumbnail',
                pithumbsize:  '320',
                clcategories:  'Category:Stub_Pages',
                cllimit:      '1'
            } );
        } ).then( function ( data ) {
            return Object.values( data.query.pages );
        } );
    }
 
    function renderFeatured( container, page ) {
        var title  = page.title || '';
        var url    = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
        var extract = page.extract || '';
        var thumb  = page.thumbnail ? page.thumbnail.source : '';
 
        var thumbHtml;
        if ( thumb ) {
            thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '">';
        } else {
            thumbHtml = '<img class="bsw-fa-thumb" src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki">';
        }
 
        container.innerHTML =
            thumbHtml +
            '<div>' +
                '<div class="bsw-fa-title">' +
                    '<a href="' + esc( url ) + '">' + esc( title ) + '</a>' +
                '</div>' +
                '<div class="bsw-fa-extract">' + extract + '</div>' +
                '<div style="margin-top:0.375rem">' +
                    '<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--color-link)">Read more \u2192</a>' +
                 '</div>' +
            '</div>';
 
        /* Update the "Read more" span in the card header to link to the article */
        var hdrLink = document.getElementById( 'bsw-featured-link' );
        if ( hdrLink ) {
            /* Replace the span with a proper anchor now that we have a URL */
            var a = document.createElement( 'a' );
            a.href = url;
            a.textContent = 'Read more \u2192';
            hdrLink.parentNode.replaceChild( a, hdrLink );
        }
    }
 
    /**
    * Fetch file count from media.battlestarwiki.org and populate
    * the #bsw-stat-files span in the statistics card.
    */
    function initMediaFileCount() {
        var el = document.getElementById( 'bsw-stat-files' );
        if ( !el ) return;
 
        apiGetFrom( MEDIA_API, {
            action: 'query',
            meta:  'siteinfo',
            siprop: 'statistics'
        } ).then( function ( data ) {
            var files = data.query.statistics.images || 0;
            el.textContent = files.toLocaleString();
        } ).catch( function () {
            el.textContent = '—';
        } );
    }
 
    /**
    * Recent changes tab switching.
    * Fetches live recent changes from the selected wiki's API
    * and replaces the #bsw-recent-list content.
    */
    var rcCurrentWiki = 'all';
    var rcCache = {};
 
    function loadRecentChanges( wiki ) {
        var list = document.getElementById( 'bsw-recent-list' );
        if ( !list ) return;
 
        if ( rcCache[ wiki ] ) {
            list.innerHTML = rcCache[ wiki ];
            return;
        }
 
        /* 'all' just shows the existing Special:RecentChanges transclude */
        if ( wiki === 'all' ) {
            if ( rcCache.all ) {
                list.innerHTML = rcCache.all;
             }
             }
            return;
         }
         }
        list.innerHTML = '<li style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Loading…</li>';
        var apiMap = {
            en:    API,
            de:    DE_API,
            media: MEDIA_API
        };
        var base = apiMap[ wiki ];
        if ( !base ) return;
        apiGetFrom( base, {
            action:      'query',
            list:        'recentchanges',
            rcprop:      'title|timestamp|user|sizes',
            rcnamespace: '0',
            rclimit:    '8',
            rctype:      'edit|new'
        } ).then( function ( data ) {
            var changes = data.query.recentchanges || [];
            if ( !changes.length ) {
                list.innerHTML = '<li style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">No recent changes.</li>';
                return;
            }
            var baseUrl = {
                en:    'https://en.battlestarwiki.org/wiki/',
                de:    'https://de.battlestarwiki.org/wiki/',
                media: 'https://media.battlestarwiki.org/wiki/'
            }[ wiki ];
            var html = changes.map( function ( rc ) {
                var title = rc.title;
                var url  = baseUrl + encodeURIComponent( title.replace( / /g, '_' ) );
                var ts    = new Date( rc.timestamp );
                var ago  = timeAgo( ts );
                return '<li class="ri">' +
                    '<a href="' + esc( url ) + '" target="_blank">' + esc( title ) + '</a>' +
                    '<span class="bsw-recent-time">' + esc( ago ) + '</span>' +
                    '</li>';
            } ).join( '' );
            rcCache[ wiki ] = html;
            list.innerHTML = html;
        } ).catch( function () {
            list.innerHTML = '<li style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Could not load recent changes.</li>';
        } );
    }
    function timeAgo( date ) {
        var diff = Math.floor( ( Date.now() - date.getTime() ) / 1000 );
        if ( diff < 60 )  return diff + 's ago';
        if ( diff < 3600 ) return Math.floor( diff / 60 ) + ' min ago';
        if ( diff < 86400 ) return Math.floor( diff / 3600 ) + ' hr ago';
        return Math.floor( diff / 86400 ) + ' days ago';
     }
     }
}
 
window.onload = hideMainPageTitle;
    /* ── Entry point ──────────────────────────────────────────────── */
 
    /**
    * Hide any .bsw-card whose .bsw-card-body contains nothing but
    * a red link (missing page transclusion). This catches subpages
    * like BattlestarWiki:Did_you_know, OTD/Month_Day etc. that
    * haven't been created yet.
    */
    function filterRedlinkCards() {
        document.querySelectorAll( '.bsw-main-page .bsw-card' ).forEach( function ( card ) {
            var body = card.querySelector( '.bsw-card-body' );
            if ( !body ) return;
 
            var text = body.textContent.trim();
            var links = body.querySelectorAll( 'a' );
 
            /* Hide if body text is empty */
            if ( text === '' ) {
                card.style.display = 'none';
                return;
            }
 
            /* Hide if body contains ONLY red links and no other text/elements */
            var redLinks = body.querySelectorAll( 'a.new' );
            if ( redLinks.length > 0 && redLinks.length === links.length ) {
                /* All links are red — check there's no other meaningful text */
                var clone = body.cloneNode( true );
                clone.querySelectorAll( 'a' ).forEach( function ( a ) { a.remove(); } );
                if ( clone.textContent.trim() === '' ) {
                    card.style.display = 'none';
                }
            }
        } );
    }
 
    /* bswSetTab — exposed globally for inline onclick on span tabs */
    window.bswSetTab = function ( el, wiki ) {
        document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) {
            t.classList.remove( 'bsw-active' );
        } );
        el.classList.add( 'bsw-active' );
        rcCurrentWiki = wiki;
        loadRecentChanges( wiki );
    };
 
    mw.hook( 'wikipage.content' ).add( function () {
        if ( !document.querySelector( '.bsw-main-page' ) ) return;
        initHero();
        initFeatured();
        initMediaFileCount();
        filterRedlinkCards();
        /* Cache the initial 'all' RC content so tab switching can restore it */
        var list = document.getElementById( 'bsw-recent-list' );
        if ( list ) rcCache.all = list.innerHTML;
    } );
 
}() );

Latest revision as of 16:39, 11 April 2026

 
  /** Interwiki links to featured articles ***************************************
  *
  *  Description: Highlights interwiki links to featured articles (or
  *               equivalents) by changing the bullet before the interwiki link
  *               into a star.
  *  Maintainers: [[Wikipedia:User:R. Koot|R. Koot]]
  */
 
function LinkFA() 
{
     if ( document.getElementById( "p-lang" ) ) {
         var InterwikiLinks = document.getElementById( "p-lang" ).getElementsByTagName( "li" );
 
         for ( var i = 0; i < InterwikiLinks.length; i++ ) {
             if ( document.getElementById( InterwikiLinks[i].className + "-fa" ) ) {
                 InterwikiLinks[i].className += " FA"
                 InterwikiLinks[i].title = "This is a featured article in another language.";
             }
         }
     }
}
 
addOnloadHook( LinkFA );
 
/** Collapsible tables *********************************************************
  *
  *  Description: Allows tables to be collapsed, showing only the header. See
  *               [[Wikipedia:Wikipedia:NavFrame|Wikipedia:NavFrame]].
  *  Maintainers: [[Wikipedia:User:R. Koot|R. Koot]]
  */
 
var autoCollapse = 2;
var collapseCaption = "hide";
var expandCaption = "show";
 
  function collapseTable( tableIndex )
{
     var Button = document.getElementById( "collapseButton" + tableIndex );
     var Table = document.getElementById( "collapsibleTable" + tableIndex );
 
     if ( !Table || !Button ) {
         return false;
     }
 
     var Rows = Table.getElementsByTagName( "tr" ); 
 
     if ( Button.firstChild.data == collapseCaption ) {
         for ( var i = 1; i < Rows.length; i++ ) {
             if(Rows[i].parentNode.parentNode.id == ("collapsibleTable" + tableIndex))
             {
                 Rows[i].style.display = "none";
             }
         }
         Button.firstChild.data = expandCaption;
     } else {
         for ( var i = 1; i < Rows.length; i++ ) {
             if(Rows[i].parentNode.parentNode.id == ("collapsibleTable" + tableIndex))
             {
                 Rows[i].style.display = Rows[0].style.display;
             }
         }
         Button.firstChild.data = collapseCaption;
     }
}
 
function createCollapseButtons()
{
     var tableIndex = 0;
     var NavigationBoxes = new Object();
     var Tables = document.getElementsByTagName( "table" );
 
     for ( var i = 0; i < Tables.length; i++ ) {
         if ( hasClass( Tables[i], "collapsible" ) ) {
             NavigationBoxes[ tableIndex ] = Tables[i];
             Tables[i].setAttribute( "id", "collapsibleTable" + tableIndex );
 
             var Button     = document.createElement( "span" );
             var ButtonLink = document.createElement( "a" );
             var ButtonText = document.createTextNode( collapseCaption );
 
             Button.style.styleFloat = "right";
             Button.style.cssFloat = "right";
             Button.style.fontWeight = "normal";
             Button.style.textAlign = "right";
             Button.style.width = "6em";
 
             ButtonLink.setAttribute( "id", "collapseButton" + tableIndex );
             ButtonLink.setAttribute( "href", "javascript:collapseTable(" + tableIndex + ");" );
             ButtonLink.appendChild( ButtonText );
 
             Button.appendChild( document.createTextNode( "[" ) );
             Button.appendChild( ButtonLink );
             Button.appendChild( document.createTextNode( "]" ) );
 
             var Header = Tables[i].getElementsByTagName( "tr" )[0].getElementsByTagName( "th" )[0];
             /* only add button and increment count if there is a header row to work with */
             if (Header) {
                 Header.insertBefore( Button, Header.childNodes[0] );
                 tableIndex++;
             }
         }
     }
 
     for ( var i = 0;  i < tableIndex; i++ ) {
         if ( hasClass( NavigationBoxes[i], "collapsed" ) || ( tableIndex >= autoCollapse && hasClass( NavigationBoxes[i], "autocollapse" ) ) ) {
             collapseTable( i );
         }
     }
}
 
addOnloadHook( createCollapseButtons );

 /** Dynamic Navigation Bars (experimental) *************************************
  *
  *  Description: See [[Wikipedia:NavFrame]].
  *  Maintainers: UNMAINTAINED
  */
 
  // set up the words in your language
  var NavigationBarHide = '[' + collapseCaption + ']';
  var NavigationBarShow = '[' + expandCaption + ']';
 
  // shows and hides content and picture (if available) of navigation bars
  // Parameters:
  //     indexNavigationBar: the index of navigation bar to be toggled
  function toggleNavigationBar(indexNavigationBar)
  {
     var NavToggle = document.getElementById("NavToggle" + indexNavigationBar);
     var NavFrame = document.getElementById("NavFrame" + indexNavigationBar);
 
     if (!NavFrame || !NavToggle) {
         return false;
     }
 
     // if shown now
     if (NavToggle.firstChild.data == NavigationBarHide) {
         for (
                 var NavChild = NavFrame.firstChild;
                 NavChild != null;
                 NavChild = NavChild.nextSibling
             ) {
             if ( hasClass( NavChild, 'NavPic' ) ) {
                 NavChild.style.display = 'none';
             }
             if ( hasClass( NavChild, 'NavContent') ) {
                 NavChild.style.display = 'none';
             }
         }
     NavToggle.firstChild.data = NavigationBarShow;
 
     // if hidden now
     } else if (NavToggle.firstChild.data == NavigationBarShow) {
         for (
                 var NavChild = NavFrame.firstChild;
                 NavChild != null;
                 NavChild = NavChild.nextSibling
             ) {
             if (hasClass(NavChild, 'NavPic')) {
                 NavChild.style.display = 'block';
             }
             if (hasClass(NavChild, 'NavContent')) {
                 NavChild.style.display = 'block';
             }
         }
     NavToggle.firstChild.data = NavigationBarHide;
     }
  }
 
  // adds show/hide-button to navigation bars
  function createNavigationBarToggleButton()
  {
     var indexNavigationBar = 0;
     // iterate over all < div >-elements 
     var divs = document.getElementsByTagName("div");
     for(
             var i=0; 
             NavFrame = divs[i]; 
             i++
         ) {
         // if found a navigation bar
         if (hasClass(NavFrame, "NavFrame")) {
 
             indexNavigationBar++;
             var NavToggle = document.createElement("a");
             NavToggle.className = 'NavToggle';
             NavToggle.setAttribute('id', 'NavToggle' + indexNavigationBar);
             NavToggle.setAttribute('href', 'javascript:toggleNavigationBar(' + indexNavigationBar + ');');
 
             var NavToggleText = document.createTextNode(NavigationBarHide);
             for (
                  var NavChild = NavFrame.firstChild;
                  NavChild != null;
                  NavChild = NavChild.nextSibling
                 ) {
                 if ( hasClass( NavChild, 'NavPic' ) || hasClass( NavChild, 'NavContent' ) ) {
                     if (NavChild.style.display == 'none') {
                         NavToggleText = document.createTextNode(NavigationBarShow);
                         break;
                     }
                 }
             }
 
             NavToggle.appendChild(NavToggleText);
             // Find the NavHead and attach the toggle link (Must be this complicated because Moz's firstChild handling is borked)
             for(
               var j=0; 
               j < NavFrame.childNodes.length; 
               j++
             ) {
               if (hasClass(NavFrame.childNodes[j], "NavHead")) {
                 NavFrame.childNodes[j].appendChild(NavToggle);
               }
             }
             NavFrame.setAttribute('id', 'NavFrame' + indexNavigationBar);
         }
     }
  }
 
  addOnloadHook( createNavigationBarToggleButton );

/* Test if an element has a certain class **************************************
  *
  * Description: Uses regular expressions and caching for better performance.
  * Maintainers: [[Wikipedia:User:Mike Dillon|Mike Dillon]], [[Wikipedia:User:R. Koot|R. Koot]], [[Wikipedia:User:SG|SG]]
  */
 
var hasClass = (function () {
     var reCache = {};
     return function (element, className) {
         return (reCache[className] ? reCache[className] : (reCache[className] = new RegExp("(?:\\s|^)" + className + "(?:\\s|$)"))).test(element.className);
     };
})();

/* Any JavaScript here will be loaded for all users on every page load. */

/** includePage ************
 * force the loading of another JavaScript file
 *
 * Maintainer: [[Commons:User:Dschwen]]
 */
 
function includePage( name )
{
 document.write('<script type="text/javascript" src="' + wgScript + '?title='
  + name 
  + '&action=raw&ctype=text/javascript"><\/script>' 
 );
}
/* End of includePage */

/**
 * BattlestarWiki — Main Page JavaScript
 * Append to MediaWiki:Common.js
 *
 * Handles:
 *   1. Hero slideshow auto-rotation + article image fetching
 *   2. Featured Article of the Day — daily deterministic pick,
 *      excluding Category:Stub_Pages, with extract + thumbnail
 *   3. Media wiki file count (from media.battlestarwiki.org)
 *   4. Recent changes tab switching (EN / DE / Media wikis)
 *   5. Red-link card filtering
 *
 * All logic is scoped to .bsw-main-page; safe to load globally.
 */

( function () {
    'use strict';

    var API       = 'https://en.battlestarwiki.org/w/api.php';
    var MEDIA_API = 'https://media.battlestarwiki.org/w/api.php';
    var DE_API    = 'https://de.battlestarwiki.org/w/api.php';

    /* ── Shared utilities ─────────────────────────────────────────── */

    function dailySeed( dateStr ) {
        var d;
        if ( dateStr ) {
            var p = dateStr.split( '-' );
            d = Date.UTC( +p[0], +p[1] - 1, +p[2] );
        } else {
            var now = new Date();
            d = Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() );
        }
        return Math.floor( d / 86400000 );
    }

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

    function apiGetFrom( baseUrl, params ) {
        params.format = 'json';
        params.origin = '*';
        var qs = Object.keys( params )
            .map( function ( k ) { return encodeURIComponent( k ) + '=' + encodeURIComponent( params[k] ); } )
            .join( '&' );
        return fetch( baseUrl + '?' + qs, { headers: { Accept: 'application/json' } } )
            .then( function ( r ) {
                if ( !r.ok ) throw new Error( 'API HTTP ' + r.status );
                return r.json();
            } );
    }

    function apiGet( params ) {
        return apiGetFrom( API, params );
    }

    /* ════════════════════════════════════════════════════════════════
       1. HERO SLIDESHOW
       Each .bsw-slide carries a data-article attribute with the
       linked article title. JS batch-fetches the page image for
       all slides in one API call and sets it as the slide
       background. Falls back gracefully to the dark overlay if
       no image is found for a given article.
       ════════════════════════════════════════════════════════════════ */

    function initHero() {
        var slides = document.querySelectorAll( '.bsw-slide' );
        var dots   = document.querySelectorAll( '.bsw-hero-dot' );
        if ( !slides.length ) return;

        var current = 0;
        var total   = slides.length;
        var timer;

        function goTo( n ) {
            slides[ current ].classList.remove( 'bsw-active' );
            dots[ current ] && dots[ current ].classList.remove( 'bsw-active' );
            current = ( n + total ) % total;
            slides[ current ].classList.add( 'bsw-active' );
            dots[ current ] && dots[ current ].classList.add( 'bsw-active' );
            resetTimer();
        }

        function resetTimer() {
            clearInterval( timer );
            timer = setInterval( function () { goTo( current + 1 ); }, 6000 );
        }

        /* Expose globally for inline onclick on span[role=button] elements */
        window.bswGoSlide  = function ( n ) { goTo( n ); };
        window.bswPrevSlide = function () { goTo( current - 1 ); };
        window.bswNextSlide = function () { goTo( current + 1 ); };

        /* Also wire by event listener as fallback */
        dots.forEach( function ( dot, i ) {
            dot.addEventListener( 'click', function () { goTo( i ); } );
        } );

        var prevBtn = document.getElementById( 'bsw-hero-prev' );
        var nextBtn = document.getElementById( 'bsw-hero-next' );
        if ( prevBtn ) prevBtn.addEventListener( 'click', function () { goTo( current - 1 ); } );
        if ( nextBtn ) nextBtn.addEventListener( 'click', function () { goTo( current + 1 ); } );

        /* pause on hover */
        var hero = document.querySelector( '.bsw-hero' );
        if ( hero ) {
            hero.addEventListener( 'mouseenter', function () { clearInterval( timer ); } );
            hero.addEventListener( 'mouseleave', resetTimer );
        }

        /* ── Fetch article images for all slides ── */
        var titles = [];
        slides.forEach( function ( slide ) {
            var t = slide.dataset.article;
            if ( t ) titles.push( t );
        } );

        if ( titles.length ) {
            apiGet( {
                action:      'query',
                titles:      titles.join( '|' ),
                prop:        'pageimages',
                piprop:      'thumbnail|original',
                pithumbsize: '1200'   /* large enough for a hero background */
            } ).then( function ( data ) {
                var pages = data.query.pages;

                /* Build a map of title → image URL.
                   Prefer the full original; fall back to thumbnail. */
                var imageMap = {};
                Object.values( pages ).forEach( function ( page ) {
                    if ( !page.title ) return;
                    var img = ( page.original && page.original.source ) ||
                              ( page.thumbnail && page.thumbnail.source ) ||
                              '';
                    if ( img ) imageMap[ page.title ] = img;
                } );

                /* Apply images to slides */
                slides.forEach( function ( slide ) {
                    var articleTitle = slide.dataset.article;
                    var imgUrl = articleTitle && imageMap[ articleTitle ];
                    if ( !imgUrl ) return;

                    /* Insert an <img> as the slide background layer,
                       behind the existing .bsw-slide-overlay and content.
                       CSS handles object-fit: cover sizing. */
                    var bg = slide.querySelector( '.bsw-slide-bg' );
                    if ( bg ) {
                        var img = document.createElement( 'img' );
                        img.src = imgUrl;
                        img.alt = '';
                        img.className = 'bsw-slide-bg-img';
                        bg.appendChild( img );
                    }
                } );
            } ).catch( function () {
                /* Silently fail — dark gradient fallback is already in CSS */
            } );
        }

        resetTimer();
    }

    /* ════════════════════════════════════════════════════════════════
       2. FEATURED ARTICLE OF THE DAY
       Daily deterministic pick from all mainspace articles,
       excluding Category:Stub_Pages.
       ════════════════════════════════════════════════════════════════ */

    function initFeatured() {
        var container = document.getElementById( 'bsw-featured-inner' );
        if ( !container ) return;

        var seed = dailySeed();

        /* We fetch batches of 20 random articles, check for stubs,
           and pick the first clean one. Two batches covers ~99.99%
           of cases given only ~9% of articles are stubs. */
        fetchCandidateBatch( seed )
            .then( function ( pages ) {
                var clean = pages.filter( function ( p ) {
                    /* If categories array is present and contains Stub_Pages,
                       exclude. Absence of categories key means not a stub. */
                    if ( !p.categories ) return true;
                    return !p.categories.some( function ( c ) {
                        return c.title === 'Category:Stub_Pages';
                    } );
                } );

                if ( !clean.length ) {
                    /* Extremely unlikely — fall back to second batch */
                    return fetchCandidateBatch( seed + 1 );
                }
                return clean;
            } )
            .then( function ( pages ) {
                /* Pick deterministically from the clean pool */
                var pick = pages[ seed % pages.length ];
                renderFeatured( container, pick );
            } )
            .catch( function ( e ) {
                container.innerHTML =
                    '<div class="bsw-error">Could not load featured article. ' + esc( e.message ) + '</div>';
            } );
    }

    /**
     * Fetch 20 random mainspace articles with extract, thumbnail,
     * and stub-category membership check in a single API call.
     * Uses rnstart derived from seed for determinism.
     */
    function fetchCandidateBatch( seed ) {
        /* Step 1: get 20 random page IDs */
        return apiGet( {
            action:      'query',
            list:        'random',
            rnnamespace: '0',
            rnlimit:     '20'
        } ).then( function ( data ) {
            var ids = data.query.random.map( function ( p ) { return p.id; } ).join( '|' );

            /* Step 2: batch fetch extract + thumbnail + stub check */
            return apiGet( {
                action:        'query',
                pageids:       ids,
                prop:          'extracts|pageimages|categories',
                exintro:       '1',
                exchars:       '600',
                exsectionformat: 'plain',
                piprop:        'thumbnail',
                pithumbsize:   '320',
                clcategories:  'Category:Stub_Pages',
                cllimit:       '1'
            } );
        } ).then( function ( data ) {
            return Object.values( data.query.pages );
        } );
    }

    function renderFeatured( container, page ) {
        var title   = page.title || '';
        var url     = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
        var extract = page.extract || '';
        var thumb   = page.thumbnail ? page.thumbnail.source : '';

        var thumbHtml;
        if ( thumb ) {
            thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '">';
        } else {
            thumbHtml = '<img class="bsw-fa-thumb" src="https://en.battlestarwiki.org/resources/BSGWIKILOGO.png" alt="Battlestar Wiki">';
        }

        container.innerHTML =
            thumbHtml +
            '<div>' +
                '<div class="bsw-fa-title">' +
                    '<a href="' + esc( url ) + '">' + esc( title ) + '</a>' +
                '</div>' +
                '<div class="bsw-fa-extract">' + extract + '</div>' +
                '<div style="margin-top:0.375rem">' +
                    '<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--color-link)">Read more \u2192</a>' +
                '</div>' +
            '</div>';

        /* Update the "Read more" span in the card header to link to the article */
        var hdrLink = document.getElementById( 'bsw-featured-link' );
        if ( hdrLink ) {
            /* Replace the span with a proper anchor now that we have a URL */
            var a = document.createElement( 'a' );
            a.href = url;
            a.textContent = 'Read more \u2192';
            hdrLink.parentNode.replaceChild( a, hdrLink );
        }
    }

    /**
     * Fetch file count from media.battlestarwiki.org and populate
     * the #bsw-stat-files span in the statistics card.
     */
    function initMediaFileCount() {
        var el = document.getElementById( 'bsw-stat-files' );
        if ( !el ) return;

        apiGetFrom( MEDIA_API, {
            action: 'query',
            meta:   'siteinfo',
            siprop: 'statistics'
        } ).then( function ( data ) {
            var files = data.query.statistics.images || 0;
            el.textContent = files.toLocaleString();
        } ).catch( function () {
            el.textContent = '—';
        } );
    }

    /**
     * Recent changes tab switching.
     * Fetches live recent changes from the selected wiki's API
     * and replaces the #bsw-recent-list content.
     */
    var rcCurrentWiki = 'all';
    var rcCache = {};

    function loadRecentChanges( wiki ) {
        var list = document.getElementById( 'bsw-recent-list' );
        if ( !list ) return;

        if ( rcCache[ wiki ] ) {
            list.innerHTML = rcCache[ wiki ];
            return;
        }

        /* 'all' just shows the existing Special:RecentChanges transclude */
        if ( wiki === 'all' ) {
            if ( rcCache.all ) {
                list.innerHTML = rcCache.all;
            }
            return;
        }

        list.innerHTML = '<li style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Loading…</li>';

        var apiMap = {
            en:    API,
            de:    DE_API,
            media: MEDIA_API
        };
        var base = apiMap[ wiki ];
        if ( !base ) return;

        apiGetFrom( base, {
            action:      'query',
            list:        'recentchanges',
            rcprop:      'title|timestamp|user|sizes',
            rcnamespace: '0',
            rclimit:     '8',
            rctype:      'edit|new'
        } ).then( function ( data ) {
            var changes = data.query.recentchanges || [];
            if ( !changes.length ) {
                list.innerHTML = '<li style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">No recent changes.</li>';
                return;
            }

            var baseUrl = {
                en:    'https://en.battlestarwiki.org/wiki/',
                de:    'https://de.battlestarwiki.org/wiki/',
                media: 'https://media.battlestarwiki.org/wiki/'
            }[ wiki ];

            var html = changes.map( function ( rc ) {
                var title = rc.title;
                var url   = baseUrl + encodeURIComponent( title.replace( / /g, '_' ) );
                var ts    = new Date( rc.timestamp );
                var ago   = timeAgo( ts );
                return '<li class="ri">' +
                    '<a href="' + esc( url ) + '" target="_blank">' + esc( title ) + '</a>' +
                    '<span class="bsw-recent-time">' + esc( ago ) + '</span>' +
                    '</li>';
            } ).join( '' );

            rcCache[ wiki ] = html;
            list.innerHTML = html;
        } ).catch( function () {
            list.innerHTML = '<li style="color:var(--color-base--subtle);font-size:0.8125rem;padding:0.5rem 0">Could not load recent changes.</li>';
        } );
    }

    function timeAgo( date ) {
        var diff = Math.floor( ( Date.now() - date.getTime() ) / 1000 );
        if ( diff < 60 )   return diff + 's ago';
        if ( diff < 3600 ) return Math.floor( diff / 60 ) + ' min ago';
        if ( diff < 86400 ) return Math.floor( diff / 3600 ) + ' hr ago';
        return Math.floor( diff / 86400 ) + ' days ago';
    }

    /* ── Entry point ──────────────────────────────────────────────── */

    /**
     * Hide any .bsw-card whose .bsw-card-body contains nothing but
     * a red link (missing page transclusion). This catches subpages
     * like BattlestarWiki:Did_you_know, OTD/Month_Day etc. that
     * haven't been created yet.
     */
    function filterRedlinkCards() {
        document.querySelectorAll( '.bsw-main-page .bsw-card' ).forEach( function ( card ) {
            var body = card.querySelector( '.bsw-card-body' );
            if ( !body ) return;

            var text = body.textContent.trim();
            var links = body.querySelectorAll( 'a' );

            /* Hide if body text is empty */
            if ( text === '' ) {
                card.style.display = 'none';
                return;
            }

            /* Hide if body contains ONLY red links and no other text/elements */
            var redLinks = body.querySelectorAll( 'a.new' );
            if ( redLinks.length > 0 && redLinks.length === links.length ) {
                /* All links are red — check there's no other meaningful text */
                var clone = body.cloneNode( true );
                clone.querySelectorAll( 'a' ).forEach( function ( a ) { a.remove(); } );
                if ( clone.textContent.trim() === '' ) {
                    card.style.display = 'none';
                }
            }
        } );
    }

    /* bswSetTab — exposed globally for inline onclick on span tabs */
    window.bswSetTab = function ( el, wiki ) {
        document.querySelectorAll( '.bsw-wtab' ).forEach( function ( t ) {
            t.classList.remove( 'bsw-active' );
        } );
        el.classList.add( 'bsw-active' );
        rcCurrentWiki = wiki;
        loadRecentChanges( wiki );
    };

    mw.hook( 'wikipage.content' ).add( function () {
        if ( !document.querySelector( '.bsw-main-page' ) ) return;
        initHero();
        initFeatured();
        initMediaFileCount();
        filterRedlinkCards();
        /* Cache the initial 'all' RC content so tab switching can restore it */
        var list = document.getElementById( 'bsw-recent-list' );
        if ( list ) rcCache.all = list.innerHTML;
    } );

}() );