User:Alexis Jazz/Kill-It-With-Fire.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
* Kill-It-With-Fire
* Forked from Restore-a-lot which was in turn forked from Cat-a-lot
* Undo multiple edits from Special:Contributions
* See ? for documentation (that inspires confidence)
*
* @rev 00:13, 10 February 2018 (UTC)
* @author Originally by Magnus Manske (2007)
* @author RegExes by Ilmari Karonen (2010)
* @author Completely rewritten by DieBuche (2010-2012)
* @author Rillke (2012-2014)
* @author Perhelion (2017)
* @author Alexis Jazz is a forking idiot (Restore-a-lot, 2020)
* @author Various fixes by Zhuyifei1999 (Restore-a-lot, 2020)
* @author Alexis Jazz is screwing around again (Kill-It-With-Fire, 2022)
* <nowiki>
*/

/* global jQuery, mediaWiki */
/* eslint one-var:0, vars-on-top:0, no-underscore-dangle:0, valid-jsdoc:0,
curly:0, camelcase:0, no-useless-escape:0, no-alert:0 */ // extends: wikimedia
/* jshint unused:true, forin:false, smarttabs:true, loopfunc:true, browser:true */

( function ( $, mw ) {
'use strict';

var ns = mw.config.get( 'wgNamespaceNumber' );

var msgs = {
// Preferences
// new: added 2012-09-19. Please translate.
// Use user language for i18n
	'restore-a-lot-comment-label': 'Custom undo reason',
	'restore-a-lot-edit-question': 'Why is this undo necessary?',

	// Progress
	// 'restore-a-lot-loading': 'Loading …',
	'restore-a-lot-editing': 'Undoing edit',
	'restore-a-lot-of': 'of ',
	'restore-a-lot-skipped-server': 'The following {{PLURAL:$1|1=page|$1 pages}} couldn’t be changed, since there were problems connecting to the server:',
	'restore-a-lot-all-done': 'Selected revisions have been undone.',
	'restore-a-lot-done': 'Done!', // mw.msg("Feedback-close")
	'restore-a-lot-undelete': 'Revision undone',

	// as in 17 files selected
	'restore-a-lot-files-selected': '{{PLURAL:$1|1=One revision|$1 revisions}} selected.',

	// Actions
	'restore-a-lot-undeletelink': 'Undo',
	'restore-a-lot-instructions': 'Select revisions, hit undo.',
	'restore-a-lot-select': 'Select',
	'restore-a-lot-all': 'all',
	'restore-a-lot-none': 'none',
	// 'restore-a-lot-none-selected': 'No files selected!', 'Ooui-selectfile-placeholder'

	// Summaries (project language):
	'restore-a-lot-summary': 'Undo revision [[Special:Diff/REVID|REVID]] by [[User:USER|USER]] [Undone using [[w:en:WP:Kill-It-With-Fire|Kill-It-With-Fire]]]',
};
mw.messages.set( msgs );

function msg( /* params */ ) {
	var args = Array.prototype.slice.call( arguments, 0 );
	args[ 0 ] = 'restore-a-lot-' + args[ 0 ];
	return ( args.length === 1 ) ?
		mw.message( args[ 0 ] ).plain() :
		mw.message.apply( mw.message, args ).parse();
}

mw.util.addCSS(
'#killitwithfire {bottom: 0;display: block;position: fixed;left: 0;z-index: 100;padding: 5px;box-shadow: 0 2px 4px rgba(0,0,0,0.5); background-color: #FEF6E7;}'+
'#killitwithfire.ui-resizable {min-width:15em;}'+
'#killitwithfire_data, #killitwithfire_mark_counter {display: none;}'+
'#killitwithfire_data ul {list-style-image: none;list-style-type: none;margin: 0 0 0 5px;}'+
//'#killitwithfire_selections, #killitwithfire_mark_counter, #killitwithfire_settings {background: url(/w/skins/Vector/images/portal-break.png) no-repeat;padding: 5px;}'+
'#killitwithfire_remove {font-weight: bold;display: block;}'+
'a {cursor:pointer;}'+
'.killitwithfire_move, .killitwithfire_action {font-weight: bold;}'+
'.killitwithfire_feedback {border: 1px #A9DE16 solid !important;background: #EAF2CB /*url(//up.wiki.x.io/wikipedia/commons/d/de/Ajax-loader.gif)*/ no-repeat 8px 14px !important;padding-left: 2.85em !important;padding-top: 10px !important;font-size: 1.1em !important;}'+
'.killitwithfire_done {background-image: url(//up.wiki.x.io/wikipedia/commons/thumb/0/0e/Dialog-apply.svg/50px-Dialog-apply.svg.png) !important;background-position: 8px 50% !important;padding-top: 0 !important;}'+
'#killitwithfire_searchcatname,#killitwithfire_comment {font-size: 112%;margin: -5px 0 5px -5px;width: 100%;}'+
'.skin-vector #killitwithfire {font-size: .75em;}'+
'.killitwithfire_markAsDone {background-color: #BBB !important;}'+
'.killitwithfire_selected {background-color: #DF6 !important;}'+
'#killitwithfire_no_found, #killitwithfire_last_selected, #killitwithfire_settings {font-weight:bold;}'+
'#killitwithfire_last_selected {outline:1px dotted #999;}'+
'#killitwithfire_category_list table {border-collapse: collapse;}'+
'#killitwithfire_category_list tr:hover {background-color: #fc3;}'+
'#killitwithfire_category_list {overflow: auto;}');

// There is only one Restore-a-lot on one page
var $body, $container, $dataContainer, $markCounter, $instructions, $selections,
	$selectFiles, $selectPages, $selectNone, $selectInvert, $head, $link;

var KIWF = mw.libs.restoreALot = {
	apiUrl: mw.util.wikiScript( 'api' ),
	origin: '',
	searchmode: false,
	version: '4.77',
	setHeight: 450,
	settings: '',

	init: function () {

		$body = $( document.body );
		$container = $( '<div>' )
			.attr( 'id', 'killitwithfire' )
			.appendTo( $body );
		$dataContainer = $( '<div>' )
			.attr( 'id', 'killitwithfire_data' )
			.appendTo( $container );
		$markCounter = $( '<div>' )
			.attr( 'id', 'killitwithfire_mark_counter' )
			.appendTo( $dataContainer );
		$instructions = $( '<div>' )
			.attr( 'id', 'killitwithfire_selections' )
			.text( msg( 'instructions' ) )
			.appendTo( $dataContainer );
		$selections = $( '<div>' )
			.attr( 'id', 'killitwithfire_selections' )
			.text( msg( 'select' ) + ':' )
			.appendTo( $dataContainer );
		$head = $( '<div>' )
			.attr( 'id', 'killitwithfire_head' )
			.appendTo( $container );
		$link = $( '<a>' )
			.attr( 'id', 'killitwithfire_toggle' )
			.text( 'Kill-It-With-Fire' )
			.appendTo( $head );
		$container.one( 'mouseover', function () { // Try load on demand earliest as possible
			mw.loader.load( [ 'jquery.ui'] );
		} );

		$( '<a>' )
		// .attr( 'id', 'killitwithfire_select_all' )
			.text( msg( 'all' ) )
			.on( 'click', function () {
				KIWF.toggleAll( true );
			} )
			.appendTo( $selections.append( ' ' ) );
		if ( this.settings.editpages ) {
			$selectFiles = $( '<a>' )
				.on( 'click', function () {
					KIWF.toggleAll( 'files' );
				} );
			$selectPages = $( '<a>' )
				.on( 'click', function () {
					KIWF.toggleAll( 'pages' );
				} );
			$selections.append( $( '<span>' ).hide().append( [ ' / ', $selectFiles, ' / ', $selectPages ] ) );
		}
		$selectNone = $( '<a>' )
		// .attr( 'id', 'killitwithfire_select_none' )
			.text( msg( 'none' ) )
			.on( 'click', function () {
				KIWF.toggleAll( false );
			} );
		$selectInvert = $( '<a>' )
			.on( 'click', function () {
				KIWF.toggleAll( null );
			} );
		$selections.append( [ ' • ', $selectNone, ' • ', $selectInvert] );

		$('#killitwithfire_data').append(
			$( '<div>' ).append( [
				$( '<input>' )
					.attr( {
						id: 'killitwithfire_comment',
						tabindex: -1,
						type: 'text',
						placeholder: msg( 'comment-label' )
					} )
			] )
		);
		
		$selections.append( $( '<a>' )
			.text( msg( 'undeletelink' ) )
			.on( 'click', function () {
				KIWF.doSomething();
			} )
			.addClass( 'killitwithfire_action ui-button' )
			.css( { margin: '5px', padding: '2px' } )
		);

		$link
			.on( 'click', function () {
				$( this ).toggleClass( 'killitwithfire_enabled' );
				// Load autocomplete on demand
				mw.loader.using( 'jquery.ui' );

				if ( !KIWF.executed ) {
					$.when( mw.loader.using( [
						'jquery.ui',
						'jquery.ui',
						'jquery.ui',
						'mediawiki.api',
						'mediawiki.jqueryMsg'
					] ), $.ready )
						.then( function () {
							return new mw.Api().loadMessagesIfMissing( [
								'Cancel',
								'Mobile-frontend-return-to-page',
								'Ooui-selectfile-placeholder',
								// 'Visualeditor-clipboard-copy',
								'Prefs-files',
								'Categories',
								'Checkbox-invert',
								//'Apifeatureusage-warnings'
							] );
						} ).then( function () {
							KIWF.run();
						} );
				} else { KIWF.run(); }
			} );
		mw.loader.using( 'mediawiki.cookie', function () { // Let catAlot stay open
			var val = mw.cookie.get( 'catAlotO' );
			if ( val && Number( val ) === ns ) { $link.click(); }
		}
		);
	},

	findAllLabels: function ( searchmode ) {
	// It's possible to allow any kind of pages as well but what happens if you click on "select all" and don't expect it
		switch ( searchmode ) {
			case 'contribs':
				this.labels = this.labels.add( $( 'div.mw-body-content li' ) );
				break;
		}
	},

	/**
*  @brief Get rev from selected pages
*  @return [array] touple of page rev and $object
*/
	getMarkedLabels: function () {
		this.selectedLabels = this.labels.filter( '.killitwithfire_selected:visible' );
		return this.selectedLabels.map( function () {
			// this might select too much in cases currently unknown, does it even matter?
			// it was 'a.new[class$="title"]' previously but this doesn't work on DRs
			var label = $( this ), revHref = label[0].querySelectorAll('.mw-changeslist-diff')[0].attributes.href;
			return [ [ revHref, label ] ];
		} );
	},

	updateSelectionCounter: function () {
		this.selectedLabels = this.labels.filter( '.killitwithfire_selected:visible' );
		var first = $markCounter.is( ':hidden' );
		$markCounter
			.html( msg( 'files-selected', this.selectedLabels.length ) )
			.show();
		if ( first && !$dataContainer.is( ':hidden' ) ) { // Workaround to fix position glitch
			first = $markCounter.innerHeight();
			$container
				.offset( { top: $container.offset().top - first } )
				.height( $container.height() + first );
			$( window ).on( 'beforeunload', function () {
				if ( KIWF.labels.filter( '.killitwithfire_selected:visible' )[ 0 ] ) { return 'You have pages selected!'; } // There is a default message in the browser
			} );
		}
	},

	makeClickable: function () {
		this.labels = $();
		this.pageLabels = $(); // only for distinct all selections
		this.findAllLabels( this.searchmode );
		this.labels.KIWFShiftClick( function () {
			KIWF.updateSelectionCounter();
		} )
			.addClass( 'killitwithfire_label' );
	},

	toggleAll: function ( select ) {
		if ( typeof select === 'string' && this.pageLabels[ 0 ] ) {
			this.pageLabels.toggleClass( 'killitwithfire_selected', true );
			if ( select === 'files' ) // pages get deselected
			{ this.labels.toggleClass( 'killitwithfire_selected' ); }
		} else {
		// invert / none / all
			this.labels.toggleClass( 'killitwithfire_selected', select );
		}
		this.updateSelectionCounter();
	},
	undeleteFile: function ( rev ) {
		var revIDToUndo;
		revIDToUndo = new mw.Uri(window.location.protocol+'//'+window.location.host+rev[0].nodeValue).query.oldid;
		var sumCmt; // summary comment
		sumCmt = msg( 'summary' ).replace(/USER/g,mw.config.get('wgRelevantUserName')).replace(/REVID/g,revIDToUndo);
		sumCmt += this.summary ? ' ' + this.summary : '';
		var data = {
			action: 'edit',
			summary: sumCmt,
			title: new mw.Uri(window.location.protocol+'//'+window.location.host+rev[0].nodeValue).query.title.replace(/_/g,' '),
			undo: revIDToUndo,
			token: this.edittoken
		};
		
		this.doAPICall( data, function ( r ) {
			delete KIWF.XHR[ rev[ 0 ] ];
			return KIWF.updateCounter( r );
		} );
		this.markAsDone( rev[ 1 ] );
	},

	markAsDone: function ( label ) {
		label.addClass( 'killitwithfire_markAsDone' ).append( '<br>' + msg( 'undelete' ) );
	},

	updateCounter: function () {
		this.counterCurrent++;
		if ( this.counterCurrent > this.counterNeeded ) { this.displayResult(); } else { this.domCounter.text( this.counterCurrent ); }
	},

	displayResult: function () {
		document.body.style.cursor = 'auto';
		this.progressDialog.parent()
			.addClass( 'killitwithfire_done' )
			.find( '.ui-dialog-buttonpane button span' ).eq( 0 )
			.text( mw.msg( 'Mobile-frontend-return-to-page' ) );
		var rep = this.domCounter.parent()
			.height( 'auto' )
			.html( '<h3>' + msg( 'done' ) + '</h3>' )
			.append( msg( 'all-done' ) + '<br>' );

		if ( this.connectionError.length ) {
			rep.append( '<h5>' + msg( 'skipped-server', this.connectionError.length ) + '</h5>' )
				.append( this.connectionError.join( '<br>' ) );
		}

	},

	doSomething: function () {
		var pages = this.getMarkedLabels();
		if ( !pages.length ) { return alert( mw.msg( 'Ooui-selectfile-placeholder' ) ); }

		this.connectionError = [];
		this.counterCurrent = 1;
		this.counterNeeded = pages.length;
		this.XHR = {};
		this.cancelled = 0;
		this.summary = '';

		if ( $( '#killitwithfire_comment' )[0].value != '' ) { this.summary = $( '#killitwithfire_comment' )[0].value; } // TODO custom pre-value

		if ( this.summary !== null ) {
			mw.loader.using( [ 'jquery.ui', 'mediawiki.util' ], function () {
				KIWF.showProgress();
				if ( !KIWF.cancelled ) {
					KIWF.doAPICall( {
						meta: 'tokens',
					}, function ( result ) {
						if ( !result || !result.query ) { return; }
						KIWF.edittoken = result.query.tokens.csrftoken;
						pages = Array.from( pages ).map( function ( page ) {
							return function () {
								var defer = $.Deferred();
								setTimeout( function timer() {
									KIWF.undeleteFile( page );
									defer.resolve();
								}, 1500 );
								return defer;
							};
						} );

						var defer = pages.shift()();
						pages.map( function ( pagefunc ) {
							defer = defer.then( pagefunc );
						} );
					} );
				}
			} );
		}

	},

	doAPICall: function ( params, callback ) {
		params = $.extend( {
			action: 'query',
			format: 'json'
		}, params );

		var i = 0,
			apiUrl = this.apiUrl,
			doCall,
			handleError = function ( jqXHR, textStatus, errorThrown ) {
				mw.log( 'Error: ', jqXHR, textStatus, errorThrown );
				if ( i < 4 ) {
					window.setTimeout( doCall, 300 );
					i++;
				} else if ( params.undo ) {
					this.connectionError.push( params.undo );
					this.updateCounter();
					return;
				}
			};
		doCall = function () {
			var xhr = $.ajax( {
				url: apiUrl,
				cache: false,
				dataType: 'json',
				data: params,
				type: 'POST',
				success: callback,
				error: handleError
			} );

			if ( params.action === 'undelete' && !KIWF.cancelled ) { KIWF.XHR[ params.undo ] = xhr; }
		};
		doCall();
	},

	doAbort: function () {
		for ( var t in this.XHR ) { this.XHR[ t ].abort(); }

		if ( this.cancelled ) { // still not for undo
			this.progressDialog.remove();
			this.toggleAll( false );
			$head.last().show();
		}
		this.cancelled = 1;
	},

	showProgress: function () {
		document.body.style.cursor = 'wait';
		this.progressDialog = $( '<div>' )
			.html( ' ' + msg( 'editing' ) + ' <span id="killitwithfire_current">' + KIWF.counterCurrent + '</span> ' + msg( 'of' ) + KIWF.counterNeeded )
			.dialog( {
				width: 450,
				height: 180,
				minHeight: 90,
				modal: true,
				resizable: false,
				draggable: false,
				// closeOnEscape: true,
				dialogClass: 'killitwithfire_feedback',
				buttons: [ {
					text: mw.msg( 'Cancel' ), // Stops all actions
					click: function () {
						$( this ).dialog( 'close' );
					}
				} ],
				close: function () {
					KIWF.cancelled = 1;
					KIWF.doAbort();
					$( this ).remove();
				},
				open: function ( event, ui ) { // Workaround modify
					ui = $( this ).parent();
					ui.find( '.ui-dialog-titlebar' ).hide();
					ui.find( '.ui-dialog-buttonpane.ui-widget-content' )
						.removeClass( 'ui-widget-content' );
				/* .find( 'span' ).css( { fontSize: '90%' } )*/
				}
			} );
		if ( $head.children().length < 3 ) {
			$( '<span>' )
				.css( {
					'float': 'right',
					fontSize: '75%'
				} );
		}

		this.domCounter = $( '#killitwithfire_current' );
	},

	minimize: function ( e ) {
		KIWF.top = Math.max( 0, $container.position().top );
		KIWF.height = $container.height();
		$dataContainer.hide();
		$container.animate( {
			height: $head.height(),
			top: $( window ).height() - $head.height() * 1.4
		}, function () {
			$( e.target ).one( 'click', KIWF.maximize );
		} );
	},

	maximize: function ( e ) {
		$dataContainer.show();
		$container.animate( {
			top: KIWF.top,
			height: KIWF.height
		}, function () {
			$( e.target ).one( 'click', KIWF.minimize );
		} );
	},

	run: function () {
		if ( $( '.killitwithfire_enabled' )[ 0 ] ) {
			this.makeClickable();
			if ( !this.executed ) { // only once
				$selectInvert.text( mw.msg( 'Checkbox-invert' ) );
				if ( this.settings.editpages && this.pageLabels[ 0 ] ) {
					$selectFiles.text( mw.msg( 'Prefs-files' ) );
					$selectPages.text( mw.msg( 'Categories' ) ).parent().show();
				}
				//$link.after( $( '<a>' )
				//	.text( '–' )
				//	.css( { fontWeight: 'bold', marginLeft: '.7em' } )
				//	.one( 'click', this.minimize ),
				$link.after( $( '<a href="https://commons.wikimedia.org/wiki/Special:MyLanguage/Help:Gadget-Restore-a-lot">' )
					.text( 'Kill-It-With-Fire 0.1' )
					.css( { float: 'right' } )
				);
			}
			$dataContainer.show();
			$container.one( 'mouseover', function () {
				$( this )
					.resizable( {
						handles: 'n',
						alsoResize: '#killitwithfire_category_list',
						start: function ( e, ui ) { // Otherwise box get static if sametime resize with draggable
							ui.helper.css( {
								top: ui.helper.offset().top - $( window ).scrollTop(),
								position: 'fixed'
							} );
						},
					} )
					.draggable( {
						cursor: 'move',
						start: function ( e, ui ) {
							ui.helper.on( 'click.prevent',
								function ( e ) { e.preventDefault(); }
							);
							ui.helper.css( 'height', ui.helper.height() );
						},
						stop: function ( e, ui ) {
							setTimeout(
								function () {
									ui.helper.off( 'click.prevent' );
								}, 300
							);
						}
					} )
					.one( 'mousedown', function () {
						$container.height( $container.height() ); // Workaround to calculate
					} );
			} );

			$link.html( $( '<span>' )
				.text( '×' )
				.css( { font: 'bold 2em monospace', lineHeight: '.75em' } )
			);
			$link.next().show();
			if ( this.cancelled ) { $head.last().show(); }
			mw.cookie.set( 'catAlotO', ns ); // Let stay open on new window
		} else { // Reset
			$dataContainer.hide();
			$container
				.draggable( 'destroy' )
				.resizable( 'destroy' )
				.removeAttr( 'style' );
			// Unbind click handlers
			this.labels.off( 'click.KIWF' );
			this.setHeight = 450;
			$link.text( 'Kill-It-With-Fire' )
				.nextAll().hide();
			this.executed = 1;
			mw.cookie.set( 'catAlotO', null );
		}
	},
};

// The gadget is not immediately needed, so let the page load normally
window.setTimeout( function () {

	switch ( ns ) {
		case -1:
			KIWF.searchmode = {
				'Contributions': 'contribs',
			}[ mw.config.get( 'wgCanonicalSpecialPageName' ) ];
			break;
	}		

	if ( KIWF.searchmode ) {
		var maybeLaunch = function () {
			function init() {
				$( function () {
					KIWF.init();
				} );
			}
			mw.loader.using( [ 'user' ], init, init );
		};
		maybeLaunch();
	}
}, 400 );

/**
 * When clicking a restore-a-lot label with Shift pressed, select all labels between the current and last-clicked one.
 */
$.fn.KIWFShiftClick = function ( cb ) {
	var prevCheckbox = null,
		$box = this;
	// When our boxes are clicked..
	$box.on( 'click.KIWF', function ( e ) {
	// Prevent following the link and text selection
		if ( !e.ctrlKey ) { e.preventDefault(); }
		// Highlight last selected
		$( '#killitwithfire_last_selected' )
			.removeAttr( 'id' );
		var $thisControl = $( e.target ),
			method;
		if ( !$thisControl.hasClass( 'killitwithfire_label' ) ) { $thisControl = $thisControl.parents( '.killitwithfire_label' ); }

		$thisControl.attr( 'id', 'killitwithfire_last_selected' )
			.toggleClass( 'killitwithfire_selected' );
		// And one has been clicked before…
		if ( prevCheckbox !== null && e.shiftKey ) {
			method = $thisControl.hasClass( 'killitwithfire_selected' ) ? 'addClass' : 'removeClass';
			// Check or uncheck this one and all in-between checkboxes
			$box.slice(
				Math.min( $box.index( prevCheckbox ), $box.index( $thisControl ) ),
				Math.max( $box.index( prevCheckbox ), $box.index( $thisControl ) ) + 1
			)[ method ]( 'killitwithfire_selected' );
		}
		// Either way, update the prevCheckbox variable to the one clicked now
		prevCheckbox = $thisControl;
		if ( $.isFunction( cb ) ) { cb(); }
	} );
	return $box;
};

}( jQuery, mediaWiki ) );
// </nowiki>