מדיה ויקי:סקריפטים/107.js

מתוך ויקיפדיה, האנציקלופדיה החופשית

הערה: לאחר הפרסום, ייתכן שיהיה צורך לנקות את זיכרון המטמון (cache) של הדפדפן כדי להבחין בשינויים.

  • פיירפוקס / ספארי: להחזיק את המקש Shift בעת לחיצה על טעינה מחדש (Reload) או ללחוץ על צירוף המקשים Ctrl-F5 או Ctrl-R (במחשב מק: ⌘-R).
  • גוגל כרום: ללחוץ על צירוף המקשים Ctrl-Shift-R (במחשב מק: ⌘-Shift-R).
  • אינטרנט אקספלורר / אדג': להחזיק את המקש Ctrl בעת לחיצה על רענן (Refresh) או ללחוץ על צירוף המקשים Ctrl-F5.
  • אופרה: ללחוץ על Ctrl-F5.
/*


== Vandal Cleaner ==

This tool allows you to handle vandalism more easily.

Use it to quickly clean all actions made by a given vandal:
Block the vandal, rollback all edits, delete all pages, hide all edits –
all at the touch of a button.

See full documentation at:
[[:en:User:Guycn2/VandalCleaner]]

See also:
* [[MediaWiki:סקריפטים/107.css]] – for the corresponding style sheet
* [[MediaWiki:סקריפטים/107.js/config.js]] – for i18n and configuration

Skins supported:
Vector (both 2022 and 2010), Monobook, Timeless, and Minerva.
Also fully supported on the mobile interface.

Dependencies:
* mediawiki.api
* mediawiki.util
* user.options
* oojs-ui-core
* oojs-ui-windows
* oojs-ui.styles.icons-accessibility
* oojs-ui.styles.icons-alerts
* oojs-ui.styles.icons-editing-core
* oojs-ui.styles.icons-interactions
* oojs-ui.styles.icons-media
* oojs-ui.styles.icons-moderation


Written by: [[User:Guycn2]]

__________________________________________________


== סקריפט לטיפול מהיר בהשחתות ==

כלי זה מקל על הטיפול בטרולים ובמשחיתים.

ניתן להשתמש בו כדי לנקות מיידית את כל הפעולות שנעשו ע"י משחית מסוים:
חסימת המשחית, שחזור כל העריכות, מחיקת כל הדפים, הסתרת כל העריכות –
כל זאת בלחיצת כפתור.

ראו תיעוד מלא בדף:
[[עזרה:טיפול מהיר בהשחתות]]

ראו גם:
* [[מדיה ויקי:סקריפטים/107.css]] – לגיליון הסגנונות המשויך
* [[מדיה ויקי:סקריפטים/107.js/config.js]] – להודעות מערכת והגדרות

עיצובים נתמכים:
וקטור (2022 ו־2010), מונובוק, מחוץ לזמן, מינרווה נויה.
הסקריפט נתמך במלואו גם בממשק למכשירים ניידים.


נכתב ע"י: [[משתמש:Guycn2]]


*/

( async () => {
	
	'use strict';
	
	const vandal = mw.config.get( 'wgRelevantUserName' );
	
	const editor = mw.config.get( 'wgUserName' );
	
	if (
		mw.config.get( 'vandalCleanerLoaded' ) ||
		!vandal ||
		mw.config.get( 'wgCanonicalSpecialPageName' ) !== 'Contributions' ||
		vandal === editor
	) {
		return;
	}
	
	mw.config.set( 'vandalCleanerLoaded', true );
	
	await mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] );
	
	const isAnon = mw.util.isIPAddress( vandal );
	
	const api = new mw.Api();
	
	const vcData = {};
	
	async function checkInitData() {
		
		const params = {
			list: 'users',
			usprop: isAnon ? 'rights' : 'rights|gender',
			ususers: isAnon ? editor : `${ editor }|${ vandal }`
		};
		
		const data = await api.get( params );
		const res = data.query.users;
		const editorRights = res[ 0 ].rights;
		const vandalRights = isAnon ? [] : res[ 1 ].rights;
		const vandalGender = isAnon ? 'unknown' : res[ 1 ].gender;
		
		if (
			vandalRights.includes( 'autopatrol' ) ||
			vandalRights.includes( 'patrol' )
		) {
			return false;
		}
		
		vcData.editorRights = editorRights;
		vcData.vandalGender = vandalGender;
		
		return true;
		
	}
	
	function i18n( key, args = [] ) {
		
		const messages = mw.config.get( 'vandalCleanerConfig' ).messages;
		
		const lang = mw.config.get( 'wgUserLanguage' );
		
		let output = '';
		
		if ( messages[ lang ] && messages[ lang ][ key ] ) {
			output = messages[ lang ][ key ];
		} else {
			output = messages.en[ key ];
		}
		
		if ( typeof output !== 'string' ) {
			return output;
		}
		
		const isAnonPattern = /{ISANON\|yes=(.*?)\|no=(.*?)\|ISANON-END}/g;
		
		const genderPattern = /{GENDER\|m=(.*?)\|f=(.*?)\|GENDER-END}/g;
		
		output = output
			.replace( isAnonPattern, isAnon ? '$1' : '$2' )
			.replace( genderPattern, vcData.vandalGender === 'female' ? '$2' : '$1' );
		
		output = convertPlural( output, args );
		
		args.forEach( ( arg, index ) =>
			output = output.replaceAll( `$${ index + 1 }`, arg )
		);
		
		return output;
		
	}
	
	function convertPlural( str, args ) {
		
		let output = str;
		
		const pattern = /{PLURAL:\$+[0-9]+\|one=(.*?)\|more=(.*?)\|PLURAL-END}/g;
		
		if ( output.match( pattern ) ) {
			
			output.match( pattern ).forEach( match => {
				
				const count = args[ Number( match.match( /[0-9]/ )[ 0 ] ) - 1 ];
				let singular = match.split( '|one=' )[ 1 ];
				let dual = match.split( '|two=' )[ 1 ];
				
				if ( typeof dual === 'string' ) {
					singular = singular.split( '|two=' )[ 0 ];
					dual = dual.split( '|more=' )[ 0 ];
				} else {
					singular = singular.split( '|more=' )[ 0 ];
				}
				
				const plural = match.split( '|more=' )[ 1 ].split( '|PLURAL-END}' )[ 0 ];
				
				if ( count === 1 ) {
					output = output.replace( match, singular );
				} else if ( count === 2 && typeof dual === 'string' ) {
					output = output.replace( match, dual );
				} else {
					output = output.replace( match, plural );
				}
				
			} );
			
		}
		
		return output;
		
	}
	
	if ( !( await checkInitData() ) ) {
		return;
	}
	
	mw.loader.load(
		'https://he.wikipedia.org/w/index.php?title=מדיה_ויקי:סקריפטים/107.css&action=raw&ctype=text/css',
		'text/css'
	);
	
	await $.when(
		mw.loader.using( [ 'oojs-ui-core', 'oojs-ui.styles.icons-editing-core' ] ),
		mw.loader.getScript( 'https://he.wikipedia.org/w/index.php?title=מדיה_ויקי:סקריפטים/107.js/config.js&action=raw&ctype=text/javascript' ),
		$.ready
	);
	
	const contribsBtn = new OO.ui.ButtonWidget( {
		flags: 'destructive',
		icon: 'editUndo',
		id: 'vandal-cleaner-contribs-btn',
		label: i18n( 'contribsBtnLabel' ),
		title: i18n( 'contribsBtnTooltip' )
	} );
	
	contribsBtn.on( 'click', init ).$element.insertBefore( '#mw-content-text' );
	
	async function init() {
		
		function ProcessDialog( config ) {
			
			ProcessDialog.super.call( this, config );
			
		}
		
		await mw.loader.using( [
			'user.options',
			'oojs-ui-windows',
			'oojs-ui.styles.icons-interactions'
		] );
		
		OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
		
		ProcessDialog.static.name = 'vandalCleanerDialog';
		ProcessDialog.static.title = i18n( 'dialogTitle' );
		ProcessDialog.static.size = 'large';
		
		ProcessDialog.static.actions = [
			{
				action: 'help',
				icon: 'help',
				label: i18n( 'helpBtnLabel' ),
				modes: [ 'firstConfig', 'secondConfig', 'working', 'final' ]
			},
			{
				action: 'cancel',
				flags: [ 'close', 'safe' ],
				label: i18n( 'cancelBtnTooltip' ),
				modes: 'firstConfig'
			},
			{
				action: 'continue',
				flags: 'primary',
				label: i18n( 'continueBtnLabel' ),
				modes: 'firstConfig'
			},
			{
				action: 'back',
				flags: [ 'back', 'safe' ],
				label: i18n( 'backBtnTooltip' ),
				modes: 'secondConfig'
			},
			{
				action: 'run',
				flags: [ 'destructive', 'primary' ],
				label: i18n( 'runBtnLabel' ),
				modes: 'secondConfig'
			},
			{
				action: 'stop',
				flags: [ 'destructive', 'safe' ],
				icon: 'stop',
				label: i18n( 'stopBtnLabel' ),
				modes: 'working'
			},
			{
				action: 'cancel',
				flags: 'safe',
				label: i18n( 'reloadBtnLabel' ),
				modes: 'final'
			}
		];
		
		ProcessDialog.prototype.initialize = function () {
			
			ProcessDialog.super.prototype.initialize.apply( this, arguments );
			
			createStackLayout.call( this );
			
		};
		
		ProcessDialog.prototype.getBodyHeight = () => {};
		
		ProcessDialog.prototype.getSetupProcess = function ( data = {} ) {
			
			return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
			
			.next( function () {
				
				this.actions.setMode( 'firstConfig' );
				
				createFeedbackArea();
				
				createFirstConfigPanel.call( this );
				
				refineErrorDialog( ProcessDialog );
				
				$( document ).on( 'keydown', e => {
					
					if (
						e.key === 'Escape' &&
						this.stackLayout.currentItem.elementId ===
						'vandal-cleaner-working-panel'
					) {
						this.executeAction( 'stop' );
					}
					
				} );
				
				this.actions.on( 'change', () =>
					$( '#vandal-cleaner-dialog .oo-ui-window-body' ).scrollTop( 0 )
				);
				
				vcData.prettifiedVandal =
					`<bdi class="vandal-cleaner-prettified-vandal">
						${ mw.util.prettifyIP( vandal ) }
					</bdi>`;
				
			}, this );
			
		};
		
		ProcessDialog.prototype.getActionProcess = function ( action ) {
			
			switch ( action ) {
				
				case 'help':
					return new OO.ui.Process( () => window.open( i18n( 'helpPageUrl' ) ) );
				
				case 'cancel':
					return new OO.ui.Process( function () {
						this.close();
					}, this );
				
				case 'continue':
					return new OO.ui.Process( processFirstConfigInput, this )
					
					.next( getEdits )
					
					.next( createSecondConfigPanel, this )
					
					.next( function () {
						this.stackLayout.setItem( this.secondConfigPanel );
						this.actions.setMode( 'secondConfig' );
					}, this );
				
				case 'back':
					return new OO.ui.Process( function () {
						this.stackLayout.setItem( this.firstConfigPanel );
						this.actions.setMode( 'firstConfig' );
					}, this );
				
				case 'run':
					return new OO.ui.Process( processSecondConfigInput, this )
					
					.next( createWorkingPanel, this )
					
					.next( function () {
						
						this.stackLayout.setItem( this.workingPanel );
						this.actions.setMode( 'working' );
						ProcessDialog.static.escapable = false;
						
						runCleaner.call( this, ProcessDialog );
						
					}, this );
				
				case 'stop':
					return new OO.ui.Process( function () {
						
						const confirmStopText = new OO.ui.HtmlSnippet( `
							<p>${ i18n( 'confirmStopAreYouSure' ) }</p>
							<p>${ i18n( 'confirmStopPleaseNote' ) }</p>
						` );
						
						OO.ui.confirm( confirmStopText ).then( confirmed => {
							
							if ( confirmed ) {
								this.executeAction( 'cancel' );
							}
							
						} );
						
					}, this );
				
				default:
					return ProcessDialog.super.prototype.getActionProcess.call( this, action );
				
			}
			
		};
		
		ProcessDialog.prototype.getTeardownProcess = function ( data = {} ) {
			
			let isReloadNeeded;
			
			return ProcessDialog.super.prototype.getTeardownProcess.call( this, data )
			
			.first( function () {
				
				api.abort();
				
				isReloadNeeded =
					[ 'vandal-cleaner-working-panel', 'vandal-cleaner-final-panel' ]
					.includes( this.stackLayout.currentItem.elementId );
				
			}, this )
			
			.next( () => {
				
				vcData.windowManager.destroy();
				
				if ( isReloadNeeded ) {
					mw.util.$content.css( 'pointer-events', 'none' ).fadeTo( '_default', 0.3 );
					mw.notify( i18n( 'reloadingPage' ), { autoHide: false } );
					window.location.reload( true );
				}
				
			} );
			
		};
		
		vcData.windowManager = new OO.ui.WindowManager();
		
		$( document.body ).append( vcData.windowManager.$element );
		
		const dialog = new ProcessDialog( { id: 'vandal-cleaner-dialog' } );
		
		// posX and posY are used to prevent the browser from jumping
		// to the top of the page when closing the dialog window.
		// See the windowManager's "closing" event listener below.
		const posX = window.scrollX;
		const posY = window.scrollY;
		
		vcData.windowManager.addWindows( [ dialog ] );
		
		vcData.windowManager.openWindow( dialog );
		
		vcData.windowManager.on( 'closing', ( win, closed ) =>
			closed.then( () => window.scrollTo( posX, posY ) )
		);
		
	}
	
	function createStackLayout() {
		
		this.firstConfigPanel = new OO.ui.PanelLayout( {
			classes: [ 'vandal-cleaner-panel' ],
			expanded: false,
			id: 'vandal-cleaner-first-config-panel',
			padded: true
		} );
		
		this.secondConfigPanel = new OO.ui.PanelLayout( {
			classes: [ 'vandal-cleaner-panel' ],
			expanded: false,
			id: 'vandal-cleaner-second-config-panel',
			padded: true
		} );
		
		this.workingPanel = new OO.ui.PanelLayout( {
			classes: [ 'vandal-cleaner-panel' ],
			expanded: false,
			id: 'vandal-cleaner-working-panel',
			padded: true
		} );
		
		this.finalPanel = new OO.ui.PanelLayout( {
			classes: [ 'vandal-cleaner-panel' ],
			expanded: false,
			id: 'vandal-cleaner-final-panel',
			padded: true
		} );
		
		this.stackLayout = new OO.ui.StackLayout( {
			items: [
				this.firstConfigPanel,
				this.secondConfigPanel,
				this.workingPanel,
				this.finalPanel
			]
		} );
		
		this.$body.append( this.stackLayout.$element );
		
	}
	
	function createFeedbackArea() {
		
		const feedbackIcon = new OO.ui.IconWidget( {
			icon: 'feedback',
			id: 'vandal-cleaner-feedback-icon'
		} );
		
		const feedbackLabel = new OO.ui.LabelWidget( {
			id: 'vandal-cleaner-feedback-label',
			label: new OO.ui.HtmlSnippet(
				i18n( 'feedbackLabel', [ i18n( 'feedbackPageUrl' ) ] )
			)
		} );
		
		const feedbackLayout = new OO.ui.HorizontalLayout( {
			id: 'vandal-cleaner-feedback-layout',
			items: [ feedbackIcon, feedbackLabel ]
		} );
		
		feedbackLayout.$element
		.appendTo( '#vandal-cleaner-dialog .oo-ui-window-foot' );
		
	}
	
	function createFirstConfigPanel() {
		
		const $broomImg = $( '<img>' ).attr( {
			alt: i18n( 'introWelcome' ),
			id: 'vandal-cleaner-broom-img',
			src: 'https://upload.wikimedia.org/wikipedia/commons/f/f5/Broom_Icon_(template-icon).svg'
		} );
		
		const $welcomeText = $( '<div>' )
			.attr( 'id', 'vandal-cleaner-welcome-text' )
			.append(
				$( '<p>' ).text( i18n( 'introWelcome' ) ),
				$( '<p>' ).text( i18n( 'introToolPurpose' ) ),
				$( '<p>' ).html( i18n( 'introReadHelp', [ i18n( 'helpPageUrl' ) ] ) ),
				$( '<p>' ).text( i18n( 'introSetOptions' ) )
			);
		
		const $welcomeContainer = $( '<div>' )
			.attr( 'id', 'vandal-cleaner-welcome-container' )
			.addClass( 'vandal-cleaner-fancy-border-bottom' )
			.append( $broomImg, $welcomeText );
		
		vcData.inputs = {};
		
		let prefilledSummary = getPref( 'summary' );
		
		if ( !prefilledSummary && !getPref( 'useEmptySummary' ) ) {
			prefilledSummary = i18n( 'defaultSummary' );
		}
		
		vcData.inputs.summaryInput = new OO.ui.TextInputWidget( {
			id: 'vandal-cleaner-summary-input',
			maxLength: 500,
			validate: value => value.length <= 500,
			value: prefilledSummary
		} );
		
		const summaryField = new OO.ui.FieldLayout(
			vcData.inputs.summaryInput,
			{
				align: 'top',
				help: i18n( 'summaryHelp' ),
				helpInline: true,
				label: $( '<span>' )
					.addClass( 'vandal-cleaner-prominent-label' )
					.text( i18n( 'summaryLabel' ) )
			}
		);
		
		vcData.inputs.rememberSummaryCbx = new OO.ui.CheckboxInputWidget( {
			classes: [ 'vandal-cleaner-checkbox' ]
		} );
		
		const rememberSummaryField = new OO.ui.FieldLayout(
			vcData.inputs.rememberSummaryCbx,
			{
				align: 'inline',
				id: 'vandal-cleaner-remember-summary-field',
				label: i18n( 'rememberSummaryLabel' )
			}
		);
		
		const summaryFieldset = new OO.ui.FieldsetLayout( {
			items: [ summaryField, rememberSummaryField ]
		} );
		
		const advancedOptionsBtn = new OO.ui.ButtonWidget( {
			flags: 'progressive',
			framed: false,
			id: 'vandal-cleaner-advanced-options-btn',
			indicator: 'down',
			label: i18n( 'advancedOptionsLabel' )
		} );
		
		vcData.inputs.numOfDaysInput = new OO.ui.NumberInputWidget( {
			classes: [ 'vandal-cleaner-number-input' ],
			max: 60,
			min: 1,
			step: 1,
			value: getPref( 'numOfDays' ) || '30'
		} );
		
		vcData.inputs.numOfDaysInput.$element
		.find( 'input' ).attr( 'required', true );
		
		const numOfDaysHelp = new OO.ui.PopupButtonWidget( {
			classes: [ 'oo-ui-fieldLayout-help' ],
			framed: false,
			icon: 'info',
			invisibleLabel: true,
			label: i18n( 'numOfDaysHelpTooltip' ),
			popup: {
				$content: $( '<p>' )
					.addClass( 'vandal-cleaner-field-help-popup-text' )
					.text( i18n( 'numOfDaysHelpText' ) ),
				align: 'backwards',
				padded: true
			}
		} );
		
		const numOfDaysField = new OO.ui.FieldLayout(
			vcData.inputs.numOfDaysInput,
			{
				align: 'top',
				label: $( '<span>' )
					.addClass( 'vandal-cleaner-prominent-label' )
					.text( i18n( 'numOfDaysLabel' ) )
			}
		);
		
		numOfDaysField.$element.find( '.oo-ui-fieldLayout-header' )
		.prepend( numOfDaysHelp.$element );
		
		vcData.inputs.rememberNumOfDaysCbx = new OO.ui.CheckboxInputWidget( {
			classes: [ 'vandal-cleaner-checkbox' ]
		} );
		
		const rememberNumOfDaysField = new OO.ui.FieldLayout(
			vcData.inputs.rememberNumOfDaysCbx,
			{
				align: 'inline',
				label: i18n( 'rememberNumOfDaysLabel' )
			}
		);
		
		const numOfDaysFieldset = new OO.ui.FieldsetLayout( {
			items: [ numOfDaysField, rememberNumOfDaysField ]
		} );
		
		vcData.inputs.numOfActionsInput = new OO.ui.NumberInputWidget( {
			classes: [ 'vandal-cleaner-number-input' ],
			max: 300,
			min: 1,
			step: 1,
			value: getPref( 'numOfActions' ) || '100'
		} );
		
		vcData.inputs.numOfActionsInput.$element
		.find( 'input' ).attr( 'required', true );
		
		const numOfActionsHelp = new OO.ui.PopupButtonWidget( {
			classes: [ 'oo-ui-fieldLayout-help' ],
			framed: false,
			icon: 'info',
			invisibleLabel: true,
			label: i18n( 'numOfActionsHelpTooltip' ),
			popup: {
				$content: $( '<p>' )
					.addClass( 'vandal-cleaner-field-help-popup-text' )
					.text( i18n( 'numOfActionsHelpText' ) ),
				align: 'backwards',
				padded: true
			}
		} );
		
		const numOfActionsField = new OO.ui.FieldLayout(
			vcData.inputs.numOfActionsInput,
			{
				align: 'top',
				label: $( '<span>' )
					.addClass( 'vandal-cleaner-prominent-label' )
					.text( i18n( 'numOfActionsLabel' ) )
			}
		);
		
		numOfActionsField.$element.find( '.oo-ui-fieldLayout-header' )
		.prepend( numOfActionsHelp.$element );
		
		vcData.inputs.rememberNumOfActionsCbx = new OO.ui.CheckboxInputWidget( {
			classes: [ 'vandal-cleaner-checkbox' ]
		} );
		
		const rememberNumOfActionsField = new OO.ui.FieldLayout(
			vcData.inputs.rememberNumOfActionsCbx,
			{
				align: 'inline',
				label: i18n( 'rememberNumOfActionsLabel' )
			}
		);
		
		const numOfActionsFieldset = new OO.ui.FieldsetLayout( {
			items: [ numOfActionsField, rememberNumOfActionsField ]
		} );
		
		vcData.inputs.skipInitialConfigCbx = new OO.ui.CheckboxInputWidget( {
			classes: [ 'vandal-cleaner-checkbox' ],
			id: 'vandal-cleaner-skip-initial-config-checkbox',
			selected: Boolean( getPref( 'skipInitialConfig' ) )
		} );
		
		const skipInitialConfigField = new OO.ui.FieldLayout(
			vcData.inputs.skipInitialConfigCbx,
			{
				align: 'inline',
				help: i18n( 'skipInitialConfigHelp' ),
				helpInline: true,
				id: 'vandal-cleaner-skip-initial-config-field',
				label: i18n( 'skipInitialConfigLabel' )
			}
		);
		
		const additionalOptionsFieldset = new OO.ui.FieldsetLayout( {
			items: [ skipInitialConfigField ],
			label: $( '<span>' )
				.attr( 'id', 'vandal-cleaner-additional-options-label' )
				.text( i18n( 'additionalOptionsLabel' ) )
		} );
		
		const $advancedOptionsContainer = $( '<div>' )
			.attr( 'id', 'vandal-cleaner-advanced-options-container' )
			.append(
				numOfDaysFieldset.$element,
				numOfActionsFieldset.$element,
				additionalOptionsFieldset.$element
			);
		
		this.firstConfigPanel.$element.append(
			$welcomeContainer,
			summaryFieldset.$element,
			advancedOptionsBtn.$element,
			$advancedOptionsContainer
		);
		
		[
			vcData.inputs.summaryInput,
			vcData.inputs.numOfDaysInput,
			vcData.inputs.numOfActionsInput
		].forEach( widget =>
			widget.on( 'enter', () => this.executeAction( 'continue' ) )
		);
		
		advancedOptionsBtn.on( 'click', () =>
			toggleAdvancedOptions( $advancedOptionsContainer, advancedOptionsBtn )
		);
		
		if ( getPref( 'skipInitialConfig' ) ) {
			this.executeAction( 'continue' );
		}
		
	}
	
	function toggleAdvancedOptions( $advancedOptionsContainer, advancedOptionsBtn ) {
		
		$advancedOptionsContainer.slideToggle( 'fast', () => {
			
			if ( advancedOptionsBtn.getIndicator() === 'down' ) {
				
				scrollIntoViewIfNeeded(
					$advancedOptionsContainer,
					-225,
					advancedOptionsBtn.$element,
					'start'
				);
				
				advancedOptionsBtn.setIndicator( 'up' );
				
			} else {
				
				advancedOptionsBtn.setIndicator( 'down' );
				
			}
			
		} );
		
	}
	
	async function processFirstConfigInput() {
		
		const defer = $.Deferred();
		
		try {
			
			await $.when(
				vcData.inputs.summaryInput.getValidity(),
				vcData.inputs.numOfDaysInput.getValidity(),
				vcData.inputs.numOfActionsInput.getValidity()
			);
			
		} catch ( e ) {
			
			document.activeElement.blur();
			
			return defer.reject( new OO.ui.Error( i18n( 'invalidInput' ) ) );
			
		}
		
		vcData.summary = vcData.inputs.summaryInput.getValue().trim();
		vcData.numOfDays = vcData.inputs.numOfDaysInput.getNumericValue();
		vcData.numOfActions = vcData.inputs.numOfActionsInput.getNumericValue();
		
		vcData.earliestEditTimestamp =
			subtractDaysFromTimestamp( mw.now(), vcData.numOfDays );
		
		if ( vcData.inputs.rememberSummaryCbx.isSelected() ) {
			setPref( 'summary', vcData.summary );
			setPref( 'useEmptySummary', vcData.summary === '' ? 1 : 0 );
		}
		
		if ( vcData.inputs.rememberNumOfDaysCbx.isSelected() ) {
			setPref( 'numOfDays', vcData.numOfDays );
		}
		
		if ( vcData.inputs.rememberNumOfActionsCbx.isSelected() ) {
			setPref( 'numOfActions', vcData.numOfActions );
		}
		
		const isSkipInitialConfigSelected =
			vcData.inputs.skipInitialConfigCbx.isSelected();
		
		if ( isSkipInitialConfigSelected && !getPref( 'skipInitialConfig' ) ) {
			setPref( 'skipInitialConfig', 1 );
		}
		
		if ( !isSkipInitialConfigSelected && getPref( 'skipInitialConfig' ) ) {
			setPref( 'skipInitialConfig', 0 );
		}
		
		return defer.resolve();
		
	}
	
	function subtractDaysFromTimestamp( timestamp, days ) {
		
		const result = new Date( timestamp );
		
		result.setDate( result.getDate() - days );
		
		return `${ result.toISOString().split( '.' )[ 0 ] }Z`;
		
	}
	
	async function getEdits() {
		
		await $.when(
			getRevertibleEdits(),
			getDeletablePages(),
			getHideableRevs(),
			getParsedSummary()
		);
		
	}
	
	async function getRevertibleEdits() {
		
		if ( !vcData.editorRights.includes( 'rollback' ) ) {
			vcData.revertibleEditsCount = 0;
			return;
		}
		
		const params = {
			list: 'usercontribs',
			uclimit: vcData.numOfActions + 1,
			ucend: vcData.earliestEditTimestamp,
			ucuser: vandal,
			ucprop: 'title|timestamp',
			ucshow: '!new|top'
		};
		
		const data = await api.get( params );
		
		vcData.revertibleEdits = data.query.usercontribs;
		
		if ( vcData.revertibleEdits.length === vcData.numOfActions + 1 ) {
			vcData.tooManyRevertibleEdits = true;
			vcData.revertibleEdits.pop();
		} else {
			vcData.tooManyRevertibleEdits = false;
		}
		
		vcData.revertibleEditsCount = vcData.revertibleEdits.length;
		
	}
	
	async function getDeletablePages() {
		
		if ( !vcData.editorRights.includes( 'delete' ) ) {
			vcData.deletablePagesCount = 0;
			return;
		}
		
		const params = {
			list: 'usercontribs',
			uclimit: vcData.numOfActions + 1,
			ucend: vcData.earliestEditTimestamp,
			ucuser: vandal,
			ucprop: 'title',
			ucshow: 'new'
		};
		
		const data = await api.get( params );
		
		vcData.deletablePages = data.query.usercontribs;
		
		if ( vcData.deletablePages.length === vcData.numOfActions + 1 ) {
			vcData.tooManyDeletablePages = true;
			vcData.deletablePages.pop();
		} else {
			vcData.tooManyDeletablePages = false;
		}
		
		vcData.deletablePagesCount = vcData.deletablePages.length;
		
	}
	
	async function getHideableRevs() {
		
		if ( !vcData.editorRights.includes( 'deleterevision' ) ) {
			vcData.hideableRevsCount = 0;
			return;
		}
		
		const params = {
			list: 'usercontribs',
			uclimit: vcData.numOfActions + 1,
			ucend: vcData.earliestEditTimestamp,
			ucuser: vandal,
			ucprop: 'ids|title'
		};
		
		const data = await api.get( params );
		
		vcData.hideableRevs = data.query.usercontribs;
		
		if ( vcData.hideableRevs.length === vcData.numOfActions + 1 ) {
			vcData.tooManyHideableRevs = true;
			vcData.hideableRevs.pop();
		} else {
			vcData.tooManyHideableRevs = false;
		}
		
		vcData.hideableRevsCount = vcData.hideableRevs.length;
		
	}
	
	async function getParsedSummary() {
		
		if ( vcData.summary === '' ) {
			vcData.parsedSummary = '';
			return;
		}
		
		const params = {
			action: 'parse',
			summary: vcData.summary,
			prop: ''
		};
		
		const data = await api.get( params );
		
		vcData.parsedSummary = data.parse.parsedsummary[ '*' ];
		
	}
	
	async function createSecondConfigPanel() {
		
		await mw.loader.using( [
			'oojs-ui.styles.icons-accessibility',
			'oojs-ui.styles.icons-alerts',
			'oojs-ui.styles.icons-moderation'
		] );
		
		this.secondConfigPanel.$element.empty();
		
		const earliestEditDate =
			new Date( vcData.earliestEditTimestamp ).toLocaleString(
				i18n( 'dateFormat' ),
				{
					dateStyle: 'long',
					timeStyle: 'short',
					hourCycle: 'h23'
				}
			);
		
		const reviewConfigHtml = new OO.ui.HtmlSnippet( `
			<p>${ i18n( 'reviewConfigHeading' ) }</p>
			<ul id="vandal-cleaner-config-list">
				<li>
					${ i18n( 'reviewConfigVandal' ) }
					<strong>${ vcData.prettifiedVandal }</strong>
				</li>
				<li>
					${ i18n( 'reviewConfigNumOfDays' ) }
					<strong>${ vcData.numOfDays }</strong><br />
					<span id="vandal-cleaner-earliest-edit-date">
						${ i18n( 'reviewConfigEarliestDate', [ earliestEditDate ] ) }
					</span>
				</li>
				<li>
					${ i18n( 'reviewConfigNumOfActions' ) }
					<strong>${ vcData.numOfActions }</strong>
				</li>
				<li>
					${ i18n( 'reviewConfigSummary' ) }
					${ vcData.parsedSummary
						? `<span class="comment">${ vcData.parsedSummary }</span>`
						: i18n( 'reviewConfigNoSummary' )
					}
				</li>
			</ul>
		` );
		
		const reviewConfigMsg = new OO.ui.MessageWidget( {
			icon: 'lightbulb',
			id: 'vandal-cleaner-review-config-msg',
			label: reviewConfigHtml,
			type: 'notice'
		} );
		
		const $configList =
			reviewConfigMsg.$element.find( '#vandal-cleaner-config-list' );
			
		const isMobile = mw.config.get( 'skin' ) === 'minerva';
		
		let shouldConfigListBeHidden;
		
		if ( isMobile ) {
			shouldConfigListBeHidden = getPref( 'hideConfigListOnMobile' );
		} else {
			shouldConfigListBeHidden = getPref( 'hideConfigListOnDesktop' );
		}
		
		if ( shouldConfigListBeHidden ) {
			$configList.hide();
			skipReviewConfigMsg( reviewConfigMsg.$element );
		}
		
		const configListToggleBtn = new OO.ui.ButtonWidget( {
			framed: false,
			id: 'vandal-cleaner-config-list-toggle-btn',
			indicator: shouldConfigListBeHidden ? 'down' : 'up',
			invisibleLabel: true,
			label: shouldConfigListBeHidden
				? i18n( 'configListShow' )
				: i18n( 'configListHide' ),
			title: shouldConfigListBeHidden
				? i18n( 'configListShow' )
				: i18n( 'configListHide' )
		} );
		
		configListToggleBtn.on( 'click', () =>
			toggleConfigList( $configList, configListToggleBtn, isMobile )
		);
		
		configListToggleBtn.$element.prependTo( reviewConfigMsg.$element );
		
		const canRollback = vcData.editorRights.includes( 'rollback' );
		
		vcData.inputs.rollbackTog = new OO.ui.ToggleSwitchWidget( {
			classes: [ 'vandal-cleaner-action-tog' ],
			disabled: !canRollback,
			value: canRollback
		} );
		
		const $rollbackNoEdits = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'rollbackNoEdits' ) );
		
		const $rollbackTooMany = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'rollbackTooMany', [ vcData.revertibleEditsCount ] ) );
		
		const $rollbackNoPermission = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'rollbackNoPermission' ) );
		
		const rollbackField = new OO.ui.FieldLayout(
			vcData.inputs.rollbackTog,
			{
				classes: [ 'vandal-cleaner-action-field' ],
				help:
					`${ i18n( 'rollbackHelp', [ vcData.numOfDays ] ) }
					${ vcData.revertibleEditsCount > 0
						? i18n( 'rollbackHelpCount', [ vcData.revertibleEditsCount ] )
						: ''
					}`,
				helpInline: true,
				label: $( '<span>' )
					.addClass( [
						'vandal-cleaner-action-label',
						'vandal-cleaner-prominent-label'
					] )
					.text( i18n( 'rollbackLabel' ) ),
				notices: [ $rollbackNoEdits ],
				warnings: [ $rollbackTooMany, $rollbackNoPermission ]
			}
		);
		
		const rollbackFieldset = new OO.ui.FieldsetLayout( {
			classes: [
				'vandal-cleaner-action-fieldset',
				'vandal-cleaner-fancy-border-bottom'
			],
			icon: 'editUndo',
			items: [ rollbackField ]
		} );
		
		if ( canRollback ) {
			
			rollbackFieldset.$element.on( 'click', e =>
				simulateLabelClick( e, vcData.inputs.rollbackTog )
			);
			
			const $rollbackFieldExtra =
				rollbackField.$element.find( '.oo-ui-fieldLayout-messages' );
			
			vcData.inputs.rollbackTog.on( 'change', () =>
				toggleFieldExtra( $rollbackFieldExtra )
			);
			
			if ( vcData.revertibleEditsCount === 0 ) {
				displayFieldMsg( $rollbackNoEdits );
			}
			
			if ( vcData.tooManyRevertibleEdits ) {
				displayFieldMsg( $rollbackTooMany );
			}
			
		} else {
			
			displayFieldMsg( $rollbackNoPermission );
			
		}
		
		const canDelete = vcData.editorRights.includes( 'delete' );
		
		vcData.inputs.deleteTog = new OO.ui.ToggleSwitchWidget( {
			classes: [ 'vandal-cleaner-action-tog' ],
			disabled: !canDelete,
			value: canDelete
		} );
		
		const $deleteNoPages = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'deleteNoPages' ) );
		
		const $deleteTooMany = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'deleteTooMany', [ vcData.deletablePagesCount ] ) );
		
		const $deleteNoPermission = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'deleteNoPermission' ) );
		
		const deleteField = new OO.ui.FieldLayout(
			vcData.inputs.deleteTog,
			{
				classes: [ 'vandal-cleaner-action-field' ],
				help:
					`${ i18n( 'deleteHelp', [ vcData.numOfDays ] ) }
					${ vcData.deletablePagesCount > 0
						? i18n( 'deleteHelpCount', [ vcData.deletablePagesCount ] )
						: ''
					}`,
				helpInline: true,
				label: $( '<span>' )
					.addClass( [
						'vandal-cleaner-action-label',
						'vandal-cleaner-prominent-label'
					] )
					.text( i18n( 'deleteLabel' ) ),
				notices: [ $deleteNoPages ],
				warnings: [ $deleteTooMany, $deleteNoPermission ]
			}
		);
		
		const deleteFieldset = new OO.ui.FieldsetLayout( {
			classes: [
				'vandal-cleaner-action-fieldset',
				'vandal-cleaner-fancy-border-bottom'
			],
			icon: 'trash',
			items: [ deleteField ]
		} );
		
		if ( canDelete ) {
			
			deleteFieldset.$element.on( 'click', e =>
				simulateLabelClick( e, vcData.inputs.deleteTog )
			);
			
			const $deleteFieldExtra =
				deleteField.$element.find( '.oo-ui-fieldLayout-messages' );
			
			vcData.inputs.deleteTog.on( 'change', () =>
				toggleFieldExtra( $deleteFieldExtra )
			);
			
			if ( vcData.deletablePagesCount === 0 ) {
				displayFieldMsg( $deleteNoPages );
			}
			
			if ( vcData.tooManyDeletablePages ) {
				displayFieldMsg( $deleteTooMany );
			}
			
		} else {
			
			displayFieldMsg( $deleteNoPermission );
			
		}
		
		const canBlock = vcData.editorRights.includes( 'block' );
		
		vcData.inputs.blockTog = new OO.ui.ToggleSwitchWidget( {
			classes: [ 'vandal-cleaner-action-tog' ],
			disabled: !canBlock,
			value: canBlock
		} );
		
		const $blockNoPermission = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'blockNoPermission' ) );
		
		const blockField = new OO.ui.FieldLayout(
			vcData.inputs.blockTog,
			{
				classes: [ 'vandal-cleaner-action-field' ],
				help: i18n( 'blockHelp' ),
				helpInline: true,
				label: $( '<span>' )
					.addClass( [
						'vandal-cleaner-action-label',
						'vandal-cleaner-prominent-label'
					] )
					.text( i18n( 'blockLabel' ) ),
				warnings: [ $blockNoPermission ]
			}
		);
		
		const blockFieldset = new OO.ui.FieldsetLayout( {
			classes: [
				'vandal-cleaner-action-fieldset',
				'vandal-cleaner-fancy-border-bottom'
			],
			icon: 'block',
			items: [ blockField ]
		} );
		
		if ( canBlock ) {
			
			blockFieldset.$element.on( 'click', e =>
				simulateLabelClick( e, vcData.inputs.blockTog )
			);
			
			const $blockFieldExtra =
				blockField.$element.find( '.oo-ui-fieldLayout-messages' );
			
			vcData.inputs.blockTog.on( 'change', () =>
				toggleFieldExtra( $blockFieldExtra )
			);
			
			const blockOptions = [
				{ data: '2 hours' },
				{ data: '1 day' },
				{ data: '3 days' },
				{ data: '1 week' },
				{ data: '2 weeks' },
				{ data: '1 month' },
				{ data: '3 months' },
				{ data: '6 months' },
				{ data: '1 year' },
				{ data: 'infinite' }
			];
			
			const blockOptionsMsg = i18n( 'blockOptions' );
			
			blockOptions.forEach( option =>
				option.label = blockOptionsMsg[ option.data ]
			);
			
			vcData.inputs.blockDurationDropdown = new OO.ui.DropdownInputWidget( {
				id: 'vandal-cleaner-block-duration-dropdown',
				options: blockOptions,
				value: isAnon ? '1 day' : 'infinite'
			} ).toggle( false );
			
			const blockDurationLabel = new OO.ui.LabelWidget( {
				id: 'vandal-cleaner-block-duration-label',
				input: vcData.inputs.blockDurationDropdown,
				label: new OO.ui.HtmlSnippet( `
					${ i18n( 'blockDurationLabel' ) }
					${ blockOptionsMsg[ vcData.inputs.blockDurationDropdown.getValue() ] }
					(<a id="vandal-cleaner-block-duration-change-btn"
					href="#" role="button">${ i18n( 'blockDurationChange' ) }</a>)
				` )
			} );
			
			blockDurationLabel.$element
			.find( '#vandal-cleaner-block-duration-change-btn' )
			.on( 'click', e => {
				
				e.preventDefault();
				
				blockDurationLabel.setLabel( i18n( 'blockDurationLabel' ) );
				
				vcData.inputs.blockDurationDropdown.toggle( true );
				
			} );
			
			const blockDurationLayout = new OO.ui.HorizontalLayout( {
				id: 'vandal-cleaner-block-duration-layout',
				items: [ blockDurationLabel, vcData.inputs.blockDurationDropdown ]
			} );
			
			blockDurationLayout.$element.prependTo( $blockFieldExtra )
			.on( 'click', e => e.stopPropagation() );
			
		} else {
			
			displayFieldMsg( $blockNoPermission );
			
		}
		
		const canRevDelete = vcData.editorRights.includes( 'deleterevision' );
		
		vcData.inputs.revDeleteTog = new OO.ui.ToggleSwitchWidget( {
			classes: [ 'vandal-cleaner-action-tog' ],
			disabled: !canRevDelete,
			value: false
		} );
		
		const $revDeleteNoRevs = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'revDeleteNoRevs' ) );
		
		const $revDeleteTooMany = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'revDeleteTooMany', [ vcData.hideableRevsCount ] ) );
		
		const $revDeleteNoPermission = $( '<div>' )
			.addClass( 'vandal-cleaner-field-msg' )
			.text( i18n( 'revDeleteNoPermission' ) );
		
		const revDeleteField = new OO.ui.FieldLayout(
			vcData.inputs.revDeleteTog,
			{
				classes: [ 'vandal-cleaner-action-field' ],
				help:
					`${ i18n( 'revDeleteHelp', [ vcData.numOfDays ] ) }
					${ vcData.hideableRevsCount > 0
						? i18n( 'revDeleteHelpCount', [ vcData.hideableRevsCount ] )
						: ''
					}`,
				helpInline: true,
				label: $( '<span>' )
					.addClass( [
						'vandal-cleaner-action-label',
						'vandal-cleaner-prominent-label'
					] )
					.text( i18n( 'revDeleteLabel' ) ),
				notices: [ $revDeleteNoRevs ],
				warnings: [ $revDeleteTooMany, $revDeleteNoPermission ]
			}
		);
		
		const revDeleteFieldset = new OO.ui.FieldsetLayout( {
			classes: [ 'vandal-cleaner-action-fieldset' ],
			icon: 'eyeClosed',
			items: [ revDeleteField ]
		} );
		
		if ( canRevDelete ) {
			
			revDeleteFieldset.$element.on( 'click', e =>
				simulateLabelClick( e, vcData.inputs.revDeleteTog )
			);
			
			const $revDeleteFieldExtra =
				revDeleteField.$element.find( '.oo-ui-fieldLayout-messages' ).hide();
			
			vcData.inputs.revDeleteTog.on( 'change', () =>
				toggleFieldExtra( $revDeleteFieldExtra )
			);
			
			if ( vcData.hideableRevsCount === 0 ) {
				displayFieldMsg( $revDeleteNoRevs );
			}
			
			if ( vcData.tooManyHideableRevs ) {
				displayFieldMsg( $revDeleteTooMany );
			}
			
			vcData.inputs.hideContentsCbx = new OO.ui.CheckboxInputWidget( {
				classes: [ 'vandal-cleaner-checkbox' ],
				selected: true
			} );
			
			const hideContentsField = new OO.ui.FieldLayout(
				vcData.inputs.hideContentsCbx,
				{
					align: 'inline',
					label: i18n( 'hideContentsLabel' )
				}
			);
			
			vcData.inputs.hideSummariesCbx = new OO.ui.CheckboxInputWidget( {
				classes: [ 'vandal-cleaner-checkbox' ]
			} );
			
			const hideSummariesField = new OO.ui.FieldLayout(
				vcData.inputs.hideSummariesCbx,
				{
					align: 'inline',
					label: i18n( 'hideSummariesLabel' )
				}
			);
			
			const revDeleteOptionsLayout = new OO.ui.HorizontalLayout( {
				id: 'vandal-cleaner-revdelete-options-layout',
				items: [ hideContentsField, hideSummariesField ]
			} );
			
			revDeleteOptionsLayout.$element.prependTo( $revDeleteFieldExtra )
			.on( 'click', e => e.stopPropagation() );
			
		} else {
			
			displayFieldMsg( $revDeleteNoPermission );
			
		}
		
		const $actionSelectionHeader = $( '<header>' )
			.attr( 'id', 'vandal-cleaner-action-selection-header' )
			.addClass( 'vandal-cleaner-fancy-border-bottom' )
			.text( i18n( 'actionSelectionHeading' ) );
		
		const $actionSelectionContainer = $( '<div>' )
			.attr( 'id', 'vandal-cleaner-action-selection-container' )
			.append(
				$actionSelectionHeader,
				rollbackFieldset.$element,
				deleteFieldset.$element,
				blockFieldset.$element,
				revDeleteFieldset.$element
			);
		
		this.secondConfigPanel.$element.append(
			reviewConfigMsg.$element,
			$actionSelectionContainer
		);
		
	}
	
	function skipReviewConfigMsg( $msgElement ) {
		
		const observer = new IntersectionObserver( entries => {
			
			if ( entries[ 0 ].isIntersecting ) {
				
				document.querySelector( '#vandal-cleaner-dialog .oo-ui-window-body' )
				.scroll( {
					top: $msgElement.innerHeight() - 8,
					behavior: 'smooth'
				} );
				
				observer.unobserve( $msgElement[ 0 ] );
				
			}
			
		} );
		
		observer.observe( $msgElement[ 0 ] );
		
	}
	
	function toggleConfigList( $configList, configListToggleBtn, isMobile ) {
		
		$configList.slideToggle( 'fast', () => {
			
			if ( configListToggleBtn.getIndicator() === 'down' ) {
				
				configListToggleBtn
				.setIndicator( 'up' )
				.setLabel( i18n( 'configListHide' ) )
				.setTitle( i18n( 'configListHide' ) );
				
				setPref(
					isMobile ? 'hideConfigListOnMobile' : 'hideConfigListOnDesktop',
					0
				);
				
			} else {
				
				configListToggleBtn
				.setIndicator( 'down' )
				.setLabel( i18n( 'configListShow' ) )
				.setTitle( i18n( 'configListShow' ) );
				
				setPref(
					isMobile ? 'hideConfigListOnMobile' : 'hideConfigListOnDesktop',
					1
				);
				
			}
			
		} );
		
	}
	
	function simulateLabelClick( e, targetOouiWidget ) {
		
		if (
			e.pointerType !== 'mouse' ||
			e.target.nodeName === 'LABEL' ||
			e.target.classList.contains( 'vandal-cleaner-action-label' )
		) {
			return;
		}
		
		targetOouiWidget.simulateLabelClick();
		
	}
	
	function toggleFieldExtra( $element ) {
		
		$element.slideToggle( 'fast', () =>
			
			scrollIntoViewIfNeeded(
				$element,
				45,
				$element.children( ':visible:last' ),
				'end'
			)
			
		);
		
	}
	
	function displayFieldMsg( $msg ) {
		
		$msg.closest( '.oo-ui-messageWidget' ).css( 'display', 'block' );
		
	}
	
	function processSecondConfigInput() {
		
		const defer = $.Deferred();
		
		vcData.isRollbackChecked = vcData.inputs.rollbackTog.getValue();
		
		vcData.isDeleteChecked = vcData.inputs.deleteTog.getValue();
		
		vcData.isBlockChecked = vcData.inputs.blockTog.getValue();
		
		if ( vcData.isBlockChecked ) {
			vcData.blockDuration = vcData.inputs.blockDurationDropdown.getValue();
		}
		
		vcData.isRevDeleteChecked = vcData.inputs.revDeleteTog.getValue();
		
		if ( vcData.isRevDeleteChecked ) {
			
			vcData.isHideContentsChecked = vcData.inputs.hideContentsCbx.isSelected();
			
			vcData.isHideSummariesChecked = vcData.inputs.hideSummariesCbx.isSelected();
			
			if ( !vcData.isHideContentsChecked && !vcData.isHideSummariesChecked ) {
				vcData.isRevDeleteChecked = false;
			}
			
		}
		
		if (
			vcData.isRollbackChecked ||
			vcData.isDeleteChecked ||
			vcData.isBlockChecked ||
			vcData.isRevDeleteChecked
		) {
			
			return defer.resolve();
			
		} else {
			
			return defer.reject( new OO.ui.Error( i18n( 'noActions' ) ) );
			
		}
		
	}
	
	async function createWorkingPanel() {
		
		await mw.loader.using( 'oojs-ui.styles.icons-media' );
		
		vcData.inputs.progressBar = new OO.ui.ProgressBarWidget( {
			classes: [ 'vandal-cleaner-progress-bar' ],
			progress: 0
		} ).pushPending();
		
		const progressField = new OO.ui.FieldLayout(
			vcData.inputs.progressBar,
			{
				align: 'top',
				id: 'vandal-cleaner-progress-field',
				label: $( '<span>' )
					.addClass( 'vandal-cleaner-prominent-label' )
					.text( i18n( 'progressLabel' ) )
			}
		);
		
		const progressFieldset = new OO.ui.FieldsetLayout( {
			items: [ progressField ]
		} );
		
		vcData.statusPane = {};
		
		vcData.statusPane.$blockStatus = $( '<p>' )
			.addClass( 'vandal-cleaner-status' )
			.attr( 'data-percent', '0' )
			.append(
				$( '<span>' ).html(
					i18n( 'blockStatus', [ vcData.prettifiedVandal ] )
				)
			);
		
		const blockError = new OO.ui.MessageWidget( {
			classes: [ 'vandal-cleaner-error' ],
			inline: true,
			label: i18n( 'blockError' ),
			type: 'error'
		} );
		
		vcData.statusPane.$blockError = blockError.$element;
		
		vcData.statusPane.$blockContainer = $( '<div>' )
			.addClass( 'vandal-cleaner-status-action-container' )
			.append(
				vcData.statusPane.$blockStatus,
				vcData.statusPane.$blockError
			);
		
		vcData.statusPane.$rollbackStatus = $( '<p>' )
			.addClass( 'vandal-cleaner-status' )
			.attr( 'data-percent', '0' )
			.append(
				$( '<span>' ).html(
					i18n( 'rollbackStatus', [ vcData.prettifiedVandal ] )
				)
			);
		
		const rollbackError = new OO.ui.MessageWidget( {
			classes: [ 'vandal-cleaner-error' ],
			inline: true,
			label: i18n( 'rollbackError' ),
			type: 'error'
		} );
		
		vcData.statusPane.$rollbackError = rollbackError.$element;
		
		vcData.statusPane.$rollbackContainer = $( '<div>' )
			.addClass( 'vandal-cleaner-status-action-container' )
			.append(
				vcData.statusPane.$rollbackStatus,
				vcData.statusPane.$rollbackError
			);
		
		vcData.statusPane.$deleteStatus = $( '<p>' )
			.addClass( 'vandal-cleaner-status' )
			.attr( 'data-percent', '0' )
			.append(
				$( '<span>' ).html(
					i18n( 'deleteStatus', [ vcData.prettifiedVandal ] )
				)
			);
		
		const deleteError = new OO.ui.MessageWidget( {
			classes: [ 'vandal-cleaner-error' ],
			inline: true,
			label: i18n( 'deleteError' ),
			type: 'error'
		} );
		
		vcData.statusPane.$deleteError = deleteError.$element;
		
		vcData.statusPane.$deleteContainer = $( '<div>' )
			.addClass( 'vandal-cleaner-status-action-container' )
			.append(
				vcData.statusPane.$deleteStatus,
				vcData.statusPane.$deleteError
			);
		
		vcData.statusPane.$revDeleteStatus = $( '<p>' )
			.addClass( 'vandal-cleaner-status' )
			.attr( 'data-percent', '0' )
			.append(
				$( '<span>' ).html(
					i18n( 'revDeleteStatus', [ vcData.prettifiedVandal ] )
				)
			);
		
		const revDeleteError = new OO.ui.MessageWidget( {
			classes: [ 'vandal-cleaner-error' ],
			inline: true,
			label: i18n( 'revDeleteError' ),
			type: 'error'
		} );
		
		vcData.statusPane.$revDeleteError = revDeleteError.$element;
		
		vcData.statusPane.$revDeleteContainer = $( '<div>' )
			.addClass( 'vandal-cleaner-status-action-container' )
			.append(
				vcData.statusPane.$revDeleteStatus,
				vcData.statusPane.$revDeleteError
			);
		
		vcData.statusPane.$leftoverVandalismStatus = $( '<p>' )
			.addClass( 'vandal-cleaner-status' )
			.attr( 'data-percent', '0' )
			.append( $( '<span>' ).text( i18n( 'leftoverVandalismStatus' ) ) );
			
		vcData.statusPane.$leftoverVandalismContainer = $( '<div>' )
			.addClass( [
				'vandal-cleaner-status-action-container',
				'vandal-cleaner-fancy-border-top'
			] )
			.append( vcData.statusPane.$leftoverVandalismStatus );
		
		const $statusPane = $( '<div>' )
			.attr( 'id', 'vandal-cleaner-status-pane' )
			.append(
				vcData.statusPane.$blockContainer,
				vcData.statusPane.$rollbackContainer,
				vcData.statusPane.$deleteContainer,
				vcData.statusPane.$revDeleteContainer,
				vcData.statusPane.$leftoverVandalismContainer
			);
			
		this.workingPanel.$element.append(
			progressFieldset.$element,
			$statusPane
		);
		
	}
	
	async function runCleaner( ProcessDialog ) {
		
		vcData.totalTasksCount = getTotalTasksCount();
		vcData.completedTasksCount = 0;
		vcData.hasFailedActions = false;
		vcData.doesTagExist = await checkIfTagExists();
		
		await doBlock();
		await doRollback();
		await doDelete();
		await doRevDelete();
		await checkForLeftoverVandalism();
		
		vcData.inputs.progressBar.popPending();
		
		await waitBeforeProceeding( 180 );
		
		this.workingPanel.$element.fadeTo( '_default', 0.15, () => {
			
			createFinalPanel.call( this );
			
			adaptFinalPanelHeight.call( this, ProcessDialog );
			
			ProcessDialog.static.escapable = true;
			
		} );
		
	}
	
	function getTotalTasksCount() {
		
		let count = 0;
		
		if ( vcData.isBlockChecked ) {
			count++;
		}
		
		if ( vcData.isRollbackChecked ) {
			if ( vcData.revertibleEditsCount === 0 ) {
				count++;
			} else {
				count += vcData.revertibleEditsCount;
			}
		}
		
		if ( vcData.isDeleteChecked ) {
			if ( vcData.deletablePagesCount === 0 ) {
				count++;
			} else {
				count += vcData.deletablePagesCount;
			}
		}
		
		if ( vcData.isRevDeleteChecked ) {
			if ( vcData.hideableRevsCount === 0 ) {
				count++;
			} else {
				count += vcData.hideableRevsCount;
			}
		}
		
		return count;
		
	}
	
	async function checkIfTagExists() {
		
		const data = await api.get( { titles: 'MediaWiki:Tag-VandalCleaner' } );
		
		if ( data.query.pages[ -1 ] ) {
			return false;
		}
		
		return true;
		
	}
	
	async function doBlock() {
		
		if ( !vcData.isBlockChecked ) {
			return;
		}
		
		vcData.statusPane.$blockContainer.fadeIn();
		
		const params = {
			action: 'block',
			user: vandal,
			expiry: vcData.blockDuration,
			nocreate: true,
			autoblock: true,
			noemail: true,
			allowusertalk: isAnon,
			reblock: true,
			reason: vcData.summary,
			tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
		};
		
		try {
			
			await api.postWithToken( 'csrf', params );
			
		} catch ( e ) {
			
			handleError(
				`${ vandal } could not be blocked`,
				e,
				'block',
				[ 'alreadyblocked' ]
			);
			
		} finally {
			
			vcData.statusPane.$blockStatus.attr( 'data-percent', '100' );
			
			updateProgressBar();
			
			await waitBeforeProceeding( 300 );
			
		}
		
	}
	
	async function doRollback() {
		
		if ( !vcData.isRollbackChecked ) {
			return;
		}
		
		addTopBorderIfNeeded( vcData.statusPane.$rollbackContainer );
		
		vcData.statusPane.$rollbackContainer.fadeIn();
		
		if ( vcData.revertibleEditsCount === 0 ) {
			
			vcData.statusPane.$rollbackStatus.attr( 'data-percent', '100' );
			
			updateProgressBar();
			
			await waitBeforeProceeding( 250 );
			
			return;
			
		}
		
		let millisecondsBetweenRollbacks = 300;
		
		if (
			vcData.revertibleEditsCount > 50 &&
			!vcData.editorRights.includes( 'noratelimit' )
		) {
			millisecondsBetweenRollbacks = 650;
		}
		
		const params = {
			summary: vcData.summary,
			tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
		};
		
		let currentEditIndex = 0;
		
		do {
			
			const title = vcData.revertibleEdits[ currentEditIndex ].title;
			
			try {
				
				await api.rollback( title, vandal, params );
				
			} catch ( e ) {
				
				handleError(
					`${ vandal }'s edits on page ${ title } could not be rollbacked`,
					e,
					'rollback',
					[ 'alreadyrolled', 'onlyauthor' ]
				);
				
			} finally {
				
				currentEditIndex++;
				
				vcData.statusPane.$rollbackStatus.attr(
					'data-percent',
					( currentEditIndex / vcData.revertibleEditsCount * 100 ).toFixed( 0 )
				);
				
				updateProgressBar();
				
				await waitBeforeProceeding( millisecondsBetweenRollbacks );
				
			}
			
		} while ( currentEditIndex < vcData.revertibleEditsCount );
		
	}
	
	async function doDelete() {
		
		if ( !vcData.isDeleteChecked ) {
			return;
		}
		
		addTopBorderIfNeeded( vcData.statusPane.$deleteContainer );
		
		vcData.statusPane.$deleteContainer.fadeIn();
		
		if ( vcData.deletablePagesCount === 0 ) {
			
			vcData.statusPane.$deleteStatus.attr( 'data-percent', '100' );
			
			updateProgressBar();
			
			await waitBeforeProceeding( 250 );
			
			return;
			
		}
		
		const params = {
			action: 'delete',
			reason: vcData.summary,
			tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
		};
		
		let currentPageIndex = 0;
		
		do {
			
			params.title = vcData.deletablePages[ currentPageIndex ].title;
			
			try {
				
				await api.postWithToken( 'csrf', params );
				
			} catch ( e ) {
				
				handleError(
					`Page ${ params.title } could not be deleted`,
					e,
					'delete',
					[ 'missingtitle' ]
				);
				
			} finally {
				
				currentPageIndex++;
				
				vcData.statusPane.$deleteStatus.attr(
					'data-percent',
					( currentPageIndex / vcData.deletablePagesCount * 100 ).toFixed( 0 )
				);
				
				updateProgressBar();
				
				await waitBeforeProceeding( 300 );
				
			}
			
		} while ( currentPageIndex < vcData.deletablePagesCount );
		
	}
	
	async function doRevDelete() {
		
		if ( !vcData.isRevDeleteChecked ) {
			return;
		}
		
		addTopBorderIfNeeded( vcData.statusPane.$revDeleteContainer );
		
		vcData.statusPane.$revDeleteContainer.fadeIn();
		
		if ( vcData.hideableRevsCount === 0 ) {
			
			vcData.statusPane.$revDeleteStatus.attr( 'data-percent', '100' );
			
			updateProgressBar();
			
			await waitBeforeProceeding( 250 );
			
			return;
			
		}
		
		let itemsToHide = 'content|comment';
		
		if ( !vcData.isHideSummariesChecked ) {
			itemsToHide = 'content';
		} else if ( !vcData.isHideContentsChecked ) {
			itemsToHide = 'comment';
		}
		
		const params = {
			action: 'revisiondelete',
			type: 'revision',
			hide: itemsToHide,
			reason: vcData.summary,
			tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
		};
		
		let currentRevIndex = 0;
		
		do {
			
			params.ids = vcData.hideableRevs[ currentRevIndex ].revid;
			params.target = vcData.hideableRevs[ currentRevIndex ].title;
			
			try {
				
				await api.postWithToken( 'csrf', params );
				
			} catch ( e ) {
				
				handleError(
					`Revision ${ params.ids } on page ${ params.target } could not be hidden`,
					e,
					'revDelete'
				);
				
			} finally {
				
				currentRevIndex++;
				
				vcData.statusPane.$revDeleteStatus.attr(
					'data-percent',
					( currentRevIndex / vcData.hideableRevsCount * 100 ).toFixed( 0 )
				);
				
				updateProgressBar();
				
				await waitBeforeProceeding( 300 );
				
			}
			
		} while ( currentRevIndex < vcData.hideableRevsCount );
		
	}
	
	async function checkForLeftoverVandalism() {
		
		if (
			!vcData.isRollbackChecked ||
			vcData.revertibleEditsCount === 0 ||
			(
				!vcData.editorRights.includes( 'patrol' ) &&
				!vcData.editorRights.includes( 'patrolmarks' )
			)
		) {
			return;
		}
		
		vcData.statusPane.$leftoverVandalismContainer.fadeIn();
		
		vcData.leftoverVandalismSuspects = [];
		
		const params = {
			list: 'recentchanges',
			rcexcludeuser: vandal,
			rcprop: 'ids|patrolled',
			rcshow: '!bot',
			rclimit: 1,
			rctype: 'edit'
		};
		
		for ( const [ index, edit ] of vcData.revertibleEdits.entries() ) {
			
			if ( ![ 0, 10, 12, 14, 100, 828 ].includes( edit.ns ) ) {
				continue;
			}
			
			const daysFromEdit =
				( new Date( mw.now() ) - new Date( edit.timestamp ) ) / 1000 / 60 / 60 / 24;
			
			if ( daysFromEdit >= 30 ) {
				break;
			}
			
			params.rcstart = edit.timestamp;
			params.rcend = subtractDaysFromTimestamp( edit.timestamp, 5 );
			params.rctitle = edit.title;
			
			const data = await api.get( params );
			
			if (
				data.query.recentchanges[ 0 ] &&
				data.query.recentchanges[ 0 ].unpatrolled === ''
			) {
				
				vcData.leftoverVandalismSuspects.push( {
					title: edit.title,
					id: data.query.recentchanges[ 0 ].revid
				} );
				
			}
			
			vcData.statusPane.$leftoverVandalismStatus.attr(
				'data-percent',
				( ( index + 1 ) / vcData.revertibleEditsCount * 100 ).toFixed( 0 )
			);
			
		}
		
		vcData.statusPane.$leftoverVandalismStatus.attr( 'data-percent', '100' );
		
	}
	
	function handleError( errorMsg, errorCode, action, ignoredErrorCodes = [] ) {
		
		console.log(
			`⚠️ Vandal Cleaner: ${ errorMsg } (%c${ errorCode }%c)`,
			'font-weight: bold;',
			''
		);
		
		if ( ignoredErrorCodes.includes( errorCode ) ) {
			return;
		}
		
		vcData.statusPane[ `$${ action }Status` ]
		.addClass( 'vandal-cleaner-status-failure' );
		
		vcData.statusPane[ `$${ action }Error` ].fadeIn();
		
		if ( vcData.hasFailedActions ) {
			return;
		}
		
		vcData.inputs.progressBar.$element
		.addClass( 'vandal-cleaner-progress-bar-failure' );
		
		vcData.hasFailedActions = true;
		
	}
	
	function addTopBorderIfNeeded( $element ) {
		
		if (
			$element.siblings( '.vandal-cleaner-status-action-container:visible' )
			.length
		) {
			$element.addClass( 'vandal-cleaner-fancy-border-top' );
		}
		
	}
	
	function updateProgressBar() {
		
		vcData.completedTasksCount++;
		
		vcData.inputs.progressBar.setProgress(
			vcData.completedTasksCount / vcData.totalTasksCount * 100
		);
		
	}
	
	function waitBeforeProceeding( milliseconds ) {
		
		const defer = $.Deferred();
		
		setTimeout( () => defer.resolve(), milliseconds );
		
		return defer;
		
	}
	
	function createFinalPanel() {
		
		const successMsg = new OO.ui.MessageWidget( {
			icon: 'success',
			id: 'vandal-cleaner-success-msg',
			label: i18n( 'finishedRunning' ),
			type: 'success'
		} );
		
		const $thumbImg = $( '<img>' ).attr( {
			alt: i18n( 'finishedRunning' ),
			id: 'vandal-cleaner-thumb-img',
			src: 'https://upload.wikimedia.org/wikipedia/commons/c/ce/Emoji_u1f44d.svg'
		} );
		
		const $outroText = $( '<div>' )
			.attr( 'id', 'vandal-cleaner-outro-text' );
		
		if ( vcData.hasFailedActions ) {
			
			const unrevertedEditsUrl = mw.util.getUrl( 'Special:Contributions', {
				target: vandal,
				topOnly: '1'
			} );
			
			const unrevertedEditsError = new OO.ui.MessageWidget( {
				classes: [ 'vandal-cleaner-error' ],
				id: 'vandal-cleaner-unreverted-edits-error',
				inline: true,
				label: new OO.ui.HtmlSnippet(
					i18n( 'unrevertedEditsError', [ unrevertedEditsUrl ] )
				),
				type: 'error'
			} );
			
			$outroText.append( unrevertedEditsError.$element );
			
		}
		
		if (
			vcData.leftoverVandalismSuspects &&
			vcData.leftoverVandalismSuspects.length > 0
		) {
			
			const $leftoverVandalismOpeningText = $( '<p>' ).text(
				i18n(
					'leftoverVandalismOpeningText',
					[ vcData.leftoverVandalismSuspects.length ]
				)
			);
			
			const $leftoverVandalismList = $( '<ul>' )
				.attr( 'id', 'vandal-cleaner-leftover-vandalism-list' );
			
			const $leftoverVandalismContainer = $( '<div>' )
				.attr( 'id', 'vandal-cleaner-leftover-vandalism-container' )
				.append( $leftoverVandalismOpeningText, $leftoverVandalismList );
			
			vcData.leftoverVandalismSuspects.forEach( item => {
				
				const pageUrl = mw.util.getUrl( item.title );
				
				const $pageLink = $( '<a>' )
					.addClass( 'vandal-cleaner-leftover-vandalism-page-link' )
					.attr( { href: pageUrl, target: '_blank' } );
				
				const $pageLinkText = $( '<bdi>' )
					.addClass( 'vandal-cleaner-leftover-vandalism-page-link-text' )
					.attr( 'title', item.title )
					.text( item.title );
				
				$pageLink.append( $pageLinkText );
				
				const diffUrl = mw.util.getUrl( `Special:Diff/${ item.id }` );
				
				const $diffLink = $( '<a>' )
					.attr( { href: diffUrl, target: '_blank' } )
					.text( i18n( 'leftoverVandalismDiff' ) );
				
				const histUrl = mw.util.getUrl( item.title, { action: 'history' } );
				
				const $histLink = $( '<a>' )
					.attr( { href: histUrl, target: '_blank' } )
					.text( i18n( 'leftoverVandalismHist' ) );
				
				const $li = $( '<li>' )
					.addClass( 'vandal-cleaner-leftover-vandalism-list-item' )
					.append( $pageLink, ' (', $diffLink, ' | ', $histLink, ')' );
				
				$leftoverVandalismList.append( $li );
				
			} );
			
			$outroText.append( $leftoverVandalismContainer );
			
		}
		
		const recentChangesUrl = mw.util.getUrl( 'Special:RecentChanges', {
			days: '30',
			enhanced: null,
			hidebyothers: '1',
			tagfilter: vcData.doesTagExist ? 'VandalCleaner' : null,
			urlversion: '2'
		} );
		
		$outroText.append(
			$( '<p>' ).text( i18n( 'outroThankYou' ) ),
			$( '<p>' ).html( i18n( 'outroReviewActions', [ recentChangesUrl ] ) ),
			$( '<p>' ).text( i18n( 'outroKeepItUp', [ mw.config.get( 'wgSiteName' ) ] ) ),
			$( '<p>' ).text( i18n( 'outroClose' ) )
		);
		
		const $outroContainer = $( '<div>' )
			.attr( 'id', 'vandal-cleaner-outro-container' )
			.append( $thumbImg, $outroText );
		
		this.finalPanel.$element.append( successMsg.$element, $outroContainer );
		
		this.stackLayout.setItem( this.finalPanel );
		this.actions.setMode( 'final' );
		this.actions.list.find( item => item.modes === 'final' ).focus();
		
		
		// Allow non-sysops to submit block requests
		// (on Hebrew WMF projects only, for now).
		// TODO: Implement this feature in a better way.
		if (
			!vcData.editorRights.includes( 'block' ) &&
			mw.config.get( 'wgWikiID' ).slice( 0, 5 ) === 'hewik'
		) {
			
			const blockRequestReasonInput = new OO.ui.TextInputWidget( {
				id: 'vandal-cleaner-block-request-reason-input-widget',
				inputId: 'vandal-cleaner-block-request-reason-input-element',
				placeholder: i18n( 'blockRequestPlaceholder' )
			} );
			
			const blockRequestReasonLabel = new OO.ui.LabelWidget( {
				input: blockRequestReasonInput,
				label: i18n( 'blockRequestLabel' )
			} );
			
			const blockRequestSubmitBtn = new OO.ui.ButtonWidget( {
				id: 'vandal-cleaner-block-request-submit-btn',
				label: i18n( 'blockRequestSubmitBtn' )
			} );
			
			blockRequestSubmitBtn.on( 'click', () => {
				
				blockRequestReasonInput.setDisabled( true );
				
				blockRequestSubmitBtn
					.setDisabled( true )
					.setLabel( i18n( 'blockRequestSubmitting' ) );
				
				const reason = blockRequestReasonInput.getValue().trim();
				
				const params = {
					action: 'edit',
					title: 'Project:בקשות ממפעילים',
					redirect: true,
					section: 2,
					appendtext: `${ '\n\n' }* {${ '{' }לחסום|${ vandal }}} – ${ reason } ~~${ '~~' }`,
					summary: `/* בקשות חסימה / הסרת חסימה */ [[משתמש:${ vandal }|${ vandal }]] ([[שיחת משתמש:${ vandal }|ש]]|[[מיוחד:תרומות/${ vandal }|ת]]|[[מיוחד:חסימה/${ vandal }|ח]])`,
					tags: vcData.doesTagExist ? 'VandalCleaner' : undefined
				};
				
				api.postWithEditToken( params ).then(
					() => blockRequestSubmitBtn.setLabel( i18n( 'blockRequestSubmitDone' ) ),
					() => blockRequestSubmitBtn.setLabel( i18n( 'blockRequestSubmitError' ) )
				);
				
			} );
			
			blockRequestReasonInput.on( 'enter', () =>
				blockRequestSubmitBtn.$element
				.children( '.oo-ui-buttonElement-button' )[ 0 ].click()
			);
			
			const blockRequestLayout = new OO.ui.HorizontalLayout( {
				id: 'vandal-cleaner-block-request-layout',
				items: [ blockRequestReasonInput, blockRequestSubmitBtn ]
			} );
			
			const $blockRequestContainer = $( '<div>' )
				.attr( 'id', 'vandal-cleaner-block-request-container' )
				.append( blockRequestReasonLabel.$element, blockRequestLayout.$element );
			
			$outroText.append( $blockRequestContainer );
			
		}
		
	}
	
	function adaptFinalPanelHeight( ProcessDialog ) {
		
		const imgElement = document.getElementById( 'vandal-cleaner-thumb-img' );
		
		const observer = new IntersectionObserver( entries => {
			
			if ( entries[ 0 ].isIntersecting ) {
				
				const currentHeight = this.$body[ 0 ].scrollHeight;
				
				ProcessDialog.prototype.getBodyHeight = () => currentHeight;
				
				this.updateSize();
				
				const newHeight = this.finalPanel.$element[ 0 ].scrollHeight + 20;
				
				ProcessDialog.prototype.getBodyHeight = () => newHeight;
				
				this.updateSize();
				
				observer.unobserve( imgElement );
				
			}
			
		} );
		
		observer.observe( imgElement );
		
	}
	
	function scrollIntoViewIfNeeded(
		$watchTarget, watchThreshold, $scrollTarget, scrollBlock
	) {
		
		if (
			$scrollTarget[ 0 ] &&
			window.innerHeight - $watchTarget[ 0 ].getBoundingClientRect().bottom
			< watchThreshold
		) {
			
			$scrollTarget[ 0 ].scrollIntoView( {
				block: scrollBlock,
				behavior: 'smooth'
			} );
			
		}
		
	}
	
	function refineErrorDialog( ProcessDialog ) {
		
		const dismissBtnSelector =
			`#vandal-cleaner-dialog .oo-ui-processDialog-errors-actions >
			.oo-ui-buttonWidget:first-child .oo-ui-buttonElement-button`;
		
		const dismissBtn = document.querySelector( dismissBtnSelector );
		
		$( dismissBtn ).find( '.oo-ui-labelElement-label' )
		.text( i18n( 'errorDialogDismissBtnLabel' ) );
		
		const observer = new IntersectionObserver( entries => {
			
			if ( entries[ 0 ].isIntersecting ) {
				
				ProcessDialog.static.escapable = false;
				
				entries[ 0 ].target.focus();
				
				$( document ).on( 'keydown.closeErrorDialog', e => {
					
					if ( e.key === 'Escape' ) {
						entries[ 0 ].target.click();
					}
					
				} );
				
			} else {
				
				$( document ).off( 'keydown.closeErrorDialog' );
				
				ProcessDialog.static.escapable = true;
				
			}
			
		} );
		
		observer.observe( dismissBtn );
		
	}
	
	function getPref( key ) {
		
		return mw.user.options.get( `userjs-VandalCleaner-${ key }` );
		
	}
	
	function setPref( key, value ) {
		
		api.saveOption( `userjs-VandalCleaner-${ key }`, value );
		
	}
	
} )();