MediaWiki:Gadget-switch-infobox.js
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>