MediaWiki:All.js: Difference between revisions
From Inkipedia, the Splatoon wiki
GuyPerfect (talk | contribs) m (Debugging) |
(fix countdown when there is only one challenge in the schedule (thank you Harimaron)) |
||
(41 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 */ | ||
// ================================================================================ | // ================================================================================ | ||
Line 12: | Line 7: | ||
//Check page specific files | //Check page specific files | ||
$(function () { | $(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'); | |||
} | |||
}); | |||
}); | |||
} | |||
}); | }); | ||
Line 53: | Line 48: | ||
jQuery(function($) { | jQuery(function($) { | ||
if (typeof(disableUsernameReplace) != 'undefined' && disableUsernameReplace) | if (typeof(disableUsernameReplace) != 'undefined' && disableUsernameReplace) | ||
return; | |||
var username = mw.config.get('wgUserName'); | var username = mw.config.get('wgUserName'); | ||
if (username == null) | if (username == null) | ||
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; | |||
$('.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; | |||
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()); | |||
ret.setMinutes(0); | |||
ret.setTime(ret.getTime() + 3600000 * ( | |||
ret.getUTCHours() & 1 ? 1 : 2 | |||
)); | |||
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 = | |||
[ "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 | // 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:]+/); | |||
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 | // Parses a last-fetched string into a Date object | ||
function parseFetched(now, text) { | 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 | // Parses a schedule string into a Date object | ||
function parseSchedule(fetched, text) { | 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 | // 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 = 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 | // Pad a number with leading zeroes | ||
function zeroPad(number, digits) { | function zeroPad(number, digits) { | ||
number = "" + number; | |||
while (number.length < digits) | |||
number = "0" + number; | |||
return number; | |||
} | } | ||
Line 177: | Line 172: | ||
var BattleSchedule = function() { | 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); | |||
}; | }; | ||
Line 206: | Line 201: | ||
BattleSchedule.prototype.onTick = function(now) { | 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) | |||
; | |||
}; | }; | ||
Line 239: | Line 234: | ||
var SalmonSchedule = function() { | 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); | |||
}; | }; | ||
Line 263: | Line 258: | ||
SalmonSchedule.prototype.onTick = function(now) { | 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 | // Parse a single Salmon Run schedule slot | ||
SalmonSchedule.prototype.parse = function(element, fetched) { | 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 | |||
}; | |||
} | } | ||
Line 307: | Line 302: | ||
// Object constructor | // Object constructor | ||
var ShopSchedule = function() { | 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(); | new ShopSchedule(); | ||
Line 330: | Line 324: | ||
// Object constructor | // Object constructor | ||
var SplatfestSchedule = function() { | 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); | |||
}; | }; | ||
Line 350: | Line 345: | ||
SplatfestSchedule.prototype.onTick = function(now) { | 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); | |||
}; | }; | ||
Line 374: | Line 369: | ||
SplatfestSchedule.prototype.parse = function(element, now) { | 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(); | 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){ | |||
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(){ | 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"); | |||
}) | |||
}) | }) | ||
Line 504: | Line 622: | ||
// Parse time fields from the number | // Parse time fields from the number | ||
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, | |||
h: hours, | |||
m: mins, | |||
s: secs, | |||
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; | |||
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"; | |||
} | |||
} | } | ||
} | } | ||
Line 564: | Line 682: | ||
if(!stage) { | 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( { | countdowns.push( { | ||
span: countdown, | |||
stage: stage, | |||
time: time, | |||
ongoingMessage: ongoingMessage, | |||
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); | |||
} | } | ||
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); | |||
} | } | ||
Line 615: | Line 733: | ||
var infoboxes = []; // Found infoboxes | var infoboxes = []; // Found infoboxes | ||
// 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 game = gobbler.games.find(function(game) { | var game = gobbler.games.find(function(game) { | ||
return | return game.gameId == infobox.gameId; | ||
}); | }); | ||
if (!game) | if (!game) | ||
return false; // No matching game button | return false; // No matching game button template | ||
// | // Produce a game button for the infobox | ||
gobbler. | 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); | ||
button.infobox = infobox; | |||
game.classList.add(" | 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 | ||
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 | // Process all gobblers | ||
gobblers.forEach(function(gobbler) { | |||
// Process all infoboxes | // Process all infoboxes | ||
infoboxes.forEach(function(infobox) { | |||
addInfobox(gobbler, | addInfobox(gobbler, infobox); | ||
}); | |||
}); | |||
// 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); })(); ////////////////////////////////////////////////////////////////////////////////