MediaWiki:All.js

/* Any JavaScript here will be loaded for all users both on desktop and mobile */

// ================================================================================ // Page specific JS/CSS // ================================================================================

//Check page specific files $(function {    var url = new URL(window.location.href);    var action = url.searchParams.get("action")    if (action === null || action === "view" || action === "submit") {        mw.loader.using("mediawiki.api", function  { var skin = mw.config.get("skin"), page = mw.config.get("wgPageName"), user = mw.config.get("wgUserName");

var pages = [ ['MediaWiki:Common.js/' + page + ".js", "js"], ['MediaWiki:Common.css/' + page + ".css", "css"], ['MediaWiki:' + skin + '.js/' + page + ".js", "js"], ['MediaWiki:' + skin + '.css/' + page + ".css", "css"] ];           if (user != null) pages.push(                ['User:' + user + '/common.js/' + page + ".js", "js"],                ['User:' + user + '/common.css/' + page + ".css", "css"],                ['User:' + user + '/' + skin + '.js/' + page + ".js", "js"],                ['User:' + user + '/' + skin + '.css/' + page + ".css", "css"]            ); pages.forEach(function (el) {               if (el[1] == "js") {                    if (new URL(window.location).searchParams.get("disable-page-js") != null) return;                    mw.loader.load('/w/index.php?title=' + encodeURIComponent(el[0]) + '&action=raw&ctype=text/javascript');                }                else {                    if (new URL(window.location).searchParams.get("disable-page-css") != null) return;                    mw.loader.load('/w/index.php?title=' + encodeURIComponent(el[0]) + '&action=raw&ctype=text/css', 'text/css');                }            }); });   } });

// ================================================================================ // Username replace function for Template:USERNAME // ================================================================================ // Inserts user name into. // Disable by setting disableUsernameReplace = true. jQuery(function($) { if (typeof(disableUsernameReplace) != 'undefined' && disableUsernameReplace)    return;  var username = mw.config.get('wgUserName');  if (username == null)    return;

$('.insertusername').text(username); });

// ================================================================================ // Editcount replace function for Template:EDITCOUNT // ================================================================================ // Inserts edit count into jQuery(function($) { var userEditCount = mw.config.get('wgUserEditCount');  if (userEditCount == null)    return;

$('.inserteditcount').text(userEditCount); });

// ================================================================================ // Registration date replace function for Template:REGISTRATIONDATE // ================================================================================ // Inserts registration date into jQuery(function($) { var userRegistrationDate = mw.config.get('wgUserRegistration');  if (userRegistrationDate == null)    return;  var d = new Date(0); // Sets the date to the epoch  d.setUTCMilliseconds(userRegistrationDate);

$('.insertregistrationdate').text(d.toLocaleString); });

/////////////////////////////////////////////////////////////////////////////// //                       Schedule Utility Functions                         // ///////////////////////////////////////////////////////////////////////////////

// Advances a timestamp to the next multiple of 2 hours function advanceDateTime(time) { var ret = new Date(time.getTime); ret.setMinutes(0); ret.setTime(ret.getTime + 3600000 * ( ret.getUTCHours & 1 ? 1 : 2   ));    return ret; }

// Formats a timestamp as a string in local time function formatDateTime(time) { var ret = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ][time.getMonth] + " " + time.getDate + " " + zeroPad(time.getHours, 2) + ":" + zeroPad(time.getMinutes, 2) ;   return ret; }

// Parses a UTC date string in the format "MMM dd hh:mm YYYY", the year at the end of the string is optional and replaces the year argument if provided function parseDateTime(text, year) { text = text.split(/[\s:]+/); if(parseInt(text[4]) != NaN && parseInt(text[4]) < 9999 && parseInt(text[4]) >= 1970) year = text[4]; return new Date(Date.UTC( year, { jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun:  5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11 }[text[0].toLowerCase], parseInt(text[1]), // Day parseInt(text[2]), // Hours parseInt(text[3]), // Minutes 0, 0 // Seconds, milliseconds )); }

// Parses a last-fetched string into a Date object function parseFetched(now, text) { var ret = parseDateTime(text, now.getUTCFullYear); if (now < ret) // Accounts for year boundary ret.setUTCFullYear(ret.getUTCFullYear - 1); return ret; }

// Parses a schedule string into a Date object function parseSchedule(fetched, text) { var ret = parseDateTime(text, fetched.getUTCFullYear); if (ret.getTime < fetched.getTime - 8640000000) ret.setUTCFullYear(ret.getUTCFullYear + 1); return ret; }

// Calculates the time remaining until a given timestamp, as a string function timeUntil(now, target) { target     = target.getTime - now.getTime; target     = Math.floor(target % 7200000 / 1000); var seconds = zeroPad(target % 60, 2); var minutes = zeroPad(Math.floor(target / 60) % 60, 2); var hours  = Math.floor(target / 3600); return hours + ":" + minutes + ":" + seconds; }

// Pad a number with leading zeroes function zeroPad(number, digits) { number = "" + number; while (number.length < digits) number = "0" + number; return number; }

/////////////////////////////////////////////////////////////////////////////// //                          BattleSchedule Class                            // ///////////////////////////////////////////////////////////////////////////////

// Maintains auto-updating Ink Battle schedule elements

// Object constructor var BattleSchedule = function {

// Initialize instance fields this.lblNow    = document.getElementById("battle1"); this.lblNext   = document.getElementById("battle2"); this.lblFetched = document.getElementById("battleFetched"); this.prev      = false;

// Error checking if (!this.lblFetched) return; // No schedule data

// Get the current and last-fetched timestamps var now    = new Date; var fetched = parseFetched(now, this.lblFetched.innerHTML);

// Determine the timestamp of the following two rotations this.next = advanceDateTime(fetched); this.later = advanceDateTime(this.next);

// Update initial display this.onTick(now); this.lblFetched.innerHTML = formatDateTime(fetched);

// Schedule periodic updates var that = this; this.timer = setInterval(function { that.onTick(new Date); }, 1000); };

// Periodic update handler BattleSchedule.prototype.onTick = function(now) {

// Determine when the "Now" row enters the past if (now >= this.next && !this.prevNow) { this.prev            = true; this.lblNow.innerHTML = "Previous"; }

// Determine when the "Next" row enters the past if (now >= this.later && !this.prevNext) { this.lblNext.innerHTML = "Previous"; clearInterval(this.timer); return; }

// Display the time until the next rotation this.lblNext.innerHTML = (this.prev ? "Now, for another " : "Next, in ") + timeUntil(now, this.prev ? this.later : this.next) ; };

new BattleSchedule;

/////////////////////////////////////////////////////////////////////////////// //                          SalmonSchedule Class                            // ///////////////////////////////////////////////////////////////////////////////

// Maintains auto-updating Salmon Run schedule elements

// Object constructor var SalmonSchedule = function {

// Get the current and last-fetched timestamps var lblFetched = document.getElementById("salmonFetched"); if (!lblFetched) return; // No schedule var now       = new Date; var fetched   = parseFetched(now, lblFetched.innerHTML);

// Initialize instance fields this.slots = [ this.parse(document.getElementById("salmon1"), fetched), this.parse(document.getElementById("salmon2"), fetched), ];

// Update initial display this.onTick(now); lblFetched.innerHTML = formatDateTime(fetched);

// Schedule periodic updates var that = this; this.timer = setInterval(function { that.onTick(new Date); }, 1000); };

// Periodic update handler SalmonSchedule.prototype.onTick = function(now) {

// Cycle through slots for (var x = 0; x < this.slots.length; x++) { var slot = this.slots[x]; if (slot.prev) continue; // Skip this slot

// Determine when this slot should stop updating slot.prev = now >= slot.end;

// Update the element slot.element.innerHTML = now >= slot.end ? "Previous" : now >= slot.start ? "Now - " + formatDateTime(slot.end) : formatDateTime(slot.start) + " - " + formatDateTime(slot.end) ;   }

// De-schedule the timer if (this.slots[this.slots.length - 1].prev) clearInterval(this.timer); };

// Parse a single Salmon Run schedule slot SalmonSchedule.prototype.parse = function(element, fetched) { var text = element.innerHTML; return { element: element, start:  parseSchedule(fetched, text.substring( 0, 12)), end:    parseSchedule(fetched, text.substring(15, 27)), prev:   false }; }

new SalmonSchedule;

/////////////////////////////////////////////////////////////////////////////// //                           ShopSchedule Class                             // ///////////////////////////////////////////////////////////////////////////////

// Maintains auto-updating SplatNet 2 Shop schedule elements

// Object constructor var ShopSchedule = function { var lblFetched = document.getElementById("shopFetched"); if (!lblFetched) return; // No schedule

// Get the current and last-fetched timestamps var now    = new Date; var fetched = parseFetched(now, lblFetched.innerHTML);

// Update initial display lblFetched.innerHTML = formatDateTime(fetched); };

new ShopSchedule;

/////////////////////////////////////////////////////////////////////////////// //                         SplatfestSchedule Class                          // ///////////////////////////////////////////////////////////////////////////////

// Maintains auto-updating Splatfest schedule elements

// Object constructor var SplatfestSchedule = function { var that = this;

// Initialize instance fields var now = new Date; this.slots = Array.from( document.querySelectorAll(".splatfestTimer") ).map( function (el) { return that.parse(el, now) } ); this.slots.push( // backwards compatibility       this.parse(document.getElementById("splatfest1"), now),        this.parse(document.getElementById("splatfest2"), now),        this.parse(document.getElementById("splatfest3"), now)    );

// Update initial display this.onTick(now);

// Schedule periodic updates this.timer = setInterval(function { that.onTick(new Date); }, 1000); };

// Periodic update handler SplatfestSchedule.prototype.onTick = function(now) {

// Cycle through slots for (var x = 0; x < this.slots.length; x++) { var slot = this.slots[x]; if (slot.prev) continue; // Skip this slot

// Determine when this slot should stop updating slot.prev = now >= slot.end;

// Update the element slot.element.innerHTML = now >= slot.end ? "Concluded" : now >= slot.start ? "Now - " + formatDateTime(slot.end) : formatDateTime(slot.start) ;   }

// De-schedule the timer if (this.slots[this.slots.length - 1].prev) clearInterval(this.timer); };

// Parse a single Splatfest schdule slot SplatfestSchedule.prototype.parse = function(element, now) {

// Error checking if (!element) return { prev: true };

// Determine the current time and start and end timestamps var start = parseDateTime(element.innerHTML, now.getUTCFullYear); return { element: element, start:  start, end:    new Date(start.getTime + 172800000), prev:   false }; };

new SplatfestSchedule;

// ================================================================================ // MediaLoader - Prevent audio from loading until clicked // Version 2 (19.04.2020) // ================================================================================

window.MediaLoader = {}; window.MediaLoader.FileCache = {};

function MLGetFileFromName(name){ return new Promise(function(k,no){       if(window.MediaLoader.FileCache[name] == null){            new mw.Api.get({ "action": "parse", "format": "json", "text": ""+name+"", "prop": "text", "contentmodel": "wikitext" }).then(function(file){ var filetext = $($.parseHTML(file.parse.text["*"])).find('p').html; window.MediaLoader.FileCache[name] = filetext; k(filetext); },no);       }        else            k(window.MediaLoader.FileCache[name]);    }) }

mw.loader.using("mediawiki.api", function{   $(".MediaLoader").each(function{ $(this).data("state", "unloaded"); var children = $(this).children; if(children.length < 1){ console.error("[MediaLoader] Error P1"); return; }       var child = $(children[0]); child.find(".MediaLoader-text").click(function{           var parent = $(this).parent.parent;            try{                if(parent.data("state") == "unloaded"){                    parent.data("state", "busy");                    $(this).text("Loading...");                    MLGetFileFromName(parent.data("file")).then(function(filetext){ parent.find(".MediaLoader-file").html(filetext); parent.find(".MediaLoader-text").text("Unload "+parent.data("name")); parent.data("state", "loaded"); }, console.error)               }                else if(parent.data("state") == "loaded"){                    parent.find(".MediaLoader-file").html("");                    $(this).text("Load "+parent.data("name"));                    parent.data("state", "unloaded");                }            }            catch(ex){                console.error(ex);                parent.data("state", "error");                $(this).text("An unexpected error has occured");                parent.find(".MediaLoader-file").html("");                parent.find(".MediaLoader-file").children("a").attr("href", "//splatoonwiki.org/wiki/"+parent.data("file"))                parent.find(".MediaLoader-file").children("a").text(parent.data("name"))                $(this).css("cursor", "");            }        }) child.find(".MediaLoader-text").text("Load "+$(this).data("name")); child.find(".MediaLoader-text").addClass("noselect"); child.find(".MediaLoader-text").css("cursor", "pointer"); child.find(".MediaLoader-file").addClass("noselect"); })   $(".MediaLoadAll").each(function{ var children = $(this).children; if(children.length < 2){ console.error("[MediaLoadAll] Error P1"); return; }       children.click(function{            var parent = $(this).parent;            try{                var load = $(this).hasClass("MediaLoadAll-load");                $(parent.data("group") != "" ? '.MediaLoader[data-group="'+parent.data("group")+'"]' : ".MediaLoader").each(function{ if(($(this).data("state") == "unloaded" && load) || ($(this).data("state") == "loaded" && !load)) $(this).find(".MediaLoader-text").click; })           }            catch(ex){                console.error(ex);                $(this).text("An unexpected error has occured");                $(this).css("cursor", "");            }        }) $(this).css("display", "") children.filter(".MediaLoadAll-load").text("Load all "+$(this).data("name")); children.filter(".MediaLoadAll-unload").text("Unload all "+$(this).data("name")); children.addClass("noselect"); children.css("cursor", "pointer"); }) })

// ================================================================================ // Countdowns // ================================================================================ // Credits go to AbelToy, Guy Perfect, Espyo for the countdown code.

// List of countdowns on the current page var countdowns = [];

// Converts from time to a clean time info structure function timeToStruct(time) {

var passed = time < 0; //Has the moment passed? // Parse time fields from the number time = Math.floor(time / 1000); var secs = ("00" + (time % 60)).slice(-2); time = Math.floor(time /   60); var mins = ("00" + (time % 60)).slice(-2); time = Math.floor(time /   60); var hours = ("00" + (time % 24)).slice(-2); time = Math.floor(time /  24);

// Construct the string representation return { d: time, h: hours, m: mins, s: secs, p: passed }; }

// Gets the time remaining until the next stage rotation function getStageCountdown(now) { var hour  = Math.floor(now / 3600000) % 24 + 2; // Add 2 for UTC bias var now   = hour * 3600000 + now % 3600000;     // Current adjusted hour var target = (hour + 4 & -4) * 3600000;         // Target hour return target - now; }

function tickCountdowns { var now = Date.now; for(var c = 0; c < countdowns.length; c++){ var diff = 0; if(countdowns[c].stage) { diff = timeToStruct(getStageCountdown(now)); } else { diff = timeToStruct(countdowns[c].time - now); }   if(diff.p && diff.d < -1) { // Over 24 hours passed countdowns[c].span.innerHTML = countdowns[c].doneMessage; } else if(diff.p){ // 24 hours haven't passed yet countdowns[c].span.innerHTML = countdowns[c].ongoingMessage; } else { // The time hasn't come yet countdowns[c].span.innerHTML = diff.d + "d " + diff.h + "h " + diff.m + "m " + diff.s + "s"; } } }

// Returns the info from a countdown span on the page. function getCountdownInfo(countdown, stage) { var time = null; var ongoingMessage = ""; var doneMessage = "";

if(!stage) { // Format is " | | " var parts = countdown.innerHTML.split("|"); doneMessage = (parts.length >= 3) ? parts[2] : parts[1]; ongoingMessage = parts[1]; var timeParts = parts[0].split(/[ \n]/); var date = timeParts[0].split("/"); var hour = timeParts[1].split(":"); time = Date.UTC(date[0], date[1] - 1, date[2], hour[0], hour[1]); } countdowns.push( {    span:           countdown,    stage:          stage,    time:           time,    ongoingMessage: ongoingMessage,    doneMessage:    doneMessage  } ); // The spans start hidden and with the info // Delete the info and show the span countdown.style.display = "inline"; countdown.innerHTML = ""; }

// Finds countdown spans on the document and sets up the countdowns function setupCountdowns { var stageCountdowns = document.getElementsByClassName("stageCountdown"); for(var sc = 0; sc < stageCountdowns.length; sc++) { getCountdownInfo(stageCountdowns[sc], true); } var countdowns = document.getElementsByClassName("countdown"); for(var c = 0; c < countdowns.length; c++) { getCountdownInfo(countdowns[c], false); } setInterval(tickCountdowns, 1000); }

setupCountdowns;

///////////////////////////// New infobox gobbler //////////////////////////////

// Keep looking for infoboxes until the page finishes loading (function {	var gobblers = []; // Found gobblers	var infoboxes = []; // Found infoboxes

// Add an infobox to a gobbler var addInfobox = function(gobbler, infobox) {

// The infobox is already gobbled if (gobbler.infoboxKeys.has(infobox)) return; gobbler.infoboxKeys.add(infobox);

// Locate the game button template that applies to this infobox var game = gobbler.games.find(function(game) {			return game.gameId == infobox.gameId;		}); if (!game) return false; // No matching game button template

// Produce a game button for the infobox var button = game.cloneNode(true); button.gameId = game.gameId; button.gobbler = gobbler; button.addEventListener("keydown", setGame); button.addEventListener("pointerdown", setGame); gobbler.gameList.append(button); gobbler.buttons.push(button);

// Add the infobox to the gobbler infobox.remove; infobox = infobox.cloneNode(true); infobox.classList.add("inactive"); gobbler.infoboxes.push(infobox); gobbler.append(infobox); button.infobox = infobox; gobbler.infoboxes.forEach(function(box) {			box.classList[box != infobox ? "add" : "remove"]("inactive");		}); // Select the first game button for the same game as the new button setGame({			type : "pointerdown",			button: 0,			target:	gobbler.buttons.find(function(btn) { return btn.gameId == button.gameId; })		});

gobbler.classList.add("active"); };

// Derive a game ID from a game button or infobox element class list var getGameId = function(element) { return Array.from(element.classList).find(function(clazz) {			return !clazz.startsWith("infobox");		}); };	// Add a newly discovered gobbler var newGobbler = function(gobbler) {

// Configure instance fields gobbler.buttons    = []; gobbler.gameList   = gobbler.querySelector(":scope > .infobox-game-list"); gobbler.games      = Array.from(gobbler.gameList.querySelectorAll(".infobox-game")); gobbler.infoboxes  = []; gobbler.infoboxKeys = new Set;

// Process game button templates gobbler.games.forEach(function(game) {			game.gameId  = getGameId(game);			game.tabIndex = 0;			game.classList.add("inactive");			game.remove;		});

return gobbler; };	// Process a newly discovered infobox var newInfobox = function(infobox) {

// Configure instance fields infobox.gameId = getGameId(infobox);

// Prevent lazy loading of any contained images Array.from(infobox.querySelectorAll("[data-src]")).forEach(function(lazy) {			var unlazy = document.createElement("img");			unlazy.src = lazy.getAttribute("data-src");			unlazy.setAttribute("style", lazy.getAttribute("style"));			lazy.before(unlazy);			lazy.remove;		});

return infobox; };	// Configure styles on game buttons var setButtonStyles = function(button) { button.gobbler.buttons.forEach(function(btn) {

// Do not display any buttons unless there are two or more if (button.gobbler.buttons.length < 2) { btn.classList.remove("active"); btn.classList.remove("inactive"); return; }			// Configure styles on visible buttons var current = button.gobbler.buttons.length > 1 && btn == button; btn.classList[ current ? "add" : "remove"]("active" ); btn.classList[!current ? "add" : "remove"]("inactive"); })		};

// Specify the selected game var setGame = function(e) { // Ignore this event if (			e.type != "keydown" && e.type != "pointerdown" ||			e.type == "keydown" && e.key != " " && e.key != "Enter" ||			e.type == "pointerdown" && e.button != 0 // Left click only		) return;

// Prevent the default user agent behavior if (e.preventDefault) { e.preventDefault; e.stopPropagation; }

// Identify the button element var button = e.target; for (button && !button.gobbler; button = button.parentNode); if (!button || !button.gobbler) return; // Failsafe

// Configure button styles setButtonStyles(button);

// Configure infobox visibility button.gobbler.infoboxes.forEach(function(infobox) {			infobox.classList[infobox == button.infobox ? "remove" : "add"]("inactive");		}); };

// Monitor for new gobblers and infoboxes var interval = setInterval(function {

// Find new gobblers and infoboxes Array.from(document.querySelectorAll(".infobox-gobbler")) .filter(function(g) { return gobblers.indexOf(g) == -1; }) .forEach(function(g) { gobblers.push(newGobbler(g)); }) ;		Array.from(document.querySelectorAll("*:not(.infobox-gobbler) > .infobox")) .forEach(function(i) { infoboxes.push(newInfobox(i)); }) ;

// Process all gobblers gobblers.forEach(function(gobbler) {			// Process all infoboxes			infoboxes.forEach(function(infobox) { addInfobox(gobbler, infobox); });		});

// Stop watching for new infoboxes if (document.readyState == "complete") clearInterval(interval); }, 100); });

////////////////////////////////////////////////////////////////////////////////