« MediaWiki:Gadget-HotCat.js » : différence entre les versions

An alternative space dedicated to technology and culture at Charleroi.
Aller à la navigation Aller à la recherche
(Page créée avec « //<source lang="javascript"> /* HotCat V2.4 Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view. Supports multiple category ... »)
 
Aucun résumé des modifications
 
Ligne 1 : Ligne 1 :
//<source lang="javascript">
/**
HotCat V2.39


/*
Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
  HotCat V2.4
Supports multiple category changes, as well as redirect and disambiguation resolution. Also
plugs into the upload form. Search engines to use for the suggestion list are configurable, and
can be selected interactively.


  Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat
  Supports multiple category changes, as well as redirect and disambiguation resolution. Also
List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Version_history
  plugs into the upload form. Search engines to use for the suggestion list are configurable, and
  can be selected interactively.


  Authors:
License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
    V0.0: July 2007 - 2010-05-26: original version by [[User:Magnus Manske]], with lots of
          additions by many editors, notably [[User:Dschwen]], [[User:TheDJ]], [[User:Superm401]],
          and [[User:Lupo]]. No explicit license, assumed multi-licensed GFDL and CC-BY-SA-3.0 per
          normal wiki submissions.
    V2.0: April-May 2010: [[User:Lupo]]. Complete rewrite reusing only a little code from V0.0.
    V2.1: May 2010: [[User:Merlissimo]] (added features: namespace case insensitive, subcategory
          engine, category template mapping for removing; developed at de-Wikipedia.)
    V2.2: May 2010: [[User:Lupo]] (porting additions from de-WP to the Commons, auto-localization
          of template namespace name, cleanup, various other improvements. New features:
          highlighting of changed categories, enabling/disabling save button, search engine name
          localization, parent category engine).
    V2.3: Aug 2010: [[User:Lupo]]: page up/down for scrolling in suggestion lists; suggestion list
          size configurable; minor bug fixes.
    V2.4: Oct 2010: [[User:TheDJ]] & [[User:Lupo]]: switch to not enable on upload form, ability to
          load translations from the Commons. Major/minor edit flag configurable; and fix a minor
          bug with the "Warn upon empty edit summary" user preference.


  License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
Choose whichever license of these you like best :-)
 
  Choose whichever license of these you like best :-)
This code should run on any MediaWiki installation >= MW 1.27.
 
For use with older versions of MediaWiki, use the archived versions below:
 
<=1.26: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-HotCat.js&oldid=211134664
*/
*/
// <nowiki>
/* eslint-disable vars-on-top, one-var, camelcase, no-alert, indent, curly */
/* global jQuery, mediaWiki, UFUI, JSconfig, UploadForm */
/* jslint strict:false, nonew:false, bitwise:true */
( function ( $, mw ) {
// Don't use mw.config.get() as that takes a copy of the config, and so doesn't
// account for values changing, e.g. wgCurRevisionId after a VE edit
var conf = mw.config.values;


// Globals:
// Guard against double inclusions (in old IE/Opera element ids become window properties)
//  (inline script on the page):
if ( ( window.HotCat && !window.HotCat.nodeName ) ||
//    wgNamespaceNumber, wgCanonicalSpecialPageName, wgNamespaceIds (optional), wgFormattedNamespaces (optional)
conf.wgAction === 'edit' ) // Not on edit mode
//    wgScript, wgServer, wgArticlePath, wgScriptPath, wgAction, wgPageName, wgTitle, wgUserName, wgIsArticle,
return;
//   wgArticleId
//  ajax.js
//    sajax_init_object
//  wikibits.js
//    addOnloadHook, window.ie6_bugs, importScript


if (typeof (HotCat) == 'undefined') { // Guard against double inclusions
   
// Configuration stuff.
// Configuration stuff.
var HotCat = {
var HC = window.HotCat = {
  isCommonsVersion : true
// Localize these messages to the main language of your wiki.
    // If you copy HotCat to your wiki, you should set this to false!
messages: {
cat_removed: 'removed [[Category:$1]]',
template_removed: 'removed {{[[Category:$1]]}}',
cat_added: 'added [[Category:$1]]',
cat_keychange: 'new key for [[Category:$1]]: "$2"', // $2 is the new key
cat_notFound: 'Category "$1" not found',
cat_exists: 'Category "$1" already exists; not added.',
cat_resolved: ' (redirect [[Category:$1]] resolved)',
uncat_removed: 'removed {{uncategorized}}',
separator: '; ',
// Some text to prefix to the edit summary.
prefix: '',
// Some text to append to the edit summary. Named 'using' for historical reasons. If you prefer
// to have a marker at the front, use prefix and set this to the empty string.
using: ' using [[Help:Gadget-HotCat|HotCat]]',
// $1 is replaced by a number. If your language has several plural forms (c.f. [[:en:Dual (grammatical form)]]),
// you can set this to an array of strings suitable for passing to mw.language.configPlural().
// If that function doesn't exist, HotCat will simply fall back to using the last
// entry in the array.
multi_change: '$1 categories',
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
// see localization hook below.
commit: 'Save',
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
// see localization hook below.
ok: 'OK',
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
// see localization hook below.
cancel: 'Cancel',
// Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
// see localization hook below.
multi_error: 'Could not retrieve the page text from the server. Therefore, your category changes ' +
'cannot be saved. We apologize for the inconvenience.',
// Defaults to '[[' + category_canonical + ':$1]]'. Can be overridden if in the short edit summaries
// not the standard category name should be used but, say, a shorter namespace alias. $1 is replaced
// by a category name.
short_catchange: null
},
// Plural of category_canonical.
categories: 'Categories',
// Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
// any items, but that contains links to other categories where stuff should be categorized. If you don't have
// that concept on your wiki, set it to null. Use blanks, not underscores.
disambig_category: 'Disambiguation',
// Any category in this category is deemed a (soft) redirect to some other category defined by a link
// to another non-blacklisted category. If your wiki doesn't have soft category redirects, set this to null.
// If a soft-redirected category contains more than one link to another non-blacklisted category, it's considered
// a disambiguation category instead.
redir_category: 'Category redirects',
// The little modification links displayed after category names. U+2212 is a minus sign; U+2193 and U+2191 are
// downward and upward pointing arrows. Do not use ↓ and ↑ in the code!
links: {
change: '(±)',
remove: '(\u2212)',
add: '(+)',
restore: '(×)',
undo: '(×)',
down: '(\u2193)',
up: '(\u2191)'
},
changeTag: conf.wgUserName ? 'HotCat' : '', // if tag is missing, edit is rejected
// The tooltips for the above links
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 HTML content of the "enter multi-mode" link at the front.
addmulti: '<span>+<sup>+</sup></span>',
// Tooltip for the "enter multi-mode" link
multi_tooltip: 'Modify several categories',
// Return true to disable HotCat.
disable: function () {
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 || // Non-existing file pages
ns === 2 && /\.(js|css)$/.test( conf.wgTitle ) || // User scripts
nsIds &&
( ns === nsIds.creator ||
ns === nsIds.timedtext ||
ns === nsIds.institution ) );
},
// A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
// If not, set it to null.
uncat_regexp: /\{\{\s*[Uu]ncategorized\s*[^}]*\}\}\s*(<!--.*?-->\s*)?/g,
// The images used for the little indication icon. Should not need changing.
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',
// 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 {}.
template_categories: {},
// Names for the search engines
engine_names: {
searchindex: 'Search index',
pagelist: 'Page list',
combined: 'Combined search',
subcat: 'Subcategories',
parentcat: 'Parent categories'
},
// Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the // any items, but that contains links to other categories where stuff should be categorized. If you don't have


  // Localize these messages to the main language of your wiki.
// ("case-sensitive"; Category:aa !== Category:Aa). It doesn't currently have a fully case-insensitive mode
  ,messages :
// (which would mean Category:aa === Category:Aa === Category:AA === Category:aA)
    { cat_removed  : 'removed [[Category:$1]]'
// HotCat tries to set this correctly automatically using an API query. It's still a good idea to manually
    ,template_removed  : 'removed {{[[Category:$1]]}}'
// configure it correctly; either directly here if you copied HotCat, or in the local configuration file
    ,cat_added    : 'added [[Category:$1]]'
// MediaWiki:Gadget-HotCat.js/local_defaults if you hotlink to the Commons-version, to ensure it is set even
    ,cat_keychange: 'new key for [[Category:$1]]: '
// if that API query should fail for some strange reason.
    ,cat_notFound : 'Category "$1" not found'
capitalizePageNames: true,
    ,cat_exists  : 'Category "$1" already exists; not added.'
// If upload_disabled is true, HotCat will not be used on the Upload form.
    ,cat_resolved : ' (redirect [[Category:$1]] resolved)'
upload_disabled: false,
    ,uncat_removed: 'removed {{uncategorized}}'
// Single regular expression matching blacklisted categories that cannot be changed or
    ,prefix      : ""
// added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
        // Some text to prefix to the edit summary.
// or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
    ,using        : ' using [[Help:Gadget-HotCat|HotCat]]'
// word "maintenance" in its title.
        // Some text to append to the edit summary. Named 'using' for historical reasons. If you prefer
blacklist: null,
        // to have a marker at the front, use prefix and set this to the empty string.
    ,multi_change : '$1 categories'
        // $1 is replaced by a number
    ,commit      : 'Save'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
    ,ok          : 'OK'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
    ,cancel      : 'Cancel'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
    ,multi_error  : 'Could not retrieve the page text from the server. Therefore, your category changes '
                    +'cannot be saved. We apologize for the inconvenience.'
        // Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
    }
,category_regexp    : '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]'
  // Regular sub-expression matching all possible names for the category namespace. Is automatically localized
  // correctly if you're running MediaWiki 1.16 or later. Otherwise, set it appropriately, e.g. at the German
  // Wikipedia, use '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]|[Kk][Aa][Tt][Ee][Gg][Oo][Rr][Ii][Ee]', or at the
  // Chinese Wikipedia, use '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]|分类|分類'. Note that namespaces are case-
  // insensitive!
,category_canonical : 'Category'
  // The standard category name on your wiki. Is automatically localized correctly if you're running
  // MediaWiki 1.16 or later; otherwise, set it to the preferred category name (e.g., "Kategorie").
,categories        : 'Categories'
  // Plural of category_canonical
,disambig_category  : 'Disambiguation'
  // Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
  // any items, but that contains links to other categories where stuff should be categorized. If you don't have
  // that concept on your wiki, set it to null.
,redir_category    : 'Category redirects'
  // Any category in this category is deemed a (soft) redirect to some other category defined by the first link
  // to another category. If your wiki doesn't have soft category redirects, set this to null.
,links : {change: '(±)', remove: '(−)', add: '(+)', restore: '(×)', undo: '(×)', down: '(↓)', up: '(↑)'}
  // The little modification links displayed after category names.
,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
      return (  wgNamespaceNumber < 0  // Special pages; Special:Upload is handled differently
              || wgNamespaceNumber == 10 // Templates
              || wgNamespaceNumber == 8  // MediaWiki
              || wgNamespaceNumber == 2
                && wgTitle && wgTitle.length >= 3 && wgTitle.lastIndexOf ('.js') + 3 == wgTitle.length
                // User scripts
              || typeof (wgNamespaceIds) != 'undefined'
                && (   wgNamespaceNumber == wgNamespaceIds['creator']
                    || wgNamespaceNumber == wgNamespaceIds['timedtext']
                    )
            );
    }
,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    : 'http://upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
,existsNo    : 'http://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_regexp    : '[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]'
  // Regexp to recognize templates. Like "category" above; autolocalized for MW 1.16+, otherwise set manually here.
  // On the German Wikipedia, you might use '[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]|[Vv][Oo][Rr][Ll][Aa][Gg][Ee]'.
,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 corretcly 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.


  // Stuff changeable by users:
// Stuff changeable by users:
,bg_changed : '#F8CCB0'
// Background for changed categories in multi-edit mode. Default is a very light salmon pink.
  // Background for changed categories in multi-edit mode. Default is a very light salmon pink.
bg_changed: '#FCA',
,no_autocommit : false
// If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
  // If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
// the changes; users must always save explicitly.
  // the changes; users must always save explicitly.
no_autocommit: false,
,suggest_delay : 100
// If true, the "category deletion" link "(-)" will never save automatically but always show an
  // Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
// edit page where the user has to save the edit manually. Is false by default because that's the
  // server to get suggestions.
// traditional behavior. This setting overrides no_autocommit for "(-)" links.
,editbox_width : 40
del_needs_diff: false,
  // Default width, in characters, of the text input field.
// Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
,suggestions : 'combined'
// server to get suggestions.
  // One of the engine_names above, to be used as the default suggestion engine.
suggest_delay: 100,
,fixed_search : false
// Default width, in characters, of the text input field.
  // If true, always use the default engine, and never display a selector.
editbox_width: 40,
,use_up_down : true
// One of the engine_names above, to be used as the default suggestion engine.
  // If false, do not display the "up" and "down" links
suggestions: 'combined',
,list_size : 5
// If true, always use the default engine, and never display a selector.
  // Default list size
fixed_search: false,
,single_minor : true
// If false, do not display the "up" and "down" links
  // If true, single category changes are marked as minor edits. If false, they're not.
use_up_down: true,
// Default list size
listSize: 5,
// If true, single category changes are marked as minor edits. If false, they're not.
single_minor: true,
// 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.
dont_add_to_watchlist: false,
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 || !v.length ) continue;
 
window.HotCat.shortcuts[ k ] = v;
}
}
};
};


if (HotCat.isCommonsVersion && wgServer.indexOf ('/commons') < 0) {
// More backwards compatibility. We have a few places where we test for the browser: once for
  // We're running in some other wiki, which hotlinks to the Commons version. The other wiki can put local settings
// Safari < 3.0, and twice for WebKit (Chrome or Safari, any versions)
  // in this file to override the Commons settings for all user languages. For instance, if on your wiki people do
var ua = navigator.userAgent.toLowerCase();
  // not like automatic saving, you'd add in that file the line HotCat.no_autocommit = true; If you hotlink, you
var is_webkit = /applewebkit\/\d+/.test( ua ) && ua.indexOf( 'spoofer' ) < 0;
  // *must* adapt HotCat.categories in this file to the local translation in wgContentLanguage of your wiki of the
var cat_prefix = null;
  // English plural "Categories", and you should provide translations in wgContentLanguage of your wiki of all messages,
var noSuggestions = false;
  // tooltips, and of the engine names.  
 
  importScript ('MediaWiki:Gadget-HotCat.js/local_defaults');
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;
}
}


if (wgUserLanguage != 'en') {
function LoadTrigger( needed ) {
  if (window.hotcat_translations_from_commons && wgServer.indexOf ('/commons') < 0) {
this.queue = [];
    importScriptURI (
this.toLoad = needed;
      ((wgServer.indexOf( "https://secure.wikimedia.org") == 0)
        ? '/wikipedia/commons/w/index.php?title='
        : 'http://commons.wikimedia.org/w/index.php?title='
      )
      + 'MediaWiki:Gadget-HotCat.js/' + wgUserLanguage
      + '&action=raw&ctype=text/javascript&smaxage=21600&maxage=86400'
    );
  } else {
    // Load translations locally
    importScript ('MediaWiki:Gadget-HotCat.js/' + wgUserLanguage);
  }
}
}
LoadTrigger.prototype = {
register: function ( callback ) {
if ( this.toLoad <= 0 ) callback(); // Execute directly
else this.queue[ this.queue.length ] = callback;
},
loaded: function () {
if ( this.toLoad > 0 ) {
this.toLoad--;
if ( this.toLoad === 0 ) {
// Run queued callbacks once
for ( var i = 0; i < this.queue.length; i++ ) this.queue[ i ]();
this.queue = [];
}
}
}
};
var setupCompleted = new LoadTrigger( 1 );
// Used to run user-registered code once HotCat is fully set up and ready.
HC.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();
};
s.onerror = afterLoad; // Clean up, but otherwise ignore errors
head.insertBefore( s, head.firstChild ); // appendChild may trigger bugs in IE6 here
}
function loadJS( page ) {
load( conf.wgServer + conf.wgScript + '?title=' + encodeURIComponent( page ) + '&action=raw&ctype=text/javascript' );
}
function loadURI( href ) {
var url = href;
if ( url.substring( 0, 2 ) === '//' ) url = window.location.protocol + url; else if ( url.substring( 0, 1 ) === '/' ) url = conf.wgServer + url;
load( url );
}
// Load local configurations, overriding the pre-set default values in the HotCat object above. This is always loaded
// from the wiki where this script is executing, even if this script itself is hotlinked from the Commons. This can
// be used to change the default settings, or to provide localized interface texts for edit summaries and so on.
loadJS( 'MediaWiki:Gadget-HotCat.js/local_defaults' );
// Load localized UI texts. These are the texts that HotCat displays on the page itself. Texts shown in edit summaries
// should be localized in /local_defaults above.
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
// explicitly if you're not on the Commons and don't want that.
if ( window.hotcat_translations_from_commons === undefined ) window.hotcat_translations_from_commons = true;
// Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
// Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
if ( window.hotcat_translations_from_commons && conf.wgServer.indexOf( '//commons' ) < 0 ) {
loadURI( '//commons.wikimedia.org/w/index.php?title=' +
'MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage +
'&action=raw&ctype=text/javascript' );
} else {
// Load translations locally
loadJS( 'MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage );
}
} else {
loadTrigger.loaded();
}


// No further changes should be necessary here.
// No further changes should be necessary here.
(function () {


  // First auto-localize the regexps for the category and the template namespaces.
// The following regular expression strings are used when searching for categories in wikitext.
  if (typeof (wgFormattedNamespaces) != 'undefined') {
var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
    function autoLocalize (namespaceNumber, fallback) {
var wikiTextBlankRE = new RegExp( wikiTextBlank, 'g' );
      function create_regexp_str (name)
// Regexp for handling blanks inside a category title or namespace name.
      {
// See http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Title.php?revision=104051&view=markup#l2722
        if (!name || name.length == 0) return "";
// See also http://www.fileformat.info/info/unicode/category/Zs/list.htm
        var regex_name = "";
//  MediaWiki collapses several contiguous blanks inside a page title to one single blank. It also replace a
        for (var i = 0; i < name.length; i++){
// number of special whitespace characters by simple blanks. And finally, blanks are treated as underscores.
          var initial = name.substr (i, 1);
// Therefore, when looking for page titles in wikitext, we must handle all these cases.
          var ll = initial.toLowerCase ();
//  Note: we _do_ include the horizontal tab in the above list, even though the MediaWiki software for some reason
          var ul = initial.toUpperCase ();
// appears to not handle it. The zero-width space \u200B is _not_ handled as a space inside titles by MW.
          if (ll == ul){
var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
            regex_name += initial;
// Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
          } else {
// a link must be on one single line.
            regex_name += '[' + ll + ul + ']';
//   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
        return regex_name.replace (/[ _]/g, '[ _]').replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
// 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.
var formattedNamespaces = conf.wgFormattedNamespaces;
var namespaceIds = conf.wgNamespaceIds;
function autoLocalize( namespaceNumber, fallback ) {
function createRegexpStr( name ) {
if ( !name || !name.length ) return '';
 
var regex_name = '';
for ( var i = 0; i < name.length; i++ ) {
var initial = name.charAt( i ),
ll = initial.toLowerCase(),
ul = initial.toUpperCase();
if ( ll === ul ) regex_name += initial; else regex_name += '[' + ll + ul + ']';
}
return regex_name
.replace( /([\\^$.?*+()])/g, '\\$1' )
.replace( wikiTextBlankRE, wikiTextBlank );
}
 
fallback = fallback.toLowerCase();
var canonical = formattedNamespaces[ String( namespaceNumber ) ].toLowerCase();
var regexp = createRegexpStr( canonical );
if ( fallback && canonical !== fallback ) regexp += '|' + createRegexpStr( fallback );
 
if ( namespaceIds ) {
for ( var cat_name in namespaceIds ) {
if (
typeof cat_name === 'string' &&
cat_name.toLowerCase() !== canonical &&
cat_name.toLowerCase() !== fallback &&
namespaceIds[ cat_name ] === namespaceNumber ) regexp += '|' + createRegexpStr( cat_name );
}
}
return regexp;
}
 
HC.category_canonical = formattedNamespaces[ '14' ];
HC.category_regexp = autoLocalize( 14, 'category' );
if ( formattedNamespaces[ '10' ] ) HC.template_regexp = autoLocalize( 10, 'template' );
 
// Utility functions. Yes, this duplicates some functionality that also exists in other places, but
// to keep this whole stuff in a single file not depending on any other on-wiki JavaScripts, we re-do
// these few operations here.
function make( arg, literal ) {
if ( !arg ) return null;
 
return literal ? document.createTextNode( arg ) : document.createElement( arg );
}
function param( name, uri ) {
uri = 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 ) ) prefix = conf.wgServer + prefix; // Fully expanded URL?
 
if ( href.indexOf( prefix ) && 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 ) 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 ) {
options = options || {};
var lead = options.indicator || '$';
var indicator = escapeRE( lead );
var lbrace = escapeRE( options.lbrace || '{' );
var rbrace = escapeRE( options.rbrace || '}' );
var re;
 
re = new RegExp(
// $$
'(?:' + indicator + '(' + indicator + '))|' +
// $0, $1
'(?:' + indicator + '(\\d+))|' +
// ${key}
'(?:' + indicator + '(?:' + lbrace + '([^' + lbrace + rbrace + ']+)' + rbrace + '))|' +
// $key (only if first char after $ is not $, digit, or { )
'(?:' + indicator + '(?!(?:[' + indicator + lbrace + ']|\\d))(\\S+?)\\b)',
'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();
var replaceShortcuts = ( function () {
var replaceHash = substituteFactory( {
indicator: '#',
lbrace: '[',
rbrace: ']'
} );
return function ( str, map ) {
var s = replaceHash( str, map );
return HC.capitalizePageNames ? capitalize( s ) : s;
};
}() );
 
// Text modification
 
var findCatsRE =
new RegExp( '\\[\\[' + wikiTextBlankOrBidi + '(?:' + HC.category_regexp + ')' + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g' );
 
function replaceByBlanks( match ) {
return match.replace( /(\s|\S)/g, ' ' ); // /./ doesn't match linebreaks. /(\s|\S)/ does.
}
 
function find_category( wikitext, category, once ) {
var cat_regex = null;
if ( HC.template_categories[ category ] ) {
cat_regex = new RegExp(
'\\{\\{' + wikiTextBlankOrBidi + '(' + HC.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi +
'(?:' + HC.template_categories[ category ] + ')' +
wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}',
'g' );
} else {
var cat_name = escapeRE( category );
var initial = cat_name.substr( 0, 1 );
cat_regex = new RegExp(
'\\[\\[' + wikiTextBlankOrBidi + '(' + HC.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi +
( initial === '\\' || !HC.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;
 
function change_category( wikitext, toRemove, toAdd, key, is_hidden ) {
 
function find_insertionpoint( wikitext ) {
var copiedtext = wikitext
.replace( /<!--(\s|\S)*?-->/g, replaceByBlanks )
.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, replaceByBlanks );
// Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
var index = -1;
findCatsRE.lastIndex = 0;
while ( findCatsRE.exec( copiedtext ) !== null ) index = findCatsRE.lastIndex;
 
if ( index < 0 ) {
// 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 {
match = interlanguageRE.exec( copiedtext );
}
if ( match ) index = match.index;
 
return {
idx: index,
onCat: false
};
}
return {
idx: index,
onCat: index >= 0
};
}
 
var summary = [];
var nameSpace = HC.category_canonical;
var cat_point = -1; // Position of removed category;
 
if ( key ) key = '|' + key;
 
var keyChange = ( toRemove && toAdd && toRemove === toAdd && toAdd.length );
var matches;
if ( toRemove && toRemove.length ) {
matches = find_category( wikitext, toRemove );
if ( !matches || !matches.length ) {
return {
text: wikitext,
summary: summary,
error: HC.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 || 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 && before.substring( before.length - 1 ).search( /\S/ ) >= 0 &&
after.length && after.substr( 0, 1 ).search( /\S/ ) >= 0 ) before += ' ';
 
cat_point = before.length;
if ( cat_point === 0 && after.length && after.substr( 0, 1 ) === '\n' ) after = after.substr( 1 );
 
wikitext = before + after;
if ( !keyChange )
if ( HC.template_categories[ toRemove ] ) { summary.push( HC.messages.template_removed.replace( /\$1/g, toRemove ) ); } else { summary.push( HC.messages.cat_removed.replace( /\$1/g, toRemove ) ); }
 
}
}
if ( toAdd && toAdd.length ) {
matches = find_category( wikitext, toAdd );
if ( matches && matches.length ) {
return {
text: wikitext,
summary: summary,
error: HC.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 && suffix.substr( 0, 1 ) !== '\n' ) wikitext += '\n' + suffix; else wikitext += suffix;
} else {
if ( wikitext.length && wikitext.substr( wikitext.length - 1, 1 ) !== '\n' ) wikitext += '\n';
 
wikitext += ( wikitext.length ? '\n' : '' ) + newcatstring;
}
if ( keyChange ) {
var k = key || '';
if ( k.length ) k = k.substr( 1 );
 
summary.push( substitute( HC.messages.cat_keychange, [ null, toAdd, k ] ) );
} else {
summary.push( HC.messages.cat_added.replace( /\$1/g, toAdd ) );
}
if ( HC.uncat_regexp && !is_hidden ) {
var txt = wikitext.replace( HC.uncat_regexp, '' ); // Remove "uncat" templates
if ( txt.length !== wikitext.length ) {
wikitext = txt;
summary.push( HC.messages.uncat_removed );
}
}
}
}
return {
text: wikitext,
summary: summary,
error: null
};
}
 
// The real HotCat UI
 
function evtKeys( e ) {
/* eslint-disable no-bitwise */
e = e || window.event; // W3C, IE
var code = 0;
if ( e.ctrlKey ) { // All modern browsers
// 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;
}
return code;
}
function evtKill( e ) {
e = e || window.event; // W3C, IE
if ( e.preventDefault ) {
e.preventDefault();
e.stopPropagation();
} else {
e.cancelBubble = true;
}
return false;
}
 
var catLine = null,
onUpload = false,
editors = [],
 
commitButton = null,
commitForm = null,
multiSpan = null,
 
pageText = null,
pageTime = null,
pageWatched = false,
watchCreate = false,
watchEdit = false,
minorEdits = false,
editToken = null,
 
is_rtl = false,
serverTime = null,
lastRevId = null,
pageTextRevId = null,
conflictingUser = null,
 
newDOM = false; // true if MediaWiki serves the new UL-LI DOM for categories
 
function CategoryEditor() {
this.initialize.apply( this, arguments );
}
 
function setPage( json ) {
var startTime = null;
if ( json && json.query ) {
if ( json.query.pages ) {
var page = json.query.pages[ !conf.wgArticleId ? '-1' : String( conf.wgArticleId ) ];
if ( page ) {
if ( page.revisions && page.revisions.length ) {
// Revisions are sorted by revision ID, hence [ 0 ] is the one we asked for, and possibly there's a [ 1 ] if we're
// not on the latest revision (edit conflicts and such).
pageText = page.revisions[ 0 ][ '*' ];
if ( page.revisions[ 0 ].timestamp ) pageTime = page.revisions[ 0 ].timestamp.replace( /\D/g, '' );
if ( page.revisions[ 0 ].revid ) pageTextRevId = page.revisions[ 0 ].revid;
if ( page.revisions.length > 1 ) conflictingUser = page.revisions[ 1 ].user;
}
if ( page.lastrevid ) lastRevId = page.lastrevid;
if ( page.starttimestamp ) startTime = page.starttimestamp.replace( /\D/g, '' );
pageWatched = typeof page.watched === 'string';
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 ) interlanguageRE = new RegExp( '((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$' );
}
}
}
// Siteinfo
if ( json.query.general ) {
// ResourceLoader's JSParser doesn't like .case, so override eslint.
// eslint-disable-next-line dot-notation
HC.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 = !HC.dont_add_to_watchlist && json.query.userinfo.options.watchcreations === '1';
watchEdit = !HC.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 ) HC.single_minor = true;
}
}
}
 
var saveInProgress = false;
function initiateEdit( doEdit, failure ) {
if ( saveInProgress ) return;
saveInProgress = true;
var oldButtonState;
if ( commitButton ) {
oldButtonState = commitButton.disabled;
commitButton.disabled = true;
}
 
function fail() {
saveInProgress = false;
if ( commitButton ) commitButton.disabled = oldButtonState;
failure.apply( this, arguments );
}
 
// Must use Ajax here to get the user options and the edit token.
$.getJSON( conf.wgServer + conf.wgScriptPath + '/api.php?' +
'format=json&action=query&rawcontinue=&titles=' + encodeURIComponent( conf.wgPageName ) +
'&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',
function ( json ) {
setPage( json );
doEdit( fail );
} ).fail( function ( req ) {
fail( req.status + ' ' + req.statusText );
} );
}
 
function multiChangeMsg( count ) {
var msg = HC.messages.multi_change;
if ( typeof msg !== 'string' && msg.length )
if ( mw.language && mw.language.convertPlural ) { msg = mw.language.convertPlural( count, msg ); } else { msg = msg[ msg.length - 1 ]; }
 
return substitute( msg, [ null, String( count ) ] );
}
 
function currentTimestamp() {
var now = new Date();
var ts = String( now.getUTCFullYear() );
function two( s ) {
return s.substr( s.length - 2 );
}
ts +=
two( '0' + ( now.getUTCMonth() + 1 ) ) +
two( '0' + now.getUTCDate() ) +
two( '00' + now.getUTCHours() ) +
two( '00' + now.getUTCMinutes() ) +
two( '00' + now.getUTCSeconds() );
return ts;
}
 
function performChanges( failure, singleEditor ) {
if ( pageText === null ) {
failure( HC.messages.multi_error );
return;
}
// Backwards compatibility after message change (added $2 to cat_keychange)
if ( HC.messages.cat_keychange.indexOf( '$2' ) < 0 ) HC.messages.cat_keychange += '"$2"';
 
// More backwards-compatibility with earlier HotCat versions:
if ( !HC.messages.short_catchange ) HC.messages.short_catchange = '[[' + HC.category_canonical + ':$1]]';
 
// Create a form and submit it. We don't use the edit API (api.php?action=edit) because
// (a) sensibly reporting back errors like edit conflicts is always a hassle, and
// (b) we want to show a diff for multi-edits anyway, and
// (c) we want to trigger onsubmit events, allowing user code to intercept the edit.
// Using the form, we can do (b) and (c), and we get (a) for free. And, of course, using the form
// automatically reloads the page with the updated categories on a successful submit, which
// 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 selfEditConflict = ( lastRevId !== null && lastRevId !== conf.wgCurRevisionId || pageTextRevId !== null &&
pageTextRevId !== conf.wgCurRevisionId ) && conflictingUser && conflictingUser === conf.wgUserName;
if ( singleEditor && !singleEditor.noCommit && !HC.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
},
changed = [],
added = [],
deleted = [],
changes = 0,
toEdit = singleEditor ? [ singleEditor ] : editors,
error = null,
edit,
i;
for ( i = 0; i < toEdit.length; i++ ) {
edit = toEdit[ i ];
if ( edit.state === CategoryEditor.CHANGED ) {
result = change_category(
result.text,
edit.originalCategory,
edit.currentCategory,
edit.currentKey,
edit.currentHidden );
if ( !result.error ) {
changes++;
if ( !edit.originalCategory || !edit.originalCategory.length ) {
added.push( edit.currentCategory );
} else {
changed.push( {
from: edit.originalCategory,
to: edit.currentCategory
} );
}
} else if ( error === null ) {
error = result.error;
}
} else if (
edit.state === CategoryEditor.DELETED && edit.originalCategory && edit.originalCategory.length ) {
result = change_category(
result.text,
edit.originalCategory,
null, null, false );
if ( !result.error ) {
changes++;
deleted.push( edit.originalCategory );
} else if ( error === null ) {
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.wpMinoredit.checked = minorEdits;
commitForm.wpWatchthis.checked = !conf.wgArticleId && watchCreate || watchEdit || pageWatched;
if ( conf.wgArticleId || !!singleEditor ) {
// Prepare change-tag save
if ( action && action.value === 'wpSave' ) {
if ( HC.changeTag ) {
commitForm.wpChangeTags.value = HC.changeTag;
HC.messages.using = '';
HC.messages.prefix = '';
}
} else {
commitForm.wpAutoSummary.value = HC.changeTag;
}
if ( changes === 1 ) {
if ( result.summary && result.summary.length ) commitForm.wpSummary.value = HC.messages.prefix + result.summary.join( HC.messages.separator ) + HC.messages.using;
commitForm.wpMinoredit.checked = HC.single_minor || minorEdits;
} else if ( changes ) {
var summary = [];
var shortSummary = [];
// Deleted
for ( i = 0; i < deleted.length; i++ ) summary.push( '-' + substitute( HC.messages.short_catchange, [ null, deleted[ i ] ] ) );
 
if ( deleted.length === 1 ) shortSummary.push( '-' + substitute( HC.messages.short_catchange, [ null, deleted[ 0 ] ] ) ); else if ( deleted.length ) shortSummary.push( '- ' + multiChangeMsg( deleted.length ) );
 
// Added
for ( i = 0; i < added.length; i++ ) summary.push( '+' + substitute( HC.messages.short_catchange, [ null, added[ i ] ] ) );
 
if ( added.length === 1 ) shortSummary.push( '+' + substitute( HC.messages.short_catchange, [ null, added[ 0 ] ] ) ); else if ( added.length ) 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( HC.messages.short_catchange, [ null, changed[ i ].from ] ) + arrow +
substitute( HC.messages.short_catchange, [ null, changed[ i ].to ] ) );
} else {
summary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ i ].from ] ) );
}
}
if ( changed.length === 1 ) {
if ( changed[ 0 ].from !== changed[ 0 ].to ) {
shortSummary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ 0 ].from ] ) + arrow +
substitute( HC.messages.short_catchange, [ null, changed[ 0 ].to ] ) );
} else {
shortSummary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ 0 ].from ] ) );
}
} else if ( changed.length ) {
shortSummary.push( '± ' + multiChangeMsg( changed.length ) );
}
if ( summary.length ) {
summary = summary.join( HC.messages.separator );
if ( summary.length > 200 - HC.messages.prefix.length - HC.messages.using.length ) summary = shortSummary.join( HC.messages.separator );
 
commitForm.wpSummary.value = HC.messages.prefix + summary + HC.messages.using;
}
}
}
 
commitForm.wpTextbox1.value = result.text;
commitForm.wpStarttime.value = serverTime || currentTimestamp();
commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
if ( selfEditConflict ) commitForm.oldid.value = String( pageTextRevId || conf.wgCurRevisionId );
 
// Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
commitForm.hcCommit.click();
}
 
function resolveOne( page, toResolve ) {
var cats = page.categories,
lks = page.links,
is_dab = false,
is_redir = typeof page.redirect === 'string', // Hard redirect?
is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden === 'string',
is_missing = typeof page.missing === 'string',
i;
for ( i = 0; i < toResolve.length; i++ ) {
if ( i && 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 ? HC.existsNo : HC.existsYes );
}
if ( is_missing ) return;
if ( !is_redir && cats && ( HC.disambig_category || HC.redir_category ) ) {
for ( var c = 0; c < cats.length; c++ ) {
var cat = cats[ c ].title;
// Strip namespace prefix
if ( cat ) {
cat = cat.substring( cat.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
if ( cat === HC.disambig_category ) {
is_dab = true;
break;
} else if ( cat === HC.redir_category ) {
is_redir = true;
break;
}
}
}
}
if ( !is_redir && !is_dab ) return;
if ( !lks || !lks.length ) return;
var titles = [];
for ( i = 0; i < lks.length; i++ ) {
if (
// Category namespace -- always true since we ask only for the category links
lks[ i ].ns === 14 &&
// Name not empty
lks[ i ].title && lks[ i ].title.length ) {
// 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 ( !HC.blacklist || !HC.blacklist.test( match ) ) titles.push( match );
}
}
if ( !titles.length ) return;
for ( i = 0; i < toResolve.length; i++ ) {
if ( i && 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( HC.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 ) {
if ( !params || !params.query || !params.query.pages ) return;
for ( var p in params.query.pages ) resolveOne( params.query.pages[ p ], toResolve );
}
 
function resolveMulti( toResolve, callback ) {
var i;
for ( i = 0; i < toResolve.length; i++ ) {
toResolve[ i ].dab = null;
toResolve[ i ].dabInput = toResolve[ i ].lastInput;
}
if ( noSuggestions ) {
callback( toResolve );
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, HC.shortcuts );
toResolve[ i ].dabInputCleaned = v;
args += encodeURIComponent( 'Category:' + v );
if ( i + 1 < toResolve.length ) args += '%7C';
}
$.getJSON( conf.wgServer + conf.wgScriptPath + '/api.php?' + args,
function ( json ) {
resolveRedirects( toResolve, json );
callback( toResolve );
} ).fail( function ( req ) {
if ( !req ) noSuggestions = true;
callback( toResolve );
} );
}
 
function makeActive( which ) {
if ( which.is_active ) return;
for ( var i = 0; i < editors.length; i++ )
if ( editors[ i ] !== which ) editors[ i ].inactivate();
 
which.is_active = true;
if ( which.dab ) {
showDab( which );
} else {
// Check for programmatic value changes.
var expectedInput = which.lastRealInput || which.lastInput || '';
var actualValue = which.text.value || '';
if ( !expectedInput.length && actualValue.length || expectedInput.length && actualValue.indexOf( expectedInput ) ) {
// 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 );
}
}
}
}
 
function showDab( which ) {
if ( !which.is_active ) {
makeActive( which );
} else {
which.showSuggestions( which.dab, false, null, null ); // do autocompletion, no key, no engine selector
which.dab = null;
}
}
 
function multiSubmit() {
var toResolve = [];
for ( var i = 0; i < editors.length; i++ )
if ( editors[ i ].state === CategoryEditor.CHANGE_PENDING || editors[ i ].state === CategoryEditor.OPEN ) toResolve.push( editors[ i ] );
 
if ( !toResolve.length ) {
initiateEdit( function ( failure ) {
performChanges( failure );
}, function ( msg ) {
alert( msg );
} );
return;
}
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 );
} );
}
} );
}
 
function setMultiInput() {
if ( commitButton || onUpload ) return;
commitButton = make( 'input' );
commitButton.type = 'button';
commitButton.value = HC.messages.commit;
commitButton.onclick = multiSubmit;
if ( multiSpan ) multiSpan.parentNode.replaceChild( commitButton, multiSpan ); else catLine.appendChild( commitButton );
}
 
function checkMultiInput() {
if ( !commitButton ) return;
var hasChanges = false;
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ].state !== CategoryEditor.UNCHANGED ) {
hasChanges = true;
break;
}
}
commitButton.disabled = !hasChanges;
}
 
var suggestionEngines = {
opensearch: {
uri: '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1', // $1 = search term
// Function to convert result of uri into an array of category names
handler: 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( '^(' + HC.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 ) {
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: {
uri: '/api.php?format=json&action=query&list=categorymembers&cmtype=subcat&cmlimit=max&cmtitle=Category:$1',
handler: function ( queryResult ) {
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 ) {
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 = {
searchindex: {
name: 'Search index',
engines: [ 'opensearch' ],
cache: {},
show: true,
temp: false,
noCompletion: false
},
pagelist: {
name: 'Page list',
engines: [ 'internalsearch', 'exists' ],
cache: {},
show: true,
temp: false,
noCompletion: false
},
combined: {
name: 'Combined search',
engines: [ 'opensearch', 'internalsearch' ],
cache: {},
show: true,
temp: false,
noCompletion: false
},
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
}
};
 
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 document.body.appendChild( dummyElement );
}
 
// Event keyCodes that we handle in the text input field/suggestion list.
var BS = 8,
TAB = 9,
RET = 13,
ESC = 27,
SPACE = 32,
PGUP = 33,
PGDOWN = 34,
UP = 38,
DOWN = 40,
DEL = 46,
IME = 229;
 
CategoryEditor.prototype = {
 
initialize: function ( line, span, after, key, is_hidden ) {
// If a span is given, 'after' is the category title, otherwise it may be an element after which to
// 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( HC.links.add, true ) );
lk.title = HC.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 = HC.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 () {
this.normalLinks = make( 'span' );
var lk = null;
if ( this.originalCategory && this.originalCategory.length ) {
lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.remove.bind( this );
lk.appendChild( make( HC.links.remove, true ) );
lk.title = HC.tooltips.remove;
this.normalLinks.appendChild( make( ' ', true ) );
this.normalLinks.appendChild( lk );
}
if ( !HC.template_categories[ this.originalCategory ] ) {
lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.open.bind( this );
lk.appendChild( make( HC.links.change, true ) );
lk.title = HC.tooltips.change;
this.normalLinks.appendChild( make( ' ', true ) );
this.normalLinks.appendChild( lk );
if ( !noSuggestions && HC.use_up_down ) {
this.upDownLinks = make( 'span' );
lk = make( 'a' );
lk.href = '#catlinks';
lk.onclick = this.down.bind( this );
lk.appendChild( make( HC.links.down, true ) );
lk.title = HC.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( HC.links.up, true ) );
lk.title = HC.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( HC.links.restore, true ) );
lk.title = HC.tooltips.restore;
this.undelLink.appendChild( make( ' ', true ) );
this.undelLink.appendChild( lk );
this.linkSpan.appendChild( this.undelLink );
},
 
invokeSuggestions: function ( dont_autocomplete ) {
if ( this.engine && suggestionConfigs[ this.engine ] && suggestionConfigs[ this.engine ].temp && !dont_autocomplete ) this.engine = HC.suggestions; // Reset to a search upon input
 
this.state = CategoryEditor.CHANGE_PENDING;
var self = this;
window.setTimeout( function () {
self.textchange( dont_autocomplete );
}, HC.suggest_delay );
},
 
makeForm: function () {
var form = make( 'form' );
form.method = 'POST';
form.onsubmit = this.accept.bind( this );
this.form = form;
var self = this;
var text = make( 'input' );
text.type = 'text';
text.size = HC.editbox_width;
if ( !noSuggestions ) {
// Be careful here to handle IME input. This is browser/OS/IME dependent, but basically there are two mechanisms:
// - Modern (DOM Level 3) browsers use compositionstart/compositionend events to signal composition; if the
//  composition is not canceled, there'll be a textInput event following. During a composition key events are
//  either all suppressed (FF/Gecko), or otherwise have keyDown === IME for all keys (Webkit).
//  - Webkit sends a textInput followed by keyDown === IME and a keyUp with the key that ended composition.
//  - Gecko doesn't send textInput but just a keyUp with the key that ended composition, without sending keyDown
//    first. Gecko doesn't send any keydown while IME is active.
// - Older browsers signal composition by keyDown === IME for the first and subsequent keys for a composition. The
//  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; // W3C, IE
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; // W3C, IE
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(
( 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 () {
self.lastKey = IME;
self.usesComposition = true;
self.ime = true;
} );
$( text ).on( 'compositionend', function () {
self.lastKey = IME;
self.usesComposition = true;
self.ime = false;
} );
$( text ).on( 'textInput', function () {
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 () {
self.usesComposition = false;
self.ime = false;
} );
}
this.text = text;
 
this.icon = make( 'img' );
 
var list = null;
if ( !noSuggestions ) {
list = make( 'select' );
list.onclick = function () {
if ( self.highlightSuggestion( 0 ) ) self.textchange( false, true );
};
list.ondblclick = function ( e ) {
if ( self.highlightSuggestion( 0 ) ) self.accept( e );
};
list.onchange = function () {
self.highlightSuggestion( 0 );
self.text.focus();
};
list.onkeyup = function ( evt ) {
evt = evt || window.event; // W3C, IE
if ( evt.keyCode === ESC ) {
self.resetKeySelection();
self.text.focus();
window.setTimeout( function () {
self.textchange( true );
}, HC.suggest_delay );
} else if ( evt.keyCode === RET ) {
self.accept( evt );
}
};
if ( !HC.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;
}
}
this.list = list;
 
function button_label( id, defaultText ) {
var label = null;
if (
onUpload &&
window.UFUI !== undefined &&
window.UIElements !== undefined &&
UFUI.getLabel instanceof 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;
}
 
// Do not use type 'submit'; we cannot detect modifier keys if we do
var OK = make( 'input' );
OK.type = 'button';
OK.value = button_label( 'wpOkUploadLbl', HC.messages.ok );
OK.onclick = this.accept.bind( this );
this.ok = OK;
 
var cancel = make( 'input' );
cancel.type = 'button';
cancel.value = button_label( 'wpCancelUploadLbl', HC.messages.cancel );
cancel.onclick = this.cancel.bind( this );
this.cancelButton = cancel;
 
var span = make( 'span' );
span.className = 'hotcatinput';
span.style.position = 'relative';
// FF3.6: add the input field first, then the two absolutely positioned elements. Otherwise, FF3.6 may leave the
// 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 ) {
// eslint-disable-next-line no-new
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 ? HC.existsYes : HC.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 ) 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 ) {
return this.show( evt, ( this.engine && suggestionConfigs[ this.engine ].temp ) ? HC.suggestions : this.engine );
},
 
down: function ( evt ) {
return this.show( evt, 'subcat', true );
},
 
up: function ( evt ) {
return this.show( evt, 'parentcat' );
},
 
cancel: function () {
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 ) { this.catLink.title = this.currentKey; } else { this.catLink.title = ''; }
 
if ( this.state === CategoryEditor.UNCHANGED ) {
if ( this.catLink ) this.catLink.style.backgroundColor = 'transparent';
} else {
if ( !onUpload ) {
try {
this.catLink.style.backgroundColor = HC.bg_changed;
} catch ( ex ) {}
}
}
checkMultiInput();
forceRedraw();
},
 
removeEditor: function () {
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();
},
 
rollback: function ( evt ) {
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 ) {
// 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( HC.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 ) {
this.sanitizeInput();
var value = this.text.value.split( '|' );
var key = null;
if ( value.length > 1 ) key = value[ 1 ];
 
var v = value[ 0 ].replace( /_/g, ' ' ).replace( /^\s+|\s+$/g, '' );
if ( HC.capitalizePageNames ) v = capitalize( v );
 
this.lastInput = v;
v = replaceShortcuts( v, HC.shortcuts );
if ( !v.length ) {
this.cancel();
return false;
}
if ( !dontCheck && (
conf.wgNamespaceNumber === 14 && v === conf.wgTitle || HC.blacklist && HC.blacklist.test( v ) ) ) {
this.cancel();
return false;
}
this.currentCategory = v;
this.currentKey = key;
this.currentExists = this.inputExists;
return true;
},
 
accept: function ( evt ) {
// eslint-disable-next-line no-bitwise
this.noCommit = ( evtKeys( evt ) & 1 ) !== 0;
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 ) ?
HC.messages.cat_resolved.replace( /\$1/g, original ) :
null );
}
}
} );
}
return result;
},
 
close: function () {
if ( !this.catLink ) {
// 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( HC.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 ) {
// eslint-disable-next-line no-new
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( HC.links.undo, true ) );
lk.title = HC.tooltips.undo;
span.appendChild( make( ' ', true ) );
span.appendChild( lk );
this.normalLinks.appendChild( span );
this.undoLink = span;
if ( !onUpload ) {
try {
this.catLink.style.backgroundColor = HC.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 () {
// Check again to catch problem cases after redirect resolution
if (
(
this.currentCategory === this.originalCategory &&
(
this.currentKey === this.originalKey ||
this.currentKey === null && !this.originalKey.length ) ) ||
conf.wgNamespaceNumber === 14 && this.currentCategory === conf.wgTitle ||
HC.blacklist && HC.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 ) {
// eslint-disable-next-line no-bitwise
this.doRemove( evtKeys( evt ) & 1 );
return evtKill( evt );
},
 
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 = HC.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 || HC.del_needs_diff;
var self = this;
initiateEdit(
function ( failure ) {
performChanges( failure, self );
},
function ( msg ) {
self.state = self.originalState;
alert( msg );
} );
}
}
},
 
restore: function ( evt ) {
// Can occur only if we do have a commit button and are not on the upload form
this.catLink.title = this.currentKey || '';
this.catLink.style.textDecoration = '';
this.state = this.originalState;
if ( this.state === CategoryEditor.UNCHANGED ) {
this.catLink.style.backgroundColor = 'transparent';
} else {
try {
this.catLink.style.backgroundColor = HC.bg_changed;
} catch ( ex ) {}
}
this.normalLinks.style.display = '';
this.undelLink.style.display = 'none';
checkMultiInput();
return evtKill( evt );
},
 
// Internal operations
 
selectEngine: function ( engineName ) {
if ( !this.engineSelector ) return;
for ( var i = 0; i < this.engineSelector.options.length; i++ ) this.engineSelector.options[ i ].selected = this.engineSelector.options[ i ].value === engineName;
},
 
sanitizeInput: function () {
var v = this.text.value || '';
v = v.replace( /^(\s|_)+/, '' ); // Trim leading blanks and underscores
var re = new RegExp( '^(' + HC.category_regexp + '):' );
if ( re.test( v ) ) v = v.substring( v.indexOf( ':' ) + 1 ).replace( /^(\s|_)+/, '' );
 
if ( HC.capitalizePageNames ) v = capitalize( v );
 
// Only update the input field if there is a difference. IE8 appears to reset the selection
// and place the cursor at the front upon reset, which makes our autocompletetion become a
// nuisance. FF and IE6 don't seem to have this problem.
if ( this.text.value !== null && this.text.value !== v ) this.text.value = v;
},
 
makeCall: function ( url, callbackObj, engine, queryKey, cleanKey ) {
var cb = callbackObj,
e = engine,
v = queryKey,
z = cleanKey,
thisObj = this;
 
function done() {
cb.callsMade++;
if ( cb.callsMade === cb.nofCalls ) {
if ( cb.exists ) cb.allTitles.exists = true;
 
if ( cb.normalized ) cb.allTitles.normalized = cb.normalized;
 
if ( !cb.dontCache && !suggestionConfigs[ cb.engineName ].cache[ z ] ) suggestionConfigs[ cb.engineName ].cache[ z ] = cb.allTitles;
 
thisObj.text.readOnly = false;
if ( !cb.cancelled ) thisObj.showSuggestions( cb.allTitles, cb.noCompletion, v, cb.engineName );
 
if ( cb === thisObj.callbackObj ) thisObj.callbackObj = null;
 
cb = undefined;
}
}
 
$.getJSON( url, function ( json ) {
var titles = e.handler( json, z );
if ( titles && titles.length ) {
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;
}
done();
} ).fail( function ( req ) {
if ( !req ) noSuggestions = true;
cb.dontCache = true;
done();
} );
},
 
callbackObj: null,
 
textchange: function ( dont_autocomplete, force ) {
// Hide all other lists
makeActive( this );
// Get input value, omit sort key, if any
this.sanitizeInput();
var v = this.text.value;
// Disregard anything after a pipe.
var pipe = v.indexOf( '|' );
if ( pipe >= 0 ) {
this.currentKey = v.substring( pipe + 1 );
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 && HC.blacklist && HC.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 ) {
this.showSuggestions( [] );
return;
}
var cleanKey = v.replace( /[\u200E\u200F\u202A-\u202E]/g, '' ).replace( wikiTextBlankRE, ' ' );
cleanKey = replaceShortcuts( cleanKey, HC.shortcuts );
cleanKey = cleanKey.replace( /^\s+|\s+$/g, '' );
if ( !cleanKey.length ) {
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 );
}
},


      fallback = fallback.toLowerCase();
showSuggestions: function ( titles, dontAutocomplete, queryKey, engineName ) {
      var canonical  = wgFormattedNamespaces["" + namespaceNumber].toLowerCase();
this.text.readOnly = false;
      var regexp    = create_regexp_str (canonical);
this.dab = null;
      if (fallback && canonical != fallback) regexp += '|' + create_regexp_str (fallback)
this.showsList = false;
      for (var cat_name in wgNamespaceIds) {
if ( !this.list ) return;
        if (   typeof (cat_name) == 'string'
if ( noSuggestions ) {
            && cat_name.toLowerCase () != canonical
if ( this.list ) this.list.style.display = 'none';
            && cat_name.toLowerCase () != fallback
            && wgNamespaceIds[cat_name] == namespaceNumber)
        {
          regexp += '|' + create_regexp_str (cat_name);
        }
      }
      return regexp;
    }


    if (wgFormattedNamespaces['14']) {
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
      HotCat.category_canonical = wgFormattedNamespaces['14'];
      HotCat.category_regexp = autoLocalize (14, 'category');
    }
    if (wgFormattedNamespaces['10']) {
      HotCat.template_regexp = autoLocalize (10, 'template');
    }
  }
 
  // Utility functions. Yes, this duplicates some functionality that also exists in other places, but
  // to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
  // these few operations here.
  function bind (func, target) {
    var f = func, tgt = target;
    return function () { return f.apply (tgt, arguments); };
  }
  function make (arg, literal) {
    if (!arg) return null;
    return literal ? document.createTextNode (arg) : document.createElement (arg);
  }
  function param (name, uri) {
    if (typeof (uri) == 'undefined' || uri === null) uri = document.location.href;
    var re = 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 = wgScript + '?';
    if (href.indexOf (script) == 0 || href.indexOf (wgServer + script) == 0) {
      // href="/w/index.php?title=..."
      return param ('title', href);
    } else {
      // href="/wiki/..."
      var prefix = wgArticlePath.replace ('$1', "");
      if (href.indexOf (prefix) != 0) prefix = wgServer + prefix; // Fully expanded URL?
      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 catually has the $1 in
    // a query parameter.
    return wgArticlePath.replace('$1', encodeURIComponent (pageName).replace(/%3A/g, ':').replace(/%2F/g, '/'));
  }


  // Text modification
if ( this.icon ) this.icon.style.display = 'none';
 
  var findCatsRE =
    new RegExp ('\\[\\[\\s*(?:' + HotCat.category_regexp + ')\\s*:\[^\\]\]+\\]\\]', 'g');
 
  function replaceByBlanks (match) {
    return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
  }


  function find_category (wikitext, category, once) {
this.inputExists = true; // Default...
    var cat_regex = null;
return;
    if(HotCat.template_categories[category]){
}
      cat_regex = new RegExp ('\\{\\{\\s*(' + HotCat.template_regexp + '(?=\\s*:))?\\s*'
this.engineName = engineName;
                              + '(?:' + HotCat.template_categories[category] + ')'
if ( engineName ) {
                              + '\\s*(\\|.*?)?\\}\\}', 'g'
if ( !this.engineSelector ) this.engineName = null;
                            );
} else {
    } else {
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
      var cat_name  = category.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
}
      var initial  = cat_name.substr (0, 1);
if ( queryKey ) {
      cat_regex = new RegExp ('\\[\\[\\s*(' + HotCat.category_regexp + ')\\s*:\\s*'
if ( this.lastInput.indexOf( queryKey ) ) return;
                              + (initial == '\\' || !HotCat.capitalizePageNames
if ( this.lastQuery && this.lastInput.indexOf( this.lastQuery ) === 0 && this.lastQuery.length > queryKey.length ) return;
                                ? initial
}
                                : '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
this.lastQuery = queryKey;
                              + cat_name.substring (1).replace (/[ _]/g, '[ _]')
                              + '\\s*(\\|.*?)?\\]\\]', '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
  }


  function change_category (wikitext, toRemove, toAdd, key) {
// Get current input text
var v = this.text.value.split( '|' );
var key = v.length > 1 ? '|' + v[ 1 ] : '';
v = ( HC.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 ( HC.blacklist ) {
for ( i = 0; i < titles.length; i++ ) {
if ( HC.blacklist.test( titles[ i ] ) ) {
titles.splice( i, 1 );
i--;
}
}
}
titles.sort(
function ( a, b ) {
if ( a === b ) return 0;


    function find_insertionpoint (wikitext) {       
if ( a.indexOf( b ) === 0 ) return 1;
      var copiedtext = wikitext.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
// a begins with b: a > b
                              .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
if ( b.indexOf( a ) === 0 ) return -1;
      // Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
// b begins with a: a < b
      var index = -1;
// Opensearch may return stuff not beginning with the search prefix!
      findCatsRE.lastIndex = 0;
var prefixMatchA = ( a.indexOf( vNormalized ) === 0 ? 1 : 0 );
      while (findCatsRE.exec(copiedtext) != null) index = findCatsRE.lastIndex;  
var prefixMatchB = ( b.indexOf( vNormalized ) === 0 ? 1 : 0 );
      // We should try to find interwiki links here, but that's for later.
if ( prefixMatchA !== prefixMatchB ) return prefixMatchB - prefixMatchA;
      return index;
    }


    var summary  = [];
// Case-insensitive prefix match!
    var nameSpace = HotCat.category_canonical;
var aLow = a.toLowerCase(),
    var cat_point = -1; // Position of removed category;
bLow = b.toLowerCase();
 
prefixMatchA = ( aLow.indexOf( vLow ) === 0 ? 1 : 0 );
    if (key) key = '|' + key;
prefixMatchB = ( bLow.indexOf( vLow ) === 0 ? 1 : 0 );
    var keyChange = (toRemove && toAdd && toRemove == toAdd && toAdd.length > 0);
if ( prefixMatchA !== prefixMatchB ) return prefixMatchB - prefixMatchA;
    if (toRemove && toRemove.length > 0) {
      var matches = find_category (wikitext, toRemove);
      if (!matches || matches.length == 0) {
        return {text: wikitext, 'summary': summary, error: HotCat.messages.cat_notFound.replace ('$1', 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;
        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', toRemove));
          }
        }
      }
    }
    if (toAdd && toAdd.length > 0) {
      var matches = find_category (wikitext, toAdd);
      if (matches && matches.length > 0) {
        return {text: wikitext, 'summary': summary, error : HotCat.messages.cat_exists.replace ('$1', toAdd)};
      } else {
        if (cat_point < 0)
          cat_point = find_insertionpoint (wikitext);
        var newcatstring = '[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
        if (cat_point >= 0) {
          wikitext = wikitext.substring (0, cat_point) + '\n' + newcatstring + wikitext.substring (cat_point);
        } else {
          if (wikitext.length > 0 && wikitext.substr (wikitext.length - 1, 1) != '\n')
            wikitext += '\n';
          wikitext += newcatstring;
        }
        if (keyChange) {
          var k = key || "";
          if (k.length > 0) k = k.substr (1);
          summary.push (HotCat.messages.cat_keychange.replace ('$1', toAdd) + '"' + k + '"');
        } else {
          summary.push (HotCat.messages.cat_added.replace ('$1', toAdd));
        }
        if (HotCat.uncat_regexp) {
          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};
  }
 
  if (wgAction == 'edit') {
    // Legacy code based on URI parameters, can add/remove/change only one single category. Still
    // used for single-category changes.
    var toRemove = param ('hotcat_removecat');
    var toAdd    = param ('hotcat_newcat');
    if (toAdd) {
      toAdd = toAdd.replace (/_/g, ' ').replace (/^\s+|\s+$/g, "");
      if (toAdd.length == 0) toAdd = null;
    }
    if (toRemove) {
      toRemove = toRemove.replace (/_/g, ' ').replace (/^\s+|\s+$/g, "");
      if (toRemove.length == 0) toRemove = null;
    }
    if (toAdd || toRemove) {
      addOnloadHook (function () {
        if (!document.editform || !document.editform.wpTextbox1) return;
        var isMajor  = param ('hotcat_major') == '1';
        var comment = param ('hotcat_comment') || "";
        var cat_key = param ('hotcat_sortkey');
        var result = change_category (document.editform.wpTextbox1.value, toRemove, toAdd, cat_key);
        var do_commit = !HotCat.noCommit && !result.error && param ('hotcat_nocommit') != '1';
        document.editform.wpTextbox1.value    = result.text;
        if (result.summary && result.summary.length > 0)
          document.editform.wpSummary.value    = HotCat.messages.prefix + result.summary.join ('; ') + comment + HotCat.messages.using;
        if (document.editform.wpMinoredit.checked) {
          // User must have "Mark all edits minor" enabled (or some other script set it...) URL param overrides
          // default setting.
          if (isMajor) document.editform.wpMinoredit.checked = false;
        } else {
          // Minor unless URL parameter present (for legacy compatibility: previously, all single-category changes
          // were always minor).
          document.editform.wpMinoredit.checked = !isMajor;
        }
        if (result.error) alert (result.error);
        if (do_commit) {
          // Hide the entire edit section so as not to tempt the user into editing...
          var content =   document.getElementById ('bodyContent')      // monobook & vector skin
                        || document.getElementById ('mw_contentholder')  // modern skin
                        || document.getElementById ('article');          // classic skins
          if (content) content.style.display = 'none';
          document.editform.submit();
        }   
      });
    }
    return;
  }
 
  // The real HotCat UI
 
  function evtKeys (e) {
    e = e || window.event || window.Event; // W3C, IE, Netscape
    var code = 0;
    if (typeof (e.ctrlKey) != 'undefined') { // All modern browsers
      // 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;
  var onUpload    = false;   
  var editors      = [];
 
  var commitButton = null;
  var commitForm  = null;
  var multiSpan    = null;


  var pageText    = null;
if ( a < b ) return -1;
  var pageTime    = null;
  var pageWatched  = false;
  var watchCreate  = false;
  var watchEdit    = false;
  var minorEdits  = false;


  var is_rtl      = false;
if ( b < a ) return 1;
  var serverTime  = null;


  function setMultiInput () {
return 0;
    if (commitButton || onUpload) return;
} );
    commitButton = make ('input');
// Remove duplicates and self-references
    commitButton.type  = 'button';
for ( i = 0; i < titles.length; i++ ) {
    commitButton.value = HotCat.messages.commit;
if ( i + 1 < titles.length && titles[ i ] === titles[ i + 1 ] ||
    commitButton.onclick = multiSubmit;
conf.wgNamespaceNumber === 14 && titles[ i ] === conf.wgTitle ) {
    if (multiSpan) {
titles.splice( i, 1 );
      multiSpan.parentNode.replaceChild (commitButton, multiSpan);
i--;
    } else {
}
      catLine.appendChild (commitButton);
}
    }
}
    // Get the preferences, so that we can set wpWatchthis correctly later on. Also get information
if ( !titles || !titles.length ) {
    // about whether the current user watches the page. Must use Ajax here.
if ( this.list ) this.list.style.display = 'none';
    if (wgUserName) {
      var request = sajax_init_object ();    
      request.open
        ('GET', wgServer + wgScriptPath + '/api.php?format=json&action=query&meta=userinfo&uiprop=options&prop=info&inprop=watched&titles=' + encodeURIComponent (wgPageName), true);
      request.onreadystatechange =
        function () {
          if (request.readyState != 4) return;
          if (request.status == 200 && request.responseText && /^\s*\{/.test(request.responseText)) {
            var json = eval ('(' + request.responseText + ')');
            if (json && json.query) {
              if (json.query.userinfo && json.query.userinfo.options) {
                watchCreate = json.query.userinfo.options.watchcreations == '1';
                watchEdit  = 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;
              }
              if (json.query.pages) {
                for (var p in json.query.pages) {
                  pageWatched = typeof (json.query.pages[p].watched) == 'string';
                  break;
                }
              }
            }           
          }
        };
      request.setRequestHeader ('Pragma', 'cache=yes');
      request.setRequestHeader ('Cache-Control', 'no-transform');
      request.send (null);       
    }
  }
 
  function checkMultiInput () {
    if (!commitButton) return;
    var has_changes = false;
    for (var i = 0; i < editors.length; i++) {
      if (editors[i].state != CategoryEditor.UNCHANGED) {
        has_changes = true;
        break;
      }
    }
    commitButton.disabled = !has_changes;
  }


  function currentTimestamp () {
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
    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;
  }


  function performChanges () {
if ( engineName && suggestionConfigs[ engineName ] && !suggestionConfigs[ engineName ].temp ) {
    // Don't use the edit API or LAPI, it's always bothersome to report back errors like edit
if ( this.icon ) this.icon.src = armorUri( HC.existsNo );
    // conflicts. Instead, make one remote call (blocking, because we can't continue anyway if
    // it doesn't succeed), getting the page text. Perform the changes on the text, then construct
    // a form to submit all this as a diff.
    // Note: we have to do this even if we already got the page text. Other scripts may have already
    // edited the text, and we don't necessarily get an edit conflict with ourself. Use case: open
    // a file page, add an image note through ImageAnnotator, then change the categories. If HotCat
    // still operates on the page text loaded initially, it'll delete the just added note again, and
    // somehow the MediaWiki software does not produce an edit conflict.
    if (wgArticleId != 0) {
      var request = sajax_init_object ();
      var uri    = wgServer + wgScriptPath
                  + '/api.php?format=json&action=query&titles=' + encodeURIComponent (wgPageName)
                  + '&prop=info%7Crevisions&inprop=watched&rvprop=content%7Ctimestamp&meta=siteinfo';
      request.open ('GET', uri, false); // Yes, synchronous
      request.send (null);
      if (request.status == 200 && request.responseText && /^\s*\{/.test (request.responseText)) {
        setPage (eval ('(' + request.responseText + ')'));
      } else {
        pageText = null;
      }
    }
    if (pageText === null) {
      alert (HotCat.messages.multi_error);
      return;
    }
    // Create a form and submit it
    if (!commitForm) {
      var formContainer = make ('div');
      formContainer.style.display = 'none';
      document.body.appendChild (formContainer);
      formContainer.innerHTML =
          '<form method="post" enctype="multipart/form-data" action="'
        + wgScript + '?title=' + encodeURIComponent (wgPageName)
        + '&action=edit">'
        + '<input type="hidden" name="wpTextbox1" />'
        + '<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" />'
        + '</form>';
      commitForm = formContainer.firstChild;
    }
    var result = { text : pageText };
    var changed = [], added = [], deleted = [], changes = 0;
    for (var i=0; i < editors.length; i++) {
      if (editors[i].state == CategoryEditor.CHANGED) {
        result = change_category (
            result.text
          , editors[i].originalCategory
          , editors[i].currentCategory
          , editors[i].currentKey
        );
        if (!result.error) {
          changes++;
          if (!editors[i].originalCategory || editors[i].originalCategory.length == 0) {
            added.push (editors[i].currentCategory);
          } else {
            changed.push ({from : editors[i].originalCategory, to : editors[i].currentCategory});
          }
        }
      } else if (  editors[i].state == CategoryEditor.DELETED
                && editors[i].originalCategory
                && editors[i].originalCategory.length > 0)
      {
        result = change_category (result.text, editors[i].originalCategory, null, null);
        if (!result.error) {
          changes++;
          deleted.push (editors[i].originalCategory);
        }
      }
    }
    // Fill in the form and submit it
    commitForm.wpAutoSummary.value = 'd41d8cd98f00b204e9800998ecf8427e'; // MD5 hash of the empty string
    commitForm.wpMinoredit.checked = minorEdits;
    commitForm.wpWatchthis.checked = wgArticleId == 0 && watchCreate || watchEdit || pageWatched;
    if (wgArticleId > 0) {
      if (changes == 1) {
        if (result.summary && result.summary.length > 0)
          commitForm.wpSummary.value = HotCat.messages.prefix + result.summary.join ('; ') + HotCat.messages.using;
        commitForm.wpMinoredit.checked = HotCat.single_minor || minorEdits;
      } else if (changes > 1) {
        var summary = [];
        var shortSummary = [];
        // Deleted
        for (var i=0; i < deleted.length; i++) {
          summary.push ('−[[' + HotCat.category_canonical + ':' + deleted[i] + ']]');
        }
        if (deleted.length == 1)
          shortSummary.push ('−[[' + HotCat.category_canonical + ':' + deleted[0] + ']]');
        else if (deleted.length > 1)
          shortSummary.push ('− ' + HotCat.messages.multi_change.replace ('$1', "" + deleted.length));
        // Added
        for (var i=0; i < added.length; i++) {
          summary.push ('+[[' + HotCat.category_canonical + ':' + added[i] + ']]');
        }
        if (added.length == 1)
          shortSummary.push ('+[[' + HotCat.category_canonical + ':' + added[0] + ']]');
        else if (added.length > 1)
          shortSummary.push ('+ ' + HotCat.messages.multi_change.replace ('$1', "" + added.length));
        // Changed
        var arrow = "]]→[[";
        if (is_rtl) arrow = "]]←[[";
        for (var i=0; i < changed.length; i++) {
          if (changed[i].from != changed[i].to) {
            summary.push ('±[[' + HotCat.category_canonical + ':' + changed[i].from + arrow
                        + HotCat.category_canonical + ':' + changed[i].to + ']]');
          } else {
            summary.push ('±[[' + HotCat.category_canonical + ':' + changed[i].from + ']]');
          }
        }
        if (changed.length == 1) {
          if (changed[0].from != changed[0].to) {
            shortSummary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + arrow
                        + HotCat.category_canonical + ':' + changed[0].to + ']]');
          } else {
            shortSummary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + ']]');
          }
        } else if (changed.length > 1) {
          shortSummary.push ('± ' + HotCat.messages.multi_change.replace ('$1', "" + changed.length));
        }
        if (summary.length > 0) {
          summary = summary.join ('; ');
          if (summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length) {
            summary = shortSummary.join ('; ');
          }
          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;
    commitForm.submit();
  }


  function resolveMulti (toResolve, callback) {
this.inputExists = false;
    for (var i = 0; i < toResolve.length; i++) {
}
      toResolve[i].dab = null;
return;
      toResolve[i].dabInput = toResolve[i].lastInput;
}
    }
    if (noSuggestions) {
      callback (toResolve);
      return;
    }
    var request = sajax_init_object ();
    if (!request) {
      noSuggestions = true;
      callback (toResolve);
      return;
    }
    var url = wgServer + wgScriptPath + '/api.php';
    // 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&plnamespace=14&pllimit=50'
            + '&cllimit=' + (toResolve.length * 10) // Category limit is global, link limit is per page
            + '&format=json&titles=';
    for (var i = 0; i < toResolve.length; i++) {
      args += encodeURIComponent ('Category:' + toResolve[i].dabInput);
      if (i+1 < toResolve.length) args += '%7C';
    }
    if (url.length + args.length + 1 > 2000) { // Lowest common denominator: IE has a URI length limit of 2083
      request.open ('POST', url, true);
      request.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
    } else {
      url += '?' + args; args = null;
      request.open ('GET', url, true);
    }
    request.onreadystatechange =
      function () {
        if (request.readyState != 4) return;
        if (request.status != 200 || !request.responseText || !/^\s*\{/.test (request.responseText)) {
          callback (toResolve);
          return;
        }
        resolveRedirects (toResolve, eval ('(' + request.responseText + ')'));
        callback (toResolve);
      };
    request.setRequestHeader ('Pragma', 'cache=yes');
    request.setRequestHeader ('Cache-Control', 'no-transform');
    request.send (args);       
  }
   
  function resolveOne (page, toResolve) {
    var cats    = page.categories;
    var lks      = page.links;
    var is_dab  = false;
    var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
    if (!is_redir && cats && (HotCat.disambig_category || HotCat.redir_category)) {
      for (var c = 0; c < cats.length; c++) {
        var cat = cats[c]['title'];
        // Strip namespace prefix
        if (cat) {
          cat = cat.substring (cat.indexOf (':') + 1).replace(/_/g, ' ');
          if (cat == HotCat.disambig_category) {
            is_dab = true; break;
          } else if (cat == HotCat.redir_category) {
            is_redir = true; break;
          }
        }
      }
    }
    if (!is_redir && !is_dab) return;
    if (!lks || lks.length == 0) return;
    var titles = [];
    for (var i = 0; i < lks.length; i++) {
      if (  lks[i]['ns'] == 14                            // Category namespace
          && 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'];
        titles.push (match.substring (match.indexOf (':') + 1));
        if (is_redir) break;
      }
    }
    for (var j = 0; j < toResolve.length; j++) {
      if (toResolve[j].dabInput != page.title.substring (page.title.indexOf (':') + 1)) continue;
      if (titles.length > 1) {
        toResolve[j].dab = titles;
      } else {
        toResolve[j].inputExists = true; // Might actually be wrong...
        toResolve[j].icon.src = HotCat.existsYes;
        toResolve[j].text.value =
          titles[0] + (toResolve[j].currentKey != null ? '|' + toResolve[j].currentKey : "");
      }
    }
  }


  function resolveRedirects (toResolve, params) {
var firstTitle = titles[ 0 ];
    if (!params || !params.query || !params.query.pages) return;     
var completed = this.autoComplete( firstTitle, v, vNormalized, key, dontAutocomplete );
    for (var p in params.query.pages) resolveOne (params.query.pages[p], toResolve);
var existing = completed || knownToExist || firstTitle === replaceShortcuts( v, HC.shortcuts );
  }
if ( engineName && suggestionConfigs[ engineName ] && !suggestionConfigs[ engineName ].temp ) {
 
this.icon.src = armorUri( existing ? HC.existsYes : HC.existsNo );
  function multiSubmit () {
this.inputExists = existing;
    var toResolve = [];
}
    for (var i = 0; i < editors.length; i++) {
if ( completed ) {
      if (editors[i].state == CategoryEditor.CHANGE_PENDING || editors[i].state == CategoryEditor.OPEN)
this.lastInput = firstTitle;
        toResolve.push (editors[i]);
if ( titles.length === 1 ) {
    }
this.list.style.display = 'none';
    if (toResolve.length == 0) {
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
      performChanges ();
      return;
    }
    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) {
            CategoryEditor.makeActive (firstDab);
          } else if (!dontChange) {
            performChanges ();
          }
        }
    );
  }


  var cat_prefix = null;
return;
  var noSuggestions = false;
}
  var suggestionEngines = {
}
    opensearch :
// (Re-)fill the list
      { uri    : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1' // $1 = search term
while ( this.list.firstChild ) this.list.removeChild( this.list.firstChild );
      ,handler : // Function to convert result of uri into an array of category names
          function (responseText, queryKey) {
            if (!/^\s*\[/.test (responseText)) return null;
            var queryResult = eval ('(' + responseText + ')');
            if (  queryResult != null && queryResult.length == 2
                && queryResult[0].toLowerCase() == 'category:' + queryKey.toLowerCase()
              )
            {
              var titles = queryResult[1];
              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
                } else {
                  titles.splice (i, 1); // Nope, it's not a category after all.
                  i--;
                }
              }
              return titles;
            }
            return null;     
          }
      }
    ,internalsearch :
      { uri    : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1'
      ,handler :
          function (responseText, queryKey) {
            if (!/^\s*\{/.test (responseText)) return null;
            var queryResult = eval ('(' + responseText + ')');
            if (queryResult && queryResult.query && queryResult.query.allpages) {
              var titles = queryResult.query.allpages;
              var key    = queryKey.toLowerCase();
              for (var i = 0; i < titles.length; i++) {
                titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
                if (titles[i].toLowerCase().indexOf (key) != 0) {
                  titles.splice (i, 1); // Doesn't start with the query key
                  i--;
                }
              }
              return titles;
            }
            return null;
          }
      }
    ,subcategories :
      { uri    : '/api.php?format=json&action=query&list=categorymembers&cmnamespace=14&cmlimit=max&cmtitle=Category:$1'
      ,handler :
          function (responseText, queryKey) {
            if (!/^\s*\{/.test (responseText)) return null;
            var queryResult = eval ('(' + responseText + ')');
            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 (responseText, queryKey) {
            if (!/^\s*\{/.test (responseText)) return null;
            var queryResult = eval ('(' + responseText + ')');
            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 = {
for ( i = 0; i < titles.length; i++ ) {
    searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}, show: true, temp: false, noCompletion : false}
var opt = make( 'option' );
  ,pagelist    : {name: 'Page list', engines: ['internalsearch'], cache: {}, show: true, temp: false, noCompletion : false}
opt.appendChild( make( titles[ i ], true ) );
  ,combined    : {name: 'Combined search', engines: ['opensearch', 'internalsearch'], cache: {}, show: true, temp: false, noCompletion : false}
opt.selected = completed && ( i === 0 );
  ,subcat      : {name: 'Subcategories', engines: ['subcategories'], cache: {}, show: true, temp: true, noCompletion : true}
this.list.appendChild( opt );
  ,parentcat  : {name: 'Parent categories', engines: ['parentcategories'], cache: {}, show: true, temp: true, noCompletion : true}
}
  };
this.displayList();
},


  function CategoryEditor () { this.initialize.apply (this, arguments); };
displayList: function () {
  CategoryEditor.UNCHANGED      = 0;
this.showsList = true;
  CategoryEditor.OPEN          = 1; // Open, but no input yet
if ( !this.is_active ) {
  CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
this.list.style.display = 'none';
  CategoryEditor.CHANGED        = 3;
if ( this.engineSelector ) this.engineSelector.style.display = 'none';
  CategoryEditor.DELETED        = 4;
 
  CategoryEditor.makeActive = function (toActivate) {
    for (var i = 0; i < editors.length; i++) {
      if (editors[i] != toActivate) editors[i].inactivate ();
    }
    toActivate.is_active = true;
    if (toActivate.dab) {
      toActivate.showSuggestions (toActivate.dab, false, null, null); // do autocompletion, no key, no engine selector
      toActivate.dab = null;
    }
  };
 
  CategoryEditor.prototype = {
   
    initialize : function (line, span, after, key) {
      // If a span is given, 'after' is the category title, otherwise it may be an element after which to
      // 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;
        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 hotcatlink';
        var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
        lk.appendChild (make (HotCat.links.add, true)); lk.title = HotCat.tooltips.add;     
        this.linkSpan.appendChild (lk);
        span = make ('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.line              = line;
      this.engine            = HotCat.suggestions;
      this.span              = span;
      this.currentCategory    = this.originalCategory;
      this.currentExists      = this.originalExists;
      this.currentKey        = this.originalKey;
      this.state              = CategoryEditor.UNCHANGED;
      this.lastSavedState    = CategoryEditor.UNCHANGED;
      this.lastSavedCategory  = this.originalCategory;
      this.lastSavedKey      = this.originalKey;
      this.lastSavedExists    = this.originalExists;
      editors[editors.length] = this;
    },
   
    makeLinkSpan : function () {
      this.normalLinks = make ('span');
      var lk = null;
      if (this.originalCategory && this.originalCategory.length > 0) {
        lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.remove, this);
        lk.appendChild (make (HotCat.links.remove, true)); lk.title = HotCat.tooltips.remove;
        this.normalLinks.appendChild (make (' ', true));
        this.normalLinks.appendChild (lk);
      }
      if (!HotCat.template_categories[this.originalCategory]) {
        lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, 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 = bind (this.down, 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 = bind (this.up, 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 hotcatlink';
      this.linkSpan.appendChild (this.normalLinks);
      this.undelLink = make ('span');
      this.undelLink.style.display = 'none';
      lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.restore, 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);
    },
   
    makeForm : function () {
      var form = make ('form');
      form.method = 'POST'; form.onsubmit = bind (this.accept, this);
      this.form = form;
     
      var text = make ('input'); text.type = 'text'; text.size = HotCat.editbox_width;
      if (!noSuggestions) {
        text.onkeyup =
          bind (
            function (evt) {
              evt = evt || window.event || window.Event; // W3C, IE, Netscape
              var key = evt.keyCode || 0;
              if (key == 38 || key == 40 || key == 33 || key == 34) { // Up and down arrows, page up/down
                // In case a browser doesn't generate keypress events for arrow keys...
                if (this.keyCount == 0) return this.processKey (evt);
              } else {
                if (key == 27) { // ESC
                  if (!this.resetKeySelection ()) {
                    // No undo of key selection: treat ESC as "cancel".
                    this.cancel ();
                    return;
                  }
                }
                // Also do this for ESC as a workaround for Firefox bug 524360
                // https://bugzilla.mozilla.org/show_bug.cgi?id=524360
                var dont_autocomplete = (key == 8 || key == 46 || key == 27); // BS, DEL, ESC
                if (this.engine && suggestionConfigs[this.engine] && suggestionConfigs[this.engine].temp && !dont_autocomplete) {
                  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);
              }
              return true;
            }
          ,this
          );
        text.onkeydown =
          bind (
            function (evt) {
              evt = evt || window.event || window.Event; // W3C, IE, Netscape
              this.lastKey = evt.keyCode || 0;
              this.keyCount = 0;
              // Handle return explicitly, to override the default form submission to be able to check for ctrl
              if (this.lastKey == 13) this.accept (evt);
              // Inhibit default behavior of ESC (revert to last real input in FF: we do that ourselves)
              if (this.lastKey == 27) return evtKill (evt);
              return true;
            }
          ,this
          );
        // And handle continued pressing of arrow keys
        text.onkeypress = bind (function (evt) {this.keyCount++; return this.processKey (evt);}, this);
      }
      text.onfocus = bind (function () { CategoryEditor.makeActive (this); }, this);
      this.text = text;
     
      this.icon = make ('img');
     
      var list = null;
      if (!noSuggestions) {
        list = make ('select');
        list.onclick    = bind ( function (e) { if (this.highlightSuggestion (0)) this.textchange (false, true); }, this);
        list.ondblclick = bind (function (e) { if (this.highlightSuggestion (0)) this.accept (e); }, this);
        list.onchange = bind (function (e) { this.highlightSuggestion (0); this.text.focus(); }, this);
        list.onkeyup =
          bind (
            function (evt) {
              evt = evt || window.event || window.Event; // W3C, IE, Netscape
              if (evt.keyCode == 27) {
                this.resetKeySelection ();
                this.text.focus();
                var self = this;
                window.setTimeout (function () {self.textchange (true);}, HotCat.suggest_delay);
              } else if (evt.keyCode == 13) {
                this.accept (evt);
              }
            }
          ,this
          );
        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 = bind (
            function () {
              this.engine = this.engineSelector.options[this.engineSelector.selectedIndex].value;
              this.textchange (true, true); // Don't autocomplete, force re-display of list
            }
          ,this
          );
          this.engineSelector = engineSelector;
        }
      }
      this.list = list;
     
      function button_label (id, defaultText) {
        var label = null;
        if (  onUpload
            && typeof (UFUI) != 'undefined'
            && 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;  
      }


      // Do not use type 'submit'; we cannot detect modifier keys if we do
return;
      var OK = make ('input'); OK.type = 'button';
}
      OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
var nofItems = ( this.list.options.length > HC.listSize ? HC.listSize : this.list.options.length );
      OK.onclick = bind (this.accept, this);
if ( nofItems <= 1 ) nofItems = 2;
      this.ok = OK;
       
      var cancel = make ('input'); cancel.type = 'button';
      cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
      cancel.onclick = bind (this.cancel, this);
      this.cancelButton = cancel;
       
      if (list) form.appendChild (list);
      if (this.engineSelector) form.appendChild (this.engineSelector);
      form.appendChild (text);
      if (!noSuggestions) form.appendChild (this.icon);
      form.appendChild (OK);
      form.appendChild (cancel);
      form.style.display = 'none';
      this.span.appendChild (form);
    },


    display : function (evt) {
this.list.size = nofItems;
      if (this.isAddCategory && !onUpload) {
this.list.style.align = is_rtl ? 'right' : 'left';
        var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
this.list.style.zIndex = 5;
      }
this.list.style.position = 'absolute';
      if (!commitButton && !onUpload) {
// Compute initial list position. First the height.
        for (var i = 0; i < editors.length; i++) {
var anchor = is_rtl ? 'right' : 'left';
          if (editors[i].state != CategoryEditor.UNCHANGED) {
var listh = 0;
            setMultiInput();
if ( this.list.style.display === 'none' ) {
            break;
// Off-screen display to get the height
          }
this.list.style.top = this.text.offsetTop + 'px';
        }
this.list.style[ anchor ] = '-10000px';
      }
this.list.style.display = '';
      if (!this.form) {
listh = this.list.offsetHeight;
        this.makeForm ();
this.list.style.display = 'none';
      }
} else {
      if (this.list) this.list.style.display = 'none';
listh = this.list.offsetHeight;
      if (this.engineSelector) this.engineSelector.style.display = 'none';
}
      this.currentCategory = this.lastSavedCategory;
// Approximate calculation of maximum list size
      this.currentExists  = this.lastSavedExists;
var maxListHeight = listh;
      this.currentKey      = this.lastSavedKey;
if ( nofItems < HC.listSize ) maxListHeight = ( listh / nofItems ) * HC.listSize;
      this.icon.src = 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;
      // Display the form
      if (this.catLink) this.catLink.style.display = 'none';
      this.linkSpan.style.display = 'none';
      this.form.style.display = 'inline';
      CategoryEditor.makeActive (this);
      // 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;
    },


    open : function (evt) {
function viewport( what ) {
      var result = this.display (evt);
if ( is_webkit && !document.evaluate ) {
      var v = this.lastSavedCategory;  
// Safari < 3.0
      if (v.length == 0) return result;
return window[ 'inner' + what ];
}
var s = 'client' + what;
if ( window.opera ) return document.body[ s ];


      if (this.engine && suggestionConfigs[this.engine].temp) this.engine = HotCat.suggestions;
return ( document.documentElement ? document.documentElement[ s ] : 0 ) || document.body[ s ] || 0;
      this.textchange (false, true); // do autocompletion, force display of suggestions
}
      return result;
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;


    down : function (evt) {
if ( !is_webkit ) result = scroll_offset( 'Width' ) - viewport( 'Width' ) - result;
      var result = this.display (evt);
      var v = this.lastSavedCategory;
      if (v.length == 0) return result;


      this.text.readOnly = true; // This request may be very slow!
// Now all have webkit behavior, i.e. zero if at the leftmost edge.
      this.engine = 'subcat';
}
      this.textchange (false, true);
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 += ( node.offsetTop || 0 );
l += ( node.offsetLeft || 0 );
node = node.offsetParent;
} while ( node );
return {
x: l,
y: t
};
}


      return result;
var textPos = position( this.text ),
    },
nl = 0,
nt = 0,
offset = 0,
// Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
if ( this.engineName ) {
this.engineSelector.style.zIndex = 5;
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.
if ( this.engineSelector.style.display === 'none' ) {
this.engineSelector.style[ anchor ] = '-10000px';
this.engineSelector.style.top = '0';
this.engineSelector.style.display = '';
offset = this.engineSelector.offsetHeight;
this.engineSelector.style.display = 'none';
} else {
offset = this.engineSelector.offsetHeight;
}
this.engineSelector.style[ anchor ] = nl + 'px';
}
if ( textPos.y < maxListHeight + offset + 1 ) {
// The list might extend beyond the upper border of the page. Let's avoid that by placing it
// below the input text field.
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;
}
var relative_offset = 0;
if ( left < scroll ) relative_offset = scroll - left; else if ( right > scroll + view_w ) relative_offset = -( right - scroll - view_w );


    up : function (evt) {
if ( is_rtl ) relative_offset = -relative_offset;
      var result = this.display (evt);
      var v = this.lastSavedCategory;
      if (v.length == 0) return result;


      this.engine = 'parentcat';
if ( relative_offset ) this.list.style[ anchor ] = ( nl + relative_offset ) + 'px';
      this.textchange (false, true);
}
},


      return result;
autoComplete: function ( newVal, actVal, normalizedActVal, key, dontModify ) {
    },
if ( newVal === actVal ) return true;


    cancel : function () {
if ( dontModify || this.ime || !this.canSelect() ) return false;
      if (this.isAddCategory && !onUpload) {
        this.removeEditor(); // We added a new adder when opening
        return;
      }
      // Close, re-display link
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      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;
      if (this.state == CategoryEditor.UNCHANGED) {
        if (this.catLink) this.catLink.style.backgroundColor = 'transparent';
      } else {
        if (!onUpload) {
          try {
            this.catLink.style.backgroundColor = HotCat.bg_changed;
          } catch (ex) {}
        }
      }
      checkMultiInput ();
    },


    removeEditor : function () {
// If we can't select properly or an IME composition is ongoing, autocompletion would be a major annoyance to the user.
      var next = this.span.nextSibling;
if ( newVal.indexOf( actVal ) ) {
      if (next) next.parentNode.removeChild (next);
// Maybe it'll work with the normalized value (NFC)?
      this.span.parentNode.removeChild (this.span);
if ( normalizedActVal && newVal.indexOf( normalizedActVal ) === 0 ) {
      for (var i = 0; i < editors.length; i++) {
if ( this.lastRealInput === actVal ) this.lastRealInput = normalizedActVal;
        if (editors[i] == this) {
          editors.splice (i, 1);
          break;
        }
      }
      checkMultiInput ();
      var self = this;
      window.setTimeout (function () {delete self;}, 10);
    },


    rollback : function (evt) {
actVal = normalizedActVal;
      this.undoLink.parentNode.removeChild (this.undoLink);
} else {
      this.undoLink = null;
return false;
      this.currentCategory = this.originalCategory;
}
      this.currentKey = this.originalKey;
}
      this.currentExists = this.originalExists;
// Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
      this.lastSavedCategory = this.originalCategory;
// such that it can be easily removed by typing backspace if the suggestion is unwanted.
      this.lastSavedKey = this.originalKey;
this.text.focus();
      this.lastSavedExists = this.originalExists;
this.text.value = newVal + key;
      this.state = CategoryEditor.UNCHANGED;
this.setSelection( actVal.length, newVal.length );
      if (!this.currentCategory || this.currentCategory.length == 0) {
return true;
        // 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.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 () {
canSelect: function () {
      if (this.list) this.list.style.display = 'none';
return this.text.setSelectionRange ||
      if (this.engineSelector) this.engineSelector.style.display = 'none';
this.text.createTextRange ||
      this.is_active = false;
this.text.selectionStart !== undefined &&
    },
this.text.selectionEnd !== undefined;
},


    acceptCheck : function (dontCheck) {
setSelection: function ( from, to ) {
      this.sanitizeInput ();
// this.text must be focused (at least on IE)
      var value = this.text.value.split('|');
if ( !this.text.value ) return;
      var key  = null;
if ( this.text.setSelectionRange ) { // e.g. khtml
      if (value.length > 1) key = value[1];
this.text.setSelectionRange( from, to );
      var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
} else if ( this.text.selectionStart !== undefined ) {
      if (HotCat.capitalizePageNames) v = capitalize (v);
if ( from > this.text.selectionStart ) {
      this.lastInput = v;
this.text.selectionEnd = to;
      if (v.length == 0) {
this.text.selectionStart = from;
        this.cancel ();
} else {
        return false;
this.text.selectionStart = from;
      }
this.text.selectionEnd = to;
      if (!dontCheck && wgNamespaceNumber == 14 && v == wgTitle) {
}
        this.cancel ();
} else if ( this.text.createTextRange ) { // IE
        return false;
var new_selection = this.text.createTextRange();
      }
new_selection.move( 'character', from );
      this.currentCategory = v;
new_selection.moveEnd( 'character', to - from );
      this.currentKey = key;
new_selection.select();
      this.currentExists = this.inputExists;
}
      return true;
},
    },
   
    accept : function (evt) {
      this.noCommit = (evtKeys (evt) & 1) != 0;
      var result = evtKill (evt);
      if (this.acceptCheck ()) {
        var toResolve = [this];
        var original  = this.currentCategory;
        resolveMulti (
            toResolve
          , function (resolved) {
              if (resolved[0].dab) {
                CategoryEditor.makeActive (resolved[0]);
              } else {
                if (resolved[0].acceptCheck(true)) {
                  resolved[0].commit (
                    (resolved[0].currentCategory != original)
                      ? HotCat.messages.cat_resolved.replace ('$1', original)
                      : null
                  );
                }
              }
            }
        );
      }
      return result;
    },


    close : function () {
getSelection: function () {
      if (!this.catLink) {
var from = 0,
        // Create a catLink
to = 0;
        this.catLink = make ('a');
// this.text must be focused (at least on IE)
        this.catLink.appendChild (make ('foo', true));
if ( !this.text.value ) {
        this.catLink.style.display = 'none';
// No text.
        this.span.insertBefore (this.catLink, this.span.firstChild.nextSibling);
} else if ( this.text.selectionStart !== undefined ) {
      }
from = this.text.selectionStart;
      this.catLink.removeChild (this.catLink.firstChild);
to = this.text.selectionEnd;
      this.catLink.appendChild (make (this.currentCategory, true));
} else if ( document.selection && document.selection.createRange ) { // IE
      this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
var rng = document.selection.createRange().duplicate();
      this.catLink.title = "";
if ( rng.parentElement() === this.text ) {
      this.catLink.className = this.currentExists ? "" : 'new';
try {
      this.lastSavedCategory = this.currentCategory;
var textRng = this.text.createTextRange();
      this.lastSavedKey      = this.currentKey;
textRng.move( 'character', 0 );
      this.lastSavedExists  = this.currentExists;
textRng.setEndPoint( 'EndToEnd', rng );
      // Close form and redisplay category
// We're in a single-line input box: no need to care about IE's strange
      if (this.list) this.list.style.display = 'none';
// handling of line ends
      if (this.engineSelector) this.engineSelector.style.display = 'none';
to = textRng.text.length;
      this.form.style.display = 'none';
textRng.setEndPoint( 'EndToStart', rng );
      this.catLink.style.display = "";
from = textRng.text.length;
      if (this.isAddCategory) {
} catch ( notFocused ) {
        if (onUpload) {
from = this.text.value.length;
          var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
to = from; // At end of text
        }
}
        this.isAddCategory = false;
}
        this.linkSpan.parentNode.removeChild (this.linkSpan);
}
        this.makeLinkSpan ();
return {
        this.span.appendChild (this.linkSpan);
start: from,
      }
end: to
      if (!this.undoLink) {
};
        // Append an undo link.
},
        var span = make ('span');
        var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.rollback, 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 ();
    },
   
    commit : function (comment) {
      // Check again to catch problem cases after redirect resolution
      if (  (  this.currentCategory == this.originalCategory
              && (this.currentKey == this.originalKey
                  || this.currentKey === null && this.originalKey.length == 0
                )
            )
          || wgNamespaceNumber == 14 && this.currentCategory == wgTitle
        )
      {
        this.cancel ();
        return;
      }
      if (commitButton || onUpload) {
        this.close ();
      } else {
        if (this.list) this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        // Execute change from this.originalCategory to this.currentCategory|this.currentKey,
        var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
                  + '&action=edit';
        var url = editlk + '&hotcat_newcat=' + encodeURIComponent (this.currentCategory); 
        if (this.currentKey != null) url += '&hotcat_sortkey=' + encodeURIComponent (this.currentKey);
        if (this.originalCategory.length > 0)
          url += '&hotcat_removecat=' + encodeURIComponent (this.originalCategory);
        if (comment) url += '&hotcat_comment=' + encodeURIComponent (comment);
        if (this.noCommit || HotCat.no_autocommit) url += '&hotcat_nocommit=1';
        if (!HotCat.single_minor) url += '&hotcat_major=1';
        document.location = url;
      }
    },
   
    remove : function (evt) {
      this.doRemove (evtKeys (evt) & 1);
      return evtKill (evt);
    },
   
    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.style.textDecoration = 'line-through';
        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 {
          // Execute single category deletion.
          var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
                    + '&action=edit';
          if (noCommit || HotCat.no_autocommit) editlk += '&hotcat_nocommit=1';
          if (!HotCat.single_minor) editlk += '&hotcat_major=1';
          document.location =
            editlk + '&hotcat_removecat=' + encodeURIComponent (this.originalCategory);
        }
      }
    },
   
    restore : function (evt) {
      // Can occur only if we do have a commit button and are not on the upload form
      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
       
    selectEngine : function (engineName) {
      if (!this.engineSelector) return;
      for (var i = 0; i < this.engineSelector.options.length; i++) {
        this.engineSelector.options[i].selected = this.engineSelector.options[i].value == engineName;
      }
    },


    sanitizeInput : function () {
saveView: function () {
      var v = this.text.value || "";
this.lastSelection = this.getSelection();
      v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
},
      var re = new RegExp ('^(' + HotCat.category_regexp + '):');
      if (re.test (v)) v = v.substring (v.indexOf (':') + 1);
      if (HotCat.capitalizePageNames) v = capitalize (v);
      // Only update the input field if there is a difference. IE8 appears to reset the selection
      // and place the cursor at the front upon reset, which makes our autocompletetion become a
      // nuisance. FF and IE6 don't seem to have this problem.
      if (this.text.value != null && this.text.value != v)
        this.text.value = v;
    },


    makeCall : function (url, callbackObj, engine, queryKey) {
processKey: function ( evt ) {
      var cb = callbackObj;
var dir = 0;
      var e  = engine;
switch ( this.lastKey ) {
      var v  = queryKey;
case UP:
      var r  = sajax_init_object ();
dir = -1;
      cb.requests.push (r);
break;
      r.open('GET', url, true);
case DOWN:
      r.onreadystatechange =
dir = 1;
        bind (
break;
          function () {
case PGUP:
            if (r.readyState == 4) {
dir = -HC.listSize;
              if (r.status != 200) cb.dontCache = true;
break;
              if (r.status == 200 && r.responseText != null) {
case PGDOWN:
                var titles = e.handler (r.responseText, v);
dir = HC.listSize;
                if (titles && titles.length > 0) {
break;
                  if (cb.allTitles == null) {
case ESC: // Inhibit default behavior (revert to last real input in FF: we do that ourselves)
                    cb.allTitles = titles;
return evtKill( evt );
                  } else {
}
                    cb.allTitles = cb.allTitles.concat (titles);
if ( dir ) {
                  }
if ( this.list.style.display !== 'none' ) {
                }
// List is visible, so there are suggestions
              }
this.highlightSuggestion( dir );
              cb.callsMade++;
// 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.
            if (cb.callsMade == cb.nofCalls) {
return evtKill( evt );
              if (!cb.dontCache && !suggestionConfigs[cb.engineName].cache[v]) {
} else if (
                suggestionConfigs[cb.engineName].cache[v] = cb.allTitles;
this.keyCount <= 1 &&
              }
( !this.callbackObj || this.callbackObj.callsMade === this.callbackObj.nofCalls ) ) {
              this.text.readOnly = false;
// If no suggestions displayed, get them, unless we're already getting them.
              if (!cb.cancelled) this.showSuggestions (cb.allTitles, cb.noCompletion, v, cb.engineName);
this.textchange();
              if (cb === this.callbackObj) this.callbackObj = null;
}
              delete cb;
}
            }
return true;
          }
},
        ,this
        );
      r.setRequestHeader ('Pragma', 'cache=yes');
      r.setRequestHeader ('Cache-Control', 'no-transform');
      r.send (null);
    },


    callbackObj : null,
highlightSuggestion: function ( dir ) {
if ( noSuggestions || !this.list || this.list.style.display === 'none' ) return false;


    textchange : function (dont_autocomplete, force) {
var curr = this.list.selectedIndex;
      // Hide all other lists
var tgt = -1;
      CategoryEditor.makeActive (this);
if ( dir === 0 ) {
      if (noSuggestions) {
if ( curr < 0 || curr >= this.list.options.length ) return false;
        // 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;
      }
     
      // Get input value, omit sort key, if any
      this.sanitizeInput ();
      var v = this.text.value;
      // Disregard anything after a pipe.
      var pipe = v.indexOf ('|');
      if (pipe >= 0) v = v.substring (0, pipe);
      if (this.lastInput == v && !force) return; // No change
      if (this.lastInput != v) checkMultiInput ();
      this.lastInput = v;
      this.lastRealInput = v;
      if (v.length == 0) { this.showSuggestions([]); return; }
      if (!sajax_init_object ()) {
        noSuggestions = true;
        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 (this.callbackObj) this.callbackObj.cancelled = true;
      var engineName  = suggestionConfigs[this.engine] ? this.engine : 'combined';


      dont_autocomplete = dont_autocomplete || suggestionConfigs[engineName].noCompletion;
tgt = curr;
      if (suggestionConfigs[engineName].cache[v]) {
} else {
        this.showSuggestions (suggestionConfigs[engineName].cache[v], dont_autocomplete, v, engineName);
tgt = curr < 0 ? 0 : curr + dir;
        return;
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;


      var engines = suggestionConfigs[engineName].engines;
this.list.options[ tgt ].selected = true;
      this.callbackObj =
// Get current input text
        {allTitles: null, requests: [], callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engineName};  
var v = this.text.value.split( '|' );
      for (var j = 0; j < engines.length; j++) {
var key = v.length > 1 ? '|' + v[ 1 ] : '';
        engine = suggestionEngines[engines[j]];
var completed = this.autoComplete( this.list.options[ tgt ].text, this.lastRealInput, null, key, false );
        var url = wgServer + wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (v));
if ( !completed || this.list.options[ tgt ].text === this.lastRealInput ) {
        this.makeCall (url, this.callbackObj, engine, v);
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;
    showSuggestions : function (titles, dontAutocomplete, queryKey, engineName) {
this.inputExists = true; // Might be wrong if from a dab list...
      this.text.readOnly = false;
if ( this.icon ) this.icon.src = armorUri( HC.existsYes );
      this.dab = null;
      if (!this.list) return;
      if (noSuggestions) {
        if (this.list) this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        if (this.icon) this.icon.style.display = 'none';
        this.inputExists = true; // Default...
        return;
      }
      var haveEngine = !!engineName;
      if (haveEngine) {
        haveEngine = this.engineSelector != 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]);


      if (titles) {
this.state = CategoryEditor.CHANGE_PENDING;
        var vLow = v.toLowerCase ();
}
        titles.sort (
return true;
          function (a, b) {
},
            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 (v) == 0 ? 1 : 0);
            var prefixMatchB = (b.indexOf (v) == 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 (var i = 0; i < titles.length; i++) {
          if (  i+1 < titles.length && titles[i] == titles[i+1]
              || wgNamespaceNumber == 14 && titles[i] == 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 = HotCat.existsNo;
          this.inputExists = false;
        }
        return;
      }
                   
      var firstTitle = titles[0];
      var completed = this.autoComplete (firstTitle, v, key, dontAutocomplete);
      if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
        this.icon.src = completed ? HotCat.existsYes : HotCat.existsNo;
        this.inputExists = completed;
      }
      if (completed) {
        this.lastInput = firstTitle;
        if (titles.length == 1) {
          this.list.style.display = 'none';
          if (this.engineSelector) this.engineSelector.style.display = 'none';
          return;
        }
      }
      if (!this.is_active) {
        this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        return;
      }
      var nofItems = (titles.length > HotCat.list_size ? HotCat.list_size : titles.length);
      if (nofItems <= 1) nofItems = 2;
      this.list.size = nofItems;
      this.list.style.align    = 'left';
      this.list.style.zIndex  = 5;
      this.list.style.position = 'absolute';
      // Compute initial list position. First the height.
      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.left = '-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 scroll_offset (what) {
resetKeySelection: function () {
        var s = 'scroll' + what;
if ( noSuggestions || !this.list || this.list.style.display === 'none' ) return false;
        return (document.documentElement ? document.documentElement[s] : 0)
              || document.body[s] || 0;
      } 
      function viewport (what) {
        if (typeof (is_safari) != 'undefined' && is_safari && !document.evaluate)
          return window['inner' + what];
        var s = 'client' + what;
        if (typeof (is_opera) != 'undefined' && is_opera) return document.body[s];
        return (document.documentElement ? document.documentElement[s] : 0)
              || document.body[s] || 0;
      }
      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};
      }


      // IE6 seems to report in this.text.offsetTop and this.text.offsetLeft global offsets??
var curr = this.list.selectedIndex;
      // Possibly this has something to do with the special status of input elements in IE as
if ( curr >= 0 && curr < this.list.options.length ) {
      // "windowed controls". Calculate the relative offsets manually.
this.list.options[ curr ].selected = false;
      var textPos = position (this.text);
// Get current input text
      var catLinePos = position (this.line);
var v = this.text.value.split( '|' );
      var textTop = textPos.y - catLinePos.y;
var key = v.length > 1 ? '|' + v[ 1 ] : '';
      var textLeft = textPos.x - catLinePos.x;
// ESC is handled strangely by some browsers (e.g., FF); somehow it resets the input value before
      if (window.ie6_bugs) {
// our event handlers ever get a chance to run.
        // IE6 somehow has a problem with inline-displayed forms (to which our list belongs), and will add the
var result = v[ 0 ] !== this.lastInput;
        // offset of the beginning of the text to the offsets we'd normally calculate, which in particular with
if ( v[ 0 ] !== this.lastRealInput ) {
        // right-aligned category lines as they occur in some older skins completely misplaces the lists, sometimes
this.text.value = this.lastRealInput + key;
        // even off-screen. This appears to affect only the horizontal positioning of the list and of the
result = true;
        // engineSelector. Try to account for this bizarre behavior. Notes: dunno if that also occurs on IE7.
}
        var textStartPos = position (this.line.firstChild);
this.lastInput = this.lastRealInput;
        textStartPos.x -= catLinePos.x;
return result;
        textLeft -= textStartPos.x;
}
      }
return false;
      var nl = textLeft;
}
      var nt = 0;
}; // end CategoryEditor.prototype
      var offset = 0;
      if (haveEngine) {
        this.engineSelector.style.zIndex = 5;
        this.engineSelector.style.position = 'absolute';
        this.engineSelector.style.width = this.text.offsetWidth + 'px';
        // Figure out the height of this selector: display it off-screen, then hide it again.
        if (this.engineSelector.style.display == 'none') {
          this.engineSelector.style.left  = '-1000px';
          this.engineSelector.style.top  = textTop + 'px';
          this.engineSelector.style.display = "";
          offset = this.engineSelector.offsetHeight;
          this.engineSelector.style.display = 'none';
        } else {
          offset = this.engineSelector.offsetHeight;
        }
        this.engineSelector.style.left  = nl + 'px';
      }
      if (textPos.y < maxListHeight + offset) {
        // The list might extend beyond the upper border of the page. Let's avoid that by placing it
        // below the input text field.
        nt = textTop + this.text.offsetHeight + offset + 1;
        if (haveEngine) this.engineSelector.style.top = textTop + this.text.offsetHeight + 'px';
      } else {
        nt = textTop - listh - offset;
        if (haveEngine) this.engineSelector.style.top = textTop - offset + 'px';
      }
      this.list.style.top = nt + 'px';
      this.list.style.width = ""; // No fixed width (yet)
      this.list.style.left = nl + 'px';
      // (Re-)fill the list
      while (this.list.firstChild) this.list.removeChild (this.list.firstChild);
      for (var 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);
      }
      if (haveEngine) {
        this.selectEngine (engineName);
        this.engineSelector.style.display = "";
      }
      this.list.style.display = 'block';
      // Set the width of the list       
      var scroll = scroll_offset ('Left');
      var view_w = viewport ('Width');
      var l_pos  = position (this.list);
      if (this.list.offsetWidth < this.text.offsetWidth) {
        this.list.style.width = this.text.offsetWidth + 'px';
        return;
      }
      // Make sure that the list fits horizontally into the browser window     
      var w      = this.list.offsetWidth;
      if (l_pos.x + w > scroll + view_w) {
        if (w > view_w) w = view_w;
        this.list.style.width = w + 'px';
        this.list.style.left = nl - (l_pos.x + w - scroll - view_w) + 'px';
      }
    },


    autoComplete : function (newVal, actVal, key, dontModify) {
function initialize() {
      if (newVal == actVal) return true;
// User configurations. Do this here, called from the onload handler, so that users can
      if (dontModify || newVal.indexOf (actVal) != 0) return false;
// override it easily in their own user script files by just declaring variables. JSconfig
      // Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
// is some feature used at Wikimedia Commons.
      // such that it can be easily removed by typing backspace if the suggestion is unwanted.
var config = ( window.JSconfig !== undefined && JSconfig.keys ) ? JSconfig.keys : {};
      if (!(  this.text.setSelectionRange
HC.dont_add_to_watchlist = ( window.hotcat_dont_add_to_watchlist !== undefined ?
            || this.text.createTextRange
!!window.hotcat_dont_add_to_watchlist :
            ||    typeof (this.text.selectionStart) != 'undefined'
( config.HotCatDontAddToWatchlist !== undefined ? config.HotCatDontAddToWatchlist :
              && typeof (this.text.selectionEnd) != 'undefined'
HC.dont_add_to_watchlist ) );
          )
HC.no_autocommit = ( window.hotcat_no_autocommit !== undefined ?
        )
!!window.hotcat_no_autocommit : ( config.HotCatNoAutoCommit !== undefined ?
        return false;
config.HotCatNoAutoCommit :
      // Here we know that we can indeed select properly. If we can't doing this would be a major
HC.no_autocommit ) );
      // annoyance.
HC.del_needs_diff = ( window.hotcat_del_needs_diff !== undefined ?
      this.text.focus();
!!window.hotcat_del_needs_diff :
      var start  = actVal.length;       
( config.HotCatDelNeedsDiff !== undefined ?
      this.text.value = newVal + key;         
config.HotCatDelNeedsDiff :
      if (this.text.setSelectionRange)     // e.g. khtml
HC.del_needs_diff ) );
        this.text.setSelectionRange (start, newVal.length);
HC.suggest_delay = window.hotcat_suggestion_delay || config.HotCatSuggestionDelay || HC.suggest_delay;
      else if (this.text.createTextRange) { // IE
HC.editbox_width = window.hotcat_editbox_width || config.HotCatEditBoxWidth || HC.editbox_width;
        var new_selection = this.text.createTextRange();
HC.suggestions = window.hotcat_suggestions || config.HotCatSuggestions || HC.suggestions;
        new_selection.move ('character', start);
if ( typeof HC.suggestions !== 'string' || !suggestionConfigs[ HC.suggestions ] ) HC.suggestions = 'combined';
        new_selection.moveEnd ('character', newVal.length - start);
        new_selection.select();
      } else {
        this.text.selectionStart = start;
        this.text.selectionEnd  = newVal.length;
      }
      return true;
    },


    processKey : function (evt) {
HC.fixed_search = ( window.hotcat_suggestions_fixed !== undefined ?
      var dir = 0;
!!window.hotcat_suggestions_fixed : ( config.HotCatFixedSuggestions !== undefined ?
      switch (this.lastKey) {
config.HotCatFixedSuggestions : HC.fixed_search ) );
        case 38: dir = -1; // Up arrow
HC.single_minor = ( window.hotcat_single_changes_are_minor !== undefined ?
        case 40: if (dir == 0) dir = 1; // Down arrow
!!window.hotcat_single_changes_are_minor :
        case 33: if (dir == 0) dir = -HotCat.list_size; // Page up
( config.HotCatMinorSingleChanges !== undefined ?
        case 34: if (dir == 0) dir = HotCat.list_size; // Page down
config.HotCatMinorSingleChanges :
          if (this.list.style.display != 'none') {
HC.single_minor ) );
            // List is visible, so there are suggestions
HC.bg_changed = window.hotcat_changed_background || config.HotCatChangedBackground || HC.bg_changed;
            this.highlightSuggestion (dir);
HC.use_up_down = ( window.hotcat_use_category_links !== undefined ?
            // Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow
!!window.hotcat_use_category_links :
            // as "place the text cursor at the front", which we don't want here.
( config.HotCatUseCategoryLinks !== undefined ?
            return evtKill (evt);
config.HotCatUseCategoryLinks :
          } else if (   this.keyCount <= 1
HC.use_up_down ) );
                    && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
HC.listSize = window.hotcat_listSize || config.HotCatListSize || HC.listSize;
                    )
if ( conf.wgDBname !== 'commonswiki' ) HC.changeTag = config.HotCatChangeTag || '';
          {
            // If no suggestions displayed, get them, unless we're already getting them.
            this.textchange ();
          }
          break;
        case 27: // ESC: inhibit default behavior (revert to last real input in FF: we do that ourselves)
          return evtKill (evt);
      }
      return true;
    },


    highlightSuggestion : function (dir) {
// The next whole shebang is needed, because manual tags get not submitted except of save
      if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
if ( HC.changeTag ) {
      var curr = this.list.selectedIndex;
var eForm = document.editform,
      var tgt  = -1;
catRegExp = new RegExp( '^\\[\\[(' + HC.category_regexp + '):' ),
      if (dir == 0) {
oldTxt;
        if (curr < 0 || curr >= this.list.options.length) return false;
// Returns true if minor change
        tgt = curr;
var isMinorChange = function () {
      } else {
var newTxt = eForm.wpTextbox1;
        tgt = curr < 0 ? 0 : curr + dir;
if ( !newTxt ) return;
        tgt = tgt < 0 ? 0 : tgt;
newTxt = newTxt.value;
        if (tgt >= this.list.options.length) tgt = this.list.options.length - 1;
var oldLines = oldTxt.match( /^.*$/gm ),
      }
newLines = newTxt.match( /^.*$/gm ),
      if (tgt != curr || dir == 0) {
cArr; // changes
        if (curr >= 0 && curr < this.list.options.length && dir != 0) this.list.options[curr].selected = false;
var except = function ( aArr, bArr ) {
        this.list.options[tgt].selected = true;
var result = [],
        // Get current input text
lArr, // larger
        var v = this.text.value.split('|');
sArr; // smaller
        var key = v.length > 1 ? '|' + v[1] : "";
if ( aArr.length < bArr.length ) {
        var completed = this.autoComplete (this.list.options[tgt].text, this.lastRealInput, key, false);
lArr = bArr;
        if (!completed) {
sArr = aArr;
          this.text.value = this.list.options[tgt].text + key;
} else {
        }
lArr = aArr;
        this.lastInput = this.list.options[tgt].text;
sArr = bArr;
        this.inputExists = true; // Might be wrong if from a dab list...
}
        if (this.icon) this.icon.src = HotCat.existsYes;
for ( var i = 0; i < lArr.length; i++ ) {
        this.state = CategoryEditor.CHANGE_PENDING;
var item = lArr[ i ];
      }
var ind = $.inArray( item, sArr );
      return true;
if ( ind === -1 ) result.push( item );
    },
else sArr.splice( ind, 1 ); // don't check this item again
}
return result.concat( sArr );
};
cArr = except( oldLines, newLines );
if ( cArr.length ) {
cArr = $.grep( cArr, function ( c ) {
c = $.trim( c );
return ( c && !catRegExp.test( c ) );
} );
}
if ( !cArr.length ) {
oldTxt = newTxt;
return true;
}
};


    resetKeySelection : function () {
if ( conf.wgAction === 'submit' && conf.wgArticleId && eForm && eForm.wpSummary && document.getElementById( 'wikiDiff' ) ) {
      if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
var sum = eForm.wpSummary,
      var curr = this.list.selectedIndex;
sumA = eForm.wpAutoSummary;
      if (curr >= 0 && curr < this.list.options.length) {
if ( sum.value && sumA.value === HC.changeTag ) { // HotCat diff
        this.list.options[curr].selected = false;
// MD5 hash of the empty string, as HotCat edit is based on empty sum
        // Get current input text
sumA.value = sumA.value.replace( HC.changeTag, 'd41d8cd98f00b204e9800998ecf8427e' );
        var v = this.text.value.split('|');
// Attr creation and event handling is not same in all (old) browsers so use $
        var key = v.length > 1 ? '|' + v[1] : "";
var $ct = $( '<input type="hidden" name="wpChangeTags">' ).val( HC.changeTag );
        // ESC is handled strangely by some browsers (e.g., FF); somehow it resets the input value before
$( eForm ).append( $ct );
        // our event handlers ever get a chance to run.
oldTxt = eForm.wpTextbox1.value;
        var result = v[0] != this.lastInput;  
$( '#wpSave' ).one( 'click', function () {
        if (v[0] != this.lastRealInput) {
if ( $ct.val() )
          this.text.value = this.lastRealInput + key;
sum.value = sum.value.replace( ( HC.messages.using || HC.messages.prefix ), '' );
          result = true;
        }
        this.lastInput = this.lastRealInput;
        return result;
      }
      return false;
    }


  }; // end CategoryEditor.prototype
} );
 
var removeChangeTag = function () {
  function initialize () {
$( eForm.wpTextbox1 ).add( sum ).one( 'input', function () {
    // User configurations. Do this here, called from the onload handler, so that users can
window.setTimeout( function () {
    // override it easily in their own user script files by just declaring variables. JSconfig
if ( !isMinorChange() ) $ct.val( '' );
    // is some feature used at Wikimedia Commons.
else removeChangeTag();
    HotCat.no_autocommit =
}, 500 );
      (typeof (hotcat_no_autocommit) != 'undefined'
} );
        ? !!hotcat_no_autocommit
};
        : (typeof (JSconfig) != 'undefined' && typeof (JSconfig.keys['HotCatNoAutoCommit']) != 'undefined'
removeChangeTag();
            ? JSconfig.keys['HotCatNoAutoCommit']
}
            : HotCat.no_autocommit
}
          )
}
      );
// Numeric input, make sure we have a numeric value
    HotCat.suggest_delay =  window.hotcat_suggestion_delay
HC.listSize = parseInt( HC.listSize, 10 );
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatSuggestionDelay']
if ( isNaN( HC.listSize ) || HC.listSize < 5 ) HC.listSize = 5;
                          || HotCat.suggest_delay;
    HotCat.editbox_width =  window.hotcat_editbox_width
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatEditBoxWidth']
                          || HotCat.editbox_width;
    HotCat.suggestions  =  window.hotcat_suggestions
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatSuggestions']
                          || HotCat.suggestions;
    if (typeof (HotCat.suggestions) != 'string' || !suggestionConfigs[HotCat.suggestions])
      HotCat.suggestions = 'combined';
    HotCat.fixed_search  =
      (typeof (hotcat_suggestions_fixed) != 'undefined'
        ? !!hotcat_suggestions_fixed
        : (typeof (JSconfig) != 'undefined' && typeof (JSconfig.keys['HotCatFixedSuggestions']) != 'undefined'
            ? JSconfig.keys['HotCatFixedSuggestions']
            : HotCat.fixed_search
          )
      );
    HotCat.single_minor  =
      (typeof (hotcat_single_changes_are_minor) != 'undefined'
        ? !!hotcat_single_changes_are_minor
        : (typeof (JSconfig) != 'undefined' && typeof (JSconfig.keys['HotCatMinorSingleChanges']) != 'undefined'
            ? JSconfig.keys['HotCatMinorSingleChanges']
            : HotCat.single_minor
          )
      );
    HotCat.bg_changed    =  window.hotcat_changed_background
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatChangedBackground']
                          || HotCat.bg_changed;
    HotCat.use_up_down  =
      (typeof (hotcat_use_category_links) != 'undefined'
        ? !!hotcat_use_category_links
        : (typeof (JSconfig) != 'undefined' && typeof (JSconfig.keys['HotCatUseCategoryLinks']) != 'undefined'
            ? JSconfig.keys['HotCatUseCategoryLinks']
            : HotCat.use_up_down
          )
      );
    HotCat.list_size =    window.hotcat_list_size
                      || typeof (JSconfig) != 'undefined' && JSconfig.keys['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]]
    is_rtl = hasClass (document.body, 'rtl');
    if (!is_rtl) {
      if (document.defaultView && document.defaultView.getComputedStyle) { // Gecko etc.
        is_rtl = document.defaultView.getComputedStyle (document.body, null).getPropertyValue ('direction');
      } else if (document.body.currentStyle) { // IE, has subtle differences to getComputedStyle
        is_rtl = document.body.currentStyle['direction'];
      } else { // Not exactly right, but best effort
        is_rtl = document.body.style['direction'];
      }
      is_rtl = (is_rtl == 'rtl');
    }
  }
       
  function can_edit () {
    var container = null;
    switch (skin) {
      case 'cologneblue':
        container = document.getElementById ('quickbar');
        // Fall through
      case 'standard':
      case 'nostalgia':
        if (!container) container = document.getElementById ('topbar');
        var lks = container.getElementsByTagName ('a');
        for (var i = 0; i < lks.length; i++) {
          if (  param ('title', lks[i].href) == wgPageName
              && param ('action', lks[i].href) == 'edit')
            return true;
        }
        return false;
      default:
        // all modern skins:
        return document.getElementById ('ca-edit') != null;
    }
    return false;
  }  
 
  function setup_upload () {
    onUpload = true;
    // Add an empty category bar above the "watch this" box, and change the onsubmit handler.
    var ip = document.getElementById ('wpWatchthis');
    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)
    ip = ip.parentNode.parentNode; // The containing <tr>
    var newRow = make ('tr');
    var labelCell = make ('td');
    var lineCell  = make ('td');
    newRow.appendChild (labelCell);
    newRow.appendChild (lineCell);
    // Create the category line
    catLine = make ('div');
    catLine.className = 'catlinks';
    catLine.id = 'catlinks';
    catLine.style.textAlign = 'left';
    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 optionsTable = document.getElementById ('mw-htmlform-options');
      if (optionsTable) optionsTable.width = '100%';
      ip.parentNode.insertBefore (newRow, ip);
      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');
          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;
            }
          }
          return true;           
        };
      }) (form.onsubmit);
    }
  }
 
  var cleanedText = null;


  function isOnPage (span) {
HC.listSize = Math.min( HC.listSize, 15 );
    var catTitle = title (span.firstChild.getAttribute ('href', 2));
    if (!catTitle) return null;
    catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' ');
    var result = { title : catTitle, match : ["", "", ""] };
    if (pageText === null) return result;
    if (cleanedText === null) {
      cleanedText = pageText.replace(/<\!--(\s|\S)*?--\>/g, "")
                            .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
    }
    result.match = find_category (cleanedText, catTitle, true);
    return result;
  }


  var initialized = false;
// Localize search engine names
  var setupTimeout = null;
if ( HC.engine_names ) {
for ( var key in HC.engine_names )
if ( suggestionConfigs[ key ] && HC.engine_names[ key ] ) suggestionConfigs[ key ].name = HC.engine_names[ key ];


  function setup () {
}
    if (initialized) return;
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
    initialized = true;
is_rtl = hasClass( document.body, 'rtl' );
    if (setupTimeout) {
if ( !is_rtl ) {
      window.clearTimeout (setupTimeout);
if ( document.defaultView && document.defaultView.getComputedStyle ) { // Gecko etc.
      setupTimeout = null;
is_rtl = document.defaultView.getComputedStyle( document.body, null ).getPropertyValue( 'direction' );
    }
} else if ( document.body.currentStyle ) { // IE, has subtle differences to getComputedStyle
    // Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
is_rtl = document.body.currentStyle.direction;
    // each category, and add the + link.
} else { // Not exactly right, but best effort
    catLine =  catLine                                                  // Special:Upload
is_rtl = document.body.style.direction;
            || document.getElementById ('mw-normal-catlinks')          // MW >= 1.13alpha
}
            || getElementsByClassName (document , 'p' , 'catlinks')[0]; // MW < 1.13
is_rtl = ( is_rtl === 'rtl' );
    var hiddenCats = document.getElementById ('mw-hidden-catlinks');
}
    if (!catLine) {
}
      var footer = null;
      if (!hiddenCats) {
        footer = getElementsByClassName (document , 'div' , 'printfooter')[0];
        if (!footer) return; // Don't know where to insert the category line
      }
      catLine = make ('div');
      catLine.id = 'mw-normal-catlinks';
      catLine.style.textAlign = 'left';
      // Add a label
      var label = make ('a');
      label.href  = 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
    catLine.style.position = 'relative';
    if (is_rtl) catLine.dir = 'rtl';


    // Create editors for all existing categories
function can_edit() {
var container = null;
switch ( mw.config.get( 'skin' ) ) {
case 'cologneblue':
container = document.getElementById( 'quickbar' );
/* fall through */
case 'standard':
case 'nostalgia':
if ( !container ) container = document.getElementById( 'topbar' );
var lks = container.getElementsByTagName( 'a' );
for ( var i = 0; i < lks.length; i++ ) {
if ( param( 'title', lks[ i ].href ) === conf.wgPageName &&
param( 'action', lks[ i ].href ) === 'edit' ) return true;
}
return false;
default:
// all modern skins:
return document.getElementById( 'ca-edit' ) !== null;
}
}


    function createEditors (line) {
// Legacy stuff
      var cats = line.getElementsByTagName ('span');
function closeForm() {
      // Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
// Close all open editors without redirect resolution and other asynchronous stuff.
      var copyCats = new Array (cats.length);
for ( var i = 0; i < editors.length; i++ ) {
      for (var i = 0; i < cats.length; i++) copyCats[i] = cats[i];
var edit = editors[ i ];
      var editor = null;
if ( edit.state === CategoryEditor.OPEN ) {
      for (var i = 0; i < copyCats.length; i++) {
edit.cancel();
        var test = isOnPage (copyCats[i]);
} else if ( edit.state === CategoryEditor.CHANGE_PENDING ) {
        if (test !== null && test.match !== null) {
edit.sanitizeInput();
          editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2]);
var value = edit.text.value.split( '|' );
        }
var key = null;
      }
if ( value.length > 1 ) key = value[ 1 ];
      return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
var v = value[ 0 ].replace( /_/g, ' ' ).replace( /^\s+|\s+$/g, '' );
    }
if ( !v.length ) {
edit.cancel();
} else {
edit.currentCategory = v;
edit.currentKey = key;
edit.currentExists = this.inputExists;
edit.close();
}
}
}
}


    var lastSpan = createEditors (catLine);
function setup_upload() {
    // Create one to add a new category
onUpload = true;
    var editor = new CategoryEditor(catLine, null, null, lastSpan != null);
// Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
    if (!onUpload) {
var ip = document.getElementById( 'mw-htmlform-description' ) || document.getElementById( 'wpDestFile' );
      if (pageText !== null && hiddenCats) {
if ( !ip ) {
        hiddenCats.style.position = 'relative';
ip = document.getElementById( 'wpDestFile' );
        if (is_rtl) hiddenCats.dir = 'rtl';
while ( ip && ip.nodeName.toLowerCase() !== 'table' ) ip = ip.parentNode;
        createEditors (hiddenCats);
}
      }
if ( !ip ) return;
      // And finally add the "multi-mode" span. (Do this at the end, otherwise it ends up in the list above.)
var reupload = document.getElementById( 'wpForReUpload' );
      var enableMulti = make ('span');
var destFile = document.getElementById( 'wpDestFile' );
      enableMulti.className = 'noprint';
if ( ( reupload && !!reupload.value ) ||
      if (is_rtl) enableMulti.dir = 'rtl';
( destFile && ( destFile.disabled || destFile.readOnly ) ) ) return; // re-upload form...
      catLine.insertBefore (enableMulti, catLine.firstChild.nextSibling);
// Insert a table row with two fields (label and empty category bar)
      enableMulti.appendChild (make ('\xa0', true)); // nbsp
var labelCell = make( 'td' );
      multiSpan = make ('span');
var lineCell = make( 'td' );
      enableMulti.appendChild (multiSpan);
// Create the category line
      multiSpan.innerHTML = '(<a>' + HotCat.addmulti + '</a>)';
catLine = make( 'div' );
      var lk = multiSpan.getElementsByTagName ('a')[0];
catLine.className = 'catlinks';
      lk.onclick = function (evt) {setMultiInput (); checkMultiInput (); return evtKill (evt);};
catLine.id = 'catlinks';
      lk.title = HotCat.multi_tooltip;
catLine.style.textAlign = is_rtl ? 'right' : 'left';
      lk.style.cursor = 'pointer';
// We'll be inside a table row. Make sure that we don't have margins or strange borders.
    }
catLine.style.margin = '0';
    cleanedText = null;
catLine.style.border = 'none';
  }
lineCell.appendChild( catLine );
// Create the label
var label = null;
if ( window.UFUI && window.UIElements && UFUI.getLabel instanceof Function ) {
try {
label = UFUI.getLabel( 'wpCategoriesUploadLbl' );
} catch ( ex ) {
label = null;
}
}
if ( !label ) {
labelCell.id = 'hotcatLabel';
labelCell.appendChild( make( HC.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' ) {
// eslint-disable-next-line no-eval
do_submit = eval( oldSubmit );
} else if ( oldSubmit instanceof 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 = '[[' + HC.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 ) );
}
}


  function setPage (json) {
var cleanedText = null;
    if (json && json.query) {
      if (json.query.pages) {
        for (var p in json.query.pages) {
          var page = json.query.pages[p];
          if (!page.revisions || page.revisions.length == 0) break;
          pageText = page.revisions[0]['*'];
          pageTime = page.revisions[0].timestamp.replace (/\D/g, "");
          pageWatched = typeof (page.watched) == 'string';
          break;
        }
      }
      // Siteinfo
      if (json.query.general) {
        HotCat.capitalizePageNames = (json.query.general['case'] == 'first-letter');
        if (json.query.general.time) serverTime = json.query.general.time.replace (/\D/g, "");
      }
    }
  }


  function getPage () {
function isOnPage( span ) {
    // We know we have an article here.
if ( span.firstChild.nodeType !== Node.ELEMENT_NODE ) return null;
    if (wgArticleId == 0) {
      // Doesn't exist yet.
      pageText = "";
      pageTime = null;
      setup ();
    } else {
      var url = wgServer + wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&titles='
              + encodeURIComponent (wgPageName)
              + '&prop=info%7Crevisions&rvprop=content%7Ctimestamp&meta=siteinfo&rvlimit=1&rvstartid='
              + wgCurRevisionId;
      var s = make ('script');
      s.src = url;
      s.type = 'text/javascript';
      HotCat.start = function (json) { setPage (json); setup (); };
      document.getElementsByTagName ('head')[0].appendChild (s);
      setupTimeout = window.setTimeout (setup, 4000); // 4 sec, just in case getting the wikitext takes longer.
    }
  }


  function run () {
var catTitle = title( span.firstChild.getAttribute( 'href' ) );
    if (HotCat.started) return;
if ( !catTitle ) return null;
    HotCat.started = true;
    initialize ();


    if (is_rtl && window.ie6_bugs) return; // Disabled! IE6 with RTL is just too broken...
catTitle = catTitle.substr( catTitle.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
if ( HC.blacklist && HC.blacklist.test( catTitle ) ) return null;


    if (!HotCat.upload_disabled && wgNamespaceNumber == -1 && wgCanonicalSpecialPageName == 'Upload' && wgUserName) {
var result = {
      setup_upload ();
title: catTitle,
      setup ();
match: [ '', '', '' ]
      // Check for state restoration
};
      if (  typeof (UploadForm) != 'undefined'
if ( pageText === null ) return result;
          && typeof (UploadForm.previous_hotcat_state) != 'undefined'
          && UploadForm.previous_hotcat_state != null)
        UploadForm.previous_hotcat_state = setState (UploadForm.previous_hotcat_state);    
    } else {
      if (!wgIsArticle || wgAction != 'view' || !can_edit() || HotCat.disable()) return;
      getPage ();
    }
  }


  // Legacy stuff
if ( cleanedText === null ) {
cleanedText = pageText
.replace( /<!--(\s|\S)*?-->/g, '' )
.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, '' );
}
result.match = find_category( cleanedText, catTitle, true );
return result;
}


  function closeForm () {
var initialized = false;
    // Close all open editors without redirect resolution and other asynchronous stuff.
var setupTimeout = null;
    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 ();
        }
      }
    }
  }


  function getState () {
function findByClass( scope, tag, className ) {
    var result = null;
var result = $( scope ).find( tag + '.' + className );
    for (var i = 0; i < editors.length; i++) {
return ( result && result.length ) ? result[ 0 ] : null;
      var text = editors[i].currentCategory;
}
      var key  = editors[i].currentKey;
 
      if (text && text.length > 0) {
function setup( additionalWork ) {
        if (key != null) text += '|' + key;
if ( initialized ) return;
        if (result == null)
initialized = true;
          result = text;
if ( setupTimeout ) {
        else
window.clearTimeout( setupTimeout );
          result = result + '\n' + text;
setupTimeout = null;
      }
}
    }
// Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
    return result;
// each category, and add the + link.
  }
catLine =
// Special:Upload
catLine ||
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 = HC.categories;
label.appendChild( make( HC.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


  function setState (state) {
function createEditors( line, is_hidden ) {
    var cats = state.split ('\n');
var i;
    if (cats.length == 0) return null;
var cats = line.getElementsByTagName( 'li' );
    if (initialized && editors.length == 1 && editors[0].isAddCategory) {
if ( cats.length ) {
      // Insert new spans and create new editors for them.
newDOM = true;
      var newSpans = [];
line = cats[ 0 ].parentNode;
      var before = editors.length == 1 ? editors[0].span : null;
} else {
      for (var i = 0; i < cats.length; i++) {
cats = line.getElementsByTagName( 'span' );
        if (cats[i].length == 0) continue;
}
        var cat = cats[i].split ('|');
// Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
        var key = cat.length > 1 ? cat[1] : null;
var copyCats = new Array( cats.length );
        cat = cat[0];
for ( i = 0; i < cats.length; i++ ) copyCats[ i ] = cats[ i ];
        var lk = make ('a'); lk.href = wikiPagePath (HotCat.category_canonical + ':' + cat);
for ( i = 0; i < copyCats.length; i++ ) {
        lk.appendChild (make (cat, true));
var test = isOnPage( copyCats[ i ] );
        lk.title = cat;
if ( test !== null && test.match !== null ) {
        var span = make ('span');
// eslint-disable-next-line no-new
        span.appendChild (lk);
new CategoryEditor( line, copyCats[ i ], test.title, test.match[ 2 ], is_hidden );
        if (i == 0) catLine.insertBefore (make (' ', true), before);
}
        catLine.insertBefore (span, before);
}
        if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
return copyCats.length ? copyCats[ copyCats.length - 1 ] : null;
        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 (var i = 0; i < newSpans.length; i++) {
        editor = new CategoryEditor (catLine, newSpans[i].element, newSpans[i].title, newSpans[i].key);
      }
    }
    return null;
  }


  // Now export these legacy functions
var lastSpan = createEditors( catLine, false );
  window.hotcat_get_state  = function () { return getState(); };
// Create one to add a new category
  window.hotcat_set_state  = function (state) { return setState (state); };
// eslint-disable-next-line no-new
  window.hotcat_close_form = function () { closeForm (); };
new CategoryEditor( newDOM ? catLine.getElementsByTagName( 'ul' )[ 0 ] : catLine, null, null, lastSpan !== null, false );
if ( !onUpload ) {
if ( pageText !== null && hiddenCats ) {
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>' + HC.addmulti + '</a>)';
var lk = multiSpan.getElementsByTagName( 'a' )[ 0 ];
lk.onclick = function ( evt ) {
setMultiInput();
checkMultiInput();
return evtKill( evt );
};
lk.title = HC.multi_tooltip;
lk.style.cursor = 'pointer';
}
cleanedText = null;
if ( additionalWork instanceof Function ) additionalWork();
setupCompleted.loaded(); // Trigger signal; execute registered functions
$( 'body' ).trigger( 'hotcatSetupCompleted' );
}


  addOnloadHook (run);
function createCommitForm() {
})();
if ( commitForm ) return;
var formContainer = make( 'div' );
formContainer.style.display = 'none';
document.body.appendChild( formContainer );
formContainer.innerHTML =
'<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="' +
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="d41d8cd98f00b204e9800998ecf8427e">' +
'<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">' +
'<input type="hidden" name="wpChangeTags">' +
'<input type="hidden" value="ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ" name="wpUnicodeCheck">' +
'</form>';
commitForm = document.getElementById( 'hotcatCommitForm' );
}
 
function getPage() {
// We know we have an article here.
if ( !conf.wgArticleId ) {
// Doesn't exist yet. Disable on non-existing User pages -- might be a global user page.
if ( conf.wgNamespaceNumber === 2 ) 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';
HC.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.
}
}
 
function setState( state ) {
var cats = state.split( '\n' );
if ( !cats.length ) 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 ) continue;
var cat = cats[ i ].split( '|' );
var key = cat.length > 1 ? cat[ 1 ] : null;
cat = cat[ 0 ];
var lk = make( 'a' );
lk.href = wikiPagePath( HC.category_canonical + ':' + cat );
lk.appendChild( make( cat, true ) );
lk.title = cat;
var span = make( 'span' );
span.appendChild( lk );
if ( !i ) 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 );
 
for ( i = 0; i < newSpans.length; i++ ) {
// eslint-disable-next-line no-new
new CategoryEditor( catLine, newSpans[ i ].element, newSpans[ i ].title, newSpans[ i ].key );
}
}
return null;
}
 
function getState() {
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 ) {
if ( key !== null ) text += '|' + key;
if ( result === null ) result = text; else result += '\n' + text;
}
}
return result;
}
 
function really_run() {
initialize();
 
if ( !HC.upload_disabled && conf.wgNamespaceNumber === -1 && conf.wgCanonicalSpecialPageName === 'Upload' && conf.wgUserName ) {
setup_upload();
setup( function () {
// Check for state restoration once the setup is done otherwise, but before signalling setup completion
if ( window.UploadForm && UploadForm.previous_hotcat_state ) UploadForm.previous_hotcat_state = setState( UploadForm.previous_hotcat_state );
} );
} else {
if ( !conf.wgIsArticle || conf.wgAction !== 'view' || param( 'diff' ) !== null || param( 'oldid' ) !== null || !can_edit() || HC.disable() ) return;
getPage();
}
}
 
function run() {
if ( HC.started ) return;
HC.started = true;
loadTrigger.register( really_run );
}
 
// 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 );
 
// Run as soon as possible. This varies depending on MediaWiki version;
// window's 'load' event is always safe, but usually we can do better than that.
 
if ( conf.wgCanonicalSpecialPageName !== 'Upload' ) {
// Reload HotCat after (VE) edits (bug T103285)
mw.hook( 'postEdit' ).add( function () {
// Reset HotCat in case this is a soft reload (VE edit)
catLine = null;
editors = [];
initialized = false;
HC.started = false;
run();
} );
}


} // end if (guard)
// We can safely trigger just after user configuration is loaded. Also start HotCat if the user module fails to load.
//</source>
// Avoid using Promise methods of mw.loader.using as those aren't supported in older
// MediaWiki versions.
$.when( mw.loader.using( 'user' ), $.ready ).then( run );
}( jQuery, mediaWiki ) );
// </nowiki>

Version actuelle datée du 26 mars 2018 à 01:02

/**
HotCat V2.39

Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
Supports multiple category changes, as well as redirect and disambiguation resolution. Also
plugs into the upload form. Search engines to use for the suggestion list are configurable, and
can be selected interactively.

Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat
List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Version_history

License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)

Choose whichever license of these you like best :-)

This code should run on any MediaWiki installation >= MW 1.27.

For use with older versions of MediaWiki, use the archived versions below:

<=1.26: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-HotCat.js&oldid=211134664
*/
// <nowiki>
/* eslint-disable vars-on-top, one-var, camelcase, no-alert, indent, curly */
/* global jQuery, mediaWiki, UFUI, JSconfig, UploadForm */
/* jslint strict:false, nonew:false, bitwise:true */
( function ( $, mw ) {
// Don't use mw.config.get() as that takes a copy of the config, and so doesn't
// account for values changing, e.g. wgCurRevisionId after a VE edit
var conf = mw.config.values;

// Guard against double inclusions (in old IE/Opera element ids become window properties)
if ( ( window.HotCat && !window.HotCat.nodeName ) ||
	conf.wgAction === 'edit' ) // Not on edit mode
	return;

// Configuration stuff.
var HC = window.HotCat = {
// Localize these messages to the main language of your wiki.
	messages: {
		cat_removed: 'removed [[Category:$1]]',
		template_removed: 'removed {{[[Category:$1]]}}',
		cat_added: 'added [[Category:$1]]',
		cat_keychange: 'new key for [[Category:$1]]: "$2"', // $2 is the new key
		cat_notFound: 'Category "$1" not found',
		cat_exists: 'Category "$1" already exists; not added.',
		cat_resolved: ' (redirect [[Category:$1]] resolved)',
		uncat_removed: 'removed {{uncategorized}}',
		separator: '; ',
	// Some text to prefix to the edit summary.
		prefix: '',
	// Some text to append to the edit summary. Named 'using' for historical reasons. If you prefer
	// to have a marker at the front, use prefix and set this to the empty string.
		using: ' using [[Help:Gadget-HotCat|HotCat]]',
	// $1 is replaced by a number. If your language has several plural forms (c.f. [[:en:Dual (grammatical form)]]),
	// you can set this to an array of strings suitable for passing to mw.language.configPlural().
	// If that function doesn't exist, HotCat will simply fall back to using the last
	// entry in the array.
		multi_change: '$1 categories',
	// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
	// see localization hook below.
		commit: 'Save',
	// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
	// see localization hook below.
		ok: 'OK',
	// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
	// see localization hook below.
		cancel: 'Cancel',
	// Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
	// see localization hook below.
		multi_error: 'Could not retrieve the page text from the server. Therefore, your category changes ' +
		'cannot be saved. We apologize for the inconvenience.',
	// Defaults to '[[' + category_canonical + ':$1]]'. Can be overridden if in the short edit summaries
	// not the standard category name should be used but, say, a shorter namespace alias. $1 is replaced
	// by a category name.
		short_catchange: null
	},
// Plural of category_canonical.
	categories: 'Categories',
// Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
// any items, but that contains links to other categories where stuff should be categorized. If you don't have
// that concept on your wiki, set it to null. Use blanks, not underscores.
	disambig_category: 'Disambiguation',
// Any category in this category is deemed a (soft) redirect to some other category defined by a link
// to another non-blacklisted category. If your wiki doesn't have soft category redirects, set this to null.
// If a soft-redirected category contains more than one link to another non-blacklisted category, it's considered
// a disambiguation category instead.
	redir_category: 'Category redirects',
// The little modification links displayed after category names. U+2212 is a minus sign; U+2193 and U+2191 are
// downward and upward pointing arrows. Do not use ↓ and ↑ in the code!
	links: {
		change: '(±)',
		remove: '(\u2212)',
		add: '(+)',
		restore: '(×)',
		undo: '(×)',
		down: '(\u2193)',
		up: '(\u2191)'
	},
	changeTag: conf.wgUserName ? 'HotCat' : '', // if tag is missing, edit is rejected
// The tooltips for the above links
	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 HTML content of the "enter multi-mode" link at the front.
	addmulti: '<span>+<sup>+</sup></span>',
// Tooltip for the "enter multi-mode" link
	multi_tooltip: 'Modify several categories',
// Return true to disable HotCat.
	disable: function () {
		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 || // Non-existing file pages
		ns === 2 && /\.(js|css)$/.test( conf.wgTitle ) || // User scripts
		nsIds &&
		( ns === nsIds.creator ||
		ns === nsIds.timedtext ||
		ns === nsIds.institution ) );
	},
// A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
// If not, set it to null.
	uncat_regexp: /\{\{\s*[Uu]ncategorized\s*[^}]*\}\}\s*(<!--.*?-->\s*)?/g,
// The images used for the little indication icon. Should not need changing.
	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',
// 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 {}.
	template_categories: {},
// Names for the search engines
	engine_names: {
		searchindex: 'Search index',
		pagelist: 'Page list',
		combined: 'Combined search',
		subcat: 'Subcategories',
		parentcat: 'Parent categories'
	},
// Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the 		// any items, but that contains links to other categories where stuff should be categorized. If you don't have

// ("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.
	capitalizePageNames: true,
// If upload_disabled is true, HotCat will not be used on the Upload form.
	upload_disabled: false,
// 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.
	blacklist: null,

// Stuff changeable by users:
// Background for changed categories in multi-edit mode. Default is a very light salmon pink.
	bg_changed: '#FCA',
// If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
// the changes; users must always save explicitly.
	no_autocommit: false,
// 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.
	del_needs_diff: false,
// Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
// server to get suggestions.
	suggest_delay: 100,
// Default width, in characters, of the text input field.
	editbox_width: 40,
// One of the engine_names above, to be used as the default suggestion engine.
	suggestions: 'combined',
// If true, always use the default engine, and never display a selector.
	fixed_search: false,
// If false, do not display the "up" and "down" links
	use_up_down: true,
// Default list size
	listSize: 5,
// If true, single category changes are marked as minor edits. If false, they're not.
	single_minor: true,
// 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.
	dont_add_to_watchlist: false,
	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 || !v.length ) continue;

			window.HotCat.shortcuts[ k ] = v;
		}
	}
};

// More backwards compatibility. We have a few places where we test for the browser: once for
// Safari < 3.0, and twice for WebKit (Chrome or Safari, any versions)
var ua = navigator.userAgent.toLowerCase();
var is_webkit = /applewebkit\/\d+/.test( ua ) && ua.indexOf( 'spoofer' ) < 0;
var cat_prefix = null;
var noSuggestions = false;

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 ) {
	this.queue = [];
	this.toLoad = needed;
}
LoadTrigger.prototype = {
	register: function ( callback ) {
		if ( this.toLoad <= 0 ) callback(); // Execute directly
		else this.queue[ this.queue.length ] = callback;
	},

	loaded: function () {
		if ( this.toLoad > 0 ) {
			this.toLoad--;
			if ( this.toLoad === 0 ) {
			// Run queued callbacks once
				for ( var i = 0; i < this.queue.length; i++ ) this.queue[ i ]();

				this.queue = [];
			}
		}
	}
};

var setupCompleted = new LoadTrigger( 1 );
// Used to run user-registered code once HotCat is fully set up and ready.
HC.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();
	};
	s.onerror = afterLoad; // Clean up, but otherwise ignore errors
	head.insertBefore( s, head.firstChild ); // appendChild may trigger bugs in IE6 here
}

function loadJS( page ) {
	load( conf.wgServer + conf.wgScript + '?title=' + encodeURIComponent( page ) + '&action=raw&ctype=text/javascript' );
}

function loadURI( href ) {
	var url = href;
	if ( url.substring( 0, 2 ) === '//' ) url = window.location.protocol + url; else if ( url.substring( 0, 1 ) === '/' ) url = conf.wgServer + url;

	load( url );
}

// Load local configurations, overriding the pre-set default values in the HotCat object above. This is always loaded
// from the wiki where this script is executing, even if this script itself is hotlinked from the Commons. This can
// be used to change the default settings, or to provide localized interface texts for edit summaries and so on.
loadJS( 'MediaWiki:Gadget-HotCat.js/local_defaults' );

// Load localized UI texts. These are the texts that HotCat displays on the page itself. Texts shown in edit summaries
// should be localized in /local_defaults above.
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
// explicitly if you're not on the Commons and don't want that.
	if ( window.hotcat_translations_from_commons === undefined ) window.hotcat_translations_from_commons = true;

// Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
	if ( window.hotcat_translations_from_commons && conf.wgServer.indexOf( '//commons' ) < 0 ) {
		loadURI( '//commons.wikimedia.org/w/index.php?title=' +
	'MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage +
	'&action=raw&ctype=text/javascript' );
	} else {
	// Load translations locally
		loadJS( 'MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage );
	}
} else {
	loadTrigger.loaded();
}

// No further changes should be necessary here.

// The following regular expression strings are used when searching for categories in wikitext.
var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
var wikiTextBlankRE = new RegExp( wikiTextBlank, 'g' );
// Regexp for handling blanks inside a category title or namespace name.
// See http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Title.php?revision=104051&view=markup#l2722
// See also http://www.fileformat.info/info/unicode/category/Zs/list.htm
//   MediaWiki collapses several contiguous blanks inside a page title to one single blank. It also replace a
// 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.
//   Note: we _do_ include the horizontal tab in the above list, even though the MediaWiki software for some reason
// appears to not handle it. The zero-width space \u200B is _not_ handled as a space inside titles by MW.
var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
// Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
// a link must be on one single line.
//   MediaWiki also removes Unicode bidi override characters in page titles (and namespace names) completely.
// This is *not* handled, as it would require us to allow any of [\u200E\u200F\u202A-\u202E] between any two
// characters inside a category link. It _could_ be done though... We _do_ handle strange spaces, including the
// zero-width space \u200B, and bidi overrides between the components of a category link (adjacent to the colon,
// or adjacent to and inside of "[[" and "]]").

// First auto-localize the regexps for the category and the template namespaces.
var formattedNamespaces = conf.wgFormattedNamespaces;
var namespaceIds = conf.wgNamespaceIds;
function autoLocalize( namespaceNumber, fallback ) {
	function createRegexpStr( name ) {
		if ( !name || !name.length ) return '';

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

	fallback = fallback.toLowerCase();
	var canonical = formattedNamespaces[ String( namespaceNumber ) ].toLowerCase();
	var regexp = createRegexpStr( canonical );
	if ( fallback && canonical !== fallback ) regexp += '|' + createRegexpStr( fallback );

	if ( namespaceIds ) {
		for ( var cat_name in namespaceIds ) {
			if (
				typeof cat_name === 'string' &&
cat_name.toLowerCase() !== canonical &&
cat_name.toLowerCase() !== fallback &&
namespaceIds[ cat_name ] === namespaceNumber ) regexp += '|' + createRegexpStr( cat_name );
		}
	}
	return regexp;
}

HC.category_canonical = formattedNamespaces[ '14' ];
HC.category_regexp = autoLocalize( 14, 'category' );
if ( formattedNamespaces[ '10' ] ) HC.template_regexp = autoLocalize( 10, 'template' );

// Utility functions. Yes, this duplicates some functionality that also exists in other places, but
// to keep this whole stuff in a single file not depending on any other on-wiki JavaScripts, we re-do
// these few operations here.
function make( arg, literal ) {
	if ( !arg ) return null;

	return literal ? document.createTextNode( arg ) : document.createElement( arg );
}
function param( name, uri ) {
	uri = 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 ) ) prefix = conf.wgServer + prefix; // Fully expanded URL?

		if ( href.indexOf( prefix ) && 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 ) 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 ) {
	options = options || {};
	var lead = options.indicator || '$';
	var indicator = escapeRE( lead );
	var lbrace = escapeRE( options.lbrace || '{' );
	var rbrace = escapeRE( options.rbrace || '}' );
	var re;

	re = new RegExp(
	// $$
		'(?:' + indicator + '(' + indicator + '))|' +
// $0, $1
'(?:' + indicator + '(\\d+))|' +
// ${key}
'(?:' + indicator + '(?:' + lbrace + '([^' + lbrace + rbrace + ']+)' + rbrace + '))|' +
// $key (only if first char after $ is not $, digit, or { )
'(?:' + indicator + '(?!(?:[' + indicator + lbrace + ']|\\d))(\\S+?)\\b)',
		'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();
var replaceShortcuts = ( function () {
	var replaceHash = substituteFactory( {
		indicator: '#',
		lbrace: '[',
		rbrace: ']'
	} );
	return function ( str, map ) {
		var s = replaceHash( str, map );
		return HC.capitalizePageNames ? capitalize( s ) : s;
	};
}() );

// Text modification

var findCatsRE =
new RegExp( '\\[\\[' + wikiTextBlankOrBidi + '(?:' + HC.category_regexp + ')' + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g' );

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

function find_category( wikitext, category, once ) {
	var cat_regex = null;
	if ( HC.template_categories[ category ] ) {
		cat_regex = new RegExp(
			'\\{\\{' + wikiTextBlankOrBidi + '(' + HC.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi +
'(?:' + HC.template_categories[ category ] + ')' +
wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}',
			'g' );
	} else {
		var cat_name = escapeRE( category );
		var initial = cat_name.substr( 0, 1 );
		cat_regex = new RegExp(
			'\\[\\[' + wikiTextBlankOrBidi + '(' + HC.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi +
( initial === '\\' || !HC.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;

function change_category( wikitext, toRemove, toAdd, key, is_hidden ) {

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

		if ( index < 0 ) {
		// 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 {
				match = interlanguageRE.exec( copiedtext );
			}
			if ( match ) index = match.index;

			return {
				idx: index,
				onCat: false
			};
		}
		return {
			idx: index,
			onCat: index >= 0
		};
	}

	var summary = [];
	var nameSpace = HC.category_canonical;
	var cat_point = -1; // Position of removed category;

	if ( key ) key = '|' + key;

	var keyChange = ( toRemove && toAdd && toRemove === toAdd && toAdd.length );
	var matches;
	if ( toRemove && toRemove.length ) {
		matches = find_category( wikitext, toRemove );
		if ( !matches || !matches.length ) {
			return {
				text: wikitext,
				summary: summary,
				error: HC.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 || 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 && before.substring( before.length - 1 ).search( /\S/ ) >= 0 &&
after.length && after.substr( 0, 1 ).search( /\S/ ) >= 0 ) before += ' ';

			cat_point = before.length;
			if ( cat_point === 0 && after.length && after.substr( 0, 1 ) === '\n' ) after = after.substr( 1 );

			wikitext = before + after;
			if ( !keyChange )
				if ( HC.template_categories[ toRemove ] ) { summary.push( HC.messages.template_removed.replace( /\$1/g, toRemove ) ); } else { summary.push( HC.messages.cat_removed.replace( /\$1/g, toRemove ) ); }

		}
	}
	if ( toAdd && toAdd.length ) {
		matches = find_category( wikitext, toAdd );
		if ( matches && matches.length ) {
			return {
				text: wikitext,
				summary: summary,
				error: HC.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 && suffix.substr( 0, 1 ) !== '\n' ) wikitext += '\n' + suffix; else wikitext += suffix;
			} else {
				if ( wikitext.length && wikitext.substr( wikitext.length - 1, 1 ) !== '\n' ) wikitext += '\n';

				wikitext += ( wikitext.length ? '\n' : '' ) + newcatstring;
			}
			if ( keyChange ) {
				var k = key || '';
				if ( k.length ) k = k.substr( 1 );

				summary.push( substitute( HC.messages.cat_keychange, [ null, toAdd, k ] ) );
			} else {
				summary.push( HC.messages.cat_added.replace( /\$1/g, toAdd ) );
			}
			if ( HC.uncat_regexp && !is_hidden ) {
				var txt = wikitext.replace( HC.uncat_regexp, '' ); // Remove "uncat" templates
				if ( txt.length !== wikitext.length ) {
					wikitext = txt;
					summary.push( HC.messages.uncat_removed );
				}
			}
		}
	}
	return {
		text: wikitext,
		summary: summary,
		error: null
	};
}

// The real HotCat UI

function evtKeys( e ) {
/* eslint-disable no-bitwise */
	e = e || window.event; // W3C, IE
	var code = 0;
	if ( e.ctrlKey ) { // All modern browsers
	// 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;
	}
	return code;
}
function evtKill( e ) {
	e = e || window.event; // W3C, IE
	if ( e.preventDefault ) {
		e.preventDefault();
		e.stopPropagation();
	} else {
		e.cancelBubble = true;
	}
	return false;
}

var catLine = null,
	onUpload = false,
	editors = [],

	commitButton = null,
	commitForm = null,
	multiSpan = null,

	pageText = null,
	pageTime = null,
	pageWatched = false,
	watchCreate = false,
	watchEdit = false,
	minorEdits = false,
	editToken = null,

	is_rtl = false,
	serverTime = null,
	lastRevId = null,
	pageTextRevId = null,
	conflictingUser = null,

	newDOM = false; // true if MediaWiki serves the new UL-LI DOM for categories

function CategoryEditor() {
	this.initialize.apply( this, arguments );
}

function setPage( json ) {
	var startTime = null;
	if ( json && json.query ) {
		if ( json.query.pages ) {
			var page = json.query.pages[ !conf.wgArticleId ? '-1' : String( conf.wgArticleId ) ];
			if ( page ) {
				if ( page.revisions && page.revisions.length ) {
				// Revisions are sorted by revision ID, hence [ 0 ] is the one we asked for, and possibly there's a [ 1 ] if we're
				// not on the latest revision (edit conflicts and such).
					pageText = page.revisions[ 0 ][ '*' ];
					if ( page.revisions[ 0 ].timestamp ) pageTime = page.revisions[ 0 ].timestamp.replace( /\D/g, '' );
					if ( page.revisions[ 0 ].revid ) pageTextRevId = page.revisions[ 0 ].revid;
					if ( page.revisions.length > 1 ) conflictingUser = page.revisions[ 1 ].user;
				}
				if ( page.lastrevid ) lastRevId = page.lastrevid;
				if ( page.starttimestamp ) startTime = page.starttimestamp.replace( /\D/g, '' );
				pageWatched = typeof page.watched === 'string';
				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 ) interlanguageRE = new RegExp( '((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$' );
				}
			}
		}
	// Siteinfo
		if ( json.query.general ) {
		// ResourceLoader's JSParser doesn't like .case, so override eslint.
		// eslint-disable-next-line dot-notation
			HC.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 = !HC.dont_add_to_watchlist && json.query.userinfo.options.watchcreations === '1';
			watchEdit = !HC.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 ) HC.single_minor = true;
		}
	}
}

var saveInProgress = false;
function initiateEdit( doEdit, failure ) {
	if ( saveInProgress ) return;
	saveInProgress = true;
	var oldButtonState;
	if ( commitButton ) {
		oldButtonState = commitButton.disabled;
		commitButton.disabled = true;
	}

	function fail() {
		saveInProgress = false;
		if ( commitButton ) commitButton.disabled = oldButtonState;
		failure.apply( this, arguments );
	}

// Must use Ajax here to get the user options and the edit token.
	$.getJSON( conf.wgServer + conf.wgScriptPath + '/api.php?' +
	'format=json&action=query&rawcontinue=&titles=' + encodeURIComponent( conf.wgPageName ) +
	'&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',
	function ( json ) {
		setPage( json );
		doEdit( fail );
	} ).fail( function ( req ) {
		fail( req.status + ' ' + req.statusText );
	} );
}

function multiChangeMsg( count ) {
	var msg = HC.messages.multi_change;
	if ( typeof msg !== 'string' && msg.length )
		if ( mw.language && mw.language.convertPlural ) { msg = mw.language.convertPlural( count, msg ); } else { msg = msg[ msg.length - 1 ]; }

	return substitute( msg, [ null, String( count ) ] );
}

function currentTimestamp() {
	var now = new Date();
	var ts = String( now.getUTCFullYear() );
	function two( s ) {
		return s.substr( s.length - 2 );
	}
	ts +=
two( '0' + ( now.getUTCMonth() + 1 ) ) +
two( '0' + now.getUTCDate() ) +
two( '00' + now.getUTCHours() ) +
two( '00' + now.getUTCMinutes() ) +
two( '00' + now.getUTCSeconds() );
	return ts;
}

function performChanges( failure, singleEditor ) {
	if ( pageText === null ) {
		failure( HC.messages.multi_error );
		return;
	}
// Backwards compatibility after message change (added $2 to cat_keychange)
	if ( HC.messages.cat_keychange.indexOf( '$2' ) < 0 ) HC.messages.cat_keychange += '"$2"';

// More backwards-compatibility with earlier HotCat versions:
	if ( !HC.messages.short_catchange ) HC.messages.short_catchange = '[[' + HC.category_canonical + ':$1]]';

// Create a form and submit it. We don't use the edit API (api.php?action=edit) because
// (a) sensibly reporting back errors like edit conflicts is always a hassle, and
// (b) we want to show a diff for multi-edits anyway, and
// (c) we want to trigger onsubmit events, allowing user code to intercept the edit.
// Using the form, we can do (b) and (c), and we get (a) for free. And, of course, using the form
// automatically reloads the page with the updated categories on a successful submit, which
// 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 selfEditConflict = ( lastRevId !== null && lastRevId !== conf.wgCurRevisionId || pageTextRevId !== null &&
		pageTextRevId !== conf.wgCurRevisionId ) && conflictingUser && conflictingUser === conf.wgUserName;
	if ( singleEditor && !singleEditor.noCommit && !HC.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
		},
		changed = [],
		added = [],
		deleted = [],
		changes = 0,
		toEdit = singleEditor ? [ singleEditor ] : editors,
		error = null,
		edit,
		i;
	for ( i = 0; i < toEdit.length; i++ ) {
		edit = toEdit[ i ];
		if ( edit.state === CategoryEditor.CHANGED ) {
			result = change_category(
				result.text,
				edit.originalCategory,
				edit.currentCategory,
				edit.currentKey,
				edit.currentHidden );
			if ( !result.error ) {
				changes++;
				if ( !edit.originalCategory || !edit.originalCategory.length ) {
					added.push( edit.currentCategory );
				} else {
					changed.push( {
						from: edit.originalCategory,
						to: edit.currentCategory
					} );
				}
			} else if ( error === null ) {
				error = result.error;
			}
		} else if (
			edit.state === CategoryEditor.DELETED && edit.originalCategory && edit.originalCategory.length ) {
			result = change_category(
				result.text,
				edit.originalCategory,
				null, null, false );
			if ( !result.error ) {
				changes++;
				deleted.push( edit.originalCategory );
			} else if ( error === null ) {
				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.wpMinoredit.checked = minorEdits;
	commitForm.wpWatchthis.checked = !conf.wgArticleId && watchCreate || watchEdit || pageWatched;
	if ( conf.wgArticleId || !!singleEditor ) {
	// Prepare change-tag save
		if ( action && action.value === 'wpSave' ) {
			if ( HC.changeTag ) {
				commitForm.wpChangeTags.value = HC.changeTag;
				HC.messages.using = '';
				HC.messages.prefix = '';
			}
		} else {
			commitForm.wpAutoSummary.value = HC.changeTag;
		}
		if ( changes === 1 ) {
			if ( result.summary && result.summary.length ) commitForm.wpSummary.value = HC.messages.prefix + result.summary.join( HC.messages.separator ) + HC.messages.using;
			commitForm.wpMinoredit.checked = HC.single_minor || minorEdits;
		} else if ( changes ) {
			var summary = [];
			var shortSummary = [];
		// Deleted
			for ( i = 0; i < deleted.length; i++ ) summary.push( '-' + substitute( HC.messages.short_catchange, [ null, deleted[ i ] ] ) );

			if ( deleted.length === 1 ) shortSummary.push( '-' + substitute( HC.messages.short_catchange, [ null, deleted[ 0 ] ] ) ); else if ( deleted.length ) shortSummary.push( '- ' + multiChangeMsg( deleted.length ) );

		// Added
			for ( i = 0; i < added.length; i++ ) summary.push( '+' + substitute( HC.messages.short_catchange, [ null, added[ i ] ] ) );

			if ( added.length === 1 ) shortSummary.push( '+' + substitute( HC.messages.short_catchange, [ null, added[ 0 ] ] ) ); else if ( added.length ) 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( HC.messages.short_catchange, [ null, changed[ i ].from ] ) + arrow +
		substitute( HC.messages.short_catchange, [ null, changed[ i ].to ] ) );
				} else {
					summary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ i ].from ] ) );
				}
			}
			if ( changed.length === 1 ) {
				if ( changed[ 0 ].from !== changed[ 0 ].to ) {
					shortSummary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ 0 ].from ] ) + arrow +
		substitute( HC.messages.short_catchange, [ null, changed[ 0 ].to ] ) );
				} else {
					shortSummary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ 0 ].from ] ) );
				}
			} else if ( changed.length ) {
				shortSummary.push( '± ' + multiChangeMsg( changed.length ) );
			}
			if ( summary.length ) {
				summary = summary.join( HC.messages.separator );
				if ( summary.length > 200 - HC.messages.prefix.length - HC.messages.using.length ) summary = shortSummary.join( HC.messages.separator );

				commitForm.wpSummary.value = HC.messages.prefix + summary + HC.messages.using;
			}
		}
	}

	commitForm.wpTextbox1.value = result.text;
	commitForm.wpStarttime.value = serverTime || currentTimestamp();
	commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
	if ( selfEditConflict ) commitForm.oldid.value = String( pageTextRevId || conf.wgCurRevisionId );

// Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
	commitForm.hcCommit.click();
}

function resolveOne( page, toResolve ) {
	var cats = page.categories,
		lks = page.links,
		is_dab = false,
		is_redir = typeof page.redirect === 'string', // Hard redirect?
		is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden === 'string',
		is_missing = typeof page.missing === 'string',
		i;
	for ( i = 0; i < toResolve.length; i++ ) {
		if ( i && 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 ? HC.existsNo : HC.existsYes );
	}
	if ( is_missing ) return;
	if ( !is_redir && cats && ( HC.disambig_category || HC.redir_category ) ) {
		for ( var c = 0; c < cats.length; c++ ) {
			var cat = cats[ c ].title;
		// Strip namespace prefix
			if ( cat ) {
				cat = cat.substring( cat.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
				if ( cat === HC.disambig_category ) {
					is_dab = true;
					break;
				} else if ( cat === HC.redir_category ) {
					is_redir = true;
					break;
				}
			}
		}
	}
	if ( !is_redir && !is_dab ) return;
	if ( !lks || !lks.length ) return;
	var titles = [];
	for ( i = 0; i < lks.length; i++ ) {
		if (
	// Category namespace -- always true since we ask only for the category links
			lks[ i ].ns === 14 &&
// Name not empty
lks[ i ].title && lks[ i ].title.length ) {
		// 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 ( !HC.blacklist || !HC.blacklist.test( match ) ) titles.push( match );
		}
	}
	if ( !titles.length ) return;
	for ( i = 0; i < toResolve.length; i++ ) {
		if ( i && 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( HC.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 ) {
	if ( !params || !params.query || !params.query.pages ) return;
	for ( var p in params.query.pages ) resolveOne( params.query.pages[ p ], toResolve );
}

function resolveMulti( toResolve, callback ) {
	var i;
	for ( i = 0; i < toResolve.length; i++ ) {
		toResolve[ i ].dab = null;
		toResolve[ i ].dabInput = toResolve[ i ].lastInput;
	}
	if ( noSuggestions ) {
		callback( toResolve );
		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, HC.shortcuts );
		toResolve[ i ].dabInputCleaned = v;
		args += encodeURIComponent( 'Category:' + v );
		if ( i + 1 < toResolve.length ) args += '%7C';
	}
	$.getJSON( conf.wgServer + conf.wgScriptPath + '/api.php?' + args,
		function ( json ) {
			resolveRedirects( toResolve, json );
			callback( toResolve );
		} ).fail( function ( req ) {
		if ( !req ) noSuggestions = true;
		callback( toResolve );
	} );
}

function makeActive( which ) {
	if ( which.is_active ) return;
	for ( var i = 0; i < editors.length; i++ )
		if ( editors[ i ] !== which ) editors[ i ].inactivate();

	which.is_active = true;
	if ( which.dab ) {
		showDab( which );
	} else {
	// Check for programmatic value changes.
		var expectedInput = which.lastRealInput || which.lastInput || '';
		var actualValue = which.text.value || '';
		if ( !expectedInput.length && actualValue.length || expectedInput.length && actualValue.indexOf( expectedInput ) ) {
		// 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 );
			}
		}
	}
}

function showDab( which ) {
	if ( !which.is_active ) {
		makeActive( which );
	} else {
		which.showSuggestions( which.dab, false, null, null ); // do autocompletion, no key, no engine selector
		which.dab = null;
	}
}

function multiSubmit() {
	var toResolve = [];
	for ( var i = 0; i < editors.length; i++ )
		if ( editors[ i ].state === CategoryEditor.CHANGE_PENDING || editors[ i ].state === CategoryEditor.OPEN ) toResolve.push( editors[ i ] );

	if ( !toResolve.length ) {
		initiateEdit( function ( failure ) {
			performChanges( failure );
		}, function ( msg ) {
			alert( msg );
		} );
		return;
	}
	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 );
			} );
		}
	} );
}

function setMultiInput() {
	if ( commitButton || onUpload ) return;
	commitButton = make( 'input' );
	commitButton.type = 'button';
	commitButton.value = HC.messages.commit;
	commitButton.onclick = multiSubmit;
	if ( multiSpan ) multiSpan.parentNode.replaceChild( commitButton, multiSpan ); else catLine.appendChild( commitButton );
}

function checkMultiInput() {
	if ( !commitButton ) return;
	var hasChanges = false;
	for ( var i = 0; i < editors.length; i++ ) {
		if ( editors[ i ].state !== CategoryEditor.UNCHANGED ) {
			hasChanges = true;
			break;
		}
	}
	commitButton.disabled = !hasChanges;
}

var suggestionEngines = {
	opensearch: {
		uri: '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1', // $1 = search term
	// Function to convert result of uri into an array of category names
		handler: 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( '^(' + HC.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 ) {
			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: {
		uri: '/api.php?format=json&action=query&list=categorymembers&cmtype=subcat&cmlimit=max&cmtitle=Category:$1',
		handler: function ( queryResult ) {
			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 ) {
			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 = {
	searchindex: {
		name: 'Search index',
		engines: [ 'opensearch' ],
		cache: {},
		show: true,
		temp: false,
		noCompletion: false
	},
	pagelist: {
		name: 'Page list',
		engines: [ 'internalsearch', 'exists' ],
		cache: {},
		show: true,
		temp: false,
		noCompletion: false
	},
	combined: {
		name: 'Combined search',
		engines: [ 'opensearch', 'internalsearch' ],
		cache: {},
		show: true,
		temp: false,
		noCompletion: false
	},
	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
	}
};

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 document.body.appendChild( dummyElement );
}

// Event keyCodes that we handle in the text input field/suggestion list.
var BS = 8,
	TAB = 9,
	RET = 13,
	ESC = 27,
	SPACE = 32,
	PGUP = 33,
	PGDOWN = 34,
	UP = 38,
	DOWN = 40,
	DEL = 46,
	IME = 229;

CategoryEditor.prototype = {

	initialize: function ( line, span, after, key, is_hidden ) {
	// If a span is given, 'after' is the category title, otherwise it may be an element after which to
	// 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( HC.links.add, true ) );
			lk.title = HC.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 = HC.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 () {
		this.normalLinks = make( 'span' );
		var lk = null;
		if ( this.originalCategory && this.originalCategory.length ) {
			lk = make( 'a' );
			lk.href = '#catlinks';
			lk.onclick = this.remove.bind( this );
			lk.appendChild( make( HC.links.remove, true ) );
			lk.title = HC.tooltips.remove;
			this.normalLinks.appendChild( make( ' ', true ) );
			this.normalLinks.appendChild( lk );
		}
		if ( !HC.template_categories[ this.originalCategory ] ) {
			lk = make( 'a' );
			lk.href = '#catlinks';
			lk.onclick = this.open.bind( this );
			lk.appendChild( make( HC.links.change, true ) );
			lk.title = HC.tooltips.change;
			this.normalLinks.appendChild( make( ' ', true ) );
			this.normalLinks.appendChild( lk );
			if ( !noSuggestions && HC.use_up_down ) {
				this.upDownLinks = make( 'span' );
				lk = make( 'a' );
				lk.href = '#catlinks';
				lk.onclick = this.down.bind( this );
				lk.appendChild( make( HC.links.down, true ) );
				lk.title = HC.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( HC.links.up, true ) );
				lk.title = HC.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( HC.links.restore, true ) );
		lk.title = HC.tooltips.restore;
		this.undelLink.appendChild( make( ' ', true ) );
		this.undelLink.appendChild( lk );
		this.linkSpan.appendChild( this.undelLink );
	},

	invokeSuggestions: function ( dont_autocomplete ) {
		if ( this.engine && suggestionConfigs[ this.engine ] && suggestionConfigs[ this.engine ].temp && !dont_autocomplete ) this.engine = HC.suggestions; // Reset to a search upon input

		this.state = CategoryEditor.CHANGE_PENDING;
		var self = this;
		window.setTimeout( function () {
			self.textchange( dont_autocomplete );
		}, HC.suggest_delay );
	},

	makeForm: function () {
		var form = make( 'form' );
		form.method = 'POST';
		form.onsubmit = this.accept.bind( this );
		this.form = form;
		var self = this;
		var text = make( 'input' );
		text.type = 'text';
		text.size = HC.editbox_width;
		if ( !noSuggestions ) {
		// Be careful here to handle IME input. This is browser/OS/IME dependent, but basically there are two mechanisms:
		// - Modern (DOM Level 3) browsers use compositionstart/compositionend events to signal composition; if the
		//   composition is not canceled, there'll be a textInput event following. During a composition key events are
		//   either all suppressed (FF/Gecko), or otherwise have keyDown === IME for all keys (Webkit).
		//   - Webkit sends a textInput followed by keyDown === IME and a keyUp with the key that ended composition.
		//   - Gecko doesn't send textInput but just a keyUp with the key that ended composition, without sending keyDown
		//     first. Gecko doesn't send any keydown while IME is active.
		// - Older browsers signal composition by keyDown === IME for the first and subsequent keys for a composition. The
		//   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; // W3C, IE
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; // W3C, IE
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(
				( 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 () {
					self.lastKey = IME;
					self.usesComposition = true;
					self.ime = true;
				} );
				$( text ).on( 'compositionend', function () {
					self.lastKey = IME;
					self.usesComposition = true;
					self.ime = false;
				} );
				$( text ).on( 'textInput', function () {
					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 () {
				self.usesComposition = false;
				self.ime = false;
			} );
		}
		this.text = text;

		this.icon = make( 'img' );

		var list = null;
		if ( !noSuggestions ) {
			list = make( 'select' );
			list.onclick = function () {
				if ( self.highlightSuggestion( 0 ) ) self.textchange( false, true );
			};
			list.ondblclick = function ( e ) {
				if ( self.highlightSuggestion( 0 ) ) self.accept( e );
			};
			list.onchange = function () {
				self.highlightSuggestion( 0 );
				self.text.focus();
			};
			list.onkeyup = function ( evt ) {
				evt = evt || window.event; // W3C, IE
				if ( evt.keyCode === ESC ) {
					self.resetKeySelection();
					self.text.focus();
					window.setTimeout( function () {
						self.textchange( true );
					}, HC.suggest_delay );
				} else if ( evt.keyCode === RET ) {
					self.accept( evt );
				}
			};
			if ( !HC.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;
			}
		}
		this.list = list;

		function button_label( id, defaultText ) {
			var label = null;
			if (
				onUpload &&
window.UFUI !== undefined &&
window.UIElements !== undefined &&
UFUI.getLabel instanceof 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;
		}

	// Do not use type 'submit'; we cannot detect modifier keys if we do
		var OK = make( 'input' );
		OK.type = 'button';
		OK.value = button_label( 'wpOkUploadLbl', HC.messages.ok );
		OK.onclick = this.accept.bind( this );
		this.ok = OK;

		var cancel = make( 'input' );
		cancel.type = 'button';
		cancel.value = button_label( 'wpCancelUploadLbl', HC.messages.cancel );
		cancel.onclick = this.cancel.bind( this );
		this.cancelButton = cancel;

		var span = make( 'span' );
		span.className = 'hotcatinput';
		span.style.position = 'relative';
	// FF3.6: add the input field first, then the two absolutely positioned elements. Otherwise, FF3.6 may leave the
	// 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 ) {
		// eslint-disable-next-line no-new
			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 ? HC.existsYes : HC.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 ) 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 ) {
		return this.show( evt, ( this.engine && suggestionConfigs[ this.engine ].temp ) ? HC.suggestions : this.engine );
	},

	down: function ( evt ) {
		return this.show( evt, 'subcat', true );
	},

	up: function ( evt ) {
		return this.show( evt, 'parentcat' );
	},

	cancel: function () {
		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 ) { this.catLink.title = this.currentKey; } else { this.catLink.title = ''; }

		if ( this.state === CategoryEditor.UNCHANGED ) {
			if ( this.catLink ) this.catLink.style.backgroundColor = 'transparent';
		} else {
			if ( !onUpload ) {
				try {
					this.catLink.style.backgroundColor = HC.bg_changed;
				} catch ( ex ) {}
			}
		}
		checkMultiInput();
		forceRedraw();
	},

	removeEditor: function () {
		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();
	},

	rollback: function ( evt ) {
		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 ) {
		// 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( HC.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 ) {
		this.sanitizeInput();
		var value = this.text.value.split( '|' );
		var key = null;
		if ( value.length > 1 ) key = value[ 1 ];

		var v = value[ 0 ].replace( /_/g, ' ' ).replace( /^\s+|\s+$/g, '' );
		if ( HC.capitalizePageNames ) v = capitalize( v );

		this.lastInput = v;
		v = replaceShortcuts( v, HC.shortcuts );
		if ( !v.length ) {
			this.cancel();
			return false;
		}
		if ( !dontCheck && (
			conf.wgNamespaceNumber === 14 && v === conf.wgTitle || HC.blacklist && HC.blacklist.test( v ) ) ) {
			this.cancel();
			return false;
		}
		this.currentCategory = v;
		this.currentKey = key;
		this.currentExists = this.inputExists;
		return true;
	},

	accept: function ( evt ) {
	// eslint-disable-next-line no-bitwise
		this.noCommit = ( evtKeys( evt ) & 1 ) !== 0;
		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 ) ?
								HC.messages.cat_resolved.replace( /\$1/g, original ) :
								null );
					}
				}
			} );
		}
		return result;
	},

	close: function () {
		if ( !this.catLink ) {
		// 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( HC.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 ) {
			// eslint-disable-next-line no-new
				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( HC.links.undo, true ) );
			lk.title = HC.tooltips.undo;
			span.appendChild( make( ' ', true ) );
			span.appendChild( lk );
			this.normalLinks.appendChild( span );
			this.undoLink = span;
			if ( !onUpload ) {
				try {
					this.catLink.style.backgroundColor = HC.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 () {
	// Check again to catch problem cases after redirect resolution
		if (
			(
				this.currentCategory === this.originalCategory &&
(
	this.currentKey === this.originalKey ||
	this.currentKey === null && !this.originalKey.length ) ) ||
conf.wgNamespaceNumber === 14 && this.currentCategory === conf.wgTitle ||
HC.blacklist && HC.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 ) {
	// eslint-disable-next-line no-bitwise
		this.doRemove( evtKeys( evt ) & 1 );
		return evtKill( evt );
	},

	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 = HC.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 || HC.del_needs_diff;
				var self = this;
				initiateEdit(
					function ( failure ) {
						performChanges( failure, self );
					},
					function ( msg ) {
						self.state = self.originalState;
						alert( msg );
					} );
			}
		}
	},

	restore: function ( evt ) {
	// Can occur only if we do have a commit button and are not on the upload form
		this.catLink.title = this.currentKey || '';
		this.catLink.style.textDecoration = '';
		this.state = this.originalState;
		if ( this.state === CategoryEditor.UNCHANGED ) {
			this.catLink.style.backgroundColor = 'transparent';
		} else {
			try {
				this.catLink.style.backgroundColor = HC.bg_changed;
			} catch ( ex ) {}
		}
		this.normalLinks.style.display = '';
		this.undelLink.style.display = 'none';
		checkMultiInput();
		return evtKill( evt );
	},

// Internal operations

	selectEngine: function ( engineName ) {
		if ( !this.engineSelector ) return;
		for ( var i = 0; i < this.engineSelector.options.length; i++ ) this.engineSelector.options[ i ].selected = this.engineSelector.options[ i ].value === engineName;
	},

	sanitizeInput: function () {
		var v = this.text.value || '';
		v = v.replace( /^(\s|_)+/, '' ); // Trim leading blanks and underscores
		var re = new RegExp( '^(' + HC.category_regexp + '):' );
		if ( re.test( v ) ) v = v.substring( v.indexOf( ':' ) + 1 ).replace( /^(\s|_)+/, '' );

		if ( HC.capitalizePageNames ) v = capitalize( v );

	// Only update the input field if there is a difference. IE8 appears to reset the selection
	// and place the cursor at the front upon reset, which makes our autocompletetion become a
	// nuisance. FF and IE6 don't seem to have this problem.
		if ( this.text.value !== null && this.text.value !== v ) this.text.value = v;
	},

	makeCall: function ( url, callbackObj, engine, queryKey, cleanKey ) {
		var cb = callbackObj,
			e = engine,
			v = queryKey,
			z = cleanKey,
			thisObj = this;

		function done() {
			cb.callsMade++;
			if ( cb.callsMade === cb.nofCalls ) {
				if ( cb.exists ) cb.allTitles.exists = true;

				if ( cb.normalized ) cb.allTitles.normalized = cb.normalized;

				if ( !cb.dontCache && !suggestionConfigs[ cb.engineName ].cache[ z ] ) suggestionConfigs[ cb.engineName ].cache[ z ] = cb.allTitles;

				thisObj.text.readOnly = false;
				if ( !cb.cancelled ) thisObj.showSuggestions( cb.allTitles, cb.noCompletion, v, cb.engineName );

				if ( cb === thisObj.callbackObj ) thisObj.callbackObj = null;

				cb = undefined;
			}
		}

		$.getJSON( url, function ( json ) {
			var titles = e.handler( json, z );
			if ( titles && titles.length ) {
				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;
			}
			done();
		} ).fail( function ( req ) {
			if ( !req ) noSuggestions = true;
			cb.dontCache = true;
			done();
		} );
	},

	callbackObj: null,

	textchange: function ( dont_autocomplete, force ) {
	// Hide all other lists
		makeActive( this );
	// Get input value, omit sort key, if any
		this.sanitizeInput();
		var v = this.text.value;
	// Disregard anything after a pipe.
		var pipe = v.indexOf( '|' );
		if ( pipe >= 0 ) {
			this.currentKey = v.substring( pipe + 1 );
			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 && HC.blacklist && HC.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 ) {
			this.showSuggestions( [] );
			return;
		}
		var cleanKey = v.replace( /[\u200E\u200F\u202A-\u202E]/g, '' ).replace( wikiTextBlankRE, ' ' );
		cleanKey = replaceShortcuts( cleanKey, HC.shortcuts );
		cleanKey = cleanKey.replace( /^\s+|\s+$/g, '' );
		if ( !cleanKey.length ) {
			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 ) {
		this.text.readOnly = false;
		this.dab = null;
		this.showsList = false;
		if ( !this.list ) return;
		if ( noSuggestions ) {
			if ( this.list ) this.list.style.display = 'none';

			if ( this.engineSelector ) this.engineSelector.style.display = 'none';

			if ( this.icon ) this.icon.style.display = 'none';

			this.inputExists = true; // Default...
			return;
		}
		this.engineName = engineName;
		if ( engineName ) {
			if ( !this.engineSelector ) this.engineName = null;
		} else {
			if ( this.engineSelector ) this.engineSelector.style.display = 'none';
		}
		if ( queryKey ) {
			if ( this.lastInput.indexOf( queryKey ) ) 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 = ( HC.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 ( HC.blacklist ) {
				for ( i = 0; i < titles.length; i++ ) {
					if ( HC.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 ) {
			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( HC.existsNo );

				this.inputExists = false;
			}
			return;
		}

		var firstTitle = titles[ 0 ];
		var completed = this.autoComplete( firstTitle, v, vNormalized, key, dontAutocomplete );
		var existing = completed || knownToExist || firstTitle === replaceShortcuts( v, HC.shortcuts );
		if ( engineName && suggestionConfigs[ engineName ] && !suggestionConfigs[ engineName ].temp ) {
			this.icon.src = armorUri( existing ? HC.existsYes : HC.existsNo );
			this.inputExists = existing;
		}
		if ( completed ) {
			this.lastInput = firstTitle;
			if ( titles.length === 1 ) {
				this.list.style.display = 'none';
				if ( this.engineSelector ) this.engineSelector.style.display = 'none';

				return;
			}
		}
	// (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 () {
		this.showsList = true;
		if ( !this.is_active ) {
			this.list.style.display = 'none';
			if ( this.engineSelector ) this.engineSelector.style.display = 'none';

			return;
		}
		var nofItems = ( this.list.options.length > HC.listSize ? HC.listSize : 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 < HC.listSize ) maxListHeight = ( listh / nofItems ) * HC.listSize;

		function viewport( what ) {
			if ( is_webkit && !document.evaluate ) {
			// Safari < 3.0
				return window[ 'inner' + what ];
			}
			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 += ( node.offsetTop || 0 );
				l += ( node.offsetLeft || 0 );
				node = node.offsetParent;
			} while ( node );
			return {
				x: l,
				y: t
			};
		}

		var textPos = position( this.text ),
			nl = 0,
			nt = 0,
			offset = 0,
		// Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
			textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
		if ( this.engineName ) {
			this.engineSelector.style.zIndex = 5;
			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.
			if ( this.engineSelector.style.display === 'none' ) {
				this.engineSelector.style[ anchor ] = '-10000px';
				this.engineSelector.style.top = '0';
				this.engineSelector.style.display = '';
				offset = this.engineSelector.offsetHeight;
				this.engineSelector.style.display = 'none';
			} else {
				offset = this.engineSelector.offsetHeight;
			}
			this.engineSelector.style[ anchor ] = nl + 'px';
		}
		if ( textPos.y < maxListHeight + offset + 1 ) {
		// The list might extend beyond the upper border of the page. Let's avoid that by placing it
		// below the input text field.
			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;
			}
			var relative_offset = 0;
			if ( left < scroll ) relative_offset = scroll - left; else if ( right > scroll + view_w ) relative_offset = -( right - scroll - view_w );

			if ( is_rtl ) relative_offset = -relative_offset;

			if ( relative_offset ) 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 ) ) {
		// 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 ||
		this.text.selectionStart !== undefined &&
		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 ( 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 ( 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 () {
		this.lastSelection = this.getSelection();
	},

	processKey: function ( evt ) {
		var dir = 0;
		switch ( this.lastKey ) {
			case UP:
				dir = -1;
				break;
			case DOWN:
				dir = 1;
				break;
			case PGUP:
				dir = -HC.listSize;
				break;
			case PGDOWN:
				dir = HC.listSize;
				break;
			case ESC: // Inhibit default behavior (revert to last real input in FF: we do that ourselves)
				return evtKill( evt );
		}
		if ( dir ) {
			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();
			}
		}
		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( HC.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;
	}
}; // end CategoryEditor.prototype

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 = ( window.JSconfig !== undefined && JSconfig.keys ) ? JSconfig.keys : {};
	HC.dont_add_to_watchlist = ( window.hotcat_dont_add_to_watchlist !== undefined ?
		!!window.hotcat_dont_add_to_watchlist :
		( config.HotCatDontAddToWatchlist !== undefined ? config.HotCatDontAddToWatchlist :
			HC.dont_add_to_watchlist ) );
	HC.no_autocommit = ( window.hotcat_no_autocommit !== undefined ?
		!!window.hotcat_no_autocommit : ( config.HotCatNoAutoCommit !== undefined ?
			config.HotCatNoAutoCommit :
			HC.no_autocommit ) );
	HC.del_needs_diff = ( window.hotcat_del_needs_diff !== undefined ?
		!!window.hotcat_del_needs_diff :
		( config.HotCatDelNeedsDiff !== undefined ?
			config.HotCatDelNeedsDiff :
			HC.del_needs_diff ) );
	HC.suggest_delay = window.hotcat_suggestion_delay || config.HotCatSuggestionDelay || HC.suggest_delay;
	HC.editbox_width = window.hotcat_editbox_width || config.HotCatEditBoxWidth || HC.editbox_width;
	HC.suggestions = window.hotcat_suggestions || config.HotCatSuggestions || HC.suggestions;
	if ( typeof HC.suggestions !== 'string' || !suggestionConfigs[ HC.suggestions ] ) HC.suggestions = 'combined';

	HC.fixed_search = ( window.hotcat_suggestions_fixed !== undefined ?
		!!window.hotcat_suggestions_fixed : ( config.HotCatFixedSuggestions !== undefined ?
			config.HotCatFixedSuggestions : HC.fixed_search ) );
	HC.single_minor = ( window.hotcat_single_changes_are_minor !== undefined ?
		!!window.hotcat_single_changes_are_minor :
		( config.HotCatMinorSingleChanges !== undefined ?
			config.HotCatMinorSingleChanges :
			HC.single_minor ) );
	HC.bg_changed = window.hotcat_changed_background || config.HotCatChangedBackground || HC.bg_changed;
	HC.use_up_down = ( window.hotcat_use_category_links !== undefined ?
		!!window.hotcat_use_category_links :
		( config.HotCatUseCategoryLinks !== undefined ?
			config.HotCatUseCategoryLinks :
			HC.use_up_down ) );
	HC.listSize = window.hotcat_listSize || config.HotCatListSize || HC.listSize;
	if ( conf.wgDBname !== 'commonswiki' ) HC.changeTag = config.HotCatChangeTag || '';

// The next whole shebang is needed, because manual tags get not submitted except of save
	if ( HC.changeTag ) {
		var eForm = document.editform,
			catRegExp = new RegExp( '^\\[\\[(' + HC.category_regexp + '):' ),
			oldTxt;
	// Returns true if minor change
		var isMinorChange = function () {
			var newTxt = eForm.wpTextbox1;
			if ( !newTxt ) return;
			newTxt = newTxt.value;
			var oldLines = oldTxt.match( /^.*$/gm ),
				newLines = newTxt.match( /^.*$/gm ),
				cArr; // changes
			var except = function ( aArr, bArr ) {
				var result = [],
					lArr, // larger
					sArr; // smaller
				if ( aArr.length < bArr.length ) {
					lArr = bArr;
					sArr = aArr;
				} else {
					lArr = aArr;
					sArr = bArr;
				}
				for ( var i = 0; i < lArr.length; i++ ) {
					var item = lArr[ i ];
					var ind = $.inArray( item, sArr );
					if ( ind === -1 ) result.push( item );
					else sArr.splice( ind, 1 ); // don't check this item again
				}
				return result.concat( sArr );
			};
			cArr = except( oldLines, newLines );
			if ( cArr.length ) {
				cArr = $.grep( cArr, function ( c ) {
					c = $.trim( c );
					return ( c && !catRegExp.test( c ) );
				} );
			}
			if ( !cArr.length ) {
				oldTxt = newTxt;
				return true;
			}
		};

		if ( conf.wgAction === 'submit' && conf.wgArticleId && eForm && eForm.wpSummary && document.getElementById( 'wikiDiff' ) ) {
			var sum = eForm.wpSummary,
				sumA = eForm.wpAutoSummary;
			if ( sum.value && sumA.value === HC.changeTag ) { // HotCat diff
			// MD5 hash of the empty string, as HotCat edit is based on empty sum
				sumA.value = sumA.value.replace( HC.changeTag, 'd41d8cd98f00b204e9800998ecf8427e' );
			// Attr creation and event handling is not same in all (old) browsers so use $
				var $ct = $( '<input type="hidden" name="wpChangeTags">' ).val( HC.changeTag );
				$( eForm ).append( $ct );
				oldTxt = eForm.wpTextbox1.value;
				$( '#wpSave' ).one( 'click', function () {
					if ( $ct.val() )
						sum.value = sum.value.replace( ( HC.messages.using || HC.messages.prefix ), '' );

				} );
				var removeChangeTag = function () {
					$( eForm.wpTextbox1 ).add( sum ).one( 'input', function () {
						window.setTimeout( function () {
							if ( !isMinorChange() ) $ct.val( '' );
							else removeChangeTag();
						}, 500 );
					} );
				};
				removeChangeTag();
			}
		}
	}
// Numeric input, make sure we have a numeric value
	HC.listSize = parseInt( HC.listSize, 10 );
	if ( isNaN( HC.listSize ) || HC.listSize < 5 ) HC.listSize = 5;

	HC.listSize = Math.min( HC.listSize, 15 );

// Localize search engine names
	if ( HC.engine_names ) {
		for ( var key in HC.engine_names )
			if ( suggestionConfigs[ key ] && HC.engine_names[ key ] ) suggestionConfigs[ key ].name = HC.engine_names[ key ];

	}
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
	is_rtl = hasClass( document.body, 'rtl' );
	if ( !is_rtl ) {
		if ( document.defaultView && document.defaultView.getComputedStyle ) { // Gecko etc.
			is_rtl = document.defaultView.getComputedStyle( document.body, null ).getPropertyValue( 'direction' );
		} else if ( document.body.currentStyle ) { // IE, has subtle differences to getComputedStyle
			is_rtl = document.body.currentStyle.direction;
		} else { // Not exactly right, but best effort
			is_rtl = document.body.style.direction;
		}
		is_rtl = ( is_rtl === 'rtl' );
	}
}

function can_edit() {
	var container = null;
	switch ( mw.config.get( 'skin' ) ) {
		case 'cologneblue':
			container = document.getElementById( 'quickbar' );
		/* fall through */
		case 'standard':
		case 'nostalgia':
			if ( !container ) container = document.getElementById( 'topbar' );
			var lks = container.getElementsByTagName( 'a' );
			for ( var i = 0; i < lks.length; i++ ) {
				if ( param( 'title', lks[ i ].href ) === conf.wgPageName &&
param( 'action', lks[ i ].href ) === 'edit' ) return true;
			}
			return false;
		default:
		// all modern skins:
			return document.getElementById( 'ca-edit' ) !== null;
	}
}

// Legacy stuff
function closeForm() {
// Close all open editors without redirect resolution and other asynchronous stuff.
	for ( var i = 0; i < editors.length; i++ ) {
		var edit = editors[ i ];
		if ( edit.state === CategoryEditor.OPEN ) {
			edit.cancel();
		} else if ( edit.state === CategoryEditor.CHANGE_PENDING ) {
			edit.sanitizeInput();
			var value = edit.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 ) {
				edit.cancel();
			} else {
				edit.currentCategory = v;
				edit.currentKey = key;
				edit.currentExists = this.inputExists;
				edit.close();
			}
		}
	}
}

function setup_upload() {
	onUpload = true;
// Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
	var ip = document.getElementById( 'mw-htmlform-description' ) || document.getElementById( 'wpDestFile' );
	if ( !ip ) {
		ip = document.getElementById( 'wpDestFile' );
		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 ( window.UFUI && window.UIElements && UFUI.getLabel instanceof Function ) {
		try {
			label = UFUI.getLabel( 'wpCategoriesUploadLbl' );
		} catch ( ex ) {
			label = null;
		}
	}
	if ( !label ) {
		labelCell.id = 'hotcatLabel';
		labelCell.appendChild( make( HC.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' ) {
					// eslint-disable-next-line no-eval
						do_submit = eval( oldSubmit );
					} else if ( oldSubmit instanceof 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 = '[[' + HC.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;

function isOnPage( span ) {
	if ( span.firstChild.nodeType !== Node.ELEMENT_NODE ) return null;

	var catTitle = title( span.firstChild.getAttribute( 'href' ) );
	if ( !catTitle ) return null;

	catTitle = catTitle.substr( catTitle.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
	if ( HC.blacklist && HC.blacklist.test( catTitle ) ) return null;

	var result = {
		title: catTitle,
		match: [ '', '', '' ]
	};
	if ( pageText === null ) return result;

	if ( cleanedText === null ) {
		cleanedText = pageText
			.replace( /<!--(\s|\S)*?-->/g, '' )
			.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, '' );
	}
	result.match = find_category( cleanedText, catTitle, true );
	return result;
}

var initialized = false;
var setupTimeout = null;

function findByClass( scope, tag, className ) {
	var result = $( scope ).find( tag + '.' + className );
	return ( result && result.length ) ? result[ 0 ] : null;
}

function setup( additionalWork ) {
	if ( initialized ) return;
	initialized = true;
	if ( setupTimeout ) {
		window.clearTimeout( setupTimeout );
		setupTimeout = null;
	}
// Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
// each category, and add the + link.
	catLine =
		// Special:Upload
		catLine ||
		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 = HC.categories;
		label.appendChild( make( HC.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

	function createEditors( line, is_hidden ) {
		var i;
		var cats = line.getElementsByTagName( 'li' );
		if ( cats.length ) {
			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!
		var copyCats = new Array( cats.length );
		for ( i = 0; i < cats.length; i++ ) copyCats[ i ] = cats[ i ];
		for ( i = 0; i < copyCats.length; i++ ) {
			var test = isOnPage( copyCats[ i ] );
			if ( test !== null && test.match !== null ) {
			// eslint-disable-next-line no-new
				new CategoryEditor( line, copyCats[ i ], test.title, test.match[ 2 ], is_hidden );
			}
		}
		return copyCats.length ? copyCats[ copyCats.length - 1 ] : null;
	}

	var lastSpan = createEditors( catLine, false );
// Create one to add a new category
// eslint-disable-next-line no-new
	new CategoryEditor( newDOM ? catLine.getElementsByTagName( 'ul' )[ 0 ] : catLine, null, null, lastSpan !== null, false );
	if ( !onUpload ) {
		if ( pageText !== null && hiddenCats ) {
			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>' + HC.addmulti + '</a>)';
		var lk = multiSpan.getElementsByTagName( 'a' )[ 0 ];
		lk.onclick = function ( evt ) {
			setMultiInput();
			checkMultiInput();
			return evtKill( evt );
		};
		lk.title = HC.multi_tooltip;
		lk.style.cursor = 'pointer';
	}
	cleanedText = null;
	if ( additionalWork instanceof Function ) additionalWork();
	setupCompleted.loaded(); // Trigger signal; execute registered functions
	$( 'body' ).trigger( 'hotcatSetupCompleted' );
}

function createCommitForm() {
	if ( commitForm ) return;
	var formContainer = make( 'div' );
	formContainer.style.display = 'none';
	document.body.appendChild( formContainer );
	formContainer.innerHTML =
		'<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="' +
		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="d41d8cd98f00b204e9800998ecf8427e">' +
		'<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">' +
		'<input type="hidden" name="wpChangeTags">' +
		'<input type="hidden" value="ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ" name="wpUnicodeCheck">' +
		'</form>';
	commitForm = document.getElementById( 'hotcatCommitForm' );
}

function getPage() {
// We know we have an article here.
	if ( !conf.wgArticleId ) {
	// Doesn't exist yet. Disable on non-existing User pages -- might be a global user page.
		if ( conf.wgNamespaceNumber === 2 ) 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';
		HC.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.
	}
}

function setState( state ) {
	var cats = state.split( '\n' );
	if ( !cats.length ) 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 ) continue;
			var cat = cats[ i ].split( '|' );
			var key = cat.length > 1 ? cat[ 1 ] : null;
			cat = cat[ 0 ];
			var lk = make( 'a' );
			lk.href = wikiPagePath( HC.category_canonical + ':' + cat );
			lk.appendChild( make( cat, true ) );
			lk.title = cat;
			var span = make( 'span' );
			span.appendChild( lk );
			if ( !i ) 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 );

		for ( i = 0; i < newSpans.length; i++ ) {
		// eslint-disable-next-line no-new
			new CategoryEditor( catLine, newSpans[ i ].element, newSpans[ i ].title, newSpans[ i ].key );
		}
	}
	return null;
}

function getState() {
	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 ) {
			if ( key !== null ) text += '|' + key;
			if ( result === null ) result = text; else result += '\n' + text;
		}
	}
	return result;
}

function really_run() {
	initialize();

	if ( !HC.upload_disabled && conf.wgNamespaceNumber === -1 && conf.wgCanonicalSpecialPageName === 'Upload' && conf.wgUserName ) {
		setup_upload();
		setup( function () {
		// Check for state restoration once the setup is done otherwise, but before signalling setup completion
			if ( window.UploadForm && UploadForm.previous_hotcat_state ) UploadForm.previous_hotcat_state = setState( UploadForm.previous_hotcat_state );
		} );
	} else {
		if ( !conf.wgIsArticle || conf.wgAction !== 'view' || param( 'diff' ) !== null || param( 'oldid' ) !== null || !can_edit() || HC.disable() ) return;
		getPage();
	}
}

function run() {
	if ( HC.started ) return;
	HC.started = true;
	loadTrigger.register( really_run );
}

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

// Run as soon as possible. This varies depending on MediaWiki version;
// window's 'load' event is always safe, but usually we can do better than that.

if ( conf.wgCanonicalSpecialPageName !== 'Upload' ) {
// Reload HotCat after (VE) edits (bug T103285)
	mw.hook( 'postEdit' ).add( function () {
	// Reset HotCat in case this is a soft reload (VE edit)
		catLine = null;
		editors = [];
		initialized = false;
		HC.started = false;
		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.
$.when( mw.loader.using( 'user' ), $.ready ).then( run );
}( jQuery, mediaWiki ) );
// </nowiki>