MediaWiki:ItemCategorySearch.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* For any questions or comments on this code contact Loramin via the forum Private Message system.
*
* NOTE: This code would be shorter/simpler/easier to read if it was in ES6 instead of ES5, but
* since I can't guarantee that every user will have ES6 support (and since I didn't want to
* bother with Babel for something so small) I chose to use the (old/ugly) ES5 syntax.
*/
// URL on the wiki where we can get all of the categories
var categoriesUrl =
'http://wiki.project1999.com/index.php?title=Special:Categories&offset=&limit=500';
/**
* Fetches all of the available categories in the wiki
*/
var getCategories = function() {
return $.get(categoriesUrl)
.then(function(html) {
var categories = $(html).find('.mw-spcontent li a').map(function(i, link) {
return $(link).text();
});
return $.makeArray(categories);
})
};
/**
* Function for retrieving all item names from a single page (which one is determined by "fromItem"
* for a provided category
* @param {string} category - an item category, with or without "Category:" (eg. "Fashion: Leather")
* @param fromItem - the item to start the pagination from (eg. "Impskin Gloves" would skip all
* items from before "Impskin Gloves" and start the page on "Impskin Gloves".
* @returns {Promise} - when this promise is resolved it will return an object with two
* properties:
* - items - the item names from the page
* - nextItem - the "fromItem" to get the next page (if any)
*/
var getItemsInCategoryPage = function(category, fromItem) {
// Add "Category:" (unless the provided category already starts with it)
if (!category.startsWith('Category:')) category = 'Category:' + category;
// Build the URL
var url = 'http://wiki.project1999.com/index.php?title=' + encodeURI(category);
// Add a "frompage" parameter if we're trying to get any page other than the first
if (fromItem) url += '&pagefrom=' + encodeURI(fromItem);
// Go get the page
return $.get(url)
.then(function(html) {
// jQuery-ify the returned HTML
var $html = $(html);
// Extract all of the <a> tags that look like: <li><a>Some Item</a></li>
var $links = $html.find('li:not(#f-list li):not(.pBody li) a');
// Map (and return) the item titles from within all of the links
var itemNames = $links.map(function(i, link) {
// Extract title
return $(link).attr('title');
});
// jQuery overwrites the native "map" function with its own, which returns a jQuery object
// (even when there isn't a single jQuery object or HTML element in the return value)
// This fixes that.
itemNames = $.makeArray(itemNames);
// Grab the href of the first "next 200" link
var nextHref = $html.find('a:contains("next 200")').eq(0).attr('href');
// Extract the next item (if any)
var match = nextHref && nextHref.match(/pagefrom=(.*?)#/);
// Figure out if there is a nextItem (and if not return null instead of causing an error)
var nextItem = match ? match[1] : null;
// Return results
return { itemNames: itemNames, nextItem: nextItem };
});
};
var showNormalCursor = function() {
$('body').css({ cursor: 'default'});
}
var showWaitCursor = function() {
$('body').css({ cursor: 'wait'});
};
var getItemsInCategoryPages = function(category, nextItem, itemNames) {
// Change the cursor to let the user know they need to wait
showWaitCursor();
// Go get all the items (and the next item) for the next page (or first page on first iteration)
return getItemsInCategoryPage(category, nextItem)
.then(function(result) {
// Combine any itemNames from previous iterations with this one
itemNames = (itemNames || []).concat(result.itemNames);
if (result.nextItem) {
// If there is a next item, get the items from a page that starts with it as the "pagefrom"
return getItemsInCategoryPages(category, result.nextItem, itemNames);
} else {
// otherwise return what we have (and restore the cursor to normal)
showNormalCursor();
return itemNames;
}
});
};
/**
* Helper function for finding items in array "left" which are also present in array "right"
*/
var findIntersection = function(left, right) {
return left.filter(function(item) { // return all of the items in left ...
return right.includes(item); // ... which are also items in right
});
};
/**
* This is the main function which powers the entire page. It takes 2-3 categories (if 0-1 are
* provided it throws an error), looks up all items in those categories, and then returns only the
* items which are present in all of those categories.
*/
var findCategoryIntersections = function(category1, category2, category3) {
return Promise.all([
getItemsInCategoryPages(category1), // go get the items in category 1
getItemsInCategoryPages(category2) // and the ones in category 2
])
.then(function(results) {
// Figure out which items are in both categories
var intersectionOf1And2 = findIntersection(results[0], results[1]);
// If there's a third category ...
if (category3) {
// Go get its items and then return ...
return getItemsInCategoryPages(category3)
.then(function(category3Results) {
// ... the interesection between those items and the previous interesection of #1 and #2
return findIntersection(intersectionOf1And2 , category3Results);
});
}
// Otherwise just return the intersection of #1 and #2
return intersectionOf1And2;
});
};
// "onCLick"
var handleSearchClick = function(e) {
// Determine which categories the user picked
var category1 = $('#category1').val();
var category2 = $('#category2').val();
var category3 = $('#category3').val();
// Yell at them if they didn't pick at least two
if (!category1 || !category2) return alert('Please select at least two categories');
// Let them know to wait while go do the search
showWaitCursor();
findCategoryIntersections(category1, category2, category3)
.then(function(results) {
// Convert the results (['itemA', 'itemB', ...]) into <li><a>item</a></li> HTML
var resultsHtml = results.map(function(item) {
return '<li><a href="http://wiki.project1999.com/' + item + '">' + item + '</a>';
});
// Add the results HTML to the page and set the cursor back to normal
$('#results').html(resultsHtml.join(''));
showNormalCursor();
});
};
// "onLoad"
showWaitCursor(); // Start the page off with the loading cursor, since we need to fetch categories
getCategories()
.then(function(categories) {
// Convert the categories (['categoryA', 'categoryB', ...]) into <option>category</option> HTML
var categoryOptions = '<option></option>' + categories.map(function(category) {
return '<option value="' + category + '">' + category + '</option>';
});
// Fill all three category selectors with the category options
$('#category1Cell').html('<select id="category1">' + categoryOptions + '</select>');
$('#category2Cell').html('<select id="category2">' + categoryOptions + '</select>');
$('#category3Cell').html('<select id="category3">' + categoryOptions + '</select>');
// Add the search button and results UL to the page (the wiki won't let us do it without JS)
$('#controlsPlaceholder')
.html('<button id="searchCategories">Search</button>')
.after('<h2>Search Results</h2><ul id="results"></ul>')
// Bind the search button to its handler and set the cursor back to normal (the page is ready)
$('#searchCategories').click(handleSearchClick);
showNormalCursor();
});