MediaWiki recent changes namespace toggler

GPL3

This is a script to enhance the recent changes in an up-to-date MediaWiki installation. It adds a series of checkboxes on that page to show or hide edits to any namespace or log type, and it works with enhanced recent changes. Defaults can be set as instructed in the code so that specific namespaces/log types are initially selected when Special:RecentChanges is loaded. Here's a live example (if there are actually any edits there...).

You can find the script with full instructions here, or everything you'll need below (version 2.0.1).

Installation

Modify the code lower down as indicated by the comments (lines starting with //). Put the result on any script page you want, such as User:Name/monobook.js, Mediawiki:Common.js, etc.. You'll also need the following CSS on a style page of your choice (*.css):

.rcnst {display: none;}

With a little knowledge, this can be modified to, for example, show all ignored edits in a different colour, or align them to the right, rather than hide them. Examples:

Greyed out:

.rcnst {color: #aaa;}
.rcnsthidden a {color: #777 !important;}

Smaller font:

.rcnst {font-size: 0.8em;}

Right-aligned (not for enhanced recent changes):

.rcnst {text-align: right;}

All three of the above:

.rcnst {text-align: right; font-size: 0.8em; color: #aaa;}
.rcnsthidden a {color: #777 !important;}

You'll probably also want to spread the buttons out a little more. The box of controls has the 'rcnst' ID, so this is just:

#rcnst [type="button"] {margin-left: 5px;}

The script tries to use Tanny O'Haley's domReady (download from here and include to make stuff load earlier) before MediaWiki's addOnloadHook.

Code:

// true to default all change types to visible, so that RCNSTStartHidden below
// works; otherwise, they default to hidden and RCNSTStartVisible works.
var RCNSTDefaultToVisible = true;
// initial selected/unselected namespaces (or log types); put each in '' or ""
// separated by commas, inside the []; use the string in RCNSTMain for the main
// namespace
var RCNSTStartHidden = [];
var RCNSTStartVisible = [];
// id and class used in created elements
var RCNSTControlsID = 'rcnst';
var RCNSTClass = 'rcnst';
// strings used in the created elements
var RCNSTMain = '(Main)';
var RCNSTToggleAllLabel = 'toggle all';
var RCNSTInvertLabel = 'invert selection';
var RCNSTRestoreLabel = 'restore default';

var hasClass = (function () {
    var reCache = {};
    return function (element, className) {
        return (reCache[className] ? reCache[className] : (reCache[className] =
            new RegExp("(?:\\s|^)" + className + "(?:\\s|$)")))
            .test(element.className);
    };
})();

function nextElement (e) {
    do e = e.nextSibling;
    while (e !== null && e.nodeType != 1);
    return e;
}

function previousElement (e) {
    do e = e.previousSibling;
    while (e !== null && e.nodeType != 1);
    return e;
}

if (!Array.indexOf) {
    Array.prototype.indexOf = function (o) {
        var i = this.length;
        while (i--) if (this[i] === o) return i;
        return -1;
    };
}

var RCNST = {};

RCNST.getDefault = function (changeType) {
    if (RCNSTDefaultToVisible)
        return RCNSTStartHidden.indexOf(changeType) == -1;
    else
        return RCNSTStartVisible.indexOf(changeType) != -1;
};

RCNST.classRegex = new RegExp('\\b' + RCNSTClass + '\\b', 'g');

RCNST.getChangeType = function (change) {
    var result;
    var lnks = change.getElementsByTagName('a');
    var lnk = lnks[0].firstChild.nodeValue;
    var isEdit = false;
    if (RCNST.enh) {
        if (lnks[0].parentNode.firstChild.nodeValue.indexOf('(') == -1)
            isEdit = true;
    } else {
        if (lnk == 'diff' || lnk == 'hist') {
            var isEdit = true;
            if (lnk == 'diff') lnk = 2;
            else lnk = 1; // new page
            lnk = lnks[lnk].firstChild.nodeValue;
        }
    }
    if (isEdit) {
        // work out namespace
        var colon = lnk.indexOf(':');
        if (colon == -1) result = RCNSTMain;
        else result = lnk.substring(0, colon);
    } else result = lnk; // log
    return result;
};

RCNST.toggle = function (changeType) {
    // show/hide changes of given type according to corresponding checkbox
    var show = RCNST.checkboxes[changeType].checked;
    var changes = RCNST.changes[changeType];
    if (show) var reg = RCNST.classRegex;
    for (var i = 0; i < changes.length; i++) {
        if (RCNST.enh) {
            var div = nextElement(changes[i]);
            if (div !== null && div.tagName.toLowerCase() == 'div') {
                // also do the same for the subchanges
                if (show)
                    // remove class
                    div.className = div.className.replace(reg, ' ');
                else div.className += ' ' + RCNSTClass; // add class
            }
        }
        if (show)
            // remove class
            changes[i].className = changes[i].className.replace(reg, ' ');
        else changes[i].className += ' ' + RCNSTClass; // add class
    }
};

RCNST.smartToggle = function (changeType, show) {
    // set changes of given type to given visibility if not already
    var c = RCNST.checkboxes[changeType];
    var shown = c.checked;
    if (shown != show) {
        c.checked = show;
        RCNST.toggle(changeType);
    }
};

RCNST.toggleAll = function () {
    // if any types are hidden, show them all, else hide them all
    var show = false;
    for (var changeType in RCNST.checkboxes) {
        if (!RCNST.checkboxes[changeType].checked) {
            show = true;
            break;
        }
    }
    for (var changeType in RCNST.checkboxes)
        RCNST.smartToggle(changeType, show);
    RCNST.updateHeadings();
};

RCNST.invert = function () {
    for (var changeType in RCNST.checkboxes)
        RCNST.smartToggle(changeType, !RCNST.checkboxes[changeType].checked);
    RCNST.updateHeadings();
};

RCNST.restore = function () {
    for (var changeType in RCNST.checkboxes)
        RCNST.smartToggle(changeType, RCNST.getDefault(changeType));
    RCNST.updateHeadings();
};

RCNST.updateHeadings = function () {
    // hide headings if all changes in them are hidden, else show them
    var enh = RCNST.enh;
    var reg = RCNST.classRegex;
    var h4s = document.getElementsByTagName('h4');
    for (var i = 0; i < h4s.length; i++) {
        var h4 = h4s[i];
        // check it's a valid header and get change list
        var section = nextElement(h4);
        if (section === null || section.tagName === undefined) continue;
        var sectionTag = section.tagName.toLowerCase();
        if (enh) {
            if (sectionTag != 'div') continue;
            var changes = getElementsByClassName(section, 'table',
                                                 'mw-enhanced-rc');
        } else {
            if (sectionTag != 'ul' || !hasClass(section, 'special')) continue;
            var changes = section.getElementsByTagName('li');
        }
        if (changes.length == 0) continue;
        // check changes under this heading
        var show = false;
        for (var j = 0; j < changes.length; j++) {
            if (changes[j].parentNode !== section) continue;
            if (!hasClass(changes[j], RCNSTClass)) {
                show = true;
                break;
            }
        }
        if (show) h4.className = h4.className.replace(reg, ' ');
        else if (!hasClass(h4, RCNSTClass)) h4.className += ' ' + RCNSTClass;
    }
};

RCNST.init = function () {
    if (!hasClass(document.body, 'page-Special_RecentChanges')
        && !hasClass(document.body, 'page-Special_Recentchanges')
        && !hasClass(document.body, 'page-Special_Watchlist'))
        return;

    // compile per-type lists of RC changes
    var changes = {};
    RCNST.changes = changes;
    var uls = getElementsByClassName(document, 'ul', 'special');
    if (uls.length) {
        // normal RC
        RCNST.enh = false;
        for (var i = 0; i < uls.length; i++) {
            var lis = uls[i].getElementsByTagName('li');
            for (var j = 0; j < lis.length; j++) {
                var change = lis[j];
                var changeType = RCNST.getChangeType(change);
                if (changes[changeType] === undefined)
                    changes[changeType] = [change];
                else changes[changeType].push(change);
            }
        }
    } else {
        var tbls = getElementsByClassName(document, 'table', 'mw-enhanced-rc');
        if (tbls.length) {
            // enhanced RC
            RCNST.enh = true;
            for (var i = 0; i < tbls.length; i++) {
                var tbl = tbls[i];
                var tds = tbl.getElementsByTagName('td');
                if (!hasClass(tds[0], 'mw-enhanced-rc')) continue;
                var changeType = RCNST.getChangeType(tds[1]);
                if (changes[changeType] === undefined)
                    changes[changeType] = [tbl];
                else changes[changeType].push(tbl);
            }
        } else return; // no changes
    }

    // add controls to page
    var rcoptions = getElementsByClassName(document, '*', 'rcoptions');
    if (rcoptions.length) {
        // put with the other controls
        rcoptions[0].appendChild(document.createElement('hr'));
        var controls = document.createElement('span');
        rcoptions[0].appendChild(controls);
    } else {
        // put before the first day heading
        var h4s = document.getElementsByTagName('h4');
        for (var i = 0; i < h4s.length; i++) {
            var e = nextElement(h4s[i]);
            if (e !== null && e.tagName.toLowerCase() == 'ul'
                && hasClass(e, 'special')) {
                var controls = document.createElement('div');
                h4s[i].parentNode.insertBefore(controls, h4s[i]);
                break;
            }
        }
        // if there are no day headings, I have no idea what's going on, so
        // just don't show them
    }
    controls.id = RCNSTControlsID;

    // create ordered list of change types
    var changeTypes = [];
    // namespaces first
    var nsSelect = document.getElementById('namespace');
    if (nsSelect !== null) {
        var optionList = nsSelect.getElementsByTagName('option');
        for (var i = 0; i < optionList.length; i++) {
            if (optionList[i].value == '0') var ns = RCNSTMain;
            else var ns = optionList[i].firstChild.nodeValue;
            if (changes[ns] !== undefined) changeTypes.push(ns);
        }
    }
    // then logs
    var toSort = [];
    for (changeType in changes)
        if (changeTypes.indexOf(changeType) == -1) toSort.push(changeType);
    toSort.sort();
    changeTypes = changeTypes.concat(toSort);

    // then create checkboxes in this order
    var checkboxes = {};
    RCNST.checkboxes = checkboxes;
    for (var i = 0; i < changeTypes.length; i++) {
        var c = document.createElement('input');
        checkboxes[changeTypes[i]] = c;
        c.type = 'checkbox';
        c.onclick = function () {
            RCNST.toggle(this.nextSibling.firstChild.nodeValue);
            RCNST.updateHeadings();
        };
        c.checked = true;
        controls.appendChild(c);
        var label = document.createElement('label');
        label.appendChild(document.createTextNode(changeTypes[i]));
        controls.appendChild(label);
    }
    RCNST.restore();
    // add buttons to controls
    function addButton (label, onclick) {
        var button = document.createElement('input');
        button.type = 'button';
        button.value = label;
        button.onclick = RCNST[onclick];
        controls.appendChild(button);
    }
    addButton(RCNSTToggleAllLabel, 'toggleAll');
    addButton(RCNSTInvertLabel, 'invert');
    addButton(RCNSTRestoreLabel, 'restore');

    if (RCNST.enh) {
        // replace enhanced RC event for the arrow things
        window.toggleVisibility = function(idNumber) {
            var openarrow = document.getElementById("mw-rc-openarrow-"+idNumber);
            var closearrow = document.getElementById("mw-rc-closearrow-"+idNumber);
            var subentries = document.getElementById("mw-rc-subentries-"+idNumber);
            if (openarrow.className == 'mw-changeslist-expanded') {
                openarrow.className = 'mw-changeslist-hidden';
                closearrow.className = 'mw-changeslist-expanded';
                subentries.className = subentries.className.replace(
                    /\bmw-changeslist-hidden\b/, 'mw-changeslist-expanded');
            } else {
                openarrow.className = 'mw-changeslist-expanded';
                closearrow.className = 'mw-changeslist-hidden';
                subentries.className = subentries.className.replace(
                    /\bmw-changeslist-expanded\b/, 'mw-changeslist-hidden');
            }
        };
    }
};

// initialise on page load
if (window.domReady !== undefined) domReady(RCNST.init);
else addOnloadHook(RCNST.init);