User:Alexis Jazz/Restore-a-lot.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Alexis Jazz/Restore-a-lot. |
/**
* 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;
case 'WPFFD':
// same thing NOW but this may change in the future
this.labels = this.labels.add( $( 'div.mw-body-content h4' ) );
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();
}, 800 );
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',
'Files for discussion': 'WPFFD',
'Administrators\' noticeboard': 'delreq',
'Deletion review': 'delreq',
}[ 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();
}
}, 800 );
/**
* 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>