MediaWiki:Common.js

From Guild Wars 2 Wiki
Jump to navigationJump to search

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* MEDIAWIKI:COMMON.JS */
console.log('[[MediaWiki:Common.js]] has loaded (revision 2023-08-20 10:36).');

// Wrapper to address Extension:MobileFrontend deficiencies
// Note that the retrieved skin will be lowercase but this will be OK for requests
var sk = mw.config.get('skin');

if ('globalVariableFromMobileJS' in window) {
    console.log('Common: mw.util function - scenario 1.');
    // Address case for global variable has been set by [[MediaWiki:Mobile.js]] directing to this script
    // Add [[MediaWiki:Common.css]] stylesheet
    mw.loader.load( '/index.php?title=MediaWiki:Common.css&action=raw&ctype=text/css', 'text/css' );

    // Add skin specific stylesheet
    mw.loader.load( '/index.php?title=MediaWiki:' + sk + '.css&action=raw&ctype=text/css', 'text/css' );
    
    loadCommonJS();
    
} else if (typeof mw.config.get('wgMFAmc') === 'object' && sk !== 'minerva') {
    console.log('Common: mw.util function - scenario 2.');
    // Address case for desktop and non-minerva skin
    loadCommonJS();
    
} else if (typeof mw.config.get('wgMFAmc') === 'object' && sk === 'minerva') {
    console.log('Common: mw.util function - scenario 3.');
    // Address case for desktop and minerva
    // Remove common.css stylesheet
    document.querySelector("link[href='/load.php?lang=en&modules=site.styles&only=styles&skin=minerva']").remove();
    
    // skip loading rest of JS
} else {
    console.log('Common: mw.util function - scenario 4.');
    // Address remaining desktop platform options with mobile display
    // skip loading rest of JS
}

function loadCommonJS() {
    console.log('Common: loadCommonJS function loaded.');
    // rest of common.js goes here
if (document.readyState === 'loading') {
    // Loading hasn't finished yet (status loading)
    console.log('Common: loadCommonJS function - dom content at loading state, adding event listener to trigger commonJS function.', document.readyState);
    document.addEventListener('DOMContentLoaded', commonJS);
} else {
    // 'DOMContentLoaded' has already fired
    console.log('Common: loadCommonJS function - dom content either at complete or interactive state, proceeding to commonJS function.');
    commonJS();
}

function commonJS() {

    console.log('Common: commonJS function executed.');

    // Semantic Mediawiki Gallery overlay bug fix
    $.support.opacity = true;
    
    /**
     * Additional scripts specified here and on other MediaWiki pages
     *   collapsible tables from [[MediaWiki:CollapsibleTables.js]]
     *   ingame chatlink searches from [[MediaWiki:ChatLinkSearch.js]]
     */
    // Scripts to use when viewing articles
    if (mw.config.get('wgIsArticle') || window.location.href.indexOf('action=submit') > -1 || mw.config.get('wgNamespaceNumber') == -1) {
        // Article namespace
        if (mw.config.get('wgNamespaceNumber') === 0 ) {
            addArticleFeedback(document);
            demarcateDialogue();
            gameUpdateIcons();
        }
        
        // Only if the page contains collapsible tables
        if ( $('.collapsible, .expandable').length > 0 ) {
            mw.loader.load( '/index.php?title=MediaWiki:CollapsibleTables.js&action=raw&ctype=text/javascript' );
        }
        tradingPostPrices()
        autoConvertUTC();
    }
    
    // Script to use in the Special namespace only
    if ( mw.config.get('wgNamespaceNumber') == -1 ) {
        uploadEnforcer();
    }
    
    // Scripts to use when searching
    if (mw.config.get('wgPageName') == 'Special:Search') {
        mw.loader.load( '/index.php?title=MediaWiki:ChatLinkSearch.js&action=raw&ctype=text/javascript' );
    }
    
    /**
     * Feedback button (see [[Help:Leaving article feedback]])
     *   Adds a button to leave feedback about a mainspace article on the talk page using a preloaded feedback format
     */
    function addArticleFeedback (document) {
        // Don't bother if its the Main Page as you can't submit feedback onto a locked talk page
        if (mw.config.get('wgPageName') === 'Main_Page') {
            return;
        }
    
        // Construct pretty date format with padded zeros YYYY-MM-DD
        var currentDate = new Date();
        var day = ('0' + currentDate.getDate().toString()).slice(-2);
        var month = ('0' + (currentDate.getMonth() + 1 ).toString()).slice(-2);
        var year = currentDate.getFullYear();
        var currentDate = year + '%2F' + month + '%2F' + day;
        
        // Construct new tab
        feedbacktab = document.createElement('li');
        feedbacktab.id = 'special-articlefeedback';
        feedbacklink = '/index.php?title=Talk:' + encodeURIComponent(mw.config.get('wgPageName')) + '&action=edit&section=new&editintro=Template:Feedback_notice&preload=Template:Feedback_preload&preview=yes&preloadtitle=Feedback+' + currentDate;
        feedbackhover = 'Leave us feedback on the content of the article so that we can improve it';
        feedbacktabtext = 'Leave article feedback';
        
        // Change format depending on whether user is using vector or monobook
        if (mw.config.get('skin') == 'vector') {
            feedbacktab.innerHTML = '<a href="' + feedbacklink + '" title="' + feedbackhover + '"><span>' + feedbacktabtext + '</span></a>'
        } else if (mw.config.get('skin') == 'monobook') {
            feedbacktab.innerHTML = '<a href="' + feedbacklink + '" title="' + feedbackhover + '">' + feedbacktabtext + '</a>'
        }
        
        // Finally add the feedback button somewhere after the discussion tab
        var talk = document.getElementById('ca-talk');
        if (talk) {
          talk.parentNode.insertBefore(feedbacktab, document.getElementById('ca-talk').nextSibling);
        }
    }
    
    /** Display a clock. **/
    /** Spliced from [[mw:MediaWiki:Gadget-LocalLiveClock.js]] and [[mw:MediaWiki:Gadget-UTCLiveClock.js]] **/
    function displayClock() {
        console.log('Common: displayClock function executed.');
        mw.loader.using( ['mediawiki.util', 'mediawiki.api'] ).then( function () {
            console.log('Common: displayClock function - mw.util and mw.api loaded.');
    
            var $target;
    
            function padWithZeroes( num ) {
                // Pad a number with zeroes. The number must be an integer where
                // 0 <= num < 100.
                return num < 10 ? '0' + num.toString() : num.toString();
            }
    
            function showTime( $target ) {
                var now = new Date();
    
                // Set the Local time.
                var hh = now.getHours();
                var mm = now.getMinutes();
                var ss = now.getSeconds();
                var time = padWithZeroes( hh ) + ':' + padWithZeroes( mm ) + ':' + padWithZeroes( ss );
    
                // Set the UTC time.
                var uhh = now.getUTCHours();
                var umm = now.getUTCMinutes();
                var uss = now.getUTCSeconds();
                var utime = padWithZeroes( uhh ) + ':' + padWithZeroes( umm ) + ':' + padWithZeroes( uss );
    
                var string;
                if (hh == uhh) {
                    string = time + ' UTC';
                } else {
                    string = time + ' (' + utime + ' UTC)';
                }
    
                // Write to page.
                $target.text( string );
    
                // Schedule the next time change.
                var ms = now.getUTCMilliseconds();
                setTimeout( function () {
                    showTime( $target );
                }, 1100 - ms );
            }
    
            function liveClock() {
                // CSS styles are set in [[Mediawiki:Common.css]] - search for utcdate.
    
                // Add the portlet link.
                var node = mw.util.addPortletLink(
                    'p-personal',
                    mw.util.getUrl( null, { action: 'purge' } ),
                    '',
                    'utcdate',
                    null,
                    null,
                    '#pt-userpage, #pt-anonuserpage'
                );
                if ( !node ) {
                    return;
                }
    
                // Purge the page when the clock is clicked.
                // If logged in, avoid the purge confirmation screen.
                if ( $('#pt-userpage').length > 0 ) {
                    $( node ).on( 'click', function ( e ) {
                      new mw.Api().post( { action: 'purge', titles: mw.config.get( 'wgPageName' ) } ).then( function () {
                        location.reload();
                      }, function () {
                        mw.notify( 'Purge failed', { type: 'error' } );
                      } );
                      e.preventDefault();
                    } );
                }
    
                // Show the clock.
                showTime( $( node ).find( 'a:first' ) );
            }
    
            $( liveClock );
        } );
    }
    displayClock();
    
    /**
     * Trading post prices. (see [[Template:Tp]])
     *   Converts placeholder content of elements with class "gw2-tpprice" to trading post prices, using "data-info" attribute values (sell or buy) if available.
     */
    function tradingPostPrices () {
        console.log('Common: tradingPostPrices function loaded.');
    
        if ( $('.gw2-tpprice').length === 0 ) {
            return;
        }
    
        function pad (s) {
            return (s < 10 ? '0' : '') + s;
        }
    
        function getCoin (coin, asHtml) {
            if (coin === null || isNaN(coin)) {
                return asHtml ? '<span class="numbers">?</span>&nbsp;<img src="/images/e/eb/Copper_coin.png" alt="Copper" />' : '?';
            }
            coin = Math.ceil(coin);
            var copper = coin % 100;
            var text = copper + 'c';
            var html = '<span class="numbers">' + (coin >= 100 ? pad(copper) : copper ) + '</span>&nbsp;<img src="/images/e/eb/Copper_coin.png" alt="Copper" />';
            if (coin >= 100) {
                var silver = (coin / 100 >> 0) % 100;
                html = '<span class="numbers">' + (coin >= 10000 ? pad(silver) : silver ) + '</span>&nbsp;<img src="/images/3/3c/Silver_coin.png" alt="Silver" />&nbsp;' + html;
                text = silver + 's ' + text;
            }
            if (coin >= 10000) {
                var gold = coin / 10000 >> 0;
                html = '<span class="numbers">' + gold + '</span>&nbsp;<img src="/images/d/d1/Gold_coin.png" alt="Gold" />&nbsp;' + html;
                text = gold + 'g ' + text;
            }
            return asHtml ? html : text;
        }
    
        // Helper function to find elements in A which are not in B. Usage: a.diff(b)
        Array.prototype.diff = function(a) {
            return this.filter(function(el) {
                return a.indexOf(el) < 0;
            });
        };
    
        var elements = {}, ids = [];
        $('.gw2-tpprice').each(function () {
            var id = +this.getAttribute('data-id');
            var qty = 1;
            if (this.hasAttribute('data-quantity')) {
                qty = +this.getAttribute('data-quantity');
            }
            if (!id || parseInt(id) != id) {
                return;
            }
            id = parseInt(id);
            if (!elements[id]) {
                elements[id] = [];
                ids.push(id);
            }
            elements[id].push({
                elem: this,
                info: this.getAttribute('data-info'),
                qty: qty
            });
        });
    
        var promises = [];
        for (var i=0; i < ids.length; i+=200) {
            var current_ids = ids.slice(i,i+200);
            promises.push({
              request: $.getJSON('https://api.guildwars2.com/v2/commerce/prices?ids='+current_ids.join(',')+'&wiki=1&lang=en'),
              requested_ids: current_ids,
              observed_ids: [],
              missing_ids: []
            });
        }
    
        $.each(promises, function(p_index, p){
            $.when(p.request)
            .done(function(data){
                $.each(data, function (index, item) {
                    // Partial success can mean that some valid ids were returned, but some were ignored, hence loop through original requests.
                    p.observed_ids.push(item.id);
    
                    var buyText = 'Highest individual buy order: ' + getCoin(item.buys.unit_price);
                    var sellText = 'Lowest individual sell offer: ' + getCoin(item.sells.unit_price);
                    $.each(elements[item.id], function () {
                        if (this.info == 'buy') {
                            this.elem.innerHTML = getCoin(item.buys.unit_price * this.qty, true);
                            this.elem.title = buyText + ' (' + item.buys.quantity + ' ordered)';
                            this.elem.setAttribute('data-sort-value', item.buys.unit_price * this.qty);
                        }
                        else if (this.info == 'sell') {
                            this.elem.innerHTML = getCoin(item.sells.unit_price * this.qty, true);
                            this.elem.title = sellText + ' (' + item.sells.quantity + ' listed)';
                            this.elem.setAttribute('data-sort-value', item.sells.unit_price * this.qty);
                        }
                        else {
                            this.elem.innerHTML = getCoin(item.sells.unit_price * this.qty, true);
                            this.elem.title = sellText + ' / ' + buyText;
                            this.elem.setAttribute('data-sort-value', item.sells.unit_price * this.qty);
                        }
                        if ((this.info == 'buy' && !item.buys.quantity) || (this.info != 'buy' && !item.sells.quantity)) {
                            this.elem.style.opacity = '0.5';
                        }
                    });
                });
    
                // Check for missing ids which were in the original request
                p.missing_ids = p.requested_ids.diff(p.observed_ids);
                $.each(p.missing_ids, function(index, id){
                    $.each(elements[id], function() {
                        this.elem.innerHTML = getCoin(0, true);
                        this.elem.title = 'No buy or sell orders found.'
                        this.elem.style.opacity = '0.5';
                    });
                });
            })
            .fail( function(d, textStatus, error) {
                console.log("Commerce API getJSON failed (request #" + p_index +"), status: " + textStatus + ", error: "+error, JSON.stringify(d.responseText));
                $.each(p.requested_ids, function(index, id){
                    $.each(elements[id], function() {
                        this.elem.innerHTML = getCoin(null, true);
                        this.elem.title = 'Unable to retrieve buy or sell orders.'
                        this.elem.style.opacity = '0.5';
                    });
                });
            });
        });
    }
    
    /**
     * Convert UTC time to local time. (see [[Template:UTC time]])
     */
    function autoConvertUTC () {
        function pad (s) {  return (s < 10 ? '0' : '') + s; }
        var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
        $('.utc-auto-convert').each(function(i,v){
            // Get UTC time using MediaWiki {{#time: U}} epoch format
            var utcseconds = v.getAttribute('data-time');
            if (utcseconds == 'error') {
                return;
            }
            var d = new Date(0);
            d.setUTCSeconds(utcseconds);
            var offset = (-1 * d.getTimezoneOffset() / 60);
            var offsetstring = '';
            if (offset > 0) { offsetstring = '+' + offset; }
            if (offset < 0) { offsetstring = offset; }
            
            // Default to showing the time only
            var datestring = pad(d.getHours()) + ':' + pad(d.getMinutes()) + ' UTC' + offsetstring;
            var titlestring = pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ' UTC';
            
            // But check for different formatting in case there is a day of the week given inside the span
            if (!v.textContent.match(/^\d/)) {
                datestring  = days[d.getDay()] + ' ' + datestring;
                titlestring = days[d.getUTCDay()] + ' ' + titlestring;
            }
            
            // Show result
            $(v).html('<span style="cursor:help; border-bottom:1px dotted silver;" title="'+titlestring+'">'+datestring+'</span>');
        });
    }
    
    
    /**
     * Force users to select a license option when uploading files. Allows other summaries if it contains 'ArenaNet image' or 'User image' license templates.
     */
    function uploadEnforcer () {
        $('#mw-upload-form').submit(function(event) {
            var licenseValue = $('#wpLicense').val();
            var uploadDescription = $('#wpUploadDescription').val();
            if (licenseValue == '' && !uploadDescription.match(/(Licensing|ArenaNet image|User image)/i)) {
                alert('Please select a Licensing option.');
                event.preventDefault();
            }
        });
    }
    
    /**
     * Additional class formatting "dialogue" for sections titled as dialogue or text on mainspace articles
     */
    function demarcateDialogue () {
        if (mw.config.get('skin') !== 'minerva') {
            $('h2').each(function (i, e) {
                var h2Content = this.innerHTML.match(/(dialogue|text)/i);
                if (h2Content) {
                    $(this).nextUntil(this.tagName).wrapAll('<div class="dialogue"></div>');
                }
            });
        }
    }
    
    /**
     * Increased line spacing for skill and trait icons on the "Game update" / "Upcoming changes and features" pages and subpages
     */
    function gameUpdateIcons () {
        if (mw.config.get('wgPageName').substring(0, 12) == 'Game_updates' || mw.config.get('wgPageName').substring(0, 29) == 'Upcoming_changes_and_features') {
            $('li .skillicon, li .effecticon, li .traiticon').each(function(){
                $(this).parent('li').addClass('patchnote');
            });
        }
    }
    
    /**
     * Add user agent strings to the HTML element to make browser specific CSS simpler
     */
    document.documentElement.setAttribute('data-useragent', navigator.userAgent);

}

}