This is a wiki for a reason. Anyone can contribute. If you see something that is inaccurate or can be improved, don't ask that it be fixed--just improve it.
[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]

Difference between revisions of "MediaWiki:Common.js"

From Project 1999 Wiki
Jump to: navigation, search
 
(38 intermediate revisions by one user not shown)
Line 1: Line 1:
 
/* Any JavaScript here will be loaded for all users on every page load. */
 
/* Any JavaScript here will be loaded for all users on every page load. */
  
importScript('MediaWiki:Simple-lightbox.min.js');
 
 
importScript('MediaWiki:Polyfills.js');
 
importScript('MediaWiki:Polyfills.js');
 +
 +
// HTTP prevents people from logging in now in (some?) Chrome browsers; redirect to HTTPS
 +
try {
 +
  var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
 +
  var onHttp = location.protocol === 'http:';
 +
  var haveNotTriedAlready = !location.search.includes('redirected_from_http');
 +
 +
  if (isChrome && onHttp && haveNotTriedAlready) {
 +
    location =
 +
      location.origin.replace('http:', 'https:') +
 +
      location.pathname +
 +
      location.search +
 +
//      (location.search[0] === '?' ? '&' : '?') +
 +
//      'redirected_from_http=1' +
 +
      location.hash;
 +
  }
 +
} catch (err) {
 +
  // Give up on trying to redirect
 +
  // maybe alert user?
 +
}
 +
 +
importScript('MediaWiki:Simple-lightbox.min.js');
 
importScript('MediaWiki:Zones.js');
 
importScript('MediaWiki:Zones.js');
 +
 +
// Get all magelos of current user
 +
// (currently un-used, but intended for future achievement code, so that
 +
//  we can have an "add this achievement to my magelo" drop-down with all
 +
//  their magelos listed)
 +
var getMagelosForCurrentUser = function () {
 +
  var userName = $('#pt-userpage a').text();
 +
  var url =
 +
    'https://wiki.project1999.com/index.php?limit=50&tagfilter=&' +
 +
    'title=Special%3AContributions&contribs=user&target=' +
 +
    userName +
 +
    '&namespace=500&topOnly=1&year=&month=-1';
 +
  fetch(url)
 +
    .then(function (response) {
 +
      return response.text();
 +
    })
 +
    .then(function (html) {
 +
      var mageloLinks = Array.from($(html).find('li a:contains("Magelo")'));
 +
      var names = mageloLinks
 +
        .filter(function (i, a) {
 +
          return !$(a).text().includes('(');
 +
        })
 +
        .map(function (i, el) {
 +
          return $(el).text();
 +
        });
 +
      console.log(names);
 +
    });
 +
};
  
 
// **********************************
 
// **********************************
Line 10: Line 59:
  
 
/**
 
/**
  * Basic "state getter": checks localStorage for *stateKey*, then return the  
+
  * Basic "state getter": checks localStorage for *stateKey*, then return the
 
  * value for *pageName* (if any) found on that object.
 
  * value for *pageName* (if any) found on that object.
 
  */
 
  */
var getPageState = function(pageName, stateKey) {
+
var getPageState = function (pageName, stateKey) {
 
   // Get the state for all pages for this key
 
   // Get the state for all pages for this key
 
   var allPageStates = JSON.parse(localStorage.getItem(stateKey) || '{}');
 
   var allPageStates = JSON.parse(localStorage.getItem(stateKey) || '{}');
Line 26: Line 75:
 
  * status state for the current page.
 
  * status state for the current page.
 
  */
 
  */
window.getStateByPage = function(stateKey) {
+
window.getStateByPage = function (stateKey) {
 
   var pageName = location.pathname.substr(1); // ditch the leading "/"
 
   var pageName = location.pathname.substr(1); // ditch the leading "/"
 
   return getPageState(pageName, stateKey);
 
   return getPageState(pageName, stateKey);
Line 36: Line 85:
 
  * about which page you are on)
 
  * about which page you are on)
 
  */
 
  */
window.getGlobalState = function(stateKey) {
+
window.getGlobalState = function (stateKey) {
 
   return getPageState('__global', stateKey);
 
   return getPageState('__global', stateKey);
 
};
 
};
Line 43: Line 92:
 
  * Basic "state setter" function
 
  * Basic "state setter" function
 
  */
 
  */
var setPageState = function(pageName, stateKey, state) {
+
var setPageState = function (pageName, stateKey, state) {
 
   // Get the state for this key (for all pages) out of local storage
 
   // Get the state for this key (for all pages) out of local storage
 
   var allPageStates = JSON.parse(localStorage.getItem(stateKey) || '{}');
 
   var allPageStates = JSON.parse(localStorage.getItem(stateKey) || '{}');
Line 60: Line 109:
 
  * value of "b".
 
  * value of "b".
 
  */
 
  */
window.setStateByPage = function(stateKey, state) {
+
window.setStateByPage = function (stateKey, state) {
 
   var pageName = location.pathname.substr(1); // ditch the leading "/"
 
   var pageName = location.pathname.substr(1); // ditch the leading "/"
 
   setPageState(pageName, stateKey, state);
 
   setPageState(pageName, stateKey, state);
 
};
 
};
  
window.setGlobalState = function(stateKey, state) {
+
window.setGlobalState = function (stateKey, state) {
 
   return setPageState('__global', stateKey, state);
 
   return setPageState('__global', stateKey, state);
 
};
 
};
 
  
 
// *******************************
 
// *******************************
Line 76: Line 124:
 
  * last update: 7 January, 2018 by Loramin
 
  * last update: 7 January, 2018 by Loramin
 
  */
 
  */
$(function() {
+
$(function () {
 +
  // Start onReady code
 +
 
 
   var hideDelay = 0;
 
   var hideDelay = 0;
 
   var trigDelay = 250;
 
   var trigDelay = 250;
Line 85: Line 135:
  
 
   // One instance that's reused to show info for the current person
 
   // One instance that's reused to show info for the current person
   var container = $('<div id="itemHoverContainer">'
+
   var container = $(
    + '<div id="HoverContent"></div>'
+
    '<div id="itemHoverContainer">' + '<div id="HoverContent"></div>' + '</div>'
    + '</div>');
+
  );
  
 
   $('body').append(container);
 
   $('body').append(container);
  
/* --- hoverbox for item/mob, currently only used in magelo --- */
+
  /* --- hoverbox for item/mob, currently only used in magelo --- */
  
 
   // Determine which "a" elements should trigger the item stats mouseover
 
   // Determine which "a" elements should trigger the item stats mouseover
 
   var $mouseoverTargets = $('span.ih a');
 
   var $mouseoverTargets = $('span.ih a');
   var isItemCategory = document.title.startsWith('Category:') && document.title.includes('Equipment - Project 1999 Wiki') &&
+
   var isItemCategory =
                      !document.title.includes('Worshiper Equipment');
+
    document.title.startsWith('Category:') &&
 +
    document.title.includes('Equipment - Project 1999 Wiki') &&
 +
    !document.title.includes('Worshiper Equipment');
 
   if (isItemCategory) {
 
   if (isItemCategory) {
 
     // Include the category's item list along with ".ih" links
 
     // Include the category's item list along with ".ih" links
Line 102: Line 154:
 
   }
 
   }
  
   $mouseoverTargets = $mouseoverTargets.filter(function(i, a) {
+
   $mouseoverTargets = $mouseoverTargets.filter(function (i, a) {
 
     // Don't add hover to links like "next 200"
 
     // Don't add hover to links like "next 200"
     return !$(a).attr('href').startsWith('/Special:') &&
+
     return (
          !$(a).attr('href').includes('title=Category:')
+
      !$(a).attr('href').startsWith('/Special:') &&
 +
      !$(a).attr('href').includes('title=Category:')
 +
    );
 
   });
 
   });
  
   $mouseoverTargets.on('mouseover', function()
+
   $mouseoverTargets.on('mouseover', function () {
  {
+
 
     var $this = $(this);
 
     var $this = $(this);
 
     var itemname = $this.attr('title');
 
     var itemname = $this.attr('title');
  
     if (itemname == '' || itemname == 'undefined')
+
     if (itemname == '' || itemname == 'undefined') return;
      return;
+
  
     if (hideTimer)
+
     if (hideTimer) clearTimeout(hideTimer);
      clearTimeout(hideTimer);
+
  
 
     if ($this.parents('div.mw-content-ltr').length) {
 
     if ($this.parents('div.mw-content-ltr').length) {
Line 124: Line 175:
  
 
       container.css({
 
       container.css({
          left: pos.left + 5 + 'px',
+
        left: pos.left + 5 + 'px',
          top: pos.top + 5 + 'px'
+
        top: pos.top + 5 + 'px',
 
       });
 
       });
 
     }
 
     }
 
 
  
 
     $(this).trigger('mousemove');
 
     $(this).trigger('mousemove');
Line 140: Line 189:
 
     //                          + '<p></p></div></div><div class="itembotbg"></div>');
 
     //                          + '<p></p></div></div><div class="itembotbg"></div>');
  
     if (ajax)
+
     if (ajax) {
    {
+
 
       ajax.abort();
 
       ajax.abort();
 
       ajax = null;
 
       ajax = null;
 
     }
 
     }
 
     ajax = $.ajax({
 
     ajax = $.ajax({
       url: window.location.protocol +
+
       url:
          '//wiki.project1999.com/index.php/Special:AjaxHoverHelper/' + itemname,
+
        window.location.protocol +
 +
        '//wiki.project1999.com/index.php/Special:AjaxHoverHelper/' +
 +
        itemname,
 
       cacheResponse: true,
 
       cacheResponse: true,
       success: function(html)
+
       success: function (html) {
      {
+
 
         var $html = $(html);
 
         var $html = $(html);
         $('#itemHoverContent').html($html[2]).prepend($html[0]).prepend($html[1]);
+
         $('#itemHoverContent')
       }
+
          .html($html[2])
 +
          .prepend($html[0])
 +
          .prepend($html[1]);
 +
       },
 
     });
 
     });
  
 
     container.css('display', 'block');
 
     container.css('display', 'block');
 
     //container.fadeIn('fast');
 
     //container.fadeIn('fast');
 
 
   }); //on mouseover
 
   }); //on mouseover
  
   $('span.ih a').on('mouseout', function()
+
   $('span.ih a').on('mouseout', function () {
  {
+
     if (hideTimer) clearTimeout(hideTimer);
     if (hideTimer)
+
     hideTimer = setTimeout(function () {
      clearTimeout(hideTimer);
+
     hideTimer = setTimeout(function()
+
    {
+
 
       container.css('display', 'none');
 
       container.css('display', 'none');
 
       //container.fadeOut('fast');
 
       //container.fadeOut('fast');
Line 172: Line 220:
 
   });
 
   });
  
   $('span.ih a').mousemove(function(e){
+
   $('span.ih a').mousemove(function (e) {
 
     var mousex = e.pageX + 20; //Get X coodrinates
 
     var mousex = e.pageX + 20; //Get X coodrinates
 
     var mousey = e.pageY + 20; //Get Y coordinates
 
     var mousey = e.pageY + 20; //Get Y coordinates
Line 183: Line 231:
 
     var tipVisY = $(window).height() - (mousey + tipHeight);
 
     var tipVisY = $(window).height() - (mousey + tipHeight);
  
     if ( tipVisX < 20 ) { //If tooltip exceeds the X coordinate of viewport
+
     if (tipVisX < 20) {
 +
      //If tooltip exceeds the X coordinate of viewport
  
       if( tipWidth > e.pageX - 20 ){
+
       if (tipWidth > e.pageX - 20) {
 
         mousex = 0;
 
         mousex = 0;
 
       } else {
 
       } else {
 
         mousex = e.pageX - tipWidth - 20;
 
         mousex = e.pageX - tipWidth - 20;
 
       }
 
       }
 
+
     }
     } if ( tipVisY < 20 ) { //If tooltip exceeds the Y coordinate of viewport
+
    if (tipVisY < 20) {
 +
      //If tooltip exceeds the Y coordinate of viewport
 
       mousey = e.pageY - tipHeight - 20;
 
       mousey = e.pageY - tipHeight - 20;
 
     }
 
     }
  
     container.css({ top: mousey, left: mousex });
+
     container.css({ top: mousey, left: mousex });
 
   });
 
   });
  
 
   // Allow mouse over of details without hiding details
 
   // Allow mouse over of details without hiding details
   $('#itemHoverContainer').mouseover(function()
+
   $('#itemHoverContainer').mouseover(function () {
  {
+
     if (hideTimer) clearTimeout(hideTimer);
     if (hideTimer)
+
      clearTimeout(hideTimer);
+
 
   });
 
   });
  
 
   // Hide after mouseout
 
   // Hide after mouseout
   $('#itemHoverContainer').mouseout(function()
+
   $('#itemHoverContainer').mouseout(function () {
  {
+
     if (hideTimer) clearTimeout(hideTimer);
     if (hideTimer)
+
     hideTimer = setTimeout(function () {
      clearTimeout(hideTimer);
+
     hideTimer = setTimeout(function()
+
    {
+
 
       container.css('display', 'none');
 
       container.css('display', 'none');
 
       //container.fadeOut('fast');
 
       //container.fadeOut('fast');
Line 218: Line 263:
  
 
   // magelo non-ajax item hover, but move box with mouse
 
   // magelo non-ajax item hover, but move box with mouse
   $('.magelohb').mousemove(function(e){
+
   $('.magelohb').mousemove(function (e) {
 
     var childContainer = $(this).children('span.hb');
 
     var childContainer = $(this).children('span.hb');
  
Line 232: Line 277:
 
     var tipVisY = $(window).height() - (mousey + tipHeight - 20);
 
     var tipVisY = $(window).height() - (mousey + tipHeight - 20);
  
     if ( tipVisX < 20 ) { //If tooltip exceeds the X coordinate of viewport
+
     if (tipVisX < 20) {
 +
      //If tooltip exceeds the X coordinate of viewport
  
       if( tipWidth > e.pageX - 20){
+
       if (tipWidth > e.pageX - 20) {
 
         mousex = 0;
 
         mousex = 0;
 
       } else {
 
       } else {
 
         mousex = e.pageX - tipWidth - 20;
 
         mousex = e.pageX - tipWidth - 20;
 
       }
 
       }
 
+
     }
     } if ( tipVisY < 20 ) { //If tooltip exceeds the Y coordinate of viewport
+
    if (tipVisY < 20) {
 +
      //If tooltip exceeds the Y coordinate of viewport
 
       mousey = e.pageY - tipHeight - 20;
 
       mousey = e.pageY - tipHeight - 20;
 
     }
 
     }
  
     childContainer.css({ top: mousey, left: mousex, 'z-index':'999' });
+
     childContainer.css({ top: mousey, left: mousex, 'z-index': '999' });
 
   });
 
   });
  
 
   // change to position:fixed on all hover divs if we have JS active
 
   // change to position:fixed on all hover divs if we have JS active
 
   // otherwise leave as position:absolute so the stationary hovers are near their items
 
   // otherwise leave as position:absolute so the stationary hovers are near their items
   $('.magelohb span.hb').each(function(i) {
+
   $('.magelohb span.hb').each(function (i) {
     $(this).css({'position':'fixed'});
+
     $(this).css({ position: 'fixed' });
 
   });
 
   });
  
Line 260: Line 307:
  
 
   // Fashion for item pages
 
   // Fashion for item pages
   var extractFashionHtml = function(html) {
+
   var extractFashionHtml = function (html) {
     return $(html).find('.fashion_show, .primary_secondary_show').map(function(i, el) {
+
     return $(html)
      var $el = $(el);
+
      .find('.fashion_show, .primary_secondary_show')
      var data = $el.data();
+
      .map(function (i, el) {
      data.race = $el.find('.fashion_race').html();
+
        var $el = $(el);
      if ($el.is('.fashion_show')) {
+
        var data = $el.data();
        data.armor = $el.find('.fashion_armor').html();
+
        data.race = $el.find('.fashion_race').html();
      }
+
        if ($el.is('.fashion_show')) {
      return data;
+
          data.armor = $el.find('.fashion_armor').html();
    }).toArray();
+
        }
  };
+
        return data;
 
+
      })
   var getFashionShows = function(fashionCategory) {
+
      .toArray();
 +
  };
 +
 
 +
   var getFashionShows = function (fashionCategory) {
 
     var url = '/Category:Fashion: ' + fashionCategory;
 
     var url = '/Category:Fashion: ' + fashionCategory;
 
     return $.get(url).then(extractFashionHtml);
 
     return $.get(url).then(extractFashionHtml);
 
   };
 
   };
  
   var getItemPageShows = function() {
+
   var getItemPageShows = function () {
     var fashionCategories =
+
     var fashionCategories = $('#catlinks li a')
      $('#catlinks li a')
+
      .map(function (i, a) {
        .map(function(i, a) { return $(a).text(); })
+
        return $(a).text();
        .filter(function(i, text) { return text.startsWith('Fashion:'); })
+
      })
        .map(function(i, text) { return text.substr('Fashion: '.length); });
+
      .filter(function (i, text) {
 +
        return text.startsWith('Fashion:');
 +
      })
 +
      .map(function (i, text) {
 +
        return text.substr('Fashion: '.length);
 +
      });
 
     var fashion = fashionCategories[0];
 
     var fashion = fashionCategories[0];
 
     if (!fashion) return $.when();
 
     if (!fashion) return $.when();
  
 
     var tint = fashionCategories[1];
 
     var tint = fashionCategories[1];
     return getFashionShows(fashion).then(function(shows) {
+
     return getFashionShows(fashion).then(function (shows) {
 
       var sameTint = [];
 
       var sameTint = [];
 
       var otherTint = [];
 
       var otherTint = [];
       shows.forEach(function(show) {
+
       shows.forEach(function (show) {
 
         var isHeld = show.primaryFashion || show.secondaryFashion;
 
         var isHeld = show.primaryFashion || show.secondaryFashion;
 
         var bothHaveSameTint = show.tint === tint;
 
         var bothHaveSameTint = show.tint === tint;
Line 297: Line 352:
 
         else otherTint.push(show);
 
         else otherTint.push(show);
 
       });
 
       });
       return { matches: sameTint, partialMatches: otherTint, fashion: fashion, tint: tint };
+
       return {
 +
        matches: sameTint,
 +
        partialMatches: otherTint,
 +
        fashion: fashion,
 +
        tint: tint,
 +
      };
 
     });
 
     });
   }
+
   };
   var addFashionSection = function(shows, isFullMatch) {
+
   var addFashionSection = function (shows, isFullMatch) {
 
     var preface = isFullMatch
 
     var preface = isFullMatch
              ? '<h2>Fashion/Appearance</h2>'  
+
      ? '<h2>Fashion/Appearance</h2>'
              : '<div>Fashion images with the wrong "tint" (i.e. coloring) which ' +
+
      : '<div>Fashion images with the wrong "tint" (i.e. coloring) which ' +
                'can only be used to get a general sense of the item\'s ' +
+
        "can only be used to get a general sense of the item's " +
                'appearance:</div>';
+
        'appearance:</div>';
  
 
     // Don't show the explanatory text if there are no shows to explain
 
     // Don't show the explanatory text if there are no shows to explain
 
     if (!shows.length && !isFullMatch) preface = '';
 
     if (!shows.length && !isFullMatch) preface = '';
  
     var $ul = $('<ul>').append(shows.map(function(show) {
+
     var $ul = $('<ul>').append(
      var img = isFullMatch
+
      shows.map(function (show) {
        ? '<br/><img style="max-height: 100px; max-width: 100px;" src="/images/' +
+
        var img = isFullMatch
          show.file.replace(/ /g, '_') + '"/>'
+
          ? '<br/><img style="max-height: 100px; max-width: 100px;" src="/images/' +
        : '';
+
            show.file.replace(/ /g, '_') +
      return '<li class="fashion-link" data-file="' + show.file +'">' +
+
            '"/>'
            '<a href="#">' + show.gender + ' ' + show.race + img + '</a></li>';
+
          : '';
    }));
+
        return (
 +
          '<li class="fashion-link" data-file="' +
 +
          show.file +
 +
          '">' +
 +
          '<a href="#">' +
 +
          show.gender +
 +
          ' ' +
 +
          show.race +
 +
          img +
 +
          '</a></li>'
 +
        );
 +
      })
 +
    );
  
 
     $('#itemfashion').before($('<div>' + preface + '</div>').append($ul));
 
     $('#itemfashion').before($('<div>' + preface + '</div>').append($ul));
 
   };
 
   };
  
   var addFashionSections = function() {
+
   var addFashionSections = function () {
     getItemPageShows().then(function(pageShows) {
+
     getItemPageShows().then(function (pageShows) {
       if(!pageShows) return;
+
       if (!pageShows) return;
 
       var matches = pageShows.matches || [];
 
       var matches = pageShows.matches || [];
 
       var partialMatches = pageShows.partialMatches || [];
 
       var partialMatches = pageShows.partialMatches || [];
 
       if (pageShows.matches.length || pageShows.partialMatches.length)
 
       if (pageShows.matches.length || pageShows.partialMatches.length)
 
         addFashionSection(pageShows.matches, true);
 
         addFashionSection(pageShows.matches, true);
       if (pageShows.partialMatches.length)  
+
       if (pageShows.partialMatches.length)
 
         addFashionSection(pageShows.partialMatches, false);
 
         addFashionSection(pageShows.partialMatches, false);
 
     });
 
     });
 
   };
 
   };
 
   addFashionSections();
 
   addFashionSections();
 
+
 
 
+
 
 
   //*****************************************
 
   //*****************************************
 
   // Add class-based filtering to item tables
 
   // Add class-based filtering to item tables
 
   //*****************************************w
 
   //*****************************************w
 
+
 
   // Determine whether or not a provided row should be shown for the provided  
+
   // Determine whether or not a provided row should be shown for the provided
 
   // class abbreviation (eg. "BRD")
 
   // class abbreviation (eg. "BRD")
   var isRowShown = function($tr, classAbbrev) {
+
   var isRowShown = function ($tr, classAbbrev) {
 
     // Extract classes (and races)
 
     // Extract classes (and races)
     var classText = $tr.find('td').eq(3).text().split('Class:')[1];
+
     var classText = $tr.find('td').eq(3).text().split('Class:')[1];
     if (!classText) return true; // Ignore (don't filter) rows without class text
+
     if (!classText) return true; // Ignore (don't filter) rows without class text
 
+
 
 
     // remove "Race: " part (if any) and upper-case text
 
     // remove "Race: " part (if any) and upper-case text
 
     classText = classText.split('Race:')[0] || classText;
 
     classText = classText.split('Race:')[0] || classText;
 
     classText = classText.toUpperCase();
 
     classText = classText.toUpperCase();
 
+
 
 
     // determine matching text
 
     // determine matching text
 
     var isMatch = classText.includes(classAbbrev);
 
     var isMatch = classText.includes(classAbbrev);
 
     var isAllMatch = classText.includes('ALL');
 
     var isAllMatch = classText.includes('ALL');
 
     var isExcept = classText.includes('ALL EXCEPT');
 
     var isExcept = classText.includes('ALL EXCEPT');
 
+
 
 
     // determine matches
 
     // determine matches
 
     if (isMatch && !isExcept) return true; // (eg. BRD in "BRD")
 
     if (isMatch && !isExcept) return true; // (eg. BRD in "BRD")
     if (!isMatch && isExcept) return true; // (eg. BRD in "ALL EXCEPT WIZ")
+
     if (!isMatch && isExcept) return true; // (eg. BRD in "ALL EXCEPT WIZ")
 
     if (!isExcept && isAllMatch) return true; // (eg. BRD in "ALL")
 
     if (!isExcept && isAllMatch) return true; // (eg. BRD in "ALL")
 
     return false;
 
     return false;
 
   };
 
   };
 
+
 
   var scrollToTable = function($table) {
+
   var scrollToTable = function ($table) {
     window.setTimeout(function() {
+
     window.setTimeout(function () {
       $('html, body').animate({ scrollTop: ($table.offset().top) }, 200);
+
       $('html, body').animate({ scrollTop: $table.offset().top }, 200);
 
     }, 100);
 
     }, 100);
 
   };
 
   };
 
+
 
   // Filter the provided table to only display rows for the provided class  
+
   // Filter the provided table to only display rows for the provided class
 
   // abbreviation
 
   // abbreviation
   var filterTable = function($table, classAbbrev) {
+
   var filterTable = function ($table, classAbbrev) {
 
     $table.find('tr').each(function (i, tr) {
 
     $table.find('tr').each(function (i, tr) {
 
       var $tr = $(tr);
 
       var $tr = $(tr);
Line 378: Line 450:
 
     });
 
     });
 
     scrollToTable($table);
 
     scrollToTable($table);
 
+
 
 
     addFilterLink($table, false);
 
     addFilterLink($table, false);
     $('.itemsUnfilterLink').click(function() {
+
     $('.itemsUnfilterLink').click(function () {
 
       unfilterTable($table);
 
       unfilterTable($table);
 
     });
 
     });
 
+
 
 
     alert('Filtered to only show rows containing ' + classAbbrev + ' gear.');
 
     alert('Filtered to only show rows containing ' + classAbbrev + ' gear.');
 
   };
 
   };
 
+
 
 
   // Restore a table to its previous, un-filtered state
 
   // Restore a table to its previous, un-filtered state
   var unfilterTable = function($table) {
+
   var unfilterTable = function ($table) {
 
     $table.find('tr').show();
 
     $table.find('tr').show();
 
     addFilterLink($table, true);
 
     addFilterLink($table, true);
 
     scrollToTable($table);
 
     scrollToTable($table);
 
   };
 
   };
 
+
 
   // When a user clicks on a table class filter link, handle it by asking them  
+
   // When a user clicks on a table class filter link, handle it by asking them
 
   // which class (and then filtering the table for that class)
 
   // which class (and then filtering the table for that class)
   var handleItemFilterLinkClick = function(e) {
+
   var handleItemFilterLinkClick = function (e) {
     var promptMessage = 'Please enter the three-letter abbreviation for the ' +
+
     var promptMessage =
        'class you want to filter by (eg. "brd" for "Bard").';
+
      'Please enter the three-letter abbreviation for the ' +
 
+
      'class you want to filter by (eg. "brd" for "Bard").';
 +
 
 
     var $table = $(e.target).closest('table');
 
     var $table = $(e.target).closest('table');
 
     var classAbbrev = prompt(promptMessage).toUpperCase();
 
     var classAbbrev = prompt(promptMessage).toUpperCase();
 
     if (!classAbbrev || classAbbrev.length !== 3 || classAbbrev === 'ALL') {
 
     if (!classAbbrev || classAbbrev.length !== 3 || classAbbrev === 'ALL') {
       alert('A 3-digit abbreviation wasn\'t entered (or "All" was entered); ' +
+
       alert(
           'filtering disabled');
+
        'A 3-digit abbreviation wasn\'t entered (or "All" was entered); ' +
 +
           'filtering disabled'
 +
      );
 
       unfilterTable($table);
 
       unfilterTable($table);
 
       return false;
 
       return false;
Line 410: Line 485:
 
     filterTable($table, classAbbrev);
 
     filterTable($table, classAbbrev);
 
   };
 
   };
 
+
 
 
   // Adds either a filter or unfilter link (as determined by showFilter) to the
 
   // Adds either a filter or unfilter link (as determined by showFilter) to the
 
   // provided table
 
   // provided table
   var addFilterLink = function($table, showFilter) {
+
   var addFilterLink = function ($table, showFilter) {
 
     var $statsHeaderCells = $table.find('th:contains("Stats")');
 
     var $statsHeaderCells = $table.find('th:contains("Stats")');
 
     var filterType = showFilter ? 'Filter' : 'Unfilter';
 
     var filterType = showFilter ? 'Filter' : 'Unfilter';
     $statsHeaderCells.html('Stats <span style="float:right">' +
+
     $statsHeaderCells.html(
         '<a style="text-decoration: underline" class="items' + filterType + 'Link" href="#">' +
+
      'Stats <span style="float:right">' +
         filterType + (showFilter ? ' by Class' : '') + '</a></span>');
+
         '<a style="text-decoration: underline" class="items' +
 
+
        filterType +
 +
        'Link" href="#">' +
 +
         filterType +
 +
        (showFilter ? ' by Class' : '') +
 +
        '</a></span>'
 +
    );
 +
 
 
     var $link = $statsHeaderCells.find('.items' + filterType + 'Link');
 
     var $link = $statsHeaderCells.find('.items' + filterType + 'Link');
 
     if (showFilter) $link.click(handleItemFilterLinkClick);
 
     if (showFilter) $link.click(handleItemFilterLinkClick);
     else $link.click(function() { unfilterTable($table); })
+
     else
 +
      $link.click(function () {
 +
        unfilterTable($table);
 +
      });
 
   };
 
   };
 
+
 
 
   // Add class filtering links to all item tables
 
   // Add class filtering links to all item tables
   var addItemFilteringLinks = function() {
+
   var addItemFilteringLinks = function () {
 
     var $tables = $('table th:contains("Item Name")').closest('table');
 
     var $tables = $('table th:contains("Item Name")').closest('table');
     $tables.each(function(i, table) {
+
     $tables.each(function (i, table) {
 
       var $table = $(table);
 
       var $table = $(table);
 
       // Make sure it has both "Item Name" and "Stats" columns
 
       // Make sure it has both "Item Name" and "Stats" columns
 
       if (!$table.has('th:contains("Stats")')) return;
 
       if (!$table.has('th:contains("Stats")')) return;
 
+
 
 
       addFilterLink($table, true);
 
       addFilterLink($table, true);
 
     });
 
     });
 
   };
 
   };
 
   addItemFilteringLinks();
 
   addItemFilteringLinks();
 
+
 
 
   //*****************************************
 
   //*****************************************
 
   // End class-based filtering to item tables
 
   // End class-based filtering to item tables
Line 445: Line 529:
 
   if (window.location.pathname.includes('Special:ClassSlotEquip')) {
 
   if (window.location.pathname.includes('Special:ClassSlotEquip')) {
 
     // Add checkboxes UI to the page
 
     // Add checkboxes UI to the page
     $('table')
+
     $('table').before(
      .before('<label><input id="showNoDrop" type="checkbox" checked/> Show No Drop</label> ' +
+
      '<label><input id="showNoDrop" type="checkbox" checked/> Show No Drop</label> ' +
              '<label><input id="showDroppable" type="checkbox" checked/> Show Droppable</label>' /*+
+
        '<label><input id="showDroppable" type="checkbox" checked/> Show Droppable</label>' /*+
 
               '| <label><input id="showEffect" type="checkbox" checked/> Show With Effects</label>' +
 
               '| <label><input id="showEffect" type="checkbox" checked/> Show With Effects</label>' +
               '<label><input id="showNoEffect" type="checkbox" checked/> Show Without Effects</label>'*/);
+
               '<label><input id="showNoEffect" type="checkbox" checked/> Show Without Effects</label>'*/
 +
    );
  
 
     // Handle when drop/no drop box is checked
 
     // Handle when drop/no drop box is checked
     $('#showNoDrop, #showDroppable').change(function() {
+
     $('#showNoDrop, #showDroppable').change(function () {
 
       var showNoDrop = $('#showNoDrop').is(':checked');
 
       var showNoDrop = $('#showNoDrop').is(':checked');
 
       var showDroppable = $('#showDroppable').is(':checked');
 
       var showDroppable = $('#showDroppable').is(':checked');
Line 458: Line 543:
 
       //      .... but those rows do have an old bgcolor="#cccccc" attribute
 
       //      .... but those rows do have an old bgcolor="#cccccc" attribute
 
       //      that we can use to identify (and not hide) them
 
       //      that we can use to identify (and not hide) them
       $('tbody tr[bgcolor!=#cccccc]').each(function(i, el) {
+
       $('tbody tr[bgcolor!=#cccccc]').each(function (i, el) {
 
         var text = $(el).find('td:eq(0) .itemdata').text();
 
         var text = $(el).find('td:eq(0) .itemdata').text();
 
         var isNoDrop = text.includes('NO DROP');
 
         var isNoDrop = text.includes('NO DROP');
Line 464: Line 549:
 
       });
 
       });
 
     });
 
     });
/*
+
    /*
 
Problem: the event handler needs to be merged so unchecking one box (eg. effect) doesn't undo the other (eg. no drop)
 
Problem: the event handler needs to be merged so unchecking one box (eg. effect) doesn't undo the other (eg. no drop)
  
Line 482: Line 567:
 
*/
 
*/
 
   }
 
   }
 
  
 
   // Generic Table Filtering (for Template:TableFilterCheckbox)
 
   // Generic Table Filtering (for Template:TableFilterCheckbox)
   $('.table-filter-checkbox-container').each(function(i, el) {
+
   $('.table-filter-checkbox-container').each(function (i, el) {
 
     var $container = $(el);
 
     var $container = $(el);
 
     var matchSelector = $container.data('match');
 
     var matchSelector = $container.data('match');
 
     var text = $container.text();
 
     var text = $container.text();
     $container.html('<label><input class="table-filter-checkbox" type="checkbox" ' +
+
     $container.html(
                      'value="' + matchSelector + '" checked />' + text + '</label>');
+
      '<label><input class="table-filter-checkbox" type="checkbox" ' +
 +
        'value="' +
 +
        matchSelector +
 +
        '" checked />' +
 +
        text +
 +
        '</label>'
 +
    );
 
   });
 
   });
  
   var shouldRowBeShown = function(tr) {
+
   var shouldRowBeShown = function (tr) {
 
     var $tr = $(tr);
 
     var $tr = $(tr);
 
     // Check all the checkboxes to see if any given row should appear
 
     // Check all the checkboxes to see if any given row should appear
Line 499: Line 589:
 
     return $('.table-filter-checkbox')
 
     return $('.table-filter-checkbox')
 
       .toArray()
 
       .toArray()
       .reduce(function(isShown, checkbox) {
+
       .reduce(function (isShown, checkbox) {
 
         var matchSelector = $(checkbox).val();
 
         var matchSelector = $(checkbox).val();
         var matches = $tr.is(matchSelector) ||
+
         var matches = $tr.is(matchSelector) || !!$tr.has(matchSelector).length;
                      !!$tr.has(matchSelector).length;
+
 
         var isChecked = $(checkbox).attr('checked');
 
         var isChecked = $(checkbox).attr('checked');
 
         return isShown && !!(isChecked || (!isChecked && !matches));
 
         return isShown && !!(isChecked || (!isChecked && !matches));
 
       }, true);
 
       }, true);
   }
+
   };
   $('body').on('change', '.table-filter-checkbox', function(e) {
+
   $('body').on('change', '.table-filter-checkbox', function (e) {
 
     $('table').show();
 
     $('table').show();
     $('table:not(.toc) tbody tr:not([bgcolor="#cccccc"])').each(function(i, tr) {
+
     $('table:not(.toc) tbody tr:not([bgcolor="#cccccc"])').each(function (
 +
      i,
 +
      tr
 +
    ) {
 
       $(tr).toggle(shouldRowBeShown(tr));
 
       $(tr).toggle(shouldRowBeShown(tr));
 
     });
 
     });
 
     // If all of a table's non-header rows are hidden, hide it
 
     // If all of a table's non-header rows are hidden, hide it
     $('table').filter(function(i, table) {
+
     $('table')
      return !$(table).has('tr:visible:not([bgcolor="#cccccc"])').length;
+
      .filter(function (i, table) {
    }).hide();
+
        return !$(table).has('tr:visible:not([bgcolor="#cccccc"])').length;
 +
      })
 +
      .hide();
 
   });
 
   });
  
 
   // Convert youtube template divs into actual iframes
 
   // Convert youtube template divs into actual iframes
   $('.youtube-placeholder').each(function(i, placeholder) {
+
   $('.youtube-placeholder').each(function (i, placeholder) {
 
     var $placeholder = $(placeholder);
 
     var $placeholder = $(placeholder);
 
     var data = $placeholder.data();
 
     var data = $placeholder.data();
Line 526: Line 620:
 
     // @see https://stackoverflow.com/questions/25661182/embed-youtube-video-refused-to-display-in-a-frame-because-it-set-x-frame-opti
 
     // @see https://stackoverflow.com/questions/25661182/embed-youtube-video-refused-to-display-in-a-frame-because-it-set-x-frame-opti
 
     //if (url.includes('/watch?') url = url.replace('/watch?', '/embed?');
 
     //if (url.includes('/watch?') url = url.replace('/watch?', '/embed?');
   
+
 
 
     // TODO: get height/width from template?
 
     // TODO: get height/width from template?
 
     $placeholder.replaceWith(
 
     $placeholder.replaceWith(
 
       '<iframe ' +
 
       '<iframe ' +
         'width="' + (data.width || '') + '" ' +
+
         'width="' +
         'height="' + (data.height || '') + '" ' +
+
        (data.width || '') +
         'src="' + url + '" ' +
+
        '" ' +
 +
         'height="' +
 +
        (data.height || '') +
 +
        '" ' +
 +
         'src="' +
 +
        url +
 +
        '" ' +
 
         'frameborder="0" ' +
 
         'frameborder="0" ' +
 
         'allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" ' +
 
         'allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" ' +
 
         'allowfullscreen' +
 
         'allowfullscreen' +
      '></iframe>'
+
        '></iframe>'
 
     );
 
     );
 
   });
 
   });
 +
  
 
   importScript('MediaWiki:CheckboxLists.js');
 
   importScript('MediaWiki:CheckboxLists.js');
 
   importScript('MediaWiki:LocMaps.js');
 
   importScript('MediaWiki:LocMaps.js');
 +
 +
  // Fix searches with url-encoded text
 +
  if (window.location.search.includes('title=Special:Search')) {
 +
    var searchText = $('#searchText').val();
 +
    var decoded = decodeURIComponent(searchText)
 +
    if (decoded !== searchText) {
 +
      // If we have URL-encoded text, which won't give results,
 +
      // replace it with the decoded text and retry
 +
      $('#searchText').val(decoded).closest('form').submit();
 +
    }
 +
  }
  
 
   var fullTitle = $('title').text();
 
   var fullTitle = $('title').text();
   var title = fullTitle.substr(0, fullTitle.length - ' - Project 1999 Wiki'.length);
+
   var title = fullTitle.substr(
   switch(title) {
+
    0,
 +
    fullTitle.length - ' - Project 1999 Wiki'.length
 +
  );
 +
   switch (title) {
 +
case 'Admin:Spam Removal':
 +
      importScript('MediaWiki:SpamRemoval.js');
 +
      break;
 +
    case 'Damage Calculator':
 +
      importScript('MediaWiki:DamageCalculator.js');
 +
      break;
 +
    case 'Magelo Blue':
 +
    case 'Magelo Green':
 +
    case 'Magelo Red':
 +
      $('.createboxInput').on('focus', function () {
 +
        this.value = '';
 +
      });
 +
      break;
 
     // Give the Per-Level Hunting Guide Page its own JS
 
     // Give the Per-Level Hunting Guide Page its own JS
 
     case 'Per-Level Hunting Guide':
 
     case 'Per-Level Hunting Guide':
Line 568: Line 696:
 
       break;
 
       break;
 
     case 'Magelo Import':
 
     case 'Magelo Import':
 +
    case 'Item ID Generator':
 
       importScript('MediaWiki:MageloImport.js');
 
       importScript('MediaWiki:MageloImport.js');
       break;  
+
       break;
 +
    case 'Qeynos':
 +
      var now = new Date();
 +
      if (now.getHours() < 9 || now.getHours() > 21) {
 +
        var $art = $('[src="/images/thumb/Qeynos_Day_Art.jpg/300px-Qeynos_Day_Art.jpg"]');
 +
        $art.attr('src', '/images/thumb/Qeynos_Night_Art.jpg/300px-Qeynos_Night_Art.jpg');
 +
        $art.parent().attr('href', 'https://wiki.project1999.com/File:Qeynos_Night_Art.jpg');
 +
      }
 +
      break;
 
     case 'Mobs By Level':
 
     case 'Mobs By Level':
 
       // Basic DOM manipulation (since the wiki won't let us add <input> tags in the wiki text
 
       // Basic DOM manipulation (since the wiki won't let us add <input> tags in the wiki text
       $('#placeholder')
+
       $('#placeholder').replaceWith(
        .replaceWith(
+
        '<form id="form">' +
          '<form id="form">' +
+
          'Class: <input id="class" style="width: 8em" value="Warrior" /> ' +
            'Class: <input id="class" style="width: 8em" value="Warrior" /> ' +
+
          'Level: <input id="level" style="width: 4em" /> ' +
            'Level: <input id="level" style="width: 4em" /> ' +
+
          '<input type="submit"/>' +
            '<input type="submit"/>' +
+
 
           '</form>'
 
           '</form>'
        );
+
      );
       $('#form').submit(function() {
+
       $('#form').submit(function () {
         var clazz = $('#class').val();  
+
         var clazz = $('#class').val();
         var level = $('#level').val();  
+
         var level = $('#level').val();
         window.location = 'https://wiki.project1999.com/index.php?title=Special:Search&limit=500&offset=0&redirs=0&profile=default&search=%22Level%5C%3A%5C+' +
+
         window.location =
        level + '%22+-%22Shopkeeper%22+-%22Merchant%22+%22Class%5C%3A%5C+'+ clazz + '%22+-"startMageloProfile"';
+
          'https://wiki.project1999.com/index.php?title=Special:Search&limit=500&offset=0&redirs=0&profile=default&search=%22Level%5C%3A%5C+' +
 +
          level +
 +
          '%22+-%22Shopkeeper%22+-%22Merchant%22+%22Class%5C%3A%5C+' +
 +
          clazz +
 +
          '%22+-"startMageloProfile"';
 
         return false;
 
         return false;
 
       });
 
       });
Line 592: Line 732:
 
       // the table of contents is shown
 
       // the table of contents is shown
  
       var setNotesColumns = function(cols) { $('#notesWrapper').css('width', cols ? cols + 'em' : ''); };
+
       var setNotesColumns = function (cols) {
       var toggleNotRelevant =function(isChecked) {
+
        $('#notesWrapper').css('width', cols ? cols + 'em' : '');
         $('#notesWrapper').find(':not(:visible)').show()
+
      };
 +
       var toggleNotRelevant = function (isChecked) {
 +
         $('#notesWrapper').find(':not(:visible)').show();
 
         if (isChecked) return;
 
         if (isChecked) return;
  
         $('#notesWrapper').children().filter(function(i, el) {
+
         $('#notesWrapper')
          var isRelevant = $(el).is('.relevant');
+
          .children()
          $(el).toggle(isRelevant);
+
          .filter(function (i, el) {
          // Show the patch header also
+
            var isRelevant = $(el).is('.relevant');
          if (isRelevant) $(el).prevUntil('h3').last().prev().show();
+
            $(el).toggle(isRelevant);
         })
+
            // Show the patch header also
 +
            if (isRelevant) $(el).prevUntil('h3').last().prev().show();
 +
          });
 +
      };
 +
      var toggleTableOfContents = function (isChecked) {
 +
         $('#toc').toggle(isChecked);
 +
      };
 +
      var togglePatchHeaders = function (isChecked) {
 +
        $('.toclevel-4').toggle(isChecked);
 
       };
 
       };
      var toggleTableOfContents = function(isChecked) { $('#toc').toggle(isChecked); };
 
      var togglePatchHeaders = function(isChecked) { $('.toclevel-4').toggle(isChecked); };
 
  
 
       var notesWidth = getStateByPage('notes-width') || '';
 
       var notesWidth = getStateByPage('notes-width') || '';
       var showTableOfContents = getStateByPage('show-table-of-contents') !== false;
+
       var showTableOfContents =
 +
        getStateByPage('show-table-of-contents') !== false;
 
       var showPatchHeaders = getStateByPage('show-patch-headers') !== false;
 
       var showPatchHeaders = getStateByPage('show-patch-headers') !== false;
 
       var showNotRelevant = getStateByPage('show-not-relevant') !== false;
 
       var showNotRelevant = getStateByPage('show-not-relevant') !== false;
Line 618: Line 767:
  
 
       $('#patchNoteOptions').html(
 
       $('#patchNoteOptions').html(
      '<div><label><input ' + (showTableOfContents ? 'checked ' : '') + 'id="show-table-of-contents" type="checkbox" /> Show Table of Contents?<label><br/>' +
+
        '<div><label><input ' +
      '<label><input ' + (showPatchHeaders ? 'checked ' : '') + 'id="show-patch-headers"  type="checkbox"/> Show patch headers in Table of Contents?<label><br/>'+
+
          (showTableOfContents ? 'checked ' : '') +
      '<label><input ' + (showNotRelevant ? 'checked ' : '') + 'id="show-not-relevant"  type="checkbox"/> Show content not marked as relevant to P99?<label></div>' +
+
          'id="show-table-of-contents" type="checkbox" /> Show Table of Contents?<label><br/>' +
      '<div><label>Limit notes to a width of <input id="notes-width" placeholder="40" style="width: 4em;" value="' + notesWidth +'" /> columns (for readability)</label></div>'
+
          '<label><input ' +
 +
          (showPatchHeaders ? 'checked ' : '') +
 +
          'id="show-patch-headers"  type="checkbox"/> Show patch headers in Table of Contents?<label><br/>' +
 +
          '<label><input ' +
 +
          (showNotRelevant ? 'checked ' : '') +
 +
          'id="show-not-relevant"  type="checkbox"/> Show content not marked as relevant to P99?<label></div>' +
 +
          '<div><label>Limit notes to a width of <input id="notes-width" placeholder="40" style="width: 4em;" value="' +
 +
          notesWidth +
 +
          '" /> columns (for readability)</label></div>'
 
       );
 
       );
       $('#notes-width').change(function(e) {
+
       $('#notes-width').change(function (e) {
 
         var cols = parseFloat($(e.target).val());
 
         var cols = parseFloat($(e.target).val());
 
         setNotesColumns(cols);
 
         setNotesColumns(cols);
 
         setStateByPage('notes-width', cols);
 
         setStateByPage('notes-width', cols);
 
       });
 
       });
       $('#show-table-of-contents').change(function(e) {
+
       $('#show-table-of-contents').change(function (e) {
         var isChecked = $(e.target).is(':checked');
+
         var isChecked = $(e.target).is(':checked');
 
         toggleTableOfContents(isChecked);
 
         toggleTableOfContents(isChecked);
 
         setStateByPage('show-table-of-contents', isChecked);
 
         setStateByPage('show-table-of-contents', isChecked);
 
       });
 
       });
       $('#show-patch-headers').change(function(e) {
+
       $('#show-patch-headers').change(function (e) {
         var isChecked = $(e.target).is(':checked');
+
         var isChecked = $(e.target).is(':checked');
 
         togglePatchHeaders(isChecked);
 
         togglePatchHeaders(isChecked);
 
         setStateByPage('show-patch-headers', isChecked);
 
         setStateByPage('show-patch-headers', isChecked);
 
       });
 
       });
       $('#show-not-relevant').change(function(e) {
+
       $('#show-not-relevant').change(function (e) {
         var isChecked = $(e.target).is(':checked');
+
         var isChecked = $(e.target).is(':checked');
 
         toggleNotRelevant(isChecked);
 
         toggleNotRelevant(isChecked);
 
         setStateByPage('show-not-relevant', isChecked);
 
         setStateByPage('show-not-relevant', isChecked);
Line 648: Line 805:
  
 
   // Warn users who accidentally try to edit a templated section
 
   // Warn users who accidentally try to edit a templated section
   if ((window.location + '').includes('title=Template:Namedmobpage&action=edit')) {
+
   if (
     alert('Warning: You are attempting you edit the template for all named mobs.  You probably didn\'t mean to do that: you probably clicked on an edit link somewhere in the page and wound up here.  To avoid this simply go back and use the edit *tab* at the top of the page instead.');
+
    (window.location + '').includes('title=Template:Namedmobpage&action=edit')
 +
  ) {
 +
     alert(
 +
      "Warning: You are attempting you edit the template for all named mobs.  You probably didn't mean to do that: you probably clicked on an edit link somewhere in the page and wound up here.  To avoid this simply go back and use the edit *tab* at the top of the page instead."
 +
    );
 
   }
 
   }
  
Line 662: Line 823:
 
     var isUnderTenLines = $('#wpTextbox1').val().split('\n').length < 10;
 
     var isUnderTenLines = $('#wpTextbox1').val().split('\n').length < 10;
 
     var url = '/' + pageName + '#' + (section + extra).replace(/ /g, '_');
 
     var url = '/' + pageName + '#' + (section + extra).replace(/ /g, '_');
     var message = 'This page uses "transclusion" to show part of another page, specifically the code:\n\n    {{#lsth:' + pageName +
+
     var message =
                  '|[[' + section + ']]' + extra + '}}\n\nIf you want to edit the transcluded section, click "Ok": you will be taken to that page.' +
+
      'This page uses "transclusion" to show part of another page, specifically the code:\n\n    {{#lsth:' +
                  '  If you want to stay on this page, click "Cancel".';
+
      pageName +
 +
      '|[[' +
 +
      section +
 +
      ']]' +
 +
      extra +
 +
      '}}\n\nIf you want to edit the transcluded section, click "Ok": you will be taken to that page.' +
 +
      '  If you want to stay on this page, click "Cancel".';
 
     if (isEdit && isUnderTenLines && confirm(message)) location = url;
 
     if (isEdit && isUnderTenLines && confirm(message)) location = url;
 
   }
 
   }
Line 671: Line 838:
  
 
   var selectedServer = getGlobalState('selectedServer') || 'Blue';
 
   var selectedServer = getGlobalState('selectedServer') || 'Blue';
   var buildTabHtml = function(serverNames) {
+
   var buildTabHtml = function (serverNames) {
     var tabDivs = serverNames.map(function(name, i) {
+
     var tabDivs = serverNames.map(function (name, i) {
       return '<div id="' + name + 'Tab" class="tab' + (name === selectedServer ? ' selected' : '') + '">'+ name + '</div>';
+
       return (
 +
        '<div id="' +
 +
        name +
 +
        'Tab" class="tab' +
 +
        (name === selectedServer ? ' selected' : '') +
 +
        '">' +
 +
        name +
 +
        '</div>'
 +
      );
 
     });
 
     });
     return '<div class="tabs">' + tabDivs.join('') + '<div class="clear"></div></div>';
+
     return (
 +
      '<div class="tabs">' +
 +
      tabDivs.join('') +
 +
      '<div class="clear"></div></div>'
 +
    );
 
   };
 
   };
  
   var selectTab = function(name) {
+
   var selectTab = function (name) {
 
     // show the server's box (and hide the others)
 
     // show the server's box (and hide the others)
 
     $trackers.hide();
 
     $trackers.hide();
Line 688: Line 867:
 
     // Select the tab for that server (and unselect the others)
 
     // Select the tab for that server (and unselect the others)
 
     $('.auctrackerbox .tab').removeClass('selected');
 
     $('.auctrackerbox .tab').removeClass('selected');
     var $tab = $('.auctrackerbox .tab:contains("' + name + '")');
+
     var $tab = $('.auctrackerbox .tab:contains("' + name + '")');
 
     if (!$tab.length) $tab = $('.auctrackerbox .tab:first');
 
     if (!$tab.length) $tab = $('.auctrackerbox .tab:first');
 
     $tab.addClass('selected');
 
     $tab.addClass('selected');
Line 698: Line 877:
 
   if ($trackers.length) {
 
   if ($trackers.length) {
 
     // Convert IDs of "auc_Blue" into an array of ["Blue", "Green", ...]
 
     // Convert IDs of "auc_Blue" into an array of ["Blue", "Green", ...]
     var servers = $trackers.toArray().map(function(tracker) {
+
     var servers = $trackers.toArray().map(function (tracker) {
       return tracker.id.substr(4);  
+
       return tracker.id.substr(4);
 
     });
 
     });
 
     $trackers.prepend(buildTabHtml(servers));
 
     $trackers.prepend(buildTabHtml(servers));
 
     selectTab(selectedServer);
 
     selectTab(selectedServer);
     $trackers.on('click', '.tab', function(e) {
+
     $trackers.on('click', '.tab', function (e) {
 
       selectTab($(e.target).text());
 
       selectTab($(e.target).text());
 
     });
 
     });
Line 714: Line 893:
 
     var forumIds = { blue: 27, green: 77, red: 59, teal: 78 };
 
     var forumIds = { blue: 27, green: 77, red: 59, teal: 78 };
  
   
+
     $('.auctrackerbox span span:contains("Project 1999 Auction Tracker")').each(
     $('.auctrackerbox span span:contains("Project 1999 Auction Tracker")').each(function(i, el) {
+
      function (i, el) {
      var $el = $(el);
+
        var $el = $(el);
      var serverName = $el.closest('.auctrackerbox').attr('id').substr(4).toLowerCase();
+
        var serverName = $el
      const forumSearchUrl = 'https://www.project1999.com/forums/search.php?do=process&forumchoice[]=' +
+
          .closest('.auctrackerbox')
                            forumIds[serverName] + '&query=%22' + $('#firstHeading').text() + '%22';
+
          .attr('id')
      $el.append(
+
          .substr(4)
        '<a ' +
+
          .toLowerCase();
          'style="font-size: 0.5em; float: right" '+
+
        const forumSearchUrl =
          'target="_new" ' +
+
          'https://www.project1999.com/forums/search.php?do=process&forumchoice[]=' +
          'href="' + forumSearchUrl + '"' +
+
          forumIds[serverName] +
        '>Search Forum</a>');
+
          '&query=%22' +
 +
          $('#firstHeading').text() +
 +
          '%22';
 +
        $el.append(
 +
          '<a ' +
 +
            'style="font-size: 0.5em; float: right" ' +
 +
            'target="_new" ' +
 +
            'href="' +
 +
            forumSearchUrl +
 +
            '"' +
 +
            '>Search Forum</a>'
 +
        );
 +
      }
 +
    );
 +
  }
 +
 
 +
  // Dynamic zone page-related
 +
  // NOTE: Dynamic zone pages are used on every zone page in the site, to
 +
  //      display tables containing every mob/item in that zone.  They find the
 +
  //      pages for this list by checking the zone's wiki category.
 +
  //
 +
  //      However, at times an errant character in any one of the pages in the
 +
  //      category (eg. an editor pastes a Unicode character in) can break the
 +
  //      dynamic zone (PHP) code.
 +
  //
 +
  //      The code below first provides a link to explain the issue to regular
 +
  //      users, and then it adds a "secret" function for admins and power
 +
  //      users to more easily find recently edited pages (that could have
 +
  //      caused the problem).
 +
 
 +
  // Add a help link to dynamic zone lists, to explain how to fix them if they break
 +
  // NOTE: This could be better.  Potential improvements:
 +
  // A) we could only show the message when the table is actually (ie. every column except the first, of every row, is empty)
 +
  // B) instead of just showing an alert, we could show a confirm, and if they click ok we could send them to the zone's category page
 +
  // C) we could add logic to zone category pages to make them have a "History Mode" button, which converts all links to go straight to the history and open in a new tab
 +
  //    (this way people could actually fix the table by clicking every link, then closing the ones without recent edits ... we *could* even click every link for them ...)
 +
  //    EDIT: Actually, we have C) below now, but currently it's "secret", for
 +
  //          admins or power users, because if used en masse it could DOS the
 +
  //          wiki
 +
  try {
 +
 
 +
    const $mobsTable = $("#What\\.27s_in_this_zone")
 +
      .parent()
 +
      .next("p")
 +
      .next("table");
 +
 
 +
 
 +
 
 +
    $mobsTable.after(
 +
      '<div style="font-size:0.85em; text-align:right; max-width: '+ $mobsTable.width()+'px"><a class="broken-dynamic-table-help" href="#">Is this table not working?</a></div>'
 +
    );
 +
 
 +
    const $itemsTable = $mobsTable.next("div").next("p").next();
 +
    $itemsTable.after(
 +
      '<div style="font-size:0.85em; text-align:right; max-width: '+ $mobsTable.width()+'px"><a class="broken-dynamic-table-help" href="#">Is this table not working?</a></div>'
 +
    );
 +
 
 +
    $(".broken-dynamic-table-help").click(function(e){
 +
      e.preventDefault();
 +
      alert(
 +
        "This table updates itself nightly with data from other mob/item pages.  If one of the pages contains even a single invalid character (eg. a smiley face or other Unicode character), it can break that update.\n\nUnfortunately, at this time the only fix is to find the offending page and correct it (and then wait up to 24 hours to see if it worked).  However, wiki administrators (see /administrators) have tools that can help, so contacting one of them is recommended."
 +
      );
 
     });
 
     });
 +
  } catch (err) {
 +
    1 + 1; /* Wed don't care; it probably was a page with no dynamic zone list */
 
   }
 
   }
 +
 +
 +
  /**
 +
* Given HTML from a wiki history page, this function determines whether
 +
* that page contains edits from the last ${months} months.
 +
* @param html a string of HTML from a wiki entry's history page
 +
* @param months a number of months to check
 +
* @returns true if the page has recent edits
 +
*/
 +
function wasEditedRecently(html, months) {
 +
  var nMonthsAgo = new Date();
 +
  nMonthsAgo.setMonth(new Date().getMonth() - months);
 +
 +
  var dateString = $(html)
 +
    .find("#pagehistory li:first() .mw-changeslist-date")
 +
    .text();
 +
  var splitDate = dateString.split(" ");
 +
  var day = splitDate.at(-3);
 +
  var month = splitDate.at(-2);
 +
  var year = splitDate.at(-1);
 +
 +
  var newDateString = month + " " + day + "," + year;
 +
  var lastEditDate = new Date(Date.parse(newDateString));
 +
 +
  return lastEditDate > nMonthsAgo;
 +
}
 +
/**
 +
* With modern Javascript (async/await) iterating through a queue of fetches
 +
* with a delay is trivially easy ... but the wiki doesn't like modern
 +
* Javascript, so we need a recursive function, a custom promise, and a timeout,
 +
* just to accomplish that basic task.
 +
*
 +
* It's worth noting that the delay from the timeout is very important: without
 +
* it this function would flood the wiki with requests (potentially 100+ , all
 +
* at once).  Each fetch also needs to run sequentially (not in parallel, which
 +
* would be faster) for that same reason.
 +
*/
 +
function fetchPagesRecursivelyWithDelay(links, months) {
 +
  var url = links.pop();
 +
  return new Promise(function (resolve) {
 +
    fetch(url).then(function (response) {
 +
      return response.text().then(function (text) {
 +
        // Let user know how many more remain
 +
        $("#pagesLeftCount").text(links.length);
 +
 +
        if (wasEditedRecently(text, months)) {
 +
          var title = url
 +
            .substring(
 +
              "https://wiki.project1999.com/index.php?action=history&title="
 +
                .length
 +
            )
 +
            .replaceAll("_", " ");
 +
          $("#recentEdits").append(
 +
            '<li><a href="' + url + '" target="_blank">' + title + "</a></li>"
 +
          );
 +
        }
 +
 +
        if (links.length) {
 +
          // This handles the recursive case:
 +
          // resolve this recursion's promise ... after the next recursion
 +
          window.setTimeout(function () {
 +
            fetchPagesRecursivelyWithDelay(links, months).then(function () {
 +
              resolve();
 +
            });
 +
          }, 1000);
 +
        } else {
 +
          // This handles the final case
 +
          resolve();
 +
        }
 +
      });
 +
    });
 +
  });
 +
}
 +
 +
function addListToPageBottom(initialLinkCount) {
 +
  $("#mw-normal-catlinks")
 +
    .parent()
 +
    .before(
 +
      "<h1>Recent (<6 months) Edits</h1>" +
 +
        '<ul id="recentEdits" style="margin-bottom:2em"></ul>' +
 +
        '<div id="waitForIt">' +
 +
        "Please be patient!  Because we don't want to overload the wiki " +
 +
        "server, history is checked <em>slowly</em>.\n" +
 +
        'There are currently <span id="pagesLeftCount">' +
 +
        initialLinkCount +
 +
        "</span> pages remaining to check." +
 +
        "</div>"
 +
    );
 +
}
 +
 +
/**
 +
* Adds a new "debugging" section to the bottom of the current wiki page,
 +
* with an initially empty list.  The function then fetches the history
 +
* of every page in the current pages dynamic zone lists, one at a time.
 +
*
 +
* Each page is checked to see if they have been edited within the provided
 +
* number of months, and if so they are added to the list.
 +
*
 +
* Six is the recommended number of months to start, but the parameter can
 +
* be changed in case a page has sat broken for over half a year (and a
 +
* larger number will be needed to find the bad edit), or in case a page was
 +
* known to have broken more recently (in which case a lower number can shorten
 +
* the search).
 +
*
 +
* @param months
 +
* @usage From the console on a page with a broken dynamic zone table:
 +
*        checkForDynamicZonePagesEditedRecently(6);
 +
*
 +
*/
 +
window.checkForDynamicZonePagesEditedRecently = function (months) {
 +
  var links = $('th:contains("NPC Name"), th:contains("Item Name")')
 +
    .closest("table")
 +
    .children("tbody")
 +
    .children("tr")
 +
    .map(function (i, tr) {
 +
      var url = $(tr).children("td").eq(0).find("a").attr("href");
 +
      return (
 +
        "https://wiki.project1999.com/index.php?action=history&title=" +
 +
        url.split("/").at(-1)
 +
      );
 +
    })
 +
    .toArray();
 +
 +
  // Reverse the links, because the recursion reverses their order.  This isn't
 +
  // really *necessary*, but it looks odd to see the Z's appear before the A's
 +
  links.reverse();
 +
 +
  // Add the header and list, with wait message
 +
  addListToPageBottom(links.length);
 +
 +
  fetchPagesRecursivelyWithDelay(links, months).then(function () {
 +
    // Remove the wait message
 +
    $("#waitForIt").remove();
 +
  });
 +
}
 +
 +
 +
// End onReady code
 
});
 
});

Latest revision as of 19:58, 18 July 2025

/* Any JavaScript here will be loaded for all users on every page load. */

importScript('MediaWiki:Polyfills.js');

// HTTP prevents people from logging in now in (some?) Chrome browsers; redirect to HTTPS
try {
  var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
  var onHttp = location.protocol === 'http:';
  var haveNotTriedAlready = !location.search.includes('redirected_from_http');

  if (isChrome && onHttp && haveNotTriedAlready) {
    location =
      location.origin.replace('http:', 'https:') +
      location.pathname +
      location.search +
//      (location.search[0] === '?' ? '&' : '?') +
//      'redirected_from_http=1' +
      location.hash;
  }
} catch (err) {
  // Give up on trying to redirect
  // maybe alert user?
}

importScript('MediaWiki:Simple-lightbox.min.js');
importScript('MediaWiki:Zones.js');

// Get all magelos of current user
// (currently un-used, but intended for future achievement code, so that
//  we can have an "add this achievement to my magelo" drop-down with all
//  their magelos listed)
var getMagelosForCurrentUser = function () {
  var userName = $('#pt-userpage a').text();
  var url =
    'https://wiki.project1999.com/index.php?limit=50&tagfilter=&' +
    'title=Special%3AContributions&contribs=user&target=' +
    userName +
    '&namespace=500&topOnly=1&year=&month=-1';
  fetch(url)
    .then(function (response) {
      return response.text();
    })
    .then(function (html) {
      var mageloLinks = Array.from($(html).find('li a:contains("Magelo")'));
      var names = mageloLinks
        .filter(function (i, a) {
          return !$(a).text().includes('(');
        })
        .map(function (i, el) {
          return $(el).text();
        });
      console.log(names);
    });
};

// **********************************
// Add localStorage helper functions
// **********************************

/**
 * Basic "state getter": checks localStorage for *stateKey*, then return the
 * value for *pageName* (if any) found on that object.
 */
var getPageState = function (pageName, stateKey) {
  // Get the state for all pages for this key
  var allPageStates = JSON.parse(localStorage.getItem(stateKey) || '{}');

  // Extract just the state for this page (if any)
  return allPageStates[pageName];
};

/**
 * Returns the state data stored for the provided stateKey and the current page.
 * For instance, getStateByPage('checkbox-status') would return the checkbox\
 * status state for the current page.
 */
window.getStateByPage = function (stateKey) {
  var pageName = location.pathname.substr(1); // ditch the leading "/"
  return getPageState(pageName, stateKey);
};

/**
 * Return the global state for a provided state key
 * (works just like getStateByPage, but for states that don't care
 * about which page you are on)
 */
window.getGlobalState = function (stateKey) {
  return getPageState('__global', stateKey);
};

/**
 * Basic "state setter" function
 */
var setPageState = function (pageName, stateKey, state) {
  // Get the state for this key (for all pages) out of local storage
  var allPageStates = JSON.parse(localStorage.getItem(stateKey) || '{}');

  // Set the state only for this page
  allPageStates[pageName] = state;

  // Put the state for the stateKey (for all pages) back into local storage
  localStorage.setItem(stateKey, JSON.stringify(allPageStates));
};

/**
 * Sets the state data stored for the provided stateKey and the current page.
 * For instance, setStateByPage('checkbox-status', {a: 'b'}) would set the
 * checkbox status state for the current page to an object with a key of "a" and
 * value of "b".
 */
window.setStateByPage = function (stateKey, state) {
  var pageName = location.pathname.substr(1); // ditch the leading "/"
  setPageState(pageName, stateKey, state);
};

window.setGlobalState = function (stateKey, state) {
  return setPageState('__global', stateKey, state);
};

// *******************************

/* p1999wiki.js
 * written by http://wiki.project1999.com/User:Ravhin
 * last update: 7 January, 2018 by Loramin
 */
$(function () {
  // Start onReady code

  var hideDelay = 0;
  var trigDelay = 250;
  var hideTimer = null;
  var ajax = null;

  var currentPosition = { left: '0px', top: '0px' };

  // One instance that's reused to show info for the current person
  var container = $(
    '<div id="itemHoverContainer">' + '<div id="HoverContent"></div>' + '</div>'
  );

  $('body').append(container);

  /* --- hoverbox for item/mob, currently only used in magelo --- */

  // Determine which "a" elements should trigger the item stats mouseover
  var $mouseoverTargets = $('span.ih a');
  var isItemCategory =
    document.title.startsWith('Category:') &&
    document.title.includes('Equipment - Project 1999 Wiki') &&
    !document.title.includes('Worshiper Equipment');
  if (isItemCategory) {
    // Include the category's item list along with ".ih" links
    $mouseoverTargets = $mouseoverTargets.add('.mw-content-ltr a');
  }

  $mouseoverTargets = $mouseoverTargets.filter(function (i, a) {
    // Don't add hover to links like "next 200"
    return (
      !$(a).attr('href').startsWith('/Special:') &&
      !$(a).attr('href').includes('title=Category:')
    );
  });

  $mouseoverTargets.on('mouseover', function () {
    var $this = $(this);
    var itemname = $this.attr('title');

    if (itemname == '' || itemname == 'undefined') return;

    if (hideTimer) clearTimeout(hideTimer);

    if ($this.parents('div.mw-content-ltr').length) {
      var pos = $this.offset();
      var width = $this.width();

      container.css({
        left: pos.left + 5 + 'px',
        top: pos.top + 5 + 'px',
      });
    }

    $(this).trigger('mousemove');

    $('#itemHoverContent').html('&nbsp;');

    //$('#itemHoverContent').html('<div class="itemtopbg"><div class="itemtitle">Loading...</div></div>'
    //                          + '<div class="itembg" style="min-height:50px;"><div class="itemdata">'
    //                          + '<div class="itemicon" style="float:right;"><img alt="" src="/images/Ajax_loader.gif" border="0"></div>'
    //                          + '<p></p></div></div><div class="itembotbg"></div>');

    if (ajax) {
      ajax.abort();
      ajax = null;
    }
    ajax = $.ajax({
      url:
        window.location.protocol +
        '//wiki.project1999.com/index.php/Special:AjaxHoverHelper/' +
        itemname,
      cacheResponse: true,
      success: function (html) {
        var $html = $(html);
        $('#itemHoverContent')
          .html($html[2])
          .prepend($html[0])
          .prepend($html[1]);
      },
    });

    container.css('display', 'block');
    //container.fadeIn('fast');
  }); //on mouseover

  $('span.ih a').on('mouseout', function () {
    if (hideTimer) clearTimeout(hideTimer);
    hideTimer = setTimeout(function () {
      container.css('display', 'none');
      //container.fadeOut('fast');
    }, hideDelay);
  });

  $('span.ih a').mousemove(function (e) {
    var mousex = e.pageX + 20; //Get X coodrinates
    var mousey = e.pageY + 20; //Get Y coordinates
    var tipWidth = container.width(); //Find width of tooltip
    var tipHeight = container.height(); //Find height of tooltip

    //Distance of element from the right edge of viewport
    var tipVisX = $(window).width() - (mousex + tipWidth);
    //Distance of element from the bottom of viewport
    var tipVisY = $(window).height() - (mousey + tipHeight);

    if (tipVisX < 20) {
      //If tooltip exceeds the X coordinate of viewport

      if (tipWidth > e.pageX - 20) {
        mousex = 0;
      } else {
        mousex = e.pageX - tipWidth - 20;
      }
    }
    if (tipVisY < 20) {
      //If tooltip exceeds the Y coordinate of viewport
      mousey = e.pageY - tipHeight - 20;
    }

    container.css({ top: mousey, left: mousex });
  });

  // Allow mouse over of details without hiding details
  $('#itemHoverContainer').mouseover(function () {
    if (hideTimer) clearTimeout(hideTimer);
  });

  // Hide after mouseout
  $('#itemHoverContainer').mouseout(function () {
    if (hideTimer) clearTimeout(hideTimer);
    hideTimer = setTimeout(function () {
      container.css('display', 'none');
      //container.fadeOut('fast');
    }, hideDelay);
  });

  // magelo non-ajax item hover, but move box with mouse
  $('.magelohb').mousemove(function (e) {
    var childContainer = $(this).children('span.hb');

    var tipWidth = childContainer.width(); //Find width of tooltip
    var tipHeight = childContainer.height(); //Find height of tooltip

    var mousex = e.pageX + 20; //Get X coodrinates
    var mousey = e.pageY + 20; //Get Y coordinates

    //Distance of element from the right edge of viewport
    var tipVisX = $(window).width() - (mousex + tipWidth - 20);
    //Distance of element from the bottom of viewport
    var tipVisY = $(window).height() - (mousey + tipHeight - 20);

    if (tipVisX < 20) {
      //If tooltip exceeds the X coordinate of viewport

      if (tipWidth > e.pageX - 20) {
        mousex = 0;
      } else {
        mousex = e.pageX - tipWidth - 20;
      }
    }
    if (tipVisY < 20) {
      //If tooltip exceeds the Y coordinate of viewport
      mousey = e.pageY - tipHeight - 20;
    }

    childContainer.css({ top: mousey, left: mousex, 'z-index': '999' });
  });

  // change to position:fixed on all hover divs if we have JS active
  // otherwise leave as position:absolute so the stationary hovers are near their items
  $('.magelohb span.hb').each(function (i) {
    $(this).css({ position: 'fixed' });
  });

  // Chrome no longer displays alt text when an image is hovered over.  However the wiki only has
  // alt attributes for images, not titles.  This fixes that by converting alt => title
  $('img[alt]').each(function (i, img) {
    $(img).attr('title', $(img).attr('alt'));
  });

  // Fashion for item pages
  var extractFashionHtml = function (html) {
    return $(html)
      .find('.fashion_show, .primary_secondary_show')
      .map(function (i, el) {
        var $el = $(el);
        var data = $el.data();
        data.race = $el.find('.fashion_race').html();
        if ($el.is('.fashion_show')) {
          data.armor = $el.find('.fashion_armor').html();
        }
        return data;
      })
      .toArray();
  };

  var getFashionShows = function (fashionCategory) {
    var url = '/Category:Fashion: ' + fashionCategory;
    return $.get(url).then(extractFashionHtml);
  };

  var getItemPageShows = function () {
    var fashionCategories = $('#catlinks li a')
      .map(function (i, a) {
        return $(a).text();
      })
      .filter(function (i, text) {
        return text.startsWith('Fashion:');
      })
      .map(function (i, text) {
        return text.substr('Fashion: '.length);
      });
    var fashion = fashionCategories[0];
    if (!fashion) return $.when();

    var tint = fashionCategories[1];
    return getFashionShows(fashion).then(function (shows) {
      var sameTint = [];
      var otherTint = [];
      shows.forEach(function (show) {
        var isHeld = show.primaryFashion || show.secondaryFashion;
        var bothHaveSameTint = show.tint === tint;
        var bothHaveNoTint = !(show.tint || tint);
        if (isHeld || bothHaveSameTint || bothHaveNoTint) sameTint.push(show);
        else otherTint.push(show);
      });
      return {
        matches: sameTint,
        partialMatches: otherTint,
        fashion: fashion,
        tint: tint,
      };
    });
  };
  var addFashionSection = function (shows, isFullMatch) {
    var preface = isFullMatch
      ? '<h2>Fashion/Appearance</h2>'
      : '<div>Fashion images with the wrong "tint" (i.e. coloring) which ' +
        "can only be used to get a general sense of the item's " +
        'appearance:</div>';

    // Don't show the explanatory text if there are no shows to explain
    if (!shows.length && !isFullMatch) preface = '';

    var $ul = $('<ul>').append(
      shows.map(function (show) {
        var img = isFullMatch
          ? '<br/><img style="max-height: 100px; max-width: 100px;" src="/images/' +
            show.file.replace(/ /g, '_') +
            '"/>'
          : '';
        return (
          '<li class="fashion-link" data-file="' +
          show.file +
          '">' +
          '<a href="#">' +
          show.gender +
          ' ' +
          show.race +
          img +
          '</a></li>'
        );
      })
    );

    $('#itemfashion').before($('<div>' + preface + '</div>').append($ul));
  };

  var addFashionSections = function () {
    getItemPageShows().then(function (pageShows) {
      if (!pageShows) return;
      var matches = pageShows.matches || [];
      var partialMatches = pageShows.partialMatches || [];
      if (pageShows.matches.length || pageShows.partialMatches.length)
        addFashionSection(pageShows.matches, true);
      if (pageShows.partialMatches.length)
        addFashionSection(pageShows.partialMatches, false);
    });
  };
  addFashionSections();


  //*****************************************
  // Add class-based filtering to item tables
  //*****************************************w

  // Determine whether or not a provided row should be shown for the provided
  // class abbreviation (eg. "BRD")
  var isRowShown = function ($tr, classAbbrev) {
    // Extract classes (and races)
    var classText = $tr.find('td').eq(3).text().split('Class:')[1];
    if (!classText) return true; // Ignore (don't filter) rows without class text

    // remove "Race: " part (if any) and upper-case text
    classText = classText.split('Race:')[0] || classText;
    classText = classText.toUpperCase();

    // determine matching text
    var isMatch = classText.includes(classAbbrev);
    var isAllMatch = classText.includes('ALL');
    var isExcept = classText.includes('ALL EXCEPT');

    // determine matches
    if (isMatch && !isExcept) return true; // (eg. BRD in "BRD")
    if (!isMatch && isExcept) return true; // (eg. BRD in "ALL EXCEPT WIZ")
    if (!isExcept && isAllMatch) return true; // (eg. BRD in "ALL")
    return false;
  };

  var scrollToTable = function ($table) {
    window.setTimeout(function () {
      $('html, body').animate({ scrollTop: $table.offset().top }, 200);
    }, 100);
  };

  // Filter the provided table to only display rows for the provided class
  // abbreviation
  var filterTable = function ($table, classAbbrev) {
    $table.find('tr').each(function (i, tr) {
      var $tr = $(tr);
      var rowIsShown = isRowShown($tr, classAbbrev);
      $tr.toggle(rowIsShown);
    });
    scrollToTable($table);

    addFilterLink($table, false);
    $('.itemsUnfilterLink').click(function () {
      unfilterTable($table);
    });

    alert('Filtered to only show rows containing ' + classAbbrev + ' gear.');
  };

  // Restore a table to its previous, un-filtered state
  var unfilterTable = function ($table) {
    $table.find('tr').show();
    addFilterLink($table, true);
    scrollToTable($table);
  };

  // When a user clicks on a table class filter link, handle it by asking them
  // which class (and then filtering the table for that class)
  var handleItemFilterLinkClick = function (e) {
    var promptMessage =
      'Please enter the three-letter abbreviation for the ' +
      'class you want to filter by (eg. "brd" for "Bard").';

    var $table = $(e.target).closest('table');
    var classAbbrev = prompt(promptMessage).toUpperCase();
    if (!classAbbrev || classAbbrev.length !== 3 || classAbbrev === 'ALL') {
      alert(
        'A 3-digit abbreviation wasn\'t entered (or "All" was entered); ' +
          'filtering disabled'
      );
      unfilterTable($table);
      return false;
    }
    filterTable($table, classAbbrev);
  };

  // Adds either a filter or unfilter link (as determined by showFilter) to the
  // provided table
  var addFilterLink = function ($table, showFilter) {
    var $statsHeaderCells = $table.find('th:contains("Stats")');
    var filterType = showFilter ? 'Filter' : 'Unfilter';
    $statsHeaderCells.html(
      'Stats <span style="float:right">' +
        '<a style="text-decoration: underline" class="items' +
        filterType +
        'Link" href="#">' +
        filterType +
        (showFilter ? ' by Class' : '') +
        '</a></span>'
    );

    var $link = $statsHeaderCells.find('.items' + filterType + 'Link');
    if (showFilter) $link.click(handleItemFilterLinkClick);
    else
      $link.click(function () {
        unfilterTable($table);
      });
  };

  // Add class filtering links to all item tables
  var addItemFilteringLinks = function () {
    var $tables = $('table th:contains("Item Name")').closest('table');
    $tables.each(function (i, table) {
      var $table = $(table);
      // Make sure it has both "Item Name" and "Stats" columns
      if (!$table.has('th:contains("Stats")')) return;

      addFilterLink($table, true);
    });
  };
  addItemFilteringLinks();

  //*****************************************
  // End class-based filtering to item tables
  //*****************************************

  // Add No-Drop-Based filtering to the class equipment pages
  if (window.location.pathname.includes('Special:ClassSlotEquip')) {
    // Add checkboxes UI to the page
    $('table').before(
      '<label><input id="showNoDrop" type="checkbox" checked/> Show No Drop</label> ' +
        '<label><input id="showDroppable" type="checkbox" checked/> Show Droppable</label>' /*+
              '| <label><input id="showEffect" type="checkbox" checked/> Show With Effects</label>' +
              '<label><input id="showNoEffect" type="checkbox" checked/> Show Without Effects</label>'*/
    );

    // Handle when drop/no drop box is checked
    $('#showNoDrop, #showDroppable').change(function () {
      var showNoDrop = $('#showNoDrop').is(':checked');
      var showDroppable = $('#showDroppable').is(':checked');
      // NOTE: Some tables are weird and don't keep their TRs in THEAD
      //       .... but those rows do have an old bgcolor="#cccccc" attribute
      //       that we can use to identify (and not hide) them
      $('tbody tr[bgcolor!=#cccccc]').each(function (i, el) {
        var text = $(el).find('td:eq(0) .itemdata').text();
        var isNoDrop = text.includes('NO DROP');
        $(el).toggle((showNoDrop && isNoDrop) || (showDroppable && !isNoDrop));
      });
    });
    /*
Problem: the event handler needs to be merged so unchecking one box (eg. effect) doesn't undo the other (eg. no drop)

    // Handle when effect/no effect box is checked
    $('#showNoEfefct, #showEffect').change(function() {
      var showNoEffect = $('#showNoEffect').is(':checked');
      var showEffect = $('#showEffect').is(':checked');
      // NOTE: Some tables are weird and don't keep their TRs in THEAD
      //       .... but those rows do have an old bgcolor="#cccccc" attribute
      //       that we can use to identify (and not hide) them
      $('tbody tr[bgcolor!=#cccccc]').each(function(i, el) {
        var text = $(el).find('td:eq(0) .itemdata').text();
        var hasEffect = text.includes('NO DROP');
        $(el).toggle((showEffect && hasEffect) || (showNoEffect && !hasEffect));
      });
    });
*/
  }

  // Generic Table Filtering (for Template:TableFilterCheckbox)
  $('.table-filter-checkbox-container').each(function (i, el) {
    var $container = $(el);
    var matchSelector = $container.data('match');
    var text = $container.text();
    $container.html(
      '<label><input class="table-filter-checkbox" type="checkbox" ' +
        'value="' +
        matchSelector +
        '" checked />' +
        text +
        '</label>'
    );
  });

  var shouldRowBeShown = function (tr) {
    var $tr = $(tr);
    // Check all the checkboxes to see if any given row should appear
    // TODO: Instead of checking *every* checkbox, check ones in the same .filter-group
    return $('.table-filter-checkbox')
      .toArray()
      .reduce(function (isShown, checkbox) {
        var matchSelector = $(checkbox).val();
        var matches = $tr.is(matchSelector) || !!$tr.has(matchSelector).length;
        var isChecked = $(checkbox).attr('checked');
        return isShown && !!(isChecked || (!isChecked && !matches));
      }, true);
  };
  $('body').on('change', '.table-filter-checkbox', function (e) {
    $('table').show();
    $('table:not(.toc) tbody tr:not([bgcolor="#cccccc"])').each(function (
      i,
      tr
    ) {
      $(tr).toggle(shouldRowBeShown(tr));
    });
    // If all of a table's non-header rows are hidden, hide it
    $('table')
      .filter(function (i, table) {
        return !$(table).has('tr:visible:not([bgcolor="#cccccc"])').length;
      })
      .hide();
  });

  // Convert youtube template divs into actual iframes
  $('.youtube-placeholder').each(function (i, placeholder) {
    var $placeholder = $(placeholder);
    var data = $placeholder.data();
    var url = data.url;
    // Youtube gets mad if you try to embed watch URLs
    // @see https://stackoverflow.com/questions/25661182/embed-youtube-video-refused-to-display-in-a-frame-because-it-set-x-frame-opti
    //if (url.includes('/watch?') url = url.replace('/watch?', '/embed?');

    // TODO: get height/width from template?
    $placeholder.replaceWith(
      '<iframe ' +
        'width="' +
        (data.width || '') +
        '" ' +
        'height="' +
        (data.height || '') +
        '" ' +
        'src="' +
        url +
        '" ' +
        'frameborder="0" ' +
        'allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" ' +
        'allowfullscreen' +
        '></iframe>'
    );
  });


  importScript('MediaWiki:CheckboxLists.js');
  importScript('MediaWiki:LocMaps.js');

  // Fix searches with url-encoded text
  if (window.location.search.includes('title=Special:Search')) {
    var searchText = $('#searchText').val();
    var decoded = decodeURIComponent(searchText)
    if (decoded !== searchText) {
      // If we have URL-encoded text, which won't give results,
      // replace it with the decoded text and retry
      $('#searchText').val(decoded).closest('form').submit();
    }
  }

  var fullTitle = $('title').text();
  var title = fullTitle.substr(
    0,
    fullTitle.length - ' - Project 1999 Wiki'.length
  );
  switch (title) {
 case 'Admin:Spam Removal':
      importScript('MediaWiki:SpamRemoval.js');
       break;
    case 'Damage Calculator':
      importScript('MediaWiki:DamageCalculator.js');
      break;
    case 'Magelo Blue':
    case 'Magelo Green':
    case 'Magelo Red':
      $('.createboxInput').on('focus', function () {
        this.value = '';
      });
      break;
    // Give the Per-Level Hunting Guide Page its own JS
    case 'Per-Level Hunting Guide':
      importScript('MediaWiki:HuntingGuide.js');
      break;
    // The item category search also needs special JS
    case 'Item Category Search':
      importScript('MediaWiki:ItemCategorySearch.js');
      break;
    // So does the Solo Artist Challenge
    case 'Solo Artist Challenge':
      importScript('MediaWiki:SoloArtistChallenge.js');
      break;
    case 'Treasure Hunting Guide':
      importScript('MediaWiki:TreasureHuntingGuide.js');
      break;
    case 'Buff Lines':
      importScript('MediaWiki:BuffLines.js');
      break;
    case 'FashionQuest Finder':
      importScript('MediaWiki:FashionQuest.js');
      break;
    case 'Magelo Import':
    case 'Item ID Generator':
      importScript('MediaWiki:MageloImport.js');
      break;
    case 'Qeynos':
      var now = new Date();
      if (now.getHours() < 9 || now.getHours() > 21) {
        var $art = $('[src="/images/thumb/Qeynos_Day_Art.jpg/300px-Qeynos_Day_Art.jpg"]');
        $art.attr('src', '/images/thumb/Qeynos_Night_Art.jpg/300px-Qeynos_Night_Art.jpg');
        $art.parent().attr('href', 'https://wiki.project1999.com/File:Qeynos_Night_Art.jpg');
      }
      break;
    case 'Mobs By Level':
      // Basic DOM manipulation (since the wiki won't let us add <input> tags in the wiki text
      $('#placeholder').replaceWith(
        '<form id="form">' +
          'Class: <input id="class" style="width: 8em" value="Warrior" /> ' +
          'Level: <input id="level" style="width: 4em" /> ' +
          '<input type="submit"/>' +
          '</form>'
      );
      $('#form').submit(function () {
        var clazz = $('#class').val();
        var level = $('#level').val();
        window.location =
          'https://wiki.project1999.com/index.php?title=Special:Search&limit=500&offset=0&redirs=0&profile=default&search=%22Level%5C%3A%5C+' +
          level +
          '%22+-%22Shopkeeper%22+-%22Merchant%22+%22Class%5C%3A%5C+' +
          clazz +
          '%22+-"startMageloProfile"';
        return false;
      });
      break;
    case 'Patch Notes':
      // Let users control patch note width and how
      // the table of contents is shown

      var setNotesColumns = function (cols) {
        $('#notesWrapper').css('width', cols ? cols + 'em' : '');
      };
      var toggleNotRelevant = function (isChecked) {
        $('#notesWrapper').find(':not(:visible)').show();
        if (isChecked) return;

        $('#notesWrapper')
          .children()
          .filter(function (i, el) {
            var isRelevant = $(el).is('.relevant');
            $(el).toggle(isRelevant);
            // Show the patch header also
            if (isRelevant) $(el).prevUntil('h3').last().prev().show();
          });
      };
      var toggleTableOfContents = function (isChecked) {
        $('#toc').toggle(isChecked);
      };
      var togglePatchHeaders = function (isChecked) {
        $('.toclevel-4').toggle(isChecked);
      };

      var notesWidth = getStateByPage('notes-width') || '';
      var showTableOfContents =
        getStateByPage('show-table-of-contents') !== false;
      var showPatchHeaders = getStateByPage('show-patch-headers') !== false;
      var showNotRelevant = getStateByPage('show-not-relevant') !== false;

      setNotesColumns(notesWidth);
      toggleTableOfContents(showTableOfContents);
      togglePatchHeaders(showPatchHeaders);
      toggleNotRelevant(showNotRelevant);

      $('#patchNoteOptions').html(
        '<div><label><input ' +
          (showTableOfContents ? 'checked ' : '') +
          'id="show-table-of-contents" type="checkbox" /> Show Table of Contents?<label><br/>' +
          '<label><input ' +
          (showPatchHeaders ? 'checked ' : '') +
          'id="show-patch-headers"  type="checkbox"/> Show patch headers in Table of Contents?<label><br/>' +
          '<label><input ' +
          (showNotRelevant ? 'checked ' : '') +
          'id="show-not-relevant"  type="checkbox"/> Show content not marked as relevant to P99?<label></div>' +
          '<div><label>Limit notes to a width of <input id="notes-width" placeholder="40" style="width: 4em;" value="' +
          notesWidth +
          '" /> columns (for readability)</label></div>'
      );
      $('#notes-width').change(function (e) {
        var cols = parseFloat($(e.target).val());
        setNotesColumns(cols);
        setStateByPage('notes-width', cols);
      });
      $('#show-table-of-contents').change(function (e) {
        var isChecked = $(e.target).is(':checked');
        toggleTableOfContents(isChecked);
        setStateByPage('show-table-of-contents', isChecked);
      });
      $('#show-patch-headers').change(function (e) {
        var isChecked = $(e.target).is(':checked');
        togglePatchHeaders(isChecked);
        setStateByPage('show-patch-headers', isChecked);
      });
      $('#show-not-relevant').change(function (e) {
        var isChecked = $(e.target).is(':checked');
        toggleNotRelevant(isChecked);
        setStateByPage('show-not-relevant', isChecked);
      });

      break;
  }

  // Warn users who accidentally try to edit a templated section
  if (
    (window.location + '').includes('title=Template:Namedmobpage&action=edit')
  ) {
    alert(
      "Warning: You are attempting you edit the template for all named mobs.  You probably didn't mean to do that: you probably clicked on an edit link somewhere in the page and wound up here.  To avoid this simply go back and use the edit *tab* at the top of the page instead."
    );
  }

  // Warn uses who try to edit a mostly-transcluded section (and offer to take them to the transcluded page)
  var text = $('#wpTextbox1').val();
  var match = text && text.match(/\{\{\#lsth\:(.*?)\|\[\[(.*?)\]\](.*)\}\}/);
  if (match) {
    var pageName = match[1];
    var section = match[2];
    var extra = match[3] || '';
    var isEdit = (window.location + '').includes('action=edit');
    var isUnderTenLines = $('#wpTextbox1').val().split('\n').length < 10;
    var url = '/' + pageName + '#' + (section + extra).replace(/ /g, '_');
    var message =
      'This page uses "transclusion" to show part of another page, specifically the code:\n\n    {{#lsth:' +
      pageName +
      '|[[' +
      section +
      ']]' +
      extra +
      '}}\n\nIf you want to edit the transcluded section, click "Ok": you will be taken to that page.' +
      '  If you want to stay on this page, click "Cancel".';
    if (isEdit && isUnderTenLines && confirm(message)) location = url;
  }

  // *** AUCTION TRACKER RELATED ***

  var selectedServer = getGlobalState('selectedServer') || 'Blue';
  var buildTabHtml = function (serverNames) {
    var tabDivs = serverNames.map(function (name, i) {
      return (
        '<div id="' +
        name +
        'Tab" class="tab' +
        (name === selectedServer ? ' selected' : '') +
        '">' +
        name +
        '</div>'
      );
    });
    return (
      '<div class="tabs">' +
      tabDivs.join('') +
      '<div class="clear"></div></div>'
    );
  };

  var selectTab = function (name) {
    // show the server's box (and hide the others)
    $trackers.hide();
    var $tracker = $('#auc_' + name).show();

    // If the selected tab doesn't exist on this item, show the first tab instead
    if (!$tracker.length) $tracker = $('.auctrackerbox:first').show();

    // Select the tab for that server (and unselect the others)
    $('.auctrackerbox .tab').removeClass('selected');
    var $tab = $('.auctrackerbox .tab:contains("' + name + '")');
    if (!$tab.length) $tab = $('.auctrackerbox .tab:first');
    $tab.addClass('selected');
    setGlobalState('selectedServer', name);
  };

  var $trackers = $('.auctrackerbox');
  // if we're on an item page with an auction tracker
  if ($trackers.length) {
    // Convert IDs of "auc_Blue" into an array of ["Blue", "Green", ...]
    var servers = $trackers.toArray().map(function (tracker) {
      return tracker.id.substr(4);
    });
    $trackers.prepend(buildTabHtml(servers));
    selectTab(selectedServer);
    $trackers.on('click', '.tab', function (e) {
      selectTab($(e.target).text());
    });

    // Add links to search the forum for items to their auction tracker

    // Forum IDs come from forum URLs, eg. red's auction forum is:
    //   https://www.project1999.com/forums/forumdisplay.php?f=59
    // so  we have "red: 59 " below
    var forumIds = { blue: 27, green: 77, red: 59, teal: 78 };

    $('.auctrackerbox span span:contains("Project 1999 Auction Tracker")').each(
      function (i, el) {
        var $el = $(el);
        var serverName = $el
          .closest('.auctrackerbox')
          .attr('id')
          .substr(4)
          .toLowerCase();
        const forumSearchUrl =
          'https://www.project1999.com/forums/search.php?do=process&forumchoice[]=' +
          forumIds[serverName] +
          '&query=%22' +
          $('#firstHeading').text() +
          '%22';
        $el.append(
          '<a ' +
            'style="font-size: 0.5em; float: right" ' +
            'target="_new" ' +
            'href="' +
            forumSearchUrl +
            '"' +
            '>Search Forum</a>'
        );
      }
    );
  }

  // Dynamic zone page-related
  // NOTE: Dynamic zone pages are used on every zone page in the site, to
  //       display tables containing every mob/item in that zone.  They find the
  //       pages for this list by checking the zone's wiki category.
  //
  //       However, at times an errant character in any one of the pages in the
  //       category (eg. an editor pastes a Unicode character in) can break the
  //       dynamic zone (PHP) code.
  //
  //       The code below first provides a link to explain the issue to regular
  //       users, and then it adds a "secret" function for admins and power
  //       users to more easily find recently edited pages (that could have
  //       caused the problem).

  // Add a help link to dynamic zone lists, to explain how to fix them if they break
  // NOTE: This could be better.  Potential improvements:
  // A) we could only show the message when the table is actually (ie. every column except the first, of every row, is empty)
  // B) instead of just showing an alert, we could show a confirm, and if they click ok we could send them to the zone's category page
  // C) we could add logic to zone category pages to make them have a "History Mode" button, which converts all links to go straight to the history and open in a new tab
  //    (this way people could actually fix the table by clicking every link, then closing the ones without recent edits ... we *could* even click every link for them ...)
  //    EDIT: Actually, we have C) below now, but currently it's "secret", for
  //          admins or power users, because if used en masse it could DOS the
  //          wiki
  try {

    const $mobsTable = $("#What\\.27s_in_this_zone")
      .parent()
      .next("p")
      .next("table");



    $mobsTable.after(
      '<div style="font-size:0.85em; text-align:right; max-width: '+ $mobsTable.width()+'px"><a class="broken-dynamic-table-help" href="#">Is this table not working?</a></div>'
    );

    const $itemsTable = $mobsTable.next("div").next("p").next();
    $itemsTable.after(
      '<div style="font-size:0.85em; text-align:right; max-width: '+ $mobsTable.width()+'px"><a class="broken-dynamic-table-help" href="#">Is this table not working?</a></div>'
    );

    $(".broken-dynamic-table-help").click(function(e){
      e.preventDefault();
      alert(
        "This table updates itself nightly with data from other mob/item pages.  If one of the pages contains even a single invalid character (eg. a smiley face or other Unicode character), it can break that update.\n\nUnfortunately, at this time the only fix is to find the offending page and correct it (and then wait up to 24 hours to see if it worked).  However, wiki administrators (see /administrators) have tools that can help, so contacting one of them is recommended."
      );
    });
  } catch (err) {
    1 + 1; /* Wed don't care; it probably was a page with no dynamic zone list */
  }


  /**
 * Given HTML from a wiki history page, this function determines whether
 * that page contains edits from the last ${months} months.
 * @param html a string of HTML from a wiki entry's history page
 * @param months a number of months to check
 * @returns true if the page has recent edits
 */
function wasEditedRecently(html, months) {
  var nMonthsAgo = new Date();
  nMonthsAgo.setMonth(new Date().getMonth() - months);

  var dateString = $(html)
    .find("#pagehistory li:first() .mw-changeslist-date")
    .text();
  var splitDate = dateString.split(" ");
  var day = splitDate.at(-3);
  var month = splitDate.at(-2);
  var year = splitDate.at(-1);

  var newDateString = month + " " + day + "," + year;
  var lastEditDate = new Date(Date.parse(newDateString));

  return lastEditDate > nMonthsAgo;
}
/**
 * With modern Javascript (async/await) iterating through a queue of fetches
 * with a delay is trivially easy ... but the wiki doesn't like modern
 * Javascript, so we need a recursive function, a custom promise, and a timeout,
 * just to accomplish that basic task.
 *
 * It's worth noting that the delay from the timeout is very important: without
 * it this function would flood the wiki with requests (potentially 100+ , all
 * at once).  Each fetch also needs to run sequentially (not in parallel, which
 * would be faster) for that same reason.
 */
function fetchPagesRecursivelyWithDelay(links, months) {
  var url = links.pop();
  return new Promise(function (resolve) {
    fetch(url).then(function (response) {
      return response.text().then(function (text) {
        // Let user know how many more remain
        $("#pagesLeftCount").text(links.length);

        if (wasEditedRecently(text, months)) {
          var title = url
            .substring(
              "https://wiki.project1999.com/index.php?action=history&title="
                .length
            )
            .replaceAll("_", " ");
          $("#recentEdits").append(
            '<li><a href="' + url + '" target="_blank">' + title + "</a></li>"
          );
        }

        if (links.length) {
          // This handles the recursive case:
          // resolve this recursion's promise ... after the next recursion
          window.setTimeout(function () {
            fetchPagesRecursivelyWithDelay(links, months).then(function () {
              resolve();
            });
          }, 1000);
        } else {
          // This handles the final case
          resolve();
        }
      });
    });
  });
}

function addListToPageBottom(initialLinkCount) {
  $("#mw-normal-catlinks")
    .parent()
    .before(
      "<h1>Recent (<6 months) Edits</h1>" +
        '<ul id="recentEdits" style="margin-bottom:2em"></ul>' +
        '<div id="waitForIt">' +
        "Please be patient!  Because we don't want to overload the wiki " +
        "server, history is checked <em>slowly</em>.\n" +
        'There are currently <span id="pagesLeftCount">' +
        initialLinkCount +
        "</span> pages remaining to check." +
        "</div>"
    );
}

/**
 * Adds a new "debugging" section to the bottom of the current wiki page,
 * with an initially empty list.  The function then fetches the history
 * of every page in the current pages dynamic zone lists, one at a time.
 *
 * Each page is checked to see if they have been edited within the provided
 * number of months, and if so they are added to the list.
 *
 * Six is the recommended number of months to start, but the parameter can
 * be changed in case a page has sat broken for over half a year (and a
 * larger number will be needed to find the bad edit), or in case a page was
 * known to have broken more recently (in which case a lower number can shorten
 * the search).
 *
 * @param months
 * @usage From the console on a page with a broken dynamic zone table:
 *        checkForDynamicZonePagesEditedRecently(6);
 *
 */
window.checkForDynamicZonePagesEditedRecently = function (months) {
  var links = $('th:contains("NPC Name"), th:contains("Item Name")')
    .closest("table")
    .children("tbody")
    .children("tr")
    .map(function (i, tr) {
      var url = $(tr).children("td").eq(0).find("a").attr("href");
      return (
        "https://wiki.project1999.com/index.php?action=history&title=" +
        url.split("/").at(-1)
      );
    })
    .toArray();

  // Reverse the links, because the recursion reverses their order.  This isn't
  // really *necessary*, but it looks odd to see the Z's appear before the A's
  links.reverse();

  // Add the header and list, with wait message
  addListToPageBottom(links.length);

  fetchPagesRecursivelyWithDelay(links, months).then(function () {
    // Remove the wait message
    $("#waitForIt").remove();
  });
}


// End onReady code
});