MediaWiki:Gadget-HotCat.js: Unterschied zwischen den Versionen

aus Stargate Wiki, dem deutschsprachigen Stargate-Lexikon
Zur Navigation springen Zur Suche springen
CF (Diskussion | Beiträge)
K verschob „MediaWiki:HotCat.js“ nach „MediaWiki:Gadget-HotCat.js“: Wieder als Helferlein eingebunden
CF (Diskussion | Beiträge)
Update auf Version 2.17
Zeile 2: Zeile 2:


/*
/*
   HotCat V2.2b
   HotCat V2.17


   Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
   Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
Zeile 9: Zeile 9:
   can be selected interactively.
   can be selected interactively.


   Authors:
   Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat
    V0.0: July 2007 - 2010-05-26: original version by [[User:Magnus Manske]], with lots of
  List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Version_history
          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).


   License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
   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 :-)
*/
*/


// Globals:
/*
//  (inline script on the page):
  This code is MW version safe. It should run on any MediaWiki installation >= MW 1.15. Note: if
//    wgNamespaceNumber, wgCanonicalSpecialPageName, wgNamespaceIds (optional), wgFormattedNamespaces (optional)
  running on MW >= 1.17 configured with $wgLegacyJavaScriptGlobals != true, it will still force
//    wgScript, wgServer, wgArticlePath, wgScriptPath, wgAction, wgPageName, wgTitle, wgUserName, wgIsArticle,
  publishing the wg* globals in the window object. Note that HotCat is supposed to run with or
//    wgArticleId
  without jQuery, and also on older installations that do not yet have window.mediaWiki. If you
// ajax.js
  use any of these newer features, make sure you qualify them by checking whether they exist at
//    sajax_init_object
  all, and by providing some meaningful fallback implementation if not. To start itself, HotCat
//  wikibits.js
  uses either jQuery(document).ready(), if available (preferred), or the old addOnloadHook().
//   addOnloadHook, window.ie6_bugs, importScript
  If neither exists, HotCat won't start.
*/
if ((typeof wgAction == 'undefined') && window.mediaWiki && window.mediaWiki.config) { // Compatibility hack
  window.wgAction = window.mediaWiki.config.get('wgAction');
}
if (typeof (window.HotCat) == 'undefined' && wgAction != 'edit') { // Guard against double inclusions, and inactivate on edit pages


if (typeof (HotCat) == 'undefined') { // Guard against double inclusions
   
// Configuration stuff.
// Configuration stuff.
var HotCat = {
var HotCat = {
  isCommonsVersion : false
    // If you copy HotCat to your wiki, you should set this to false!
   // Localize these messages to the main language of your wiki.
   // Localize these messages to the main language of your wiki.
   messages :
   ,messages :
     { cat_removed  : 'Entferne [[Kategorie:$1]]'
     { cat_removed  : 'Entferne [[Kategorie:$1]]'
     ,template_removed  : 'Entferne {{[[Kategorie:$1|$1]]}}'
     ,template_removed  : 'Entferne {{[[Kategorie:$1|$1]]}}'
     ,cat_added    : 'Ergänze [[Kategorie:$1]]'
     ,cat_added    : 'Ergänze [[Kategorie:$1]]'
     ,cat_keychange: 'neuer Sortierschlüssel für [[Kategorie:$1]]: '
     ,cat_keychange: 'neuer Sortierschlüssel für [[Kategorie:$1]]: "$2"' // $2 is the new key
     ,cat_notFound : 'Kategorie "$1" konnte nicht gefunden werden'
     ,cat_notFound : 'Kategorie "$1" konnte nicht gefunden werden'
     ,cat_exists  : 'Kategorie "$1" bereits enthalten; nicht ergänzt'
     ,cat_exists  : 'Kategorie "$1" bereits enthalten; nicht ergänzt'
     ,cat_resolved : ' (Weiterleitung [[Kategorie:$1]] aufgelöst)' //wird nicht für dewiki benötigt
     ,cat_resolved : ' (Weiterleitung [[Kategorie:$1]] aufgelöst)'
     ,uncat_removed: 'entferne {{uncategorized}}' //wird nicht für dewiki benötigt
     ,uncat_removed: 'entferne {{uncategorized}}'
    ,separator    : '; '
     ,prefix      : '[[Hilfe:HotCat|HC]]: '
     ,prefix      : '[[Hilfe:HotCat|HC]]: '
         // Some text to prefix to the edit summary.
         // Some text to prefix to the edit summary.
     ,using        : ''
     ,using        : ""
         // Some text to append to the edit summary. Named 'using' for historical reasons. If you prefer
         // 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.
         // to have a marker at the front, use prefix and set this to the empty string.
     ,multi_change : '$1 Kategorien'
     ,multi_change : '$1 Kategorien'
         // $1 is replaced by a number
         // $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.
     ,commit      : 'Speichern'
     ,commit      : 'Speichern'
         // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
         // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
Zeile 71: Zeile 71:
         // see localization hook below.
         // see localization hook below.
     }
     }
  ,category_regexp    : '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]|[Kk][Aa][Tt][Ee][Gg][Oo][Rr][Ii][Ee]'
  ,category_regexp    : '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]'
   // Regular sub-expression matching all possible names for the category namespace. Is automatically localized
   // 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
   // correctly if you're running MediaWiki 1.16 or later. Otherwise, set it appropriately, e.g. at the German
Zeile 81: Zeile 81:
   // MediaWiki 1.16 or later; otherwise, set it to the preferred category name (e.g., "Kategorie").
   // MediaWiki 1.16 or later; otherwise, set it to the preferred category name (e.g., "Kategorie").
  ,categories        : 'Kategorien'
  ,categories        : 'Kategorien'
   // Plural of category_canonical
   // Plural of category_canonical.
  ,disambig_category  : null
  ,disambig_category  : null
   // Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
   // Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
Zeile 106: Zeile 106:
   // Tooltip for the "enter multi-mode" link
   // Tooltip for the "enter multi-mode" link
  ,disable            :
  ,disable            :
     function () { // Return true to disable HotCat
     function () { // Return true to disable HotCat. HotCat guarantees that the wg* globals exist here.
       return (  wgNamespaceNumber < 0  // Special pages; Special:Upload is handled differently
      var ns = wgNamespaceNumber;
               || wgNamespaceNumber == 10 // Templates
       return (  ns < 0  // Special pages; Special:Upload is handled differently
               || wgNamespaceNumber == 8  // MediaWiki
               || ns === 10 // Templates
               || wgNamespaceNumber == 2
               || ns === 8  // MediaWiki
                && wgTitle && wgTitle.length >= 3 && wgTitle.lastIndexOf ('.js') + 3 == wgTitle.length
               || ns === 6 && wgArticleId === 0 // Non-existing file pages
                // User scripts
              || ns === 2 && /\.(js|css)$/.test(wgTitle) // User scripts
               || typeof (wgNamespaceIds) != 'unknown'
               || typeof (wgNamespaceIds) != 'undefined'
                 && (  wgNamespaceNumber == wgNamespaceIds['creator']
                 && (  ns === wgNamespaceIds['creator']
                     || wgNamespaceNumber == wgNamespaceIds['timedtext']
                     || ns === wgNamespaceIds['timedtext']
                    || ns === wgNamespaceIds['institution']
                     )
                     )
             );
             );
Zeile 122: Zeile 123:
   // A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
   // A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
   // If not, set it to null.
   // If not, set it to null.
  ,existsYes    : 'http://upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
  ,existsYes    : '//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'
  ,existsNo    : '//upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png'
   // The images used for the little indication icon. Should not need changing.
   // The images used for the little indication icon. Should not need changing.
  ,template_regexp : '[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]'  
  ,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.
   // 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]'.
   // On the German Wikipedia, you might use '[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]|[Vv][Oo][Rr][Ll][Aa][Gg][Ee]'.
  ,template_categories : {}
  ,template_categories : {}
   // a list of categories which can be removed by removing a template
   // a list of categories which can be removed by removing a template
Zeile 140: Zeile 141:
   ,parentcat  : 'Überkategorien'
   ,parentcat  : 'Überkategorien'
   }
   }
  // Names for the search engines
  // Names for the search engines
  ,capitalizePageNames : true
,capitalizePageNames : true
   // Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the first letter
   // 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
   // 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
   // ("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)
   // (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
   // 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
   // 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
   // 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.
   // if that API query should fail for some strange reason.
,upload_disabled : false
  // If upload_disabled is true, HotCat will not be used on the Upload form.
,blacklist : null
  // Single regular expression matching blacklisted categories that cannot be changed or
  // added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
  // or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
  // word "maintenance" in its title.
   // Stuff changeable by users:
   // Stuff changeable by users:
  ,bg_changed : '#F8CCB0'
  ,bg_changed : '#F8CCB0'
Zeile 167: Zeile 176:
  ,use_up_down : true
  ,use_up_down : true
   // If false, do not display the "up" and "down" links
   // If false, do not display the "up" and "down" links
,list_size : 5
  // Default list size
,single_minor : true
  // If true, single category changes are marked as minor edits. If false, they're not.
,dont_add_to_watchlist : false
  // If true, never add a page to the user's watchlist. If false, pages get added to the watchlist if
  // the user has the "Add pages I edit to my watchlist" or the "Add pages I create to my watchlist"
  // options in his or her preferences set.
};
};


importScript ('MediaWiki:Gadget-HotCat.js/' + wgUserLanguage);
// Make sure this is exported, so that localizations *can* actually modify parts of it, and the
// Localization hook to localize HotCat.messages.commit and HotCat.messages.multi_error. For German, the
// guard at the top actually works. (If we're loaded as an extension module through the resource
// file would be "MediaWiki:Gadget-HotCat.js/de", and its contents could be for instance
// loader, this outer scope may actually be a closure, not the global "window" scope.)
//
if (typeof (window.HotCat) == 'undefined') window.HotCat = HotCat;
// HotCat.messages.commit      = 'Speichern';
 
// HotCat.messages.ok          = 'OK';
(function () { // Local scope to avoid polluting the global namespace with declarations
// HotCat.messages.cancel      = 'Abbrechen';
// HotCat.messages.multi_error = 'Seitentext konnte nicht vom Server geladen werden. Die Änderungen können '
//                             +'leider nicht gespeichert werden.';


// No further changes should be necessary here.
  // Backwards compatibility stuff. We want HotCat to work with either wg* globals, or with mw.config.get().
   // Our "solution" is to publish the wg* globals if they're not already published.
(function () {
   if (window.mediaWiki && window.mediaWiki.config) {
 
    var globals = window.mediaWiki.config.get();
   // First auto-localize the regexps for the category and the template namespaces.
     if (globals && globals !== window) {
   if (typeof (wgFormattedNamespaces) != 'undefined') {
       for (var k in globals) window[k] = globals[k];
     function autoLocalize (namespaceNumber, fallback) {
       window.mediWiki.config = new window.mediaWiki.Map(true); // Make config point to window again.
       function create_regexp_str (name)
    }
       {
    globals = null;
        if (!name || name.length == 0) return "";
  }
        var regex_name = "";
  // More backwards compatibility. We have four places where we test for the browser: once for
        for (var i = 0; i < name.length; i++){
  // Safari < 3.0, once for WebKit (Chrome or Safari, any versions), and twice for IE <= 6.
          var initial = name.substr (i, 1);
  var ua = navigator.userAgent.toLowerCase();
          var ll = initial.toLowerCase ();
  var is_ie6 = /msie ([0-9]{1,}[\.0-9]{0,})/.exec(ua) != null && parseFloat(RegExp.$1) <= 6.0;
          var ul = initial.toUpperCase ();
  var is_webkit = /applewebkit\/\d+/.test(ua) && ua.indexOf ('spoofer') < 0;
          if (ll == ul){
  // And even more compatbility. HotCat was developed without jQuery, and anyway current jQuery
            regex_name += initial;
  // (1.7.1) doesn't seem to support in jquery.getJSON() or jQuery.ajax() the automatic
           } else {
  // switching from GET to POST requests if the query arguments would make the uri too long.
             regex_name += '[' + ll + ul + ']';
  // (IE has a hard limit of 2083 bytes, and the servers may have limits around 4 or 8kB.)
  //    Anyway, HotCat is supposed to run on wikis without jQuery, so we'd have to supply some
  // ajax routines ourselves in any case. We can't rely on the old sajax_init_object(), newer
  // MW versions (>= 1.19) might not have it.
  var getJSON = (function () {
    function getRequest () {
      var request = null;
      try {
        request = new window.XMLHttpRequest();
      } catch (anything) {
        if (window.ActiveXObject) {
           try {
             request = new window.ActiveXObject('Microsoft.XMLHTTP');
          } catch (any) {
           }
           }
         }
         } // end if IE
        return regex_name.replace (/[ _]/g, '[ _]').replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
      } // end try-catch
      }
      return request;
    }


       fallback = fallback.toLowerCase();
    function makeRequest (settings) {
       var canonical  = wgFormattedNamespaces["" + namespaceNumber].toLowerCase();
       var req = getRequest();
       var regexp    = create_regexp_str (canonical);
      if (!req && settings && settings.error) settings.error (req);
       if (fallback && canonical != fallback) regexp += '|' + create_regexp_str (fallback)
      if (!req || !settings || !settings.uri) return req;
      for (var cat_name in wgNamespaceIds) {
       var uri = armorUri (settings.uri);
         if (  typeof (cat_name) == 'string'
       var args = settings.data || null;
            && cat_name.toLowerCase () != canonical
      var method;
            && cat_name.toLowerCase () != fallback
       if (args && uri.length + args.length + 1 > 2000) {
            && wgNamespaceIds[cat_name] == namespaceNumber)
        // We lose caching, but at least we can make the request
         {
         method = 'POST';
          regexp += '|' + create_regexp_str (cat_name);
        req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
         }
      } else {
        method = 'GET';
         if (args) uri += '?' + args;
         args = null;
       }
       }
       return regexp;
       req.open (method, uri, true);
      req.onreadystatechange = function () {
        if (req.readyState != 4) return;
        if (req.status != 200 || !req.responseText || !(/^\s*[\{\[]/.test(req.responseText))) {
          if (settings.error) settings.error (req);
        } else {
          if (settings.success) settings.success (eval ('(' + req.responseText + ')'));
        }         
      };
      req.setRequestHeader ('Pragma', 'cache=yes');
      req.setRequestHeader ('Cache-Control', 'no-transform');
      req.send (args);
      return req;
     }
     }


     if (wgFormattedNamespaces['14']) {
     return makeRequest;
      HotCat.category_canonical = wgFormattedNamespaces['14'];
  })();
      HotCat.category_regexp = autoLocalize (14, 'category');
 
     }
  function armorUri (uri) {
     if (wgFormattedNamespaces['10']) {
     // Avoid protocol-relative URIs, IE7 has a bug with them in Ajax calls
      HotCat.template_regexp = autoLocalize (10, 'template');
     if (uri.length >= 2 && uri.substring(0, 2) == '//') return document.location.protocol + uri;
     }
     return uri;
   }
   }
    
 
   // Utility functions. Yes, this duplicates some functionality that also exists in other places, but
   function LoadTrigger () { this.initialize.apply (this, arguments); };
   // to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
   LoadTrigger.prototype = {
   // these few operations here.
    initialize : function (needed) {
   function bind (func, target) {
      this.queue = [];
     var f = func, tgt = target;
      this.toLoad = needed;
     return function () { return f.apply (tgt, arguments); };
    },
 
    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.
  HotCat.runWhenReady = function (callback) {setupCompleted.register(callback);};
 
  var loadTrigger = new LoadTrigger(2);
   // Used to delay running the HotCat setup until /local_defaults and localizations have been loaded.
 
   function load (uri) {
     var head = document.getElementsByTagName ('head')[0];
    var s = document.createElement ('script');
    s.setAttribute ('src', armorUri(uri));
    s.setAttribute ('type', 'text/javascript');
    var done = false;
 
     function afterLoad () {
      if (done) return;
      done = true;
      s.onload = s.onreadystatechange = s.onerror = null; // Properly clean up to avoid memory leaks in IE
      if (head && s.parentNode) head.removeChild (s);
      loadTrigger.loaded();
    }
 
    s.onload = s.onreadystatechange = function () { // onreadystatechange for IE, onload for all others
      if (done) return;
      if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
        afterLoad ();
      }
    };
    s.onerror = afterLoad; // Clean up, but otherwise ignore errors
    head.insertBefore (s, head.firstChild); // appendChild may trigger bugs in IE6 here
   }
   }
   function make (arg, literal) {
 
     if (!arg) return null;
   function loadJS (page) {
    return literal ? document.createTextNode (arg) : document.createElement (arg);
     load (wgServer + wgScript + '?title=' + encodeURIComponent (page) + '&action=raw&ctype=text/javascript');
   }
   }
   function param (name, uri) {
 
     if (typeof (uri) == 'undefined' || uri === null) uri = document.location.href;
   function loadURI (href) {
     var re = RegExp ('[&?]' + name + '=([^&#]*)');
    var url = href;
     var m = re.exec (uri);
     if (url.substring (0, 2) == '//') {
     if (m && m.length > 1) return decodeURIComponent(m[1]);
      url = window.location.protocol + url;
    return null;
     } else if (url.substring (0, 1) == '/') {
      url = wgServer + url;
     }
     load (url);
   }
   }
   function title (href) {
 
     if (!href) return null;
   if (HotCat.isCommonsVersion && wgServer.indexOf ('/commons') < 0) {
     var script = wgScript + '?';
     // We're running in some other wiki, which hotlinks to the Commons version. The other wiki can put local settings
     if (href.indexOf (script) == 0 || href.indexOf (wgServer + script) == 0) {
    // in this file to override the Commons settings for all user languages. For instance, if on your wiki people do
      // href="/w/index.php?title=..."
    // not like automatic saving, you'd add in that file the line HotCat.no_autocommit = true; If you hotlink, you
      return param ('title', href);
    // *must* adapt HotCat.categories in this file to the local translation in wgContentLanguage of your wiki of the
    // English plural "Categories", and you should provide translations in wgContentLanguage of your wiki of all messages,
    // tooltips, and of the engine names.
    loadJS ('MediaWiki:Gadget-HotCat.js/local_defaults');
  } else {
     loadTrigger.loaded();
  }
 
  if (wgUserLanguage != 'en') {
    // Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
     if (window.hotcat_translations_from_commons && wgServer.indexOf ('/commons') < 0) {
      loadURI (
        ((wgServer.indexOf( "https://secure.wikimedia.org") === 0)
          ? '/wikipedia/commons/w/index.php?title='
          : '//commons.wikimedia.org/w/index.php?title='
        )
        + 'MediaWiki:Gadget-HotCat.js/' + wgUserLanguage
        + '&action=raw&ctype=text/javascript&smaxage=21600&maxage=86400'
      );
     } else {
     } else {
       // href="/wiki/..."
       // Load translations locally
       var prefix = wgArticlePath.replace ('$1', "");
       loadJS ('MediaWiki:Gadget-HotCat.js/' + wgUserLanguage);
      if (href.indexOf (prefix) != 0) prefix = wgServer + prefix; // Fully expanded URL?
      if (href.indexOf (prefix) == 0)
        return decodeURIComponent (href.substring (prefix.length));
     }
     }
    return null;
   } else {
  }
     loadTrigger.loaded();
  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
   // No further changes should be necessary here.
 
  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) {
   // The following regular expression strings are used when searching for categories in wikitext.
    var cat_regex = null;
  var wikiTextBlank  = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
    if(HotCat.template_categories[category]){
  var wikiTextBlankRE = new RegExp (wikiTextBlank, 'g');
      cat_regex = new RegExp ('\\{\\{\\s*(' + HotCat.template_regexp + '(?=\\s*:))?\\s*'
  // Regexp for handling blanks inside a category title or namespace name.
                              + '(?:' + HotCat.template_categories[category] + ')'
  // See http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Title.php?revision=104051&view=markup#l2722
                              + '\\s*(\\|.*?)?\\}\\}', 'g'
  // 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
    } else {
  // number of special whitespace characters by simple blanks. And finally, blanks are treated as underscores.
      var cat_name  = category.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
  // Therefore, when looking for page titles in wikitext, we must handle all these cases.
      var initial  = cat_name.substr (0, 1);
  //  Note: we _do_ include the horizontal tab in the above list, even though the MediaWiki software for some reason
      cat_regex = new RegExp ('\\[\\[\\s*(' + HotCat.category_regexp + ')\\s*:\\s*'
  // appears to not handle it. The zero-width space \u200B is _not_ handled as a space inside titles by MW.
                              + (initial == '\\' || !HotCat.capitalizePageNames
  var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
                                ? initial
  // Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
                                : '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
  // a link must be on one single line.
                              + cat_name.substring (1).replace (/[ _]/g, '[ _]')
  //  MediaWiki also removes Unicode bidi override characters in page titles (and namespace names) completely.
                              + '\\s*(\\|.*?)?\\]\\]', 'g'
  // 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,
    if (once) return cat_regex.exec (wikitext);
  // or adjacent to and inside of "[[" and "]]").
    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) {
   // First auto-localize the regexps for the category and the template namespaces.
 
  if (typeof (wgFormattedNamespaces) != 'undefined') {
     function find_insertionpoint (wikitext) {      
     function autoLocalize (namespaceNumber, fallback) {
       var copiedtext = wikitext.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
       function create_regexp_str (name)
                              .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
      {
      // Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
        if (!name || name.length === 0) return "";
      var index = -1;
        var regex_name = "";
      findCatsRE.lastIndex = 0;
        for (var i = 0; i < name.length; i++){
      while (findCatsRE.exec(copiedtext) != null) index = findCatsRE.lastIndex;  
          var initial = name.substr (i, 1);
      // We should try to find interwiki links here, but that's for later.
          var ll = initial.toLowerCase ();
       return index;
          var ul = initial.toUpperCase ();
    }
          if (ll == ul){
            regex_name += initial;
          } else {
            regex_name += '[' + ll + ul + ']';
          }
        }
        return regex_name.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1')
                        .replace (wikiTextBlankRE, wikiTextBlank);
       }


    var summary  = [];
      fallback = fallback.toLowerCase();
    var nameSpace = HotCat.category_canonical;
      var canonical  = wgFormattedNamespaces["" + namespaceNumber].toLowerCase();
     var cat_point = -1; // Position of removed category;
      var regexp     = create_regexp_str (canonical);
 
      if (fallback && canonical != fallback) regexp += '|' + create_regexp_str (fallback);
    if (key) key = '|' + key;
      for (var cat_name in wgNamespaceIds) {
    var keyChange = (toRemove && toAdd && toRemove == toAdd && toAdd.length > 0);
        if (  typeof (cat_name) == 'string'
    if (toRemove && toRemove.length > 0) {
            && cat_name.toLowerCase () != canonical
      var matches = find_category (wikitext, toRemove);
            && cat_name.toLowerCase () != fallback
      if (!matches || matches.length == 0) {
            && wgNamespaceIds[cat_name] == namespaceNumber)
        return {text: wikitext, 'summary': summary, error: HotCat.messages.cat_notFound.replace ('$1', toRemove)};
         {
      } else {
           regexp += '|' + create_regexp_str (cat_name);
        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;
      return regexp;
          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 (wgFormattedNamespaces['14']) {
        // If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
      HotCat.category_canonical = wgFormattedNamespaces['14'];
        // whitespace characters, insert a blank.
      HotCat.category_regexp = autoLocalize (14, 'category');
        var i = before.length - 1;
    }
        while (i >= 0 && before.charAt (i) != '\n' && before.substr (i, 1).search (/\s/) >= 0) i--;
    if (wgFormattedNamespaces['10']) {
        var j = 0;
      HotCat.template_regexp = autoLocalize (10, 'template');
        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';
          if (wikitext.length > 0 && wikitext.substr (wikitext.length - 2, 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') {
   // Utility functions. Yes, this duplicates some functionality that also exists in other places, but
    // Legacy code based on URI parameters, can add/remove/change only one single category. Still
  // to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
    // used for single-category changes.
  // these few operations here.
     var toRemove = param ('hotcat_removecat');
  function bind (func, target) {
     var toAdd    = param ('hotcat_newcat');
     var f = func, tgt = target;
     if (toAdd) {
    return function () { return f.apply (tgt, arguments); };
      toAdd = toAdd.replace (/_/g, ' ').replace (/^\s+|\s+$/g, "");
  }
      if (toAdd.length == 0) {
  function make (arg, literal) {
        toAdd = null;
     if (!arg) return null;
      } else if (HotCat.capitalizePageNames) {
     return literal ? document.createTextNode (arg) : document.createElement (arg);
        toAdd = capitalize (toAdd);
  }
      }
  function param (name, uri) {
     }
    if (typeof (uri) == 'undefined' || uri === null) uri = document.location.href;
     if (toRemove) {
    var re = new RegExp ('[&?]' + name + '=([^&#]*)');
       toRemove = toRemove.replace (/_/g, ' ').replace (/^\s+|\s+$/g, "");
    var m = re.exec (uri);
       if (toRemove.length == 0) {
    if (m && m.length > 1) return decodeURIComponent(m[1]);
        toRemove = null;
    return null;
       } else if (HotCat.capitalizePageNames) {
  }
         toRemove = capitalize (toRemove);
  function title (href) {
      }
    if (!href) return null;
     var script = wgScript + '?';
     if (href.indexOf (script) === 0 || href.indexOf (wgServer + script) === 0 || wgServer.substring(0, 2) == '//' && href.indexOf (document.location.protocol + 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 && prefix.substring(0, 2) == '//') prefix = document.location.protocol + prefix; // Protocol-relative wgServer?
       if (href.indexOf (prefix) === 0)
         return decodeURIComponent (href.substring (prefix.length));
     }
     }
    if (toAdd || toRemove) {
     return null;
      addOnloadHook (function () {
        if (!document.editform || !document.editform.wpTextbox1) return;
        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;
        document.editform.wpMinoredit.checked = true ;
        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;
   }
   }
 
   function hasClass (elem, name) {
  // The real HotCat UI
     return (' ' + elem.className + ' ').indexOf (' ' + name + ' ') >= 0;
 
  }
   function evtKeys (e) {
  function capitalize (str) {
     e = e || window.event || window.Event; // W3C, IE, Netscape
    if (!str || str.length === 0) return str;
    var code = 0;
     return str.substr(0, 1).toUpperCase() + str.substr (1);
    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) {
   function wikiPagePath (pageName) {
     e = e || window.event || window.Event; // W3C, IE, Netscape
     // Note: do not simply use encodeURI, it doesn't encode '&', which might break if wgArticlePath actually has the $1 in
     if (typeof (e.preventDefault) != 'undefined') {
     // a query parameter.
      e.preventDefault ();
    return wgArticlePath.replace('$1', encodeURIComponent (pageName).replace(/%3A/g, ':').replace(/%2F/g, '/'));
      e.stopPropagation ();
    } else
      e.cancelBubble = true;
    return false;
   }
   }
    
   function substitute (str, map) {
  var catLine     = null;
    // Replace $1, $2, or ${key1}, ${key2} by values from map. $$ is replaced by a single $.
  var onUpload    = false;  
    return str.replace(
  var editors      = [];
      /\$(\$|(\d+)|\{([^{}]+)\})/g
 
     ,function (match, dollar, idx, key) {
  var commitButton = null;
        if (dollar == '$') return '$';
   var commitForm  = null;
        var k = key || idx;
   var multiSpan    = null;
        var replacement = typeof (map[k]) === 'function' ? map[k](match, k) : map[k];
        return typeof (replacement) === 'string' ? replacement : (replacement || match);
      }
    );
   }
 
   // Text modification


   var pageText    = null;
   var findCatsRE =
  var pageTime     = null;
     new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(?:' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g');
  var pageWatched  = false;
  var watchCreate  = false;
  var watchEdit    = false;
  var minorEdits  = false;


   var is_rtl      = false;
   function replaceByBlanks (match) {
   var serverTime  = null;
    return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
   }


   function setMultiInput () {
   function find_category (wikitext, category, once) {
     if (commitButton || onUpload) return;
    var cat_regex = null;
    commitButton = make ('input');
     if(HotCat.template_categories[category]){
    commitButton.type  = 'button';
      cat_regex = new RegExp ('\\{\\{' + wikiTextBlankOrBidi + '(' + HotCat.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi
    commitButton.value = HotCat.messages.commit;
                              + '(?:' + HotCat.template_categories[category] + ')'
    commitButton.onclick = multiSubmit;
                              + wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}', 'g'
    if (multiSpan) {
                            );
      multiSpan.parentNode.replaceChild (commitButton, multiSpan);
     } else {
     } else {
       catLine.appendChild (commitButton);
       var cat_name  = category.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
      var initial  = cat_name.substr (0, 1);
      cat_regex = new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi
                              + (initial == '\\' || !HotCat.capitalizePageNames
                                ? initial
                                : '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
                              + cat_name.substring (1).replace (wikiTextBlankRE, wikiTextBlank)
                              + wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]', 'g'
                            );
     }
     }
    // Get the preferences, so that we can set wpWatchthis correctly later on. Also get information
     if (once) return cat_regex.exec (wikitext);
    // about whether the current user watches the page. Must use Ajax here.
    var copiedtext = wikitext.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
     if (wgUserName) {
                            .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
      var request = sajax_init_object ();    
    var result = [];
      request.open
    var curr_match = null;
        ('GET', wgServer + wgScriptPath + '/api.php?format=json&action=query&meta=userinfo&uiprop=options&prop=info&inprop=watched&titles=' + encodeURIComponent (wgPageName), true);
    while ((curr_match = cat_regex.exec (copiedtext)) != null) {
      request.onreadystatechange =
       result.push ({match : curr_match});
        function () {
          if (request.readyState != 4) return;
          if (request.status == 200 && request.responseText && request.responseText.charAt(0) == '{') {
            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 (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);      
     }
     }
    result.re = cat_regex;
    return result; // An array containing all matches, with positions, in result[i].match
   }
   }
    
 
   function checkMultiInput () {
   var interlanguageRE = null;
     if (!commitButton) return;
 
    var has_changes = false;
   function change_category (wikitext, toRemove, toAdd, key, is_hidden) {
    for (var i = 0; i < editors.length; i++) {
 
      if (editors[i].state != CategoryEditor.UNCHANGED) {
     function find_insertionpoint (wikitext) {
         has_changes = true;
      var copiedtext = wikitext.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
         break;
                              .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};
     }
     }
    commitButton.disabled = !has_changes;
  }


  function currentTimestamp () {
     var summary  = [];
     var now = new Date();
     var nameSpace = HotCat.category_canonical;
     var ts  = "" + now.getUTCFullYear();
     var cat_point = -1; // Position of removed category;
     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 (key) key = '|' + key;
     // Don't use the edit API or LAPI, it's always bothersome to report back errors like edit
     var keyChange = (toRemove && toAdd && toRemove == toAdd && toAdd.length > 0);
    // conflicts. Instead, make one remote call (blocking, because we can't continue anyway if
    if (toRemove && toRemove.length > 0) {
    // it doesn't succeed), getting the page text. Perform the changes on the text, then construct
      var matches = find_category (wikitext, toRemove);
    // a form to submit all this as a diff.
       if (!matches || matches.length === 0) {
    // Note: we have to do this even if we already got the page text. Other scripts may have already 
        return {text: wikitext, 'summary': summary, error: HotCat.messages.cat_notFound.replace (/\$1/g, toRemove)};
    // edited the text, and we don't necessarily get an edit conflict with ourself. Use case: open
       } else {
    // a file page, add an image note through ImageAnnotator, then change the categories. If HotCat
        var before = wikitext.substring (0, matches[0].match.index);
    // still operates on the page text loaded initially, it'll delete the just added note again, and
         var after  = wikitext.substring (matches[0].match.index + matches[0].match[0].length);
    // somehow the MediaWiki software does not produce an edit conflict.
         if (matches.length > 1) {
     if (wgArticleId != 0) {
           // Remove all occurrences in after
      var request = sajax_init_object ();
           matches.re.lastIndex = 0;
      var uri    = wgServer + wgScriptPath
           after = after.replace (matches.re, "");
                  + '/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 && request.responseText.charAt(0) == '{') {
        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="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
        if (toAdd) {
                && editors[i].originalCategory
          nameSpace = matches[0].match[1] || nameSpace;
                && editors[i].originalCategory.length > 0)
          if (key == null) key = matches[0].match[2]; // Remember the category key, if any.
      {
        }
         result = change_category (result.text, editors[i].originalCategory, null, null);
        // Remove whitespace (properly): strip whitespace, but only up to the next line feed.
         if (!result.error) {
        // If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
          changes++;
        // whitespace characters, insert a blank.
           deleted.push (editors[i].originalCategory);
        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/g, toRemove));
          }
         }
         }
       }
       }
     }
     }
     // Fill in the form and submit it
     if (toAdd && toAdd.length > 0) {
    commitForm.wpMinoredit.checked = minorEdits;
       var matches = find_category (wikitext, toAdd);
    commitForm.wpWatchthis.checked = wgArticleId == 0 && watchCreate || watchEdit || pageWatched;
      if (matches && matches.length > 0) {
    if (wgArticleId > 0) {
        return {text: wikitext, 'summary': summary, error : HotCat.messages.cat_exists.replace (/\$1/g, toAdd)};
       if (changes == 1) {
      } else {
        if (result.summary && result.summary.length > 0)
         var onCat = false;
          commitForm.wpSummary.value = HotCat.messages.prefix + result.summary.join ('; ') + HotCat.messages.using;
        if (cat_point < 0) {
         commitForm.wpMinoredit.checked = true;
          var point = find_insertionpoint (wikitext);
      } else if (changes > 1) {
          cat_point = point.idx;
        var summary = [];
          onCat = point.onCat;
        var shortSummary = [];
        } else {
        // Deleted
           onCat = true;
        for (var i=0; i < deleted.length; i++) {
           summary.push ('−[[' + HotCat.category_canonical + ':' + deleted[i] + ']]');
         }
         }
         if (deleted.length == 1)
         var newcatstring = '[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
          shortSummary.push ('[[' + HotCat.category_canonical + ':' + deleted[0] + ']]');
         if (cat_point >= 0) {
         else if (deleted.length > 1)
           var suffix = wikitext.substring (cat_point);
          shortSummary.push ('− ' + HotCat.messages.multi_change.replace ('$1', "" + deleted.length));
          wikitext = wikitext.substring (0, cat_point) + (cat_point > 0 ? '\n' : "") + newcatstring + (!onCat ? '\n' : "");
        // Added
          if (suffix.length > 0 && suffix.substr(0, 1) != '\n') {
        for (var i=0; i < added.length; i++) {
             wikitext += '\n' + suffix;
           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
        for (var i=0; i < changed.length; i++) {
          if (changed[i].from != changed[i].to) {
             summary.push ('±[[' + HotCat.category_canonical + ':' + changed[i].from + ']]→[['
                        + HotCat.category_canonical + ':' + changed[i].to + ']]');
           } else {
           } else {
             summary.push ('±[[' + HotCat.category_canonical + ':' + changed[i].from + ']]');
             wikitext += suffix;
           }
           }
        } else {
          if (wikitext.length > 0 && wikitext.substr (wikitext.length - 1, 1) != '\n')
            wikitext += '\n';
          wikitext += '\n' + newcatstring;
         }
         }
         if (changed.length == 1) {
         if (keyChange) {
           if (changed[0].from != changed[0].to) {
          var k = key || "";
            shortSummary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + ']]→[['
           if (k.length > 0) k = k.substr (1);
                        + HotCat.category_canonical + ':' + changed[0].to + ']]');
           summary.push (substitute (HotCat.messages.cat_keychange, [null, toAdd, k]));
           } else {
         } else {
            shortSummary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + ']]');
           summary.push (HotCat.messages.cat_added.replace (/\$1/g, toAdd));
          }
         } else if (changed.length > 1) {
           shortSummary.push ('± ' + HotCat.messages.multi_change.replace ('$1', "" + changed.length));
         }
         }
         if (summary.length > 0) {
         if (HotCat.uncat_regexp && !is_hidden) {
           summary = summary.join ('; ');
           var txt = wikitext.replace (HotCat.uncat_regexp, ""); // Remove "uncat" templates
           if (summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length) {
           if (txt.length != wikitext.length) {
             summary = shortSummary.join ('; ');
            wikitext = txt;
             summary.push (HotCat.messages.uncat_removed);
           }
           }
          commitForm.wpSummary.value = HotCat.messages.prefix + summary + HotCat.messages.using;
         }
         }
       }
       }
     }
     }
     commitForm.wpTextbox1.value = result.text;
     return {text: wikitext, 'summary': summary, error: null};
    commitForm.wpStarttime.value = serverTime || currentTimestamp ();
    commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
    commitForm.submit();
   }
   }


   function resolveMulti (toResolve, callback) {
  // The real HotCat UI
     for (var i = 0; i < toResolve.length; i++) {
 
       toResolve[i].dab = null;
   function evtKeys (e) {
       toResolve[i].dabInput = toResolve[i].lastInput;
     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;
     }
     }
     if (noSuggestions) {
     return code;
      callback (toResolve);
  }
      return;
  function evtKill (e) {
    }
     e = e || window.event || window.Event; // W3C, IE, Netscape
    var request = sajax_init_object ();
     if (typeof (e.preventDefault) != 'undefined') {
    if (!request) {
       e.preventDefault ();
      noSuggestions = true;
       e.stopPropagation ();
      callback (toResolve);
     } else
      return;
       e.cancelBubble = true;
    }
    return false;
     var url = wgServer + wgScriptPath + '/api.php';
  }
    // Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
  function addEvent (node, evt, f, capture)
     // 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'
    if (window.jQuery && (!capture || !node.addEventListener)) window.jQuery (node).bind (evt, f);
            + '&cllimit=' + (toResolve.length * 10) // Category limit is global, link limit is per page
    else if (node.attachEvent) node.attachEvent ('on' + evt, f);
            + '&format=json&titles=';
     else if (node.addEventListener) node.addEventListener (evt, f, capture);
    for (var i = 0; i < toResolve.length; i++) {
     else node['on' + evt] = f;
      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) {
          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 catLine      = null;
    var cats     = page.categories;
  var onUpload     = false;
    var lks     = page.links;
  var editors     = [];
    var is_dab   = false;
 
    var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
  var commitButton = null;
     if (!is_redir && cats && (HotCat.disambig_category || HotCat.redir_category)) {
  var commitForm   = null;
      for (var c = 0; c < cats.length; c++) {
  var multiSpan    = null;
        var cat = cats[c]['title'];
 
        // Strip namespace prefix
  var pageText    = null;
        if (cat) {
  var pageTime     = null;
          cat = cat.substring (cat.indexOf (':') + 1).replace(/_/g, ' ');
  var pageWatched  = false;
          if (cat == HotCat.disambig_category) {
  var watchCreate  = false;
            is_dab = true; break;
  var watchEdit    = false;
          } else if (cat == HotCat.redir_category) {
  var minorEdits  = false;
            is_redir = true; break;
  var editToken    = null;
          }
 
        }
  var is_rtl      = false;
       }
  var serverTime  = null;
    }
 
    if (!is_redir && !is_dab) return;
  var newDOM       = false; // true if MediaWiki serves the new UL-LI DOM for categories
     if (!lks || lks.length == 0) return;
 
     var titles = [];
  function setMultiInput () {
    for (var i = 0; i < lks.length; i++) {
     if (commitButton || onUpload) return;
      if (  lks[i]['ns'] == 14                            // Category namespace
     commitButton = make ('input');
          && lks[i]['title'] && lks[i]['title'].length > 0) // Name not empty
    commitButton.type  = 'button';
      {
    commitButton.value = HotCat.messages.commit;
        // Internal link to existing thingy. Extract the page name and remove the namespace.
    commitButton.onclick = multiSubmit;
        var match = lks[i]['title'];
     if (multiSpan) {
        titles.push (match.substring (match.indexOf (':') + 1));
       multiSpan.parentNode.replaceChild (commitButton, multiSpan);
        if (is_redir) break;
    } else {
      }
      catLine.appendChild (commitButton);
     }
    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) {
   function checkMultiInput () {
     if (!params || !params.query || !params.query.pages) return;    
     if (!commitButton) return;
    for (var p in params.query.pages) resolveOne (params.query.pages[p], toResolve);
     var has_changes = false;
  }
 
  function multiSubmit () {
     var toResolve = [];
     for (var i = 0; i < editors.length; i++) {
     for (var i = 0; i < editors.length; i++) {
       if (editors[i].state == CategoryEditor.CHANGE_PENDING || editors[i].state == CategoryEditor.OPEN)
       if (editors[i].state != CategoryEditor.UNCHANGED) {
         toResolve.push (editors[i]);
        has_changes = true;
         break;
      }
     }
     }
     if (toResolve.length == 0) {
     commitButton.disabled = !has_changes;
      performChanges ();
  }
      return;
 
    }
  function currentTimestamp () {
    resolveMulti (
    var now = new Date();
        toResolve
    var ts  = "" + now.getUTCFullYear();
      , function (resolved) {
    function two (s) { return s.substr (s.length - 2); }
          var firstDab = null;
    ts = ts
          var dontChange = false;
      + two ('0' + (now.getUTCMonth() + 1))
          for (var i = 0; i < resolved.length; i++) {
      + two ('0' + now.getUTCDate())
            if (resolved[i].lastInput != resolved[i].dabInput) {
      + two ('00' + now.getUTCHours())
              // We didn't disable all the open editors, but we did asynchronous calls. It is
      + two ('00' + now.getUTCMinutes())
              // theoretically possible that the user changed something...
      + two ('00' + now.getUTCSeconds());
              dontChange = true;
    return ts;
            } else {
  }
              if (resolved[i].dab) {
 
                if (!firstDab) firstDab = resolved[i];
  function initiateEdit (doEdit, failure) {
              } else {
    // Must use Ajax here to get the user options and the edit token.
                if (resolved[i].acceptCheck(true)) resolved[i].commit();
    getJSON ({
              }
      uri : wgServer + wgScriptPath + '/api.php'
            }
      ,data : 'format=json&action=query&titles=' + encodeURIComponent (wgPageName)
          }
        + '&prop=info%7Crevisions%7Clanglinks&inprop=watched&intoken=edit&rvprop=content%7Ctimestamp&lllimit=500'
          if (firstDab) {
        + '&rvlimit=1&rvstartid=' + wgCurRevisionId
            CategoryEditor.makeActive (firstDab);
        + '&meta=siteinfo%7Cuserinfo&uiprop=options'
          } else if (!dontChange) {
      ,success : function (json) { setPage(json); doEdit(failure); }
            performChanges ();
      ,error : function (req) { failure(req.status + ' ' + req.statusText); }
          }
    });
        }
    );
   }
   }


   var cat_prefix = null;
   function multiChangeMsg (count) {
  var noSuggestions = false;
    var msg = HotCat.messages.multi_change;
  var suggestionEngines = {
    if (typeof (msg) != 'string' && msg.length) {
    opensearch :
       if (window.mediaWiki && window.mediaWiki.language && window.mediaWiki.language.convertPlural) {
       { uri    : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1' // $1 = search term
        msg = window.mediaWiki.language.convertPlural (count, msg);
      ,handler : // Function to convert result of uri into an array of category names
      } else {
          function (responseText, queryKey) {
        msg = msg[msg.length-1];
            if (responseText.charAt (0) != '[') return null;
      }
            var queryResult = eval ('(' + responseText + ')');
    }
            if (   queryResult != null && queryResult.length == 2
    return substitute (msg, [null, "" + count]);   
                && queryResult[0].toLowerCase() == 'category:' + queryKey.toLowerCase()
  }
              )
 
            {
  function performChanges (failure, singleEditor) {
              var titles = queryResult[1];
    if (pageText === null) {
              if (!cat_prefix) cat_prefix = new RegExp ('^(' + HotCat.category_regexp + ':)');
      failure (HotCat.messages.multi_error);
              for (var i = 0; i < titles.length; i++) {
      return;
                cat_prefix.lastIndex = 0;
    }
                var m = cat_prefix.exec (titles[i]);
    // Backwards compatibility after message change (added $2 to cat_keychange)
                if (m && m.length > 1) {
    if (HotCat.messages.cat_keychange.indexOf ('$2') < 0) {
                  titles[i] = titles[i].substring (titles[i].indexOf (':') + 1); // rm namespace
      HotCat.messages.cat_keychange += '"$2"';
                } else {
    }
                  titles.splice (i, 1); // Nope, it's not a category after all.
    // Create a form and submit it. We don't use the edit API (api.php?action=edit) because
                  i--;
    // (a) sensibly reporting back errors like edit conflicts is always a hassle, and
                }
    // (b) we want to show a diff for multi-edits anyway.
              }
    // Using the form, we can do (b) and we get (a) for free. And, of course, using the form
              return titles;
    // 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.
            return null;    
    var action;
    if (singleEditor && !singleEditor.noCommit && !HotCat.no_autocommit && editToken) {
      commitForm.wpEditToken.value = editToken;
      action = commitForm.wpDiff;
      if (action) action.name = action.value = 'wpSave';
    } else {
      action = commitForm.wpSave;
      if (action) action.name = action.value = 'wpDiff';
    }
    var result = { text : pageText };
    var changed = [], added = [], deleted = [], changes = 0;
    var toEdit = !!singleEditor ? [singleEditor] : editors;
    var error = null;
    for (var i=0; i < toEdit.length; i++) {
      if (toEdit[i].state == CategoryEditor.CHANGED) {
        result = change_category (
            result.text
          , toEdit[i].originalCategory
          , toEdit[i].currentCategory
          , toEdit[i].currentKey
          , toEdit[i].currentHidden
        );
        if (!result.error) {
          changes++;
          if (!toEdit[i].originalCategory || toEdit[i].originalCategory.length === 0) {
            added.push (toEdit[i].currentCategory);
          } else {
            changed.push ({from : toEdit[i].originalCategory, to : toEdit[i].currentCategory});
           }
           }
        } else if (error === null) {
          error = result.error;
        }
      } else if (  toEdit[i].state == CategoryEditor.DELETED
                && toEdit[i].originalCategory
                && toEdit[i].originalCategory.length > 0)
      {
        result = change_category (result.text, toEdit[i].originalCategory, null, null, false);
        if (!result.error) {
          changes++;
          deleted.push (toEdit[i].originalCategory);
        } else if (error === null) {
          error = result.error;
        }
       }
       }
     ,internalsearch :
     }
      { uri    : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1'
    if (error !== null) { // Do not commit if there were errors
      ,handler :
      action = commitForm.wpSave;
          function (responseText, queryKey) {
      if (action) action.name = action.value = 'wpDiff';
            if (responseText.charAt (0) != '{') return null;
    }
            var queryResult = eval ('(' + responseText + ')');
    if (changes === 0 && !singleEditor) return;
            if (queryResult && queryResult.query && queryResult.query.allpages) {
    // Fill in the form and submit it
              var titles = queryResult.query.allpages;
    commitForm.wpAutoSummary.value = 'd41d8cd98f00b204e9800998ecf8427e'; // MD5 hash of the empty string
              var key    = queryKey.toLowerCase();
    commitForm.wpMinoredit.checked = minorEdits;
              for (var i = 0; i < titles.length; i++) {
    commitForm.wpWatchthis.checked = wgArticleId == 0 && watchCreate || watchEdit || pageWatched;
                titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
    if (wgArticleId > 0 || !!singleEditor) {
                if (titles[i].toLowerCase().indexOf (key) != 0) {
      if (changes == 1) {
                  titles.splice (i, 1); // Doesn't start with the query key
        if (result.summary && result.summary.length > 0)
                  i--;
          commitForm.wpSummary.value = HotCat.messages.prefix + result.summary.join (HotCat.messages.separator) + HotCat.messages.using;
                }
        commitForm.wpMinoredit.checked = HotCat.single_minor || minorEdits;
              }
      } else if (changes > 1) {
              return titles;
        var summary = [];
             }
        var shortSummary = [];
            return null;
        // 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 ('− ' + multiChangeMsg (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 ('+ ' + multiChangeMsg (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 + ']]');
           }
           }
      }
        }
    ,subcategories :
        if (changed.length == 1) {
      { uri    : '/api.php?format=json&action=query&list=categorymembers&cmnamespace=14&cmlimit=max&cmtitle=Category:$1'
           if (changed[0].from != changed[0].to) {
      ,handler :
             shortSummary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + arrow
           function (responseText, queryKey) {
                        + HotCat.category_canonical + ':' + changed[0].to + ']]');
             if (responseText.charAt (0) != '{') return null;
          } else {
             var queryResult = eval ('(' + responseText + ')');
             shortSummary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + ']]');
            if (queryResult && queryResult.query && queryResult.query.categorymembers) {
          }
              var titles = queryResult.query.categorymembers;
        } else if (changed.length > 1) {
              for (var i = 0; i < titles.length; i++) {
          shortSummary.push ('± ' + multiChangeMsg (changed.length));
                titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
        }
              }
        if (summary.length > 0) {
              return titles;
          summary = summary.join (HotCat.messages.separator);
             }
          if (summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length) {
            return null;
             summary = shortSummary.join (HotCat.messages.separator);
           }
           }
          commitForm.wpSummary.value = HotCat.messages.prefix + summary + HotCat.messages.using;
        }
       }
       }
  ,parentcategories :
    }
      { uri     : '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max'
     commitForm.wpTextbox1.value = result.text;
      ,handler :
    commitForm.wpStarttime.value = serverTime || currentTimestamp ();
          function (responseText, queryKey) {
    commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
            if (responseText.charAt (0) != '{') return null;
    // Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
            var queryResult = eval ('(' + responseText + ')');
    commitForm.hcCommit.click();
            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 = {
   function resolveMulti (toResolve, callback) {
    searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}, show: true, temp: false}
     for (var i = 0; i < toResolve.length; i++) {
  ,pagelist    : {name: 'Page list', engines: ['internalsearch'], cache: {}, show: true, temp: false}
       toResolve[i].dab = null;
  ,combined    : {name: 'Combined search', engines: ['opensearch', 'internalsearch'], cache: {}, show: true, temp: false}
      toResolve[i].dabInput = toResolve[i].lastInput;
  ,subcat      : {name: 'Subcategories', engines: ['subcategories'], cache: {}, show: true, temp: true}
    }
  ,parentcat  : {name: 'Parent categories', engines: ['parentcategories'], cache: {}, show: true, temp: true}
    if (noSuggestions) {
  };
      callback (toResolve);
 
      return;
   function CategoryEditor () { this.initialize.apply (this, arguments); };
  CategoryEditor.UNCHANGED      = 0;
  CategoryEditor.OPEN          = 1; // Open, but no input yet
  CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
  CategoryEditor.CHANGED        = 3;
  CategoryEditor.DELETED        = 4;
 
  CategoryEditor.makeActive = function (toActivate) {
     for (var i = 0; i < editors.length; i++) {
       if (editors[i] != toActivate) editors[i].inactivate ();
     }
     }
     toActivate.is_active = true;
     // Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
     if (toActivate.dab) {
    // category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
       toActivate.showSuggestions (toActivate.dab, false, null, null); // do autocompletion, no key, no engine selector
    var args = 'action=query&prop=info%7Clinks%7Ccategories%7Ccategoryinfo&plnamespace=14'
       toActivate.dab = null;
            + '&pllimit=' + (toResolve.length * 10)
            + '&cllimit=' + (toResolve.length * 10)
            + '&format=json&titles=';
     for (var i = 0; i < toResolve.length; i++) {
       args += encodeURIComponent ('Category:' + toResolve[i].dabInput);
       if (i+1 < toResolve.length) args += '%7C';
     }
     }
  };
    getJSON({
    
      uri : wgServer + wgScriptPath + '/api.php'
   CategoryEditor.prototype = {
    ,data : args
      
    ,success: function (json) { resolveRedirects (toResolve, json); callback (toResolve); }
     initialize : function (line, span, after, key) {
    ,error: function (req) { if (!req) noSuggestions = true; callback (toResolve); }
      // 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) {
   function resolveOne (page, toResolve) {
        this.isAddCategory = true;
    var cats    = page.categories;
        // Create add span and append to catLinks
    var lks      = page.links;
        this.originalCategory = "";
     var is_dab  = false;
        this.originalKey = null;
     var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
        this.originalExists  = false;
    var is_hidden = page.categoryinfo && typeof (page.categoryinfo.hidden) == 'string';
         span = make ('span');
    for (var j = 0; j < toResolve.length; j++) {
         span.className = 'noprint';
       if (toResolve[j].dabInput != page.title.substring (page.title.indexOf (':') + 1)) continue;
         if (key) {
      toResolve[j].currentHidden = is_hidden;
           span.appendChild (make (' | ', true));
    }
           if (after) {
    if (!is_redir && cats && (HotCat.disambig_category || HotCat.redir_category)) {
             after.parentNode.insertBefore (span, after.nextSibling);
      for (var c = 0; c < cats.length; c++) {
            after = after.nextSibling;
         var cat = cats[c]['title'];
           } else {
         // Strip namespace prefix
             line.appendChild (span);
         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;
           }
           }
        } else if (line.firstChild) {
          span.appendChild (make (' ', true));
          line.appendChild (span);
         }
         }
        this.linkSpan = make ('span');
      }
        this.linkSpan.className = 'noprint';
    }
        var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
    if (!is_redir && !is_dab) return;
        lk.appendChild (make (HotCat.links.add, true)); lk.title = HotCat.tooltips.add;     
    if (!lks || lks.length === 0) return;
         this.linkSpan.appendChild (lk);
    var titles = [];
         span = make ('span');
    for (var i = 0; i < lks.length; i++) {
         span.className = 'noprint';
      if (   lks[i]['ns'] == 14                            // Category namespace
         if (is_rtl) span.dir = 'rtl';
          && lks[i]['title'] && lks[i]['title'].length > 0) // Name not empty
        span.appendChild (this.linkSpan);
      {
        if (after)
         // Internal link to existing thingy. Extract the page name and remove the namespace.
          after.parentNode.insertBefore (span, after.nextSibling);
         var match = lks[i]['title'];
        else
         titles.push (match.substring (match.indexOf (':') + 1));
          line.appendChild (span);
         if (is_redir) break;
        this.normalLinks = null;
      }
        this.undelLink = null;
    }
         this.catLink = null;
    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 {
       } else {
         if (is_rtl) span.dir = 'rtl';
         toResolve[j].inputExists = true; // Might actually be wrong...
        this.isAddCategory = false;
         toResolve[j].icon.src = armorUri(HotCat.existsYes);
        this.catLink = span.firstChild;
         toResolve[j].text.value =
         this.originalCategory = after;
          titles[0] + (toResolve[j].currentKey != null ? '|' + toResolve[j].currentKey : "");
        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;
  function resolveRedirects (toResolve, params) {
      this.currentExists      = this.originalExists;
    if (!params || !params.query || !params.query.pages) return;
      this.currentKey        = this.originalKey;
     for (var p in params.query.pages) resolveOne (params.query.pages[p], toResolve);
      this.state              = CategoryEditor.UNCHANGED;
  }
      this.lastSavedState     = CategoryEditor.UNCHANGED;
 
      this.lastSavedCategory  = this.originalCategory;
  function multiSubmit () {
      this.lastSavedKey      = this.originalKey;
    var toResolve = [];
      this.lastSavedExists    = this.originalExists;
    for (var i = 0; i < editors.length; i++) {
      editors[editors.length] = this;
      if (editors[i].state == CategoryEditor.CHANGE_PENDING || editors[i].state == CategoryEditor.OPEN)
    },
         toResolve.push (editors[i]);
   
    }
    makeLinkSpan : function () {
    if (toResolve.length === 0) {
      this.normalLinks = make ('span');
      initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
      var lk = null;
      return;
      if (this.originalCategory && this.originalCategory.length > 0) {
    }
        lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.remove, this);
    resolveMulti (
        lk.appendChild (make (HotCat.links.remove, true)); lk.title = HotCat.tooltips.remove;
         toResolve
        this.normalLinks.appendChild (make (' ', true));
      , function (resolved) {
         this.normalLinks.appendChild (lk);
           var firstDab = null;
      }
           var dontChange = false;
      if (!HotCat.template_categories[this.originalCategory]) {
          for (var i = 0; i < resolved.length; i++) {
        lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
            if (resolved[i].lastInput != resolved[i].dabInput) {
        lk.appendChild (make (HotCat.links.change, true)); lk.title = HotCat.tooltips.change;
              // We didn't disable all the open editors, but we did asynchronous calls. It is
        this.normalLinks.appendChild (make (' ', true));
              // theoretically possible that the user changed something...
         this.normalLinks.appendChild (lk);
              dontChange = true;
        if (!noSuggestions && HotCat.use_up_down) {
            } else {
           this.upDownLinks = make ('span');
              if (resolved[i].dab) {
           lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.down, this);
                if (!firstDab) firstDab = resolved[i];
          lk.appendChild (make (HotCat.links.down, true)); lk.title = HotCat.tooltips.down;
              } else {
          this.upDownLinks.appendChild (make (' ', true));
                if (resolved[i].acceptCheck(true)) resolved[i].commit();
          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));
           if (firstDab) {
          this.upDownLinks.appendChild (lk);
            CategoryEditor.makeActive (firstDab);
           this.normalLinks.appendChild (this.upDownLinks);
           } else if (!dontChange) {
            initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
           }
         }
         }
      }
    );
      this.linkSpan = make ('span');
  }
      this.linkSpan.className = 'noprint';
 
      this.linkSpan.appendChild (this.normalLinks);
  var cat_prefix = null;
       this.undelLink = make ('span');
  var noSuggestions = false;
      this.undelLink.style.display = 'none';
  var suggestionEngines = {
      lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.restore, this);
    opensearch :
      lk.appendChild (make (HotCat.links.restore, true)); lk.title = HotCat.tooltips.restore;
       { uri    : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1' // $1 = search term
      this.undelLink.appendChild (make (' ', true));
      ,handler : // Function to convert result of uri into an array of category names
      this.undelLink.appendChild (lk);
          function (queryResult, queryKey) {
      this.linkSpan.appendChild (this.undelLink);
            if (  queryResult != null && queryResult.length == 2
    },
                && queryResult[0].toLowerCase() == 'category:' + queryKey.toLowerCase()
   
              )
    makeForm : function () {
             {
      var form = make ('form');
               var titles = queryResult[1];
      form.method = 'POST'; form.onsubmit = bind (this.accept, this);
               if (!cat_prefix) cat_prefix = new RegExp ('^(' + HotCat.category_regexp + ':)');
      this.form = form;
               for (var i = 0; i < titles.length; i++) {
     
                 cat_prefix.lastIndex = 0;
      var text = make ('input'); text.type = 'text'; text.size = HotCat.editbox_width;
                 var m = cat_prefix.exec (titles[i]);
      if (!noSuggestions) {
                 if (m && m.length > 1) {
        text.onkeyup =
                  titles[i] = titles[i].substring (titles[i].indexOf (':') + 1); // rm namespace
          bind (
                } else {
             function (evt) {
                   titles.splice (i, 1); // Nope, it's not a category after all.
              evt = evt || window.event || window.Event; // W3C, IE, Netscape
                  i--;
               var key = evt.keyCode || 0;
               if (key == 38 || key == 40) { // Up and down arrows
                // In case a browser doesn't generate keypress events for arrow keys...
                if (this.keyCount == 0) return this.processKey (evt);
               } else {
                if (key == 27) this.resetKeySelection (); // ESC
                 // 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 titles;
             }
             }
          ,this
            return null;
           );
           }
        text.onkeydown =
      }
           bind (
    ,internalsearch :
             function (evt) {
      { uri    : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1'
               evt = evt || window.event || window.Event; // W3C, IE, Netscape
      ,handler :
               this.lastKey = evt.keyCode || 0;
           function (queryResult, queryKey) {
               this.keyCount = 0;
             if (queryResult && queryResult.query && queryResult.query.allpages) {
              // Handle return explicitly, to override the default form submission to be able to check for ctrl
               var titles = queryResult.query.allpages;
              if (evt.keyCode == 13) this.accept (evt);
               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;
             }
             }
          ,this
            return null;
           );
           }
        // 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);
    ,subcategories :
       this.text = text;
       // I don't understand why they didn't map cmnamespace=14 automatically to cmtype=subcat,
        
       // which gives better results and is faster.
      this.icon = make ('img');
       { uri    : '/api.php?format=json&action=query&list=categorymembers'
     
                +(function (version) {
      var list = null;
                    var m = version.match(/^(\d+)\.(\d+)/);
      if (!noSuggestions) {
                    var major = 0, minor = 0;
        list = make ('select');
                    if (m && m.length > 1) {
        list.onclick    = bind (function () { if (this.setValueFromList ()) this.textchange (); }, this);
                      major = parseInt (m[1], 10);
        list.ondblclick = bind (function (e) { if (this.setValueFromList ()) this.accept (e); }, this);
                      minor = (m.length > 2 ? parseInt (m[2], 10) : 0);
        list.onchange = bind (function (e) { this.setValueFromList (); this.text.focus(); }, this);
                    }
        list.onkeyup =
                    if (major > 1 || major === 1 && minor > 17) return '&cmtype=subcat'; // Since MW1.18
          bind (
                    return '&cmnamespace=14';
            function (evt) {
                  }
              evt = evt || window.event || window.Event; // W3C, IE, Netscape
                  )(wgVersion)
              if (evt.keyCode == 27) {
                +'&cmlimit=max&cmtitle=Category:$1'
                this.resetKeySelection ();
      ,handler :
                this.text.focus();
          function (queryResult, queryKey) {
                var self = this;
            if (queryResult && queryResult.query && queryResult.query.categorymembers) {
                window.setTimeout (function () {self.textchange (true);}, HotCat.suggest_delay);
              var titles = queryResult.query.categorymembers;
               } else if (evt.keyCode == 13) {
               for (var i = 0; i < titles.length; i++) {
                 this.accept (evt);
                 titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
               }
               }
              return titles;
             }
             }
          ,this
            return null;
           );
          }
        if (!HotCat.fixed_search) {
      }
          var engineSelector = make ('select');
  ,parentcategories :
          for (var key in suggestionConfigs) {
      { uri    : '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max'
            if (suggestionConfigs[key].show) {
      ,handler :
              var opt = make ('option');
           function (queryResult, queryKey) {
              opt.value = key;
            if (queryResult && queryResult.query && queryResult.query.pages) {
              if (key == this.engine) opt.selected = true;
              for (var p in queryResult.query.pages) {
              opt.appendChild (make (suggestionConfigs[key].name, true));
                if (queryResult.query.pages[p].categories) {
               engineSelector.appendChild (opt);
                  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;
           }
           }
          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;   
       }
       }
  };
  var suggestionConfigs = {
    searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}, show: true, temp: false, noCompletion : false}
  ,pagelist    : {name: 'Page list', engines: ['internalsearch'], 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}
  };
  function CategoryEditor () { this.initialize.apply (this, arguments); };
  CategoryEditor.UNCHANGED      = 0;
  CategoryEditor.OPEN          = 1; // Open, but no input yet
  CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
  CategoryEditor.CHANGED        = 3;
  CategoryEditor.DELETED        = 4;


      // Do not use type 'submit'; we cannot detect modifier keys if we do 
  // IE6 sometimes forgets to redraw the list when editors are opened or closed.
      var OK = make ('input'); OK.type = 'button';
  // Adding/removing a dummy element helps, at least when opening editors.
      OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
  CategoryEditor.dummyElement  = make ('\xa0', true);
      OK.onclick = bind (this.accept, this);
 
      this.ok = OK;
  CategoryEditor.forceRedraw = function () {
       
    if (!is_ie6) return;
      var cancel = make ('input'); cancel.type = 'button';
    if (CategoryEditor.dummyElement.parentNode) {
      cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
       document.body.removeChild (CategoryEditor.dummyElement);
      cancel.onclick = bind (this.cancel, this);
    } else {
      this.cancelButton = cancel;
       document.body.appendChild (CategoryEditor.dummyElement);
       
     }
      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) {
  CategoryEditor.makeActive = function (toActivate) {
       if (this.isAddCategory && !onUpload) {
    if (toActivate.is_active) return;
         var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
     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;
    } else {
      if (toActivate.showsList) toActivate.displayList();
      if (toActivate.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 () { toActivate.setSelection (toActivate.lastSelection.start, toActivate.lastSelection.end); }
            ,1
          );
        } else {
          toActivate.setSelection (toActivate.lastSelection.start, toActivate.lastSelection.end);
        }
       }
       }
       if (!commitButton && !onUpload) {
    }
         for (var i = 0; i < editors.length; i++) {
  };
          if (editors[i].state != CategoryEditor.UNCHANGED) {
 
             setMultiInput();
  CategoryEditor.prototype = {
             break;
 
    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');
      if (!this.form) {
         this.linkSpan.className = 'noprint nopopups hotcatlink';
         this.makeForm ();
        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;
      if (this.list) this.list.style.display = 'none';
        this.linkSpan.appendChild (lk);
      if (this.engineSelector) this.engineSelector.style.display = 'none';
        span = make (newDOM ? 'li' : 'span');
      this.currentCategroy = this.lastSavedCategory;
        span.className = 'noprint';
       this.currentExists  = this.lastSavedExists;
        if (is_rtl) span.dir = 'rtl';
      this.currentKey      = this.lastSavedKey;
        span.appendChild (this.linkSpan);
      this.icon.src = this.currentExists ? HotCat.existsYes : HotCat.existsNo;
        if (after)
      this.text.value = this.currentCategory + (this.currentKey != null ? '|' + this.currentKey : "");
          after.parentNode.insertBefore (span, after.nextSibling);
       this.originalState = this.state;
        else
       this.lastInput    = this.currentCategory;
          line.appendChild (span);
       this.inputExists  = this.currentExists;
        this.normalLinks = null;
       this.state         = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
        this.undelLink = null;
       // Display the form
        this.catLink = null;
      if (this.catLink) this.catLink.style.display = 'none';
       } else {
       this.linkSpan.style.display = 'none';
        if (is_rtl) span.dir = 'rtl';
       this.form.style.display = 'inline';
        this.isAddCategory = false;
       CategoryEditor.makeActive (this);
        this.catLink = span.firstChild;
       // Kill the event before focussing, otherwise IE will kill the onfocus event!
        this.originalCategory = after;
      var result = evtKill (evt);
        this.originalKey = (key && key.length > 1) ? key.substr(1) : null; // > 1 because it includes the leading bar
      this.text.focus();
        this.originalExists  = !hasClass (this.catLink, 'new');
      this.text.readOnly = false;
        // Create change and del links
       checkMultiInput ();
        this.makeLinkSpan ();
       return result;
        if (!this.originalExists && this.upDownLinks) this.upDownLinks.style.display = 'none';
        span.appendChild (this.linkSpan);
      }
       this.originalHidden    = is_hidden;
      this.line              = line;
      this.engine            = HotCat.suggestions;
       this.span              = span;
      this.currentCategory   = this.originalCategory;
      this.currentExists      = this.originalExists;
       this.currentHidden      = this.originalHidden;
       this.currentKey         = this.originalKey;
      this.state             = CategoryEditor.UNCHANGED;
      this.lastSavedState    = CategoryEditor.UNCHANGED;
       this.lastSavedCategory  = this.originalCategory;
       this.lastSavedKey      = this.originalKey;
       this.lastSavedExists    = this.originalExists;
       this.lastSavedHidden    = this.originalHidden;
       if (this.catLink && this.currentKey) {
        this.catLink.title = this.currentKey;
       }
       editors[editors.length] = this;
     },
     },


     open : function (evt) {
     makeLinkSpan : function () {
       var result = this.display (evt);
       this.normalLinks = make ('span');
       var v = this.lastSavedCategory;
       var lk = null;
      if (v.length == 0) return result;
       if (this.originalCategory && this.originalCategory.length > 0) {
 
        lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.remove, this);
       if (this.engine && suggestionConfigs[this.engine].temp) this.engine = HotCat.suggestions;
        lk.appendChild (make (HotCat.links.remove, true)); lk.title = HotCat.tooltips.remove;
      this.textchange (false, true); // do autocompletion, force display of suggestions
        this.normalLinks.appendChild (make (' ', true));
      return result;
        this.normalLinks.appendChild (lk);
    },
 
    down : function (evt) {
      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!
      this.engine = 'subcat';
      this.textchange (false, true);
 
      return result;
    },
 
    up : function (evt) {
      var result = this.display (evt);
      var v = this.lastSavedCategory;
      if (v.length == 0) return result;
 
      this.engine = 'parentcat';
      this.textchange (false, true);
 
      return result;
    },
 
    cancel : function () {
      if (this.isAddCategory && !onUpload) {
        this.removeEditor(); // We added a new adder when opening
        return;
       }
       }
      // Close, re-display link
       if (!HotCat.template_categories[this.originalCategory]) {
       if (this.list) this.list.style.display = 'none';
        lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
      if (this.engineSelector) this.engineSelector.style.display = 'none';
        lk.appendChild (make (HotCat.links.change, true)); lk.title = HotCat.tooltips.change;
      this.form.style.display = 'none';
        this.normalLinks.appendChild (make (' ', true));
      if (this.catLink) this.catLink.style.display = "";
        this.normalLinks.appendChild (lk);
      this.linkSpan.style.display = "";
        if (!noSuggestions && HotCat.use_up_down) {
      this.state = this.originalState;
          this.upDownLinks = make ('span');
      this.currentCategory = this.lastSavedCategory;
          lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.down, this);
      this.currentKey      = this.lastSavedKey;
          lk.appendChild (make (HotCat.links.down, true)); lk.title = HotCat.tooltips.down;
      this.currentExists  = this.lastSavedExists;
          this.upDownLinks.appendChild (make (' ', true));
      if (this.state == CategoryEditor.UNCHANGED) {
          this.upDownLinks.appendChild (lk);
        if (this.catLink) this.catLink.style.backgroundColor = 'transparent';
          lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.up, this);
      } else {
          lk.appendChild (make (HotCat.links.up, true)); lk.title = HotCat.tooltips.up;
        if (!onUpload) {
          this.upDownLinks.appendChild (make (' ', true));
           try {
           this.upDownLinks.appendChild (lk);
            this.catLink.style.backgroundColor = HotCat.bg_changed;
           this.normalLinks.appendChild (this.upDownLinks);
           } catch (ex) {}
         }
         }
       }
       }
       checkMultiInput ();
       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 = 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);
     },
     },


     removeEditor : function () {
     makeForm : function () {
       var next = this.span.nextSibling;
       var form = make ('form');
      if (next) next.parentNode.removeChild (next);
       form.method = 'POST'; form.onsubmit = bind (this.accept, this);
       this.span.parentNode.removeChild (this.span);
       this.form = form;
      for (var i = 0; i < editors.length; i++) {
        if (editors[i] == this) {
          editors.splice (i, 1);
          break;
        }
       }
      checkMultiInput ();
      var self = this;
      window.setTimeout (function () {delete self;}, 10);
    },


    rollback : function (evt) {
       var text = make ('input'); text.type = 'text'; text.size = HotCat.editbox_width;
       this.undoLink.parentNode.removeChild (this.undoLink);
       if (!noSuggestions) {
      this.undoLink = null;
        text.onkeyup =
      this.currentCategory = this.originalCategory;
          bind (
       this.currentKey = this.originalKey;
            function (evt) {
      this.currentExists = this.originalExists;
              evt = evt || window.event || window.Event; // W3C, IE, Netscape
      this.lastSavedCategory = this.originalCategory;
              var key = evt.keyCode || 0;
      this.lastSavedKey = this.originalKey;
              if (key === 38 || key === 40 || key === 33 || key === 34) { // Up and down arrows, page up/down
      this.lastSavedExists = this.originalExists;
                // In case a browser doesn't generate keypress events for arrow keys...
      this.state = CategoryEditor.UNCHANGED;
                if (this.keyCount === 0) return this.processKey (evt);
      if (!this.currentCategory || this.currentCategory.length == 0) {
              } else {
        // It was a newly added category. Remove the whole editor.
                if (key == 27) { // ESC
        this.removeEditor();
                  if (!this.resetKeySelection ()) {
      } else {
                    // No undo of key selection: treat ESC as "cancel".
        // Redisplay the link...
                    this.cancel ();
        this.catLink.removeChild (this.catLink.firstChild);
                    return;
        this.catLink.appendChild (make (this.currentCategory, true));
                  }
         this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
                }
        this.catLink.title = "";
                // Also do this for ESC as a workaround for Firefox bug 524360
        this.catLink.className = this.currentExists ? "" : 'new';
                // https://bugzilla.mozilla.org/show_bug.cgi?id=524360
        this.catLink.style.backgroundColor = 'transparent';
                var dont_autocomplete = (key == 8 || key == 46 || key == 27); // BS, DEL, ESC
        if (this.upDownLinks) this.upDownLinks.style.display = this.currentExists ? "" : 'none';
                if (this.engine && suggestionConfigs[this.engine] && suggestionConfigs[this.engine].temp && !dont_autocomplete) {
        checkMultiInput ();
                  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) return 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);
       }
       }
       return evtKill (evt);
       this.text = text;
    },


    inactivate : function () {
       this.icon = make ('img');
       if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.is_active = false;
    },


    acceptCheck : function (dontCheck) {
      var list = null;
       this.sanitizeInput ();
       if (!noSuggestions) {
      var value = this.text.value.split('|');
        list = make ('select');
      var key  = null;
        list.onclick    = bind ( function (e) { if (this.highlightSuggestion (0)) this.textchange (false, true); }, this);
      if (value.length > 1) key = value[1];
        list.ondblclick = bind (function (e) { if (this.highlightSuggestion (0)) this.accept (e); }, this);
      var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
        list.onchange = bind (function (e) { this.highlightSuggestion (0); this.text.focus(); }, this);
      if (HotCat.capitalizePageNames) v = capitalize (v);
         list.onkeyup =
      this.lastInput = v;
          bind (
      if (v.length == 0) {
            function (evt) {
        this.cancel ();
              evt = evt || window.event || window.Event; // W3C, IE, Netscape
         return false;
               if (evt.keyCode == 27) {
      }
                this.resetKeySelection ();
      if (   !dontCheck
                this.text.focus();
          && (  v == this.lastSavedCategory && key == this.lastSavedKey
                var self = this;
               || wgNamespaceNumber == 14 && v == wgTitle
                window.setTimeout (function () {self.textchange (true);}, HotCat.suggest_delay);
            )
              } else if (evt.keyCode == 13) {
        )
                this.accept (evt);
      {
              }
        this.cancel ();
            }
        return false;
          ,this
      }
          );
      this.currentCategory = v;
        if (!HotCat.fixed_search) {
      this.currentKey = key;
          var engineSelector = make ('select');
      this.currentExists = this.inputExists;
           for (var key in suggestionConfigs) {
      return true;
            if (suggestionConfigs[key].show) {
    },
              var opt = make ('option');
   
               opt.value = key;
    accept : function (evt) {
              if (key == this.engine) opt.selected = true;
      this.noCommit = (evtKeys (evt) & 1) != 0;
              opt.appendChild (make (suggestionConfigs[key].name, true));
      var result = evtKill (evt);
              engineSelector.appendChild (opt);
      if (this.acceptCheck ()) {
            }
        var toResolve = [this];
          }
        var original  = this.currentCategory;
          engineSelector.onchange = bind (
        resolveMulti (
            function () {
            toResolve
              this.engine = this.engineSelector.options[this.engineSelector.selectedIndex].value;
           , function (resolved) {
              this.text.focus();
              if (resolved[0].dab) {
              this.textchange (true, true); // Don't autocomplete, force re-display of list
                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
                  );
                }
              }
             }
             }
        );
          ,this
          );
          this.engineSelector = engineSelector;
        }
       }
       }
       return result;
       this.list = list;
    },


    close : function () {
      function button_label (id, defaultText) {
      if (!this.catLink) {
        var label = null;
        // Create a catLink
        if (   onUpload
        this.catLink = make ('a');
            && typeof (UFUI) != 'undefined'
        this.catLink.appendChild (make ('foo', true));
            && typeof (UIElements) != 'undefined'
        this.catLink.style.display = 'none';
            && typeof (UFUI.getLabel) == 'function') {
         this.span.insertBefore (this.catLink, this.span.firstChild.nextSibling);
          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;
       }
       }
       this.catLink.removeChild (this.catLink.firstChild);
 
       this.catLink.appendChild (make (this.currentCategory, true));
       // Do not use type 'submit'; we cannot detect modifier keys if we do
       this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
      var OK = make ('input'); OK.type = 'button';
       this.catLink.title = "";
      OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
       this.catLink.className = this.currentExists ? "" : 'new';
       OK.onclick = bind (this.accept, this);
       this.lastSavedCategory = this.currentCategory;
       this.ok = OK;
       this.lastSavedKey      = this.currentKey;
 
       this.lastSavedExists  = this.currentExists;
      var cancel = make ('input'); cancel.type = 'button';
       // Close form and redisplay category
      cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
       if (this.list) this.list.style.display = 'none';
      cancel.onclick = bind (this.cancel, this);
       if (this.engineSelector) this.engineSelector.style.display = 'none';
       this.cancelButton = cancel;
       this.form.style.display = 'none';
 
       this.catLink.style.display = "";
      var span = make ('span');
       if (this.isAddCategory) {
       span.className = 'hotcatinput';
        if (onUpload) {
       span.style.position = 'relative';
          var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
       // 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
        this.isAddCategory = false;
       // moving the form to the front of the next line.
        this.linkSpan.parentNode.removeChild (this.linkSpan);
      span.appendChild (text);
        this.makeLinkSpan ();
 
        this.span.appendChild (this.linkSpan);
       // 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);
      addEvent (text, 'focus', bind (function () { CategoryEditor.makeActive (this); }, this));
       // 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.
       addEvent (text, (typeof text.onbeforedeactivate != 'undefined' && text.createTextRange) ? 'beforedeactivate' : 'blur', bind (this.saveView, this));
    },
 
    display : function (evt) {
      if (this.isAddCategory && !onUpload) {
        var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
       }
       }
       if (!this.undoLink) {
       if (!commitButton && !onUpload) {
         // Append an undo link.
         for (var i = 0; i < editors.length; i++) {
        var span = make ('span');
          if (editors[i].state != CategoryEditor.UNCHANGED) {
        var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.rollback, this);
            setMultiInput();
        lk.appendChild (make (HotCat.links.undo, true)); lk.title = HotCat.tooltips.undo;
             break;
        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';
       if (!this.form) {
       this.linkSpan.style.display = "";
        this.makeForm ();
       this.state = CategoryEditor.CHANGED;
      }
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.currentCategory = this.lastSavedCategory;
      this.currentExists  = this.lastSavedExists;
      this.currentHidden  = this.lastSavedHidden;
      this.currentKey      = this.lastSavedKey;
      this.icon.src = armorUri(this.currentExists ? HotCat.existsYes : HotCat.existsNo);
      this.text.value = this.currentCategory + (this.currentKey != null ? '|' + this.currentKey : "");
      this.originalState = this.state;
      this.lastInput    = this.currentCategory;
      this.inputExists  = this.currentExists;
      this.state        = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
      this.lastSelection = {start: this.currentCategory.length, end: this.currentCategory.length};
      this.showsList = false;
      // Display the form
      if (this.catLink) this.catLink.style.display = 'none';
       this.linkSpan.style.display = 'none';
      this.form.style.display = 'inline';
       this.ok.disabled = false;
      // Kill the event before focussing, otherwise IE will kill the onfocus event!
      var result = evtKill (evt);
      CategoryEditor.makeActive (this);
      this.text.focus();
      this.text.readOnly = false;
       checkMultiInput ();
       checkMultiInput ();
      return result;
     },
     },
   
 
     commit : function (comment) {
     show : function (evt, engine, readOnly) {
       // Check again to catch problem cases after redirect resolution
       var result = this.display (evt);
      if (  (  this.currentCategory == this.originalCategory
      var v = this.lastSavedCategory;
              && (this.currentKey == this.originalKey
      if (v.length === 0) return result;
                  || this.currentKey === null && this.originalKey.length == 0
      this.text.readOnly = !!readOnly;
                )
      this.engine = engine;
            )
      this.textchange (false, true); // do autocompletion, force display of suggestions
          || wgNamespaceNumber == 14 && this.currentCategory == wgTitle
       CategoryEditor.forceRedraw ();
        )
      return result;
       {
    },
        this.cancel ();
        return;
    open : function (evt) {
      }
      return this.show (evt, (this.engine && suggestionConfigs[this.engine].temp) ? HotCat.suggestions : this.engine);
      if (commitButton || onUpload) {
    },
        this.close ();
 
      } else {
    down : function (evt) {
        if (this.list) this.list.style.display = 'none';
      return this.show (evt, 'subcat', true);
        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 = url + '&hotcat_comment=' + encodeURIComponent (comment);
        if (this.noCommit || HotCat.no_autocommit) url = url + '&hotcat_nocommit=1';
        document.location = url;
      }
     },
     },
   
 
     remove : function (evt) {
     up : function (evt) {
       this.doRemove (evtKeys (evt) & 1);
       return this.show (evt, 'parentcat');
      return evtKill (evt);
     },
     },
   
 
     doRemove : function (noCommit) {
     cancel : function () {
       if (this.isAddCategory) { // Empty input on adding a new category
       if (this.isAddCategory && !onUpload) {
         this.cancel ();
         this.removeEditor(); // We added a new adder when opening
         return;
         return;
       }
       }
       if (!commitButton && !onUpload) {
      // Close, re-display link
        for (var i = 0; i < editors.length; i++) {
      this.inactivate();
          if (editors[i].state != CategoryEditor.UNCHANGED) {
      this.form.style.display = 'none';
            setMultiInput();
       if (this.catLink) this.catLink.style.display = "";
            break;
      this.linkSpan.style.display = "";
           }
      this.state = this.originalState;
      this.currentCategory = this.lastSavedCategory;
      this.currentKey      = this.lastSavedKey;
      this.currentExists  = this.lastSavedExists;
      this.currentHidden  = this.lastSavedHidden;
      if (this.catLink) {
        if (this.currentkey && this.currentKey.length > 0) {
          this.catLink.title = this.currentKey;
        } else {
           this.catLink.title = null;
         }
         }
       }
       }
       if (commitButton) {
       if (this.state == CategoryEditor.UNCHANGED) {
        this.catLink.style.textDecoration = 'line-through';
         if (this.catLink) this.catLink.style.backgroundColor = 'transparent';
        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 {
       } else {
         if (onUpload) {
         if (!onUpload) {
           // Remove this editor completely
           try {
          this.removeEditor ();
            this.catLink.style.backgroundColor = HotCat.bg_changed;
        } else {
           } catch (ex) {}
          // Execute single category deletion.
          var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
                    + '&action=edit';
          if (noCommit || HotCat.no_autocommit) editlk += '&hotcat_nocommit=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 ();
       checkMultiInput ();
       return evtKill (evt);
       CategoryEditor.forceRedraw ();
     },
     },
   
 
    // Internal operations
     removeEditor : function () {
   
       if (!newDOM) {
     setValueFromList : function (idx) {
         var next = this.span.nextSibling;
       if (typeof (idx) == 'undefined') idx = this.list.selectedIndex;
         if (next) next.parentNode.removeChild (next);
      if (idx >= 0 && idx < this.list.options.length) {
         var v = this.text.value.split ('|');
         this.text.value = this.list.options[idx].text + (v.length > 1 ? '|' + v[1] : "");
        this.inputExists = true; // Might be wrong if from a dab list...
        if (this.icon) this.icon.src = HotCat.existsYes;
        return true;
       }
       }
       return false;
       this.span.parentNode.removeChild (this.span);
    },
       for (var i = 0; i < editors.length; i++) {
   
         if (editors[i] == this) {
    selectEngine : function (engineName) {
          editors.splice (i, 1);
      if (!this.engineSelector) return;
          break;
       for (var i = 0; i < this.engineSelector.options.length; i++) {
        }
         this.engineSelector.options[i].selected = this.engineSelector.options[i].value == engineName;
       }
       }
      checkMultiInput ();
      var self = this;
      window.setTimeout (function () {delete self;}, 10);
     },
     },


     sanitizeInput : function () {
     rollback : function (evt) {
       var v = this.text.value || "";
       this.undoLink.parentNode.removeChild (this.undoLink);
       v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
      this.undoLink = null;
      var re = new RegExp ('^(' + HotCat.category_regexp + '):');
      this.currentCategory = this.originalCategory;
      if (re.test (v)) v = v.substring (v.indexOf (':') + 1);
      this.currentKey = this.originalKey;
      if (HotCat.capitalizePageNames) v = capitalize (v);
      this.currentExists = this.originalExists;
       // Only update the input field if there is a difference. IE8 appears to reset the selection
      this.currentHidden = this.originalHidden;
       // and place the cursor at the front upon reset, which makes our autocompletetion become a
      this.lastSavedCategory = this.originalCategory;
       // nuisance. FF and IE6 don't seem to have this problem.
      this.lastSavedKey = this.originalKey;
       if (this.text.value != null && this.text.value != v)
      this.lastSavedExists = this.originalExists;
        this.text.value = v;
      this.lastSavedHidden = this.originalHidden;
       this.state = CategoryEditor.UNCHANGED;
      if (!this.currentCategory || this.currentCategory.length === 0) {
        // It was a newly added category. Remove the whole editor.
        this.removeEditor();
      } else {
        // Redisplay the link...
        this.catLink.removeChild (this.catLink.firstChild);
        this.catLink.appendChild (make (this.currentCategory, true));
        this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
        this.catLink.title = this.currentKey;
        this.catLink.className = this.currentExists ? "" : 'new';
        this.catLink.style.backgroundColor = 'transparent';
        if (this.upDownLinks) this.upDownLinks.style.display = this.currentExists ? "" : 'none';
        checkMultiInput ();
       }
       return evtKill (evt);
    },
 
    inactivate : function () {
       if (this.list) this.list.style.display = 'none';
       if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.is_active = false;
     },
     },


     makeCall : function (url, callbackObj, engine, queryKey) {
     acceptCheck : function (dontCheck) {
       var cb = callbackObj;
       this.sanitizeInput ();
       var = engine;
       var value = this.text.value.split('|');
       var = queryKey;
       var key  = null;
       var r  = sajax_init_object ();
       if (value.length > 1) key = value[1];
       cb.requests.push (r);
       var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
      r.open('GET', url, true);
       if (HotCat.capitalizePageNames) v = capitalize (v);
       r.onreadystatechange =
      this.lastInput = v;
        bind (
      if (v.length === 0) {
          function () {
        this.cancel ();
            if (r.readyState == 4) {
        return false;
              if (r.status != 200) cb.dontCache = true;
      }
              if (r.status == 200 && r.responseText != null) {
      if (!dontCheck
                var titles = e.handler (r.responseText, v);
          && (  wgNamespaceNumber == 14 && v == wgTitle
                if (titles && titles.length > 0) {
               || HotCat.blacklist != null && HotCat.blacklist.test(v))
                  if (cb.allTitles == null) {
        ) {
                    cb.allTitles = titles;
        this.cancel ();
                  } else {
        return false;
                    cb.allTitles = cb.allTitles.concat (titles);
      }
                  }
      this.currentCategory = v;
                }
       this.currentKey = key;
              }
       this.currentExists = this.inputExists;
              cb.callsMade++;
       return true;
            }
            if (cb.callsMade == cb.nofCalls) {
              if (!cb.dontCache && !suggestionConfigs[cb.engineName].cache[v]) {
                suggestionConfigs[cb.engineName].cache[v] = cb.allTitles;
              }
               this.text.readOnly = false;
              if (!cb.cancelled) this.showSuggestions (cb.allTitles, cb.noCompletion, v, cb.engineName);
              if (cb === this.callbackObj) this.callbackObj = null;
              delete cb;
            }
          }
        ,this
        );
       r.setRequestHeader ('Pragma', 'cache=yes');
       r.setRequestHeader ('Cache-Control', 'no-transform');
       r.send (null);
     },
     },


     callbackObj : null,
     accept : function (evt) {
 
       this.noCommit = (evtKeys (evt) & 1) != 0;
    textchange : function (dont_autocomplete, force) {
       var result = evtKill (evt);
       // Hide all other lists
       if (this.acceptCheck ()) {
       CategoryEditor.makeActive (this);
         var toResolve = [this];
       if (noSuggestions) {
        var original  = this.currentCategory;
         // No Ajax: just make sure the list is hidden
         resolveMulti (
         if (this.list) this.list.style.display = 'none';
            toResolve
        if (this.engineSelector) this.engineSelector.style.display = 'none';
          , function (resolved) {
        if (this.icon) this.icon.style.display = 'none';
              if (resolved[0].dab) {
         return;
                CategoryEditor.makeActive (resolved[0]);
              } else {
                if (resolved[0].acceptCheck(true)) {
                  resolved[0].commit (
                    (resolved[0].currentCategory != original)
                      ? HotCat.messages.cat_resolved.replace (/\$1/g, original)
                      : null
                  );
                }
              }
            }
         );
       }
       }
        
       return result;
      // 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';


       if (suggestionConfigs[engineName].cache[v]) {
    close : function () {
         this.showSuggestions (suggestionConfigs[engineName].cache[v], dont_autocomplete, v, engineName);
       if (!this.catLink) {
         return;
        // 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);
       var engines = suggestionConfigs[engineName].engines;
       this.catLink.appendChild (make (this.currentCategory, true));
       this.callbackObj =
       this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
        {allTitles: null, requests: [], callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engineName};  
      this.catLink.title = "";
       for (var j = 0; j < engines.length; j++) {
      this.catLink.className = this.currentExists ? "" : 'new';
        engine = suggestionEngines[engines[j]];
      this.lastSavedCategory = this.currentCategory;
        var url = wgServer + wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (v));
       this.lastSavedKey      = this.currentKey;
        this.makeCall (url, this.callbackObj, engine, v);
       this.lastSavedExists  = this.currentExists;
      }   
       this.lastSavedHidden  = this.currentHidden;
    },
       // Close form and redisplay category
   
      this.inactivate();
    showSuggestions : function (titles, dontAutocomplete, queryKey, engineName) {
      this.form.style.display = 'none';
       this.text.readOnly = false;
      this.catLink.title = this.currentKey;
       this.dab = null;
      this.catLink.style.display = "";
       if (!this.list) return;
      if (this.isAddCategory) {
       if (noSuggestions) {
        if (onUpload) {
        if (this.list) this.list.style.display = 'none';
          var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        }
        if (this.icon) this.icon.style.display = 'none';
        this.isAddCategory = false;
         this.inputExists = true; // Default...
         this.linkSpan.parentNode.removeChild (this.linkSpan);
         return;
        this.makeLinkSpan ();
         this.span.appendChild (this.linkSpan);
       }
       }
       var haveEngine = !!engineName;
       if (!this.undoLink) {
      if (haveEngine) {
        // Append an undo link.
         haveEngine = this.engineSelector != null;
        var span = make ('span');
      } else {
        var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.rollback, this);
         if (this.engineSelector) this.engineSelector.style.display = 'none';
         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 (queryKey) {
       if (this.upDownLinks) this.upDownLinks.style.display = this.lastSavedExists ? "" : 'none';
        if (this.lastInput.indexOf (queryKey) != 0) return;
      this.linkSpan.style.display = "";
        if (this.lastQuery && this.lastInput.indexOf (this.lastQuery) == 0 && this.lastQuery.length > queryKey.length)
       this.state = CategoryEditor.CHANGED;
          return;
       checkMultiInput ();
      }
       CategoryEditor.forceRedraw ();
       this.lastQuery = queryKey;
    },
        
      // Get current input text
      var v = this.text.value.split('|');
       var key = v.length > 1 ? '|' + v[1] : "";
      v = capitalize (v[0]);


      if (titles) {
    commit : function (comment) {
        var vLow = v.toLowerCase ();
      // Check again to catch problem cases after redirect resolution
        titles.sort (
      if (   (  this.currentCategory == this.originalCategory
          function (a, b) {
              && (this.currentKey == this.originalKey
            if (a.indexOf (b) == 0) return 1; // a begins with b: a > b
                  || this.currentKey === null && this.originalKey.length === 0
            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
             )
             )
           {
           || wgNamespaceNumber == 14 && this.currentCategory == wgTitle
            titles.splice (i, 1);
           || HotCat.blacklist != null && HotCat.blacklist.test (this.currentCategory)
            i--;
        )
           }
      {
        }
        this.cancel ();
      }
      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;
         return;
       }
       }
                   
       if (commitButton || onUpload) {
       var firstTitle = titles[0];
        this.close ();
      var completed = this.autoComplete (firstTitle, v, key, dontAutocomplete);
       } else {
       if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
         this.close ();
         this.icon.src = completed ? HotCat.existsYes : HotCat.existsNo;
        var self = this;
         this.inputExists = completed;
         initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {alert (msg);});
       }
       }
       if (completed) {
    },
         this.lastInput = firstTitle;
 
         if (titles.length == 1) {
    remove : function (evt) {
           this.list.style.display = 'none';
      this.doRemove (evtKeys (evt) & 1);
          if (this.engineSelector) this.engineSelector.style.display = 'none';
      return evtKill (evt);
           return;
    },
 
    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 (!this.is_active) {
       if (commitButton) {
         this.list.style.display = 'none';
         this.catLink.title = "";
         if (this.engineSelector) this.engineSelector.style.display = 'none';
         this.catLink.style.cssText += '; text-decoration : line-through !important;';
        return;
        try {
      }
          this.catLink.style.backgroundColor = HotCat.bg_changed;
      var nofItems = (titles.length > 5 ? 5 : titles.length);
        } catch (ex) {}
      if (nofItems <= 1) nofItems = 2;
         this.originalState = this.state;
      this.list.size = nofItems;
         this.state = CategoryEditor.DELETED;
      this.list.style.align    = 'left';
         this.normalLinks.style.display = 'none';
      this.list.style.zIndex  = 5;
         this.undelLink.style.display = "";
      this.list.style.position = 'absolute';
         checkMultiInput ();
      // 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 {
       } else {
         listh = this.list.offsetHeight;
         if (onUpload) {
          // Remove this editor completely
          this.removeEditor ();
        } else {
          this.originalState = this.state;
          this.state = CategoryEditor.DELETED;
          this.noCommit = noCommit;
          var self = this;
          initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {self.state = self.originalState; alert (msg);});
        }
       }
       }
      // Approximate calculation of maximum list size
    },
      var maxListHeight = listh;
      if (nofItems < 5) maxListHeight = (listh / nofItems) * 5;


      function scroll_offset (what) {
    restore : function (evt) {
        var s = 'scroll' + what;
       // Can occur only if we do have a commit button and are not on the upload form
        return (document.documentElement ? document.documentElement[s] : 0)
       this.catLink.title = this.currentKey;
              || document.body[s] || 0;
      this.catLink.style.textDecoration = "";
      
       this.state = this.originalState;
       function viewport (what) {
      if (this.state == CategoryEditor.UNCHANGED) {
        if (typeof (is_safari) != 'undefined' && is_safari && !document.evaluate)
        this.catLink.style.backgroundColor = 'transparent';
          return window['inner' + what];
      } else {
        var s = 'client' + what;
         try {
        if (typeof (is_opera) != 'undefined' && is_opera) return document.body[s];
           this.catLink.style.backgroundColor = HotCat.bg_changed;
        return (document.documentElement ? document.documentElement[s] : 0)
         } catch (ex) {}
              || 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};
       }
       }
      this.normalLinks.style.display = "";
      this.undelLink.style.display = 'none';
      checkMultiInput ();
      return evtKill (evt);
    },


      // IE6 seems to report in this.text.offsetTop and this.text.offsetLeft global offsets??
    // Internal operations
      // Possibly this has something to do with the special status of input elements in IE as
 
      // "windowed controls". Calculate the relative offsets manually.
    selectEngine : function (engineName) {
      var textPos = position (this.text);
       if (!this.engineSelector) return;
       var catLinePos = position (this.line);
       for (var i = 0; i < this.engineSelector.options.length; i++) {
       var textTop = textPos.y - catLinePos.y;
         this.engineSelector.options[i].selected = this.engineSelector.options[i].value == engineName;
      var textLeft = textPos.x - catLinePos.x;
      if (window.ie6_bugs) {
         // IE6 somehow has a problem with inline-displayed forms (to which our list belongs), and will add the
        // offset of the beginning of the text to the offsets we'd normally calculate, which in particular with
        // right-aligned category lines as they occur in some older skins completely misplaces the lists, sometimes
        // even off-screen. This appears to affect only the horizontal positioning of the list and of the
        // engineSelector. Try to account for this bizarre behavior. Notes: dunno if that also occurs on IE7.
        var textStartPos = position (this.line.firstChild);
        textStartPos.x -= catLinePos.x;
        textLeft -= textStartPos.x;
       }
       }
       var nl = textLeft;
    },
       var nt = 0;
 
       var offset = 0;
    sanitizeInput : function () {
       if (haveEngine) {
       var v = this.text.value || "";
        this.engineSelector.style.zIndex = 5;
       v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
        this.engineSelector.style.position = 'absolute';
       var re = new RegExp ('^(' + HotCat.category_regexp + '):');
        this.engineSelector.style.width = this.text.offsetWidth + 'px';
       if (re.test (v)) v = v.substring (v.indexOf (':') + 1);
        // Figure out the height of this selector: display it off-screen, then hide it again.
      if (HotCat.capitalizePageNames) v = capitalize (v);
         if (this.engineSelector.style.display == 'none') {
      // Only update the input field if there is a difference. IE8 appears to reset the selection
           this.engineSelector.style.left  = '-1000px';
      // and place the cursor at the front upon reset, which makes our autocompletetion become a
           this.engineSelector.style.top  = textTop + 'px';
      // nuisance. FF and IE6 don't seem to have this problem.
           this.engineSelector.style.display = "";
      if (this.text.value != null && this.text.value != v)
          offset = this.engineSelector.offsetHeight;
        this.text.value = v;
           this.engineSelector.style.display = 'none';
    },
        } else {
 
           offset = this.engineSelector.offsetHeight;
    makeCall : function (url, callbackObj, engine, queryKey, cleanKey) {
      var cb = callbackObj;
      var e  = engine;
      var v  = queryKey;
      var z  = cleanKey;
      var thisObj = this;
 
      function done () {
        cb.callsMade++;
         if (cb.callsMade === cb.nofCalls) {
           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;
           delete cb;
         }
         }
        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)
       getJSON ({
       this.list.style.left = nl + 'px';
        uri : url
       // (Re-)fill the list
      ,success : function (json) {
       while (this.list.firstChild) this.list.removeChild (this.list.firstChild);
          var titles = e.handler (json, z);
       for (var i = 0 ; i < titles.length ; i++) {
          if (titles && titles.length > 0) {
         var opt = make ('option') ;
            if (cb.allTitles === null) {
         opt.appendChild (make (titles[i], true));
              cb.allTitles = titles;
         this.list.appendChild (opt);
            } else {
              cb.allTitles = cb.allTitles.concat (titles);
            }
          }
          done();
        }
      ,error : function (req) {if (!req) noSuggestions = true; cb.dontCache = true; done(); }
       });          
    },
 
    callbackObj : null,
 
    textchange : function (dont_autocomplete, force) {
       // Hide all other lists
      CategoryEditor.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 (haveEngine) {
       if (this.lastInput == v && !force) return; // No change
        this.selectEngine (engineName);
      if (this.lastInput != v) checkMultiInput ();
        this.engineSelector.style.display = "";
      this.lastInput = v;
      }
       this.lastRealInput = v;
       this.list.style.display = 'block';
 
       // Set the width of the list       
       // Mark blacklisted inputs.
       var scroll = scroll_offset ('Left');
       this.ok.disabled = v.length > 0 && HotCat.blacklist != null && HotCat.blacklist.test (v);
       var view_w = viewport ('Width');
 
      var l_pos  = position (this.list);
       if (noSuggestions) {
      if (this.list.offsetWidth < this.text.offsetWidth) {
        // No Ajax: just make sure the list is hidden
         this.list.style.width = this.text.offsetWidth + 'px';
        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;
         return;
       }
       }
      // Make sure that the list fits horizontally into the browser window     
 
       var w      = this.list.offsetWidth;
       if (v.length === 0) { this.showSuggestions([]); return; }
       if (l_pos.x + w > scroll + view_w) {
       if (this.callbackObj) this.callbackObj.cancelled = true;
        if (w > view_w) w = view_w;
      var engineName  = suggestionConfigs[this.engine] ? this.engine : 'combined';
        this.list.style.width = w + 'px';
 
         this.list.style.left = nl - (l_pos.x + w - scroll - view_w) + 'px';
      var cleanKey = v.replace(/[\u200E\u200F\u202A-\u202E]/g, "")
                      .replace(wikiTextBlankRE, ' ');
      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);
     },
     },


     autoComplete : function (newVal, actVal, key, dontModify) {
     makeCalls : function (engines, cb, v, cleanKey) {
       if (newVal == actVal) return true;
       for (var j = 0; j < engines.length; j++) {
      if (dontModify || newVal.indexOf (actVal) != 0) return false;
         var engine = suggestionEngines[engines[j]];
      // Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
        var url = wgServer + wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (cleanKey));
      // such that it can be easily removed by typing backspace if the suggestion is unwanted.
         this.makeCall (url, cb, engine, v, cleanKey);
      if (!(  this.text.setSelectionRange
            || this.text.createTextRange
            ||    typeof (this.text.selectionStart) != 'undefined'
              && typeof (this.text.selectionEnd) != 'undefined'
          )
        )
         return false;
      // Here we know that we can indeed select properly. If we can't doing this would be a major
      // annoyance.
      this.text.focus();
      var start  = actVal.length;      
      this.text.value = newVal + key;         
      if (this.text.setSelectionRange)      // e.g. khtml
        this.text.setSelectionRange (start, newVal.length);
      else if (this.text.createTextRange) { // IE
        var new_selection = this.text.createTextRange();
         new_selection.move ('character', start);
        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) {
     showSuggestions : function (titles, dontAutocomplete, queryKey, engineName) {
       if (this.lastKey == 38 || this.lastKey == 40) { // Up and down arrows
       this.text.readOnly = false;
         if (this.list.style.display != 'none') {
      this.dab = null;
          // List is visible, so there are suggestions
      this.showsList = false;
          this.highlightSuggestion (this.lastKey == 38 ? -1 : 1);
      if (!this.list) return;
          // Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow as
      if (noSuggestions) {
          // "place the text cursor at the front", which we don't want here.
         if (this.list) this.list.style.display = 'none';
          return evtKill (evt);
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        } else if (  this.keyCount <= 1
        if (this.icon) this.icon.style.display = 'none';
                  && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
        this.inputExists = true; // Default...
                  )
        return;
         {
      }    
          // If no suggestions displayed, get them, unless we're already getting them.
      this.engineName = engineName;
          this.textchange ();
      if (engineName) {
        }
        if (!this.engineSelector) this.engineName = null;
      } else {
         if (this.engineSelector) this.engineSelector.style.display = 'none';
       }
       }
       return true;
       if (queryKey) {
    },
        if (this.lastInput.indexOf (queryKey) != 0) return;
 
        if (this.lastQuery && this.lastInput.indexOf (this.lastQuery) === 0 && this.lastQuery.length > queryKey.length)
    highlightSuggestion : function (dir) {
           return;
      if (noSuggestions || !this.list || this.list.style.display == 'none') return;
      var curr = this.list.selectedIndex;
      var tgt = curr < 0 ? 0 : curr + dir;
      tgt = tgt < 0 ? 0 : tgt;
      if (tgt != curr && tgt < this.list.options.length) {
        if (curr >= 0 && curr < this.list.options.length) 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.lastInput, key, false);
        if (!completed) {
           this.text.value = this.list.options[tgt].text + key;
        }
        this.lastInput = this.list.options[tgt].text;
       }
       }
    },
      this.lastQuery = queryKey;


    resetKeySelection : function () {
       // Get current input text
      if (noSuggestions || !this.list || this.list.style.display == 'none') return;
      var v = this.text.value.split('|');
       var curr = this.list.selectedIndex;
      var key = v.length > 1 ? '|' + v[1] : "";
      if (curr >= 0 && curr < this.list.options.length) {
      v = (HotCat.capitalizePageNames ? capitalize (v[0]) : v[0]);
        this.list.options[curr].selected = false;
        // Get current input text
        var v = this.text.value.split('|');
        var key = v.length > 1 ? '|' + v[1] : "";
        this.text.value = this.lastRealInput + key;
        this.lastInput = this.lastRealInput;
      }
    }


  }; // end CategoryEditor.prototype
      if (titles) {
 
        var vLow = v.toLowerCase ();
  function initialize () {
        // Strip blacklisted categories
    // User configurations. Do this here, called from the onload handler, so that users can
        if (HotCat.blacklist != null) {
    // override it easily in their own user script files by just declaring variables. JSconfig
          for (var i = 0; i < titles.length; i++) {
    // is some feature used at Wikimedia Commons.
             if (HotCat.blacklist.test (titles[i])) {
    HotCat.no_autocommit =  
              titles.splice(i, 1);
      (typeof (hotcat_no_autocommit) != 'undefined'
              i--;
        ? !!hotcat_no_autocommit
            }
        : (typeof (JSconfig) != 'undefined' && typeof (JSconfig.keys['HotCatNoAutoCommit']) != 'undefined'
          }
             ? JSconfig.keys['HotCatNoAutoCommit']
        }
            : HotCat.no_autocommit
        titles.sort (
          )
          function (a, b) {
      );
            if (a.indexOf (b) === 0) return 1; // a begins with b: a > b
    HotCat.suggest_delay =  window.hotcat_suggestion_delay
            if (b.indexOf (a) === 0) return -1; // b begins with a: a < b
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatSuggestionDelay']
            // Opensearch may return stuff not beginning with the search prefix!
                          || HotCat.suggest_delay;
            var prefixMatchA = (a.indexOf (v) === 0 ? 1 : 0);
    HotCat.editbox_width =  window.hotcat_editbox_width
            var prefixMatchB = (b.indexOf (v) === 0 ? 1 : 0);
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatEditBoxWidth']
            if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
                          || HotCat.editbox_width;
             // Case-insensitive prefix match!
    HotCat.suggestions  =  window.hotcat_suggestions
             var aLow = a.toLowerCase(), bLow = b.toLowerCase();
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatSuggestions']
            prefixMatchA = (aLow.indexOf (vLow) === 0 ? 1 : 0);
                          || HotCat.suggestions;
            prefixMatchB = (bLow.indexOf (vLow) === 0 ? 1 : 0);
    if (typeof (HotCat.suggestions) != 'string' || !suggestionConfigs[HotCat.suggestions])
            if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
      HotCat.suggestions = 'combined';
            if (a < b) return -1;
    HotCat.fixed_search  =
             if (b < a) return 1;
      (typeof (hotcat_suggestions_fixed) != 'undefined'
             return 0;
        ? !!hotcat_suggestions_fixed
           }
        : (typeof (JSconfig) != 'undefined' && typeof (JSconfig.keys['HotCatFixedSuggestions']) != 'undefined'
        );
             ? JSconfig.keys['HotCatFixedSuggestions']
        // Remove duplicates and self-references
             : HotCat.fixed_search
        for (var i = 0; i < titles.length; i++) {
          )
          if (   i+1 < titles.length && titles[i] == titles[i+1]
      );
              || wgNamespaceNumber == 14 && titles[i] == wgTitle
    HotCat.bg_changed    =   window.hotcat_changed_background
            )
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatChangedBackground']
          {
                          || HotCat.bg_changed;
            titles.splice (i, 1);
    HotCat.use_up_down  =
            i--;
      (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
           )
      );
    // 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];
         }
         }
       }
       }
    }
      if (!titles || titles.length === 0) {
    // Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
        if (this.list) this.list.style.display = 'none';
    is_rtl = hasClass (document.body, 'rtl');
         if (this.engineSelector) this.engineSelector.style.display = 'none';
    if (!is_rtl) {
        if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
      if (document.defaultView && document.defaultView.getComputedStyle) { // Gecko etc.
          if (this.icon) this.icon.src = armorUri(HotCat.existsNo);
         is_rtl = document.defaultView.getComputedStyle (document.body, null).getPropertyValue ('direction');
          this.inputExists = false;
      } else if (document.body.currentStyle) { // IE, has subtle differences to getComputedStyle
        }
        is_rtl = document.body.currentStyle['direction'];
         return;
      } else { // Not exactly right, but best effort
         is_rtl = document.body.style['direction'];
       }
       }
       is_rtl = (is_rtl == 'rtl');
 
    }
       var firstTitle = titles[0];
  }
      var completed = this.autoComplete (firstTitle, v, key, dontAutocomplete);
       
      if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
  function can_edit () {
         this.icon.src = armorUri(completed ? HotCat.existsYes : HotCat.existsNo);
    var container = null;
         this.inputExists = completed;
    switch (skin) {
       }
      case 'cologneblue':
       if (completed) {
         container = document.getElementById ('quickbar');
         this.lastInput = firstTitle;
         // Fall through
         if (titles.length === 1) {
       case 'standard':
           this.list.style.display = 'none';
       case 'nostalgia':
          if (this.engineSelector) this.engineSelector.style.display = 'none';
        if (!container) container = document.getElementById ('topbar');
          return;
         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;
       }
       }
    }
      // (Re-)fill the list
    if (!label) {
      while (this.list.firstChild) this.list.removeChild (this.list.firstChild);
       labelCell.id = 'hotcatLabel';
       for (var i = 0 ; i < titles.length ; i++) {
      labelCell.appendChild (make (HotCat.categories), true);
        var opt = make ('option') ;
     } else {
        opt.appendChild (make (titles[i], true));
       labelCell.id = 'hotcatLabelTranslated';
        opt.selected = completed && (i === 0);
       labelCell.appendChild (label);
        this.list.appendChild (opt);
    }
      }
    labelCell.className          = 'mw-label';
      this.displayList();
    labelCell.style.textAlign    = 'right';
     },
    labelCell.style.verticalAlign = 'middle';
 
    // Change the onsubmit handler
    displayList : function () {
    var form = document.getElementById ('upload') || document.getElementById ('mw-upload-form');
       this.showsList = true;
    if (form) {
      if (!this.is_active) {
      var optionsTable = document.getElementById ('mw-htmlform-options');
        this.list.style.display = 'none';
       if (optionsTable) optionsTable.width = '100%';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
      ip.parentNode.insertBefore (newRow, ip);
        return;
       form.onsubmit = (function (oldSubmit) {
      }
         return function () {        
       var nofItems = (this.list.options.length > HotCat.list_size ? HotCat.list_size : this.list.options.length);
           var do_submit = true;
      if (nofItems <= 1) nofItems = 2;
           if (oldSubmit) {
      this.list.size = nofItems;
            if (typeof (oldSubmit) == 'string')
      this.list.style.align    = is_rtl ? 'right' : 'left';
              do_submit = eval (oldSubmit);
      this.list.style.zIndex  = 5;
            else if (typeof (oldSubmit) == 'function')
      this.list.style.position = 'absolute';
              do_submit = oldSubmit.apply (form, arguments);
      // Compute initial list position. First the height.
          }
      var listh = 0;
          if (!do_submit) return false;
      if (this.list.style.display == 'none') {
          closeForm ();
        // Off-screen display to get the height
          // Copy the categories
        this.list.style.top = this.text.offsetTop + 'px';
          var eb =   document.getElementById ('wpUploadDescription')
        this.list.style.left = '-10000px';
                  || document.getElementById ('wpDesc');
        this.list.style.display = "";
           for (var i = 0; i < editors.length; i++) {
        listh = this.list.offsetHeight;
            var t = editors[i].currentCategory;
        this.list.style.display = 'none';
            if (!t) continue ;
      } else {
            var key = editors[i].currentKey;
        listh = this.list.offsetHeight;
            var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
      }
            // Only add if not already present
      // Approximate calculation of maximum list size
            var cleanedText = eb.value.replace(/<\!--(\s|\S)*?--\>/g, "")
      var maxListHeight = listh;
                                      .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
      if (nofItems < HotCat.list_size) maxListHeight = (listh / nofItems) * HotCat.list_size;
            if (!find_category (cleanedText, t, true)) {
 
              eb.value += '\n' + new_cat;
      function scroll_offset (what) {
            }
        var s = 'scroll' + what;
          }
        return (document.documentElement ? document.documentElement[s] : 0)
          return true;          
              || document.body[s] || 0;
         };
       }
      }) (form.onsubmit);
      function viewport (what) {
    }
        if (is_webkit && !document.evaluate)
  }
          return window['inner' + what]; // Safari < 3.0
 
        var s = 'client' + what;
  var cleanedText = null;
        if (window.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};
      }
 
      var textPos = position (this.text);
      var nl = 0;
      var nt = 0;
      var offset = 0;
      // Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
      var 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.left  = '-10000px';
          this.engineSelector.style.top  = '0px';
          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 + 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.left = nl + 'px';
      if (this.engineName) {
        this.selectEngine (this.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 < textBoxWidth ) {
        this.list.style.width = textBoxWidth + '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';
      }
    },


  function isOnPage (span) {
    autoComplete : function (newVal, actVal, key, dontModify) {
    var catTitle = title (span.firstChild.getAttribute ('href', 2));
      if (newVal == actVal) return true;
    if (!catTitle) return null;
      if (dontModify || newVal.indexOf (actVal) != 0) return false;
    catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' ');
      // Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
    var result = { title : catTitle, match : ["", "", ""] };
      // such that it can be easily removed by typing backspace if the suggestion is unwanted.
    if (pageText === null) return result;
      if (!this.canSelect()) return false;
    if (cleanedText === null) {
       // If we can't select properly, autocompletion would be a major annoyance to the user.
       cleanedText = pageText.replace(/<\!--(\s|\S)*?--\>/g, "")
      this.text.focus();
                            .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
      this.text.value = newVal + key;
    }
      this.setSelection (actVal.length, newVal.length);
    result.match = find_category (cleanedText, catTitle, true);
      return true;
    return result;
    },
  }


  var initialized = false;
    canSelect : function () {
      return    this.text.setSelectionRange
              || this.text.createTextRange
              ||    typeof (this.text.selectionStart) != 'undefined'
                  && typeof (this.text.selectionEnd) != 'undefined';
    },


  function setup () {
    setSelection : function (from, to) {
    if (initialized) return;
      // this.text must be focused (at least on IE)
    initialized = true;
      if (!this.text.value) return;
    // Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
      if (this.text.setSelectionRange) {    // e.g. khtml
    // each category, and add the + link.
        this.text.setSelectionRange (from, to);
    catLine catLine                                                  // Special:Upload
      } else if (typeof (this.text.selectionStart) != 'undefined') {
            || document.getElementById ('mw-normal-catlinks')           // MW >= 1.13alpha
        if (from > this.text.selectionStart) {
            || getElementsByClassName (document , 'p' , 'catlinks')[0]; // MW < 1.13
          this.text.selectionEnd  = to;
    var hiddenCats = document.getElementById ('mw-hidden-catlinks');
          this.text.selectionStart = from;
    if (!catLine) {
        } else {
      var footer = null;
          this.text.selectionStart = from;
      if (!hiddenCats) {
          this.text.selectionEnd   = to;
        footer = getElementsByClassName (document , 'div' , 'printfooter')[0];
        }
        if (!footer) return; // Don't know where to insert the category line
      } else if (this.text.createTextRange) { // IE
        var new_selection = this.text.createTextRange();
        new_selection.move ('character', from);
        new_selection.moveEnd ('character', to - from);
        new_selection.select();
      }
    },
 
    getSelection : function () {
      var from = 0, to = 0;
      // this.text must be focused (at least on IE)
      if (!this.text.value) {
        // No text.
      } else if (typeof (this.text.selectionStart) != 'undefined') {
        from = this.text.selectionStart;
        to  = this.text.selectionEnd;
      } else if (document.selection && document.selection.createRange) { // IE
        var rng = document.selection.createRange().duplicate();
        if (rng.parentElement() === this.text) {
          try {
            var textRng = this.text.createTextRange();
            textRng.move('character', 0);
            textRng.setEndPoint('EndToEnd', rng);
            // We're in a single-line input box: no need to care about IE's strange
            // handling of line ends
            to = textRng.text.length;
            textRng.setEndPoint('EndToStart', rng);
            from = textRng.text.length;
          } catch (notFocused) {
            from = this.text.value.length; to = from; // At end of text
          }
        }
       }
       }
       catLine = make ('div');
       return {start: from, end: to};
       catLine.id = 'mw-normal-catlinks';
    },
      catLine.style.textAlign = 'left';
 
      // Add a label
    saveView : function (evt) {
       var label = make ('a');
       this.lastSelection = this.getSelection ();
       label.href  = wgArticlePath.replace ('$1', 'Special:Categories');
    },
      label.title = HotCat.categories;
 
      label.appendChild (make (HotCat.categories, true));
    processKey : function (evt) {
      catLine.appendChild (label);
       var dir = 0;
      catLine.appendChild (make (':', true));
       switch (this.lastKey) {
      // Insert the new category line
        case 38: dir = -1; // Up arrow
      var container = (hiddenCats ? hiddenCats.parentNode : document.getElementById ('catlinks'));
        case 40: if (dir === 0) dir = 1; // Down arrow
      if (!container) {
        case 33: if (dir === 0) dir = -HotCat.list_size; // Page up
        container = make ('div');
        case 34: if (dir === 0) dir = HotCat.list_size; // Page down
        container.id = 'catlinks';
          if (this.list.style.display != 'none') {
        footer.parentNode.insertBefore (container, footer.nextSibling);
            // List is visible, so there are suggestions
            this.highlightSuggestion (dir);
            // Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow
            // as "place the text cursor at the front", which we don't want here.
            return evtKill (evt);
          } else if (  this.keyCount <= 1
                    && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
                    )
          {
            // If no suggestions displayed, get them, unless we're already getting them.
            this.textchange ();
          }
          break;
        case 27: // ESC: inhibit default behavior (revert to last real input in FF: we do that ourselves)
          return evtKill (evt);
       }
       }
       container.className = 'catlinks noprint';
       return true;
       container.style.display = "";
    },
       if (!hiddenCats) {
 
         container.appendChild (catLine);
    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 {
       } else {
         container.insertBefore (catLine, hiddenCats);
         tgt = curr < 0 ? 0 : curr + dir;
        tgt = tgt < 0 ? 0 : tgt;
        if (tgt >= this.list.options.length) tgt = this.list.options.length - 1;
       }
       }
    } // end if catLine exists
      if (tgt != curr || dir === 0) {
    catLine.style.position = 'relative';
        if (curr >= 0 && curr < this.list.options.length && dir != 0) this.list.options[curr].selected = false;
    if (is_rtl) catLine.dir = 'rtl';
        this.list.options[tgt].selected = true;
 
        // Get current input text
    // Create editors for all existing categories
        var v = this.text.value.split('|');
 
        var key = v.length > 1 ? '|' + v[1] : "";
     function createEditors (line) {
        var completed = this.autoComplete (this.list.options[tgt].text, this.lastRealInput, key, false);
       var cats = line.getElementsByTagName ('span');
        if (!completed) {
      // Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
          this.text.value = this.list.options[tgt].text + key;
       var copyCats = new Array (cats.length);
        }
       for (var i = 0; i < cats.length; i++) copyCats[i] = cats[i];
        this.lastInput = this.list.options[tgt].text;
      var editor = null;
        this.inputExists = true; // Might be wrong if from a dab list...
      for (var i = 0; i < copyCats.length; i++) {
        if (this.icon) this.icon.src = armorUri(HotCat.existsYes);
         var test = isOnPage (copyCats[i]);
        this.state = CategoryEditor.CHANGE_PENDING;
         if (test !== null && test.match !== null) {
      }
           editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2]);
      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 copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
       return false;
     }
     }


    var lastSpan = createEditors (catLine);
  }; // end CategoryEditor.prototype
     // Create one to add a new category
 
     var editor = new CategoryEditor(catLine, null, null, lastSpan != null);
  function initialize () {
     if (!onUpload) {
    // User configurations. Do this here, called from the onload handler, so that users can
      if (pageText !== null && hiddenCats) {
    // override it easily in their own user script files by just declaring variables. JSconfig
        hiddenCats.style.position = 'relative';
     // is some feature used at Wikimedia Commons.
         if (is_rtl) hiddenCats.dir = 'rtl';
     var config = (typeof (JSconfig) != 'undefined' && JSconfig.keys) ? JSconfig.keys : {};
        createEditors (hiddenCats);
     HotCat.dont_add_to_watchlist =
       }
      (typeof (window.hotcat_dont_add_to_watchlist) != 'undefined'
      // And finally add the "multi-mode" span. (Do this at the end, otherwise it ends up in the list above.)
        ? !!window.hotcat_dont_add_to_watchlist
      var enableMulti = make ('span');
        : (typeof (config['HotCatDontAddToWatchlist']) != 'undefined'
      enableMulti.className = 'noprint';
            ? config['HotCatDontAddToWatchlist']
      if (is_rtl) enableMulti.dir = 'rtl';
            : HotCat.dont_add_to_watchlist
       catLine.insertBefore (enableMulti, catLine.firstChild.nextSibling);
          )
      enableMulti.appendChild (make ('\xa0', true)); // nbsp
      );
      multiSpan = make ('span');
    HotCat.no_autocommit =
       enableMulti.appendChild (multiSpan);
      (typeof (window.hotcat_no_autocommit) != 'undefined'
       multiSpan.innerHTML = '(<a>' + HotCat.addmulti + '</a>)';
         ? !!window.hotcat_no_autocommit
      var lk = multiSpan.getElementsByTagName ('a')[0];
        : (typeof (config['HotCatNoAutoCommit']) != 'undefined'
      lk.onclick = function (evt) {setMultiInput (); checkMultiInput (); return evtKill (evt);};
            ? config['HotCatNoAutoCommit']
      lk.title = HotCat.multi_tooltip;
            : HotCat.no_autocommit
      lk.style.cursor = 'pointer';
          )
     }
       );
     cleanedText = null;
    HotCat.suggest_delay =  window.hotcat_suggestion_delay
  }
                          || config['HotCatSuggestionDelay']
 
                          || HotCat.suggest_delay;
  function setPage (json) {
    HotCat.editbox_width =   window.hotcat_editbox_width
     if (json && json.query) {
                          || config['HotCatEditBoxWidth']
      if (json.query.pages) {
                          || HotCat.editbox_width;
        for (var p in json.query.pages) {
    HotCat.suggestions  =   window.hotcat_suggestions
          var page = json.query.pages[p];
                          || config['HotCatSuggestions']
          if (!page.revisions || page.revisions.length == 0) break;
                          || HotCat.suggestions;
          pageText = page.revisions[0]['*'];
    if (typeof (HotCat.suggestions) != 'string' || !suggestionConfigs[HotCat.suggestions])
           pageTime = page.revisions[0].timestamp.replace (/\D/g, "");
      HotCat.suggestions = 'combined';
          pageWatched = typeof (page.watched) == 'string';
    HotCat.fixed_search  =
          break;
       (typeof (window.hotcat_suggestions_fixed) != 'undefined'
        ? !!window.hotcat_suggestions_fixed
        : (typeof (config['HotCatFixedSuggestions']) != 'undefined'
            ? config['HotCatFixedSuggestions']
            : HotCat.fixed_search
          )
       );
    HotCat.single_minor  =
       (typeof (window.hotcat_single_changes_are_minor) != 'undefined'
        ? !!window.hotcat_single_changes_are_minor
        : (typeof (config['HotCatMinorSingleChanges']) != 'undefined'
            ? config['HotCatMinorSingleChanges']
            : HotCat.single_minor
          )
      );
    HotCat.bg_changed    =   window.hotcat_changed_background
                          || config['HotCatChangedBackground']
                          || HotCat.bg_changed;
    HotCat.use_up_down  =
      (typeof (window.hotcat_use_category_links) != 'undefined'
        ? !!window.hotcat_use_category_links
        : (typeof (config['HotCatUseCategoryLinks']) != 'undefined'
            ? config['HotCatUseCategoryLinks']
            : HotCat.use_up_down
          )
      );
    HotCat.list_size =    window.hotcat_list_size
                      || config['HotCatListSize']
                      || HotCat.list_size;
     // Numeric input, make sure we have a numeric value
     HotCat.list_size = parseInt (HotCat.list_size, 10);
    if (isNaN (HotCat.list_size) || HotCat.list_size < 5) HotCat.list_size = 5;
     if (HotCat.list_size > 15) HotCat.list_size = 15;
    // Localize search engine names
    if (HotCat.engine_names) {
      for (var key in HotCat.engine_names) {
        if (suggestionConfigs[key] && HotCat.engine_names[key]) {
           suggestionConfigs[key].name = HotCat.engine_names[key];
         }
         }
       }
       }
      // Siteinfo
    }
       if (json.query.general) {
    // Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
         HotCat.capitalizePageNames = (json.query.general['case'] == 'first-letter');
    is_rtl = hasClass (document.body, 'rtl');
        if (json.query.general.time) serverTime = json.query.general.time.replace (/\D/g, "");
    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 getPage () {
   function can_edit () {
     // We know we have an article here.
     var container = null;
     if (wgArticleId == 0) {
     switch (skin) {
       // Doesn't exist yet.
       case 'cologneblue':
      pageText = "";
        container = document.getElementById ('quickbar');
      pageTime = null;
        // Fall through
       setup ();
       case 'standard':
    } else {
       case 'nostalgia':
       var url = wgServer + wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&titles='
        if (!container) container = document.getElementById ('topbar');
              + encodeURIComponent (wgPageName) + '&prop=info%7Crevisions&rvprop=content%7Ctimestamp&meta=siteinfo';
        var lks = container.getElementsByTagName ('a');
      var s = make ('script');
        for (var i = 0; i < lks.length; i++) {
      s.src = url;
          if (   param ('title', lks[i].href) == wgPageName
      s.type = 'text/javascript';
              && param ('action', lks[i].href) == 'edit')
      HotCat.start = function (json) { setPage (json); setup (); };
            return true;
      document.getElementsByTagName ('head')[0].appendChild (s);
        }
       window.setTimeout (setup, 4000); // 4 seconds. Just in case getting the wikitext takes longer.
        return false;
       default:
        // all modern skins:
        return document.getElementById ('ca-edit') != null;
     }
     }
    return false;
   }
   }


   function run () {
   function setup_upload () {
     if (HotCat.started) return;
     onUpload = true;
    HotCat.started = true;
     // Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
     initialize ();
     var ip = document.getElementById ('mw-htmlform-description') || document.getElementById ('wpDestFile');
 
    if (!ip) {
    if (is_rtl && window.ie6_bugs) return; // Disabled! IE6 with RTL is just too broken...
       ip = document.getElementById ('wpDestFile');
 
      while (ip && ip.nodeName.toLowerCase() != 'table') ip = ip.parentNode;
     if (wgNamespaceNumber == -1 && wgCanonicalSpecialPageName == 'Upload' && wgUserName) {
      setup_upload ();
      setup ();
       // Check for state restoration
      if (  typeof (UploadForm) != 'undefined'
          && 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 ();
     }
     }
   }
    if (!ip) return;
 
    var reupload = document.getElementById ('wpForReUpload');
   // Legacy stuff
    var destFile = document.getElementById ('wpDestFile');
 
    if (  (reupload && !!reupload.value)
   function closeForm () {
        || (destFile && (destFile.disabled || destFile.readOnly)))
     // Close all open editors without redirect resolution and other asynchronous stuff.
      return; // re-upload form...
     for (var i = 0; i < editors.length; i++) {
    // Insert a table row with two fields (label and empty category bar)
       if (editors[i].state == CategoryEditor.OPEN) {
    var labelCell = make ('td');
         editors[i].cancel();
    var lineCell  = make ('td');
       } else if (editors[i].state == CategoryEditor.CHANGE_PENDING) {
    // Create the category line
         editors[i].sanitizeInput ();
    catLine = make ('div');
         var value = editors[i].text.value.split('|');
    catLine.className = 'catlinks';
         var key  = null;
    catLine.id = 'catlinks';
         if (value.length > 1) key = value[1];
    catLine.style.textAlign = is_rtl ? 'right' : 'left';
         var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
    // We'll be inside a table row. Make sure that we don't have margins or strange borders.
         if (v.length == 0) {
    catLine.style.margin = '0';
           editors[i].cancel ();
    catLine.style.border = 'none';
    lineCell.appendChild (catLine);
    // Create the label
    var label = null;
    if (  typeof (UFUI) != 'undefined'
        && typeof (UIElements) != 'undefined'
        && typeof (UFUI.getLabel) == 'function') {
      try {
        label = UFUI.getLabel ('wpCategoriesUploadLbl');
      } catch (ex) {
        label = null;
      }
    }
    if (!label) {
      labelCell.id = 'hotcatLabel';
      labelCell.appendChild (make (HotCat.categories, true));
    } else {
      labelCell.id = 'hotcatLabelTranslated';
      labelCell.appendChild (label);
    }
    labelCell.className          = 'mw-label';
    labelCell.style.textAlign    = 'right';
    labelCell.style.verticalAlign = 'middle';
    // Change the onsubmit handler
    var form = document.getElementById ('upload') || document.getElementById ('mw-upload-form');
    if (form) {
      var newRow = ip.insertRow (-1);
      newRow.appendChild (labelCell);
      newRow.appendChild (lineCell);
      form.onsubmit = (function (oldSubmit) {
        return function () {
          var do_submit = true;
          if (oldSubmit) {
            if (typeof (oldSubmit) == 'string')
              do_submit = eval (oldSubmit);
            else if (typeof (oldSubmit) == 'function')
              do_submit = oldSubmit.apply (form, arguments);
          }
          if (!do_submit) return false;
          closeForm ();
          // Copy the categories
          var eb =    document.getElementById ('wpUploadDescription')
                  || document.getElementById ('wpDesc');
          var addedOne = false;
          for (var i = 0; i < editors.length; i++) {
            var t = editors[i].currentCategory;
            if (!t) continue ;
            var key = editors[i].currentKey;
            var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
            // Only add if not already present
            var cleanedText = eb.value.replace(/<\!--(\s|\S)*?--\>/g, "")
                                      .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
            if (!find_category (cleanedText, t, true)) {
              eb.value += '\n' + new_cat;
              addedOne = true;
            }
          }
          if (addedOne) {
            // Remove "subst:unc" added by Flinfo if it didn't find categories
            eb.value = eb.value.replace(/\{\{subst:unc\}\}/g, "");
          }
          return true;
        };
      }) (form.onsubmit);
    }
  }
 
  var cleanedText = null;
 
  function isOnPage (span) {
    var catTitle = title (span.firstChild.getAttribute ('href', 2));
    if (!catTitle) return null;
    catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' ');
    if (HotCat.blacklist != null && HotCat.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 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 =  catLine                                                  // Special:Upload
            || document.getElementById ('mw-normal-catlinks')          // MW >= 1.13alpha
            || getElementsByClassName (document , 'p' , 'catlinks')[0]; // MW < 1.13
    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 = is_rtl ? 'right' : '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
    if (is_rtl) catLine.dir = 'rtl';
 
    // Create editors for all existing categories
 
    function createEditors (line, is_hidden) {
      var cats = line.getElementsByTagName ('li');
      if (cats.length > 0) {
        newDOM = true; line = cats[0].parentNode;
      } else {
        cats = line.getElementsByTagName ('span');
      }
      // Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
      var copyCats = new Array (cats.length);
      for (var i = 0; i < cats.length; i++) copyCats[i] = cats[i];
      var editor = null;
      for (var i = 0; i < copyCats.length; i++) {
        var test = isOnPage (copyCats[i]);
        if (test !== null && test.match !== null) {
          editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2], is_hidden);
        }
      }
      return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
    }
 
    var lastSpan = createEditors (catLine, false);
    // Create one to add a new category
    var editor = 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>' + HotCat.addmulti + '</a>)';
      var lk = multiSpan.getElementsByTagName ('a')[0];
      lk.onclick = function (evt) {setMultiInput (); checkMultiInput (); return evtKill (evt);};
      lk.title = HotCat.multi_tooltip;
      lk.style.cursor = 'pointer';
    }
    cleanedText = null;
    if (typeof (additionalWork) == 'function') additionalWork();
    setupCompleted.loaded(); // Trigger signal; execute registered functions
    if (window.jQuery) window.jQuery('body').trigger ('hotcatSetupCompleted');
  }
 
  function setPage (json) {
    if (json && json.query) {
      if (json.query.pages) {
        var page = json.query.pages[wgArticleId == 0 ? "-1" : "" + wgArticleId];
        if (page) {
          if (page.revisions && page.revisions.length > 0) {
            pageText = page.revisions[0]['*'];
            pageTime = page.revisions[0].timestamp.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 > 0) {
              interlanguageRE = new RegExp ('((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$');
            }
          }
 
        }
      }
      // 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, "");
      }
      // Userinfo
      if (json.query.userinfo && json.query.userinfo.options) {
        watchCreate = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchcreations == '1';
        watchEdit  = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchdefault == '1';
        minorEdits  = json.query.userinfo.options.minordefault == 1;
        // If the user has the "All edits are minor" preference enabled, we should honor that
        // for single category changes, no matter what the site configuration is.
        if (minorEdits) HotCat.single_minor = true;
      }
    }
  }
 
  function createCommitForm () {
    if (commitForm) return;
    var formContainer = make ('div');
    formContainer.style.display = 'none';
    document.body.appendChild (formContainer);
    var formText =
        '<form id="hotcatCommitForm" 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="wpEditToken" />'
      + '<input type="hidden" name="wpDiff" value="wpDiff" />'
      + '<input type="submit" name="hcCommit" value="hcCommit" />'
      + '</form>';
    formContainer.innerHTML = formText;
    commitForm = document.getElementById ('hotcatCommitForm');
  }
 
  function getPage () {
    // We know we have an article here.
    if (wgArticleId === 0) {
      // Doesn't exist yet.
      pageText = "";
      pageTime = null;
      setup (createCommitForm);
    } 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 = armorUri(url);
      s.type = 'text/javascript';
      HotCat.start = function (json) { setPage (json); setup (createCommitForm); };
      document.getElementsByTagName ('head')[0].appendChild (s);
      setupTimeout = window.setTimeout (function () {setup (createCommitForm);}, 4000); // 4 sec, just in case getting the wikitext takes longer.
    }
  }
 
  function run () {
    if (HotCat.started) return;
    HotCat.started = true;
    loadTrigger.register(really_run);
  }
 
  function really_run () {
    initialize ();
 
    if (is_rtl && is_ie6) return; // Disabled! IE6 with RTL is just too broken...
    if (!HotCat.upload_disabled && wgNamespaceNumber === -1 && wgCanonicalSpecialPageName == 'Upload' && wgUserName) {
      setup_upload ();
      setup (function () {
        // Check for state restoration once the setup is done otherwise, but before signalling setup completion
        if (  typeof (UploadForm) != 'undefined'
            && 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' || param('diff') != null || !can_edit() || HotCat.disable()) return;
      getPage ();
    }
   }
 
   // Legacy stuff
 
   function closeForm () {
     // Close all open editors without redirect resolution and other asynchronous stuff.
     for (var i = 0; i < editors.length; i++) {
       if (editors[i].state == CategoryEditor.OPEN) {
         editors[i].cancel();
       } else if (editors[i].state == CategoryEditor.CHANGE_PENDING) {
         editors[i].sanitizeInput ();
         var value = editors[i].text.value.split('|');
         var key  = null;
         if (value.length > 1) key = value[1];
         var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
         if (v.length === 0) {
           editors[i].cancel ();
         } else {
         } else {
           editors[i].currentCategory = v;
           editors[i].currentCategory = v;
           editors[i].currentKey = key;
           editors[i].currentKey = key;
           editors[i].currentExists = this.inputExists;
           editors[i].currentExists = this.inputExists;
           editors[i].close ();
           editors[i].close ();
         }
         }
       }
       }
     }
     }
   }
   }
 
  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 > 0) {
        if (key != null) text += '|' + key;
        if (result == null)
          result = text;
        else
          result = result + '\n' + text;
      }
    }
    return result;
  }
 
  function setState (state) {
    var cats = state.split ('\n');
    if (cats.length === 0) return null;
    if (initialized && editors.length == 1 && editors[0].isAddCategory) {
      // Insert new spans and create new editors for them.
      var newSpans = [];
      var before = editors.length == 1 ? editors[0].span : null;
      for (var i = 0; i < cats.length; i++) {
        if (cats[i].length === 0) continue;
        var cat = cats[i].split ('|');
        var key = cat.length > 1 ? cat[1] : null;
        cat = cat[0];
        var lk = make ('a'); lk.href = wikiPagePath (HotCat.category_canonical + ':' + cat);
        lk.appendChild (make (cat, true));
        lk.title = cat;
        var span = make ('span');
        span.appendChild (lk);
        if (i === 0) catLine.insertBefore (make (' ', true), before);
        catLine.insertBefore (span, before);
        if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
        newSpans.push ({element: span, title: cat, 'key': key});
      }
      // And change the last one...
      if (before) {
        before.parentNode.insertBefore (make (' | ', true), before);
      }
      var editor = null;
      for (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
  window.hotcat_get_state  = function () { return getState(); };
  window.hotcat_set_state  = function (state) { return setState (state); };
  window.hotcat_close_form = function () { closeForm (); };


   function getState () {
   if (window.mediaWiki && window.mediaWiki.config) {
     var result = null;
     // Make sure we don't get conflicts with AjaxCategories (core development that should one day
     for (var i = 0; i < editors.length; i++) {
     // replace HotCat).
      var text = editors[i].currentCategory;
    window.mediaWiki.config.set('disableAJAXCategories', true);
      var key  = editors[i].currentKey;
      if (text && text.length > 0) {
        if (key != null) text += '|' + key;
        if (result == null)
          result = text;
        else
          result = result + '\n' + text;
      }
    }
    return result;
   }
   }
 
   if (window.jQuery) {
   function setState (state) {
     window.jQuery(document).ready(run);
    var cats = state.split ('\n');
  } else {
    if (cats.length == 0) return null;
    addOnloadHook (run);
     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;
      for (var i = 0; i < cats.length; i++) {
        if (cats[i].length == 0) continue;
        var cat = cats[i].split ('|');
        var key = cat.length > 1 ? cat[1] : null;
        cat = cat[0];
        var lk = make ('a'); lk.href = wikiPagePath (HotCat.category_canonical + ':' + cat);
        lk.appendChild (make (cat, true));
        lk.title = cat;
        var span = make ('span');
        span.appendChild (lk);
        if (i == 0) catLine.insertBefore (make (' ', true), before);
        catLine.insertBefore (span, before);
        if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
        newSpans.push ({element: span, title: cat, 'key': key});
      }
      // And change the last one...
      if (before) {
        before.parentNode.insertBefore (make (' | ', true), before);
      }
      var editor = null;
      for (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
  window.hotcat_get_state  = function () { return getState(); };
  window.hotcat_set_state  = function (state) { return setState (state); };
  window.hotcat_close_form = function () { closeForm (); };
  addOnloadHook (run);
})();
})();


} // end if (guard)
} // end if (guard)
//</source>
//</source>

Version vom 21. Februar 2012, 00:42 Uhr

//<source lang="javascript">

/*
  HotCat V2.17

  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 is MW version safe. It should run on any MediaWiki installation >= MW 1.15. Note: if
  running on MW >= 1.17 configured with $wgLegacyJavaScriptGlobals != true, it will still force
  publishing the wg* globals in the window object. Note that HotCat is supposed to run with or
  without jQuery, and also on older installations that do not yet have window.mediaWiki. If you
  use any of these newer features, make sure you qualify them by checking whether they exist at
  all, and by providing some meaningful fallback implementation if not. To start itself, HotCat
  uses either jQuery(document).ready(), if available (preferred), or the old addOnloadHook().
  If neither exists, HotCat won't start.
*/
if ((typeof wgAction == 'undefined') && window.mediaWiki && window.mediaWiki.config) { // Compatibility hack
  window.wgAction = window.mediaWiki.config.get('wgAction');
}
if (typeof (window.HotCat) == 'undefined' && wgAction != 'edit') { // Guard against double inclusions, and inactivate on edit pages

// Configuration stuff.
var HotCat = {
  isCommonsVersion : false
    // If you copy HotCat to your wiki, you should set this to false!

  // Localize these messages to the main language of your wiki.
  ,messages :
    { cat_removed  : 'Entferne [[Kategorie:$1]]'
     ,template_removed  : 'Entferne {{[[Kategorie:$1|$1]]}}'
     ,cat_added    : 'Ergänze [[Kategorie:$1]]'
     ,cat_keychange: 'neuer Sortierschlüssel für [[Kategorie:$1]]: "$2"' // $2 is the new key
     ,cat_notFound : 'Kategorie "$1" konnte nicht gefunden werden'
     ,cat_exists   : 'Kategorie "$1" bereits enthalten; nicht ergänzt'
     ,cat_resolved : ' (Weiterleitung [[Kategorie:$1]] aufgelöst)'
     ,uncat_removed: 'entferne {{uncategorized}}'
     ,separator    : '; '
     ,prefix       : '[[Hilfe:HotCat|HC]]: '
        // Some text to prefix to the edit summary.
     ,using        : ""
        // 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.
     ,multi_change : '$1 Kategorien'
        // $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.
     ,commit       : 'Speichern'
        // 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       : 'Abbrechen'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
     ,multi_error  : 'Quelltext konnte nicht abrufen werden. Deine Änderungen wurden deshalb nicht gespeichert.'
        // 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 : 'Kategorie'
   // 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         : 'Kategorien'
   // Plural of category_canonical.
 ,disambig_category  : null
   // 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     : null
   // 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:  'Ändern'
   ,remove:  'Entfernen'
   ,add:     'Neue Kategorie hinzufügen'
   ,restore: 'Wiederherstellen'
   ,undo:    'Zurücksetzen'
   ,down:    'durch Unterkategorie ersetzen'
   ,up:      'durch Überkategorie ersetzen'
  }
   // 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      : 'Mehrere Kategorien ändern'
   // Tooltip for the "enter multi-mode" link
 ,disable            :
    function () { // Return true to disable HotCat. HotCat guarantees that the wg* globals exist here.
      var ns = wgNamespaceNumber;
      return (   ns < 0   // Special pages; Special:Upload is handled differently
              || ns === 10 // Templates
              || ns === 8  // MediaWiki
              || ns === 6 && wgArticleId === 0 // Non-existing file pages
              || ns === 2 && /\.(js|css)$/.test(wgTitle) // User scripts
              || typeof (wgNamespaceIds) != 'undefined'
                 && (   ns === wgNamespaceIds['creator']
                     || ns === wgNamespaceIds['timedtext']
                     || ns === wgNamespaceIds['institution']
                    )
             );
    }
 ,uncat_regexp : null
   // A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
   // If not, set it to null.
 ,existsYes    : '//upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
 ,existsNo     : '//upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png'
   // The images used for the little indication icon. Should not need changing.
 ,template_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 : 'Indexsuche'
   ,pagelist    : 'Seitenliste'
   ,combined    : 'Kombinierte Suche'
   ,subcat      : 'Unterkategorien'
   ,parentcat   : 'Überkategorien'
  }
   // Names for the search engines
 ,capitalizePageNames : true
   // Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the first letter
   // of a page is automatically capitalized ("first-letter"; Category:aa == Category:Aa), or it isn't
   // ("case-sensitive"; Category:aa != Category:Aa). It doesn't currently have a fully case-insensitive mode
   // (which would mean Category:aa == Category:Aa == Category:AA == Category:aA)
   // HotCat tries to set this correctly automatically using an API query. It's still a good idea to manually
   // configure it correctly; either directly here if you copied HotCat, or in the local configuration file
   // MediaWiki:Gadget-HotCat.js/local_defaults if you hotlink to the Commons-version, to ensure it is set even
   // if that API query should fail for some strange reason.
 ,upload_disabled : false
   // If upload_disabled is true, HotCat will not be used on the Upload form.
 ,blacklist : null
   // Single regular expression matching blacklisted categories that cannot be changed or
   // added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
   // or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
   // word "maintenance" in its title.

  // Stuff changeable by users:
 ,bg_changed : '#F8CCB0'
   // Background for changed categories in multi-edit mode. Default is a very light salmon pink.
 ,no_autocommit : false
   // If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
   // the changes; users must always save explicitly.
 ,suggest_delay : 100
   // Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
   // server to get suggestions.
 ,editbox_width : 40
   // Default width, in characters, of the text input field.
 ,suggestions : 'combined'
   // One of the engine_names above, to be used as the default suggestion engine.
 ,fixed_search : false
   // If true, always use the default engine, and never display a selector.
 ,use_up_down : true
   // If false, do not display the "up" and "down" links
 ,list_size : 5
   // Default list size
 ,single_minor : true
   // If true, single category changes are marked as minor edits. If false, they're not.
 ,dont_add_to_watchlist : false
   // If true, never add a page to the user's watchlist. If false, pages get added to the watchlist if
   // the user has the "Add pages I edit to my watchlist" or the "Add pages I create to my watchlist"
   // options in his or her preferences set.
};

// Make sure this is exported, so that localizations *can* actually modify parts of it, and the
// guard at the top actually works. (If we're loaded as an extension module through the resource
// loader, this outer scope may actually be a closure, not the global "window" scope.)
if (typeof (window.HotCat) == 'undefined') window.HotCat = HotCat;

(function () { // Local scope to avoid polluting the global namespace with declarations

  // Backwards compatibility stuff. We want HotCat to work with either wg* globals, or with mw.config.get().
  // Our "solution" is to publish the wg* globals if they're not already published.
  if (window.mediaWiki && window.mediaWiki.config) {
    var globals = window.mediaWiki.config.get();
    if (globals && globals !== window) {
      for (var k in globals) window[k] = globals[k];
      window.mediWiki.config = new window.mediaWiki.Map(true); // Make config point to window again.
    }
    globals = null;
  }
  // More backwards compatibility. We have four places where we test for the browser: once for
  // Safari < 3.0, once for WebKit (Chrome or Safari, any versions), and twice for IE <= 6.
  var ua = navigator.userAgent.toLowerCase();
  var is_ie6 = /msie ([0-9]{1,}[\.0-9]{0,})/.exec(ua) != null && parseFloat(RegExp.$1) <= 6.0;
  var is_webkit = /applewebkit\/\d+/.test(ua) && ua.indexOf ('spoofer') < 0;  
  // And even more compatbility. HotCat was developed without jQuery, and anyway current jQuery
  // (1.7.1) doesn't seem to support in jquery.getJSON() or jQuery.ajax() the automatic
  // switching from GET to POST requests if the query arguments would make the uri too long.
  // (IE has a hard limit of 2083 bytes, and the servers may have limits around 4 or 8kB.)
  //    Anyway, HotCat is supposed to run on wikis without jQuery, so we'd have to supply some
  // ajax routines ourselves in any case. We can't rely on the old sajax_init_object(), newer
  // MW versions (>= 1.19) might not have it.
  var getJSON = (function () {
    function getRequest () {
      var request = null;
      try {
        request = new window.XMLHttpRequest();
      } catch (anything) {
        if (window.ActiveXObject) {
          try {
            request = new window.ActiveXObject('Microsoft.XMLHTTP');
          } catch (any) {
          }
        } // end if IE
      } // end try-catch
      return request;
    }

    function makeRequest (settings) {
      var req = getRequest();
      if (!req && settings && settings.error) settings.error (req);
      if (!req || !settings || !settings.uri) return req;
      var uri = armorUri (settings.uri);
      var args = settings.data || null;
      var method;
      if (args && uri.length + args.length + 1 > 2000) {
        // We lose caching, but at least we can make the request
        method = 'POST';
        req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
      } else {
        method = 'GET';
        if (args) uri += '?' + args;
        args = null;
      }
      req.open (method, uri, true);
      req.onreadystatechange = function () {
        if (req.readyState != 4) return;
        if (req.status != 200 || !req.responseText || !(/^\s*[\{\[]/.test(req.responseText))) {
          if (settings.error) settings.error (req);
        } else {
          if (settings.success) settings.success (eval ('(' + req.responseText + ')'));
        }          
      };
      req.setRequestHeader ('Pragma', 'cache=yes');
      req.setRequestHeader ('Cache-Control', 'no-transform');
      req.send (args);
      return req;
    }

    return makeRequest;
  })();

  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 () { this.initialize.apply (this, arguments); };
  LoadTrigger.prototype = {
    initialize : function (needed) {
      this.queue = [];
      this.toLoad = needed;
    },

    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.
  HotCat.runWhenReady = function (callback) {setupCompleted.register(callback);};

  var loadTrigger = new LoadTrigger(2);
  // Used to delay running the HotCat setup until /local_defaults and localizations have been loaded.

  function load (uri) {
    var head = document.getElementsByTagName ('head')[0];
    var s = document.createElement ('script');
    s.setAttribute ('src', armorUri(uri));
    s.setAttribute ('type', 'text/javascript');
    var done = false;

    function afterLoad () {
      if (done) return;
      done = true;
      s.onload = s.onreadystatechange = s.onerror = null; // Properly clean up to avoid memory leaks in IE
      if (head && s.parentNode) head.removeChild (s);
      loadTrigger.loaded();
    }

    s.onload = s.onreadystatechange = function () { // onreadystatechange for IE, onload for all others
      if (done) return;
      if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
        afterLoad ();
      }
    };
    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 (wgServer + 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 = wgServer + url;
    }
    load (url);
  }

  if (HotCat.isCommonsVersion && wgServer.indexOf ('/commons') < 0) {
    // We're running in some other wiki, which hotlinks to the Commons version. The other wiki can put local settings
    // in this file to override the Commons settings for all user languages. For instance, if on your wiki people do
    // not like automatic saving, you'd add in that file the line HotCat.no_autocommit = true; If you hotlink, you
    // *must* adapt HotCat.categories in this file to the local translation in wgContentLanguage of your wiki of the
    // English plural "Categories", and you should provide translations in wgContentLanguage of your wiki of all messages,
    // tooltips, and of the engine names.
    loadJS ('MediaWiki:Gadget-HotCat.js/local_defaults');
  } else {
    loadTrigger.loaded();
  }

  if (wgUserLanguage != 'en') {
    // Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
    if (window.hotcat_translations_from_commons && wgServer.indexOf ('/commons') < 0) {
      loadURI (
        ((wgServer.indexOf( "https://secure.wikimedia.org") === 0)
           ? '/wikipedia/commons/w/index.php?title='
           : '//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
      loadJS ('MediaWiki:Gadget-HotCat.js/' + 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.
  if (typeof (wgFormattedNamespaces) != 'undefined') {
    function autoLocalize (namespaceNumber, fallback) {
      function create_regexp_str (name)
      {
        if (!name || name.length === 0) return "";
        var regex_name = "";
        for (var i = 0; i < name.length; i++){
          var initial = name.substr (i, 1);
          var ll = initial.toLowerCase ();
          var ul = initial.toUpperCase ();
          if (ll == ul){
            regex_name += initial;
          } else {
            regex_name += '[' + ll + ul + ']';
          }
        }
        return regex_name.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1')
                         .replace (wikiTextBlankRE, wikiTextBlank);
      }

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

    if (wgFormattedNamespaces['14']) {
      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 = 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 = wgScript + '?';
    if (href.indexOf (script) === 0 || href.indexOf (wgServer + script) === 0 || wgServer.substring(0, 2) == '//' && href.indexOf (document.location.protocol + 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 && prefix.substring(0, 2) == '//') prefix = document.location.protocol + prefix; // Protocol-relative wgServer?
      if (href.indexOf (prefix) === 0)
        return decodeURIComponent (href.substring (prefix.length));
    }
    return null;
  }
  function hasClass (elem, name) {
    return (' ' + elem.className + ' ').indexOf (' ' + name + ' ') >= 0;
  }
  function capitalize (str) {
    if (!str || str.length === 0) return str;
    return str.substr(0, 1).toUpperCase() + str.substr (1);
  }
  function wikiPagePath (pageName) {
    // Note: do not simply use encodeURI, it doesn't encode '&', which might break if wgArticlePath actually has the $1 in
    // a query parameter.
    return wgArticlePath.replace('$1', encodeURIComponent (pageName).replace(/%3A/g, ':').replace(/%2F/g, '/'));
  }
  function substitute (str, map) {
    // Replace $1, $2, or ${key1}, ${key2} by values from map. $$ is replaced by a single $.
    return str.replace(
      /\$(\$|(\d+)|\{([^{}]+)\})/g
     ,function (match, dollar, idx, key) {
        if (dollar == '$') return '$';
        var k = key || idx;
        var replacement = typeof (map[k]) === 'function' ? map[k](match, k) : map[k];
        return typeof (replacement) === 'string' ? replacement : (replacement || match);
      }
    );
  }

  // Text modification

  var findCatsRE =
    new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(?:' + HotCat.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(HotCat.template_categories[category]){
      cat_regex = new RegExp ('\\{\\{' + wikiTextBlankOrBidi + '(' + HotCat.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi
                              + '(?:' + HotCat.template_categories[category] + ')'
                              + wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}', 'g'
                             );
    } else {
      var cat_name  = category.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
      var initial   = cat_name.substr (0, 1);
      cat_regex = new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi
                              + (initial == '\\' || !HotCat.capitalizePageNames
                                 ? initial
                                 : '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
                              + cat_name.substring (1).replace (wikiTextBlankRE, wikiTextBlank)
                              + wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]', 'g'
                             );
    }
    if (once) return cat_regex.exec (wikitext);
    var copiedtext = wikitext.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
                             .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
    var result = [];
    var curr_match = null;
    while ((curr_match = cat_regex.exec (copiedtext)) != null) {
      result.push ({match : curr_match});
    }
    result.re = cat_regex;
    return result; // An array containing all matches, with positions, in result[i].match
  }

  var interlanguageRE = null;

  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 = HotCat.category_canonical;
    var cat_point = -1; // Position of removed category;

    if (key) key = '|' + key;
    var keyChange = (toRemove && toAdd && toRemove == toAdd && toAdd.length > 0);
    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/g, toRemove)};
      } else {
        var before = wikitext.substring (0, matches[0].match.index);
        var after  = wikitext.substring (matches[0].match.index + matches[0].match[0].length);
        if (matches.length > 1) {
          // Remove all occurrences in after
          matches.re.lastIndex = 0;
          after = after.replace (matches.re, "");
        }
        if (toAdd) {
          nameSpace = matches[0].match[1] || nameSpace;
          if (key == null) key = matches[0].match[2]; // Remember the category key, if any.
        }
        // Remove whitespace (properly): strip whitespace, but only up to the next line feed.
        // If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
        // whitespace characters, insert a blank.
        var i = before.length - 1;
        while (i >= 0 && before.charAt (i) != '\n' && before.substr (i, 1).search (/\s/) >= 0) i--;
        var j = 0;
        while (j < after.length && after.charAt (j) != '\n' && after.substr (j, 1).search (/\s/) >= 0)
          j++;
        if (i >= 0 && before.charAt (i) == '\n' && (after.length === 0 || j < after.length && after.charAt (j) == '\n'))
          i--;
        if (i >= 0) before = before.substring (0, i+1); else before = "";
        if (j < after.length) after = after.substring (j); else after = "";
        if (before.length > 0 && before.substring (before.length - 1).search (/\S/) >= 0
            && after.length > 0 && after.substr (0, 1).search (/\S/) >= 0)
          before += ' ';
        cat_point = before.length;
        wikitext = before + after;
        if (!keyChange) {
          if(HotCat.template_categories[toRemove]) {
            summary.push (HotCat.messages.template_removed.replace (/\$1/g, toRemove));
          } else {
            summary.push (HotCat.messages.cat_removed.replace (/\$1/g, toRemove));
          }
        }
      }
    }
    if (toAdd && toAdd.length > 0) {
      var matches = find_category (wikitext, toAdd);
      if (matches && matches.length > 0) {
        return {text: wikitext, 'summary': summary, error : HotCat.messages.cat_exists.replace (/\$1/g, toAdd)};
      } else {
        var onCat = false;
        if (cat_point < 0) {
          var point = find_insertionpoint (wikitext);
          cat_point = point.idx;
          onCat = point.onCat;
        } else {
          onCat = true;
        }
        var newcatstring = '[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
        if (cat_point >= 0) {
          var suffix = wikitext.substring (cat_point);
          wikitext = wikitext.substring (0, cat_point) + (cat_point > 0 ? '\n' : "") + newcatstring + (!onCat ? '\n' : "");
          if (suffix.length > 0 && suffix.substr(0, 1) != '\n') {
            wikitext += '\n' + suffix;
          } else {
            wikitext += suffix;
          }
        } else {
          if (wikitext.length > 0 && wikitext.substr (wikitext.length - 1, 1) != '\n')
            wikitext += '\n';
          wikitext += '\n' + newcatstring;
        }
        if (keyChange) {
          var k = key || "";
          if (k.length > 0) k = k.substr (1);
          summary.push (substitute (HotCat.messages.cat_keychange, [null, toAdd, k]));
        } else {
          summary.push (HotCat.messages.cat_added.replace (/\$1/g, toAdd));
        }
        if (HotCat.uncat_regexp && !is_hidden) {
          var txt = wikitext.replace (HotCat.uncat_regexp, ""); // Remove "uncat" templates
          if (txt.length != wikitext.length) {
            wikitext = txt;
            summary.push (HotCat.messages.uncat_removed);
          }
        }
      }
    }
    return {text: wikitext, 'summary': summary, error: null};
  }

  // 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;
  }
  function addEvent (node, evt, f, capture)
  {
    if (window.jQuery && (!capture || !node.addEventListener)) window.jQuery (node).bind (evt, f);
    else if (node.attachEvent) node.attachEvent ('on' + evt, f);
    else if (node.addEventListener) node.addEventListener (evt, f, capture);
    else node['on' + evt] = f;
  }

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

  var commitButton = null;
  var commitForm   = null;
  var multiSpan    = null;

  var pageText     = null;
  var pageTime     = null;
  var pageWatched  = false;
  var watchCreate  = false;
  var watchEdit    = false;
  var minorEdits   = false;
  var editToken    = null;

  var is_rtl       = false;
  var serverTime   = null;

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

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

  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 () {
    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 initiateEdit (doEdit, failure) {
    // Must use Ajax here to get the user options and the edit token.
    getJSON ({
       uri : wgServer + wgScriptPath + '/api.php'
      ,data : 'format=json&action=query&titles=' + encodeURIComponent (wgPageName)
         + '&prop=info%7Crevisions%7Clanglinks&inprop=watched&intoken=edit&rvprop=content%7Ctimestamp&lllimit=500'
         + '&rvlimit=1&rvstartid=' + wgCurRevisionId
         + '&meta=siteinfo%7Cuserinfo&uiprop=options'
      ,success : function (json) { setPage(json); doEdit(failure); }
      ,error : function (req) { failure(req.status + ' ' + req.statusText); }
    });
  }

  function multiChangeMsg (count) {
    var msg = HotCat.messages.multi_change;
    if (typeof (msg) != 'string' && msg.length) {
      if (window.mediaWiki && window.mediaWiki.language && window.mediaWiki.language.convertPlural) {
        msg = window.mediaWiki.language.convertPlural (count, msg);
      } else {
        msg = msg[msg.length-1];
      }
    }
    return substitute (msg, [null, "" + count]);     
  }

  function performChanges (failure, singleEditor) {
    if (pageText === null) {
      failure (HotCat.messages.multi_error);
      return;
    }
    // Backwards compatibility after message change (added $2 to cat_keychange)
    if (HotCat.messages.cat_keychange.indexOf ('$2') < 0) {
      HotCat.messages.cat_keychange += '"$2"';
    }
    // 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.
    // Using the form, we can do (b) 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;
    if (singleEditor && !singleEditor.noCommit && !HotCat.no_autocommit && editToken) {
      commitForm.wpEditToken.value = editToken;
      action = commitForm.wpDiff;
      if (action) action.name = action.value = 'wpSave';
    } else {
      action = commitForm.wpSave;
      if (action) action.name = action.value = 'wpDiff';
    }
    var result = { text : pageText };
    var changed = [], added = [], deleted = [], changes = 0;
    var toEdit = !!singleEditor ? [singleEditor] : editors;
    var error = null;
    for (var i=0; i < toEdit.length; i++) {
      if (toEdit[i].state == CategoryEditor.CHANGED) {
        result = change_category (
            result.text
          , toEdit[i].originalCategory
          , toEdit[i].currentCategory
          , toEdit[i].currentKey
          , toEdit[i].currentHidden
        );
        if (!result.error) {
          changes++;
          if (!toEdit[i].originalCategory || toEdit[i].originalCategory.length === 0) {
            added.push (toEdit[i].currentCategory);
          } else {
            changed.push ({from : toEdit[i].originalCategory, to : toEdit[i].currentCategory});
          }
        } else if (error === null) {
          error = result.error;
        }
      } else if (   toEdit[i].state == CategoryEditor.DELETED
                 && toEdit[i].originalCategory
                 && toEdit[i].originalCategory.length > 0)
      {
        result = change_category (result.text, toEdit[i].originalCategory, null, null, false);
        if (!result.error) {
          changes++;
          deleted.push (toEdit[i].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';
    }
    if (changes === 0 && !singleEditor) return;
    // 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 || !!singleEditor) {
      if (changes == 1) {
        if (result.summary && result.summary.length > 0)
          commitForm.wpSummary.value = HotCat.messages.prefix + result.summary.join (HotCat.messages.separator) + HotCat.messages.using;
        commitForm.wpMinoredit.checked = HotCat.single_minor || minorEdits;
      } else if (changes > 1) {
        var summary = [];
        var shortSummary = [];
        // Deleted
        for (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 ('− ' + multiChangeMsg (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 ('+ ' + multiChangeMsg (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 ('± ' + multiChangeMsg (changed.length));
        }
        if (summary.length > 0) {
          summary = summary.join (HotCat.messages.separator);
          if (summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length) {
            summary = shortSummary.join (HotCat.messages.separator);
          }
          commitForm.wpSummary.value = HotCat.messages.prefix + summary + HotCat.messages.using;
        }
      }
    }
    commitForm.wpTextbox1.value = result.text;
    commitForm.wpStarttime.value = serverTime || currentTimestamp ();
    commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
    // Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
    commitForm.hcCommit.click();
  }

  function resolveMulti (toResolve, callback) {
    for (var 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 (var i = 0; i < toResolve.length; i++) {
      args += encodeURIComponent ('Category:' + toResolve[i].dabInput);
      if (i+1 < toResolve.length) args += '%7C';
    }
    getJSON({
      uri : wgServer + wgScriptPath + '/api.php'
     ,data : args
     ,success: function (json) { resolveRedirects (toResolve, json); callback (toResolve); }
     ,error: function (req) { if (!req) noSuggestions = true; callback (toResolve); }
    });
  }

  function resolveOne (page, toResolve) {
    var cats     = page.categories;
    var lks      = page.links;
    var is_dab   = false;
    var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
    var is_hidden = page.categoryinfo && typeof (page.categoryinfo.hidden) == 'string';
    for (var j = 0; j < toResolve.length; j++) {
      if (toResolve[j].dabInput != page.title.substring (page.title.indexOf (':') + 1)) continue;
      toResolve[j].currentHidden = is_hidden;
    }
    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 = armorUri(HotCat.existsYes);
        toResolve[j].text.value =
          titles[0] + (toResolve[j].currentKey != null ? '|' + toResolve[j].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 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 === 0) {
      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) {
            CategoryEditor.makeActive (firstDab);
          } else if (!dontChange) {
            initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
          }
        }
    );
  }

  var cat_prefix = null;
  var noSuggestions = false;
  var suggestionEngines = {
    opensearch :
      { uri     : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1' // $1 = search term
       ,handler : // Function to convert result of uri into an array of category names
          function (queryResult, queryKey) {
            if (   queryResult != 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 (queryResult, queryKey) {
            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 :
      // I don't understand why they didn't map cmnamespace=14 automatically to cmtype=subcat,
      // which gives better results and is faster.
      { uri     : '/api.php?format=json&action=query&list=categorymembers'
                 +(function (version) {
                     var m = version.match(/^(\d+)\.(\d+)/);
                     var major = 0, minor = 0;
                     if (m && m.length > 1) {
                       major = parseInt (m[1], 10);
                       minor = (m.length > 2 ? parseInt (m[2], 10) : 0);
                     }
                     if (major > 1 || major === 1 && minor > 17) return '&cmtype=subcat'; // Since MW1.18
                     return '&cmnamespace=14';
                   }
                  )(wgVersion)
                 +'&cmlimit=max&cmtitle=Category:$1'
       ,handler :
          function (queryResult, queryKey) {
            if (queryResult && queryResult.query && queryResult.query.categorymembers) {
              var titles = queryResult.query.categorymembers;
              for (var i = 0; i < titles.length; i++) {
                titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
              }
              return titles;
            }
            return null;
          }
      }
   ,parentcategories :
      { uri     : '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max'
       ,handler :
          function (queryResult, queryKey) {
            if (queryResult && queryResult.query && queryResult.query.pages) {
              for (var p in queryResult.query.pages) {
                if (queryResult.query.pages[p].categories) {
                  var titles = queryResult.query.pages[p].categories;
                  for (var i = 0; i < titles.length; i++) {
                    titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
                  }
                  return titles;
                }
              }
            }
            return null;
          }
      }
  };

  var suggestionConfigs = {
    searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}, show: true, temp: false, noCompletion : false}
   ,pagelist    : {name: 'Page list', engines: ['internalsearch'], 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}
  };

  function CategoryEditor () { this.initialize.apply (this, arguments); };
  CategoryEditor.UNCHANGED      = 0;
  CategoryEditor.OPEN           = 1; // Open, but no input yet
  CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
  CategoryEditor.CHANGED        = 3;
  CategoryEditor.DELETED        = 4;

  // IE6 sometimes forgets to redraw the list when editors are opened or closed.
  // Adding/removing a dummy element helps, at least when opening editors.
  CategoryEditor.dummyElement   = make ('\xa0', true);

  CategoryEditor.forceRedraw = function () {
    if (!is_ie6) return;
    if (CategoryEditor.dummyElement.parentNode) {
      document.body.removeChild (CategoryEditor.dummyElement);
    } else {
      document.body.appendChild (CategoryEditor.dummyElement);
    }
  }

  CategoryEditor.makeActive = function (toActivate) {
    if (toActivate.is_active) return;
    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;
    } else {
      if (toActivate.showsList) toActivate.displayList();
      if (toActivate.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 () { toActivate.setSelection (toActivate.lastSelection.start, toActivate.lastSelection.end); }
            ,1
          );
        } else {
          toActivate.setSelection (toActivate.lastSelection.start, toActivate.lastSelection.end);
        }
      }
    }
  };

  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 = bind (this.open, this);
        lk.appendChild (make (HotCat.links.add, true)); lk.title = HotCat.tooltips.add;
        this.linkSpan.appendChild (lk);
        span = make (newDOM ? 'li' : 'span');
        span.className = 'noprint';
        if (is_rtl) span.dir = 'rtl';
        span.appendChild (this.linkSpan);
        if (after)
          after.parentNode.insertBefore (span, after.nextSibling);
        else
          line.appendChild (span);
        this.normalLinks = null;
        this.undelLink = null;
        this.catLink = null;
      } else {
        if (is_rtl) span.dir = 'rtl';
        this.isAddCategory = false;
        this.catLink = span.firstChild;
        this.originalCategory = after;
        this.originalKey = (key && key.length > 1) ? key.substr(1) : null; // > 1 because it includes the leading bar
        this.originalExists   = !hasClass (this.catLink, 'new');
        // Create change and del links
        this.makeLinkSpan ();
        if (!this.originalExists && this.upDownLinks) this.upDownLinks.style.display = 'none';
        span.appendChild (this.linkSpan);
      }
      this.originalHidden     = is_hidden;
      this.line               = line;
      this.engine             = HotCat.suggestions;
      this.span               = span;
      this.currentCategory    = this.originalCategory;
      this.currentExists      = this.originalExists;
      this.currentHidden      = this.originalHidden;
      this.currentKey         = this.originalKey;
      this.state              = CategoryEditor.UNCHANGED;
      this.lastSavedState     = CategoryEditor.UNCHANGED;
      this.lastSavedCategory  = this.originalCategory;
      this.lastSavedKey       = this.originalKey;
      this.lastSavedExists    = this.originalExists;
      this.lastSavedHidden    = this.originalHidden;
      if (this.catLink && this.currentKey) {
        this.catLink.title = this.currentKey;
      }
      editors[editors.length] = this;
    },

    makeLinkSpan : function () {
      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 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 = 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) return 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);
      }
      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.text.focus();
              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
      var OK = make ('input'); OK.type = 'button';
      OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
      OK.onclick = bind (this.accept, this);
      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;

      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);
      addEvent (text, 'focus', bind (function () { CategoryEditor.makeActive (this); }, this));
      // 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.
      addEvent (text, (typeof text.onbeforedeactivate != 'undefined' && text.createTextRange) ? 'beforedeactivate' : 'blur', bind (this.saveView, this)); 
    },

    display : function (evt) {
      if (this.isAddCategory && !onUpload) {
        var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
      }
      if (!commitButton && !onUpload) {
        for (var i = 0; i < editors.length; i++) {
          if (editors[i].state != CategoryEditor.UNCHANGED) {
            setMultiInput();
            break;
          }
        }
      }
      if (!this.form) {
        this.makeForm ();
      }
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.currentCategory = this.lastSavedCategory;
      this.currentExists   = this.lastSavedExists;
      this.currentHidden   = this.lastSavedHidden;
      this.currentKey      = this.lastSavedKey;
      this.icon.src = armorUri(this.currentExists ? HotCat.existsYes : HotCat.existsNo);
      this.text.value = this.currentCategory + (this.currentKey != null ? '|' + this.currentKey : "");
      this.originalState = this.state;
      this.lastInput     = this.currentCategory;
      this.inputExists   = this.currentExists;
      this.state         = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
      this.lastSelection = {start: this.currentCategory.length, end: this.currentCategory.length};
      this.showsList = false;
      // Display the form
      if (this.catLink) this.catLink.style.display = 'none';
      this.linkSpan.style.display = 'none';
      this.form.style.display = 'inline';
      this.ok.disabled = false;
      // Kill the event before focussing, otherwise IE will kill the onfocus event!
      var result = evtKill (evt);
      CategoryEditor.makeActive (this);
      this.text.focus();
      this.text.readOnly = false;
      checkMultiInput ();
      return result;
    },

    show : function (evt, engine, readOnly) {
      var result = this.display (evt);
      var v = this.lastSavedCategory;
      if (v.length === 0) return result;
      this.text.readOnly = !!readOnly;
      this.engine = engine;
      this.textchange (false, true); // do autocompletion, force display of suggestions
      CategoryEditor.forceRedraw ();
      return result;
    },
 
    open : function (evt) {
      return this.show (evt, (this.engine && suggestionConfigs[this.engine].temp) ? HotCat.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 > 0) {
          this.catLink.title = this.currentKey;
        } else {
          this.catLink.title = null;
        }
      }
      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 ();
      CategoryEditor.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 ();
      var self = this;
      window.setTimeout (function () {delete self;}, 10);
    },

    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 === 0) {
        // It was a newly added category. Remove the whole editor.
        this.removeEditor();
      } else {
        // Redisplay the link...
        this.catLink.removeChild (this.catLink.firstChild);
        this.catLink.appendChild (make (this.currentCategory, true));
        this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
        this.catLink.title = this.currentKey;
        this.catLink.className = this.currentExists ? "" : 'new';
        this.catLink.style.backgroundColor = 'transparent';
        if (this.upDownLinks) this.upDownLinks.style.display = this.currentExists ? "" : 'none';
        checkMultiInput ();
      }
      return evtKill (evt);
    },

    inactivate : function () {
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.is_active = false;
    },

    acceptCheck : function (dontCheck) {
      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 (HotCat.capitalizePageNames) v = capitalize (v);
      this.lastInput = v;
      if (v.length === 0) {
        this.cancel ();
        return false;
      }
      if (!dontCheck
          && (   wgNamespaceNumber == 14 && v == wgTitle
              || HotCat.blacklist != null && HotCat.blacklist.test(v))
         ) {
        this.cancel ();
        return false;
      }
      this.currentCategory = v;
      this.currentKey = key;
      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/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 (HotCat.category_canonical + ':' + this.currentCategory);
      this.catLink.title = "";
      this.catLink.className = this.currentExists ? "" : 'new';
      this.lastSavedCategory = this.currentCategory;
      this.lastSavedKey      = this.currentKey;
      this.lastSavedExists   = this.currentExists;
      this.lastSavedHidden   = this.currentHidden;
      // Close form and redisplay category
      this.inactivate();
      this.form.style.display = 'none';
      this.catLink.title = this.currentKey;
      this.catLink.style.display = "";
      if (this.isAddCategory) {
        if (onUpload) {
          var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
        }
        this.isAddCategory = false;
        this.linkSpan.parentNode.removeChild (this.linkSpan);
        this.makeLinkSpan ();
        this.span.appendChild (this.linkSpan);
      }
      if (!this.undoLink) {
        // Append an undo link.
        var span = make ('span');
        var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = 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 ();
      CategoryEditor.forceRedraw ();
    },

    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
          || HotCat.blacklist != null && HotCat.blacklist.test (this.currentCategory)
         )
      {
        this.cancel ();
        return;
      }
      if (commitButton || onUpload) {
        this.close ();
      } else {
        this.close ();
        var self = this;
        initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {alert (msg);});
      }
    },

    remove : function (evt) {
      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 = HotCat.bg_changed;
        } catch (ex) {}
        this.originalState = this.state;
        this.state = CategoryEditor.DELETED;
        this.normalLinks.style.display = 'none';
        this.undelLink.style.display = "";
        checkMultiInput ();
      } else {
        if (onUpload) {
          // Remove this editor completely
          this.removeEditor ();
        } else {
          this.originalState = this.state;
          this.state = CategoryEditor.DELETED;
          this.noCommit = noCommit;
          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 = 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 () {
      var v = this.text.value || "";
      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, cleanKey) {
      var cb = callbackObj;
      var e  = engine;
      var v  = queryKey;
      var z  = cleanKey;
      var thisObj = this;

      function done () {
        cb.callsMade++;
        if (cb.callsMade === cb.nofCalls) {
          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;
          delete cb;
        }
      }

      getJSON ({
        uri : url
       ,success : function (json) {
          var titles = e.handler (json, z);
          if (titles && titles.length > 0) {
            if (cb.allTitles === null) {
              cb.allTitles = titles;
            } else {
              cb.allTitles = cb.allTitles.concat (titles);
            }
          }
          done();
        }
       ,error : function (req) {if (!req) noSuggestions = true; cb.dontCache = true; done(); }
      });           
    },

    callbackObj : null,

    textchange : function (dont_autocomplete, force) {
      // Hide all other lists
      CategoryEditor.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 > 0 && HotCat.blacklist != null && HotCat.blacklist.test (v);

      if (noSuggestions) {
        // No Ajax: just make sure the list is hidden
        if (this.list) this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        if (this.icon) this.icon.style.display = 'none';
        return;
      }

      if (v.length === 0) { this.showSuggestions([]); return; }
      if (this.callbackObj) this.callbackObj.cancelled = true;
      var engineName  = suggestionConfigs[this.engine] ? this.engine : 'combined';

      var cleanKey = v.replace(/[\u200E\u200F\u202A-\u202E]/g, "")
                      .replace(wikiTextBlankRE, ' ');
      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 = wgServer + 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) != 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) {
        var vLow = v.toLowerCase ();
        // Strip blacklisted categories
        if (HotCat.blacklist != null) {
          for (var i = 0; i < titles.length; i++) {
            if (HotCat.blacklist.test (titles[i])) {
              titles.splice(i, 1);
              i--;
            }
          }
        }
        titles.sort (
          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 = armorUri(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 = armorUri(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;
        }
      }
      // (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);
      }
      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 > HotCat.list_size ? HotCat.list_size : this.list.options.length);
      if (nofItems <= 1) nofItems = 2;
      this.list.size = nofItems;
      this.list.style.align    = is_rtl ? 'right' : 'left';
      this.list.style.zIndex   = 5;
      this.list.style.position = 'absolute';
      // Compute initial list position. First the height.
      var 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) {
        var s = 'scroll' + what;
        return (document.documentElement ? document.documentElement[s] : 0)
               || document.body[s] || 0;
      }
      function viewport (what) {
        if (is_webkit && !document.evaluate)
          return window['inner' + what]; // Safari < 3.0
        var s = 'client' + what;
        if (window.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};
      }

      var textPos = position (this.text);
      var nl = 0;
      var nt = 0;
      var offset = 0;
      // Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
      var 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.left  = '-10000px';
          this.engineSelector.style.top   = '0px';
          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 + 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.left = nl + 'px';
      if (this.engineName) {
        this.selectEngine (this.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 < textBoxWidth ) {
        this.list.style.width = textBoxWidth + '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) {
      if (newVal == actVal) return true;
      if (dontModify || newVal.indexOf (actVal) != 0) 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.
      if (!this.canSelect()) return false;
      // If we can't select properly, autocompletion would be a major annoyance to the user.
      this.text.focus();
      this.text.value = newVal + key;
      this.setSelection (actVal.length, newVal.length);
      return true;
    },

    canSelect : function () {
      return    this.text.setSelectionRange
               || this.text.createTextRange
               ||    typeof (this.text.selectionStart) != 'undefined'
                  && typeof (this.text.selectionEnd) != 'undefined';
    },

    setSelection : function (from, to) {
      // this.text must be focused (at least on IE)
      if (!this.text.value) return;
      if (this.text.setSelectionRange) {    // e.g. khtml
        this.text.setSelectionRange (from, to);
      } else if (typeof (this.text.selectionStart) != 'undefined') {
        if (from > this.text.selectionStart) {
          this.text.selectionEnd   = to;
          this.text.selectionStart = from;
        } else {
          this.text.selectionStart = from;
          this.text.selectionEnd   = to;
        }
      } else if (this.text.createTextRange) { // IE
        var new_selection = this.text.createTextRange();
        new_selection.move ('character', from);
        new_selection.moveEnd ('character', to - from);
        new_selection.select();
      }
    },

    getSelection : function () {
      var from = 0, to = 0;
      // this.text must be focused (at least on IE)
      if (!this.text.value) {
        // No text.
      } else if (typeof (this.text.selectionStart) != 'undefined') {
        from = this.text.selectionStart;
        to   = this.text.selectionEnd;
      } else if (document.selection && document.selection.createRange) { // IE
        var rng = document.selection.createRange().duplicate();
        if (rng.parentElement() === this.text) {
          try {
            var textRng = this.text.createTextRange();
            textRng.move('character', 0);
            textRng.setEndPoint('EndToEnd', rng);
            // We're in a single-line input box: no need to care about IE's strange
            // handling of line ends
            to = textRng.text.length;
            textRng.setEndPoint('EndToStart', rng);
            from = textRng.text.length;
          } catch (notFocused) {
            from = this.text.value.length; to = from; // At end of text
          }
        }
      }
      return {start: from, end: to};
    },

    saveView : function (evt) {
      this.lastSelection = this.getSelection ();
    },

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

    highlightSuggestion : function (dir) {
      if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
      var curr = this.list.selectedIndex;
      var tgt  = -1;
      if (dir === 0) {
        if (curr < 0 || curr >= this.list.options.length) return false;
        tgt = curr;
      } else {
        tgt = curr < 0 ? 0 : curr + dir;
        tgt = tgt < 0 ? 0 : tgt;
        if (tgt >= this.list.options.length) tgt = this.list.options.length - 1;
      }
      if (tgt != curr || dir === 0) {
        if (curr >= 0 && curr < this.list.options.length && dir != 0) this.list.options[curr].selected = false;
        this.list.options[tgt].selected = true;
        // Get current input text
        var v = this.text.value.split('|');
        var key = v.length > 1 ? '|' + v[1] : "";
        var completed = this.autoComplete (this.list.options[tgt].text, this.lastRealInput, key, false);
        if (!completed) {
          this.text.value = this.list.options[tgt].text + key;
        }
        this.lastInput = this.list.options[tgt].text;
        this.inputExists = true; // Might be wrong if from a dab list...
        if (this.icon) this.icon.src = armorUri(HotCat.existsYes);
        this.state = CategoryEditor.CHANGE_PENDING;
      }
      return true;
    },

    resetKeySelection : function () {
      if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
      var curr = this.list.selectedIndex;
      if (curr >= 0 && curr < this.list.options.length) {
        this.list.options[curr].selected = false;
        // Get current input text
        var v = this.text.value.split('|');
        var key = v.length > 1 ? '|' + v[1] : "";
        // ESC is handled strangely by some browsers (e.g., FF); somehow it resets the input value before
        // our event handlers ever get a chance to run.
        var result = v[0] != this.lastInput;
        if (v[0] != this.lastRealInput) {
          this.text.value = this.lastRealInput + key;
          result = true;
        }
        this.lastInput = this.lastRealInput;
        return result;
      }
      return false;
    }

  }; // 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 = (typeof (JSconfig) != 'undefined' && JSconfig.keys) ? JSconfig.keys : {};  
    HotCat.dont_add_to_watchlist =
      (typeof (window.hotcat_dont_add_to_watchlist) != 'undefined'
        ? !!window.hotcat_dont_add_to_watchlist
        : (typeof (config['HotCatDontAddToWatchlist']) != 'undefined'
            ? config['HotCatDontAddToWatchlist']
            : HotCat.dont_add_to_watchlist
          )
      );
    HotCat.no_autocommit =
      (typeof (window.hotcat_no_autocommit) != 'undefined'
        ? !!window.hotcat_no_autocommit
        : (typeof (config['HotCatNoAutoCommit']) != 'undefined'
            ? config['HotCatNoAutoCommit']
            : HotCat.no_autocommit
          )
      );
    HotCat.suggest_delay =   window.hotcat_suggestion_delay
                          || config['HotCatSuggestionDelay']
                          || HotCat.suggest_delay;
    HotCat.editbox_width =   window.hotcat_editbox_width
                          || config['HotCatEditBoxWidth']
                          || HotCat.editbox_width;
    HotCat.suggestions   =   window.hotcat_suggestions
                          || config['HotCatSuggestions']
                          || HotCat.suggestions;
    if (typeof (HotCat.suggestions) != 'string' || !suggestionConfigs[HotCat.suggestions])
      HotCat.suggestions = 'combined';
    HotCat.fixed_search  =
      (typeof (window.hotcat_suggestions_fixed) != 'undefined'
        ? !!window.hotcat_suggestions_fixed
        : (typeof (config['HotCatFixedSuggestions']) != 'undefined'
            ? config['HotCatFixedSuggestions']
            : HotCat.fixed_search
          )
      );
    HotCat.single_minor  =
      (typeof (window.hotcat_single_changes_are_minor) != 'undefined'
        ? !!window.hotcat_single_changes_are_minor
        : (typeof (config['HotCatMinorSingleChanges']) != 'undefined'
            ? config['HotCatMinorSingleChanges']
            : HotCat.single_minor
          )
      );
    HotCat.bg_changed    =   window.hotcat_changed_background
                          || config['HotCatChangedBackground']
                          || HotCat.bg_changed;
    HotCat.use_up_down   =
      (typeof (window.hotcat_use_category_links) != 'undefined'
        ? !!window.hotcat_use_category_links
        : (typeof (config['HotCatUseCategoryLinks']) != 'undefined'
            ? config['HotCatUseCategoryLinks']
            : HotCat.use_up_down
          )
      );
    HotCat.list_size =    window.hotcat_list_size
                       || config['HotCatListSize']
                       || HotCat.list_size;
    // Numeric input, make sure we have a numeric value
    HotCat.list_size = parseInt (HotCat.list_size, 10);
    if (isNaN (HotCat.list_size) || HotCat.list_size < 5) HotCat.list_size = 5;
    if (HotCat.list_size > 15) HotCat.list_size = 15;
    // Localize search engine names
    if (HotCat.engine_names) {
      for (var key in HotCat.engine_names) {
        if (suggestionConfigs[key] && HotCat.engine_names[key]) {
          suggestionConfigs[key].name = HotCat.engine_names[key];
        }
      }
    }
    // Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
    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 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 (   typeof (UFUI) != 'undefined'
        && typeof (UIElements) != 'undefined'
        && typeof (UFUI.getLabel) == 'function') {
      try {
        label = UFUI.getLabel ('wpCategoriesUploadLbl');
      } catch (ex) {
        label = null;
      }
    }
    if (!label) {
      labelCell.id = 'hotcatLabel';
      labelCell.appendChild (make (HotCat.categories, true));
    } else {
      labelCell.id = 'hotcatLabelTranslated';
      labelCell.appendChild (label);
    }
    labelCell.className           = 'mw-label';
    labelCell.style.textAlign     = 'right';
    labelCell.style.verticalAlign = 'middle';
    // Change the onsubmit handler
    var form = document.getElementById ('upload') || document.getElementById ('mw-upload-form');
    if (form) {
      var newRow = ip.insertRow (-1);
      newRow.appendChild (labelCell);
      newRow.appendChild (lineCell);
      form.onsubmit = (function (oldSubmit) {
        return function () {
          var do_submit = true;
          if (oldSubmit) {
            if (typeof (oldSubmit) == 'string')
              do_submit = eval (oldSubmit);
            else if (typeof (oldSubmit) == 'function')
              do_submit = oldSubmit.apply (form, arguments);
          }
          if (!do_submit) return false;
          closeForm ();
          // Copy the categories
          var eb =    document.getElementById ('wpUploadDescription')
                   || document.getElementById ('wpDesc');
          var addedOne = false;
          for (var i = 0; i < editors.length; i++) {
            var t = editors[i].currentCategory;
            if (!t) continue ;
            var key = editors[i].currentKey;
            var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
            // Only add if not already present
            var cleanedText = eb.value.replace(/<\!--(\s|\S)*?--\>/g, "")
                                      .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
            if (!find_category (cleanedText, t, true)) {
              eb.value += '\n' + new_cat;
              addedOne = true;
            }
          }
          if (addedOne) {
            // Remove "subst:unc" added by Flinfo if it didn't find categories
            eb.value = eb.value.replace(/\{\{subst:unc\}\}/g, "");
          }
          return true;
        };
      }) (form.onsubmit);
    }
  }

  var cleanedText = null;

  function isOnPage (span) {
    var catTitle = title (span.firstChild.getAttribute ('href', 2));
    if (!catTitle) return null;
    catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' ');
    if (HotCat.blacklist != null && HotCat.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 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 =   catLine                                                  // Special:Upload
             || document.getElementById ('mw-normal-catlinks')           // MW >= 1.13alpha
             || getElementsByClassName (document , 'p' , 'catlinks')[0]; // MW < 1.13
    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 = is_rtl ? 'right' : '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
    if (is_rtl) catLine.dir = 'rtl';

    // Create editors for all existing categories

    function createEditors (line, is_hidden) {
      var cats = line.getElementsByTagName ('li');
      if (cats.length > 0) {
        newDOM = true; line = cats[0].parentNode;
      } else {
        cats = line.getElementsByTagName ('span');
      }
      // Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
      var copyCats = new Array (cats.length);
      for (var i = 0; i < cats.length; i++) copyCats[i] = cats[i];
      var editor = null;
      for (var i = 0; i < copyCats.length; i++) {
        var test = isOnPage (copyCats[i]);
        if (test !== null && test.match !== null) {
          editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2], is_hidden);
        }
      }
      return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
    }

    var lastSpan = createEditors (catLine, false);
    // Create one to add a new category
    var editor = 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>' + HotCat.addmulti + '</a>)';
      var lk = multiSpan.getElementsByTagName ('a')[0];
      lk.onclick = function (evt) {setMultiInput (); checkMultiInput (); return evtKill (evt);};
      lk.title = HotCat.multi_tooltip;
      lk.style.cursor = 'pointer';
    }
    cleanedText = null;
    if (typeof (additionalWork) == 'function') additionalWork();
    setupCompleted.loaded(); // Trigger signal; execute registered functions
    if (window.jQuery) window.jQuery('body').trigger ('hotcatSetupCompleted');
  }

  function setPage (json) {
    if (json && json.query) {
      if (json.query.pages) {
        var page = json.query.pages[wgArticleId == 0 ? "-1" : "" + wgArticleId];
        if (page) {
          if (page.revisions && page.revisions.length > 0) {
            pageText = page.revisions[0]['*'];
            pageTime = page.revisions[0].timestamp.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 > 0) {
              interlanguageRE = new RegExp ('((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$');
            }
          }

        }
      }
      // 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, "");
      }
      // Userinfo
      if (json.query.userinfo && json.query.userinfo.options) {
        watchCreate = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchcreations == '1';
        watchEdit   = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchdefault == '1';
        minorEdits  = json.query.userinfo.options.minordefault == 1;
        // If the user has the "All edits are minor" preference enabled, we should honor that
        // for single category changes, no matter what the site configuration is.
        if (minorEdits) HotCat.single_minor = true;
      }
    }
  }

  function createCommitForm () {
    if (commitForm) return;
    var formContainer = make ('div');
    formContainer.style.display = 'none';
    document.body.appendChild (formContainer);
    var formText =
        '<form id="hotcatCommitForm" 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="wpEditToken" />'
      + '<input type="hidden" name="wpDiff" value="wpDiff" />'
      + '<input type="submit" name="hcCommit" value="hcCommit" />'
      + '</form>';
    formContainer.innerHTML = formText;
    commitForm = document.getElementById ('hotcatCommitForm');
  }

  function getPage () {
    // We know we have an article here.
    if (wgArticleId === 0) {
      // Doesn't exist yet.
      pageText = "";
      pageTime = null;
      setup (createCommitForm);
    } 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 = armorUri(url);
      s.type = 'text/javascript';
      HotCat.start = function (json) { setPage (json); setup (createCommitForm); };
      document.getElementsByTagName ('head')[0].appendChild (s);
      setupTimeout = window.setTimeout (function () {setup (createCommitForm);}, 4000); // 4 sec, just in case getting the wikitext takes longer.
    }
  }

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

  function really_run () {
    initialize ();

    if (is_rtl && is_ie6) return; // Disabled! IE6 with RTL is just too broken...
    if (!HotCat.upload_disabled && wgNamespaceNumber === -1 && wgCanonicalSpecialPageName == 'Upload' && wgUserName) {
      setup_upload ();
      setup (function () {
        // Check for state restoration once the setup is done otherwise, but before signalling setup completion
        if (   typeof (UploadForm) != 'undefined'
            && 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' || param('diff') != null || !can_edit() || HotCat.disable()) return;
      getPage ();
    }
  }

  // Legacy stuff

  function closeForm () {
    // Close all open editors without redirect resolution and other asynchronous stuff.
    for (var i = 0; i < editors.length; i++) {
      if (editors[i].state == CategoryEditor.OPEN) {
        editors[i].cancel();
      } else if (editors[i].state == CategoryEditor.CHANGE_PENDING) {
        editors[i].sanitizeInput ();
        var value = editors[i].text.value.split('|');
        var key   = null;
        if (value.length > 1) key = value[1];
        var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
        if (v.length === 0) {
          editors[i].cancel ();
        } else {
          editors[i].currentCategory = v;
          editors[i].currentKey = key;
          editors[i].currentExists = this.inputExists;
          editors[i].close ();
        }
      }
    }
  }

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

  function setState (state) {
    var cats = state.split ('\n');
    if (cats.length === 0) return null;
    if (initialized && editors.length == 1 && editors[0].isAddCategory) {
      // Insert new spans and create new editors for them.
      var newSpans = [];
      var before = editors.length == 1 ? editors[0].span : null;
      for (var i = 0; i < cats.length; i++) {
        if (cats[i].length === 0) continue;
        var cat = cats[i].split ('|');
        var key = cat.length > 1 ? cat[1] : null;
        cat = cat[0];
        var lk = make ('a'); lk.href = wikiPagePath (HotCat.category_canonical + ':' + cat);
        lk.appendChild (make (cat, true));
        lk.title = cat;
        var span = make ('span');
        span.appendChild (lk);
        if (i === 0) catLine.insertBefore (make (' ', true), before);
        catLine.insertBefore (span, before);
        if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
        newSpans.push ({element: span, title: cat, 'key': key});
      }
      // And change the last one...
      if (before) {
        before.parentNode.insertBefore (make (' | ', true), before);
      }
      var editor = null;
      for (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
  window.hotcat_get_state  = function () { return getState(); };
  window.hotcat_set_state  = function (state) { return setState (state); };
  window.hotcat_close_form = function () { closeForm (); };

  if (window.mediaWiki && window.mediaWiki.config) {
    // Make sure we don't get conflicts with AjaxCategories (core development that should one day
    // replace HotCat).
    window.mediaWiki.config.set('disableAJAXCategories', true);
  }
  if (window.jQuery) {
    window.jQuery(document).ready(run);
  } else {
    addOnloadHook (run);
  }
})();

} // end if (guard)
//</source>