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

From Project 1999 Wiki
Jump to: navigation, search
Line 1: Line 1:
 
// Basic DOM manipulation onReady (since the wiki won't let us add <input> tags in the wiki text
 
// Basic DOM manipulation onReady (since the wiki won't let us add <input> tags in the wiki text
  
var buildEraCheckbox = function(era) {
+
var buildEraCheckbox = function (era) {
   return '<label><input checked class="eraFilter" type="checkbox" value="' + era.substr(0,3).toLowerCase() + '"/>' + era + '</label>';
+
   return (
}
+
    '<label><input checked class="eraFilter" type="checkbox" value="' +
 +
    era.substr(0, 3).toLowerCase() +
 +
    '"/>' +
 +
    era +
 +
    '</label>'
 +
  );
 +
};
 
var eras = ['Classic', 'Kunark', 'Velious'];
 
var eras = ['Classic', 'Kunark', 'Velious'];
  
 
$('#eraFilterPlaceholder').replaceWith(eras.map(buildEraCheckbox).join(''));
 
$('#eraFilterPlaceholder').replaceWith(eras.map(buildEraCheckbox).join(''));
$('#levelFilterPlaceholder').replaceWith('<input id="levelFilter" style="width:30px"/>');
+
$('#levelFilterPlaceholder').replaceWith('<input id="level-filter" style="width:30px"/>');
$('#zoneFilterPlaceholder').replaceWith('<input id="zoneFilter" style="width:50em"/>');
+
$('#zoneFilterPlaceholder').replaceWith('<input id="zone-filter" style="width:50em"/>');
  
 
// Create filter results table
 
// Create filter results table
Line 14: Line 20:
  
 
var $headerRow = $('.hunting-guide-row:first').clone();
 
var $headerRow = $('.hunting-guide-row:first').clone();
var clearFilterResults = function() {
+
var clearFilterResults = function () {
   return $filterResults.empty()
+
   return $filterResults.empty().append($headerRow);
                      .append($headerRow);
+
 
};
 
};
 
clearFilterResults();
 
clearFilterResults();
  
 
$('h4:contains("Hunting Spots 1-4")').before($filterResults);
 
$('h4:contains("Hunting Spots 1-4")').before($filterResults);
 +
 +
// Add radio buttons (since MediaWiki won't allow them as content)
 +
 +
$('.filter-link').each(function (i, el) {
 +
  var $el = $(el);
 +
  var eqClass = $el.text();
 +
  var htmlClass = $el.attr('class');
 +
  var name = htmlClass.split('-', 1)[0];
 +
  var inputHtml = '<input type="radio" name="' + name + '" ' + (i ? '' : ' checked') + '/>';
 +
  $el.replaceWith('<label class="' + htmlClass + '">' + inputHtml + eqClass + '</label>');
 +
});
  
 
// Get jQuery object of every row in the hunting guide (because all of the filter functions need it)
 
// Get jQuery object of every row in the hunting guide (because all of the filter functions need it)
Line 49: Line 65:
 
   spots.push({
 
   spots.push({
 
     soloLevel: $tr.children('td:eq(0)').text(),
 
     soloLevel: $tr.children('td:eq(0)').text(),
     groupLevel,
+
     groupLevel: groupLevel,
 
     zone: $tr.children('td:eq(2)').text(),
 
     zone: $tr.children('td:eq(2)').text(),
 
     area: $tr.children('td:eq(3)').text(),
 
     area: $tr.children('td:eq(3)').text(),
Line 57: Line 73:
 
     image: $tr.children('td:eq(7)').text(),
 
     image: $tr.children('td:eq(7)').text(),
 
     notes: $tr.children('td:eq(8)').text(),
 
     notes: $tr.children('td:eq(8)').text(),
     $tr:$tr
+
    for: $tr.find('.for').text(),
 +
     $tr: $tr,
 
   });
 
   });
 
});
 
});
Line 69: Line 86:
 
$('#numSpots').text(cleanedSpots.length);
 
$('#numSpots').text(cleanedSpots.length);
  
var filterSpots = function(filterLabel, filter, filterData) {
+
var toClassAbbreviation = function (eqClass) {
   var showUnfiltered = !filter;
+
   return {
 
+
    bard: 'BRD',
  $('h4, .wikitable').toggle(showUnfiltered);
+
    cleric: 'CLR',
  $filterResults.toggle(!showUnfiltered);
+
     druid: 'DRU',
 
+
    enchanter: 'ENC',
  if (showUnfiltered) {
+
     magician: 'MAG',
     $('#levelFilter').val('');
+
     monk: 'MNK',
  } else {
+
     necromancer: 'NEC',
     var filteredSpots = cleanedSpots.filter(function(spot) { return filter(spot, filterData); });
+
     paladin: 'PAL',
     var rows = filteredSpots.map(function(spot) { return spot.$tr; });
+
     ranger: 'RNG',
     var $resultsTbody = clearFilterResults();
+
     'shadow knight': 'SHD',
     $.each(rows, function(i, row) {
+
    shaman: 'SHM',
      var $rowClone = $(row).clone();
+
    wizard: 'WIZ',
      $resultsTbody.append($rowClone);
+
   }[eqClasss.toLowerCase()];
     });
+
     if (!filteredSpots.length) $resultsTbody.append('<tr><th colspan="50" style="color:red; font-weight: bold; font-size: 2em; padding: 1em;">No results for those filters</th></tr>');
+
   }
+
  selectFilter(filterLabel);
+
 
};
 
};
  
var filterByClass = function(spot, eqClass) {
+
var getFilteredSpots = function (
   var lowerCaseNotes = spot.notes.toLowerCase();
+
  selectedClass,
  var goodForPattern = new RegExp('good for [^\\.;]*?' + eqClass.toLowerCase());
+
  selectedMonster,
  var greatForPattern = new RegExp('great for [^\\.;]*?' + eqClass.toLowerCase());
+
  selectedLevel,
  var isGood = lowerCaseNotes.match(goodForPattern);
+
  selectedZone,
  var isGreat = lowerCaseNotes.match(greatForPattern);
+
  selectedEras
  return isGood || isGreat;
+
) {
};
+
   return cleanedSpots.filter(function (spot) {
 +
    var classMatches = !selectedClass || spot.for.includes(toClassAbbreviation(selectedClass));
 +
    var monsterMatches =
 +
      !selectedMonster ||
 +
      spot.monsters.includes(selectedMonster) ||
 +
      spot.notes.includes(selectedMonster);
  
var filterByLevel = function(spot, level) {
+
    var levelRangeText = spot.$tr.children('td:first').text();
  var levelRangeText = spot.$tr.children('td:first').text();
+
    var minLevel = parseFloat(levelRangeText.split('-')[0]);
  var splitLevelRanges = levelRangeText.split('-');
+
    var maxLevel = parseFloat(levelRangeText.split('-')[1] || splitLevelRanges[0]);
  var minLevel = parseInt(splitLevelRanges[0], 10);
+
    var levelMatches = minLevel <= level && maxLevel >= level;
  var maxLevel = parseInt(splitLevelRanges[1] || splitLevelRanges[0], 10);
+
  return minLevel <= level && maxLevel >= level;
+
};
+
  
var filterByMonster = function(spot, monsterType) {
+
    var lowerCaseZone = spot.zone.toLowerCase();
  var monsterPattern = new RegExp(monsterType);
+
    var zonePattern = new RegExp(zone.toLowerCase());
  var lowerCaseMonsters = spot.monsters.toLowerCase();
+
    var zoneMatches = lowerCaseZone.match(zonePattern);
  var lowerCaseNotes = spot.notes.toLowerCase();
+
  return lowerCaseMonsters.match(monsterPattern) ||
+
        lowerCaseNotes.match(monsterPattern);
+
};
+
  
var filterByZone = function(spot, zone) {
+
    var lowerCaseEra = spot.era.toLowerCase().trim();
  var lowerCaseZone = spot.zone.toLowerCase();
+
    var eraMatches = eras.includes(lowerCaseEra);
  var zonePattern = new RegExp(zone.toLowerCase());
+
  return lowerCaseZone.match(zonePattern);
+
};
+
  
var filterByEra = function(spot, eras) {
+
    return classMatches && monsterMatches && levelMatches && zoneMatches && eraMatches;
  var lowerCaseEra = spot.era.toLowerCase().trim();
+
   });
   return eras.includes(lowerCaseEra);
+
 
};
 
};
 
  
 
/**
 
/**
  * Filters out (hides) rows which aren't specifically "good for" the clicked class; also handles
+
  * Replaces the contents of the search results UI with the rows of the provided hunting spots
* the "no filter" case (TODO: Make a separate handler/event binding for it)
+
 
  */
 
  */
var classFilterHandler = function(e) {
+
var setResults = function (spots) {
   var eqClass = $(e.target).text();
+
  // Clear the results (but add back the header row), then append (clones of) the result rows
   if (eqClass.trim().toLowerCase() === "no filter") filterSpots(e.target, null);
+
   var $resultsTbody = clearFilterResults();
   else filterSpots(e.target, filterByClass, eqClass);
+
   spots.each(function (spot) {
}
+
    $resultsTbody.append(spot.$tr.clone());
 +
   });
  
 +
  if (spots.length) return; // Show "no matches" UI only if there are no matches
 +
  $resultsTbody.append(
 +
    '<tr><th colspan="50" style="color:red; font-weight: bold; font-size: 2em; padding: 1em;">' +
 +
      'No results for those filters</th></tr>'
 +
  );
 +
};
 
/**
 
/**
  * Filter out (hides) rows which aren't for the level the user entered
+
  * Re-filter the page when one of the filtering UI elements changes
 
  */
 
  */
var levelFilterHandler = function () {
+
var handleAnyFilteringElementChange = function (e) {
   var level = parseInt($('#levelFilter').val(), 10);
+
   var noFilterWasClicked = $(e.target).is('.no-filter');
   filterSpots('#levelFilterLabel', level && filterByLevel, level);
+
  if (noFilterWasClicked) {
 +
    // Clear the other radio buttons (the browser won't do it since they have different names)
 +
    $('.filter-link :radio').attr('checked', false);
 +
    // Clear the text inputs
 +
    $('#level-filter').val('');
 +
    $('#zone-filter').val('');
 +
    // Re-check all three era checkboxes
 +
    $('.era-filter').attr('checked', true);
 +
   } else {
 +
    $('.no-filter').attr('checked', false);
 +
  }
 +
 
 +
  var selectedClass = $('.class-filter :checked').parent().text();
 +
  var selectedMonster = $('.monster-filter :checked').parent().text();
 +
  var selectedLevel = $('#level-filter').val();
 +
  var selectedZone = $('#zone-filter').val();
 +
  var selectedEras = $('.eraFilter:checked').map(function (i, el) {
 +
    return $(el).val();
 +
  });
 +
 
 +
  // Are we actually filtering out anything?
 +
  var willFilter =
 +
    selectedClass || selectedMonster || selectedLevel || selectedZone || selectedEras.length < 3;
 +
 
 +
  // If so, hide the normal UI and show results (otherwise show normal UI)
 +
  $('h4, .wikitable').toggle(!willFilter);
 +
  $filterResults.toggle(willFilter);
 +
 
 +
  var filteredSpots = getFilteredSpots(
 +
    selectedClass,
 +
    selectedMonster,
 +
    selectedLevel,
 +
    selectedZone,
 +
    selectedEras
 +
  );
 +
  setResults(filteredSpots);
 
};
 
};
 +
// /**
 +
//  * When there is no filter specified, restore the page to normal; when there
 +
//  * is one, hide headers and the normal tables, so that we can instead show
 +
//  * a filtered results table.  The results in that table are generated using the
 +
//  * provided filter function/data
 +
//  */
 +
// var filterSpots = function (
 +
//  filterLabel,
 +
//  filter, // eg. filterByClass
 +
//  filterData // eg. the class
 +
// ) {
 +
//  var showUnfiltered = !filter;
  
/**
+
//  $('h4, .wikitable').toggle(showUnfiltered);
* Filters out (hides) rows which don't involve the specified monster type
+
//   $filterResults.toggle(!showUnfiltered);
*/
+
var monsterFilterHandler = function(e) {
+
   var monsterType = $(e.target).text().toLowerCase();
+
   filterSpots(e.target, filterByMonster, monsterType);
+
}
+
  
/**
+
//  if (showUnfiltered) {
* Filters out (hides) rows which don't involve the specified zone
+
//    $('#levelFilter').val('');
*/
+
//  } else {
var zoneFilterHandler = function(e) {
+
//    var filteredSpots = cleanedSpots.filter(function (spot) {
  var zone = $('#zoneFilter').val().toLowerCase();
+
//      return filter(spot, filterData);
  filterSpots('#zoneFilterLabel', filterByZone, zone);
+
//    });
}
+
//    var rows = filteredSpots.map(function (spot) {
 +
//      return spot.$tr;
 +
//    });
 +
//    var $resultsTbody = clearFilterResults();
 +
//    $.each(rows, function (i, row) {
 +
//      var $rowClone = $(row).clone();
 +
//      $resultsTbody.append($rowClone);
 +
//    });
 +
//    if (!filteredSpots.length)
 +
//      $resultsTbody.append(
 +
//        '<tr><th colspan="50" style="color:red; font-weight: bold; font-size: 2em; padding: 1em;">No results for those filters</th></tr>'
 +
//      );
 +
//  }
 +
//  selectFilter(filterLabel);
 +
// };
  
/**
+
// var filterByClass = function (spot, eqClass) {
* Filters out (hides) rows which don't involve the specified era
+
/var lowerCaseNotes = spot.notes.toLowerCase();
*/
+
//   var goodForPattern = new RegExp('good for [^\\.;]*?' + eqClass.toLowerCase());
var eraFilterHandler = function(e) {
+
//  var greatForPattern = new RegExp('great for [^\\.;]*?' + eqClass.toLowerCase());
   var eras = $('.eraFilter:checked').toArray().map(function(eraCheckbox) {
+
//   var isGood = lowerCaseNotes.match(goodForPattern);
    return $(eraCheckbox).val();
+
//   var isGreat = lowerCaseNotes.match(greatForPattern);
   });
+
//  return isGood || isGreat;
   filterSpots('#eraFilterLabel', filterByEra, eras);
+
// };
}
+
  
 +
// var filterByLevel = function (spot, level) {
 +
//  var levelRangeText = spot.$tr.children('td:first').text();
 +
//  var splitLevelRanges = levelRangeText.split('-');
 +
//  var minLevel = parseInt(splitLevelRanges[0], 10);
 +
//  var maxLevel = parseInt(splitLevelRanges[1] || splitLevelRanges[0], 10);
 +
//  return minLevel <= level && maxLevel >= level;
 +
// };
  
/**
+
// var filterByMonster = function (spot, monsterType) {
* Hides the table headers and level range headings for level ranges that have been completely
+
//  var monsterPattern = new RegExp(monsterType);
* filtered out
+
//  var lowerCaseMonsters = spot.monsters.toLowerCase();
*/
+
//  var lowerCaseNotes = spot.notes.toLowerCase();
var hideHeaders = function () {
+
//  return lowerCaseMonsters.match(monsterPattern) || lowerCaseNotes.match(monsterPattern);
  // If anything got hidden by previous filters, show it (because this filter might not hide it)
+
// };
   $('.wikitable, h4').show();
+
  // Hide the tables
+
   $('.wikitable').each(function(i, table) {
+
    var $table = $(table);
+
    if (!$table.find('tbody').children('tr:visible').length) $table.hide();
+
  });
+
  // Hide the headings
+
   $('h4').hide();
+
  // Hide level range headers
+
   $('h4:first').next('.wikitable').show().find('tr:first').show();
+
}
+
  
/**
+
// var filterByZone = function (spot, zone) {
  * Visually marks the currently-used filter with an underline
+
//  var lowerCaseZone = spot.zone.toLowerCase();
  */
+
//  var zonePattern = new RegExp(zone.toLowerCase());
var selectFilter = function (target) {
+
//  return lowerCaseZone.match(zonePattern);
   $('.filterLink, .monsterFilterLink, #eraFilterLabel, #levelFilterLabel, #zoneFilterLabel').css({textDecoration: ''});
+
// };
   $(target).css({textDecoration: 'underline'});
+
 
}
+
// var filterByEra = function (spot, eras) {
 +
//  var lowerCaseEra = spot.era.toLowerCase().trim();
 +
//  return eras.includes(lowerCaseEra);
 +
// };
 +
 
 +
// /**
 +
//  * Filters out (hides) rows which aren't specifically "good for" the clicked class; also handles
 +
//  * the "no filter" case (TODO: Make a separate handler/event binding for it)
 +
//  */
 +
// var classFilterHandler = function (e) {
 +
//  var eqClass = $(e.target).text();
 +
//  if (eqClass.trim().toLowerCase() === 'no filter') filterSpots(e.target, null);
 +
//  else filterSpots(e.target, filterByClass, eqClass);
 +
// };
 +
 
 +
// /**
 +
//  * Filter out (hides) rows which aren't for the level the user entered
 +
//  */
 +
// var levelFilterHandler = function () {
 +
//  var level = parseInt($('#level-filter').val(), 10);
 +
//  filterSpots('#levelFilterLabel', level && filterByLevel, level);
 +
// };
 +
 
 +
// /**
 +
//  * Filters out (hides) rows which don't involve the specified monster type
 +
//  */
 +
// var monsterFilterHandler = function (e) {
 +
//  var monsterType = $(e.target).text().toLowerCase();
 +
//  filterSpots(e.target, filterByMonster, monsterType);
 +
// };
 +
 
 +
// /**
 +
//  * Filters out (hides) rows which don't involve the specified zone
 +
//  */
 +
// var zoneFilterHandler = function (e) {
 +
//  var zone = $('#zone-filter').val().toLowerCase();
 +
//  filterSpots('#zoneFilterLabel', filterByZone, zone);
 +
// };
 +
 
 +
// /**
 +
//  * Filters out (hides) rows which don't involve the specified era
 +
//  */
 +
// var eraFilterHandler = function (e) {
 +
//  var eras = $('.era-filter:checked')
 +
//    .toArray()
 +
//    .map(function (eraCheckbox) {
 +
//      return $(eraCheckbox).val();
 +
//    });
 +
//  filterSpots('#eraFilterLabel', filterByEra, eras);
 +
// };
 +
 
 +
// /**
 +
//  * Hides the table headers and level range headings for level ranges that have been completely
 +
//  * filtered out
 +
//  */
 +
// var hideHeaders = function () {
 +
//  // If anything got hidden by previous filters, show it (because this filter might not hide it)
 +
//  $('.wikitable, h4').show();
 +
//  // Hide the tables
 +
//  $('.wikitable').each(function (i, table) {
 +
//    var $table = $(table);
 +
//    if (!$table.find('tbody').children('tr:visible').length) $table.hide();
 +
//  });
 +
//  // Hide the headings
 +
//  $('h4').hide();
 +
//  // Hide level range headers
 +
//  $('h4:first').next('.wikitable').show().find('tr:first').show();
 +
// };
 +
 
 +
// /**
 +
// * Visually marks the currently-used filter with an underline
 +
// */
 +
// var selectFilter = function (target) {
 +
//   $('.filterLink, .monsterFilterLink, #eraFilterLabel, #levelFilterLabel, #zoneFilterLabel').css({
 +
//    textDecoration: '',
 +
//  });
 +
//   $(target).css({ textDecoration: 'underline' });
 +
// };
  
 
// Bind event handlers
 
// Bind event handlers
$('.filterLink').click(classFilterHandler);
+
// $('.class-filter-link').click(classFilterHandler);
$('.monsterFilterLink').click(monsterFilterHandler);
+
// $('.monster-filter-link').click(monsterFilterHandler);
$('#levelFilter').change(levelFilterHandler);
+
// $('#level-filter').change(levelFilterHandler);
$('#zoneFilter').change(zoneFilterHandler);
+
// $('#zone-filter').change(zoneFilterHandler);
$('.eraFilter').change(eraFilterHandler);
+
// $('.era-filter').change(eraFilterHandler);
 +
$('.class-filter-link, .monster-filter-link').click(handleAnyFilteringElementChange);
 +
$('#level-filter, #zone-filter').keyDown(handleAnyFilteringElementChange);
 +
$('.era-filter').change(handleAnyFilteringElementChange);

Revision as of 19:17, 4 June 2020

// Basic DOM manipulation onReady (since the wiki won't let us add <input> tags in the wiki text

var buildEraCheckbox = function (era) {
  return (
    '<label><input checked class="eraFilter" type="checkbox" value="' +
    era.substr(0, 3).toLowerCase() +
    '"/>' +
    era +
    '</label>'
  );
};
var eras = ['Classic', 'Kunark', 'Velious'];

$('#eraFilterPlaceholder').replaceWith(eras.map(buildEraCheckbox).join(''));
$('#levelFilterPlaceholder').replaceWith('<input id="level-filter" style="width:30px"/>');
$('#zoneFilterPlaceholder').replaceWith('<input id="zone-filter" style="width:50em"/>');

// Create filter results table
var $filterResults = $('table.eoTable3.wikitable.sortable').eq(0).clone().hide();

var $headerRow = $('.hunting-guide-row:first').clone();
var clearFilterResults = function () {
  return $filterResults.empty().append($headerRow);
};
clearFilterResults();

$('h4:contains("Hunting Spots 1-4")').before($filterResults);

// Add radio buttons (since MediaWiki won't allow them as content)

$('.filter-link').each(function (i, el) {
  var $el = $(el);
  var eqClass = $el.text();
  var htmlClass = $el.attr('class');
  var name = htmlClass.split('-', 1)[0];
  var inputHtml = '<input type="radio" name="' + name + '" ' + (i ? '' : ' checked') + '/>';
  $el.replaceWith('<label class="' + htmlClass + '">' + inputHtml + eqClass + '</label>');
});

// Get jQuery object of every row in the hunting guide (because all of the filter functions need it)
var $rows = $('.wikitable tbody tr');

/**
 * Build a in-memory data object with the data parsed from each row (which will tell us which
 * levels/classes should be shown for any filter).
 * @example a "spot" created by this function for critters in Butcherblock Mountains: {
 *   soloLevel: '04-06',
 *   groupLevel: '03-05',
 *   zone: 'Butcherblock Mountains',
 *   area: 'Greater Faydark zoneline',
 *   monsters: 'Assorted Critters	',
 *   xpMod: '100%',
 *   notes: '',
 *   $tr: *a jQuery object pointing to the <tr> that all this data originally came from*
 * }
 */
var spots = [];
$rows.each(function (i, el) {
  var groupLevel = $(el).children('td:eq(1)').text();
  groupLevel = groupLevel.trim() === '-' ? null : groupLevel;
  var $tr = $(el);

  if ($tr.children('th').length) return; // Header row

  spots.push({
    soloLevel: $tr.children('td:eq(0)').text(),
    groupLevel: groupLevel,
    zone: $tr.children('td:eq(2)').text(),
    area: $tr.children('td:eq(3)').text(),
    monsters: $tr.children('td:eq(4)').text(),
    xpMod: $tr.children('td:eq(5)').text(),
    era: $tr.children('td:eq(6)').text(),
    image: $tr.children('td:eq(7)').text(),
    notes: $tr.children('td:eq(8)').text(),
    for: $tr.find('.for').text(),
    $tr: $tr,
  });
});

// Clear garbage entries (wish I had ES6 filter ..)
var cleanedSpots = [];
$.each(spots, function (i, spot) {
  if (spot.soloLevel && spot.zone) cleanedSpots.push(spot);
});

$('#numSpots').text(cleanedSpots.length);

var toClassAbbreviation = function (eqClass) {
  return {
    bard: 'BRD',
    cleric: 'CLR',
    druid: 'DRU',
    enchanter: 'ENC',
    magician: 'MAG',
    monk: 'MNK',
    necromancer: 'NEC',
    paladin: 'PAL',
    ranger: 'RNG',
    'shadow knight': 'SHD',
    shaman: 'SHM',
    wizard: 'WIZ',
  }[eqClasss.toLowerCase()];
};

var getFilteredSpots = function (
  selectedClass,
  selectedMonster,
  selectedLevel,
  selectedZone,
  selectedEras
) {
  return cleanedSpots.filter(function (spot) {
    var classMatches = !selectedClass || spot.for.includes(toClassAbbreviation(selectedClass));
    var monsterMatches =
      !selectedMonster ||
      spot.monsters.includes(selectedMonster) ||
      spot.notes.includes(selectedMonster);

    var levelRangeText = spot.$tr.children('td:first').text();
    var minLevel = parseFloat(levelRangeText.split('-')[0]);
    var maxLevel = parseFloat(levelRangeText.split('-')[1] || splitLevelRanges[0]);
    var levelMatches = minLevel <= level && maxLevel >= level;

    var lowerCaseZone = spot.zone.toLowerCase();
    var zonePattern = new RegExp(zone.toLowerCase());
    var zoneMatches = lowerCaseZone.match(zonePattern);

    var lowerCaseEra = spot.era.toLowerCase().trim();
    var eraMatches = eras.includes(lowerCaseEra);

    return classMatches && monsterMatches && levelMatches && zoneMatches && eraMatches;
  });
};

/**
 * Replaces the contents of the search results UI with the rows of the provided hunting spots
 */
var setResults = function (spots) {
  // Clear the results (but add back the header row), then append (clones of) the result rows
  var $resultsTbody = clearFilterResults();
  spots.each(function (spot) {
    $resultsTbody.append(spot.$tr.clone());
  });

  if (spots.length) return; // Show "no matches" UI only if there are no matches
  $resultsTbody.append(
    '<tr><th colspan="50" style="color:red; font-weight: bold; font-size: 2em; padding: 1em;">' +
      'No results for those filters</th></tr>'
  );
};
/**
 * Re-filter the page when one of the filtering UI elements changes
 */
var handleAnyFilteringElementChange = function (e) {
  var noFilterWasClicked = $(e.target).is('.no-filter');
  if (noFilterWasClicked) {
    // Clear the other radio buttons (the browser won't do it since they have different names)
    $('.filter-link :radio').attr('checked', false);
    // Clear the text inputs
    $('#level-filter').val('');
    $('#zone-filter').val('');
    // Re-check all three era checkboxes
    $('.era-filter').attr('checked', true);
  } else {
    $('.no-filter').attr('checked', false);
  }

  var selectedClass = $('.class-filter :checked').parent().text();
  var selectedMonster = $('.monster-filter :checked').parent().text();
  var selectedLevel = $('#level-filter').val();
  var selectedZone = $('#zone-filter').val();
  var selectedEras = $('.eraFilter:checked').map(function (i, el) {
    return $(el).val();
  });

  // Are we actually filtering out anything?
  var willFilter =
    selectedClass || selectedMonster || selectedLevel || selectedZone || selectedEras.length < 3;

  // If so, hide the normal UI and show results (otherwise show normal UI)
  $('h4, .wikitable').toggle(!willFilter);
  $filterResults.toggle(willFilter);

  var filteredSpots = getFilteredSpots(
    selectedClass,
    selectedMonster,
    selectedLevel,
    selectedZone,
    selectedEras
  );
  setResults(filteredSpots);
};
// /**
//  * When there is no filter specified, restore the page to normal; when there
//  * is one, hide headers and the normal tables, so that we can instead show
//  * a filtered results table.  The results in that table are generated using the
//  * provided filter function/data
//  */
// var filterSpots = function (
//   filterLabel,
//   filter, // eg. filterByClass
//   filterData // eg. the class
// ) {
//   var showUnfiltered = !filter;

//   $('h4, .wikitable').toggle(showUnfiltered);
//   $filterResults.toggle(!showUnfiltered);

//   if (showUnfiltered) {
//     $('#levelFilter').val('');
//   } else {
//     var filteredSpots = cleanedSpots.filter(function (spot) {
//       return filter(spot, filterData);
//     });
//     var rows = filteredSpots.map(function (spot) {
//       return spot.$tr;
//     });
//     var $resultsTbody = clearFilterResults();
//     $.each(rows, function (i, row) {
//       var $rowClone = $(row).clone();
//       $resultsTbody.append($rowClone);
//     });
//     if (!filteredSpots.length)
//       $resultsTbody.append(
//         '<tr><th colspan="50" style="color:red; font-weight: bold; font-size: 2em; padding: 1em;">No results for those filters</th></tr>'
//       );
//   }
//   selectFilter(filterLabel);
// };

// var filterByClass = function (spot, eqClass) {
//   var lowerCaseNotes = spot.notes.toLowerCase();
//   var goodForPattern = new RegExp('good for [^\\.;]*?' + eqClass.toLowerCase());
//   var greatForPattern = new RegExp('great for [^\\.;]*?' + eqClass.toLowerCase());
//   var isGood = lowerCaseNotes.match(goodForPattern);
//   var isGreat = lowerCaseNotes.match(greatForPattern);
//   return isGood || isGreat;
// };

// var filterByLevel = function (spot, level) {
//   var levelRangeText = spot.$tr.children('td:first').text();
//   var splitLevelRanges = levelRangeText.split('-');
//   var minLevel = parseInt(splitLevelRanges[0], 10);
//   var maxLevel = parseInt(splitLevelRanges[1] || splitLevelRanges[0], 10);
//   return minLevel <= level && maxLevel >= level;
// };

// var filterByMonster = function (spot, monsterType) {
//   var monsterPattern = new RegExp(monsterType);
//   var lowerCaseMonsters = spot.monsters.toLowerCase();
//   var lowerCaseNotes = spot.notes.toLowerCase();
//   return lowerCaseMonsters.match(monsterPattern) || lowerCaseNotes.match(monsterPattern);
// };

// var filterByZone = function (spot, zone) {
//   var lowerCaseZone = spot.zone.toLowerCase();
//   var zonePattern = new RegExp(zone.toLowerCase());
//   return lowerCaseZone.match(zonePattern);
// };

// var filterByEra = function (spot, eras) {
//   var lowerCaseEra = spot.era.toLowerCase().trim();
//   return eras.includes(lowerCaseEra);
// };

// /**
//  * Filters out (hides) rows which aren't specifically "good for" the clicked class; also handles
//  * the "no filter" case (TODO: Make a separate handler/event binding for it)
//  */
// var classFilterHandler = function (e) {
//   var eqClass = $(e.target).text();
//   if (eqClass.trim().toLowerCase() === 'no filter') filterSpots(e.target, null);
//   else filterSpots(e.target, filterByClass, eqClass);
// };

// /**
//  * Filter out (hides) rows which aren't for the level the user entered
//  */
// var levelFilterHandler = function () {
//   var level = parseInt($('#level-filter').val(), 10);
//   filterSpots('#levelFilterLabel', level && filterByLevel, level);
// };

// /**
//  * Filters out (hides) rows which don't involve the specified monster type
//  */
// var monsterFilterHandler = function (e) {
//   var monsterType = $(e.target).text().toLowerCase();
//   filterSpots(e.target, filterByMonster, monsterType);
// };

// /**
//  * Filters out (hides) rows which don't involve the specified zone
//  */
// var zoneFilterHandler = function (e) {
//   var zone = $('#zone-filter').val().toLowerCase();
//   filterSpots('#zoneFilterLabel', filterByZone, zone);
// };

// /**
//  * Filters out (hides) rows which don't involve the specified era
//  */
// var eraFilterHandler = function (e) {
//   var eras = $('.era-filter:checked')
//     .toArray()
//     .map(function (eraCheckbox) {
//       return $(eraCheckbox).val();
//     });
//   filterSpots('#eraFilterLabel', filterByEra, eras);
// };

// /**
//  * Hides the table headers and level range headings for level ranges that have been completely
//  * filtered out
//  */
// var hideHeaders = function () {
//   // If anything got hidden by previous filters, show it (because this filter might not hide it)
//   $('.wikitable, h4').show();
//   // Hide the tables
//   $('.wikitable').each(function (i, table) {
//     var $table = $(table);
//     if (!$table.find('tbody').children('tr:visible').length) $table.hide();
//   });
//   // Hide the headings
//   $('h4').hide();
//   // Hide level range headers
//   $('h4:first').next('.wikitable').show().find('tr:first').show();
// };

// /**
//  * Visually marks the currently-used filter with an underline
//  */
// var selectFilter = function (target) {
//   $('.filterLink, .monsterFilterLink, #eraFilterLabel, #levelFilterLabel, #zoneFilterLabel').css({
//     textDecoration: '',
//   });
//   $(target).css({ textDecoration: 'underline' });
// };

// Bind event handlers
// $('.class-filter-link').click(classFilterHandler);
// $('.monster-filter-link').click(monsterFilterHandler);
// $('#level-filter').change(levelFilterHandler);
// $('#zone-filter').change(zoneFilterHandler);
// $('.era-filter').change(eraFilterHandler);
$('.class-filter-link, .monster-filter-link').click(handleAnyFilteringElementChange);
$('#level-filter, #zone-filter').keyDown(handleAnyFilteringElementChange);
$('.era-filter').change(handleAnyFilteringElementChange);