User:Alexis Jazz/Restore-a-lot.js
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.
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;
}
},
/**
* @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' );
var title = file.text();
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',
}[ 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>