User:Nardog/CatChangesViewer-core.js

mw.loader.using([

'mediawiki.api', 'mediawiki.util', 'mediawiki.DateFormatter',

'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(require) {

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 { formatTimeAndDate } = require('mediawiki.DateFormatter');

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(

$('').append('', '', '', '', '')[0]

);

}

if (!this.$table) {

this.$tbody = $('');

this.$table = $('

').addClass('wikitable catchangesviewer-table').append(

$('

').append(

$('

').append(

$('

').attr({

class: rc.action ? 'catchangesviewer-' + rc.action : null,

'data-mw-revid': rc.revid

}).append(

$('

').text('±'),

$('

').text('Date'),

$('

').text('Page'),

$('

').text('User'),

$('

').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' ? '−' : '?';

let $userLink = this.makeLink(

(rc.anon || rc.temp ? 'Special:Contributions/' : 'User:') + rc.user,

rc.user

);

if (rc.temp) {

$userLink.addClass('mw-tempuserlink').attr('data-mw-target', rc.user);

this.hasTemps = true;

}

rc.$row = $('

').text(symbol),

$('

').append(

$('').attr('href', mw.util.getUrl(page, {

oldid: rc.revid

})).text(formatTimeAndDate(new Date(rc.timestamp))),

' ',

$('').addClass('mw-changeslist-links').append(

$('').append(

$('').attr('href', mw.util.getUrl(page, {

diff: rc.revid

})).text('diff')

),

$('').append(

$('').attr('href', mw.util.getUrl(page, {

curid: rc.pageid,

action: 'history'

})).text('hist')

)

)

),

$('

').append(this.makeLink(page)),

$('

').append(

$userLink,

' ',

$('').addClass('mw-changeslist-links').append(

$('').append(

this.makeLink('User talk:' + rc.user, 'talk')

),

!rc.anon && !rc.temp && $('').append(

this.makeLink('Special:Contributions/' + rc.user, 'contribs')

)

)

),

$('

').text(rc.bot ? 'Yes' : 'No')

);

this.newRcs.push(rc);

}

makeLink(title, text) {

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 = $('').attr({

href: mw.util.getUrl(obj.canonical || title, params),

title: obj.canonical || title

}).addClass(obj.classes).text(text || title);

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 = $('

').hide().append(tempRows)

.insertAfter(this.$table);

mw.hook('wikipage.content').fire($tempTable);

if (this.hasTemps && mw.loader.getState('ext.checkUser.tempAccounts') === 'ready') {

try {

mw.loader.moduleRegistry['ext.checkUser.tempAccounts']

.packageExports['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 = $('

');

let $div = $('

').addClass('catchangesviewer').append(

$('

').text('Recent changes'), navLayout.$element, form.$element, $error

);

$(() => {

$('.mw-category-generated').first().before($div);

});

if (mw.config.get('wgCheckUserCanAccessTemporaryAccountIPAddresses')) {

mw.loader.using('ext.checkUser.tempAccounts').then(() => {

try {

mw.loader.moduleRegistry['ext.checkUser.tempAccounts']

.packageExports['dispatcher.js']();

} catch (e) {}

});

}

});