MediaWiki:All.js: Difference between revisions

From Inkipedia, the Splatoon wiki
m (Fix game button styles in infobox gobbler)
m (Infobox gobbler: Initially select the first game button for the same game as the last button added)
Line 638: Line 638:
gobbler.gameList.append(button);
gobbler.gameList.append(button);
gobbler.buttons.push(button);
gobbler.buttons.push(button);
setButtonStyles(button);


// Add the infobox to the gobbler
// Add the infobox to the gobbler
Line 649: Line 648:
gobbler.infoboxes.forEach(function(box) {
gobbler.infoboxes.forEach(function(box) {
box.classList[box != infobox ? "add" : "remove"]("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;
})
});
});


Line 730: Line 738:


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


// Identify the button element
// Identify the button element

Revision as of 16:55, 7 October 2022

/* 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')

// ================================================================================
// 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();

// ================================================================================
// 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.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);
		infobox.remove();

		// 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);
})();

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