MediaWiki:Gadget-switch-infobox.js

// /* switch infobox code for infoboxes * contains switching code for both: * * originalInfoboxes: *		older infobox switching *		which works by 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) */	if (!($('.switch-infobox').length || $('.infobox-buttons').length)) { return; }
 * (function ($, mw) {

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 * 	 * 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.rtswiki && typeof(rtswiki.initQtyBox) == 'function') {				rtswiki.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 = $(' ') .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( $(' ').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 = $(' ') .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( $(' ').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 rts-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.rts-infobox').length > 0) {				if ($item.find('table.rts-infobox caption').length < 1) {					$item.find('table.rts-infobox').prepend(' ');				}				$item.find('table.rts-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 (.rts-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('.rts-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('.rts-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('.rts-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('.rts-synced-switch-item').removeClass('showing'); $toShow = self.$syncedswitch.find('.rts-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('.rts-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('.rts-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 ($('.rts-infobox.infobox-switch').length) { self.attachedLabels = true; var $linkedButtonTextInfobox = $('.rts-infobox.infobox-switch').first; var allVers = '', defVer = false; self.$syncedswitch.find('.rts-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('rts-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++);		}); $('.rts-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        || $('.infobox-switch-resources .mw-kartographer-map').length         || $('.switch-infobox .mw-kartographer-map').length         || $('.rts-synced-switch .mw-kartographer-map').length) { window.switchEventManager.addPostSwitchEvent(function {				mw.hook('wikipage.content').fire($('a.mw-kartographer-map').parent);			}); }		window.setTimeout(window.switchEventManager.applyDefaultVersion); }

$(init); })(jQuery, mediaWiki); //