MediaWiki:Gadget-Cat-a-lot.js: Difference between revisions

From Meta, a Wikimedia project coordination wiki
Content deleted Content added
 
Maintenance: mw:RL/MGU - Updated deprecated module name
 
(7 intermediate revisions by 5 users not shown)
Line 1: Line 1:
/**
/**
* Cat-A-Lot
* Cat-a-lot
* Changes category of multiple files
* Changes category of multiple files
*
*
* @rev 00:13, 10 February 2018 (UTC)
* Originally by Magnus Manske
* @author Originally by Magnus Manske (2007)
* RegExes by Ilmari Karonen
* @author RegExes by Ilmari Karonen (2010)
* Completely rewritten by DieBuche
* @author Completely rewritten by DieBuche (2010-2012)
*
* @author Rillke (2012-2014)
* Requires [[MediaWiki:Gadget-SettingsManager.js]] and [[MediaWiki:Gadget-SettingsUI.js]] (properly registered) for per-user-settings
* @author Perhelion (2017)
*
* READ THIS PAGE IF YOU WANT TO TRANSLATE OR USE THIS ON ANOTHER SITE:
* http://commons.wikimedia.org/wiki/MediaWiki:Gadget-Cat-a-lot.js/translating
* <nowiki>
*/


* Requires [[MediaWiki:Gadget-SettingsManager.js]] and [[MediaWiki:Gadget-SettingsUI.js]] (properly registered) for per-user-settings
/*global jQuery:false, mediaWiki:false, alert:false, importStylesheet:false */
*
/*jshint curly:false, unused:true, unused:true, forin:false, smarttabs:true, loopfunc:true, browser:true */
* READ THIS PAGE IF YOU WANT TO TRANSLATE OR USE THIS ON ANOTHER SITE:
* http://commons.wikimedia.org/wiki/MediaWiki:Gadget-Cat-a-lot.js/translating
* <nowiki>
*/


/* global jQuery, mediaWiki */
( function( $, mw ) {
/* eslint one-var:0, vars-on-top:0, no-underscore-dangle:0, valid-jsdoc:0,
'use strict';
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 ) {
var nsNumber = mw.config.get( 'wgNamespaceNumber' ),
'use strict';
nsCat = 14,
currentCat = mw.config.get( 'wgTitle' ),
formattedNS = mw.config.get( 'wgFormattedNamespaces' ),
nsIDs = mw.config.get( 'wgNamespaceIds' ),
catALot;


var formattedNS = mw.config.get( 'wgFormattedNamespaces' ),
var msgs = {
ns = mw.config.get( 'wgNamespaceNumber' ),
// Preferences
nsIDs = mw.config.get( 'wgNamespaceIds' ),
// new: added 2012-09-19. Please translate.
userGrp = mw.config.get( 'wgUserGroups' ),
// Use user language for i18n
project = mw.config.get( 'wgDBname' );
'cat-a-lot-watchlistpref': "Watchlist preference concerning files edited with Cat-A-Lot",
'cat-a-lot-watch_pref': "According to your general preferences",
'cat-a-lot-watch_nochange': "Do not change watchstatus",
'cat-a-lot-watch_watch': "Watch pages edited with Cat-A-Lot",
'cat-a-lot-watch_unwatch': "Remove pages while editing with Cat-A-Lot from your watchlist",
'cat-a-lot-minorpref': "Mark edits as minor (if you generally mark your edits as minor, this won't change anything)",
'cat-a-lot-editpagespref': "Allow categorising pages (including categories) that are not files",
'cat-a-lot-docleanuppref': "Remove {{Check categories}} and other minor cleanup",
'cat-a-lot-subcatcountpref': "Sub-categories to show at most",
'cat-a-lot-config-settings': "Preferences",
// Use site language for i18n
'cat-a-lot-pref-save-summary': "[[Help:Gadget-Cat-a-lot|Cat-a-lot]] is updating user preferences",


var msgs = {
//Progress
// Preferences
'cat-a-lot-loading': 'Loading...',
// new: added 2012-09-19. Please translate.
'cat-a-lot-editing': 'Editing page',
// Use user language for i18n
'cat-a-lot-of': 'of ',
'cat-a-lot-watchlistpref': 'Watchlist preference concerning files edited with Cat-a-lot',
'cat-a-lot-skipped-already': 'The following {{PLURAL:$1|page was|$1 pages were}} skipped, because the page was already in the category:',
'cat-a-lot-watch_pref': 'According to your general preferences',
'cat-a-lot-skipped-not-found': 'The following {{PLURAL:$1|page was|$1 pages were}} skipped, because the old category could not be found:',
'cat-a-lot-watch_nochange': 'Do not change watchstatus',
'cat-a-lot-skipped-server': 'The following {{PLURAL:$1|page|$1 pages}} couldn\'t be changed, since there were problems connecting to the server:',
'cat-a-lot-all-done': 'All pages are processed.',
'cat-a-lot-watch_watch': 'Watch pages edited with Cat-a-lot',
'cat-a-lot-watch_unwatch': 'Remove pages while editing with Cat-a-lot from your watchlist',
'cat-a-lot-done': 'Done!',
'cat-a-lot-minorpref': 'Mark edits as minor (if you generally mark your edits as minor, this won’t change anything)',
'cat-a-lot-added-cat': 'Added category $1',
'cat-a-lot-editpagespref': 'Allow categorising pages (including categories) that are not files',
'cat-a-lot-copied-cat': 'Copied to category $1',
'cat-a-lot-moved-cat': 'Moved to category $1',
'cat-a-lot-docleanuppref': 'Remove {{Check categories}} and other minor cleanup',
'cat-a-lot-removed-cat': 'Removed from category $1',
'cat-a-lot-uncatpref': 'Remove {{Uncategorized}}',
'cat-a-lot-return-to-page': 'Return to page',
'cat-a-lot-subcatcountpref': 'Sub-categories to show at most',
'cat-a-lot-cat-not-found': 'Category not found.',
'cat-a-lot-config-settings': 'Preferences',
'cat-a-lot-buttonpref': 'Use buttons instead of text-links',
'cat-a-lot-comment-label': 'Custom edit comment',
'cat-a-lot-edit-question': 'Why is this change necessary?',


// Progress
// 'cat-a-lot-loading': 'Loading …',
'cat-a-lot-editing': 'Editing page',
'cat-a-lot-of': 'of ',
'cat-a-lot-skipped-already': 'The following {{PLURAL:$1|1=page was|$1 pages were}} skipped, because the page was already in the category:',
'cat-a-lot-skipped-not-found': 'The following {{PLURAL:$1|1=page was|$1 pages were}} skipped, because the old category could not be found:',
'cat-a-lot-skipped-server': 'The following {{PLURAL:$1|1=page|$1 pages}} couldn’t be changed, since there were problems connecting to the server:',
'cat-a-lot-all-done': 'All pages are processed.',
'cat-a-lot-done': 'Done!', // mw.msg("Feedback-close")
'cat-a-lot-added-cat': 'Added category $1',
'cat-a-lot-copied-cat': 'Copied to category $1',
'cat-a-lot-moved-cat': 'Moved to category $1',
'cat-a-lot-removed-cat': 'Removed from category $1',
// 'cat-a-lot-return-to-page': 'Return to page',
// 'cat-a-lot-cat-not-found': 'Category not found.',


//as in 17 files selected
// as in 17 files selected
'cat-a-lot-files-selected': '{{PLURAL:$1|One file|$1 files}} selected.',
'cat-a-lot-files-selected': '{{PLURAL:$1|1=One file|$1 files}} selected.',
'cat-a-lot-pe_file': '$1 {{PLURAL:$1|page|pages}} of $2 affected',
'cat-a-lot-parent-cat': 'Has parent-category: ',
'cat-a-lot-sub-cat': 'Has sub-category: ',


//Actions
// Actions
'cat-a-lot-copy': 'Copy',
'cat-a-lot-copy': 'Copy',
'cat-a-lot-move': 'Move',
'cat-a-lot-move': 'Move',
'cat-a-lot-add': 'Add',
'cat-a-lot-add': 'Add',
'cat-a-lot-remove-from-cat': 'Remove from this category',
// 'cat-a-lot-remove-from-cat': 'Remove from this category',
'cat-a-lot-enter-name': 'Enter category name',
'cat-a-lot-overcat': 'Check over-categorization',
'cat-a-lot-select': 'Select',
'cat-a-lot-enter-name': 'Enter category name',
'cat-a-lot-all': 'all',
'cat-a-lot-select': 'Select',
'cat-a-lot-none': 'none',
'cat-a-lot-all': 'all',
'cat-a-lot-none': 'none',
// 'cat-a-lot-none-selected': 'No files selected!', 'Ooui-selectfile-placeholder'


// Summaries (project language):
'cat-a-lot-none-selected': 'No files selected!',
'cat-a-lot-pref-save-summary': 'Updating user preferences',
'cat-a-lot-summary-add': 'Adding [[Category:$1]]',
'cat-a-lot-summary-copy': 'Copying from [[Category:$1]] to [[Category:$2]]',
'cat-a-lot-summary-move': 'Moving from [[Category:$1]] to [[Category:$2]]',
'cat-a-lot-summary-remove': 'Removing from [[Category:$1]]',
'cat-a-lot-prefix-summary': '',
'cat-a-lot-using-summary': ' using [[c:Help:Cat-a-lot|Cat-a-lot]]'
};
mw.messages.set( msgs );


function msg( /* params */ ) {
//Summaries:
var args = Array.prototype.slice.call( arguments, 0 );
'cat-a-lot-summary-add': '[[Help:Cat-a-lot|Cat-a-lot]]: Adding [[Category:$1]]',
args[ 0 ] = 'cat-a-lot-' + args[ 0 ];
'cat-a-lot-summary-copy': '[[Help:Cat-a-lot|Cat-a-lot]]: Copying from [[Category:$1]] to [[Category:$2]]',
return ( args.length === 1 ) ?
'cat-a-lot-summary-move': '[[Help:Cat-a-lot|Cat-a-lot]]: Moving from [[Category:$1]] to [[Category:$2]]',
mw.message( args[ 0 ] ).plain() :
'cat-a-lot-summary-remove': '[[Help:Cat-a-lot|Cat-a-lot]]: Removing from [[Category:$1]]'
mw.message.apply( mw.message, args ).parse();
};
}
mw.messages.set( msgs );
function msg( /*params*/ ) {
var args = Array.prototype.slice.call( arguments, 0 );
args[0] = 'cat-a-lot-' + args[0];
return mw.message.apply( mw.message, args ).parse();
}
function msgPlain( key ) {
return mw.message( 'cat-a-lot-' + key ).plain();
}
// There is only one cat-a-lot on one page
var $removeLink,
$body, $container, $dataContainer, $searchInputContainer, $searchInput, $resultList, $markCounter,
$selections, $selectAll, $selectNone, $settingsWrapper, $settingsLink, $head, $link;
catALot = window.catALot = {
apiUrl: mw.util.wikiScript( 'api' ),
searchmode: false,
version: 3.5,
setHeight: 450,
settings: {},
init: function() {
this._initSettings();


// There is only one Cat-a-lot on one page
$body = $( document.body );
var $body, $container, $dataContainer, $searchInputContainer, $searchInput, $resultList, $markCounter, $selections,
$container = $( '<div id="cat_a_lot"></div>' )
$selectFiles, $selectPages, $selectNone, $selectInvert, $settingsWrapper, $settingsLink, $head, $link, $overcat,
.appendTo( $body );
commonsURL = 'https://commons.wikimedia.org/w/index.php',
$dataContainer = $( '<div id="cat_a_lot_data"></div>' )
is_rtl = $( 'body' ).hasClass( 'rtl' ),
.appendTo( $container );
reCat, // localized category search regexp
$searchInputContainer = $( '<div>' )
non,
.appendTo( $dataContainer );
r; // result file count for overcat
$searchInput = $( '<input type="text" id="cat_a_lot_searchcatname" />' )
.attr( 'placeholder', msgPlain( 'enter-name' ) )
.appendTo( $searchInputContainer );
$resultList = $( '<div id="cat_a_lot_category_list"></div>' )
.appendTo( $dataContainer );
$markCounter = $( '<div id="cat_a_lot_mark_counter"> </div>' )
.appendTo( $dataContainer );
$selections = $( '<div id="cat_a_lot_selections"></div>' )
.text( msgPlain( 'select' ) )
.appendTo( $dataContainer );
$selectAll = $( '<a id="cat_a_lot_select_all"></a>' )
.text( msgPlain( 'all' ) )
.appendTo( $selections.append(' ') );
$selectNone = $( '<a id="cat_a_lot_select_none"></a>' )
.text( msgPlain( 'none' ) )
.appendTo( $selections.append(' • ') );
$settingsWrapper = $( '<div id="cat_a_lot_settings"></div>' )
.appendTo( $dataContainer );
$settingsLink = $( '<a id="cat_a_lot_config_settings"></a>' )
.text( msgPlain( 'config-settings' ) )
.appendTo( $settingsWrapper );
$head = $( '<div id="cat_a_lot_head"></div>' )
.appendTo( $container );
$link = $( '<a id="cat_a_lot_toggle"></a>' )
.text( 'Cat-a-lot' )
.appendTo( $head );


var CAL = mw.libs.catALot = {
if ( !this.searchmode ) {
apiUrl: mw.util.wikiScript( 'api' ),
$removeLink = $( '<a id="cat_a_lot_remove"></a>' )
origin: '',
.html( msg( 'remove-from-cat' ) )
searchmode: false,
.appendTo( $selections )
version: '4.77',
.click( function() {
setHeight: 450,
catALot.remove();
changeTag: 'Cat-a-lot',
} );
}


settings: {
if ( ( 'MediaWiki:Gadget-Cat-a-lot.js' === mw.util.getParamValue( 'withJS' ) &&
/* Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
!mw.util.getParamValue( 'withCSS' ) ) ||
any items, but that contains links to other categories where stuff should be categorized. If you don't have
mw.loader.getState('ext.gadget.Cat-a-lot') === 'registered' ) {
that concept on your wiki, set it to null. Use blanks, not underscores. */
importStylesheet( 'MediaWiki:Gadget-Cat-a-lot.css' );
disambig_category: 'Disambiguation', // Commons and EnWP
}
/* Any category in this category is deemed a (soft) redirect to some other category defined by a link
* to another non-blacklisted category. If your wiki doesn't have soft category redirects, set this to null.
* If a soft-redirected category contains more than one link to another non-blacklisted category, it's considered
* a disambiguation category instead. */
redir_category: 'Category redirects'


},
var reCat = new RegExp( '^\\s*' + catALot.localizedRegex( nsCat, 'Category' ) + ':', '' );


$searchInput.keypress( function( e ) {
init: function () {
// Prevent historical double marker (maybe remove in future)
if ( e.which === 13 ) {
if ( /Cat-?a-?lot/i.test( msgs[ 'cat-a-lot-pref-save-summary' ] ) ) { mw.messages.set( { 'cat-a-lot-prefix-summary': '', 'cat-a-lot-using-summary': '' } ); } else {
catALot.updateCats( $.trim( $( this )
.val() ) );
mw.messages.set( {
'cat-a-lot-pref-save-summary': msgs[ 'cat-a-lot-prefix-summary' ] + msgs[ 'cat-a-lot-pref-save-summary' ] + msgs[ 'cat-a-lot-using-summary' ]
}
} );
}

// TODO: better extern project support for possible change-tag? (needs currently change after init)
if ( project === 'commonswiki' ) { mw.messages.set( { 'cat-a-lot-using-summary': '' } ); } else { // Reset
this.changeTag = '';
this.settings.redir_category = '';
}

this._initSettings();
$body = $( document.body );
$container = $( '<div>' )
.attr( 'id', 'cat_a_lot' )
.appendTo( $body );
$dataContainer = $( '<div>' )
.attr( 'id', 'cat_a_lot_data' )
.appendTo( $container );
$searchInputContainer = $( '<div>' )
.appendTo( $dataContainer );
$searchInput = $( '<input>', {
id: 'cat_a_lot_searchcatname',
placeholder: msg( 'enter-name' ),
type: 'text'
} )
.appendTo( $searchInputContainer );
$resultList = $( '<div>' )
.attr( 'id', 'cat_a_lot_category_list' )
.appendTo( $dataContainer );
$markCounter = $( '<div>' )
.attr( 'id', 'cat_a_lot_mark_counter' )
.appendTo( $dataContainer );
$selections = $( '<div>' )
.attr( 'id', 'cat_a_lot_selections' )
.text( msg( 'select' ) + ':' )
.appendTo( $dataContainer );
$settingsWrapper = $( '<div>' )
.attr( 'id', 'cat_a_lot_settings' )
.appendTo( $dataContainer );
$settingsLink = $( '<a>', {
id: 'cat_a_lot_config_settings',
title: 'Version ' + this.version,
text: msg( 'config-settings' )
} )
.appendTo( $settingsWrapper );
$head = $( '<div>' )
.attr( 'id', 'cat_a_lot_head' )
.appendTo( $container );
$link = $( '<a>' )
.attr( 'id', 'cat_a_lot_toggle' )
.text( 'Cat-a-lot' )
.appendTo( $head );
$settingsWrapper.append( $( '<a>', {
href: commonsURL + '?title=Special:MyLanguage/Help:Gadget-Cat-a-lot',
target: '_blank',
style: 'float:right',
title: ( $( '#n-help a' ).attr( 'title' ) || '' ) + ' (v. ' + this.version + ')'
} ).text( '?' ) );
$container.one( 'mouseover', function () { // Try load on demand earliest as possible
mw.loader.load( [ 'jquery.ui'] );
} );

if ( this.origin && !non ) {
$overcat = $( '<a>' )
.attr( 'id', 'cat_a_lot_overcat' )
.html( msg( 'overcat' ) )
.on( 'click', function ( e ) {
CAL.getOverCat( e );
} )
} )
.insertBefore( $selections );
.bind( 'input keyup', function() {
}
var oldVal = this.value,

newVal = oldVal.replace( reCat, '' );
if ( ( mw.util.getParamValue( 'withJS' ) === 'MediaWiki:Gadget-Cat-a-lot.js' &&
if ( newVal !== oldVal ) this.value = newVal;
!mw.util.getParamValue( 'withCSS' ) ) ||
} );
mw.loader.getState( 'ext.gadget.Cat-a-lot' ) === 'registered' ) {
if ( this.searchmode ) {
mw.loader.load( mw.config.get( 'wgServer' ) + '/w/index.php?title=MediaWiki:Gadget-Cat-a-lot.css&action=raw&ctype=text/css', 'text/css' );
$searchInput.val( mw.util.getParamValue( 'search' ) );
// importStylesheet( 'MediaWiki:Gadget-Cat-a-lot.css' );
}

reCat = new RegExp( '^\\s*' + CAL.localizedRegex( 14, 'Category' ) + ':', '' );

$searchInput.on( 'keypress', function ( e ) {
if ( e.which === 13 ) {
CAL.updateCats( $.trim( $( this ).val().replace( /[\u200E\u200F\u202A-\u202E]/g, '' ) ) );
mw.cookie.set( 'catAlot', CAL.currentCategory );
}
}
} )
function initAutocomplete() {
.on( 'input keyup', function () {
if ( catALot.autoCompleteIsEnabled ) return;
var oldVal = this.value,
catALot.autoCompleteIsEnabled = true;
newVal = oldVal.replace( reCat, '' );
if ( newVal !== oldVal ) { this.value = newVal; }

if ( !newVal ) { mw.cookie.set( 'catAlot', null ); }
} );

function initAutocomplete() {
if ( CAL.autoCompleteIsEnabled ) { return; }

CAL.autoCompleteIsEnabled = true;

if ( !$searchInput.val() && mw.cookie && mw.cookie.get( 'catAlot' ) ) { $searchInput.val( mw.cookie.get( 'catAlot' ) ); }


$searchInput.autocomplete( {
$searchInput.autocomplete( {
source: function( request, response ) {
source: function ( request, response ) {
catALot.doAPICall( {
CAL.doAPICall( {
action: 'opensearch',
action: 'opensearch',
search: request.term,
search: request.term,
namespace: nsCat
redirects: 'resolve',
namespace: 14
}, function( data ) {
if ( data[ 1 ] ) response( $( data[ 1 ] )
}, function ( data ) {
.map( function( index, item ) {
if ( data[ 1 ] ) {
response( $( data[ 1 ] )
.map( function ( index, item ) {
return item.replace( reCat, '' );
return item.replace( reCat, '' );
} ) );
} ) );
} );
}
},
open: function() {
$( ".ui-autocomplete" )
.position( {
my: $( 'body' )
.is( '.rtl' ) ? "left bottom" : "right bottom",
at: $( 'body' )
.is( '.rtl' ) ? "left top" : "right top",
of: $searchInput
} );
},
appendTo: '#cat_a_lot'
} );
}


$selectAll
} );
},
.click( function() {
open: function () {
catALot.toggleAll( true );
$( '.ui-autocomplete' )
.position( {
my: is_rtl ? 'left bottom' : 'right bottom',
at: is_rtl ? 'left top' : 'right top',
of: $searchInput
} );
},
appendTo: '#cat_a_lot'
} );
}
$( '<a>' )
// .attr( 'id', 'cat_a_lot_select_all' )
.text( msg( 'all' ) )
.on( 'click', function () {
CAL.toggleAll( true );
} )
.appendTo( $selections.append( ' ' ) );
if ( this.settings.editpages ) {
$selectFiles = $( '<a>' )
.on( 'click', function () {
CAL.toggleAll( 'files' );
} );
} );
$selectPages = $( '<a>' )
$selectNone
.click( function() {
.on( 'click', function () {
catALot.toggleAll( false );
CAL.toggleAll( 'pages' );
} );
$link
.click( function() {
$( this ).toggleClass( 'cat_a_lot_enabled' );
// Load autocomplete on demand
mw.loader.using( ['jquery.ui.autocomplete'], initAutocomplete );
catALot.run();
} );
$settingsLink
.click( function() {
catALot.manageSettings();
} );
} );
$selections.append( $( '<span>' ).hide().append( [ ' / ', $selectFiles, ' / ', $selectPages ] ) );
}
$selectNone = $( '<a>' )
// .attr( 'id', 'cat_a_lot_select_none' )
.text( msg( 'none' ) )
.on( 'click', function () {
CAL.toggleAll( false );
} );
$selectInvert = $( '<a>' )
.on( 'click', function () {
CAL.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'
} )
] )
] );


$link
this.localCatName = formattedNS[ nsCat ];
.on( 'click', function () {
},
$( this ).toggleClass( 'cat_a_lot_enabled' );
findAllLabels: function() {
// Load autocomplete on demand
// 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
mw.loader.using( 'jquery.ui', initAutocomplete );
if ( this.searchmode ) {

this.labels = $( 'table.searchResultImage' )
if ( !CAL.executed ) {
.find( 'tr>td:eq(1)' );
if ( this.settings.editpages ) {
$.when( mw.loader.using( [
'jquery.ui',
this.labels = this.labels.add( 'div.mw-search-result-heading' );
'jquery.ui',
'jquery.ui',
'mediawiki.api',
'mediawiki.jqueryMsg'
] ), $.ready )
.then( function () {
return new mw.Api().loadMessagesIfMissing( [
'Cancel',
'Categorytree-not-found',
// 'Checkuser-all',
// 'Code-field-select',
// 'Export-addcat',
'Filerevert-submit',
'Mobile-frontend-return-to-page',
'Ooui-selectfile-placeholder',
// 'Visualeditor-clipboard-copy',
'Wikieditor-loading',
'Prefs-files',
'Categories',
'Checkbox-invert',
'Centralnotice-remove', // 'Ooui-item-remove'
'Apifeatureusage-warnings'
] );
} ).then( function () {
CAL.run();
} );
} else { CAL.run(); }
} );
$settingsLink
.on( 'click', CAL.manageSettings );
this.localCatName = formattedNS[ 14 ] + ':';
mw.loader.using( 'mediawiki.cookie', function () { // Let catAlot stay open
var val = mw.cookie.get( 'catAlotO' );
if ( val && Number( val ) === ns ) { $link.click(); }
}
);
},

getOverCat: function ( e ) {
var files = [];
r = 0; // result counter
if ( e ) {
e.preventDefault();
this.files = this.getMarkedLabels(); // .toArray() not working
for ( var f = 0; f < this.files.length; f++ ) { files.push( this.files[ f ] ); }

}
if ( !files.length || !( files instanceof Array ) ) { return alert( mw.msg( 'Ooui-selectfile-placeholder' ) ); }
this.files = files;
mw.loader.using( [ 'jquery.spinner' ], function () {
$markCounter.injectSpinner( 'overcat' );
CAL.getFileCats();
} );
},

getFileCats: function () {
var aLen = this.files.length;
var bLen = this.selectedLabels.length;
var file = this.files[ aLen - 1 ][ 0 ];
$overcat.text( '…' + aLen + '\/' + bLen );
if ( file ) {
this.doAPICall( {
prop: 'categories',
titles: file
}, this.checkFileCats
);
}

},

checkFileCats: function ( data ) {
var cc = 0; // current cat counter;
var file = CAL.files.pop();
if ( data.query && data.query.pages ) {
$.each( data.query.pages, function ( id, page ) {
if ( page.categories ) {
var target = file[ 1 ].removeClass( 'cat_a_lot_selected' );
$.each( page.categories, function ( c, cat ) {
var title = cat.title.replace( reCat, '' ),
color = 'orange',
mark = function ( kind ) { // kind of category
// TODO: store data to use this for special remove function
if ( kind === 'sub' ) { color = 'green'; }
var border = '3px dotted ';
if ( $.inArray( title, CAL[ kind + 'Cats' ] ) !== -1 ) {
cc++;
target = target.parents( '.gallerybox' );
target = target[ 0 ] ? target : file[ 1 ];
target.css( {
border: border + color
} ).prop( 'title', msg( kind + '-cat' ) + title );
color = 'red';
return false;
}
};
mark( 'sub' );
return mark( 'parent' );
} );
if ( cc ) { r++; }
}
}
} else {
} );
} else { mw.log( 'Api-fail', file, data ); }
this.labels = $( 'div.gallerytext' )
if ( CAL.files[ 0 ] ) { return setTimeout( function () { CAL.getFileCats(); }, 100 ); } // Api has bad performance here, so we can get only each file separately
.add( $( 'div#mw-category-media' )
$overcat.text( msg( 'pe_file', r, CAL.selectedLabels.length ) );
.find( 'li[class!="gallerybox"]' ) );
$.removeSpinner( 'overcat' );
},

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 'search':
this.labels = this.labels.add( $( 'table.searchResultImage' ).find( 'tr>td:eq(1)' ) );
if ( this.settings.editpages ) { this.labels = this.labels.add( 'div.mw-search-result-heading' ); }


break;
case 'category':
this.findAllLabels( 'gallery' );
this.labels = this.labels.add( $( '#mw-category-media' ).find( 'li[class!="gallerybox"]' ) );
if ( this.settings.editpages ) {
if ( this.settings.editpages ) {
var $pgs = $( 'div#mw-pages, div#mw-subcategories' )
this.pageLabels = $( '#mw-pages, #mw-subcategories' ).find( 'li' );
.find( 'li' );
// this.files = this.labels;
this.labels = this.labels.add( $pgs );
this.labels = this.labels.add( this.pageLabels );
}
}
}
break;
case 'contribs':
},
this.labels = this.labels.add( $( 'ul.mw-contributions-list li' ) );
// FIXME: Filter if !this.settings.editpages
break;
case 'prefix':
this.labels = this.labels.add( $( 'ul.mw-prefixindex-list li' ) );
break;
case 'listfiles':
// this.labels = this.labels.add( $( 'table.listfiles>tbody>tr' ).find( 'td:eq(1)' ) );
this.labels = this.labels.add( $( '.TablePager_col_img_name' ) );
break;
case 'gallery':
// this.labels = this.labels.add( '.gallerybox' ); // TODO incombatible with GalleryDetails
this.labels = this.labels.add( '.gallerytext' );
break;
}
},


getTitleFromLink: function( href ) {
getTitleFromLink: function ( $a ) {
try {
try {
return decodeURIComponent( href )
return decodeURIComponent( $a.attr( 'href' ) )
.match( /wiki\/(.+?)(?:#.+)?$/ )[ 1 ].replace( /_/g, ' ' );
.match( /wiki\/(.+?)(?:#.+)?$/ )[ 1 ].replace( /_/g, ' ' );
} catch ( ex ) {
} catch ( ex ) {
return '';
return '';
}
}
},
},


/**
getMarkedLabels: function() {
* @brief Get title from selected pages
var marked = [];
* @return [array] touple of page title and $object
this.selectedLabels = this.labels.filter( '.cat_a_lot_selected' );
*/
this.selectedLabels.each( function() {
getMarkedLabels: function () {
var file = $( this )
this.selectedLabels = this.labels.filter( '.cat_a_lot_selected:visible' );
.find( 'a[title]' ),
return this.selectedLabels.map( function () {
title = file.attr( 'title' ) || catALot.getTitleFromLink( file.attr( 'href' ) ) || catALot.getTitleFromLink( $( this )
.find( 'a' )
var label = $( this ), file = label.find( 'a[title][class$="title"]' );
file = file.length ? file : label.find( 'a[title]' );
.attr( 'href' ) );
var title = file.attr( 'title' ) ||
CAL.getTitleFromLink( file ) ||
CAL.getTitleFromLink( label.find( 'a' ) ) ||
CAL.getTitleFromLink( label.parent().find( 'a' ) ); // TODO needs optimization
if ( title.indexOf( formattedNS[ 2 ] + ':' ) ) { return [ [ title, label ] ]; }
} );
},


updateSelectionCounter: function () {
marked.push( [ title, $( this ) ] );
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 ( CAL.labels.filter( '.cat_a_lot_selected:visible' )[ 0 ] ) { return 'You have pages selected!'; } // There is a default message in the browser
} );
} );
}
return marked;
},
},


updateSelectionCounter: function() {
makeClickable: function () {
this.labels = $();
this.selectedLabels = this.labels.filter( '.cat_a_lot_selected' );
this.pageLabels = $(); // only for distinct all selections
$markCounter
this.findAllLabels( this.searchmode );
.show()
this.labels.catALotShiftClick( function () {
.html( msg( 'files-selected', this.selectedLabels.length ) );
CAL.updateSelectionCounter();
},
} )
.addClass( 'cat_a_lot_label' );
},


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

toggleAll: function( select ) {
this.labels.toggleClass( 'cat_a_lot_selected', select );
this.labels.toggleClass( 'cat_a_lot_selected', select );
}
this.updateSelectionCounter();
this.updateSelectionCounter();
},
},


getSubCats: function() {
getSubCats: function () {
var data = {
var data = {
action: 'query',
list: 'categorymembers',
list: 'categorymembers',
cmtype: 'subcat',
cmlimit: this.settings.subcatcount,
cmtype: 'subcat',
cmtitle: 'Category:' + this.currentCategory
cmlimit: this.settings.subcatcount,
};
cmtitle: 'Category:' + this.currentCategory
};


this.doAPICall( data, function( result ) {
this.doAPICall( data, function ( result ) {
var cats = result.query.categorymembers;
CAL.subCats = [];
for ( var i = 0; i < cats.length; i++ ) { CAL.subCats.push( cats[ i ].title.replace( /^[^:]+:/, '' ) ); }


CAL.catCounter++;
var cats = result.query.categorymembers;
if ( CAL.catCounter === 2 ) { CAL.showCategoryList(); }


} );
catALot.subCats = [];
},
for ( var i = 0; i < cats.length; i++ ) {
catALot.subCats.push( cats[ i ].title.replace( /^[^:]+:/, "" ) );
}
catALot.catCounter++;
if ( catALot.catCounter === 2 ) catALot.showCategoryList();
} );
},


getParentCats: function() {
getParentCats: function () {
var data = {
var data = {
action: 'query',
prop: 'categories',
titles: 'Category:' + this.currentCategory
prop: 'categories',
};
titles: 'Category:' + this.currentCategory
this.doAPICall( data, function ( result ) {
};
CAL.parentCats = [];
this.doAPICall( data, function( result ) {
var cats,
catALot.parentCats = [];
var cats, pages = result.query.pages;
pages = result.query.pages,
table = $( '<table>' );
if ( pages[ -1 ] && pages[ -1 ].missing === '' ) {
$resultList.html( '<span id="cat_a_lot_no_found">' + msg( 'cat-not-found' ) + '</span>' );
document.body.style.cursor = 'auto';


if ( pages[ -1 ] && pages[ -1 ].missing === '' ) {
$resultList.append( '<table></table>' );
catALot.createCatLinks( "", [ catALot.currentCategory ] );
$resultList.html( '<span id="cat_a_lot_no_found">' + mw.msg( 'Categorytree-not-found', this.currentCategory ) + '</span>' );
document.body.style.cursor = 'auto';
return;
CAL.createCatLinks( '→', [ CAL.currentCategory ], table );
}
$resultList.append( table );
// there should be only one, but we don't know its ID
return;
for ( var id in pages ) {
}
cats = pages[ id ].categories;
// there should be only one, but we don't know its ID
}
for ( var i = 0; i < cats.length; i++ ) {
for ( var id in pages ) { cats = pages[ id ].categories || []; }
catALot.parentCats.push( cats[ i ].title.replace( /^[^:]+:/, "" ) );
}


for ( var i = 0; i < cats.length; i++ ) { CAL.parentCats.push( cats[ i ].title.replace( /^[^:]+:/, '' ) ); }
catALot.catCounter++;
if ( catALot.catCounter === 2 ) catALot.showCategoryList();
} );
},
localizedRegex: function( namespaceNumber, fallback ) {
//Copied from HotCat. Thanks Lupo.
var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
var wikiTextBlankRE = new RegExp( wikiTextBlank, 'g' );


CAL.catCounter++;
var createRegexStr = function( name ) {
if ( !name || name.length === 0 ) return "";
if ( CAL.catCounter === 2 ) { CAL.showCategoryList(); }
var regex_name = "";
for ( var i = 0; i < name.length; i++ ) {
var initial = name.substr( i, 1 );
var ll = initial.toLowerCase();
var ul = initial.toUpperCase();
if ( ll === ul ) {
regex_name += initial;
} else {
regex_name += '[' + ll + ul + ']';
}
}
return regex_name.replace( /([\\\^\$\.\?\*\+\(\)])/g, '\\$1' )
.replace( wikiTextBlankRE, wikiTextBlank );
};


} );
fallback = fallback.toLowerCase();
},
var canonical = formattedNS[ namespaceNumber ].toLowerCase();

var RegexString = createRegexStr( canonical );
localizedRegex: function ( namespaceNumber, fallback ) {
if ( fallback && canonical !== fallback ) RegexString += '|' + createRegexStr( fallback );
// Copied from HotCat, thanks Lupo.
for ( var catName in nsIDs ) {
var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
if ( typeof( catName ) === 'string' && catName.toLowerCase() !== canonical && catName.toLowerCase() !== fallback && nsIDs[ catName ] === namespaceNumber ) {
var wikiTextBlankRE = new RegExp( wikiTextBlank, 'g' );
RegexString += '|' + createRegexStr( catName );
var createRegexStr = function ( name ) {
}
if ( !name || !name.length ) { return ''; }

var regexName = '';
for ( var i = 0; i < name.length; i++ ) {
var ii = name[ i ];
var ll = ii.toLowerCase();
var ul = ii.toUpperCase();
regexName += ( ll === ul ) ? ii : '[' + ll + ul + ']';
}
}
return ( '(?:' + RegexString + ')' );
return regexName.replace( /([\\\^\$\.\?\*\+\(\)])/g, '\\$1' )
.replace( wikiTextBlankRE, wikiTextBlank );
},
};
regexBuilder: function( category ) {
var catname = this.localizedRegex( nsCat, 'Category' );


fallback = fallback.toLowerCase();
// Build a regexp string for matching the given category:
var canonical = formattedNS[ namespaceNumber ].toLowerCase();
// trim leading/trailing whitespace and underscores
var RegexString = createRegexStr( canonical );
category = category.replace( /^[\s_]+/, "" )
if ( fallback && canonical !== fallback ) { RegexString += '|' + createRegexStr( fallback ); }
.replace( /[\s_]+$/, "" );


for ( var catName in nsIDs ) { if ( typeof catName === 'string' && catName.toLowerCase() !== canonical && catName.toLowerCase() !== fallback && nsIDs[ catName ] === namespaceNumber ) { RegexString += '|' + createRegexStr( catName ); } }
// escape regexp metacharacters (= any ASCII punctuation except _)
category = mw.RegExp.escape( category );


return ( '(?:' + RegexString + ')' );
// any sequence of spaces and underscores should match any other
},
category = category.replace( /[\s_]+/g, '[\\s_]+' );


regexCatBuilder: function ( category ) {
// Make the first character case-insensitive:
var first = category.substr( 0, 1 );
var catname = this.localizedRegex( 14, 'Category' );
if ( first.toUpperCase() !== first.toLowerCase() ) category = '[' + first.toUpperCase() + first.toLowerCase() + ']' + category.substr( 1 );


// Compile it into a RegExp that matches MediaWiki category syntax (yeah, it looks ugly):
// Build a regexp string for matching the given category:
// trim leading/trailing whitespace and underscores
// XXX: the first capturing parens are assumed to match the sortkey, if present, including the | but excluding the ]]
category = category.replace( /^[\s_]+|[\s_]+$/g, '' );
return new RegExp( '\\[\\[[\\s_]*' + catname + '[\\s_]*:[\\s_]*' + category + '[\\s_]*(\\|[^\\]]*(?:\\][^\\]]+)*)?\\]\\]', 'g' );
},


// escape regexp metacharacters (= any ASCII punctuation except _)
getContent: function( file, targetcat, mode ) {
category = mw.RegExp.escape( category );


// any sequence of spaces and underscores should match any other
var data = {
category = category.replace( /[\s_]+/g, '[\\s_]+' );
action: 'query',

prop: 'info|revisions',
// Make the first character case-insensitive:
var first = category.substr( 0, 1 );
if ( first.toUpperCase() !== first.toLowerCase() ) { category = '[' + first.toUpperCase() + first.toLowerCase() + ']' + category.substr( 1 ); }

// Compile it into a RegExp that matches MediaWiki category syntax (yeah, it looks ugly):
// XXX: the first capturing parens are assumed to match the sortkey, if present, including the | but excluding the ]]
return new RegExp( '\\[\\[[\\s_]*' + catname + '[\\s_]*:[\\s_]*' + category + '[\\s_]*(\\|[^\\]]*(?:\\][^\\]]+)*)?\\]\\]\\s*', 'g' );
},

getContent: function ( page, targetcat, mode ) {
if ( !this.cancelled ) {
this.doAPICall( {
curtimestamp: 1,
// meta: 'tokens',
prop: 'revisions',
rvprop: 'content|timestamp',
rvprop: 'content|timestamp',
intoken: 'edit',
titles: page[ 0 ]
}, function ( result ) {
titles: file[ 0 ]
CAL.editCategories( result, page, targetcat, mode );
};
} );
}

},

getTargetCat: function ( pages, targetcat, mode ) {
if ( !this.cancelled ) {
this.doAPICall( {
meta: 'tokens',
prop: 'categories|categoryinfo',
titles: 'Category:' + targetcat
}, function ( result ) {
if ( !result || !result.query ) { return; }
CAL.edittoken = result.query.tokens.csrftoken;
result = CAL._getPageQuery( result );
CAL.checkTargetCat( result );
for ( var i = 0; i < pages.length; i++ ) { CAL.getContent( pages[ i ], targetcat, mode ); }


this.doAPICall( data, function( result ) {
catALot.editCategories( result, file, targetcat, mode );
} );
} );
},
}


},
// Remove {{Uncategorized}}. No need to replace it with anything.
removeUncat: function( text ) {
return text.replace( /\{\{\s*[Uu]ncategorized\s*(\|?.*?)\}\}/, "" );
},


doCleanup: function( text ) {
checkTargetCat: function ( page ) {
var is_dab = false; // disambiguation
if ( this.settings.docleanup ) {
var is_redir = typeof page.redirect === 'string'; // Hard redirect?
return text.replace( /\{\{\s*[Ch]eck categories\s*(\|?.*?)\}\}/, "" );
if ( typeof page.missing === 'string' ) { return alert( mw.msg( 'Apifeatureusage-warnings', mw.msg( 'Categorytree-not-found', page.title ) ) ); }
} else {
var cats = page.categories;
return text;
this.is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden === 'string';

if ( !is_redir && cats && ( CAL.disambig_category || CAL.redir_category ) ) {
for ( var c = 0; c < cats.length; c++ ) {
var cat = cats[ c ].title;
if ( cat ) { // Strip namespace prefix
cat = cat.substring( cat.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
if ( cat === CAL.disambig_category ) {
is_dab = true; break;
} else if ( cat === CAL.redir_category ) {
is_redir = true; break;
}
}
}
}
},
}


if ( !is_redir && !is_dab ) { return; }
editCategories: function( result, file, targetcat, mode ) {
alert( mw.msg( 'Apifeatureusage-warnings', page.title + ' is a ' + CAL.disambig_category ) );
var otext, starttimestamp, timestamp;
},
if ( !result ) {

//Happens on unstable wifi connections..
// Remove {{Uncategorized}} (also with comment). No need to replace it with anything.
this.connectionError.push( file[ 0 ] );
removeUncat: function ( text ) {
return ( this.settings.uncat ? text.replace( /\{\{\s*[Uu]ncategorized\s*[^}]*\}\}\s*(<!--.*?-->\s*)?/, '' ) : text );
},

doCleanup: function ( text ) {
return ( this.settings.docleanup ? text.replace( /\{\{\s*[Cc]heck categories\s*(\|?.*?)\}\}/, '' ) : text );
},

editCategories: function ( result, file, targetcat, mode ) {
if ( !result || !result.query ) {
// Happens on unstable wifi connections..
this.connectionError.push( file[ 0 ] );
this.updateCounter();
return;
}
var otext,
timestamp,
page = CAL._getPageQuery( result );
if ( page.ns === 2 ) { return; }
var id = page.revisions[ 0 ],
catNS = this.localCatName; // canonical cat-name

this.starttimestamp = result.curtimestamp;
otext = id[ '*' ];
timestamp = id.timestamp;

var sourcecat = this.origin;
// Check if that file is already in that category
if ( mode !== 'remove' && this.regexCatBuilder( targetcat ).test( otext ) ) {
// If the new cat is already there, just remove the old one
if ( mode === 'move' ) {
mode = 'remove';
targetcat = sourcecat;
} else {
this.alreadyThere.push( file[ 0 ] );
this.updateCounter();
this.updateCounter();
return;
return;
}
}
}
var pages = result.query.pages;


// Text modification (following 3 functions are partialy taken from HotCat)
// there should be only one, but we don't know its ID
var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
for ( var id in pages ) {
// Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
// The edittoken only changes between logins
// a link must be on one single line.
this.edittoken = pages[ id ].edittoken;
// MediaWiki also removes Unicode bidi override characters in page titles (and namespace names) completely.
otext = pages[ id ].revisions[ 0 ][ '*' ];
// This is *not* handled, as it would require us to allow any of [\u200E\u200F\u202A-\u202E] between any two
starttimestamp = pages[ id ].starttimestamp;
// characters inside a category link. It _could_ be done though... We _do_ handle strange spaces, including the
timestamp = pages[ id ].revisions[ 0 ].timestamp;
// zero-width space \u200B, and bidi overrides between the components of a category link (adjacent to the colon,
}
// or adjacent to and inside of "[[" and "]]").
var findCatsRE = new RegExp( '\\[\\[' + wikiTextBlankOrBidi + this.localizedRegex( 14, 'Category' ) + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g' );


function replaceByBlanks( match ) {
return match.replace( /(\s|\S)/g, ' ' ); // /./ doesn't match linebreaks. /(\s|\S)/ does.
}


function find_insertionpoint( wikitext ) {
var sourcecat = currentCat;
var copiedtext = wikitext
// Check if that file is already in that category
.replace( /<!--(\s|\S)*?-->/g, replaceByBlanks )
if ( mode !== "remove" && this.regexBuilder( targetcat )
.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, replaceByBlanks );
.test( otext ) ) {
// Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
var index = -1;
findCatsRE.lastIndex = 0;
while ( findCatsRE.exec( copiedtext ) !== null ) { index = findCatsRE.lastIndex; }

return index;
}


/**
//If the new cat is already there, just remove the old one.
* @brief Adds the new Category by searching the right insert point,
if ( mode === 'move' ) {
* if there is text after the category section
mode = 'remove';
* @param [string] wikitext
* @param [string] toAdd
* @return Return wikitext
*/
function addCategory( wikitext, toAdd ) {
if ( toAdd && toAdd[ 0 ] ) {
// TODO: support sort key
var cat_point = find_insertionpoint( wikitext ); // Position of last category
var newcatstring = '[[' + catNS + toAdd + ']]';
if ( cat_point > -1 ) {
var suffix = wikitext.substring( cat_point );
wikitext = wikitext.substring( 0, cat_point ) + ( cat_point ? '\n' : '' ) + newcatstring;
if ( suffix[ 0 ] && suffix.substr( 0, 1 ) !== '\n' ) { wikitext += '\n'; }
wikitext += suffix;
} else {
} else {
if ( wikitext[ 0 ] && wikitext.substr( wikitext.length - 1, 1 ) !== '\n' ) { wikitext += '\n'; }
this.alreadyThere.push( file[ 0 ] );

this.updateCounter();
wikitext += ( wikitext[ 0 ] ? '\n' : '' ) + newcatstring;
return;
}
}
}
}
return wikitext;
}
// End HotCat functions


var text = otext;
var text = otext,
arr = is_rtl ? '\u2190' : '\u2192', // left and right arrows. Don't use ← and → in the code.
var comment;
sumCmt, // summary comment
sumCmtShort;
// Fix text
switch ( mode ) {
case 'add':
text = addCategory( text, targetcat );
sumCmt = msg( 'summary-add' ).replace( '$1', targetcat );
sumCmtShort = '+[[' + catNS + targetcat + ']]';
break;
case 'copy':
text = text.replace( this.regexCatBuilder( sourcecat ), '[[' + catNS + sourcecat + '$1]]\n[[' + catNS + targetcat + '$1]]\n' );
sumCmt = msg( 'summary-copy' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
sumCmtShort = '+[[' + catNS + sourcecat + ']]' + arr + '[[' + catNS + targetcat + ']]';
// If category is added through template:
if ( otext === text ) { text = addCategory( text, targetcat ); }


// Fix text
break;
case 'move':
switch ( mode ) {
text = text.replace( this.regexCatBuilder( sourcecat ), '[[' + catNS + targetcat + '$1]]\n' );
case 'add':
sumCmt = msg( 'summary-move' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
text += "\n[[" + this.localCatName + ":" + targetcat + "]]\n";
comment = msgPlain( 'summary-add' ).replace( '$1', targetcat );
sumCmtShort = '±[[' + catNS + sourcecat + ']]' + arr + '[[' + catNS + targetcat + ']]';
break;
break;
case 'copy':
case 'remove':
text = text.replace( this.regexBuilder( sourcecat ), "[[" + this.localCatName + ":" + sourcecat + "$1]]\n[[" + this.localCatName + ":" + targetcat + "$1]]" );
text = text.replace( this.regexCatBuilder( targetcat ), '' );
comment = msgPlain( 'summary-copy' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
sumCmt = msg( 'summary-remove' ).replace( '$1', targetcat );
sumCmtShort = '-[[' + catNS + targetcat + ']]';
//If category is added through template:
break;
if ( otext === text ) {
}
text += "\n[[" + this.localCatName + ":" + targetcat + "]]";
}
break;
case 'move':
text = text.replace( this.regexBuilder( sourcecat ), "[[" + this.localCatName + ":" + targetcat + "$1]]" );
comment = msgPlain( 'summary-move' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
break;
case 'remove':
text = text.replace( this.regexBuilder( sourcecat ), "" );
comment = msgPlain( 'summary-remove' ).replace( '$1', sourcecat );
break;
}


if ( text === otext ) {
if ( text === otext ) {
this.notFound.push( file[ 0 ] );
this.notFound.push( file[ 0 ] );
this.updateCounter();
this.updateCounter();
return;
return;
}
}
otext = text;


// Remove uncat after we checked whether we changed the text successfully.
// Remove {{uncat}} after we checked whether we changed the text successfully.
// Otherwise we might fail to do the changes, but still replace {{uncat}}
// Otherwise we might fail to do the changes, but still replace {{uncat}}
if ( mode !== 'remove' ) {
if ( mode !== 'remove' && ( !non || userGrp.indexOf( 'autoconfirmed' ) > -1 ) ) {
text = this.doCleanup( this.removeUncat( text ) );
if ( !this.is_hidden ) {
text = this.removeUncat( text );
if ( text.length !== otext.length ) { sumCmt += '; ' + msg( 'uncatpref' ); }
}
}
text = this.doCleanup( text );
var data = {
}
action: 'edit',
summary: comment,
title: file[ 0 ],
text: text,
starttimestamp: starttimestamp,
basetimestamp: timestamp,
watchlist: this.settings.watchlist,
token: this.edittoken
};
if ( this.settings.minor ) data.minor = true;


sumCmt += this.summary ? ' ' + this.summary : '';
this.doAPICall( data, function() {
catALot.updateCounter();
} );
this.markAsDone( file[ 1 ], mode, targetcat );
},
markAsDone: function( label, mode, targetcat ) {


label.addClass( 'cat_a_lot_markAsDone' );
var preM = msg( 'prefix-summary' );
var usgM = msg( 'using-summary' );
switch ( mode ) {
// Try shorten summary
case 'add':
if ( preM || usgM ) {
label.append( '<br>' + msg( 'added-cat', targetcat ) );
sumCmt = ( sumCmt.length > 250 - preM.length - usgM.length ) ?
break;
sumCmt + ' (CatAlot)' : preM + sumCmt + usgM;
case 'copy':
}
label.append( '<br>' + msg( 'copied-cat', targetcat ) );
break;
case 'move':
label.append( '<br>' + msg( 'moved-cat', targetcat ) );
break;
case 'remove':
label.append( '<br>' + msg( 'removed-cat' ) );
break;
}
},
updateCounter: function() {


if ( sumCmt.length > 254 ) // Try short summary
this.counterCurrent++;
{ sumCmt = sumCmtShort; }
if ( this.counterCurrent > this.counterNeeded ) this.displayResult();
else this.domCounter.text( this.counterCurrent );
},


var data = {
displayResult: function() {
action: 'edit',
assert: 'user',
summary: sumCmt,
title: file[ 0 ],
text: text,
bot: true,
starttimestamp: this.starttimestamp,
basetimestamp: timestamp,
watchlist: this.settings.watchlist,
minor: this.settings.minor,
tags: this.changeTag,
token: this.edittoken
};


this.doAPICall( data, function ( r ) {
document.body.style.cursor = 'auto';
delete CAL.XHR[ file[ 0 ] ];
$( '.cat_a_lot_feedback' )
return CAL.updateUndoCounter( r );
.addClass( 'cat_a_lot_done' );
} );
$( '.ui-dialog-content' )
this.markAsDone( file[ 1 ], mode, targetcat );
.height( 'auto' );
},
var rep = this.domCounter.parent();
rep.html( '<h3>' + msg( 'done' ) + '</h3>' );
rep.append( msg( 'all-done' ) + '<br />' );


markAsDone: function ( label, mode, targetcat ) {
var close = $( '<a>' )
.text( msgPlain( 'return-to-page' ) );
mode = ( function ( m ) {
close.click( function() {
switch ( m ) {
case 'add': return 'added-cat';
catALot.progressDialog.remove();
case 'copy': return 'copied-cat';
catALot.toggleAll( false );
case 'move': return 'moved-cat';
} );
case 'remove': return 'removed-cat';
rep.append( close );
if ( this.alreadyThere.length ) {
rep.append( '<h5>' + msg( 'skipped-already', this.alreadyThere.length ) + '</h5>' );
rep.append( this.alreadyThere.join( '<br>' ) );
}
if ( this.notFound.length ) {
rep.append( '<h5>' + msg( 'skipped-not-found', this.notFound.length ) + '</h5>' );
rep.append( this.notFound.join( '<br>' ) );
}
if ( this.connectionError.length ) {
rep.append( '<h5>' + msg( 'skipped-server', this.connectionError.length ) + '</h5>' );
rep.append( this.connectionError.join( '<br>' ) );
}
}
}( mode ) );
label.addClass( 'cat_a_lot_markAsDone' ).append( '<br>' + msg( mode, targetcat ) );
},


updateUndoCounter: function ( r ) {
},
this.updateCounter();
if ( !r.edit || r.edit.result !== 'Success' ) { return; }
r = r.edit;


this.undoList.push( {
moveHere: function( targetcat ) {
title: r.title,
this.doSomething( targetcat, 'move' );
id: r.newrevid,
},
timestamp: r.newtimestamp
} );
},


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


addHere: function( targetcat ) {
displayResult: function () {
document.body.style.cursor = 'auto';
this.doSomething( targetcat, 'add' );
$.removeSpinner( 'fb-dialog' );
},
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.alreadyThere.length ) {
rep.append( '<h5>' + msg( 'skipped-already', this.alreadyThere.length ) + '</h5>' )
.append( this.alreadyThere.join( '<br>' ) );
}


if ( this.notFound.length ) {
remove: function() {
rep.append( '<h5>' + msg( 'skipped-not-found', this.notFound.length ) + '</h5>' )
this.doSomething( '', 'remove' );
.append( this.notFound.join( '<br>' ) );
},
}


if ( this.connectionError.length ) {
doSomething: function( targetcat, mode ) {
rep.append( '<h5>' + msg( 'skipped-server', this.connectionError.length ) + '</h5>' )
var files = this.getMarkedLabels();
.append( this.connectionError.join( '<br>' ) );
if ( files.length === 0 ) {
}
alert( msgPlain( 'none-selected' ) );

return;
},
}

this.notFound = [];
/**
this.alreadyThere = [];
* @brief set parameters for API call,
this.connectionError = [];
* convert targetcat to string, get selected pages/files
this.counterCurrent = 1;
* @param [dom object] targetcat with data
this.counterNeeded = files.length;
* @param [string] mode action
mw.loader.using( ['jquery.ui.dialog', 'mediawiki.RegExp'], function() {
* @return Return API call getTargetCat with pages
catALot.showProgress();
*/
for ( var i = 0; i < files.length; i++ ) {
catALot.getContent( files[ i ], targetcat, mode );
doSomething: function ( targetcat, mode ) {
var pages = this.getMarkedLabels();
}
if ( !pages.length ) { return alert( mw.msg( 'Ooui-selectfile-placeholder' ) ); }
targetcat = $( targetcat ).closest( 'tr' ).data( 'cat' );

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

if ( $( '#cat_a_lot_comment' ).prop( 'checked' ) ) { this.summary = window.prompt( msg( 'edit-question' ), '' ); } // TODO custom pre-value
if ( this.summary !== null ) {
mw.loader.using( [ 'jquery.ui', 'jquery.spinner', 'mediawiki.util' ], function () {
CAL.showProgress();
CAL.getTargetCat( pages, targetcat, mode );
} );
} );
},
}


},
doAPICall: function( params, callback ) {

params.format = 'json';
doAPICall: function ( params, callback ) {
var i = 0;
params = $.extend( {
var apiUrl = this.apiUrl;
action: 'query',
var handleError = function( jqXHR, textStatus, errorThrown ) {
format: 'json'
if ( window.console && $.isFunction( window.console.log ) ) {
}, params );
window.console.log( 'Error: ', jqXHR, textStatus, errorThrown );

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


if ( params.action === 'edit' && !CAL.cancelled ) { CAL.XHR[ params.title ] = xhr; }
createCatLinks: function( symbol, list ) {
};
list.sort();
doCall();
var domlist = $resultList.find( 'table' );
},
for ( var i = 0; i < list.length; i++ ) {
var $tr = $( '<tr>' );


createCatLinks: function ( symbol, list, table ) {
var $link = $( '<a>' ),
list.sort();
$add, $move, $copy;
var button = ( this.settings.button && mw.loader.getState( 'jquery.ui' ) === 'ready' ) ? 1 : 0;
for ( var c = 0; c < list.length; c++ ) {
var $tr = $( '<tr>' ),
$link = $( '<a>', {
href: mw.util.getUrl( CAL.localCatName + list[ c ] ),
text: list[ c ]
} ),
$buttons = [];
$tr.data( 'cat', list[ c ] );
$link.on( 'click', function ( e ) {
if ( !e.ctrlKey ) {
e.preventDefault();
CAL.updateCats( $( this ).closest( 'tr' ).data( 'cat' ) );
}
} );


$link.text( list[ i ] );
$tr.append( $( '<td>' ).text( symbol ) )
$tr.data( 'cat', list[ i ] );
.append( $( '<td>' ).append( $link ) );

$link.click( function() {
$buttons.push( $( '<a>' )
catALot.updateCats( $( this ).closest('tr').data( 'cat' ) );
.text( mw.msg( 'Centralnotice-remove' ) )
.on( 'click', function () {
CAL.doSomething( this, 'remove' );
} )
.addClass( 'cat_a_lot_move' )
);
if ( button ) {
$buttons.slice( -1 )[ 0 ].button( {
icons: { primary: 'ui-icon-minusthick' },
showLabel: false,
text: false
} );
} );
}


if ( this.searchmode ) {
if ( this.origin ) {
// Can't move to source category
$add = $( '<a class="cat_a_lot_action"></a>' )
if ( list[ c ] !== this.origin ) {
.text( msgPlain( 'add' ) )
.click( function() {
$buttons.push( $( '<a>' )
catALot.addHere( $( this ).closest('tr').data( 'cat' ) );
.text( msg( 'move' ) )
} );
.on( 'click', function () {
CAL.doSomething( this, 'move' );
} else {
} )
$move = $( '<a class="cat_a_lot_move"></a>' )
.text( msgPlain( 'move' ) )
.addClass( 'cat_a_lot_move' )
.click( function() {
);
if ( button ) {
catALot.moveHere( $( this ).closest('tr').data( 'cat' ) );
$buttons.slice( -1 )[ 0 ].button( {
icons: { primary: 'ui-icon-arrowthick-1-e' },
showLabel: false,
text: false
} );
} );
}


$copy = $( '<a class="cat_a_lot_action"></a>' )
$buttons.push( $( '<a>' )
.text( msgPlain( 'copy' ) )
.text( msg( 'copy' ) )
.click( function() {
.on( 'click', function () {
catALot.copyHere( $( this ).closest('tr').data( 'cat' ) );
CAL.doSomething( this, 'copy' );
} )
.addClass( 'cat_a_lot_action' )
);
if ( button ) {
$buttons.slice( -1 )[ 0 ].button( {
icons: { primary: 'ui-icon-plusthick' },
showLabel: false,
text: false
} );
} );
}

}
} else {
$buttons.push( $( '<a>' )
.text( msg( 'add' ) )
.on( 'click', function () {
CAL.doSomething( this, 'add' );
} )
.addClass( 'cat_a_lot_action' )
);
if ( button ) {
$buttons.slice( -1 )[ 0 ].button( {
icons: { primary: 'ui-icon-plusthick' },
showLabel: false,
text: false
} );
}
}


}
$tr.append( $('<td>').text( symbol ) )
// TODO CSS may extern
.append( $('<td>').append( $link ) );
var css = button ? { fontSize: '.6em', margin: '0', width: '2.5em' } : {};
for ( var b = 0; b < $buttons.length; b++ ) { $tr.append( $( '<td>' ).append( $buttons[ b ].css( css ) ) ); }


table.append( $tr );
// Can't move to source category
}
if ( list[ i ] !== currentCat && this.searchmode ) {
},
$tr.append( $('<td>').append( $add ) );
} else if ( list[ i ] !== currentCat && !this.searchmode ) {
$tr.append( $('<td>').append( $move ), $('<td>').append( $copy ) );
}


getCategoryList: function () {
domlist.append( $tr );
this.catCounter = 0;
this.getParentCats();
this.getSubCats();
},

_getPageQuery: function ( data ) {
// There should be only one, but we don't know its ID
if ( data && data.query && data.query.pages ) {
data = data.query.pages;
for ( var p in data ) { return data[ p ]; }
}
},

/**
* @brief takes this.currentCategory if redir_category is configured
** Cat pages with more than one cat link are still not supported for sure
* @return soft redirected cat
*/
solveSoftRedirect: function () {
this.doAPICall( {
prop: 'links', // TODO: For more accuracy the revisions could be checked
titles: 'Category:' + this.currentCategory,
// 'rvprop': 'content',
// 'pllimit': 'max',
plnamespace: 14
}, function ( page ) {
page = CAL._getPageQuery( page );
if ( page ) {
var lks = page.links;
if ( lks && lks.length === 1 && lks[ 0 ].title ) {
CAL.currentCategory = lks[ 0 ].title.replace( reCat, '' );
$searchInput.val( CAL.currentCategory );
return CAL.getCategoryList();
} else {
// TODO? better translatable warning message: "Please solve the category soft redirect manually!"
$resultList.html( '<span id="cat_a_lot_no_found">' + mw.msg( 'Apifeatureusage-warnings', mw.msg( 'Categorytree-not-found', CAL.currentCategory ) ) + '</span>' );
}
}
}
},
} );
},


getCategoryList: function() {
showCategoryList: function () {
if ( this.settings.redir_category && this.settings.redir_category === this.parentCats[ 0 ] ) { return this.solveSoftRedirect(); }
this.catCounter = 0;
this.getParentCats();
this.getSubCats();
},


var table = $( '<table>' );
showCategoryList: function() {
var thiscat = [ this.currentCategory ];


this.createCatLinks( '↑', this.parentCats, table );
$resultList.empty();
this.createCatLinks( '→', [ this.currentCategory ], table );
$resultList.append( '<table></table>' );
// Show on soft-redirect
if ( $searchInput.val() === this.currentCategory && this.origin !== this.currentCategory ) { this.createCatLinks( '→', [ this.origin ], table ); }
this.createCatLinks( '↓', this.subCats, table );


$resultList.empty();
this.createCatLinks( "↑", this.parentCats );
$resultList.append( table );
this.createCatLinks( "→", thiscat );
this.createCatLinks( "↓", this.subCats );


document.body.style.cursor = 'auto';
document.body.style.cursor = 'auto';
//Reset width
$container.width( '' );
$container.height( '' );
$container.width( Math.min( $container.width() * 1.1 + 15, $( window ).width() - 10 ) );


// Reset width
$resultList.css( {
$container.width( '' );
maxHeight: this.setHeight + 'px',
height: ''
$container.height( '' );
$container.width( Math.min( table.width() * 1.1 + 15, $( window ).width() - 10 ) );

$resultList.css( {
maxHeight: Math.min( this.setHeight, $( window ).height() - $container.position().top - $settingsLink.outerHeight() - $selections.outerHeight() - 15 ),
height: ''
} );
table.width( '100%' );
$container.height( Math.min( $container.height(), $head.offset().top - $container.offset().top + 10 ) );
$container.offset( { left: $( window ).width() - $container.outerWidth() } ); // Fix overlap
},

updateCats: function ( newcat ) {
document.body.style.cursor = 'wait';
this.currentCategory = newcat;
$resultList.html( '<div class="cat_a_lot_loading">' + mw.msg( 'Wikieditor-loading' ) + '</div>' );
this.getCategoryList();
},

doUndo: function () {
this.cancelled = 0;
this.doAbort();
if ( !this.undoList.length ) { return; }

$( '.cat_a_lot_feedback' ).removeClass( 'cat_a_lot_done' );
this.counterNeeded = this.undoList.length;
this.counterCurrent = 1;

document.body.style.cursor = 'wait';

var query = {
action: 'edit',
user: mw.config.get( 'wgUserName' ),
bot: true,
minor: this.settings.minor,
starttimestamp: this.starttimestamp,
watchlist: this.settings.watchlist,
tags: this.changeTag,
token: this.edittoken
};
for ( var i = 0; i < this.undoList.length; i++ ) {
var uID = this.undoList[ i ];
query.title = uID.title;
query.undo = uID.id;
query.basetimestamp = uID.timestamp;
this.doAPICall( query, function ( r ) {
// TODO: Add "details" to progressbar?
// $resultList.append( [mw.msg('Filerevert-submit') + " done " + r.edit.title, '<br>' ] );
if ( r && r.edit ) { mw.log( 'Revert done', r.edit.title ); }
CAL.updateCounter();
} );
} );
},
}
},


updateCats: function( newcat ) {
doAbort: function () {
for ( var t in this.XHR ) { this.XHR[ t ].abort(); }
document.body.style.cursor = 'wait';


if ( this.cancelled ) { // still not for undo
this.currentCategory = newcat;
this.progressDialog.remove();
$resultList.html( '<div class="cat_a_lot_loading"></div>' ).text( msgPlain( 'loading' ) );
this.getCategoryList();
this.toggleAll( false );
$head.last().show();
},
}
showProgress: function() {
this.cancelled = 1;
document.body.style.cursor = 'wait';
},


showProgress: function () {
this.progressDialog = $( '<div></div>' )
document.body.style.cursor = 'wait';
.html( msg( 'editing' ) + ' <span id="cat_a_lot_current">' + this.counterCurrent + '</span> ' + msg( 'of' ) + this.counterNeeded )
this.progressDialog = $( '<div>' )
.dialog( {
.html( ' ' + msg( 'editing' ) + ' <span id="cat_a_lot_current">' + CAL.counterCurrent + '</span> ' + msg( 'of' ) + CAL.counterNeeded )
width: 450,
.prepend( $.createSpinner( { id: 'fb-dialog', size: 'large' } ) )
height: 90,
minHeight: 90,
.dialog( {
modal: true,
width: 450,
resizable: false,
height: 180,
draggable: false,
minHeight: 90,
closeOnEscape: false,
modal: true,
resizable: false,
dialogClass: "cat_a_lot_feedback"
} );
draggable: false,
// closeOnEscape: true,
$( '.ui-dialog-titlebar' )
dialogClass: 'cat_a_lot_feedback',
.hide();
buttons: [ {
this.domCounter = $( '#cat_a_lot_current' );
text: mw.msg( 'Cancel' ), // Stops all actions
click: function () {
$( this ).dialog( 'close' );
}
} ],
close: function () {
CAL.cancelled = 1;
CAL.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%'
} )
.append( [ '[ ',
$( '<a>', { title: 'Revert all last done edits' } ) // TODO i18n
.on( 'click', function () {
if ( window.confirm( mw.msg( 'Apifeatureusage-warnings', this.title + '⁉' ) ) ) {
CAL.doUndo();
$( this ).parent().remove();
}
return false;
} )
.addClass( 'new' )
.text( mw.msg( 'Filerevert-submit' ) ),
' ]'
] ).insertAfter( $link );
}


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


run: function() {
minimize: function ( e ) {
CAL.top = Math.max( 0, $container.position().top );
if ( $( '.cat_a_lot_enabled' ).length ) {
CAL.height = $container.height();
this.makeClickable();
$dataContainer
$dataContainer.hide();
$container.animate( {
.show();
height: $head.height(),
$container
top: $( window ).height() - $head.height() * 1.4
}, function () {
$( e.target ).one( 'click', CAL.maximize );
} );
},

maximize: function ( e ) {
$dataContainer.show();
$container.animate( {
top: CAL.top,
height: CAL.height
}, function () {
$( e.target ).one( 'click', CAL.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 )
);
}
$dataContainer.show();
$container.one( 'mouseover', function () {
$( this )
.resizable( {
.resizable( {
handles: 'n',
handles: 'n',
alsoResize: '#cat_a_lot_category_list',
alsoResize: '#cat_a_lot_category_list',
resize: function() {
resize: function () {
$( this )
.css( {
left: '',
top: ''
} );
catALot.setHeight = $( this )
.height();
$resultList
$resultList
.css( {
.css( {
Line 766: Line 1,312:
width: ''
width: ''
} );
} );
},
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'
} );
},
stop: function () {
CAL.setHeight = $resultList.height();
}
}
} )
.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
} );
} );
$resultList
$resultList
.css( {
.css( { maxHeight: 450 } );
} );
maxHeight: '450px'
this.updateCats( this.origin || 'Images' );
} );
if ( this.searchmode ) this.updateCats( "Pictures and images" );
else this.updateCats( currentCat );


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


manageSettings: function() {
manageSettings: function () {
mw.loader.using( [ 'ext.gadget.SettingsManager', 'ext.gadget.SettingsUI', 'jquery.ui.progressbar' ], function() {
mw.loader.using( [ 'ext.gadget.SettingsManager', 'ext.gadget.SettingsUI', 'jquery.ui' ], CAL._manageSettings );
},
catALot._manageSettings();
} );
},
_manageSettings: function() {
window.mw.libs.SettingsUI( this.defaults, "Cat-A-Lot" )
.show()
.done( function( s, verbose, loc, settingsOut, $dlg ) {
var mustRestart = false,
_restart = function() {
if ( !mustRestart ) return;


_manageSettings: function () {
$container.remove();
mw.libs.SettingsUI( CAL.defaults, 'Cat-a-lot' )
catALot.labels.unbind( 'click.catALot' );
catALot.init();
.show()
.done( function ( s, verbose, loc, settingsOut, $dlg ) {
},
var mustRestart = false,
_saveToJS = function() {
var opt = mw.libs.settingsManager.option( {
_restart = function () {
if ( !mustRestart ) { return; }
optionName: 'catALotPrefs',
value: catALot.settings,
$container.remove();
encloseSignature: 'catALot',
CAL.labels.off( 'click.catALot' );
CAL.init();
encloseBlock: '////////// Cat-A-Lot user preferences //////////\n',
},
triggerSaveAt: /Cat.?A.?Lot/i,
_saveToJS = function () {
editSummary: msgPlain( 'pref-save-summary' )
var opt = mw.libs.settingsManager.option( {
} ),
oldHeight = $dlg.height(),
optionName: 'catALotPrefs',
$prog = $( '<div>' );
value: CAL.settings,
encloseSignature: 'catALot',
encloseBlock: '////////// Cat-a-lot user preferences //////////\n',
triggerSaveAt: /Cat.?A.?Lot/i,
editSummary: msg( 'pref-save-summary' )
} ),
oldHeight = $dlg.height(),
$prog = $( '<div>' );


$dlg.css( 'height', oldHeight )
$dlg.css( 'height', oldHeight )
.html( '' );
.html( '' );
$prog.css( {
$prog.css( {
'height': Math.round( oldHeight / 8 ),
height: Math.round( oldHeight / 8 ),
'margin-top': Math.round( ( 7 * oldHeight ) / 16 )
'margin-top': Math.round( ( 7 * oldHeight ) / 16 )
} )
} )
.appendTo( $dlg );
.appendTo( $dlg );


$dlg.parent()
$dlg.parent()
.find( '.ui-dialog-buttonpane button' )
.find( '.ui-dialog-buttonpane button' )
.button( 'option', 'disabled', true );
.button( 'option', 'disabled', true );


opt.save()
opt.save()
.done( function( text, progress ) {
.done( function ( text, progress ) {
$prog.progressbar( {
$prog.progressbar( {
value: progress
value: progress
} );
$prog.fadeOut( function() {
$dlg.dialog( 'close' );
_restart();
} );
} )
.progress( function( text, progress ) {
$prog.progressbar( {
value: progress
} );
// TODO: Add "details" to progressbar
} )
.fail( function( text ) {
$prog.addClass( 'ui-state-error' );
$dlg.prepend( $( '<p>' )
.text( text ) );
} );
} );
$prog.fadeOut( function () {
};
$.each( settingsOut, function( n, v ) {
$dlg.dialog( 'close' );
_restart();
if ( v.forcerestart && catALot.settings[ v.name ] !== v.value ) {
mustRestart = true;
} );
}
} )
catALot.settings[ v.name ] = v.value;
.progress( function ( text, progress ) {
$prog.progressbar( {
window.catALotPrefs[ v.name ] = v.value;
} );
value: progress
switch ( loc ) {
} );
// TODO: Add "details" to progressbar
case 'page':
$dlg.dialog( 'close' );
} )
_restart();
.fail( function ( text ) {
$prog.addClass( 'ui-state-error' );
break;
$dlg.prepend( $( '<p>' )
case 'account-publicly':
_saveToJS();
.text( text ) );
break;
} );
}
};
$.each( settingsOut, function ( n, v ) {
if ( v.forcerestart && CAL.settings[ v.name ] !== v.value ) { mustRestart = true; }
CAL.settings[ v.name ] = CAL.catALotPrefs[ v.name ] = v.value;
} );
} );
switch ( loc ) {
},
case 'page':
_initSettings: function() {
$dlg.dialog( 'close' );
if ( this.settings.watchlist ) return;
_restart();
if ( !window.catALotPrefs ) window.catALotPrefs = {};
break;
$.each( this.defaults, function( n, v ) {
case 'account-publicly':
v.value = catALot.settings[ v.name ] = ( window.catALotPrefs[ v.name ] || v[ 'default' ] );
_saveToJS();
v.label = msgPlain( v.label_i18n );
break;
if ( v.select_i18n ) {
v.select = {};
$.each( v.select_i18n, function( i18nk, val ) {
v.select[ msgPlain( i18nk ) ] = val;
} );
}
}
} );
} );
},
},

defaults: [ {
_initSettings: function () {
name: 'watchlist',
if ( this.settings.watchlist ) { return; }
'default': 'preferences',
this.catALotPrefs = window.catALotPrefs || {};
label_i18n: 'watchlistpref',
for ( var i = 0; i < this.defaults.length; i++ ) {
select_i18n: {
var v = this.defaults[ i ];
'watch_pref': 'preferences',
v.value = this.settings[ v.name ] = ( this.catALotPrefs[ v.name ] || v['default'] );
'watch_nochange': 'nochange',
v.label = msg( v.label_i18n );
'watch_watch': 'watch',
if ( v.select_i18n ) {
'watch_unwatch': 'unwatch'
v.select = {};
$.each( v.select_i18n, function ( i18nk, val ) {
v.select[ msg( i18nk ) ] = val;
} );
}
}
}, {
}
},
name: 'minor',
/* eslint-disable camelcase */
'default': false,
defaults: [ {
label_i18n: 'minorpref'
name: 'watchlist',
}, {
name: 'editpages',
'default': 'preferences',
label_i18n: 'watchlistpref',
'default': false,
select_i18n: {
label_i18n: 'editpagespref',
watch_pref: 'preferences',
forcerestart: true
watch_nochange: 'nochange',
}, {
name: 'docleanup',
watch_watch: 'watch',
watch_unwatch: 'unwatch'
'default': false,
}
label_i18n: 'docleanuppref'
}, {
}, {
name: 'subcatcount',
name: 'minor',
'default': 50,
'default': false,
label_i18n: 'minorpref'
'min': 5,
}, {
'max': 500,
label_i18n: 'subcatcountpref',
name: 'editpages',
'default': project !== 'commonswiki', // on Commons false
forcerestart: true
label_i18n: 'editpagespref',
} ]
forcerestart: true
};
}, {
name: 'docleanup',
'default': false,
label_i18n: 'docleanuppref'
}, {
name: 'subcatcount',
'default': 50,
min: 5,
max: 500,
label_i18n: 'subcatcountpref',
forcerestart: true
}, {
name: 'uncat',
'default': project === 'commonswiki', // on Commons true
label_i18n: 'uncatpref'
}, {
name: 'button',
'default': true,
label_i18n: 'buttonpref'
} ]
/* eslint-enable camelcase */
};


// The gadget is not immediately needed, so let the page load normally
if ( ( nsNumber === -1 && mw.config.get( 'wgCanonicalSpecialPageName' ) === "Search" ) || nsNumber === nsCat ) {
window.setTimeout( function () {
if ( nsNumber === -1 ) {
non = mw.config.get( 'wgUserName' );
catALot.searchmode = true;
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 14:
CAL.searchmode = 'category';
CAL.origin = mw.config.get( 'wgTitle' );
break;
case -1:
CAL.searchmode = {
// list of accepted special page names mapped to search mode names
Contributions: 'contribs',
Listfiles: non ? null : 'listfiles',
Prefixindex: non ? null : 'prefix',
Search: 'search',
Uncategorizedimages: 'gallery'
}[ mw.config.get( 'wgCanonicalSpecialPageName' ) ];
break;
case 2:
case 0:
CAL.searchmode = 'gallery';
var parents = $( '#mw-normal-catlinks ul' ).find( 'a[title]' ), n;
parents.each( function ( i ) {
if ( new RegExp( mw.config.get( 'wgTitle' ), 'i' ).test( $( this ).text() ) ) {
n = i;
return false;
}
} );
CAL.origin = parents.eq( n || 0 ).text();
}

if ( CAL.searchmode ) {
var loadingLocalizations = 1;
var loadingLocalizations = 1;
var loadLocalization = function( lang, cb ) {
var loadLocalization = function ( lang, cb ) {
loadingLocalizations++;
loadingLocalizations++;
switch ( lang ) {
switch ( lang ) {
Line 932: Line 1,561:
lang = 'zh-hans';
lang = 'zh-hans';
break;
break;

}
}

$.ajax( {
$.ajax( {
url: mw.util.wikiScript(),
url: commonsURL,
dataType: 'script',
dataType: 'script',
data: {
data: {
Line 950: Line 1,579:
} );
} );
};
};
var maybeLaunch = function() {
var maybeLaunch = function () {
loadingLocalizations--;
loadingLocalizations--;
function init() {
function init() {
$( document ).ready( function() {
$( function () {
catALot.init();
CAL.init();
} );
} );
}
if ( 0 === loadingLocalizations ) {
mw.loader.using( [ 'user' ], init, init );
}
}
if ( !loadingLocalizations ) { mw.loader.using( [ 'user' ], init, init ); }
};
};


if ( mw.config.get( 'wgUserLanguage' ) !== 'en' ) {
var userlang = mw.config.get( 'wgUserLanguage' ),
loadLocalization( mw.config.get( 'wgUserLanguage' ), maybeLaunch );
contlang = mw.config.get( 'wgContentLanguage' );
if ( userlang !== 'en' ) { loadLocalization( userlang, maybeLaunch ); }
}
if ( $.inArray( contlang, [ 'en', userlang ] ) === -1 ) { loadLocalization( contlang, maybeLaunch ); }
if ( mw.config.get( 'wgContentLanguage' ) !== 'en' ) {
loadLocalization( mw.config.get( 'wgContentLanguage' ), maybeLaunch );
}
maybeLaunch();
maybeLaunch();
}
}
}, 400 );

} )( jQuery, mediaWiki );


/**
/**
* Derivative work of
* Derivative work of
* (replace "checkboxes" with cat-a-lot labels in your mind)
* (replace "checkboxes" with cat-a-lot labels in your mind)
*/
**
* jQuery checkboxShiftClick
/**
*
* jQuery checkboxShiftClick
* This will enable checkboxes to be checked or unchecked in a row by clicking one, holding shift and clicking another one
*
*
* This will enable checkboxes to be checked or unchecked in a row by clicking one, holding shift and clicking another one
* @author Krinkle <krinklemail@gmail.com>
*
* @license GPL v2
* @author Krinkle <krinklemail@gmail.com>
*/
* @license GPL v2
$.fn.catALotShiftClick = function ( cb ) {
*/
var prevCheckbox = null,
( function( $ ) {
$box = this;
$.fn.catALotShiftClick = function( cb ) {
// When our boxes are clicked..
var prevCheckbox = null,
$box.on( 'click.catALot', function ( e ) {
$box = this;
// Prevent following the link and text selection
// When our boxes are clicked..
if ( !e.ctrlKey ) { e.preventDefault(); }
$box.bind( 'click.catALot', function( e ) {
// 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' )
// Highlight last selected
.toggleClass( 'cat_a_lot_selected' );
$( '#cat_a_lot_last_selected' )
// And one has been clicked before…
.removeAttr( 'id' );
var $thisControl = $( e.target ),
if ( prevCheckbox !== null && e.shiftKey ) {
method = $thisControl.hasClass( 'cat_a_lot_selected' ) ? 'addClass' : 'removeClass';
method;
// Check or uncheck this one and all in-between checkboxes
if ( !$thisControl.hasClass( 'cat_a_lot_label' ) ) {
$box.slice(
$thisControl = $thisControl.parents( '.cat_a_lot_label' );
Math.min( $box.index( prevCheckbox ), $box.index( $thisControl ) ),
}
Math.max( $box.index( prevCheckbox ), $box.index( $thisControl ) ) + 1
$thisControl.attr( 'id', 'cat_a_lot_last_selected' )
.toggleClass( 'cat_a_lot_selected' );
)[ method ]( 'cat_a_lot_selected' );
}

// And one has been clicked before...
// Either way, update the prevCheckbox variable to the one clicked now
if ( prevCheckbox !== null && e.shiftKey ) {
prevCheckbox = $thisControl;
if ( $.isFunction( cb ) ) { cb(); }
// Prevent selection
} );
e.preventDefault();
return $box;

};
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 ) );


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

Latest revision as of 23:41, 25 October 2019

/**
* Cat-a-lot
* Changes category of multiple files
*
* @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)

* Requires [[MediaWiki:Gadget-SettingsManager.js]] and [[MediaWiki:Gadget-SettingsUI.js]] (properly registered) for per-user-settings
*
* READ THIS PAGE IF YOU WANT TO TRANSLATE OR USE THIS ON ANOTHER SITE:
* http://commons.wikimedia.org/wiki/MediaWiki:Gadget-Cat-a-lot.js/translating
* <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' ),
	nsIDs = mw.config.get( 'wgNamespaceIds' ),
	userGrp = mw.config.get( 'wgUserGroups' ),
	project = mw.config.get( 'wgDBname' );

var msgs = {
// Preferences
// new: added 2012-09-19. Please translate.
// Use user language for i18n
	'cat-a-lot-watchlistpref': 'Watchlist preference concerning files edited with Cat-a-lot',
	'cat-a-lot-watch_pref': 'According to your general preferences',
	'cat-a-lot-watch_nochange': 'Do not change watchstatus',
	'cat-a-lot-watch_watch': 'Watch pages edited with Cat-a-lot',
	'cat-a-lot-watch_unwatch': 'Remove pages while editing with Cat-a-lot from your watchlist',
	'cat-a-lot-minorpref': 'Mark edits as minor (if you generally mark your edits as minor, this won’t change anything)',
	'cat-a-lot-editpagespref': 'Allow categorising pages (including categories) that are not files',
	'cat-a-lot-docleanuppref': 'Remove {{Check categories}} and other minor cleanup',
	'cat-a-lot-uncatpref': 'Remove {{Uncategorized}}',
	'cat-a-lot-subcatcountpref': 'Sub-categories to show at most',
	'cat-a-lot-config-settings': 'Preferences',
	'cat-a-lot-buttonpref': 'Use buttons instead of text-links',
	'cat-a-lot-comment-label': 'Custom edit comment',
	'cat-a-lot-edit-question': 'Why is this change necessary?',

	// Progress
	// 'cat-a-lot-loading': 'Loading …',
	'cat-a-lot-editing': 'Editing page',
	'cat-a-lot-of': 'of ',
	'cat-a-lot-skipped-already': 'The following {{PLURAL:$1|1=page was|$1 pages were}} skipped, because the page was already in the category:',
	'cat-a-lot-skipped-not-found': 'The following {{PLURAL:$1|1=page was|$1 pages were}} skipped, because the old category could not be found:',
	'cat-a-lot-skipped-server': 'The following {{PLURAL:$1|1=page|$1 pages}} couldn’t be changed, since there were problems connecting to the server:',
	'cat-a-lot-all-done': 'All pages are processed.',
	'cat-a-lot-done': 'Done!', // mw.msg("Feedback-close")
	'cat-a-lot-added-cat': 'Added category $1',
	'cat-a-lot-copied-cat': 'Copied to category $1',
	'cat-a-lot-moved-cat': 'Moved to category $1',
	'cat-a-lot-removed-cat': 'Removed from category $1',
	// 'cat-a-lot-return-to-page': 'Return to page',
	// 'cat-a-lot-cat-not-found': 'Category not found.',

	// as in 17 files selected
	'cat-a-lot-files-selected': '{{PLURAL:$1|1=One file|$1 files}} selected.',
	'cat-a-lot-pe_file': '$1 {{PLURAL:$1|page|pages}} of $2 affected',
	'cat-a-lot-parent-cat': 'Has parent-category: ',
	'cat-a-lot-sub-cat': 'Has sub-category: ',

	// Actions
	'cat-a-lot-copy': 'Copy',
	'cat-a-lot-move': 'Move',
	'cat-a-lot-add': 'Add',
	// 'cat-a-lot-remove-from-cat': 'Remove from this category',
	'cat-a-lot-overcat': 'Check over-categorization',
	'cat-a-lot-enter-name': 'Enter category name',
	'cat-a-lot-select': 'Select',
	'cat-a-lot-all': 'all',
	'cat-a-lot-none': 'none',
	// 'cat-a-lot-none-selected': 'No files selected!', 'Ooui-selectfile-placeholder'

	// Summaries (project language):
	'cat-a-lot-pref-save-summary': 'Updating user preferences',
	'cat-a-lot-summary-add': 'Adding [[Category:$1]]',
	'cat-a-lot-summary-copy': 'Copying from [[Category:$1]] to [[Category:$2]]',
	'cat-a-lot-summary-move': 'Moving from [[Category:$1]] to [[Category:$2]]',
	'cat-a-lot-summary-remove': 'Removing from [[Category:$1]]',
	'cat-a-lot-prefix-summary': '',
	'cat-a-lot-using-summary': ' using [[c:Help:Cat-a-lot|Cat-a-lot]]'
};
mw.messages.set( msgs );

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

// There is only one Cat-a-lot on one page
var $body, $container, $dataContainer, $searchInputContainer, $searchInput, $resultList, $markCounter, $selections,
	$selectFiles, $selectPages, $selectNone, $selectInvert, $settingsWrapper, $settingsLink, $head, $link, $overcat,
	commonsURL = 'https://commons.wikimedia.org/w/index.php',
	is_rtl = $( 'body' ).hasClass( 'rtl' ),
	reCat, // localized category search regexp
	non,
	r; // result file count for overcat

var CAL = mw.libs.catALot = {
	apiUrl: mw.util.wikiScript( 'api' ),
	origin: '',
	searchmode: false,
	version: '4.77',
	setHeight: 450,
	changeTag: 'Cat-a-lot',

	settings: {
	/* Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
any items, but that contains links to other categories where stuff should be categorized. If you don't have
that concept on your wiki, set it to null. Use blanks, not underscores. */
		disambig_category: 'Disambiguation', // Commons and EnWP
		/* Any category in this category is deemed a (soft) redirect to some other category defined by a link
* to another non-blacklisted category. If your wiki doesn't have soft category redirects, set this to null.
* If a soft-redirected category contains more than one link to another non-blacklisted category, it's considered
* a disambiguation category instead. */
		redir_category: 'Category redirects'

	},

	init: function () {
		// Prevent historical double marker (maybe remove in future)
		if ( /Cat-?a-?lot/i.test( msgs[ 'cat-a-lot-pref-save-summary' ] ) ) { mw.messages.set( { 'cat-a-lot-prefix-summary': '', 'cat-a-lot-using-summary': '' } ); } else {
			mw.messages.set( {
				'cat-a-lot-pref-save-summary': msgs[ 'cat-a-lot-prefix-summary' ] + msgs[ 'cat-a-lot-pref-save-summary' ] + msgs[ 'cat-a-lot-using-summary' ]
			} );
		}

		// TODO: better extern project support for possible change-tag? (needs currently change after init)
		if ( project === 'commonswiki' ) { mw.messages.set( { 'cat-a-lot-using-summary': '' } ); } else { // Reset
			this.changeTag = '';
			this.settings.redir_category = '';
		}

		this._initSettings();
		$body = $( document.body );
		$container = $( '<div>' )
			.attr( 'id', 'cat_a_lot' )
			.appendTo( $body );
		$dataContainer = $( '<div>' )
			.attr( 'id', 'cat_a_lot_data' )
			.appendTo( $container );
		$searchInputContainer = $( '<div>' )
			.appendTo( $dataContainer );
		$searchInput = $( '<input>', {
			id: 'cat_a_lot_searchcatname',
			placeholder: msg( 'enter-name' ),
			type: 'text'
		} )
			.appendTo( $searchInputContainer );
		$resultList = $( '<div>' )
			.attr( 'id', 'cat_a_lot_category_list' )
			.appendTo( $dataContainer );
		$markCounter = $( '<div>' )
			.attr( 'id', 'cat_a_lot_mark_counter' )
			.appendTo( $dataContainer );
		$selections = $( '<div>' )
			.attr( 'id', 'cat_a_lot_selections' )
			.text( msg( 'select' ) + ':' )
			.appendTo( $dataContainer );
		$settingsWrapper = $( '<div>' )
			.attr( 'id', 'cat_a_lot_settings' )
			.appendTo( $dataContainer );
		$settingsLink = $( '<a>', {
			id: 'cat_a_lot_config_settings',
			title: 'Version ' + this.version,
			text: msg( 'config-settings' )
		} )
			.appendTo( $settingsWrapper );
		$head = $( '<div>' )
			.attr( 'id', 'cat_a_lot_head' )
			.appendTo( $container );
		$link = $( '<a>' )
			.attr( 'id', 'cat_a_lot_toggle' )
			.text( 'Cat-a-lot' )
			.appendTo( $head );
		$settingsWrapper.append( $( '<a>', {
			href: commonsURL + '?title=Special:MyLanguage/Help:Gadget-Cat-a-lot',
			target: '_blank',
			style: 'float:right',
			title: ( $( '#n-help a' ).attr( 'title' ) || '' ) + ' (v. ' + this.version + ')'
		} ).text( '?' ) );
		$container.one( 'mouseover', function () { // Try load on demand earliest as possible
			mw.loader.load( [ 'jquery.ui'] );
		} );

		if ( this.origin && !non ) {
			$overcat = $( '<a>' )
				.attr( 'id', 'cat_a_lot_overcat' )
				.html( msg( 'overcat' ) )
				.on( 'click', function ( e ) {
					CAL.getOverCat( e );
				} )
				.insertBefore( $selections );
		}

		if ( ( mw.util.getParamValue( 'withJS' ) === 'MediaWiki:Gadget-Cat-a-lot.js' &&
			!mw.util.getParamValue( 'withCSS' ) ) ||
			mw.loader.getState( 'ext.gadget.Cat-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' );
			// importStylesheet( 'MediaWiki:Gadget-Cat-a-lot.css' );
		}

		reCat = new RegExp( '^\\s*' + CAL.localizedRegex( 14, 'Category' ) + ':', '' );

		$searchInput.on( 'keypress', function ( e ) {
			if ( e.which === 13 ) {
				CAL.updateCats( $.trim( $( this ).val().replace( /[\u200E\u200F\u202A-\u202E]/g, '' ) ) );
				mw.cookie.set( 'catAlot', CAL.currentCategory );
			}
		} )
			.on( 'input keyup', function () {
				var oldVal = this.value,
					newVal = oldVal.replace( reCat, '' );
				if ( newVal !== oldVal ) { this.value = newVal; }

				if ( !newVal ) { mw.cookie.set( 'catAlot', null ); }
			} );

		function initAutocomplete() {
			if ( CAL.autoCompleteIsEnabled ) { return; }

			CAL.autoCompleteIsEnabled = true;

			if ( !$searchInput.val() && mw.cookie && mw.cookie.get( 'catAlot' ) ) { $searchInput.val( mw.cookie.get( 'catAlot' ) ); }

			$searchInput.autocomplete( {
				source: function ( request, response ) {
					CAL.doAPICall( {
						action: 'opensearch',
						search: request.term,
						redirects: 'resolve',
						namespace: 14
					}, function ( data ) {
						if ( data[ 1 ] ) {
							response( $( data[ 1 ] )
								.map( function ( index, item ) {
									return item.replace( reCat, '' );
								} ) );
						}

					} );
				},
				open: function () {
					$( '.ui-autocomplete' )
						.position( {
							my: is_rtl ? 'left bottom' : 'right bottom',
							at: is_rtl ? 'left top' : 'right top',
							of: $searchInput
						} );
				},
				appendTo: '#cat_a_lot'
			} );
		}
		$( '<a>' )
		// .attr( 'id', 'cat_a_lot_select_all' )
			.text( msg( 'all' ) )
			.on( 'click', function () {
				CAL.toggleAll( true );
			} )
			.appendTo( $selections.append( ' ' ) );
		if ( this.settings.editpages ) {
			$selectFiles = $( '<a>' )
				.on( 'click', function () {
					CAL.toggleAll( 'files' );
				} );
			$selectPages = $( '<a>' )
				.on( 'click', function () {
					CAL.toggleAll( 'pages' );
				} );
			$selections.append( $( '<span>' ).hide().append( [ ' / ', $selectFiles, ' / ', $selectPages ] ) );
		}
		$selectNone = $( '<a>' )
		// .attr( 'id', 'cat_a_lot_select_none' )
			.text( msg( 'none' ) )
			.on( 'click', function () {
				CAL.toggleAll( false );
			} );
		$selectInvert = $( '<a>' )
			.on( 'click', function () {
				CAL.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'
					} )
			] )
		] );

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

				if ( !CAL.executed ) {
					$.when( mw.loader.using( [
						'jquery.ui',
						'jquery.ui',
						'jquery.ui',
						'mediawiki.api',
						'mediawiki.jqueryMsg'
					] ), $.ready )
						.then( function () {
							return new mw.Api().loadMessagesIfMissing( [
								'Cancel',
								'Categorytree-not-found',
								// 'Checkuser-all',
								// 'Code-field-select',
								// 'Export-addcat',
								'Filerevert-submit',
								'Mobile-frontend-return-to-page',
								'Ooui-selectfile-placeholder',
								// 'Visualeditor-clipboard-copy',
								'Wikieditor-loading',
								'Prefs-files',
								'Categories',
								'Checkbox-invert',
								'Centralnotice-remove', // 'Ooui-item-remove'
								'Apifeatureusage-warnings'
							] );
						} ).then( function () {
							CAL.run();
						} );
				} else { CAL.run(); }
			} );
		$settingsLink
			.on( 'click', CAL.manageSettings );
		this.localCatName = formattedNS[ 14 ] + ':';
		mw.loader.using( 'mediawiki.cookie', function () { // Let catAlot stay open
			var val = mw.cookie.get( 'catAlotO' );
			if ( val && Number( val ) === ns ) { $link.click(); }
		}
		);
	},

	getOverCat: function ( e ) {
		var files = [];
		r = 0; // result counter
		if ( e ) {
			e.preventDefault();
			this.files = this.getMarkedLabels(); // .toArray() not working
			for ( var f = 0; f < this.files.length; f++ ) { files.push( this.files[ f ] ); }

		}
		if ( !files.length || !( files instanceof Array ) ) { return alert( mw.msg( 'Ooui-selectfile-placeholder' ) ); }
		this.files = files;
		mw.loader.using( [ 'jquery.spinner' ], function () {
			$markCounter.injectSpinner( 'overcat' );
			CAL.getFileCats();
		} );
	},

	getFileCats: function () {
		var aLen = this.files.length;
		var bLen = this.selectedLabels.length;
		var file = this.files[ aLen - 1 ][ 0 ];
		$overcat.text( '…' + aLen + '\/' + bLen );
		if ( file ) {
			this.doAPICall( {
				prop: 'categories',
				titles: file
			}, this.checkFileCats
			);
		}

	},

	checkFileCats: function ( data ) {
		var cc = 0; // current cat counter;
		var file = CAL.files.pop();
		if ( data.query && data.query.pages ) {
			$.each( data.query.pages, function ( id, page ) {
				if ( page.categories ) {
					var target = file[ 1 ].removeClass( 'cat_a_lot_selected' );
					$.each( page.categories, function ( c, cat ) {
						var title = cat.title.replace( reCat, '' ),
							color = 'orange',
							mark = function ( kind ) { // kind of category
							// TODO: store data to use this for special remove function
								if ( kind === 'sub' ) { color = 'green'; }
								var border = '3px dotted ';
								if ( $.inArray( title, CAL[ kind + 'Cats' ] ) !== -1 ) {
									cc++;
									target = target.parents( '.gallerybox' );
									target = target[ 0 ] ? target : file[ 1 ];
									target.css( {
										border: border + color
									} ).prop( 'title', msg( kind + '-cat' ) + title );
									color = 'red';
									return false;
								}
							};
						mark( 'sub' );
						return mark( 'parent' );
					} );
					if ( cc ) { r++; }
				}
			} );
		} else { mw.log( 'Api-fail', file, data ); }
		if ( CAL.files[ 0 ] ) { return setTimeout( function () { CAL.getFileCats(); }, 100 ); } // Api has bad performance here, so we can get only each file separately
		$overcat.text( msg( 'pe_file', r, CAL.selectedLabels.length ) );
		$.removeSpinner( 'overcat' );
	},

	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 'search':
				this.labels = this.labels.add( $( 'table.searchResultImage' ).find( 'tr>td:eq(1)' ) );
				if ( this.settings.editpages ) { this.labels = this.labels.add( 'div.mw-search-result-heading' ); }

				break;
			case 'category':
				this.findAllLabels( 'gallery' );
				this.labels = this.labels.add( $( '#mw-category-media' ).find( 'li[class!="gallerybox"]' ) );
				if ( this.settings.editpages ) {
					this.pageLabels = $( '#mw-pages, #mw-subcategories' ).find( 'li' );
					// this.files = this.labels;
					this.labels = this.labels.add( this.pageLabels );
				}
				break;
			case 'contribs':
				this.labels = this.labels.add( $( 'ul.mw-contributions-list li' ) );
				// FIXME: Filter if !this.settings.editpages
				break;
			case 'prefix':
				this.labels = this.labels.add( $( 'ul.mw-prefixindex-list li' ) );
				break;
			case 'listfiles':
			// this.labels = this.labels.add( $( 'table.listfiles>tbody>tr' ).find( 'td:eq(1)' ) );
				this.labels = this.labels.add( $( '.TablePager_col_img_name' ) );
				break;
			case 'gallery':
			// this.labels = this.labels.add( '.gallerybox' ); // TODO incombatible with GalleryDetails
				this.labels = this.labels.add( '.gallerytext' );
				break;
		}
	},

	getTitleFromLink: function ( $a ) {
		try {
			return decodeURIComponent( $a.attr( 'href' ) )
				.match( /wiki\/(.+?)(?:#.+)?$/ )[ 1 ].replace( /_/g, ' ' );
		} catch ( ex ) {
			return '';
		}
	},

	/**
*  @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 () {
			var label = $( this ), file = label.find( 'a[title][class$="title"]' );
			file = file.length ? file : label.find( 'a[title]' );
			var title = file.attr( 'title' ) ||
CAL.getTitleFromLink( file ) ||
CAL.getTitleFromLink( label.find( 'a' ) ) ||
CAL.getTitleFromLink( label.parent().find( 'a' ) ); // TODO needs optimization
			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 ( CAL.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 () {
			CAL.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();
	},

	getSubCats: function () {
		var data = {
			list: 'categorymembers',
			cmtype: 'subcat',
			cmlimit: this.settings.subcatcount,
			cmtitle: 'Category:' + this.currentCategory
		};

		this.doAPICall( data, function ( result ) {
			var cats = result.query.categorymembers;
			CAL.subCats = [];
			for ( var i = 0; i < cats.length; i++ ) { CAL.subCats.push( cats[ i ].title.replace( /^[^:]+:/, '' ) ); }

			CAL.catCounter++;
			if ( CAL.catCounter === 2 ) { CAL.showCategoryList(); }

		} );
	},

	getParentCats: function () {
		var data = {
			prop: 'categories',
			titles: 'Category:' + this.currentCategory
		};
		this.doAPICall( data, function ( result ) {
			CAL.parentCats = [];
			var cats,
				pages = result.query.pages,
				table = $( '<table>' );

			if ( pages[ -1 ] && pages[ -1 ].missing === '' ) {
				$resultList.html( '<span id="cat_a_lot_no_found">' + mw.msg( 'Categorytree-not-found', this.currentCategory ) + '</span>' );
				document.body.style.cursor = 'auto';
				CAL.createCatLinks( '→', [ CAL.currentCategory ], table );
				$resultList.append( table );
				return;
			}
			// there should be only one, but we don't know its ID
			for ( var id in pages ) { cats = pages[ id ].categories || []; }

			for ( var i = 0; i < cats.length; i++ ) { CAL.parentCats.push( cats[ i ].title.replace( /^[^:]+:/, '' ) ); }

			CAL.catCounter++;
			if ( CAL.catCounter === 2 ) { CAL.showCategoryList(); }

		} );
	},

	localizedRegex: function ( namespaceNumber, fallback ) {
	// Copied from HotCat, thanks Lupo.
		var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
		var wikiTextBlankRE = new RegExp( wikiTextBlank, 'g' );
		var createRegexStr = function ( name ) {
			if ( !name || !name.length ) { return ''; }

			var regexName = '';
			for ( var i = 0; i < name.length; i++ ) {
				var ii = name[ i ];
				var ll = ii.toLowerCase();
				var ul = ii.toUpperCase();
				regexName += ( ll === ul ) ? ii : '[' + ll + ul + ']';
			}
			return regexName.replace( /([\\\^\$\.\?\*\+\(\)])/g, '\\$1' )
				.replace( wikiTextBlankRE, wikiTextBlank );
		};

		fallback = fallback.toLowerCase();
		var canonical = formattedNS[ namespaceNumber ].toLowerCase();
		var RegexString = createRegexStr( canonical );
		if ( fallback && canonical !== fallback ) { RegexString += '|' + createRegexStr( fallback ); }

		for ( var catName in nsIDs ) { if ( typeof catName === 'string' && catName.toLowerCase() !== canonical && catName.toLowerCase() !== fallback && nsIDs[ catName ] === namespaceNumber ) { RegexString += '|' + createRegexStr( catName ); } }

		return ( '(?:' + RegexString + ')' );
	},

	regexCatBuilder: function ( category ) {
		var catname = this.localizedRegex( 14, 'Category' );

		// Build a regexp string for matching the given category:
		// trim leading/trailing whitespace and underscores
		category = category.replace( /^[\s_]+|[\s_]+$/g, '' );

		// escape regexp metacharacters (= any ASCII punctuation except _)
		category = mw.RegExp.escape( category );

		// any sequence of spaces and underscores should match any other
		category = category.replace( /[\s_]+/g, '[\\s_]+' );

		// Make the first character case-insensitive:
		var first = category.substr( 0, 1 );
		if ( first.toUpperCase() !== first.toLowerCase() ) { category = '[' + first.toUpperCase() + first.toLowerCase() + ']' + category.substr( 1 ); }

		// Compile it into a RegExp that matches MediaWiki category syntax (yeah, it looks ugly):
		// XXX: the first capturing parens are assumed to match the sortkey, if present, including the | but excluding the ]]
		return new RegExp( '\\[\\[[\\s_]*' + catname + '[\\s_]*:[\\s_]*' + category + '[\\s_]*(\\|[^\\]]*(?:\\][^\\]]+)*)?\\]\\]\\s*', 'g' );
	},

	getContent: function ( page, targetcat, mode ) {
		if ( !this.cancelled ) {
			this.doAPICall( {
				curtimestamp: 1,
				// meta: 'tokens',
				prop: 'revisions',
				rvprop: 'content|timestamp',
				titles: page[ 0 ]
			}, function ( result ) {
				CAL.editCategories( result, page, targetcat, mode );
			} );
		}

	},

	getTargetCat: function ( pages, targetcat, mode ) {
		if ( !this.cancelled ) {
			this.doAPICall( {
				meta: 'tokens',
				prop: 'categories|categoryinfo',
				titles: 'Category:' + targetcat
			}, function ( result ) {
				if ( !result || !result.query ) { return; }
				CAL.edittoken = result.query.tokens.csrftoken;
				result = CAL._getPageQuery( result );
				CAL.checkTargetCat( result );
				for ( var i = 0; i < pages.length; i++ ) { CAL.getContent( pages[ i ], targetcat, mode ); }

			} );
		}

	},

	checkTargetCat: function ( page ) {
		var is_dab = false; // disambiguation
		var is_redir = typeof page.redirect === 'string'; // Hard redirect?
		if ( typeof page.missing === 'string' ) { return alert( mw.msg( 'Apifeatureusage-warnings', mw.msg( 'Categorytree-not-found', page.title ) ) ); }
		var cats = page.categories;
		this.is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden === 'string';

		if ( !is_redir && cats && ( CAL.disambig_category || CAL.redir_category ) ) {
			for ( var c = 0; c < cats.length; c++ ) {
				var cat = cats[ c ].title;
				if ( cat ) { // Strip namespace prefix
					cat = cat.substring( cat.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
					if ( cat === CAL.disambig_category ) {
						is_dab = true; break;
					} else if ( cat === CAL.redir_category ) {
						is_redir = true; break;
					}
				}
			}
		}

		if ( !is_redir && !is_dab ) { return; }
		alert( mw.msg( 'Apifeatureusage-warnings', page.title + ' is a ' + CAL.disambig_category ) );
	},

	// Remove {{Uncategorized}} (also with comment). No need to replace it with anything.
	removeUncat: function ( text ) {
		return ( this.settings.uncat ? text.replace( /\{\{\s*[Uu]ncategorized\s*[^}]*\}\}\s*(<!--.*?-->\s*)?/, '' ) : text );
	},

	doCleanup: function ( text ) {
		return ( this.settings.docleanup ? text.replace( /\{\{\s*[Cc]heck categories\s*(\|?.*?)\}\}/, '' ) : text );
	},

	editCategories: function ( result, file, targetcat, mode ) {
		if ( !result || !result.query ) {
		// Happens on unstable wifi connections..
			this.connectionError.push( file[ 0 ] );
			this.updateCounter();
			return;
		}
		var otext,
			timestamp,
			page = CAL._getPageQuery( result );
		if ( page.ns === 2 ) { return; }
		var id = page.revisions[ 0 ],
			catNS = this.localCatName; // canonical cat-name

		this.starttimestamp = result.curtimestamp;
		otext = id[ '*' ];
		timestamp = id.timestamp;

		var sourcecat = this.origin;
		// Check if that file is already in that category
		if ( mode !== 'remove' && this.regexCatBuilder( targetcat ).test( otext ) ) {
			// If the new cat is already there, just remove the old one
			if ( mode === 'move' ) {
				mode = 'remove';
				targetcat = sourcecat;
			} else {
				this.alreadyThere.push( file[ 0 ] );
				this.updateCounter();
				return;
			}
		}

		// Text modification (following 3 functions are partialy taken from HotCat)
		var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
		// Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
		// a link must be on one single line.
		// MediaWiki also removes Unicode bidi override characters in page titles (and namespace names) completely.
		// This is *not* handled, as it would require us to allow any of [\u200E\u200F\u202A-\u202E] between any two
		// characters inside a category link. It _could_ be done though... We _do_ handle strange spaces, including the
		// zero-width space \u200B, and bidi overrides between the components of a category link (adjacent to the colon,
		// or adjacent to and inside of "[[" and "]]").
		var findCatsRE = new RegExp( '\\[\\[' + wikiTextBlankOrBidi + this.localizedRegex( 14, 'Category' ) + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g' );

		function replaceByBlanks( match ) {
			return match.replace( /(\s|\S)/g, ' ' ); // /./ doesn't match linebreaks. /(\s|\S)/ does.
		}

		function find_insertionpoint( wikitext ) {
			var copiedtext = wikitext
				.replace( /<!--(\s|\S)*?-->/g, replaceByBlanks )
				.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, replaceByBlanks );
			// Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
			var index = -1;
			findCatsRE.lastIndex = 0;
			while ( findCatsRE.exec( copiedtext ) !== null ) { index = findCatsRE.lastIndex; }

			return index;
		}

		/**
*  @brief Adds the new Category by searching the right insert point,
*         if there is text after the category section
*  @param [string] wikitext
*  @param [string] toAdd
*  @return Return wikitext
*/
		function addCategory( wikitext, toAdd ) {
			if ( toAdd && toAdd[ 0 ] ) {
			// TODO: support sort key
				var cat_point = find_insertionpoint( wikitext ); // Position of last category
				var newcatstring = '[[' + catNS + toAdd + ']]';
				if ( cat_point > -1 ) {
					var suffix = wikitext.substring( cat_point );
					wikitext = wikitext.substring( 0, cat_point ) + ( cat_point ? '\n' : '' ) + newcatstring;
					if ( suffix[ 0 ] && suffix.substr( 0, 1 ) !== '\n' ) { wikitext += '\n'; }
					wikitext += suffix;
				} else {
					if ( wikitext[ 0 ] && wikitext.substr( wikitext.length - 1, 1 ) !== '\n' ) { wikitext += '\n'; }

					wikitext += ( wikitext[ 0 ] ? '\n' : '' ) + newcatstring;
				}
			}
			return wikitext;
		}
		// End HotCat functions

		var text = otext,
			arr = is_rtl ? '\u2190' : '\u2192', // left and right arrows. Don't use ← and → in the code.
			sumCmt, // summary comment
			sumCmtShort;
		// Fix text
		switch ( mode ) {
			case 'add':
				text = addCategory( text, targetcat );
				sumCmt = msg( 'summary-add' ).replace( '$1', targetcat );
				sumCmtShort = '+[[' + catNS + targetcat + ']]';
				break;
			case 'copy':
				text = text.replace( this.regexCatBuilder( sourcecat ), '[[' + catNS + sourcecat + '$1]]\n[[' + catNS + targetcat + '$1]]\n' );
				sumCmt = msg( 'summary-copy' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
				sumCmtShort = '+[[' + catNS + sourcecat + ']]' + arr + '[[' + catNS + targetcat + ']]';
				// If category is added through template:
				if ( otext === text ) { text = addCategory( text, targetcat ); }

				break;
			case 'move':
				text = text.replace( this.regexCatBuilder( sourcecat ), '[[' + catNS + targetcat + '$1]]\n' );
				sumCmt = msg( 'summary-move' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
				sumCmtShort = '±[[' + catNS + sourcecat + ']]' + arr + '[[' + catNS + targetcat + ']]';
				break;
			case 'remove':
				text = text.replace( this.regexCatBuilder( targetcat ), '' );
				sumCmt = msg( 'summary-remove' ).replace( '$1', targetcat );
				sumCmtShort = '-[[' + catNS + targetcat + ']]';
				break;
		}

		if ( text === otext ) {
			this.notFound.push( file[ 0 ] );
			this.updateCounter();
			return;
		}
		otext = text;

		// Remove {{uncat}} after we checked whether we changed the text successfully.
		// Otherwise we might fail to do the changes, but still replace {{uncat}}
		if ( mode !== 'remove' && ( !non || userGrp.indexOf( 'autoconfirmed' ) > -1 ) ) {
			if ( !this.is_hidden ) {
				text = this.removeUncat( text );
				if ( text.length !== otext.length ) { sumCmt += '; ' + msg( 'uncatpref' ); }
			}
			text = this.doCleanup( text );
		}

		sumCmt += this.summary ? ' ' + this.summary : '';

		var preM = msg( 'prefix-summary' );
		var usgM = msg( 'using-summary' );
		// Try shorten summary
		if ( preM || usgM )	{
			sumCmt = ( sumCmt.length > 250 - preM.length - usgM.length ) ?
				sumCmt + ' (CatAlot)' : preM + sumCmt + usgM;
		}

		if ( sumCmt.length > 254 ) // Try short summary
		{ sumCmt = sumCmtShort; }

		var data = {
			action: 'edit',
			assert: 'user',
			summary: sumCmt,
			title: file[ 0 ],
			text: text,
			bot: true,
			starttimestamp: this.starttimestamp,
			basetimestamp: timestamp,
			watchlist: this.settings.watchlist,
			minor: this.settings.minor,
			tags: this.changeTag,
			token: this.edittoken
		};

		this.doAPICall( data, function ( r ) {
			delete CAL.XHR[ file[ 0 ] ];
			return CAL.updateUndoCounter( r );
		} );
		this.markAsDone( file[ 1 ], mode, targetcat );
	},

	markAsDone: function ( label, mode, targetcat ) {
		mode = ( function ( m ) {
			switch ( m ) {
				case 'add': return 'added-cat';
				case 'copy': return 'copied-cat';
				case 'move': return 'moved-cat';
				case 'remove': return 'removed-cat';
			}
		}( mode ) );
		label.addClass( 'cat_a_lot_markAsDone' ).append( '<br>' + msg( mode, targetcat ) );
	},

	updateUndoCounter: function ( r ) {
		this.updateCounter();
		if ( !r.edit || r.edit.result !== 'Success' ) { return; }
		r = r.edit;

		this.undoList.push( {
			title: r.title,
			id: r.newrevid,
			timestamp: r.newtimestamp
		} );
	},

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

	displayResult: function () {
		document.body.style.cursor = 'auto';
		$.removeSpinner( 'fb-dialog' );
		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.alreadyThere.length ) {
			rep.append( '<h5>' + msg( 'skipped-already', this.alreadyThere.length ) + '</h5>' )
				.append( this.alreadyThere.join( '<br>' ) );
		}

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

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

	},

	/**
*  @brief set parameters for API call,
*  	convert targetcat to string, get selected pages/files
*  @param [dom object] targetcat with data
*  @param [string] mode action
*  @return Return API call getTargetCat with pages
*/
	doSomething: function ( targetcat, mode ) {
		var pages = this.getMarkedLabels();
		if ( !pages.length ) { return alert( mw.msg( 'Ooui-selectfile-placeholder' ) ); }
		targetcat = $( targetcat ).closest( 'tr' ).data( 'cat' );

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

		if ( $( '#cat_a_lot_comment' ).prop( 'checked' ) ) { this.summary = window.prompt( msg( 'edit-question' ), '' ); } // TODO custom pre-value
		if ( this.summary !== null ) {
			mw.loader.using( [ 'jquery.ui', 'jquery.spinner', 'mediawiki.util' ], function () {
				CAL.showProgress();
				CAL.getTargetCat( pages, targetcat, mode );
			} );
		}

	},

	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 === 'edit' && !CAL.cancelled ) { CAL.XHR[ params.title ] = xhr; }
		};
		doCall();
	},

	createCatLinks: function ( symbol, list, table ) {
		list.sort();
		var button = ( this.settings.button && mw.loader.getState( 'jquery.ui' ) === 'ready' ) ? 1 : 0;
		for ( var c = 0; c < list.length; c++ ) {
			var $tr = $( '<tr>' ),
				$link = $( '<a>', {
					href: mw.util.getUrl( CAL.localCatName + list[ c ] ),
					text: list[ c ]
				} ),
				$buttons = [];
			$tr.data( 'cat', list[ c ] );
			$link.on( 'click', function ( e ) {
				if ( !e.ctrlKey ) {
					e.preventDefault();
					CAL.updateCats( $( this ).closest( 'tr' ).data( 'cat' ) );
				}
			} );

			$tr.append( $( '<td>' ).text( symbol ) )
				.append( $( '<td>' ).append( $link ) );

			$buttons.push( $( '<a>' )
				.text( mw.msg( 'Centralnotice-remove' ) )
				.on( 'click', function () {
					CAL.doSomething( this, 'remove' );
				} )
				.addClass( 'cat_a_lot_move' )
			);
			if ( button ) {
				$buttons.slice( -1 )[ 0 ].button( {
					icons: { primary: 'ui-icon-minusthick' },
					showLabel: false,
					text: false
				} );
			}

			if ( this.origin ) {
			// Can't move to source category
				if ( list[ c ] !== this.origin ) {
					$buttons.push( $( '<a>' )
						.text( msg( 'move' ) )
						.on( 'click', function () {
							CAL.doSomething( this, 'move' );
						} )
						.addClass( 'cat_a_lot_move' )
					);
					if ( button ) {
						$buttons.slice( -1 )[ 0 ].button( {
							icons: { primary: 'ui-icon-arrowthick-1-e' },
							showLabel: false,
							text: false
						} );
					}

					$buttons.push( $( '<a>' )
						.text( msg( 'copy' ) )
						.on( 'click', function () {
							CAL.doSomething( this, 'copy' );
						} )
						.addClass( 'cat_a_lot_action' )
					);
					if ( button ) {
						$buttons.slice( -1 )[ 0 ].button( {
							icons: { primary: 'ui-icon-plusthick' },
							showLabel: false,
							text: false
						} );
					}

				}
			} else {
				$buttons.push( $( '<a>' )
					.text( msg( 'add' ) )
					.on( 'click', function () {
						CAL.doSomething( this, 'add' );
					} )
					.addClass( 'cat_a_lot_action' )
				);
				if ( button ) {
					$buttons.slice( -1 )[ 0 ].button( {
						icons: { primary: 'ui-icon-plusthick' },
						showLabel: false,
						text: false
					} );
				}

			}
			// TODO CSS may extern
			var css = button ? { fontSize: '.6em', margin: '0', width: '2.5em' } : {};
			for ( var b = 0; b < $buttons.length; b++ ) { $tr.append( $( '<td>' ).append( $buttons[ b ].css( css ) ) ); }

			table.append( $tr );
		}
	},

	getCategoryList: function () {
		this.catCounter = 0;
		this.getParentCats();
		this.getSubCats();
	},

	_getPageQuery: function ( data ) {
	// There should be only one, but we don't know its ID
		if ( data && data.query && data.query.pages ) {
			data = data.query.pages;
			for ( var p in data ) { return data[ p ]; }
		}
	},

	/**
*  @brief takes this.currentCategory if redir_category is configured
** Cat pages with more than one cat link are still not supported for sure
*  @return soft redirected cat
*/
	solveSoftRedirect: function () {
		this.doAPICall( {
			prop: 'links', // TODO: For more accuracy the revisions could be checked
			titles: 'Category:' + this.currentCategory,
			// 'rvprop': 'content',
			// 'pllimit': 'max',
			plnamespace: 14
		}, function ( page ) {
			page = CAL._getPageQuery( page );
			if ( page ) {
				var lks = page.links;
				if ( lks && lks.length === 1 && lks[ 0 ].title ) {
					CAL.currentCategory = lks[ 0 ].title.replace( reCat, '' );
					$searchInput.val( CAL.currentCategory );
					return CAL.getCategoryList();
				} else {
				// TODO? better translatable warning message: "Please solve the category soft redirect manually!"
					$resultList.html( '<span id="cat_a_lot_no_found">' + mw.msg( 'Apifeatureusage-warnings', mw.msg( 'Categorytree-not-found', CAL.currentCategory ) ) + '</span>' );
				}
			}
		} );
	},

	showCategoryList: function () {
		if ( this.settings.redir_category && this.settings.redir_category === this.parentCats[ 0 ] ) { return this.solveSoftRedirect(); }

		var table = $( '<table>' );

		this.createCatLinks( '↑', this.parentCats, table );
		this.createCatLinks( '→', [ this.currentCategory ], table );
		// Show on soft-redirect
		if ( $searchInput.val() === this.currentCategory && this.origin !== this.currentCategory ) { this.createCatLinks( '→', [ this.origin ], table ); }
		this.createCatLinks( '↓', this.subCats, table );

		$resultList.empty();
		$resultList.append( table );

		document.body.style.cursor = 'auto';

		// Reset width
		$container.width( '' );
		$container.height( '' );
		$container.width( Math.min( table.width() * 1.1 + 15, $( window ).width() - 10 ) );

		$resultList.css( {
			maxHeight: Math.min( this.setHeight, $( window ).height() - $container.position().top - $settingsLink.outerHeight() - $selections.outerHeight() - 15 ),
			height: ''
		} );
		table.width( '100%' );
		$container.height( Math.min( $container.height(), $head.offset().top - $container.offset().top + 10 ) );
		$container.offset( { left: $( window ).width() - $container.outerWidth() } ); // Fix overlap
	},

	updateCats: function ( newcat ) {
		document.body.style.cursor = 'wait';
		this.currentCategory = newcat;
		$resultList.html( '<div class="cat_a_lot_loading">' + mw.msg( 'Wikieditor-loading' ) + '</div>' );
		this.getCategoryList();
	},

	doUndo: function () {
		this.cancelled = 0;
		this.doAbort();
		if ( !this.undoList.length ) { return; }

		$( '.cat_a_lot_feedback' ).removeClass( 'cat_a_lot_done' );
		this.counterNeeded = this.undoList.length;
		this.counterCurrent = 1;

		document.body.style.cursor = 'wait';

		var query = {
			action: 'edit',
			user: mw.config.get( 'wgUserName' ),
			bot: true,
			minor: this.settings.minor,
			starttimestamp: this.starttimestamp,
			watchlist: this.settings.watchlist,
			tags: this.changeTag,
			token: this.edittoken
		};
		for ( var i = 0; i < this.undoList.length; i++ ) {
			var uID = this.undoList[ i ];
			query.title = uID.title;
			query.undo = uID.id;
			query.basetimestamp = uID.timestamp;
			this.doAPICall( query, function ( r ) {
			// TODO: Add "details" to progressbar?
			// $resultList.append( [mw.msg('Filerevert-submit') + " done " + r.edit.title, '<br>' ] );
				if ( r && r.edit ) { mw.log( 'Revert done', r.edit.title ); }
				CAL.updateCounter();
			} );
		}
	},

	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">' + CAL.counterCurrent + '</span> ' + msg( 'of' ) + CAL.counterNeeded )
			.prepend( $.createSpinner( { id: 'fb-dialog', size: 'large' } ) )
			.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 () {
					CAL.cancelled = 1;
					CAL.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%'
				} )
				.append( [ '[ ',
					$( '<a>', { title: 'Revert all last done edits' } ) // TODO i18n
						.on( 'click', function () {
							if ( window.confirm( mw.msg( 'Apifeatureusage-warnings', this.title + '⁉' ) ) ) {
								CAL.doUndo();
								$( this ).parent().remove();
							}
							return false;
						} )
						.addClass( 'new' )
						.text( mw.msg( 'Filerevert-submit' ) ),
					' ]'
				] ).insertAfter( $link );
		}

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

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

	maximize: function ( e ) {
		$dataContainer.show();
		$container.animate( {
			top: CAL.top,
			height: CAL.height
		}, function () {
			$( e.target ).one( 'click', CAL.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 )
				);
			}
			$dataContainer.show();
			$container.one( 'mouseover', function () {
				$( this )
					.resizable( {
						handles: 'n',
						alsoResize: '#cat_a_lot_category_list',
						resize: function () {
							$resultList
								.css( {
									maxHeight: '',
									width: ''
								} );
						},
						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'
							} );
						},
						stop: function () {
							CAL.setHeight = $resultList.height();
						}
					} )
					.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
					} );
				$resultList
					.css( { maxHeight: 450 } );
			} );
			this.updateCats( this.origin || 'Images' );

			$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( 'Cat-a-lot' )
				.nextAll().hide();
			this.executed = 1;
			mw.cookie.set( 'catAlotO', null );
		}
	},

	manageSettings: function () {
		mw.loader.using( [ 'ext.gadget.SettingsManager', 'ext.gadget.SettingsUI', 'jquery.ui' ], CAL._manageSettings );
	},

	_manageSettings: function () {
		mw.libs.SettingsUI( CAL.defaults, 'Cat-a-lot' )
			.show()
			.done( function ( s, verbose, loc, settingsOut, $dlg ) {
				var mustRestart = false,
					_restart = function () {
						if ( !mustRestart ) { return; }
						$container.remove();
						CAL.labels.off( 'click.catALot' );
						CAL.init();
					},
					_saveToJS = function () {
						var opt = mw.libs.settingsManager.option( {
								optionName: 'catALotPrefs',
								value: CAL.settings,
								encloseSignature: 'catALot',
								encloseBlock: '////////// Cat-a-lot user preferences //////////\n',
								triggerSaveAt: /Cat.?A.?Lot/i,
								editSummary: msg( 'pref-save-summary' )
							} ),
							oldHeight = $dlg.height(),
							$prog = $( '<div>' );

						$dlg.css( 'height', oldHeight )
							.html( '' );
						$prog.css( {
							height: Math.round( oldHeight / 8 ),
							'margin-top': Math.round( ( 7 * oldHeight ) / 16 )
						} )
							.appendTo( $dlg );

						$dlg.parent()
							.find( '.ui-dialog-buttonpane button' )
							.button( 'option', 'disabled', true );

						opt.save()
							.done( function ( text, progress ) {
								$prog.progressbar( {
									value: progress
								} );
								$prog.fadeOut( function () {
									$dlg.dialog( 'close' );
									_restart();
								} );
							} )
							.progress( function ( text, progress ) {
								$prog.progressbar( {
									value: progress
								} );
							// TODO: Add "details" to progressbar
							} )
							.fail( function ( text ) {
								$prog.addClass( 'ui-state-error' );
								$dlg.prepend( $( '<p>' )
									.text( text ) );
							} );
					};
				$.each( settingsOut, function ( n, v ) {
					if ( v.forcerestart && CAL.settings[ v.name ] !== v.value ) { mustRestart = true; }
					CAL.settings[ v.name ] = CAL.catALotPrefs[ v.name ] = v.value;
				} );
				switch ( loc ) {
					case 'page':
						$dlg.dialog( 'close' );
						_restart();
						break;
					case 'account-publicly':
						_saveToJS();
						break;
				}
			} );
	},

	_initSettings: function () {
		if ( this.settings.watchlist ) { return; }
		this.catALotPrefs = window.catALotPrefs || {};
		for ( var i = 0; i < this.defaults.length; i++ ) {
			var v = this.defaults[ i ];
			v.value = this.settings[ v.name ] = ( this.catALotPrefs[ v.name ] || v['default'] );
			v.label = msg( v.label_i18n );
			if ( v.select_i18n ) {
				v.select = {};
				$.each( v.select_i18n, function ( i18nk, val ) {
					v.select[ msg( i18nk ) ] = val;
				} );
			}
		}
	},
	/* eslint-disable camelcase */
	defaults: [ {
		name: 'watchlist',
		'default': 'preferences',
		label_i18n: 'watchlistpref',
		select_i18n: {
			watch_pref: 'preferences',
			watch_nochange: 'nochange',
			watch_watch: 'watch',
			watch_unwatch: 'unwatch'
		}
	}, {
		name: 'minor',
		'default': false,
		label_i18n: 'minorpref'
	}, {
		name: 'editpages',
		'default': project !== 'commonswiki', // on Commons false
		label_i18n: 'editpagespref',
		forcerestart: true
	}, {
		name: 'docleanup',
		'default': false,
		label_i18n: 'docleanuppref'
	}, {
		name: 'subcatcount',
		'default': 50,
		min: 5,
		max: 500,
		label_i18n: 'subcatcountpref',
		forcerestart: true
	}, {
		name: 'uncat',
		'default': project === 'commonswiki', // on Commons true
		label_i18n: 'uncatpref'
	}, {
		name: 'button',
		'default': true,
		label_i18n: 'buttonpref'
	} ]
/* eslint-enable camelcase */
};

// 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 14:
			CAL.searchmode = 'category';
			CAL.origin = mw.config.get( 'wgTitle' );
			break;
		case -1:
			CAL.searchmode = {
			// list of accepted special page names mapped to search mode names
				Contributions: 'contribs',
				Listfiles: non ? null : 'listfiles',
				Prefixindex: non ? null : 'prefix',
				Search: 'search',
				Uncategorizedimages: 'gallery'
			}[ mw.config.get( 'wgCanonicalSpecialPageName' ) ];
			break;
		case 2:
		case 0:
			CAL.searchmode = 'gallery';
			var parents = $( '#mw-normal-catlinks ul' ).find( 'a[title]' ), n;
			parents.each( function ( i ) {
				if ( new RegExp( mw.config.get( 'wgTitle' ), 'i' ).test( $( this ).text() ) ) {
					n = i;
					return false;
				}
			} );
			CAL.origin = parents.eq( n || 0 ).text();
	}

	if ( CAL.searchmode ) {
		var loadingLocalizations = 1;
		var loadLocalization = function ( lang, cb ) {
			loadingLocalizations++;
			switch ( lang ) {
				case 'zh-hk':
				case 'zh-mo':
				case 'zh-tw':
					lang = 'zh-hant';
					break;
				case 'zh':
				case 'zh-cn':
				case 'zh-my':
				case 'zh-sg':
					lang = 'zh-hans';
					break;
			}

			$.ajax( {
				url: commonsURL,
				dataType: 'script',
				data: {
					title: 'MediaWiki:Gadget-Cat-a-lot.js/' + lang,
					action: 'raw',
					ctype: 'text/javascript',
					// Allow caching for 28 days
					maxage: 2419200,
					smaxage: 2419200
				},
				cache: true,
				success: cb,
				error: cb
			} );
		};
		var maybeLaunch = function () {
			loadingLocalizations--;
			function init() {
				$( function () {
					CAL.init();
				} );
			}
			if ( !loadingLocalizations ) { mw.loader.using( [ 'user' ], init, init ); }
		};

		var userlang = mw.config.get( 'wgUserLanguage' ),
			contlang = mw.config.get( 'wgContentLanguage' );
		if ( userlang !== 'en' ) { loadLocalization( userlang, maybeLaunch ); }
		if ( $.inArray( contlang, [ 'en', userlang ] ) === -1 ) { loadLocalization( contlang, maybeLaunch ); }
		maybeLaunch();
	}
}, 400 );

/**
*  Derivative work of
*  (replace "checkboxes" with cat-a-lot labels in your mind)
**
* jQuery checkboxShiftClick
*
* This will enable checkboxes to be checked or unchecked in a row by clicking one, holding shift and clicking another one
*
* @author Krinkle <krinklemail@gmail.com>
* @license GPL v2
*/
$.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>