MediaWiki:Gadget-switch-infobox.js

This is an old revision of this page, as edited by Alex (talk | contribs) at 17:15, 17 October 2024. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

After saving, you may need to bypass your browser's cache to see the changes. For further information, see Wikipedia:Bypass your cache.

  • In most Windows and Linux browsers: Hold down Ctrl and press F5.
  • In Safari: Hold down ⇧ Shift and click the Reload button.
  • In Chrome and Firefox for Mac: Hold down both ⌘ Cmd+⇧ Shift and press R.
/* switch infobox code for infoboxes
 * contains switching code for both:
 * * originalInfoboxes:
 *		older infobox switching, such as [[Template:Infobox Bonuses]]
 *		which works my generating complete infoboxes for each version
 * * moduleInfoboxes:
 *		newer switching, as implemented by [[Module:Infobox]]
 *		which generates one infobox and a resources pool for switching
 * * synced switches
 *		as generated by [[Module:Synced switch]] and its template
 * 
 * The script also facilitates synchronising infoboxes, so that if a button of one is pressed
 *	and another switchfobox on the same page also has that button, it will 'press' itself
 * This only activates if there are matching version parameters in the infoboxes (i.e. the button text is the same)
 * - thus it works best if the version parameters are all identical
 * 
 * TODO: OOUI? (probably not, its a little clunky and large for this. It'd need so much styling it isn't worthwhile)
 */
$(function () {
	var SWITCH_REF_REGEX = /^\$(\d+)/,
		CAN_LOCAL_STORAGE = true;
	function getGenderFromLS() {
		if (CAN_LOCAL_STORAGE) {
			var x = window.localStorage.getItem('gender-render');
			if (['m', 'f'].indexOf(x) > -1) {
				return x;
			}
		}
		return 'm';
	}
	/**
	 * Switch infobox psuedo-interface
	 * 
	 * Switch infoboxes are given several similar functions so that they can be called similarly
	 * This is essentially like an interface or class structure, except I'm too lazy to implement that
	 * 
	 * 		switchfo.beginSwitchEvent(event)
	 * 			the reactionary event to buttons being clicked/selects being selected/etc
	 * 			tells SwitchEventManager to switch all the boxes
	 * 			should extract an index and anchor from the currentTarget and pass that to the SwitchEventManager.trigger function
	 * 			event		the jQuery event fired from $.click/$.change/etc
	 * 
	 * 		switchfo.switch(index, anchor)
	 * 			do all the actual switching of the infobox to the infobox specified by the anchor and index
	 * 			prefer using the anchor if there is a conflict
	 * 
	 * 		switchfo.defaultVer()
	 * 			called during script init
	 * 			returns either an anchor for the default version, if manually specified, or false if there is no default specified
	 * 			the page will automatically switch to the default version, or to version 1, when loaded.
	 * 
	 */
	/** 
	 * Switch Infoboxes based on [[Module:Infobox]]
	 * 
	 * - the preferred way to do switch infoboxes
	 * - generates one table and a resources table, swaps resources into the table as required
	 * - with enough buttons, becomes a dropdown <select>
	 * 
	 * parameters
	 *	  $box	jQuery object representing the infobox itself (.infobox-switch)
	 *	  index   index of this infobox, from $.each
	 */
	function SwitchInfobox($box, index, version_index_offset) {
		var self = this;
		this.index = index;
		this.version_index_offset = version_index_offset;
		this.$infobox = $box;
		this.$infobox.data('SwitchInfobox', self);
		this.$resources = self.$infobox.next();
		this.$buttons = self.$infobox.find('div.infobox-buttons');
		this.version_count = this.$buttons.find('span.button').length;
		this.isSelect = self.$buttons.hasClass('infobox-buttons-select');
		this.$select = null;
		this.originalClasses = {};

		/* click/change event - triggers switch event manager */
		this.beginSwitchEvent = function(e) {
			var $tgt = $(e.currentTarget);
			mw.log('beginSwitchEvent triggered in module infobox, id '+self.index);
			if (self.isSelect) {
				window.switchEventManager.trigger($tgt.val(), $tgt.find(' > option[data-switch-index='+$tgt.val()+']').attr('data-switch-anchor'), self);
			} else {
				window.switchEventManager.trigger($tgt.attr('data-switch-index'), $tgt.attr('data-switch-anchor'), self);
			}
		};

		/* switch event, triggered by manager */
		this.switchInfobox = function(index, text) {
			if (text === '@init@') {
				text = self.$buttons.find('[data-switch-index="1"]').attr('data-switch-anchor');
			}
			var ind, txt, $thisButton = self.$buttons.find('[data-switch-anchor="'+text+'"]');
			mw.log('switching module infobox, id '+self.index);
			// prefer text
			if ($thisButton.length) {
				txt = text;
				ind = $thisButton.attr('data-switch-index');
			} 
			if (ind === undefined) {
				return;
				/*ind = index;
				$thisButton = self.$buttons.find('[data-switch-index="'+ind+'"]');
				if ($thisButton.length) {
					txt = $thisButton.attr('data-switch-anchor');
				}*/
			}
			if (txt === undefined) {
				return;
			}
			if (self.isSelect) {
				self.$select.val(ind);
			} else {
				self.$buttons.find('span.button').removeClass('button-selected');
				$thisButton.addClass('button-selected');
			}
			
			self.$infobox.find('[data-attr-param][data-attr-param!=""]').each(function(i,e) {
				var $e = $(e),
					param = $e.attr('data-attr-param'),
					$switches = self.$resources.find('span[data-attr-param="'+param+'"]'),
					m,
					$val,
					$classTgt;
				
				// check if we found some switch data
				if (!$switches.length) return;

				// find value
				$val = $switches.find('span[data-attr-index="'+ind+'"]');
				if (!$val.length) {
					// didn't find it, use default value
					$val = $switches.find('span[data-attr-index="0"]');
					if (!$val.length) return;
				}
				// switch references support - $2 -> use the value for index 2
				m = SWITCH_REF_REGEX.exec($val.html());
				if (m) { // m is null if no matches
					$val = $switches.find('span[data-attr-index="'+m[1]+'"]'); // m is [ entire match, capture ]
					if (!$val.length) {
						$val = $switches.find('span[data-attr-index="0"]'); // fallback again
						if (!$val.length) return;
					}
				}
				$val = $val.clone(true,true);
				$e.empty().append($val.contents());

				// class switching
				// find the thing we're switching classes for
				if ($e.is('td, th')) {
					$classTgt = $e.parent('tr');
				} else {
					$classTgt = $e;
				}

				// reset classes
				if (self.originalClasses.hasOwnProperty(param)) {
					$classTgt.attr('class', self.originalClasses[param]);
				} else {
					$classTgt.removeAttr('class');
				}

				// change classes if needed
				if ($val.attr('data-addclass') !== undefined) {
					$classTgt.addClass($val.attr('data-addclass'));
				}
			});
			// trigger complete event for inter-script functions
			self.$buttons.trigger('switchinfoboxComplete', {txt:txt, num:ind});
			//re-initialise quantity boxes, if any
			if (window.rswiki && typeof(rswiki.initQtyBox) == 'function') {
				rswiki.initQtyBox(self.$infobox)
			}
			//console.log(this);
		};
		
		/* default version, return the anchor of the switchable if it exists */
		this.defaultVer = function () {
			var defver = self.$buttons.attr('data-default-version');
			if (defver !== undefined) {
				return { idx: defver, txt: self.$buttons.find('[data-switch-index="'+defver+'"]').attr('data-switch-anchor') };
			}
			return false;
		};
		
		this.isParentOf = function ($triggerer) {
			return self.$infobox.find($triggerer).length > 0;
		};
		
		this.currentlyShowing = function(){
			if (self.isSelect) {
				var sel = self.$select.val();
				return {index: sel, text: self.$select.find('option[value="'+sel+'"]').attr('data-switch-anchor')}
			} else {
				var buttn = self.$buttons.find('.button-selected');
				return {index: buttn.attr('data-switch-index'), text: buttn.attr('data-switch-anchor')}
			}
		}

		/* init */
		mw.log('setting up module infobox, id '+self.index);
		// setup original classes
		this.$infobox.find('[data-attr-param][data-attr-param!=""]').each(function(i,e){
			var $e = $(e), $classElem = $e, clas;
			if ($e.is('td, th')) {
				$classElem = $e.parent('tr');
			}
			clas = $classElem.attr('class');
			if (typeof clas === 'string') {
				self.originalClasses[$e.attr('data-attr-param')] = clas;
			}
		});

		// setup select/buttons and events
		if (self.isSelect) {
			self.$select = $('<select>')
				.attr({
					id: 'infobox-select-' + self.index,
					name: 'infobox-select-' + self.index,
				});
			self.$buttons.find('span.button').each(function(i, e){
				var $e = $(e);
				self.$select.append(
					$('<option>').attr({
						value: $e.attr('data-switch-index'),
						'data-switch-index': $e.attr('data-switch-index'),
						'data-switch-anchor': $e.attr('data-switch-anchor')
					}).text($e.text())
				);
			});
			self.$buttons.empty().append(self.$select);
			self.$select.change(self.beginSwitchEvent);
		} else {
			self.$buttons
				.attr({
					id: 'infobox-buttons-'+self.index
				})
				.find('span').each(function(i,e) {
					$(e).click(self.beginSwitchEvent);
				});
		}

		self.$buttons.css('display', 'flex');
		self.switchInfobox(1, '@init@');

		window.switchEventManager.addSwitchInfobox(this);
		if (this.$infobox.find('.infobox-bonuses-image.render-m').length === 1 && this.$infobox.find('.infobox-bonuses-image.render-f').length === 1) {
			this.genderswitch = new GenderRenderSwitcher(this.$infobox, this.index);
		}
	}
	
	/**
	 * Special support for gender render switching in infobox bonuses (& synced switch)
	 * Currently specifically only supports male & female
	 * potential TODO: generalise?
	 * 
	 * parameters
	 *	  $box	jQuery object representing the infobox itself (.infobox-switch)
	 */
	function GenderRenderSwitcher($box, index, version_index_offset) {
		var self = this;
		this.$box = $box;
		this.$box.data('SwitchInfobox', self);
		this.index = index;
		this.version_index_offset = version_index_offset;
		this.version_count = 2;
		this.$buttons = $('<div>').addClass('infobox-buttons').css('display', 'flex');
		this.button = {
			m: $('<span>').addClass('button').attr('data-gender-render', 'm').text('Male'),
			f: $('<span>').addClass('button').attr('data-gender-render', 'f').text('Female')
		};
		this.$td = $('<td>');
		this.$td_inner = $('<div class="gender-render-inner">');
		this.visible_gender = '';
		
		// from interface, we can just get the SyncedSwitches to switch
		this.beginSwitchEvent = function(event){
			var $e = $(event.currentTarget);
			var gen = $e.attr('data-gender-render');
			mw.log('beginSwitchEvent for genderswitcher '+self.index+' - switching to '+gen);
			window.switchEventManager.triggerGenderRenderSwitch(gen);
			if (CAN_LOCAL_STORAGE) {
				window.localStorage.setItem('gender-render', gen);
			}
		};
		// do the actual switching
		this.genderSwitch = function(gender) {
			mw.log('switching gender for genderswitcher for '+self.index+' to '+gender);
			self.$buttons.find('.button-selected').removeClass('button-selected');
			self.button[gender].addClass('button-selected');

			var x = self.$box.find('.infobox-bonuses-image.render-'+gender+'');
			self.$td_inner.empty().append(x.find('>*').clone());
			self.visible_gender = gender;
		};
		this.refreshImage = function(index,anchor) {
			// for when a main infobox switch happens
			// this is a post-switch function so the new images are in the original cells
			// we just gotta clone them into the visible cell again
			self.genderSwitch(self.visible_gender);
			mw.log('refreshed image for genderswitcher '+self.index);
		};
		this.currentlyShowing = function(){
			return {index: -1, text: self.visible_gender}
		}
		
		// other 'interface' methods just so stuff doesn't break, just in case
		this.switchInfobox = function(ind,anchor){/* do nothing */};
		this.defaultVer = function(){ return false; };

		mw.log('Initialising genderswitcher for '+self.index);
		var $c_m = this.$box.find('.infobox-bonuses-image.render-m'), $c_f=this.$box.find('.infobox-bonuses-image.render-f');
		this.$td.addClass('gender-render').attr({
			'style': $c_m.attr('style'),
			'rowspan': $c_m.attr('rowspan')
		}).append(this.$td_inner);
		$c_m.parent().append(this.$td);
		this.$buttons.append(this.button.m, this.button.f);
		this.$td.append(this.$buttons);
		this.$buttons.find('span.button').on('click', this.beginSwitchEvent);

		$c_m.addClass('gender-render-hidden').attr('data-gender-render', 'm');
		$c_f.addClass('gender-render-hidden').attr('data-gender-render', 'f');
		window.switchEventManager.addGenderRenderSwitch(self);
		window.switchEventManager.addPostSwitchEvent(this.refreshImage);
		this.genderSwitch(getGenderFromLS());
	}

	/**
	 * Legacy switch infoboxes, as generated by [[Template:Switch infobox]]
	 * 
	 * 
	 * parameters
	 *	  $box	jQuery object representing the infobox itself (.switch-infobox)
	 *	  index   index of this infobox, from $.each
	 */
	function LegacySwitchInfobox($box, index, version_index_offset) {
		var self = this;
		this.$infobox = $box;
		this.$infobox.data('SwitchInfobox', self);
		this.$parent = $box;
		this.index = index;
		this.version_index_offset = version_index_offset;
		this.$originalButtons = self.$parent.find('.switch-infobox-triggers');
		this.$items = self.$parent.find('.item');
		this.version_count = self.$originalButtons.find('span.trigger.button').length;

		/* click/change event - triggers switch event manager */
		this.beginSwitchEvent = function(e) {
			var $tgt = $(e.currentTarget);
			mw.log('beginSwitchEvent triggered in legacy infobox, id '+self.index);
			window.switchEventManager.trigger($tgt.attr('data-id'), $tgt.attr('data-anchor'), self);
		};

		/* click/change event - triggers switch event manager */
		this.switchInfobox = function(index, text){
			if (text === '@init@') {
				text = self.$buttons.find('[data-id="1"]').attr('data-anchor');
			}
			var ind, txt, $thisButton = self.$buttons.find('[data-anchor="'+text+'"]').first();
			mw.log('switching legacy infobox, id '+self.index);
			if ($thisButton.length) {
				txt = text;
				ind = $thisButton.attr('data-id');
			} else {
				return;
				/*ind = index;
				$thisButton = self.$buttons.find('[data-id="'+ind+'"]');
				if ($thisButton.length) {
					txt = $thisButton.attr('data-anchor');
				}*/
			}
			if (txt === undefined) {
				return;
			}
			self.$buttons.find('.trigger').removeClass('button-selected');
			self.$buttons.find('.trigger[data-id="'+ind+'"]').addClass('button-selected');
			
			self.$items.filter('.showing').removeClass('showing');
			self.$items.filter('[data-id="'+ind+'"]').addClass('showing');
		};
		
		/* default version - not supported by legacy, always false */
		this.defaultVer = function () { return false; };
		
		this.isParentOf = function ($triggerer) {
			return self.$parent.find($triggerer).length > 0;
		};
		this.currentlyShowing = function(){
			var buttn = self.$buttons.find('.button-selected');
			return {index: buttn.attr('data-id'), text: buttn.attr('data-anchor')}
		}

		/* init */
		mw.log('setting up legacy infobox, id '+self.index);
		// add anchor text
		self.$originalButtons.find('span.trigger.button').each(function(i,e){
			var $e = $(e);
			var anchorText = $e.text().split(' ').join('_');
			$e.attr('data-anchor', '#'+anchorText);
		});

		// append triggers to every item
		// if contents has a infobox, add to a caption of that
		// else just put at top
		self.$items.each(function(i,e){
			var $item = $(e);
			if ($item.find('table.infobox').length > 0) {
				if ($item.find('table.infobox caption').length < 1) {
					$item.find('table.infobox').prepend('<caption>');
				}
				$item.find('table.infobox caption').first().prepend(self.$originalButtons.clone());
			} else {
				$item.prepend(self.$originalButtons.clone());
			}
		});
		// remove buttons from current location
		self.$originalButtons.remove();

		// update selection
		this.$buttons = self.$parent.find('.switch-infobox-triggers');
		self.$buttons.find('.trigger').each(function (i,e) {
			$(e).click(self.beginSwitchEvent);
		});
		self.switchInfobox(1, '@init@');
		
		window.switchEventManager.addSwitchInfobox(this);
		self.$parent.removeClass('loading').find('span.loading-button').remove();
	}

	/**
	 * Synced switches, as generated by [[Template:Synced switch]]
	 * 
	 * 
	 * parameters
	 *	  $box	jQuery object representing the synced switch itself (.rsw-synced-switch)
	 *	  index   index of this infobox, from $.each
	 */
	function SyncedSwitch($box, index, version_index_offset) {
		var self = this;
		this.index = index;
		this.version_index_offset = version_index_offset; //not actually used
		this.version_count = 0; // we don't increment from this
		this.$syncedswitch = $box;
		this.$syncedswitch.data('SwitchInfobox', self);
		this.attachedLabels = false;
		this.is_synced_switch = true;

		/* filling in interface - synced switch has no buttons to press so cannot trigger an event by itself */
		this.beginSwitchEvent = function (){};

		this.switchInfobox = function(index, text){
			mw.log('switching synced switch, id '+self.index+", looking for "+index+' - '+text);
			if (text === '@init@') {
				text = self.$syncedswitch.find('[data-item="1"]').attr('data-item-text');
			}
			var $toShow = self.$syncedswitch.find('[data-item-text="'+text+'"]');
			if (!(self.attachedLabels && $toShow.length)) {
				//return;
				$toShow = self.$syncedswitch.find('[data-item="'+index+'"]');
			}
			if (!$toShow.length) {
				// show default instead
				self.$syncedswitch.find('.rsw-synced-switch-item').removeClass('showing');
				self.$syncedswitch.find('[data-item="0"]').addClass('showing');
			} else {
				self.$syncedswitch.find('.rsw-synced-switch-item').removeClass('showing');
				$toShow.addClass('showing');
			}
		};

		this.genderSwitch = function(gender){
			var $gens = self.$syncedswitch.find('.render-m, .render-f');
			var srch = '.render-'+gender;
			if ($gens.length) {
				$gens.each(function(i,e){
					var $e = $(e);
					if ($e.is(srch)) {
						$e.removeClass('gender-render-hidden').addClass('gender-render-showing');
					} else {
						$e.removeClass('gender-render-showing').addClass('gender-render-hidden');
					}
				});
			}
		};
		
		/* default version - not supported by synced switches, always false */
		this.defaultVer = function () { return false; };
		
		this.isParentOf = function ($triggerer) {
			return self.$syncedswitch.find($triggerer).length > 0;
		};
		this.currentlyShowing = function(){
			var buttn = self.$syncedswitch.find('.rsw-synced-switch-item.showing');
			return {index: buttn.attr('data-item'), text: buttn.attr('data-item-text')}
		}
		
		/* init */
		mw.log('setting up synced switch, id '+self.index);
		// attempt to apply some button text from a SwitchInfobox
		if ($('.infobox.infobox-switch').length && !$('.multi-infobox').length) {
			self.attachedLabels = true;
			var $linkedButtonTextInfobox = $('.infobox.infobox-switch').first();
			self.$syncedswitch.find('.rsw-synced-switch-item').each(function(i,e){
				var $e = $(e);
				if ($e.attr('data-item-text') === undefined) {
					$e.attr('data-item-text', $linkedButtonTextInfobox.find('[data-switch-index="'+i+'"]').attr('data-switch-anchor'));
				}
			});
		}
		self.switchInfobox(1, '@init@');
		window.switchEventManager.addSwitchInfobox(this);
		if (self.$syncedswitch.find('.render-m, .render-f').length) {
			window.switchEventManager.addGenderRenderSwitch(self);
			this.genderSwitch(getGenderFromLS());
		}
	}
	
	/** 
	 * An infobox that doesn't switch
	 * used to make sure MultiInfoboxes interact with SyncedSwitches correctly
	 * 
	 */
	function NonSwitchingInfobox($box, index, version_index_offset){
		var self = this;
		this.$infobox = $box;
		this.index = index;
		this.version_index_offset = version_index_offset;
		this.$infobox.data('SwitchInfobox', self);
		this.version_count = 1;
		
		this.beginSwitchEvent = function (){}; //do nothing
		this.switchInfobox = function(index, text){return}; //do nothing
		this.defaultVer = function () {return true;};
		this.isParentOf = function ($triggerer) {return false;};
		this.currentlyShowing = function(){
			return {text:null, index: 1};
		};
	}

	/**
	 * Event manager
	 * Observer pattern
	 * Globally available as window.switchEventManager
	 * 
	 * Methods
	 *	  addSwitchInfobox(l)
	 *		  adds switch infobox (of any type) to the list of switch infoboxes listening to trigger events
	 *		  l	   switch infobox
	 * 
	 * 		addPreSwitchEvent(f)
	 * 			adds the function to a list of functions that runs when the switch event is triggered but before any other action is taken
	 * 			the function is passed the index and anchor (in that order) that was passed to the trigger function
	 * 			returning the boolean true from the function will cancel the switch event
	 * 			trying to add a non-function is a noop
	 * 			e		function to run
	 * 
	 * 		addPostSwitchEvent(f)
	 * 			adds the function to a list of functions that runs when the switch event is completed, after all of the switching is completed (including the hash change)
	 * 			the function is passed the index and anchor (in that order) that was passed to the trigger function
	 * 			the return value is ignored
	 * 			trying to add a non-function is a noop
	 * 			e		function to run
	 * 
	 *	  trigger(i, a)
	 *		  triggers the switch event on all listeners
	 *		  will prefer switching to the anchor if available
	 *		  i	   index to switch to
	 *		  a	   anchor to switch to
	 * 
	 * 		makeSwitchInfobox($box)
	 * 			creates the correct object for the passed switch infobox, based on the classes of the infobox
	 * 			is a noop if it does not match any of the selectors
	 * 			infobox is given an index based on the internal counter for the switch
	 * 			$box		jQuery object for the switch infobox (the jQuery object passed to the above functions, see above for selectors checked)
	 * 
	 * 		addIndex(i)
	 * 			updates the internal counter by adding i to it
	 * 			if i is not a number or is negative, is a noop
	 * 			used for manually setting up infoboxes (init) or creating a new type to plugin
	 * 			i	number to add
	 */

	function SwitchEventManager() {
		var self = this, switchInfoboxes = [], syncedSwitches=[], genderRenderSwitchers = [], preSwitchEvents = [], postSwitchEvents = [], index = 0, version_offset = 0;
		window.switchEventManager = this;
		
		// actual switch infoboxes to change
		this.addSwitchInfobox = function(l) {
			switchInfoboxes.push(l);
			if (l.is_synced_switch) {
				syncedSwitches.push(l);
			}
		};

		this.addGenderRenderSwitch = function(gs) {
			gs.version_index_offset = version_offset;
			genderRenderSwitchers.push(gs);
			version_offset += gs.version_count;
		};
		
		// things to do when switch button is clicked but before any switching
		this.addPreSwitchEvent = function(e) {
			if (typeof(e) === 'function') {
				preSwitchEvents.push(e);
			}
		};
		this.addPostSwitchEvent = function(e) {
			if (typeof(e) === 'function') {
				postSwitchEvents.push(e);
			}
		};

		this.trigger = function(index, anchor, triggerer) {
			mw.log('Triggering switch event for index '+index+'; text '+anchor);
			// using a real for loop so we can use return to exit the trigger function
			for (var i=0; i < preSwitchEvents.length; i++){
				var ret = preSwitchEvents[i](index,anchor);
				if (typeof(ret) === 'boolean') {
					if (ret) {
						mw.log('switching was cancelled');
						return;
					}
				}
			}

			// close all tooltips on the page
			$('.js-tooltip-wrapper').trigger('js-tooltip-close');

			// trigger switching on listeners
			switchInfoboxes.forEach(function (e) {
				if (triggerer === null || !e.isParentOf(triggerer.$infobox)) {
					if (e.is_synced_switch && triggerer !== null) {
						e.switchInfobox(parseInt(index)+triggerer.version_index_offset, anchor);
					} else {
						e.switchInfobox(index, anchor);
					}
				}
			});

			// update hash
			if (typeof anchor === 'string') {
				var _anchor = anchor;
				if (_anchor === '@init@') {
					_anchor = '';
				}
				
				if (window.history && window.history.replaceState) {
					if (window.location.hash !== '') {
						window.history.replaceState({}, '', window.location.href.replace(window.location.hash, _anchor));
					} else {
						window.history.replaceState({}, '', window.location.href + _anchor);
					}
				} else {
					// replaceState not supported, I guess we just change the hash normally?
					window.location.hash = _anchor;
				}
			}

			postSwitchEvents.forEach(function(e){
				e(index, anchor);
			});
		};

		this.triggerGenderRenderSwitch = function(gender){
			mw.log(genderRenderSwitchers);
			for (var i = 0; i<genderRenderSwitchers.length; i++) {
				genderRenderSwitchers[i].genderSwitch(gender);
			}
		};
		
		this.triggerMultiInfoboxTabChange = function($multiInfobox) {
			mw.log('switching syncedswitches from tabber click', $multiInfobox)
			setTimeout(function(){
				var $tabcontents = $multiInfobox.find('div.tabber > div.tabbertab[style=""]');
				var $infobox = $tabcontents.find('.infobox').first();
				var swinfo = $infobox.data('SwitchInfobox');
				mw.log('switchingdata', $tabcontents, $infobox, swinfo);
				if (swinfo !== null && swinfo !== undefined) {
					var cs = swinfo.currentlyShowing();
					var ind = parseInt(cs.index) + swinfo.version_index_offset;
					mw.log('inside if', cs, ind)
					syncedSwitches.forEach(function (e) {
						mw.log('inside foreach', e);
						e.switchInfobox(ind, '');
					});
				} else {mw.log('swinfo is undefnull');}
			}, 20);
		};
		
		/* attempts to detect what type of switch infobox this is and applies the relevant type */
		// mostly for external access
		this.makeSwitchInfobox = function($e) {
			if ($e.is('.infobox-switch')) {
				return new SwitchInfobox($e, index++, version_offset);
			}
			if ($e.hasClass('switch-infobox')) {
				return new LegacySwitchInfobox($e, index++, version_offset);
			}
			if ($e.hasClass('rsw-synced-switch')) {
				return new SyncedSwitch($e, index++, version_offset);
			}
			if ($e.hasClass('infobox')) {
				return new NonSwitchingInfobox($e, index++, version_offset);
			}
			console.log('Invalid element sent to SwitchEventManager.makeSwitchInfobox:', $e)
		};
		this.addIndex = function(i) {
			if (typeof(i) === 'number') {
				 i += Math.max(Math.floor(i), 0);
			}
		};
		this.applyDefaultVersion = function() {
			if (window.location.hash !== '') {
				self.trigger(1, window.location.hash, null);
				return;
			} else {
			// real for loop so we can return out of the function
				for (var i = 0; i<switchInfoboxes.length; i++) {
					var defver = switchInfoboxes[i].defaultVer();
					if (typeof(defver) === 'object') {
						self.trigger(defver.idx, defver.txt, null);
						return;
					}
				}
			}
			self.trigger(1, '@init@', null);
		};
		
		// init
		this.init = function(){
			$('.infobox, .switch-infobox, .rsw-synced-switch').each(function(i,e){
				var obj = self.makeSwitchInfobox($(e));
				version_offset += obj.version_count;
			});
			
			
			// for {{Multi Infobox}}
			// there isn't a hook for tabber being ready, so we just gotta check until it is
			function initMultiInfobox(){
				if ($('#mw-content-text .multi-infobox .tabber.tabberlive').length) { // class tabberlive is added when it is ready
					$('#mw-content-text .multi-infobox').each(function(i,e){
						$(e).find('.tabber > ul.tabbernav > li').click(function(ev){
							self.triggerMultiInfoboxTabChange($(ev.currentTarget).parents('.multi-infobox'));
						});
					});
					$('#mw-content-text .multi-infobox .tabber.tabberlive ul.tabbernav li.tabberactive').click(); //trigger event once now
				} else {
					window.setTimeout(initMultiInfobox, 20);
				}
			}
			if ($('#mw-content-text .multi-infobox').length) {
				initMultiInfobox();
			}
			
			self.applyDefaultVersion();
		}
		this.init();
	}

	mw.hook('wikipage.content').add(function init( $content ) {
		if (!($content.find('.switch-infobox').length || $content.find('.infobox-buttons').length)) {
			return;
		}
		// mirror rsw-util
		try {
			localStorage.setItem('test', 'test');
			localStorage.removeItem('test');
			CAN_LOCAL_STORAGE = true;
		} catch (e) {
			CAN_LOCAL_STORAGE = false;
		}
		window.switchEventManager = new SwitchEventManager();

		// reinitialize any kartographer map frames added due to a switch
		if ($content.find('.infobox-switch .mw-kartographer-map').length
		|| $content.find('.infobox-switch-resources .mw-kartographer-map').length
		|| $content.find('.switch-infobox .mw-kartographer-map').length
		|| $content.find('.rsw-synced-switch .mw-kartographer-map').length) {
			window.switchEventManager.addPostSwitchEvent(function() {
				mw.hook('wikipage.content').fire($content.find('a.mw-kartographer-map').parent());
			});
		}
	});
})