MediaWiki:All.js: Difference between revisions

From Inkipedia, the Splatoon wiki
m (No more debugging)
(fix countdown when there is only one challenge in the schedule (thank you Harimaron))
 
(39 intermediate revisions by 3 users not shown)
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users both on desktop and mobile */
/* Any JavaScript here will be loaded for all users both on desktop and mobile */
/* strawpoll.me */
mw.loader.load('//splatoonwiki.org/wiki/MediaWiki:StrawpollIntegrator.js?action=raw&ctype=text/javascript')
/* strawpoll.com */
mw.loader.load('//splatoonwiki.org/wiki/MediaWiki:StrawpollIntegrator2.js?action=raw&ctype=text/javascript')


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


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


Line 53: Line 48:
jQuery(function($) {
jQuery(function($) {
   if (typeof(disableUsernameReplace) != 'undefined' && disableUsernameReplace)
   if (typeof(disableUsernameReplace) != 'undefined' && disableUsernameReplace)
    return;
return;
    
    
   var username = mw.config.get('wgUserName');
   var username = mw.config.get('wgUserName');
   if (username == null)
   if (username == null)
    return;
return;


   $('.insertusername').text(username);
   $('.insertusername').text(username);
Line 69: Line 64:
   var userEditCount = mw.config.get('wgUserEditCount');
   var userEditCount = mw.config.get('wgUserEditCount');
   if (userEditCount == null)
   if (userEditCount == null)
    return;
return;


   $('.inserteditcount').text(userEditCount);
   $('.inserteditcount').text(userEditCount);
Line 81: Line 76:
   var userRegistrationDate = mw.config.get('wgUserRegistration');
   var userRegistrationDate = mw.config.get('wgUserRegistration');
   if (userRegistrationDate == null)
   if (userRegistrationDate == null)
    return;
return;
    
    
   var d = new Date(0); // Sets the date to the epoch
   var d = new Date(0); // Sets the date to the epoch
Line 95: Line 90:
// Advances a timestamp to the next multiple of 2 hours
// Advances a timestamp to the next multiple of 2 hours
function advanceDateTime(time) {
function advanceDateTime(time) {
    var ret = new Date(time.getTime());
var ret = new Date(time.getTime());
    ret.setMinutes(0);
ret.setMinutes(0);
    ret.setTime(ret.getTime() + 3600000 * (
ret.setTime(ret.getTime() + 3600000 * (
        ret.getUTCHours() & 1 ? 1 : 2
ret.getUTCHours() & 1 ? 1 : 2
    ));
));
    return ret;
return ret;
}
}


// Formats a timestamp as a string in local time
// Formats a timestamp as a string in local time
function formatDateTime(time) {
function formatDateTime(time) {
    var ret =
var ret =
        [ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
[ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
        ][time.getMonth()] + " " +
][time.getMonth()] + " " +
        time.getDate() + " " +
time.getDate() + " " +
        zeroPad(time.getHours(), 2) + ":" +
zeroPad(time.getHours(), 2) + ":" +
        zeroPad(time.getMinutes(), 2)
zeroPad(time.getMinutes(), 2)
    ;
;
    return ret;
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
// 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) {
function parseDateTime(text, year) {
    text = text.split(/[\s:]+/);
text = text.split(/[\s:]+/);
    if(parseInt(text[4]) != NaN && parseInt(text[4]) < 9999 && parseInt(text[4]) >= 1970) year = text[4];
if(parseInt(text[4]) != NaN && parseInt(text[4]) < 9999 && parseInt(text[4]) >= 1970) year = text[4];
    return new Date(Date.UTC(
return new Date(Date.UTC(
        year,
year,
        { jan: 0, feb: 1, mar: 2, apr: 3, may:  4, jun:  5,
{ jan: 0, feb: 1, mar: 2, apr: 3, may:  4, jun:  5,
          jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11
  jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11
        }[text[0].toLowerCase()],
}[text[0].toLowerCase()],
        parseInt(text[1]), // Day
parseInt(text[1]), // Day
        parseInt(text[2]), // Hours
parseInt(text[2]), // Hours
        parseInt(text[3]), // Minutes
parseInt(text[3]), // Minutes
        0, 0 // Seconds, milliseconds
0, 0 // Seconds, milliseconds
    ));
));
}
}


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


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


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


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


Line 177: Line 172:
var BattleSchedule = function() {
var BattleSchedule = function() {


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


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


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


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


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


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


Line 206: Line 201:
BattleSchedule.prototype.onTick = function(now) {
BattleSchedule.prototype.onTick = function(now) {


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


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


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


Line 239: Line 234:
var SalmonSchedule = function() {
var SalmonSchedule = function() {


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


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


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


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


Line 263: Line 258:
SalmonSchedule.prototype.onTick = function(now) {
SalmonSchedule.prototype.onTick = function(now) {


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


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


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


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


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


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


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


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


new ShopSchedule();
new ShopSchedule();




Line 330: Line 324:
// Object constructor
// Object constructor
var SplatfestSchedule = function() {
var SplatfestSchedule = function() {
var that = this;


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


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


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


Line 350: Line 345:
SplatfestSchedule.prototype.onTick = function(now) {
SplatfestSchedule.prototype.onTick = function(now) {


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


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


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


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


Line 374: Line 369:
SplatfestSchedule.prototype.parse = function(element, now) {
SplatfestSchedule.prototype.parse = function(element, now) {


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


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


new SplatfestSchedule();
new SplatfestSchedule();
///////////////////////////////////////////////////////////////////////////////
//                          ChallengeSchedule Class                        //
///////////////////////////////////////////////////////////////////////////////
// By user Harimaron
// Maintains auto-updating Challenge schedule elements
// Object constructor
var ChallengeSchedule = function() {
"use strict";
this.timesEl = [
document.getElementById("challengeTime1"),
document.getElementById("challengeTime2"),
];
if (!this.timesEl[0])
return; // No schedule data
var lblFetched = document.getElementById("challengeFetched");
if (!lblFetched)
return;
var now = new Date();
var fetched = parseFetched(now, lblFetched.innerHTML);
/**
* @param {string} str
*/
function parse(str) {
if (!str.endsWith(" UTC"))
return null;
var dates = str.split("-", 2);
if (dates.length != 2)
return null;
return ({
start: parseSchedule(fetched, dates[0].trim()),
end: parseSchedule(fetched, dates[1].slice(0, -4).trim()),
});
}
this.data = [];
for (var i = 0; i < this.timesEl.length; i++) {
var timeEl = this.timesEl[i];
if (!timeEl)
continue;
var orderEl = timeEl.getElementsByClassName("challengeOrder");
if (!orderEl.length)
continue;
var current = {
orderEl: orderEl[0],
slots: [/*{
el: new HTMLElement(),
start: new Date(),
end: new Date(),
},*/],
};
var slotsEl = timeEl.getElementsByClassName("challengeTimeSlot");
for (var j = 0; j < slotsEl.length; j++) {
var slotEl = slotsEl[j];
var slot = parse(slotEl.innerText);
if (!slot)
continue;
current.slots.push({
el: slotEl,
start: slot.start,
end: slot.end,
});
}
if (current.slots.length > 0)
this.data.push(current);
}
if (!this.data.length)
return;
var _this = this;
this.timer = setInterval(function() { _this.onTick(new Date()); }, 1000);
};
// Periodic update handler
/**
* @param now {Date}
*/
ChallengeSchedule.prototype.onTick = function(now) {
"use strict";
var upcomingOrNext = 1;
var hasFuture = false;
for (var i = 0; i < this.data.length; i++) {
var current = this.data[i];
for (var j = 0; j < current.slots.length; j++) {
var slot = current.slots[j];
var startStr = formatDateTime(slot.start);
var endStr = zeroPad(slot.end.getHours(), 2) + ":" + zeroPad(slot.end.getMinutes(), 2);
if (now >= slot.end) {
slot.el.style.textDecoration = "line-through";
slot.el.style.fontWeight = "normal";
} else if (now >= slot.start) {
slot.el.style.textDecoration = "underline";
startStr = "Now";
endStr += " (" + timeUntil(now, slot.end) + " left)";
}
slot.el.innerText = startStr + " - " + endStr;
}
var start = current.slots[0].start;
var end = current.slots[current.slots.length - 1].end;
if (now >= end) {
upcomingOrNext = 0;
current.orderEl.innerText = "Past";
} else if (now >= start) {
upcomingOrNext = 0;
hasFuture = true;
current.orderEl.innerText = "Current";
} else if (upcomingOrNext <= 1) {
hasFuture = true;
current.orderEl.innerText = "Upcoming";
} else {
hasFuture = true;
current.orderEl.innerText = "Next";
}
upcomingOrNext++;
}
if (!hasFuture) {
clearInterval(this.timer);
}
};
new ChallengeSchedule();


// ================================================================================
// ================================================================================
Line 398: Line 516:


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


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


Line 504: Line 622:
    
    
   // Parse time fields from the number
   // Parse time fields from the number
                                              time = Math.floor(time / 1000);
  time = Math.floor(time / 1000);
   var secs  = ("00" + (time % 60)).slice(-2); time = Math.floor(time /  60);
   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 mins  = ("00" + (time % 60)).slice(-2); time = Math.floor(time /  60);
Line 511: Line 629:
   // Construct the string representation
   // Construct the string representation
   return {
   return {
    d: time,
d: time,
    h: hours,
h: hours,
    m: mins,
m: mins,
    s: secs,
s: secs,
    p: passed
p: passed
   };
   };
    
    
Line 532: Line 650:
    
    
   for(var c = 0; c < countdowns.length; c++){
   for(var c = 0; c < countdowns.length; c++){
   
    var diff = 0;
var diff = 0;
    if(countdowns[c].stage) {
if(countdowns[c].stage) {
      diff = timeToStruct(getStageCountdown(now));
  diff = timeToStruct(getStageCountdown(now));
    } else {
} else {
      diff = timeToStruct(countdowns[c].time - now);
  diff = timeToStruct(countdowns[c].time - now);
    }
}
   
    if(diff.p && diff.d < -1) {
if(diff.p && diff.d < -1) {
      // Over 24 hours passed
  // Over 24 hours passed
      countdowns[c].span.innerHTML = countdowns[c].doneMessage;
  countdowns[c].span.innerHTML = countdowns[c].doneMessage;
    } else if(diff.p){
} else if(diff.p){
      // 24 hours haven't passed yet
  // 24 hours haven't passed yet
      countdowns[c].span.innerHTML = countdowns[c].ongoingMessage;
  countdowns[c].span.innerHTML = countdowns[c].ongoingMessage;
    } else {
} else {
      // The time hasn't come yet
  // The time hasn't come yet
      countdowns[c].span.innerHTML =
  countdowns[c].span.innerHTML =
        diff.d + "d " +
diff.d + "d " +
        diff.h + "h " +
diff.h + "h " +
        diff.m + "m " +
diff.m + "m " +
        diff.s + "s";
diff.s + "s";
    }
}
   }
   }
}
}
Line 564: Line 682:


   if(!stage) {
   if(!stage) {
    // Format is "<day> <hour>|<24 hour msg>|<afterwards msg>"
// Format is "<day> <hour>|<24 hour msg>|<afterwards msg>"
    var parts = countdown.innerHTML.split("|");
var parts = countdown.innerHTML.split("|");
    doneMessage = (parts.length >= 3) ? parts[2] : parts[1];
doneMessage = (parts.length >= 3) ? parts[2] : parts[1];
    ongoingMessage = parts[1];
ongoingMessage = parts[1];
   
    var timeParts = parts[0].split(/[ \n]/);
var timeParts = parts[0].split(/[ \n]/);
    var date = timeParts[0].split("/");
var date = timeParts[0].split("/");
    var hour = timeParts[1].split(":");
var hour = timeParts[1].split(":");
    time = Date.UTC(date[0], date[1] - 1, date[2], hour[0], hour[1]);
time = Date.UTC(date[0], date[1] - 1, date[2], hour[0], hour[1]);
   }
   }
    
    
   countdowns.push( {
   countdowns.push( {
    span:          countdown,
span:          countdown,
    stage:          stage,
stage:          stage,
    time:          time,
time:          time,
    ongoingMessage: ongoingMessage,
ongoingMessage: ongoingMessage,
    doneMessage:    doneMessage
doneMessage:    doneMessage
   } );
   } );
    
    
Line 593: Line 711:
   var stageCountdowns = document.getElementsByClassName("stageCountdown");
   var stageCountdowns = document.getElementsByClassName("stageCountdown");
   for(var sc = 0; sc < stageCountdowns.length; sc++) {
   for(var sc = 0; sc < stageCountdowns.length; sc++) {
    getCountdownInfo(stageCountdowns[sc], true);
getCountdownInfo(stageCountdowns[sc], true);
   }
   }
    
    
   var countdowns = document.getElementsByClassName("countdown");
   var countdowns = document.getElementsByClassName("countdown");
   for(var c = 0; c < countdowns.length; c++) {
   for(var c = 0; c < countdowns.length; c++) {
    getCountdownInfo(countdowns[c], false);
getCountdownInfo(countdowns[c], false);
   }
   }
    
    
Line 615: Line 733:
var infoboxes = []; // Found infoboxes
var infoboxes = []; // Found infoboxes


// Specify the selected game
var setGame = function(e) {
// Ignore this event
if (
e.type == "keydown" && e.key != " " && e.key != "Enter" ||
e.type == "poitnerdown" && e.button != 0 // Left click only
) return;
// Switch games
var game = e.target;
for (; game && !game.game; game = game.parentNode);
if (!game)
return;
for (var z = 0; z < game.gobbler.infoboxes.length; z++) {
var infobox = game.gobbler.infoboxes[z];
infobox.classList[infobox == game.infobox ? "remove" : "add"]("inactive");
}
for (var z = 0; z < game.gobbler.games.length; z++) {
var button = game.gobbler.games[z];
if (!button.infobox)
continue;
button.classList[button == game ? "add" : "remove"]("active");
button.classList[button == game ? "remove" : "add"]("inactive");
}
}
// Add a new gobbler
var addGobbler = function(gobbler) {
gobbler.games = Array.from(gobbler.querySelectorAll(":scope > .infobox-game-list .infobox-game"));
for (var x = 0; x < gobbler.games.length; x++) {
var game = gobbler.games[x];
game.game = Array.from(game.classList).find(function(clazz) {
return clazz != "infobox-game";
});
game.gobbler = gobbler;
game.tabIndex = 0;
game.addEventListener("keydown", setGame);
game.addEventListener("pointerdown", setGame);
}
gobbler.infoboxes = [];
gobbler.infoboxKeys = new Set();
gobblers.push(gobbler);
};
// Add an infobox to a gobbler
// Add an infobox to a gobbler
var addInfobox = function(gobbler, infobox) {
var addInfobox = function(gobbler, infobox) {
 
// The infobox is already gobbled
// The infobox is already gobbled
if (gobbler.infoboxKeys.has(infobox))
if (gobbler.infoboxKeys.has(infobox))
return;
return;
gobbler.infoboxKeys.add(infobox);


// Locate the game button that applies to this infobox
// Locate the game button template that applies to this infobox
var classes = Array.from(infobox.classList);
var game = gobbler.games.find(function(game) {
var game = gobbler.games.find(function(game) {
return classes.indexOf(game.game) != -1;
return game.gameId == infobox.gameId;
});
});
if (!game)
if (!game)
return false; // No matching game button
return false; // No matching game button template
 
// Configure the infobox
// Produce a game button for the infobox
gobbler.infoboxes.push(infobox = infobox.cloneNode(true));
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);
gobbler.append(infobox);
game.infobox = infobox;
button.infobox = infobox;
game.classList.add("inactive");
gobbler.infoboxes.forEach(function(box) {
infobox.classList.add("inactive");
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
// Monitor for new gobblers and infoboxes
var interval = setInterval(function() {
var interval = setInterval(function() {


// Find new gobblers and infoboxes
// Find new gobblers and infoboxes
var newGobblers = Array.from(document.querySelectorAll(".infobox-gobbler:not(.active)"));
Array.from(document.querySelectorAll(".infobox-gobbler"))
for (var x = 0; x < newGobblers.length; x++)
.filter(function(g) { return gobblers.indexOf(g) == -1; })
addGobbler(newGobblers[x]);
.forEach(function(g) { gobblers.push(newGobbler(g)); })
var newInfoboxes = Array.from(document.querySelectorAll("*:not(.infobox-gobbler) .infobox"));
;
for (var x = 0; x < newInfoboxes.length; x++) {
Array.from(document.querySelectorAll("*:not(.infobox-gobbler) > .infobox"))
var infobox = newInfoboxes[x];
.forEach(function(i) { infoboxes.push(newInfobox(i)); })
infobox.remove();
;
infoboxes.push(infobox);
}


// Process all gobblers
// Process all gobblers
for (var x = 0; x < gobblers.length; x++) {
gobblers.forEach(function(gobbler) {
var gobbler = gobblers[x];
// No supported games
if (gobbler.games.length == 0)
continue;
// Process all infoboxes
// Process all infoboxes
for (var y = 0; y < infoboxes.length; y++)
infoboxes.forEach(function(infobox) {
addInfobox(gobbler, infoboxes[y]);
addInfobox(gobbler, infobox);
});
// Select the last game
});
gobbler.querySelector(".infobox-game-list").style
 
[gobbler.games.length == 1 ? "setProperty" : "removeProperty"]("display", "none");
for (var y = gobbler.games.length - 1; y >= 0; y--) {
var game = gobbler.games[y];
if (!game.infobox)
continue;
setGame({ target: game });
gobbler.classList.add("active");
break;
}
}
// Stop watching for new infoboxes
// Stop watching for new infoboxes
if (document.readyState == "complete")
if (document.readyState == "complete")

Latest revision as of 10:37, 13 February 2024

/* 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 <span class="insertusername"></span>.
// 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 <span class="inserteditcount"></span>
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 <span class="insertregistrationdate"></span>
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();

///////////////////////////////////////////////////////////////////////////////
//                           ChallengeSchedule Class                         //
///////////////////////////////////////////////////////////////////////////////
// By user Harimaron

// Maintains auto-updating Challenge schedule elements

// Object constructor
var ChallengeSchedule = function() {
	"use strict";
	this.timesEl = [
		document.getElementById("challengeTime1"),
		document.getElementById("challengeTime2"),
	];
	if (!this.timesEl[0])
		return; // No schedule data
	
	var lblFetched = document.getElementById("challengeFetched");
	if (!lblFetched)
		return;
	var now = new Date();
	var fetched = parseFetched(now, lblFetched.innerHTML);
	/**
	 * @param {string} str
	 */
	function parse(str) {
		if (!str.endsWith(" UTC"))
			return null;
		var dates = str.split("-", 2);
		if (dates.length != 2)
			return null;
		return ({
			start: parseSchedule(fetched, dates[0].trim()),
			end: parseSchedule(fetched, dates[1].slice(0, -4).trim()),
		});
	}
	this.data = [];
	for (var i = 0; i < this.timesEl.length; i++) {
		var timeEl = this.timesEl[i];
		if (!timeEl)
			continue;
		var orderEl = timeEl.getElementsByClassName("challengeOrder");
		if (!orderEl.length)
			continue;
		var current = {
			orderEl: orderEl[0],
			slots: [/*{
				el: new HTMLElement(),
				start: new Date(),
				end: new Date(),
			},*/],
		};
		var slotsEl = timeEl.getElementsByClassName("challengeTimeSlot");
		for (var j = 0; j < slotsEl.length; j++) {
			var slotEl = slotsEl[j];
			var slot = parse(slotEl.innerText);
			if (!slot)
				continue;
			current.slots.push({
				el: slotEl,
				start: slot.start,
				end: slot.end,
			});
		}
		if (current.slots.length > 0)
			this.data.push(current);
	}

	if (!this.data.length)
		return;
	var _this = this;
	this.timer = setInterval(function() { _this.onTick(new Date()); }, 1000);
};

// Periodic update handler
/**
 * @param now {Date}
 */
ChallengeSchedule.prototype.onTick = function(now) {
	"use strict";
	var upcomingOrNext = 1;
	var hasFuture = false;
	for (var i = 0; i < this.data.length; i++) {
		var current = this.data[i];
		for (var j = 0; j < current.slots.length; j++) {
			var slot = current.slots[j];
			var startStr = formatDateTime(slot.start);
			var endStr = zeroPad(slot.end.getHours(), 2) + ":" + zeroPad(slot.end.getMinutes(), 2);
			if (now >= slot.end) {
				slot.el.style.textDecoration = "line-through";
				slot.el.style.fontWeight = "normal";
			} else if (now >= slot.start) {
				slot.el.style.textDecoration = "underline";
				startStr = "Now";
				endStr += " (" + timeUntil(now, slot.end) + " left)";
			}
			slot.el.innerText = startStr + " - " + endStr;
		}
		var start = current.slots[0].start;
		var end = current.slots[current.slots.length - 1].end;
		if (now >= end) {
			upcomingOrNext = 0;
			current.orderEl.innerText = "Past";
		} else if (now >= start) {
			upcomingOrNext = 0;
			hasFuture = true;
			current.orderEl.innerText = "Current";
		} else if (upcomingOrNext <= 1) {
			hasFuture = true;
			current.orderEl.innerText = "Upcoming";
		} else {
			hasFuture = true;
			current.orderEl.innerText = "Next";
		}
		upcomingOrNext++;
	}
	if (!hasFuture) {
		clearInterval(this.timer);
	}
};

new ChallengeSchedule();

// ================================================================================
// 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("<a></a>");
				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") != "{{{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 "<day> <hour>|<24 hour msg>|<afterwards msg>"
	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);
})();

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