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:LocMaps.js"

From Project 1999 Wiki
Jump to: navigation, search
Line 1: Line 1:
  try {
+
/* Any JavaScript here will be loaded for all users on every page load. */
  (function() {
+
  
    // TODO: Add support for cropping maps that have multiple maps.
+
importScript('MediaWiki:Polyfills.js');
    // For instance, the following CSS styles show only one of the three maps for
+
importScript('MediaWiki:Zones.js');
    // Erudin Palace:
+
    // background-image:url(/images/Erudinpalace.jpg);  
+
    // background-position: 250px 0px; width: 275px; height: 272px
+
    // Figure out how to make the Xs line up with those styles added
+
  
    var locIsWithinAlternateData = function(loc, alternateData) {
+
/* p1999wiki.js
      return loc.x < alternateData.maxX &&
+
* written by http://wiki.project1999.com/User:Ravhin
      loc.x > alternateData.minX &&
+
* last update: 7 January, 2018 by Loramin
      loc.y < alternateData.maxY &&
+
*/
      loc.y > alternateData.minY;
+
$(function() {
    };
+
  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
    * Determines if the provided name contains any of the provided words
+
  var container = $('<div id="itemHoverContainer">'
    */
+
    + '<div id="HoverContent"></div>'
    // TODO: Add a some polyfill to make this a lot simpler
+
    + '</div>');
    var containsAny = function(name/* word1, word2, etc. */) {
+
      for(var arg = 1; arg < arguments.length; ++ arg) {
+
        var word = arguments[arg];
+
        if (name.includes(word)) return true;
+
      }
+
      return false;
+
    };
+
  
    var containsAnyNumber = function(name, number, suffix) {
+
  $('body').append(container);
      return containsAny.call(null, name,
+
      number + suffix,
+
      'level ' + number,
+
      'level: ' + number
+
      );
+
    };
+
  
    var getZoneLevelData = function(levels, text) {
+
/* --- hoverbox for item/mob, currently only used in magelo --- */
      // Check for part "level" aliases (eg. "1st floor" vs. "Level One")
+
      text = text.toLowerCase();
+
      if (containsAnyNumber(text, 0, 'th') || containsAny(text, 'basement', 'underground')) return levels[0];
+
      if (containsAnyNumber(text, 1, 'st') || containsAny(text, 'one', 'first', 'ground')) return levels[1];
+
      if (containsAnyNumber(text, 2, 'nd') || containsAny(text, 'two', 'second')) return levels[2];
+
      if (containsAnyNumber(text, 3, 'rd') || containsAny(text, 'three', 'third')) return levels[3];
+
      if (containsAnyNumber(text, 4, 'th') || containsAny(text, 'four', 'fourth')) return levels[4];
+
      if (containsAnyNumber(text, 5, 'th') || containsAny(text, 'five', 'fifth')) return levels[5];
+
      if (containsAnyNumber(text, 6, 'th') || containsAny(text, 'six', 'sixth')) return levels[6];
+
      if (containsAnyNumber(text, 7, 'th') || containsAny(text, 'seven', 'seventh')) return levels[7];
+
      if (containsAnyNumber(text, 8, 'th') || containsAny(text, 'eight', 'eighth')) return levels[8];
+
      if (containsAnyNumber(text, 9, 'th') || containsAny(text, 'nine', 'ninth')) return levels[9];
+
  
      var levelKeys = Object.keys(levels);
+
  // Determine which "a" elements should trigger the item stats mouseover
      var keyOfLastLevel = levelKeys[levelKeys.length - 1];
+
  var $mouseoverTargets = $('span.ih a');
      if (containsAny(text, 'top')) return levels[keyOfLastLevel];
+
  var isItemCategory = document.title.startsWith('Category:') && document.title.includes('Equipment - Project 1999 Wiki') &&
 
+
                      !document.title.includes('Worshiper Equipment');
      // If we still couldn't match, and there is a ground/1st floor, use it
+
  if (isItemCategory) {
      return levels[1];
+
    // 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:')
 +
  });
  
     var findZoneData = function(zoneName, locs, nonLocParts) {
+
  $mouseoverTargets.on('mouseover', function()
      var zone = zoneData[zoneName];
+
  {
 +
     var $this = $(this);
 +
    var itemname = $this.attr('title');
  
    // Handle aliases (eg. "North Ro" instead of "Northern Desert of Ro")
+
     if (itemname == '' || itemname == 'undefined')
     if (typeof(zone) === 'string') zone = zoneData[zone];
+
      return;
  
     if (!zone) return null;
+
     if (hideTimer)
 +
      clearTimeout(hideTimer);
  
     // Handle Multi-Level Zones (with different maps for 1st, 2nd, etc. floor)
+
     if ($this.parents('div.mw-content-ltr').length) {
    if (zone.levels) return getZoneLevelData(zone.levels, nonLocParts);
+
      var pos = $this.offset();
 +
      var width = $this.width();
  
    // Handle Zones with alternate maps on the same level (eg. Kelethin in GFay)
+
      container.css({
    if (!zone.alternateMaps) return zone;
+
          left: pos.left + 5 + 'px',
 
+
          top: pos.top + 5 + 'px'
    for (var i = 0; i < zone.alternateMaps.length; i++) {
+
      var alternateData = zone.alternateMaps[i];
+
      var allLocsAreWithin = true;
+
      // wish I had ES6 [].every
+
      $.each(locs, function(i, loc) {
+
        allLocsAreWithin = allLocsAreWithin &&
+
        locIsWithinAlternateData(loc, alternateData);
+
 
       });
 
       });
      if (allLocsAreWithin) return alternateData;
+
     }
     };
+
    return zone;
+
  };
+
  
  var addImageUrl = function(zoneData) {
 
    zoneData.imageUrl = '/images/' + zoneData.image;
 
    return zoneData;
 
  };
 
  
  var getZoneData = function(zoneName, locs, nonLocParts) {
 
    var zoneData = findZoneData(zoneName, locs, nonLocParts);
 
    if (!zoneData) return null;
 
    zoneData.zoneName = zoneName;
 
    return addImageUrl(zoneData);
 
  };
 
  
     var build$MapImage = function(zoneData) {
+
     $(this).trigger('mousemove');
      return $(
+
        '<img alt="Map of ' + zoneData.zoneName + '" ' +
+
        '    class="thumbborder" ' +
+
        '    height="'+ zoneData.height + '" ' +
+
        '    src="' + zoneData.imageUrl + '" ' +
+
        '    title="Map of ' + zoneData.zoneName + '"' +
+
        '    width="' + zoneData.width + '" ' +
+
        '>'
+
        );
+
    };``
+
  
     var build$XContainer = function() {
+
     $('#itemHoverContent').html('&nbsp;');
      return $('<div class="x-container"></div>')
+
        .css({ position: 'relative' });
+
    }
+
  
     var build$WrappedMap = function(zoneData) {
+
     //$('#itemHoverContent').html('<div class="itemtopbg"><div class="itemtitle">Loading...</div></div>'
      return build$XContainer().append(build$MapImage(zoneData));
+
    //                          + '<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>');
  
     var build$LightboxFramedMap = function(zoneData) {
+
     if (ajax)
       var $map = build$WrappedMap(zoneData);
+
    {
       return addFramingStyles($map, zoneData.width, zoneData.height)
+
       ajax.abort();
    };
+
       ajax = null;
 
+
    var addFramingStyles = function($el, width, height) {
+
      return $el.css({
+
          left: '50%',
+
          marginLeft: '-' + (width / 2) + 'px', // centering
+
          marginTop: '-' + (height / 2) + 'px',
+
          opacity: 1,
+
          top: '50%'
+
        });
+
 
     }
 
     }
 +
    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]);
 +
      }
 +
    });
  
     var build$MapFrame = function() {
+
     container.css('display', 'block');
      return $('<div class="map-wrapper"></div>')
+
    //container.fadeIn('fast');
              .css({ position: 'absolute' });
+
    };
+
  
    var removeOpenMap = function(e) {
+
  }); //on mouseover
      // Do nothing if there's no open map
+
      if (!window.$openImg) return false;
+
  
      // Do nothing if the user clicked to open a map, then moused out of an area
+
  $('span.ih a').on('mouseout', function()
      // (otherwise it can close immediately)
+
  {
       var position = window.$openImg.css('position');
+
    if (hideTimer)
       if (e && e.type === 'mouseleave' && position === 'fixed') return false;
+
       clearTimeout(hideTimer);
 +
    hideTimer = setTimeout(function()
 +
    {
 +
      container.css('display', 'none');
 +
       //container.fadeOut('fast');
 +
    }, hideDelay);
 +
  });
  
      window.$openImg.remove();
+
  $('span.ih a').mousemove(function(e){
      return false;
+
    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);
  
     var build$FullScreenFrame = function() {
+
     if ( tipVisX < 20 ) { //If tooltip exceeds the X coordinate of viewport
      return build$MapFrame()
+
        .css({
+
          background: 'rgba(0,0,0,0.8)',
+
          height: '100%',
+
          left: 0,
+
          position: 'fixed',
+
          top: 0,
+
          width: '100%',
+
          zIndex: 4 // one higher than #p-search's 3
+
        })
+
        .click(removeOpenMap);
+
    }
+
  
    var build$FullMap = function(zoneData) {
+
      if( tipWidth > e.pageX - 20 ){
      var $frame = build$FullScreenFrame();
+
        mousex = 0;
       if (zoneData) {
+
       } else {
         $frame.html(build$LightboxFramedMap(zoneData));
+
         mousex = e.pageX - tipWidth - 20;
        $frame.zoneData = zoneData;
+
 
       }
 
       }
      return $frame;
 
    };
 
  
     var build$SmallMap = function(zoneData) {
+
     } if ( tipVisY < 20 ) { //If tooltip exceeds the Y coordinate of viewport
       return build$MapFrame().append(build$WrappedMap(zoneData));
+
       mousey = e.pageY - tipHeight - 20;
 
     }
 
     }
  
 +
    container.css({  top: mousey, left: mousex });
 +
  });
  
   var buildX = function(left, top, sizeInEm) {
+
   // Allow mouse over of details without hiding details
    sizeInEm = sizeInEm || 2.5;
+
  $('#itemHoverContainer').mouseover(function()
    return $('<div class="x">x</div>')
+
  {
      .css({
+
    if (hideTimer)
        color: 'red',
+
       clearTimeout(hideTimer);
        fontSize: sizeInEm + 'em',
+
   });
        fontWeight: 'bold',
+
        left: left,
+
        position: 'absolute',
+
        top: top
+
       })
+
   }
+
  
   /**
+
   // Hide after mouseout
  * Draws a red "X" on the map at the provided coordinate
+
   $('#itemHoverContainer').mouseout(function()
  */
+
  {
   var addX = function($xContainer, zoneData, x, y, xSize) {
+
     if (hideTimer)
     var left = (zoneData.zeroX || 0) + x * -1 * (zoneData.zoomX || 0.1);
+
      clearTimeout(hideTimer);
     var top = (zoneData.zeroY || 0) + y * -1 * (zoneData.zoomY || 0.1);
+
     hideTimer = setTimeout(function()
     $xContainer.append(buildX(left, top, xSize));
+
     {
   }
+
      container.css('display', 'none');
 +
      //container.fadeOut('fast');
 +
    }, hideDelay);
 +
   });
  
   var addXs = function($xContainer, zoneData, locs, xSize) {
+
   // magelo non-ajax item hover, but move box with mouse
    $.each(locs, function(i, loc) {
+
  $('.magelohb').mousemove(function(e){
      addX($xContainer, zoneData, loc.x, loc.y, xSize);
+
    var childContainer = $(this).children('span.hb');
    });
+
  }
+
  
 +
    var tipWidth = childContainer.width(); //Find width of tooltip
 +
    var tipHeight = childContainer.height(); //Find height of tooltip
  
  var parseLoc = function(locText) {
+
    var mousex = e.pageX + 20; //Get X coodrinates
     if (typeof locText !== 'string') return locText;
+
     var mousey = e.pageY + 20; //Get Y coordinates
  
     var match = locText.match(/\(? *([\+\-]?\d+\.?\d*), *([\+\-]?\d+\.?\d*)\)?/);
+
    //Distance of element from the right edge of viewport
     return {x: parseFloat(match[2]), y: parseFloat(match[1]) };
+
     var tipVisX = $(window).width() - (mousex + tipWidth - 20);
  }
+
     //Distance of element from the bottom of viewport
 +
    var tipVisY = $(window).height() - (mousey + tipHeight - 20);
  
  var isLoc = function(locBit) {
+
    if ( tipVisX < 20 ) { //If tooltip exceeds the X coordinate of viewport
      // If we can't split the string by its comma and find a number on either side, it's not a loc
+
 
       try {
+
       if( tipWidth > e.pageX - 20){
         return locBit.split(',')[0].match(/\d+/) && locBit.split(',')[1].match(/\d+/);
+
         mousex = 0;
       } catch (err) {
+
       } else {
         return false;
+
         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' });
    * Helper function to power parseLocs/extractNonLocText, since they both use
+
  });
    * the same logic.
+
    *
+
    * SIDE NOTE: If only the wiki didn't have to use 1999 JS, and could
+
    *            destructure return values with ES2015, the two related functions
+
    *            wouldn't even need to exist :(
+
    */
+
    var parseLocString = function(locString) {
+
      var nonLocParts = '';
+
      var bits = locString.split(/([\+\-]?\d+\.?\d*\D*,\D*[\+\-]?\d+\.?\d*)/g);
+
      var relevantBits = bits.filter(function(part) {
+
        // Filter out the non-loc parts, but save them
+
        if (isLoc(part)) return true;
+
        nonLocParts += part; // save as it might be something like "1st floor"
+
      });
+
      var locs = relevantBits.map(parseLoc);
+
      return [locs, nonLocParts];
+
    };
+
  
    /**
+
  // change to position:fixed on all hover divs if we have JS active
    * Extracts all of the locs (objects with x and y properties) from a provided
+
  // otherwise leave as position:absolute so the stationary hovers are near their items
    * string such as:
+
   $('.magelohb span.hb').each(function(i) {
    *   "500, 200"
+
     $(this).css({'position':'fixed'});
    *  "(200, 300), (300, 200)"
+
  });
    * and returns an array containing any locs found.
+
    */
+
    var parseLocs = function(locString) {
+
      return parseLocString(locString)[0];
+
     };
+
   
+
    /**
+
    * Extracts the non-loc parts (eg. "Level 1") from a provided string of text
+
    * (presumably from an NPC's location <td> or somewhere similar). Although not
+
    * actually a part of the loc, this text may still be useful in determining
+
    * which map to show for the loc.
+
    */
+
    var extractNonLocText = function(locString) {
+
      return parseLocString(locString)[1];
+
    };
+
  
 +
  // 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'));
 +
  });
  
    var showImage = function($img, $container) {
+
  // Fashion for item pages
      if ($container) {
+
  var extractFashionHtml = function(html) {
        $img.css({ marginTop: 40 });
+
    return $(html).find('.fashion_show, .primary_secondary_show').map(function(i, el) {
        var isAboveHalf = $container.position().top < ($('body').height() / 2);
+
      var $el = $(el);
        var isLeftOfHalf = $container.position().left < ($('body').width() / 2);
+
      var data = $el.data();
        $container.css('position', 'relative')
+
      data.race = $el.find('.fashion_race').html();
                  .prepend($img);
+
      if ($el.is('.fashion_show')) {
        // TODO: Do the same thing with left/right
+
         data.armor = $el.find('.fashion_armor').html();
         $img.css({
+
          [isAboveHalf  ? 'top' : 'bottom']: '1.5em',
+
          [isLeftOfHalf  ? 'left' : 'right']: '1.5em'
+
        })
+
 
       }
 
       }
       else $('body').append($img);
+
       return data;
 +
    }).toArray();
 +
  };
 +
 
 +
  var getFashionShows = function(fashionCategory) {
 +
    var url = 'http://wiki.project1999.com/Category:Fashion: ' + fashionCategory;
 +
    return $.get(url).then(extractFashionHtml);
 +
  };
  
       window.$openImg = $img;
+
  var getItemPageShows = function() {
      return $img;
+
    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];
    * Adds a loc map image to the page.
+
    return getFashionShows(fashion).then(function(shows) {
    */
+
       var sameTint = [];
    var showMapWithLocs = function(zoneData, locs, $container) {
+
       var otherTint = [];
       removeOpenMap();
+
       shows.forEach(function(show) {
       zoneData = addImageUrl(zoneData);
+
        // Primary/secondary slots have no tint
       var $map = $container ? build$SmallMap(zoneData) : build$FullMap(zoneData);
+
        if (show.primaryFashion || show.secondaryFashion) sameTint.push(show);
 
+
         else (show.tint === tint ? sameTint : otherTint).push(show);
      var $xContainer = $map.find('.x-container');
+
      $map.find('img').load(function() {
+
         addXs($xContainer, zoneData, locs);
+
 
       });
 
       });
       return showImage($map, $container);
+
       return { matches: sameTint, otherTint: otherTint, fashion: fashion, tint: tint };
     };
+
    });
 +
  }
 +
  var addFashionSection = function() {
 +
    getItemPageShows().then(function(pageShows) {
 +
    if(!pageShows || !pageShows.matches) return;
 +
    // Build fashion section
 +
      var lis = pageShows.matches.map(function(show) {
 +
        return '<li class="fashion-link" data-file="' + show.file +'">' +
 +
                '<a href="#">' + show.gender + ' ' + show.race + '</a></li>';
 +
      });
 +
      var $ul = $('<ul>').append(lis);
 +
      var $div = $('<div><h2>Fashion/Appearance</h2></div>').append($ul);
 +
      // Not sure why we have two IDs: two different item templates?
 +
      $('#Drop_looted_from, #Drops_From').parent().before($div);
 +
     });
 +
  };
 +
  addFashionSection();
 +
 
 +
 
 +
  //*****************************************
 +
  // 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
 +
  //*****************************************
  
    // Handle "Where to obtain" rows for spells
+
  // Add No-Drop-Based filtering to the class equipment pages
    var handleWhereToObtainRows = function() {
+
  if (window.location.pathname.includes('Special:ClassSlotEquip')) {
      $('table:has(th:contains("Area"))' +
+
    // Add checkboxes UI to the page
        ':has(th:contains("Location"), th:contains("Loc"))' +
+
    $('table')
        ':has(th:contains("Zone"))')
+
      .before('<label><input id="showNoDrop" type="checkbox" checked/> Show No Drop</label> ' +
        .each(function(i, table) {
+
              '<label><input id="showDroppable" type="checkbox" checked/> Show Droppable</label>');
          var $table = $(table);
+
          var areaColumnIndex = $table.find('th:contains("Area")').index();
+
          var locationColumnIndex =
+
            $table.find('th:contains("Location"), th:contains("Loc")').index();
+
          var zoneColumnIndex = $table.find('th:contains("Zone")').index();
+
  
          $table.find('tr').each(function(i, tr) {
+
    // Handle when either box is checked
            var $tr = $(tr);
+
    $('#showNoDrop, #showDroppable').change(function() {
            var $loc = $tr.find('td:eq(' + locationColumnIndex + ')');
+
      var showNoDrop = $('#showNoDrop').is(':checked');
            var loc = $loc.text().trim();
+
      var showDroppable = $('#showDroppable').is(':checked');
            // Include the area in case it contains relevant non-loc location info
+
      // NOTE: Some tables are weird and don't keep their TRs in THEAD
            // (such as a floor number)
+
      //       .... but those rows do have an old bgcolor="#cccccc" attribute
            var nonLoc = $tr.find('td:eq(' + areaColumnIndex + ')').text().trim();
+
      //      that we can use to identify (and not hide) them
            var zone = $tr.find('td:eq(' + zoneColumnIndex + ')').text().trim();
+
      $('tbody tr[bgcolor!=#cccccc]').each(function(i, el) {
            var $locLink = $('<a href="' + zone + '" ' +
+
        var text = $(el).find('td:eq(0) .itemdata').text();
                            '  data-loc="' + nonLoc + ' ' + loc + '" ' +
+
        var isNoDrop = text.includes('NO DROP');
                            '  data-zone="' + zone + '"' +
+
        $(el).toggle((showNoDrop && isNoDrop) || (showDroppable && !isNoDrop));
                            '>' + loc + '</a>');
+
      });
            $loc.html($locLink);
+
    });
          });
+
  }
        });
+
    };
+
  
    var getZoneName = function() {
 
      try {
 
        return $('b:contains("Zone:")').parent().text().split('Zone:')[1].trim();
 
      } catch (err) {
 
        return null;
 
      };
 
    };
 
  
    /**
+
  // Generic Table Filtering (for Template:TableFilterCheckbox)
    * Determines whether the page has a "Location" <td> (as all NPC pages do)
+
  $('.table-filter-checkbox-container').each(function(i, el) {
    * with parsable locs inside it (as only some do). Also checks that
+
    var $container = $(el);
    * "loc-mapped" zone data exists for the NPC's zone.
+
    var matchSelector = $container.data('match');
    */
+
    var text = $container.text();
    var getZoneDataIfThereIsALocBox = function($locTd, zoneName) {
+
    $container.html('<label><input class="table-filter-checkbox" type="checkbox" ' +
      if (!$locTd.length) return false; // page isn't an NPC page (no loc box)
+
                      'value="' + matchSelector + '" checked />' + text + '</label>');
     
+
  });
      var locs = parseLocs($locTd.text());
+
      if (!locs.length) return false; // no locs existed (or could be parsed)
+
  
       var nonLocParts = extractNonLocText($locTd.text());
+
  var shouldRowBeShown = function(tr) {
      return !!getZoneData(zoneName, locs, nonLocParts); // does zone data exist?
+
    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();
 +
  });
  
    var handleLocBoxes = function() {
 
      var zoneName = getZoneName();
 
      var $locTd = $('b:contains("Location:")').parent();
 
      var zoneData = getZoneDataIfThereIsALocBox($locTd, zoneName);
 
      if (!zoneData) return;
 
     
 
      // Get the mob's loc(s)
 
      var locs = parseLocs($locTd.text());
 
      var nonLocParts = extractNonLocText($locTd.text());
 
     
 
      // Do we have data for that zone's map?
 
      var zoneData = getZoneData(zoneName, locs, nonLocParts);
 
      if (!zoneData) return;  // If not, stop here
 
 
      // Add the mouse-enter link
 
      var $link = $(' <a href="#">(Map)</a>')
 
        // When it's moused-over, show the map nearby
 
        .on('mouseenter', function(e) {
 
          // If there is already a map "open" (because of a click), do nothing
 
          if (window.$openImg && window.$openImg.parent().length) return false;
 
 
          var $map = showMapWithLocs(zoneData, locs, $locTd);
 
          $map.parent().one('mouseleave', removeOpenMap);
 
        })
 
        // When it's clicked, show the map full-screen/lightbox style
 
        .on('click', function(e) {
 
          showMapWithLocs(zoneData, locs);
 
        });
 
 
      // $locTd.find('b').html($('<span>Location </span>').append($link).append('<span>:</span>'));
 
      $locTd
 
        .find('b')
 
        .html($('<span>Location </span>')
 
        .append($link)
 
        .append('<span>:</span>'));
 
    };
 
 
    // Setup event listeners (code "starts" here)
 
    var startLocFunctionality = function() {
 
      handleWhereToObtainRows();
 
      handleLocBoxes();
 
    };
 
 
    // Sometimes zoneData isn't ready when this code is, so retry for 10 secs in hope that it becomes available
 
    var tries = 10;
 
    var timeBetweenTries = 1000; // 1 second
 
    var interval = window.setInterval(function() {
 
      if (window.zoneData) {
 
        window.clearInterval(interval);
 
        startLocFunctionality();
 
      } else {
 
        tries -= 1;
 
        if (!tries) window.clearInterval(interval); // Failure :(
 
      }
 
    }, timeBetweenTries);
 
 
    var alertNoData = function(zone) {
 
      alert('We\'re sorry, but ' + zone + ' has not been loc mapped yet ... ' +
 
            'please see "/Loc_Maps" for more information.');
 
    }
 
  
    var showMapForData = function(data, $target) {
+
  importScript('MediaWiki:CheckboxLists.js');
      if (!data.loc || ! data.zone) return;
+
  importScript('MediaWiki:LocMaps.js');
  
      var locs = [parseLoc(data.loc)];
+
  var fullTitle = $('title').text();
      var zoneData = getZoneData(data.zone, locs);
+
  var title = fullTitle.substr(0, fullTitle.length - ' - Project 1999 Wiki'.length);
      if (!zoneData) {
+
  switch(title) {
        // If it was a click and not a mouseover
+
    // Give the Per-Level Hunting Guide Page its own JS
        if (!$target) alertNoData(data.zone);
+
    case 'Per-Level Hunting Guide':
        return;
+
      importScript('MediaWiki:HuntingGuide.js');
      }
+
      break;
       showMapWithLocs(zoneData, locs, $target);
+
    // The item category search also needs special JS
    };
+
    case 'Item Category Search':
 
+
       importScript('MediaWiki:ItemCategorySearch.js');
     // TODO: this is in here because fashion links and loc links share global
+
      break;
     // event watchers, but this file needs to be renamed now
+
     // So does the Solo Artist Challenge
     var showFashionForData = function(data) {
+
    case 'Solo Artist Challenge':
      if (!data.file) return;
+
      importScript('MediaWiki:SoloArtistChallenge.js');
      var $img = $('<img src="/images/' + data.file + '"">');
+
      break;
      var $div = $('<div class="framed-image"></div>')
+
     case 'Treasure Hunting Guide':
                    .html($img)
+
      importScript('MediaWiki:TreasureHuntingGuide.js');
      var $framedImage =  build$FullScreenFrame().html($div)
+
      break;
      showImage($framedImage);
+
     case 'Buff Lines':
    };
+
      importScript('MediaWiki:BuffLines.js');
 
+
      break;
    // Hook up loc-link events
+
    case 'FashionQuest Finder':
    $('body')
+
      importScript('MediaWiki:FashionQuest.js');
      .on('click', '.loc-link, .fashion-link', function() {
+
      break;
         var data = $(this).data();
+
    case 'Mobs By Level':
         showMapForData(data);
+
      // Basic DOM manipulation (since the wiki won't let us add <input> tags in the wiki text
         showFashionForData(data);
+
      $('#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;
 
         return false;
      })
 
      .on('mouseenter', '.loc-link', function(e) {
 
        var $this = $(e.target).on('mouseleave', removeOpenMap);
 
        showMapForData($(this).data(), $this);
 
 
       });
 
       });
 +
      break;
 +
  }
  
     
+
  // Warn users who accidentally try to edit a templated section
     // *** HELPER FUNCTIONS ***
+
  if ((window.location + '').includes('title=Template:Namedmobpage&action=edit')) {
    // Define two helper functions for building new zone definitions
+
     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.');
 +
  }
  
    // 1) Use this function to find the correct 0,0 point
+
  // Warn uses who try to edit a mostly-transcluded section (and offer to take them to the transcluded page)
    window.testZero = window.testZeroZero = function(zoneData) {
+
  var text = $('#wpTextbox1').val();
      removeOpenMap(); // test functions don't clean up properly
+
  var match = text && text.match(/\{\{\#lsth\:(.*?)\|\[\[(.*?)\]\]\}\}/);
      var locs = [{ x: 0, y: 0 }];
+
  if (match) {
      showMapWithLocs(zoneData, locs);
+
    var pageName = match[1];
    };
+
    var section = match[2];
 +
    var isEdit = (window.location + '').includes('action=edit');
 +
    var isUnderTenLines = $('#wpTextbox1').val().split('\n').length < 10;
 +
    var url = '/' + pageName + '#' + section;
 +
    var message = 'This page uses "transclusion" to show part of another page, specifically the code:\n\n    {{#lsth:' + pageName +
 +
                  '|[[' + section + ']]}}\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;
 +
  }
  
 +
  // Add links to search the forum for items to their auction tracker
 +
  const forumSearchUrl = 'https://www.project1999.com/forums/search.php?do=process&forumchoice[]=27&query=%22' +
 +
                        $('#firstHeading').text() + '%22';
 +
  $('.auctrackerbox span span:contains("Project 1999 Auction Tracker")')
 +
    .append(
 +
      '<a ' +
 +
        'style="font-size: 0.5em; float: right" '+
 +
        'target="_new" ' +
 +
        'href="' + forumSearchUrl + '"' +
 +
      '>Search Forum</a>');
  
    // 2) Use this function to generate a grid of alignment of X's
+
});
    window.testGrid = function(zoneData) {
+
      removeOpenMap(); // test functions don't clean up properly
+
      var $locTd = $('b:contains("Location:")').parent();
+
      var locs = [];
+
      for (var x = zoneData.maxX; x >= zoneData.minX; x -= zoneData.interval) {
+
        for (var y = zoneData.maxY; y >= zoneData.minY; y -= zoneData.interval) {
+
          locs.push({ x: x, y: y });
+
        }
+
      };
+
      showMapWithLocs(zoneData, locs);
+
    };
+
 
+
  })();
+
  } catch (err) {
+
    console.error(err); // If anything goes wrong, move on but log the error
+
  }
+

Revision as of 16:02, 14 September 2019

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

importScript('MediaWiki:Polyfills.js');
importScript('MediaWiki:Zones.js');

/* p1999wiki.js
 * written by http://wiki.project1999.com/User:Ravhin
 * last update: 7 January, 2018 by Loramin
 */
$(function() {
  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 = 'http://wiki.project1999.com/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) {
        // Primary/secondary slots have no tint
        if (show.primaryFashion || show.secondaryFashion) sameTint.push(show);
        else (show.tint === tint ? sameTint : otherTint).push(show);
      });
      return { matches: sameTint, otherTint: otherTint, fashion: fashion, tint: tint };
    });
  }
  var addFashionSection = function() {
    getItemPageShows().then(function(pageShows) {
    if(!pageShows || !pageShows.matches) return;
     // Build fashion section
      var lis = pageShows.matches.map(function(show) {
        return '<li class="fashion-link" data-file="' + show.file +'">' +
                '<a href="#">' + show.gender + ' ' + show.race + '</a></li>';
      });
      var $ul = $('<ul>').append(lis);
      var $div = $('<div><h2>Fashion/Appearance</h2></div>').append($ul);
      // Not sure why we have two IDs: two different item templates?
      $('#Drop_looted_from, #Drops_From').parent().before($div);
    });
  };
  addFashionSection();
  
  
  //*****************************************
  // 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>');

    // Handle when either 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));
      });
    });
  }


  // 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();
  });


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

  var fullTitle = $('title').text();
  var title = fullTitle.substr(0, fullTitle.length - ' - Project 1999 Wiki'.length);
  switch(title) {
    // 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 '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;
  }

  // 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 isEdit = (window.location + '').includes('action=edit');
    var isUnderTenLines = $('#wpTextbox1').val().split('\n').length < 10;
    var url = '/' + pageName + '#' + section;
    var message = 'This page uses "transclusion" to show part of another page, specifically the code:\n\n    {{#lsth:' + pageName +
                  '|[[' + section + ']]}}\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;
  }

  // Add links to search the forum for items to their auction tracker
  const forumSearchUrl = 'https://www.project1999.com/forums/search.php?do=process&forumchoice[]=27&query=%22' + 
                         $('#firstHeading').text() + '%22';
  $('.auctrackerbox span span:contains("Project 1999 Auction Tracker")')
    .append(
      '<a ' +
        'style="font-size: 0.5em; float: right" '+
        'target="_new" ' +
        'href="' + forumSearchUrl + '"' +
      '>Search Forum</a>');

});