User:Nardog/CatChangesViewer-core.js
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
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', 'user.options'
], 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:var(--background-color-success-subtle,#d5fdf4)} .catchangesviewer-removal{background:var(--background-color-error-subtle,#fee7e6)} .catchangesviewer-table td:empty::after{content:"\\a0"}');
let api = new mw.Api({
ajax: { headers: { 'Api-User-Agent': 'CatChangesViewer (https://en.wikipedia.org/wiki/User:Nardog/CatChangesViewer)' } }
});
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);
class CatChangesSearch {
constructor() {
this.options = getOptions();
this.params = Object.assign({
action: 'query',
list: 'recentchanges',
rctype: 'categorize',
rctitle: mw.config.get('wgPageName'),
rcprop: 'ids|timestamp|comment|user|flags',
formatversion: 2
}, this.options);
this.rcs = [];
this.latest = {};
this.curPage = 0;
this.titles = {};
this.newRcs = [];
}
load(isRefresh) {
isRefresh = isRefresh && !!this.rcs.length;
if (isRefresh) {
this.params.rcdir = 'newer';
this.params.rclimit = Math.min(limitInput.getNumericValue() + 1, 500);
this.params.rccontinue = this.rcs[0].timestamp.replace(/\D/g, '') + '|' + this.rcs[0].revid;
} else {
delete this.params.rcdir;
this.params.rclimit = limitInput.getNumericValue();
this.params.rccontinue = this.rccontinue;
}
this.setDisabledAll(true);
$error.empty();
let msgPromise = api.loadMessagesIfMissing(msgKeys);
api.get(this.params).then(response => {
if (!isRefresh) {
this.rccontinue = (response.continue || {}).rccontinue;
this.complete = !this.rccontinue && response.batchcomplete;
}
return msgPromise.then(() => {
this.processChanges(isRefresh, response.query.recentchanges);
});
}).catch(e => {
$error.text(((e || {}).error || {}).info || e);
}).always(() => {
this.setDisabledAll(false);
this.resetNavButtons();
this.updateButton();
refreshButton.toggle(true);
});
}
updateButton() {
button.setLabel(
this.rcs.length
? this.complete ? 'No more results' : 'Load more'
: this.complete ? 'No results' : 'Search'
).setDisabled(this.complete);
}
processChanges(isRefresh, rcs = []) {
if (isRefresh && (rcs[0] || {}).revid === this.rcs[0].revid) {
rcs.shift();
}
if (!rcs.length) return;
rcs.forEach(rc => {
if (!rc.comment) return;
let page = rc.comment.match(/\[\[:?([^|\]]+)\]\]/)[1];
if (rc.comment.includes(']] added to category')) {
rc.action = 'addition';
} else if (rc.comment.includes(']] removed from category')) {
rc.action = 'removal';
} else if (addedKeys.some(key => rc.comment === mw.msg(key, page))) {
rc.action = 'addition';
} else if (removedKeys.some(key => rc.comment === mw.msg(key, page))) {
rc.action = 'removal';
}
if (this.latest.hasOwnProperty(page)) {
if (isRefresh) {
this.latest[page].duplicate = true;
this.latest[page] = rc;
} else {
rc.duplicate = true;
}
} else {
this.latest[page] = rc;
}
this.rcs[isRefresh ? 'unshift' : 'push'](rc);
this.addRow(rc, page);
});
this.initNav();
this.queryTitles(
Object.entries(this.titles)
.filter(([k, v]) => !v.processed).map(([k]) => k)
);
}
initNav() {
let rcsToShow = hideAdditionsCheckbox.isSelected()
? this.rcs.filter(rc => rc.action !== 'addition')
: hideRemovalsCheckbox.isSelected()
? this.rcs.filter(rc => rc.action !== 'removal')
: this.rcs;
if (hideDuplicatesCheckbox.isSelected()) {
rcsToShow = rcsToShow.filter(rc => !rc.duplicate);
}
this.visibleRows = rcsToShow.map(rc => rc.$row[0]);
this.pageCount = Math.ceil(this.visibleRows.length / perPageNum) || 1;
let z = this.rcs.length > perPageNum
? perPageNum * this.pageCount - this.visibleRows.length
: this.rcs.length - this.visibleRows.length;
for (let i = 0; i < z; i++) {
this.visibleRows.push(
$('<tr>').append('<td>', '<td>', '<td>', '<td>', '<td>')[0]
);
}
if (!this.$table) {
this.$tbody = $('<tbody>');
this.$table = $('<table>').addClass('wikitable catchangesviewer-table').append(
$('<thead>').append(
$('<tr>').append(
$('<th>').text('±'),
$('<th>').text('Date'),
$('<th>').text('Page'),
$('<th>').text('User'),
$('<th>').text('Bot')
)
),
this.$tbody
);
}
this.setPage();
navLayout.toggle(true).$element.before(this.$table);
}
setPage(increment) {
if (this.pageCount > 1) {
if (increment === 'first') {
this.curPage = 0;
} else if (increment === 'last') {
this.curPage = this.pageCount - 1;
} else if (increment) {
this.curPage += increment;
if (this.curPage < 0) {
this.curPage = this.pageCount - 1;
}
if (this.curPage > this.pageCount - 1) {
this.curPage = 0;
}
} else if (this.curPage > this.pageCount - 1) {
this.curPage = this.pageCount - 1;
}
} else {
this.curPage = 0;
}
let start = this.curPage * perPageNum;
this.$tbody.html(
this.visibleRows.slice(start, start + perPageNum)
);
navLabel.setLabel(this.curPage + 1 + ' / ' + this.pageCount);
this.resetNavButtons();
}
resetNavButtons() {
firstButton.setDisabled(this.curPage === 0);
prevButton.setDisabled(this.pageCount < 2);
nextButton.setDisabled(this.pageCount < 2);
lastButton.setDisabled(this.curPage === this.pageCount - 1);
}
setDisabledAll(disabled) {
[
limitInput, filtersButton, userInput, untilInput, button,
refreshButton, firstButton, prevButton, nextButton, lastButton,
hideAdditionsCheckbox, hideRemovalsCheckbox, hideDuplicatesCheckbox
].forEach(widget => {
widget.setDisabled(disabled);
});
}
addRow(rc, page) {
let symbol = rc.action === 'addition' ? '+' : rc.action === 'removal' ? '−' : '?';
rc.$row = $('<tr>').attr({
class: rc.action ? 'catchangesviewer-' + rc.action : null,
'data-mw-revid': rc.revid
}).append(
$('<td>').text(symbol),
$('<td>').append(
$('<a>').attr('href', mw.util.getUrl(page, {
oldid: rc.revid
})).text(rc.timestamp),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
$('<a>').attr('href', mw.util.getUrl(page, {
diff: rc.revid
})).text('diff')
),
$('<span>').append(
$('<a>').attr('href', mw.util.getUrl(page, {
curid: rc.pageid,
action: 'history'
})).text('hist')
)
)
),
$('<td>').append(this.makeLink(page)),
$('<td>').append(
this.makeLink(
(rc.anon || rc.temp ? 'Special:Contributions/' : 'User:') + rc.user,
rc.user,
rc.temp
),
' ',
$('<span>').addClass('mw-changeslist-links').append(
$('<span>').append(
this.makeLink('User talk:' + rc.user, 'talk')
),
!rc.anon && !rc.temp && $('<span>').append(
this.makeLink('Special:Contributions/' + rc.user, 'contribs')
)
)
),
$('<td>').text(rc.bot ? 'Yes' : 'No')
);
this.newRcs.push(rc);
}
makeLink(title, text, isTemp) {
let obj;
if (this.titles.hasOwnProperty(title)) {
obj = this.titles[title];
} else {
obj = { links: [] };
this.titles[title] = obj;
}
let params = obj.red && { action: 'edit', redlink: 1 } ||
obj.redirect && { redirect: 'no' };
let $link = $('<a>').attr({
href: mw.util.getUrl(obj.canonical || title, params),
title: obj.canonical || title
}).addClass(obj.classes).text(text || title);
if (isTemp) {
$link.addClass('mw-tempuserlink');
this.hasTemps = true;
}
if (!obj.processed) {
obj.links.push($link[0]);
}
return $link;
}
queryTitles(titles) {
if (!titles.length) {
this.fireHook();
return;
}
let curTitles = titles.slice(0, 50);
curTitles.forEach(title => {
this.titles[title].processed = true;
});
api.post({
action: 'query',
titles: curTitles,
prop: 'info',
inprop: 'linkclasses',
inlinkcontext: mw.config.get('wgPageName'),
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
}).always(response => {
let query = response && response.query;
if (!query) {
this.fireHook();
return;
}
(query.normalized || []).forEach(entry => {
if (!this.titles.hasOwnProperty(entry.from)) return;
let obj = this.titles[entry.from];
obj.canonical = entry.to;
this.titles[entry.to] = obj;
});
(query.pages || []).forEach(page => {
if (!this.titles.hasOwnProperty(page.title)) return;
let obj = this.titles[page.title];
let classes = page.linkclasses || [];
if (page.missing && !page.known) {
classes.push('new');
obj.red = true;
} else if (classes.includes('mw-redirect')) {
obj.redirect = true;
}
if (classes.length) {
obj.classes = classes;
}
});
curTitles.forEach(title => {
let obj = this.titles[title];
let $links = $(obj.links).addClass(obj.classes);
$links.attr('href', mw.util.getUrl(
obj.canonical || title,
obj.red && { action: 'edit', redlink: 1 }
));
if (obj.canonical) {
$links.attr('title', obj.canonical);
}
delete obj.links;
});
this.queryTitles(titles.slice(50));
});
}
fireHook() {
if (!this.newRcs.length) return;
let tempRows = this.newRcs.map(rc => rc.$row.clone()[0]);
let $tempTable = $('<table>').hide().append(tempRows)
.insertAfter(this.$table);
mw.hook('wikipage.content').fire($tempTable);
if (this.hasTemps && mw.loader.getState('ext.checkUser') === 'ready') {
try {
mw.loader.moduleRegistry['ext.checkUser']
.packageExports['temporaryaccount/initOnLoad.js']();
} catch (e) {}
}
this.newRcs.forEach((rc, i) => {
rc.$row.html(tempRows[i].children);
});
$tempTable.remove();
this.newRcs = [];
this.hasTemps = false;
}
destroy() {
if (this.$table) {
this.$table.remove();
}
navLayout.toggle(false);
}
}
let curSearch;
let getOptions = () => {
let options = {};
Object.entries(filters).forEach(([k, v]) => {
if (v.widget.getIcon() === 'check') {
if (v.input) {
let value = v.input.getValue();
if (value) {
options[k] = value;
}
} else {
options.rcshow = options.rcshow || [];
options.rcshow.push(k);
}
}
});
return options;
};
let isModified = () => {
if (!curSearch) return false;
let options = getOptions();
return ['rcshow', 'rcuser', 'rcexcludeuser', 'rcstart'].some(k => (
String(options[k]) !== String(curSearch.options[k])
));
};
let updateButton = () => {
if (isModified()) {
button.setLabel('Search').setDisabled(false);
} else if (curSearch) {
curSearch.updateButton();
}
};
let perPageNum = window.catchangesviewerChangesPerPage || 20;
let limitInput = new OO.ui.NumberInputWidget({
max: 500,
min: 1,
required: true,
showButtons: false,
title: 'Number of changes to load (1–500)',
value: window.catchangesviewerDefaultLimit || 50
}).setIndicator();
let userInput = new mw.widgets.UserInputWidget({
placeholder: 'User'
}).on('change', updateButton).toggle();
let untilInput = new mw.widgets.datetime.DateTimeInputWidget({
clearable: false,
min: new Date(Date.now() - 2592000000)
}).on('change', updateButton).toggle();
let filters = {
'!anon': {
widget: new OO.ui.MenuOptionWidget({
data: '!anon',
label: 'Registered only',
icon: 'none'
}),
incompatibleWith: 'anon'
},
anon: {
widget: new OO.ui.MenuOptionWidget({
data: 'anon',
label: 'Unregistered only',
icon: 'none'
}),
incompatibleWith: '!anon'
},
'!bot': {
widget: new OO.ui.MenuOptionWidget({
data: '!bot',
label: 'No bots',
icon: 'none'
}),
incompatibleWith: 'bot'
},
bot: {
widget: new OO.ui.MenuOptionWidget({
data: 'bot',
label: 'Bots only',
icon: 'none'
}),
incompatibleWith: '!bot'
},
rcuser: {
widget: new OO.ui.MenuOptionWidget({
data: 'rcuser',
label: 'This user:',
icon: 'none'
}),
incompatibleWith: 'rcexcludeuser',
input: userInput
},
rcexcludeuser: {
widget: new OO.ui.MenuOptionWidget({
data: 'rcexcludeuser',
label: 'Not this user:',
icon: 'none'
}),
incompatibleWith: 'rcuser',
input: userInput
},
rcstart: {
widget: new OO.ui.MenuOptionWidget({
data: 'rcstart',
label: 'Until:',
icon: 'none'
}),
input: untilInput
}
};
let filtersButton = new OO.ui.ButtonMenuSelectWidget({
icon: 'funnel',
menu: { items: Object.values(filters).map(o => o.widget) },
invisibleLabel: true,
label: 'Filters'
});
filtersButton.getMenu().on('choose', option => {
let data = filters[option.getData()];
if (option.getIcon() === 'none') {
option.setIcon('check');
if (data.incompatibleWith) {
filters[data.incompatibleWith].widget.setIcon('none');
}
filtersButton.setIndicator('required');
if (data.input) {
data.input.toggle(true);
}
} else {
option.setIcon('none');
if (!Object.values(filters).some(o => o.widget.getIcon() === 'check')) {
filtersButton.setIndicator();
}
if (data.input) {
data.input.toggle(false);
}
}
updateButton();
});
let button = new OO.ui.ButtonInputWidget({
flags: ['primary', 'progressive'],
label: 'Search',
type: 'submit'
}).on('click', () => {
if (curSearch) {
if (isModified()) {
curSearch.destroy();
curSearch = new CatChangesSearch();
}
} else {
curSearch = new CatChangesSearch();
}
curSearch.load();
});
let refreshButton = new OO.ui.ButtonWidget({
icon: 'reload',
invisibleLabel: true,
label: 'Load new'
}).toggle().on('click', () => {
curSearch.load(true);
});
let form = new OO.ui.FormLayout({
classes: ['oo-ui-horizontalLayout'],
items: [
limitInput, filtersButton, userInput, untilInput, button, refreshButton
]
});
let navLabel = new OO.ui.LabelWidget();
let firstButton = new OO.ui.ButtonWidget({
icon: 'first',
invisibleLabel: true,
label: 'Newest ' + perPageNum
}).on('click', () => {
curSearch.setPage('first');
});
let prevButton = new OO.ui.ButtonWidget({
icon: 'previous',
invisibleLabel: true,
label: 'Newer ' + perPageNum
}).on('click', () => {
curSearch.setPage(-1);
});
let nextButton = new OO.ui.ButtonWidget({
icon: 'next',
invisibleLabel: true,
label: 'Older ' + perPageNum
}).on('click', () => {
curSearch.setPage(1);
});
let lastButton = new OO.ui.ButtonWidget({
icon: 'last',
invisibleLabel: true,
label: 'Oldest ' + perPageNum
}).on('click', () => {
curSearch.setPage('last');
});
let hideAdditionsCheckbox = new OO.ui.CheckboxInputWidget().on('change', selected => {
if (selected) {
hideRemovalsCheckbox.setSelected(false, true);
}
curSearch.initNav();
});
let hideRemovalsCheckbox = new OO.ui.CheckboxInputWidget().on('change', selected => {
if (selected) {
hideAdditionsCheckbox.setSelected(false, true);
}
curSearch.initNav();
});
let hideDuplicatesCheckbox = new OO.ui.CheckboxInputWidget().on('change', () => {
curSearch.initNav();
});
let navLayout = new OO.ui.HorizontalLayout({
items: [
navLabel,
new OO.ui.ButtonGroupWidget({
items: [firstButton, prevButton, nextButton, lastButton]
}),
new OO.ui.HorizontalLayout({
items: [
new OO.ui.LabelWidget({ label: 'Hide:' }),
new OO.ui.FieldLayout(hideAdditionsCheckbox, {
align: 'inline',
label: 'Additions'
}),
new OO.ui.FieldLayout(hideRemovalsCheckbox, {
align: 'inline',
label: 'Removals'
}),
new OO.ui.FieldLayout(hideDuplicatesCheckbox, {
align: 'inline',
label: 'Duplicates'
})
]
})
]
}).toggle();
let $error = $('<div>');
let $div = $('<div>').addClass('catchangesviewer').append(
$('<h2>').text('Recent changes'), navLayout.$element, form.$element, $error
);
$(() => {
$('.mw-category-generated').first().before($div);
});
if (Number(mw.user.options.get('checkuser-temporary-account-enable'))) {
mw.loader.load('ext.checkUser');
}
});