MediaWiki:Gadget-switch-infobox.js

After saving, you may need to bypass your browser's cache to see the changes. For further information, see Wikipedia:Bypass your cache.

  • In most Windows and Linux browsers: Hold down Ctrl and press F5.
  • In Safari: Hold down ⇧ Shift and click the Reload button.
  • In Chrome and Firefox for Mac: Hold down both ⌘ Cmd+⇧ Shift and press R.
"use strict";

function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
/* 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());
      });
    }
  });
});