User:Nardog/CatChangesViewer.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | This user script seems to have a documentation page at User:Nardog/CatChangesViewer. |
mw.config.get('wgNamespaceNumber') === 14 && mw.config.get('wgAction') === 'view' &&
mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'oojs-ui-widgets', 'mediawiki.widgets',
'mediawiki.widgets.UserInputWidget', 'mediawiki.widgets.datetime',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-movement',
'mediawiki.interface.helpers.styles'
], function catChangesViewer() {
mw.loader.addStyleTag('.catchangesviewer .oo-ui-numberInputWidget{width:4em} .catchangesviewer .oo-ui-numberInputWidget input{text-align:center} .catchangesviewer .oo-ui-menuSelectWidget, .catchangesviewer .mw-widgets-datetime-dateTimeInputWidget{width:min-content} .catchangesviewer .mw-widget-userInputWidget{width:8em} .catchangesviewer .oo-ui-fieldLayout-align-inline{vertical-align:top} .catchangesviewer-table{white-space:nowrap} .catchangesviewer-addition{background:#e6ffe6} .catchangesviewer-removal{background:#ffe6e6} .catchangesviewer-table td:empty::after{content:"\\00a0"}');
let defLimit = window.catchangesviewerDefaultLimit || 50;
let perPageNum = window.catchangesviewerChangesPerPage || 20;
let limitInput = new OO.ui.NumberInputWidget({
required: true,
min: 1,
max: 500,
value: defLimit,
showButtons: false,
title: 'Number of changes to load (1–500)'
}).setIndicator();
let filterOptions = {
'!anon': new OO.ui.MenuOptionWidget({
data: { incompatibleWith: 'anon' },
label: 'No IPs',
icon: 'none'
}),
anon: new OO.ui.MenuOptionWidget({
data: { incompatibleWith: '!anon' },
label: 'IPs only',
icon: 'none'
}),
'!bot': new OO.ui.MenuOptionWidget({
data: { incompatibleWith: 'bot' },
label: 'No bots',
icon: 'none'
}),
bot: new OO.ui.MenuOptionWidget({
data: { incompatibleWith: '!bot' },
label: 'Bots only',
icon: 'none'
}),
rcuser: new OO.ui.MenuOptionWidget({
data: { param: 'rcuser', incompatibleWith: 'rcexcludeuser', input: 'user' },
label: 'This user:',
icon: 'none'
}),
rcexcludeuser: new OO.ui.MenuOptionWidget({
data: { param: 'rcexcludeuser', incompatibleWith: 'rcuser', input: 'user' },
label: 'Not this user:',
icon: 'none'
}),
rcstart: new OO.ui.MenuOptionWidget({
data: { param: 'rcstart', input: 'until' },
label: 'Until:',
icon: 'none'
})
};
let now = Date.now();
let filterInputs = {
user: new mw.widgets.UserInputWidget({
placeholder: 'User'
}).toggle(),
until: new mw.widgets.datetime.DateTimeInputWidget({
min: new Date(now - 2592000000), // 30 days ago
clearable: false
}).toggle()
};
let filtersButton = new OO.ui.ButtonMenuSelectWidget({
icon: 'funnel',
title: 'Filters',
menu: { items: Object.values(filterOptions) }
});
let filtersMenu = filtersButton.getMenu();
let filtersLayout = new OO.ui.HorizontalLayout({
items: [filtersButton, filterInputs.user, filterInputs.until]
});
let button = new OO.ui.ButtonInputWidget({
label: 'Search',
flags: ['primary', 'progressive'],
type: 'submit'
});
let refreshButton = new OO.ui.ButtonWidget({
icon: 'reload',
title: 'Load new',
disabled: true
});
let buttonsLayout = new OO.ui.HorizontalLayout({
items: [button, refreshButton]
});
let form = new OO.ui.FormLayout({
items: [limitInput, filtersLayout, buttonsLayout],
classes: ['oo-ui-horizontalLayout']
});
let $div = $('<div>').addClass('catchangesviewer').append(form.$element);
let $table, $tbody, $error = $('<div>');
let navLabel = new OO.ui.LabelWidget();
let firstButton = new OO.ui.ButtonWidget({
icon: 'first',
title: 'Newest ' + perPageNum
});
let prevButton = new OO.ui.ButtonWidget({
icon: 'previous',
title: 'Newer ' + perPageNum
});
let nextButton = new OO.ui.ButtonWidget({
icon: 'next',
title: 'Older ' + perPageNum
});
let lastButton = new OO.ui.ButtonWidget({
icon: 'last',
title: 'Oldest ' + perPageNum
});
let hideAdditionsCheckbox = new OO.ui.CheckboxInputWidget();
let hideRemovalsCheckbox = new OO.ui.CheckboxInputWidget();
let hideDuplicatesCheckbox = new OO.ui.CheckboxInputWidget();
let hideLayout = new OO.ui.HorizontalLayout({
items: [
new OO.ui.LabelWidget({ label: 'Hide:' }),
new OO.ui.FieldLayout(
hideAdditionsCheckbox,
{ label: 'Additions', align: 'inline' }
),
new OO.ui.FieldLayout(
hideRemovalsCheckbox,
{ label: 'Removals', align: 'inline' }
),
new OO.ui.FieldLayout(
hideDuplicatesCheckbox,
{ label: 'Duplicates', align: 'inline' }
)
]
});
let navLayout = new OO.ui.HorizontalLayout({
items: [
navLabel,
new OO.ui.ButtonGroupWidget({
items: [firstButton, prevButton, nextButton, lastButton]
}),
hideLayout
]
}).toggle();
let args = {
action: 'query',
list: 'recentchanges',
rctype: 'categorize',
rctitle: mw.config.get('wgPageName'),
rcprop: 'ids|timestamp|comment|user|flags',
formatversion: 2
};
let prevArgs, modified = new Set(), prevLabel, prevDisabled;
let setArg = (k, v) => {
if (v) {
args[k] = v;
} else {
delete args[k];
}
if (prevArgs) {
if (args[k] == prevArgs[k]) {
modified.delete(k);
} else {
modified.add(k);
}
if (modified.size) {
button.setLabel('Search').setDisabled(false);
} else {
button.setLabel(prevLabel).setDisabled(prevDisabled);
}
}
};
filterInputs.user.on('change', v => {
setArg(filterOptions.rcuser.getIcon() === 'check' ? 'rcuser' : 'rcexcludeuser', v);
});
filterInputs.until.on('change', setArg, ['rcstart']);
filtersMenu.on('choose', option => {
let data = option.getData();
if (option.getIcon() === 'none') {
option.setIcon('check');
if (data.incompatibleWith) filterOptions[data.incompatibleWith].setIcon('none');
filtersButton.setIndicator('required');
if (data.input) {
filterInputs[data.input].toggle(true);
if (data.incompatibleWith) setArg(data.incompatibleWith);
setArg(data.param, filterInputs[data.input].getValue());
}
} else {
option.setIcon('none');
if (!filtersMenu.getItems().some(item => item.getIcon() === 'check')) {
filtersButton.setIndicator();
}
if (data.input) {
filterInputs[data.input].toggle(false);
setArg(data.param);
}
}
if (!data.input) {
setArg(
'rcshow',
Object.keys(filterOptions).filter(k => {
let o = filterOptions[k];
return o.getIcon() === 'check' && !o.getData().input;
}).join('|')
);
}
});
let setDisabledAll = disabled => {
[
limitInput, filtersButton, filtersMenu, filterInputs.user,
filterInputs.until, button, refreshButton, firstButton, prevButton,
nextButton, lastButton, hideAdditionsCheckbox, hideRemovalsCheckbox,
hideDuplicatesCheckbox
].forEach(widget => {
widget.setDisabled(disabled);
});
if (!disabled) resetNavButtons();
};
let api, recentchanges = [], additions = [], removals = [], newest = {}, refArgs;
let load = isRefresh => {
if (isRefresh) {
prevDisabled = button.isDisabled();
if (!refArgs) refArgs = Object.assign({ rcdir: 'newer' }, prevArgs);
refArgs.rclimit = limitInput.getNumericValue() + 1;
refArgs.rccontinue = recentchanges[0].timestamp.replace(/\D/g, '') + '|' + recentchanges[0].revid;
} else {
prevLabel = button.getLabel();
button.setLabel('Loading...');
args.rclimit = limitInput.getNumericValue();
if (modified.size) {
delete args.rccontinue;
recentchanges = [];
additions = [];
removals = [];
newest = {};
refArgs = null;
$table.detach();
$tbody.empty();
navLayout.toggle(false);
modified.clear();
}
prevArgs = Object.assign({}, args);
}
setDisabledAll(true);
$error.empty();
if (!api) api = new mw.Api({
ajax: { headers: { 'Api-User-Agent': 'CatChangesViewer (https://en.wikipedia.org/wiki/User:Nardog/CatChangesViewer)' } }
});
api.get(isRefresh ? refArgs : args).always((response, errorObj) => {
setDisabledAll(false);
if (isRefresh) button.setDisabled(prevDisabled);
let errorMsg = ((errorObj || {}).error || {}).info;
if (!response || typeof response === 'string' || errorMsg) {
button.setLabel(prevLabel);
$error.text(errorMsg ? 'Error: ' + errorMsg : 'Unknown error').appendTo($div);
return;
}
let newRc = ((response || {}).query || {}).recentchanges || [];
let rccontinue = ((response || {}).continue || {}).rccontinue;
if (!isRefresh) {
args.rccontinue = rccontinue;
if (rccontinue) {
button.setLabel('Load more');
} else if (response.batchcomplete) {
let rcCount = recentchanges.length + newRc.length;
button.setLabel(rcCount ? 'No more results' : 'No results').setDisabled(true);
}
prevLabel = button.getLabel();
prevDisabled = button.isDisabled();
}
processRc(newRc, isRefresh);
if (isRefresh && !rccontinue || !isRefresh && !prevArgs.rccontinue) {
refreshButton.setDisabled(true);
if (!prevArgs.rcstart || prevArgs.rcstart > new Date().toISOString()) {
setTimeout(() => {
refreshButton.setDisabled(false);
}, 5000);
}
}
});
};
let msgKeys = mw.config.get('wgContentLanguage') === 'en' ? [] : [
'recentchanges-page-added-to-category',
'recentchanges-page-added-to-category-bundled',
'recentchanges-page-removed-from-category',
'recentchanges-page-removed-from-category-bundled'
];
let addedKeys = msgKeys.slice(0, 2), removedKeys = msgKeys.slice(2);
let processRc = (newRc, isRefresh) => {
if (isRefresh && (newRc[0] || {}).revid === recentchanges[0].revid) newRc.shift();
if (!newRc.length) return;
api.loadMessagesIfMissing(msgKeys).always(() => {
let method = isRefresh ? 'unshift' : 'push';
newRc.forEach(rc => {
if (!rc.comment) return;
let page = rc.comment.match(/\[\[:?([^\]]+)\]\]/)[1];
let action, actionClass;
if (rc.comment.includes(']] added to category')) {
action = true;
} else if (rc.comment.includes(']] removed from category')) {
action = false;
} else if (addedKeys.some(key => rc.comment === mw.msg(key, page))) {
action = true;
} else if (removedKeys.some(key => rc.comment === mw.msg(key, page))) {
action = false;
}
if (action === true) {
action = '+';
actionClass = 'catchangesviewer-addition';
additions[method](rc);
} else if (action === false) {
action = '−';
actionClass = 'catchangesviewer-removal';
removals[method](rc);
} else {
action = '?';
}
rc.$row = $('<tr>').addClass(actionClass).append(
$('<td>').text(action),
$('<td>').append(
$('<a>')
.attr('href', mw.util.getUrl(page, { oldid: rc.revid }))
.text(rc.timestamp),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<a>')
.attr('href', mw.util.getUrl(page, { diff: rc.revid }))
.text('diff')
.wrap('<span>').parent(),
$('<a>')
.attr('href', mw.util.getUrl(page, { curid: rc.pageid, action: 'history' }))
.text('hist')
.wrap('<span>').parent()
)
),
$('<td>').append(
$('<a>').attr('href', mw.util.getUrl(page)).text(page)
),
$('<td>').append(
$('<a>')
.attr('href', mw.util.getUrl((rc.anon ? 'Special:Contributions/' : 'User:') + rc.user))
.text(rc.user),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<a>')
.attr('href', mw.util.getUrl('User talk:' + rc.user))
.text('talk')
.wrap('<span>').parent(),
!rc.anon && $('<a>')
.attr('href', mw.util.getUrl('Special:Contributions/' + rc.user))
.text('contribs')
.wrap('<span>').parent()
)
),
$('<td>').text(rc.bot ? 'Yes' : 'No')
);
if (newest.hasOwnProperty(page)) {
if (isRefresh) {
newest[page].duplicate = true;
newest[page] = rc;
} else {
rc.duplicate = true;
}
} else {
newest[page] = rc;
}
recentchanges[method](rc);
});
initializeNavigation();
});
};
let currentPage = 0, pageCount, visibleRows;
let initializeNavigation = () => {
let rcToShow = hideAdditionsCheckbox.isSelected() ? removals :
hideRemovalsCheckbox.isSelected() ? additions :
recentchanges;
if (hideDuplicatesCheckbox.isSelected()) {
rcToShow = rcToShow.filter(rc => !rc.duplicate);
}
visibleRows = rcToShow.map(rc => rc.$row[0]);
pageCount = Math.ceil(visibleRows.length / perPageNum) || 1;
let z = recentchanges.length > perPageNum
? perPageNum * pageCount - visibleRows.length
: recentchanges.length - visibleRows.length;
for (let i = 0, j; i < z; i++) {
let $row = $('<tr>');
for (j = 0; j < 5; j++) $('<td>').appendTo($row);
visibleRows.push($row[0]);
}
if (!$table) {
$tbody = $('<tbody>');
$table = $('<table>').addClass('wikitable catchangesviewer-table').append(
$('<tr>').append(
$('<th>').text('±'),
$('<th>').text('Date'),
$('<th>').text('Page'),
$('<th>').text('User'),
$('<th>').text('Bot')
).wrap('<thead>').parent(),
$tbody
);
}
setPage();
$table.prependTo($div);
if (!navLayout.isVisible()) navLayout.toggle(true).$element.insertAfter($table);
};
let setPage = increment => {
if (pageCount > 1) {
if (increment === false) {
currentPage = 0;
} else if (increment === true) {
currentPage = pageCount - 1;
} else if (increment) {
currentPage += increment;
if (currentPage < 0) currentPage = pageCount - 1;
if (currentPage > pageCount - 1) currentPage = 0;
} else {
if (currentPage > pageCount - 1) currentPage = pageCount - 1;
}
} else {
currentPage = 0;
}
$tbody.empty();
let start = currentPage * perPageNum;
$tbody.append(visibleRows.slice(start, start + perPageNum));
navLabel.setLabel(currentPage + 1 + ' / ' + pageCount);
resetNavButtons();
};
let resetNavButtons = () => {
firstButton.setDisabled(currentPage === 0);
prevButton.setDisabled(pageCount < 2);
nextButton.setDisabled(pageCount < 2);
lastButton.setDisabled(currentPage === pageCount - 1);
};
firstButton.on('click', setPage, [false]);
prevButton.on('click', setPage, [-1]);
nextButton.on('click', setPage, [1]);
lastButton.on('click', setPage, [true]);
hideAdditionsCheckbox.on('change', selected => {
if (selected) hideRemovalsCheckbox.setSelected(false);
initializeNavigation();
});
hideRemovalsCheckbox.on('change', selected => {
if (selected) hideAdditionsCheckbox.setSelected(false);
initializeNavigation();
});
hideDuplicatesCheckbox.on('change', initializeNavigation);
button.on('click', load);
refreshButton.on('click', load, [true]);
$('<h2>').text('Recent changes').insertAfter('.mw-parser-output').after($div);
});