לדלג לתוכן

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

מתוך צפונות ויקי
מאין תקציר עריכה
Update to V2.0
שורה 1: שורה 1:
//<source lang="javascript">
//<source lang="javascript">


if (typeof (hotcat_loaded) == 'undefined') {
/*
var hotcat_loaded = false; // Guard against double inclusions
  Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
var hotcat_running = 0 ;
  Supports multiple category changes, as well as redirect and disambiguation resolution. Also
var hotcat_last_v = "" ;
  plugs into the upload form. Search engines to use for the suggestion list are configurable, and
var hotcat_exists_yes = "http://upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png" ;
  can be selected interactively.
var hotcat_exists_no = "http://upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png" ;
var hotcat_upload = 0 ;
var hotcat_no_autocommit = 0;
var hotcat_old_onsubmit = null;
var hotcat_nosuggestions = false;
// hotcat_nosuggestions is set to true if we don't have XMLHttp! (On IE6, XMLHttp uses
// ActiveX, and the user may deny execution.) If true, no suggestions will ever be
// displayed, and there won't be any checking whether the category  exists.
// Lupo, 2008-01-20


var hotcat_suggestion_delay = 100;
  Author: [[User:Lupo]], April-May 2010
var hotcat_editbox_width    = 40;
  License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
// Fallbacks if we don't have JSconfig. Lupo, 2009-06-24
  Choose whichever license of these you like best :-)
*/


var hotcat_modify_blacklist = new Array (
// Globals:
"CC-" ,
//  (inline script on the page):
"GFDL" ,
//    wgNamespaceNumber, wgCanonicalSpecialPageName, wgNamespaceIds (optional), wgFormattedNamespaces (optional)
"PD"
//    wgScript, wgServer, wgArticlePath, wgScriptPath, wgAction, wgPageName, wgTitle, wgUserName, wgIsArticle,
) ;
//    wgArticleId
//  ajax.js
//    sajax_init_object
//  wikibits.js
//    addOnloadHook, window.ie6_bugs


// Localization. On MW 1.16+, this is automatically localized. On earlier MediaWiki
if (typeof (HotCat) == 'undefined') { // Guard against double inclusions
// installations, add localized namespace names manually, for instance for the Chinese
   
// Wikipedia, set it to hotcat_cnames_regexp="[Cc]ategory|分类|分類";
// Configuration stuff.
var hotcat_cnames_regexp="[Cc]ategory";
var HotCat = {
// The following is used for inserting new categories. Also localized automatically from
  // Localize these messages to the main language of your wiki.
// MW 1.16 on. When a category is replaced, the namespace prefix of the removed category
  messages :
// is used for the newly added category.
    { cat_removed  : 'removed [[Category:$1]]'
var hotcat_canonical="Category";
    ,cat_added    : 'added [[Category:$1]]'
// The following should be the same as [[MediaWiki:Categories]]. Localize manually.
    ,cat_keychange: 'new key for [[Category:$1]]: '
var hotcat_categories="Categories";
    ,cat_notFound : 'Category "$1" not found'
 
    ,cat_exists  : 'Category "$1" already exists; not added.'
function hotcat_remove_upload ( text ) {
    ,cat_resolved : ' (redirect [[Category:$1]] resolved)'
  var cats = document.getElementById ( "catlinks" ) ;
    ,uncat_removed: 'removed {{uncategorized}}'
  cats = cats.getElementsByTagName ( "span" ) ;
    ,using        : ' using [[Help:Gadget-HotCat|HotCat]]'
  for ( var i = 0 ; i < cats.length ; i++ ) {
    ,multi_change : '$1 categories'
    if (cats[i].hotcat_name && cats[i].hotcat_name == text) {
        // $1 is replaced by a number
      cats[i].parentNode.removeChild ( cats[i].nextSibling ) ;
    ,commit      : 'Save'
      cats[i].parentNode.removeChild ( cats[i] ) ;
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
      break ;
        // see localization hook below.
    ,ok          : 'OK'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
    ,cancel      : 'Cancel'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
    ,multi_error  : 'Could not retrieve the page text from the server. Therefore, your category changes '
                    +'cannot be saved. We apologize for the inconvenience.'
        // Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
     }
     }
  }
,category_regexp    : '[Cc]ategory'
}
  // 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
function hotcat_check_upload () {
  // Wikipedia, use '[Cc]ategory|[Kk]ategorie', or at the Chinese Wikipedia, use '[Cc]ategory|分类|分類'.
  // Don't do anything if not "Special:Upload", or user not logged in.
,category_canonical : 'Category'
  if ( wgNamespaceNumber != -1 || wgCanonicalSpecialPageName != "Upload" || wgUserName == null) return ;
  // The standard category name on your wiki. Is automatically localized correctly if you're running
  var ip = document.getElementById ( "wpWatchthis" ) ;
  // MediaWiki 1.16 or later; otherwise, set it to the preferred category name (e.g., "Kategorie").
  // Go to Special:Upload, choose a local file, enter a target file name without extension,
,categories        : 'Categories'
  // then submit: you get a page that is "Special:Upload", but that doesn't have any form!
  // Plural of category_canonical
  if (ip == null) return;
,disambig_category  : 'Disambiguation'
  var reupload = document.getElementById ('wpForReUpload');
  // Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
  var destFile = document.getElementById ('wpDestFile');
  // any items, but that contains links to other categories where stuff should be categorized. If you don't have
  if (   (reupload && !!reupload.value)
  // that concept on your wiki, set it to null.
      || (destFile && (destFile.disabled || destFile.readonly)))
,redir_category    : 'Category redirects'
    return; // re-upload form...
  // Any category in this category is deemed a (soft) redirect to some other category defined by the first link
  hotcat_upload = 1 ;
  // to another category. If your wiki doesn't have soft category redirects, set this to null.
  var tr = ip.parentNode.parentNode ;
,links : {change: '(±)', remove: '(-)', add: '(+)', restore: '(×)', undo: '(×)'}
  var ntr = document.createElement ( "tr" ) ;
  // The little modification links displayed after category names.
  var ntd = document.createElement ( "td" ) ;
,addmulti          : '<span>+<sup>+</sup></span>'
  var ntde = document.createElement ( "td" ) ;
  // The HTML content of the "enter multi-mode" link at the front.
  var catline = document.createElement ( "div" ) ;
,disable            :
   var np = document.createElement ( "p" ) ;
    function () { // Return true to disable HotCat
 
      return (  wgNamespaceNumber < 0   // Special pages; Special:Upload is handled differently
   ntde.setAttribute ('id', 'hotcatLabel');
              || wgNamespaceNumber == 10 // Templates
  var label = null;
              || typeof (wgNamespaceIds) != 'unknown'
  if (typeof (UFUI) != 'undefined' && typeof (UIElements) != 'undefined'
                && (   wgNamespaceNumber == wgNamespaceIds['creator']
      && typeof (UFUI.getLabel) == 'function') {
                    || wgNamespaceNumber == wgNamespaceIds['timedtext']
    try {
                    )
      label = UFUI.getLabel ('wpCategoriesUploadLbl');
            );
    } catch (ex) {
      label = null;
     }
     }
  }
,uncat_regexp : /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}/g
  if (label == null)
  // A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
    ntde.appendChild (document.createTextNode (hotcat_categories));
  // If not, set it to null.
  else {
,existsYes    : 'http://upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
    ntde.setAttribute ('id', 'hotcatLabelTranslated');
,existsNo    : 'http://upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png'
    // Change the ID to avoid that UploadForm tries to translate it again.
  // The images used for the little indication icon. Should not need changing.
    ntde.appendChild (label);
};
  }
  ntde.style.textAlign = "right" ;
  ntde.style.verticalAlign = "middle" ;
  catline.id = "catlinks" ;
  // On the upload form, the suggestion box appears at the very top of the page. That is because
  // the innermost enclosing div of the upload form (and its table) that has position "relative"
  // is the bodyContent div. Try to fix that by giving catline relative positioning, so absolute
  // positioning within should be relative to catline. Lupo, 2008-01-18
  catline.style.position ="relative";
  catline.style.textAlign = "left";
  // Otherwise, it looks bad in the Classic skin on the upload form. Lupo, 2008-05-16
  np.className = "catlinks" ;
  np.style.textAlign = "left";
  catline.appendChild ( np ) ;
  ntd.appendChild ( catline ) ;
  ntde.className = 'mw-label';
  ntr.appendChild ( ntde ) ;
  ntr.appendChild ( ntd ) ;


  // Add handler for submit (changed by Lupo, 2008-01-18)
if (wgUserLanguage != wgContentLanguage) importScript ('MediaWiki:Gadget-HotCat.js/' + wgUserLanguage);
  var form = document.getElementById ('upload');
// Localization hook to localize HotCat.messages.commit and HotCat.messages.multi_error. For German, the
  // Grrr... they changed the upload form!
// file would be "MediaWiki:Gadget-HotCat.js/de", and its contents could be for instance
  // http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/SpecialUpload.php?r1=32033&r2=32190
//
  if (!form) form = document.getElementById ('mw-upload-form');
// HotCat.messages.commit      = 'Speichern';
  if (form) {
// HotCat.messages.ok          = 'OK';
    hotcat_old_onsubmit = form.onsubmit;
// HotCat.messages.cancel      = 'Abbrechen';
    form.onsubmit = hotcat_on_upload;
// HotCat.messages.multi_error = 'Seitentext konnte nicht vom Server geladen werden. Die Änderungen können '
    tr.parentNode.insertBefore ( ntr , tr ) ; // Insert *above* "Watch this" box
//                              +'leider nicht gespeichert werden.';
    var optionsTable = document.getElementById ('mw-htmlform-options'); // MW 1.16wmf4
    if (optionsTable) optionsTable.width = '100%';
  }
}


function hotcat_on_upload () {
// No further changes should be necessary here. However, you should also localize the search engine names.
  // First, make sure that if we have an open category input form, we close it.
// Search in this file for "Localize the names here", and localize the names there.
  var input = document.getElementById ('hotcat_text');
  if (input != null) hotcat_closeform ();
(function () {
 
    
  var do_submit = true;
  // Call previous onsubmit handler, if any
  if (hotcat_old_onsubmit) {
    if (typeof hotcat_old_onsubmit == 'string')
      do_submit = eval (hotcat_old_onsubmit);
    else if (typeof hotcat_old_onsubmit == 'function')
      do_submit = hotcat_old_onsubmit ();
  }
  if (!do_submit) return false;
  // Only copy the categories if we do submit
  var cats = document.getElementById ( "catlinks" ) ;
  cats = cats.getElementsByTagName ( "span" ) ;
  var eb = document.getElementById ( "wpUploadDescription" )
          || document.getElementById ( "wpDesc" ); // New upload form
  for ( var i = 0 ; i < cats.length ; i++ ) {
    var t = cats[i].hotcat_name;
    if (!t) continue ;
    var key = cats[i].hotcat_key;
    var new_cat = "\[\[" + hotcat_canonical + ":" + t + (key ? "|" + key : "") + "\]\]" ;
    // Only add if not already present
    if (eb.value.indexOf (new_cat) < 0) eb.value += "\n" + new_cat ;
  }
  return true;
}
 
function hotcat () {
   // Disable in some namespaces
  if (  wgNamespaceNumber == 10 // Templates
      || typeof (wgNamespaceIds) != 'unknown'
        && (  wgNamespaceNumber == wgNamespaceIds['creator']
            || wgNamespaceNumber == wgNamespaceIds['timedtext']
            )
    )
  {
    return;
  }
   // First auto-localize the category names
   // First auto-localize the category names
   if (typeof (wgFormattedNamespaces) != 'undefined' && wgFormattedNamespaces['14']) {
   if (typeof (wgFormattedNamespaces) != 'undefined' && wgFormattedNamespaces['14']) {
שורה 165: שורה 112:
       if (!name || name.length == 0) return "";
       if (!name || name.length == 0) return "";
       var initial = name.substr (0, 1);
       var initial = name.substr (0, 1);
       name = name.substring(1).replace (/[ _]/g, "[ _]").replace(/([\\\^\$\.\?\*\+\(\)])/g, "\\$1");
       name = name.substring(1).replace (/[ _]/g, '[ _]').replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
       return '[' + initial.toLowerCase () + initial.toUpperCase () + ']' + name;
       return '[' + initial.toLowerCase () + initial.toUpperCase () + ']' + name;
     }
     }


     hotcat_canonical = wgFormattedNamespaces['14'];
     HotCat.category_canonical = wgFormattedNamespaces['14'];
     hotcat_cnames_regexp += '|' + create_regexp_str (hotcat_canonical);
     HotCat.category_regexp    += '|' + create_regexp_str (HotCat.category_canonical);
     for (var cat_name in wgNamespaceIds) {
     for (var cat_name in wgNamespaceIds) {
       if (  typeof (cat_name) == 'string'
       if (  typeof (cat_name) == 'string'
           && cat_name.toLowerCase () != hotcat_canonical.toLowerCase ()
           && cat_name.toLowerCase () != HotCat.category_canonical.toLowerCase ()
           && wgNamespaceIds[cat_name] == 14)
           && wgNamespaceIds[cat_name] == 14)
       {
       {
         hotcat_cnames_regexp += '|' + create_regexp_str (cat_name);
         HotCat.category_regexp += '|' + create_regexp_str (cat_name);
       }
       }
     }
     }
   }
   }
   // Note: although we use JSconfig for our user-preferences, these won't show up in your preference
 
   // page because gadgets are not loaded on Special:Preferences!
   // Utility functions. Yes, this duplicates some functionality that also exists in other places, but
   if (typeof (JSconfig) != 'undefined') {
   // to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
     JSconfig.registerKey('HotCatDelay', 100, 'HotCat autocompletion delay (ms):', 5);
  // these few operations here.
     JSconfig.registerKey('HotCatEditBoxWidth', 40, 'Width of Input box of HotCat (# of characters):', 5);
  function bind (func, target) {
    var f = func, tgt = target;
    return function () { return f.apply (tgt, arguments); };
  }
  function make (arg, literal) {
    if (!arg) return null;
    return literal ? document.createTextNode (arg) : document.createElement (arg);
  }
   function param (name, uri) {
    if (typeof (uri) == 'undefined' || uri === null) uri = document.location.href;
     var re = RegExp ('[&?]' + name + '=([^&#]*)');
     var m = re.exec (uri);
    if (m && m.length > 1) return decodeURIComponent(m[1]);
    return null;
   }
   }
 
   function title (href) {
   if ( hotcat_check_action() ) return ; // Edited page, reloading anyway
    if (!href) return null;
  if (hotcat_loaded) return; // Guard against double inclusions
    var script = wgScript + '?';
  hotcat_loaded = true;
    if (href.indexOf (script) == 0 || href.indexOf (wgServer + script) == 0) {
  hotcat_check_upload () ;
       // href="/w/index.php?title=..."
 
      return param ('title', href);
  function can_edit ()
    } else {
  {
      // href="/wiki/..."
    var container = null;
       var prefix = wgArticlePath.replace ('$1', "");
    switch (skin) {
      if (href.indexOf (prefix) != 0) prefix = wgServer + prefix; // Fully expanded URL?
       case 'cologneblue':
      if (href.indexOf (prefix) == 0)
        container = document.getElementById ('quickbar');
         return decodeURIComponent (href.substring (prefix.length));
        // 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 (   hotcatGetParamValue ('title', lks[i].href) == wgPageName
              && hotcatGetParamValue ('action', lks[i].href) == 'edit')
            return true;
        }
         return false;
      default:
        // all modern skins:
        return document.getElementById ('ca-edit') != null;
     }
     }
     return false;
     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);
   }
   }
  if(    (!can_edit () && !hotcat_upload)          // User has no permission to edit
      || wgAction != 'view'                        // User is editing or previewing or...
      || wgNamespaceNumber == -1 && !hotcat_upload) // Special page other than Special:Upload
  return;
    
    
   if (!wgIsArticle && !hotcat_upload) return;      // Diff pages...
   // Text modification
   // Note that wgIsArticle is also set to true for category, talk, user, etc. pages: anything that
    
  // can be edited. It is false for diff pages, special pages, and ...
   var findCatsRE =
 
     new RegExp ('\\[\\[\\s*(?:' + HotCat.category_regexp + ')\\s*:\[^\\]\]+\\]\\]', 'g');
   var visible_cats =  
    
     document.getElementById ('mw-normal-catlinks') ||          // MW 1.13alpha
   function replaceByBlanks (match) {
    getElementsByClassName ( document , "p" , "catlinks" ) [0]; // MW < 1.13 && Special:Upload
     return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
  var hidden_cats =
    document.getElementById ('mw-hidden-catlinks');
  if (visible_cats == null) {
    // Insert an empty category line
    var footer = null;
    if (hidden_cats == null) {
      footer = getElementsByClassName (document , "div" , "printfooter")[0];
      if (!footer) return; // Don't know where to insert the category line
    }
    visible_cats = document.createElement ('div');
    visible_cats.setAttribute ('id', 'mw-normal-catlinks');
    var label = document.createElement ('a');
    label.setAttribute ('href', wgArticlePath.replace (/\$1/, 'Special:Categories'));
    label.setAttribute ('title', 'Special:Categories');
    label.appendChild (document.createTextNode (hotcat_categories));
    visible_cats.appendChild (label);
    visible_cats.appendChild (document.createTextNode (':'));
    var container = (hidden_cats ? hidden_cats.parentNode : document.getElementById ('catlinks'));
    if (!container) {
      container = document.createElement ('div');
      container.id = 'catlinks';
      footer.parentNode.insertBefore (container, footer.nextSibling);
    }
    container.className = 'catlinks';
    container.style.display = "";
    if (!hidden_cats) {
      container.appendChild (visible_cats);
    } else {
      container.insertBefore (visible_cats, hidden_cats);
    }
  } // end if no categories
 
  visible_cats.style.position = 'relative';
  hotcat_modify_existing ( visible_cats ) ;
   hotcat_append_add_span ( visible_cats ) ;
 
  // Check for state restoration (Lupo, 2008-02-06)
   if (  hotcat_upload
      && typeof (UploadForm) != 'undefined'
      && typeof (UploadForm.previous_hotcat_state) != 'undefined'
      && UploadForm.previous_hotcat_state != null)
    UploadForm.previous_hotcat_state = hotcat_set_state (UploadForm.previous_hotcat_state);
}
 
function hotcat_append_add_span ( catline ) {
  var span_add = document.createElement ( "span" ) ;
  if ( catline.getElementsByTagName('span')[0] )
     catline.appendChild (document.createTextNode (" | "));
  else if (catline.firstChild)
    catline.appendChild (document.createTextNode (' '));
  catline.appendChild ( span_add );
  hotcat_create_span ( span_add );
}
 
String.prototype.ucFirst = function () {
  if (this.length < 1) return this;
  return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
}
 
function hotcat_is_on_blacklist ( cat_title ) {
  if ( !cat_title ) return 0 ;
  // cat_title = cat_title.split(":",2).pop() ; // Not needed anymore: we work without 'Category:'
  for ( var i = 0 ; i < hotcat_modify_blacklist.length ; i++ ) {
    if ( cat_title.substr ( 0 , hotcat_modify_blacklist[i].length ) == hotcat_modify_blacklist[i] ) return 1 ;
   }
   }
  return 0 ;
}


function hotcat_modify_span ( span , i ) {
  function find_category (wikitext, category, once) {
  //var cat_title = span.firstChild.getAttribute ( "title" ) ;
    var cat_name  = category.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
  // This fails with MW 1.13alpha if the category is a redlink, because MW 1.13alpha appends
    var initial   = cat_name.substr (0, 1);
   // [[MediaWiki:Red-link-title]] to the category name... it also fails if the category name
    var cat_regex = new RegExp ('\\[\\[\\s*(' + HotCat.category_regexp + ')\\s*:\\s*'
  // contains "&" (because that is represented by &amp; in the XHTML both in the title and in
                                + (initial == '\\'
  // the link's content (innerHTML). Extract the category name from the href instead:
                                  ? initial
  var cat_title = null;
                                  : '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
  var classes  = " " + span.firstChild.className + " ";
                                + cat_name.substring (1).replace (/[ _]/g, '[ _]')
  var href      = span.firstChild.getAttribute ('href', 2);
                                + '\\s*(\\|.*?)?\\]\\]', 'g'
  // Extra param "2" is ignored on W3C compliant browsers. It's for IE only. Note:
                              );
  // span.firstChild.href is the normalized URL, getAttribute ('href') should be the text from
    if (once) return cat_regex.exec (wikitext);
  // the XHTML source, but IE somehow (a) also returns a full URL with server part, and (b)
    var copiedtext = wikitext.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
  // IE6 insists on wrongly decoding encoded UTF-8 characters ("K%C3%B6ln-Riehl" becomes
                            .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
  // "Köln-Riehl"). The work-around is to use the special IE variant with the extra parameter,
    var result = [];
  // which Microsoft says returns the simple string as found in the XHTML. See their docu at
     var curr_match = null;
  // http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx .
     while ((curr_match = cat_regex.exec (copiedtext)) != null) {
  if (!href) return;
       result.push ({match : curr_match});
  if (classes && classes.indexOf (' new ') >= 0) { // href="/w/index.php?title=...&action=edit"
    cat_title = hotcatGetParamValue ('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) {
       cat_title = decodeURIComponent (href.substring (prefix.length));
     }
     }
    result.re = cat_regex;
    return result; // An array containing all matches, with positions, in result[i].match
   }
   }
  if (!cat_title) return;
  // Strip namespace, replace _ by blank
  cat_title = cat_title.substring (cat_title.indexOf (':') + 1).replace (/_/g, ' ');


   var remove_link = document.createElement ( "a" ) ;
   function change_category (wikitext, toRemove, toAdd, key) {
  // Set the href to a dummy value to make sure we don't move if somehow the onclick handler
  // is bypassed.
  remove_link.href = "#catlinks";
  remove_link.onclick = hotcat_remove;
  remove_link.appendChild ( document.createTextNode ( "(−)" ) ) ;
  span.appendChild ( document.createTextNode ( " " ) ) ;
  span.appendChild ( remove_link ) ;


  if ( hotcat_is_on_blacklist ( cat_title ) ) return ;
    function find_insertionpoint (wikitext) {       
  var mod_id = "hotcat_modify_" + i ;
      var copiedtext = wikitext.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
  var modify_link = document.createElement ( "a" ) ;
                              .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
  modify_link.id = mod_id ;
      // Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
  modify_link.href = "javascript:hotcat_modify(\"" + mod_id + "\");" ;
      var index = -1;
  modify_link.appendChild ( document.createTextNode ( "(±)" ) ) ;
      findCatsRE.lastIndex = 0;
  span.appendChild ( document.createTextNode ( " " ) ) ;
      while (findCatsRE.exec(copiedtext) != null) index = findCatsRE.lastIndex;  
  span.appendChild ( modify_link ) ;
      // We should try to find interwiki links here, but that's for later.
  //Store the extracted category name in our own new property of the span DOM node
      return index;
  span.hotcat_name = cat_title;
    }
}


function hotcat_modify_existing ( catline ) {
    var summary  = [];
  var spans = catline.getElementsByTagName ( "span" ) ;
    var nameSpace = HotCat.category_canonical;
  for ( var i = 0 ; i < spans.length ; i++ ) {
    var cat_point = -1; // Position of removed category;
    hotcat_modify_span ( spans[i] , i ) ;
 
    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', 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) 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' && 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) 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 = '\n[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
        if (cat_point >= 0) {
          wikitext = wikitext.substring (0, cat_point) + newcatstring + wikitext.substring (cat_point);
        } else {
          wikitext += newcatstring;
        }
        if (keyChange) {
          summary.push (HotCat.messages.cat_keychange.replace ('$1', toAdd) + '"' + key.substring(1) + '"');
        } 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};
   }
   }
}
function hotcat_getEvt (evt) {
  return evt || window.event || window.Event; // Gecko, IE, Netscape
}
    
    
function hotcat_evt2node (evt) {
  if (wgAction == 'edit') {
  var node = null;
    // Legacy code based on URI parameters, can add/remove/change only one single category. Still
  try {
    // used for single-category changes, and newly to supply a default edit summary for multi category
     var e = hotcat_getEvt (evt);
    // changes.   
     node = e.target;
    var toRemove = param ('hotcat_removecat');
     if (!node) node = e.srcElement;
     var toAdd    = param ('hotcat_newcat');
  } catch (ex) {
     if (toAdd) {
     node = null;
      toAdd = capitalize (toAdd.replace (/_/g, ' ').replace (/^\s+|\s+$/g, ""));
      if (toAdd.length == 0) toAdd = null;
    }
     if (toRemove) {
      toRemove = capitalize (toRemove.replace (/_/g, ' ').replace (/^\s+|\s+$/g, ""));
      if (toRemove.length == 0) toRemove = null;
    }
    if (toAdd || toRemove) {
      addOnloadHook (function () {
        if (!document.editform || !document.editform.wpTextbox1) return;
        var 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     = 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;
   }
   }
   return node;
    
}
  // The real HotCat UI
 
 
function hotcat_evtkeys (evt) {
  function evtKeys (e) {
  var code = 0;
    e = e || window.event || window.Event; // W3C, IE, Netscape
  try {
     var code = 0;
     var e = hotcat_getEvt (evt);
     if (typeof (e.ctrlKey) != 'undefined') { // All modern browsers
     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
       if (e.ctrlKey)  code |= 1;
      // as a ctrl-click, too.
       if (e.ctrlKey || e.metaKey)  code |= 1;
       if (e.shiftKey) code |= 2;
       if (e.shiftKey) code |= 2;
     } else if (typeof (e.modifiers) != 'undefined') { // Netscape...
     } else if (typeof (e.modifiers) != 'undefined') { // Netscape...
       if (e.modifiers & Event.CONTROL_MASK) code |= 1;
       if (e.modifiers & (Event.CONTROL_MASK | Event.META_MASK)) code |= 1;
       if (e.modifiers & Event.SHIFT_MASK)   code |= 2;
       if (e.modifiers & Event.SHIFT_MASK) code |= 2;
     }
     }
  } catch (ex) {
    return code;
   }
   }
   return code;
   function evtKill (e) {
}
     e = e || window.event || window.Event; // W3C, IE, Netscape
 
function hotcat_killEvt (evt)
{
  try {
     var e = hotcat_getEvt (evt);
     if (typeof (e.preventDefault) != 'undefined') {
     if (typeof (e.preventDefault) != 'undefined') {
       e.preventDefault ();
       e.preventDefault ();
שורה 395: שורה 347:
     } else
     } else
       e.cancelBubble = true;
       e.cancelBubble = true;
  } catch (ex) {
    return false;
   }
   }
}
    
    
function hotcat_remove (evt) {
   var catLine      = null;
   var node = hotcat_evt2node (evt);
   var onUpload    = false;  
   if (!node) return false;
   var editors      = [];
   // Get the category name from the original link to the category, which is at
    
   // node.parentNode.firstChild (the DOM structure here is
   var commitButton = null;
   // <span><a...>Category</a> <a...>(−)</a>...</span>).
   var commitForm  = null;
   var cat_title = node.parentNode.hotcat_name;  
   var multiSpan    = null;
   if ( hotcat_upload ) {
 
    hotcat_remove_upload ( cat_title ) ;
  var pageText     = null;
     hotcat_killEvt (evt);
  var pageTime     = null;
     return false;
   var watchCreate  = false;
  }
   var watchEdit    = false;
   var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
   var minorEdits   = false;
            + '&action=edit';
 
   if (hotcat_evtkeys (evt) & 1) // CTRL pressed?
   var is_rtl      = false;
    editlk = editlk + '&hotcat_nocommit=1';
   hotcat_killEvt (evt);
   document.location = editlk + '&hotcat_removecat=' + encodeURIComponent (cat_title);
   return false;
}


function hotcatGetParamValue(paramName, h) {
  function setMultiInput () {
  if (typeof h == 'undefined' ) { h = document.location.href; }
    if (commitButton || onUpload) return;
  var cmdRe=RegExp('[&?]'+paramName+'=([^&]*)');
    commitButton = make ('input');
  var m=cmdRe.exec(h);
    commitButton.type  = 'button';
  if (m) {
    commitButton.value = HotCat.messages.commit;
    try {
    commitButton.onclick = multiSubmit;
       return decodeURIComponent(m[1]);
    if (multiSpan) {
    } catch (someError) {}
      multiSpan.parentNode.replaceChild (commitButton, multiSpan);
    } else {
      catLine.appendChild (commitButton);
    }
    // Get the preferences, so that we can set wpWatchthis correctly later on. Must use Ajax here.
    if (wgUserName) {
      var request = sajax_init_object ();     
      request.open
        ('GET', wgServer + wgScriptPath + '/api.php?format=json&action=query&meta=userinfo&uiprop=options', true);
      request.onreadystatechange =
        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 && 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;
            }
          }
        };
      request.setRequestHeader ('Pragma', 'cache=yes');
       request.setRequestHeader ('Cache-Control', 'no-transform');
      request.send (null);       
    }
   }
   }
   return null;
    
}
  function currentTimestamp () {
 
    var now = new Date();
// New. Code by Lupo & Superm401, added by Lupo, 2008-02-27
    var ts  = "" + now.getUTCFullYear();
function hotcat_find_category (wikitext, category)
    function two (s) { return s.substr (s.length - 2); }
{
    ts = ts
  var cat_name  = category.replace(/([\\\^\$\.\?\*\+\(\)])/g, "\\$1");
      + two ('0' + (now.getUTCMonth() + 1))
  var initial  = cat_name.substr (0, 1);
      + two ('0' + now.getUTCDate())
  var cat_regex = new RegExp ("\\[\\[\\s*(" + hotcat_cnames_regexp + ")\\s*:\\s*"
      + two ('00' + now.getUTCHours())
                              + (initial == "\\"
      + two ('00' + now.getUTCMinutes())
                                ? initial
      + two ('00' + now.getUTCSeconds());
                                : "[" + initial.toUpperCase() + initial.toLowerCase() + "]")
     return ts;
                              + cat_name.substring (1).replace (/[ _]/g, "[ _]")
                              + "\\s*(\\|.*?)?\\]\\]", "g"
                            );
  var result = new Array ();
  var curr_match  = null;
  while ((curr_match = cat_regex.exec (wikitext)) != null) {
     result [result.length] = {match : curr_match};
   }
   }
  return result; // An array containing all matches, with positions, in result[i].match
}


// New. Code by TheDJ, 2008-03-12. Comment and nowiki skipping by Lupo, 2010-01-31.
  function performChanges () {
function hotcat_find_insertionpoint (wikitext)
    // Don't use the edit API or LAPI, it's always bothersome to report back errors like edit
{
    // conflicts. Instead, make one remote call (blocking, because we can't continue anyway if
  function replaceByBlanks (match)
    // it doesn't succeed), getting the page text. Perform the changes on the text, then construct
  {
    // a form to submit all this as a diff.
     return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
    if (pageText === null) {
      var request = sajax_init_object ();
      var uri    = wgServer + wgScriptPath
                  + '/api.php?format=json&action=query&titles=' + encodeURIComponent (wgPageName)
                  + '&prop=info%7Crevisions&rvprop=content%7Ctimestamp';
      request.open ('GET', uri, false); // Yes, synchronous
      request.send (null);
      if (request.status == 200 && request.responseText && request.responseText.charAt(0) == '{') {
        setPage (eval ('(' + request.responseText + ')'));
      }
    }
    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) {
        result = change_category (result.text, editors[i].originalCategory, null, null);
        if (!result.error) {
          changes++;
          deleted.push (editors[i].originalCategory);
        }
      }
    }
    // Fill in the form and submit it
    commitForm.wpMinoredit.checked = minorEdits;
    commitForm.wpWatchthis.checked = wgArticleId == 0 && watchCreate || watchEdit;
    if (wgArticleId > 0) {
      if (changes == 1) {
        if (result.summary && result.summary.length > 0)
          commitForm.wpSummary.value = result.summary.join ('; ') + HotCat.messages.using;
        commitForm.wpMinoredit.checked = true;
      } else if (changes > 1) {
        var summary = [];
        if (deleted.length == 1)
          summary.push ('-[[' + HotCat.category_canonical + ':' + deleted[0] + ']]');
        else if (deleted.length > 1)
          summary.push ('- ' + HotCat.messages.multi_change.replace ('$1', "" + deleted.length));
        if (added.length == 1)
          summary.push ('+[[' + HotCat.category_canonical + ':' + added[0] + ']]');
        else if (added.length > 1)
          summary.push ('+ ' + HotCat.messages.multi_change.replace ('$1', "" + added.length));
        if (changed.length == 1) {
          if (changed[0].from != changed[0].to) {
            summary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + ']]→[['
                        + HotCat.category_canonical + ':' + changed[0].to + ']]');
          } else {
            summary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + ']]');
          }
        } else if (changed.length > 1) {
          summary.push ('± ' + HotCat.messages.multi_change.replace ('$1', "" + changed.length));
        }
        if (summary.length > 0)
          commitForm.wpSummary.value = summary.join ('; ') + HotCat.messages.using;
      }
    }
    commitForm.wpTextbox1.value = result.text;
    commitForm.wpStarttime.value = currentTimestamp ();
    commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
    commitForm.submit();
   }
   }
  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 re = new RegExp("\\[\\[\\s*(?:" + hotcat_cnames_regexp + ")\\s*:\[^\\]\]+\\]\\]", "ig" );
  var index = -1;
  while( re.exec(copiedtext) != null ) index = re.lastIndex;
  //we should try to find interwiki links here, but that's for later.
  return index;
}


// All redirects to Template:Uncategorized
  function resolveMulti (toResolve, callback) {
var hotcat_uncat_regex =
    for (var i = 0; i < toResolve.length; i++) {
  /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}/g;
      toResolve[i].dab = null;
 
      toResolve[i].dabInput = toResolve[i].lastInput;
// Rewritten (nearly) from scratch. Lupo, 2008-02-27
    }
function hotcat_check_action () {
    if (noSuggestions) {
  var ret = 0;
      callback (toResolve);
  if (wgAction != 'edit') return ret; // Not an edit page, so not our business...
      return;
  if (!document.editform || !document.editform.wpTextbox1) return ret; // No edit form??
    }
  var summary = new Array () ;
    var request = sajax_init_object ();
  var t = document.editform.wpTextbox1.value ;
    if (!request) {
  var prevent_autocommit = 0;
      noSuggestions = true;
  if (   (typeof hotcat_no_autocommit != "undefined" && hotcat_no_autocommit)
       callback (toResolve);
       || hotcatGetParamValue ('hotcat_nocommit') == '1')
      return;
    prevent_autocommit = 1;
    }
 
    var url = wgServer + wgScriptPath + '/api.php';
  var cat_rm    = hotcatGetParamValue ('hotcat_removecat');
    // Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
  var cat_add  = hotcatGetParamValue ('hotcat_newcat');
    // category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
  var comment  = hotcatGetParamValue ('hotcat_comment') || "";
    var args = 'action=query&prop=info%7Clinks%7Ccategories&plnamespace=14&pllimit=50'
  var cat_key  = hotcatGetParamValue ('hotcat_sortkey');
            + '&cllimit=' + (toResolve.length * 10) // Category limit is global, link limit is per page
  var cat_name  = hotcat_canonical;
            + '&format=json&titles=';
  var cat_point = -1; // Position of removed category;
     for (var i = 0; i < toResolve.length; i++) {
 
       args += encodeURIComponent ('Category:' + toResolve[i].dabInput);
  if (cat_key != null) cat_key = '|' + cat_key;
       if (i+1 < toResolve.length) args += '%7C';
  if (cat_rm != null && cat_rm.length > 0) {
     }
    var matches = hotcat_find_category (t, cat_rm);
    if (url.length + args.length + 1 > 2000) { // Lowest common denominator: IE has a URI length limit of 2083
     if (!matches || matches.length == 0) {
       request.open ('POST', url, true);
       alert ('Category "' + cat_rm + '" not found; maybe it is in a template?');
       request.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
       prevent_autocommit = 1;
     } else if (matches.length > 1) {
       alert ('Category "' + cat_rm
            + "\" found several times; don't know which occurrence to remove.");
       prevent_autocommit = 1;
     } else {
     } else {
       if (cat_add != null && cat_add.length > 0 && matches[0].match.length > 1) {
       url += '?' + args; args = null;
         cat_name = matches[0].match[1] || cat_name;
      request.open ('GET', url, true);
         if (cat_key == null) cat_key  = matches[0].match[2]; // Remember the category key, if any.
    }
    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 cats    = page.categories;
    var lks      = page.links;
    var is_dab  = false;
    var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
    if (!is_redir && cats && (HotCat.disambig_category || HotCat.redir_category)) {
      for (var c = 0; c < cats.length; c++) {
         var cat = cats[c]['title'];
        // Strip namespace prefix
         if (cat) {
          cat = cat.substring (cat.indexOf (':') + 1).replace(/_/g, ' ');
          if (cat == HotCat.disambig_category) {
            is_dab = true; break;
          } else if (cat == HotCat.redir_category) {
            is_redir = true; break;
          }
        }
       }
       }
      var t1 = t.substring (0, matches[0].match.index);
      var t2 = t.substring (matches[0].match.index + matches[0].match[0].length);
      // 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 = t1.length - 1;
      while (i >= 0 && t1.charAt (i) != '\n' && t1.substr (i, 1).search (/\s/) >= 0) i--;
      var j = 0;
      while (j < t2.length && t2.charAt (j) != '\n' && t1.substr (j, 1).search (/\s/) >= 0) j++;
      if (i >= 0 && t1.charAt (i) == '\n' && j < t2.length && t2.charAt (j) == '\n')
        i--;
      if (i >= 0) t1 = t1.substring (0, i+1); else t1 = "";
      if (j < t2.length) t2 = t2.substring (j); else t2 = "";
      if (t1.length > 0 && t1.substring (t1.length - 1).search (/\S/) >= 0
          && t2.length > 0 && t2.substr (0, 1).search (/\S/) >= 0)
        t1 = t1 + ' ';
      cat_point = t1.length;
      t = t1 + t2;
      summary.push ( "removed \[\[:Category:" + cat_rm + "\]\]" ) ;
      ret = 1;
     }
     }
  }
    if (!is_redir && !is_dab) return;
  if (cat_add != null && cat_add.length > 0) {
    if (!lks || lks.length == 0) return;
     var matches = hotcat_find_category (t, cat_add);
     var titles = [];
     if (matches && matches.length > 0) {
     for (var i = 0; i < lks.length; i++) {
       alert ('Category "' + cat_add + '" already exists; not added.');
       if (   lks[i]['ns'] == 14                            // Category namespace
       prevent_autocommit = 1;
          && lks[i]['title'] && lks[i]['title'].length > 0) // Name not empty
    } else {
       {
      var insertionpoint = hotcat_find_insertionpoint (t);
        // Internal link to existing thingy. Extract the page name and remove the namespace.
      if (insertionpoint < 0) {
        var match = lks[i]['title'];
        // No categories found. If we removed one earlier, insert at that position.
        titles.push (match.substring (match.indexOf (':') + 1));
        insertionpoint = cat_point;
        if (is_redir) break;
       }
       }
      var newcatstring = '\n\[\[' + cat_name + ':' + cat_add + (cat_key || "") + '\]\]';
    }
       if (insertionpoint >= 0) {
    for (var j = 0; j < toResolve.length; j++) {
         t = t.substring (0, insertionpoint) + newcatstring + t.substring (insertionpoint);
      if (toResolve[j].dabInput != page.title.substring (page.title.indexOf (':') + 1)) continue;
       if (titles.length > 1) {
         toResolve[j].dab = titles;
       } else {
       } else {
         t = t + newcatstring;
         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 : "");
       }
       }
      summary.push ("added \[\[Category:" + cat_add + "\]\]" + comment);
      var t2 = t.replace(hotcat_uncat_regex, ""); // Remove "uncat" templates
      if (t2.length != t.length) {
        t = t2;
        summary.push ( "removed {{uncategorized}}" ) ;
      }
      ret = 1;
     }
     }
   }
   }
   if (ret) {
 
     document.editform.wpTextbox1.value = t ;
   function resolveRedirects (toResolve, params) {
     document.editform.wpSummary.value = summary.join( "; " )
     if (!params || !params.query || !params.query.pages) return;    
                                      + " using [[Help:Gadget-HotCat|HotCat]]" ;
     for (var p in params.query.pages) resolveOne (params.query.pages[p], toResolve);
     document.editform.wpMinoredit.checked = true ;
  }
    if (!prevent_autocommit) {
 
       // Hide the entire edit section so as not to tempt the user into editing...
  function multiSubmit () {
      var content =   document.getElementById ("bodyContent")      // "monobook" skin
    var toResolve = [];
                    || document.getElementById ("mw_contentholder") // "modern" skin
     for (var i = 0; i < editors.length; i++) {
                    || document.getElementById ("article");         // classic skins
       if (editors[i].state == CategoryEditor.CHANGE_PENDING || editors[i].state == CategoryEditor.OPEN)
      if (content) content.style.display = "none" ;
        toResolve.push (editors[i]);
       document.editform.submit ();
    }
    if (toResolve.length == 0) {
       performChanges ();
      return;
     }
     }
    resolveMulti (
        toResolve
      , function (resolved) {
          var firstDab = null;
          var dontChange = false;
          for (var i = 0; i < resolved.length; i++) {
            if (resolved[i].lastInput != resolved[i].dabInput) {
              // We didn't disable all the open editors, but we did asynchronous calls. It is
              // theoretically possible that the user changed something...
              dontChange = true;
            } else {
              if (resolved[i].dab) {
                if (!firstDab) firstDab = resolved[i];
              } else {
                if (resolved[i].acceptCheck(true)) resolved[i].commit();
              }
            }
          }
          if (firstDab) {
            CategoryEditor.makeActive (firstDab);
          } else if (!dontChange) {
            performChanges ();
          }
        }
    );
   }
   }
  return ret;
}


function hotcat_clear_span ( span_add ) {
  var noSuggestions = false;
  while ( span_add.firstChild ) span_add.removeChild ( span_add.firstChild ) ;
  var suggestionEngines = {
}
    opensearch :
      { uri    : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=$1' // $1 = search term
      ,handler : // Function to convert result of uri into an array of category names
          function (responseText, queryKey) {
            if (responseText.charAt (0) != '[') return null;
            var queryResult = eval ('(' + responseText + ')');
            if (queryResult != null && queryResult.length == 2 && queryResult[0] == queryKey) {
              var titles = queryResult[1];
              for (var i = 0; i < titles.length; i++) {
                titles[i] = titles[i].substring (titles[i].indexOf (':') + 1); // rm namespace
              }
              return titles;
            }
            return null;     
          }
      }
    ,internalsearch :
      { uri    : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1'
      ,handler :
          function (responseText, queryKey) {
            if (responseText.charAt (0) != '{') return null;
            var queryResult = eval ('(' + responseText + ')');
            if (queryResult && queryResult.query && queryResult.query.allpages) {
              var titles = queryResult.query.allpages;
              for (var i = 0; i < titles.length; i++) {
                titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
                if (titles[i].indexOf (queryKey) != 0) {
                  titles.splice (i, 1); // Doesn't start with the query key
                  i--;
                }
              }
              return titles;
            }
            return null;
          }
      }
  };


function hotcat_create_span ( span_add ) {
   // Localize the names here.
   hotcat_clear_span ( span_add ) ;
   var suggestionConfigs = {
  var a_add = document.createElement ( "a" ) ;
    searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}}
   var a_text = document.createTextNode ( "(+)" ) ;
  ,pagelist    : {name: 'Page list', engines: ['internalsearch'], cache: {}}
  span_add.id = "hotcat_add" ;
  ,combined    : {name: 'Combined search', engines: ['opensearch', 'internalsearch'], cache: {}}
  a_add.href = "javascript:hotcat_add_new()" ;
   };
  a_add.appendChild ( a_text ) ;
   span_add.appendChild ( a_add ) ;
}


function hotcat_modify ( link_id ) {
  function CategoryEditor () { this.initialize.apply (this, arguments); };
   var link = document.getElementById ( link_id ) ;
   CategoryEditor.UNCHANGED      = 0;
   var span = link.parentNode ;
  CategoryEditor.OPEN          = 1; // Open, but no input yet
   var catname = span.hotcat_name;
   CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
  CategoryEditor.CHANGED        = 3;
   CategoryEditor.DELETED        = 4;
    
    
   while ( span.firstChild.nextSibling ) span.removeChild ( span.firstChild.nextSibling ) ;
   CategoryEditor.makeActive = function (toActivate) {
   span.firstChild.style.display = "none" ;
    for (var i = 0; i < editors.length; i++) {
  hotcat_create_new_span ( span , catname ) ;
      if (editors[i] != toActivate) editors[i].inactivate ();
  hotcat_last_v = "" ;
    }
   hotcat_text_changed () ; // Update icon
    toActivate.is_active = true;
}
    if (toActivate.dab) {
      toActivate.showSuggestions (toActivate.dab, false, null, true); // do autocompletion, no key, no engine selector
      toActivate.dab = null;
    }
  };
 
  CategoryEditor.prototype = {
   
    initialize : function (line, span, after, key) {
      // If a span is given, 'after' is the category title, otherwise it may be an element after which to
      // insert the new span. 'key' is likewise overloaded; if a span is given, it is the category key (if
      // known), otherwise it is a boolean indicating whether a bar shall be prepended.
      if (!span) {
        this.isAddCategory = true;
        // Create add span and append to catLinks
        this.originalCategory = "";
        this.originalKey = null;
        this.originalExists  = false;
        span = make ('span');
        span.className = 'noprint';
        if (key) {
          span.appendChild (make (' | ', true));
          if (after) {
            after.parentNode.insertBefore (span, after.nextSibling);
            after = after.nextSibling;
          } else {
            line.appendChild (span);
          }
        } else if (line.firstChild) {
          span.appendChild (make (' ', true));
          line.appendChild (span);
        }
        this.linkSpan = make ('span');
        this.linkSpan.className = 'noprint';
        var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
        lk.appendChild (make (HotCat.links.add, true));         
        this.linkSpan.appendChild (lk);
        span = make ('span');
        span.className = 'noprint';
        if (is_rtl) span.dir = 'rtl';
        span.appendChild (this.linkSpan);
        if (after)
          after.parentNode.insertBefore (span, after.nextSibling);
        else
          line.appendChild (span);
        this.normalLinks = null;
        this.undelLink = null;
        this.catLink = null;
      } else {
        if (is_rtl) span.dir = 'rtl';
        this.isAddCategory = false;
        this.catLink = span.firstChild;
        this.originalCategory = after;
        this.originalKey = (key && key.length > 1) ? key.substr(1) : null; // > 1 because it includes the leading bar
        this.originalExists   = !hasClass (this.catLink, 'new');
        // Create change and del links
        this.makeLinkSpan ();
        span.appendChild (this.linkSpan);
      }
      this.line              = line;
      this.engine            = HotCat.suggestions;
      this.span              = span;
      this.currentCategory    = this.originalCategory;
      this.currentExists      = this.originalExists;
      this.currentKey        = this.originalKey;
      this.state              = CategoryEditor.UNCHANGED;
      this.lastSavedState    = CategoryEditor.UNCHANGED;
      this.lastSavedCategory  = this.originalCategory;
      this.lastSavedKey      = this.originalKey;
      this.lastSavedExists    = this.originalExists;
      editors[editors.length] = this;
    },
   
    makeLinkSpan : function () {
      this.normalLinks = make ('span');
      var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.remove, this);
      lk.appendChild (make (HotCat.links.remove, true));
      this.normalLinks.appendChild (make (' ', true));
      this.normalLinks.appendChild (lk);
      lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
      lk.appendChild (make (HotCat.links.change, true));
      this.normalLinks.appendChild (make (' ', true));
      this.normalLinks.appendChild (lk);
      this.linkSpan = make ('span');
      this.linkSpan.className = 'noprint';
      this.linkSpan.appendChild (this.normalLinks);
      this.undelLink = make ('span');
      this.undelLink.style.display = 'none';
      lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.restore, this);
      lk.appendChild (make (HotCat.links.restore, true));
      this.undelLink.appendChild (make (' ', true));
      this.undelLink.appendChild (lk);
      this.linkSpan.appendChild (this.undelLink);
    },
   
    open : 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) {
        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) {
                  // 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 ();
                  // 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
                  this.state = CategoryEditor.CHANGE_PENDING;
                  var self = this;
                  window.setTimeout (function () {self.textchange (dont_autocomplete);}, HotCat.suggest_delay);
                }
              }
            ,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 (evt.keyCode == 13) this.accept (evt);
              }
            ,this
            );
          // And handle continued pressing of arrow keys
          text.onkeypress = bind (function (evt) {this.keyCount++; return this.processKey (evt);}, this);
        }
        text.onfocus = bind (function () { CategoryEditor.makeActive (this); }, this);
        this.text = text;
       
        this.icon = make ('img');
       
        var list = null;
        if (!noSuggestions) {
          list = make ('select');
          list.onclick    = bind (function () { if (this.setValueFromList ()) this.textchange (); }, this);
          list.ondblclick = bind (function (e) { if (this.setValueFromList ()) this.accept (e); }, this);
          list.onchange = bind (function (e) { this.setValueFromList (); 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) {
              var opt = make ('option');
              opt.value = key;
              if (key == this.engine) opt.selected = true;
              opt.appendChild (make (suggestionConfigs[key].name, true));
              engineSelector.appendChild (opt);
            }
            engineSelector.onchange = bind (
              function () {
                this.engine = this.engineSelector.options[this.engineSelector.selectedIndex].value;
                this.textchange (true, true); // Don't autocomplete, force re-display of list
              }
            ,this
            );
            this.engineSelector = engineSelector;
          }
        }
        this.list = list;
       
        function button_label (id, defaultText) {
          var label = null;
          if (   onUpload
              && typeof (UFUI) != 'undefined'
              && typeof (UIElements) != 'undefined'
              && typeof (UFUI.getLabel) == 'function') {
            try {
              label = UFUI.getLabel (id, true);
              // Extract the plain text. IE doesn't know that Node.TEXT_NODE == 3
              while (label && label.nodeType != 3) label = label.firstChild;
            } catch (ex) {
              label = null;
            }
          }
          if (!label || !label.data) return defaultText;
          return label.data;   
        }


function hotcat_add_new () {
        var OK = make ('input'); OK.type = 'submit';
  var span_add = document.getElementById ( "hotcat_add" ) ;
        OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
   hotcat_clear_span ( span_add ) ;
        this.ok = OK;
  hotcat_last_v = "" ;
       
  hotcat_create_new_span ( span_add , "" ) ;
        var cancel = make ('input'); cancel.type = 'button';
}
        cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
        cancel.onclick = bind (this.cancel, this);
        this.cancelButton = cancel;
       
        if (list) form.appendChild (list);
        if (this.engineSelector) form.appendChild (this.engineSelector);
        form.appendChild (text);
        if (!noSuggestions) form.appendChild (this.icon);
        form.appendChild (OK);
        form.appendChild (cancel);
        form.style.display = 'none';
        this.span.appendChild (form);
      }
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.currentCategroy = this.lastSavedCategory;
      this.currentExits    = this.lastSavedExists;
      this.currentKey      = this.lastSavedKey;
      this.icon.src = this.currentExists ? HotCat.existsYes : HotCat.existsNo;
      this.text.value = this.currentCategory + (this.currentKey ? '|' + this.currentKey : "");
      this.originalState = this.state;
      this.lastInput    = this.currentCategory;
      this.inputExists   = this.currentExists;
      this.state        = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
      // Display the form
      if (this.catLink) this.catLink.style.display = 'none';
      this.linkSpan.style.display = 'none';
      this.form.style.display = 'inline';
      CategoryEditor.makeActive (this);
      // Kill the event before focussing, otherwise IE will kill the onfocus event!
      var result = evtKill (evt);
      this.text.focus();
      return result;
    },


function hotcat_button_label (id, defaultText)
    cancel : function () {
{
      if (this.isAddCategory && !onUpload) {
  var label = null;
        this.removeEditor(); // We added a new adder when opening
  if (hotcat_upload
        return;
       && typeof (UFUI) != 'undefined'
      }
       && typeof (UFUI.getLabel) == 'function') {
      // Close, re-display link
     try {
       if (this.list) this.list.style.display = 'none';
       label = UFUI.getLabel (id, true);
       if (this.engineSelector) this.engineSelector.style.display = 'none';
       // Extract the plain text. IE doesn't know that Node.TEXT_NODE == 3
      this.form.style.display = 'none';
       while (label && label.nodeType != 3) label = label.firstChild;
      if (this.catLink) this.catLink.style.display = "";
     } catch (ex) {
      this.linkSpan.style.display = "";
       label = null;
      this.state = this.originalState;
    }
      this.currentCategory = this.lastSavedCategory;
  }
      this.currentKey      = this.lastSavedKey;
  if (label == null || !label.data) return defaultText;
      this.currentExists  = this.lastSavedExists;
  return label.data;  
    },
}
 
     removeEditor : function () {
       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;
        }
      }
      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.lastSavedCategory = this.originalCategory;
      this.lastSavedKey = this.originalKey;
      this.lastSavedExists = this.originalExists;
      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 = wgArticlePath.replace ('$1', 'Category:' + this.currentCategory);
        this.catLink.title = "";
        this.catLink.className = this.currentExists ? "" : 'new';
      }
      return evtKill (evt);
    },


function hotcat_create_new_span ( thespan , init_text ) {
    inactivate : function () {
  var form = document.createElement ( "form" ) ;
      if (this.list) this.list.style.display = 'none';
  form.method = "post" ;
      if (this.engineSelector) this.engineSelector.style.display = 'none';
  form.onsubmit = function () { hotcat_ok(); return false; } ;
      this.is_active = false;
  form.id = "hotcat_form" ;
    },
  form.style.display = "inline" ;


  var list = null;
    acceptCheck : function (dontCheck) {
    
      this.sanitizeInput ();
  if (!hotcat_nosuggestions) {
      var value = this.text.value.split('|');
    // Only do this if we may actually use XMLHttp...
      var key   = null;
    list = document.createElement ( "select" ) ;
      if (value.length > 1) key = value[1];
    list.id = "hotcat_list" ;
      var v = capitalize (value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, ""));
    list.onclick = function ()
      this.lastInput = v;
      if (v.length == 0) {
        this.cancel ();
        return false;
      }
      if (  !dontCheck
          && (  (  v == this.lastSavedCategory
                  && (  key == this.lastSavedKey
                      || key === null && this.lastSavedKey.length == 0
                      || this.lastSavedKey === null && key.length == 0
                    )
                )
              || wgNamespaceNumber == 14 && v == wgTitle
            )
        )
       {
       {
         var l = document.getElementById("hotcat_list");
         this.cancel ();
         if (l != null) {
        return false;
          var text = document.getElementById("hotcat_text");
      }
          var v = text.value.split('|');
      this.currentCategory = v;
          text.value = l.options[l.selectedIndex].text + (v.length > 1 ? '|' + v[1] : "");
      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', 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 = wgArticlePath.replace ('$1', 'Category:' + this.currentCategory);
      this.catLink.title = "";
      this.catLink.className = this.currentExists ? "" : 'new';
      this.lastSavedCategory = this.currentCategory;
      this.lastSavedKey      = this.currentKey;
      this.lastSavedExists  = this.currentExists;
      // Close form and redisplay category
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.form.style.display = 'none';
      this.catLink.style.display = "";
      if (this.isAddCategory) {
        if (onUpload) {
          var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
         }
         }
         hotcat_text_changed();
         this.isAddCategory = false;
       };
        this.linkSpan.parentNode.removeChild (this.linkSpan);
     list.ondblclick = function (evt)
        this.makeLinkSpan ();
        this.span.appendChild (this.linkSpan);
       }
      if (!this.undoLink && !onUpload) {
        // 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));
        span.appendChild (make (' ', true));
        span.appendChild (lk);
        this.normalLinks.appendChild (span);
        this.undoLink = span;
      }
      this.linkSpan.style.display = "";
      this.state = CategoryEditor.CHANGED;
     },
   
    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
                  || this.originalKey === null && this.currentKey.length == 0
                )
            )
          || wgNamespaceNumber == 14 && this.currentCategory == wgTitle
        )
       {
       {
         var l = document.getElementById("hotcat_list");
        this.cancel ();
         if (l != null) {
        return;
           var text = document.getElementById("hotcat_text");
      }
           var v = text.value.split('|');
      if (commitButton || onUpload) {
           text.value = l.options[l.selectedIndex].text + (v.length > 1 ? '|' + v[1] : "");
        this.close ();
      } else {
        if (this.list) this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        // Execute change from this.originalCategory to this.currentCategory|this.currentKey,
         var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
                  + '&action=edit';
        var url = editlk + '&hotcat_newcat=' + encodeURIComponent (this.currentCategory); 
        if (this.currentKey) 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) {
      this.doRemove (evtKeys (evt) & 1);
      return evtKill (evt);
    },
   
    doRemove : function (noCommit) {
      if (this.isAddCategory) { // Empty input on adding a new category
        this.cancel ();
         return;
      }
      if (!commitButton && !onUpload) {
        for (var i = 0; i < editors.length; i++) {
           if (editors[i].state != CategoryEditor.UNCHANGED) {
            setMultiInput();
            break;
           }
        }
      }
      if (commitButton) {
        this.catLink.style.textDecoration = 'line-through';
        this.originalState = this.state;
        this.state = CategoryEditor.DELETED;
        this.normalLinks.style.display = 'none';
        this.undelLink.style.display = "";
      } else {
        if (onUpload) {
          // Remove this editor completely
          this.removeEditor ();
        } else {
           // Execute single category deletion.
          var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
                    + '&action=edit';
          if (noCommit || HotCat.no_autocommit) editlk += '&hotcat_nocommit=1';
          document.location =
            editlk + '&hotcat_removecat=' + encodeURIComponent (this.originalCategory);
         }
         }
        // Don't call text_changed here if on upload form: hotcat_ok will remove the list
      }
        // anyway, so we must not ask for new suggestions since show_suggestions might
    },
        // raise an exception if it tried to show a no longer existing list.
   
        // Lupo, 2008-01-20
    restore : function (evt) {
        if (!hotcat_upload) hotcat_text_changed();
      // Can occur only if we do have a commit button and are not on the upload form
         hotcat_ok(hotcat_evtkeys (evt) & 1); // CTRL pressed?
      this.catLink.style.textDecoration = "";
       };
      this.state = this.originalState;
     list.style.display = "none" ;
      this.normalLinks.style.display = "";
  }
      this.undelLink.style.display = 'none';
 
      return evtKill (evt);
  var text = document.createElement ( "input" ) ;
    },
  var default_width =
   
    (typeof (JSconfig) != 'undefined'
    // Internal operations
    ? JSconfig.keys['HotCatEditBoxWidth']
   
    : hotcat_editbox_width
    setValueFromList : function (idx) {
    );
      if (typeof (idx) == 'undefined') idx = this.list.selectedIndex;
  var default_delay =
      if (idx >= 0 && idx < this.list.options.length) {
    (typeof (JSconfig) != 'undefined'
        var v = this.text.value.split ('|');
    ? JSconfig.keys['HotCatDelay']
         this.text.value = this.list.options[idx].text + (v.length > 1 ? '|' + v[1] : "");
    : hotcat_suggestion_delay
        this.inputExists = true; // Might be wrong if from a dab list...
    );
        if (this.icon) this.icon.src = HotCat.existsYes;
  if (default_delay < 0) default_delay = 0;
        return true;
  text.size = (default_width < 40 ? 40 : default_width);
       }
  text.id = "hotcat_text" ;
      return false;
  text.type = "text" ;
     },
  text.value = init_text ;
   
  text.onkeyup =
    sanitizeInput : function () {
    function ()
      var v = this.text.value || "";
    {
      v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
      window.setTimeout ("hotcat_text_changed ();", default_delay);
      var re = new RegExp ('^(' + HotCat.category_regexp + '):');
     };
      if (re.test (v)) v = v.substring (v.indexOf (':') + 1);
      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;
     },


  var exists = null;
    makeCall : function (url, callbackObj, engine, queryKey) {
  if (!hotcat_nosuggestions) {
      var cb = callbackObj;
    exists = document.createElement ( "img" ) ;
      var e  = engine;
    exists.id = "hotcat_exists" ;
      var v  = queryKey;
    exists.src = hotcat_exists_no ;
      var r  = sajax_init_object ();
  }
      cb.requests.push (r);
      r.open('GET', url, true);
      r.onreadystatechange =
        bind (
          function () {
            if (r.readyState == 4) {
              if (r.status != 200) cb.dontCache = true;
              if (r.status == 200 && r.responseText != null) {
                var titles = e.handler (r.responseText, v);
                if (titles && titles.length > 0) {
                  if (cb.allTitles == null) {
                    cb.allTitles = titles;
                  } else {
                    cb.allTitles = cb.allTitles.concat (titles);
                  }
                }
              }
              cb.callsMade++;
            }
            if (cb.callsMade == cb.nofCalls) {
              if (!cb.dontCache && !suggestionConfigs[cb.engineName].cache[v]) {
                suggestionConfigs[cb.engineName].cache[v] = cb.allTitles;
              }
              if (!cb.cancelled) this.showSuggestions (cb.allTitles, cb.noCompletion, v);
              if (cb === this.callbackObj) this.callbackObj = null;
              delete cb;
            }
          }
        ,this
        );
      r.setRequestHeader ('Pragma', 'cache=yes');
      r.setRequestHeader ('Cache-Control', 'no-transform');
      r.send (null);
    },


  var OK = document.createElement ( "input" ) ;
    callbackObj : null,
  OK.type = "button" ;
  OK.value = hotcat_button_label ('wpOkUploadLbl', 'OK') ;
  OK.onclick = function (evt) { hotcat_ok (hotcat_evtkeys (evt) & 1); };


  var cancel = document.createElement ( "input" ) ;
    textchange : function (dont_autocomplete, force) {
  cancel.type = "button" ;
      // Hide all other lists
  cancel.value = hotcat_button_label ('wpCancelUploadLbl', 'Cancel') ;
      CategoryEditor.makeActive (this);
  cancel.onclick = hotcat_cancel ;
      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;
      }
     
      // 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
      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 engine  = suggestionConfigs[this.engine] ? this.engine : 'combined';


  if (list != null) form.appendChild ( list ) ;
      if (suggestionConfigs[engine].cache[v]) {
  form.appendChild ( text ) ;
        this.showSuggestions (suggestionConfigs[engine].cache[v], dont_autocomplete, v);
  if (exists != null) form.appendChild ( exists ) ;
        return;
  form.appendChild ( OK ) ;
      }
  form.appendChild ( cancel ) ;
  thespan.appendChild ( form ) ;
  text.focus () ;
}


function hotcat_sanitize_input () {
      var engines = suggestionConfigs[engine].engines;
  var text = document.getElementById ( "hotcat_text" ) ;
      this.callbackObj =
  if (!text) return;
        {allTitles: null, requests: [], callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engine};  
  var v = text.value || "";
      for (var j = 0; j < engines.length; j++) {
  v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
        engine = suggestionEngines[engines[j]];
  var re = new RegExp ('^(' + hotcat_cnames_regexp + '):');
        var url = wgServer + wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (v));
  if (re.test (v)) {
        this.makeCall (url, this.callbackObj, engine, v);
    v = v.substring (v.indexOf (':') + 1);
      }   
  }
    },
  v = v.ucFirst();
   
  // Only update the input field if there is a difference. IE8 appears to reset the selection
    showSuggestions : function (titles, dontAutocomplete, queryKey, noEngine) {
  // and place the cursor at the front upon reset, which makes our autocompletetion become a
      this.dab = null;
  // nuisance. FF and IE6 don't seem to have this problem.
      if (!this.list) return;
  if (text.value != null && text.value != v)
      if (noSuggestions) {
    text.value = v;
        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;
      }
      if (!noEngine) {
        noEngine = this.engineSelector == null;
      } else {
        if (this.engineSelector) this.engineSelector.style.display = 'none';
      }
      if (queryKey) {
        if (this.lastInput.indexOf (queryKey) != 0) return;
        if (this.lastQuery && this.lastInput.indexOf (this.lastQuery) == 0 && this.lastQuery.length > queryKey.length)
          return;
      }
      this.lastQuery = queryKey;
     
      if (!titles || titles.length == 0) {
        if (this.list) this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        if (this.icon) this.icon.src = HotCat.existsNo;
        this.inputExists = false;
        return;
      }
                   
      // Get current input text
      var v = this.text.value.split('|');
      var key = v.length > 1 ? '|' + v[1] : "";
      v = capitalize (v[0]);


function hotcat_ok (nocommit) {
      titles.sort (
  hotcat_sanitize_input ();
        function (a, b) {
  var text = document.getElementById ( "hotcat_text" ) ;
          if (a.indexOf (b) == 0) return 1; // a begins with b: a > b
  var v   = text.value || "";
          if (b.indexOf (a) == 0) return -1; // b begins with a: a < b
  var pipe = v.indexOf ('|');
          // Opensearch may return stuff not beginning with the search prefix!
  if (pipe >= 0) v = v.substring (0, pipe);
          var prefixMatchA = (a.indexOf (v) == 0 ? 1 : 0);
  v = v.replace(/_/g, ' ').replace(/\s\s*$/, "");
          var prefixMatchB = (b.indexOf (v) == 0 ? 1 : 0);
  // Empty category ?
          if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
  if (!v) {
          if (a < b) return -1;
    hotcat_cancel() ;
          if (b < a) return 1;
    return ;
          return 0;
  }
        }
  // Get the links and the categories of the chosen category page
      );
  var url = wgServer + wgScriptPath + '/api.php?action=query&titles='
      // Remove duplicates
           + encodeURIComponent ('Category:' + v)
      for (var i = 0; i+1 < titles.length; i++) {
           + '&prop=info|links|categories&plnamespace=14&pllimit=50&format=json';
        if (titles[i] == titles[i+1]) {
  var request = sajax_init_object() ;
           titles.splice (i+1, 1);
  if (request == null) {
           i--;
    //Oops! We don't have XMLHttp...
        }
    hotcat_nosuggestions = true;
      }
    hotcat_closeform (nocommit);
      var firstTitle = titles[0];
    hotcat_running = 0;
      var completed = this.autoComplete (firstTitle, v, key, dontAutocomplete);
    return;
      this.icon.src = completed ? HotCat.existsYes : HotCat.existsNo;
  }
      this.inputExists = completed;
  request.open ('GET', url, true);
      if (completed) {
   request.onreadystatechange =
        this.lastInput = firstTitle;
    function () {
        if (titles.length == 1) {
       if (request.readyState != 4) return;
          this.list.style.display = 'none';
       if (request.status != 200) {
          if (this.engineSelector) this.engineSelector.style.display = 'none';
         hotcat_closeform (nocommit);
          return;
        }
      }
      if (!this.is_active) {
        this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        return;
      }
      var nofItems = (titles.length > 5 ? 5 : titles.length);
      if (nofItems <= 1) nofItems = 2;
      this.list.size = nofItems;
      this.list.style.align    = 'left';
      this.list.style.zIndex   = 5;
      this.list.style.position = 'absolute';
      // Compute initial list position. First the height.
       var listh = 0;
       if (this.list.style.display == 'none') {
         // Off-screen display to get the height
        this.list.style.top = this.text.offsetTop + 'px';
        this.list.style.left = '-10000px';
        this.list.style.display = "";
        listh = this.list.offsetHeight;
        this.list.style.display = 'none';
       } else {
       } else {
         var txt = document.getElementById ('hotcat_text');
         listh = this.list.offsetHeight;
         var original = txt.value;
      }
         var do_submit = hotcat_json_resolve (eval ('(' + request.responseText + ')'));
      // Approximate calculation of maximum list size
         if (do_submit) {
      var maxListHeight = listh;
           hotcat_closeform (
      if (nofItems < 5) maxListHeight = (listh / nofItems) * 5;
            nocommit
 
            ,(txt.value != original) ? " (redirect \[\[Category:" + v + "\]\] resolved)" : null
      function scroll_offset (what) {
          );
         var s = 'scroll' + what;
        return (document.documentElement ? document.documentElement[s] : 0)
              || document.body[s] || 0;
      } 
      function viewport (what) {
         if (typeof (is_safari) != 'undefined' && is_safari && !document.evaluate)
          return window['inner' + what];
        var s = 'client' + what;
        if (typeof (is_opera) != 'undefined' && is_opera) return document.body[s];
        return (document.documentElement ? document.documentElement[s] : 0)
              || document.body[s] || 0;
      }
      function position (node) {
        // Stripped-down simplified position function. It's good enough for our purposes.
         if (node.getBoundingClientRect) {
           var box    = node.getBoundingClientRect ();
          return { x : Math.round (box.left + scroll_offset ('Left'))
                  ,y : Math.round (box.top + scroll_offset ('Top'))
                };
         }
         }
        var t = 0, l = 0;
        do {
          t = t + (node.offsetTop  || 0);
          l = l + (node.offsetLeft || 0);
          node = node.offsetParent;
        } while (node);
        return {x : l, y : t};
       }
       }
    };
  request.setRequestHeader ('Pragma', 'cache=yes');
  request.setRequestHeader ('Cache-Control', 'no-transform');
  request.send (null);
}


function hotcat_json_resolve (params)
      // IE6 seems to report in this.text.offsetTop and this.text.offsetLeft global offsets??
{
      // Possibly this has something to do with the special status of input elements in IE as
  function resolve (page)
      // "windowed controls". Calculate the relative offsets manually.
  {
      var textPos = position (this.text);
    var cats    = page.categories;
      var catLinePos = position (this.line);
    var is_dab  = false;
      var textTop = textPos.y - catLinePos.y;
    var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
      var textLeft = textPos.x - catLinePos.x;
    if (!is_redir && cats) {
      if (window.ie6_bugs) {
       for (var c = 0; c < cats.length; c++) {
        // IE6 somehow has a problem with inline-displayed forms (to which our list belongs), and will add the
         var cat = cats[c]["title"];
        // offset of the beginning of the text to the offsets we'd normally calculate, which in particular with
         if (cat) cat = cat.substring (cat.indexOf (':') + 1); // Strip namespace prefix
        // right-aligned category lines as they occur in some older skins completely misplaces the lists, sometimes
         if (cat == 'Disambiguation') {
        // even off-screen. This appears to affect only the horizontal positioning of the list and of the
           is_dab = true; break;
        // engineSelector. Try to account for this bizarre behavior. Notes: dunno if that also occurs on IE7.
        } else if (cat == 'Category_redirects' || cat == 'Category redirects') {
        var textStartPos = position (this.line.firstChild);
           is_redir = true; break;
        textStartPos.x -= catLinePos.x;
        textLeft -= textStartPos.x;
      }
      var nl = textLeft;
       var nt = 0;
      var offset = 0;
      if (!noEngine) {
         this.engineSelector.style.zIndex = 5;
        this.engineSelector.style.position = 'absolute';
         this.engineSelector.style.width = this.text.offsetWidth + 'px';
        // Figure out the height of this selector: display it off-screen, then hide it again.
         if (this.engineSelector.style.display == 'none') {
           this.engineSelector.style.left  = '-1000px';
          this.engineSelector.style.top  = textTop + 'px';
          this.engineSelector.style.display = "";
          offset = this.engineSelector.offsetHeight;
          this.engineSelector.style.display = 'none';
        } else {
           offset = this.engineSelector.offsetHeight;
         }
         }
        this.engineSelector.style.left  = nl + 'px';
      }
      if (textPos.y < maxListHeight + offset) {
        // The list might extend beyond the upper border of the page. Let's avoid that by placing it
        // below the input text field.
        nt = textTop + this.text.offsetHeight + offset + 1;
        if (!noEngine) this.engineSelector.style.top = textTop + this.text.offsetHeight + 'px';
      } else {
        nt = textTop - listh - offset;
        if (!noEngine) this.engineSelector.style.top = textTop - offset + 'px';
       }
       }
    }
      this.list.style.top = nt + 'px';
    if (!is_redir && !is_dab) return true;
      this.list.style.width = ""; // No fixed width (yet)
    var lks = page.links;
      this.list.style.left = nl + 'px';
    var titles = new Array ();
      // (Re-)fill the list
    for (i = 0; i < lks.length; i++) {
      while (this.list.firstChild) this.list.removeChild (this.list.firstChild);
      if (   lks[i]["ns"] == 14                              // Category namespace
      for (var i = 0 ; i < titles.length ; i++) {
          && lks[i]["title"] && lks[i]["title"].length > 0) { // Name not empty
        var opt = make ('option') ;
         // Internal link to existing thingy. Extract the page name.
        opt.appendChild (make (titles[i], true));
         var match = lks[i]["title"];
        this.list.appendChild (opt);
        // Remove the category prefix
      }
        match = match.substring (match.indexOf (':') + 1);
      if (!noEngine) this.engineSelector.style.display = "";
         titles.push (match);
      this.list.style.display = 'block';
         if (is_redir) break;
      // Set the width of the list       
      var scroll = scroll_offset ('Left');
      var view_w = viewport ('Width');
      var l_pos  = position (this.list);
      if (this.list.offsetWidth < this.text.offsetWidth) {
         this.list.style.width = this.text.offsetWidth + 'px';
         return;
      }
      // Make sure that the list fits horizontally into the browser window     
      var w      = this.list.offsetWidth;
      if (l_pos.x + w > scroll + view_w) {
        if (w > view_w) w = view_w;
         this.list.style.width = w + 'px';
         this.list.style.left = nl - (l_pos.x + w - scroll - view_w) + 'px';
       }
       }
     }
     },
    if (titles.length > 1) {
      // Disambiguation page
      hotcat_show_suggestions (titles);
      return false;
    } else if (titles.length == 1) {
      var text = document.getElementById ("hotcat_text");
      var v = text.value.split('|');
      text.value = titles[0] + (v.length > 1 ? '|' + v[1] : "");
    }
    return true;
  } // end local function resolve


   // We should have at most one page here
    autoComplete : function (newVal, actVal, key, dontModify) {
  for (var page in params.query.pages) return resolve (params.query.pages[page]);
      if (newVal == actVal) return true;
   return true; // In case we have none.
      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.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;
    },


function hotcat_closeform (nocommit, comment)
    processKey : function (evt) {
{
      if (this.lastKey == 38 || this.lastKey == 40) { // Up and down arrows
  hotcat_sanitize_input ();
        if (this.list.style.display != 'none') {
  var text = document.getElementById ( "hotcat_text" ) ;
          // List is visible, so there are suggestions
  var v = text.value.split('|');
          this.highlightSuggestion (this.lastKey == 38 ? -1 : 1);
  var key = v.length > 1 ? v[1] : null;
          // Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow as
  v = v[0].replace(/_/g, ' ').replace(/\s\s*$/, "");
          // "place the text cursor at the front", which we don't want here.
  if (!v                                                // Empty
          return evtKill (evt);
      || wgNamespaceNumber == 14 && v == wgTitle        // Self-reference
        } else if (  this.keyCount <= 1
      || text.parentNode.parentNode.id != 'hotcat_add'   // Modifying, but
                  && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
        && text.parentNode.parentNode.hotcat_name == v) //   name unchanged
                  )
  {
        {
    hotcat_cancel ();
          // If no suggestions displayed, get them, unless we're already getting them.
    return;
          this.textchange ();
  }
        }
 
      }
  if (hotcat_upload) {
      return true;
    hotcat_just_add (v, key) ; // Close the form
    },
    return ;
  }
  var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
            + '&action=edit';
   var url = editlk + '&hotcat_newcat=' + encodeURIComponent( v ) ;
 
  if (key && key.length > 0) url += '&hotcat_sortkey=' + encodeURIComponent (key);
  // Editing existing?
  var span = text.parentNode.parentNode ; // span.form.text
  if ( span.id != "hotcat_add" ) { // Not plain "addition" 
    url += '&hotcat_removecat=' + encodeURIComponent (span.hotcat_name);
  }
  if (nocommit) url = url + '&hotcat_nocommit=1';
  if (comment) url = url + '&hotcat_comment=' + encodeURIComponent (comment);
  // Make the list disappear:
  var list = document.getElementById ( "hotcat_list" ) ;
  if (list) list.style.display = 'none';
   
  document.location = url ;
}


function hotcat_just_add ( text, key ) {
    highlightSuggestion : function (dir) {
  var span = document.getElementById("hotcat_form") ;
      if (noSuggestions || !this.list || this.list.style.display == 'none') return;
  while ( span.tagName != "SPAN" ) span = span.parentNode ;
      var curr = this.list.selectedIndex;
  var add = 0 ;
      var tgt = curr < 0 ? 0 : curr + dir;
  if ( span.id == "hotcat_add" ) add = 1 ;
      tgt = tgt < 0 ? 0 : tgt;
  span.id = "" ;
      if (tgt != curr && tgt < this.list.options.length) {
  while ( span.firstChild ) span.removeChild ( span.firstChild ) ;
        if (curr >= 0 && curr < this.list.options.length) this.list.options[curr].selected = false;
  var na = document.createElement ( "a" ) ;
        this.list.options[tgt].selected = true;
  na.href = wgArticlePath.split("$1").join("Category:" + encodeURI (text)) ;
        // Get current input text
  na.appendChild ( document.createTextNode ( text ) ) ;
        var v = this.text.value.split('|');
  na.setAttribute ( "title" , "Category:" + text ) ;
        var key = v.length > 1 ? '|' + v[1] : "";
  span.appendChild ( na ) ;
        var completed = this.autoComplete (this.list.options[tgt].text, this.lastInput, key, false);
  var catline = getElementsByClassName ( document , "p" , "catlinks" ) [0] ;
        if (!completed) {
  if ( add ) hotcat_append_add_span ( catline ) ;
          this.text.value = this.list.options[tgt].text + key;
        }
        this.lastInput = this.list.options[tgt].text;
      }
    },


  for ( var i = 0 ; i < span.parentNode.childNodes.length ; i++ ) {
    resetKeySelection : function () {
    if ( span.parentNode.childNodes[i] != span ) continue ;
      if (noSuggestions || !this.list || this.list.style.display == 'none') return;
    hotcat_modify_span ( span , i ) ;
      var curr = this.list.selectedIndex;
    span.hotcat_key = (key && key.length > 0) ? key : null;
      if (curr >= 0 && curr < this.list.options.length) {
    break ;
        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;
      }
    }


function hotcat_cancel () {
  }; // end CategoryEditor.prototype
   var span = document.getElementById("hotcat_form").parentNode ;
 
   if ( span.id == "hotcat_add" ) {
  function initialize () {
    hotcat_create_span ( span ) ;
    // User configurations. Do this here, called from the onload handler, so that users can
   } else {
    // override it easily in their own user script files by just declaring variables. JSconfig
    while ( span.firstChild.nextSibling ) span.removeChild ( span.firstChild.nextSibling ) ;
    // is some feature used at Wikimedia Commons.
     span.firstChild.style.display = "" ;
    HotCat.no_autocommit =   window.hotcat_no_autocommit
     for ( var i = 0 ; i < span.parentNode.childNodes.length ; i++ ) {
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatNoAutoCommit']
       if ( span.parentNode.childNodes[i] != span ) continue ;
                          || 0;
       hotcat_modify_span ( span , i ) ;
    HotCat.suggest_delay =   window.hotcat_suggestion_delay
       break ;
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatSuggestionDelay']
                          || 100;
    HotCat.editbox_width =   window.hotcat_editbox_width
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatEditBoxWidth']
                          || 40;
    HotCat.suggestions   =  window.hotcat_suggestions
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatSuggestions']
                          || 'combined';
    if (typeof (HotCat.suggestions) != 'string' || !suggestionConfigs[HotCat.suggestions])
      HotCat.suggestions = 'combined';
     HotCat.fixed_search  =  window.hotcat_suggestions_fixed
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatFixedSuggestions']
                          || false;
    // 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 () {
function hotcat_text_changed () {
    var container = null;
  if ( hotcat_running ) return ;
    switch (skin) {
  hotcat_sanitize_input ();
      case 'cologneblue':
  var text = document.getElementById ( "hotcat_text" ) ;
        container = document.getElementById ('quickbar');
  var v = text.value;
        // Fall through
  // Disregard anything after a pipe.
      case 'standard':
  var pipe = v.indexOf ('|');
      case 'nostalgia':
  if (pipe >= 0) v = v.substring (0, pipe);
        if (!container) container = document.getElementById ('topbar');
  if ( hotcat_last_v == v ) return ; // Nothing's changed...
        var lks = container.getElementsByTagName ('a');
 
        for (var i = 0; i < lks.length; i++) {
  if (hotcat_nosuggestions) {
          if (  param ('title', lks[i].href) == wgPageName
    // On IE, XMLHttp uses ActiveX, and the user may deny execution... just make sure
              && param ('action', lks[i].href) == 'edit')
    // the list is not displayed.
            return true;
    var list = document.getElementById ('hotcat_list');
        }
    if (list != null) list.style.display = "none" ;
        return false;
     var exists = document.getElementById ('hotcat_exists');
      default:
    if (exists != null) exists.style.display = "none" ;
        // all modern skins:
     return;
        return document.getElementById ('ca-edit') != null;
   }
     }
     return false;
   }  
    
    
   hotcat_running = 1 ;
   function setup_upload () {
  hotcat_last_v = v ;
    onUpload = true;
 
    // Add an empty category bar above the "watch this" box, and change the onsubmit handler.
  if ( v != "" ) {
    var ip = document.getElementById ('wpWatchthis');
     var url = wgServer + wgScriptPath
    if (!ip) return;
            + "/api.php?format=json&action=opensearch&namespace=14&limit=30&search="
     var reupload = document.getElementById ('wpForReUpload');
            + encodeURIComponent( v ) ;
     var destFile = document.getElementById ('wpDestFile');
     var request = sajax_init_object() ;
     if (   (reupload && !!reupload.value)
     if (request == null) {
        || (destFile && (destFile.disabled || destFile.readonly)))
       //Oops! We don't have XMLHttp...
       return; // re-upload form...
      hotcat_nosuggestions = true;
    // Insert a table row with two fields (label and empty category bar)
      var list = document.getElementById ('hotcat_list');
    ip = ip.parentNode.parentNode; // The containing <tr>
      if (list != null) list.style.display = "none" ;
    var newRow = make ('tr');
      var exists = document.getElementById ('hotcat_exists');
    var labelCell = make ('td');
      if (exists != null) exists.style.display = "none" ;
    var lineCell  = make ('td');
       hotcat_running = 0;
    newRow.appendChild (labelCell);
       return;
    newRow.appendChild (lineCell);
     }  
    // Create the category line
     request.open('GET', url, true);
    catLine = make ('div');
     request.onreadystatechange =
    catLine.className = 'catlinks';
       function () {
    catLine.id = 'catlinks';
        if (request.readyState == 4 && request.responseText != null && request.responseText.indexOf ('[') == 0) {
    catLine.style.textAlign = 'left';
          var result = eval ('(' + request.responseText + ')');
    lineCell.appendChild (catLine);
           if (result != null && result.length == 2 && result[0] == v) {
    // Create the label
            var titles = result[1];
    var label = null;
            for (var i = 0; i < titles.length; i++) {
    if (typeof (UFUI) != 'undefined' &&
              // Remove the namespace. No hardcoding of 'Category:', please, other Wikis may have
        typeof (UFUI.getLabel) == 'function') {
              // local names ("Kategorie:" on de-WP, for instance). Also don't break on category
      try {
              // names containing a colon
        label = UFUI.getLabel ('wpCategoriesUploadLbl');
              titles[i] = titles[i].substring (titles[i].indexOf (':') + 1);
       } catch (ex) {
        label = null;
       }
     }
     if (!label) {
      labelCell.id = 'hotcatLabel';
      labelCell.appendChild (make (HotCat.categories), true);
     } else {
      labelCell.id = 'hotcatLabelTranslated';
       labelCell.appendChild (label);
    }
    labelCell.className          = 'mw-label';
    labelCell.style.textAlign    = 'right';
    labelCell.style.verticalAlign = 'middle';
    // Change the onsubmit handler
    var form = document.getElementById ('upload') || document.getElementById ('mw-upload-form');
    if (form) {
      var optionsTable = document.getElementById ('mw-htmlform-options');
      if (optionsTable) optionsTable.width = '100%';
      ip.parentNode.insertBefore (newRow, ip);
      textHeight = labelCell.firstChild.offsetHeight;
      form.onsubmit = (function (oldSubmit) {
        return function () {         
          var do_submit = true;
           if (oldSubmit) {
            if (typeof (oldSubmit) == 'string')
              do_submit = eval (oldSubmit);
            else if (typeof (oldSubmit) == 'function')
              do_submit = oldSubmit.apply (form, arguments);
          }
          if (!do_submit) return false;
          closeForm ();
          // Copy the categories
          var eb =   document.getElementById ('wpUploadDescription')
                  || document.getElementById ('wpDesc');
          for (var i = 0; i < editors.length; i++) {
            var t = editors[i].currentCategory;
            if (!t) continue ;
            var key = editors[i].currentKey;
            var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
            // Only add if not already present
            var cleanedText = eb.value.replace(/<\!--(\s|\S)*?--\>/g, "")
                                      .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
            if (!find_category (cleanedText, t, true)) {
              eb.value += '\n' + new_cat;
             }
             }
            // Opensearch may (or may not, depending on which search backend it uses) return entries out of alphabetical
            // order (and even containing "similar characters", for instance, for prefix "Sidl", you might get back
            // "Šidlovská" or "Sídliště Háje"!). Hmmm... shall we sort them again here? But then how to sort "í" with
            // "i"? We do *not* want a full-blown UCA http://www.unicode.org/reports/tr10/tr10-16.html here! Anyway, if
            // you want to sort the suggestions in some way, do it here.
            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;
                if (a < b) return -1;
                if (b < a) return 1;
                return 0;
              }
            );
            hotcat_show_suggestions (titles) ;
           }
           }
         }
          return true;           
       };
         };
    request.setRequestHeader ('Pragma', 'cache=yes');
       }) (form.onsubmit);
    request.setRequestHeader ('Cache-Control', 'no-transform');
     }
     request.send (null);
  } else {
    hotcat_show_suggestions ([]);
   }
   }
   hotcat_running = 0 ;
    
}
  var cleanedText = null;


function hotcat_show_suggestions ( titles ) {
  function isOnPage (span) {
  var text = document.getElementById ( "hotcat_text" ) ;
    var catTitle = title (span.firstChild.getAttribute ('href', 2));
  var list = document.getElementById ( "hotcat_list" ) ;
    if (!catTitle) return null;
  var icon = document.getElementById ( "hotcat_exists" ) ;
    catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' ');
  // Somehow, after a double click on the selection list, we still get here in IE, but
    var result = { title : catTitle, match : ["", "", ""] };
  // the list may no longer exist... Lupo, 2008-01-20
    if (pageText === null) return result;
  if (list == null) return;
    if (cleanedText === null) {
  if (hotcat_nosuggestions) {
      cleanedText = pageText.replace(/<\!--(\s|\S)*?--\>/g, "")
    list.style.display = "none" ;
                            .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
    if (icon != null) icon.style.display = "none";
     }
     return;
    result.match = find_category (cleanedText, catTitle, true);
  }
     return result;
  if ( titles.length == 0 ) {
    list.style.display = "none" ;
    icon.src = hotcat_exists_no ;
     return ;
   }
   }
 
  // Set list size to minimum of 5 and actual number of titles. Formerly was just 5.
  // Lupo, 2008-01-20
  list.size = (titles.length > 5 ? 5 : titles.length) ;
  // Avoid list height 1: double-click doesn't work in FF. Lupo, 2008-02-27
  if (list.size == 1) list.size = 2;
  list.style.align = "left" ;
  list.style.zIndex = 5 ;
  list.style.position = "absolute" ;


  // Was listh = titles.length * 20: that makes no sense if titles.length > list.size
   var initialized = false;
  // Lupo, 2008-01-20
  var listh = list.size * 20;
  var nl = parseInt (text.offsetLeft) - 1 ;
  var nt = parseInt (text.offsetTop) - listh ;
  if (skin == 'nostalgia' || skin == 'cologneblue' || skin == 'standard') {
    // These three skins have the category line at the top of the page. Make the suggestions
    // appear *below* out input field.
    nt = parseInt (text.offsetTop) + parseInt (text.offsetHeight) + 3;
  }
  list.style.top = nt + "px" ;
  list.style.width = ""; // No fixed width (yet)
  list.style.height = listh + "px" ;
  list.style.left = nl + "px" ;
  while ( list.firstChild ) list.removeChild ( list.firstChild ) ;
  for ( var i = 0 ; i < titles.length ; i++ ) {
    var opt = document.createElement ( "option" ) ;
    var ot = document.createTextNode ( titles[i] ) ;
    opt.appendChild ( ot ) ;
    //opt.value = titles[i] ;
    list.appendChild ( opt ) ;
  }
 
  icon.src = hotcat_exists_yes ;
 
   var nof_titles = titles.length;


   var first_title = titles.shift () ;
   function setup () {
  var v = text.value.split('|');
    if (initialized) return;
  var key = v.length > 1 ? '|' + v[1] : "";
    initialized = true;
  v = v[0].ucFirst();
    // Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
  text.focus ();
    // each category, and add the + link.
  if ( first_title == v ) {
    catLine =  catLine                                                  // Special:Upload
    if (nof_titles == 1) {
            || document.getElementById ('mw-normal-catlinks')          // MW >= 1.13alpha
       // Only one result, and it's the same as whatever is in the input box: makes no sense
            || getElementsByClassName (document , 'p' , 'catlinks')[0]; // MW < 1.13
       // to show the list.
    var hiddenCats = document.getElementById ('mw-hidden-catlinks');
       list.style.display = 'none';
    if (!catLine) {
     }
      var footer = null;
     return ;
      if (!hiddenCats) {
  }
        footer = getElementsByClassName (document , 'div' , 'printfooter')[0];
        if (!footer) return; // Don't know where to insert the category line
      }
      catLine = make ('div');
      catLine.id = 'mw-normal-catlinks';
      catLine.style.textAlign = 'left';
      // Add a label
      var label = make ('a');
      label.href  = wgArticlePath.replace ('$1', 'Special:Categories');
      label.title = HotCat.categories;
       label.appendChild (make (HotCat.categories, true));
      catLine.appendChild (label);
      catLine.appendChild (make (':', true));
       // Insert the new category line
      var container = (hiddenCats ? hiddenCats.parentNode : document.getElementById ('catlinks'));
       if (!container) {
        container = make ('div');
        container.id = 'catlinks';
        footer.parentNode.insertBefore (container, footer.nextSibling);
      }
      container.className = 'catlinks noprint';
      container.style.display = "";
      if (!hiddenCats) {
        container.appendChild (catLine);
      } else {
        container.insertBefore (catLine, hiddenCats);
      }
      textHeight = label.firstChild.offsetHeight;
     } // end if catLine exists
     catLine.style.position = 'relative';
    if (is_rtl) catLine.dir = 'rtl';


  if (list.offsetWidth < text.offsetWidth)
     // Create editors for all existing categories
     list.style.width = text.offsetWidth + "px";
  else {


     function position (node)
     function createEditors (line) {
    {
       var cats = line.getElementsByTagName ('span');
       var t = 0, l = 0;
      // Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
       do {
      var copyCats = new Array (cats.length);
         t = t + (node.offsetTop  || 0);
      for (var i = 0; i < cats.length; i++) copyCats[i] = cats[i];
         l = l + (node.offsetLeft || 0);
       var editor = null;
         node = node.offsetParent;
      for (var i = 0; i < copyCats.length; i++) {
       } while (node);
         var test = isOnPage (copyCats[i]);
       return {x : l, y : t};
         if (test !== null && test.match !== null) {
          editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2]);
         }
       }
       return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
     }
     }


     function scroll_offset (what)
     var lastSpan = createEditors (catLine);
     {
    // Create one to add a new category
       var s = 'scroll' + what;
    var editor = new CategoryEditor(catLine, null, null, lastSpan != null);
       return (document.documentElement ? document.documentElement[s] : 0)
     if (!onUpload) {
            || document.body[s] || 0;
      if (pageText !== null && hiddenCats) {
        hiddenCats.style.position = 'relative';
        if (is_rtl) hiddenCats.dir = 'rtl';
        createEditors (hiddenCats);
      }
      // 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 (); return evtKill (evt);};
      lk.style.cursor = 'pointer';
     }
     }
    cleanedText = null;
  }


    function viewport (what)
  function setPage (json) {
     {
     if (!json || !json.query || !json.query.pages) return;
      if (typeof (is_safari) != 'undefined' && is_safari && !document.evaluate)
    for (var p in json.query.pages) {
        return window['inner' + what];
       var page = json.query.pages[p];
       var s = 'client' + what;
       if (!page.revisions || page.revisions.length == 0) break;
       if (typeof (is_opera) != 'undefined' && is_opera) return document.body[s];
      pageText = page.revisions[0]['*'];
       return (document.documentElement ? document.documentElement[s] : 0)
       pageTime = page.revisions[0].timestamp.replace (/\D/g, "");
            || document.body[s] || 0;
      break;
     }
     }
  }


     var scroll = scroll_offset ('Left');
  function getPage () {
     var view_w = viewport ('Width');
     // We know we have an article here.
    var l_pos  = position (list);
    if (wgArticleId == 0) {
    var w      = list.offsetWidth;
      // Doesn't exist yet.
    if (l_pos.x + w > scroll + view_w) {
      pageText = "";
      if (w > view_w) w = view_w;
      pageTime = null;
       list.style.width = w + "px";
      setup ();
       list.style.left = nl - (l_pos.x + w - scroll - view_w) + "px";
     } else {
      var url = wgServer + wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&titles='
              + encodeURIComponent (wgPageName) + '&prop=info%7Crevisions&rvprop=content%7Ctimestamp';
      var s = make ('script');
      s.src = url;
      s.type = 'text/javascript';
      HotCat.start = function (json) { setPage (json); setup (); };
       document.getElementsByTagName ('head')[0].appendChild (s);
       window.setTimeout (setup, 4000); // 4 seconds. Just in case getting the wikitext takes longer.
     }
     }
   }
   }


   list.style.display = "block" ;
   function run () {
 
    initialize ();
  // Put the first entry of the title list into the text field, and select the
 
  // new suffix such that it'll be overwritten if the user keeps typing.
    if (is_rtl && window.ie6_bugs) return; // Disabled! IE6 with RTL is just too broken...
  // ONLY do this if we have a way to select parts of the content of a text
 
  // field, otherwise, this is very annoying for the user. Note: IE does it
    if (wgNamespaceNumber == -1 && wgCanonicalSpecialPageName == 'Upload' && wgUserName) {
  // again differently from the two versions previously implemented.
      setup_upload ();
  // Lupo, 2008-01-20
      setup ();
  // Only put first entry into the list if the user hasn't typed something
      // Check for state restoration
  // conflicting yet Dschwen 2008-02-18
      if (   typeof (UploadForm) != 'undefined'
  if ( ( text.setSelectionRange ||
          && typeof (UploadForm.previous_hotcat_state) != 'undefined'
        text.createTextRange ||
          && UploadForm.previous_hotcat_state != null)
        typeof (text.selectionStart) != 'undefined' &&
        UploadForm.previous_hotcat_state = setState (UploadForm.previous_hotcat_state);    
        typeof (text.selectionEnd) != 'undefined' ) &&
        v == first_title.substr(0,v.length) )
  {
    // taking hotcat_last_v was a major annoyance,
    // since it constantly killed text that was typed in
    // _since_ the last AJAX request was fired! Dschwen 2008-02-18
    var nosel = v.length ;
 
    text.value = first_title + key;
   
    if (text.setSelectionRange)      // e.g. khtml
      text.setSelectionRange (nosel, first_title.length);
    else if (text.createTextRange) { // IE
      var new_selection = text.createTextRange();
      new_selection.move ("character", nosel);
      new_selection.moveEnd ("character", first_title.length - nosel);
      new_selection.select();
     } else {
     } else {
       text.selectionStart = nosel;
       if (!wgIsArticle || wgAction != 'view' || !can_edit() || HotCat.disable()) return;
       text.selectionEnd  = first_title.length;
       getPage ();
     }
     }
     if (nof_titles == 1) {
  }
       // Only one result, and we've just put it into the input box: makes no sense
 
      // to show the list.
  // Legacy stuff
      list.style.display = 'none';
 
  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 hotcat_get_state ()
  function getState () {
{
    var result = null;
  var cats = document.getElementById ('catlinks');
    for (var i = 0; i < editors.length; i++) {
  if (cats == null) return "";
      var text = editors[i].currentCategory;
  var result = null;
      var key  = editors[i].currentKey;
  cats = cats.getElementsByTagName ('span') ;
      if (text && text.length > 0) {
  for (var i = 0; i < cats.length; i++ ) {
        if (key && key.length > 0) text += '|' + key;
    var text = cats[i].hotcat_name;
        if (result == null)
    var key  = cats[i].hotcat_key;
          result = text;
    if (text) {
        else
      if (key) text += '|' + key;
          result = result + '\n' + text;
      if (result == null)
      }
        result = text;
      else
        result = result + '\n' + text;
     }
     }
    return result;
   }
   }
  return result;
}


function hotcat_set_state (state)
  function setState (state) {
{
    var cats = state.split ('\n');
  var cats = state.split ('\n');
    if (cats.length == 0) return null;
  if (cats.length == 0) return null;
    if (initialized && editors.length == 1 && editors[0].isAddCategory) {
  var parent = document.getElementById ('catlinks');
      // Insert new spans and create new editors for them.
  if (parent == null) return state;
      var newSpans = [];
  // HotCat uses a 'p' element inside the 'div' to wrap its spans...
      var before = editors.length == 1 ? editors[0].span : null;
  parent = parent.firstChild;
      for (var i = 0; i < cats.length; i++) {
  if (parent == null || parent.className != 'catlinks') return state;
        if (cats[i].length == 0) continue;
  var n = (parent.childNodes ? parent.childNodes.length - 1 : 0);
        var cat = cats[i].split ('|');
  if (n < 0) n = 0;
        var key = cat.length > 1 ? cat[1] : null;
  var before = parent.lastChild;
        cat = cat[0];
  for (var i = 0; i < cats.length; i++) {
        var lk = make ('a'); lk.href = wgArticlePath.split ('$1').join ('Category:' + encodeURI (cat));
    if (cats[i].length > 0) {
        lk.appendChild (make (cat, true));
      var cat = cats[i].split ('|');
        lk.title = cat;
      var key = cat.length > 1 ? cat[1] : null;
        var span = make ('span');
      cat = cat[0];
        span.appendChild (lk);
      var lk = document.createElement ('a');
        if (i == 0) catLine.insertBefore (make (' ', true), before);
      lk.href = wgArticlePath.split ('$1').join ('Category:' + encodeURI (cat));
        catLine.insertBefore (span, before);
      lk.appendChild (document.createTextNode (cat));
        if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
      lk.setAttribute ('title', cat);
        newSpans.push ({element: span, title: cat, 'key': key});
      var span = document.createElement ('span');
      }
      span.appendChild (lk);
      // And change the last one...
      parent.insertBefore (span, before);
       if (before) {
      if (before != null) parent.insertBefore (document.createTextNode (' | '), before);
        before.parentNode.insertBefore (make (' | ', true), before);
       hotcat_modify_span (span, n++);
       }
       span.hotcat_key = key;
      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;
   }
   }
  return null;
}


addOnloadHook ( hotcat ) ;
  // 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>

גרסה מ־07:31, 26 במאי 2010

//<source lang="javascript">

/*
  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.

  Author: [[User:Lupo]], April-May 2010
  License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
 
  Choose whichever license of these you like best :-)
*/

// Globals:
//  (inline script on the page):
//    wgNamespaceNumber, wgCanonicalSpecialPageName, wgNamespaceIds (optional), wgFormattedNamespaces (optional)
//    wgScript, wgServer, wgArticlePath, wgScriptPath, wgAction, wgPageName, wgTitle, wgUserName, wgIsArticle,
//    wgArticleId
//  ajax.js
//    sajax_init_object
//  wikibits.js
//    addOnloadHook, window.ie6_bugs

if (typeof (HotCat) == 'undefined') { // Guard against double inclusions
    
// Configuration stuff.
var HotCat = {
  // Localize these messages to the main language of your wiki.
  messages :
    { cat_removed  : 'removed [[Category:$1]]'
     ,cat_added    : 'added [[Category:$1]]'
     ,cat_keychange: 'new key for [[Category:$1]]: '
     ,cat_notFound : 'Category "$1" not found'
     ,cat_exists   : 'Category "$1" already exists; not added.'
     ,cat_resolved : ' (redirect [[Category:$1]] resolved)'
     ,uncat_removed: 'removed {{uncategorized}}'
     ,using        : ' using [[Help:Gadget-HotCat|HotCat]]'
     ,multi_change : '$1 categories'
        // $1 is replaced by a number
     ,commit       : 'Save'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
     ,ok           : 'OK'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
     ,cancel       : 'Cancel'
        // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
     ,multi_error  : 'Could not retrieve the page text from the server. Therefore, your category changes '
                    +'cannot be saved. We apologize for the inconvenience.'
        // Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
        // see localization hook below.
    }
 ,category_regexp    : '[Cc]ategory'
   // 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]ategory|[Kk]ategorie', or at the Chinese Wikipedia, use '[Cc]ategory|分类|分類'.
 ,category_canonical : 'Category'
   // The standard category name on your wiki. Is automatically localized correctly if you're running
   // MediaWiki 1.16 or later; otherwise, set it to the preferred category name (e.g., "Kategorie").
 ,categories         : 'Categories'
   // Plural of category_canonical
 ,disambig_category  : 'Disambiguation'
   // Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
   // any items, but that contains links to other categories where stuff should be categorized. If you don't have
   // that concept on your wiki, set it to null.
 ,redir_category     : 'Category redirects'
   // Any category in this category is deemed a (soft) redirect to some other category defined by the first link
   // to another category. If your wiki doesn't have soft category redirects, set this to null.
 ,links : {change: '(±)', remove: '(-)', add: '(+)', restore: '(×)', undo: '(×)'}
   // The little modification links displayed after category names.
 ,addmulti           : '<span>+<sup>+</sup></span>'
   // The HTML content of the "enter multi-mode" link at the front.
 ,disable            :
    function () { // Return true to disable HotCat
      return (   wgNamespaceNumber < 0   // Special pages; Special:Upload is handled differently
              || wgNamespaceNumber == 10 // Templates
              || typeof (wgNamespaceIds) != 'unknown'
                 && (   wgNamespaceNumber == wgNamespaceIds['creator']
                     || wgNamespaceNumber == wgNamespaceIds['timedtext']
                    )
             );
    }
 ,uncat_regexp : /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}/g
   // A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
   // If not, set it to null.
 ,existsYes    : 'http://upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
 ,existsNo     : 'http://upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png'
   // The images used for the little indication icon. Should not need changing.
};

if (wgUserLanguage != wgContentLanguage) importScript ('MediaWiki:Gadget-HotCat.js/' + wgUserLanguage);
// Localization hook to localize HotCat.messages.commit and HotCat.messages.multi_error. For German, the
// file would be "MediaWiki:Gadget-HotCat.js/de", and its contents could be for instance
//
// HotCat.messages.commit      = 'Speichern';
// HotCat.messages.ok          = 'OK';
// 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. However, you should also localize the search engine names.
// Search in this file for "Localize the names here", and localize the names there.
 
(function () {
  
  // First auto-localize the category names
  if (typeof (wgFormattedNamespaces) != 'undefined' && wgFormattedNamespaces['14']) {
    function create_regexp_str (name)
    {
      if (!name || name.length == 0) return "";
      var initial = name.substr (0, 1);
      name = name.substring(1).replace (/[ _]/g, '[ _]').replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
      return '[' + initial.toLowerCase () + initial.toUpperCase () + ']' + name;
    }

    HotCat.category_canonical = wgFormattedNamespaces['14'];
    HotCat.category_regexp    += '|' + create_regexp_str (HotCat.category_canonical);
    for (var cat_name in wgNamespaceIds) {
      if (   typeof (cat_name) == 'string'
          && cat_name.toLowerCase () != HotCat.category_canonical.toLowerCase ()
          && wgNamespaceIds[cat_name] == 14)
      {
        HotCat.category_regexp += '|' + create_regexp_str (cat_name);
      }
    }
  }
  
  // Utility functions. Yes, this duplicates some functionality that also exists in other places, but
  // to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
  // these few operations here.
  function bind (func, target) {
    var f = func, tgt = target;
    return function () { return f.apply (tgt, arguments); };
  }
  function make (arg, literal) {
    if (!arg) return null;
    return literal ? document.createTextNode (arg) : document.createElement (arg);
  }
  function param (name, uri) {
    if (typeof (uri) == 'undefined' || uri === null) uri = document.location.href;
    var re = RegExp ('[&?]' + name + '=([^&#]*)');
    var m = re.exec (uri);
    if (m && m.length > 1) return decodeURIComponent(m[1]);
    return null;
  }
  function title (href) {
    if (!href) return null;
    var script = wgScript + '?';
    if (href.indexOf (script) == 0 || href.indexOf (wgServer + script) == 0) {
      // href="/w/index.php?title=..."
      return param ('title', href);
    } else {
      // href="/wiki/..."
      var prefix = wgArticlePath.replace ('$1', "");
      if (href.indexOf (prefix) != 0) prefix = wgServer + prefix; // Fully expanded URL?
      if (href.indexOf (prefix) == 0)
        return decodeURIComponent (href.substring (prefix.length));
    }
    return null;
  }
  function hasClass (elem, name) {
    return (' ' + elem.className + ' ').indexOf (' ' + name + ' ') >= 0;
  }
  function capitalize (str) {
    if (!str || str.length == 0) return str;
    return str.substr(0, 1).toUpperCase() + str.substr (1);
  }
  
  // Text modification
  
  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) {
    var cat_name  = category.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
    var initial   = cat_name.substr (0, 1);
    var cat_regex = new RegExp ('\\[\\[\\s*(' + HotCat.category_regexp + ')\\s*:\\s*'
                                + (initial == '\\'
                                   ? initial
                                   : '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
                                + cat_name.substring (1).replace (/[ _]/g, '[ _]')
                                + '\\s*(\\|.*?)?\\]\\]', 'g'
                               );
    if (once) return cat_regex.exec (wikitext);
    var copiedtext = wikitext.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
                             .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
    var result = [];
    var curr_match = null;
    while ((curr_match = cat_regex.exec (copiedtext)) != null) {
      result.push ({match : curr_match});
    }
    result.re = cat_regex;
    return result; // An array containing all matches, with positions, in result[i].match
  }

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

    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; 
      // We should try to find interwiki links here, but that's for later.
      return index;
    }

    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', 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) 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' && 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) 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 = '\n[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
        if (cat_point >= 0) {
          wikitext = wikitext.substring (0, cat_point) + newcatstring + wikitext.substring (cat_point);
        } else {
          wikitext += newcatstring;
        }
        if (keyChange) {
          summary.push (HotCat.messages.cat_keychange.replace ('$1', toAdd) + '"' + key.substring(1) + '"');
        } else {
          summary.push (HotCat.messages.cat_added.replace ('$1', toAdd));
        }
        if (HotCat.uncat_regexp) {
          var txt = wikitext.replace (HotCat.uncat_regexp, ""); // Remove "uncat" templates
          if (txt.length != wikitext.length) {
            wikitext = txt;
            summary.push (HotCat.messages.uncat_removed);
          }
        }
      }
    }
    return {text: wikitext, 'summary': summary, error: null};
  }
  
  if (wgAction == 'edit') {
    // Legacy code based on URI parameters, can add/remove/change only one single category. Still
    // used for single-category changes, and newly to supply a default edit summary for multi category
    // changes.    
    var toRemove = param ('hotcat_removecat');
    var toAdd    = param ('hotcat_newcat');
    if (toAdd) {
      toAdd = capitalize (toAdd.replace (/_/g, ' ').replace (/^\s+|\s+$/g, ""));
      if (toAdd.length == 0) toAdd = null;
    }
    if (toRemove) {
      toRemove = capitalize (toRemove.replace (/_/g, ' ').replace (/^\s+|\s+$/g, ""));
      if (toRemove.length == 0) toRemove = null;
    }
    if (toAdd || toRemove) {
      addOnloadHook (function () {
        if (!document.editform || !document.editform.wpTextbox1) return;
        var 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     = 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;
  }
  
  // The real HotCat UI
  
  function evtKeys (e) {
    e = e || window.event || window.Event; // W3C, IE, Netscape
    var code = 0;
    if (typeof (e.ctrlKey) != 'undefined') { // All modern browsers
      // Ctrl-click seems to be overloaded in FF/Mac (it opens a pop-up menu), so treat cmd-click
      // as a ctrl-click, too.
      if (e.ctrlKey || e.metaKey)  code |= 1;
      if (e.shiftKey) code |= 2;
    } else if (typeof (e.modifiers) != 'undefined') { // Netscape...
      if (e.modifiers & (Event.CONTROL_MASK | Event.META_MASK)) code |= 1;
      if (e.modifiers & Event.SHIFT_MASK) code |= 2;
    }
    return code;
  }
  function evtKill (e) {
    e = e || window.event || window.Event; // W3C, IE, Netscape
    if (typeof (e.preventDefault) != 'undefined') {
      e.preventDefault ();
      e.stopPropagation ();
    } else
      e.cancelBubble = true;
    return false;
  }
  
  var catLine      = null;
  var onUpload     = false;    
  var editors      = [];
  
  var commitButton = null;
  var commitForm   = null;
  var multiSpan    = null;

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

  var is_rtl       = false;

  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);
    }
    // Get the preferences, so that we can set wpWatchthis correctly later on. Must use Ajax here.
    if (wgUserName) {
      var request = sajax_init_object ();      
      request.open
        ('GET', wgServer + wgScriptPath + '/api.php?format=json&action=query&meta=userinfo&uiprop=options', true);
      request.onreadystatechange =
        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 && 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;
            }
          }
        };
      request.setRequestHeader ('Pragma', 'cache=yes');
      request.setRequestHeader ('Cache-Control', 'no-transform');
      request.send (null);        
    }
  }
  
  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 performChanges () {
    // Don't use the edit API or LAPI, it's always bothersome to report back errors like edit
    // conflicts. Instead, make one remote call (blocking, because we can't continue anyway if
    // it doesn't succeed), getting the page text. Perform the changes on the text, then construct
    // a form to submit all this as a diff.
    if (pageText === null) {
      var request = sajax_init_object ();
      var uri     = wgServer + wgScriptPath
                  + '/api.php?format=json&action=query&titles=' + encodeURIComponent (wgPageName)
                  + '&prop=info%7Crevisions&rvprop=content%7Ctimestamp';
      request.open ('GET', uri, false); // Yes, synchronous
      request.send (null);
      if (request.status == 200 && request.responseText && request.responseText.charAt(0) == '{') {
        setPage (eval ('(' + request.responseText + ')'));
      }
    }
    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) {
        result = change_category (result.text, editors[i].originalCategory, null, null);
        if (!result.error) {
          changes++;
          deleted.push (editors[i].originalCategory);
        }
      }
    }
    // Fill in the form and submit it
    commitForm.wpMinoredit.checked = minorEdits;
    commitForm.wpWatchthis.checked = wgArticleId == 0 && watchCreate || watchEdit;
    if (wgArticleId > 0) {
      if (changes == 1) {
        if (result.summary && result.summary.length > 0)
          commitForm.wpSummary.value = result.summary.join ('; ') + HotCat.messages.using;
        commitForm.wpMinoredit.checked = true;
      } else if (changes > 1) {
        var summary = [];
        if (deleted.length == 1)
          summary.push ('-[[' + HotCat.category_canonical + ':' + deleted[0] + ']]');
        else if (deleted.length > 1)
          summary.push ('- ' + HotCat.messages.multi_change.replace ('$1', "" + deleted.length));
        if (added.length == 1)
          summary.push ('+[[' + HotCat.category_canonical + ':' + added[0] + ']]');
        else if (added.length > 1)
          summary.push ('+ ' + HotCat.messages.multi_change.replace ('$1', "" + added.length));
        if (changed.length == 1) {
          if (changed[0].from != changed[0].to) {
            summary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + ']]→[['
                         + HotCat.category_canonical + ':' + changed[0].to + ']]');
          } else {
            summary.push ('±[[' + HotCat.category_canonical + ':' + changed[0].from + ']]');
          }
        } else if (changed.length > 1) {
          summary.push ('± ' + HotCat.messages.multi_change.replace ('$1', "" + changed.length));
        }
        if (summary.length > 0)
          commitForm.wpSummary.value = summary.join ('; ') + HotCat.messages.using;
      }
    }
    commitForm.wpTextbox1.value = result.text;
    commitForm.wpStarttime.value = currentTimestamp ();
    commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
    commitForm.submit();
  }

  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;
    }
    var request = sajax_init_object ();
    if (!request) {
      noSuggestions = true;
      callback (toResolve);
      return;
    }
    var url = wgServer + wgScriptPath + '/api.php';
    // Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
    // category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
    var args = 'action=query&prop=info%7Clinks%7Ccategories&plnamespace=14&pllimit=50'
             + '&cllimit=' + (toResolve.length * 10) // Category limit is global, link limit is per page
             + '&format=json&titles=';
    for (var i = 0; i < toResolve.length; i++) {
      args += encodeURIComponent ('Category:' + toResolve[i].dabInput);
      if (i+1 < toResolve.length) args += '%7C';
    }
    if (url.length + args.length + 1 > 2000) { // Lowest common denominator: IE has a URI length limit of 2083
      request.open ('POST', url, true);
      request.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
    } else {
      url += '?' + args; args = null;
      request.open ('GET', url, true);
    }
    request.onreadystatechange =
      function () {
        if (request.readyState != 4) return;
        if (request.status != 200) {
          callback (toResolve);
          return;
        }
        resolveRedirects (toResolve, eval ('(' + request.responseText + ')'));
        callback (toResolve);
      };
    request.setRequestHeader ('Pragma', 'cache=yes');
    request.setRequestHeader ('Cache-Control', 'no-transform');
    request.send (args);        
  }
    
  function resolveOne (page, toResolve) {
    var cats     = page.categories;
    var lks      = page.links;
    var is_dab   = false;
    var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
    if (!is_redir && cats && (HotCat.disambig_category || HotCat.redir_category)) {
      for (var c = 0; c < cats.length; c++) {
        var cat = cats[c]['title'];
        // Strip namespace prefix
        if (cat) {
          cat = cat.substring (cat.indexOf (':') + 1).replace(/_/g, ' ');
          if (cat == HotCat.disambig_category) {
            is_dab = true; break;
          } else if (cat == HotCat.redir_category) {
            is_redir = true; break;
          }
        }
      }
    }
    if (!is_redir && !is_dab) return;
    if (!lks || lks.length == 0) return;
    var titles = [];
    for (var i = 0; i < lks.length; i++) {
      if (   lks[i]['ns'] == 14                             // Category namespace
          && lks[i]['title'] && lks[i]['title'].length > 0) // Name not empty
      {
        // Internal link to existing thingy. Extract the page name and remove the namespace.
        var match = lks[i]['title'];
        titles.push (match.substring (match.indexOf (':') + 1));
        if (is_redir) break;
      }
    }
    for (var j = 0; j < toResolve.length; j++) {
      if (toResolve[j].dabInput != page.title.substring (page.title.indexOf (':') + 1)) continue;
      if (titles.length > 1) {
        toResolve[j].dab = titles;
      } else {
        toResolve[j].inputExists = true; // Might actually be wrong...
        toResolve[j].icon.src = HotCat.existsYes;
        toResolve[j].text.value =
          titles[0] + (toResolve[j].currentKey != null ? '|' + toResolve[j].currentKey : "");
      }
    }
  }

  function resolveRedirects (toResolve, params) {
    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) {
      performChanges ();
      return;
    }
    resolveMulti (
        toResolve
      , function (resolved) {
          var firstDab = null;
          var dontChange = false;
          for (var i = 0; i < resolved.length; i++) {
            if (resolved[i].lastInput != resolved[i].dabInput) {
              // We didn't disable all the open editors, but we did asynchronous calls. It is
              // theoretically possible that the user changed something...
              dontChange = true;
            } else {
              if (resolved[i].dab) {
                if (!firstDab) firstDab = resolved[i];
              } else {
                if (resolved[i].acceptCheck(true)) resolved[i].commit();
              }
            }
          }
          if (firstDab) {
            CategoryEditor.makeActive (firstDab);
          } else if (!dontChange) {
            performChanges ();
          }
        }
    );
  }

  var noSuggestions = false;
  var suggestionEngines = {
    opensearch :
      { uri     : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=$1' // $1 = search term
       ,handler : // Function to convert result of uri into an array of category names
          function (responseText, queryKey) {
            if (responseText.charAt (0) != '[') return null;
            var queryResult = eval ('(' + responseText + ')');
            if (queryResult != null && queryResult.length == 2 && queryResult[0] == queryKey) {
              var titles = queryResult[1];
              for (var i = 0; i < titles.length; i++) {
                titles[i] = titles[i].substring (titles[i].indexOf (':') + 1); // rm namespace
              }
              return titles;
            }
            return null;      
          }
      }
    ,internalsearch :
      { uri     : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1'
       ,handler : 
          function (responseText, queryKey) {
            if (responseText.charAt (0) != '{') return null;
            var queryResult = eval ('(' + responseText + ')');
            if (queryResult && queryResult.query && queryResult.query.allpages) {
              var titles = queryResult.query.allpages;
              for (var i = 0; i < titles.length; i++) {
                titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
                if (titles[i].indexOf (queryKey) != 0) {
                  titles.splice (i, 1); // Doesn't start with the query key
                  i--;
                }
              }
              return titles;
            }
            return null;
          }
      }
  };

  // Localize the names here.
  var suggestionConfigs = {
    searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}}
   ,pagelist    : {name: 'Page list', engines: ['internalsearch'], cache: {}}
   ,combined    : {name: 'Combined search', engines: ['opensearch', 'internalsearch'], cache: {}}
  };

  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;
    if (toActivate.dab) {
      toActivate.showSuggestions (toActivate.dab, false, null, true); // do autocompletion, no key, no engine selector
      toActivate.dab = null;
    }
  };
  
  CategoryEditor.prototype = {
    
    initialize : function (line, span, after, key) {
      // If a span is given, 'after' is the category title, otherwise it may be an element after which to
      // insert the new span. 'key' is likewise overloaded; if a span is given, it is the category key (if
      // known), otherwise it is a boolean indicating whether a bar shall be prepended.
      if (!span) {
        this.isAddCategory = true;
        // Create add span and append to catLinks
        this.originalCategory = "";
        this.originalKey = null;
        this.originalExists   = false;
        span = make ('span');
        span.className = 'noprint';
        if (key) {
          span.appendChild (make (' | ', true));
          if (after) {
            after.parentNode.insertBefore (span, after.nextSibling);
            after = after.nextSibling;
          } else {
            line.appendChild (span);
          }
        } else if (line.firstChild) {
          span.appendChild (make (' ', true));
          line.appendChild (span);
        }
        this.linkSpan = make ('span');
        this.linkSpan.className = 'noprint';
        var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
        lk.appendChild (make (HotCat.links.add, true));          
        this.linkSpan.appendChild (lk);
        span = make ('span');
        span.className = 'noprint';
        if (is_rtl) span.dir = 'rtl';
        span.appendChild (this.linkSpan);
        if (after) 
          after.parentNode.insertBefore (span, after.nextSibling);
        else
          line.appendChild (span);
        this.normalLinks = null;
        this.undelLink = null;
        this.catLink = null;
      } else {
        if (is_rtl) span.dir = 'rtl';
        this.isAddCategory = false;
        this.catLink = span.firstChild;
        this.originalCategory = after;
        this.originalKey = (key && key.length > 1) ? key.substr(1) : null; // > 1 because it includes the leading bar
        this.originalExists   = !hasClass (this.catLink, 'new');
        // Create change and del links
        this.makeLinkSpan ();
        span.appendChild (this.linkSpan);
      }
      this.line               = line;
      this.engine             = HotCat.suggestions;
      this.span               = span;
      this.currentCategory    = this.originalCategory;
      this.currentExists      = this.originalExists;
      this.currentKey         = this.originalKey;
      this.state              = CategoryEditor.UNCHANGED;
      this.lastSavedState     = CategoryEditor.UNCHANGED;
      this.lastSavedCategory  = this.originalCategory;
      this.lastSavedKey       = this.originalKey;
      this.lastSavedExists    = this.originalExists;
      editors[editors.length] = this;
    },
    
    makeLinkSpan : function () {
      this.normalLinks = make ('span');
      var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.remove, this);
      lk.appendChild (make (HotCat.links.remove, true));
      this.normalLinks.appendChild (make (' ', true));
      this.normalLinks.appendChild (lk);
      lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
      lk.appendChild (make (HotCat.links.change, true));
      this.normalLinks.appendChild (make (' ', true));
      this.normalLinks.appendChild (lk);
      this.linkSpan = make ('span');
      this.linkSpan.className = 'noprint';
      this.linkSpan.appendChild (this.normalLinks);
      this.undelLink = make ('span');
      this.undelLink.style.display = 'none';
      lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.restore, this);
      lk.appendChild (make (HotCat.links.restore, true));
      this.undelLink.appendChild (make (' ', true));
      this.undelLink.appendChild (lk);
      this.linkSpan.appendChild (this.undelLink);
    },
    
    open : 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) {
        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) {
                  // 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 ();
                  // 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
                  this.state = CategoryEditor.CHANGE_PENDING;
                  var self = this;
                  window.setTimeout (function () {self.textchange (dont_autocomplete);}, HotCat.suggest_delay);
                }
              }
             ,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 (evt.keyCode == 13) this.accept (evt);
              }
             ,this
            );
          // And handle continued pressing of arrow keys
          text.onkeypress = bind (function (evt) {this.keyCount++; return this.processKey (evt);}, this);
        }
        text.onfocus = bind (function () { CategoryEditor.makeActive (this); }, this);
        this.text = text;
        
        this.icon = make ('img');
        
        var list = null;
        if (!noSuggestions) {
          list = make ('select');
          list.onclick    = bind (function () { if (this.setValueFromList ()) this.textchange (); }, this);
          list.ondblclick = bind (function (e) { if (this.setValueFromList ()) this.accept (e); }, this);
          list.onchange = bind (function (e) { this.setValueFromList (); 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) {
              var opt = make ('option');
              opt.value = key;
              if (key == this.engine) opt.selected = true;
              opt.appendChild (make (suggestionConfigs[key].name, true));
              engineSelector.appendChild (opt);
            }
            engineSelector.onchange = bind (
              function () { 
                this.engine = this.engineSelector.options[this.engineSelector.selectedIndex].value;
                this.textchange (true, true); // Don't autocomplete, force re-display of list
              }
             ,this
            );
            this.engineSelector = engineSelector;
          }
        }
        this.list = list;
        
        function button_label (id, defaultText) {
          var label = null;
          if (   onUpload
              && typeof (UFUI) != 'undefined'
              && typeof (UIElements) != 'undefined'
              && typeof (UFUI.getLabel) == 'function') {
            try {
              label = UFUI.getLabel (id, true);
              // Extract the plain text. IE doesn't know that Node.TEXT_NODE == 3
              while (label && label.nodeType != 3) label = label.firstChild;
            } catch (ex) {
              label = null;
            }
          }
          if (!label || !label.data) return defaultText;
          return label.data;    
        }

        var OK = make ('input'); OK.type = 'submit';
        OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
        this.ok = OK;
        
        var cancel = make ('input'); cancel.type = 'button';
        cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
        cancel.onclick = bind (this.cancel, this);
        this.cancelButton = cancel;
        
        if (list) form.appendChild (list);
        if (this.engineSelector) form.appendChild (this.engineSelector);
        form.appendChild (text);
        if (!noSuggestions) form.appendChild (this.icon);
        form.appendChild (OK);
        form.appendChild (cancel);
        form.style.display = 'none';
        this.span.appendChild (form);
      }
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.currentCategroy = this.lastSavedCategory;
      this.currentExits    = this.lastSavedExists;
      this.currentKey      = this.lastSavedKey;
      this.icon.src = this.currentExists ? HotCat.existsYes : HotCat.existsNo;
      this.text.value = this.currentCategory + (this.currentKey ? '|' + this.currentKey : "");
      this.originalState = this.state;
      this.lastInput     = this.currentCategory;
      this.inputExists   = this.currentExists;
      this.state         = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
      // Display the form
      if (this.catLink) this.catLink.style.display = 'none';
      this.linkSpan.style.display = 'none';
      this.form.style.display = 'inline';
      CategoryEditor.makeActive (this);
      // Kill the event before focussing, otherwise IE will kill the onfocus event!
      var result = evtKill (evt);
      this.text.focus();
      return result;
    },

    cancel : function () {
      if (this.isAddCategory && !onUpload) {
        this.removeEditor(); // We added a new adder when opening
        return;
      }
      // Close, re-display link
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.form.style.display = 'none';
      if (this.catLink) this.catLink.style.display = "";
      this.linkSpan.style.display = "";
      this.state = this.originalState;
      this.currentCategory = this.lastSavedCategory;
      this.currentKey      = this.lastSavedKey;
      this.currentExists   = this.lastSavedExists;
    },

    removeEditor : function () {
      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;
        }
      }
      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.lastSavedCategory = this.originalCategory;
      this.lastSavedKey = this.originalKey;
      this.lastSavedExists = this.originalExists;
      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 = wgArticlePath.replace ('$1', 'Category:' + this.currentCategory);
        this.catLink.title = "";
        this.catLink.className = this.currentExists ? "" : 'new';
      }
      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 = capitalize (value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, ""));
      this.lastInput = v;
      if (v.length == 0) {
        this.cancel ();
        return false;
      }
      if (   !dontCheck
          && (   (   v == this.lastSavedCategory
                  && (   key == this.lastSavedKey
                      || key === null && this.lastSavedKey.length == 0
                      || this.lastSavedKey === null && key.length == 0
                     )
                 )
              || wgNamespaceNumber == 14 && v == wgTitle
             )
         )
      {
        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', 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 = wgArticlePath.replace ('$1', 'Category:' + this.currentCategory);
      this.catLink.title = "";
      this.catLink.className = this.currentExists ? "" : 'new';
      this.lastSavedCategory = this.currentCategory;
      this.lastSavedKey      = this.currentKey;
      this.lastSavedExists   = this.currentExists;
      // Close form and redisplay category
      if (this.list) this.list.style.display = 'none';
      if (this.engineSelector) this.engineSelector.style.display = 'none';
      this.form.style.display = 'none';
      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 && !onUpload) {
        // 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));
        span.appendChild (make (' ', true));
        span.appendChild (lk);
        this.normalLinks.appendChild (span);
        this.undoLink = span;
      }
      this.linkSpan.style.display = "";
      this.state = CategoryEditor.CHANGED;
    },
    
    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
                  || this.originalKey === null && this.currentKey.length == 0
                 )
             )
          || wgNamespaceNumber == 14 && this.currentCategory == wgTitle
         )
      {
        this.cancel ();
        return;
      }
      if (commitButton || onUpload) {
        this.close ();
      } else {
        if (this.list) this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        // Execute change from this.originalCategory to this.currentCategory|this.currentKey,
        var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
                   + '&action=edit';
        var url = editlk + '&hotcat_newcat=' + encodeURIComponent (this.currentCategory);  
        if (this.currentKey) 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) {
      this.doRemove (evtKeys (evt) & 1);
      return evtKill (evt);
    },
    
    doRemove : function (noCommit) {
      if (this.isAddCategory) { // Empty input on adding a new category
        this.cancel ();
        return;
      }
      if (!commitButton && !onUpload) {
        for (var i = 0; i < editors.length; i++) {
          if (editors[i].state != CategoryEditor.UNCHANGED) {
            setMultiInput();
            break;
          }
        }
      }
      if (commitButton) {
        this.catLink.style.textDecoration = 'line-through';
        this.originalState = this.state;
        this.state = CategoryEditor.DELETED;
        this.normalLinks.style.display = 'none';
        this.undelLink.style.display = "";
      } else {
        if (onUpload) {
          // Remove this editor completely
          this.removeEditor ();
        } else {
          // Execute single category deletion.
          var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
                     + '&action=edit';
          if (noCommit || HotCat.no_autocommit) editlk += '&hotcat_nocommit=1';
          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;
      this.normalLinks.style.display = "";
      this.undelLink.style.display = 'none';
      return evtKill (evt);
    },
    
    // Internal operations
    
    setValueFromList : function (idx) {
      if (typeof (idx) == 'undefined') idx = this.list.selectedIndex;
      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;
    },
    
    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);
      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) {
      var cb = callbackObj;
      var e  = engine;
      var v  = queryKey;
      var r  = sajax_init_object ();
      cb.requests.push (r);
      r.open('GET', url, true);
      r.onreadystatechange =
        bind (
          function () {
            if (r.readyState == 4) {
              if (r.status != 200) cb.dontCache = true;
              if (r.status == 200 && r.responseText != null) {
                var titles = e.handler (r.responseText, v);
                if (titles && titles.length > 0) {
                  if (cb.allTitles == null) {
                    cb.allTitles = titles;
                  } else {
                    cb.allTitles = cb.allTitles.concat (titles);
                  }
                }
              }
              cb.callsMade++;
            }
            if (cb.callsMade == cb.nofCalls) {
              if (!cb.dontCache && !suggestionConfigs[cb.engineName].cache[v]) {
                suggestionConfigs[cb.engineName].cache[v] = cb.allTitles;
              }
              if (!cb.cancelled) this.showSuggestions (cb.allTitles, cb.noCompletion, v);
              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,

    textchange : function (dont_autocomplete, force) {
      // Hide all other lists
      CategoryEditor.makeActive (this);
      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;
      }
      
      // 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
      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 engine  = suggestionConfigs[this.engine] ? this.engine : 'combined';

      if (suggestionConfigs[engine].cache[v]) {
        this.showSuggestions (suggestionConfigs[engine].cache[v], dont_autocomplete, v);
        return;
      }

      var engines = suggestionConfigs[engine].engines;
      this.callbackObj =
        {allTitles: null, requests: [], callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engine}; 
      for (var j = 0; j < engines.length; j++) {
        engine = suggestionEngines[engines[j]];
        var url = wgServer + wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (v));
        this.makeCall (url, this.callbackObj, engine, v);
      }     
    },
    
    showSuggestions : function (titles, dontAutocomplete, queryKey, noEngine) {
      this.dab = null;
      if (!this.list) return;
      if (noSuggestions) {
        if (this.list) this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        if (this.icon) this.icon.style.display = 'none';
        this.inputExists = true; // Default...
        return;
      }
      if (!noEngine) {
        noEngine = this.engineSelector == null;
      } else {
        if (this.engineSelector) this.engineSelector.style.display = 'none';
      }
      if (queryKey) {
        if (this.lastInput.indexOf (queryKey) != 0) return;
        if (this.lastQuery && this.lastInput.indexOf (this.lastQuery) == 0 && this.lastQuery.length > queryKey.length)
          return;
      }
      this.lastQuery = queryKey;
      
      if (!titles || titles.length == 0) {
        if (this.list) this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        if (this.icon) this.icon.src = HotCat.existsNo;
        this.inputExists = false;
        return;
      }
                    
      // Get current input text
      var v = this.text.value.split('|');
      var key = v.length > 1 ? '|' + v[1] : "";
      v = capitalize (v[0]);

      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;
          if (a < b) return -1;
          if (b < a) return 1;
          return 0;
        }
      );
      // Remove duplicates
      for (var i = 0; i+1 < titles.length; i++) {
        if (titles[i] == titles[i+1]) {
          titles.splice (i+1, 1);
          i--;
        }
      }
      var firstTitle = titles[0];
      var completed = this.autoComplete (firstTitle, v, key, dontAutocomplete);
      this.icon.src = completed ? HotCat.existsYes : HotCat.existsNo;
      this.inputExists = completed;
      if (completed) {
        this.lastInput = firstTitle;
        if (titles.length == 1) {
          this.list.style.display = 'none';
          if (this.engineSelector) this.engineSelector.style.display = 'none';
          return;
        }
      }
      if (!this.is_active) {
        this.list.style.display = 'none';
        if (this.engineSelector) this.engineSelector.style.display = 'none';
        return;
      }
      var nofItems = (titles.length > 5 ? 5 : titles.length);
      if (nofItems <= 1) nofItems = 2;
      this.list.size = nofItems;
      this.list.style.align    = 'left';
      this.list.style.zIndex   = 5;
      this.list.style.position = 'absolute';
      // Compute initial list position. First the height.
      var listh = 0;
      if (this.list.style.display == 'none') {
        // Off-screen display to get the height
        this.list.style.top = this.text.offsetTop + 'px';
        this.list.style.left = '-10000px';
        this.list.style.display = "";
        listh = this.list.offsetHeight;
        this.list.style.display = 'none';
      } else {
        listh = this.list.offsetHeight;
      }
      // Approximate calculation of maximum list size
      var maxListHeight = listh;
      if (nofItems < 5) maxListHeight = (listh / nofItems) * 5;

      function scroll_offset (what) {
        var s = 'scroll' + what;
        return (document.documentElement ? document.documentElement[s] : 0)
               || document.body[s] || 0;
      }  
      function viewport (what) {
        if (typeof (is_safari) != 'undefined' && is_safari && !document.evaluate)
          return window['inner' + what];
        var s = 'client' + what;
        if (typeof (is_opera) != 'undefined' && is_opera) return document.body[s];
        return (document.documentElement ? document.documentElement[s] : 0)
               || document.body[s] || 0;
      }
      function position (node) {
        // Stripped-down simplified position function. It's good enough for our purposes.
        if (node.getBoundingClientRect) {
          var box    = node.getBoundingClientRect ();
          return { x : Math.round (box.left + scroll_offset ('Left'))
                  ,y : Math.round (box.top + scroll_offset ('Top'))
                 };
        }
        var t = 0, l = 0;
        do {
          t = t + (node.offsetTop  || 0);
          l = l + (node.offsetLeft || 0);
          node = node.offsetParent;
        } while (node);
        return {x : l, y : t};
      }

      // IE6 seems to report in this.text.offsetTop and this.text.offsetLeft global offsets??
      // Possibly this has something to do with the special status of input elements in IE as
      // "windowed controls". Calculate the relative offsets manually.
      var textPos = position (this.text);
      var catLinePos = position (this.line);
      var textTop = textPos.y - catLinePos.y;
      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;
      if (!noEngine) {
        this.engineSelector.style.zIndex = 5;
        this.engineSelector.style.position = 'absolute';
        this.engineSelector.style.width = this.text.offsetWidth + 'px';
        // Figure out the height of this selector: display it off-screen, then hide it again.
        if (this.engineSelector.style.display == 'none') {
          this.engineSelector.style.left  = '-1000px';
          this.engineSelector.style.top   = textTop + 'px';
          this.engineSelector.style.display = "";
          offset = this.engineSelector.offsetHeight;
          this.engineSelector.style.display = 'none';
        } else {
          offset = this.engineSelector.offsetHeight;
        }
        this.engineSelector.style.left  = nl + 'px';
      } 
      if (textPos.y < maxListHeight + offset) {
        // The list might extend beyond the upper border of the page. Let's avoid that by placing it
        // below the input text field.
        nt = textTop + this.text.offsetHeight + offset + 1;
        if (!noEngine) this.engineSelector.style.top = textTop + this.text.offsetHeight + 'px';
      } else {
        nt = textTop - listh - offset;
        if (!noEngine) this.engineSelector.style.top = textTop - offset + 'px';
      }
      this.list.style.top = nt + 'px';
      this.list.style.width = ""; // No fixed width (yet)
      this.list.style.left = nl + 'px';
      // (Re-)fill the list
      while (this.list.firstChild) this.list.removeChild (this.list.firstChild);
      for (var i = 0 ; i < titles.length ; i++) {
        var opt = make ('option') ;
        opt.appendChild (make (titles[i], true));
        this.list.appendChild (opt);
      }
      if (!noEngine) this.engineSelector.style.display = "";
      this.list.style.display = 'block';
      // Set the width of the list        
      var scroll = scroll_offset ('Left');
      var view_w = viewport ('Width');
      var l_pos  = position (this.list);
      if (this.list.offsetWidth < this.text.offsetWidth) {
        this.list.style.width = this.text.offsetWidth + 'px';
        return;
      }
      // Make sure that the list fits horizontally into the browser window      
      var w      = this.list.offsetWidth;
      if (l_pos.x + w > scroll + view_w) {
        if (w > view_w) w = view_w;
        this.list.style.width = w + 'px';
        this.list.style.left = nl - (l_pos.x + w - scroll - view_w) + 'px';
      }
    },

    autoComplete : function (newVal, actVal, key, dontModify) {
      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.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) {
      if (this.lastKey == 38 || this.lastKey == 40) { // Up and down arrows
        if (this.list.style.display != 'none') {
          // List is visible, so there are suggestions
          this.highlightSuggestion (this.lastKey == 38 ? -1 : 1);
           // Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow as
           // "place the text cursor at the front", which we don't want here.
           return evtKill (evt);
        } else if (   this.keyCount <= 1
                   && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
                  )
        {
          // If no suggestions displayed, get them, unless we're already getting them.
          this.textchange ();
        }
      }
      return true;
    },

    highlightSuggestion : function (dir) {
      if (noSuggestions || !this.list || this.list.style.display == 'none') return;
      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;
      }
    },

    resetKeySelection : function () {
      if (noSuggestions || !this.list || this.list.style.display == 'none') return;
      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] : "";
        this.text.value = this.lastRealInput + key;
        this.lastInput = this.lastRealInput;
      }
    }

  }; // 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.
    HotCat.no_autocommit =   window.hotcat_no_autocommit
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatNoAutoCommit']
                          || 0;
    HotCat.suggest_delay =   window.hotcat_suggestion_delay
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatSuggestionDelay']
                          || 100;
    HotCat.editbox_width =   window.hotcat_editbox_width
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatEditBoxWidth']
                          || 40;
    HotCat.suggestions   =   window.hotcat_suggestions
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatSuggestions']
                          || 'combined';
    if (typeof (HotCat.suggestions) != 'string' || !suggestionConfigs[HotCat.suggestions])
      HotCat.suggestions = 'combined';
    HotCat.fixed_search  =   window.hotcat_suggestions_fixed
                          || typeof (JSconfig) != 'undefined' && JSconfig.keys['HotCatFixedSuggestions']
                          || false;
    // Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
    is_rtl = hasClass (document.body, 'rtl');
    if (!is_rtl) {
      if (document.defaultView && document.defaultView.getComputedStyle) { // Gecko etc.
        is_rtl = document.defaultView.getComputedStyle (document.body, null).getPropertyValue ('direction');
      } else if (document.body.currentStyle) { // IE, has subtle differences to getComputedStyle
        is_rtl = document.body.currentStyle['direction'];
      } else { // Not exactly right, but best effort
        is_rtl = document.body.style['direction'];
      }
      is_rtl = (is_rtl == 'rtl');
    }
  }
        
  function can_edit () {
    var container = null;
    switch (skin) {
      case 'cologneblue':
        container = document.getElementById ('quickbar');
        // Fall through
      case 'standard':
      case 'nostalgia':
        if (!container) container = document.getElementById ('topbar');
        var lks = container.getElementsByTagName ('a');
        for (var i = 0; i < lks.length; i++) {
          if (   param ('title', lks[i].href) == wgPageName
              && param ('action', lks[i].href) == 'edit')
            return true;
        }
        return false;
      default:
        // all modern skins:
        return document.getElementById ('ca-edit') != null;
    }
    return false;
  }    
  
  function setup_upload () {
    onUpload = true;
    // Add an empty category bar above the "watch this" box, and change the onsubmit handler.
    var ip = document.getElementById ('wpWatchthis');
    if (!ip) return;
    var reupload = document.getElementById ('wpForReUpload');
    var destFile = document.getElementById ('wpDestFile');
    if (   (reupload && !!reupload.value)
        || (destFile && (destFile.disabled || destFile.readonly)))
      return; // re-upload form...
    // Insert a table row with two fields (label and empty category bar)
    ip = ip.parentNode.parentNode; // The containing <tr>
    var newRow = make ('tr');
    var labelCell = make ('td');
    var lineCell  = make ('td');
    newRow.appendChild (labelCell);
    newRow.appendChild (lineCell);
    // Create the category line
    catLine = make ('div');
    catLine.className = 'catlinks';
    catLine.id = 'catlinks';
    catLine.style.textAlign = 'left';
    lineCell.appendChild (catLine);
    // Create the label
    var label = null;
    if (typeof (UFUI) != 'undefined' &&
        typeof (UFUI.getLabel) == 'function') {
      try {
        label = UFUI.getLabel ('wpCategoriesUploadLbl');
      } catch (ex) {
        label = null;
      }
    }
    if (!label) {
      labelCell.id = 'hotcatLabel';
      labelCell.appendChild (make (HotCat.categories), true);
    } else {
      labelCell.id = 'hotcatLabelTranslated';
      labelCell.appendChild (label);
    }
    labelCell.className           = 'mw-label';
    labelCell.style.textAlign     = 'right';
    labelCell.style.verticalAlign = 'middle';
    // Change the onsubmit handler
    var form = document.getElementById ('upload') || document.getElementById ('mw-upload-form');
    if (form) {
      var optionsTable = document.getElementById ('mw-htmlform-options');
      if (optionsTable) optionsTable.width = '100%';
      ip.parentNode.insertBefore (newRow, ip);
      textHeight = labelCell.firstChild.offsetHeight;
      form.onsubmit = (function (oldSubmit) {
        return function () {          
          var do_submit = true;
          if (oldSubmit) {
            if (typeof (oldSubmit) == 'string')
              do_submit = eval (oldSubmit);
            else if (typeof (oldSubmit) == 'function')
              do_submit = oldSubmit.apply (form, arguments);
          }
          if (!do_submit) return false;
          closeForm ();
          // Copy the categories
          var eb =    document.getElementById ('wpUploadDescription')
                   || document.getElementById ('wpDesc');
          for (var i = 0; i < editors.length; i++) {
            var t = editors[i].currentCategory;
            if (!t) continue ;
            var key = editors[i].currentKey;
            var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
            // Only add if not already present
            var cleanedText = eb.value.replace(/<\!--(\s|\S)*?--\>/g, "")
                                      .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
            if (!find_category (cleanedText, t, true)) {
              eb.value += '\n' + new_cat;
            }
          }
          return true;            
        };
      }) (form.onsubmit);
    }
  }
  
  var cleanedText = null;

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

  var initialized = false;

  function setup () {
    if (initialized) return;
    initialized = true;
    // 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 = '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);
      }
      textHeight = label.firstChild.offsetHeight;
    } // end if catLine exists
    catLine.style.position = 'relative';
    if (is_rtl) catLine.dir = 'rtl';

    // Create editors for all existing categories

    function createEditors (line) {
      var 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]);
        }
      }
      return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
    }

    var lastSpan = createEditors (catLine);
    // Create one to add a new category
    var editor = new CategoryEditor(catLine, null, null, lastSpan != null);
    if (!onUpload) {
      if (pageText !== null && hiddenCats) {
        hiddenCats.style.position = 'relative';
        if (is_rtl) hiddenCats.dir = 'rtl';
        createEditors (hiddenCats);
      }
      // 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 (); return evtKill (evt);};
      lk.style.cursor = 'pointer';
    }
    cleanedText = null;
  }

  function setPage (json) {
    if (!json || !json.query || !json.query.pages) return;
    for (var p in json.query.pages) {
      var page = json.query.pages[p];
      if (!page.revisions || page.revisions.length == 0) break;
      pageText = page.revisions[0]['*'];
      pageTime = page.revisions[0].timestamp.replace (/\D/g, "");
      break;
    }
  }

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

  function run () {
    initialize ();

    if (is_rtl && window.ie6_bugs) return; // Disabled! IE6 with RTL is just too broken...

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

  // 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 && key.length > 0) 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 = wgArticlePath.split ('$1').join ('Category:' + encodeURI (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)
//</source>