User:Nardog/CatChangesViewer-core.js

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: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(					$(' ').append(' ', ' ', ' ', ' ', ' ')[0]				); }			if (!this.$table) { this.$tbody = $(' '); this.$table = $(' ').addClass('wikitable catchangesviewer-table').append(					$(' ').append( $(' ').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' ? '−' : '?';			rc.$row = $(' ').addClass(rc.action && 'catchangesviewer-' + rc.action).append(				$(' ').text(symbol),				$(' ').append( $('').attr('href', mw.util.getUrl(page, { oldid: rc.revid })).text(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( this.makeLink((rc.anon ? 'Special:Contributions/' : 'User:') + rc.user, rc.user), ' ',					$(' ').addClass('mw-changeslist-links').append(						$(' ').append( this.makeLink('User talk:' + rc.user, 'talk') ),						!rc.anon && $(' ').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); this.newRcs.forEach((rc, i) => {				rc.$row.html(tempRows[i].children);			}); $tempTable.remove; this.newRcs = []; }		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: 'No IPs',				icon: 'none'			}), incompatibleWith: 'anon' },		anon: { widget: new OO.ui.MenuOptionWidget({				data: 'anon',				label: 'IPs 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);	}); });