האיגוד הישראלי לרפואת משפחה

הבדלים בין גרסאות בדף "מדיה ויקי:Gadget-Cat-a-lot.js"

מתוך ויקירפואה

(updated version)
 
שורה 1: שורה 1:
// <nowiki>
+
/**
/*
+
* Cat-a-lot
HotCat V2.35
+
* Changes category of multiple files
 +
*
 +
* Originally by Magnus Manske
 +
* RegExes by Ilmari Karonen
 +
* Completely rewritten by DieBuche
 +
*
 +
* 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>
 +
*/
  
Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
+
/* global jQuery, mediaWiki, importStylesheet */
Supports multiple category changes, as well as redirect and disambiguation resolution. Also
+
/* eslint one-var: 0, vars-on-top: 0, no-underscore-dangle:0 */ // extends: wikimedia
plugs into the upload form. Search engines to use for the suggestion list are configurable, and
+
/* jshint unused:true, forin:false, smarttabs:true, loopfunc:true, browser:true */
can be selected interactively.
 
  
Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat
+
( function( $, mw ) {
List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Version_history
+
'use strict';
  
License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
+
var NS_CAT = 14,
 +
formattedNS = mw.config.get( 'wgFormattedNamespaces' ),
 +
nsIDs = mw.config.get( 'wgNamespaceIds' );
  
Choose whichever license of these you like best :-)
+
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-subcatcountpref': 'Sub-categories to show at most',
 +
'cat-a-lot-config-settings': 'Preferences',
  
/*
+
// Progress
This code should run on any MediaWiki installation >= MW 1.27.
+
'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!',
 +
'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.',
  
For use with older versions of MediaWiki, use the archived versions below:
+
// as in 17 files selected
 +
'cat-a-lot-files-selected': '{{PLURAL:$1|1=One file|$1 files}} selected.',
  
<=1.26: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-HotCat.js&oldid=211134664
+
// Actions
*/
+
'cat-a-lot-copy': 'Copy',
/* eslint-disable */ // This old code uses too many coding conventions incompatible with eslint.
+
'cat-a-lot-move': 'Move',
(function ($, mw) {
+
'cat-a-lot-add': 'Add',
// Don't use mw.config.get() as that takes a copy of the config, and so doesn't
+
'cat-a-lot-remove-from-cat': 'Remove from this category',
// account for values changing, e.g. wgCurRevisionId after a VE edit
+
'cat-a-lot-enter-name': 'Enter category name',
var conf = mw.config.values;
+
'cat-a-lot-select': 'Select',
 +
'cat-a-lot-all': 'all',
 +
'cat-a-lot-none': 'none',
 +
'cat-a-lot-none-selected': 'No files selected!',
  
if (
+
// Summaries:
// Guard against double inclusions (in old IE/Opera element ids become window properties)
+
'cat-a-lot-pref-save-summary': '[[c:Help:Gadget-Cat-a-lot|Cat-a-lot]]: updating user preferences',
(window.HotCat && !window.HotCat.nodeName)
+
'cat-a-lot-summary-add': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Adding [[Category:$1]]',
// Not on edit pages
+
'cat-a-lot-summary-copy': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Copying from [[Category:$1]] to [[Category:$2]]',
|| conf.wgAction == 'edit'
+
'cat-a-lot-summary-move': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Moving from [[Category:$1]] to [[Category:$2]]',
 +
'cat-a-lot-summary-remove': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Removing from [[Category:$1]]'
 +
};
 +
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 $body, $container, $dataContainer, $searchInputContainer, $searchInput, $resultList, $markCounter,
 +
$selections, $selectAll, $selectNone, $settingsWrapper, $settingsLink, $head, $link;
 +
var commons_url = 'https://commons.wikimedia.org/w/index.php';
  
) {
+
var catALot = window.catALot = {
return;
+
apiUrl: mw.util.wikiScript( 'api' ),
}
+
origin: false,
 +
searchmode: false,
 +
version: 3.7,
 +
setHeight: 450,
 +
settings: {},
 +
init: function() {
 +
this._initSettings();
  
// Configuration stuff.
+
$body = $( document.body );
window.HotCat = {
+
$container = $( '<div>' )
// Localize these messages to the main language of your wiki.
+
.attr( 'id', 'cat_a_lot' )
messages :
+
.appendTo( $body );
{cat_removed  : 'removed [[Category:$1]]'
+
$dataContainer = $( '<div>' )
,template_removed  : 'removed {{[[Category:$1]]}}'
+
.attr( 'id', 'cat_a_lot_data' )
,cat_added    : 'added [[Category:$1]]'
+
.appendTo( $container );
,cat_keychange: 'new key for [[Category:$1]]: "$2"' // $2 is the new key
+
$searchInputContainer = $( '<div>' )
,cat_notFound : 'Category "$1" not found'
+
.appendTo( $dataContainer );
,cat_exists  : 'Category "$1" already exists; not added.'
+
$searchInput = $( '<input>' )
,cat_resolved : ' (redirect [[Category:$1]] resolved)'
+
.attr({
,uncat_removed: 'removed {{uncategorized}}'
+
id: 'cat_a_lot_searchcatname',
,separator    : '; '
+
placeholder: msgPlain( 'enter-name' ),
,prefix      : ""
+
type: 'text'
// Some text to prefix to the edit summary.
+
})
,using        : ' using [[Help:Gadget-HotCat|HotCat]]'
+
.appendTo( $searchInputContainer );
// Some text to append to the edit summary. Named 'using' for historical reasons. If you prefer
+
$resultList = $( '<div>' )
// to have a marker at the front, use prefix and set this to the empty string.
+
.attr( 'id', 'cat_a_lot_category_list' )
,multi_change : '$1 categories'
+
.appendTo( $dataContainer );
// $1 is replaced by a number. If your language has several plural forms (c.f. [[:en:Dual (grammatical form)]]),
+
$markCounter = $( '<div>' )
// you can set this to an array of strings suitable for passing to mw.language.configPlural().
+
.attr( 'id', 'cat_a_lot_mark_counter' )
// If that function doesn't exist, HotCat will simply fall back to using the last
+
.appendTo( $dataContainer );
// entry in the array.
+
$selections = $( '<div>' )
,commit      : 'Save'
+
.attr( 'id', 'cat_a_lot_selections' )
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
+
.text( msgPlain( 'select' ) )
// see localization hook below.
+
.appendTo( $dataContainer );
,ok          : 'OK'
+
$selectAll = $( '<a>' )
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
+
.attr( 'id', 'cat_a_lot_select_all' )
// see localization hook below.
+
.text( msgPlain( 'all' ) )
,cancel      : 'Cancel'
+
.appendTo( $selections.append( ' ' ) );
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
+
$selectNone = $( '<a>' )
// see localization hook below.
+
.attr( 'id', 'cat_a_lot_select_none' )
,multi_error  : 'Could not retrieve the page text from the server. Therefore, your category changes '
+
.text( msgPlain( 'none' ) )
+'cannot be saved. We apologize for the inconvenience.'
+
.appendTo( $selections.append( ' ' ) );
// Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
+
$settingsWrapper = $( '<div>' )
// see localization hook below.
+
.attr( 'id', 'cat_a_lot_settings' )
,short_catchange : null
+
.appendTo( $dataContainer );
// Defaults to '[[' + category_canonical + ':$1]]'. Can be overridden if in the short edit summaries
+
$settingsLink = $( '<a>' )
// not the standard category name should be used but, say, a shorter namespace alias. $1 is replaced
+
.attr( 'id', 'cat_a_lot_config_settings' )
// by a category name.
+
.text( msgPlain( 'config-settings' ) )
}
+
.appendTo( $settingsWrapper );
,categories        : 'Categories'
+
$head = $( '<div>' )
// Plural of category_canonical.
+
.attr( 'id', 'cat_a_lot_head' )
,disambig_category  : 'Disambiguation'
+
.appendTo( $container );
// Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
+
$link = $( '<a>' )
// any items, but that contains links to other categories where stuff should be categorized. If you don't have
+
.attr( 'id', 'cat_a_lot_toggle' )
// that concept on your wiki, set it to null. Use blanks, not underscores.
+
.text( 'Cat-a-lot' )
,redir_category    : 'Category redirects'
+
.appendTo( $head );
// Any category in this category is deemed a (soft) redirect to some other category defined by a link
+
$settingsWrapper.append( $( '<a>' )
// to another non-blacklisted category. If your wiki doesn't have soft category redirects, set this to null.
+
.attr( {
    // If a soft-redirected category contains more than one link to another non-blacklisted category, it's considered
+
href: commons_url + '?title=Special:MyLanguage/Help:Gadget-Cat-a-lot',
    // a disambiguation category instead.
+
target: '_blank',
,links : {change: '(±)', remove: '(\u2212)', add: '(+)', restore: ')', undo: '(×)', down: '(\u2193)', up: '(\u2191)'}
+
style: 'float:right',
// The little modification links displayed after category names. U+2212 is a minus sign; U+2193 and U+2191 are
+
title: $( '#n-help' ).attr( 'title' )
// downward and upward pointing arrows. Do not use ↓ and ↑ in the code!
+
} ).text( '?' ) );
,tooltips : {
 
change:  'Modify'
 
,remove:  'Remove'
 
,add:    'Add a new category'
 
,restore: 'Undo changes'
 
,undo:    'Undo changes'
 
,down:    'Open for modifying and display subcategories'
 
,up:      'Open for modifying and display parent categories'
 
  }
 
// The tooltips for the above links
 
,addmulti          : '<span>+<sup>+</sup></span>'
 
// The HTML content of the "enter multi-mode" link at the front.
 
,multi_tooltip      : 'Modify several categories'
 
// Tooltip for the "enter multi-mode" link
 
,disable            :
 
function () { // Return true to disable HotCat.
 
var ns = conf.wgNamespaceNumber;
 
var nsIds = conf.wgNamespaceIds;
 
return (   ns < 0    // Special pages; Special:Upload is handled differently
 
|| ns === 10  // Templates
 
|| ns === 828 // Module (Lua)
 
|| ns === 8  // MediaWiki
 
|| ns === 6 && conf.wgArticleId === 0 // Non-existing file pages
 
|| ns === 2 && /\.(js|css)$/.test(conf.wgTitle) // User scripts
 
|| nsIds
 
&& (   ns === nsIds['creator']
 
|| ns === nsIds['timedtext']
 
|| ns === nsIds['institution']
 
  )
 
  );
 
}
 
,uncat_regexp : /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}\s*(<\!--.*?--\>)?/g
 
// A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
 
// If not, set it to null.
 
,existsYes    : '//upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
 
,existsNo    : '//upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png'
 
// The images used for the little indication icon. Should not need changing.
 
,template_categories : {}
 
// a list of categories which can be removed by removing a template
 
// key: the category without namespace
 
// value: A regexp matching the template name, again without namespace
 
// If you don't have this at your wiki, or don't want this, set it to an empty object {}.
 
,engine_names : {
 
searchindex : 'Search index'
 
,pagelist    : 'Page list'
 
,combined    : 'Combined search'
 
,subcat      : 'Subcategories'
 
,parentcat  : 'Parent categories'
 
  }
 
// Names for the search engines
 
,capitalizePageNames : true
 
// Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the first letter
 
// of a page is automatically capitalized ("first-letter"; Category:aa == Category:Aa), or it isn't
 
// ("case-sensitive"; Category:aa != Category:Aa). It doesn't currently have a fully case-insensitive mode
 
// (which would mean Category:aa == Category:Aa == Category:AA == Category:aA)
 
// HotCat tries to set this correctly automatically using an API query. It's still a good idea to manually
 
// configure it correctly; either directly here if you copied HotCat, or in the local configuration file
 
// MediaWiki:Gadget-HotCat.js/local_defaults if you hotlink to the Commons-version, to ensure it is set even
 
// if that API query should fail for some strange reason.
 
,upload_disabled : false
 
// If upload_disabled is true, HotCat will not be used on the Upload form.
 
,blacklist : null
 
// Single regular expression matching blacklisted categories that cannot be changed or
 
// added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
 
// or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
 
// word "maintenance" in its title.
 
  
// Stuff changeable by users:
+
if ( this.origin ) {
,bg_changed : '#F8CCB0'
+
$( '<a>' )
// Background for changed categories in multi-edit mode. Default is a very light salmon pink.
+
.attr( 'id', 'cat_a_lot_remove' )
,no_autocommit : false
+
.html( msg( 'remove-from-cat' ) )
// If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
+
.appendTo( $selections )
// the changes; users must always save explicitly.
+
.click( function() {
,del_needs_diff : false
+
catALot.remove();
// If true, the "category deletion" link "(-)" will never save automatically but always show an
+
} );
// edit page where the user has to save the edit manually. Is false by default because that's the
 
// traditional behavior. This setting overrides no_autocommit for "(-)" links.
 
,suggest_delay : 100
 
// Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
 
// server to get suggestions.
 
,editbox_width : 40
 
// Default width, in characters, of the text input field.
 
,suggestions : 'combined'
 
// One of the engine_names above, to be used as the default suggestion engine.
 
,fixed_search : false
 
// If true, always use the default engine, and never display a selector.
 
,use_up_down : true
 
// If false, do not display the "up" and "down" links
 
,list_size : 5
 
// Default list size
 
,single_minor : true
 
// If true, single category changes are marked as minor edits. If false, they're not.
 
,dont_add_to_watchlist : false
 
// If true, never add a page to the user's watchlist. If false, pages get added to the watchlist if
 
// the user has the "Add pages I edit to my watchlist" or the "Add pages I create to my watchlist"
 
// options in his or her preferences set.
 
,shortcuts : null
 
,addShortcuts :
 
function (map) {
 
if (!map) return;
 
window.HotCat.shortcuts = window.HotCat.shortcuts || {};
 
for (var k in map) {
 
if (!map.hasOwnProperty (k) || typeof k != 'string') continue;
 
var v = map[k];
 
if (typeof v != 'string') continue;
 
k = k.replace (/^\s+|\s+$/g, "");
 
v = v.replace (/^\s+|\s+$/g, "");
 
if (k.length === 0 || v.length === 0) continue;
 
window.HotCat.shortcuts[k] = v;
 
}
 
 
}
 
}
};
 
  
// More backwards compatibility. We have a few places where we test for the browser: once for
+
if ( ( mw.util.getParamValue( 'withJS' ) === 'MediaWiki:Gadget-Cat-a-lot.js' &&
// Safari < 3.0, and twice for WebKit (Chrome or Safari, any versions)
+
!mw.util.getParamValue( 'withCSS' ) ) ||
var ua = navigator.userAgent.toLowerCase();
+
mw.loader.getState( 'ext.gadget.Cat-a-lot' ) === 'registered' ) {
var is_webkit = /applewebkit\/\d+/.test(ua) && ua.indexOf ('spoofer') < 0;
+
importStylesheet( 'MediaWiki:Gadget-Cat-a-lot.css' );
// And even more compatbility. HotCat was developed without jQuery, and anyway current jQuery
 
// (1.7.1) doesn't seem to support in jquery.getJSON() or jQuery.ajax() the automatic
 
// switching from GET to POST requests if the query arguments would make the uri too long.
 
// (IE has a hard limit of 2083 bytes, and the servers may have limits around 4 or 8kB.)
 
//    Anyway, HotCat is supposed to run on wikis without jQuery, so we'd have to supply some
 
// ajax routines ourselves in any case. We can't rely on the old sajax_init_object(), newer
 
// MW versions (>= 1.19) might not have it.
 
var getJSON = (function () {
 
function getRequest () {
 
var request = null;
 
try {
 
request = new window.XMLHttpRequest();
 
} catch (anything) {
 
if (window.ActiveXObject) {
 
try {
 
request = new window.ActiveXObject('Microsoft.XMLHTTP');
 
} catch (any) {
 
}
 
} // end if IE
 
} // end try-catch
 
return request;
 
 
}
 
}
  
return function (settings) {
+
var reCat = new RegExp( '^\\s*' + catALot.localizedRegex( NS_CAT, 'Category' ) + ':', '' );
var req = getRequest();
 
if (!req && settings && settings.error) settings.error (req);
 
if (!req || !settings || !settings.uri) return req;
 
var uri = armorUri (settings.uri);
 
var args = settings.data || null;
 
var method;
 
if (args && uri.length + args.length + 1 > 2000) {
 
// We lose caching, but at least we can make the request
 
method = 'POST';
 
req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
 
} else {
 
method = 'GET';
 
if (args) uri += '?' + args;
 
args = null;
 
}
 
req.open (method, uri, true);
 
req.onreadystatechange = function () {
 
if (req.readyState != 4) return;
 
if (req.status != 200 || !req.responseText || !(/^\s*[\{\[]/.test(req.responseText))) {
 
if (settings.error) settings.error (req);
 
} else {
 
if (settings.success) settings.success (eval ('(' + req.responseText + ')'));
 
}
 
};
 
req.setRequestHeader ('Pragma', 'cache=yes');
 
req.setRequestHeader ('Cache-Control', 'no-transform');
 
req.send (args);
 
return req;
 
};
 
})();
 
 
 
function armorUri (uri) {
 
// Avoid protocol-relative URIs, IE7 has a bug with them in Ajax calls
 
if (uri.length >= 2 && uri.substring(0, 2) == '//') return document.location.protocol + uri;
 
return uri;
 
}
 
  
function LoadTrigger (needed) {
+
$searchInput.keypress( function( e ) {
this.queue = [];
+
if ( e.which === 13 ) {
this.toLoad = needed;
+
catALot.updateCats( $.trim( $( this ).val() ) );
}
 
LoadTrigger.prototype = {
 
register : function (callback) {
 
if (this.toLoad <= 0) {
 
callback (); // Execute directly
 
} else {
 
this.queue[this.queue.length] = callback;
 
 
}
 
}
},
+
} )
 
+
.bind( 'input keyup', function() {
loaded : function () {
+
var oldVal = this.value,
if (this.toLoad > 0) {
+
newVal = oldVal.replace( reCat, '' );
this.toLoad--;
+
if ( newVal !== oldVal ) {
if (this.toLoad === 0) {
+
this.value = newVal;
// Run queued callbacks once
 
for (var i = 0; i < this.queue.length; i++) this.queue[i]();
 
this.queue = [];
 
 
}
 
}
}
+
} );
 +
if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Search' ) {
 +
$searchInput.val( mw.util.getParamValue( 'search' ) );
 
}
 
}
 
+
function initAutocomplete() {
};
+
if ( catALot.autoCompleteIsEnabled ) {
 
+
return;
var setupCompleted = new LoadTrigger(1);
 
// Used to run user-registered code once HotCat is fully set up and ready.
 
HotCat.runWhenReady = function (callback) {setupCompleted.register(callback);};
 
 
 
var loadTrigger = new LoadTrigger(2);
 
// Used to delay running the HotCat setup until /local_defaults and localizations have been loaded.
 
 
 
function load (uri) {
 
var head = document.getElementsByTagName ('head')[0];
 
var s = document.createElement ('script');
 
s.setAttribute ('src', armorUri(uri));
 
s.setAttribute ('type', 'text/javascript');
 
var done = false;
 
 
 
function afterLoad () {
 
if (done) return;
 
done = true;
 
s.onload = s.onreadystatechange = s.onerror = null; // Properly clean up to avoid memory leaks in IE
 
if (head && s.parentNode) head.removeChild (s);
 
loadTrigger.loaded();
 
}
 
 
 
s.onload = s.onreadystatechange = function () { // onreadystatechange for IE, onload for all others
 
if (done) return;
 
if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
 
afterLoad ();
 
 
}
 
}
};
+
catALot.autoCompleteIsEnabled = true;
s.onerror = afterLoad; // Clean up, but otherwise ignore errors
 
head.insertBefore (s, head.firstChild); // appendChild may trigger bugs in IE6 here
 
}
 
  
function loadJS (page) {
+
$searchInput.autocomplete( {
load (conf.wgServer + conf.wgScript + '?title=' + encodeURIComponent (page) + '&action=raw&ctype=text/javascript');
+
source: function( request, response ) {
}
+
catALot.doAPICall( {
 
+
action: 'opensearch',
function loadURI (href) {
+
search: request.term,
var url = href;
+
redirects: 'resolve',
if (url.substring (0, 2) == '//') {
+
namespace: NS_CAT
url = window.location.protocol + url;
+
}, function( data ) {
} else if (url.substring (0, 1) == '/') {
+
if ( data[ 1 ] ) {
url = conf.wgServer + url;
+
response( $( data[ 1 ] )
 +
.map( function( index, item ) {
 +
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'
 +
} );
 
}
 
}
load (url);
 
}
 
  
// Load local configurations, overriding the pre-set default values in the HotCat object above. This is always loaded
+
$selectAll
// from the wiki where this script is executing, even if this script itself is hotlinked from the Commons. This can
+
.click( function() {
// be used to change the default settings, or to provide localized interface texts for edit summaries and so on.
+
catALot.toggleAll( true );
loadJS ('MediaWiki:Gadget-HotCat.js/local_defaults');
+
} );
 
+
$selectNone
// Load localized UI texts. These are the texts that HotCat displays on the page itself. Texts shown in edit summaries
+
.click( function() {
// should be localized in /local_defaults above.
+
catALot.toggleAll( false );
if (conf.wgUserLanguage != 'en') {
+
} );
// Lupo: somebody thought it would be a good idea to add this. So the default is true, and you have to set it to false
+
$link
// explicitly if you're not on the Commons and don't want that.
+
.click( function() {
if (typeof window.hotcat_translations_from_commons == 'undefined') {
+
$( this ).toggleClass( 'cat_a_lot_enabled' );
window.hotcat_translations_from_commons = true;
+
// Load autocomplete on demand
}
+
mw.loader.using( [ 'jquery.ui.autocomplete' ], initAutocomplete );
// Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
+
catALot.run();
if (window.hotcat_translations_from_commons && conf.wgServer.indexOf('//commons') < 0) {
+
} );
loadURI ('//commons.wikimedia.org/w/index.php?title='
+
$settingsLink
+ 'MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage
+
.click( function() {
+ '&action=raw&ctype=text/javascript'
+
catALot.manageSettings();
);
+
} );
} else {
 
// Load translations locally
 
loadJS ('MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage);
 
}
 
} else {
 
loadTrigger.loaded();
 
}
 
  
// No further changes should be necessary here.
+
this.localCatName = formattedNS[ NS_CAT ];
 +
},
  
// The following regular expression strings are used when searching for categories in wikitext.
+
findAllLabels: function( searchmode ) {
var wikiTextBlank  = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
+
// 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
var wikiTextBlankRE = new RegExp (wikiTextBlank, 'g');
+
switch ( searchmode ) {
// Regexp for handling blanks inside a category title or namespace name.
+
case 'search':
// See http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Title.php?revision=104051&view=markup#l2722
+
this.labels = this.labels.add( $( 'table.searchResultImage' ).find( 'tr>td:eq(1)' ) );
// See also http://www.fileformat.info/info/unicode/category/Zs/list.htm
+
if ( this.settings.editpages ) {
//  MediaWiki collapses several contiguous blanks inside a page title to one single blank. It also replace a
+
this.labels = this.labels.add( 'div.mw-search-result-heading' );
// number of special whitespace characters by simple blanks. And finally, blanks are treated as underscores.
+
}
// Therefore, when looking for page titles in wikitext, we must handle all these cases.
+
break;
//  Note: we _do_ include the horizontal tab in the above list, even though the MediaWiki software for some reason
+
case 'category':
// appears to not handle it. The zero-width space \u200B is _not_ handled as a space inside titles by MW.
+
this.findAllLabels( 'gallery' );
var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
+
this.labels = this.labels.add( $( 'div#mw-category-media' ).find( 'li[class!="gallerybox"]' ) );
// 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 "]]").
 
  
// First auto-localize the regexps for the category and the template namespaces.
+
if ( this.settings.editpages ) {
var formattedNamespaces = conf.wgFormattedNamespaces;
+
this.labels = this.labels.add( $( 'div#mw-pages, div#mw-subcategories' ).find( 'li' ) );
var namespaceIds = conf.wgNamespaceIds;
 
function autoLocalize (namespaceNumber, fallback) {
 
function create_regexp_str (name)
 
{
 
if (!name || name.length === 0) return "";
 
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 + ']';
 
 
}
 
}
}
+
break;
return regex_name
+
case 'contribs':
.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1')
+
this.labels = this.labels.add( $( 'ul.mw-contributions-list li' ) );
.replace (wikiTextBlankRE, wikiTextBlank);
+
// 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( 'div.gallerytext' );
 +
break;
 
}
 
}
 +
},
  
fallback = fallback.toLowerCase();
+
getTitleFromLink: function( href ) {
var canonical  = formattedNamespaces["" + namespaceNumber].toLowerCase();
+
try {
var regexp    = create_regexp_str (canonical);
+
return decodeURIComponent( href )
if (fallback && canonical != fallback) regexp += '|' + create_regexp_str(fallback);
+
.match( /wiki\/(.+?)(?:#.+)?$/ )[ 1 ].replace( /_/g, ' ' );
if (namespaceIds) {
+
} catch ( ex ) {
for (var cat_name in namespaceIds) {
+
return '';
if (   typeof cat_name == 'string'
 
&& cat_name.toLowerCase() != canonical
 
&& cat_name.toLowerCase() != fallback
 
&& namespaceIds[cat_name] == namespaceNumber)
 
{
 
regexp += '|' + create_regexp_str(cat_name);
 
}
 
}
 
 
}
 
}
return regexp;
+
},
}
+
 
 +
getMarkedLabels: function() {
 +
this.selectedLabels = this.labels.filter( '.cat_a_lot_selected:visible' );
 +
return this.selectedLabels.map( function() {
 +
var file = $( this ).find( 'a[title][class$="title"]' );
 +
file = file.length ? file : $( this ).find( 'a[title]' );
 +
var title = file.attr( 'title' ) ||
 +
catALot.getTitleFromLink( file.attr( 'href' ) ) ||
 +
catALot.getTitleFromLink( $( this ).find( 'a' )
 +
.attr( 'href' ) );
  
HotCat.category_canonical = formattedNamespaces['14'];
+
return [ [ title, $( this ) ] ];
HotCat.category_regexp = autoLocalize (14, 'category');
+
} );
if (formattedNamespaces['10']) {
+
},
HotCat.template_regexp = autoLocalize (10, 'template');
 
}
 
  
// Utility functions. Yes, this duplicates some functionality that also exists in other places, but
+
updateSelectionCounter: function() {
// to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
+
this.selectedLabels = this.labels.filter( '.cat_a_lot_selected' );
// these few operations here.
+
$markCounter
function make (arg, literal) {
+
.show()
if (!arg) return null;
+
.html( msg( 'files-selected', this.selectedLabels.length ) );
return literal ? document.createTextNode (arg) : document.createElement (arg);
+
},
}
 
function param (name, uri) {
 
if (typeof uri == 'undefined' || uri === null) uri = document.location.href;
 
var re = new RegExp ('[&?]' + name + '=([^&#]*)');
 
var m = re.exec (uri);
 
if (m && m.length > 1) return decodeURIComponent(m[1]);
 
return null;
 
}
 
function title (href) {
 
if (!href) return null;
 
var script = conf.wgScript + '?';
 
if (href.indexOf (script) === 0 || href.indexOf (conf.wgServer + script) === 0 || conf.wgServer.substring(0, 2) == '//' && href.indexOf (document.location.protocol + conf.wgServer + script) === 0) {
 
// href="/w/index.php?title=..."
 
return param ('title', href);
 
} else {
 
// href="/wiki/..."
 
var prefix = conf.wgArticlePath.replace ('$1', "");
 
if (href.indexOf (prefix) !== 0) prefix = conf.wgServer + prefix; // Fully expanded URL?
 
if (href.indexOf (prefix) !== 0 && prefix.substring(0, 2) == '//') prefix = document.location.protocol + prefix; // Protocol-relative wgServer?
 
if (href.indexOf (prefix) === 0)
 
return decodeURIComponent (href.substring (prefix.length));
 
}
 
return null;
 
}
 
function hasClass (elem, name) {
 
return (' ' + elem.className + ' ').indexOf (' ' + name + ' ') >= 0;
 
}
 
function capitalize (str) {
 
if (!str || str.length === 0) return str;
 
return str.substr(0, 1).toUpperCase() + str.substr (1);
 
}
 
function wikiPagePath (pageName) {
 
// Note: do not simply use encodeURI, it doesn't encode '&', which might break if wgArticlePath actually has the $1 in
 
// a query parameter.
 
return conf.wgArticlePath.replace('$1', encodeURIComponent (pageName).replace(/%3A/g, ':').replace(/%2F/g, '/'));
 
}
 
function escapeRE(str) {
 
return str.replace(/([\\\^\$\.\?\*\+\(\)\[\]])/g, '\\$1');
 
}
 
  
function substituteFactory (options) {
+
makeClickable: function() {
options = options || {};
+
this.labels = $();
var lead = options.indicator || '$';
+
this.findAllLabels( this.searchmode );
var indicator = escapeRE (lead);
+
this.labels.catALotShiftClick( function() {
var lbrace = escapeRE (options.lbrace || '{');
+
catALot.updateSelectionCounter();
var rbrace = escapeRE (options.rbrace || '}');
+
} )
var re;
+
.addClass( 'cat_a_lot_label' );
 +
},
  
re = new RegExp(
+
toggleAll: function( select ) {
'(?:' + indicator + '(' + indicator + '))|'                                          // $$
+
this.labels.toggleClass( 'cat_a_lot_selected', select );
+'(?:' + indicator + '(\\d+))|'                                                        // $0, $1
+
this.updateSelectionCounter();
+'(?:' + indicator + '(?:' + lbrace + '([^' + lbrace + rbrace + ']+)' + rbrace + '))|' // ${key}
+
},
+'(?:' + indicator + '(?!(?:[' + indicator + lbrace + ']|\\d))(\\S+?)\\b)'            // $key (only if first char after $ is not $, digit, or { )
 
,'g');
 
// Replace $1, $2, or ${key1}, ${key2}, or $key1, $key2 by values from map. $$ is replaced by a single $.
 
return function (str, map) {
 
if (!map) return str;
 
return str.replace(re
 
,function (match, prefix, idx, key, alpha) {
 
if (prefix == lead) return lead;
 
var k = alpha || key || idx;
 
var replacement = typeof map[k] === 'function' ? map[k](match, k) : map[k];
 
return typeof replacement === 'string' ? replacement : (replacement || match);
 
}
 
);
 
};
 
}
 
  
var substitute = substituteFactory();
+
getSubCats: function() {
var replaceShortcuts = (function () {
+
var data = {
var replaceHash = substituteFactory({indicator:'#',lbrace:'[',rbrace:']'});
+
action: 'query',
return function (str, map) {
+
list: 'categorymembers',
var s = replaceHash (str, map);
+
cmtype: 'subcat',
return HotCat.capitalizePageNames ? capitalize(s) : s;
+
cmlimit: this.settings.subcatcount,
 +
cmtitle: 'Category:' + this.currentCategory
 
};
 
};
})();
 
  
// Text modification
+
this.doAPICall( data, function( result ) {
 +
var cats = result.query.categorymembers;
 +
 +
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();
 +
}
 +
} );
 +
},
  
var findCatsRE =
+
getParentCats: function() {
new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(?:' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g');
+
var data = {
 +
action: 'query',
 +
prop: 'categories',
 +
titles: 'Category:' + this.currentCategory
 +
};
 +
this.doAPICall( data, function( result ) {
 +
catALot.parentCats = [];
 +
var cats, pages = result.query.pages;
 +
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';
  
function replaceByBlanks (match) {
+
$resultList.append( '<table>' );
return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
+
catALot.createCatLinks( '', [ catALot.currentCategory ] );
}
+
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++ ) {
 +
catALot.parentCats.push( cats[ i ].title.replace( /^[^:]+:/, '' ) );
 +
}
  
function find_category (wikitext, category, once) {
+
catALot.catCounter++;
var cat_regex = null;
+
if ( catALot.catCounter === 2 ) {
if(HotCat.template_categories[category]){
+
catALot.showCategoryList();
cat_regex = new RegExp ('\\{\\{' + wikiTextBlankOrBidi + '(' + HotCat.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi
+
}
+ '(?:' + HotCat.template_categories[category] + ')'
+
} );
+ wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}', 'g'
+
},
);
 
} else {
 
var cat_name  = escapeRE (category);
 
var initial  = cat_name.substr (0, 1);
 
cat_regex = new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi
 
+ (initial == '\\' || !HotCat.capitalizePageNames
 
? initial
 
: '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
 
+ cat_name.substring (1).replace (wikiTextBlankRE, wikiTextBlank)
 
+ wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]', 'g'
 
);
 
}
 
if (once) return cat_regex.exec (wikitext);
 
var copiedtext = wikitext
 
.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
 
.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
 
var result = [];
 
var curr_match = null;
 
while ((curr_match = cat_regex.exec (copiedtext)) !== null) {
 
result.push ({match : curr_match});
 
}
 
result.re = cat_regex;
 
return result; // An array containing all matches, with positions, in result[i].match
 
}
 
  
var interlanguageRE = null;
+
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' );
  
function change_category (wikitext, toRemove, toAdd, key, is_hidden) {
+
var createRegexStr = function( name ) {
 
+
if ( !name || name.length === 0 ) {
function find_insertionpoint (wikitext) {
+
return '';
var copiedtext = wikitext
+
}
.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
+
var regexName = '';
.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
+
for ( var i = 0; i < name.length; i++ ) {
// Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
+
var initial = name.substr( i, 1 );
var index = -1;
+
var ll = initial.toLowerCase();
findCatsRE.lastIndex = 0;
+
var ul = initial.toUpperCase();
while (findCatsRE.exec(copiedtext) !== null) index = findCatsRE.lastIndex;
+
if ( ll === ul ) {
if (index < 0) {
+
regexName += initial;
// Find the index of the first interlanguage link...
 
var match = null;
 
if (!interlanguageRE) {
 
// Approximation without API: interlanguage links start with 2 to 3 lower case letters, optionally followed by
 
// a sequence of groups consisting of a dash followed by one or more lower case letters. Exceptions are "simple"
 
// and "tokipona".
 
match = /((^|\n\r?)(\[\[\s*(([a-z]{2,3}(-[a-z]+)*)|simple|tokipona)\s*:[^\]]+\]\]\s*))+$/.exec (copiedtext);
 
 
} else {
 
} else {
match = interlanguageRE.exec(copiedtext);
+
regexName += '[' + ll + ul + ']';
 
}
 
}
if (match) index = match.index;
 
return {idx : index, onCat : false};
 
 
}
 
}
return {idx : index, onCat : index >= 0};
+
return regexName.replace( /([\\\^\$\.\?\*\+\(\)])/g, '\\$1' )
}
+
.replace( wikiTextBlankRE, wikiTextBlank );
 +
};
  
var summary  = [];
+
fallback = fallback.toLowerCase();
var nameSpace = HotCat.category_canonical;
+
var canonical = formattedNS[ namespaceNumber ].toLowerCase();
var cat_point = -1; // Position of removed category;
+
var RegexString = createRegexStr( canonical );
 
+
if ( fallback && canonical !== fallback ) {
if (key) key = '|' + key;
+
RegexString += '|' + createRegexStr( fallback );
var keyChange = (toRemove && toAdd && toRemove == toAdd && toAdd.length > 0);
 
var matches;
 
if (toRemove && toRemove.length > 0) {
 
matches = find_category (wikitext, toRemove);
 
if (!matches || matches.length === 0) {
 
return {text: wikitext, 'summary': summary, error: HotCat.messages.cat_notFound.replace (/\$1/g, toRemove)};
 
} else {
 
var before = wikitext.substring (0, matches[0].match.index);
 
var after  = wikitext.substring (matches[0].match.index + matches[0].match[0].length);
 
if (matches.length > 1) {
 
// Remove all occurrences in after
 
matches.re.lastIndex = 0;
 
after = after.replace (matches.re, "");
 
}
 
if (toAdd) {
 
nameSpace = matches[0].match[1] || nameSpace;
 
if (key === null) key = matches[0].match[2]; // Remember the category key, if any.
 
}
 
// Remove whitespace (properly): strip whitespace, but only up to the next line feed.
 
// If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
 
// whitespace characters, insert a blank.
 
var i = before.length - 1;
 
while (i >= 0 && before.charAt (i) != '\n' && before.substr (i, 1).search (/\s/) >= 0) i--;
 
var j = 0;
 
while (j < after.length && after.charAt (j) != '\n' && after.substr (j, 1).search (/\s/) >= 0)
 
j++;
 
if (i >= 0 && before.charAt (i) == '\n' && (after.length === 0 || j < after.length && after.charAt (j) == '\n'))
 
i--;
 
if (i >= 0) before = before.substring (0, i+1); else before = "";
 
if (j < after.length) after = after.substring (j); else after = "";
 
if (before.length > 0 && before.substring (before.length - 1).search (/\S/) >= 0
 
&& after.length > 0 && after.substr (0, 1).search (/\S/) >= 0)
 
before += ' ';
 
cat_point = before.length;
 
if (cat_point === 0 && after.length > 0 && after.substr(0,1) == '\n') {
 
after = after.substr(1);
 
}
 
wikitext = before + after;
 
if (!keyChange) {
 
if(HotCat.template_categories[toRemove]) {
 
summary.push (HotCat.messages.template_removed.replace (/\$1/g, toRemove));
 
} else {
 
summary.push (HotCat.messages.cat_removed.replace (/\$1/g, toRemove));
 
}
 
}
 
}
 
 
}
 
}
if (toAdd && toAdd.length > 0) {
+
for ( var catName in nsIDs ) {
matches = find_category (wikitext, toAdd);
+
if ( typeof catName === 'string' && catName.toLowerCase() !== canonical && catName.toLowerCase() !== fallback && nsIDs[ catName ] === namespaceNumber ) {
if (matches && matches.length > 0) {
+
RegexString += '|' + createRegexStr( catName );
return {text: wikitext, 'summary': summary, error : HotCat.messages.cat_exists.replace (/\$1/g, toAdd)};
 
} else {
 
var onCat = false;
 
if (cat_point < 0) {
 
var point = find_insertionpoint (wikitext);
 
cat_point = point.idx;
 
onCat = point.onCat;
 
} else {
 
onCat = true;
 
}
 
var newcatstring = '[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
 
if (cat_point >= 0) {
 
var suffix = wikitext.substring (cat_point);
 
wikitext = wikitext.substring (0, cat_point) + (cat_point > 0 ? '\n' : "") + newcatstring + (!onCat ? '\n' : "");
 
if (suffix.length > 0 && suffix.substr(0, 1) != '\n') {
 
wikitext += '\n' + suffix;
 
} else {
 
wikitext += suffix;
 
}
 
} else {
 
if (wikitext.length > 0 && wikitext.substr (wikitext.length - 1, 1) != '\n')
 
wikitext += '\n';
 
wikitext += (wikitext.length > 0 ? '\n' : "") + newcatstring;
 
}
 
if (keyChange) {
 
var k = key || "";
 
if (k.length > 0) k = k.substr (1);
 
summary.push (substitute (HotCat.messages.cat_keychange, [null, toAdd, k]));
 
} else {
 
summary.push (HotCat.messages.cat_added.replace (/\$1/g, toAdd));
 
}
 
if (HotCat.uncat_regexp && !is_hidden) {
 
var txt = wikitext.replace (HotCat.uncat_regexp, ""); // Remove "uncat" templates
 
if (txt.length != wikitext.length) {
 
wikitext = txt;
 
summary.push (HotCat.messages.uncat_removed);
 
}
 
}
 
 
}
 
}
 
}
 
}
return {text: wikitext, 'summary': summary, error: null};
+
return ( '(?:' + RegexString + ')' );
}
+
},
 +
 
 +
regexBuilder: function( category ) {
 +
var catname = this.localizedRegex( NS_CAT, '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 );
  
// The real HotCat UI
+
// any sequence of spaces and underscores should match any other
 +
category = category.replace( /[\s_]+/g, '[\\s_]+' );
  
function evtKeys (e) {
+
// Make the first character case-insensitive:
e = e || window.event || window.Event; // W3C, IE, Netscape
+
var first = category.substr( 0, 1 );
var code = 0;
+
if ( first.toUpperCase() !== first.toLowerCase() ) {
if (typeof e.ctrlKey != 'undefined') { // All modern browsers
+
category = '[' + first.toUpperCase() + first.toLowerCase() + ']' + category.substr( 1 );
// Ctrl-click seems to be overloaded in FF/Mac (it opens a pop-up menu), so treat cmd-click
 
// as a ctrl-click, too.
 
if (e.ctrlKey || e.metaKey) code |= 1;
 
if (e.shiftKey) code |= 2;
 
} else if (typeof e.modifiers != 'undefined') { // Netscape...
 
if (e.modifiers & (Event.CONTROL_MASK | Event.META_MASK)) code |= 1;
 
if (e.modifiers & Event.SHIFT_MASK) code |= 2;
 
 
}
 
}
return code;
 
}
 
function evtKill (e) {
 
e = e || window.event || window.Event; // W3C, IE, Netscape
 
if (typeof e.preventDefault != 'undefined') {
 
e.preventDefault ();
 
e.stopPropagation ();
 
} else
 
e.cancelBubble = true;
 
return false;
 
}
 
  
var catLine      = null;
+
// Compile it into a RegExp that matches MediaWiki category syntax (yeah, it looks ugly):
var onUpload    = false;
+
// XXX: the first capturing parens are assumed to match the sortkey, if present, including the | but excluding the ]]
var editors      = [];
+
return new RegExp( '\\[\\[[\\s_]*' + catname + '[\\s_]*:[\\s_]*' + category + '[\\s_]*(\\|[^\\]]*(?:\\][^\\]]+)*)?\\]\\]\\s*', 'g' );
 +
},
  
var commitButton = null;
+
getContent: function( file, targetcat, mode ) {
var commitForm  = null;
+
var data = {
var multiSpan    = null;
+
action: 'query',
 +
prop: 'info|revisions',
 +
rvprop: 'content|timestamp',
 +
intoken: 'edit',
 +
titles: file[ 0 ]
 +
};
  
var pageText    = null;
+
this.doAPICall( data, function( result ) {
var pageTime    = null;
+
catALot.editCategories( result, file, targetcat, mode );
var pageWatched  = false;
+
} );
var watchCreate  = false;
+
},
var watchEdit    = false;
 
var minorEdits  = false;
 
var editToken    = null;
 
  
var is_rtl      = false;
+
// Remove {{Uncategorized}}. No need to replace it with anything.
var serverTime  = null;
+
removeUncat: function( text ) {
var lastRevId    = null;
+
return text.replace( /\{\{\s*[Uu]ncategorized\s*(\|?.*?)\}\}/, '' );
var pageTextRevId = null;
+
},
var conflictingUser = null;
 
  
var newDOM      = false; // true if MediaWiki serves the new UL-LI DOM for categories
+
doCleanup: function( text ) {
 
+
if ( this.settings.docleanup ) {
function setMultiInput () {
+
return text.replace( /\{\{\s*[Cc]heck categories\s*(\|?.*?)\}\}/, '' );
if (commitButton || onUpload) return;
 
commitButton = make ('input');
 
commitButton.type  = 'button';
 
commitButton.value = HotCat.messages.commit;
 
commitButton.onclick = multiSubmit;
 
if (multiSpan) {
 
multiSpan.parentNode.replaceChild (commitButton, multiSpan);
 
 
} else {
 
} else {
catLine.appendChild (commitButton);
+
return text;
 
}
 
}
}
+
},
  
function checkMultiInput () {
+
editCategories: function( result, file, targetcat, mode ) {
if (!commitButton) return;
+
var otext, starttimestamp, timestamp;
var has_changes = false;
+
if ( !result ) {
for (var i = 0; i < editors.length; i++) {
+
// Happens on unstable wifi connections..
if (editors[i].state != CategoryEditor.UNCHANGED) {
+
this.connectionError.push( file[ 0 ] );
has_changes = true;
+
this.updateCounter();
break;
+
return;
}
 
}
 
commitButton.disabled = !has_changes;
 
}
 
 
 
function currentTimestamp () {
 
var now = new Date();
 
var ts  = "" + now.getUTCFullYear();
 
function two (s) { return s.substr (s.length - 2); }
 
ts = ts
 
+ two ('0' + (now.getUTCMonth() + 1))
 
+ two ('0' + now.getUTCDate())
 
+ two ('00' + now.getUTCHours())
 
+ two ('00' + now.getUTCMinutes())
 
+ two ('00' + now.getUTCSeconds());
 
return ts;
 
}
 
 
 
var saveInProgress = false;
 
function initiateEdit (doEdit, failure) {
 
if (saveInProgress) return;
 
saveInProgress = true;
 
var oldButtonState;
 
if (commitButton) {
 
oldButtonState = commitButton.disabled;
 
commitButton.disabled = true;
 
 
}
 
}
 +
var pages = result.query.pages;
  
function fail() {
+
// there should be only one, but we don't know its ID
saveInProgress = false;
+
for ( var id in pages ) {
if (commitButton) commitButton.disabled = oldButtonState;
+
// The edittoken only changes between logins
failure.apply(this, arguments);
+
this.edittoken = pages[ id ].edittoken;
 +
otext = pages[ id ].revisions[ 0 ][ '*' ];
 +
starttimestamp = pages[ id ].starttimestamp;
 +
timestamp = pages[ id ].revisions[ 0 ].timestamp;
 
}
 
}
  
// Must use Ajax here to get the user options and the edit token.
 
  
getJSON ({
+
var sourcecat = this.origin;
uri : conf.wgServer + conf.wgScriptPath + '/api.php'
+
// Check if that file is already in that category
,data : 'format=json&action=query&rawcontinue=&titles=' + encodeURIComponent (conf.wgPageName)
+
if ( mode !== 'remove' && this.regexBuilder( targetcat ).test( otext ) ) {
+ '&prop=info%7Crevisions%7Clanglinks&inprop=watched&intoken=edit&rvprop=content%7Ctimestamp%7Cids%7Cuser&lllimit=500'
 
+ '&rvlimit=2&rvdir=newer&rvstartid=' + conf.wgCurRevisionId
 
+ '&meta=siteinfo%7Cuserinfo&uiprop=options'
 
,success : function (json) { setPage(json); doEdit(fail); }
 
,error : function (req) { fail(req.status + ' ' + req.statusText); }
 
});
 
}
 
  
function multiChangeMsg (count) {
+
// If the new cat is already there, just remove the old one.
var msg = HotCat.messages.multi_change;
+
if ( mode === 'move' ) {
if (typeof msg != 'string' && msg.length) {
+
mode = 'remove';
if (mw.language && mw.language.convertPlural) {
 
msg = mw.language.convertPlural (count, msg);
 
 
} else {
 
} else {
msg = msg[msg.length-1];
+
this.alreadyThere.push( file[ 0 ] );
 +
this.updateCounter();
 +
return;
 
}
 
}
 
}
 
}
return substitute (msg, [null, "" + count]);
 
}
 
  
function performChanges (failure, singleEditor) {
+
var text = otext;
if (pageText === null) {
+
var comment;
failure (HotCat.messages.multi_error);
+
 
return;
+
// Fix text
}
+
switch ( mode ) {
// Backwards compatibility after message change (added $2 to cat_keychange)
+
case 'add':
if (HotCat.messages.cat_keychange.indexOf ('$2') < 0) HotCat.messages.cat_keychange += '"$2"';
+
text += '\n[[' + this.localCatName + ':' + targetcat + ']]\n';
// More backwards-compatibility with earlier HotCat versions:
+
comment = msgPlain( 'summary-add' ).replace( '$1', targetcat );
if (!HotCat.messages.short_catchange) HotCat.messages.short_catchange = '[[' + HotCat.category_canonical + ':$1]]';
+
break;
// Create a form and submit it. We don't use the edit API (api.php?action=edit) because
+
case 'copy':
// (a) sensibly reporting back errors like edit conflicts is always a hassle, and
+
text = text.replace( this.regexBuilder( sourcecat ), '[[' + this.localCatName + ':' + sourcecat + '$1]]\n[[' + this.localCatName + ':' + targetcat + '$1]]\n' );
// (b) we want to show a diff for multi-edits anyway, and
+
comment = msgPlain( 'summary-copy' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
// (c) we want to trigger onsubmit events, allowing user code to intercept the edit.
+
// If category is added through template:
// Using the form, we can do (b) and (c), and we get (a) for free. And, of course, using the form
+
if ( otext === text ) {
// automatically reloads the page with the updated categories on a successful submit, which
+
text += '\n[[' + this.localCatName + ':' + targetcat + ']]';
// we would have to do explicitly if we used the edit API.
 
var action;
 
// Normally, we don't have to care about edit conflicts. If some other user edited the page in the meantime, the
 
// server will take care of it and merge the edit automatically or present an edit conflict screen. However, the
 
// server suppresses edit conflicts with oneself. Hence, if we have a conflict, and the conflicting user is the
 
// current user, then we set the "oldid" value and switch to diff, which gives the "you are editing an old version;
 
// if you save, any more recent changes will be lost" screen.
 
var editingOldVersion = lastRevId !== null && lastRevId != conf.wgCurRevisionId || pageTextRevId !== null && pageTextRevId != conf.wgCurRevisionId;
 
var selfEditConflict = editingOldVersion && conflictingUser && conflictingUser == conf.wgUserName;
 
if (singleEditor && !singleEditor.noCommit && !HotCat.no_autocommit && editToken && !selfEditConflict) {
 
// If we do have an edit conflict, but not with ourself, that's no reason not to attempt to save: the server side may actually be able to
 
// merge the changes. We just need to make sure that we do present a diff view if it's a self edit conflict.
 
commitForm.wpEditToken.value = editToken;
 
action = commitForm.wpDiff;
 
if (action) action.name = action.value = 'wpSave';
 
} else {
 
action = commitForm.wpSave;
 
if (action) action.name = action.value = 'wpDiff';
 
}
 
var result = { text : pageText };
 
var changed = [], added = [], deleted = [], changes = 0;
 
var toEdit = !!singleEditor ? [singleEditor] : editors;
 
var error = null;
 
var i;
 
for (i=0; i < toEdit.length; i++) {
 
if (toEdit[i].state == CategoryEditor.CHANGED) {
 
result = change_category (
 
result.text
 
, toEdit[i].originalCategory
 
, toEdit[i].currentCategory
 
, toEdit[i].currentKey
 
, toEdit[i].currentHidden
 
);
 
if (!result.error) {
 
changes++;
 
if (!toEdit[i].originalCategory || toEdit[i].originalCategory.length === 0) {
 
added.push (toEdit[i].currentCategory);
 
} else {
 
changed.push ({from : toEdit[i].originalCategory, to : toEdit[i].currentCategory});
 
}
 
} else if (error === null) {
 
error = result.error;
 
 
}
 
}
} else if (  toEdit[i].state == CategoryEditor.DELETED
+
break;
  && toEdit[i].originalCategory
+
case 'move':
  && toEdit[i].originalCategory.length > 0)
+
text = text.replace( this.regexBuilder( sourcecat ), '[[' + this.localCatName + ':' + targetcat + '$1]]\n' );
{
+
comment = msgPlain( 'summary-move' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
result = change_category (result.text, toEdit[i].originalCategory, null, null, false);
+
break;
if (!result.error) {
+
case 'remove':
changes++;
+
text = text.replace( this.regexBuilder( sourcecat ), '' );
deleted.push (toEdit[i].originalCategory);
+
comment = msgPlain( 'summary-remove' ).replace( '$1', sourcecat );
} else if (error === null) {
+
break;
error = result.error;
 
}
 
}
 
}
 
if (error !== null) { // Do not commit if there were errors
 
action = commitForm.wpSave;
 
if (action) action.name = action.value = 'wpDiff';
 
}
 
// Fill in the form and submit it
 
commitForm.wpAutoSummary.value = 'd41d8cd98f00b204e9800998ecf8427e'; // MD5 hash of the empty string
 
commitForm.wpMinoredit.checked = minorEdits;
 
commitForm.wpWatchthis.checked = conf.wgArticleId === 0 && watchCreate || watchEdit || pageWatched;
 
if (conf.wgArticleId > 0 || !!singleEditor) {
 
if (changes == 1) {
 
if (result.summary && result.summary.length > 0)
 
commitForm.wpSummary.value = HotCat.messages.prefix + result.summary.join (HotCat.messages.separator) + HotCat.messages.using;
 
commitForm.wpMinoredit.checked = HotCat.single_minor || minorEdits;
 
} else if (changes > 1) {
 
var summary = [];
 
var shortSummary = [];
 
// Deleted
 
for (i = 0; i < deleted.length; i++) {
 
summary.push ('-' + substitute (HotCat.messages.short_catchange, [null, deleted[i]]));
 
}
 
if (deleted.length == 1)
 
shortSummary.push ('-' + substitute (HotCat.messages.short_catchange, [null, deleted[0]]));
 
else if (deleted.length > 1)
 
shortSummary.push ('- ' + multiChangeMsg (deleted.length));
 
// Added
 
for (i = 0; i < added.length; i++) {
 
summary.push ('+' + substitute (HotCat.messages.short_catchange, [null, added[i]]));
 
}
 
if (added.length == 1)
 
shortSummary.push ('+' + substitute (HotCat.messages.short_catchange, [null, added[0]]));
 
else if (added.length > 1)
 
shortSummary.push ('+ ' + multiChangeMsg (added.length));
 
// Changed
 
var arrow = is_rtl ? '\u2190' : '\u2192'; // left and right arrows. Don't use ← and → in the code.
 
for (i = 0; i < changed.length; i++) {
 
if (changed[i].from != changed[i].to) {
 
summary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[i].from]) + arrow
 
+ substitute (HotCat.messages.short_catchange, [null, changed[i].to]));
 
} else {
 
summary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[i].from]));
 
}
 
}
 
if (changed.length == 1) {
 
if (changed[0].from != changed[0].to) {
 
shortSummary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[0].from]) + arrow
 
+ substitute (HotCat.messages.short_catchange, [null, changed[0].to]));
 
} else {
 
shortSummary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[0].from]));
 
}
 
} else if (changed.length > 1) {
 
shortSummary.push ('± ' + multiChangeMsg (changed.length));
 
}
 
if (summary.length > 0) {
 
summary = summary.join (HotCat.messages.separator);
 
if (summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length) {
 
summary = shortSummary.join (HotCat.messages.separator);
 
}
 
commitForm.wpSummary.value = HotCat.messages.prefix + summary + HotCat.messages.using;
 
}
 
}
 
 
}
 
}
commitForm.wpTextbox1.value = result.text;
 
commitForm.wpStarttime.value = serverTime || currentTimestamp ();
 
commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
 
if (selfEditConflict) commitForm.oldid.value = "" + (pageTextRevId || conf.wgCurRevisionId);
 
// Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
 
commitForm.hcCommit.click();
 
}
 
  
function resolveMulti (toResolve, callback) {
+
if ( text === otext ) {
var i;
+
this.notFound.push( file[ 0 ] );
for (i = 0; i < toResolve.length; i++) {
+
this.updateCounter();
toResolve[i].dab = null;
 
toResolve[i].dabInput = toResolve[i].lastInput;
 
}
 
if (noSuggestions) {
 
callback (toResolve);
 
 
return;
 
return;
 
}
 
}
// Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
 
// category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
 
var args = 'action=query&prop=info%7Clinks%7Ccategories%7Ccategoryinfo&plnamespace=14'
 
+ '&pllimit=' + (toResolve.length * 10)
 
+ '&cllimit=' + (toResolve.length * 10)
 
+ '&format=json&titles=';
 
for (i = 0; i < toResolve.length; i++) {
 
var v = toResolve[i].dabInput;
 
v = replaceShortcuts (v, HotCat.shortcuts);
 
toResolve[i].dabInputCleaned = v;
 
args += encodeURIComponent ('Category:' + v);
 
if (i+1 < toResolve.length) args += '%7C';
 
}
 
getJSON({
 
uri : conf.wgServer + conf.wgScriptPath + '/api.php'
 
,data : args
 
,success: function (json) { resolveRedirects (toResolve, json); callback (toResolve); }
 
,error: function (req) { if (!req) noSuggestions = true; callback (toResolve); }
 
});
 
}
 
  
function resolveOne (page, toResolve) {
+
// Remove uncat after we checked whether we changed the text successfully.
var cats    = page.categories;
+
// Otherwise we might fail to do the changes, but still replace {{uncat}}
var lks      = page.links;
+
if ( mode !== 'remove' ) {
var is_dab  = false;
+
text = this.doCleanup( this.removeUncat( text ) );
var is_redir = typeof page.redirect == 'string'; // Hard redirect?
 
var is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden == 'string';
 
var is_missing = typeof page.missing == 'string';
 
var i;
 
for (i = 0; i < toResolve.length; i++) {
 
if (toResolve.length > 1 && toResolve[i].dabInputCleaned != page.title.substring (page.title.indexOf (':') + 1)) continue;
 
// Note: the server returns in page an NFC normalized Unicode title. If our input was not NFC normalized, we may not find
 
// any entry here. If we have only one editor to resolve (the most common case, I presume), we may simply skip the check.
 
toResolve[i].currentHidden = is_hidden;
 
toResolve[i].inputExists = !is_missing;
 
toResolve[i].icon.src = armorUri(is_missing ? HotCat.existsNo : HotCat.existsYes);
 
 
}
 
}
if (is_missing) return;
+
var data = {
if (!is_redir && cats && (HotCat.disambig_category || HotCat.redir_category)) {
+
action: 'edit',
for (var c = 0; c < cats.length; c++) {
+
assert: 'user',
var cat = cats[c]['title'];
+
summary: comment,
// Strip namespace prefix
+
title: file[ 0 ],
if (cat) {
+
text: text,
cat = cat.substring (cat.indexOf (':') + 1).replace(/_/g, ' ');
+
bot: true,
if (cat == HotCat.disambig_category) {
+
starttimestamp: starttimestamp,
is_dab = true; break;
+
basetimestamp: timestamp,
} else if (cat == HotCat.redir_category) {
+
watchlist: this.settings.watchlist,
is_redir = true; break;
+
token: this.edittoken
}
+
};
}
+
if ( this.settings.minor ) {
}
+
data.minor = true;
}
 
if (!is_redir && !is_dab) return;
 
if (!lks || lks.length === 0) return;
 
var titles = [];
 
for (i = 0; i < lks.length; i++) {
 
if (  lks[i]['ns'] == 14                            // Category namespace -- always true since we ask only for the category links
 
&& lks[i]['title'] && lks[i]['title'].length > 0) // Name not empty
 
{
 
// Internal link to existing thingy. Extract the page name and remove the namespace.
 
var match = lks[i]['title'];
 
match = match.substring (match.indexOf (':') + 1);
 
// Exclude blacklisted categories.
 
if (!HotCat.blacklist || !HotCat.blacklist.test (match)) {
 
titles.push (match);
 
}
 
}
 
}
 
if (titles.length === 0) {
 
return;
 
}
 
for (i = 0; i < toResolve.length; i++) {
 
if (toResolve.length > 1 && toResolve[i].dabInputCleaned != page.title.substring (page.title.indexOf (':') + 1)) continue;
 
toResolve[i].inputExists = true; // Might actually be wrong if it's a redirect pointing to a non-existing category
 
toResolve[i].icon.src = armorUri(HotCat.existsYes);
 
if (titles.length > 1) {
 
toResolve[i].dab = titles;
 
} else {
 
toResolve[i].text.value =
 
titles[0] + (toResolve[i].currentKey !== null ? '|' + toResolve[i].currentKey : "");
 
}
 
 
}
 
}
}
 
  
function resolveRedirects (toResolve, params) {
+
this.doAPICall( data, function() {
if (!params || !params.query || !params.query.pages) return;
+
catALot.updateCounter();
for (var p in params.query.pages) resolveOne (params.query.pages[p], toResolve);
+
} );
}
+
this.markAsDone( file[ 1 ], mode, targetcat );
 +
},
  
function multiSubmit () {
+
markAsDone: function( label, mode, targetcat ) {
var toResolve = [];
+
label.addClass( 'cat_a_lot_markAsDone' );
for (var i = 0; i < editors.length; i++) {
+
switch ( mode ) {
if (editors[i].state == CategoryEditor.CHANGE_PENDING || editors[i].state == CategoryEditor.OPEN)
+
case 'add':
toResolve.push (editors[i]);
+
label.append( '<br>' + msg( 'added-cat', targetcat ) );
}
+
break;
if (toResolve.length === 0) {
+
case 'copy':
initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
+
label.append( '<br>' + msg( 'copied-cat', targetcat ) );
return;
+
break;
 +
case 'move':
 +
label.append( '<br>' + msg( 'moved-cat', targetcat ) );
 +
break;
 +
case 'remove':
 +
label.append( '<br>' + msg( 'removed-cat' ) );
 +
break;
 
}
 
}
resolveMulti (
+
},
  toResolve
 
, function (resolved) {
 
var firstDab = null;
 
var dontChange = false;
 
for (var i = 0; i < resolved.length; i++) {
 
if (resolved[i].lastInput != resolved[i].dabInput) {
 
// We didn't disable all the open editors, but we did asynchronous calls. It is
 
// theoretically possible that the user changed something...
 
dontChange = true;
 
} else {
 
if (resolved[i].dab) {
 
if (!firstDab) firstDab = resolved[i];
 
} else {
 
if (resolved[i].acceptCheck(true)) resolved[i].commit();
 
}
 
}
 
}
 
if (firstDab) {
 
showDab (firstDab);
 
} else if (!dontChange) {
 
initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
 
}
 
}
 
);
 
}
 
 
 
var cat_prefix = null;
 
var noSuggestions = false;
 
var suggestionEngines = {
 
opensearch :
 
{ uri    : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1' // $1 = search term
 
,handler : // Function to convert result of uri into an array of category names
 
function (queryResult, queryKey) {
 
if (queryResult && queryResult.length >= 2) {
 
var key = queryResult[0].substring(queryResult[0].indexOf(':') + 1);
 
var titles = queryResult[1];
 
var exists = false;
 
if (!cat_prefix) cat_prefix = new RegExp ('^(' + HotCat.category_regexp + ':)');
 
for (var i = 0; i < titles.length; i++) {
 
cat_prefix.lastIndex = 0;
 
var m = cat_prefix.exec (titles[i]);
 
if (m && m.length > 1) {
 
titles[i] = titles[i].substring (titles[i].indexOf (':') + 1); // rm namespace
 
if (key == titles[i]) exists = true;
 
} else {
 
titles.splice (i, 1); // Nope, it's not a category after all.
 
i--;
 
}
 
}
 
titles.exists = exists;
 
if (queryKey != key) titles.normalized = key; // Remember the NFC normalized key we got back from the server
 
return titles;
 
}
 
return null;
 
}
 
}
 
,internalsearch :
 
{ uri    : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1&apprefix=$1'
 
,handler :
 
function (queryResult, queryKey) {
 
if (queryResult && queryResult.query && queryResult.query.allpages) {
 
var titles = queryResult.query.allpages;
 
for (var i = 0; i < titles.length; i++) {
 
titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
 
}
 
return titles;
 
}
 
return null;
 
}
 
}
 
,exists :
 
{ uri    : '/api.php?format=json&action=query&prop=info&titles=Category:$1'
 
,handler :
 
function (queryResult, queryKey) {
 
if (queryResult && queryResult.query && queryResult.query.pages && !queryResult.query.pages[-1]) {
 
// Should have exactly 1
 
for (var p in queryResult.query.pages) {
 
var title = queryResult.query.pages[p].title;
 
title = title.substring (title.indexOf (':') + 1);
 
var titles = [title];
 
titles.exists = true;
 
if (queryKey != title) titles.normalized = title; // NFC
 
return titles;
 
}
 
}
 
return null;
 
}
 
}
 
,subcategories :
 
// I don't understand why they didn't map cmnamespace=14 automatically to cmtype=subcat,
 
// which gives better results and is faster.
 
{ uri    : '/api.php?format=json&action=query&list=categorymembers&cmtype=subcat&cmlimit=max&cmtitle=Category:$1'
 
,handler :
 
function (queryResult, queryKey) {
 
if (queryResult && queryResult.query && queryResult.query.categorymembers) {
 
var titles = queryResult.query.categorymembers;
 
for (var i = 0; i < titles.length; i++) {
 
titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
 
}
 
return titles;
 
}
 
return null;
 
}
 
}
 
,parentcategories :
 
{ uri    : '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max'
 
,handler :
 
function (queryResult, queryKey) {
 
if (queryResult && queryResult.query && queryResult.query.pages) {
 
for (var p in queryResult.query.pages) {
 
if (queryResult.query.pages[p].categories) {
 
var titles = queryResult.query.pages[p].categories;
 
for (var i = 0; i < titles.length; i++) {
 
titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
 
}
 
return titles;
 
}
 
}
 
}
 
return null;
 
}
 
}
 
};
 
  
var suggestionConfigs = {
+
updateCounter: function() {
searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}, show: true, temp: false, noCompletion : false}
+
this.counterCurrent++;
,pagelist    : {name: 'Page list', engines: ['internalsearch', 'exists'], cache: {}, show: true, temp: false, noCompletion : false}
+
if ( this.counterCurrent > this.counterNeeded ) {
,combined    : {name: 'Combined search', engines: ['opensearch', 'internalsearch'], cache: {}, show: true, temp: false, noCompletion : false}
+
this.displayResult();
,subcat      : {name: 'Subcategories', engines: ['subcategories'], cache: {}, show: true, temp: true, noCompletion : true}
 
,parentcat  : {name: 'Parent categories', engines: ['parentcategories'], cache: {}, show: true, temp: true, noCompletion : true}
 
};
 
 
 
function CategoryEditor () { this.initialize.apply (this, arguments); }
 
CategoryEditor.UNCHANGED      = 0;
 
CategoryEditor.OPEN          = 1; // Open, but no input yet
 
CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
 
CategoryEditor.CHANGED        = 3;
 
CategoryEditor.DELETED        = 4;
 
 
 
// IE6 sometimes forgets to redraw the list when editors are opened or closed.
 
// Adding/removing a dummy element helps, at least when opening editors.
 
var dummyElement = make ('\xa0', true);
 
 
 
function forceRedraw () {
 
if (dummyElement.parentNode) {
 
document.body.removeChild (dummyElement);
 
 
} else {
 
} else {
document.body.appendChild (dummyElement);
+
this.domCounter.text( this.counterCurrent );
 
}
 
}
}
+
},
  
// Event keyCodes that we handle in the text input field/suggestion list.
+
displayResult: function() {
var BS = 8, TAB = 9, RET = 13, ESC = 27, SPACE = 32, PGUP = 33, PGDOWN = 34, UP = 38, DOWN = 40, DEL = 46, IME = 229;
+
document.body.style.cursor = 'auto';
 +
$( '.cat_a_lot_feedback' )
 +
.addClass( 'cat_a_lot_done' );
 +
$( '.ui-dialog-content' )
 +
.height( 'auto' );
 +
var rep = this.domCounter.parent();
 +
rep.html( '<h3>' + msg( 'done' ) + '</h3>' );
 +
rep.append( msg( 'all-done' ) + '<br />' );
  
function makeActive (which) {
+
var close = $( '<a>' )
if (which.is_active) return;
+
.text( msgPlain( 'return-to-page' ) );
for (var i = 0; i < editors.length; i++) {
+
close.click( function() {
if (editors[i] !== which) editors[i].inactivate ();
+
catALot.progressDialog.remove();
 +
catALot.toggleAll( false );
 +
} );
 +
rep.append( close );
 +
if ( this.alreadyThere.length ) {
 +
rep.append( '<h5>' + msg( 'skipped-already', this.alreadyThere.length ) + '</h5>' );
 +
rep.append( this.alreadyThere.join( '<br>' ) );
 
}
 
}
which.is_active = true;
+
if ( this.notFound.length ) {
if (which.dab) {
+
rep.append( '<h5>' + msg( 'skipped-not-found', this.notFound.length ) + '</h5>' );
showDab (which);
+
rep.append( this.notFound.join( '<br>' ) );
} else {
 
// Check for programmatic value changes.
 
var expectedInput = which.lastRealInput || which.lastInput || "";
 
var actualValue = which.text.value || "";
 
if (expectedInput.length === 0 && actualValue.length > 0 || expectedInput.length > 0 && actualValue.indexOf (expectedInput) !== 0) {
 
// Somehow the field's value appears to have changed, and which.lastSelection therefore is no longer valid. Try to set the
 
// cursor at the end of the category, and do not display the old suggestion list.
 
which.showsList = false;
 
var v = actualValue.split('|');
 
which.lastRealInput = which.lastInput = v[0];
 
if (v.length > 1) which.currentKey = v[1];
 
if (which.lastSelection) which.lastSelection = {start: v[0].length, end: v[0].length};
 
}
 
if (which.showsList) which.displayList();
 
if (which.lastSelection) {
 
if (is_webkit) {
 
// WebKit (Safari, Chrome) has problems selecting inside focus()
 
// See http://code.google.com/p/chromium/issues/detail?id=32865#c6
 
window.setTimeout (
 
function () { which.setSelection (which.lastSelection.start, which.lastSelection.end); }
 
,1
 
);
 
} else {
 
which.setSelection (which.lastSelection.start, which.lastSelection.end);
 
}
 
}
 
 
}
 
}
}
+
if ( this.connectionError.length ) {
 
+
rep.append( '<h5>' + msg( 'skipped-server', this.connectionError.length ) + '</h5>' );
function showDab (which) {
+
rep.append( this.connectionError.join( '<br>' ) );
if (!which.is_active) {
 
makeActive(which);
 
} else {
 
which.showSuggestions (which.dab, false, null, null); // do autocompletion, no key, no engine selector
 
which.dab = null;
 
 
}
 
}
}
 
  
CategoryEditor.prototype = {
+
},
  
initialize : function (line, span, after, key, is_hidden) {
+
moveHere: function( targetcat ) {
// If a span is given, 'after' is the category title, otherwise it may be an element after which to
+
this.doSomething( targetcat, 'move' );
// insert the new span. 'key' is likewise overloaded; if a span is given, it is the category key (if
+
},
// known), otherwise it is a boolean indicating whether a bar shall be prepended.
 
if (!span) {
 
this.isAddCategory = true;
 
// Create add span and append to catLinks
 
this.originalCategory = "";
 
this.originalKey = null;
 
this.originalExists  = false;
 
if (!newDOM) {
 
span = make ('span');
 
span.className = 'noprint';
 
if (key) {
 
span.appendChild (make (' | ', true));
 
if (after) {
 
after.parentNode.insertBefore (span, after.nextSibling);
 
after = after.nextSibling;
 
} else {
 
line.appendChild (span);
 
}
 
} else if (line.firstChild) {
 
span.appendChild (make (' ', true));
 
line.appendChild (span);
 
}
 
}
 
this.linkSpan = make ('span');
 
this.linkSpan.className = 'noprint nopopups hotcatlink';
 
var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.open.bind (this);
 
lk.appendChild (make (HotCat.links.add, true)); lk.title = HotCat.tooltips.add;
 
this.linkSpan.appendChild (lk);
 
span = make (newDOM ? 'li' : 'span');
 
span.className = 'noprint';
 
if (is_rtl) span.dir = 'rtl';
 
span.appendChild (this.linkSpan);
 
if (after)
 
after.parentNode.insertBefore (span, after.nextSibling);
 
else
 
line.appendChild (span);
 
this.normalLinks = null;
 
this.undelLink = null;
 
this.catLink = null;
 
} else {
 
if (is_rtl) span.dir = 'rtl';
 
this.isAddCategory = false;
 
this.catLink = span.firstChild;
 
this.originalCategory = after;
 
this.originalKey = (key && key.length > 1) ? key.substr(1) : null; // > 1 because it includes the leading bar
 
this.originalExists  = !hasClass (this.catLink, 'new');
 
// Create change and del links
 
this.makeLinkSpan ();
 
if (!this.originalExists && this.upDownLinks) this.upDownLinks.style.display = 'none';
 
span.appendChild (this.linkSpan);
 
}
 
this.originalHidden    = is_hidden;
 
this.line              = line;
 
this.engine            = HotCat.suggestions;
 
this.span              = span;
 
this.currentCategory    = this.originalCategory;
 
this.currentExists      = this.originalExists;
 
this.currentHidden      = this.originalHidden;
 
this.currentKey        = this.originalKey;
 
this.state              = CategoryEditor.UNCHANGED;
 
this.lastSavedState    = CategoryEditor.UNCHANGED;
 
this.lastSavedCategory  = this.originalCategory;
 
this.lastSavedKey      = this.originalKey;
 
this.lastSavedExists    = this.originalExists;
 
this.lastSavedHidden    = this.originalHidden;
 
if (this.catLink && this.currentKey) {
 
this.catLink.title = this.currentKey;
 
}
 
editors[editors.length] = this;
 
},
 
  
makeLinkSpan : function () {
+
copyHere: function( targetcat ) {
this.normalLinks = make ('span');
+
this.doSomething( targetcat, 'copy' );
var lk = null;
+
},
if (this.originalCategory && this.originalCategory.length > 0) {
+
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.remove.bind (this);
+
addHere: function( targetcat ) {
lk.appendChild (make (HotCat.links.remove, true)); lk.title = HotCat.tooltips.remove;
+
this.doSomething( targetcat, 'add' );
this.normalLinks.appendChild (make (' ', true));
+
},
this.normalLinks.appendChild (lk);
 
}
 
if (!HotCat.template_categories[this.originalCategory]) {
 
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.open.bind (this);
 
lk.appendChild (make (HotCat.links.change, true)); lk.title = HotCat.tooltips.change;
 
this.normalLinks.appendChild (make (' ', true));
 
this.normalLinks.appendChild (lk);
 
if (!noSuggestions && HotCat.use_up_down) {
 
this.upDownLinks = make ('span');
 
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.down.bind (this);
 
lk.appendChild (make (HotCat.links.down, true)); lk.title = HotCat.tooltips.down;
 
this.upDownLinks.appendChild (make (' ', true));
 
this.upDownLinks.appendChild (lk);
 
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.up.bind (this);
 
lk.appendChild (make (HotCat.links.up, true)); lk.title = HotCat.tooltips.up;
 
this.upDownLinks.appendChild (make (' ', true));
 
this.upDownLinks.appendChild (lk);
 
this.normalLinks.appendChild (this.upDownLinks);
 
}
 
}
 
this.linkSpan = make ('span');
 
this.linkSpan.className = 'noprint nopopups hotcatlink';
 
this.linkSpan.appendChild (this.normalLinks);
 
this.undelLink = make ('span');
 
this.undelLink.className = 'nopopups hotcatlink';
 
this.undelLink.style.display = 'none';
 
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.restore.bind (this);
 
lk.appendChild (make (HotCat.links.restore, true)); lk.title = HotCat.tooltips.restore;
 
this.undelLink.appendChild (make (' ', true));
 
this.undelLink.appendChild (lk);
 
this.linkSpan.appendChild (this.undelLink);
 
},
 
  
invokeSuggestions : function (dont_autocomplete) {
+
remove: function() {
if (this.engine && suggestionConfigs[this.engine] && suggestionConfigs[this.engine].temp && !dont_autocomplete) {
+
this.doSomething( '', 'remove' );
this.engine = HotCat.suggestions; // Reset to a search upon input
+
},
}
 
this.state = CategoryEditor.CHANGE_PENDING;
 
var self = this;
 
window.setTimeout (function () {self.textchange (dont_autocomplete);}, HotCat.suggest_delay);
 
},
 
  
makeForm : function () {
+
doSomething: function( targetcat, mode ) {
var form = make ('form');
+
var files = this.getMarkedLabels();
form.method = 'POST'; form.onsubmit = this.accept.bind (this);
+
if ( files.length === 0 ) {
this.form = form;
+
alert( msgPlain( 'none-selected' ) );
var self = this;
+
return;
var text = make ('input'); text.type = 'text'; text.size = HotCat.editbox_width;
+
}
if (!noSuggestions) {
+
this.notFound = [];
// Be careful here to handle IME input. This is browser/OS/IME dependent, but basically there are two mechanisms:
+
this.alreadyThere = [];
// - Modern (DOM Level 3) browsers use compositionstart/compositionend events to signal composition; if the
+
this.connectionError = [];
//  composition is not canceled, there'll be a textInput event following. During a composition key events are
+
this.counterCurrent = 1;
//  either all suppressed (FF/Gecko), or otherwise have keyDown === IME for all keys (Webkit).
+
this.counterNeeded = files.length;
//  - Webkit sends a textInput followed by keyDown === IME and a keyUp with the key that ended composition.
+
mw.loader.using( [ 'jquery.ui.dialog', 'mediawiki.RegExp' ], function() {
//  - Gecko doesn't send textInput but just a keyUp with the key that ended composition, without sending keyDown
+
catALot.showProgress();
//    first. Gecko doesn't send any keydown while IME is active.
+
for ( var i = 0; i < files.length; i++ ) {
// - Older browsers signal composition by keyDown === IME for the first and subsequent keys for a composition. The
+
catALot.getContent( files[ i ], targetcat, mode );
//  first keyDown !== IME is certainly after the end of the composition. Typically, composition end can also be
 
//  detected by a keyDown IME with a keyUp of space, tab, escape, or return. (Example: IE8)
 
text.onkeyup =
 
function (evt) {
 
evt = evt || window.event || window.Event; // W3C, IE, Netscape
 
var key = evt.keyCode || 0;
 
if (self.ime && self.lastKey === IME && !self.usesComposition && (key === TAB || key === RET || key == ESC || key === SPACE)) self.ime = false;
 
if (self.ime) return true;
 
if (key === UP || key === DOWN || key === PGUP || key === PGDOWN) {
 
// In case a browser doesn't generate keypress events for arrow keys...
 
if (self.keyCount === 0) return self.processKey (evt);
 
} else {
 
if (key === ESC && self.lastKey !== IME) {
 
if (!self.resetKeySelection ()) {
 
// No undo of key selection: treat ESC as "cancel".
 
self.cancel ();
 
return;
 
}
 
}
 
// Also do this for ESC as a workaround for Firefox bug 524360
 
// https://bugzilla.mozilla.org/show_bug.cgi?id=524360
 
self.invokeSuggestions (key === BS || key === DEL || key === ESC);
 
}
 
return true;
 
};
 
text.onkeydown =
 
function (evt) {
 
evt = evt || window.event || window.Event; // W3C, IE, Netscape
 
var key = evt.keyCode || 0;
 
self.lastKey = key;
 
self.keyCount = 0;
 
// DOM Level < 3 IME input
 
if (!self.ime && key === IME && !self.usesComposition) {
 
// self.usesComposition catches browsers that may emit spurious keydown IME after a composition has ended
 
self.ime = true;
 
} else if (self.ime && key !== IME && !(key >= 16 && key <= 20 || key >= 91 && key <= 93 || key === 144)) {
 
// Ignore control keys: ctrl, shift, alt, alt gr, caps lock, windows/apple cmd keys, num lock. Only the windows keys
 
// terminate IME (apple cmd doesn't), but they also cause a blur, so it's OK to ignore them here.
 
// Note: Safari 4 (530.17) propagates ESC out of an IME composition (observed at least on Win XP).
 
self.ime = false;
 
}
 
if (self.ime) return true;
 
// Handle return explicitly, to override the default form submission to be able to check for ctrl
 
if (key === RET) return self.accept (evt);
 
// Inhibit default behavior of ESC (revert to last real input in FF: we do that ourselves)
 
return (key === ESC) ? evtKill(evt) : true;
 
};
 
// And handle continued pressing of arrow keys
 
text.onkeypress = function (evt) {self.keyCount++; return self.processKey (evt);};
 
$(text).on ('focus', function () { makeActive(self); });
 
// On IE, blur events are asynchronous, and may thus arrive after the element has lost the focus. Since IE
 
// can get the selection only while the element is active (has the focus), we may not always get the selection.
 
// Therefore, use an IE-specific synchronous event on IE...
 
// Don't test for text.selectionStart being defined; FF3.6.4 raises an exception when trying to access that
 
// property while the element is not being displayed.
 
$(text).on (
 
(typeof text.onbeforedeactivate != 'undefined' && text.createTextRange) ? 'beforedeactivate' : 'blur'
 
, this.saveView.bind (this)
 
);
 
// DOM Level 3 IME handling
 
try {
 
// Setting lastKey = IME provides a fake keyDown for Gecko's single keyUp after a cmposition. If we didn't do this,
 
// cancelling a composition via ESC would also cancel and close the whole category input editor.
 
$(text).on ('compositionstart', function (evt) { self.lastKey = IME; self.usesComposition = true; self.ime = true; });
 
$(text).on ('compositionend', function (evt) { self.lastKey = IME; self.usesComposition = true; self.ime = false; });
 
$(text).on ('textInput', function (evt) { self.ime = false; self.invokeSuggestions(false); });
 
} catch (any) {
 
// Just in case some browsers might produce exceptions with these DOM Level 3 events
 
}
 
$(text).on ('blur', function (evt) { self.usesComposition = false; self.ime = false; });
 
 
}
 
}
this.text = text;
+
} );
 
+
},
this.icon = make ('img');
 
  
var list = null;
+
doAPICall: function( params, callback ) {
if (!noSuggestions) {
+
params.format = 'json';
list = make ('select');
+
var i = 0,
list.onclick    = function (e) { if (self.highlightSuggestion(0)) self.textchange (false, true); };
+
apiUrl = this.apiUrl,
list.ondblclick = function (e) { if (self.highlightSuggestion(0)) self.accept (e); };
+
doCall,
list.onchange = function (e) { self.highlightSuggestion(0); self.text.focus(); };
+
handleError = function( jqXHR, textStatus, errorThrown ) {
list.onkeyup =
+
if ( window.console && $.isFunction( window.console.log ) ) {
function (evt) {
+
window.console.log( 'Error: ', jqXHR, textStatus, errorThrown );
evt = evt || window.event || window.Event; // W3C, IE, Netscape
 
if (evt.keyCode === ESC) {
 
self.resetKeySelection ();
 
self.text.focus();
 
window.setTimeout (function () {self.textchange (true);}, HotCat.suggest_delay);
 
} else if (evt.keyCode === RET) {
 
self.accept (evt);
 
}
 
};
 
if (!HotCat.fixed_search) {
 
var engineSelector = make ('select');
 
for (var key in suggestionConfigs) {
 
if (suggestionConfigs[key].show) {
 
var opt = make ('option');
 
opt.value = key;
 
if (key == this.engine) opt.selected = true;
 
opt.appendChild (make (suggestionConfigs[key].name, true));
 
engineSelector.appendChild (opt);
 
}
 
}
 
engineSelector.onchange =
 
function () {
 
self.engine = self.engineSelector.options[self.engineSelector.selectedIndex].value;
 
self.text.focus();
 
self.textchange (true, true); // Don't autocomplete, force re-display of list
 
};
 
this.engineSelector = engineSelector;
 
 
}
 
}
}
+
if ( i < 4 ) {
this.list = list;
+
window.setTimeout( doCall, 300 );
 
+
i++;
function button_label (id, defaultText) {
+
} else if ( params.title ) {
var label = null;
+
this.connectionError.push( params.title );
if (   onUpload
+
this.updateCounter();
&& typeof UFUI != 'undefined'
+
return;
&& typeof UIElements != 'undefined'
 
&& typeof UFUI.getLabel == 'function')
 
{
 
try {
 
label = UFUI.getLabel (id, true);
 
// Extract the plain text. IE doesn't know that Node.TEXT_NODE === 3
 
while (label && label.nodeType != 3) label = label.firstChild;
 
} catch (ex) {
 
label = null;
 
}
 
 
}
 
}
if (!label || !label.data) return defaultText;
+
};
return label.data;
+
doCall = function() {
}
+
$.ajax( {
 +
url: apiUrl,
 +
cache: false,
 +
dataType: 'json',
 +
data: params,
 +
type: 'POST',
 +
success: callback,
 +
error: handleError
 +
} );
 +
};
 +
doCall();
 +
},
  
// Do not use type 'submit'; we cannot detect modifier keys if we do
+
createCatLinks: function( symbol, list ) {
var OK = make ('input'); OK.type = 'button';
+
list.sort();
OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
+
var domlist = $resultList.find( 'table' );
OK.onclick = this.accept.bind (this);
+
for ( var i = 0; i < list.length; i++ ) {
this.ok = OK;
+
var $tr = $( '<tr>' );
  
var cancel = make ('input'); cancel.type = 'button';
+
var $link = $( '<a>' ),
cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
+
$add, $move, $copy;
cancel.onclick = this.cancel.bind (this);
 
this.cancelButton = cancel;
 
  
var span = make ('span');
+
$link.text( list[ i ] );
span.className = 'hotcatinput';
+
$tr.data( 'cat', list[ i ] );
span.style.position = 'relative';
+
$link.click( function() {
// FF3.6: add the input field first, then the two absolutely positioned elements. Otherwise, FF3.6 may leave the
+
catALot.updateCats( $( this ).closest( 'tr' ).data( 'cat' ) );
// suggestions and the selector at the right edge of the screen if display of the input field causes a re-layout
+
} );
// moving the form to the front of the next line.
 
span.appendChild (text);
 
 
 
// IE8/IE9: put some text into this span (a0 is nbsp) and make sure it always stays on the
 
// same line as the input field, otherwise, IE8/9 miscalculates the height of the span and
 
// then the engine selector may overlap the input field.
 
span.appendChild (make ('\xa0', true));
 
span.style.whiteSpace = 'nowrap';
 
 
 
if (list) span.appendChild (list);
 
if (this.engineSelector) span.appendChild (this.engineSelector);
 
if (!noSuggestions) span.appendChild (this.icon);
 
span.appendChild (OK);
 
span.appendChild (cancel);
 
form.appendChild(span);
 
form.style.display = 'none';
 
this.span.appendChild (form);
 
},
 
 
 
display : function (evt) {
 
if (this.isAddCategory && !onUpload) {
 
var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
 
}
 
if (!commitButton && !onUpload) {
 
for (var i = 0; i < editors.length; i++) {
 
if (editors[i].state != CategoryEditor.UNCHANGED) {
 
setMultiInput();
 
break;
 
}
 
}
 
}
 
if (!this.form) {
 
this.makeForm ();
 
}
 
if (this.list) this.list.style.display = 'none';
 
if (this.engineSelector) this.engineSelector.style.display = 'none';
 
this.currentCategory = this.lastSavedCategory;
 
this.currentExists  = this.lastSavedExists;
 
this.currentHidden  = this.lastSavedHidden;
 
this.currentKey      = this.lastSavedKey;
 
this.icon.src = armorUri(this.currentExists ? HotCat.existsYes : HotCat.existsNo);
 
this.text.value = this.currentCategory + (this.currentKey !== null ? '|' + this.currentKey : "");
 
this.originalState = this.state;
 
this.lastInput    = this.currentCategory;
 
this.inputExists  = this.currentExists;
 
this.state        = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
 
this.lastSelection = {start: this.currentCategory.length, end: this.currentCategory.length};
 
this.showsList = false;
 
// Display the form
 
if (this.catLink) this.catLink.style.display = 'none';
 
this.linkSpan.style.display = 'none';
 
this.form.style.display = 'inline';
 
this.ok.disabled = false;
 
// Kill the event before focussing, otherwise IE will kill the onfocus event!
 
var result = evtKill (evt);
 
this.text.focus();
 
this.text.readOnly = false;
 
checkMultiInput ();
 
return result;
 
},
 
 
 
show : function (evt, engine, readOnly) {
 
var result = this.display (evt);
 
var v = this.lastSavedCategory;
 
if (v.length === 0) return result;
 
this.text.readOnly = !!readOnly;
 
this.engine = engine;
 
this.textchange (false, true); // do autocompletion, force display of suggestions
 
forceRedraw ();
 
return result;
 
},
 
  
open : function (evt) {
+
$tr.append( $( '<td>' ).text( symbol ) )
return this.show (evt, (this.engine && suggestionConfigs[this.engine].temp) ? HotCat.suggestions : this.engine);
+
.append( $( '<td>' ).append( $link ) );
},
 
  
down : function (evt) {
+
if ( this.origin ) {
return this.show (evt, 'subcat', true);
+
// Can't move to source category
},
+
if ( list[ i ] !== this.origin ) {
 +
$move = $( '<a>' )
 +
.addClass( 'cat_a_lot_move' )
 +
.text( msgPlain( 'move' ) )
 +
.click( function() {
 +
catALot.moveHere( $( this ).closest( 'tr' ).data( 'cat' ) );
 +
} );
  
up : function (evt) {
+
$copy = $( '<a>' )
return this.show (evt, 'parentcat');
+
.addClass( 'cat_a_lot_action' )
},
+
.text( msgPlain( 'copy' ) )
 +
.click( function() {
 +
catALot.copyHere( $( this ).closest( 'tr' ).data( 'cat' ) );
 +
} );
  
cancel : function () {
+
$tr.append( $( '<td>' ).append( $move ), $( '<td>' ).append( $copy ) );
if (this.isAddCategory && !onUpload) {
 
this.removeEditor(); // We added a new adder when opening
 
return;
 
}
 
// Close, re-display link
 
this.inactivate();
 
this.form.style.display = 'none';
 
if (this.catLink) this.catLink.style.display = "";
 
this.linkSpan.style.display = "";
 
this.state = this.originalState;
 
this.currentCategory = this.lastSavedCategory;
 
this.currentKey      = this.lastSavedKey;
 
this.currentExists  = this.lastSavedExists;
 
this.currentHidden  = this.lastSavedHidden;
 
if (this.catLink) {
 
if (this.currentKey && this.currentKey.length > 0) {
 
this.catLink.title = this.currentKey;
 
} else {
 
this.catLink.title = "";
 
 
}
 
}
}
 
if (this.state == CategoryEditor.UNCHANGED) {
 
if (this.catLink) this.catLink.style.backgroundColor = 'transparent';
 
 
} else {
 
} else {
if (!onUpload) {
+
$add = $( '<a>' )
try {
+
.addClass( 'cat_a_lot_action' )
this.catLink.style.backgroundColor = HotCat.bg_changed;
+
.text( msgPlain( 'add' ) )
} catch (ex) {}
+
.click( function() {
}
+
catALot.addHere( $( this ).closest( 'tr' ).data( 'cat' ) );
}
+
} );
checkMultiInput ();
 
forceRedraw ();
 
},
 
  
removeEditor : function () {
+
$tr.append( $( '<td>' ).append( $add ) );
if (!newDOM) {
 
var next = this.span.nextSibling;
 
if (next) next.parentNode.removeChild (next);
 
 
}
 
}
this.span.parentNode.removeChild (this.span);
 
for (var i = 0; i < editors.length; i++) {
 
if (editors[i] == this) {
 
editors.splice (i, 1);
 
break;
 
}
 
}
 
checkMultiInput ();
 
var self = this;
 
window.setTimeout (function () {delete self;}, 10);
 
},
 
  
rollback : function (evt) {
+
domlist.append( $tr );
this.undoLink.parentNode.removeChild (this.undoLink);
+
}
this.undoLink = null;
+
},
this.currentCategory = this.originalCategory;
 
this.currentKey = this.originalKey;
 
this.currentExists = this.originalExists;
 
this.currentHidden = this.originalHidden;
 
this.lastSavedCategory = this.originalCategory;
 
this.lastSavedKey = this.originalKey;
 
this.lastSavedExists = this.originalExists;
 
this.lastSavedHidden = this.originalHidden;
 
this.state = CategoryEditor.UNCHANGED;
 
if (!this.currentCategory || this.currentCategory.length === 0) {
 
// It was a newly added category. Remove the whole editor.
 
this.removeEditor();
 
} else {
 
// Redisplay the link...
 
this.catLink.removeChild (this.catLink.firstChild);
 
this.catLink.appendChild (make (this.currentCategory, true));
 
this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
 
this.catLink.title = this.currentKey || "";
 
this.catLink.className = this.currentExists ? "" : 'new';
 
this.catLink.style.backgroundColor = 'transparent';
 
if (this.upDownLinks) this.upDownLinks.style.display = this.currentExists ? "" : 'none';
 
checkMultiInput ();
 
}
 
return evtKill (evt);
 
},
 
 
 
inactivate : function () {
 
if (this.list) this.list.style.display = 'none';
 
if (this.engineSelector) this.engineSelector.style.display = 'none';
 
this.is_active = false;
 
},
 
  
acceptCheck : function (dontCheck) {
+
getCategoryList: function() {
this.sanitizeInput ();
+
this.catCounter = 0;
var value = this.text.value.split('|');
+
this.getParentCats();
var key  = null;
+
this.getSubCats();
if (value.length > 1) key = value[1];
+
},
var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
 
if (HotCat.capitalizePageNames) v = capitalize (v);
 
this.lastInput = v;
 
v = replaceShortcuts(v, HotCat.shortcuts);
 
if (v.length === 0) {
 
this.cancel ();
 
return false;
 
}
 
if (!dontCheck
 
&& (  conf.wgNamespaceNumber === 14 && v == conf.wgTitle
 
|| HotCat.blacklist && HotCat.blacklist.test(v))
 
  )
 
{
 
this.cancel ();
 
return false;
 
}
 
this.currentCategory = v;
 
this.currentKey = key;
 
this.currentExists = this.inputExists;
 
return true;
 
},
 
  
accept : function (evt) {
+
showCategoryList: function() {
this.noCommit = (evtKeys (evt) & 1) !== 0;
+
var thiscat = [ this.currentCategory ];
var result = evtKill (evt);
 
if (this.acceptCheck ()) {
 
var toResolve = [this];
 
var original  = this.currentCategory;
 
resolveMulti (
 
toResolve
 
,function (resolved) {
 
if (resolved[0].dab) {
 
showDab (resolved[0]);
 
} else {
 
if (resolved[0].acceptCheck(true)) {
 
resolved[0].commit (
 
(resolved[0].currentCategory != original)
 
? HotCat.messages.cat_resolved.replace (/\$1/g, original)
 
: null
 
);
 
}
 
}
 
}
 
);
 
}
 
return result;
 
},
 
  
close : function () {
+
$resultList.empty();
if (!this.catLink) {
+
$resultList.append( '<table>' );
// Create a catLink
 
this.catLink = make ('a');
 
this.catLink.appendChild (make ('foo', true));
 
this.catLink.style.display = 'none';
 
this.span.insertBefore (this.catLink, this.span.firstChild.nextSibling);
 
}
 
this.catLink.removeChild (this.catLink.firstChild);
 
this.catLink.appendChild (make (this.currentCategory, true));
 
this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
 
this.catLink.className = this.currentExists ? "" : 'new';
 
this.lastSavedCategory = this.currentCategory;
 
this.lastSavedKey      = this.currentKey;
 
this.lastSavedExists  = this.currentExists;
 
this.lastSavedHidden  = this.currentHidden;
 
// Close form and redisplay category
 
this.inactivate();
 
this.form.style.display = 'none';
 
this.catLink.title = this.currentKey || "";
 
this.catLink.style.display = "";
 
if (this.isAddCategory) {
 
if (onUpload) {
 
var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
 
}
 
this.isAddCategory = false;
 
this.linkSpan.parentNode.removeChild (this.linkSpan);
 
this.makeLinkSpan ();
 
this.span.appendChild (this.linkSpan);
 
}
 
if (!this.undoLink) {
 
// Append an undo link.
 
var span = make ('span');
 
var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.rollback.bind (this);
 
lk.appendChild (make (HotCat.links.undo, true)); lk.title = HotCat.tooltips.undo;
 
span.appendChild (make (' ', true));
 
span.appendChild (lk);
 
this.normalLinks.appendChild (span);
 
this.undoLink = span;
 
if (!onUpload) {
 
try {
 
this.catLink.style.backgroundColor = HotCat.bg_changed;
 
} catch (ex) {}
 
}
 
}
 
if (this.upDownLinks) this.upDownLinks.style.display = this.lastSavedExists ? "" : 'none';
 
this.linkSpan.style.display = "";
 
this.state = CategoryEditor.CHANGED;
 
checkMultiInput ();
 
forceRedraw ();
 
},
 
  
commit : function (comment) {
+
this.createCatLinks( '↑', this.parentCats );
// Check again to catch problem cases after redirect resolution
+
this.createCatLinks( '→', thiscat );
if (  (  this.currentCategory == this.originalCategory
+
this.createCatLinks( '↓', this.subCats );
&& (this.currentKey == this.originalKey
 
|| this.currentKey === null && this.originalKey.length === 0
 
  )
 
  )
 
|| conf.wgNamespaceNumber == 14 && this.currentCategory == conf.wgTitle
 
|| HotCat.blacklist && HotCat.blacklist.test (this.currentCategory)
 
  )
 
{
 
this.cancel ();
 
return;
 
}
 
if (commitButton || onUpload) {
 
this.close ();
 
} else {
 
this.close ();
 
var self = this;
 
initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {alert (msg);});
 
}
 
},
 
  
remove : function (evt) {
+
document.body.style.cursor = 'auto';
this.doRemove (evtKeys (evt) & 1);
+
// Reset width
return evtKill (evt);
+
$container.width( '' );
},
+
$container.height( '' );
 
+
$container.width( Math.min( $container.width() * 1.1 + 15, $( window ).width() - 10 ) );
doRemove : function (noCommit) {
 
if (this.isAddCategory) { // Empty input on adding a new category
 
this.cancel ();
 
return;
 
}
 
if (!commitButton && !onUpload) {
 
for (var i = 0; i < editors.length; i++) {
 
if (editors[i].state != CategoryEditor.UNCHANGED) {
 
setMultiInput();
 
break;
 
}
 
}
 
}
 
if (commitButton) {
 
this.catLink.title = "";
 
this.catLink.style.cssText += '; text-decoration : line-through !important;';
 
try {
 
this.catLink.style.backgroundColor = HotCat.bg_changed;
 
} catch (ex) {}
 
this.originalState = this.state;
 
this.state = CategoryEditor.DELETED;
 
this.normalLinks.style.display = 'none';
 
this.undelLink.style.display = "";
 
checkMultiInput ();
 
} else {
 
if (onUpload) {
 
// Remove this editor completely
 
this.removeEditor ();
 
} else {
 
this.originalState = this.state;
 
this.state = CategoryEditor.DELETED;
 
this.noCommit = noCommit || HotCat.del_needs_diff;
 
var self = this;
 
initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {self.state = self.originalState; alert (msg);});
 
}
 
}
 
},
 
  
restore : function (evt) {
+
$resultList.css( {
// Can occur only if we do have a commit button and are not on the upload form
+
maxHeight: this.setHeight + 'px',
this.catLink.title = this.currentKey || "";
+
height: ''
this.catLink.style.textDecoration = "";
+
} );
this.state = this.originalState;
+
},
if (this.state == CategoryEditor.UNCHANGED) {
 
this.catLink.style.backgroundColor = 'transparent';
 
} else {
 
try {
 
this.catLink.style.backgroundColor = HotCat.bg_changed;
 
} catch (ex) {}
 
}
 
this.normalLinks.style.display = "";
 
this.undelLink.style.display = 'none';
 
checkMultiInput ();
 
return evtKill (evt);
 
},
 
  
// Internal operations
+
updateCats: function( newcat ) {
 +
document.body.style.cursor = 'wait';
  
selectEngine : function (engineName) {
+
this.currentCategory = newcat;
if (!this.engineSelector) return;
+
$resultList.html( '<div class="cat_a_lot_loading">' + msgPlain( 'loading' ) + '</div>' );
for (var i = 0; i < this.engineSelector.options.length; i++) {
+
this.getCategoryList();
this.engineSelector.options[i].selected = this.engineSelector.options[i].value == engineName;
+
},
}
+
},
+
showProgress: function() {
 +
document.body.style.cursor = 'wait';
  
sanitizeInput : function () {
+
this.progressDialog = $( '<div>' )
var v = this.text.value || "";
+
.html( msg( 'editing' ) + ' <span id="cat_a_lot_current">' + this.counterCurrent + '</span> ' + msg( 'of' ) + this.counterNeeded )
v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
+
.dialog( {
var re = new RegExp ('^(' + HotCat.category_regexp + '):');
+
width: 450,
if (re.test (v)) {
+
height: 90,
v = v.substring (v.indexOf (':') + 1).replace(/^(\s|_)+/, "");
+
minHeight: 90,
}
+
modal: true,
if (HotCat.capitalizePageNames) v = capitalize (v);
+
resizable: false,
// Only update the input field if there is a difference. IE8 appears to reset the selection
+
draggable: false,
// and place the cursor at the front upon reset, which makes our autocompletetion become a
+
closeOnEscape: false,
// nuisance. FF and IE6 don't seem to have this problem.
+
dialogClass: 'cat_a_lot_feedback'
if (this.text.value !== null && this.text.value != v)
+
} );
this.text.value = v;
+
$( '.ui-dialog-titlebar' )
},
+
.hide();
 +
this.domCounter = $( '#cat_a_lot_current' );
  
makeCall : function (url, callbackObj, engine, queryKey, cleanKey) {
+
},
var cb = callbackObj;
 
var e  = engine;
 
var v  = queryKey;
 
var z  = cleanKey;
 
var thisObj = this;
 
  
function done () {
+
run: function() {
cb.callsMade++;
+
if ( $( '.cat_a_lot_enabled' ).length ) {
if (cb.callsMade === cb.nofCalls) {
+
this.makeClickable();
if (cb.exists) cb.allTitles.exists = true;
+
$dataContainer
if (cb.normalized) cb.allTitles.normalized = cb.normalized;
+
.show();
if (!cb.dontCache && !suggestionConfigs[cb.engineName].cache[z]) {
+
$container
suggestionConfigs[cb.engineName].cache[z] = cb.allTitles;
+
.resizable( {
 +
handles: 'n',
 +
alsoResize: '#cat_a_lot_category_list',
 +
resize: function() {
 +
$( this )
 +
.css( {
 +
left: '',
 +
top: ''
 +
} );
 +
catALot.setHeight = $( this ).height();
 +
$resultList
 +
.css( {
 +
maxHeight: '',
 +
width: ''
 +
} );
 
}
 
}
thisObj.text.readOnly = false;
+
} )
if (!cb.cancelled) thisObj.showSuggestions (cb.allTitles, cb.noCompletion, v, cb.engineName);
+
/*.draggable( { // FIXME: Box get static if sametime resize
if (cb === thisObj.callbackObj) thisObj.callbackObj = null;
+
cursor: 'move',
delete cb;
+
start: function() {
}
+
$( this ).css( 'height', $( this ).height() );
}
 
 
 
getJSON ({
 
  uri : url
 
,success : function (json) {
 
var titles = e.handler (json, z);
 
if (titles && titles.length > 0) {
 
if (cb.allTitles === null) {
 
cb.allTitles = titles;
 
} else {
 
cb.allTitles = cb.allTitles.concat (titles);
 
 
}
 
}
if (titles.exists) cb.exists = true;
+
} )*/;
if (titles.normalized) cb.normalized = titles.normalized;
+
$resultList
}
+
.css( {
done();
+
maxHeight: '450px'
  }
+
} );
,error : function (req) {if (!req) noSuggestions = true; cb.dontCache = true; done(); }
 
});
 
},
 
  
callbackObj : null,
+
this.updateCats( this.origin || 'Images' );
 
+
$link.text( 'X' );
textchange : function (dont_autocomplete, force) {
+
} else {
// Hide all other lists
+
$dataContainer
makeActive (this);
+
.hide();
// Get input value, omit sort key, if any
+
$container
this.sanitizeInput ();
+
// .draggable( 'destroy' )
var v = this.text.value;
+
.resizable( 'destroy' )
// Disregard anything after a pipe.
+
.removeAttr( 'style' );
var pipe = v.indexOf ('|');
+
// Unbind click handlers
if (pipe >= 0) {
+
this.labels.unbind( 'click.catALot' );
this.currentKey = v.substring (pipe+1);
+
$link.text( 'Cat-a-lot' );
v = v.substring (0, pipe);
+
}
} else {
+
},
this.currentKey = null;
 
}
 
if (this.lastInput == v && !force) return; // No change
 
if (this.lastInput != v) checkMultiInput ();
 
this.lastInput = v;
 
this.lastRealInput = v;
 
 
 
// Mark blacklisted inputs.
 
this.ok.disabled = v.length > 0 && HotCat.blacklist && HotCat.blacklist.test (v);
 
 
 
if (noSuggestions) {
 
// No Ajax: just make sure the list is hidden
 
if (this.list) this.list.style.display = 'none';
 
if (this.engineSelector) this.engineSelector.style.display = 'none';
 
if (this.icon) this.icon.style.display = 'none';
 
return;
 
}
 
 
 
if (v.length === 0) { this.showSuggestions([]); return; }
 
var cleanKey = v.replace(/[\u200E\u200F\u202A-\u202E]/g, "").replace(wikiTextBlankRE, ' ');
 
cleanKey = replaceShortcuts(cleanKey, HotCat.shortcuts);
 
cleanKey = cleanKey.replace(/^\s+|\s+$/g, '');
 
if (cleanKey.length === 0) { this.showSuggestions([]); return; }
 
 
 
if (this.callbackObj) this.callbackObj.cancelled = true;
 
var engineName  = suggestionConfigs[this.engine] ? this.engine : 'combined';
 
 
 
dont_autocomplete = dont_autocomplete || suggestionConfigs[engineName].noCompletion;
 
if (suggestionConfigs[engineName].cache[cleanKey]) {
 
this.showSuggestions (suggestionConfigs[engineName].cache[cleanKey], dont_autocomplete, v, engineName);
 
return;
 
}
 
 
 
var engines = suggestionConfigs[engineName].engines;
 
this.callbackObj =
 
{allTitles: null, callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engineName};
 
this.makeCalls (engines, this.callbackObj, v, cleanKey);
 
},
 
 
 
makeCalls : function (engines, cb, v, cleanKey) {
 
for (var j = 0; j < engines.length; j++) {
 
var engine = suggestionEngines[engines[j]];
 
var url = conf.wgServer + conf.wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (cleanKey));
 
this.makeCall (url, cb, engine, v, cleanKey);
 
}
 
},
 
  
showSuggestions : function (titles, dontAutocomplete, queryKey, engineName) {
+
manageSettings: function() {
this.text.readOnly = false;
+
mw.loader.using( [ 'ext.gadget.SettingsManager', 'ext.gadget.SettingsUI', 'jquery.ui.progressbar' ], function() {
this.dab = null;
+
catALot._manageSettings();
this.showsList = false;
+
} );
if (!this.list) return;
+
},
if (noSuggestions) {
+
if (this.list) this.list.style.display = 'none';
+
_manageSettings: function() {
if (this.engineSelector) this.engineSelector.style.display = 'none';
+
mw.libs.SettingsUI( this.defaults, 'Cat-a-lot' )
if (this.icon) this.icon.style.display = 'none';
+
.show()
this.inputExists = true; // Default...
+
.done( function( s, verbose, loc, settingsOut, $dlg ) {
return;
+
var mustRestart = false,
}
+
_restart = function() {
this.engineName = engineName;
+
if ( !mustRestart ) {
if (engineName) {
+
return;
if (!this.engineSelector) this.engineName = null;
 
} else {
 
if (this.engineSelector) this.engineSelector.style.display = 'none';
 
}
 
if (queryKey) {
 
if (this.lastInput.indexOf (queryKey) !== 0) return;
 
if (this.lastQuery && this.lastInput.indexOf (this.lastQuery) === 0 && this.lastQuery.length > queryKey.length)
 
return;
 
}
 
this.lastQuery = queryKey;
 
 
 
// Get current input text
 
var v = this.text.value.split('|');
 
var key = v.length > 1 ? '|' + v[1] : "";
 
v = (HotCat.capitalizePageNames ? capitalize (v[0]) : v[0]);
 
var vNormalized = v;
 
var knownToExist = titles && titles.exists;
 
var i;
 
if (titles) {
 
if (titles.normalized && v.indexOf(queryKey) === 0) {
 
// We got back a different normalization than what is in the input field
 
vNormalized = titles.normalized + v.substring(queryKey.length);
 
}
 
var vLow = vNormalized.toLowerCase ();
 
// Strip blacklisted categories
 
if (HotCat.blacklist) {
 
for (i = 0; i < titles.length; i++) {
 
if (HotCat.blacklist.test (titles[i])) {
 
titles.splice(i, 1);
 
i--;
 
 
}
 
}
}
 
}
 
titles.sort (
 
function (a, b) {
 
if (a == b) return 0;
 
if (a.indexOf (b) === 0) return 1; // a begins with b: a > b
 
if (b.indexOf (a) === 0) return -1; // b begins with a: a < b
 
// Opensearch may return stuff not beginning with the search prefix!
 
var prefixMatchA = (a.indexOf (vNormalized) === 0 ? 1 : 0);
 
var prefixMatchB = (b.indexOf (vNormalized) === 0 ? 1 : 0);
 
if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
 
// Case-insensitive prefix match!
 
var aLow = a.toLowerCase(), bLow = b.toLowerCase();
 
prefixMatchA = (aLow.indexOf (vLow) === 0 ? 1 : 0);
 
prefixMatchB = (bLow.indexOf (vLow) === 0 ? 1 : 0);
 
if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
 
if (a < b) return -1;
 
if (b < a) return 1;
 
return 0;
 
}
 
);
 
// Remove duplicates and self-references
 
for (i = 0; i < titles.length; i++) {
 
if (  i+1 < titles.length && titles[i] == titles[i+1]
 
|| conf.wgNamespaceNumber == 14 && titles[i] == conf.wgTitle
 
  )
 
{
 
titles.splice (i, 1);
 
i--;
 
}
 
}
 
}
 
if (!titles || titles.length === 0) {
 
if (this.list) this.list.style.display = 'none';
 
if (this.engineSelector) this.engineSelector.style.display = 'none';
 
if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
 
if (this.icon) this.icon.src = armorUri(HotCat.existsNo);
 
this.inputExists = false;
 
}
 
return;
 
}
 
  
var firstTitle = titles[0];
+
$container.remove();
var completed = this.autoComplete (firstTitle, v, vNormalized, key, dontAutocomplete);
+
catALot.labels.unbind( 'click.catALot' );
var existing = completed || knownToExist || firstTitle == replaceShortcuts(v, HotCat.shortcuts);
+
catALot.init();
if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
+
},
this.icon.src = armorUri(existing ? HotCat.existsYes : HotCat.existsNo);
+
_saveToJS = function() {
this.inputExists = existing;
+
var opt = mw.libs.settingsManager.option( {
}
+
optionName: 'catALotPrefs',
if (completed) {
+
value: catALot.settings,
this.lastInput = firstTitle;
+
encloseSignature: 'catALot',
if (titles.length === 1) {
+
encloseBlock: '////////// Cat-a-lot user preferences //////////\n',
this.list.style.display = 'none';
+
triggerSaveAt: /Cat.?A.?Lot/i,
if (this.engineSelector) this.engineSelector.style.display = 'none';
+
editSummary: msgPlain( 'pref-save-summary' )
return;
+
} ),
}
+
oldHeight = $dlg.height(),
}
+
$prog = $( '<div>' );
// (Re-)fill the list
 
while (this.list.firstChild) this.list.removeChild (this.list.firstChild);
 
for (i = 0 ; i < titles.length ; i++) {
 
var opt = make ('option') ;
 
opt.appendChild (make (titles[i], true));
 
opt.selected = completed && (i === 0);
 
this.list.appendChild (opt);
 
}
 
this.displayList();
 
},
 
  
displayList : function () {
+
$dlg.css( 'height', oldHeight )
this.showsList = true;
+
.html( '' );
if (!this.is_active) {
+
$prog.css( {
this.list.style.display = 'none';
+
height: Math.round( oldHeight / 8 ),
if (this.engineSelector) this.engineSelector.style.display = 'none';
+
'margin-top': Math.round( ( 7 * oldHeight ) / 16 )
return;
+
} )
}
+
.appendTo( $dlg );
var nofItems = (this.list.options.length > HotCat.list_size ? HotCat.list_size : this.list.options.length);
 
if (nofItems <= 1) nofItems = 2;
 
this.list.size = nofItems;
 
this.list.style.align    = is_rtl ? 'right' : 'left';
 
this.list.style.zIndex  = 5;
 
this.list.style.position = 'absolute';
 
// Compute initial list position. First the height.
 
var anchor = is_rtl ? 'right' : 'left';
 
var listh = 0;
 
if (this.list.style.display == 'none') {
 
// Off-screen display to get the height
 
this.list.style.top = this.text.offsetTop + 'px';
 
this.list.style[anchor] = '-10000px';
 
this.list.style.display = "";
 
listh = this.list.offsetHeight;
 
this.list.style.display = 'none';
 
} else {
 
listh = this.list.offsetHeight;
 
}
 
// Approximate calculation of maximum list size
 
var maxListHeight = listh;
 
if (nofItems < HotCat.list_size) maxListHeight = (listh / nofItems) * HotCat.list_size;
 
  
function viewport (what) {
+
$dlg.parent()
if (is_webkit && !document.evaluate)
+
.find( '.ui-dialog-buttonpane button' )
return window['inner' + what]; // Safari < 3.0
+
.button( 'option', 'disabled', true );
var s = 'client' + what;
 
if (window.opera) return document.body[s];
 
return (document.documentElement ? document.documentElement[s] : 0)
 
|| document.body[s] || 0;
 
}
 
function scroll_offset (what) {
 
var s = 'scroll' + what;
 
var result = (document.documentElement ? document.documentElement[s] : 0)
 
|| document.body[s] || 0;
 
if (is_rtl && what == 'Left') {
 
// RTL inconsistencies.
 
// FF: 0 at the far right, then increasingly negative values.
 
// IE >= 8: 0 at the far right, then increasingly positive values.
 
// Webkit: scrollWidth - clientWidth at the far right, then down to zero.
 
// IE 7: like webkit; IE6: disabled in RTL anyway since too many problems.
 
// Opera: don't know...
 
if (result < 0) result = - result;
 
if (!is_webkit) {
 
result = scroll_offset('Width') - viewport('Width') - result;
 
}
 
// Now all have webkit behavior, i.e. zero if at the leftmost edge.
 
}
 
return result;
 
}
 
function position (node) {
 
// Stripped-down simplified position function. It's good enough for our purposes.
 
if (node.getBoundingClientRect) {
 
var box    = node.getBoundingClientRect ();
 
return { x : Math.round (box.left + scroll_offset ('Left'))
 
,y : Math.round (box.top + scroll_offset ('Top'))
 
  };
 
}
 
var t = 0, l = 0;
 
do {
 
t = t + (node.offsetTop  || 0);
 
l = l + (node.offsetLeft || 0);
 
node = node.offsetParent;
 
} while (node);
 
return {x : l, y : t};
 
}
 
  
var textPos = position (this.text);
+
opt.save()
var nl = 0;
+
.done( function( text, progress ) {
var nt = 0;
+
$prog.progressbar( {
var offset = 0;
+
value: progress
// Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
+
} );
var textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
+
$prog.fadeOut( function() {
if (this.engineName) {
+
$dlg.dialog( 'close' );
this.engineSelector.style.zIndex = 5;
+
_restart();
this.engineSelector.style.position = 'absolute';
+
} );
this.engineSelector.style.width = textBoxWidth + 'px';
+
} )
// Figure out the height of this selector: display it off-screen, then hide it again.
+
.progress( function( text, progress ) {
if (this.engineSelector.style.display == 'none') {
+
$prog.progressbar( {
this.engineSelector.style[anchor] = '-10000px';
+
value: progress
this.engineSelector.style.top = '0px';
+
} );
this.engineSelector.style.display = "";
+
// TODO: Add "details" to progressbar
offset = this.engineSelector.offsetHeight;
+
} )
this.engineSelector.style.display = 'none';
+
.fail( function( text ) {
} else {
+
$prog.addClass( 'ui-state-error' );
offset = this.engineSelector.offsetHeight;
+
$dlg.prepend( $( '<p>' )
}
+
.text( text ) );
this.engineSelector.style[anchor]  = nl + 'px';
+
} );
}
+
};
if (textPos.y < maxListHeight + offset + 1) {
+
$.each( settingsOut, function( n, v ) {
// The list might extend beyond the upper border of the page. Let's avoid that by placing it
+
if ( v.forcerestart && catALot.settings[ v.name ] !== v.value ) {
// below the input text field.
+
mustRestart = true;
nt = this.text.offsetHeight + offset + 1;
 
if (this.engineName) this.engineSelector.style.top = this.text.offsetHeight + 'px';
 
} else {
 
nt = - listh - offset - 1;
 
if (this.engineName) this.engineSelector.style.top = - (offset + 1) + 'px';
 
}
 
this.list.style.top = nt + 'px';
 
this.list.style.width = ""; // No fixed width (yet)
 
this.list.style[anchor] = nl + 'px';
 
if (this.engineName) {
 
this.selectEngine (this.engineName);
 
this.engineSelector.style.display = "";
 
}
 
this.list.style.display = 'block';
 
// Set the width of the list
 
if (this.list.offsetWidth < textBoxWidth ) {
 
this.list.style.width = textBoxWidth + 'px';
 
return;
 
}
 
// If the list is wider than the textbox: make sure it fits horizontally into the browser window
 
var scroll = scroll_offset ('Left');
 
var view_w = viewport ('Width');
 
var w      = this.list.offsetWidth;
 
var l_pos  = position (this.list);
 
var left  = l_pos.x;
 
var right  = left + w;
 
if (left < scroll || right > scroll + view_w) {
 
if (w > view_w) {
 
w = view_w;
 
this.list.style.width = w + 'px';
 
if (is_rtl) {
 
left = right - w;
 
} else {
 
right = left + w;
 
 
}
 
}
 +
catALot.settings[ v.name ] = v.value;
 +
window.catALotPrefs[ v.name ] = v.value;
 +
} );
 +
switch ( loc ) {
 +
case 'page':
 +
$dlg.dialog( 'close' );
 +
_restart();
 +
break;
 +
case 'account-publicly':
 +
_saveToJS();
 +
break;
 
}
 
}
var relative_offset = 0;
+
} );
if (left < scroll) {
+
},
relative_offset = scroll - left;
+
} else if (right > scroll + view_w) {
+
_initSettings: function() {
relative_offset = - (right - scroll - view_w);
+
if ( this.settings.watchlist ) {
}
+
return;
if (is_rtl) relative_offset = - relative_offset;
 
if (relative_offset !== 0) {
 
this.list.style[anchor] = (nl + relative_offset) + 'px';
 
}
 
}
 
},
 
 
 
autoComplete : function (newVal, actVal, normalizedActVal, key, dontModify) {
 
if (newVal == actVal) return true;
 
if (dontModify || this.ime || !this.canSelect()) return false;
 
// If we can't select properly or an IME composition is ongoing, autocompletion would be a major annoyance to the user.
 
if (newVal.indexOf (actVal) !== 0) {
 
// Maybe it'll work with the normalized value (NFC)?
 
if (normalizedActVal && newVal.indexOf(normalizedActVal) === 0) {
 
if (this.lastRealInput == actVal) this.lastRealInput = normalizedActVal;
 
actVal = normalizedActVal;
 
} else {
 
return false;
 
}
 
}
 
// Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
 
// such that it can be easily removed by typing backspace if the suggestion is unwanted.
 
this.text.focus();
 
this.text.value = newVal + key;
 
this.setSelection (actVal.length, newVal.length);
 
return true;
 
},
 
 
 
canSelect : function () {
 
return this.text.setSelectionRange
 
|| this.text.createTextRange
 
||    typeof this.text.selectionStart != 'undefined'
 
  && typeof this.text.selectionEnd != 'undefined';
 
},
 
 
 
setSelection : function (from, to) {
 
// this.text must be focused (at least on IE)
 
if (!this.text.value) return;
 
if (this.text.setSelectionRange) {    // e.g. khtml
 
this.text.setSelectionRange (from, to);
 
} else if (typeof this.text.selectionStart != 'undefined') {
 
if (from > this.text.selectionStart) {
 
this.text.selectionEnd  = to;
 
this.text.selectionStart = from;
 
} else {
 
this.text.selectionStart = from;
 
this.text.selectionEnd  = to;
 
}
 
} else if (this.text.createTextRange) { // IE
 
var new_selection = this.text.createTextRange();
 
new_selection.move ('character', from);
 
new_selection.moveEnd ('character', to - from);
 
new_selection.select();
 
}
 
},
 
 
 
getSelection : function () {
 
var from = 0, to = 0;
 
// this.text must be focused (at least on IE)
 
if (!this.text.value) {
 
// No text.
 
} else if (typeof this.text.selectionStart != 'undefined') {
 
from = this.text.selectionStart;
 
to  = this.text.selectionEnd;
 
} else if (document.selection && document.selection.createRange) { // IE
 
var rng = document.selection.createRange().duplicate();
 
if (rng.parentElement() === this.text) {
 
try {
 
var textRng = this.text.createTextRange();
 
textRng.move('character', 0);
 
textRng.setEndPoint('EndToEnd', rng);
 
// We're in a single-line input box: no need to care about IE's strange
 
// handling of line ends
 
to = textRng.text.length;
 
textRng.setEndPoint('EndToStart', rng);
 
from = textRng.text.length;
 
} catch (notFocused) {
 
from = this.text.value.length; to = from; // At end of text
 
}
 
}
 
}
 
return {start: from, end: to};
 
},
 
 
 
saveView : function (evt) {
 
this.lastSelection = this.getSelection ();
 
},
 
 
 
processKey : function (evt) {
 
var dir = 0;
 
switch (this.lastKey) {
 
case UP: dir = -1;
 
case DOWN: if (dir === 0) dir = 1;
 
case PGUP: if (dir === 0) dir = -HotCat.list_size;
 
case PGDOWN: if (dir === 0) dir = HotCat.list_size;
 
if (this.list.style.display != 'none') {
 
// List is visible, so there are suggestions
 
this.highlightSuggestion (dir);
 
// Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow
 
// as "place the text cursor at the front", which we don't want here.
 
return evtKill (evt);
 
} else if (  this.keyCount <= 1
 
  && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
 
  )
 
{
 
// If no suggestions displayed, get them, unless we're already getting them.
 
this.textchange ();
 
}
 
break;
 
case ESC: // Inhibit default behavior (revert to last real input in FF: we do that ourselves)
 
return evtKill (evt);
 
}
 
return true;
 
},
 
 
 
highlightSuggestion : function (dir) {
 
if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
 
var curr = this.list.selectedIndex;
 
var tgt  = -1;
 
if (dir === 0) {
 
if (curr < 0 || curr >= this.list.options.length) return false;
 
tgt = curr;
 
} else {
 
tgt = curr < 0 ? 0 : curr + dir;
 
tgt = tgt < 0 ? 0 : tgt;
 
if (tgt >= this.list.options.length) tgt = this.list.options.length - 1;
 
}
 
if (tgt != curr || dir === 0) {
 
if (curr >= 0 && curr < this.list.options.length && dir !== 0) this.list.options[curr].selected = false;
 
this.list.options[tgt].selected = true;
 
// Get current input text
 
var v = this.text.value.split('|');
 
var key = v.length > 1 ? '|' + v[1] : "";
 
var completed = this.autoComplete (this.list.options[tgt].text, this.lastRealInput, null, key, false);
 
if (!completed || this.list.options[tgt].text == this.lastRealInput) {
 
this.text.value = this.list.options[tgt].text + key;
 
if (this.canSelect()) this.setSelection (this.list.options[tgt].text.length, this.list.options[tgt].text.length);
 
}
 
this.lastInput = this.list.options[tgt].text;
 
this.inputExists = true; // Might be wrong if from a dab list...
 
if (this.icon) this.icon.src = armorUri(HotCat.existsYes);
 
this.state = CategoryEditor.CHANGE_PENDING;
 
}
 
return true;
 
},
 
 
 
resetKeySelection : function () {
 
if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
 
var curr = this.list.selectedIndex;
 
if (curr >= 0 && curr < this.list.options.length) {
 
this.list.options[curr].selected = false;
 
// Get current input text
 
var v = this.text.value.split('|');
 
var key = v.length > 1 ? '|' + v[1] : "";
 
// ESC is handled strangely by some browsers (e.g., FF); somehow it resets the input value before
 
// our event handlers ever get a chance to run.
 
var result = v[0] != this.lastInput;
 
if (v[0] != this.lastRealInput) {
 
this.text.value = this.lastRealInput + key;
 
result = true;
 
}
 
this.lastInput = this.lastRealInput;
 
return result;
 
}
 
return false;
 
 
}
 
}
 
+
if ( !window.catALotPrefs ) {
}; // end CategoryEditor.prototype
+
window.catALotPrefs = {};
 
 
function initialize () {
 
// User configurations. Do this here, called from the onload handler, so that users can
 
// override it easily in their own user script files by just declaring variables. JSconfig
 
// is some feature used at Wikimedia Commons.
 
var config = (typeof JSconfig != 'undefined' && JSconfig.keys) ? JSconfig.keys : {};
 
HotCat.dont_add_to_watchlist =
 
(typeof window.hotcat_dont_add_to_watchlist != 'undefined'
 
? !!window.hotcat_dont_add_to_watchlist
 
: (typeof config.HotCatDontAddToWatchlist != 'undefined'
 
? config.HotCatDontAddToWatchlist
 
: HotCat.dont_add_to_watchlist
 
  )
 
);
 
HotCat.no_autocommit =
 
(typeof window.hotcat_no_autocommit != 'undefined'
 
? !!window.hotcat_no_autocommit
 
: (typeof config.HotCatNoAutoCommit != 'undefined'
 
? config.HotCatNoAutoCommit
 
: HotCat.no_autocommit
 
  )
 
);
 
HotCat.del_needs_diff =
 
(typeof window.hotcat_del_needs_diff != 'undefined'
 
? !!window.hotcat_del_needs_diff
 
: (typeof config.HotCatDelNeedsDiff != 'undefined'
 
? config.HotCatDelNeedsDiff
 
: HotCat.del_needs_diff
 
  )
 
);
 
HotCat.suggest_delay = window.hotcat_suggestion_delay
 
|| config['HotCatSuggestionDelay']
 
|| HotCat.suggest_delay;
 
HotCat.editbox_width = window.hotcat_editbox_width
 
|| config['HotCatEditBoxWidth']
 
|| HotCat.editbox_width;
 
HotCat.suggestions  = window.hotcat_suggestions
 
|| config['HotCatSuggestions']
 
|| HotCat.suggestions;
 
if (typeof HotCat.suggestions != 'string' || !suggestionConfigs[HotCat.suggestions])
 
HotCat.suggestions = 'combined';
 
HotCat.fixed_search  =
 
(typeof window.hotcat_suggestions_fixed != 'undefined'
 
? !!window.hotcat_suggestions_fixed
 
: (typeof config.HotCatFixedSuggestions != 'undefined'
 
? config.HotCatFixedSuggestions
 
: HotCat.fixed_search
 
  )
 
);
 
HotCat.single_minor =
 
(typeof window.hotcat_single_changes_are_minor != 'undefined'
 
? !!window.hotcat_single_changes_are_minor
 
: (typeof config.HotCatMinorSingleChanges != 'undefined'
 
? config.HotCatMinorSingleChanges
 
: HotCat.single_minor
 
  )
 
);
 
HotCat.bg_changed = window.hotcat_changed_background
 
|| config.HotCatChangedBackground
 
|| HotCat.bg_changed;
 
HotCat.use_up_down =
 
(typeof window.hotcat_use_category_links != 'undefined'
 
? !!window.hotcat_use_category_links
 
: (typeof config.HotCatUseCategoryLinks != 'undefined'
 
? config.HotCatUseCategoryLinks
 
: HotCat.use_up_down
 
  )
 
);
 
HotCat.list_size = window.hotcat_list_size
 
|| config.HotCatListSize
 
|| HotCat.list_size;
 
// Numeric input, make sure we have a numeric value
 
HotCat.list_size = parseInt (HotCat.list_size, 10);
 
if (isNaN (HotCat.list_size) || HotCat.list_size < 5) HotCat.list_size = 5;
 
if (HotCat.list_size > 15) HotCat.list_size = 15;
 
// Localize search engine names
 
if (HotCat.engine_names) {
 
for (var key in HotCat.engine_names) {
 
if (suggestionConfigs[key] && HotCat.engine_names[key]) {
 
suggestionConfigs[key].name = HotCat.engine_names[key];
 
}
 
}
 
 
}
 
}
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
+
$.each( this.defaults, function( n, v ) {
is_rtl = hasClass (document.body, 'rtl');
+
v.value = catALot.settings[ v.name ] = ( window.catALotPrefs[ v.name ] || v[ 'default' ] );
if (!is_rtl) {
+
v.label = msgPlain( v.label_i18n );
if (document.defaultView && document.defaultView.getComputedStyle) { // Gecko etc.
+
if ( v.select_i18n ) {
is_rtl = document.defaultView.getComputedStyle (document.body, null).getPropertyValue ('direction');
+
v.select = {};
} else if (document.body.currentStyle) { // IE, has subtle differences to getComputedStyle
+
$.each( v.select_i18n, function( i18nk, val ) {
is_rtl = document.body.currentStyle['direction'];
+
v.select[ msgPlain( i18nk ) ] = val;
} else { // Not exactly right, but best effort
+
} );
is_rtl = document.body.style['direction'];
 
 
}
 
}
is_rtl = (is_rtl == 'rtl');
+
} );
 +
},
 +
/* 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',
function can_edit () {
+
'default': false,
var container = null;
+
label_i18n: 'minorpref'
switch (mw.config.get('skin')) {
+
}, {
case 'cologneblue':
+
name: 'editpages',
container = document.getElementById ('quickbar');
+
'default': false,
// Fall through
+
label_i18n: 'editpagespref',
case 'standard':
+
forcerestart: true
case 'nostalgia':
+
}, {
if (!container) container = document.getElementById ('topbar');
+
name: 'docleanup',
var lks = container.getElementsByTagName ('a');
+
'default': false,
for (var i = 0; i < lks.length; i++) {
+
label_i18n: 'docleanuppref'
if (  param ('title', lks[i].href) == conf.wgPageName
+
}, {
&& param ('action', lks[i].href) == 'edit')
+
name: 'subcatcount',
return true;
+
'default': 50,
}
+
min: 5,
return false;
+
max: 500,
default:
+
label_i18n: 'subcatcountpref',
// all modern skins:
+
forcerestart: true
return document.getElementById ('ca-edit') !== null;
+
} ]
}
+
/* eslint-enable camelcase */
return false;
+
};
}
 
  
function setup_upload () {
+
// The gadget is not immediately needed, so let the page load normally
onUpload = true;
+
window.setTimeout( function () {
// Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
+
var userGrp = mw.config.get('wgUserGroups');
var ip = document.getElementById ('mw-htmlform-description') || document.getElementById ('wpDestFile');
+
var trusted = ( $.inArray( 'sysop', userGrp ) > -1 ||
if (!ip) {
+
$.inArray( 'autoconfirmed', userGrp ) > -1 ||
ip = document.getElementById ('wpDestFile');
+
mw.config.get( 'wgRelevantUserName' ) === mw.config.get( 'wgUserName' ) );
while (ip && ip.nodeName.toLowerCase() != 'table') ip = ip.parentNode;
 
}
 
if (!ip) return;
 
var reupload = document.getElementById ('wpForReUpload');
 
var destFile = document.getElementById ('wpDestFile');
 
if (  (reupload && !!reupload.value)
 
|| (destFile && (destFile.disabled || destFile.readOnly)))
 
return; // re-upload form...
 
// Insert a table row with two fields (label and empty category bar)
 
var labelCell = make ('td');
 
var lineCell  = make ('td');
 
// Create the category line
 
catLine = make ('div');
 
catLine.className = 'catlinks';
 
catLine.id = 'catlinks';
 
catLine.style.textAlign = is_rtl ? 'right' : 'left';
 
// We'll be inside a table row. Make sure that we don't have margins or strange borders.
 
catLine.style.margin = '0';
 
catLine.style.border = 'none';
 
lineCell.appendChild (catLine);
 
// Create the label
 
var label = null;
 
if (  typeof UFUI != 'undefined'
 
&& typeof UIElements != 'undefined'
 
&& typeof UFUI.getLabel == 'function'
 
  )
 
{
 
try {
 
label = UFUI.getLabel('wpCategoriesUploadLbl');
 
} catch (ex) {
 
label = null;
 
}
 
}
 
if (!label) {
 
labelCell.id = 'hotcatLabel';
 
labelCell.appendChild (make (HotCat.categories, true));
 
} else {
 
labelCell.id = 'hotcatLabelTranslated';
 
labelCell.appendChild (label);
 
}
 
labelCell.className          = 'mw-label';
 
labelCell.style.textAlign    = 'right';
 
labelCell.style.verticalAlign = 'middle';
 
// Change the onsubmit handler
 
var form = document.getElementById('upload') || document.getElementById('mw-upload-form');
 
if (form) {
 
var newRow = ip.insertRow (-1);
 
newRow.appendChild (labelCell);
 
newRow.appendChild (lineCell);
 
form.onsubmit = (function (oldSubmit) {
 
return function () {
 
var do_submit = true;
 
if (oldSubmit) {
 
if (typeof oldSubmit == 'string')
 
do_submit = eval (oldSubmit);
 
else if (typeof oldSubmit == 'function')
 
do_submit = oldSubmit.apply (form, arguments);
 
}
 
if (!do_submit) return false;
 
closeForm ();
 
// Copy the categories
 
var eb = document.getElementById ('wpUploadDescription')
 
|| document.getElementById ('wpDesc');
 
var addedOne = false;
 
for (var i = 0; i < editors.length; i++) {
 
var t = editors[i].currentCategory;
 
if (!t) continue ;
 
var key = editors[i].currentKey;
 
var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
 
// Only add if not already present
 
var cleanedText = eb.value
 
.replace(/<\!--(\s|\S)*?--\>/g, "")
 
.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
 
if (!find_category (cleanedText, t, true)) {
 
eb.value += '\n' + new_cat;
 
addedOne = true;
 
}
 
}
 
if (addedOne) {
 
// Remove "subst:unc" added by Flinfo if it didn't find categories
 
eb.value = eb.value.replace(/\{\{subst:unc\}\}/g, "");
 
}
 
return true;
 
};
 
}) (form.onsubmit);
 
}
 
}
 
  
var cleanedText = null;
+
switch ( mw.config.get( 'wgNamespaceNumber' ) ) {
 
+
case NS_CAT:
function isOnPage (span) {
+
catALot.searchmode = 'category';
if (span.firstChild.nodeType !== Node.ELEMENT_NODE) return null;
+
catALot.origin = mw.config.get( 'wgTitle' );
var catTitle = title (span.firstChild.getAttribute ('href', 2));
+
break;
if (!catTitle) return null;
+
case -1:
catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' ');
+
catALot.searchmode = {
if (HotCat.blacklist && HotCat.blacklist.test (catTitle)) return null;
+
// list of accepted special page names mapped to search mode names
var result = { title : catTitle, match : ["", "", ""] };
+
Contributions: 'contribs',
if (pageText === null) return result;
+
Listfiles: trusted ? 'listfiles' : null,
if (cleanedText === null) {
+
Prefixindex: trusted ? 'prefix' : null,
cleanedText = pageText
+
Search: 'search',
.replace(/<\!--(\s|\S)*?--\>/g, "")
+
Uncategorizedimages: 'gallery'
.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
+
}[ mw.config.get( 'wgCanonicalSpecialPageName' ) ];
}
+
break;
result.match = find_category (cleanedText, catTitle, true);
 
return result;
 
 
}
 
}
  
var initialized = false;
+
if ( catALot.searchmode ) {
var setupTimeout = null;
+
var loadingLocalizations = 1;
 
+
var loadLocalization = function( lang, cb ) {
function findByClass (scope, tag, className) {
+
loadingLocalizations++;
var result = window.jQuery(scope).find(tag + '.' + className);
+
switch ( lang ) {
return (result && result.length) ? result[0] : null;
+
case 'zh-hk':
}
+
case 'zh-mo':
 
+
case 'zh-tw':
function setup (additionalWork) {
+
lang = 'zh-hant';
if (initialized) return;
+
break;
initialized = true;
+
case 'zh':
if (setupTimeout) {
+
case 'zh-cn':
window.clearTimeout (setupTimeout);
+
case 'zh-my':
setupTimeout = null;
+
case 'zh-sg':
}
+
lang = 'zh-hans';
// Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
+
break;
// each category, and add the + link.
 
catLine =  catLine                                        // Special:Upload
 
|| document.getElementById ('mw-normal-catlinks');
 
var hiddenCats = document.getElementById ('mw-hidden-catlinks');
 
if (!catLine) {
 
var footer = null;
 
if (!hiddenCats) {
 
footer = findByClass (document , 'div' , 'printfooter');
 
if (!footer) return; // Don't know where to insert the category line
 
}
 
catLine = make ('div');
 
catLine.id = 'mw-normal-catlinks';
 
catLine.style.textAlign = is_rtl ? 'right' : 'left';
 
// Add a label
 
var label = make ('a');
 
label.href  = conf.wgArticlePath.replace ('$1', 'Special:Categories');
 
label.title = HotCat.categories;
 
label.appendChild (make (HotCat.categories, true));
 
catLine.appendChild (label);
 
catLine.appendChild (make (':', true));
 
// Insert the new category line
 
var container = (hiddenCats ? hiddenCats.parentNode : document.getElementById ('catlinks'));
 
if (!container) {
 
container = make ('div');
 
container.id = 'catlinks';
 
footer.parentNode.insertBefore (container, footer.nextSibling);
 
}
 
container.className = 'catlinks noprint';
 
container.style.display = "";
 
if (!hiddenCats) {
 
container.appendChild (catLine);
 
} else {
 
container.insertBefore (catLine, hiddenCats);
 
 
}
 
}
} // end if catLine exists
 
if (is_rtl) catLine.dir = 'rtl';
 
  
// Create editors for all existing categories
+
$.ajax( {
 +
url: commons_url,
 +
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 createEditors (line, is_hidden) {
+
function init() {  
var i;
+
$( function() {
var cats = line.getElementsByTagName ('li');
+
catALot.init();
if (cats.length > 0) {
+
} );
newDOM = true; line = cats[0].parentNode;
 
} else {
 
cats = line.getElementsByTagName ('span');
 
 
}
 
}
// Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
+
if ( !loadingLocalizations )
var copyCats = new Array (cats.length);
+
mw.loader.using( [ 'user' ], init, init );
for (i = 0; i < cats.length; i++) copyCats[i] = cats[i];
+
};
var editor = null;
 
for (i = 0; i < copyCats.length; i++) {
 
var test = isOnPage (copyCats[i]);
 
if (test !== null && test.match !== null) {
 
editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2], is_hidden);
 
}
 
}
 
return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
 
}
 
  
var lastSpan = createEditors (catLine, false);
+
if ( mw.config.get( 'wgUserLanguage' ) !== 'en' )
// Create one to add a new category
+
loadLocalization( mw.config.get( 'wgUserLanguage' ), maybeLaunch );
var editor = new CategoryEditor(newDOM ? catLine.getElementsByTagName('ul')[0] : catLine, null, null, lastSpan !== null, false);
+
if ( mw.config.get( 'wgContentLanguage' ) !== 'en' )
if (!onUpload) {
+
loadLocalization( mw.config.get( 'wgContentLanguage' ), maybeLaunch );
if (pageText !== null && hiddenCats) {
+
maybeLaunch();
if (is_rtl) hiddenCats.dir = 'rtl';
 
createEditors (hiddenCats, true);
 
}
 
// And finally add the "multi-mode" span. (Do this at the end, otherwise it ends up in the list above.)
 
var enableMulti = make ('span');
 
enableMulti.className = 'noprint';
 
if (is_rtl) enableMulti.dir = 'rtl';
 
catLine.insertBefore (enableMulti, catLine.firstChild.nextSibling);
 
enableMulti.appendChild (make ('\xa0', true)); // nbsp
 
multiSpan = make ('span');
 
enableMulti.appendChild (multiSpan);
 
multiSpan.innerHTML = '(<a>' + HotCat.addmulti + '</a>)';
 
var lk = multiSpan.getElementsByTagName ('a')[0];
 
lk.onclick = function (evt) {setMultiInput (); checkMultiInput (); return evtKill (evt);};
 
lk.title = HotCat.multi_tooltip;
 
lk.style.cursor = 'pointer';
 
}
 
cleanedText = null;
 
if (typeof additionalWork == 'function') additionalWork();
 
setupCompleted.loaded(); // Trigger signal; execute registered functions
 
$('body').trigger('hotcatSetupCompleted');
 
 
}
 
}
 +
}, 400);
  
function setPage (json) {
+
/**
var startTime = null;
+
*  Derivative work of
if (json && json.query) {
+
(replace "checkboxes" with cat-a-lot labels in your mind)
if (json.query.pages) {
+
*/
var page = json.query.pages[conf.wgArticleId === 0 ? "-1" : "" + conf.wgArticleId];
+
/**
if (page) {
+
* jQuery checkboxShiftClick
if (page.revisions && page.revisions.length > 0) {
+
*
// Revisions are sorted by revision ID, hence [0] is the one we asked for, and possibly there's a [1] if we're
+
* This will enable checkboxes to be checked or unchecked in a row by clicking one, holding shift and clicking another one
// not on the latest revision (edit conflicts and such).
+
*
pageText = page.revisions[0]['*'];
+
* @author Krinkle <krinklemail@gmail.com>
if (page.revisions[0].timestamp) pageTime = page.revisions[0].timestamp.replace (/\D/g, "");
+
* @license GPL v2
if (page.revisions[0].revid) pageTextRevId = page.revisions[0].revid;
+
*/
if (page.revisions.length > 1) conflictingUser = page.revisions[1].user;
+
$.fn.catALotShiftClick = function( cb ) {
}
+
var prevCheckbox = null,
if (page.lastrevid) lastRevId = page.lastrevid;
+
$box = this;
if (page.starttimestamp) startTime = page.starttimestamp.replace (/\D/g, "");
+
// When our boxes are clicked..
pageWatched = typeof page.watched == 'string';
+
$box.bind( 'click.catALot', function( e ) {
editToken = page.edittoken;
 
if (page.langlinks && (!json['query-continue'] || !json['query-continue'].langlinks)) {
 
// We have interlanguage links, and we got them all.
 
var re = "";
 
for (var i = 0; i < page.langlinks.length; i++) {
 
re += (i > 0 ? '|' : "") + page.langlinks[i].lang.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
 
}
 
if (re.length > 0) {
 
interlanguageRE = new RegExp ('((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$');
 
}
 
}
 
  
}
+
// Prevent following the link and text selection
}
+
e.preventDefault();
// Siteinfo
 
if (json.query.general) {
 
HotCat.capitalizePageNames = (json.query.general['case'] == 'first-letter');
 
if (json.query.general.time && !startTime) startTime = json.query.general.time.replace (/\D/g, "");
 
}
 
serverTime = startTime;
 
// Userinfo
 
if (json.query.userinfo && json.query.userinfo.options) {
 
watchCreate = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchcreations == '1';
 
watchEdit  = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchdefault == '1';
 
minorEdits  = json.query.userinfo.options.minordefault == 1;
 
// If the user has the "All edits are minor" preference enabled, we should honor that
 
// for single category changes, no matter what the site configuration is.
 
if (minorEdits) HotCat.single_minor = true;
 
}
 
}
 
}
 
  
function createCommitForm () {
+
// Highlight last selected
if (commitForm) return;
+
$( '#cat_a_lot_last_selected' )
var formContainer = make ('div');
+
.removeAttr( 'id' );
formContainer.style.display = 'none';
+
var $thisControl = $( e.target ),
document.body.appendChild (formContainer);
+
method;
formContainer.innerHTML =
+
if ( !$thisControl.hasClass( 'cat_a_lot_label' ) ) {
'<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="'
+
$thisControl = $thisControl.parents( '.cat_a_lot_label' );
+ conf.wgScript + '?title=' + encodeURIComponent (conf.wgPageName)
 
+ '&action=submit">'
 
+ '<input type="hidden" name="wpTextbox1" />'
 
+ '<input type="hidden" name="model" value="wikitext" />'
 
+ '<input type="hidden" name="format" value="text/x-wiki" />'
 
+ '<input type="hidden" name="wpSummary" value="" />'
 
+ '<input type="checkbox" name="wpMinoredit" value="1" />'
 
+ '<input type="checkbox" name="wpWatchthis" value="1" />'
 
+ '<input type="hidden" name="wpAutoSummary" value="" />'
 
+ '<input type="hidden" name="wpEdittime" />'
 
+ '<input type="hidden" name="wpStarttime" />'
 
+ '<input type="hidden" name="wpDiff" value="wpDiff" />'
 
+ '<input type="hidden" name="oldid" value="0" />'
 
+ '<input type="submit" name="hcCommit" value="hcCommit" />'
 
+ '<input type="hidden" name="wpEditToken" />'
 
+ '<input type="hidden" name="wpUltimateParam" value="1" />'
 
+ '</form>';
 
commitForm = document.getElementById ('hotcatCommitForm');
 
}
 
 
 
function getPage () {
 
// We know we have an article here.
 
if (conf.wgArticleId === 0) {
 
// Doesn't exist yet.
 
if (conf.wgNamespaceNumber === 2) {
 
// Disable on non-existing User pages -- might be a global user page.
 
return;
 
}
 
pageText = "";
 
pageTime = null;
 
setup (createCommitForm);
 
} else {
 
var url = conf.wgServer + conf.wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&rawcontinue=&titles='
 
+ encodeURIComponent (conf.wgPageName)
 
+ '&prop=info%7Crevisions&rvprop=content%7Ctimestamp%7Cids&meta=siteinfo&rvlimit=1&rvstartid='
 
+ conf.wgCurRevisionId;
 
var s = make ('script');
 
s.src = armorUri(url);
 
s.type = 'text/javascript';
 
HotCat.start = function (json) { setPage (json); setup (createCommitForm); };
 
document.getElementsByTagName ('head')[0].appendChild (s);
 
setupTimeout = window.setTimeout (function () {setup (createCommitForm);}, 4000); // 4 sec, just in case getting the wikitext takes longer.
 
 
}
 
}
}
+
$thisControl.attr( 'id', 'cat_a_lot_last_selected' )
 
+
.toggleClass( 'cat_a_lot_selected' );
function run () {
 
if (HotCat.started) return;
 
HotCat.started = true;
 
loadTrigger.register(really_run);
 
}
 
  
function really_run () {
+
// And one has been clicked before...
initialize ();
+
if ( prevCheckbox !== null && e.shiftKey ) {
 +
method = $thisControl.hasClass( 'cat_a_lot_selected' ) ? 'addClass' : 'removeClass';
  
if (!HotCat.upload_disabled && conf.wgNamespaceNumber === -1 && conf.wgCanonicalSpecialPageName == 'Upload' && conf.wgUserName) {
+
// Check or uncheck this one and all in-between checkboxes
setup_upload ();
+
$box.slice(
setup (function () {
+
Math.min( $box.index( prevCheckbox ), $box.index( $thisControl ) ),
// Check for state restoration once the setup is done otherwise, but before signalling setup completion
+
Math.max( $box.index( prevCheckbox ), $box.index( $thisControl ) ) + 1
if (   typeof UploadForm != 'undefined'
+
)[ method ]( 'cat_a_lot_selected' );
&& typeof UploadForm.previous_hotcat_state != 'undefined'
 
&& UploadForm.previous_hotcat_state !== null)
 
{
 
UploadForm.previous_hotcat_state = setState (UploadForm.previous_hotcat_state);
 
}
 
});
 
} else {
 
if (!conf.wgIsArticle || conf.wgAction != 'view' || param('diff') !== null || param('oldid') !== null || !can_edit() || HotCat.disable()) return;
 
getPage ();
 
 
}
 
}
}
+
// Either way, update the prevCheckbox variable to the one clicked now
 +
prevCheckbox = $thisControl;
  
// Legacy stuff
+
if ( $.isFunction( cb ) ) {
 
+
cb();
function closeForm () {
 
// Close all open editors without redirect resolution and other asynchronous stuff.
 
for (var i = 0; i < editors.length; i++) {
 
if (editors[i].state == CategoryEditor.OPEN) {
 
editors[i].cancel();
 
} else if (editors[i].state == CategoryEditor.CHANGE_PENDING) {
 
editors[i].sanitizeInput ();
 
var value = editors[i].text.value.split('|');
 
var key  = null;
 
if (value.length > 1) key = value[1];
 
var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
 
if (v.length === 0) {
 
editors[i].cancel ();
 
} else {
 
editors[i].currentCategory = v;
 
editors[i].currentKey = key;
 
editors[i].currentExists = this.inputExists;
 
editors[i].close ();
 
}
 
}
 
 
}
 
}
}
+
} );
 +
return $box;
 +
};
  
function getState () {
+
}( jQuery, mediaWiki ) );
var result = null;
 
for (var i = 0; i < editors.length; i++) {
 
var text = editors[i].currentCategory;
 
var key  = editors[i].currentKey;
 
if (text && text.length > 0) {
 
if (key !== null) text += '|' + key;
 
if (result === null)
 
result = text;
 
else
 
result = result + '\n' + text;
 
}
 
}
 
return result;
 
}
 
 
 
function setState (state) {
 
var cats = state.split ('\n');
 
if (cats.length === 0) return null;
 
if (initialized && editors.length == 1 && editors[0].isAddCategory) {
 
// Insert new spans and create new editors for them.
 
var newSpans = [];
 
var before = editors.length == 1 ? editors[0].span : null;
 
var i;
 
for (i = 0; i < cats.length; i++) {
 
if (cats[i].length === 0) continue;
 
var cat = cats[i].split ('|');
 
var key = cat.length > 1 ? cat[1] : null;
 
cat = cat[0];
 
var lk = make ('a'); lk.href = wikiPagePath (HotCat.category_canonical + ':' + cat);
 
lk.appendChild (make (cat, true));
 
lk.title = cat;
 
var span = make ('span');
 
span.appendChild (lk);
 
if (i === 0) catLine.insertBefore (make (' ', true), before);
 
catLine.insertBefore (span, before);
 
if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
 
newSpans.push ({element: span, title: cat, 'key': key});
 
}
 
// And change the last one...
 
if (before) {
 
before.parentNode.insertBefore (make (' | ', true), before);
 
}
 
var editor = null;
 
for (i = 0; i < newSpans.length; i++) {
 
editor = new CategoryEditor (catLine, newSpans[i].element, newSpans[i].title, newSpans[i].key);
 
}
 
}
 
return null;
 
}
 
 
 
// Export legacy functions
 
window.hotcat_get_state  = function () { return getState(); };
 
window.hotcat_set_state  = function (state) { return setState (state); };
 
window.hotcat_close_form = function () { closeForm (); };
 
 
 
// Make sure we don't get conflicts with AjaxCategories (core development that should one day
 
// replace HotCat).
 
mw.config.set('disableAJAXCategories', true);
 
 
 
if (conf.wgCanonicalSpecialPageName !== 'Upload') {
 
// Use wikipage.content hook so that HotCat reloads after VE edits (bug T103285)
 
var startHotCat = function() {
 
mw.hook('wikipage.content').add( function() {
 
// Reset HotCat in case this is a soft reload (VE edit)
 
catLine = null;
 
editors = [];
 
initialized = false;
 
HotCat.started = false;
 
run ();
 
} );
 
};
 
} else {
 
// We're running on Special:Upload, where the 'wikipage.content' hook is fired for
 
// various previewed wikitext snippets, which shouldn't reload HotCat interface.
 
var startHotCat = function() {
 
$(document).ready(run);
 
};
 
}
 
// We can safely trigger just after user configuration is loaded. Also start HotCat if the user module fails to load.
 
// Avoid using Promise methods of mw.loader.using as those aren't supported in older
 
// MediaWiki versions.
 
mw.loader.using('user', startHotCat, startHotCat);
 
})(jQuery, mediaWiki);
 
  
 
// </nowiki>
 
// </nowiki>

גרסה אחרונה מ־21:07, 23 ביולי 2017

/**
 * Cat-a-lot
 * Changes category of multiple files
 *
 * Originally by Magnus Manske
 * RegExes by Ilmari Karonen
 * Completely rewritten by DieBuche
 *
 * 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, importStylesheet */
/* eslint one-var: 0, vars-on-top: 0, no-underscore-dangle:0 */ // extends: wikimedia
/* jshint unused:true, forin:false, smarttabs:true, loopfunc:true, browser:true */

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

var NS_CAT = 14,
	formattedNS = mw.config.get( 'wgFormattedNamespaces' ),
	nsIDs = mw.config.get( 'wgNamespaceIds' );

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-subcatcountpref': 'Sub-categories to show at most',
	'cat-a-lot-config-settings': 'Preferences',

	// 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!',
	'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.',

	// 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-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!',

	// Summaries:
	'cat-a-lot-pref-save-summary': '[[c:Help:Gadget-Cat-a-lot|Cat-a-lot]]: updating user preferences',
	'cat-a-lot-summary-add': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Adding [[Category:$1]]',
	'cat-a-lot-summary-copy': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Copying from [[Category:$1]] to [[Category:$2]]',
	'cat-a-lot-summary-move': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Moving from [[Category:$1]] to [[Category:$2]]',
	'cat-a-lot-summary-remove': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Removing from [[Category:$1]]'
};
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 $body, $container, $dataContainer, $searchInputContainer, $searchInput, $resultList, $markCounter,
	$selections, $selectAll, $selectNone, $settingsWrapper, $settingsLink, $head, $link;
var commons_url = 'https://commons.wikimedia.org/w/index.php';

var catALot = window.catALot = {
	apiUrl: mw.util.wikiScript( 'api' ),
	origin: false,
	searchmode: false,
	version: 3.7,
	setHeight: 450,
	settings: {},
	init: function() {
		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>' )
			.attr({
				id: 'cat_a_lot_searchcatname',
				placeholder: msgPlain( '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( msgPlain( 'select' ) )
			.appendTo( $dataContainer );
		$selectAll = $( '<a>' )
			.attr( 'id', 'cat_a_lot_select_all' )
			.text( msgPlain( 'all' ) )
			.appendTo( $selections.append( ' ' ) );
		$selectNone = $( '<a>' )
			.attr( 'id', 'cat_a_lot_select_none' )
			.text( msgPlain( 'none' ) )
			.appendTo( $selections.append( ' • ' ) );
		$settingsWrapper = $( '<div>' )
			.attr( 'id', 'cat_a_lot_settings' )
			.appendTo( $dataContainer );
		$settingsLink = $( '<a>' )
			.attr( 'id', 'cat_a_lot_config_settings' )
			.text( msgPlain( '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>' )
			.attr( {
				href: commons_url + '?title=Special:MyLanguage/Help:Gadget-Cat-a-lot',
				target: '_blank',
				style: 'float:right',
				title: $( '#n-help' ).attr( 'title' )
			} ).text( '?' ) );

		if ( this.origin ) {
			$( '<a>' )
				.attr( 'id', 'cat_a_lot_remove' )
				.html( msg( 'remove-from-cat' ) )
				.appendTo( $selections )
				.click( function() {
					catALot.remove();
				} );
		}

		if ( ( mw.util.getParamValue( 'withJS' ) === 'MediaWiki:Gadget-Cat-a-lot.js' &&
				!mw.util.getParamValue( 'withCSS' ) ) ||
				mw.loader.getState( 'ext.gadget.Cat-a-lot' ) === 'registered' ) {
			importStylesheet( 'MediaWiki:Gadget-Cat-a-lot.css' );
		}

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

		$searchInput.keypress( function( e ) {
			if ( e.which === 13 ) {
				catALot.updateCats( $.trim( $( this ).val() ) );
			}
		} )
			.bind( 'input keyup', function() {
				var oldVal = this.value,
					newVal = oldVal.replace( reCat, '' );
				if ( newVal !== oldVal ) {
					this.value = newVal;
				}
			} );
		if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Search' ) {
			$searchInput.val( mw.util.getParamValue( 'search' ) );
		}
		function initAutocomplete() {
			if ( catALot.autoCompleteIsEnabled ) {
				return;
			}
			catALot.autoCompleteIsEnabled = true;

			$searchInput.autocomplete( {
				source: function( request, response ) {
					catALot.doAPICall( {
						action: 'opensearch',
						search: request.term,
						redirects: 'resolve',
						namespace: NS_CAT
					}, function( data ) {
						if ( data[ 1 ] ) {
							response( $( data[ 1 ] )
								.map( function( index, item ) {
									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() {
				catALot.toggleAll( true );
			} );
		$selectNone
			.click( function() {
				catALot.toggleAll( false );
			} );
		$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();
			} );

		this.localCatName = formattedNS[ NS_CAT ];
	},

	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( $( 'div#mw-category-media' ).find( 'li[class!="gallerybox"]' ) );

				if ( this.settings.editpages ) {
					this.labels = this.labels.add( $( 'div#mw-pages, div#mw-subcategories' ).find( 'li' ) );
				}
				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( 'div.gallerytext' );
				break;
		}
	},

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

	getMarkedLabels: function() {
		this.selectedLabels = this.labels.filter( '.cat_a_lot_selected:visible' );
		return this.selectedLabels.map( function() {
			var file = $( this ).find( 'a[title][class$="title"]' );
			file = file.length ? file : $( this ).find( 'a[title]' );
			var title = file.attr( 'title' ) ||
					catALot.getTitleFromLink( file.attr( 'href' ) ) ||
					catALot.getTitleFromLink( $( this ).find( 'a' )
			.attr( 'href' ) );

			return [ [ title, $( this ) ] ];
		} );
	},

	updateSelectionCounter: function() {
		this.selectedLabels = this.labels.filter( '.cat_a_lot_selected' );
		$markCounter
			.show()
			.html( msg( 'files-selected', this.selectedLabels.length ) );
	},

	makeClickable: function() {
		this.labels = $();
		this.findAllLabels( this.searchmode );
		this.labels.catALotShiftClick( function() {
			catALot.updateSelectionCounter();
		} )
			.addClass( 'cat_a_lot_label' );
	},

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

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

		this.doAPICall( data, function( result ) {
			var cats = result.query.categorymembers;
			
			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() {
		var data = {
			action: 'query',
			prop: 'categories',
			titles: 'Category:' + this.currentCategory
		};
		this.doAPICall( data, function( result ) {
			catALot.parentCats = [];
			var cats, pages = result.query.pages;
			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';

				$resultList.append( '<table>' );
				catALot.createCatLinks( '→', [ catALot.currentCategory ] );
				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++ ) {
				catALot.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' );

		var createRegexStr = function( name ) {
			if ( !name || name.length === 0 ) {
				return '';
			}
			var regexName = '';
			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 ) {
					regexName += initial;
				} else {
					regexName += '[' + 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 + ')' );
	},

	regexBuilder: function( category ) {
		var catname = this.localizedRegex( NS_CAT, '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( file, targetcat, mode ) {
		var data = {
			action: 'query',
			prop: 'info|revisions',
			rvprop: 'content|timestamp',
			intoken: 'edit',
			titles: file[ 0 ]
		};

		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 ) {
		if ( this.settings.docleanup ) {
			return text.replace( /\{\{\s*[Cc]heck categories\s*(\|?.*?)\}\}/, '' );
		} else {
			return text;
		}
	},

	editCategories: function( result, file, targetcat, mode ) {
		var otext, starttimestamp, timestamp;
		if ( !result ) {
			// Happens on unstable wifi connections..
			this.connectionError.push( file[ 0 ] );
			this.updateCounter();
			return;
		}
		var pages = result.query.pages;

		// there should be only one, but we don't know its ID
		for ( var id in pages ) {
			// The edittoken only changes between logins
			this.edittoken = pages[ id ].edittoken;
			otext = pages[ id ].revisions[ 0 ][ '*' ];
			starttimestamp = pages[ id ].starttimestamp;
			timestamp = pages[ id ].revisions[ 0 ].timestamp;
		}


		var sourcecat = this.origin;
		// Check if that file is already in that category
		if ( mode !== 'remove' && this.regexBuilder( targetcat ).test( otext ) ) {

			// If the new cat is already there, just remove the old one.
			if ( mode === 'move' ) {
				mode = 'remove';
			} else {
				this.alreadyThere.push( file[ 0 ] );
				this.updateCounter();
				return;
			}
		}

		var text = otext;
		var comment;

		// Fix text
		switch ( mode ) {
			case 'add':
				text += '\n[[' + this.localCatName + ':' + targetcat + ']]\n';
				comment = msgPlain( 'summary-add' ).replace( '$1', targetcat );
				break;
			case 'copy':
				text = text.replace( this.regexBuilder( sourcecat ), '[[' + this.localCatName + ':' + sourcecat + '$1]]\n[[' + this.localCatName + ':' + targetcat + '$1]]\n' );
				comment = msgPlain( 'summary-copy' ).replace( '$1', sourcecat ).replace( '$2', targetcat );
				// If category is added through template:
				if ( otext === text ) {
					text += '\n[[' + this.localCatName + ':' + targetcat + ']]';
				}
				break;
			case 'move':
				text = text.replace( this.regexBuilder( sourcecat ), '[[' + this.localCatName + ':' + targetcat + '$1]]\n' );
				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 ) {
			this.notFound.push( file[ 0 ] );
			this.updateCounter();
			return;
		}

		// 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' ) {
			text = this.doCleanup( this.removeUncat( text ) );
		}
		var data = {
			action: 'edit',
			assert: 'user',
			summary: comment,
			title: file[ 0 ],
			text: text,
			bot: true,
			starttimestamp: starttimestamp,
			basetimestamp: timestamp,
			watchlist: this.settings.watchlist,
			token: this.edittoken
		};
		if ( this.settings.minor ) {
			data.minor = true;
		}

		this.doAPICall( data, function() {
			catALot.updateCounter();
		} );
		this.markAsDone( file[ 1 ], mode, targetcat );
	},

	markAsDone: function( label, mode, targetcat ) {
		label.addClass( 'cat_a_lot_markAsDone' );
		switch ( mode ) {
			case 'add':
				label.append( '<br>' + msg( 'added-cat', targetcat ) );
				break;
			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() {
		this.counterCurrent++;
		if ( this.counterCurrent > this.counterNeeded ) {
			this.displayResult();
		} else {
			this.domCounter.text( this.counterCurrent );
		}
	},

	displayResult: function() {
		document.body.style.cursor = 'auto';
		$( '.cat_a_lot_feedback' )
			.addClass( 'cat_a_lot_done' );
		$( '.ui-dialog-content' )
			.height( 'auto' );
		var rep = this.domCounter.parent();
		rep.html( '<h3>' + msg( 'done' ) + '</h3>' );
		rep.append( msg( 'all-done' ) + '<br />' );

		var close = $( '<a>' )
			.text( msgPlain( 'return-to-page' ) );
		close.click( function() {
			catALot.progressDialog.remove();
			catALot.toggleAll( false );
		} );
		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>' ) );
		}

	},

	moveHere: function( targetcat ) {
		this.doSomething( targetcat, 'move' );
	},

	copyHere: function( targetcat ) {
		this.doSomething( targetcat, 'copy' );
	},
	
	addHere: function( targetcat ) {
		this.doSomething( targetcat, 'add' );
	},

	remove: function() {
		this.doSomething( '', 'remove' );
	},

	doSomething: function( targetcat, mode ) {
		var files = this.getMarkedLabels();
		if ( files.length === 0 ) {
			alert( msgPlain( 'none-selected' ) );
			return;
		}
		this.notFound = [];
		this.alreadyThere = [];
		this.connectionError = [];
		this.counterCurrent = 1;
		this.counterNeeded = files.length;
		mw.loader.using( [ 'jquery.ui.dialog', 'mediawiki.RegExp' ], function() {
			catALot.showProgress();
			for ( var i = 0; i < files.length; i++ ) {
				catALot.getContent( files[ i ], targetcat, mode );
			}
		} );
	},

	doAPICall: function( params, callback ) {
		params.format = 'json';
		var i = 0,
			apiUrl = this.apiUrl,
			doCall,
			handleError = function( jqXHR, textStatus, errorThrown ) {
				if ( window.console && $.isFunction( window.console.log ) ) {
					window.console.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() {
			$.ajax( {
				url: apiUrl,
				cache: false,
				dataType: 'json',
				data: params,
				type: 'POST',
				success: callback,
				error: handleError
			} );
		};
		doCall();
	},

	createCatLinks: function( symbol, list ) {
		list.sort();
		var domlist = $resultList.find( 'table' );
		for ( var i = 0; i < list.length; i++ ) {
			var $tr = $( '<tr>' );

			var $link = $( '<a>' ),
				$add, $move, $copy;

			$link.text( list[ i ] );
			$tr.data( 'cat', list[ i ] );
			$link.click( function() {
				catALot.updateCats( $( this ).closest( 'tr' ).data( 'cat' ) );
			} );

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

			if ( this.origin ) {
				// Can't move to source category
				if ( list[ i ] !== this.origin ) {
					$move = $( '<a>' )
						.addClass( 'cat_a_lot_move' )
						.text( msgPlain( 'move' ) )
						.click( function() {
							catALot.moveHere( $( this ).closest( 'tr' ).data( 'cat' ) );
						} );

					$copy = $( '<a>' )
						.addClass( 'cat_a_lot_action' )
						.text( msgPlain( 'copy' ) )
						.click( function() {
							catALot.copyHere( $( this ).closest( 'tr' ).data( 'cat' ) );
						} );

					$tr.append( $( '<td>' ).append( $move ), $( '<td>' ).append( $copy ) );
				}
			} else {
				$add = $( '<a>' )
						.addClass( 'cat_a_lot_action' )
						.text( msgPlain( 'add' ) )
						.click( function() {
							catALot.addHere( $( this ).closest( 'tr' ).data( 'cat' ) );
						} );

				$tr.append( $( '<td>' ).append( $add ) );
			}

			domlist.append( $tr );
		}
	},

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

	showCategoryList: function() {
		var thiscat = [ this.currentCategory ];

		$resultList.empty();
		$resultList.append( '<table>' );

		this.createCatLinks( '↑', this.parentCats );
		this.createCatLinks( '→', thiscat );
		this.createCatLinks( '↓', this.subCats );

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

		$resultList.css( {
			maxHeight: this.setHeight + 'px',
			height: ''
		} );
	},

	updateCats: function( newcat ) {
		document.body.style.cursor = 'wait';

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

		this.progressDialog = $( '<div>' )
			.html( msg( 'editing' ) + ' <span id="cat_a_lot_current">' + this.counterCurrent + '</span> ' + msg( 'of' ) + this.counterNeeded )
			.dialog( {
				width: 450,
				height: 90,
				minHeight: 90,
				modal: true,
				resizable: false,
				draggable: false,
				closeOnEscape: false,
				dialogClass: 'cat_a_lot_feedback'
			} );
		$( '.ui-dialog-titlebar' )
			.hide();
		this.domCounter = $( '#cat_a_lot_current' );

	},

	run: function() {
		if ( $( '.cat_a_lot_enabled' ).length ) {
			this.makeClickable();
			$dataContainer
				.show();
			$container
				.resizable( {
					handles: 'n',
					alsoResize: '#cat_a_lot_category_list',
					resize: function() {
						$( this )
							.css( {
								left: '',
								top: ''
							} );
						catALot.setHeight = $( this ).height();
						$resultList
							.css( {
								maxHeight: '',
								width: ''
							} );
					}
				} )
				/*.draggable( { // FIXME: Box get static if sametime resize
					cursor: 'move',
					start: function() {
						$( this ).css( 'height', $( this ).height() );
					}
				} )*/;
			 $resultList
				.css( {
					maxHeight: '450px'
				} );

			this.updateCats( this.origin || 'Images' );
			$link.text( 'X' );
		} else {
			$dataContainer
				.hide();
			$container
				// .draggable( 'destroy' )
				.resizable( 'destroy' )
				.removeAttr( 'style' );
			// Unbind click handlers
			this.labels.unbind( 'click.catALot' );
			$link.text( 'Cat-a-lot' );
		}
	},

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

						$container.remove();
						catALot.labels.unbind( 'click.catALot' );
						catALot.init();
					},
					_saveToJS = function() {
						var opt = mw.libs.settingsManager.option( {
								optionName: 'catALotPrefs',
								value: catALot.settings,
								encloseSignature: 'catALot',
								encloseBlock: '////////// Cat-a-lot user preferences //////////\n',
								triggerSaveAt: /Cat.?A.?Lot/i,
								editSummary: msgPlain( '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 && catALot.settings[ v.name ] !== v.value ) {
						mustRestart = true;
					}
					catALot.settings[ v.name ] = v.value;
					window.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;
		}
		if ( !window.catALotPrefs ) {
			window.catALotPrefs = {};
		}
		$.each( this.defaults, function( n, v ) {
			v.value = catALot.settings[ v.name ] = ( window.catALotPrefs[ v.name ] || v[ 'default' ] );
			v.label = msgPlain( v.label_i18n );
			if ( v.select_i18n ) {
				v.select = {};
				$.each( v.select_i18n, function( i18nk, val ) {
					v.select[ msgPlain( 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': 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
	} ]
	/* eslint-enable camelcase */
};

// The gadget is not immediately needed, so let the page load normally
window.setTimeout( function () {
	var userGrp = mw.config.get('wgUserGroups');
	var trusted = ( $.inArray( 'sysop', userGrp ) > -1 ||
		$.inArray( 'autoconfirmed', userGrp ) > -1 ||
		mw.config.get( 'wgRelevantUserName' ) === mw.config.get( 'wgUserName' ) );

	switch ( mw.config.get( 'wgNamespaceNumber' ) ) {
		case NS_CAT:
			catALot.searchmode = 'category';
			catALot.origin = mw.config.get( 'wgTitle' );
			break;
		case -1:
			catALot.searchmode = {
				// list of accepted special page names mapped to search mode names
				Contributions: 'contribs',
				Listfiles: trusted ? 'listfiles' : null,
				Prefixindex: trusted ? 'prefix' : null,
				Search: 'search',
				Uncategorizedimages: 'gallery'
			}[ mw.config.get( 'wgCanonicalSpecialPageName' ) ];
			break;
	}

	if ( catALot.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: commons_url,
				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() {
					catALot.init();
				} );
			}
			if ( !loadingLocalizations )
				mw.loader.using( [ 'user' ], init, init );
		};

		if ( mw.config.get( 'wgUserLanguage' ) !== 'en' )
			loadLocalization( mw.config.get( 'wgUserLanguage' ), maybeLaunch );
		if ( mw.config.get( 'wgContentLanguage' ) !== 'en' )
			loadLocalization( mw.config.get( 'wgContentLanguage' ), 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.bind( 'click.catALot', function( e ) {

		// Prevent following the link and text selection
		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>