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.
/*
allow a category navigation based on a list of categories (instead of the usual single category navigation)
*/
// Used files: [[File:CatNav delete.png]]
// note: the 'section#WikiaPageBackground' selector in the bottom is used for non-ucp wikis. may be removed in the future once all wikis have been transfered to the unified community platform
/* <nowiki> */
;(function(window, $, mw) {
'use strict';
var config = mw.config.get([
'wgTitle',
'wgNamespaceNumber',
'wgArticlePath',
'wgScriptPath',
'wgBlankImgUrl',
'wgUserName'
]);
// re-add wgBlankImgUrl since it is no longer available as a wg variable in Fandom
// from CSS-Tricks
// https://css-tricks.com/snippets/html/base64-encode-of-1x1px-transparent-gif/
config.wgBlankImgUrl = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
var msg;
var cndata = { // catnav data
details: {}, // details about pages
pageids: {}, // pageids by titles
current: [],
selectedPage: null,
storage: {
url: "//community.fandom.com", // url to the wiki that hosts your imports
scriptPath: "" // e.g. "/w" for wikipedia, empty string for fandom
},
markup: {
html: null,
css: null
},
_g: window.CatNav // global modifiers
};
var settings = {
rows: 3, // default number of rows
itemsInNavigator: Math.floor(($("#mw-content-text").width() - 100) / 146) // number of columns determined by the width of the user's screen
};
/* ================================== *\
# wiki links to [[Special:CatNav]]
\* ================================== */
$("#WikiaBarWrapper .tools").append('<li><a href="' + config.wgArticlePath.replace("$1", "Special:CatNav") + '">CatNav</a></li>');
/* ================================== *\
# console
\* ================================== */
var customConsole = {
logs: [],
debug: true // will be changed to 'false' after defining data, unless 'window.CatNav.debug' is also 'true'
};
customConsole.add = function(type, data) {
var debug = customConsole.debug;
if (["log", "error", "info", "warn"].indexOf(type) > -1) {
type = "log";
}
customConsole.logs.push([type, debug, data]);
if (debug) {
console[type]("[-] CatNav :: ", data);
}
};
customConsole.log = function(data) {
customConsole.add("log", data);
};
customConsole.error = function(data) {
customConsole.add("error", data);
};
customConsole.info = function(data) {
customConsole.add("info", data);
};
customConsole.warn = function(data) {
customConsole.add("warn", data);
};
// error message
function errorMsg(bool, msg) {
customConsole.error(["'errorMsg' call ::", {bool: bool, msg: msg}]);
// if 'bool' show message, otherwise hide
// 'msg' is the new html content
$("#catnav-noneerror").html(msg)[bool ? "show" : "hide"]();
}
// collaps text
function collapseText(s) {
return s.replace(/\n+[ ]+\n+|\n{2,}/g, "\n").trim();
}
// escape string for regex
function stringToRegex(s) {
return s.replace(/[\\\/\[\]\(\)\?\!\=\-\+\*\.\{\}\<\>\,\^\$]/g, function(a) {
return "\\" + a;
});
}
// html
function htmlContent() {
return '<nav id="catnav">' +
'<p>' + msg('instruction').escape() + '</p>' +
// categories' input
'<div class="catnav-gui-group">' +
'<div class="catnav-commoncats">' +
'<h3>' +
msg('commonCats').escape() +
' <input type="button" class="wds-button" value="add" id="catnav-commoncats-add" />' +
'</h3>' +
'<h5>' +
msg('merge').parse() +
' <input type="button" class="wds-button catnav-commoncats-global" value="import" id="catnav-commoncats-global-import" />' +
' <input type="button" class="wds-button catnav-commoncats-global" value="export" id="catnav-commoncats-global-export" />' +
'</h5>' +
'<div id="catnav-commoncats-wrapper">' +
'<span style="font-size: smaller;">' + msg('click').escape() + '</span>' +
'<div id="catnav-commoncats-container">' +
'</div>' +
'</div>' +
'</div>' +
'<table><tbody>' +
'<tr>' +
'<th>' + msg('addFrom').escape() + '</th>' +
'<th>' + msg('notFrom').escape() + '</th>' +
'</tr>' +
'<tr>' +
'<td><textarea id="catnav-textarea-include"></textarea></td>' +
'<td><textarea id="catnav-textarea-exclude"></textarea></td>' +
'</tr>' +
'</tbody></table>' +
'</div>' +
// filter options
'<div class="catnav-gui-group">' +
'<p>' +
'<input type="checkbox" id="catnav-filter-state" /><label for="catnav-filter-state">Filter page titles</label><br />' +
'<input type="text" id="catnav-filter-text" placeholder="Filter pattern" /><br />' +
'<input type="checkbox" id="catnav-filter-case" checked /><label for="catnav-filter-case">Case-insensitive search</label><br />' +
'<input type="checkbox" id="catnav-filter-regex" /><label for="catnav-filter-regex">Regex pattern</label>' +
'</p>' +
'</div>' +
// settings
'<div class="catnav-gui-group">' +
'<p>' +
msg('rowsNumber').escape() + ' <input type="text" id="catnav-rows" value="3" /><br />' +
'<input type="checkbox" id="catnav-ns" checked /><label for="catnav-ns">' + msg('nsOnly').escape() + '</label><br />' +
'<input type="radio" name="catnav-sort" id="catnav-sort-alphabet" value="alphabet" checked /><label for="catnav-sort-alphabet">' + msg('sortAlphabet').escape() + '</label> <span style="font-size: 66%;">' + msg('alphabeticAlert').escape() + '</span><br />' +
'<input type="radio" name="catnav-sort" id="catnav-sort-bypageid" value="bypageid" /><label for="catnav-sort-bypageid">' + msg('sortID').escape() + '</label><br />' +
'<!-- <input type="radio" name="catnav-sort" id="catnav-sort-creation" value="creation" /><label for="catnav-sort-creation">' + msg('sortCreation').escape() + '</label><br /> -->' +
'<input type="radio" name="catnav-sort" id="catnav-sort-lastedit" value="lastedit" /><label for="catnav-sort-lastedit">' + msg('sortLastEdit').escape() + '</label><br />' +
'<input type="checkbox" id="catnav-dir" /><label for="catnav-dir">' + msg('descending').escape() + '</label><br />' +
'</p>' +
'<input type="button" class="wds-button" id="catnav-go" value="generate" /> <span id="catnav-loading">' +
'<svg width="12" height="12" style="margin-right: 4px;">' +
'<style type="text/css"><![CDATA[@keyframes spin{to{transform:rotate(360deg);}}circle{animation: spin 1s linear infinite;transform-origin:6px 6px;}]]></style>' +
'<circle cx="6" cy="6" r="5.5" fill="none" stroke="#999" stroke-dasharray="8.63937979737193 8.63937979737193" />' +
'</svg>' +
msg('loading').escape() +
'</span>' +
'</div>' +
// error messages
'<div id="catnav-noneerror" style="display: none;">' +
msg('noPagesFound').escape() +
'</div>' +
// nav result
'<div id="catnav-container">' +
'</div>' +
'<div id="catnav-resultscounter">' +
'</div>' +
'<div id="catnav-pagenav">' +
'</div>' +
// lol what is this even...
'<div id="catnav-generator">' +
'</div>' +
'</nav>';
}
/* global favorites */
var globalString = location.href.match(/^https?\:\/{2}(.+)\.fandom\.com(?:\/(.+))?\/wiki\//);
globalString = (typeof globalString[2] === "string" ? globalString[2] + "." : "") + globalString[1];
// get members of a given category
function catMembers(cat, ns, arr, cont, cb) {
/*
getMembersOfCat("Foo", 0, [], "", function(data) {
customConsole.log(data);
});
*/
var req = new XMLHttpRequest();
req.open("GET", config.wgScriptPath + "/api.php?action=query&format=json&list=categorymembers&cmtitle=Category:" + encodeURIComponent(cat) + "&cmnamespace=" + ns + "&cmcontinue=" + encodeURIComponent(cont) + "&rawcontinue=&cmlimit=max&cb=" + new Date().getTime(), true);
req.onload = function() {
var data = JSON.parse(this.responseText);
if (data.hasOwnProperty("query")) {
var a = data.query.categorymembers,
b;
for (var i in a) {
b = a[i];
arr.push(b.title);
cndata.pageids[b.title] = b.pageid;
}
if (typeof data["query-continue"] === "object") {
return catMembers(cat, ns, arr, data["query-continue"].categorymembers.cmcontinue, cb);
} else {
cb(arr);
}
} else {
onLoadingEnd();
errorMsg(1, msg('errorLooking', cat).escape() + "<br />\n" + msg('errorCode').escape() + " <code>" + data.error.code + "</code><br />\n" + msg('errorInfo').escape() + " " + data.error.info + "<br /><hr />" + msg('noEmptyLines').escape());
}
};
req.send();
}
// get members of multiple categories
function catMembersMulti(catstr, ns, cb) {
var cats = catstr.split("|"),
completed = 0,
allcats = {};
function query() {
if (completed == cats.length) {
// all requests have been made
cb(allcats);
} else {
catMembers(cats[completed], ns, [], "", function(data) {
allcats[cats[completed]] = data;
completed++;
query();
});
}
}
query();
}
// search for members that exist in all specified categories
function joinMembers(data, isCommonMembers, fn) {
var smallestCat,
finalList = [];
if (isCommonMembers === true) {
// mode for the "find-in-categories" list: only list a page if it has all the listed categories
smallestCat = data[getSmallestCat(data)]; // start from smallest category - should take less time
for (var i in smallestCat) {
var itemLeSmall = smallestCat[i], // current item for check
isSharedCommon = true;
// if 'isCommonMembers == true', only list pages that are listed in all categories
// otherwise, list all pages anyway
for (var cat in data) {
if (cat != smallestCat && data[cat].indexOf(itemLeSmall) == -1) {
// the page 'itemLeSmall' is not categorized in one of these cats
isSharedCommon = false;
break;
}
}
if (isSharedCommon) {
finalList.push(itemLeSmall);
}
}
} else {
// mode for the "unwanted categories" list: list any categorized page that is categorized at least once
for (var cat2 in data) {
for (var j in data[cat2]) {
var currCat = data[cat2][j];
if (finalList.indexOf(finalList) == -1) {
// although we categorize all pages, we still don't want a category to repeat- it's time consuming and pointless
finalList.push(currCat);
}
}
}
}
fn(finalList);
}
// find the category with the fewest members
function getSmallestCat(data) {
customConsole.log("smallest cat!", data);
var small = {
cat: null,
length: Infinity
};
for (var cat in data) {
if (data[cat].length < small.length) {
small.cat = cat;
small.length = data[cat].length;
}
}
return small.cat; // return name of property with the smallest number of items
}
// divide to pages - take a long list of titles, and split them to groups, each with no more than 'n' titles
function divideToPages(titles) {
var result = [];
while (titles.length > 0) {
result.push(titles.splice(0, settings.itemsInNavigator * getNumberOfRows()));
}
return result;
}
// get number of rows
function getNumberOfRows() {
var rows = settings.rows,
specified = $("#catnav-rows").val();
if ($.isNumeric(specified) && specified > 0 && specified == Math.round(specified)) {
// specified number of rows is a valid positive integer
rows = specified;
}
return rows;
}
// implement new members
function implementNewTitles(titles, categories) { // needs 'categories' parameter if advanced category sorting is desired
// 'titles' is an array of page titles
sortTitles(titles, categories, $('[name="catnav-sort"]:checked').val(), $('#catnav-dir').prop("checked"), function(sortedTitles) {
var asPages = divideToPages(sortedTitles); // divide the 'titles' array to a list of smaller groups of titles
if (asPages.length > 0) {
cndata.current = asPages;
onLoadingEnd();
errorMsg(0, null); // hide error
updatePagesListNav();
gotoPage(0, true);
$("#catnav-resultscounter").text(function() {
var n = 0,
c = cndata.current,
i;
for (i = 0; i < c.length; i++) {
n += c[i].length;
}
return n + " " + msg('results', n).plain();
});
} else {
onLoadingEnd();
errorMsg(1, msg('nothingFound').escape()); // show error
}
});
}
// update pages' numbers
function updatePagesListNav() {
var a,
pagenav = $("#catnav-pagenav");
$(pagenav).html("");
for (var i = 0; i < cndata.current.length; i++) {
a = $('<a data-catnav-page="' + i + '" />').html("(" + (Number(i) + 1) + ")");
$(pagenav).append(a);
$(a).click(function() {
gotoPage(Number($(this).attr("data-catnav-page")));
});
}
}
// clear results
function clear() {
cndata.current = [];
updateMarkup();
updatePagesListNav();
}
// go to page 'n + 1'
function gotoPage(n, uponGeneration) {
cndata.selectedPage = n;
$("#catnav-pagenav .catnav-pagenav-selected").removeClass("catnav-pagenav-selected");
$("#catnav-pagenav a").eq(n).addClass("catnav-pagenav-selected");
queryPages(cndata.current[n], function(titles) {
updateMarkup(titles);
// trigger events
var pageloadEvent = new Event("catnavpageload");
if (uponGeneration) {
document.querySelector("#catnav").dispatchEvent(new Event("catnavgenerated"));
pageloadEvent.uponGeneration = true;
}
document.querySelector("#catnav").dispatchEvent(pageloadEvent);
});
}
// query missing titles for queryPages
function queryMissingPages(missingTitles, cb, totalRequestLength) {
var req = new XMLHttpRequest(),
f = new FormData(),
fD,
fDProp,
maxTitlesPerRequest = 500, // limit max titles per request, for very large requests can cause load issues
currentTitlesToCheck = missingTitles.splice(0, maxTitlesPerRequest), // titles for the current check
i;
customConsole.error(currentTitlesToCheck);
// check if there are any titles to check
if (currentTitlesToCheck.length === 0) {
// data about at least 1 page needs to be requested
$("#catnav-loading-fandomapi-progress").remove();
cb();
return;
}
// form data for requestest
fD = {
abstract: 0,
width: 140,
height: 140,
ids: currentTitlesToCheck.join(",")
};
for (fDProp in fD) {
f.append(fDProp, fD[fDProp]);
}
// the actual request
req.open("POST", config.wgScriptPath + "/api/v1/Articles/Details", true);
req.onload = function() {
parsePagesQuery(JSON.parse(this.responseText));
// call queryMissingPages again in case there are more titles left
queryMissingPages(missingTitles, cb, totalRequestLength);
};
$("#catnav-loading-fandomapi-progress").text(((1 - (missingTitles.length + currentTitlesToCheck.length) / totalRequestLength) * 100).toFixed(2) + "%");
req.send(f);
}
// get info about pages (url, thumb, etc.)
function queryPages(titles, cb) {
var missingTitles = [],
i;
for (i = 0; i < titles.length; i++) {
// missingTitles lists all articles that haven't been previously loaded by 'queryPages'
// articles already in 'cndata.details' will not be requested again
if (!cndata.details.hasOwnProperty(titles[i])) {
missingTitles.push(cndata.pageids[titles[i]]);
}
}
// query all missing titles
$("#catnav-loading").append('<span id="catnav-loading-fandomapi-progress"></span>');
queryMissingPages(missingTitles, function() {
// call queryPages's own cb() and pass the original titles
cb(titles);
}, missingTitles.length);
}
// process info about pages from json
function parsePagesQuery(data) {
for (var pageid in data.items) {
var a = data.items[pageid],
title = decodeURIComponent(a.url.replace(/^[^\n]*\/wiki\//, "")).replace(/_/g, " "); // a.title doesn't provide the namespace - easiest method is to do this
cndata.details[title] = {
id: a.id,
title: title,
url: a.url,
img: a.thumbnail,
lastedit: a.revision.timestamp
};
}
}
// update markup
function updateMarkup(titles) {
var container = $('<div />');
for (var i in titles) {
var a = cndata.details[titles[i]],
item = $('<div class="catnav-item' + (a.img ? "" : " catnav-item-noimage") + '"><a href="' + a.url + '" title="' + a.title.replace(/["'&<>]/g, function(m) {return "&#" + m.charCodeAt(0) + ";";}) + '"><img src="' + (a.img ? a.img : config.wgBlankImgUrl) + '" width="140" height="140" /><span class="catnav-item-label"></span></a></div>');
$(item).find("span").text(a.title);
$(container).append(item);
}
$("#catnav-container").html($(container).html());
//window.q = container; // lol why do i always use window.q, i keep finding old lines with this whenever i set it somewhere else in the code for debugging
}
// sort pages by user preferences
function sortTitles(titles, categories, mode, reverse, cb) {
//window.q = [titles.concat(), mode, reverse];
/*
modes:
{
creation => by creation time
lastedit => by last edit time
alphabet => by alphabetic order
}
'reverse' is a boolean: set as true in order to reverse the list of pages in the end of the process
'categories' parameter may be needed for specific advanced sorting methods
*/
/* first, check if there's need to filter titles */
if ($("#catnav-filter-state")[0].checked) {
var filter = {
value: $("#catnav-filter-text").val(),
ci: $("#catnav-filter-case")[0].checked,
re: $("#catnav-filter-regex")[0].checked
};
try {
filter.pattern = new RegExp(filter.re ? filter.value : stringToRegex(filter.value), filter.ci ? "i" : "");
// filter patterns
titles = titles.filter(function(title) {
if (filter.pattern.test(title)) {
return true;
}
});
} catch(err) {
alert(msg('regex').escape());
console.error("[catnav] :: regex error", err);
customConsole.error(err);
}
}
/* start sorting */
mode = ["bypageid", "lastedit", "alphabet"].indexOf(mode) > -1 ? mode : "alphabet";
var sortedTitles;
customConsole.log("sortTitles", arguments);
switch (mode) {
// @ mode == "alphabet"
case "alphabet":
// sort by alphabet order
sortedTitles = titles.sort();
if (reverse) {
sortedTitles.reverse();
}
cb(sortedTitles);
break;
// @ mode == "lastedit"
case "lastedit":
// sort by lastedit
queryPages(titles, function(titles) {
// 'queryPages' is required to know how to sort all titles before splitting into navpages
var details = cndata.details,
title,
curr,
arr = [], // array of values (last edit timestamp)
pagesBySortingMethod = {}, // object: key => sorting method value (timestamp, value => array with titles with that associated value (in case 2+ articles have the value)
i;
// copy info from 'cndata.details' about the wanted titles
for (title in details) {
if (titles.indexOf(title) > -1) {
curr = details[title].lastedit;
if (arr.indexOf(Number(curr)) === -1) {
arr.push(Number(curr));
}
pagesBySortingMethod[curr] = pagesBySortingMethod[curr] || [];
pagesBySortingMethod[curr].push(title);
}
}
arr.sort(function(a, b) {
// sort by numerically ascending order
return a - b;
});
sortedTitles = [];
for (i = 0; i < arr.length; i++ ) {
// combine the mini-arrays, from the lowest timestamp to the highest
sortedTitles = sortedTitles.concat(pagesBySortingMethod[arr[i]]);
}
if (reverse) {
sortedTitles.reverse();
}
window.q = {
details: details,
title: title,
curr: curr,
arr: arr,
pagesBySortingMethod: pagesBySortingMethod,
i: i,
sortedTitles: sortedTitles
};
cb(sortedTitles);
});
break;
// @ mode == "bypageid"
case "bypageid":
// sort by pageid (=wgArticleId)
queryPages(titles, function(titles) {
// now when 'queryPages' was used, 'cndata.details' has been created
var pagesByIds = {},
details = cndata.details,
title,
i,
id,
ids = [];
// 'pagesByIds': key => pageid, value => title
customConsole.info("details as of running: " + JSON.stringify(details));
for (title in details) {
if (titles.indexOf(title) > -1) {
id = details[title].id;
pagesByIds[id] = title;
ids.push(id);
}
}
// sort by ascending ids
ids.sort(function(a, b) {
return a - b;
});
// map by the now-ordered ids
sortedTitles = [];
for (i = 0; i < ids.length; i++) {
sortedTitles.push(pagesByIds[ids[i]]);
}
if (reverse) {
sortedTitles.reverse();
}
cb(sortedTitles);
});
break;
}
}
// get creation time of pages
function getCreationTime(titles, cb) {
// lol don't use this stupid module, while running it i figured it would be much more efficient to use wgArticleId :P
if (titles.length === 0) {
cb();
} else {
var req = new XMLHttpRequest();
req.open("GET", config.wgScriptPath + "/api.php?action=query&format=json&prop=revisions&rvprop=timestamp&rvlimit=1&rvdir=newer&titles=" + encodeURIComponent(titles.shift()), true);
// can only get 1 title at a time when getting the first revisions :(
req.onload = function() {
var data = JSON.parse(req.responseText),
pageid,
page;
for (pageid in data.query.pages) {
page = data.query.pages[pageid];
cndata.details[page.title] = cndata.details[page.title] || {};
cndata.details[page.title].creation = page.revisions[0].timestamp;
// continue getting info about the remaining titles
getCreationTime(titles, cb);
}
};
req.send();
}
}
// manipulate storage
function storageFn(method, savingData) {
var gs = globalString;
switch (method) {
case "get":
var store = localStorage.getItem("catnav");
customConsole.log(method, store);
store = store ? JSON.parse(store) : {favorites: {}};
if (!store.favorites.hasOwnProperty(gs)) {
store.favorites[gs] = [];
}
customConsole.log(method, store);
return store;
case "set":
localStorage.setItem("catnav", JSON.stringify(savingData));
return true;
}
}
// add favorite to list
function insertFavorite(category) {
var item = $('<div class="catnav-commoncats-item"></div>')
.data({name: category})
.text(category)
.prepend('<img src="https://images.wikia.nocookie.net/dev/images/7/79/CatNav_delete.png" class="catnav-commoncats-item-delete" />', ' ');
$(item).contextmenu(function(e) {
if ($(e.target).is(this)) {
e.preventDefault();
var a = $("#catnav-textarea-exclude"),
b = $(a).val().split("\n");
b.push($(this).data("name"));
$(a).val(b.join("\n").trim());
}
}).click(function(e) {
customConsole.log(e.target, this);
if ($(e.target).is(this) && e.which === 1) {
var a = $("#catnav-textarea-include"),
b = $(a).val().split("\n");
b.push($(this).data("name"));
$(a).val(b.join("\n").trim());
}
});
$(item).find("img").click(function() {
var storage = storageFn("get"),
gs = globalString,
fave = storage.favorites[gs],
index = fave.indexOf($(this).parent().data("name"));
if (index > -1) {
fave.splice(index, 1);
storage = storageFn("set", storage);
$(this).parent().remove();
}
});
$("#catnav-commoncats-container").append(item);
}
// import global settings ([[w:Special:MyPage/catnav.css]])
function addGlobalFavorites() {
var user = encodeURIComponent(config.wgUserName);
$.ajax({
url: cndata.storage.url + (cndata.storage.url === "//community.fandom.com" ? "" : config.wgScriptPath) + "/api.php?" + new Date().getTime(),
data: {
action: 'query',
format: 'json',
prop: 'revisions',
rvprop: 'content',
titles: 'User:' + user + '/catnav.css',
callback: 'catnavcb'
},
action: "GET",
dataType: "jsonp",
jsonpCallback: "catnavcb",
success: function(data) {
var pages = data.query.pages,
pageid,
content,
cities = {},
currCity,
wiki = globalString,
storage;
for (pageid in pages) {
if (pageid == -1) {
alert(msg('checkCSS').escape());
} else {
content = pages[pageid].revisions[0]["*"];
content.split("\n").forEach(function(line, lineNumber) {
customConsole.warn(lineNumber, lineNumber + 1, line);
var temp;
switch (line.charAt(0)) {
case "@":
temp = line.substr(1).trim().toLowerCase();
try {
currCity = temp;
cities[temp] = cities[temp] || [];
} catch(err) {
alert(msg('invalidSubdomain', lineNumber + 1).escape() + " " + temp);
}
break;
case "#":
temp = line.substr(1).trim();
temp = temp.charAt(0).toUpperCase() + temp.substr(1); // make first letter uppercase per case-insensitivity in pages' initials
if (currCity) {
cities[currCity].push(temp);
} else {
alert(msg('categoryAlert', lineNumber + 1).escape());
}
break;
}
});
}
break;
}
if (cities.hasOwnProperty(wiki)) {
storage = storageFn("get");
customConsole.info(storage);
cities[wiki].forEach(function(category) {
customConsole.info(category);
if (storage.favorites[wiki].indexOf(category) === -1) {
storage.favorites[wiki].push(category);
insertFavorite(category);
}
});
storage = storageFn("set", storage);
}
}
});
}
// get sizes of categories
function getCategorySizes(categories, sizelist, cb) {
// arguments => ["Foo1", "Foo2", "Fooetc."], {}, function() {}
customConsole.log("inite :: getCategorySizes");
var req = new XMLHttpRequest(),
curr = categories.splice(0, 50);
for (var i = 0; i < curr.length; i++) {
curr[i] = "Category:" + curr[i];
}
req.open("GET", config.wgScriptPath + "/api.php?action=query&format=json&prop=categoryinfo&titles=" + encodeURIComponent(curr.join("|")) + "&cb=" + new Date().getTime(), true);
req.onload = function() {
var data = JSON.parse(req.responseText),
cat,
catname;
for (var pageid in data.query.pages) {
cat = data.query.pages[pageid];
catname = cat.title.replace(/^[^\:]+\:/, "");
if (!cat.hasOwnProperty("categoryinfo")) {
// no category members - continue
// note: pageid can be negative if the category is being used in pages, but its category page has yet to be created
sizelist[catname] = 0; // note that this will be smaller than any other category!!!
continue;
}
sizelist[catname] = cat.categoryinfo.size; // size = any page (including other categories) that uses this category
}
if (categories.length > 0) {
// not done
getCategorySizes(categories, sizelist, cb);
} else {
// done
cb(sizelist);
}
};
req.send();
}
// when no further resources are to be loaded
// when all required resources have loaded and processed, or when an error prevents the process completion
function onLoadingEnd() {
unsealUI();
$("#catnav-loading").hide();
}
// convert local favorites to global favorites syntax (that the user can copy and paste in their global list)
function exportGlobalFavorites() {
var wiki = globalString,
favorites = storageFn("get").favorites[wiki],
lines = ["@" + wiki];
for (var i = 0; i < favorites.length; i++) {
lines.push("#" + favorites[i]);
}
return lines.join("\n");
}
// init
function initMain() {
// # html
// interface markup
$("#mw-content-text").html(htmlContent());
// # triggers
// 'generate' button
$("#catnav-go").click(function() {
clear();
sealUI();
$("#catnav-loading").show(); // show loading text
var incCats = collapseText($("#catnav #catnav-textarea-include").val()).replace(/\n/g, "|"), // included categories
disCats = collapseText($("#catnav #catnav-textarea-exclude").val()).replace(/\n/g, "|"), // exclude categories
catList = incCats.split("|"), // array of included categories - needed for advanced page sorting methods
nsStr = $("#catnav-ns")[0].checked ? "0" : "";
// get included categories (object: key => categoryname, val => array of listed pages in that category)
if (incCats.length === 0) {
unsealUI();
$("#catnav-loading").hide();
errorMsg(1, "Please enter at least one valid category under the \"Add from categories\" field.");
return;
}
catMembersMulti(incCats, nsStr, function(incData) {
// sort the pages into a single array
joinMembers(incData, true, function(incTitles) {
if (disCats.length === 0) {
// no unwated categories requested - update immediately
implementNewTitles(incTitles, catList);
} else {
// unwated categories requiested - get their categorized pages
catMembersMulti(disCats, nsStr, function(disData) {
// sort the pages into a single array
joinMembers(disData, false, function(disTitles) {
for (var i in disTitles) {
if (incTitles.indexOf(disTitles[i]) > -1) {
// unwanted page detected - remove from 'incTitles'
incTitles.splice(incTitles.indexOf(disTitles[i]), 1);
}
}
implementNewTitles(incTitles, catList);
});
});
}
});
});
});
// adding categories to favorites
$("#catnav-commoncats-add").click(function() {
var category = prompt(msg('insertCategory').escape()),
storage,
gs = globalString;
if (category) {
storage = storageFn("get");
if (storage.favorites[gs].indexOf(category) === -1) {
storage.favorites[gs].push(category);
storage = storageFn("set", storage);
insertFavorite(category);
}
}
});
// importing global categories
$("#catnav-commoncats-global-import").click(function() {
addGlobalFavorites();
});
// initiating favorites
$(function() {
var storage = storageFn("get"),
gs = globalString,
fave = storage.favorites[gs],
i;
for (i = 0; i < fave.length; i++) {
insertFavorite(fave[i]);
}
});
// export global categories
$("#catnav-commoncats-global-export").click(function() {
$("#catnav-export-modal-textarea").val(exportGlobalFavorites());
$("#catnav-export-modal").css("display", "flex");
$("#catnav-export-modal-textarea").select();
});
// close favorites export modal
$("#catnav-export-modal-close").click(function() {
$("#catnav-export-modal").hide();
});
$("#catnav-export-modal").keydown(function(e) {
if (e.which == 27) {
$("#catnav-export-modal").hide();
}
});
$("#catnav-export-modal-textarea").keydown(function(e) {
if (e.which == 67 && e.ctrlKey && this.selectionEnd - this.selectionStart === this.value.length) {
// ctrl+c, while all text is selected
// close modal after 80ms (to prevent deselection of target text before copying is done)
setTimeout(function() {
$("#catnav-export-modal").hide();
}, 80);
}
});
// mark as ready
document.body.dispatchEvent(new Event("catnavready"));
}
// set contrast color
function contrastfy(colorValue) {
// myst use canvas because apparently sometimes the color value will be a color name and not a convertable hex value
var c = document.createElement("canvas"),
ctx,
rgb,
avg,
contrast;
c.width = 1;
c.height = 1;
ctx = c.getContext("2d");
ctx.fillStyle = colorValue;
ctx.fillRect(0, 0, 1, 1);
rgb = ctx.getImageData(0, 0, 1, 1).data.slice(0, 3);
avg = (rgb[0] + rgb[1] + rgb[2]) / 3;
contrast = Math.floor(128 + avg - (avg < 128 ? 0 : 255));
contrast = contrast.toString(16);
contrast = contrast.length == 2 ? contrast : "0" + contrast;
return "#" + new Array(4).join(contrast);
}
// seal ui fields while searching
function sealUI() {
$("nav#catnav .catnav-gui-group").find("textarea, input").attr("disabled", "disabled");
}
function unsealUI() {
$("nav#catnav .catnav-gui-group").find("textarea, input").removeAttr("disabled");
}
function init() {
/* ================================== *\
# global CatNav object management
\* ================================== */
var catnav = {};
if (catnav._g instanceof Object) {
// global CatNav is an object
if (Array.isArray(catnav._g.storage)) {
// import+export storage - base url to hosting wiki
cndata.storage.url = catnav._g.storage[0];
cndata.storage.scriptPath = catnav._g.storage[1];
} else if (catnav._g.hasOwnProperty("storage")) {
customConsole.error("catnav :: global 'CatNav.storage' has been defined, but it is not a valid array");
}
} else {
// window.CatNav has not been defined - define it
window.CatNav = {};
}
if (window.CatNav.debug !== true) {
customConsole.debug = false;
}
/* ================================== *\
# implementations
\* ================================== */
importArticles({
type: 'script',
articles: 'u:dev:MediaWiki:I18n-js/code.js'
});
/* css */
importArticle({
type: 'style',
article: 'u:dev:MediaWiki:CatNav.css'
});
// redefine results counter
mw.util.addCSS(
'#catnav-resultscounter {' +
// support for pre-ucp, ucp-compatible, fandom desktop, and legacy gamepedia
// intentionally not adding fallback (e.g. 'document.body') as possible layout changes in the future might make detecting style issues problematic
'color: ' + contrastfy(getComputedStyle(document.querySelector("section#WikiaPageBackground") || document.querySelector(".WikiaPageContentWrapper") || document.querySelector("main.page__main") || document.querySelector("body > #global-wrapper > #pageWrapper > #content")).backgroundColor) + ';' +
'}' +
'#catnav .catnav-gui-group {' +
'border: 1px solid ' + getComputedStyle(document.body).backgroundColor + ';' +
'}\n'
);
/* document title */
document.title = document.title.split(" | ").map(function(a, b) {
return b === 0 ? "CatNav" : a;
}).join(" | ");
/* markup */
// export modal (main content loaded using 'initMain'
$("body").append(
'<nav id="catnav-export-modal">' +
'<nav id="catnav-export-modal-content">' +
'<h3>' +
msg('exportFavs').escape() +
'<span id="catnav-export-modal-close" />' +
'</h3>' +
'<p>' +
msg('copyAndPaste').parse() + '<br />' + msg('mergeTwo').escape() +
'</p>' +
'<textarea id="catnav-export-modal-textarea"></textarea>' +
'</nav>'+
'</nav>'
);
// update titles
$(".page-header__title").html("CatNav");
// init
initMain();
// provide global access
window.CatNav.init = initMain;
}
// check if the page is [[Special:CatNav]]
if (config.wgNamespaceNumber === -1 && config.wgTitle === "CatNav") {
mw.hook('dev.i18n').add(function (i18n) {
i18n.loadMessages('CatNav').done(function (i18no) {
msg = i18no.msg;
init();
});
});
importArticle({ type: 'script', article: 'u:dev:MediaWiki:I18n-js/code.js' });
} else {
// this is not [[Special:CatNav]]
window.CatNav = window.CatNav || {};
window.CatNav.init = function() {
customConsole.warn("catnav :: 'CatNav.init' was requested, but has not been defined on [[Special:CatNav]]");
};
}
})(window, window.jQuery, window.mediaWiki);
/* </nowiki> */