MediaWiki:Gadget-switch-infobox.js
Revision as of 07:59, 16 February 2022 by Soulgazer (talk | contribs) (Created page with "// <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 *...")
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 * * 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+)/, 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) { var self = this; this.index = index; this.$infobox = $box; this.$resources = self.$infobox.next(); 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')); } 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) { 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; }; /* 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'); 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) { var self = this; this.$box = $box; this.index = index; this.$buttons = $('<div>').addClass('infobox-buttons').css('display', 'block'); 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); }; // 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) { var self = this; this.$parent = $box; this.index = index; this.$originalButtons = self.$parent.find('.switch-infobox-triggers'); 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); 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){ if (text === '@init@') { text = self.$buttons.find('[data-switch-index="1"]').attr('data-switch-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; }; /* 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(' ', '_')); }); // 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) { var self = this; this.index = index; this.$syncedswitch = $box; this.attachedLabels = false; /* 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); 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; }; /* init */ mw.log('setting up synced switch, id '+self.index); // attempt to apply some button text from a SwitchInfobox if ($('.infobox.infobox-switch').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()); } } /** * 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 = [], genderRenderSwitchers = [], preSwitchEvents = [], postSwitchEvents = [], index = 0; // actual switch infoboxes to change this.addSwitchInfobox = function(l) { switchInfoboxes.push(l); }; this.addGenderRenderSwitch = function(gs) { genderRenderSwitchers.push(gs); }; // 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') { 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); } }; /* 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, '@init@'); }; } function init() { // mirror rsw-util try { localStorage.setItem('test', 'test'); localStorage.removeItem('test'); CAN_LOCAL_STORAGE = true; } catch (e) { CAN_LOCAL_STORAGE = false; } 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 || $('.infobox-switch-resources .mw-kartographer-map').length || $('.switch-infobox .mw-kartographer-map').length || $('.rsw-synced-switch .mw-kartographer-map').length) { window.switchEventManager.addPostSwitchEvent(function() { mw.hook('wikipage.content').fire($('a.mw-kartographer-map').parent()); }); } window.switchEventManager.applyDefaultVersion(); } $(init); })(jQuery, mediaWiki); // </nowiki>