MediaWiki:Gadget-switch-infobox.js

From WIDEVERSE Wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// <nowiki>
/* 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
 *		now also has option for buttons, and mutliple versions per tab
 * 
 * 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 ($, mw) {
	if (!($('.switch-infobox').length || $('.infobox-buttons').length)) {
		return;
	}

	var SWITCH_REF_REGEX = /^\$(\d+)/;
	/**
	 * 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) {
		var self = this;
		this.index = index;
		this.$infobox = $box;
		this.$resources = self.$infobox.parent().find('.infobox-switch[data-resource-class="'+self.$infobox.attr('data-resource-class')+'"] + .infobox-switch-resources'+self.$infobox.attr('data-resource-class'));
		this.$buttons = self.$infobox.find('div.infobox-buttons');
		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.$infobox);
			} else {
				window.switchEventManager.trigger($tgt.attr('data-switch-index'), $tgt.attr('data-switch-anchor'), self.$infobox);
			}
		};

		/* switch event, triggered by manager */
		this.switchInfobox = function(index, text) {
			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) {
				ind = index;
				$thisButton = self.$buttons.find('[data-switch-index="'+ind+'"]');
				if ($thisButton.length) {
					txt = $thisButton.attr('data-switch-anchor');
				}
			}
			// for all things set to switch
			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)
			}
		};
		
		/* 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;
		};

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

		window.switchEventManager.addSwitchInfobox(this);

	}

	/**
	 * 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) {
		var self = this;
		this.$parent = $box;
		this.index = index;
		this.$originalButtons = self.$parent.find('.switch-infobox-triggers');
		this.isSelect = self.$originalButtons.hasClass('infobox-triggers-select');
		this.$items = self.$parent.find('.item');

		/* 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);
			if (self.isSelect) {
				window.switchEventManager.trigger($tgt.val(), $tgt.find(' > option[data-id='+$tgt.val()+']').attr('data-anchor'), self.$parent);
			} else {
				window.switchEventManager.trigger($tgt.attr('data-id'), $tgt.attr('data-anchor'), self.$parent);
			}
		};

		/* click/change event - triggers switch event manager */
		this.switchInfobox = function(index, text){
			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 {
				ind = index;
				$thisButton = self.$buttons.find('[data-id="'+ind+'"]');
				if ($thisButton.length) {
					txt = $thisButton.attr('data-anchor');
				}
			}
			if (txt === undefined) {
				return;
			}
			
			if (self.isSelect) {
				self.$buttons.find('select').val(ind);
			} else {
				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;
		};

		/* 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);
			$e.attr('data-anchor', '#'+$e.text().replace(' ', '_'));
		});
		
		// setup select/buttons and events
		if (self.isSelect) {
			self.$select = $('<select>')
				.attr({
					id: 'infobox-select-' + self.index,
					name: 'infobox-select-' + self.index,
				});
			self.$originalButtons.find('span.trigger.button').each(function(i, e){
				var $e = $(e);
				self.$select.append(
					$('<option>').attr({
						value: $e.attr('data-id'),
						'data-id': $e.attr('data-id'),
						'data-anchor': $e.attr('data-anchor')
					}).text($e.text())
				);
			});
			self.$originalButtons.empty().append(self.$select);
			self.$select.change(self.beginSwitchEvent);
		}

		// append triggers to every item
		// if contents has a rsw-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.rsw-infobox').length > 0) {
				if ($item.find('table.rsw-infobox caption').length < 1) {
					$item.find('table.rsw-infobox').prepend('<caption>');
				}
				$item.find('table.rsw-infobox caption').first().prepend(self.$originalButtons.clone());
			} else {
				$item.prepend(self.$originalButtons.clone());
			}
		});
		// remove buttons from current location
		self.$originalButtons.remove();
		
		// update selection
		self.$buttons = self.$parent.find('.switch-infobox-triggers');
		if (self.isSelect) {
			self.$buttons.find('select').change(self.beginSwitchEvent);
		} else {
			self.$buttons.find('.trigger').each(function (i,e) {
				$(e).click(self.beginSwitchEvent);
			});
		}
		
		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) {
		var self = this;
		this.index = index;
		this.$syncedswitch = $box;
		this.$buttons = self.$syncedswitch.find('div.synced-buttons');
		this.attachedLabels = false;

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

		/* switch event, triggered by manager */
		this.switchInfobox = function(index, text){
			mw.log('switching synced switch, id '+self.index);
			var $toShow = self.$syncedswitch.find('.rsw-synced-switch-item[data-item-text="'+text+'"]'),
				$thisButton = self.$buttons.find('[data-item-text="'+text+'"]');
			if (!$toShow.length) {
				// Check for multi version data
				self.$syncedswitch.find('.rsw-synced-switch-item[data-item-vers]').each(function(j,k){
					var term = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '#';
					if (term != '#' && $(k).attr('data-item-vers').match(term)) {
						$toShow = $(k);
					}
				});
			}
			if (!(self.attachedLabels && $toShow.length)) {
				$toShow = self.$syncedswitch.find('.rsw-synced-switch-item[data-item="'+index+'"]');
			}
			if ( self.$buttons && !$thisButton.length && $toShow.length) {
				$thisButton = self.$buttons.find('[data-item="'+$toShow.attr('data-item')+'"]');
			}
			if (!$toShow.length) {
				// show default instead
				self.$syncedswitch.find('.rsw-synced-switch-item').removeClass('showing');
				$toShow = self.$syncedswitch.find('.rsw-synced-switch-item[data-item="0"]');
				$toShow.addClass('showing');
				if (self.$buttons.length) {
					self.$buttons.find('.button-selected').removeClass('button-selected');
					self.$buttons.find('.default-button').addClass('button-selected');
				}
			} else {
				self.$syncedswitch.find('.rsw-synced-switch-item').removeClass('showing');
				$toShow.addClass('showing');
				if (self.$buttons.length) {
					self.$buttons.find('.button-selected').removeClass('button-selected');
					$thisButton.addClass('button-selected');
				}
			}
			// show/hide categories in TOC
			this.$syncedswitch.find('.rsw-synced-switch-item .mw-header > a:first-child').each(function(j,k){
				$('#toc ul a[href="#'+$(k).attr('id')+'"]').parent().addClass('sync-toc-hidden');
			});
			$toShow.find('.mw-header > a:first-child').each(function(i,v){
				$('#toc ul a[href="#'+$(v).attr('id')+'"]').parent().removeClass('sync-toc-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;
		};
		
		/* init */
		mw.log('setting up synced switch, id '+self.index);
		// attempt to apply some button text from a SwitchInfobox
		if ($('.rsw-infobox.infobox-switch').length) {
			self.attachedLabels = true;
			var $linkedButtonTextInfobox = $('.rsw-infobox.infobox-switch').first();
			var allVers = '', defVer = false;
			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'));
				}
				allVers = allVers + $e.attr('data-item-text') + '#' + $e.attr('data-item-vers') + '#';
			});
			self.$buttons.find('[data-item]').each(function(i,e){
				var $e = $(e);
				if ( $e.attr('data-item-text') === undefined) {
					var it = $linkedButtonTextInfobox.find('[data-switch-index="'+$e.attr('data-item')+'"]').attr('data-switch-anchor');
					if (it) {
						$e.attr('data-item-text', it);
						if ( $e.text().length == 0 ) {
							$e.text( it.replace(/#/g, '').replace(/_/g, ' ') );
						}
					}
				}
				allVers = allVers + $e.attr('data-item-text') + '#';
			});
			// infobox value for default button
			$linkedButtonTextInfobox.find('.infobox-buttons [data-switch-index]').each(function(i,e){
				if (defVer) {
					return false;
				} else {
					var term = $(e).attr('data-switch-anchor').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+'#';
					if (!allVers.match(term)) {
						self.$buttons.find('[data-item="0"]').attr({
							'data-item':$(e).attr('data-switch-index'),
							'data-item-text':$(e).attr('data-switch-anchor')
						});
						defVer = true;
					}
				}
			});
			// remove default button if none apply
			self.$buttons.find('[data-item="0"]').remove();
			if (self.$buttons.length) {
				self.$buttons
					.attr({ id: 'sync-buttons-'+self.index})
					.find('span').each(function(i,e) {
						$(e).click(self.beginSwitchEvent);
					});
			}
		}
		// add events to buttons
		
		window.switchEventManager.addSwitchInfobox(this);
	}

	/**
	 * 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 = [], preSwitchEvents = [], postSwitchEvents = [], index = 0;
		
		// actual switch infoboxes to change
		this.addSwitchInfobox = function(l) {
			switchInfoboxes.push(l);
		};
		
		// 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 (!e.isParentOf($triggerer)) {
					e.switchInfobox(index, anchor);
				}
			});

			// update hash
			if (typeof anchor === 'string') {
				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);
			});
		};
		
		/* 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++);
			}
			if ($e.hasClass('switch-infobox')) {
				return new LegacySwitchInfobox($e, index++);
			}
			if ($e.hasClass('rsw-synced-switch')) {
				return new SyncedSwitch($e, index++);
			}
		};
		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);
				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);
						return;
					}
				}
			}
			self.trigger(1, '');
		};
	}

	function init() {
		var index = 0;
		window.switchEventManager = new SwitchEventManager();
		$('.infobox-switch').each(function(i,e){
			return new SwitchInfobox($(e), index++);
		});
		$('.switch-infobox').each(function(i,e){
			return new LegacySwitchInfobox($(e), index++);
		});
		$('.rsw-synced-switch').each(function(i,e){
			return new SyncedSwitch($(e), index++);
		});
		window.switchEventManager.addIndex(index);
		
		// reinitialize any kartographer map frames added due to a switch
		if ($('.infobox-switch .mw-kartographer-map').length
		 || $('.switch-infobox .mw-kartographer-map').length
		 || $('.rsw-synced-switch .mw-karographer-map').length) {
			window.switchEventManager.addPostSwitchEvent(function() {
				mw.hook('wikipage.content').fire($('a.mw-kartographer-map').parent());
			});
		}
		window.switchEventManager.applyDefaultVersion();
	}

	$(init);
})(jQuery, mediaWiki);
// </nowiki>