MediaWiki:Gadget-switch-infobox.js: Difference between revisions

From RuneRealm Wiki
Jump to navigation Jump to search
Content added Content deleted
(Created page with "/* 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 scri...")
(No difference)

Revision as of 01:11, 13 October 2024

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