MediaWiki:CatNav.js

/*	allow a category navigation based on a list of categories (instead of the usual single category navigation) mw.loader.using(['mediawiki.util', 'mediawiki.api'], function {   // add link to Special:CatNav    $("#my-tools-menu").append('Tìm Kiếm Nâng Cao');

// check if the page is Special:CatNav if (mw.config.get("wgPageName") == "Special:CatNav") {

/* ================================== *\       	# core objects \* ================================== */

//var catnav = { var catnav = { fn: {}, settings: { rows: 3, // default number of rows itemsInNavigator: Math.floor(($("#mw-content-text").width - 100) / 154) // number of columns determined by the width of the user's screen }       };        catnav.data = { details: {}, // details about pages current: [], selectedPage: null };

/* ================================== *\       	# functions \* ================================== */

/* functions for getting info about categories */

// get members of a given category catnav.fn.catMembers = function(cat, ns, arr, cont, cb) { /*           catnav.fn.getMembersOfCat("Foo", 0, [], "", function(data) {            	console.log(data);            }); */           var req = new XMLHttpRequest; req.open("get", "/api.php?action=query&format=json&list=categorymembers&cmtitle=Category:" + encodeURIComponent(cat) + "&cmnamespace=" + ns + "&cmcontinue=" + encodeURIComponent(cont) + "&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; for (var i in a) { arr.push(data.query.categorymembers[i].title); }                   if (typeof data["query-continue"] === "object") { return catnav.fn.catMembers(cat, ns, arr, data["query-continue"].categorymembers.cmcontinue, cb); } else { cb(arr); }               } else { catnav.fn.error(1, "Đã có lỗi xảy ra khi tìm trang trong thể loại " + cat + ". Thông tin lỗi như sau: \nError code: \nError info: " + data.error.info + " Hãy đảm bảo là bạn đã nhập đúng thông tin. Không để dòng trống."); }           };            req.send; };

// get members of multiple categories catnav.fn.catMembersMulti = function(catstr, ns, cb) { var cats = catstr.split("|"), completed = 0, allcats = {};

function query { if (completed == cats.length) { // all requests have been made cb(allcats); } else { catnav.fn.catMembers(cats[completed], ns, [], "", function(data) {                       allcats[cats[completed]] = data;                        completed++;                        query;                    }); }           }            query; };

// search for members that exist in all specified categories catnav.fn.joinMembers = function(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[catnav.fn.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 cat in data) { for (var i in data[cat]) { var currCat = data[cat][i]; 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 catnav.fn.getSmallestCat = function(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 catnav.fn.divideToPages = function(titles) { var result = []; while (titles.length > 0) { result.push(titles.splice(0, catnav.settings.itemsInNavigator * catnav.fn.getNumberOfRows)); }           return result; };

// get number of rows catnav.fn.getNumberOfRows = function { var rows = catnav.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; };

/* functions for getting info about pages */

// implement new members catnav.fn.implementNewTitles = function(titles) { // 'titles' is an array of page titles catnav.fn.sortTitles(titles, $('[name="catnav-sort"]:checked').val, $('#catnav-dir').prop("checked"), function(sortedTitles) {               var asPages = catnav.fn.divideToPages(sortedTitles); // divide the 'titles' array to a list of smaller groups of titles                if (asPages.length > 0) {                    catnav.data.current = asPages;                    catnav.fn.error(0, null); // hide error                    catnav.fn.updatePagesListNav;                    catnav.fn.gotoPage(0);                } else {                    catnav.fn.error(1, "Không tìm thấy trang với các thể loại được liệt kê!"); // show error                }            }); };

// update pages' numbers catnav.fn.updatePagesListNav = function { /*var container = $(' '), a; for (var i = 0; i < catnav.data.current.length; i++) { a = $('').html("(" + (Number(i) + 1) + ")"); $(container).append(a); }           $("#catnav-pagenav").html($(container).html).find("a").click(function {            	catnav.fn.gotoPage($(this).attr("data-catnav-page"));            });*/ var a,               pagenav = $("#catnav-pagenav"); $(pagenav).html(""); for (var i = 0; i < catnav.data.current.length; i++) { a = $('').html("(" + (Number(i) + 1) + ")"); $(pagenav).append(a); $(a).click(function {                   catnav.fn.gotoPage(Number($(this).attr("data-catnav-page")));                }); }       };

// go to page 'n + 1' catnav.fn.gotoPage = function(n) { catnav.data.selectedPage = n;           $("#catnav-pagenav .catnav-pagenav-selected").removeClass("catnav-pagenav-selected"); $("#catnav-pagenav a").eq(n).addClass("catnav-pagenav-selected"); catnav.fn.queryPages(catnav.data.current[n], function(titles) {               catnav.fn.updateMarkup(titles);            }); };

// get info about pages (url, thumb, etc.) catnav.fn.queryPages = function(titles, cb) { var req = new XMLHttpRequest, missingTitles = []; for (var i in titles) { // missingTitles lists all articles that haven't been previously loaded by 'queryPages' // articles already in 'catnav.data.details' will not be requested again if (!catnav.data.details.hasOwnProperty(titles[i])) { missingTitles.push(titles[i]); }           }            req.open("get", "/api/v1/Articles/Details?&abstract=0&width=140&height=140&titles=" + encodeURIComponent(missingTitles.join(",")), true); req.onload = function { catnav.fn.parsePagesQuery(JSON.parse(this.responseText)); cb(titles); };           if (missingTitles.length > 0) { // data about at least 1 page needs to be requested req.send; } else { // info about those pages has already loaded cb(titles); }       };

// process info about pages from json catnav.fn.parsePagesQuery = function(data) { for (var pageid in data.items) { var a = data.items[pageid], title = decodeURIComponent(a.url.substr(6)).replace(/_/g, " "); // a.title doesn't provide the namespace - easiest method is to do this catnav.data.details[title] = { id: a.id, title: title, url: a.url, img: a.thumbnail, lastedit: a.revision.timestamp };           }        };

// update markup catnav.fn.updateMarkup = function(titles) { var container = $(' '); for (var i in titles) { var a = catnav.data.details[titles[i]], item = $(']/g, function(m) { return "&#" + m.charCodeAt(0) + ";"; }) + '">  ');               $(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        };

// error message catnav.fn.error = function(bool, msg) { // if 'bool' show message, otherwise hide // 'msg' is the new html content $("#catnav-noneerror").html(msg)[bool ? "show" : "hide"]; };

// collaps text catnav.fn.collapseText = function(s) { return s.replace(/\n+[ \t]+\n+|\n{2,}/g, "\n").trim; };

// sort pages by user preferences catnav.fn.sortTitles = function(titles, 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 */           mode = ["bypageid", /*"creation",*/ "lastedit", "alphabet"].indexOf(mode) > -1 ? mode : "alphabet"; var sortedTitles; 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 catnav.fn.queryPages(titles, function(titles) {                       // 'catnav.fn.queryPages' is required to know how to sort all titles before splitting into navpages                        var details = catnav.data.details,                            title,                            ts,                            timestamps = [], // array of timestamps                            pagesByTimestamps = {}, // object: key => timestamp, value => array with titles that were last edited at that time (in case 2+ articles have the same timestamp)                            i;                        // copy data from 'catnav.data.details' about the wanted titles                        for (title in details) {                            if (titles.indexOf(title) > -1) {                                ts = details[title].lastedit;                                if (timestamps.indexOf(Number(ts)) === -1) { timestamps.push(Number(ts)); }                               pagesByTimestamps[ts] = pagesByTimestamps[ts] || []; pagesByTimestamps[ts].push(title); }                       }                        timestamps.sort(function(a, b) {                            // sort timestamps by numerically ascending order                            return a - b;                        }); sortedTitles = []; for (i = 0; i < timestamps.length; i++) { // combine the mini-arrays, from the lowest timestamp to the highest sortedTitles = sortedTitles.concat(pagesByTimestamps[timestamps[i]]); }                       if (reverse) { sortedTitles.reverse; }                       cb(sortedTitles); });                   break;

// @ mode == "bypageid" case "bypageid": // sort by pageid (=wgArticleId) catnav.fn.queryPages(titles, function(titles) {                       // now when 'catnav.fn.queryPages' was used, 'catnav.data.details' has been created                        var pagesByIds = {},                            details = catnav.data.details,                            title,                            i,                            id,                            ids = [];                        // 'pagesByIds': key => pageid, value => title                        console.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 catnav.fn.getCreationTime = function(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", "/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];                        catnav.data.details[page.title] = catnav.data.details[page.title] || {};                        catnav.data.details[page.title].creation = page.revisions[0].timestamp;                        // continue getting data about the remaining titles                        catnav.fn.getCreationTime(titles, cb);                    }                };                req.send;            }        };

// manipulate storage catnav.fn.storage = function(method, savingData) { switch (method) { case "get": var storage = localStorage.getItem("catnav"); storage = storage ? JSON.parse(storage) : { favorites: [] };                   return storage; break; case "set": localStorage.setItem("catnav", JSON.stringify(savingData)); return true; break; }       };

// add favorite to list catnav.fn.insertFavorite = function(category) { var child = $(' ') .data({                   name: category                }) .text(category) .prepend('', ' '); $(child).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) {                console.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);                }            }); $(child).find("img").click(function {               var storage = catnav.fn.storage("get"),                    fave = storage.favorites,                    index = fave.indexOf($(this).parent.data("name"));                if (index > -1) {                    fave.splice(index, 1);                    storage = catnav.fn.storage("set", storage);                    $(this).parent.remove;                }            }); $("#catnav-commoncats-container").append(child); };

/* ================================== *\       	# css and markup \* ================================== */

/* css */ mw.util.addCSS(           '#catnav {\n' +            '\twidth: 100%;\n' +            '\tmargin: 0;\n' +            '\tpadding: 0;\n' +            '}\n' +            '#catnav-container {\n' +            '\tdisplay: flex;\n' +            '\tflex-wrap: wrap;\n' +            '\tmargin: 10px auto 20px auto;\n' +            '\tmargin: 10px 50px 20px 50px;\n' +            '}\n' +            '#catnav-container .catnav-item {\n' +            '\tdisplay: inline-block;\n' +            '\twidth: 140px;\n' +            '\theight: 140px;\n' +            '\tmargin: 3px;\n' +            '\tpadding: 2px;\n' +            '\tposition: relative;\n' +            '\toverflow: hidden;\n' +            '\tborder: 2px solid navy;\n' +            '\tborder-radius: 10px;\n' +            '}\n' +            '#catnav-container .catnav-item .catnav-item-label {\n' +            '\tmax-width: 90px;\n' +            '\theight: 18px;\n' + '\tpadding: 0 4px;\n' + '\tposition: absolute;\n' + '\tbottom: 0;\n' + '\tright: 0;\n' + '\toverflow: hidden;\n' + '\tbackground: #006cb0;\n' + '\tborder-top-left-radius: 7px;\n' + '\tcolor: #fff;\n' + '\tfont-size: 14px;\n' + '\tline-height: 18px;\n' + '}\n' + '#catnav-container img {\n' + '\tborder-radius: 7px;\n' + '}\n' + '#catnav-container .catnav-item-noimage {\n' + '\tborder-color: #c00;\n' + '}\n' + '#catnav-container .catnav-item-noimage .catnav-item-label {\n' + '\tbackground: #c00;\n' + '}\n' + '#catnav-pagenav {\n' + '\tpadding: 3px 7px;\n' + '\ttext-align: center;\n' + '\tfont-size: 18px;\n' + '\tline-height: 18px;\n' + '}\n' + '#catnav-pagenav a:not(.catnav-pagenav-selected) {\n' + '\tcolor: #a6d1ec;\n' + '\ttext-shadow: 1px 1px 0 navy;\n' + '}\n' + '#catnav-pagenav a ~ a {\n' + '\tmargin-left: 10px;\n' + '}\n' + '#catnav-pagenav a.catnav-pagenav-selected {\n' + '\tcolor: black;\n' + '\tcursor: pointer;\n' + '\tfont-weight: bold;\n' + '}\n' + '#catnav textarea {\n' + '\twidth: 100%;\n' + '\twidth: calc(100% - 6px);\n' + '\theight: 100px;\n' + '\tresize: none;\n' + '}\n' + '#catnav table {\n' + '\twidth: 100%;\n' + '\tmargin: 0;\n' + '\tpadding: 0;\n' + '}\n' + '#catnav #catnav-rows {\n' + '\twidth: 30px;\n' + '}\n' + '#catnav th {\n' + '\twidth: 50%;\n' + '}\n' + '#catnav label {\n' + '\tfont-size: smaller;\n' + '}\n' + '#catnav #catnav-noneerror {\n' + '\tcolor: #c00;\n' + '\tfont-weight: bold;\n' + '}\n' + '#catnav .catnav-gui-group {\n' + '\tpadding: 3px 7px;\n' + '\tborder: 1px solid ' + mw.config.get("wgSassParams")["color-body"] + ';\n' + '\tborder-radius: 7px;\n' + '}\n' + '#catnav .catnav-gui-group + .catnav-gui-group {\n' + '\tmargin-top: 5px;\n' + '}\n' + '#catnav .catnav-commoncats {\n' + '\tpadding: 4px 8px;\n' + '\tbackground: repeating-linear-gradient(-30deg, #f4e3d7 0, #f4e3d7 10px, #ffccaa 10px, #ffccaa 20px);\n' + '\tborder: 3px solid #f4e3d7;\n' + '\tborder-radius: 10px;\n' + '}\n' + '#catnav #catnav-commoncats-container {\n' + '\tmax-height: 300px;\n' + '\toverflow-y: scroll;\n' + '\tpadding: 2px 7px;\n' + '\tbackground: rgba(250, 250, 250, 0.6);\n' + '}\n' + '#catnav .catnav-commoncats-item {\n' + '\tcursor: pointer;\n' + '}\n' + '#catnav .catnav-commoncats-item + .catnav-commoncats-item {\n' + '\tmargin-top: 2px;\n' + '}\n' + '#catnav .catnav-commoncats-item-delete {\n' + '\tcursor: pointer;\n' + '}'       );

/* markup */ // interface markup $("#mw-content-text").html(           ' \n' +            '\t \n' +            '\t\tTrang này cho phép bạn lọc bỏ và chỉ tìm những trang từ các thể loại bạn muốn. Nhập thể loại bạn muốn lọc vào ô tìm kiếm ở phía dưới, mỗi thể loại một dòng rồi nhấn nút Tìm kiếm ở phía dưới để lấy danh sách các trang:\n' +            // categories' input            '\t \n' +            '\t\t \n' +            '\t\t\t \n' +            '\t\t\t\tThể loại ưa thích' +            ' \n' +            '\t\t\t \n' +            '\t\t\t \n' +            '\t\t\t\tClick chuột trái để chèn vào ô thể loại muốn tìm, hoặc click chuột phải để thêm vào ô thể loại muốn loại trừ. Click vào biểu tượng xóa sẽ xóa tên những thể loại bạn đã thêm vào mục ưa thích của mình.' +            '\t\t\t \n' +            '\t\t \n' +            '\t\t \n' + '\t \n' + // settings '\t \n' + '\t\t \n' + '\t\t\tSố dòng hiển thị:  \n' + '\t\t\tChỉ tìm trong trang miền chính \n' + '\t\t\t<label for="catnav-sort-alphabet">Xếp theo bảng chữ cái \n' + '\t\t\t<input type="radio" name="catnav-sort" id="catnav-sort-bypageid" value="bypageid" /><label for="catnav-sort-bypageid" style="border-bottom: 1px dotted; cursor: help;" title="Lưu ý! Lựa chọn này sắp xếp trang dựa theo ID thay vì thời gian tạo thực tế nhằm giảm thời gian tải. Những trang cũ nếu bị xóa và được khôi phục trở lại sẽ được cấp ID mới, và vì thế sẽ lại thành 1 trang mới so với thực tế đáng lẽ ra chúng chỉ là những trang cũ.">Xếp theo thời gian tạo (ID của trang)* \n' + '\t\t\t\n' + '\t\t\t<input type="radio" name="catnav-sort" id="catnav-sort-lastedit" value="lastedit" /><label for="catnav-sort-lastedit">Xếp theo lần sửa cuối \n' + '\t\t\t<input type="checkbox" id="catnav-dir" /><label for="catnav-dir">Xếp từ trên xuống dưới \n' + '\t\t \n' + '\t\t<input type="button" id="catnav-go" value="Tìm Kiếm" />\n' + '\t \n' + // error messages '\t \n' + '\t\tKhông tìm thấy trang nào!\n' + '\t \n' + // nav result '\t \n' + '\t \n' + '\t \n' + '\t \n' + // lol what is this even... '\t \n' + '\t \n' + ' '       );        // update titles        $("head title, #WikiaPageHeader h1, h1#firstHeading").html("Tìm Kiếm Nâng Cao");

/* ================================== *\       	# triggers \* ================================== */       // 'generate' button $("#catnav-go").click(function {           var incCats = catnav.fn.collapseText($("#catnav #catnav-textarea-include").val).replace(/\n/g, "|"), // included categories                disCats = catnav.fn.collapseText($("#catnav #catnav-textarea-exclude").val).replace(/\n/g, "|"), // exclude categories                nsStr = $("#catnav-ns")[0].checked ? "0" : "";            // get included categories (object: key => categoryname, val => array of listed pages in that category)            catnav.fn.catMembersMulti(incCats, nsStr, function(incData) { // sort the pages into a single array catnav.fn.joinMembers(incData, true, function(incTitles) {                   if (disCats.length === 0) {                        // no unwated categories requested - update immediately                        catnav.fn.implementNewTitles(incTitles);                    } else {                        // unwated categories requiested - get their categorized pages                        catnav.fn.catMembersMulti(disCats, nsStr, function(disData) { // sort the pages into a single array catnav.fn.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);                                    }                                }                                catnav.fn.implementNewTitles(incTitles);                            }); });                   }                });            });        });        // adding categories to favorites $("#catnav-commoncats-add").click(function {           var category = prompt("Thêm tên thể loại bạn muốn"),                storage;            if (category) {                storage = catnav.fn.storage("get");                if (storage.favorites.indexOf(category) === -1) {                    storage.favorites.push(category);                    storage = catnav.fn.storage("set", storage);                    catnav.fn.insertFavorite(category);                }            }        }); // initiating favorites $(function {           var storage = catnav.fn.storage("get"),                fave = storage.favorites,                i;            for (i = 0; i < fave.length; i++) {                catnav.fn.insertFavorite(fave[i]);            }        }); // Drop-down menu var textboxinclude = document.getElementById('catnav-textarea-include'); var dropdowninclude = document.getElementById('catnav-dropdown-include'); var textboxexclude = document.getElementById('catnav-textarea-exclude'); var dropdownexclude = document.getElementById('catnav-dropdown-exclude'); dropdowninclude.onchange = function { if (!textboxinclude.value) textboxinclude.value = this.value; //first case or else you will have a newline at first else textboxinclude.value = textboxinclude.value + '\n' + this.value; }       dropdownexclude.onchange = function { if (!textboxexclude.value) textboxexclude.value = this.value; //first case or else you will have a newline at first else textboxexclude.value = textboxexclude.value + '\n' + this.value; }       // Reset button $("#catnav-clear-include").click(function(e) {           e.preventDefault;            $("#catnav-textarea-include").val('');        }); $("#catnav-clear-exclude").click(function(e) {           e.preventDefault;            $("#catnav-textarea-exclude").val('');        }); } });