MediaWiki:Gadget-Restore-a-lot.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
* Restore-a-lot
* Forked from Cat-a-lot
* Undelete multiple files from Special:DeletedContributions, deletion requests and COM:UDR
* See talk page for documentation
*
* @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)

* <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 formattedNS = mw.config.get( 'wgFormattedNamespaces' ),
	ns = mw.config.get( 'wgNamespaceNumber' ),
	userGrp = mw.config.get( 'wgUserGroups' );

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

	// Progress
	// 'restore-a-lot-loading': 'Loading …',
	'restore-a-lot-editing': 'Undeleting page',
	'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 pages have been undeleted.',
	'restore-a-lot-done': 'Done!', // mw.msg("Feedback-close")
	'restore-a-lot-undelete': 'File undeleted',

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

	// Actions
	'restore-a-lot-undeletelink': 'Undelete',
	'restore-a-lot-instructions': 'Select files, hit undelete.',
	'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': 'Undeleted using Restore-a-lot',
};
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();
}

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

var RAL = 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', 'cat_a_lot' )
			.appendTo( $body );
		$dataContainer = $( '<div>' )
			.attr( 'id', 'cat_a_lot_data' )
			.appendTo( $container );
		$markCounter = $( '<div>' )
			.attr( 'id', 'cat_a_lot_mark_counter' )
			.appendTo( $dataContainer );
		$instructions = $( '<div>' )
			.attr( 'id', 'cat_a_lot_selections' )
			.text( msg( 'instructions' ) )
			.appendTo( $dataContainer );
		$selections = $( '<div>' )
			.attr( 'id', 'cat_a_lot_selections' )
			.text( msg( 'select' ) + ':' )
			.appendTo( $dataContainer );
		$head = $( '<div>' )
			.attr( 'id', 'cat_a_lot_head' )
			.appendTo( $container );
		$link = $( '<a>' )
			.attr( 'id', 'cat_a_lot_toggle' )
			.text( 'Restore-a-lot' )
			.appendTo( $head );
		$container.one( 'mouseover', function () { // Try load on demand earliest as possible
			mw.loader.load( [ 'jquery.ui'] );
		} );

		// NOTE ZYF: Update this?
		// NOTE AJ: I'm not sure what this does? It gets a css file (which restore-a-lot shares with cat-a-lot so that's fine) and I guess it registers the gadget?
		// NOTE ZYF: It means, if the url contains withJS=MediaWiki:Gadget-Restore-a-lot.js but without withCSS it loads the css.
		if ( ( mw.util.getParamValue( 'withJS' ) === 'MediaWiki:Gadget-Restore-a-lot.js' &&
			!mw.util.getParamValue( 'withCSS' ) ) ||
			mw.loader.getState( 'ext.gadget.Restore-a-lot' ) === 'registered' ) {
			mw.loader.load( mw.config.get( 'wgServer' ) + '/w/index.php?title=MediaWiki:Gadget-Cat-a-lot.css&action=raw&ctype=text/css', 'text/css' );
		}

		$( '<a>' )
		// .attr( 'id', 'cat_a_lot_select_all' )
			.text( msg( 'all' ) )
			.on( 'click', function () {
				RAL.toggleAll( true );
			} )
			.appendTo( $selections.append( ' ' ) );
		if ( this.settings.editpages ) {
			$selectFiles = $( '<a>' )
				.on( 'click', function () {
					RAL.toggleAll( 'files' );
				} );
			$selectPages = $( '<a>' )
				.on( 'click', function () {
					RAL.toggleAll( 'pages' );
				} );
			$selections.append( $( '<span>' ).hide().append( [ ' / ', $selectFiles, ' / ', $selectPages ] ) );
		}
		$selectNone = $( '<a>' )
		// .attr( 'id', 'cat_a_lot_select_none' )
			.text( msg( 'none' ) )
			.on( 'click', function () {
				RAL.toggleAll( false );
			} );
		$selectInvert = $( '<a>' )
			.on( 'click', function () {
				RAL.toggleAll( null );
			} );
		$selections.append( [ ' • ', $selectNone, ' • ', $selectInvert,
			$( '<div>' ).append( [
				$( '<label>' )
					.attr( {
						'for': 'cat_a_lot_comment',
						style: 'line-height:1.5em;vertical-align:bottom'
					} )
					.text( msg( 'comment-label' ) ),
				$( '<input>' )
					.attr( {
						id: 'cat_a_lot_comment',
						type: 'checkbox'
					} )
			] )
		] );
		
		$selections.append( $( '<a>' )
			.text( msg( 'undeletelink' ) )
			.on( 'click', function () {
				RAL.doSomething();
			} )
			.addClass( 'cat_a_lot_action ui-button' )
			.css( { margin: '5px', padding: '2px' } )
		);

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

				if ( !RAL.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 () {
							RAL.run();
						} );
				} else { RAL.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 'deletedcontribs':
				this.labels = this.labels.add( $( 'div.mw-body-content li' ) );
				break;
			case 'delreq':
				// same thing NOW but this may change in the future
				this.labels = this.labels.add( $( 'div.mw-body-content li' ) );
				break;
			case 'UDR':
				// same thing NOW but this may change in the future
				this.labels = this.labels.add( $( 'div.mw-body-content li' ) );
				break;				
		}
	},

	/**
*  @brief Get title from selected pages
*  @return [array] touple of page title and $object
*/
	getMarkedLabels: function () {
		this.selectedLabels = this.labels.filter( '.cat_a_lot_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 ), file = label.find( 'a.new' ).eq( 0 );
			var title = new mw.Uri( file.attr( 'href' ) ).query.title.replace( /_/g, ' ' );
			if ( title.indexOf( formattedNS[ 2 ] + ':' ) ) { return [ [ title, label ] ]; }
		} );
	},

	updateSelectionCounter: function () {
		this.selectedLabels = this.labels.filter( '.cat_a_lot_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 ( RAL.labels.filter( '.cat_a_lot_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.catALotShiftClick( function () {
			RAL.updateSelectionCounter();
		} )
			.addClass( 'cat_a_lot_label' );
	},

	toggleAll: function ( select ) {
		if ( typeof select === 'string' && this.pageLabels[ 0 ] ) {
			this.pageLabels.toggleClass( 'cat_a_lot_selected', true );
			if ( select === 'files' ) // pages get deselected
			{ this.labels.toggleClass( 'cat_a_lot_selected' ); }
		} else {
		// invert / none / all
			this.labels.toggleClass( 'cat_a_lot_selected', select );
		}
		this.updateSelectionCounter();
	},

	undeleteFile: function ( file ) {
		var sumCmt; // summary comment
		sumCmt = msg( 'summary' );
		sumCmt += this.summary ? '' + this.summary : '';

		var data = {
			action: 'undelete',
			reason: sumCmt,
			title: file[ 0 ],
			token: this.edittoken
		};
		
		this.doAPICall( data, function ( r ) {
			delete RAL.XHR[ file[ 0 ] ];
			return RAL.updateCounter( r );
		} );
		this.markAsDone( file[ 1 ] );
	},

	markAsDone: function ( label ) {
		label.addClass( 'cat_a_lot_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( 'cat_a_lot_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 ( $( '#cat_a_lot_comment' ).prop( 'checked' ) ) { this.summary = ': '+window.prompt( msg( 'edit-question' ), 'restored per request' ); } // TODO custom pre-value
		if ( this.summary !== null ) {
			mw.loader.using( [ 'jquery.ui', 'mediawiki.util' ], function () {
				RAL.showProgress();
				if ( !RAL.cancelled ) {
					RAL.doAPICall( {
						meta: 'tokens',
					}, function ( result ) {
						if ( !result || !result.query ) { return; }
						RAL.edittoken = result.query.tokens.csrftoken;
						
						pages = Array.from( pages ).map( function ( page ) {
							return function () {
								var defer = $.Deferred();
								setTimeout( function timer() {
									RAL.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.title ) {
					this.connectionError.push( params.title );
					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' && !RAL.cancelled ) { RAL.XHR[ params.title ] = 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="cat_a_lot_current">' + RAL.counterCurrent + '</span> ' + msg( 'of' ) + RAL.counterNeeded )
			.dialog( {
				width: 450,
				height: 180,
				minHeight: 90,
				modal: true,
				resizable: false,
				draggable: false,
				// closeOnEscape: true,
				dialogClass: 'cat_a_lot_feedback',
				buttons: [ {
					text: mw.msg( 'Cancel' ), // Stops all actions
					click: function () {
						$( this ).dialog( 'close' );
					}
				} ],
				close: function () {
					RAL.cancelled = 1;
					RAL.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 = $( '#cat_a_lot_current' );
	},

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

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

	run: function () {
		if ( $( '.cat_a_lot_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( 'RESTORE-A-LOT 0.112' )
					.css( { float: 'right' } )
				);
			}
			$dataContainer.show();
			$container.one( 'mouseover', function () {
				$( this )
					.resizable( {
						handles: 'n',
						alsoResize: '#cat_a_lot_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.catALot' );
			this.setHeight = 450;
			$link.text( 'Restore-a-lot' )
				.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 () {
	non = mw.config.get( 'wgUserName' );
	if ( non ) {
		if ( mw.config.get( 'wgRelevantUserName' ) === non ) { non = 0; } else {
			$.each( [ 'sysop', 'filemover', 'editor', 'rollbacker', 'patroller', 'autopatrolled', 'image-reviewer', 'reviewer', 'extendedconfirmed' ], function ( i, v ) {
				non = $.inArray( v, userGrp ) === -1;
				return non;
			} );
		}
	} else { non = 1; }

	switch ( ns ) {
		case -1:
			RAL.searchmode = {
				'DeletedContributions': 'deletedcontribs',
			}[ mw.config.get( 'wgCanonicalSpecialPageName' ) ];
			break;
		//Commons: namespace
		case 4:
			RAL.searchmode = {
				'Deletion requests': 'delreq',
				'Undeletion requests': 'UDR',
			}[ mw.config.get( 'wgTitle' ).replace( /\/.*/g, '' ) ]; // basepagename
			break;			
	}		

	if ( RAL.searchmode ) {
		var maybeLaunch = function () {
			function init() {
				$( function () {
					RAL.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.catALotShiftClick = function ( cb ) {
	var prevCheckbox = null,
		$box = this;
	// When our boxes are clicked..
	$box.on( 'click.catALot', function ( e ) {
	// Prevent following the link and text selection
		if ( !e.ctrlKey ) { e.preventDefault(); }
		// Highlight last selected
		$( '#cat_a_lot_last_selected' )
			.removeAttr( 'id' );
		var $thisControl = $( e.target ),
			method;
		if ( !$thisControl.hasClass( 'cat_a_lot_label' ) ) { $thisControl = $thisControl.parents( '.cat_a_lot_label' ); }

		$thisControl.attr( 'id', 'cat_a_lot_last_selected' )
			.toggleClass( 'cat_a_lot_selected' );
		// And one has been clicked before…
		if ( prevCheckbox !== null && e.shiftKey ) {
			method = $thisControl.hasClass( 'cat_a_lot_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 ]( 'cat_a_lot_selected' );
		}
		// Either way, update the prevCheckbox variable to the one clicked now
		prevCheckbox = $thisControl;
		if ( $.isFunction( cb ) ) { cb(); }
	} );
	return $box;
};

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