Diferencia entre revisiones de «MediaWiki:Gadget-HotCat.js»

De WikiEducator
Saltar a: navegación, buscar
m (Spanish localization)
(update HotCat gadget after MW1.23+ upgrade)
Línea 1: Línea 1:
 
//<source lang="javascript">
 
//<source lang="javascript">
  
if (typeof (hotcat_loaded) == 'undefined') {
+
/*
var hotcat_loaded = false; // Guard against double inclusions
+
  HotCat V2.29
var hotcat_running = 0 ;
+
var hotcat_last_v = "" ;
+
var hotcat_exists_yes = "http://upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png" ;
+
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;
+
Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
var hotcat_editbox_width    = 40;
+
Supports multiple category changes, as well as redirect and disambiguation resolution. Also
// Fallbacks if we don't have JSconfig. Lupo, 2009-06-24
+
plugs into the upload form. Search engines to use for the suggestion list are configurable, and
 +
can be selected interactively.
  
var hotcat_modify_blacklist = new Array (
+
Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat
"CC-" ,
+
List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Version_history
"GFDL" ,
+
"PD"
+
) ;
+
  
// Localization. On MW 1.16+, this is automatically localized. On earlier MediaWiki
+
License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
// installations, add localized namespace names manually, for instance for the Chinese
+
// Wikipedia, set it to hotcat_cnames_regexp="[Cc]ategory|分类|分類";
+
var hotcat_cnames_regexp="[Cc]ategory|[Cc]ategoría";
+
// The following is used for inserting new categories. Also localized automatically from
+
// MW 1.16 on. When a category is replaced, the namespace prefix of the removed category
+
// is used for the newly added category.
+
var hotcat_canonical="Categoría";
+
// The following should be the same as [[MediaWiki:Categories]]. Localize manually.
+
var hotcat_categories="Categorías";
+
  
function hotcat_remove_upload ( text ) {
+
Choose whichever license of these you like best :-)
  var cats = document.getElementById ( "catlinks" ) ;
+
*/
  cats = cats.getElementsByTagName ( "span" ) ;
+
 
  for ( var i = 0 ; i < cats.length ; i++ ) {
+
/*
    if (cats[i].hotcat_name && cats[i].hotcat_name == text) {
+
This code is MW version safe. It should run on any MediaWiki installation >= MW 1.15. Note: if
      cats[i].parentNode.removeChild ( cats[i].nextSibling ) ;
+
running on MW >= 1.17 configured with $wgLegacyJavaScriptGlobals != true, it will still force
      cats[i].parentNode.removeChild ( cats[i] ) ;
+
publishing the wg* globals in the window object. Note that HotCat is supposed to run with or
      break ;
+
without jQuery, and also on older installations that do not yet have window.mediaWiki. If you
    }
+
use any of these newer features, make sure you qualify them by checking whether they exist at
  }
+
all, and by providing some meaningful fallback implementation if not. To start itself, HotCat
 +
uses jQuery(document).ready(). If it doesn't exist, HotCat won't start.
 +
*/
 +
/* jshint ignore:start */ // This old code uses too many coding conventions incompatible with jshint.
 +
 
 +
if (typeof wgAction == 'undefined' && window.mediaWiki && window.mediaWiki.config) { // Compatibility hack
 +
window.wgAction = window.mediaWiki.config.get('wgAction');
 
}
 
}
 +
if ((typeof window.HotCat == 'undefined' || window.HotCat.nodeName) && wgAction != 'edit') { // Guard against double inclusions, and inactivate on edit pages
  
function hotcat_check_upload () {
+
// Configuration stuff.
  // Don't do anything if not "Special:Upload", or user not logged in.
+
window.HotCat = {
  if ( wgNamespaceNumber != -1 || wgCanonicalSpecialPageName != "Upload" || wgUserName == null) return ;
+
// Localize these messages to the main language of your wiki.
  var ip = document.getElementById ( "wpWatchthis" ) ;
+
messages :
  // Go to Special:Upload, choose a local file, enter a target file name without extension,
+
{cat_removed  : 'removed [[Category:$1]]'
  // then submit: you get a page that is "Special:Upload", but that doesn't have any form!
+
,template_removed  : 'removed {{[[Category:$1]]}}'
  if (ip == null) return;
+
,cat_added    : 'added [[Category:$1]]'
  var reupload = document.getElementById ('wpForReUpload');
+
,cat_keychange: 'new key for [[Category:$1]]: "$2"' // $2 is the new key
  var destFile = document.getElementById ('wpDestFile');
+
,cat_notFound : 'Category "$1" not found'
  if (  (reupload && !!reupload.value)
+
,cat_exists  : 'Category "$1" already exists; not added.'
      || (destFile && (destFile.disabled || destFile.readonly)))
+
,cat_resolved : ' (redirect [[Category:$1]] resolved)'
    return; // re-upload form...
+
,uncat_removed: 'removed {{uncategorized}}'
  hotcat_upload = 1 ;
+
,separator    : '; '
  var tr = ip.parentNode.parentNode ;
+
,prefix      : ""
  var ntr = document.createElement ( "tr" ) ;
+
// Some text to prefix to the edit summary.
  var ntd = document.createElement ( "td" ) ;
+
,using        : ' using [[Help:Gadget-HotCat|HotCat]]'
  var ntde = document.createElement ( "td" ) ;
+
// Some text to append to the edit summary. Named 'using' for historical reasons. If you prefer
  var catline = document.createElement ( "div" ) ;
+
// to have a marker at the front, use prefix and set this to the empty string.
  var np = document.createElement ( "p" ) ;
+
,multi_change : '$1 categories'
 
+
// $1 is replaced by a number. If your language has several plural forms (c.f. [[:en:Dual (grammatical form)]]),
  ntde.setAttribute ('id', 'hotcatLabel');
+
// you can set this to an array of strings suitable for passing to mw.language.configPlural().
  var label = null;
+
// If that function doesn't exist, HotCat will simply fall back to using the last
  if (typeof (UFUI) != 'undefined' &&
+
// entry in the array.
      typeof (UFUI.getLabel) == 'function') {
+
,commit      : 'Save'
    try {
+
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
      label = UFUI.getLabel ('wpCategoriesUploadLbl');
+
// see localization hook below.
    } catch (ex) {
+
,ok          : 'OK'
      label = null;
+
// 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.
 +
,short_catchange : null
 +
// Defaults to '[[' + category_canonical + ':$1]]'. Can be overridden if in the short edit summaries
 +
// not the standard category name should be used but, say, a shorter namespace alias. $1 is replaced
 +
// by a category name.
 +
}
 +
,category_regexp    : '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]'
 +
// Regular sub-expression matching all possible names for the category namespace. Is automatically localized
 +
// correctly if you're running MediaWiki 1.16 or later. Otherwise, set it appropriately, e.g. at the German
 +
// Wikipedia, use '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]|[Kk][Aa][Tt][Ee][Gg][Oo][Rr][Ii][Ee]', or at the
 +
// Chinese Wikipedia, use '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]|分类|分類'. Note that namespaces are case-
 +
// insensitive!
 +
,category_canonical : 'Category'
 +
// The standard category name on your wiki. Is automatically localized correctly if you're running
 +
// MediaWiki 1.16 or later; otherwise, set it to the preferred category name (e.g., "Kategorie").
 +
,categories        : 'Categories'
 +
// Plural of category_canonical.
 +
,disambig_category  : 'Disambiguation'
 +
// Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
 +
// any items, but that contains links to other categories where stuff should be categorized. If you don't have
 +
// that concept on your wiki, set it to null. Use blanks, not underscores.
 +
,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: '(\u2212)', add: '(+)', restore: ')', undo: '(×)', down: '(\u2193)', up: '(\u2191)'}
 +
// The little modification links displayed after category names. U+2212 is a minus sign; U+2193 and U+2191 are
 +
// downward and upward pointing arrows. Do not use ↓ and ↑ in the code!
 +
,tooltips : {
 +
change:  'Modify'
 +
,remove:  'Remove'
 +
,add:     'Add a new category'
 +
,restore: 'Undo changes'
 +
,undo:    'Undo changes'
 +
,down:    'Open for modifying and display subcategories'
 +
,up:      'Open for modifying and display parent categories'
 
   }
 
   }
  if (label == null)
+
// The tooltips for the above links
    ntde.appendChild (document.createTextNode (hotcat_categories));
+
,addmulti          : '<span>+<sup>+</sup></span>'
   else {
+
// The HTML content of the "enter multi-mode" link at the front.
    ntde.setAttribute ('id', 'hotcatLabelTranslated');
+
,multi_tooltip      : 'Modify several categories'
    // Change the ID to avoid that UploadForm tries to translate it again.
+
// Tooltip for the "enter multi-mode" link
     ntde.appendChild (label);
+
,disable            :
 +
function () { // Return true to disable HotCat. HotCat guarantees that the wg* globals exist here.
 +
var ns = wgNamespaceNumber;
 +
return (  ns < 0    // Special pages; Special:Upload is handled differently
 +
|| ns === 10  // Templates
 +
|| ns === 828 // Module (Lua)
 +
|| ns === 8  // MediaWiki
 +
|| ns === 6 && wgArticleId === 0 // Non-existing file pages
 +
|| ns === 2 && /\.(js|css)$/.test(wgTitle) // User scripts
 +
|| typeof (wgNamespaceIds) != 'undefined'
 +
&& (   ns === wgNamespaceIds['creator']
 +
|| ns === wgNamespaceIds['timedtext']
 +
|| ns === wgNamespaceIds['institution']
 +
  )
 +
  );
 +
}
 +
,uncat_regexp : /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}\s*(<\!--.*?--\>)?/g
 +
// A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
 +
// If not, set it to null.
 +
,existsYes    : '//upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
 +
,existsNo     : '//upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png'
 +
// The images used for the little indication icon. Should not need changing.
 +
,template_regexp    : '[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]'
 +
// Regexp to recognize templates. Like "category" above; autolocalized for MW 1.16+, otherwise set manually here.
 +
// On the German Wikipedia, you might use '[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]|[Vv][Oo][Rr][Ll][Aa][Gg][Ee]'.
 +
,template_categories : {}
 +
// a list of categories which can be removed by removing a template
 +
// key: the category without namespace
 +
// value: A regexp matching the template name, again without namespace
 +
// If you don't have this at your wiki, or don't want this, set it to an empty object {}.
 +
,engine_names : {
 +
searchindex : 'Search index'
 +
,pagelist    : 'Page list'
 +
,combined    : 'Combined search'
 +
,subcat      : 'Subcategories'
 +
,parentcat  : 'Parent categories'
 
   }
 
   }
  ntde.style.textAlign = "right" ;
+
// Names for the search engines
  ntde.style.verticalAlign = "middle" ;
+
,capitalizePageNames : true
  catline.id = "catlinks" ;
+
// Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the first letter
  // On the upload form, the suggestion box appears at the very top of the page. That is because
+
// of a page is automatically capitalized ("first-letter"; Category:aa == Category:Aa), or it isn't
  // the innermost enclosing div of the upload form (and its table) that has position "relative"
+
// ("case-sensitive"; Category:aa != Category:Aa). It doesn't currently have a fully case-insensitive mode
  // is the bodyContent div. Try to fix that by giving catline relative positioning, so absolute
+
// (which would mean Category:aa == Category:Aa == Category:AA == Category:aA)
  // positioning within should be relative to catline. Lupo, 2008-01-18
+
// HotCat tries to set this correctly automatically using an API query. It's still a good idea to manually
  catline.style.position ="relative";
+
// configure it correctly; either directly here if you copied HotCat, or in the local configuration file
  catline.style.textAlign = "left";
+
// MediaWiki:Gadget-HotCat.js/local_defaults if you hotlink to the Commons-version, to ensure it is set even
  // Otherwise, it looks bad in the Classic skin on the upload form. Lupo, 2008-05-16
+
// if that API query should fail for some strange reason.
  np.className = "catlinks" ;
+
,upload_disabled : false
  np.style.textAlign = "left";
+
// If upload_disabled is true, HotCat will not be used on the Upload form.
  catline.appendChild ( np ) ;
+
,blacklist : null
  ntd.appendChild ( catline ) ;
+
// Single regular expression matching blacklisted categories that cannot be changed or
  ntde.className = 'mw-label';
+
// added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
  ntr.appendChild ( ntde ) ;
+
// or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
  ntr.appendChild ( ntd ) ;
+
// word "maintenance" in its title.
  
  // Add handler for submit (changed by Lupo, 2008-01-18)
+
// Stuff changeable by users:
  var form = document.getElementById ('upload');
+
,bg_changed : '#F8CCB0'
  // Grrr... they changed the upload form!
+
// Background for changed categories in multi-edit mode. Default is a very light salmon pink.
  // http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/SpecialUpload.php?r1=32033&r2=32190
+
,no_autocommit : false
  if (!form) form = document.getElementById ('mw-upload-form');
+
// If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
  if (form) {
+
// the changes; users must always save explicitly.
    hotcat_old_onsubmit = form.onsubmit;
+
,del_needs_diff : false
    form.onsubmit = hotcat_on_upload;
+
// If true, the "category deletion" link "(-)" will never save automatically but always show an
    tr.parentNode.insertBefore ( ntr , tr ) ; // Insert *above* "Watch this" box
+
// edit page where the user has to save the edit manually. Is false by default because that's the
  }
+
// traditional behavior. This setting overrides no_autocommit for "(-)" links.
}
+
,suggest_delay : 100
 +
// Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
 +
// server to get suggestions.
 +
,editbox_width : 40
 +
// Default width, in characters, of the text input field.
 +
,suggestions : 'combined'
 +
// One of the engine_names above, to be used as the default suggestion engine.
 +
,fixed_search : false
 +
// If true, always use the default engine, and never display a selector.
 +
,use_up_down : true
 +
// If false, do not display the "up" and "down" links
 +
,list_size : 5
 +
// Default list size
 +
,single_minor : true
 +
// If true, single category changes are marked as minor edits. If false, they're not.
 +
,dont_add_to_watchlist : false
 +
// If true, never add a page to the user's watchlist. If false, pages get added to the watchlist if
 +
// the user has the "Add pages I edit to my watchlist" or the "Add pages I create to my watchlist"
 +
// options in his or her preferences set.
 +
,shortcuts : null
 +
,addShortcuts :
 +
function (map) {
 +
if (!map) return;
 +
window.HotCat.shortcuts = window.HotCat.shortcuts || {};
 +
for (var k in map) {
 +
if (!map.hasOwnProperty (k) || typeof k != 'string') continue;
 +
var v = map[k];
 +
if (typeof v != 'string') continue;
 +
k = k.replace (/^\s+|\s+$/g, "");
 +
v = v.replace (/^\s+|\s+$/g, "");
 +
if (k.length === 0 || v.length === 0) continue;
 +
window.HotCat.shortcuts[k] = v;
 +
}
 +
}
 +
};
  
function hotcat_on_upload () {
+
(function () { // Local scope to avoid polluting the global namespace with declarations
  // First, make sure that if we have an open category input form, we close it.
+
  var input = document.getElementById ('hotcat_text');
+
  if (input != null) hotcat_closeform ();
+
  
  var do_submit = true;
+
// Backwards compatibility stuff. We want HotCat to work with either wg* globals, or with mw.config.get().
  // Call previous onsubmit handler, if any
+
// Our "solution" is to publish the wg* globals if they're not already published.
  if (hotcat_old_onsubmit) {
+
if (window.mediaWiki && window.mediaWiki.config) {
    if (typeof hotcat_old_onsubmit == 'string')
+
var globals = window.mediaWiki.config.get();
      do_submit = eval (hotcat_old_onsubmit);
+
if (globals && globals !== window) {
    else if (typeof hotcat_old_onsubmit == 'function')
+
for (var k in globals) window[k] = globals[k];
      do_submit = hotcat_old_onsubmit ();
+
window.mediaWiki.config = new window.mediaWiki.Map(true); // Make config point to window again.
  }
+
}
  if (!do_submit) return false;
+
globals = null;
  // Only copy the categories if we do submit
+
}
  var cats = document.getElementById ( "catlinks" ) ;
+
// More backwards compatibility. We have a few places where we test for the browser: once for
  cats = cats.getElementsByTagName ( "span" ) ;
+
// Safari < 3.0, twice for WebKit (Chrome or Safari, any versions), twice for IE <= 6, and
  var eb = document.getElementById ( "wpUploadDescription" )
+
// once for IE < 8.
          || document.getElementById ( "wpDesc" ); // New upload form
+
var ua = navigator.userAgent.toLowerCase();
  for ( var i = 0 ; i < cats.length ; i++ ) {
+
var is_ie6 = /msie ([0-9]{1,}[\.0-9]{0,})/.exec(ua) !== null && parseFloat(RegExp.$1) <= 6.0;
    var t = cats[i].hotcat_name;
+
var is_ie_lt8 = /msie ([0-9]{1,}[\.0-9]{0,})/.exec(ua) !== null && parseFloat(RegExp.$1) < 8.0;
    if (!t) continue ;
+
var is_webkit = /applewebkit\/\d+/.test(ua) && ua.indexOf ('spoofer') < 0;
    var key = cats[i].hotcat_key;
+
// And even more compatbility. HotCat was developed without jQuery, and anyway current jQuery
    var new_cat = "\[\[" + hotcat_canonical + ":" + t + (key ? "|" + key : "") + "\]\]" ;
+
// (1.7.1) doesn't seem to support in jquery.getJSON() or jQuery.ajax() the automatic
    // Only add if not already present
+
// switching from GET to POST requests if the query arguments would make the uri too long.
    if (eb.value.indexOf (new_cat) < 0) eb.value += "\n" + new_cat ;
+
// (IE has a hard limit of 2083 bytes, and the servers may have limits around 4 or 8kB.)
  }
+
//    Anyway, HotCat is supposed to run on wikis without jQuery, so we'd have to supply some
  return true;
+
// ajax routines ourselves in any case. We can't rely on the old sajax_init_object(), newer
}
+
// MW versions (>= 1.19) might not have it.
 +
var getJSON = (function () {
 +
function getRequest () {
 +
var request = null;
 +
try {
 +
request = new window.XMLHttpRequest();
 +
} catch (anything) {
 +
if (window.ActiveXObject) {
 +
try {
 +
request = new window.ActiveXObject('Microsoft.XMLHTTP');
 +
} catch (any) {
 +
}
 +
} // end if IE
 +
} // end try-catch
 +
return request;
 +
}
  
function hotcat () {
+
return function (settings) {
  // Disable in some namespaces
+
var req = getRequest();
  if (   wgNamespaceNumber == 10 // Templates
+
if (!req && settings && settings.error) settings.error (req);
    // || typeof (wgNamespaceIds) != 'unknown'
+
if (!req || !settings || !settings.uri) return req;
    //    && (  wgNamespaceNumber == wgNamespaceIds['creator']
+
var uri = armorUri (settings.uri);
    //       || wgNamespaceNumber == wgNamespaceIds['timedtext']
+
var args = settings.data || null;
    //      )
+
var method;
    )
+
if (args && uri.length + args.length + 1 > 2000) {
  {
+
// We lose caching, but at least we can make the request
    return;
+
method = 'POST';
  }
+
req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
  // First auto-localize the category names
+
} else {
  if (typeof (wgFormattedNamespaces) != 'undefined' && wgFormattedNamespaces['14']) {
+
method = 'GET';
    function create_regexp_str (name)
+
if (args) uri += '?' + args;
    {
+
args = null;
      if (!name || name.length == 0) return "";
+
}
      var initial = name.substr (0, 1);
+
req.open (method, uri, true);
      name = name.substring(1).replace (/[ _]/g, "[ _]").replace(/([\\\^\$\.\?\*\+\(\)])/g, "\\$1");
+
req.onreadystatechange = function () {
      return '[' + initial.toLowerCase () + initial.toUpperCase () + ']' + name;
+
if (req.readyState != 4) return;
    }
+
if (req.status != 200 || !req.responseText || !(/^\s*[\{\[]/.test(req.responseText))) {
 +
if (settings.error) settings.error (req);
 +
} else {
 +
if (settings.success) settings.success (eval ('(' + req.responseText + ')'));
 +
}
 +
};
 +
req.setRequestHeader ('Pragma', 'cache=yes');
 +
req.setRequestHeader ('Cache-Control', 'no-transform');
 +
req.send (args);
 +
return req;
 +
};
 +
})();
  
    hotcat_canonical = wgFormattedNamespaces['14'];
+
function armorUri (uri) {
    hotcat_cnames_regexp += '|' + create_regexp_str (hotcat_canonical);
+
// Avoid protocol-relative URIs, IE7 has a bug with them in Ajax calls
    for (var cat_name in wgNamespaceIds) {
+
if (uri.length >= 2 && uri.substring(0, 2) == '//') return document.location.protocol + uri;
      if (   typeof (cat_name) == 'string'
+
return uri;
          && cat_name.toLowerCase () != hotcat_canonical.toLowerCase ()
+
}
          && wgNamespaceIds[cat_name] == 14)
+
      {
+
        hotcat_cnames_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!
+
  if (typeof (JSconfig) != 'undefined') {
+
    JSconfig.registerKey('HotCatDelay', 100, 'HotCat autocompletion delay (ms):', 5);
+
    JSconfig.registerKey('HotCatEditBoxWidth', 40, 'Width of Input box of HotCat (# of characters):', 5);
+
  }
+
  
  if ( hotcat_check_action() ) return ; // Edited page, reloading anyway
+
function LoadTrigger (needed) {
  if (hotcat_loaded) return; // Guard against double inclusions
+
this.queue = [];
  hotcat_loaded = true;
+
this.toLoad = needed;
  hotcat_check_upload () ;
+
}
 +
LoadTrigger.prototype = {
 +
register : function (callback) {
 +
if (this.toLoad <= 0) {
 +
callback (); // Execute directly
 +
} else {
 +
this.queue[this.queue.length] = callback;
 +
}
 +
},
  
  function can_edit ()
+
loaded : function () {
  {
+
if (this.toLoad > 0) {
    var container = null;
+
this.toLoad--;
    switch (skin) {
+
if (this.toLoad === 0) {
      case 'cologneblue':
+
// Run queued callbacks once
        container = document.getElementById ('quickbar');
+
for (var i = 0; i < this.queue.length; i++) this.queue[i]();
        // Fall through
+
this.queue = [];
      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;
+
  }
+
  
  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...
+
  // 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 visible_cats =  
+
var setupCompleted = new LoadTrigger(1);
    document.getElementById ('mw-normal-catlinks') ||          // MW 1.13alpha
+
// Used to run user-registered code once HotCat is fully set up and ready.
    getElementsByClassName ( document , "p" , "catlinks" ) [0]; // MW < 1.13 && Special:Upload
+
HotCat.runWhenReady = function (callback) {setupCompleted.register(callback);};
  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';
+
var loadTrigger = new LoadTrigger(2);
  hotcat_modify_existing ( visible_cats ) ;
+
// Used to delay running the HotCat setup until /local_defaults and localizations have been loaded.
  hotcat_append_add_span ( visible_cats ) ;
+
  
  // Check for state restoration (Lupo, 2008-02-06)
+
function load (uri) {
  if (  hotcat_upload
+
var head = document.getElementsByTagName ('head')[0];
      && typeof (UploadForm) != 'undefined'
+
var s = document.createElement ('script');
      && typeof (UploadForm.previous_hotcat_state) != 'undefined'
+
s.setAttribute ('src', armorUri(uri));
      && UploadForm.previous_hotcat_state != null)
+
s.setAttribute ('type', 'text/javascript');
    UploadForm.previous_hotcat_state = hotcat_set_state (UploadForm.previous_hotcat_state);
+
var done = false;
}
+
  
function hotcat_append_add_span ( catline ) {
+
function afterLoad () {
  var span_add = document.createElement ( "span" ) ;
+
if (done) return;
  if ( catline.getElementsByTagName('span')[0] )
+
done = true;
    catline.appendChild (document.createTextNode (" | "));
+
s.onload = s.onreadystatechange = s.onerror = null; // Properly clean up to avoid memory leaks in IE
  else if (catline.firstChild)
+
if (head && s.parentNode) head.removeChild (s);
    catline.appendChild (document.createTextNode (' '));
+
loadTrigger.loaded();
  catline.appendChild ( span_add );
+
}
  hotcat_create_span ( span_add );
+
}
+
  
String.prototype.ucFirst = function () {
+
s.onload = s.onreadystatechange = function () { // onreadystatechange for IE, onload for all others
  if (this.length < 1) return this;
+
if (done) return;
  return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
+
if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
}
+
afterLoad ();
 +
}
 +
};
 +
s.onerror = afterLoad; // Clean up, but otherwise ignore errors
 +
head.insertBefore (s, head.firstChild); // appendChild may trigger bugs in IE6 here
 +
}
  
function hotcat_is_on_blacklist ( cat_title ) {
+
function loadJS (page) {
  if ( !cat_title ) return 0 ;
+
load (wgServer + wgScript + '?title=' + encodeURIComponent (page) + '&action=raw&ctype=text/javascript');
  // 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 loadURI (href) {
  //var cat_title = span.firstChild.getAttribute ( "title" ) ;
+
var url = href;
  // This fails with MW 1.13alpha if the category is a redlink, because MW 1.13alpha appends
+
if (url.substring (0, 2) == '//') {
  // [[MediaWiki:Red-link-title]] to the category name... it also fails if the category name
+
url = window.location.protocol + url;
  // contains "&" (because that is represented by &amp; in the XHTML both in the title and in
+
} else if (url.substring (0, 1) == '/') {
  // the link's content (innerHTML). Extract the category name from the href instead:
+
url = wgServer + url;
  var cat_title = null;
+
}
  var classes  = " " + span.firstChild.className + " ";
+
load (url);
  var href      = span.firstChild.getAttribute ('href', 2);
+
}
  // 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
+
  // the XHTML source, but IE somehow (a) also returns a full URL with server part, and (b)
+
  // IE6 insists on wrongly decoding encoded UTF-8 characters ("K%C3%B6ln-Riehl" becomes
+
  // "Köln-Riehl"). The work-around is to use the special IE variant with the extra parameter,
+
  // which Microsoft says returns the simple string as found in the XHTML. See their docu at
+
  // http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx .
+
  if (!href) return;
+
  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));
+
    }
+
  }
+
  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" ) ;
+
// Load local configurations, overriding the pre-set default values in the HotCat object above. This is always loaded
  // Set the href to a dummy value to make sure we don't move if somehow the onclick handler
+
// from the wiki where this script is executing, even if this script itself is hotlinked from the Commons. This can
  // is bypassed.
+
// be used to change the default settings, or to provide localized interface texts for edit summaries and so on.
  remove_link.href = "#catlinks";
+
loadJS ('MediaWiki:Gadget-HotCat.js/local_defaults');
  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 ;
+
// Load localized UI texts. These are the texts that HotCat displays on the page itself. Texts shown in edit summaries
  var mod_id = "hotcat_modify_" + i ;
+
// should be localized in /local_defaults above.
  var modify_link = document.createElement ( "a" ) ;
+
if (wgUserLanguage != 'en') {
  modify_link.id = mod_id ;
+
// Lupo: somebody thought it would be a good idea to add this. So the default is true, and you have to set it to false
  modify_link.href = "javascript:hotcat_modify(\"" + mod_id + "\");" ;
+
// explicitly if you're not on the Commons and don't want that.
  modify_link.appendChild ( document.createTextNode ( "(±)" ) ) ;
+
if (typeof window.hotcat_translations_from_commons == 'undefined') {
  span.appendChild ( document.createTextNode ( " " ) ) ;
+
window.hotcat_translations_from_commons = wgServer.indexOf('//commons') < 0;
  span.appendChild ( modify_link ) ;
+
}
  //Store the extracted category name in our own new property of the span DOM node
+
// Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
  span.hotcat_name = cat_title;
+
if (window.hotcat_translations_from_commons && wgServer.indexOf('//commons') < 0) {
}
+
loadURI ('//commons.wikimedia.org/w/index.php?title='
 +
+ 'MediaWiki:Gadget-HotCat.js/' + wgUserLanguage
 +
+ '&action=raw&ctype=text/javascript&smaxage=21600&maxage=86400'
 +
);
 +
} else {
 +
// Load translations locally
 +
loadJS ('MediaWiki:Gadget-HotCat.js/' + wgUserLanguage);
 +
}
 +
} else {
 +
loadTrigger.loaded();
 +
}
  
function hotcat_modify_existing ( catline ) {
+
// No further changes should be necessary here.
  var spans = catline.getElementsByTagName ( "span" ) ;
+
  for ( var i = 0 ; i < spans.length ; i++ ) {
+
    hotcat_modify_span ( spans[i] , i ) ;
+
  }
+
}
+
  
function hotcat_getEvt (evt) {
+
// The following regular expression strings are used when searching for categories in wikitext.
  return evt || window.event || window.Event; // Gecko, IE, Netscape
+
var wikiTextBlank  = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
}
+
var wikiTextBlankRE = new RegExp (wikiTextBlank, 'g');
    
+
// Regexp for handling blanks inside a category title or namespace name.
function hotcat_evt2node (evt) {
+
// See http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Title.php?revision=104051&view=markup#l2722
  var node = null;
+
// See also http://www.fileformat.info/info/unicode/category/Zs/list.htm
   try {
+
//   MediaWiki collapses several contiguous blanks inside a page title to one single blank. It also replace a
    var e = hotcat_getEvt (evt);
+
// number of special whitespace characters by simple blanks. And finally, blanks are treated as underscores.
    node = e.target;
+
// Therefore, when looking for page titles in wikitext, we must handle all these cases.
    if (!node) node = e.srcElement;
+
//   Note: we _do_ include the horizontal tab in the above list, even though the MediaWiki software for some reason
   } catch (ex) {
+
// appears to not handle it. The zero-width space \u200B is _not_ handled as a space inside titles by MW.
    node = null;
+
var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
  }
+
// Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
  return node;
+
// a link must be on one single line.
}
+
//   MediaWiki also removes Unicode bidi override characters in page titles (and namespace names) completely.
 +
// This is *not* handled, as it would require us to allow any of [\u200E\u200F\u202A-\u202E] between any two
 +
// characters inside a category link. It _could_ be done though... We _do_ handle strange spaces, including the
 +
// zero-width space \u200B, and bidi overrides between the components of a category link (adjacent to the colon,
 +
// or adjacent to and inside of "[[" and "]]").
  
function hotcat_evtkeys (evt) {
+
// First auto-localize the regexps for the category and the template namespaces.
  var code = 0;
+
if (typeof (wgFormattedNamespaces) != 'undefined') {
  try {
+
function autoLocalize (namespaceNumber, fallback) {
    var e = hotcat_getEvt (evt);
+
function create_regexp_str (name)
    if (typeof(e.ctrlKey) != 'undefined') { // All modern browsers
+
{
      if (e.ctrlKey) code |= 1;
+
if (!name || name.length === 0) return "";
      if (e.shiftKey) code |= 2;
+
var regex_name = "";
    } else if (typeof (e.modifiers) != 'undefined') { // Netscape...
+
for (var i = 0; i < name.length; i++){
      if (e.modifiers & Event.CONTROL_MASK) code |= 1;
+
var initial = name.substr (i, 1);
      if (e.modifiers & Event.SHIFT_MASK)   code |= 2;
+
var ll = initial.toLowerCase ();
    }
+
var ul = initial.toUpperCase ();
  } catch (ex) {
+
if (ll == ul){
  }
+
regex_name += initial;
  return code;
+
} else {
}
+
regex_name += '[' + ll + ul + ']';
 +
}
 +
}
 +
return regex_name
 +
.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1')
 +
.replace (wikiTextBlankRE, wikiTextBlank);
 +
}
  
function hotcat_killEvt (evt)
+
fallback = fallback.toLowerCase();
{
+
var canonical  = wgFormattedNamespaces["" + namespaceNumber].toLowerCase();
  try {
+
var regexp     = create_regexp_str (canonical);
    var e = hotcat_getEvt (evt);
+
if (fallback && canonical != fallback) regexp += '|' + create_regexp_str (fallback);
    if (typeof (e.preventDefault) != 'undefined') {
+
for (var cat_name in wgNamespaceIds) {
      e.preventDefault ();
+
if (   typeof (cat_name) == 'string'
      e.stopPropagation ();
+
&& cat_name.toLowerCase () != canonical
     } else
+
&& cat_name.toLowerCase () != fallback
      e.cancelBubble = true;
+
&& wgNamespaceIds[cat_name] == namespaceNumber)
  } catch (ex) {
+
{
  }
+
regexp += '|' + create_regexp_str (cat_name);
}
+
}
 
+
}
function hotcat_remove (evt) {
+
return regexp;
  var node = hotcat_evt2node (evt);
+
}
  if (!node) return false;
+
  // Get the category name from the original link to the category, which is at
+
  // node.parentNode.firstChild (the DOM structure here is
+
  // <span><a...>Category</a> <a...>(−)</a>...</span>).
+
  var cat_title = node.parentNode.hotcat_name;  
+
  if ( hotcat_upload ) {
+
    hotcat_remove_upload ( cat_title ) ;
+
    hotcat_killEvt (evt);
+
    return false;
+
  }
+
  var editlk = wgServer + wgScript + '?title=' + encodeURIComponent (wgPageName)
+
            + '&action=edit';
+
  if (hotcat_evtkeys (evt) & 1) // CTRL pressed?
+
    editlk = editlk + '&hotcat_nocommit=1';
+
  hotcat_killEvt (evt);
+
  document.location = editlk + '&hotcat_removecat=' + encodeURIComponent (cat_title);
+
  return false;
+
}
+
  
function hotcatGetParamValue(paramName, h) {
+
if (wgFormattedNamespaces['14']) {
  if (typeof h == 'undefined' ) { h = document.location.href; }
+
HotCat.category_canonical = wgFormattedNamespaces['14'];
  var cmdRe=RegExp('[&?]'+paramName+'=([^&]*)');
+
HotCat.category_regexp = autoLocalize (14, 'category');
  var m=cmdRe.exec(h);
+
}
  if (m) {
+
if (wgFormattedNamespaces['10']) {
    try {
+
HotCat.template_regexp = autoLocalize (10, 'template');
      return decodeURIComponent(m[1]);
+
}
    } catch (someError) {}
+
}
  }
+
  return null;
+
}
+
  
// New. Code by Lupo & Superm401, added by Lupo, 2008-02-27
+
// Utility functions. Yes, this duplicates some functionality that also exists in other places, but
function hotcat_find_category (wikitext, category)
+
// to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
{
+
// these few operations here.
  var cat_name  = category.replace(/([\\\^\$\.\?\*\+\(\)])/g, "\\$1");
+
function bind (func, target) {
  var initial  = cat_name.substr (0, 1);
+
var f = func, tgt = target;
  var cat_regex = new RegExp ("\\[\\[\\s*(" + hotcat_cnames_regexp + ")\\s*:\\s*"
+
return function () { return f.apply (tgt, arguments); };
                              + (initial == "\\"
+
}
                                ? initial
+
function make (arg, literal) {
                                : "[" + initial.toUpperCase() + initial.toLowerCase() + "]")
+
if (!arg) return null;
                              + cat_name.substring (1).replace (/[ _]/g, "[ _]")
+
return literal ? document.createTextNode (arg) : document.createElement (arg);
                              + "\\s*(\\|.*?)?\\]\\]", "g"
+
}
                            );
+
function param (name, uri) {
  var result = new Array ();
+
if (typeof (uri) == 'undefined' || uri === null) uri = document.location.href;
  var curr_match  = null;
+
var re = new RegExp ('[&?]' + name + '=([^&#]*)');
  while ((curr_match = cat_regex.exec (wikitext)) != null) {
+
var m = re.exec (uri);
    result [result.length] = {match : curr_match};
+
if (m && m.length > 1) return decodeURIComponent(m[1]);
  }
+
return null;
  return result; // An array containing all matches, with positions, in result[i].match
+
}
}
+
function title (href) {
 +
if (!href) return null;
 +
var script = wgScript + '?';
 +
if (href.indexOf (script) === 0 || href.indexOf (wgServer + script) === 0 || wgServer.substring(0, 2) == '//' && href.indexOf (document.location.protocol + wgServer + script) === 0) {
 +
// href="/w/index.php?title=..."
 +
return param ('title', href);
 +
} else {
 +
// href="/wiki/..."
 +
var prefix = wgArticlePath.replace ('$1', "");
 +
if (href.indexOf (prefix) !== 0) prefix = wgServer + prefix; // Fully expanded URL?
 +
if (href.indexOf (prefix) !== 0 && prefix.substring(0, 2) == '//') prefix = document.location.protocol + prefix; // Protocol-relative wgServer?
 +
if (href.indexOf (prefix) === 0)
 +
return decodeURIComponent (href.substring (prefix.length));
 +
}
 +
return null;
 +
}
 +
function hasClass (elem, name) {
 +
return (' ' + elem.className + ' ').indexOf (' ' + name + ' ') >= 0;
 +
}
 +
function capitalize (str) {
 +
if (!str || str.length === 0) return str;
 +
return str.substr(0, 1).toUpperCase() + str.substr (1);
 +
}
 +
function wikiPagePath (pageName) {
 +
// Note: do not simply use encodeURI, it doesn't encode '&', which might break if wgArticlePath actually has the $1 in
 +
// a query parameter.
 +
return wgArticlePath.replace('$1', encodeURIComponent (pageName).replace(/%3A/g, ':').replace(/%2F/g, '/'));
 +
}
 +
function escapeRE(str) {
 +
return str.replace(/([\\\^\$\.\?\*\+\(\)\[\]])/g, '\\$1');
 +
}
  
// New. Code by TheDJ, 2008-03-12. Comment and nowiki skipping by Lupo, 2010-01-31.
+
function substituteFactory (options) {
function hotcat_find_insertionpoint (wikitext)
+
options = options || {};
{
+
var lead = options.indicator || '$';
  function replaceByBlanks (match)
+
var indicator = escapeRE (lead);
  {
+
var lbrace = escapeRE (options.lbrace || '{');
    return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
+
var rbrace = escapeRE (options.rbrace || '}');
  }
+
var re;
  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
+
re = new RegExp(
var hotcat_uncat_regex =
+
'(?:' + indicator + '(' + indicator + '))|'                                          // $$
  /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}/g;
+
+'(?:' + indicator + '(\\d+))|'                                                        // $0, $1
 +
+'(?:' + indicator + '(?:' + lbrace + '([^' + lbrace + rbrace + ']+)' + rbrace + '))|' // ${key}
 +
+'(?:' + indicator + '(?!(?:[' + indicator + lbrace + ']|\\d))(\\S+?)\\b)'            // $key (only if first char after $ is not $, digit, or { )
 +
,'g');
 +
// Replace $1, $2, or ${key1}, ${key2}, or $key1, $key2 by values from map. $$ is replaced by a single $.
 +
return function (str, map) {
 +
if (!map) return str;
 +
return str.replace(re
 +
,function (match, prefix, idx, key, alpha) {
 +
if (prefix == lead) return lead;
 +
var k = alpha || key || idx;
 +
var replacement = typeof (map[k]) === 'function' ? map[k](match, k) : map[k];
 +
return typeof (replacement) === 'string' ? replacement : (replacement || match);
 +
}
 +
);
 +
};
 +
}
  
// Rewritten (nearly) from scratch. Lupo, 2008-02-27
+
var substitute = substituteFactory();
function hotcat_check_action () {
+
var replaceShortcuts = (function () {
  var ret = 0;
+
var replaceHash = substituteFactory({indicator:'#',lbrace:'[',rbrace:']'});
  if (wgAction != 'edit') return ret; // Not an edit page, so not our business...
+
return function (str, map) {
  if (!document.editform || !document.editform.wpTextbox1) return ret; // No edit form??
+
var s = replaceHash (str, map);
  var summary = new Array () ;
+
return HotCat.capitalizePageNames ? capitalize(s) : s;
  var t = document.editform.wpTextbox1.value ;
+
};
  var prevent_autocommit = 0;
+
})();
  if (  (typeof hotcat_no_autocommit != "undefined" && hotcat_no_autocommit)
+
      || hotcatGetParamValue ('hotcat_nocommit') == '1')
+
    prevent_autocommit = 1;
+
  
  var cat_rm    = hotcatGetParamValue ('hotcat_removecat');
+
// Text modification
  var cat_add  = hotcatGetParamValue ('hotcat_newcat');
+
  var comment  = hotcatGetParamValue ('hotcat_comment') || "";
+
  var cat_key  = hotcatGetParamValue ('hotcat_sortkey');
+
  var cat_name  = hotcat_canonical;
+
  var cat_point = -1; // Position of removed category;
+
  
  if (cat_key != null) cat_key = '|' + cat_key;
+
var findCatsRE =
  if (cat_rm != null && cat_rm.length > 0) {
+
new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(?:' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g');
    var matches = hotcat_find_category (t, cat_rm);
+
    if (!matches || matches.length == 0) {
+
      alert ('Category "' + cat_rm + '" not found; maybe it is in a template?');
+
      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 {
+
      if (cat_add != null && cat_add.length > 0 && matches[0].match.length > 1) {
+
        cat_name = matches[0].match[1] || cat_name;
+
        if (cat_key == null) cat_key  = matches[0].match[2]; // Remember the category key, if any.
+
      }
+
      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 (cat_add != null && cat_add.length > 0) {
+
    var matches = hotcat_find_category (t, cat_add);
+
    if (matches && matches.length > 0) {
+
      alert ('Category "' + cat_add + '" already exists; not added.');
+
      prevent_autocommit = 1;
+
    } else {
+
      var insertionpoint = hotcat_find_insertionpoint (t);
+
      if (insertionpoint < 0) {
+
        // No categories found. If we removed one earlier, insert at that position.
+
        insertionpoint = cat_point;
+
      }
+
      var newcatstring = '\n\[\[' + cat_name + ':' + cat_add + (cat_key || "") + '\]\]';
+
      if (insertionpoint >= 0) {
+
        t = t.substring (0, insertionpoint) + newcatstring + t.substring (insertionpoint);
+
      } else {
+
        t = t + newcatstring;
+
      }
+
      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 ;
+
    document.editform.wpSummary.value = summary.join( "; " )
+
                                      + " using [[Help:Gadget-HotCat|HotCat]]" ;
+
    document.editform.wpMinoredit.checked = true ;
+
    if (!prevent_autocommit) {
+
      // Hide the entire edit section so as not to tempt the user into editing...
+
      var content =    document.getElementById ("bodyContent")      // "monobook" skin
+
                    || document.getElementById ("mw_contentholder")  // "modern" skin
+
                    || document.getElementById ("article");          // classic skins
+
      if (content) content.style.display = "none" ;
+
      document.editform.submit ();
+
    }
+
  }
+
  return ret;
+
}
+
  
function hotcat_clear_span ( span_add ) {
+
function replaceByBlanks (match) {
  while ( span_add.firstChild ) span_add.removeChild ( span_add.firstChild ) ;
+
return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
}
+
}
  
function hotcat_create_span ( span_add ) {
+
function find_category (wikitext, category, once) {
  hotcat_clear_span ( span_add ) ;
+
var cat_regex = null;
  var a_add = document.createElement ( "a" ) ;
+
if(HotCat.template_categories[category]){
  var a_text = document.createTextNode ( "(+)" ) ;
+
cat_regex = new RegExp ('\\{\\{' + wikiTextBlankOrBidi + '(' + HotCat.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi
  span_add.id = "hotcat_add" ;
+
+ '(?:' + HotCat.template_categories[category] + ')'
  a_add.href = "javascript:hotcat_add_new()" ;
+
+ wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}', 'g'
  a_add.appendChild ( a_text ) ;
+
);
  span_add.appendChild ( a_add ) ;
+
} else {
}
+
var cat_name  = escapeRE (category);
 +
var initial  = cat_name.substr (0, 1);
 +
cat_regex = new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi
 +
+ (initial == '\\' || !HotCat.capitalizePageNames
 +
? initial
 +
: '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
 +
+ cat_name.substring (1).replace (wikiTextBlankRE, wikiTextBlank)
 +
+ wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]', 'g'
 +
);
 +
}
 +
if (once) return cat_regex.exec (wikitext);
 +
var copiedtext = wikitext
 +
.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
 +
.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
 +
var result = [];
 +
var curr_match = null;
 +
while ((curr_match = cat_regex.exec (copiedtext)) !== null) {
 +
result.push ({match : curr_match});
 +
}
 +
result.re = cat_regex;
 +
return result; // An array containing all matches, with positions, in result[i].match
 +
}
  
function hotcat_modify ( link_id ) {
+
var interlanguageRE = null;
  var link = document.getElementById ( link_id ) ;
+
  var span = link.parentNode ;
+
  var catname = span.hotcat_name;
+
 
+
  while ( span.firstChild.nextSibling ) span.removeChild ( span.firstChild.nextSibling ) ;
+
  span.firstChild.style.display = "none" ;
+
  hotcat_create_new_span ( span , catname ) ;
+
  hotcat_last_v = "" ;
+
  hotcat_text_changed () ; // Update icon
+
}
+
  
function hotcat_add_new () {
+
function change_category (wikitext, toRemove, toAdd, key, is_hidden) {
  var span_add = document.getElementById ( "hotcat_add" ) ;
+
  hotcat_clear_span ( span_add ) ;
+
  hotcat_last_v = "" ;
+
  hotcat_create_new_span ( span_add , "" ) ;
+
}
+
  
function hotcat_button_label (id, defaultText)
+
function find_insertionpoint (wikitext) {
{
+
var copiedtext = wikitext
  var label = null;
+
.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
  if (hotcat_upload
+
.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
      && typeof (UFUI) != 'undefined'
+
// Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
      && typeof (UFUI.getLabel) == 'function') {
+
var index = -1;
    try {
+
findCatsRE.lastIndex = 0;
      label = UFUI.getLabel (id, true);
+
while (findCatsRE.exec(copiedtext) !== null) index = findCatsRE.lastIndex;
      // Extract the plain text. IE doesn't know that Node.TEXT_NODE == 3
+
if (index < 0) {
      while (label && label.nodeType != 3) label = label.firstChild;
+
// Find the index of the first interlanguage link...
    } catch (ex) {
+
var match = null;
      label = null;
+
if (!interlanguageRE) {
    }
+
// Approximation without API: interlanguage links start with 2 to 3 lower case letters, optionally followed by
  }
+
// a sequence of groups consisting of a dash followed by one or more lower case letters. Exceptions are "simple"
  if (label == null || !label.data) return defaultText;
+
// and "tokipona".
  return label.data;  
+
match = /((^|\n\r?)(\[\[\s*(([a-z]{2,3}(-[a-z]+)*)|simple|tokipona)\s*:[^\]]+\]\]\s*))+$/.exec (copiedtext);
}
+
} else {
 +
match = interlanguageRE.exec(copiedtext);
 +
}
 +
if (match) index = match.index;
 +
return {idx : index, onCat : false};
 +
}
 +
return {idx : index, onCat : index >= 0};
 +
}
  
function hotcat_create_new_span ( thespan , init_text ) {
+
var summary   = [];
   var form = document.createElement ( "form" ) ;
+
var nameSpace = HotCat.category_canonical;
  form.method = "post" ;
+
var cat_point = -1; // Position of removed category;
  form.onsubmit = function () { hotcat_ok(); return false; } ;  
+
  form.id = "hotcat_form" ;
+
  form.style.display = "inline" ;
+
  
  var list = null;
+
if (key) key = '|' + key;
 
+
var keyChange = (toRemove && toAdd && toRemove == toAdd && toAdd.length > 0);
  if (!hotcat_nosuggestions) {
+
var matches;
    // Only do this if we may actually use XMLHttp...
+
if (toRemove && toRemove.length > 0) {
    list = document.createElement ( "select" ) ;
+
matches = find_category (wikitext, toRemove);
    list.id = "hotcat_list" ;
+
if (!matches || matches.length === 0) {
    list.onclick = function ()
+
return {text: wikitext, 'summary': summary, error: HotCat.messages.cat_notFound.replace (/\$1/g, toRemove)};
      {
+
} else {
        var l = document.getElementById("hotcat_list");
+
var before = wikitext.substring (0, matches[0].match.index);
        if (l != null) {
+
var after  = wikitext.substring (matches[0].match.index + matches[0].match[0].length);
          var text = document.getElementById("hotcat_text");
+
if (matches.length > 1) {
          var v = text.value.split('|');
+
// Remove all occurrences in after
          text.value = l.options[l.selectedIndex].text + (v.length > 1 ? '|' + v[1] : "");
+
matches.re.lastIndex = 0;
        }
+
after = after.replace (matches.re, "");
        hotcat_text_changed();
+
}
      };
+
if (toAdd) {
    list.ondblclick = function (evt)
+
nameSpace = matches[0].match[1] || nameSpace;
      {
+
if (key === null) key = matches[0].match[2]; // Remember the category key, if any.
        var l = document.getElementById("hotcat_list");
+
}
        if (l != null) {
+
// Remove whitespace (properly): strip whitespace, but only up to the next line feed.
          var text = document.getElementById("hotcat_text");
+
// If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
          var v = text.value.split('|');
+
// whitespace characters, insert a blank.
          text.value = l.options[l.selectedIndex].text + (v.length > 1 ? '|' + v[1] : "");
+
var i = before.length - 1;
        }
+
while (i >= 0 && before.charAt (i) != '\n' && before.substr (i, 1).search (/\s/) >= 0) i--;
        // Don't call text_changed here if on upload form: hotcat_ok will remove the list
+
var j = 0;
        // anyway, so we must not ask for new suggestions since show_suggestions might
+
while (j < after.length && after.charAt (j) != '\n' && after.substr (j, 1).search (/\s/) >= 0)
        // raise an exception if it tried to show a no longer existing list.
+
j++;
        // Lupo, 2008-01-20
+
if (i >= 0 && before.charAt (i) == '\n' && (after.length === 0 || j < after.length && after.charAt (j) == '\n'))
        if (!hotcat_upload) hotcat_text_changed();
+
i--;
        hotcat_ok(hotcat_evtkeys (evt) & 1); // CTRL pressed?
+
if (i >= 0) before = before.substring (0, i+1); else before = "";
      };
+
if (j < after.length) after = after.substring (j); else after = "";
    list.style.display = "none" ;
+
if (before.length > 0 && before.substring (before.length - 1).search (/\S/) >= 0
  }
+
&& after.length > 0 && after.substr (0, 1).search (/\S/) >= 0)
 
+
before += ' ';
  var text = document.createElement ( "input" ) ;
+
cat_point = before.length;
  var default_width =
+
if (cat_point === 0 && after.length > 0 && after.substr(0,1) == '\n') {
    (typeof (JSconfig) != 'undefined'
+
after = after.substr(1);
    ? JSconfig.keys['HotCatEditBoxWidth']
+
}
    : hotcat_editbox_width
+
wikitext = before + after;
    );
+
if (!keyChange) {
  var default_delay =
+
if(HotCat.template_categories[toRemove]) {
    (typeof (JSconfig) != 'undefined'
+
summary.push (HotCat.messages.template_removed.replace (/\$1/g, toRemove));
    ? JSconfig.keys['HotCatDelay']
+
} else {
    : hotcat_suggestion_delay
+
summary.push (HotCat.messages.cat_removed.replace (/\$1/g, toRemove));
    );
+
}
  if (default_delay < 0) default_delay = 0;
+
}
  text.size = (default_width < 40 ? 40 : default_width);
+
}
  text.id = "hotcat_text" ;
+
}
  text.type = "text" ;
+
if (toAdd && toAdd.length > 0) {
  text.value = init_text ;
+
matches = find_category (wikitext, toAdd);
  text.onkeyup =
+
if (matches && matches.length > 0) {
    function ()
+
return {text: wikitext, 'summary': summary, error : HotCat.messages.cat_exists.replace (/\$1/g, toAdd)};
    {
+
} else {
      window.setTimeout ("hotcat_text_changed ();", default_delay);
+
var onCat = false;
    };
+
if (cat_point < 0) {
 +
var point = find_insertionpoint (wikitext);
 +
cat_point = point.idx;
 +
onCat = point.onCat;
 +
} else {
 +
onCat = true;
 +
}
 +
var newcatstring = '[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
 +
if (cat_point >= 0) {
 +
var suffix = wikitext.substring (cat_point);
 +
wikitext = wikitext.substring (0, cat_point) + (cat_point > 0 ? '\n' : "") + newcatstring + (!onCat ? '\n' : "");
 +
if (suffix.length > 0 && suffix.substr(0, 1) != '\n') {
 +
wikitext += '\n' + suffix;
 +
} else {
 +
wikitext += suffix;
 +
}
 +
} else {
 +
if (wikitext.length > 0 && wikitext.substr (wikitext.length - 1, 1) != '\n')
 +
wikitext += '\n';
 +
wikitext += (wikitext.length > 0 ? '\n' : "") + newcatstring;
 +
}
 +
if (keyChange) {
 +
var k = key || "";
 +
if (k.length > 0) k = k.substr (1);
 +
summary.push (substitute (HotCat.messages.cat_keychange, [null, toAdd, k]));
 +
} else {
 +
summary.push (HotCat.messages.cat_added.replace (/\$1/g, toAdd));
 +
}
 +
if (HotCat.uncat_regexp && !is_hidden) {
 +
var txt = wikitext.replace (HotCat.uncat_regexp, ""); // Remove "uncat" templates
 +
if (txt.length != wikitext.length) {
 +
wikitext = txt;
 +
summary.push (HotCat.messages.uncat_removed);
 +
}
 +
}
 +
}
 +
}
 +
return {text: wikitext, 'summary': summary, error: null};
 +
}
  
  var exists = null;
+
// The real HotCat UI
  if (!hotcat_nosuggestions) {
+
    exists = document.createElement ( "img" ) ;
+
    exists.id = "hotcat_exists" ;
+
    exists.src = hotcat_exists_no ;
+
  }
+
  
  var OK = document.createElement ( "input" ) ;
+
function evtKeys (e) {
  OK.type = "button" ;
+
e = e || window.event || window.Event; // W3C, IE, Netscape
  OK.value = hotcat_button_label ('wpOkUploadLbl', 'OK') ;
+
var code = 0;
  OK.onclick = function (evt) { hotcat_ok (hotcat_evtkeys (evt) & 1); };
+
if (typeof (e.ctrlKey) != 'undefined') { // All modern browsers
 +
// Ctrl-click seems to be overloaded in FF/Mac (it opens a pop-up menu), so treat cmd-click
 +
// as a ctrl-click, too.
 +
if (e.ctrlKey || e.metaKey) code |= 1;
 +
if (e.shiftKey) code |= 2;
 +
} else if (typeof (e.modifiers) != 'undefined') { // Netscape...
 +
if (e.modifiers & (Event.CONTROL_MASK | Event.META_MASK)) code |= 1;
 +
if (e.modifiers & Event.SHIFT_MASK) code |= 2;
 +
}
 +
return code;
 +
}
 +
function evtKill (e) {
 +
e = e || window.event || window.Event; // W3C, IE, Netscape
 +
if (typeof (e.preventDefault) != 'undefined') {
 +
e.preventDefault ();
 +
e.stopPropagation ();
 +
} else
 +
e.cancelBubble = true;
 +
return false;
 +
}
 +
function addEvent (node, evt, f, capture) {
 +
if (window.jQuery && (!capture || !node.addEventListener)) window.jQuery (node).bind (evt, f);
 +
else if (node.addEventListener) node.addEventListener (evt, f, capture); // FF etc; IE >= 9
 +
else if (node.attachEvent) node.attachEvent ('on' + evt, f); // Older IE; Opera
 +
else node['on' + evt] = f; // Very old!
 +
}
  
  var cancel = document.createElement ( "input" ) ;
+
var catLine      = null;
  cancel.type = "button" ;
+
var onUpload    = false;
  cancel.value = hotcat_button_label ('wpCancelUploadLbl', 'Cancel') ;
+
var editors      = [];
  cancel.onclick = hotcat_cancel ;
+
  
  if (list != null) form.appendChild ( list ) ;
+
var commitButton = null;
   form.appendChild ( text ) ;
+
var commitForm   = null;
  if (exists != null) form.appendChild ( exists ) ;
+
var multiSpan    = null;
  form.appendChild ( OK ) ;
+
  form.appendChild ( cancel ) ;
+
  thespan.appendChild ( form ) ;
+
  text.focus () ;
+
}
+
  
function hotcat_ok (nocommit) {
+
var pageText    = null;
  var text = document.getElementById ( "hotcat_text" ) ;
+
var pageTime    = null;
  var = text.value || "";
+
var pageWatched  = false;
  var pipe = v.indexOf ('|');
+
var watchCreate  = false;
  if (pipe >= 0) v = v.substring (0, pipe);
+
var watchEdit    = false;
  v = v.replace(/_/g, ' ').replace(/^\s\s*/, "").replace(/\s\s*$/, ""); // Trim leading and trailing blanks
+
var minorEdits   = false;
   // Empty category ?
+
var editToken    = null;
  if (!v) {
+
    hotcat_cancel() ;
+
    return ;
+
  }
+
  
  v = v.ucFirst ();
+
var is_rtl      = false;
  // Get the links and the categories of the chosen category page
+
var serverTime   = null;
  var url = wgServer + wgScriptPath + '/api.php?action=query&titles='
+
var lastRevId    = null;
          + encodeURIComponent ('Category:' + v)
+
var pageTextRevId = null;
          + '&prop=info|links|categories&plnamespace=14&format=json';
+
var conflictingUser = null;
  var request = sajax_init_object() ;
+
   if (request == null) {
+
    //Oops! We don't have XMLHttp...
+
    hotcat_nosuggestions = true;
+
    hotcat_closeform (nocommit);
+
    hotcat_running = 0;
+
    return;
+
  }
+
  request.open ('GET', url, true);
+
  request.onreadystatechange =
+
    function () {
+
      if (request.readyState != 4) return;
+
      if (request.status != 200) {
+
        hotcat_closeform (nocommit);
+
      } else {
+
        var txt = document.getElementById ('hotcat_text');
+
        var original = txt.value;
+
        var do_submit = hotcat_json_resolve (eval ('(' + request.responseText + ')'));
+
        if (do_submit) {
+
          hotcat_closeform (
+
            nocommit
+
            ,(txt.value != original) ? " (redirect \[\[Category:" + v + "\]\] resolved)" : null
+
          );
+
        }
+
      }
+
    };
+
  request.setRequestHeader ('Pragma', 'cache=yes');
+
  request.setRequestHeader ('Cache-Control', 'no-transform');
+
  request.send (null);
+
}
+
  
function hotcat_json_resolve (params)
+
var newDOM      = false; // true if MediaWiki serves the new UL-LI DOM for categories
{
+
  function resolve (page)
+
  {
+
    var cats    = page.categories;
+
    var is_dab  = false;
+
    var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
+
    if (!is_redir && cats) {
+
      for (var c = 0; c < cats.length; c++) {
+
        var cat = cats[c]["title"];
+
        if (cat) cat = cat.substring (cat.indexOf (':') + 1); // Strip namespace prefix
+
        if (cat == 'Disambiguation') {
+
          is_dab = true; break;
+
        } else if (cat == 'Category_redirects' || cat == 'Category redirects') {
+
          is_redir = true; break;
+
        }
+
      }
+
    }
+
    if (!is_redir && !is_dab) return true;
+
    var lks = page.links;
+
    var titles = new Array ();
+
    for (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.
+
        var match = lks[i]["title"];
+
        // Remove the category prefix
+
        match = match.substring (match.indexOf (':') + 1);
+
        titles.push (match);
+
        if (is_redir) break;
+
      }
+
    }
+
    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
+
function setMultiInput () {
  for (var page in params.query.pages) return resolve (params.query.pages[page]);
+
if (commitButton || onUpload) return;
  return true; // In case we have none.
+
commitButton = make ('input');
}
+
commitButton.type  = 'button';
 +
commitButton.value = HotCat.messages.commit;
 +
commitButton.onclick = multiSubmit;
 +
if (multiSpan) {
 +
multiSpan.parentNode.replaceChild (commitButton, multiSpan);
 +
} else {
 +
catLine.appendChild (commitButton);
 +
}
 +
}
  
function hotcat_closeform (nocommit, comment)
+
function checkMultiInput () {
{
+
if (!commitButton) return;
  var text = document.getElementById ( "hotcat_text" ) ;
+
var has_changes = false;
  var v = text.value.split('|');
+
for (var i = 0; i < editors.length; i++) {
  var key = v.length > 1 ? v[1] : null;
+
if (editors[i].state != CategoryEditor.UNCHANGED) {
  v = v[0];
+
has_changes = true;
  v = v.replace(/_/g, ' ').replace(/^\s\s*/, "").replace(/\s\s*$/, ""); // Trim leading and trailing blanks
+
break;
  v = v.ucFirst();
+
}
  if (!v                                                // Empty
+
}
      || wgNamespaceNumber == 14 && v == wgTitle        // Self-reference
+
commitButton.disabled = !has_changes;
      || text.parentNode.parentNode.id != 'hotcat_add'  // Modifying, but
+
}
        && text.parentNode.parentNode.hotcat_name == v) //  name unchanged
+
  {
+
    hotcat_cancel ();
+
    return;
+
  }
+
 
+
  if (hotcat_upload) {
+
    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 ) {
+
function currentTimestamp () {
  var span = document.getElementById("hotcat_form") ;
+
var now = new Date();
  while ( span.tagName != "SPAN" ) span = span.parentNode ;
+
var ts  = "" + now.getUTCFullYear();
  var add = 0 ;
+
function two (s) { return s.substr (s.length - 2); }
  if ( span.id == "hotcat_add" ) add = 1 ;
+
ts = ts
  span.id = "" ;
+
+ two ('0' + (now.getUTCMonth() + 1))
  while ( span.firstChild ) span.removeChild ( span.firstChild ) ;
+
+ two ('0' + now.getUTCDate())
  var na = document.createElement ( "a" ) ;
+
+ two ('00' + now.getUTCHours())
  na.href = wgArticlePath.split("$1").join("Category:" + encodeURI (text)) ;
+
+ two ('00' + now.getUTCMinutes())
  na.appendChild ( document.createTextNode ( text ) ) ;
+
+ two ('00' + now.getUTCSeconds());
  na.setAttribute ( "title" , "Category:" + text ) ;
+
return ts;
  span.appendChild ( na ) ;
+
}
  var catline = getElementsByClassName ( document , "p" , "catlinks" ) [0] ;
+
  if ( add ) hotcat_append_add_span ( catline ) ;
+
  
  for ( var i = 0 ; i < span.parentNode.childNodes.length ; i++ ) {
+
var saveInProgress = false;
    if ( span.parentNode.childNodes[i] != span ) continue ;
+
function initiateEdit (doEdit, failure) {
    hotcat_modify_span ( span , i ) ;
+
if (saveInProgress) return;
    span.hotcat_key = (key && key.length > 0) ? key : null;
+
saveInProgress = true;
    break ;
+
var oldButtonState;
  }
+
if (commitButton) {
}
+
oldButtonState = commitButton.disabled;
 +
commitButton.disabled = true;
 +
}
  
function hotcat_cancel () {
+
function fail() {
  var span = document.getElementById("hotcat_form").parentNode ;
+
saveInProgress = false;
  if ( span.id == "hotcat_add" ) {
+
if (commitButton) commitButton.disabled = oldButtonState;
    hotcat_create_span ( span ) ;
+
failure.apply(this, arguments);
  } else {
+
}
    while ( span.firstChild.nextSibling ) span.removeChild ( span.firstChild.nextSibling ) ;
+
    span.firstChild.style.display = "" ;
+
    for ( var i = 0 ; i < span.parentNode.childNodes.length ; i++ ) {
+
      if ( span.parentNode.childNodes[i] != span ) continue ;
+
      hotcat_modify_span ( span , i ) ;
+
      break ;
+
    }
+
  }
+
}
+
  
function hotcat_text_changed () {
+
// Must use Ajax here to get the user options and the edit token.
  if ( hotcat_running ) return ;
+
  var text = document.getElementById ( "hotcat_text" ) ;
+
  var v = text.value.ucFirst() ;
+
  // Disregard anything after a pipe.
+
  var pipe = v.indexOf ('|');
+
  if (pipe >= 0) v = v.substring (0, pipe);
+
  if ( hotcat_last_v == v ) return ; // Nothing's changed...
+
  
  if (hotcat_nosuggestions) {
+
getJSON ({
    // On IE, XMLHttp uses ActiveX, and the user may deny execution... just make sure
+
uri : wgServer + wgScriptPath + '/api.php'
    // the list is not displayed.
+
,data : 'format=json&action=query&titles=' + encodeURIComponent (wgPageName)
    var list = document.getElementById ('hotcat_list');
+
+ '&prop=info%7Crevisions%7Clanglinks&inprop=watched&intoken=edit&rvprop=content%7Ctimestamp%7Cids%7Cuser&lllimit=500'
    if (list != null) list.style.display = "none" ;
+
+ '&rvlimit=2&rvdir=newer&rvstartid=' + wgCurRevisionId
    var exists = document.getElementById ('hotcat_exists');
+
+ '&meta=siteinfo%7Cuserinfo&uiprop=options'
    if (exists != null) exists.style.display = "none" ;
+
,success : function (json) { setPage(json); doEdit(fail); }
    return;
+
,error : function (req) { fail(req.status + ' ' + req.statusText); }
  }
+
});
 
+
}
  hotcat_running = 1 ;
+
  hotcat_last_v = v ;
+
  
  if ( v != "" ) {
+
function multiChangeMsg (count) {
    var url = wgServer + wgScriptPath
+
var msg = HotCat.messages.multi_change;
            + "/api.php?format=json&action=opensearch&namespace=14&limit=30&search="
+
if (typeof (msg) != 'string' && msg.length) {
            + encodeURIComponent( v ) ;
+
if (window.mediaWiki && window.mediaWiki.language && window.mediaWiki.language.convertPlural) {
    var request = sajax_init_object() ;
+
msg = window.mediaWiki.language.convertPlural (count, msg);
    if (request == null) {
+
} else {
      //Oops! We don't have XMLHttp...
+
msg = msg[msg.length-1];
      hotcat_nosuggestions = true;
+
}
      var list = document.getElementById ('hotcat_list');
+
}
      if (list != null) list.style.display = "none" ;
+
return substitute (msg, [null, "" + count]);
      var exists = document.getElementById ('hotcat_exists');
+
}
      if (exists != null) exists.style.display = "none" ;
+
      hotcat_running = 0;
+
      return;
+
    }
+
    request.open('GET', url, true);
+
    request.onreadystatechange =
+
      function () {
+
        if (request.readyState == 4 && request.responseText != null && request.responseText.indexOf ('[') == 0) {
+
          var result = eval ('(' + request.responseText + ')');
+
          if (result != null && result.length == 2 && result[0] == v) {
+
            var titles = result[1];
+
            for (var i = 0; i < titles.length; i++) {
+
              // Remove the namespace. No hardcoding of 'Category:', please, other Wikis may have
+
              // local names ("Kategorie:" on de-WP, for instance). Also don't break on category
+
              // names containing a colon
+
              titles[i] = titles[i].substring (titles[i].indexOf (':') + 1);
+
            }
+
            // 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) ;
+
          }
+
        }
+
      };
+
    request.setRequestHeader ('Pragma', 'cache=yes');
+
    request.setRequestHeader ('Cache-Control', 'no-transform');
+
    request.send (null);
+
  } else {
+
    hotcat_show_suggestions ([]);
+
  }
+
  hotcat_running = 0 ;
+
}
+
  
function hotcat_show_suggestions ( titles ) {
+
function performChanges (failure, singleEditor) {
  var text = document.getElementById ( "hotcat_text" ) ;
+
if (pageText === null) {
  var list = document.getElementById ( "hotcat_list" ) ;
+
failure (HotCat.messages.multi_error);
  var icon = document.getElementById ( "hotcat_exists" ) ;
+
return;
  // Somehow, after a double click on the selection list, we still get here in IE, but
+
}
  // the list may no longer exist... Lupo, 2008-01-20
+
// Backwards compatibility after message change (added $2 to cat_keychange)
  if (list == null) return;
+
if (HotCat.messages.cat_keychange.indexOf ('$2') < 0) HotCat.messages.cat_keychange += '"$2"';
  if (hotcat_nosuggestions) {
+
// More backwards-compatibility with earlier HotCat versions:
    list.style.display = "none" ;
+
if (!HotCat.messages.short_catchange) HotCat.messages.short_catchange = '[[' + HotCat.category_canonical + ':$1]]';
    if (icon != null) icon.style.display = "none";
+
// Create a form and submit it. We don't use the edit API (api.php?action=edit) because
    return;
+
// (a) sensibly reporting back errors like edit conflicts is always a hassle, and
  }
+
// (b) we want to show a diff for multi-edits anyway, and
  if ( titles.length == 0 ) {
+
// (c) we want to trigger onsubmit events, allowing user code to intercept the edit.
    list.style.display = "none" ;
+
// Using the form, we can do (b) and (c), and we get (a) for free. And, of course, using the form
    icon.src = hotcat_exists_no ;
+
// automatically reloads the page with the updated categories on a successful submit, which
    return ;
+
// we would have to do explicitly if we used the edit API.
  }
+
var action;
 
+
// Normally, we don't have to care about edit conflicts. If some other user edited the page in the meantime, the
  // Set list size to minimum of 5 and actual number of titles. Formerly was just 5.
+
// server will take care of it and merge the edit automatically or present an edit conflict screen. However, the
  // Lupo, 2008-01-20
+
// server suppresses edit conflicts with oneself. Hence, if we have a conflict, and the conflicting user is the
  list.size = (titles.length > 5 ? 5 : titles.length) ;
+
// current user, then we set the "oldid" value and switch to diff, which gives the "you are editing an old version;
  // Avoid list height 1: double-click doesn't work in FF. Lupo, 2008-02-27
+
// if you save, any more recent changes will be lost" screen.
  if (list.size == 1) list.size = 2;
+
var editingOldVersion = lastRevId !== null && lastRevId != wgCurRevisionId || pageTextRevId !== null && pageTextRevId != wgCurRevisionId;
  list.style.align = "left" ;
+
var selfEditConflict = editingOldVersion && conflictingUser && conflictingUser == wgUserName;
  list.style.zIndex = 5 ;
+
if (singleEditor && !singleEditor.noCommit && !HotCat.no_autocommit && editToken && !selfEditConflict) {
  list.style.position = "absolute" ;
+
// If we do have an edit conflict, but not with ourself, that's no reason not to attempt to save: the server side may actually be able to
 +
// merge the changes. We just need to make sure that we do present a diff view if it's a self edit conflict.
 +
commitForm.wpEditToken.value = editToken;
 +
action = commitForm.wpDiff;
 +
if (action) action.name = action.value = 'wpSave';
 +
} else {
 +
action = commitForm.wpSave;
 +
if (action) action.name = action.value = 'wpDiff';
 +
}
 +
var result = { text : pageText };
 +
var changed = [], added = [], deleted = [], changes = 0;
 +
var toEdit = !!singleEditor ? [singleEditor] : editors;
 +
var error = null;
 +
var i;
 +
for (i=0; i < toEdit.length; i++) {
 +
if (toEdit[i].state == CategoryEditor.CHANGED) {
 +
result = change_category (
 +
result.text
 +
, toEdit[i].originalCategory
 +
, toEdit[i].currentCategory
 +
, toEdit[i].currentKey
 +
, toEdit[i].currentHidden
 +
);
 +
if (!result.error) {
 +
changes++;
 +
if (!toEdit[i].originalCategory || toEdit[i].originalCategory.length === 0) {
 +
added.push (toEdit[i].currentCategory);
 +
} else {
 +
changed.push ({from : toEdit[i].originalCategory, to : toEdit[i].currentCategory});
 +
}
 +
} else if (error === null) {
 +
error = result.error;
 +
}
 +
} else if (  toEdit[i].state == CategoryEditor.DELETED
 +
  && toEdit[i].originalCategory
 +
  && toEdit[i].originalCategory.length > 0)
 +
{
 +
result = change_category (result.text, toEdit[i].originalCategory, null, null, false);
 +
if (!result.error) {
 +
changes++;
 +
deleted.push (toEdit[i].originalCategory);
 +
} else if (error === null) {
 +
error = result.error;
 +
}
 +
}
 +
}
 +
if (error !== null) { // Do not commit if there were errors
 +
action = commitForm.wpSave;
 +
if (action) action.name = action.value = 'wpDiff';
 +
}
 +
// Fill in the form and submit it
 +
commitForm.wpAutoSummary.value = 'd41d8cd98f00b204e9800998ecf8427e'; // MD5 hash of the empty string
 +
commitForm.wpMinoredit.checked = minorEdits;
 +
commitForm.wpWatchthis.checked = wgArticleId === 0 && watchCreate || watchEdit || pageWatched;
 +
if (wgArticleId > 0 || !!singleEditor) {
 +
if (changes == 1) {
 +
if (result.summary && result.summary.length > 0)
 +
commitForm.wpSummary.value = HotCat.messages.prefix + result.summary.join (HotCat.messages.separator) + HotCat.messages.using;
 +
commitForm.wpMinoredit.checked = HotCat.single_minor || minorEdits;
 +
} else if (changes > 1) {
 +
var summary = [];
 +
var shortSummary = [];
 +
// Deleted
 +
for (i = 0; i < deleted.length; i++) {
 +
summary.push ('-' + substitute (HotCat.messages.short_catchange, [null, deleted[i]]));
 +
}
 +
if (deleted.length == 1)
 +
shortSummary.push ('-' + substitute (HotCat.messages.short_catchange, [null, deleted[0]]));
 +
else if (deleted.length > 1)
 +
shortSummary.push ('- ' + multiChangeMsg (deleted.length));
 +
// Added
 +
for (i = 0; i < added.length; i++) {
 +
summary.push ('+' + substitute (HotCat.messages.short_catchange, [null, added[i]]));
 +
}
 +
if (added.length == 1)
 +
shortSummary.push ('+' + substitute (HotCat.messages.short_catchange, [null, added[0]]));
 +
else if (added.length > 1)
 +
shortSummary.push ('+ ' + multiChangeMsg (added.length));
 +
// Changed
 +
var arrow = is_rtl ? '\u2190' : '\u2192'; // left and right arrows. Don't use ← and → in the code.
 +
for (i = 0; i < changed.length; i++) {
 +
if (changed[i].from != changed[i].to) {
 +
summary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[i].from]) + arrow
 +
+ substitute (HotCat.messages.short_catchange, [null, changed[i].to]));
 +
} else {
 +
summary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[i].from]));
 +
}
 +
}
 +
if (changed.length == 1) {
 +
if (changed[0].from != changed[0].to) {
 +
shortSummary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[0].from]) + arrow
 +
+ substitute (HotCat.messages.short_catchange, [null, changed[0].to]));
 +
} else {
 +
shortSummary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[0].from]));
 +
}
 +
} else if (changed.length > 1) {
 +
shortSummary.push ('± ' + multiChangeMsg (changed.length));
 +
}
 +
if (summary.length > 0) {
 +
summary = summary.join (HotCat.messages.separator);
 +
if (summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length) {
 +
summary = shortSummary.join (HotCat.messages.separator);
 +
}
 +
commitForm.wpSummary.value = HotCat.messages.prefix + summary + HotCat.messages.using;
 +
}
 +
}
 +
}
 +
commitForm.wpTextbox1.value = result.text;
 +
commitForm.wpStarttime.value = serverTime || currentTimestamp ();
 +
commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
 +
if (selfEditConflict) commitForm.oldid.value = "" + (pageTextRevId || wgCurRevisionId);
 +
// Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
 +
commitForm.hcCommit.click();
 +
}
  
  // Was listh = titles.length * 20: that makes no sense if titles.length > list.size
+
function resolveMulti (toResolve, callback) {
  // Lupo, 2008-01-20
+
var i;
  var listh = list.size * 20;
+
for (i = 0; i < toResolve.length; i++) {
  var nl = parseInt (text.offsetLeft) - 1 ;
+
toResolve[i].dab = null;
  var nt = parseInt (text.offsetTop) - listh ;
+
toResolve[i].dabInput = toResolve[i].lastInput;
  if (skin == 'nostalgia' || skin == 'cologneblue' || skin == 'standard') {
+
}
    // These three skins have the category line at the top of the page. Make the suggestions
+
if (noSuggestions) {
    // appear *below* out input field.
+
callback (toResolve);
    nt = parseInt (text.offsetTop) + parseInt (text.offsetHeight) + 3;
+
return;
  }
+
}
  list.style.top = nt + "px" ;
+
// Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
  list.style.width = ""; // No fixed width (yet)
+
// category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
  list.style.height = listh + "px" ;
+
var args = 'action=query&prop=info%7Clinks%7Ccategories%7Ccategoryinfo&plnamespace=14'
  list.style.left = nl + "px" ;
+
+ '&pllimit=' + (toResolve.length * 10)
  while ( list.firstChild ) list.removeChild ( list.firstChild ) ;
+
+ '&cllimit=' + (toResolve.length * 10)
  for ( var i = 0 ; i < titles.length ; i++ ) {
+
+ '&format=json&titles=';
    var opt = document.createElement ( "option" ) ;
+
for (i = 0; i < toResolve.length; i++) {
    var ot = document.createTextNode ( titles[i] ) ;
+
var v = toResolve[i].dabInput;
    opt.appendChild ( ot ) ;
+
v = replaceShortcuts (v, HotCat.shortcuts);
    //opt.value = titles[i] ;
+
toResolve[i].dabInputCleaned = v;
    list.appendChild ( opt ) ;
+
args += encodeURIComponent ('Category:' + v);
  }
+
if (i+1 < toResolve.length) args += '%7C';
 
+
}
  icon.src = hotcat_exists_yes ;
+
getJSON({
 
+
uri : wgServer + wgScriptPath + '/api.php'
  var nof_titles = titles.length;
+
,data : args
 +
,success: function (json) { resolveRedirects (toResolve, json); callback (toResolve); }
 +
,error: function (req) { if (!req) noSuggestions = true; callback (toResolve); }
 +
});
 +
}
  
  var first_title = titles.shift () ;
+
function resolveOne (page, toResolve) {
  var v = text.value.split('|');
+
var cats    = page.categories;
  var key = v.length > 1 ? '|' + v[1] : "";
+
var lks      = page.links;
  v = v[0].ucFirst();
+
var is_dab  = false;
  text.focus ();
+
var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
  if ( first_title == v ) {
+
var is_hidden = page.categoryinfo && typeof (page.categoryinfo.hidden) == 'string';
    if (nof_titles == 1) {
+
var is_missing = typeof(page.missing) == 'string';
      // Only one result, and it's the same as whatever is in the input box: makes no sense
+
var i;
      // to show the list.
+
for (i = 0; i < toResolve.length; i++) {
      list.style.display = 'none';
+
if (toResolve.length > 1 && toResolve[i].dabInputCleaned != page.title.substring (page.title.indexOf (':') + 1)) continue;
    }
+
// Note: the server returns in page an NFC normalized Unicode title. If our input was not NFC normalized, we may not find
    return ;
+
// any entry here. If we have only one editor to resolve (the most common case, I presume), we may simply skip the check.
  }
+
toResolve[i].currentHidden = is_hidden;
 +
toResolve[i].inputExists = !is_missing;
 +
toResolve[i].icon.src = armorUri(is_missing ? HotCat.existsNo : HotCat.existsYes);
 +
}
 +
if (is_missing) return;
 +
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 (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 (i = 0; i < toResolve.length; i++) {
 +
if (toResolve.length > 1 && toResolve[i].dabInputCleaned != page.title.substring (page.title.indexOf (':') + 1)) continue;
 +
toResolve[i].inputExists = true; // Might actually be wrong if it's a redirect pointing to a non-existing category
 +
toResolve[i].icon.src = armorUri(HotCat.existsYes);
 +
if (titles.length > 1) {
 +
toResolve[i].dab = titles;
 +
} else {
 +
toResolve[i].text.value =
 +
titles[0] + (toResolve[i].currentKey !== null ? '|' + toResolve[i].currentKey : "");
 +
}
 +
}
 +
}
  
  if (list.offsetWidth < text.offsetWidth)
+
function resolveRedirects (toResolve, params) {
    list.style.width = text.offsetWidth + "px";
+
if (!params || !params.query || !params.query.pages) return;
  else {
+
for (var p in params.query.pages) resolveOne (params.query.pages[p], toResolve);
 +
}
  
    function position (node)
+
function multiSubmit () {
    {
+
var toResolve = [];
      var t = 0, l = 0;
+
for (var i = 0; i < editors.length; i++) {
      do {
+
if (editors[i].state == CategoryEditor.CHANGE_PENDING || editors[i].state == CategoryEditor.OPEN)
        t = t + (node.offsetTop  || 0);
+
toResolve.push (editors[i]);
        l = l + (node.offsetLeft || 0);
+
}
        node = node.offsetParent;
+
if (toResolve.length === 0) {
      } while (node);
+
initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
      return {x : l, y : t};
+
return;
    }
+
}
 +
resolveMulti (
 +
  toResolve
 +
, function (resolved) {
 +
var firstDab = null;
 +
var dontChange = false;
 +
for (var i = 0; i < resolved.length; i++) {
 +
if (resolved[i].lastInput != resolved[i].dabInput) {
 +
// We didn't disable all the open editors, but we did asynchronous calls. It is
 +
// theoretically possible that the user changed something...
 +
dontChange = true;
 +
} else {
 +
if (resolved[i].dab) {
 +
if (!firstDab) firstDab = resolved[i];
 +
} else {
 +
if (resolved[i].acceptCheck(true)) resolved[i].commit();
 +
}
 +
}
 +
}
 +
if (firstDab) {
 +
showDab (firstDab);
 +
} else if (!dontChange) {
 +
initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
 +
}
 +
}
 +
);
 +
}
  
     function scroll_offset (what)
+
var cat_prefix = null;
    {
+
var noSuggestions = false;
      var s = 'scroll' + what;
+
var suggestionEngines = {
      return (document.documentElement ? document.documentElement[s] : 0)
+
opensearch :
            || document.body[s] || 0;
+
{ uri     : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1' // $1 = search term
     }
+
,handler : // Function to convert result of uri into an array of category names
 +
function (queryResult, queryKey) {
 +
if (queryResult && queryResult.length === 2) {
 +
var key = queryResult[0].substring(queryResult[0].indexOf(':') + 1);
 +
var titles = queryResult[1];
 +
var exists = false;
 +
if (!cat_prefix) cat_prefix = new RegExp ('^(' + HotCat.category_regexp + ':)');
 +
for (var i = 0; i < titles.length; i++) {
 +
cat_prefix.lastIndex = 0;
 +
var m = cat_prefix.exec (titles[i]);
 +
if (m && m.length > 1) {
 +
titles[i] = titles[i].substring (titles[i].indexOf (':') + 1); // rm namespace
 +
if (key == titles[i]) exists = true;
 +
} else {
 +
titles.splice (i, 1); // Nope, it's not a category after all.
 +
i--;
 +
}
 +
}
 +
titles.exists = exists;
 +
if (queryKey != key) titles.normalized = key; // Remember the NFC normalized key we got back from the server
 +
return titles;
 +
}
 +
return null;
 +
}
 +
}
 +
,internalsearch :
 +
{ uri    : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1&apprefix=$1'
 +
,handler :
 +
function (queryResult, queryKey) {
 +
if (queryResult && queryResult.query && queryResult.query.allpages) {
 +
var titles = queryResult.query.allpages;
 +
for (var i = 0; i < titles.length; i++) {
 +
titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
 +
}
 +
return titles;
 +
}
 +
return null;
 +
}
 +
}
 +
,exists :
 +
{ uri    : '/api.php?format=json&action=query&prop=info&titles=Category:$1'
 +
,handler :
 +
function (queryResult, queryKey) {
 +
if (queryResult && queryResult.query && queryResult.query.pages && !queryResult.query.pages[-1]) {
 +
// Should have exactly 1
 +
for (var p in queryResult.query.pages) {
 +
var title = queryResult.query.pages[p].title;
 +
title = title.substring (title.indexOf (':') + 1);
 +
var titles = [title];
 +
titles.exists = true;
 +
if (queryKey != title) titles.normalized = title; // NFC
 +
return titles;
 +
}
 +
}
 +
return null;
 +
}
 +
}
 +
,subcategories :
 +
// I don't understand why they didn't map cmnamespace=14 automatically to cmtype=subcat,
 +
// which gives better results and is faster.
 +
{ uri    : '/api.php?format=json&action=query&list=categorymembers'
 +
+(function (version) {
 +
var m = version.match(/^(\d+)\.(\d+)/);
 +
var major = 0, minor = 0;
 +
if (m && m.length > 1) {
 +
major = parseInt (m[1], 10);
 +
minor = (m.length > 2 ? parseInt (m[2], 10) : 0);
 +
}
 +
if (major > 1 || major === 1 && minor > 17) return '&cmtype=subcat'; // Since MW1.18
 +
return '&cmnamespace=14';
 +
  }
 +
)(wgVersion)
 +
+'&cmlimit=max&cmtitle=Category:$1'
 +
,handler :
 +
function (queryResult, queryKey) {
 +
if (queryResult && queryResult.query && queryResult.query.categorymembers) {
 +
var titles = queryResult.query.categorymembers;
 +
for (var i = 0; i < titles.length; i++) {
 +
titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
 +
}
 +
return titles;
 +
}
 +
return null;
 +
}
 +
}
 +
,parentcategories :
 +
{ uri     : '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max'
 +
,handler :
 +
function (queryResult, queryKey) {
 +
if (queryResult && queryResult.query && queryResult.query.pages) {
 +
for (var p in queryResult.query.pages) {
 +
if (queryResult.query.pages[p].categories) {
 +
var titles = queryResult.query.pages[p].categories;
 +
for (var i = 0; i < titles.length; i++) {
 +
titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
 +
}
 +
return titles;
 +
}
 +
}
 +
}
 +
return null;
 +
}
 +
}
 +
};
  
    function viewport (what)
+
var suggestionConfigs = {
    {
+
searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}, show: true, temp: false, noCompletion : false}
      if (typeof (is_safari) != 'undefined' && is_safari && !document.evaluate)
+
,pagelist    : {name: 'Page list', engines: ['internalsearch', 'exists'], cache: {}, show: true, temp: false, noCompletion : false}
        return window['inner' + what];
+
,combined    : {name: 'Combined search', engines: ['opensearch', 'internalsearch'], cache: {}, show: true, temp: false, noCompletion : false}
      var s = 'client' + what;
+
,subcat      : {name: 'Subcategories', engines: ['subcategories'], cache: {}, show: true, temp: true, noCompletion : true}
      if (typeof (is_opera) != 'undefined' && is_opera) return document.body[s];
+
,parentcat  : {name: 'Parent categories', engines: ['parentcategories'], cache: {}, show: true, temp: true, noCompletion : true}
      return (document.documentElement ? document.documentElement[s] : 0)
+
};
            || document.body[s] || 0;
+
    }
+
  
    var scroll = scroll_offset ('Left');
+
function CategoryEditor () { this.initialize.apply (this, arguments); }
    var view_w = viewport ('Width');
+
CategoryEditor.UNCHANGED      = 0;
    var l_pos  = position (list);
+
CategoryEditor.OPEN          = 1; // Open, but no input yet
    var w      = list.offsetWidth;
+
CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
    if (l_pos.x + w > scroll + view_w) {
+
CategoryEditor.CHANGED        = 3;
      if (w > view_w) w = view_w;
+
CategoryEditor.DELETED        = 4;
      list.style.width = w + "px";
+
      list.style.left = nl - (l_pos.x + w - scroll - view_w) + "px";
+
    }
+
  }
+
  
  list.style.display = "block" ;
+
// IE6 sometimes forgets to redraw the list when editors are opened or closed.
 
+
// Adding/removing a dummy element helps, at least when opening editors.
  // Put the first entry of the title list into the text field, and select the
+
var dummyElement = make ('\xa0', true);
  // new suffix such that it'll be overwritten if the user keeps typing.
+
  // 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
+
  // again differently from the two versions previously implemented.
+
  // Lupo, 2008-01-20
+
  // Only put first entry into the list if the user hasn't typed something
+
  // conflicting yet Dschwen 2008-02-18
+
  if ( ( text.setSelectionRange ||
+
        text.createTextRange ||
+
        typeof (text.selectionStart) != 'undefined' &&
+
        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 {
+
      text.selectionStart = nosel;
+
      text.selectionEnd  = first_title.length;
+
    }
+
    if (nof_titles == 1) {
+
      // Only one result, and we've just put it into the input box: makes no sense
+
      // to show the list.
+
      list.style.display = 'none';
+
    }
+
  
  }
+
function forceRedraw () {
}
+
if (!is_ie6) return;
 +
if (dummyElement.parentNode) {
 +
document.body.removeChild (dummyElement);
 +
} else {
 +
document.body.appendChild (dummyElement);
 +
}
 +
}
  
function hotcat_get_state ()
+
// Event keyCodes that we handle in the text input field/suggestion list.
{
+
var BS = 8, TAB = 9, RET = 13, ESC = 27, SPACE = 32, PGUP = 33, PGDOWN = 34, UP = 38, DOWN = 40, DEL = 46, IME = 229;
  var cats = document.getElementById ('catlinks');
+
  if (cats == null) return "";
+
function makeActive (which) {
  var result = null;
+
if (which.is_active) return;
  cats = cats.getElementsByTagName ('span') ;
+
for (var i = 0; i < editors.length; i++) {
  for (var i = 0; i < cats.length; i++ ) {
+
if (editors[i] !== which) editors[i].inactivate ();
    var text = cats[i].hotcat_name;
+
}
    var key  = cats[i].hotcat_key;
+
which.is_active = true;
    if (text) {
+
if (which.dab) {
      if (key) text += '|' + key;
+
showDab (which);
      if (result == null)
+
} else {
        result = text;
+
// Check for programmatic value changes.
      else
+
var expectedInput = which.lastRealInput || which.lastInput || "";
        result = result + '\n' + text;
+
var actualValue = which.text.value || "";
    }
+
if (expectedInput.length === 0 && actualValue.length > 0 || expectedInput.length > 0 && actualValue.indexOf (expectedInput) !== 0) {
  }
+
// Somehow the field's value appears to have changed, and which.lastSelection therefore is no longer valid. Try to set the
  return result;
+
// cursor at the end of the category, and do not display the old suggestion list.
}
+
which.showsList = false;
 +
var v = actualValue.split('|');
 +
which.lastRealInput = which.lastInput = v[0];
 +
if (v.length > 1) which.currentKey = v[1];
 +
if (which.lastSelection) which.lastSelection = {start: v[0].length, end: v[0].length};
 +
}
 +
if (which.showsList) which.displayList();
 +
if (which.lastSelection) {
 +
if (is_webkit) {
 +
// WebKit (Safari, Chrome) has problems selecting inside focus()
 +
// See http://code.google.com/p/chromium/issues/detail?id=32865#c6
 +
window.setTimeout (
 +
function () { which.setSelection (which.lastSelection.start, which.lastSelection.end); }
 +
,1
 +
);
 +
} else {
 +
which.setSelection (which.lastSelection.start, which.lastSelection.end);
 +
}
 +
}
 +
}
 +
}
  
function hotcat_set_state (state)
+
function showDab (which) {
{
+
if (!which.is_active) {
   var cats = state.split ('\n');
+
makeActive(which);
   if (cats.length == 0) return null;
+
} else {
   var parent = document.getElementById ('catlinks');
+
which.showSuggestions (which.dab, false, null, null); // do autocompletion, no key, no engine selector
  if (parent == null) return state;
+
which.dab = null;
   // HotCat uses a 'p' element inside the 'div' to wrap its spans...
+
}
   parent = parent.firstChild;
+
}
   if (parent == null || parent.className != 'catlinks') return state;
+
 
   var n = (parent.childNodes ? parent.childNodes.length - 1 : 0);
+
CategoryEditor.prototype = {
  if (n < 0) n = 0;
+
 
   var before = parent.lastChild;
+
initialize : function (line, span, after, key, is_hidden) {
   for (var i = 0; i < cats.length; i++) {
+
// If a span is given, 'after' is the category title, otherwise it may be an element after which to
    if (cats[i].length > 0) {
+
// insert the new span. 'key' is likewise overloaded; if a span is given, it is the category key (if
      var cat = cats[i].split ('|');
+
// known), otherwise it is a boolean indicating whether a bar shall be prepended.
      var key = cat.length > 1 ? cat[1] : null;
+
if (!span) {
      cat = cat[0];
+
this.isAddCategory = true;
      var lk = document.createElement ('a');
+
// Create add span and append to catLinks
      lk.href = wgArticlePath.split ('$1').join ('Category:' + encodeURI (cat));
+
this.originalCategory = "";
      lk.appendChild (document.createTextNode (cat));
+
this.originalKey = null;
      lk.setAttribute ('title', cat);
+
this.originalExists  = false;
      var span = document.createElement ('span');
+
if (!newDOM) {
      span.appendChild (lk);
+
span = make ('span');
      parent.insertBefore (span, before);
+
span.className = 'noprint';
      if (before != null) parent.insertBefore (document.createTextNode (' | '), before);
+
if (key) {
      hotcat_modify_span (span, n++);
+
span.appendChild (make (' | ', true));
      span.hotcat_key = key;
+
if (after) {
    }
+
after.parentNode.insertBefore (span, after.nextSibling);
  }
+
after = after.nextSibling;
  return null;
+
} else {
}
+
line.appendChild (span);
 +
}
 +
} else if (line.firstChild) {
 +
span.appendChild (make (' ', true));
 +
line.appendChild (span);
 +
}
 +
}
 +
this.linkSpan = make ('span');
 +
this.linkSpan.className = 'noprint nopopups hotcatlink';
 +
var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
 +
lk.appendChild (make (HotCat.links.add, true)); lk.title = HotCat.tooltips.add;
 +
this.linkSpan.appendChild (lk);
 +
span = make (newDOM ? 'li' : 'span');
 +
span.className = 'noprint';
 +
if (is_rtl) span.dir = 'rtl';
 +
span.appendChild (this.linkSpan);
 +
if (after)
 +
after.parentNode.insertBefore (span, after.nextSibling);
 +
else
 +
line.appendChild (span);
 +
this.normalLinks = null;
 +
this.undelLink = null;
 +
this.catLink = null;
 +
} else {
 +
if (is_rtl) span.dir = 'rtl';
 +
this.isAddCategory = false;
 +
this.catLink = span.firstChild;
 +
this.originalCategory = after;
 +
this.originalKey = (key && key.length > 1) ? key.substr(1) : null; // > 1 because it includes the leading bar
 +
this.originalExists  = !hasClass (this.catLink, 'new');
 +
// Create change and del links
 +
this.makeLinkSpan ();
 +
if (!this.originalExists && this.upDownLinks) this.upDownLinks.style.display = 'none';
 +
span.appendChild (this.linkSpan);
 +
}
 +
this.originalHidden    = is_hidden;
 +
this.line              = line;
 +
this.engine            = HotCat.suggestions;
 +
this.span              = span;
 +
this.currentCategory    = this.originalCategory;
 +
this.currentExists      = this.originalExists;
 +
this.currentHidden      = this.originalHidden;
 +
this.currentKey        = this.originalKey;
 +
this.state             = CategoryEditor.UNCHANGED;
 +
this.lastSavedState    = CategoryEditor.UNCHANGED;
 +
this.lastSavedCategory  = this.originalCategory;
 +
this.lastSavedKey      = this.originalKey;
 +
this.lastSavedExists    = this.originalExists;
 +
this.lastSavedHidden    = this.originalHidden;
 +
if (this.catLink && this.currentKey) {
 +
this.catLink.title = this.currentKey;
 +
}
 +
editors[editors.length] = this;
 +
},
 +
 
 +
makeLinkSpan : function () {
 +
this.normalLinks = make ('span');
 +
var lk = null;
 +
if (this.originalCategory && this.originalCategory.length > 0) {
 +
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.remove, this);
 +
lk.appendChild (make (HotCat.links.remove, true)); lk.title = HotCat.tooltips.remove;
 +
this.normalLinks.appendChild (make (' ', true));
 +
this.normalLinks.appendChild (lk);
 +
}
 +
if (!HotCat.template_categories[this.originalCategory]) {
 +
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
 +
lk.appendChild (make (HotCat.links.change, true)); lk.title = HotCat.tooltips.change;
 +
this.normalLinks.appendChild (make (' ', true));
 +
this.normalLinks.appendChild (lk);
 +
if (!noSuggestions && HotCat.use_up_down) {
 +
this.upDownLinks = make ('span');
 +
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.down, this);
 +
lk.appendChild (make (HotCat.links.down, true)); lk.title = HotCat.tooltips.down;
 +
this.upDownLinks.appendChild (make (' ', true));
 +
this.upDownLinks.appendChild (lk);
 +
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.up, this);
 +
lk.appendChild (make (HotCat.links.up, true)); lk.title = HotCat.tooltips.up;
 +
this.upDownLinks.appendChild (make (' ', true));
 +
this.upDownLinks.appendChild (lk);
 +
this.normalLinks.appendChild (this.upDownLinks);
 +
}
 +
}
 +
this.linkSpan = make ('span');
 +
this.linkSpan.className = 'noprint nopopups hotcatlink';
 +
this.linkSpan.appendChild (this.normalLinks);
 +
this.undelLink = make ('span');
 +
this.undelLink.className = 'nopopups hotcatlink';
 +
this.undelLink.style.display = 'none';
 +
lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.restore, this);
 +
lk.appendChild (make (HotCat.links.restore, true)); lk.title = HotCat.tooltips.restore;
 +
this.undelLink.appendChild (make (' ', true));
 +
this.undelLink.appendChild (lk);
 +
this.linkSpan.appendChild (this.undelLink);
 +
},
 +
 
 +
invokeSuggestions : function (dont_autocomplete) {
 +
if (this.engine && suggestionConfigs[this.engine] && suggestionConfigs[this.engine].temp && !dont_autocomplete) {
 +
this.engine = HotCat.suggestions; // Reset to a search upon input
 +
}
 +
this.state = CategoryEditor.CHANGE_PENDING;
 +
var self = this;
 +
window.setTimeout (function () {self.textchange (dont_autocomplete);}, HotCat.suggest_delay);
 +
},
 +
 
 +
makeForm : function () {
 +
var form = make ('form');
 +
form.method = 'POST'; form.onsubmit = bind (this.accept, this);
 +
this.form = form;
 +
var self = this;
 +
var text = make ('input'); text.type = 'text'; text.size = HotCat.editbox_width;
 +
if (!noSuggestions) {
 +
// Be careful here to handle IME input. This is browser/OS/IME dependent, but basically there are two mechanisms:
 +
// - Modern (DOM Level 3) browsers use compositionstart/compositionend events to signal composition; if the
 +
//   composition is not canceled, there'll be a textInput event following. During a composition key events are
 +
//  either all suppressed (FF/Gecko), or otherwise have keyDown === IME for all keys (Webkit).
 +
//  - Webkit sends a textInput followed by keyDown === IME and a keyUp with the key that ended composition.
 +
//  - Gecko doesn't send textInput but just a keyUp with the key that ended composition, without sending keyDown
 +
//    first. Gecko doesn't send any keydown while IME is active.
 +
// - Older browsers signal composition by keyDown === IME for the first and subsequent keys for a composition. The
 +
//  first keyDown !== IME is certainly after the end of the composition. Typically, composition end can also be
 +
//  detected by a keyDown IME with a keyUp of space, tab, escape, or return. (Example: IE8)
 +
text.onkeyup =
 +
function (evt) {
 +
evt = evt || window.event || window.Event; // W3C, IE, Netscape
 +
var key = evt.keyCode || 0;
 +
if (self.ime && self.lastKey === IME && !self.usesComposition && (key === TAB || key === RET || key == ESC || key === SPACE)) self.ime = false;
 +
if (self.ime) return true;
 +
if (key === UP || key === DOWN || key === PGUP || key === PGDOWN) {
 +
// In case a browser doesn't generate keypress events for arrow keys...
 +
if (self.keyCount === 0) return self.processKey (evt);
 +
} else {
 +
if (key === ESC && self.lastKey !== IME) {
 +
if (!self.resetKeySelection ()) {
 +
// No undo of key selection: treat ESC as "cancel".
 +
self.cancel ();
 +
return;
 +
}
 +
}
 +
// Also do this for ESC as a workaround for Firefox bug 524360
 +
// https://bugzilla.mozilla.org/show_bug.cgi?id=524360
 +
self.invokeSuggestions (key === BS || key === DEL || key === ESC);
 +
}
 +
return true;
 +
};
 +
text.onkeydown =
 +
function (evt) {
 +
evt = evt || window.event || window.Event; // W3C, IE, Netscape
 +
var key = evt.keyCode || 0;
 +
self.lastKey = key;
 +
self.keyCount = 0;
 +
// DOM Level < 3 IME input
 +
if (!self.ime && key === IME && !self.usesComposition) {
 +
// self.usesComposition catches browsers that may emit spurious keydown IME after a composition has ended
 +
self.ime = true;
 +
} else if (self.ime && key !== IME && !(key >= 16 && key <= 20 || key >= 91 && key <= 93 || key === 144)) {
 +
// Ignore control keys: ctrl, shift, alt, alt gr, caps lock, windows/apple cmd keys, num lock. Only the windows keys
 +
// terminate IME (apple cmd doesn't), but they also cause a blur, so it's OK to ignore them here.
 +
// Note: Safari 4 (530.17) propagates ESC out of an IME composition (observed at least on Win XP).
 +
self.ime = false;
 +
}
 +
if (self.ime) return true;
 +
// Handle return explicitly, to override the default form submission to be able to check for ctrl
 +
if (key === RET) return self.accept (evt);
 +
// Inhibit default behavior of ESC (revert to last real input in FF: we do that ourselves)
 +
return (key === ESC) ? evtKill(evt) : true;
 +
};
 +
// And handle continued pressing of arrow keys
 +
text.onkeypress = function (evt) {self.keyCount++; return self.processKey (evt);};
 +
addEvent (text, 'focus', function () { makeActive(self); });
 +
// On IE, blur events are asynchronous, and may thus arrive after the element has lost the focus. Since IE
 +
// can get the selection only while the element is active (has the focus), we may not always get the selection.
 +
// Therefore, use an IE-specific synchronous event on IE...
 +
// Don't test for text.selectionStart being defined; FF3.6.4 raises an exception when trying to access that
 +
// property while the element is not being displayed.
 +
addEvent (text
 +
, (typeof text.onbeforedeactivate != 'undefined' && text.createTextRange) ? 'beforedeactivate' : 'blur'
 +
, bind (this.saveView, this)
 +
);
 +
// DOM Level 3 IME handling
 +
try {
 +
// Setting lastKey = IME provides a fake keyDown for Gecko's single keyUp after a cmposition. If we didn't do this,
 +
// cancelling a composition via ESC would also cancel and close the whole category input editor.
 +
addEvent(text, 'compositionstart', function (evt) { self.lastKey = IME; self.usesComposition = true; self.ime = true; });
 +
addEvent(text, 'compositionend', function (evt) { self.lastKey = IME; self.usesComposition = true; self.ime = false; });
 +
addEvent(text, 'textInput', function (evt) { self.ime = false; self.invokeSuggestions(false); });
 +
} catch (any) {
 +
// Just in case some browsers might produce exceptions with these DOM Level 3 events
 +
}
 +
addEvent(text, 'blur', function (evt) { self.usesComposition = false; self.ime = false; });
 +
}
 +
this.text = text;
 +
 
 +
this.icon = make ('img');
 +
 
 +
var list = null;
 +
if (!noSuggestions) {
 +
list = make ('select');
 +
list.onclick    = function (e) { if (self.highlightSuggestion(0)) self.textchange (false, true); };
 +
list.ondblclick = function (e) { if (self.highlightSuggestion(0)) self.accept (e); };
 +
list.onchange = function (e) { self.highlightSuggestion(0); self.text.focus(); };
 +
list.onkeyup =
 +
function (evt) {
 +
evt = evt || window.event || window.Event; // W3C, IE, Netscape
 +
if (evt.keyCode === ESC) {
 +
self.resetKeySelection ();
 +
self.text.focus();
 +
window.setTimeout (function () {self.textchange (true);}, HotCat.suggest_delay);
 +
} else if (evt.keyCode === RET) {
 +
self.accept (evt);
 +
}
 +
};
 +
if (!HotCat.fixed_search) {
 +
var engineSelector = make ('select');
 +
for (var key in suggestionConfigs) {
 +
if (suggestionConfigs[key].show) {
 +
var opt = make ('option');
 +
opt.value = key;
 +
if (key == this.engine) opt.selected = true;
 +
opt.appendChild (make (suggestionConfigs[key].name, true));
 +
engineSelector.appendChild (opt);
 +
}
 +
}
 +
engineSelector.onchange =
 +
function () {
 +
self.engine = self.engineSelector.options[self.engineSelector.selectedIndex].value;
 +
self.text.focus();
 +
self.textchange (true, true); // Don't autocomplete, force re-display of list
 +
};
 +
this.engineSelector = engineSelector;
 +
}
 +
}
 +
this.list = list;
 +
 
 +
function button_label (id, defaultText) {
 +
var label = null;
 +
if (  onUpload
 +
&& typeof (UFUI) != 'undefined'
 +
&& typeof (UIElements) != 'undefined'
 +
&& typeof (UFUI.getLabel) == 'function')
 +
{
 +
try {
 +
label = UFUI.getLabel (id, true);
 +
// Extract the plain text. IE doesn't know that Node.TEXT_NODE === 3
 +
while (label && label.nodeType != 3) label = label.firstChild;
 +
} catch (ex) {
 +
label = null;
 +
}
 +
}
 +
if (!label || !label.data) return defaultText;
 +
return label.data;
 +
}
 +
 
 +
// Do not use type 'submit'; we cannot detect modifier keys if we do
 +
var OK = make ('input'); OK.type = 'button';
 +
OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
 +
OK.onclick = bind (this.accept, this);
 +
this.ok = OK;
 +
 
 +
var cancel = make ('input'); cancel.type = 'button';
 +
cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
 +
cancel.onclick = bind (this.cancel, this);
 +
this.cancelButton = cancel;
 +
 
 +
var span = make ('span');
 +
span.className = 'hotcatinput';
 +
span.style.position = 'relative';
 +
// FF3.6: add the input field first, then the two absolutely positioned elements. Otherwise, FF3.6 may leave the
 +
// suggestions and the selector at the right edge of the screen if display of the input field causes a re-layout
 +
// moving the form to the front of the next line.
 +
span.appendChild (text);
 +
 
 +
// IE8/IE9: put some text into this span (a0 is nbsp) and make sure it always stays on the
 +
// same line as the input field, otherwise, IE8/9 miscalculates the height of the span and
 +
// then the engine selector may overlap the input field.
 +
span.appendChild (make ('\xa0', true));
 +
span.style.whiteSpace = 'nowrap';
 +
 
 +
if (list) span.appendChild (list);
 +
if (this.engineSelector) span.appendChild (this.engineSelector);
 +
if (!noSuggestions) span.appendChild (this.icon);
 +
span.appendChild (OK);
 +
span.appendChild (cancel);
 +
form.appendChild(span);
 +
form.style.display = 'none';
 +
this.span.appendChild (form);
 +
},
 +
 
 +
display : function (evt) {
 +
if (this.isAddCategory && !onUpload) {
 +
var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
 +
}
 +
if (!commitButton && !onUpload) {
 +
for (var i = 0; i < editors.length; i++) {
 +
if (editors[i].state != CategoryEditor.UNCHANGED) {
 +
setMultiInput();
 +
break;
 +
}
 +
}
 +
}
 +
if (!this.form) {
 +
this.makeForm ();
 +
}
 +
if (this.list) this.list.style.display = 'none';
 +
if (this.engineSelector) this.engineSelector.style.display = 'none';
 +
this.currentCategory = this.lastSavedCategory;
 +
this.currentExists  = this.lastSavedExists;
 +
this.currentHidden  = this.lastSavedHidden;
 +
this.currentKey      = this.lastSavedKey;
 +
this.icon.src = armorUri(this.currentExists ? HotCat.existsYes : HotCat.existsNo);
 +
this.text.value = this.currentCategory + (this.currentKey !== null ? '|' + this.currentKey : "");
 +
this.originalState = this.state;
 +
this.lastInput    = this.currentCategory;
 +
this.inputExists  = this.currentExists;
 +
this.state        = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
 +
this.lastSelection = {start: this.currentCategory.length, end: this.currentCategory.length};
 +
this.showsList = false;
 +
// Display the form
 +
if (this.catLink) this.catLink.style.display = 'none';
 +
this.linkSpan.style.display = 'none';
 +
this.form.style.display = 'inline';
 +
this.ok.disabled = false;
 +
// Kill the event before focussing, otherwise IE will kill the onfocus event!
 +
var result = evtKill (evt);
 +
this.text.focus();
 +
this.text.readOnly = false;
 +
checkMultiInput ();
 +
return result;
 +
},
 +
 
 +
show : function (evt, engine, readOnly) {
 +
var result = this.display (evt);
 +
var v = this.lastSavedCategory;
 +
if (v.length === 0) return result;
 +
this.text.readOnly = !!readOnly;
 +
this.engine = engine;
 +
this.textchange (false, true); // do autocompletion, force display of suggestions
 +
forceRedraw ();
 +
return result;
 +
},
 +
 
 +
open : function (evt) {
 +
return this.show (evt, (this.engine && suggestionConfigs[this.engine].temp) ? HotCat.suggestions : this.engine);
 +
},
 +
 
 +
down : function (evt) {
 +
return this.show (evt, 'subcat', true);
 +
},
 +
 
 +
up : function (evt) {
 +
return this.show (evt, 'parentcat');
 +
},
 +
 
 +
cancel : function () {
 +
if (this.isAddCategory && !onUpload) {
 +
this.removeEditor(); // We added a new adder when opening
 +
return;
 +
}
 +
// Close, re-display link
 +
this.inactivate();
 +
this.form.style.display = 'none';
 +
if (this.catLink) this.catLink.style.display = "";
 +
this.linkSpan.style.display = "";
 +
this.state = this.originalState;
 +
this.currentCategory = this.lastSavedCategory;
 +
this.currentKey      = this.lastSavedKey;
 +
this.currentExists  = this.lastSavedExists;
 +
this.currentHidden  = this.lastSavedHidden;
 +
if (this.catLink) {
 +
if (this.currentKey && this.currentKey.length > 0) {
 +
this.catLink.title = this.currentKey;
 +
} else {
 +
this.catLink.title = "";
 +
}
 +
}
 +
if (this.state == CategoryEditor.UNCHANGED) {
 +
if (this.catLink) this.catLink.style.backgroundColor = 'transparent';
 +
} else {
 +
if (!onUpload) {
 +
try {
 +
this.catLink.style.backgroundColor = HotCat.bg_changed;
 +
} catch (ex) {}
 +
}
 +
}
 +
checkMultiInput ();
 +
forceRedraw ();
 +
},
 +
 
 +
removeEditor : function () {
 +
if (!newDOM) {
 +
var next = this.span.nextSibling;
 +
if (next) next.parentNode.removeChild (next);
 +
}
 +
this.span.parentNode.removeChild (this.span);
 +
for (var i = 0; i < editors.length; i++) {
 +
if (editors[i] == this) {
 +
editors.splice (i, 1);
 +
break;
 +
}
 +
}
 +
checkMultiInput ();
 +
var self = this;
 +
window.setTimeout (function () {delete self;}, 10);
 +
},
 +
 
 +
rollback : function (evt) {
 +
this.undoLink.parentNode.removeChild (this.undoLink);
 +
this.undoLink = null;
 +
this.currentCategory = this.originalCategory;
 +
this.currentKey = this.originalKey;
 +
this.currentExists = this.originalExists;
 +
this.currentHidden = this.originalHidden;
 +
this.lastSavedCategory = this.originalCategory;
 +
this.lastSavedKey = this.originalKey;
 +
this.lastSavedExists = this.originalExists;
 +
this.lastSavedHidden = this.originalHidden;
 +
this.state = CategoryEditor.UNCHANGED;
 +
if (!this.currentCategory || this.currentCategory.length === 0) {
 +
// It was a newly added category. Remove the whole editor.
 +
this.removeEditor();
 +
} else {
 +
// Redisplay the link...
 +
this.catLink.removeChild (this.catLink.firstChild);
 +
this.catLink.appendChild (make (this.currentCategory, true));
 +
this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
 +
this.catLink.title = this.currentKey || "";
 +
this.catLink.className = this.currentExists ? "" : 'new';
 +
this.catLink.style.backgroundColor = 'transparent';
 +
if (this.upDownLinks) this.upDownLinks.style.display = this.currentExists ? "" : 'none';
 +
checkMultiInput ();
 +
}
 +
return evtKill (evt);
 +
},
 +
 
 +
inactivate : function () {
 +
if (this.list) this.list.style.display = 'none';
 +
if (this.engineSelector) this.engineSelector.style.display = 'none';
 +
this.is_active = false;
 +
},
 +
 
 +
acceptCheck : function (dontCheck) {
 +
this.sanitizeInput ();
 +
var value = this.text.value.split('|');
 +
var key   = null;
 +
if (value.length > 1) key = value[1];
 +
var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
 +
if (HotCat.capitalizePageNames) v = capitalize (v);
 +
this.lastInput = v;
 +
v = replaceShortcuts(v, HotCat.shortcuts);
 +
if (v.length === 0) {
 +
this.cancel ();
 +
return false;
 +
}
 +
if (!dontCheck
 +
&& (  wgNamespaceNumber === 14 && v == wgTitle
 +
|| HotCat.blacklist && HotCat.blacklist.test(v))
 +
  )
 +
{
 +
this.cancel ();
 +
return false;
 +
}
 +
this.currentCategory = v;
 +
this.currentKey = key;
 +
this.currentExists = this.inputExists;
 +
return true;
 +
},
 +
 
 +
accept : function (evt) {
 +
this.noCommit = (evtKeys (evt) & 1) !== 0;
 +
var result = evtKill (evt);
 +
if (this.acceptCheck ()) {
 +
var toResolve = [this];
 +
var original  = this.currentCategory;
 +
resolveMulti (
 +
toResolve
 +
,function (resolved) {
 +
if (resolved[0].dab) {
 +
showDab (resolved[0]);
 +
} else {
 +
if (resolved[0].acceptCheck(true)) {
 +
resolved[0].commit (
 +
(resolved[0].currentCategory != original)
 +
? HotCat.messages.cat_resolved.replace (/\$1/g, original)
 +
: null
 +
);
 +
}
 +
}
 +
}
 +
);
 +
}
 +
return result;
 +
},
 +
 
 +
close : function () {
 +
if (!this.catLink) {
 +
// Create a catLink
 +
this.catLink = make ('a');
 +
this.catLink.appendChild (make ('foo', true));
 +
this.catLink.style.display = 'none';
 +
this.span.insertBefore (this.catLink, this.span.firstChild.nextSibling);
 +
}
 +
this.catLink.removeChild (this.catLink.firstChild);
 +
this.catLink.appendChild (make (this.currentCategory, true));
 +
this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
 +
this.catLink.className = this.currentExists ? "" : 'new';
 +
this.lastSavedCategory = this.currentCategory;
 +
this.lastSavedKey      = this.currentKey;
 +
this.lastSavedExists   = this.currentExists;
 +
this.lastSavedHidden  = this.currentHidden;
 +
// Close form and redisplay category
 +
this.inactivate();
 +
this.form.style.display = 'none';
 +
this.catLink.title = this.currentKey || "";
 +
this.catLink.style.display = "";
 +
if (this.isAddCategory) {
 +
if (onUpload) {
 +
var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
 +
}
 +
this.isAddCategory = false;
 +
this.linkSpan.parentNode.removeChild (this.linkSpan);
 +
this.makeLinkSpan ();
 +
this.span.appendChild (this.linkSpan);
 +
}
 +
if (!this.undoLink) {
 +
// Append an undo link.
 +
var span = make ('span');
 +
var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.rollback, this);
 +
lk.appendChild (make (HotCat.links.undo, true)); lk.title = HotCat.tooltips.undo;
 +
span.appendChild (make (' ', true));
 +
span.appendChild (lk);
 +
this.normalLinks.appendChild (span);
 +
this.undoLink = span;
 +
if (!onUpload) {
 +
try {
 +
this.catLink.style.backgroundColor = HotCat.bg_changed;
 +
} catch (ex) {}
 +
}
 +
}
 +
if (this.upDownLinks) this.upDownLinks.style.display = this.lastSavedExists ? "" : 'none';
 +
this.linkSpan.style.display = "";
 +
this.state = CategoryEditor.CHANGED;
 +
checkMultiInput ();
 +
forceRedraw ();
 +
},
 +
 
 +
commit : function (comment) {
 +
// Check again to catch problem cases after redirect resolution
 +
if (  (  this.currentCategory == this.originalCategory
 +
&& (this.currentKey == this.originalKey
 +
|| this.currentKey === null && this.originalKey.length === 0
 +
  )
 +
  )
 +
|| wgNamespaceNumber == 14 && this.currentCategory == wgTitle
 +
|| HotCat.blacklist && HotCat.blacklist.test (this.currentCategory)
 +
  )
 +
{
 +
this.cancel ();
 +
return;
 +
}
 +
if (commitButton || onUpload) {
 +
this.close ();
 +
} else {
 +
this.close ();
 +
var self = this;
 +
initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {alert (msg);});
 +
}
 +
},
 +
 
 +
remove : function (evt) {
 +
this.doRemove (evtKeys (evt) & 1);
 +
return evtKill (evt);
 +
},
 +
 
 +
doRemove : function (noCommit) {
 +
if (this.isAddCategory) { // Empty input on adding a new category
 +
this.cancel ();
 +
return;
 +
}
 +
if (!commitButton && !onUpload) {
 +
for (var i = 0; i < editors.length; i++) {
 +
if (editors[i].state != CategoryEditor.UNCHANGED) {
 +
setMultiInput();
 +
break;
 +
}
 +
}
 +
}
 +
if (commitButton) {
 +
this.catLink.title = "";
 +
this.catLink.style.cssText += '; text-decoration : line-through !important;';
 +
try {
 +
this.catLink.style.backgroundColor = HotCat.bg_changed;
 +
} catch (ex) {}
 +
this.originalState = this.state;
 +
this.state = CategoryEditor.DELETED;
 +
this.normalLinks.style.display = 'none';
 +
this.undelLink.style.display = "";
 +
checkMultiInput ();
 +
} else {
 +
if (onUpload) {
 +
// Remove this editor completely
 +
this.removeEditor ();
 +
} else {
 +
this.originalState = this.state;
 +
this.state = CategoryEditor.DELETED;
 +
this.noCommit = noCommit || HotCat.del_needs_diff;
 +
var self = this;
 +
initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {self.state = self.originalState; alert (msg);});
 +
}
 +
}
 +
},
 +
 
 +
restore : function (evt) {
 +
// Can occur only if we do have a commit button and are not on the upload form
 +
this.catLink.title = this.currentKey || "";
 +
this.catLink.style.textDecoration = "";
 +
this.state = this.originalState;
 +
if (this.state == CategoryEditor.UNCHANGED) {
 +
this.catLink.style.backgroundColor = 'transparent';
 +
} else {
 +
try {
 +
this.catLink.style.backgroundColor = HotCat.bg_changed;
 +
} catch (ex) {}
 +
}
 +
this.normalLinks.style.display = "";
 +
this.undelLink.style.display = 'none';
 +
checkMultiInput ();
 +
return evtKill (evt);
 +
},
 +
 
 +
// Internal operations
 +
 
 +
selectEngine : function (engineName) {
 +
if (!this.engineSelector) return;
 +
for (var i = 0; i < this.engineSelector.options.length; i++) {
 +
this.engineSelector.options[i].selected = this.engineSelector.options[i].value == engineName;
 +
}
 +
},
 +
 
 +
sanitizeInput : function () {
 +
var v = this.text.value || "";
 +
v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
 +
var re = new RegExp ('^(' + HotCat.category_regexp + '):');
 +
if (re.test (v)) {
 +
v = v.substring (v.indexOf (':') + 1).replace(/^(\s|_)+/, "");
 +
}
 +
if (HotCat.capitalizePageNames) v = capitalize (v);
 +
// Only update the input field if there is a difference. IE8 appears to reset the selection
 +
// and place the cursor at the front upon reset, which makes our autocompletetion become a
 +
// nuisance. FF and IE6 don't seem to have this problem.
 +
if (this.text.value !== null && this.text.value != v)
 +
this.text.value = v;
 +
},
 +
 
 +
makeCall : function (url, callbackObj, engine, queryKey, cleanKey) {
 +
var cb = callbackObj;
 +
var e  = engine;
 +
var v  = queryKey;
 +
var z  = cleanKey;
 +
var thisObj = this;
 +
 
 +
function done () {
 +
cb.callsMade++;
 +
if (cb.callsMade === cb.nofCalls) {
 +
if (cb.exists) cb.allTitles.exists = true;
 +
if (cb.normalized) cb.allTitles.normalized = cb.normalized;
 +
if (!cb.dontCache && !suggestionConfigs[cb.engineName].cache[z]) {
 +
suggestionConfigs[cb.engineName].cache[z] = cb.allTitles;
 +
}
 +
thisObj.text.readOnly = false;
 +
if (!cb.cancelled) thisObj.showSuggestions (cb.allTitles, cb.noCompletion, v, cb.engineName);
 +
if (cb === thisObj.callbackObj) thisObj.callbackObj = null;
 +
delete cb;
 +
}
 +
}
 +
 
 +
getJSON ({
 +
  uri : url
 +
,success : function (json) {
 +
var titles = e.handler (json, z);
 +
if (titles && titles.length > 0) {
 +
if (cb.allTitles === null) {
 +
cb.allTitles = titles;
 +
} else {
 +
cb.allTitles = cb.allTitles.concat (titles);
 +
}
 +
if (titles.exists) cb.exists = true;
 +
if (titles.normalized) cb.normalized = titles.normalized;
 +
}
 +
done();
 +
  }
 +
,error : function (req) {if (!req) noSuggestions = true; cb.dontCache = true; done(); }
 +
});
 +
},
 +
 
 +
callbackObj : null,
 +
 
 +
textchange : function (dont_autocomplete, force) {
 +
// Hide all other lists
 +
makeActive (this);
 +
// Get input value, omit sort key, if any
 +
this.sanitizeInput ();
 +
var v = this.text.value;
 +
// Disregard anything after a pipe.
 +
var pipe = v.indexOf ('|');
 +
if (pipe >= 0) {
 +
this.currentKey = v.substring (pipe+1);
 +
v = v.substring (0, pipe);
 +
} else {
 +
this.currentKey = null;
 +
}
 +
if (this.lastInput == v && !force) return; // No change
 +
if (this.lastInput != v) checkMultiInput ();
 +
this.lastInput = v;
 +
this.lastRealInput = v;
 +
 
 +
// Mark blacklisted inputs.
 +
this.ok.disabled = v.length > 0 && HotCat.blacklist && HotCat.blacklist.test (v);
 +
 
 +
if (noSuggestions) {
 +
// No Ajax: just make sure the list is hidden
 +
if (this.list) this.list.style.display = 'none';
 +
if (this.engineSelector) this.engineSelector.style.display = 'none';
 +
if (this.icon) this.icon.style.display = 'none';
 +
return;
 +
}
 +
 
 +
if (v.length === 0) { this.showSuggestions([]); return; }
 +
var cleanKey = v.replace(/[\u200E\u200F\u202A-\u202E]/g, "").replace(wikiTextBlankRE, ' ');
 +
cleanKey = replaceShortcuts(cleanKey, HotCat.shortcuts);
 +
cleanKey = cleanKey.replace(/^\s+|\s+$/g, '');
 +
if (cleanKey.length === 0) { this.showSuggestions([]); return; }
 +
 
 +
if (this.callbackObj) this.callbackObj.cancelled = true;
 +
var engineName  = suggestionConfigs[this.engine] ? this.engine : 'combined';
 +
 
 +
dont_autocomplete = dont_autocomplete || suggestionConfigs[engineName].noCompletion;
 +
if (suggestionConfigs[engineName].cache[cleanKey]) {
 +
this.showSuggestions (suggestionConfigs[engineName].cache[cleanKey], dont_autocomplete, v, engineName);
 +
return;
 +
}
 +
 
 +
var engines = suggestionConfigs[engineName].engines;
 +
this.callbackObj =
 +
{allTitles: null, callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engineName};
 +
this.makeCalls (engines, this.callbackObj, v, cleanKey);
 +
},
 +
 
 +
makeCalls : function (engines, cb, v, cleanKey) {
 +
for (var j = 0; j < engines.length; j++) {
 +
var engine = suggestionEngines[engines[j]];
 +
var url = wgServer + wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (cleanKey));
 +
this.makeCall (url, cb, engine, v, cleanKey);
 +
}
 +
},
 +
 
 +
showSuggestions : function (titles, dontAutocomplete, queryKey, engineName) {
 +
this.text.readOnly = false;
 +
this.dab = null;
 +
this.showsList = false;
 +
if (!this.list) return;
 +
if (noSuggestions) {
 +
if (this.list) this.list.style.display = 'none';
 +
if (this.engineSelector) this.engineSelector.style.display = 'none';
 +
if (this.icon) this.icon.style.display = 'none';
 +
this.inputExists = true; // Default...
 +
return;
 +
}
 +
this.engineName = engineName;
 +
if (engineName) {
 +
if (!this.engineSelector) this.engineName = null;
 +
} else {
 +
if (this.engineSelector) this.engineSelector.style.display = 'none';
 +
}
 +
if (queryKey) {
 +
if (this.lastInput.indexOf (queryKey) !== 0) return;
 +
if (this.lastQuery && this.lastInput.indexOf (this.lastQuery) === 0 && this.lastQuery.length > queryKey.length)
 +
return;
 +
}
 +
this.lastQuery = queryKey;
 +
 
 +
// Get current input text
 +
var v = this.text.value.split('|');
 +
var key = v.length > 1 ? '|' + v[1] : "";
 +
v = (HotCat.capitalizePageNames ? capitalize (v[0]) : v[0]);
 +
var vNormalized = v;
 +
var knownToExist = titles && titles.exists;
 +
var i;
 +
if (titles) {
 +
if (titles.normalized && v.indexOf(queryKey) === 0) {
 +
// We got back a different normalization than what is in the input field
 +
vNormalized = titles.normalized + v.substring(queryKey.length);
 +
}
 +
var vLow = vNormalized.toLowerCase ();
 +
// Strip blacklisted categories
 +
if (HotCat.blacklist) {
 +
for (i = 0; i < titles.length; i++) {
 +
if (HotCat.blacklist.test (titles[i])) {
 +
titles.splice(i, 1);
 +
i--;
 +
}
 +
}
 +
}
 +
titles.sort (
 +
function (a, b) {
 +
if (a == b) return 0;
 +
if (a.indexOf (b) === 0) return 1; // a begins with b: a > b
 +
if (b.indexOf (a) === 0) return -1; // b begins with a: a < b
 +
// Opensearch may return stuff not beginning with the search prefix!
 +
var prefixMatchA = (a.indexOf (vNormalized) === 0 ? 1 : 0);
 +
var prefixMatchB = (b.indexOf (vNormalized) === 0 ? 1 : 0);
 +
if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
 +
// Case-insensitive prefix match!
 +
var aLow = a.toLowerCase(), bLow = b.toLowerCase();
 +
prefixMatchA = (aLow.indexOf (vLow) === 0 ? 1 : 0);
 +
prefixMatchB = (bLow.indexOf (vLow) === 0 ? 1 : 0);
 +
if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
 +
if (a < b) return -1;
 +
if (b < a) return 1;
 +
return 0;
 +
}
 +
);
 +
// Remove duplicates and self-references
 +
for (i = 0; i < titles.length; i++) {
 +
if (   i+1 < titles.length && titles[i] == titles[i+1]
 +
|| wgNamespaceNumber == 14 && titles[i] == wgTitle
 +
  )
 +
{
 +
titles.splice (i, 1);
 +
i--;
 +
}
 +
}
 +
}
 +
if (!titles || titles.length === 0) {
 +
if (this.list) this.list.style.display = 'none';
 +
if (this.engineSelector) this.engineSelector.style.display = 'none';
 +
if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
 +
if (this.icon) this.icon.src = armorUri(HotCat.existsNo);
 +
this.inputExists = false;
 +
}
 +
return;
 +
}
 +
 
 +
var firstTitle = titles[0];
 +
var completed = this.autoComplete (firstTitle, v, vNormalized, key, dontAutocomplete);
 +
var existing = completed || knownToExist || firstTitle == replaceShortcuts(v, HotCat.shortcuts);
 +
if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
 +
this.icon.src = armorUri(existing ? HotCat.existsYes : HotCat.existsNo);
 +
this.inputExists = existing;
 +
}
 +
if (completed) {
 +
this.lastInput = firstTitle;
 +
if (titles.length === 1) {
 +
this.list.style.display = 'none';
 +
if (this.engineSelector) this.engineSelector.style.display = 'none';
 +
return;
 +
}
 +
}
 +
// (Re-)fill the list
 +
while (this.list.firstChild) this.list.removeChild (this.list.firstChild);
 +
for (i = 0 ; i < titles.length ; i++) {
 +
var opt = make ('option') ;
 +
opt.appendChild (make (titles[i], true));
 +
opt.selected = completed && (i === 0);
 +
this.list.appendChild (opt);
 +
}
 +
this.displayList();
 +
},
 +
 
 +
displayList : function () {
 +
this.showsList = true;
 +
if (!this.is_active) {
 +
this.list.style.display = 'none';
 +
if (this.engineSelector) this.engineSelector.style.display = 'none';
 +
return;
 +
}
 +
var nofItems = (this.list.options.length > HotCat.list_size ? HotCat.list_size : this.list.options.length);
 +
if (nofItems <= 1) nofItems = 2;
 +
this.list.size = nofItems;
 +
this.list.style.align    = is_rtl ? 'right' : 'left';
 +
this.list.style.zIndex  = 5;
 +
this.list.style.position = 'absolute';
 +
// Compute initial list position. First the height.
 +
var anchor = is_rtl ? 'right' : 'left';
 +
var listh = 0;
 +
if (this.list.style.display == 'none') {
 +
// Off-screen display to get the height
 +
this.list.style.top = this.text.offsetTop + 'px';
 +
this.list.style[anchor] = '-10000px';
 +
this.list.style.display = "";
 +
listh = this.list.offsetHeight;
 +
this.list.style.display = 'none';
 +
} else {
 +
listh = this.list.offsetHeight;
 +
}
 +
// Approximate calculation of maximum list size
 +
var maxListHeight = listh;
 +
if (nofItems < HotCat.list_size) maxListHeight = (listh / nofItems) * HotCat.list_size;
 +
 
 +
function viewport (what) {
 +
if (is_webkit && !document.evaluate)
 +
return window['inner' + what]; // Safari < 3.0
 +
var s = 'client' + what;
 +
if (window.opera) return document.body[s];
 +
return (document.documentElement ? document.documentElement[s] : 0)
 +
|| document.body[s] || 0;
 +
}
 +
function scroll_offset (what) {
 +
var s = 'scroll' + what;
 +
var result = (document.documentElement ? document.documentElement[s] : 0)
 +
|| document.body[s] || 0;
 +
if (is_rtl && what == 'Left') {
 +
// RTL inconsistencies.
 +
// FF: 0 at the far right, then increasingly negative values.
 +
// IE >= 8: 0 at the far right, then increasingly positive values.
 +
// Webkit: scrollWidth - clientWidth at the far right, then down to zero.
 +
// IE 7: like webkit; IE6: disabled in RTL anyway since too many problems.
 +
// Opera: don't know...
 +
if (result < 0) result = - result;
 +
if (!is_webkit && !is_ie_lt8) {
 +
result = scroll_offset('Width') - viewport('Width') - result;
 +
}
 +
// Now all have webkit behavior, i.e. zero if at the leftmost edge.
 +
}
 +
return result;
 +
}
 +
function position (node) {
 +
// Stripped-down simplified position function. It's good enough for our purposes.
 +
if (node.getBoundingClientRect) {
 +
var box    = node.getBoundingClientRect ();
 +
return { x : Math.round (box.left + scroll_offset ('Left'))
 +
,y : Math.round (box.top + scroll_offset ('Top'))
 +
  };
 +
}
 +
var t = 0, l = 0;
 +
do {
 +
t = t + (node.offsetTop  || 0);
 +
l = l + (node.offsetLeft || 0);
 +
node = node.offsetParent;
 +
} while (node);
 +
return {x : l, y : t};
 +
}
 +
 
 +
var textPos = position (this.text);
 +
var nl = 0;
 +
var nt = 0;
 +
var offset = 0;
 +
// Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
 +
var textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
 +
if (this.engineName) {
 +
this.engineSelector.style.zIndex = 5;
 +
this.engineSelector.style.position = 'absolute';
 +
this.engineSelector.style.width = textBoxWidth + 'px';
 +
// Figure out the height of this selector: display it off-screen, then hide it again.
 +
if (this.engineSelector.style.display == 'none') {
 +
this.engineSelector.style[anchor] = '-10000px';
 +
this.engineSelector.style.top = '0px';
 +
this.engineSelector.style.display = "";
 +
offset = this.engineSelector.offsetHeight;
 +
this.engineSelector.style.display = 'none';
 +
} else {
 +
offset = this.engineSelector.offsetHeight;
 +
}
 +
this.engineSelector.style[anchor]  = nl + 'px';
 +
}
 +
if (textPos.y < maxListHeight + offset + 1) {
 +
// The list might extend beyond the upper border of the page. Let's avoid that by placing it
 +
// below the input text field.
 +
nt = this.text.offsetHeight + offset + 1;
 +
if (this.engineName) this.engineSelector.style.top = this.text.offsetHeight + 'px';
 +
} else {
 +
nt = - listh - offset - 1;
 +
if (this.engineName) this.engineSelector.style.top = - (offset + 1) + 'px';
 +
}
 +
this.list.style.top = nt + 'px';
 +
this.list.style.width = ""; // No fixed width (yet)
 +
this.list.style[anchor] = nl + 'px';
 +
if (this.engineName) {
 +
this.selectEngine (this.engineName);
 +
this.engineSelector.style.display = "";
 +
}
 +
this.list.style.display = 'block';
 +
// Set the width of the list
 +
if (this.list.offsetWidth < textBoxWidth ) {
 +
this.list.style.width = textBoxWidth + 'px';
 +
return;
 +
}
 +
// If the list is wider than the textbox: make sure it fits horizontally into the browser window
 +
var scroll = scroll_offset ('Left');
 +
var view_w = viewport ('Width');
 +
var w      = this.list.offsetWidth;
 +
var l_pos  = position (this.list);
 +
var left  = l_pos.x;
 +
var right  = left + w;
 +
if (left < scroll || right > scroll + view_w) {
 +
if (w > view_w) {
 +
w = view_w;
 +
this.list.style.width = w + 'px';
 +
if (is_rtl) {
 +
left = right - w;
 +
} else {
 +
right = left + w;
 +
}
 +
}
 +
var relative_offset = 0;
 +
if (left < scroll) {
 +
relative_offset = scroll - left;
 +
} else if (right > scroll + view_w) {
 +
relative_offset = - (right - scroll - view_w);
 +
}
 +
if (is_rtl) relative_offset = - relative_offset;
 +
if (relative_offset !== 0) {
 +
this.list.style[anchor] = (nl + relative_offset) + 'px';
 +
}
 +
}
 +
},
 +
 
 +
autoComplete : function (newVal, actVal, normalizedActVal, key, dontModify) {
 +
if (newVal == actVal) return true;
 +
if (dontModify || this.ime || !this.canSelect()) return false;
 +
// If we can't select properly or an IME composition is ongoing, autocompletion would be a major annoyance to the user.
 +
if (newVal.indexOf (actVal) !== 0) {
 +
// Maybe it'll work with the normalized value (NFC)?
 +
if (normalizedActVal && newVal.indexOf(normalizedActVal) === 0) {
 +
if (this.lastRealInput == actVal) this.lastRealInput = normalizedActVal;
 +
actVal = normalizedActVal;
 +
} else {
 +
return false;
 +
}
 +
}
 +
// Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
 +
// such that it can be easily removed by typing backspace if the suggestion is unwanted.
 +
this.text.focus();
 +
this.text.value = newVal + key;
 +
this.setSelection (actVal.length, newVal.length);
 +
return true;
 +
},
 +
 
 +
canSelect : function () {
 +
return this.text.setSelectionRange
 +
|| this.text.createTextRange
 +
||    typeof (this.text.selectionStart) != 'undefined'
 +
  && typeof (this.text.selectionEnd) != 'undefined';
 +
},
 +
 
 +
setSelection : function (from, to) {
 +
// this.text must be focused (at least on IE)
 +
if (!this.text.value) return;
 +
if (this.text.setSelectionRange) {    // e.g. khtml
 +
this.text.setSelectionRange (from, to);
 +
} else if (typeof (this.text.selectionStart) != 'undefined') {
 +
if (from > this.text.selectionStart) {
 +
this.text.selectionEnd  = to;
 +
this.text.selectionStart = from;
 +
} else {
 +
this.text.selectionStart = from;
 +
this.text.selectionEnd  = to;
 +
}
 +
} else if (this.text.createTextRange) { // IE
 +
var new_selection = this.text.createTextRange();
 +
new_selection.move ('character', from);
 +
new_selection.moveEnd ('character', to - from);
 +
new_selection.select();
 +
}
 +
},
 +
 
 +
getSelection : function () {
 +
var from = 0, to = 0;
 +
// this.text must be focused (at least on IE)
 +
if (!this.text.value) {
 +
// No text.
 +
} else if (typeof (this.text.selectionStart) != 'undefined') {
 +
from = this.text.selectionStart;
 +
to  = this.text.selectionEnd;
 +
} else if (document.selection && document.selection.createRange) { // IE
 +
var rng = document.selection.createRange().duplicate();
 +
if (rng.parentElement() === this.text) {
 +
try {
 +
var textRng = this.text.createTextRange();
 +
textRng.move('character', 0);
 +
textRng.setEndPoint('EndToEnd', rng);
 +
// We're in a single-line input box: no need to care about IE's strange
 +
// handling of line ends
 +
to = textRng.text.length;
 +
textRng.setEndPoint('EndToStart', rng);
 +
from = textRng.text.length;
 +
} catch (notFocused) {
 +
from = this.text.value.length; to = from; // At end of text
 +
}
 +
}
 +
}
 +
return {start: from, end: to};
 +
},
 +
 
 +
saveView : function (evt) {
 +
this.lastSelection = this.getSelection ();
 +
},
 +
 
 +
processKey : function (evt) {
 +
var dir = 0;
 +
switch (this.lastKey) {
 +
case UP: dir = -1;
 +
case DOWN: if (dir === 0) dir = 1;
 +
case PGUP: if (dir === 0) dir = -HotCat.list_size;
 +
case PGDOWN: if (dir === 0) dir = HotCat.list_size;
 +
if (this.list.style.display != 'none') {
 +
// List is visible, so there are suggestions
 +
this.highlightSuggestion (dir);
 +
// Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow
 +
// as "place the text cursor at the front", which we don't want here.
 +
return evtKill (evt);
 +
} else if (  this.keyCount <= 1
 +
  && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
 +
  )
 +
{
 +
// If no suggestions displayed, get them, unless we're already getting them.
 +
this.textchange ();
 +
}
 +
break;
 +
case ESC: // Inhibit default behavior (revert to last real input in FF: we do that ourselves)
 +
return evtKill (evt);
 +
}
 +
return true;
 +
},
 +
 
 +
highlightSuggestion : function (dir) {
 +
if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
 +
var curr = this.list.selectedIndex;
 +
var tgt  = -1;
 +
if (dir === 0) {
 +
if (curr < 0 || curr >= this.list.options.length) return false;
 +
tgt = curr;
 +
} else {
 +
tgt = curr < 0 ? 0 : curr + dir;
 +
tgt = tgt < 0 ? 0 : tgt;
 +
if (tgt >= this.list.options.length) tgt = this.list.options.length - 1;
 +
}
 +
if (tgt != curr || dir === 0) {
 +
if (curr >= 0 && curr < this.list.options.length && dir !== 0) this.list.options[curr].selected = false;
 +
this.list.options[tgt].selected = true;
 +
// Get current input text
 +
var v = this.text.value.split('|');
 +
var key = v.length > 1 ? '|' + v[1] : "";
 +
var completed = this.autoComplete (this.list.options[tgt].text, this.lastRealInput, null, key, false);
 +
if (!completed || this.list.options[tgt].text == this.lastRealInput) {
 +
this.text.value = this.list.options[tgt].text + key;
 +
if (this.canSelect()) this.setSelection (this.list.options[tgt].text.length, this.list.options[tgt].text.length);
 +
}
 +
this.lastInput = this.list.options[tgt].text;
 +
this.inputExists = true; // Might be wrong if from a dab list...
 +
if (this.icon) this.icon.src = armorUri(HotCat.existsYes);
 +
this.state = CategoryEditor.CHANGE_PENDING;
 +
}
 +
return true;
 +
},
 +
 
 +
resetKeySelection : function () {
 +
if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
 +
var curr = this.list.selectedIndex;
 +
if (curr >= 0 && curr < this.list.options.length) {
 +
this.list.options[curr].selected = false;
 +
// Get current input text
 +
var v = this.text.value.split('|');
 +
var key = v.length > 1 ? '|' + v[1] : "";
 +
// ESC is handled strangely by some browsers (e.g., FF); somehow it resets the input value before
 +
// our event handlers ever get a chance to run.
 +
var result = v[0] != this.lastInput;
 +
if (v[0] != this.lastRealInput) {
 +
this.text.value = this.lastRealInput + key;
 +
result = true;
 +
}
 +
this.lastInput = this.lastRealInput;
 +
return result;
 +
}
 +
return false;
 +
}
 +
 
 +
}; // end CategoryEditor.prototype
 +
 
 +
function initialize () {
 +
// User configurations. Do this here, called from the onload handler, so that users can
 +
// override it easily in their own user script files by just declaring variables. JSconfig
 +
// is some feature used at Wikimedia Commons.
 +
var config = (typeof (JSconfig) != 'undefined' && JSconfig.keys) ? JSconfig.keys : {};
 +
HotCat.dont_add_to_watchlist =
 +
(typeof (window.hotcat_dont_add_to_watchlist) != 'undefined'
 +
? !!window.hotcat_dont_add_to_watchlist
 +
: (typeof (config['HotCatDontAddToWatchlist']) != 'undefined'
 +
? config['HotCatDontAddToWatchlist']
 +
: HotCat.dont_add_to_watchlist
 +
  )
 +
);
 +
HotCat.no_autocommit =
 +
(typeof (window.hotcat_no_autocommit) != 'undefined'
 +
? !!window.hotcat_no_autocommit
 +
: (typeof (config['HotCatNoAutoCommit']) != 'undefined'
 +
? config['HotCatNoAutoCommit']
 +
: HotCat.no_autocommit
 +
  )
 +
);
 +
HotCat.del_needs_diff =
 +
(typeof (window.hotcat_del_needs_diff) != 'undefined'
 +
? !!window.hotcat_del_needs_diff
 +
: (typeof (config['HotCatDelNeedsDiff']) != 'undefined'
 +
? config['HotCatDelNeedsDiff']
 +
: HotCat.del_needs_diff
 +
  )
 +
);
 +
HotCat.suggest_delay = window.hotcat_suggestion_delay
 +
|| config['HotCatSuggestionDelay']
 +
|| HotCat.suggest_delay;
 +
HotCat.editbox_width = window.hotcat_editbox_width
 +
|| config['HotCatEditBoxWidth']
 +
|| HotCat.editbox_width;
 +
HotCat.suggestions   = window.hotcat_suggestions
 +
|| config['HotCatSuggestions']
 +
|| HotCat.suggestions;
 +
if (typeof (HotCat.suggestions) != 'string' || !suggestionConfigs[HotCat.suggestions])
 +
HotCat.suggestions = 'combined';
 +
HotCat.fixed_search  =
 +
(typeof (window.hotcat_suggestions_fixed) != 'undefined'
 +
? !!window.hotcat_suggestions_fixed
 +
: (typeof (config['HotCatFixedSuggestions']) != 'undefined'
 +
? config['HotCatFixedSuggestions']
 +
: HotCat.fixed_search
 +
  )
 +
);
 +
HotCat.single_minor  =
 +
(typeof (window.hotcat_single_changes_are_minor) != 'undefined'
 +
? !!window.hotcat_single_changes_are_minor
 +
: (typeof (config['HotCatMinorSingleChanges']) != 'undefined'
 +
? config['HotCatMinorSingleChanges']
 +
: HotCat.single_minor
 +
  )
 +
);
 +
HotCat.bg_changed    = window.hotcat_changed_background
 +
|| config['HotCatChangedBackground']
 +
|| HotCat.bg_changed;
 +
HotCat.use_up_down   =
 +
(typeof (window.hotcat_use_category_links) != 'undefined'
 +
? !!window.hotcat_use_category_links
 +
: (typeof (config['HotCatUseCategoryLinks']) != 'undefined'
 +
? config['HotCatUseCategoryLinks']
 +
: HotCat.use_up_down
 +
  )
 +
);
 +
HotCat.list_size    = window.hotcat_list_size
 +
|| config['HotCatListSize']
 +
|| HotCat.list_size;
 +
// Numeric input, make sure we have a numeric value
 +
HotCat.list_size = parseInt (HotCat.list_size, 10);
 +
if (isNaN (HotCat.list_size) || HotCat.list_size < 5) HotCat.list_size = 5;
 +
if (HotCat.list_size > 15) HotCat.list_size = 15;
 +
// Localize search engine names
 +
if (HotCat.engine_names) {
 +
for (var key in HotCat.engine_names) {
 +
if (suggestionConfigs[key] && HotCat.engine_names[key]) {
 +
suggestionConfigs[key].name = HotCat.engine_names[key];
 +
}
 +
}
 +
}
 +
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
 +
is_rtl = hasClass (document.body, 'rtl');
 +
if (!is_rtl) {
 +
if (document.defaultView && document.defaultView.getComputedStyle) { // Gecko etc.
 +
is_rtl = document.defaultView.getComputedStyle (document.body, null).getPropertyValue ('direction');
 +
} else if (document.body.currentStyle) { // IE, has subtle differences to getComputedStyle
 +
is_rtl = document.body.currentStyle['direction'];
 +
} else { // Not exactly right, but best effort
 +
is_rtl = document.body.style['direction'];
 +
}
 +
is_rtl = (is_rtl == 'rtl');
 +
}
 +
}
 +
 
 +
function can_edit () {
 +
var container = null;
 +
switch (skin) {
 +
case 'cologneblue':
 +
container = document.getElementById ('quickbar');
 +
// Fall through
 +
case 'standard':
 +
case 'nostalgia':
 +
if (!container) container = document.getElementById ('topbar');
 +
var lks = container.getElementsByTagName ('a');
 +
for (var i = 0; i < lks.length; i++) {
 +
if (  param ('title', lks[i].href) == wgPageName
 +
&& param ('action', lks[i].href) == 'edit')
 +
return true;
 +
}
 +
return false;
 +
default:
 +
// all modern skins:
 +
return document.getElementById ('ca-edit') !== null;
 +
}
 +
return false;
 +
}
 +
 
 +
function setup_upload () {
 +
onUpload = true;
 +
// Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
 +
var ip = document.getElementById ('mw-htmlform-description') || document.getElementById ('wpDestFile');
 +
if (!ip) {
 +
ip = document.getElementById ('wpDestFile');
 +
while (ip && ip.nodeName.toLowerCase() != 'table') ip = ip.parentNode;
 +
}
 +
if (!ip) return;
 +
var reupload = document.getElementById ('wpForReUpload');
 +
var destFile = document.getElementById ('wpDestFile');
 +
if (   (reupload && !!reupload.value)
 +
|| (destFile && (destFile.disabled || destFile.readOnly)))
 +
return; // re-upload form...
 +
// Insert a table row with two fields (label and empty category bar)
 +
var labelCell = make ('td');
 +
var lineCell  = make ('td');
 +
// Create the category line
 +
catLine = make ('div');
 +
catLine.className = 'catlinks';
 +
catLine.id = 'catlinks';
 +
catLine.style.textAlign = is_rtl ? 'right' : 'left';
 +
// We'll be inside a table row. Make sure that we don't have margins or strange borders.
 +
catLine.style.margin = '0';
 +
catLine.style.border = 'none';
 +
lineCell.appendChild (catLine);
 +
// Create the label
 +
var label = null;
 +
if (  typeof (UFUI) != 'undefined'
 +
&& typeof (UIElements) != 'undefined'
 +
&& typeof (UFUI.getLabel) == 'function'
 +
  )
 +
{
 +
try {
 +
label = UFUI.getLabel ('wpCategoriesUploadLbl');
 +
} catch (ex) {
 +
label = null;
 +
}
 +
}
 +
if (!label) {
 +
labelCell.id = 'hotcatLabel';
 +
labelCell.appendChild (make (HotCat.categories, true));
 +
} else {
 +
labelCell.id = 'hotcatLabelTranslated';
 +
labelCell.appendChild (label);
 +
}
 +
labelCell.className          = 'mw-label';
 +
labelCell.style.textAlign    = 'right';
 +
labelCell.style.verticalAlign = 'middle';
 +
// Change the onsubmit handler
 +
var form = document.getElementById ('upload') || document.getElementById ('mw-upload-form');
 +
if (form) {
 +
var newRow = ip.insertRow (-1);
 +
newRow.appendChild (labelCell);
 +
newRow.appendChild (lineCell);
 +
form.onsubmit = (function (oldSubmit) {
 +
return function () {
 +
var do_submit = true;
 +
if (oldSubmit) {
 +
if (typeof (oldSubmit) == 'string')
 +
do_submit = eval (oldSubmit);
 +
else if (typeof (oldSubmit) == 'function')
 +
do_submit = oldSubmit.apply (form, arguments);
 +
}
 +
if (!do_submit) return false;
 +
closeForm ();
 +
// Copy the categories
 +
var eb = document.getElementById ('wpUploadDescription')
 +
|| document.getElementById ('wpDesc');
 +
var addedOne = false;
 +
for (var i = 0; i < editors.length; i++) {
 +
var t = editors[i].currentCategory;
 +
if (!t) continue ;
 +
var key = editors[i].currentKey;
 +
var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
 +
// Only add if not already present
 +
var cleanedText = eb.value
 +
.replace(/<\!--(\s|\S)*?--\>/g, "")
 +
.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
 +
if (!find_category (cleanedText, t, true)) {
 +
eb.value += '\n' + new_cat;
 +
addedOne = true;
 +
}
 +
}
 +
if (addedOne) {
 +
// Remove "subst:unc" added by Flinfo if it didn't find categories
 +
eb.value = eb.value.replace(/\{\{subst:unc\}\}/g, "");
 +
}
 +
return true;
 +
};
 +
}) (form.onsubmit);
 +
}
 +
}
 +
 
 +
var cleanedText = null;
 +
 
 +
function isOnPage (span) {
 +
var catTitle = title (span.firstChild.getAttribute ('href', 2));
 +
if (!catTitle) return null;
 +
catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' ');
 +
if (HotCat.blacklist && HotCat.blacklist.test (catTitle)) return null;
 +
var result = { title : catTitle, match : ["", "", ""] };
 +
if (pageText === null) return result;
 +
if (cleanedText === null) {
 +
cleanedText = pageText
 +
.replace(/<\!--(\s|\S)*?--\>/g, "")
 +
.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
 +
}
 +
result.match = find_category (cleanedText, catTitle, true);
 +
return result;
 +
}
 +
 
 +
var initialized = false;
 +
var setupTimeout = null;
 +
 
 +
function findByClass (scope, tag, className) {
 +
// Compatibility routine. Uses jQuery if available, otherwise works with older getElementsByClassName
 +
var result;
 +
if (window.jQuery) {
 +
result = window.jQuery(scope).find(tag + '.' + className);
 +
} else {
 +
result = getElementsByClassName(scope, tag, className);
 +
}
 +
return (result && result.length) ? result[0] : null;
 +
}
 +
 
 +
function setup (additionalWork) {
 +
if (initialized) return;
 +
initialized = true;
 +
if (setupTimeout) {
 +
window.clearTimeout (setupTimeout);
 +
setupTimeout = null;
 +
}
 +
// Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
 +
// each category, and add the + link.
 +
catLine =  catLine                                        // Special:Upload
 +
|| document.getElementById ('mw-normal-catlinks') // MW >= 1.13alpha
 +
|| findByClass (document , 'p' , 'catlinks');    // MW < 1.13
 +
var hiddenCats = document.getElementById ('mw-hidden-catlinks');
 +
if (!catLine) {
 +
var footer = null;
 +
if (!hiddenCats) {
 +
footer = findByClass (document , 'div' , 'printfooter');
 +
if (!footer) return; // Don't know where to insert the category line
 +
}
 +
catLine = make ('div');
 +
catLine.id = 'mw-normal-catlinks';
 +
catLine.style.textAlign = is_rtl ? 'right' : 'left';
 +
// Add a label
 +
var label = make ('a');
 +
label.href  = wgArticlePath.replace ('$1', 'Special:Categories');
 +
label.title = HotCat.categories;
 +
label.appendChild (make (HotCat.categories, true));
 +
catLine.appendChild (label);
 +
catLine.appendChild (make (':', true));
 +
// Insert the new category line
 +
var container = (hiddenCats ? hiddenCats.parentNode : document.getElementById ('catlinks'));
 +
if (!container) {
 +
container = make ('div');
 +
container.id = 'catlinks';
 +
footer.parentNode.insertBefore (container, footer.nextSibling);
 +
}
 +
container.className = 'catlinks noprint';
 +
container.style.display = "";
 +
if (!hiddenCats) {
 +
container.appendChild (catLine);
 +
} else {
 +
container.insertBefore (catLine, hiddenCats);
 +
}
 +
} // end if catLine exists
 +
if (is_rtl) catLine.dir = 'rtl';
 +
 
 +
// Create editors for all existing categories
 +
 
 +
function createEditors (line, is_hidden) {
 +
var i;
 +
var cats = line.getElementsByTagName ('li');
 +
if (cats.length > 0) {
 +
newDOM = true; line = cats[0].parentNode;
 +
} else {
 +
cats = line.getElementsByTagName ('span');
 +
}
 +
// Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
 +
var copyCats = new Array (cats.length);
 +
for (i = 0; i < cats.length; i++) copyCats[i] = cats[i];
 +
var editor = null;
 +
for (i = 0; i < copyCats.length; i++) {
 +
var test = isOnPage (copyCats[i]);
 +
if (test !== null && test.match !== null) {
 +
editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2], is_hidden);
 +
}
 +
}
 +
return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
 +
}
 +
 
 +
var lastSpan = createEditors (catLine, false);
 +
// Create one to add a new category
 +
var editor = new CategoryEditor(newDOM ? catLine.getElementsByTagName('ul')[0] : catLine, null, null, lastSpan !== null, false);
 +
if (!onUpload) {
 +
if (pageText !== null && hiddenCats) {
 +
if (is_rtl) hiddenCats.dir = 'rtl';
 +
createEditors (hiddenCats, true);
 +
}
 +
// And finally add the "multi-mode" span. (Do this at the end, otherwise it ends up in the list above.)
 +
var enableMulti = make ('span');
 +
enableMulti.className = 'noprint';
 +
if (is_rtl) enableMulti.dir = 'rtl';
 +
catLine.insertBefore (enableMulti, catLine.firstChild.nextSibling);
 +
enableMulti.appendChild (make ('\xa0', true)); // nbsp
 +
multiSpan = make ('span');
 +
enableMulti.appendChild (multiSpan);
 +
multiSpan.innerHTML = '(<a>' + HotCat.addmulti + '</a>)';
 +
var lk = multiSpan.getElementsByTagName ('a')[0];
 +
lk.onclick = function (evt) {setMultiInput (); checkMultiInput (); return evtKill (evt);};
 +
lk.title = HotCat.multi_tooltip;
 +
lk.style.cursor = 'pointer';
 +
}
 +
cleanedText = null;
 +
if (typeof (additionalWork) == 'function') additionalWork();
 +
setupCompleted.loaded(); // Trigger signal; execute registered functions
 +
if (window.jQuery) window.jQuery('body').trigger ('hotcatSetupCompleted');
 +
}
 +
 
 +
function setPage (json) {
 +
var startTime = null;
 +
if (json && json.query) {
 +
if (json.query.pages) {
 +
var page = json.query.pages[wgArticleId === 0 ? "-1" : "" + wgArticleId];
 +
if (page) {
 +
if (page.revisions && page.revisions.length > 0) {
 +
// Revisions are sorted by revision ID, hence [0] is the one we asked for, and possibly there's a [1] if we're
 +
// not on the latest revision (edit conflicts and such).
 +
pageText = page.revisions[0]['*'];
 +
if (page.revisions[0].timestamp) pageTime = page.revisions[0].timestamp.replace (/\D/g, "");
 +
if (page.revisions[0].revid) pageTextRevId = page.revisions[0].revid;
 +
if (page.revisions.length > 1) conflictingUser = page.revisions[1].user;
 +
}
 +
if (page.lastrevid) lastRevId = page.lastrevid;
 +
if (page.starttimestamp) startTime = page.starttimestamp.replace (/\D/g, "");
 +
pageWatched = typeof (page.watched) == 'string';
 +
editToken = page.edittoken;
 +
if (page.langlinks && (!json['query-continue'] || !json['query-continue'].langlinks)) {
 +
// We have interlanguage links, and we got them all.
 +
var re = "";
 +
for (var i = 0; i < page.langlinks.length; i++) {
 +
re += (i > 0 ? '|' : "") + page.langlinks[i].lang.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
 +
}
 +
if (re.length > 0) {
 +
interlanguageRE = new RegExp ('((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$');
 +
}
 +
}
 +
 
 +
}
 +
}
 +
// Siteinfo
 +
if (json.query.general) {
 +
HotCat.capitalizePageNames = (json.query.general['case'] == 'first-letter');
 +
if (json.query.general.time && !startTime) startTime = json.query.general.time.replace (/\D/g, "");
 +
}
 +
serverTime = startTime;
 +
// Userinfo
 +
if (json.query.userinfo && json.query.userinfo.options) {
 +
watchCreate = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchcreations == '1';
 +
watchEdit   = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchdefault == '1';
 +
minorEdits  = json.query.userinfo.options.minordefault == 1;
 +
// If the user has the "All edits are minor" preference enabled, we should honor that
 +
// for single category changes, no matter what the site configuration is.
 +
if (minorEdits) HotCat.single_minor = true;
 +
}
 +
}
 +
}
 +
 
 +
function createCommitForm () {
 +
if (commitForm) return;
 +
var formContainer = make ('div');
 +
formContainer.style.display = 'none';
 +
document.body.appendChild (formContainer);
 +
formContainer.innerHTML =
 +
'<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="'
 +
+ wgScript + '?title=' + encodeURIComponent (wgPageName)
 +
+ '&action=edit">'
 +
+ '<input type="hidden" name="wpTextbox1" />'
 +
+ '<input type="hidden" name="model" value="wikitext" />'
 +
+ '<input type="hidden" name="format" value="text/x-wiki" />'
 +
+ '<input type="hidden" name="wpSummary" value="" />'
 +
+ '<input type="checkbox" name="wpMinoredit" value="1" />'
 +
+ '<input type="checkbox" name="wpWatchthis" value="1" />'
 +
+ '<input type="hidden" name="wpAutoSummary" value="" />'
 +
+ '<input type="hidden" name="wpEdittime" />'
 +
+ '<input type="hidden" name="wpStarttime" />'
 +
+ '<input type="hidden" name="wpDiff" value="wpDiff" />'
 +
+ '<input type="hidden" name="oldid" value="0" />'
 +
+ '<input type="submit" name="hcCommit" value="hcCommit" />'
 +
+ '<input type="hidden" name="wpEditToken" />'
 +
+ '<input type="hidden" name="wpUltimateParam" value="1" />'
 +
+ '</form>';
 +
commitForm = document.getElementById ('hotcatCommitForm');
 +
}
 +
 
 +
function getPage () {
 +
// We know we have an article here.
 +
if (wgArticleId === 0) {
 +
// Doesn't exist yet.
 +
pageText = "";
 +
pageTime = null;
 +
setup (createCommitForm);
 +
} else {
 +
var url = wgServer + wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&titles='
 +
+ encodeURIComponent (wgPageName)
 +
+ '&prop=info%7Crevisions&rvprop=content%7Ctimestamp%7Cids&meta=siteinfo&rvlimit=1&rvstartid='
 +
+ wgCurRevisionId;
 +
var s = make ('script');
 +
s.src = armorUri(url);
 +
s.type = 'text/javascript';
 +
HotCat.start = function (json) { setPage (json); setup (createCommitForm); };
 +
document.getElementsByTagName ('head')[0].appendChild (s);
 +
setupTimeout = window.setTimeout (function () {setup (createCommitForm);}, 4000); // 4 sec, just in case getting the wikitext takes longer.
 +
}
 +
}
 +
 
 +
function run () {
 +
if (HotCat.started) return;
 +
HotCat.started = true;
 +
loadTrigger.register(really_run);
 +
}
 +
 
 +
function really_run () {
 +
initialize ();
 +
 
 +
if (is_rtl && is_ie6) return; // Disabled! IE6 with RTL is just too broken...
 +
if (!HotCat.upload_disabled && wgNamespaceNumber === -1 && wgCanonicalSpecialPageName == 'Upload' && wgUserName) {
 +
setup_upload ();
 +
setup (function () {
 +
// Check for state restoration once the setup is done otherwise, but before signalling setup completion
 +
if (  typeof (UploadForm) != 'undefined'
 +
&& typeof (UploadForm.previous_hotcat_state) != 'undefined'
 +
&& UploadForm.previous_hotcat_state !== null)
 +
{
 +
UploadForm.previous_hotcat_state = setState (UploadForm.previous_hotcat_state);
 +
}
 +
});
 +
} else {
 +
if (!wgIsArticle || wgAction != 'view' || param('diff') !== null || param('oldid') !== null || !can_edit() || HotCat.disable()) return;
 +
getPage ();
 +
}
 +
}
 +
 
 +
// Legacy stuff
 +
 
 +
function closeForm () {
 +
// Close all open editors without redirect resolution and other asynchronous stuff.
 +
for (var i = 0; i < editors.length; i++) {
 +
if (editors[i].state == CategoryEditor.OPEN) {
 +
editors[i].cancel();
 +
} else if (editors[i].state == CategoryEditor.CHANGE_PENDING) {
 +
editors[i].sanitizeInput ();
 +
var value = editors[i].text.value.split('|');
 +
var key   = null;
 +
if (value.length > 1) key = value[1];
 +
var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
 +
if (v.length === 0) {
 +
editors[i].cancel ();
 +
} else {
 +
editors[i].currentCategory = v;
 +
editors[i].currentKey = key;
 +
editors[i].currentExists = this.inputExists;
 +
editors[i].close ();
 +
}
 +
}
 +
}
 +
}
 +
 
 +
function getState () {
 +
var result = null;
 +
for (var i = 0; i < editors.length; i++) {
 +
var text = editors[i].currentCategory;
 +
var key  = editors[i].currentKey;
 +
if (text && text.length > 0) {
 +
if (key !== null) text += '|' + key;
 +
if (result === null)
 +
result = text;
 +
else
 +
result = result + '\n' + text;
 +
}
 +
}
 +
return result;
 +
}
 +
 
 +
function setState (state) {
 +
var cats = state.split ('\n');
 +
if (cats.length === 0) return null;
 +
if (initialized && editors.length == 1 && editors[0].isAddCategory) {
 +
// Insert new spans and create new editors for them.
 +
var newSpans = [];
 +
var before = editors.length == 1 ? editors[0].span : null;
 +
var i;
 +
for (i = 0; i < cats.length; i++) {
 +
if (cats[i].length === 0) continue;
 +
var cat = cats[i].split ('|');
 +
var key = cat.length > 1 ? cat[1] : null;
 +
cat = cat[0];
 +
var lk = make ('a'); lk.href = wikiPagePath (HotCat.category_canonical + ':' + cat);
 +
lk.appendChild (make (cat, true));
 +
lk.title = cat;
 +
var span = make ('span');
 +
span.appendChild (lk);
 +
if (i === 0) catLine.insertBefore (make (' ', true), before);
 +
catLine.insertBefore (span, before);
 +
if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
 +
newSpans.push ({element: span, title: cat, 'key': key});
 +
}
 +
// And change the last one...
 +
if (before) {
 +
before.parentNode.insertBefore (make (' | ', true), before);
 +
}
 +
var editor = null;
 +
for (i = 0; i < newSpans.length; i++) {
 +
editor = new CategoryEditor (catLine, newSpans[i].element, newSpans[i].title, newSpans[i].key);
 +
}
 +
}
 +
return null;
 +
}
 +
 
 +
// Now export these legacy functions
 +
window.hotcat_get_state  = function () { return getState(); };
 +
window.hotcat_set_state  = function (state) { return setState (state); };
 +
window.hotcat_close_form = function () { closeForm (); };
 +
 
 +
if (window.mediaWiki && window.mediaWiki.config) {
 +
// Make sure we don't get conflicts with AjaxCategories (core development that should one day
 +
// replace HotCat).
 +
window.mediaWiki.config.set('disableAJAXCategories', true);
 +
}
 +
// Run as soon as possible. This varies depending on MediaWiki version;
 +
// window's 'load' event is always safe, but usually we can do better than that.
  
addOnloadHook ( hotcat ) ;
+
// Check for version to avoid MediaWiki bug 32537.
 +
var mwVersion = (window.mediaWiki && mediaWiki.config) ? mediaWiki.config.get('wgVersion') : window.wgVersion;
 +
if (parseFloat(mwVersion) > 1.20) {
 +
// We can safely trigger just after user configuration is loaded. Also start HotCat if the user module fails to load.
 +
// Won't start on IE6 start with this. If you need HotCat with MW > 1.20 on IE6, use window.attachEvent('onload', run);
 +
var startHotCat = function(){ jQuery(document).ready(run); };
 +
mediaWiki.loader.using('user', startHotCat, startHotCat);
 +
} else {
 +
// mw.loader.using('user', ...) could have unintended side-effects. Fall back to DOMContentLoaded.
 +
jQuery(document.body).on('DOMContentLoaded', run);
 +
// And in case we're loaded after DOMContentLoaded fires or in a browser that doesn't support it,
 +
// fall back to window.onload (which is definitely supported on MW 1.20 and lower).
 +
// The run function itself protects against double initialization, so it's okay.
 +
jQuery(window).on('load', run);
 +
}
 +
})();
  
 
} // end if (guard)
 
} // end if (guard)
 
//</source>
 
//</source>

Revisión de 16:02 31 ago 2014

//<source lang="javascript">
 
/*
 HotCat V2.29
 
 Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
 Supports multiple category changes, as well as redirect and disambiguation resolution. Also
 plugs into the upload form. Search engines to use for the suggestion list are configurable, and
 can be selected interactively.
 
 Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat
 List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Version_history
 
 License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
 
 Choose whichever license of these you like best :-)
*/
 
/*
 This code is MW version safe. It should run on any MediaWiki installation >= MW 1.15. Note: if
 running on MW >= 1.17 configured with $wgLegacyJavaScriptGlobals != true, it will still force
 publishing the wg* globals in the window object. Note that HotCat is supposed to run with or
 without jQuery, and also on older installations that do not yet have window.mediaWiki. If you
 use any of these newer features, make sure you qualify them by checking whether they exist at
 all, and by providing some meaningful fallback implementation if not. To start itself, HotCat
 uses jQuery(document).ready(). If it doesn't exist, HotCat won't start.
*/
/* jshint ignore:start */ // This old code uses too many coding conventions incompatible with jshint.
 
if (typeof wgAction == 'undefined' && window.mediaWiki && window.mediaWiki.config) { // Compatibility hack
	window.wgAction = window.mediaWiki.config.get('wgAction');
}
if ((typeof window.HotCat == 'undefined' || window.HotCat.nodeName) && wgAction != 'edit') { // Guard against double inclusions, and inactivate on edit pages
 
// Configuration stuff.
window.HotCat = {
	// Localize these messages to the main language of your wiki.
	messages :
		{cat_removed  : 'removed [[Category:$1]]'
		,template_removed  : 'removed {{[[Category:$1]]}}'
		,cat_added    : 'added [[Category:$1]]'
		,cat_keychange: 'new key for [[Category:$1]]: "$2"' // $2 is the new key
		,cat_notFound : 'Category "$1" not found'
		,cat_exists   : 'Category "$1" already exists; not added.'
		,cat_resolved : ' (redirect [[Category:$1]] resolved)'
		,uncat_removed: 'removed {{uncategorized}}'
		,separator    : '; '
		,prefix       : ""
			// Some text to prefix to the edit summary.
		,using        : ' using [[Help:Gadget-HotCat|HotCat]]'
			// Some text to append to the edit summary. Named 'using' for historical reasons. If you prefer
			// to have a marker at the front, use prefix and set this to the empty string.
		,multi_change : '$1 categories'
			// $1 is replaced by a number. If your language has several plural forms (c.f. [[:en:Dual (grammatical form)]]),
			// you can set this to an array of strings suitable for passing to mw.language.configPlural().
			// If that function doesn't exist, HotCat will simply fall back to using the last
			// entry in the array.
		,commit       : '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.
		,short_catchange : null
			// Defaults to '[[' + category_canonical + ':$1]]'. Can be overridden if in the short edit summaries
			// not the standard category name should be used but, say, a shorter namespace alias. $1 is replaced
			// by a category name.
		}
 ,category_regexp    : '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]'
	// Regular sub-expression matching all possible names for the category namespace. Is automatically localized
	// correctly if you're running MediaWiki 1.16 or later. Otherwise, set it appropriately, e.g. at the German
	// Wikipedia, use '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]|[Kk][Aa][Tt][Ee][Gg][Oo][Rr][Ii][Ee]', or at the
	// Chinese Wikipedia, use '[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]|分类|分類'. Note that namespaces are case-
	// insensitive!
 ,category_canonical : 'Category'
	// The standard category name on your wiki. Is automatically localized correctly if you're running
	// MediaWiki 1.16 or later; otherwise, set it to the preferred category name (e.g., "Kategorie").
 ,categories         : 'Categories'
	// Plural of category_canonical.
 ,disambig_category  : 'Disambiguation'
	// Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
	// any items, but that contains links to other categories where stuff should be categorized. If you don't have
	// that concept on your wiki, set it to null. Use blanks, not underscores.
 ,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: '(\u2212)', add: '(+)', restore: '(×)', undo: '(×)', down: '(\u2193)', up: '(\u2191)'}
	// The little modification links displayed after category names. U+2212 is a minus sign; U+2193 and U+2191 are
	// downward and upward pointing arrows. Do not use ↓ and ↑ in the code!
 ,tooltips : {
	 change:  'Modify'
	,remove:  'Remove'
	,add:     'Add a new category'
	,restore: 'Undo changes'
	,undo:    'Undo changes'
	,down:    'Open for modifying and display subcategories'
	,up:      'Open for modifying and display parent categories'
  }
	// The tooltips for the above links
 ,addmulti           : '<span>+<sup>+</sup></span>'
	// The HTML content of the "enter multi-mode" link at the front.
 ,multi_tooltip      : 'Modify several categories'
	// Tooltip for the "enter multi-mode" link
 ,disable            :
		function () { // Return true to disable HotCat. HotCat guarantees that the wg* globals exist here.
			var ns = wgNamespaceNumber;
			return (   ns < 0     // Special pages; Special:Upload is handled differently
					|| ns === 10  // Templates
					|| ns === 828 // Module (Lua)
					|| ns === 8   // MediaWiki
					|| ns === 6 && wgArticleId === 0 // Non-existing file pages
					|| ns === 2 && /\.(js|css)$/.test(wgTitle) // User scripts
					|| typeof (wgNamespaceIds) != 'undefined'
						&& (   ns === wgNamespaceIds['creator']
							|| ns === wgNamespaceIds['timedtext']
							|| ns === wgNamespaceIds['institution']
						   )
				   );
		}
 ,uncat_regexp : /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}\s*(<\!--.*?--\>)?/g
	// A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
	// If not, set it to null.
 ,existsYes    : '//upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
 ,existsNo     : '//upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png'
	// The images used for the little indication icon. Should not need changing.
 ,template_regexp    : '[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]'
	// Regexp to recognize templates. Like "category" above; autolocalized for MW 1.16+, otherwise set manually here.
	// On the German Wikipedia, you might use '[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]|[Vv][Oo][Rr][Ll][Aa][Gg][Ee]'.
 ,template_categories : {}
	// a list of categories which can be removed by removing a template
	// key: the category without namespace
	// value: A regexp matching the template name, again without namespace
	// If you don't have this at your wiki, or don't want this, set it to an empty object {}.
 ,engine_names : {
	 searchindex : 'Search index'
	,pagelist    : 'Page list'
	,combined    : 'Combined search'
	,subcat      : 'Subcategories'
	,parentcat   : 'Parent categories'
  }
	// Names for the search engines
 ,capitalizePageNames : true
	// Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the first letter
	// of a page is automatically capitalized ("first-letter"; Category:aa == Category:Aa), or it isn't
	// ("case-sensitive"; Category:aa != Category:Aa). It doesn't currently have a fully case-insensitive mode
	// (which would mean Category:aa == Category:Aa == Category:AA == Category:aA)
	// HotCat tries to set this correctly automatically using an API query. It's still a good idea to manually
	// configure it correctly; either directly here if you copied HotCat, or in the local configuration file
	// MediaWiki:Gadget-HotCat.js/local_defaults if you hotlink to the Commons-version, to ensure it is set even
	// if that API query should fail for some strange reason.
 ,upload_disabled : false
	// If upload_disabled is true, HotCat will not be used on the Upload form.
 ,blacklist : null
	// Single regular expression matching blacklisted categories that cannot be changed or
	// added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
	// or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
	// word "maintenance" in its title.
 
// Stuff changeable by users:
 ,bg_changed : '#F8CCB0'
	// Background for changed categories in multi-edit mode. Default is a very light salmon pink.
 ,no_autocommit : false
	// If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
	// the changes; users must always save explicitly.
 ,del_needs_diff : false
	// If true, the "category deletion" link "(-)" will never save automatically but always show an
	// edit page where the user has to save the edit manually. Is false by default because that's the
	// traditional behavior. This setting overrides no_autocommit for "(-)" links.
 ,suggest_delay : 100
	// Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
	// server to get suggestions.
 ,editbox_width : 40
	// Default width, in characters, of the text input field.
 ,suggestions : 'combined'
	// One of the engine_names above, to be used as the default suggestion engine.
 ,fixed_search : false
	// If true, always use the default engine, and never display a selector.
 ,use_up_down : true
	// If false, do not display the "up" and "down" links
 ,list_size : 5
	// Default list size
 ,single_minor : true
	// If true, single category changes are marked as minor edits. If false, they're not.
 ,dont_add_to_watchlist : false
	// If true, never add a page to the user's watchlist. If false, pages get added to the watchlist if
	// the user has the "Add pages I edit to my watchlist" or the "Add pages I create to my watchlist"
	// options in his or her preferences set.
 ,shortcuts : null
 ,addShortcuts :
		function (map) {
			if (!map) return;
			window.HotCat.shortcuts = window.HotCat.shortcuts || {};
			for (var k in map) {
				if (!map.hasOwnProperty (k) || typeof k != 'string') continue;
				var v = map[k];
				if (typeof v != 'string') continue;
				k = k.replace (/^\s+|\s+$/g, "");
				v = v.replace (/^\s+|\s+$/g, "");
				if (k.length === 0 || v.length === 0) continue;
				window.HotCat.shortcuts[k] = v;
			}
		}
};
 
(function () { // Local scope to avoid polluting the global namespace with declarations
 
	// Backwards compatibility stuff. We want HotCat to work with either wg* globals, or with mw.config.get().
	// Our "solution" is to publish the wg* globals if they're not already published.
	if (window.mediaWiki && window.mediaWiki.config) {
		var globals = window.mediaWiki.config.get();
		if (globals && globals !== window) {
			for (var k in globals) window[k] = globals[k];
			window.mediaWiki.config = new window.mediaWiki.Map(true); // Make config point to window again.
		}
		globals = null;
	}
	// More backwards compatibility. We have a few places where we test for the browser: once for
	// Safari < 3.0, twice for WebKit (Chrome or Safari, any versions), twice for IE <= 6, and
	// once for IE < 8.
	var ua = navigator.userAgent.toLowerCase();
	var is_ie6 = /msie ([0-9]{1,}[\.0-9]{0,})/.exec(ua) !== null && parseFloat(RegExp.$1) <= 6.0;
	var is_ie_lt8 = /msie ([0-9]{1,}[\.0-9]{0,})/.exec(ua) !== null && parseFloat(RegExp.$1) < 8.0;
	var is_webkit = /applewebkit\/\d+/.test(ua) && ua.indexOf ('spoofer') < 0;
	// And even more compatbility. HotCat was developed without jQuery, and anyway current jQuery
	// (1.7.1) doesn't seem to support in jquery.getJSON() or jQuery.ajax() the automatic
	// switching from GET to POST requests if the query arguments would make the uri too long.
	// (IE has a hard limit of 2083 bytes, and the servers may have limits around 4 or 8kB.)
	//    Anyway, HotCat is supposed to run on wikis without jQuery, so we'd have to supply some
	// ajax routines ourselves in any case. We can't rely on the old sajax_init_object(), newer
	// MW versions (>= 1.19) might not have it.
	var getJSON = (function () {
		function getRequest () {
			var request = null;
			try {
				request = new window.XMLHttpRequest();
			} catch (anything) {
				if (window.ActiveXObject) {
					try {
						request = new window.ActiveXObject('Microsoft.XMLHTTP');
					} catch (any) {
					}
				} // end if IE
			} // end try-catch
			return request;
		}
 
		return function (settings) {
			var req = getRequest();
			if (!req && settings && settings.error) settings.error (req);
			if (!req || !settings || !settings.uri) return req;
			var uri = armorUri (settings.uri);
			var args = settings.data || null;
			var method;
			if (args && uri.length + args.length + 1 > 2000) {
				// We lose caching, but at least we can make the request
				method = 'POST';
				req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
			} else {
				method = 'GET';
				if (args) uri += '?' + args;
				args = null;
			}
			req.open (method, uri, true);
			req.onreadystatechange = function () {
				if (req.readyState != 4) return;
				if (req.status != 200 || !req.responseText || !(/^\s*[\{\[]/.test(req.responseText))) {
					if (settings.error) settings.error (req);
				} else {
					if (settings.success) settings.success (eval ('(' + req.responseText + ')'));
				}
			};
			req.setRequestHeader ('Pragma', 'cache=yes');
			req.setRequestHeader ('Cache-Control', 'no-transform');
			req.send (args);
			return req;
		};
	})();
 
	function armorUri (uri) {
		// Avoid protocol-relative URIs, IE7 has a bug with them in Ajax calls
		if (uri.length >= 2 && uri.substring(0, 2) == '//') return document.location.protocol + uri;
		return uri;
	}
 
	function LoadTrigger (needed) {
		this.queue = [];
		this.toLoad = needed;
	}
	LoadTrigger.prototype = {
		register : function (callback) {
			if (this.toLoad <= 0) {
				callback (); // Execute directly
			} else {
				this.queue[this.queue.length] = callback;
			}
		},
 
		loaded : function () {
			if (this.toLoad > 0) {
				this.toLoad--;
				if (this.toLoad === 0) {
					// Run queued callbacks once
					for (var i = 0; i < this.queue.length; i++) this.queue[i]();
					this.queue = [];
				}
			}
		}
 
	};
 
	var setupCompleted = new LoadTrigger(1);
	// Used to run user-registered code once HotCat is fully set up and ready.
	HotCat.runWhenReady = function (callback) {setupCompleted.register(callback);};
 
	var loadTrigger = new LoadTrigger(2);
	// Used to delay running the HotCat setup until /local_defaults and localizations have been loaded.
 
	function load (uri) {
		var head = document.getElementsByTagName ('head')[0];
		var s = document.createElement ('script');
		s.setAttribute ('src', armorUri(uri));
		s.setAttribute ('type', 'text/javascript');
		var done = false;
 
		function afterLoad () {
			if (done) return;
			done = true;
			s.onload = s.onreadystatechange = s.onerror = null; // Properly clean up to avoid memory leaks in IE
			if (head && s.parentNode) head.removeChild (s);
			loadTrigger.loaded();
		}
 
		s.onload = s.onreadystatechange = function () { // onreadystatechange for IE, onload for all others
			if (done) return;
			if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
				afterLoad ();
			}
		};
		s.onerror = afterLoad; // Clean up, but otherwise ignore errors
		head.insertBefore (s, head.firstChild); // appendChild may trigger bugs in IE6 here
	}
 
	function loadJS (page) {
		load (wgServer + wgScript + '?title=' + encodeURIComponent (page) + '&action=raw&ctype=text/javascript');
	}
 
	function loadURI (href) {
		var url = href;
		if (url.substring (0, 2) == '//') {
			url = window.location.protocol + url;
		} else if (url.substring (0, 1) == '/') {
			url = wgServer + url;
		}
		load (url);
	}
 
	// Load local configurations, overriding the pre-set default values in the HotCat object above. This is always loaded
	// from the wiki where this script is executing, even if this script itself is hotlinked from the Commons. This can
	// be used to change the default settings, or to provide localized interface texts for edit summaries and so on.
	loadJS ('MediaWiki:Gadget-HotCat.js/local_defaults');
 
	// Load localized UI texts. These are the texts that HotCat displays on the page itself. Texts shown in edit summaries
	// should be localized in /local_defaults above.
	if (wgUserLanguage != 'en') {
		// Lupo: somebody thought it would be a good idea to add this. So the default is true, and you have to set it to false
		// explicitly if you're not on the Commons and don't want that.
		if (typeof window.hotcat_translations_from_commons == 'undefined') {
			window.hotcat_translations_from_commons = wgServer.indexOf('//commons') < 0;
		}
		// Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
		if (window.hotcat_translations_from_commons && wgServer.indexOf('//commons') < 0) {
			loadURI ('//commons.wikimedia.org/w/index.php?title='
				+ 'MediaWiki:Gadget-HotCat.js/' + wgUserLanguage
				+ '&action=raw&ctype=text/javascript&smaxage=21600&maxage=86400'
			);
		} else {
			// Load translations locally
			loadJS ('MediaWiki:Gadget-HotCat.js/' + wgUserLanguage);
		}
	} else {
		loadTrigger.loaded();
	}
 
	// No further changes should be necessary here.
 
	// The following regular expression strings are used when searching for categories in wikitext.
	var wikiTextBlank   = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
	var wikiTextBlankRE = new RegExp (wikiTextBlank, 'g');
	// Regexp for handling blanks inside a category title or namespace name.
	// See http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Title.php?revision=104051&view=markup#l2722
	// See also http://www.fileformat.info/info/unicode/category/Zs/list.htm
	//   MediaWiki collapses several contiguous blanks inside a page title to one single blank. It also replace a
	// number of special whitespace characters by simple blanks. And finally, blanks are treated as underscores.
	// Therefore, when looking for page titles in wikitext, we must handle all these cases.
	//   Note: we _do_ include the horizontal tab in the above list, even though the MediaWiki software for some reason
	// appears to not handle it. The zero-width space \u200B is _not_ handled as a space inside titles by MW.
	var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
	// Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
	// a link must be on one single line.
	//   MediaWiki also removes Unicode bidi override characters in page titles (and namespace names) completely.
	// This is *not* handled, as it would require us to allow any of [\u200E\u200F\u202A-\u202E] between any two
	// characters inside a category link. It _could_ be done though... We _do_ handle strange spaces, including the
	// zero-width space \u200B, and bidi overrides between the components of a category link (adjacent to the colon,
	// or adjacent to and inside of "[[" and "]]").
 
	// First auto-localize the regexps for the category and the template namespaces.
	if (typeof (wgFormattedNamespaces) != 'undefined') {
		function autoLocalize (namespaceNumber, fallback) {
			function create_regexp_str (name)
			{
				if (!name || name.length === 0) return "";
				var regex_name = "";
				for (var i = 0; i < name.length; i++){
					var initial = name.substr (i, 1);
					var ll = initial.toLowerCase ();
					var ul = initial.toUpperCase ();
					if (ll == ul){
						regex_name += initial;
					} else {
						regex_name += '[' + ll + ul + ']';
					}
				}
				return regex_name
							.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1')
							.replace (wikiTextBlankRE, wikiTextBlank);
			}
 
			fallback = fallback.toLowerCase();
			var canonical  = wgFormattedNamespaces["" + namespaceNumber].toLowerCase();
			var regexp     = create_regexp_str (canonical);
			if (fallback && canonical != fallback) regexp += '|' + create_regexp_str (fallback);
			for (var cat_name in wgNamespaceIds) {
				if (   typeof (cat_name) == 'string'
					&& cat_name.toLowerCase () != canonical
					&& cat_name.toLowerCase () != fallback
					&& wgNamespaceIds[cat_name] == namespaceNumber)
				{
					regexp += '|' + create_regexp_str (cat_name);
				}
			}
			return regexp;
		}
 
		if (wgFormattedNamespaces['14']) {
			HotCat.category_canonical = wgFormattedNamespaces['14'];
			HotCat.category_regexp = autoLocalize (14, 'category');
		}
		if (wgFormattedNamespaces['10']) {
			HotCat.template_regexp = autoLocalize (10, 'template');
		}
	}
 
	// Utility functions. Yes, this duplicates some functionality that also exists in other places, but
	// to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
	// these few operations here.
	function bind (func, target) {
		var f = func, tgt = target;
		return function () { return f.apply (tgt, arguments); };
	}
	function make (arg, literal) {
		if (!arg) return null;
		return literal ? document.createTextNode (arg) : document.createElement (arg);
	}
	function param (name, uri) {
		if (typeof (uri) == 'undefined' || uri === null) uri = document.location.href;
		var re = new RegExp ('[&?]' + name + '=([^&#]*)');
		var m = re.exec (uri);
		if (m && m.length > 1) return decodeURIComponent(m[1]);
		return null;
	}
	function title (href) {
		if (!href) return null;
		var script = wgScript + '?';
		if (href.indexOf (script) === 0 || href.indexOf (wgServer + script) === 0 || wgServer.substring(0, 2) == '//' && href.indexOf (document.location.protocol + wgServer + script) === 0) {
			// href="/w/index.php?title=..."
			return param ('title', href);
		} else {
			// href="/wiki/..."
			var prefix = wgArticlePath.replace ('$1', "");
			if (href.indexOf (prefix) !== 0) prefix = wgServer + prefix; // Fully expanded URL?
			if (href.indexOf (prefix) !== 0 && prefix.substring(0, 2) == '//') prefix = document.location.protocol + prefix; // Protocol-relative wgServer?
			if (href.indexOf (prefix) === 0)
				return decodeURIComponent (href.substring (prefix.length));
		}
		return null;
	}
	function hasClass (elem, name) {
		return (' ' + elem.className + ' ').indexOf (' ' + name + ' ') >= 0;
	}
	function capitalize (str) {
		if (!str || str.length === 0) return str;
		return str.substr(0, 1).toUpperCase() + str.substr (1);
	}
	function wikiPagePath (pageName) {
		// Note: do not simply use encodeURI, it doesn't encode '&', which might break if wgArticlePath actually has the $1 in
		// a query parameter.
		return wgArticlePath.replace('$1', encodeURIComponent (pageName).replace(/%3A/g, ':').replace(/%2F/g, '/'));
	}
	function escapeRE(str) {
		return str.replace(/([\\\^\$\.\?\*\+\(\)\[\]])/g, '\\$1');
	}
 
	function substituteFactory (options) {
		options = options || {};
		var lead = options.indicator || '$';
		var indicator = escapeRE (lead);
		var lbrace = escapeRE (options.lbrace || '{');
		var rbrace = escapeRE (options.rbrace || '}');
		var re;
 
		re = new RegExp(
			 '(?:' + indicator + '(' + indicator + '))|'                                           // $$
			+'(?:' + indicator + '(\\d+))|'                                                        // $0, $1
			+'(?:' + indicator + '(?:' + lbrace + '([^' + lbrace + rbrace + ']+)' + rbrace + '))|' // ${key}
			+'(?:' + indicator + '(?!(?:[' + indicator + lbrace + ']|\\d))(\\S+?)\\b)'             // $key (only if first char after $ is not $, digit, or { )
			,'g');
		// Replace $1, $2, or ${key1}, ${key2}, or $key1, $key2 by values from map. $$ is replaced by a single $.
		return function (str, map) {
			if (!map) return str;
			return str.replace(re
				,function (match, prefix, idx, key, alpha) {
					if (prefix == lead) return lead;
					var k = alpha || key || idx;
					var replacement = typeof (map[k]) === 'function' ? map[k](match, k) : map[k];
					return typeof (replacement) === 'string' ? replacement : (replacement || match);
				}
			);
		};
	}
 
	var substitute = substituteFactory();
	var replaceShortcuts = (function () {
		var replaceHash = substituteFactory({indicator:'#',lbrace:'[',rbrace:']'});
		return function (str, map) {
			var s = replaceHash (str, map);
			return HotCat.capitalizePageNames ? capitalize(s) : s;
		};
	})();
 
	// Text modification
 
	var findCatsRE =
		new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(?:' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g');
 
	function replaceByBlanks (match) {
		return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
	}
 
	function find_category (wikitext, category, once) {
		var cat_regex = null;
		if(HotCat.template_categories[category]){
			cat_regex = new RegExp ('\\{\\{' + wikiTextBlankOrBidi + '(' + HotCat.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi
									+ '(?:' + HotCat.template_categories[category] + ')'
									+ wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}', 'g'
			);
		} else {
			var cat_name  = escapeRE (category);
			var initial   = cat_name.substr (0, 1);
			cat_regex = new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi
									+ (initial == '\\' || !HotCat.capitalizePageNames
										? initial
										: '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
									+ cat_name.substring (1).replace (wikiTextBlankRE, wikiTextBlank)
									+ wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]', 'g'
			);
		}
		if (once) return cat_regex.exec (wikitext);
		var copiedtext = wikitext
							.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
							.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
		var result = [];
		var curr_match = null;
		while ((curr_match = cat_regex.exec (copiedtext)) !== null) {
			result.push ({match : curr_match});
		}
		result.re = cat_regex;
		return result; // An array containing all matches, with positions, in result[i].match
	}
 
	var interlanguageRE = null;
 
	function change_category (wikitext, toRemove, toAdd, key, is_hidden) {
 
		function find_insertionpoint (wikitext) {
			var copiedtext = wikitext
								.replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
								.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
			// Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
			var index = -1;
			findCatsRE.lastIndex = 0;
			while (findCatsRE.exec(copiedtext) !== null) index = findCatsRE.lastIndex;
			if (index < 0) {
				// Find the index of the first interlanguage link...
				var match = null;
				if (!interlanguageRE) {
					// Approximation without API: interlanguage links start with 2 to 3 lower case letters, optionally followed by
					// a sequence of groups consisting of a dash followed by one or more lower case letters. Exceptions are "simple"
					// and "tokipona".
					match = /((^|\n\r?)(\[\[\s*(([a-z]{2,3}(-[a-z]+)*)|simple|tokipona)\s*:[^\]]+\]\]\s*))+$/.exec (copiedtext);
				} else {
					match = interlanguageRE.exec(copiedtext);
				}
				if (match) index = match.index;
				return {idx : index, onCat : false};
			}
			return {idx : index, onCat : index >= 0};
		}
 
		var summary   = [];
		var nameSpace = HotCat.category_canonical;
		var cat_point = -1; // Position of removed category;
 
		if (key) key = '|' + key;
		var keyChange = (toRemove && toAdd && toRemove == toAdd && toAdd.length > 0);
		var matches;
		if (toRemove && toRemove.length > 0) {
			matches = find_category (wikitext, toRemove);
			if (!matches || matches.length === 0) {
				return {text: wikitext, 'summary': summary, error: HotCat.messages.cat_notFound.replace (/\$1/g, toRemove)};
			} else {
				var before = wikitext.substring (0, matches[0].match.index);
				var after  = wikitext.substring (matches[0].match.index + matches[0].match[0].length);
				if (matches.length > 1) {
					// Remove all occurrences in after
					matches.re.lastIndex = 0;
					after = after.replace (matches.re, "");
				}
				if (toAdd) {
					nameSpace = matches[0].match[1] || nameSpace;
					if (key === null) key = matches[0].match[2]; // Remember the category key, if any.
				}
				// Remove whitespace (properly): strip whitespace, but only up to the next line feed.
				// If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
				// whitespace characters, insert a blank.
				var i = before.length - 1;
				while (i >= 0 && before.charAt (i) != '\n' && before.substr (i, 1).search (/\s/) >= 0) i--;
				var j = 0;
				while (j < after.length && after.charAt (j) != '\n' && after.substr (j, 1).search (/\s/) >= 0)
					j++;
				if (i >= 0 && before.charAt (i) == '\n' && (after.length === 0 || j < after.length && after.charAt (j) == '\n'))
					i--;
				if (i >= 0) before = before.substring (0, i+1); else before = "";
				if (j < after.length) after = after.substring (j); else after = "";
				if (before.length > 0 && before.substring (before.length - 1).search (/\S/) >= 0
						&& after.length > 0 && after.substr (0, 1).search (/\S/) >= 0)
					before += ' ';
				cat_point = before.length;
				if (cat_point === 0 && after.length > 0 && after.substr(0,1) == '\n') {
					after = after.substr(1);
				}
				wikitext = before + after;
				if (!keyChange) {
					if(HotCat.template_categories[toRemove]) {
						summary.push (HotCat.messages.template_removed.replace (/\$1/g, toRemove));
					} else {
						summary.push (HotCat.messages.cat_removed.replace (/\$1/g, toRemove));
					}
				}
			}
		}
		if (toAdd && toAdd.length > 0) {
			matches = find_category (wikitext, toAdd);
			if (matches && matches.length > 0) {
				return {text: wikitext, 'summary': summary, error : HotCat.messages.cat_exists.replace (/\$1/g, toAdd)};
			} else {
				var onCat = false;
				if (cat_point < 0) {
					var point = find_insertionpoint (wikitext);
					cat_point = point.idx;
					onCat = point.onCat;
				} else {
					onCat = true;
				}
				var newcatstring = '[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
				if (cat_point >= 0) {
					var suffix = wikitext.substring (cat_point);
					wikitext = wikitext.substring (0, cat_point) + (cat_point > 0 ? '\n' : "") + newcatstring + (!onCat ? '\n' : "");
					if (suffix.length > 0 && suffix.substr(0, 1) != '\n') {
						wikitext += '\n' + suffix;
					} else {
						wikitext += suffix;
					}
				} else {
					if (wikitext.length > 0 && wikitext.substr (wikitext.length - 1, 1) != '\n')
						wikitext += '\n';
					wikitext += (wikitext.length > 0 ? '\n' : "") + newcatstring;
				}
				if (keyChange) {
					var k = key || "";
					if (k.length > 0) k = k.substr (1);
					summary.push (substitute (HotCat.messages.cat_keychange, [null, toAdd, k]));
				} else {
					summary.push (HotCat.messages.cat_added.replace (/\$1/g, toAdd));
				}
				if (HotCat.uncat_regexp && !is_hidden) {
					var txt = wikitext.replace (HotCat.uncat_regexp, ""); // Remove "uncat" templates
					if (txt.length != wikitext.length) {
						wikitext = txt;
						summary.push (HotCat.messages.uncat_removed);
					}
				}
			}
		}
		return {text: wikitext, 'summary': summary, error: null};
	}
 
	// The real HotCat UI
 
	function evtKeys (e) {
		e = e || window.event || window.Event; // W3C, IE, Netscape
		var code = 0;
		if (typeof (e.ctrlKey) != 'undefined') { // All modern browsers
			// Ctrl-click seems to be overloaded in FF/Mac (it opens a pop-up menu), so treat cmd-click
			// as a ctrl-click, too.
			if (e.ctrlKey || e.metaKey) code |= 1;
			if (e.shiftKey) code |= 2;
		} else if (typeof (e.modifiers) != 'undefined') { // Netscape...
			if (e.modifiers & (Event.CONTROL_MASK | Event.META_MASK)) code |= 1;
			if (e.modifiers & Event.SHIFT_MASK) code |= 2;
		}
		return code;
	}
	function evtKill (e) {
		e = e || window.event || window.Event; // W3C, IE, Netscape
		if (typeof (e.preventDefault) != 'undefined') {
			e.preventDefault ();
			e.stopPropagation ();
		} else
			e.cancelBubble = true;
		return false;
	}
	function addEvent (node, evt, f, capture) {
		if (window.jQuery && (!capture || !node.addEventListener)) window.jQuery (node).bind (evt, f);
		else if (node.addEventListener) node.addEventListener (evt, f, capture); // FF etc; IE >= 9
		else if (node.attachEvent) node.attachEvent ('on' + evt, f); // Older IE; Opera
		else node['on' + evt] = f; // Very old!
	}
 
	var catLine      = null;
	var onUpload     = false;
	var editors      = [];
 
	var commitButton = null;
	var commitForm   = null;
	var multiSpan    = null;
 
	var pageText     = null;
	var pageTime     = null;
	var pageWatched  = false;
	var watchCreate  = false;
	var watchEdit    = false;
	var minorEdits   = false;
	var editToken    = null;
 
	var is_rtl       = false;
	var serverTime   = null;
	var lastRevId    = null;
	var pageTextRevId = null;
	var conflictingUser = null;
 
	var newDOM       = false; // true if MediaWiki serves the new UL-LI DOM for categories
 
	function setMultiInput () {
		if (commitButton || onUpload) return;
		commitButton = make ('input');
		commitButton.type  = 'button';
		commitButton.value = HotCat.messages.commit;
		commitButton.onclick = multiSubmit;
		if (multiSpan) {
			multiSpan.parentNode.replaceChild (commitButton, multiSpan);
		} else {
			catLine.appendChild (commitButton);
		}
	}
 
	function checkMultiInput () {
		if (!commitButton) return;
		var has_changes = false;
		for (var i = 0; i < editors.length; i++) {
			if (editors[i].state != CategoryEditor.UNCHANGED) {
				has_changes = true;
				break;
			}
		}
		commitButton.disabled = !has_changes;
	}
 
	function currentTimestamp () {
		var now = new Date();
		var ts  = "" + now.getUTCFullYear();
		function two (s) { return s.substr (s.length - 2); }
		ts = ts
			+ two ('0' + (now.getUTCMonth() + 1))
			+ two ('0' + now.getUTCDate())
			+ two ('00' + now.getUTCHours())
			+ two ('00' + now.getUTCMinutes())
			+ two ('00' + now.getUTCSeconds());
		return ts;
	}
 
	var saveInProgress = false;
	function initiateEdit (doEdit, failure) {
		if (saveInProgress) return;
		saveInProgress = true;
		var oldButtonState;
		if (commitButton) {
			oldButtonState = commitButton.disabled;
			commitButton.disabled = true;
		}
 
		function fail() {
			saveInProgress = false;
			if (commitButton) commitButton.disabled = oldButtonState;
			failure.apply(this, arguments);
		}
 
		// Must use Ajax here to get the user options and the edit token.
 
		getJSON ({
			 uri : wgServer + wgScriptPath + '/api.php'
			,data : 'format=json&action=query&titles=' + encodeURIComponent (wgPageName)
				+ '&prop=info%7Crevisions%7Clanglinks&inprop=watched&intoken=edit&rvprop=content%7Ctimestamp%7Cids%7Cuser&lllimit=500'
				+ '&rvlimit=2&rvdir=newer&rvstartid=' + wgCurRevisionId
				+ '&meta=siteinfo%7Cuserinfo&uiprop=options'
			,success : function (json) { setPage(json); doEdit(fail); }
			,error : function (req) { fail(req.status + ' ' + req.statusText); }
		});
	}
 
	function multiChangeMsg (count) {
		var msg = HotCat.messages.multi_change;
		if (typeof (msg) != 'string' && msg.length) {
			if (window.mediaWiki && window.mediaWiki.language && window.mediaWiki.language.convertPlural) {
				msg = window.mediaWiki.language.convertPlural (count, msg);
			} else {
				msg = msg[msg.length-1];
			}
		}
		return substitute (msg, [null, "" + count]);
	}
 
	function performChanges (failure, singleEditor) {
		if (pageText === null) {
			failure (HotCat.messages.multi_error);
			return;
		}
		// Backwards compatibility after message change (added $2 to cat_keychange)
		if (HotCat.messages.cat_keychange.indexOf ('$2') < 0) HotCat.messages.cat_keychange += '"$2"';
		// More backwards-compatibility with earlier HotCat versions:
		if (!HotCat.messages.short_catchange) HotCat.messages.short_catchange = '[[' + HotCat.category_canonical + ':$1]]';
		// Create a form and submit it. We don't use the edit API (api.php?action=edit) because
		// (a) sensibly reporting back errors like edit conflicts is always a hassle, and
		// (b) we want to show a diff for multi-edits anyway, and
		// (c) we want to trigger onsubmit events, allowing user code to intercept the edit.
		// Using the form, we can do (b) and (c), and we get (a) for free. And, of course, using the form
		// automatically reloads the page with the updated categories on a successful submit, which
		// we would have to do explicitly if we used the edit API.
		var action;
		// Normally, we don't have to care about edit conflicts. If some other user edited the page in the meantime, the
		// server will take care of it and merge the edit automatically or present an edit conflict screen. However, the
		// server suppresses edit conflicts with oneself. Hence, if we have a conflict, and the conflicting user is the
		// current user, then we set the "oldid" value and switch to diff, which gives the "you are editing an old version;
		// if you save, any more recent changes will be lost" screen.
		var editingOldVersion = lastRevId !== null && lastRevId != wgCurRevisionId || pageTextRevId !== null && pageTextRevId != wgCurRevisionId;
		var selfEditConflict = editingOldVersion && conflictingUser && conflictingUser == wgUserName;
		if (singleEditor && !singleEditor.noCommit && !HotCat.no_autocommit && editToken && !selfEditConflict) {
			// If we do have an edit conflict, but not with ourself, that's no reason not to attempt to save: the server side may actually be able to
			// merge the changes. We just need to make sure that we do present a diff view if it's a self edit conflict.
			commitForm.wpEditToken.value = editToken;
			action = commitForm.wpDiff;
			if (action) action.name = action.value = 'wpSave';
		} else {
			action = commitForm.wpSave;
			if (action) action.name = action.value = 'wpDiff';
		}
		var result = { text : pageText };
		var changed = [], added = [], deleted = [], changes = 0;
		var toEdit = !!singleEditor ? [singleEditor] : editors;
		var error = null;
		var i;
		for (i=0; i < toEdit.length; i++) {
			if (toEdit[i].state == CategoryEditor.CHANGED) {
				result = change_category (
						result.text
					, toEdit[i].originalCategory
					, toEdit[i].currentCategory
					, toEdit[i].currentKey
					, toEdit[i].currentHidden
				);
				if (!result.error) {
					changes++;
					if (!toEdit[i].originalCategory || toEdit[i].originalCategory.length === 0) {
						added.push (toEdit[i].currentCategory);
					} else {
						changed.push ({from : toEdit[i].originalCategory, to : toEdit[i].currentCategory});
					}
				} else if (error === null) {
					error = result.error;
				}
			} else if (   toEdit[i].state == CategoryEditor.DELETED
					   && toEdit[i].originalCategory
					   && toEdit[i].originalCategory.length > 0)
			{
				result = change_category (result.text, toEdit[i].originalCategory, null, null, false);
				if (!result.error) {
					changes++;
					deleted.push (toEdit[i].originalCategory);
				} else if (error === null) {
					error = result.error;
				}
			}
		}
		if (error !== null) { // Do not commit if there were errors
			action = commitForm.wpSave;
			if (action) action.name = action.value = 'wpDiff';
		}
		// Fill in the form and submit it
		commitForm.wpAutoSummary.value = 'd41d8cd98f00b204e9800998ecf8427e'; // MD5 hash of the empty string
		commitForm.wpMinoredit.checked = minorEdits;
		commitForm.wpWatchthis.checked = wgArticleId === 0 && watchCreate || watchEdit || pageWatched;
		if (wgArticleId > 0 || !!singleEditor) {
			if (changes == 1) {
				if (result.summary && result.summary.length > 0)
					commitForm.wpSummary.value = HotCat.messages.prefix + result.summary.join (HotCat.messages.separator) + HotCat.messages.using;
				commitForm.wpMinoredit.checked = HotCat.single_minor || minorEdits;
			} else if (changes > 1) {
				var summary = [];
				var shortSummary = [];
				// Deleted
				for (i = 0; i < deleted.length; i++) {
					summary.push ('-' + substitute (HotCat.messages.short_catchange, [null, deleted[i]]));
				}
				if (deleted.length == 1)
					shortSummary.push ('-' + substitute (HotCat.messages.short_catchange, [null, deleted[0]]));
				else if (deleted.length > 1)
					shortSummary.push ('- ' + multiChangeMsg (deleted.length));
				// Added
				for (i = 0; i < added.length; i++) {
					summary.push ('+' + substitute (HotCat.messages.short_catchange, [null, added[i]]));
				}
				if (added.length == 1)
					shortSummary.push ('+' + substitute (HotCat.messages.short_catchange, [null, added[0]]));
				else if (added.length > 1)
					shortSummary.push ('+ ' + multiChangeMsg (added.length));
				// Changed
				var arrow = is_rtl ? '\u2190' : '\u2192'; // left and right arrows. Don't use ← and → in the code.
				for (i = 0; i < changed.length; i++) {
					if (changed[i].from != changed[i].to) {
						summary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[i].from]) + arrow
										+ substitute (HotCat.messages.short_catchange, [null, changed[i].to]));
					} else {
						summary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[i].from]));
					}
				}
				if (changed.length == 1) {
					if (changed[0].from != changed[0].to) {
						shortSummary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[0].from]) + arrow
											+ substitute (HotCat.messages.short_catchange, [null, changed[0].to]));
					} else {
						shortSummary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[0].from]));
					}
				} else if (changed.length > 1) {
					shortSummary.push ('± ' + multiChangeMsg (changed.length));
				}
				if (summary.length > 0) {
					summary = summary.join (HotCat.messages.separator);
					if (summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length) {
						summary = shortSummary.join (HotCat.messages.separator);
					}
					commitForm.wpSummary.value = HotCat.messages.prefix + summary + HotCat.messages.using;
				}
			}
		}
		commitForm.wpTextbox1.value = result.text;
		commitForm.wpStarttime.value = serverTime || currentTimestamp ();
		commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
		if (selfEditConflict) commitForm.oldid.value = "" + (pageTextRevId || wgCurRevisionId);
		// Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
		commitForm.hcCommit.click();
	}
 
	function resolveMulti (toResolve, callback) {
		var i;
		for (i = 0; i < toResolve.length; i++) {
			toResolve[i].dab = null;
			toResolve[i].dabInput = toResolve[i].lastInput;
		}
		if (noSuggestions) {
			callback (toResolve);
			return;
		}
		// Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
		// category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
		var args = 'action=query&prop=info%7Clinks%7Ccategories%7Ccategoryinfo&plnamespace=14'
				+ '&pllimit=' + (toResolve.length * 10)
				+ '&cllimit=' + (toResolve.length * 10)
				+ '&format=json&titles=';
		for (i = 0; i < toResolve.length; i++) {
			var v = toResolve[i].dabInput;
			v = replaceShortcuts (v, HotCat.shortcuts);
			toResolve[i].dabInputCleaned = v;
			args += encodeURIComponent ('Category:' + v);
			if (i+1 < toResolve.length) args += '%7C';
		}
		getJSON({
			 uri : wgServer + wgScriptPath + '/api.php'
			,data : args
			,success: function (json) { resolveRedirects (toResolve, json); callback (toResolve); }
			,error: function (req) { if (!req) noSuggestions = true; callback (toResolve); }
		});
	}
 
	function resolveOne (page, toResolve) {
		var cats     = page.categories;
		var lks      = page.links;
		var is_dab   = false;
		var is_redir = typeof (page.redirect) == 'string'; // Hard redirect?
		var is_hidden = page.categoryinfo && typeof (page.categoryinfo.hidden) == 'string';
		var is_missing = typeof(page.missing) == 'string';
		var i;
		for (i = 0; i < toResolve.length; i++) {
			if (toResolve.length > 1 && toResolve[i].dabInputCleaned != page.title.substring (page.title.indexOf (':') + 1)) continue;
			// Note: the server returns in page an NFC normalized Unicode title. If our input was not NFC normalized, we may not find
			// any entry here. If we have only one editor to resolve (the most common case, I presume), we may simply skip the check.
			toResolve[i].currentHidden = is_hidden;
			toResolve[i].inputExists = !is_missing;
			toResolve[i].icon.src = armorUri(is_missing ? HotCat.existsNo : HotCat.existsYes);
		}
		if (is_missing) return;
		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 (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 (i = 0; i < toResolve.length; i++) {
			if (toResolve.length > 1 && toResolve[i].dabInputCleaned != page.title.substring (page.title.indexOf (':') + 1)) continue;
			toResolve[i].inputExists = true; // Might actually be wrong if it's a redirect pointing to a non-existing category
			toResolve[i].icon.src = armorUri(HotCat.existsYes);
			if (titles.length > 1) {
				toResolve[i].dab = titles;
			} else {
				toResolve[i].text.value =
					titles[0] + (toResolve[i].currentKey !== null ? '|' + toResolve[i].currentKey : "");
			}
		}
	}
 
	function resolveRedirects (toResolve, params) {
		if (!params || !params.query || !params.query.pages) return;
		for (var p in params.query.pages) resolveOne (params.query.pages[p], toResolve);
	}
 
	function multiSubmit () {
		var toResolve = [];
		for (var i = 0; i < editors.length; i++) {
			if (editors[i].state == CategoryEditor.CHANGE_PENDING || editors[i].state == CategoryEditor.OPEN)
				toResolve.push (editors[i]);
		}
		if (toResolve.length === 0) {
			initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
			return;
		}
		resolveMulti (
			  toResolve
			, function (resolved) {
					var firstDab = null;
					var dontChange = false;
					for (var i = 0; i < resolved.length; i++) {
						if (resolved[i].lastInput != resolved[i].dabInput) {
							// We didn't disable all the open editors, but we did asynchronous calls. It is
							// theoretically possible that the user changed something...
							dontChange = true;
						} else {
							if (resolved[i].dab) {
								if (!firstDab) firstDab = resolved[i];
							} else {
								if (resolved[i].acceptCheck(true)) resolved[i].commit();
							}
						}
					}
					if (firstDab) {
						showDab (firstDab);
					} else if (!dontChange) {
						initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
					}
				}
		);
	}
 
	var cat_prefix = null;
	var noSuggestions = false;
	var suggestionEngines = {
		opensearch :
			{ uri     : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1' // $1 = search term
			 ,handler : // Function to convert result of uri into an array of category names
				function (queryResult, queryKey) {
					if (queryResult && queryResult.length === 2) {
						var key = queryResult[0].substring(queryResult[0].indexOf(':') + 1);
						var titles = queryResult[1];
						var exists = false;
						if (!cat_prefix) cat_prefix = new RegExp ('^(' + HotCat.category_regexp + ':)');
						for (var i = 0; i < titles.length; i++) {
							cat_prefix.lastIndex = 0;
							var m = cat_prefix.exec (titles[i]);
							if (m && m.length > 1) {
								titles[i] = titles[i].substring (titles[i].indexOf (':') + 1); // rm namespace
								if (key == titles[i]) exists = true;
							} else {
								titles.splice (i, 1); // Nope, it's not a category after all.
								i--;
							}
						}
						titles.exists = exists;
						if (queryKey != key) titles.normalized = key; // Remember the NFC normalized key we got back from the server
						return titles;
					}
					return null;
				}
			}
		,internalsearch :
			{ uri     : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1&apprefix=$1'
			 ,handler :
				function (queryResult, queryKey) {
					if (queryResult && queryResult.query && queryResult.query.allpages) {
						var titles = queryResult.query.allpages;
						for (var i = 0; i < titles.length; i++) {
							titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
						}
						return titles;
					}
					return null;
				}
			}
		,exists :
			{ uri     : '/api.php?format=json&action=query&prop=info&titles=Category:$1'
			 ,handler :
				function (queryResult, queryKey) {
					if (queryResult && queryResult.query && queryResult.query.pages && !queryResult.query.pages[-1]) {
						// Should have exactly 1
						for (var p in queryResult.query.pages) {
							var title = queryResult.query.pages[p].title;
							title = title.substring (title.indexOf (':') + 1);
							var titles = [title];
							titles.exists = true;
							if (queryKey != title) titles.normalized = title; // NFC
							return titles;
						}
					}
					return null;
				}
			}
		,subcategories :
			// I don't understand why they didn't map cmnamespace=14 automatically to cmtype=subcat,
			// which gives better results and is faster.
			{ uri     : '/api.php?format=json&action=query&list=categorymembers'
				+(function (version) {
					var m = version.match(/^(\d+)\.(\d+)/);
					var major = 0, minor = 0;
					if (m && m.length > 1) {
						major = parseInt (m[1], 10);
						minor = (m.length > 2 ? parseInt (m[2], 10) : 0);
					}
					if (major > 1 || major === 1 && minor > 17) return '&cmtype=subcat'; // Since MW1.18
					return '&cmnamespace=14';
				  }
				)(wgVersion)
				+'&cmlimit=max&cmtitle=Category:$1'
			 ,handler :
				function (queryResult, queryKey) {
					if (queryResult && queryResult.query && queryResult.query.categorymembers) {
						var titles = queryResult.query.categorymembers;
						for (var i = 0; i < titles.length; i++) {
							titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
						}
						return titles;
					}
					return null;
				}
			}
		,parentcategories :
			{ uri     : '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max'
			 ,handler :
				function (queryResult, queryKey) {
					if (queryResult && queryResult.query && queryResult.query.pages) {
						for (var p in queryResult.query.pages) {
							if (queryResult.query.pages[p].categories) {
								var titles = queryResult.query.pages[p].categories;
								for (var i = 0; i < titles.length; i++) {
									titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
								}
								return titles;
							}
						}
					}
					return null;
				}
			}
	};
 
	var suggestionConfigs = {
		 searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}, show: true, temp: false, noCompletion : false}
		,pagelist    : {name: 'Page list', engines: ['internalsearch', 'exists'], cache: {}, show: true, temp: false, noCompletion : false}
		,combined    : {name: 'Combined search', engines: ['opensearch', 'internalsearch'], cache: {}, show: true, temp: false, noCompletion : false}
		,subcat      : {name: 'Subcategories', engines: ['subcategories'], cache: {}, show: true, temp: true, noCompletion : true}
		,parentcat   : {name: 'Parent categories', engines: ['parentcategories'], cache: {}, show: true, temp: true, noCompletion : true}
	};
 
	function CategoryEditor () { this.initialize.apply (this, arguments); }
	CategoryEditor.UNCHANGED      = 0;
	CategoryEditor.OPEN           = 1; // Open, but no input yet
	CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
	CategoryEditor.CHANGED        = 3;
	CategoryEditor.DELETED        = 4;
 
	// IE6 sometimes forgets to redraw the list when editors are opened or closed.
	// Adding/removing a dummy element helps, at least when opening editors.
	var dummyElement = make ('\xa0', true);
 
	function forceRedraw () {
		if (!is_ie6) return;
		if (dummyElement.parentNode) {
			document.body.removeChild (dummyElement);
		} else {
			document.body.appendChild (dummyElement);
		}
	}
 
	// Event keyCodes that we handle in the text input field/suggestion list.
	var BS = 8, TAB = 9, RET = 13, ESC = 27, SPACE = 32, PGUP = 33, PGDOWN = 34, UP = 38, DOWN = 40, DEL = 46, IME = 229;
 
	function makeActive (which) {
		if (which.is_active) return;
		for (var i = 0; i < editors.length; i++) {
			if (editors[i] !== which) editors[i].inactivate ();
		}
		which.is_active = true;
		if (which.dab) {
			showDab (which);
		} else {
			// Check for programmatic value changes.
			var expectedInput = which.lastRealInput || which.lastInput || "";
			var actualValue = which.text.value || "";
			if (expectedInput.length === 0 && actualValue.length > 0 || expectedInput.length > 0 && actualValue.indexOf (expectedInput) !== 0) {
				// Somehow the field's value appears to have changed, and which.lastSelection therefore is no longer valid. Try to set the
				// cursor at the end of the category, and do not display the old suggestion list.
				which.showsList = false;
				var v = actualValue.split('|');
				which.lastRealInput = which.lastInput = v[0];
				if (v.length > 1) which.currentKey = v[1];
				if (which.lastSelection) which.lastSelection = {start: v[0].length, end: v[0].length};
			}
			if (which.showsList) which.displayList();
			if (which.lastSelection) {
				if (is_webkit) {
					// WebKit (Safari, Chrome) has problems selecting inside focus()
					// See http://code.google.com/p/chromium/issues/detail?id=32865#c6
					window.setTimeout (
						 function () { which.setSelection (which.lastSelection.start, which.lastSelection.end); }
						,1
					);
				} else {
					which.setSelection (which.lastSelection.start, which.lastSelection.end);
				}
			}
		}
	}
 
	function showDab (which) {
		if (!which.is_active) {
			makeActive(which);
		} else {
			which.showSuggestions (which.dab, false, null, null); // do autocompletion, no key, no engine selector
			which.dab = null;
		}
	}
 
	CategoryEditor.prototype = {
 
		initialize : function (line, span, after, key, is_hidden) {
			// If a span is given, 'after' is the category title, otherwise it may be an element after which to
			// insert the new span. 'key' is likewise overloaded; if a span is given, it is the category key (if
			// known), otherwise it is a boolean indicating whether a bar shall be prepended.
			if (!span) {
				this.isAddCategory = true;
				// Create add span and append to catLinks
				this.originalCategory = "";
				this.originalKey = null;
				this.originalExists   = false;
				if (!newDOM) {
					span = make ('span');
					span.className = 'noprint';
					if (key) {
						span.appendChild (make (' | ', true));
						if (after) {
							after.parentNode.insertBefore (span, after.nextSibling);
							after = after.nextSibling;
						} else {
							line.appendChild (span);
						}
					} else if (line.firstChild) {
						span.appendChild (make (' ', true));
						line.appendChild (span);
					}
				}
				this.linkSpan = make ('span');
				this.linkSpan.className = 'noprint nopopups hotcatlink';
				var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
				lk.appendChild (make (HotCat.links.add, true)); lk.title = HotCat.tooltips.add;
				this.linkSpan.appendChild (lk);
				span = make (newDOM ? 'li' : 'span');
				span.className = 'noprint';
				if (is_rtl) span.dir = 'rtl';
				span.appendChild (this.linkSpan);
				if (after)
					after.parentNode.insertBefore (span, after.nextSibling);
				else
					line.appendChild (span);
				this.normalLinks = null;
				this.undelLink = null;
				this.catLink = null;
			} else {
				if (is_rtl) span.dir = 'rtl';
				this.isAddCategory = false;
				this.catLink = span.firstChild;
				this.originalCategory = after;
				this.originalKey = (key && key.length > 1) ? key.substr(1) : null; // > 1 because it includes the leading bar
				this.originalExists   = !hasClass (this.catLink, 'new');
				// Create change and del links
				this.makeLinkSpan ();
				if (!this.originalExists && this.upDownLinks) this.upDownLinks.style.display = 'none';
				span.appendChild (this.linkSpan);
			}
			this.originalHidden     = is_hidden;
			this.line               = line;
			this.engine             = HotCat.suggestions;
			this.span               = span;
			this.currentCategory    = this.originalCategory;
			this.currentExists      = this.originalExists;
			this.currentHidden      = this.originalHidden;
			this.currentKey         = this.originalKey;
			this.state              = CategoryEditor.UNCHANGED;
			this.lastSavedState     = CategoryEditor.UNCHANGED;
			this.lastSavedCategory  = this.originalCategory;
			this.lastSavedKey       = this.originalKey;
			this.lastSavedExists    = this.originalExists;
			this.lastSavedHidden    = this.originalHidden;
			if (this.catLink && this.currentKey) {
				this.catLink.title = this.currentKey;
			}
			editors[editors.length] = this;
		},
 
		makeLinkSpan : function () {
			this.normalLinks = make ('span');
			var lk = null;
			if (this.originalCategory && this.originalCategory.length > 0) {
				lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.remove, this);
				lk.appendChild (make (HotCat.links.remove, true)); lk.title = HotCat.tooltips.remove;
				this.normalLinks.appendChild (make (' ', true));
				this.normalLinks.appendChild (lk);
			}
			if (!HotCat.template_categories[this.originalCategory]) {
				lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.open, this);
				lk.appendChild (make (HotCat.links.change, true)); lk.title = HotCat.tooltips.change;
				this.normalLinks.appendChild (make (' ', true));
				this.normalLinks.appendChild (lk);
				if (!noSuggestions && HotCat.use_up_down) {
					this.upDownLinks = make ('span');
					lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.down, this);
					lk.appendChild (make (HotCat.links.down, true)); lk.title = HotCat.tooltips.down;
					this.upDownLinks.appendChild (make (' ', true));
					this.upDownLinks.appendChild (lk);
					lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.up, this);
					lk.appendChild (make (HotCat.links.up, true)); lk.title = HotCat.tooltips.up;
					this.upDownLinks.appendChild (make (' ', true));
					this.upDownLinks.appendChild (lk);
					this.normalLinks.appendChild (this.upDownLinks);
				}
			}
			this.linkSpan = make ('span');
			this.linkSpan.className = 'noprint nopopups hotcatlink';
			this.linkSpan.appendChild (this.normalLinks);
			this.undelLink = make ('span');
			this.undelLink.className = 'nopopups hotcatlink';
			this.undelLink.style.display = 'none';
			lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.restore, this);
			lk.appendChild (make (HotCat.links.restore, true)); lk.title = HotCat.tooltips.restore;
			this.undelLink.appendChild (make (' ', true));
			this.undelLink.appendChild (lk);
			this.linkSpan.appendChild (this.undelLink);
		},
 
		invokeSuggestions : function (dont_autocomplete) {
			if (this.engine && suggestionConfigs[this.engine] && suggestionConfigs[this.engine].temp && !dont_autocomplete) {
				this.engine = HotCat.suggestions; // Reset to a search upon input
			}
			this.state = CategoryEditor.CHANGE_PENDING;
			var self = this;
			window.setTimeout (function () {self.textchange (dont_autocomplete);}, HotCat.suggest_delay);
		},
 
		makeForm : function () {
			var form = make ('form');
			form.method = 'POST'; form.onsubmit = bind (this.accept, this);
			this.form = form;
			var self = this;
			var text = make ('input'); text.type = 'text'; text.size = HotCat.editbox_width;
			if (!noSuggestions) {
				// Be careful here to handle IME input. This is browser/OS/IME dependent, but basically there are two mechanisms:
				// - Modern (DOM Level 3) browsers use compositionstart/compositionend events to signal composition; if the
				//   composition is not canceled, there'll be a textInput event following. During a composition key events are
				//   either all suppressed (FF/Gecko), or otherwise have keyDown === IME for all keys (Webkit).
				//   - Webkit sends a textInput followed by keyDown === IME and a keyUp with the key that ended composition.
				//   - Gecko doesn't send textInput but just a keyUp with the key that ended composition, without sending keyDown
				//     first. Gecko doesn't send any keydown while IME is active.
				// - Older browsers signal composition by keyDown === IME for the first and subsequent keys for a composition. The
				//   first keyDown !== IME is certainly after the end of the composition. Typically, composition end can also be
				//   detected by a keyDown IME with a keyUp of space, tab, escape, or return. (Example: IE8)
				text.onkeyup =
					function (evt) {
						evt = evt || window.event || window.Event; // W3C, IE, Netscape
						var key = evt.keyCode || 0;
						if (self.ime && self.lastKey === IME && !self.usesComposition && (key === TAB || key === RET || key == ESC || key === SPACE)) self.ime = false;
						if (self.ime) return true;
						if (key === UP || key === DOWN || key === PGUP || key === PGDOWN) {
							// In case a browser doesn't generate keypress events for arrow keys...
							if (self.keyCount === 0) return self.processKey (evt);
						} else {
							if (key === ESC && self.lastKey !== IME) {
								if (!self.resetKeySelection ()) {
									// No undo of key selection: treat ESC as "cancel".
									self.cancel ();
									return;
								}
							}
							// Also do this for ESC as a workaround for Firefox bug 524360
							// https://bugzilla.mozilla.org/show_bug.cgi?id=524360
							self.invokeSuggestions (key === BS || key === DEL || key === ESC);
						}
						return true;
					};
				text.onkeydown =
					function (evt) {
						evt = evt || window.event || window.Event; // W3C, IE, Netscape
						var key = evt.keyCode || 0;
						self.lastKey = key;
						self.keyCount = 0;
						// DOM Level < 3 IME input	
						if (!self.ime && key === IME && !self.usesComposition) {
							// self.usesComposition catches browsers that may emit spurious keydown IME after a composition has ended
							self.ime = true;
						} else if (self.ime && key !== IME && !(key >= 16 && key <= 20 || key >= 91 && key <= 93 || key === 144)) {
							// Ignore control keys: ctrl, shift, alt, alt gr, caps lock, windows/apple cmd keys, num lock. Only the windows keys
							// terminate IME (apple cmd doesn't), but they also cause a blur, so it's OK to ignore them here.
							// Note: Safari 4 (530.17) propagates ESC out of an IME composition (observed at least on Win XP).
							self.ime = false;
						}
						if (self.ime) return true;
						// Handle return explicitly, to override the default form submission to be able to check for ctrl
						if (key === RET) return self.accept (evt);
						// Inhibit default behavior of ESC (revert to last real input in FF: we do that ourselves)
						return (key === ESC) ? evtKill(evt) : true;
					};
				// And handle continued pressing of arrow keys
				text.onkeypress = function (evt) {self.keyCount++; return self.processKey (evt);};
				addEvent (text, 'focus', function () { makeActive(self); });
				// On IE, blur events are asynchronous, and may thus arrive after the element has lost the focus. Since IE
				// can get the selection only while the element is active (has the focus), we may not always get the selection.
				// Therefore, use an IE-specific synchronous event on IE...
				// Don't test for text.selectionStart being defined; FF3.6.4 raises an exception when trying to access that
				// property while the element is not being displayed.
				addEvent (text
					, (typeof text.onbeforedeactivate != 'undefined' && text.createTextRange) ? 'beforedeactivate' : 'blur'
					, bind (this.saveView, this)
				);
				// DOM Level 3 IME handling
				try {
					// Setting lastKey = IME provides a fake keyDown for Gecko's single keyUp after a cmposition. If we didn't do this,
					// cancelling a composition via ESC would also cancel and close the whole category input editor.
					addEvent(text, 'compositionstart', function (evt) { self.lastKey = IME; self.usesComposition = true; self.ime = true; });
					addEvent(text, 'compositionend', function (evt) { self.lastKey = IME; self.usesComposition = true; self.ime = false; });
					addEvent(text, 'textInput', function (evt) { self.ime = false; self.invokeSuggestions(false); });
				} catch (any) {
					// Just in case some browsers might produce exceptions with these DOM Level 3 events
				}
				addEvent(text, 'blur', function (evt) { self.usesComposition = false; self.ime = false; });
			}
			this.text = text;
 
			this.icon = make ('img');
 
			var list = null;
			if (!noSuggestions) {
				list = make ('select');
				list.onclick    = function (e) { if (self.highlightSuggestion(0)) self.textchange (false, true); };
				list.ondblclick = function (e) { if (self.highlightSuggestion(0)) self.accept (e); };
				list.onchange = function (e) { self.highlightSuggestion(0); self.text.focus(); };
				list.onkeyup =
					function (evt) {
						evt = evt || window.event || window.Event; // W3C, IE, Netscape
						if (evt.keyCode === ESC) {
							self.resetKeySelection ();
							self.text.focus();
							window.setTimeout (function () {self.textchange (true);}, HotCat.suggest_delay);
						} else if (evt.keyCode === RET) {
							self.accept (evt);
						}
					};
				if (!HotCat.fixed_search) {
					var engineSelector = make ('select');
					for (var key in suggestionConfigs) {
						if (suggestionConfigs[key].show) {
							var opt = make ('option');
							opt.value = key;
							if (key == this.engine) opt.selected = true;
							opt.appendChild (make (suggestionConfigs[key].name, true));
							engineSelector.appendChild (opt);
						}
					}
					engineSelector.onchange =
						function () {
							self.engine = self.engineSelector.options[self.engineSelector.selectedIndex].value;
							self.text.focus();
							self.textchange (true, true); // Don't autocomplete, force re-display of list
						};
					this.engineSelector = engineSelector;
				}
			}
			this.list = list;
 
			function button_label (id, defaultText) {
				var label = null;
				if (   onUpload
					&& typeof (UFUI) != 'undefined'
					&& typeof (UIElements) != 'undefined'
					&& typeof (UFUI.getLabel) == 'function')
				{
					try {
						label = UFUI.getLabel (id, true);
						// Extract the plain text. IE doesn't know that Node.TEXT_NODE === 3
						while (label && label.nodeType != 3) label = label.firstChild;
					} catch (ex) {
						label = null;
					}
				}
				if (!label || !label.data) return defaultText;
				return label.data;
			}
 
			// Do not use type 'submit'; we cannot detect modifier keys if we do
			var OK = make ('input'); OK.type = 'button';
			OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok);
			OK.onclick = bind (this.accept, this);
			this.ok = OK;
 
			var cancel = make ('input'); cancel.type = 'button';
			cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
			cancel.onclick = bind (this.cancel, this);
			this.cancelButton = cancel;
 
			var span = make ('span');
			span.className = 'hotcatinput';
			span.style.position = 'relative';
			// FF3.6: add the input field first, then the two absolutely positioned elements. Otherwise, FF3.6 may leave the
			// suggestions and the selector at the right edge of the screen if display of the input field causes a re-layout
			// moving the form to the front of the next line.
			span.appendChild (text);
 
			// IE8/IE9: put some text into this span (a0 is nbsp) and make sure it always stays on the
			// same line as the input field, otherwise, IE8/9 miscalculates the height of the span and
			// then the engine selector may overlap the input field.
			span.appendChild (make ('\xa0', true));
			span.style.whiteSpace = 'nowrap';
 
			if (list) span.appendChild (list);
			if (this.engineSelector) span.appendChild (this.engineSelector);
			if (!noSuggestions) span.appendChild (this.icon);
			span.appendChild (OK);
			span.appendChild (cancel);
			form.appendChild(span);
			form.style.display = 'none';
			this.span.appendChild (form);
		},
 
		display : function (evt) {
			if (this.isAddCategory && !onUpload) {
				var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
			}
			if (!commitButton && !onUpload) {
				for (var i = 0; i < editors.length; i++) {
					if (editors[i].state != CategoryEditor.UNCHANGED) {
						setMultiInput();
						break;
					}
				}
			}
			if (!this.form) {
				this.makeForm ();
			}
			if (this.list) this.list.style.display = 'none';
			if (this.engineSelector) this.engineSelector.style.display = 'none';
			this.currentCategory = this.lastSavedCategory;
			this.currentExists   = this.lastSavedExists;
			this.currentHidden   = this.lastSavedHidden;
			this.currentKey      = this.lastSavedKey;
			this.icon.src = armorUri(this.currentExists ? HotCat.existsYes : HotCat.existsNo);
			this.text.value = this.currentCategory + (this.currentKey !== null ? '|' + this.currentKey : "");
			this.originalState = this.state;
			this.lastInput     = this.currentCategory;
			this.inputExists   = this.currentExists;
			this.state         = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
			this.lastSelection = {start: this.currentCategory.length, end: this.currentCategory.length};
			this.showsList = false;
			// Display the form
			if (this.catLink) this.catLink.style.display = 'none';
			this.linkSpan.style.display = 'none';
			this.form.style.display = 'inline';
			this.ok.disabled = false;
			// Kill the event before focussing, otherwise IE will kill the onfocus event!
			var result = evtKill (evt);
			this.text.focus();
			this.text.readOnly = false;
			checkMultiInput ();
			return result;
		},
 
		show : function (evt, engine, readOnly) {
			var result = this.display (evt);
			var v = this.lastSavedCategory;
			if (v.length === 0) return result;
			this.text.readOnly = !!readOnly;
			this.engine = engine;
			this.textchange (false, true); // do autocompletion, force display of suggestions
			forceRedraw ();
			return result;
		},
 
		open : function (evt) {
			return this.show (evt, (this.engine && suggestionConfigs[this.engine].temp) ? HotCat.suggestions : this.engine);
		},
 
		down : function (evt) {
			return this.show (evt, 'subcat', true);
		},
 
		up : function (evt) {
			return this.show (evt, 'parentcat');
		},
 
		cancel : function () {
			if (this.isAddCategory && !onUpload) {
				this.removeEditor(); // We added a new adder when opening
				return;
			}
			// Close, re-display link
			this.inactivate();
			this.form.style.display = 'none';
			if (this.catLink) this.catLink.style.display = "";
			this.linkSpan.style.display = "";
			this.state = this.originalState;
			this.currentCategory = this.lastSavedCategory;
			this.currentKey      = this.lastSavedKey;
			this.currentExists   = this.lastSavedExists;
			this.currentHidden   = this.lastSavedHidden;
			if (this.catLink) {
				if (this.currentKey && this.currentKey.length > 0) {
					this.catLink.title = this.currentKey;
				} else {
					this.catLink.title = "";
				}
			}
			if (this.state == CategoryEditor.UNCHANGED) {
				if (this.catLink) this.catLink.style.backgroundColor = 'transparent';
			} else {
				if (!onUpload) {
					try {
						this.catLink.style.backgroundColor = HotCat.bg_changed;
					} catch (ex) {}
				}
			}
			checkMultiInput ();
			forceRedraw ();
		},
 
		removeEditor : function () {
			if (!newDOM) {
				var next = this.span.nextSibling;
				if (next) next.parentNode.removeChild (next);
			}
			this.span.parentNode.removeChild (this.span);
			for (var i = 0; i < editors.length; i++) {
				if (editors[i] == this) {
					editors.splice (i, 1);
					break;
				}
			}
			checkMultiInput ();
			var self = this;
			window.setTimeout (function () {delete self;}, 10);
		},
 
		rollback : function (evt) {
			this.undoLink.parentNode.removeChild (this.undoLink);
			this.undoLink = null;
			this.currentCategory = this.originalCategory;
			this.currentKey = this.originalKey;
			this.currentExists = this.originalExists;
			this.currentHidden = this.originalHidden;
			this.lastSavedCategory = this.originalCategory;
			this.lastSavedKey = this.originalKey;
			this.lastSavedExists = this.originalExists;
			this.lastSavedHidden = this.originalHidden;
			this.state = CategoryEditor.UNCHANGED;
			if (!this.currentCategory || this.currentCategory.length === 0) {
				// It was a newly added category. Remove the whole editor.
				this.removeEditor();
			} else {
				// Redisplay the link...
				this.catLink.removeChild (this.catLink.firstChild);
				this.catLink.appendChild (make (this.currentCategory, true));
				this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
				this.catLink.title = this.currentKey || "";
				this.catLink.className = this.currentExists ? "" : 'new';
				this.catLink.style.backgroundColor = 'transparent';
				if (this.upDownLinks) this.upDownLinks.style.display = this.currentExists ? "" : 'none';
				checkMultiInput ();
			}
			return evtKill (evt);
		},
 
		inactivate : function () {
			if (this.list) this.list.style.display = 'none';
			if (this.engineSelector) this.engineSelector.style.display = 'none';
			this.is_active = false;
		},
 
		acceptCheck : function (dontCheck) {
			this.sanitizeInput ();
			var value = this.text.value.split('|');
			var key   = null;
			if (value.length > 1) key = value[1];
			var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
			if (HotCat.capitalizePageNames) v = capitalize (v);
			this.lastInput = v;
			v = replaceShortcuts(v, HotCat.shortcuts);
			if (v.length === 0) {
				this.cancel ();
				return false;
			}
			if (!dontCheck
				&& (   wgNamespaceNumber === 14 && v == wgTitle
					|| HotCat.blacklist && HotCat.blacklist.test(v))
			   )
			{
				this.cancel ();
				return false;
			}
			this.currentCategory = v;
			this.currentKey = key;
			this.currentExists = this.inputExists;
			return true;
		},
 
		accept : function (evt) {
			this.noCommit = (evtKeys (evt) & 1) !== 0;
			var result = evtKill (evt);
			if (this.acceptCheck ()) {
				var toResolve = [this];
				var original  = this.currentCategory;
				resolveMulti (
					 toResolve
					,function (resolved) {
						if (resolved[0].dab) {
							showDab (resolved[0]);
						} else {
							if (resolved[0].acceptCheck(true)) {
								resolved[0].commit (
									(resolved[0].currentCategory != original)
										? HotCat.messages.cat_resolved.replace (/\$1/g, original)
										: null
								);
							}
						}
					 }
				);
			}
			return result;
		},
 
		close : function () {
			if (!this.catLink) {
				// Create a catLink
				this.catLink = make ('a');
				this.catLink.appendChild (make ('foo', true));
				this.catLink.style.display = 'none';
				this.span.insertBefore (this.catLink, this.span.firstChild.nextSibling);
			}
			this.catLink.removeChild (this.catLink.firstChild);
			this.catLink.appendChild (make (this.currentCategory, true));
			this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
			this.catLink.className = this.currentExists ? "" : 'new';
			this.lastSavedCategory = this.currentCategory;
			this.lastSavedKey      = this.currentKey;
			this.lastSavedExists   = this.currentExists;
			this.lastSavedHidden   = this.currentHidden;
			// Close form and redisplay category
			this.inactivate();
			this.form.style.display = 'none';
			this.catLink.title = this.currentKey || "";
			this.catLink.style.display = "";
			if (this.isAddCategory) {
				if (onUpload) {
					var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
				}
				this.isAddCategory = false;
				this.linkSpan.parentNode.removeChild (this.linkSpan);
				this.makeLinkSpan ();
				this.span.appendChild (this.linkSpan);
			}
			if (!this.undoLink) {
				// Append an undo link.
				var span = make ('span');
				var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = bind (this.rollback, this);
				lk.appendChild (make (HotCat.links.undo, true)); lk.title = HotCat.tooltips.undo;
				span.appendChild (make (' ', true));
				span.appendChild (lk);
				this.normalLinks.appendChild (span);
				this.undoLink = span;
				if (!onUpload) {
					try {
						this.catLink.style.backgroundColor = HotCat.bg_changed;
					} catch (ex) {}
				}
			}
			if (this.upDownLinks) this.upDownLinks.style.display = this.lastSavedExists ? "" : 'none';
			this.linkSpan.style.display = "";
			this.state = CategoryEditor.CHANGED;
			checkMultiInput ();
			forceRedraw ();
		},
 
		commit : function (comment) {
			// Check again to catch problem cases after redirect resolution
			if (   (   this.currentCategory == this.originalCategory
					&& (this.currentKey == this.originalKey
						|| this.currentKey === null && this.originalKey.length === 0
					   )
				   )
				|| wgNamespaceNumber == 14 && this.currentCategory == wgTitle
				|| HotCat.blacklist && HotCat.blacklist.test (this.currentCategory)
			   )
			{
				this.cancel ();
				return;
			}
			if (commitButton || onUpload) {
				this.close ();
			} else {
				this.close ();
				var self = this;
				initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {alert (msg);});
			}
		},
 
		remove : function (evt) {
			this.doRemove (evtKeys (evt) & 1);
			return evtKill (evt);
		},
 
		doRemove : function (noCommit) {
			if (this.isAddCategory) { // Empty input on adding a new category
				this.cancel ();
				return;
			}
			if (!commitButton && !onUpload) {
				for (var i = 0; i < editors.length; i++) {
					if (editors[i].state != CategoryEditor.UNCHANGED) {
						setMultiInput();
						break;
					}
				}
			}
			if (commitButton) {
				this.catLink.title = "";
				this.catLink.style.cssText += '; text-decoration : line-through !important;';
				try {
					this.catLink.style.backgroundColor = HotCat.bg_changed;
				} catch (ex) {}
				this.originalState = this.state;
				this.state = CategoryEditor.DELETED;
				this.normalLinks.style.display = 'none';
				this.undelLink.style.display = "";
				checkMultiInput ();
			} else {
				if (onUpload) {
					// Remove this editor completely
					this.removeEditor ();
				} else {
					this.originalState = this.state;
					this.state = CategoryEditor.DELETED;
					this.noCommit = noCommit || HotCat.del_needs_diff;
					var self = this;
					initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {self.state = self.originalState; alert (msg);});
				}
			}
		},
 
		restore : function (evt) {
			// Can occur only if we do have a commit button and are not on the upload form
			this.catLink.title = this.currentKey || "";
			this.catLink.style.textDecoration = "";
			this.state = this.originalState;
			if (this.state == CategoryEditor.UNCHANGED) {
				this.catLink.style.backgroundColor = 'transparent';
			} else {
				try {
					this.catLink.style.backgroundColor = HotCat.bg_changed;
				} catch (ex) {}
			}
			this.normalLinks.style.display = "";
			this.undelLink.style.display = 'none';
			checkMultiInput ();
			return evtKill (evt);
		},
 
		// Internal operations
 
		selectEngine : function (engineName) {
			if (!this.engineSelector) return;
			for (var i = 0; i < this.engineSelector.options.length; i++) {
				this.engineSelector.options[i].selected = this.engineSelector.options[i].value == engineName;
			}
		},
 
		sanitizeInput : function () {
			var v = this.text.value || "";
			v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
			var re = new RegExp ('^(' + HotCat.category_regexp + '):');
			if (re.test (v)) {
				v = v.substring (v.indexOf (':') + 1).replace(/^(\s|_)+/, "");
			}
			if (HotCat.capitalizePageNames) v = capitalize (v);
			// Only update the input field if there is a difference. IE8 appears to reset the selection
			// and place the cursor at the front upon reset, which makes our autocompletetion become a
			// nuisance. FF and IE6 don't seem to have this problem.
			if (this.text.value !== null && this.text.value != v)
				this.text.value = v;
		},
 
		makeCall : function (url, callbackObj, engine, queryKey, cleanKey) {
			var cb = callbackObj;
			var e  = engine;
			var v  = queryKey;
			var z  = cleanKey;
			var thisObj = this;
 
			function done () {
				cb.callsMade++;
				if (cb.callsMade === cb.nofCalls) {
					if (cb.exists) cb.allTitles.exists = true;
					if (cb.normalized) cb.allTitles.normalized = cb.normalized;
					if (!cb.dontCache && !suggestionConfigs[cb.engineName].cache[z]) {
						suggestionConfigs[cb.engineName].cache[z] = cb.allTitles;
					}
					thisObj.text.readOnly = false;
					if (!cb.cancelled) thisObj.showSuggestions (cb.allTitles, cb.noCompletion, v, cb.engineName);
					if (cb === thisObj.callbackObj) thisObj.callbackObj = null;
					delete cb;
				}
			}
 
			getJSON ({
			  uri : url
			 ,success : function (json) {
				var titles = e.handler (json, z);
				if (titles && titles.length > 0) {
					if (cb.allTitles === null) {
						cb.allTitles = titles;
					} else {
						cb.allTitles = cb.allTitles.concat (titles);
					}
					if (titles.exists) cb.exists = true;
					if (titles.normalized) cb.normalized = titles.normalized;
				}
				done();
			  }
			 ,error : function (req) {if (!req) noSuggestions = true; cb.dontCache = true; done(); }
			});
		},
 
		callbackObj : null,
 
		textchange : function (dont_autocomplete, force) {
			// Hide all other lists
			makeActive (this);
			// Get input value, omit sort key, if any
			this.sanitizeInput ();
			var v = this.text.value;
			// Disregard anything after a pipe.
			var pipe = v.indexOf ('|');
			if (pipe >= 0) {
				this.currentKey = v.substring (pipe+1);
				v = v.substring (0, pipe);
			} else {
				this.currentKey = null;
			}
			if (this.lastInput == v && !force) return; // No change
			if (this.lastInput != v) checkMultiInput ();
			this.lastInput = v;
			this.lastRealInput = v;
 
			// Mark blacklisted inputs.
			this.ok.disabled = v.length > 0 && HotCat.blacklist && HotCat.blacklist.test (v);
 
			if (noSuggestions) {
				// No Ajax: just make sure the list is hidden
				if (this.list) this.list.style.display = 'none';
				if (this.engineSelector) this.engineSelector.style.display = 'none';
				if (this.icon) this.icon.style.display = 'none';
				return;
			}
 
			if (v.length === 0) { this.showSuggestions([]); return; }
			var cleanKey = v.replace(/[\u200E\u200F\u202A-\u202E]/g, "").replace(wikiTextBlankRE, ' ');
			cleanKey = replaceShortcuts(cleanKey, HotCat.shortcuts);
			cleanKey = cleanKey.replace(/^\s+|\s+$/g, '');
			if (cleanKey.length === 0) { this.showSuggestions([]); return; }
 
			if (this.callbackObj) this.callbackObj.cancelled = true;
			var engineName  = suggestionConfigs[this.engine] ? this.engine : 'combined';
 
			dont_autocomplete = dont_autocomplete || suggestionConfigs[engineName].noCompletion;
			if (suggestionConfigs[engineName].cache[cleanKey]) {
				this.showSuggestions (suggestionConfigs[engineName].cache[cleanKey], dont_autocomplete, v, engineName);
				return;
			}
 
			var engines = suggestionConfigs[engineName].engines;
			this.callbackObj =
				{allTitles: null, callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engineName};
			this.makeCalls (engines, this.callbackObj, v, cleanKey);
		},
 
		makeCalls : function (engines, cb, v, cleanKey) {
			for (var j = 0; j < engines.length; j++) {
				var engine = suggestionEngines[engines[j]];
				var url = wgServer + wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (cleanKey));
				this.makeCall (url, cb, engine, v, cleanKey);
			}
		},
 
		showSuggestions : function (titles, dontAutocomplete, queryKey, engineName) {
			this.text.readOnly = false;
			this.dab = null;
			this.showsList = false;
			if (!this.list) return;
			if (noSuggestions) {
				if (this.list) this.list.style.display = 'none';
				if (this.engineSelector) this.engineSelector.style.display = 'none';
				if (this.icon) this.icon.style.display = 'none';
				this.inputExists = true; // Default...
				return;
			}
			this.engineName = engineName;
			if (engineName) {
				if (!this.engineSelector) this.engineName = null;
			} else {
				if (this.engineSelector) this.engineSelector.style.display = 'none';
			}
			if (queryKey) {
				if (this.lastInput.indexOf (queryKey) !== 0) return;
				if (this.lastQuery && this.lastInput.indexOf (this.lastQuery) === 0 && this.lastQuery.length > queryKey.length)
					return;
			}
			this.lastQuery = queryKey;
 
			// Get current input text
			var v = this.text.value.split('|');
			var key = v.length > 1 ? '|' + v[1] : "";
			v = (HotCat.capitalizePageNames ? capitalize (v[0]) : v[0]);
			var vNormalized = v;
			var knownToExist = titles && titles.exists;
			var i;
			if (titles) {
				if (titles.normalized && v.indexOf(queryKey) === 0) {
					// We got back a different normalization than what is in the input field
					vNormalized = titles.normalized + v.substring(queryKey.length);
				}
				var vLow = vNormalized.toLowerCase ();
				// Strip blacklisted categories
				if (HotCat.blacklist) {
					for (i = 0; i < titles.length; i++) {
						if (HotCat.blacklist.test (titles[i])) {
							titles.splice(i, 1);
							i--;
						}
					}
				}
				titles.sort (
					function (a, b) {
						if (a == b) return 0;
						if (a.indexOf (b) === 0) return 1; // a begins with b: a > b
						if (b.indexOf (a) === 0) return -1; // b begins with a: a < b
						// Opensearch may return stuff not beginning with the search prefix!
						var prefixMatchA = (a.indexOf (vNormalized) === 0 ? 1 : 0);
						var prefixMatchB = (b.indexOf (vNormalized) === 0 ? 1 : 0);
						if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
						// Case-insensitive prefix match!
						var aLow = a.toLowerCase(), bLow = b.toLowerCase();
						prefixMatchA = (aLow.indexOf (vLow) === 0 ? 1 : 0);
						prefixMatchB = (bLow.indexOf (vLow) === 0 ? 1 : 0);
						if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
						if (a < b) return -1;
						if (b < a) return 1;
						return 0;
					}
				);
				// Remove duplicates and self-references
				for (i = 0; i < titles.length; i++) {
					if (   i+1 < titles.length && titles[i] == titles[i+1]
						|| wgNamespaceNumber == 14 && titles[i] == wgTitle
					   )
					{
						titles.splice (i, 1);
						i--;
					}
				}
			}
			if (!titles || titles.length === 0) {
				if (this.list) this.list.style.display = 'none';
				if (this.engineSelector) this.engineSelector.style.display = 'none';
				if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
					if (this.icon) this.icon.src = armorUri(HotCat.existsNo);
					this.inputExists = false;
				}
				return;
			}
 
			var firstTitle = titles[0];
			var completed = this.autoComplete (firstTitle, v, vNormalized, key, dontAutocomplete);
			var existing = completed || knownToExist || firstTitle == replaceShortcuts(v, HotCat.shortcuts);
			if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
				this.icon.src = armorUri(existing ? HotCat.existsYes : HotCat.existsNo);
				this.inputExists = existing;
			}
			if (completed) {
				this.lastInput = firstTitle;
				if (titles.length === 1) {
					this.list.style.display = 'none';
					if (this.engineSelector) this.engineSelector.style.display = 'none';
					return;
				}
			}
			// (Re-)fill the list
			while (this.list.firstChild) this.list.removeChild (this.list.firstChild);
			for (i = 0 ; i < titles.length ; i++) {
				var opt = make ('option') ;
				opt.appendChild (make (titles[i], true));
				opt.selected = completed && (i === 0);
				this.list.appendChild (opt);
			}
			this.displayList();
		},
 
		displayList : function () {
			this.showsList = true;
			if (!this.is_active) {
				this.list.style.display = 'none';
				if (this.engineSelector) this.engineSelector.style.display = 'none';
				return;
			}
			var nofItems = (this.list.options.length > HotCat.list_size ? HotCat.list_size : this.list.options.length);
			if (nofItems <= 1) nofItems = 2;
			this.list.size = nofItems;
			this.list.style.align    = is_rtl ? 'right' : 'left';
			this.list.style.zIndex   = 5;
			this.list.style.position = 'absolute';
			// Compute initial list position. First the height.
			var anchor = is_rtl ? 'right' : 'left';
			var listh = 0;
			if (this.list.style.display == 'none') {
				// Off-screen display to get the height
				this.list.style.top = this.text.offsetTop + 'px';
				this.list.style[anchor] = '-10000px';
				this.list.style.display = "";
				listh = this.list.offsetHeight;
				this.list.style.display = 'none';
			} else {
				listh = this.list.offsetHeight;
			}
			// Approximate calculation of maximum list size
			var maxListHeight = listh;
			if (nofItems < HotCat.list_size) maxListHeight = (listh / nofItems) * HotCat.list_size;
 
			function viewport (what) {
				if (is_webkit && !document.evaluate)
					return window['inner' + what]; // Safari < 3.0
				var s = 'client' + what;
				if (window.opera) return document.body[s];
				return (document.documentElement ? document.documentElement[s] : 0)
					|| document.body[s] || 0;
			}
			function scroll_offset (what) {
				var s = 'scroll' + what;
				var result = (document.documentElement ? document.documentElement[s] : 0)
							|| document.body[s] || 0;
				if (is_rtl && what == 'Left') {
					// RTL inconsistencies.
					// FF: 0 at the far right, then increasingly negative values.
					// IE >= 8: 0 at the far right, then increasingly positive values.
					// Webkit: scrollWidth - clientWidth at the far right, then down to zero.
					// IE 7: like webkit; IE6: disabled in RTL anyway since too many problems.
					// Opera: don't know...
					if (result < 0) result = - result;
					if (!is_webkit && !is_ie_lt8) {
						result = scroll_offset('Width') - viewport('Width') - result;
					}
					// Now all have webkit behavior, i.e. zero if at the leftmost edge.
				}
				return result;
			}
			function position (node) {
				// Stripped-down simplified position function. It's good enough for our purposes.
				if (node.getBoundingClientRect) {
					var box    = node.getBoundingClientRect ();
					return { x : Math.round (box.left + scroll_offset ('Left'))
							,y : Math.round (box.top + scroll_offset ('Top'))
						   };
				}
				var t = 0, l = 0;
				do {
					t = t + (node.offsetTop  || 0);
					l = l + (node.offsetLeft || 0);
					node = node.offsetParent;
				} while (node);
				return {x : l, y : t};
			}
 
			var textPos = position (this.text);
			var nl = 0;
			var nt = 0;
			var offset = 0;
			// Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
			var textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
			if (this.engineName) {
				this.engineSelector.style.zIndex = 5;
				this.engineSelector.style.position = 'absolute';
				this.engineSelector.style.width = textBoxWidth + 'px';
				// Figure out the height of this selector: display it off-screen, then hide it again.
				if (this.engineSelector.style.display == 'none') {
					this.engineSelector.style[anchor] = '-10000px';
					this.engineSelector.style.top = '0px';
					this.engineSelector.style.display = "";
					offset = this.engineSelector.offsetHeight;
					this.engineSelector.style.display = 'none';
				} else {
					offset = this.engineSelector.offsetHeight;
				}
				this.engineSelector.style[anchor]  = nl + 'px';
			}
			if (textPos.y < maxListHeight + offset + 1) {
				// The list might extend beyond the upper border of the page. Let's avoid that by placing it
				// below the input text field.
				nt = this.text.offsetHeight + offset + 1;
				if (this.engineName) this.engineSelector.style.top = this.text.offsetHeight + 'px';
			} else {
				nt = - listh - offset - 1;
				if (this.engineName) this.engineSelector.style.top = - (offset + 1) + 'px';
			}
			this.list.style.top = nt + 'px';
			this.list.style.width = ""; // No fixed width (yet)
			this.list.style[anchor] = nl + 'px';
			if (this.engineName) {
				this.selectEngine (this.engineName);
				this.engineSelector.style.display = "";
			}
			this.list.style.display = 'block';
			// Set the width of the list
			if (this.list.offsetWidth < textBoxWidth ) {
				this.list.style.width = textBoxWidth + 'px';
				return;
			}
			// If the list is wider than the textbox: make sure it fits horizontally into the browser window
			var scroll = scroll_offset ('Left');
			var view_w = viewport ('Width');
			var w      = this.list.offsetWidth;
			var l_pos  = position (this.list);
			var left   = l_pos.x;
			var right  = left + w;
			if (left < scroll || right > scroll + view_w) {
				if (w > view_w) {
					w = view_w;
					this.list.style.width = w + 'px';
					if (is_rtl) {
						left = right - w;
					} else {
						right = left + w;
					}
				}
				var relative_offset = 0;
				if (left < scroll) {
					relative_offset = scroll - left;
				} else if (right > scroll + view_w) {
					relative_offset = - (right - scroll - view_w);
				}
				if (is_rtl) relative_offset = - relative_offset;
				if (relative_offset !== 0) {
					this.list.style[anchor] = (nl + relative_offset) + 'px';
				}
			}
		},
 
		autoComplete : function (newVal, actVal, normalizedActVal, key, dontModify) {
			if (newVal == actVal) return true;
			if (dontModify || this.ime || !this.canSelect()) return false;
			// If we can't select properly or an IME composition is ongoing, autocompletion would be a major annoyance to the user.
			if (newVal.indexOf (actVal) !== 0) {
				// Maybe it'll work with the normalized value (NFC)?
				if (normalizedActVal && newVal.indexOf(normalizedActVal) === 0) {
					if (this.lastRealInput == actVal) this.lastRealInput = normalizedActVal;
					actVal = normalizedActVal;
				} else {
					return false;
				}
			}
			// Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
			// such that it can be easily removed by typing backspace if the suggestion is unwanted.
			this.text.focus();
			this.text.value = newVal + key;
			this.setSelection (actVal.length, newVal.length);
			return true;
		},
 
		canSelect : function () {
			return this.text.setSelectionRange
				|| this.text.createTextRange
				||    typeof (this.text.selectionStart) != 'undefined'
				   && typeof (this.text.selectionEnd) != 'undefined';
		},
 
		setSelection : function (from, to) {
			// this.text must be focused (at least on IE)
			if (!this.text.value) return;
			if (this.text.setSelectionRange) {    // e.g. khtml
				this.text.setSelectionRange (from, to);
			} else if (typeof (this.text.selectionStart) != 'undefined') {
				if (from > this.text.selectionStart) {
					this.text.selectionEnd   = to;
					this.text.selectionStart = from;
				} else {
					this.text.selectionStart = from;
					this.text.selectionEnd   = to;
				}
			} else if (this.text.createTextRange) { // IE
				var new_selection = this.text.createTextRange();
				new_selection.move ('character', from);
				new_selection.moveEnd ('character', to - from);
				new_selection.select();
			}
		},
 
		getSelection : function () {
			var from = 0, to = 0;
			// this.text must be focused (at least on IE)
			if (!this.text.value) {
				// No text.
			} else if (typeof (this.text.selectionStart) != 'undefined') {
				from = this.text.selectionStart;
				to   = this.text.selectionEnd;
			} else if (document.selection && document.selection.createRange) { // IE
				var rng = document.selection.createRange().duplicate();
				if (rng.parentElement() === this.text) {
					try {
						var textRng = this.text.createTextRange();
						textRng.move('character', 0);
						textRng.setEndPoint('EndToEnd', rng);
						// We're in a single-line input box: no need to care about IE's strange
						// handling of line ends
						to = textRng.text.length;
						textRng.setEndPoint('EndToStart', rng);
						from = textRng.text.length;
					} catch (notFocused) {
						from = this.text.value.length; to = from; // At end of text
					}
				}
			}
			return {start: from, end: to};
		},
 
		saveView : function (evt) {
			this.lastSelection = this.getSelection ();
		},
 
		processKey : function (evt) {
			var dir = 0;
			switch (this.lastKey) {
				case UP: dir = -1;
				case DOWN: if (dir === 0) dir = 1; 
				case PGUP: if (dir === 0) dir = -HotCat.list_size;
				case PGDOWN: if (dir === 0) dir = HotCat.list_size;
					if (this.list.style.display != 'none') {
						// List is visible, so there are suggestions
						this.highlightSuggestion (dir);
						// Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow
						// as "place the text cursor at the front", which we don't want here.
						return evtKill (evt);
					} else if (   this.keyCount <= 1
							   && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
							  )
					{
						// If no suggestions displayed, get them, unless we're already getting them.
						this.textchange ();
					}
					break;
				case ESC: // Inhibit default behavior (revert to last real input in FF: we do that ourselves)
					return evtKill (evt);
			}
			return true;
		},
 
		highlightSuggestion : function (dir) {
			if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
			var curr = this.list.selectedIndex;
			var tgt  = -1;
			if (dir === 0) {
				if (curr < 0 || curr >= this.list.options.length) return false;
				tgt = curr;
			} else {
				tgt = curr < 0 ? 0 : curr + dir;
				tgt = tgt < 0 ? 0 : tgt;
				if (tgt >= this.list.options.length) tgt = this.list.options.length - 1;
			}
			if (tgt != curr || dir === 0) {
				if (curr >= 0 && curr < this.list.options.length && dir !== 0) this.list.options[curr].selected = false;
				this.list.options[tgt].selected = true;
				// Get current input text
				var v = this.text.value.split('|');
				var key = v.length > 1 ? '|' + v[1] : "";
				var completed = this.autoComplete (this.list.options[tgt].text, this.lastRealInput, null, key, false);
				if (!completed || this.list.options[tgt].text == this.lastRealInput) {
					this.text.value = this.list.options[tgt].text + key;
					if (this.canSelect()) this.setSelection (this.list.options[tgt].text.length, this.list.options[tgt].text.length);
				}
				this.lastInput = this.list.options[tgt].text;
				this.inputExists = true; // Might be wrong if from a dab list...
				if (this.icon) this.icon.src = armorUri(HotCat.existsYes);
				this.state = CategoryEditor.CHANGE_PENDING;
			}
			return true;
		},
 
		resetKeySelection : function () {
			if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
			var curr = this.list.selectedIndex;
			if (curr >= 0 && curr < this.list.options.length) {
				this.list.options[curr].selected = false;
				// Get current input text
				var v = this.text.value.split('|');
				var key = v.length > 1 ? '|' + v[1] : "";
				// ESC is handled strangely by some browsers (e.g., FF); somehow it resets the input value before
				// our event handlers ever get a chance to run.
				var result = v[0] != this.lastInput;
				if (v[0] != this.lastRealInput) {
					this.text.value = this.lastRealInput + key;
					result = true;
				}
				this.lastInput = this.lastRealInput;
				return result;
			}
			return false;
		}
 
	}; // end CategoryEditor.prototype
 
	function initialize () {
		// User configurations. Do this here, called from the onload handler, so that users can
		// override it easily in their own user script files by just declaring variables. JSconfig
		// is some feature used at Wikimedia Commons.
		var config = (typeof (JSconfig) != 'undefined' && JSconfig.keys) ? JSconfig.keys : {};
		HotCat.dont_add_to_watchlist =
			(typeof (window.hotcat_dont_add_to_watchlist) != 'undefined'
				? !!window.hotcat_dont_add_to_watchlist
				: (typeof (config['HotCatDontAddToWatchlist']) != 'undefined'
					? config['HotCatDontAddToWatchlist']
					: HotCat.dont_add_to_watchlist
				  )
			);
		HotCat.no_autocommit =
			(typeof (window.hotcat_no_autocommit) != 'undefined'
				? !!window.hotcat_no_autocommit
				: (typeof (config['HotCatNoAutoCommit']) != 'undefined'
					? config['HotCatNoAutoCommit']
					: HotCat.no_autocommit
				  )
			);
		HotCat.del_needs_diff =
			(typeof (window.hotcat_del_needs_diff) != 'undefined'
				? !!window.hotcat_del_needs_diff
				: (typeof (config['HotCatDelNeedsDiff']) != 'undefined'
					? config['HotCatDelNeedsDiff']
					: HotCat.del_needs_diff
				  )
			);
		HotCat.suggest_delay = window.hotcat_suggestion_delay
							|| config['HotCatSuggestionDelay']
							|| HotCat.suggest_delay;
		HotCat.editbox_width = window.hotcat_editbox_width
							|| config['HotCatEditBoxWidth']
							|| HotCat.editbox_width;
		HotCat.suggestions   = window.hotcat_suggestions
							|| config['HotCatSuggestions']
							|| HotCat.suggestions;
		if (typeof (HotCat.suggestions) != 'string' || !suggestionConfigs[HotCat.suggestions])
			HotCat.suggestions = 'combined';
		HotCat.fixed_search  =
			(typeof (window.hotcat_suggestions_fixed) != 'undefined'
				? !!window.hotcat_suggestions_fixed
				: (typeof (config['HotCatFixedSuggestions']) != 'undefined'
					? config['HotCatFixedSuggestions']
					: HotCat.fixed_search
				  )
			);
		HotCat.single_minor  =
			(typeof (window.hotcat_single_changes_are_minor) != 'undefined'
				? !!window.hotcat_single_changes_are_minor
				: (typeof (config['HotCatMinorSingleChanges']) != 'undefined'
					? config['HotCatMinorSingleChanges']
					: HotCat.single_minor
				  )
			);
		HotCat.bg_changed    = window.hotcat_changed_background
							|| config['HotCatChangedBackground']
							|| HotCat.bg_changed;
		HotCat.use_up_down   =
			(typeof (window.hotcat_use_category_links) != 'undefined'
				? !!window.hotcat_use_category_links
				: (typeof (config['HotCatUseCategoryLinks']) != 'undefined'
					? config['HotCatUseCategoryLinks']
					: HotCat.use_up_down
				  )
			);
		HotCat.list_size     = window.hotcat_list_size
							|| config['HotCatListSize']
							|| HotCat.list_size;
		// Numeric input, make sure we have a numeric value
		HotCat.list_size = parseInt (HotCat.list_size, 10);
		if (isNaN (HotCat.list_size) || HotCat.list_size < 5) HotCat.list_size = 5;
		if (HotCat.list_size > 15) HotCat.list_size = 15;
		// Localize search engine names
		if (HotCat.engine_names) {
			for (var key in HotCat.engine_names) {
				if (suggestionConfigs[key] && HotCat.engine_names[key]) {
					suggestionConfigs[key].name = HotCat.engine_names[key];
				}
			}
		}
		// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
		is_rtl = hasClass (document.body, 'rtl');
		if (!is_rtl) {
			if (document.defaultView && document.defaultView.getComputedStyle) { // Gecko etc.
				is_rtl = document.defaultView.getComputedStyle (document.body, null).getPropertyValue ('direction');
			} else if (document.body.currentStyle) { // IE, has subtle differences to getComputedStyle
				is_rtl = document.body.currentStyle['direction'];
			} else { // Not exactly right, but best effort
				is_rtl = document.body.style['direction'];
			}
			is_rtl = (is_rtl == 'rtl');
		}
	}
 
	function can_edit () {
		var container = null;
		switch (skin) {
			case 'cologneblue':
				container = document.getElementById ('quickbar');
				// Fall through
			case 'standard':
			case 'nostalgia':
				if (!container) container = document.getElementById ('topbar');
				var lks = container.getElementsByTagName ('a');
				for (var i = 0; i < lks.length; i++) {
					if (   param ('title', lks[i].href) == wgPageName
						&& param ('action', lks[i].href) == 'edit')
						return true;
				}
				return false;
			default:
				// all modern skins:
				return document.getElementById ('ca-edit') !== null;
		}
		return false;
	}
 
	function setup_upload () {
		onUpload = true;
		// Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
		var ip = document.getElementById ('mw-htmlform-description') || document.getElementById ('wpDestFile');
		if (!ip) {
			ip = document.getElementById ('wpDestFile');
			while (ip && ip.nodeName.toLowerCase() != 'table') ip = ip.parentNode;
		}
		if (!ip) return;
		var reupload = document.getElementById ('wpForReUpload');
		var destFile = document.getElementById ('wpDestFile');
		if (   (reupload && !!reupload.value)
			|| (destFile && (destFile.disabled || destFile.readOnly)))
			return; // re-upload form...
		// Insert a table row with two fields (label and empty category bar)
		var labelCell = make ('td');
		var lineCell  = make ('td');
		// Create the category line
		catLine = make ('div');
		catLine.className = 'catlinks';
		catLine.id = 'catlinks';
		catLine.style.textAlign = is_rtl ? 'right' : 'left';
		// We'll be inside a table row. Make sure that we don't have margins or strange borders.
		catLine.style.margin = '0';
		catLine.style.border = 'none';
		lineCell.appendChild (catLine);
		// Create the label
		var label = null;
		if (   typeof (UFUI) != 'undefined'
			&& typeof (UIElements) != 'undefined'
			&& typeof (UFUI.getLabel) == 'function'
		   )
		{
			try {
				label = UFUI.getLabel ('wpCategoriesUploadLbl');
			} catch (ex) {
				label = null;
			}
		}
		if (!label) {
			labelCell.id = 'hotcatLabel';
			labelCell.appendChild (make (HotCat.categories, true));
		} else {
			labelCell.id = 'hotcatLabelTranslated';
			labelCell.appendChild (label);
		}
		labelCell.className           = 'mw-label';
		labelCell.style.textAlign     = 'right';
		labelCell.style.verticalAlign = 'middle';
		// Change the onsubmit handler
		var form = document.getElementById ('upload') || document.getElementById ('mw-upload-form');
		if (form) {
			var newRow = ip.insertRow (-1);
			newRow.appendChild (labelCell);
			newRow.appendChild (lineCell);
			form.onsubmit = (function (oldSubmit) {
				return function () {
					var do_submit = true;
					if (oldSubmit) {
						if (typeof (oldSubmit) == 'string')
							do_submit = eval (oldSubmit);
						else if (typeof (oldSubmit) == 'function')
							do_submit = oldSubmit.apply (form, arguments);
					}
					if (!do_submit) return false;
					closeForm ();
					// Copy the categories
					var eb = document.getElementById ('wpUploadDescription')
						|| document.getElementById ('wpDesc');
					var addedOne = false;
					for (var i = 0; i < editors.length; i++) {
						var t = editors[i].currentCategory;
						if (!t) continue ;
						var key = editors[i].currentKey;
						var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
						// Only add if not already present
						var cleanedText = eb.value
								.replace(/<\!--(\s|\S)*?--\>/g, "")
								.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
						if (!find_category (cleanedText, t, true)) {
							eb.value += '\n' + new_cat;
							addedOne = true;
						}
					}
					if (addedOne) {
						// Remove "subst:unc" added by Flinfo if it didn't find categories
						eb.value = eb.value.replace(/\{\{subst:unc\}\}/g, "");
					}
					return true;
				};
			}) (form.onsubmit);
		}
	}
 
	var cleanedText = null;
 
	function isOnPage (span) {
		var catTitle = title (span.firstChild.getAttribute ('href', 2));
		if (!catTitle) return null;
		catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' ');
		if (HotCat.blacklist && HotCat.blacklist.test (catTitle)) return null;
		var result = { title : catTitle, match : ["", "", ""] };
		if (pageText === null) return result;
		if (cleanedText === null) {
			cleanedText = pageText
				.replace(/<\!--(\s|\S)*?--\>/g, "")
				.replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
		}
		result.match = find_category (cleanedText, catTitle, true);
		return result;
	}
 
	var initialized = false;
	var setupTimeout = null;
 
	function findByClass (scope, tag, className) {
		// Compatibility routine. Uses jQuery if available, otherwise works with older getElementsByClassName
		var result;
		if (window.jQuery) {
			result = window.jQuery(scope).find(tag + '.' + className);
		} else {
			result = getElementsByClassName(scope, tag, className);
		}
		return (result && result.length) ? result[0] : null;
	}
 
	function setup (additionalWork) {
		if (initialized) return;
		initialized = true;
		if (setupTimeout) {
			window.clearTimeout (setupTimeout);
			setupTimeout = null;
		}
		// Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
		// each category, and add the + link.
		catLine =  catLine                                        // Special:Upload
				|| document.getElementById ('mw-normal-catlinks') // MW >= 1.13alpha
				|| findByClass (document , 'p' , 'catlinks');     // MW < 1.13
		var hiddenCats = document.getElementById ('mw-hidden-catlinks');
		if (!catLine) {
			var footer = null;
			if (!hiddenCats) {
				footer = findByClass (document , 'div' , 'printfooter');
				if (!footer) return; // Don't know where to insert the category line
			}
			catLine = make ('div');
			catLine.id = 'mw-normal-catlinks';
			catLine.style.textAlign = is_rtl ? 'right' : 'left';
			// Add a label
			var label = make ('a');
			label.href  = wgArticlePath.replace ('$1', 'Special:Categories');
			label.title = HotCat.categories;
			label.appendChild (make (HotCat.categories, true));
			catLine.appendChild (label);
			catLine.appendChild (make (':', true));
			// Insert the new category line
			var container = (hiddenCats ? hiddenCats.parentNode : document.getElementById ('catlinks'));
			if (!container) {
				container = make ('div');
				container.id = 'catlinks';
				footer.parentNode.insertBefore (container, footer.nextSibling);
			}
			container.className = 'catlinks noprint';
			container.style.display = "";
			if (!hiddenCats) {
				container.appendChild (catLine);
			} else {
				container.insertBefore (catLine, hiddenCats);
			}
		} // end if catLine exists
		if (is_rtl) catLine.dir = 'rtl';
 
		// Create editors for all existing categories
 
		function createEditors (line, is_hidden) {
			var i;
			var cats = line.getElementsByTagName ('li');
			if (cats.length > 0) {
				newDOM = true; line = cats[0].parentNode;
			} else {
				cats = line.getElementsByTagName ('span');
			}
			// Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
			var copyCats = new Array (cats.length);
			for (i = 0; i < cats.length; i++) copyCats[i] = cats[i];
			var editor = null;
			for (i = 0; i < copyCats.length; i++) {
				var test = isOnPage (copyCats[i]);
				if (test !== null && test.match !== null) {
					editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2], is_hidden);
				}
			}
			return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
		}
 
		var lastSpan = createEditors (catLine, false);
		// Create one to add a new category
		var editor = new CategoryEditor(newDOM ? catLine.getElementsByTagName('ul')[0] : catLine, null, null, lastSpan !== null, false);
		if (!onUpload) {
			if (pageText !== null && hiddenCats) {
				if (is_rtl) hiddenCats.dir = 'rtl';
				createEditors (hiddenCats, true);
			}
			// And finally add the "multi-mode" span. (Do this at the end, otherwise it ends up in the list above.)
			var enableMulti = make ('span');
			enableMulti.className = 'noprint';
			if (is_rtl) enableMulti.dir = 'rtl';
			catLine.insertBefore (enableMulti, catLine.firstChild.nextSibling);
			enableMulti.appendChild (make ('\xa0', true)); // nbsp
			multiSpan = make ('span');
			enableMulti.appendChild (multiSpan);
			multiSpan.innerHTML = '(<a>' + HotCat.addmulti + '</a>)';
			var lk = multiSpan.getElementsByTagName ('a')[0];
			lk.onclick = function (evt) {setMultiInput (); checkMultiInput (); return evtKill (evt);};
			lk.title = HotCat.multi_tooltip;
			lk.style.cursor = 'pointer';
		}
		cleanedText = null;
		if (typeof (additionalWork) == 'function') additionalWork();
		setupCompleted.loaded(); // Trigger signal; execute registered functions
		if (window.jQuery) window.jQuery('body').trigger ('hotcatSetupCompleted');
	}
 
	function setPage (json) {
		var startTime = null;
		if (json && json.query) {
			if (json.query.pages) {
				var page = json.query.pages[wgArticleId === 0 ? "-1" : "" + wgArticleId];
				if (page) {
					if (page.revisions && page.revisions.length > 0) {
						// Revisions are sorted by revision ID, hence [0] is the one we asked for, and possibly there's a [1] if we're
						// not on the latest revision (edit conflicts and such).
						pageText = page.revisions[0]['*'];
						if (page.revisions[0].timestamp) pageTime = page.revisions[0].timestamp.replace (/\D/g, "");
						if (page.revisions[0].revid) pageTextRevId = page.revisions[0].revid;
						if (page.revisions.length > 1) conflictingUser = page.revisions[1].user;
					}
					if (page.lastrevid) lastRevId = page.lastrevid;
					if (page.starttimestamp) startTime = page.starttimestamp.replace (/\D/g, "");
					pageWatched = typeof (page.watched) == 'string';
					editToken = page.edittoken;
					if (page.langlinks && (!json['query-continue'] || !json['query-continue'].langlinks)) {
						// We have interlanguage links, and we got them all.
						var re = "";
						for (var i = 0; i < page.langlinks.length; i++) {
							re += (i > 0 ? '|' : "") + page.langlinks[i].lang.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
						}
						if (re.length > 0) {
							interlanguageRE = new RegExp ('((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$');
						}
					}
 
				}
			}
			// Siteinfo
			if (json.query.general) {
				HotCat.capitalizePageNames = (json.query.general['case'] == 'first-letter');
				if (json.query.general.time && !startTime) startTime = json.query.general.time.replace (/\D/g, "");
			}
			serverTime = startTime;
			// Userinfo
			if (json.query.userinfo && json.query.userinfo.options) {
				watchCreate = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchcreations == '1';
				watchEdit   = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchdefault == '1';
				minorEdits  = json.query.userinfo.options.minordefault == 1;
				// If the user has the "All edits are minor" preference enabled, we should honor that
				// for single category changes, no matter what the site configuration is.
				if (minorEdits) HotCat.single_minor = true;
			}
		}
	}
 
	function createCommitForm () {
		if (commitForm) return;
		var formContainer = make ('div');
		formContainer.style.display = 'none';
		document.body.appendChild (formContainer);
		formContainer.innerHTML =
			'<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="'
			+ wgScript + '?title=' + encodeURIComponent (wgPageName)
			+ '&action=edit">'
			+ '<input type="hidden" name="wpTextbox1" />'
			+ '<input type="hidden" name="model" value="wikitext" />'
			+ '<input type="hidden" name="format" value="text/x-wiki" />'
			+ '<input type="hidden" name="wpSummary" value="" />'
			+ '<input type="checkbox" name="wpMinoredit" value="1" />'
			+ '<input type="checkbox" name="wpWatchthis" value="1" />'
			+ '<input type="hidden" name="wpAutoSummary" value="" />'
			+ '<input type="hidden" name="wpEdittime" />'
			+ '<input type="hidden" name="wpStarttime" />'
			+ '<input type="hidden" name="wpDiff" value="wpDiff" />'
			+ '<input type="hidden" name="oldid" value="0" />'
			+ '<input type="submit" name="hcCommit" value="hcCommit" />'
			+ '<input type="hidden" name="wpEditToken" />'
			+ '<input type="hidden" name="wpUltimateParam" value="1" />'
			+ '</form>';
		commitForm = document.getElementById ('hotcatCommitForm');
	}
 
	function getPage () {
		// We know we have an article here.
		if (wgArticleId === 0) {
			// Doesn't exist yet.
			pageText = "";
			pageTime = null;
			setup (createCommitForm);
		} else {
			var url = wgServer + wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&titles='
					+ encodeURIComponent (wgPageName)
					+ '&prop=info%7Crevisions&rvprop=content%7Ctimestamp%7Cids&meta=siteinfo&rvlimit=1&rvstartid='
					+ wgCurRevisionId;
			var s = make ('script');
			s.src = armorUri(url);
			s.type = 'text/javascript';
			HotCat.start = function (json) { setPage (json); setup (createCommitForm); };
			document.getElementsByTagName ('head')[0].appendChild (s);
			setupTimeout = window.setTimeout (function () {setup (createCommitForm);}, 4000); // 4 sec, just in case getting the wikitext takes longer.
		}
	}
 
	function run () {
		if (HotCat.started) return;
		HotCat.started = true;
		loadTrigger.register(really_run);
	}
 
	function really_run () {
		initialize ();
 
		if (is_rtl && is_ie6) return; // Disabled! IE6 with RTL is just too broken...
		if (!HotCat.upload_disabled && wgNamespaceNumber === -1 && wgCanonicalSpecialPageName == 'Upload' && wgUserName) {
			setup_upload ();
			setup (function () {
				// Check for state restoration once the setup is done otherwise, but before signalling setup completion
				if (   typeof (UploadForm) != 'undefined'
					&& typeof (UploadForm.previous_hotcat_state) != 'undefined'
					&& UploadForm.previous_hotcat_state !== null)
				{
					UploadForm.previous_hotcat_state = setState (UploadForm.previous_hotcat_state);
				}
			});
		} else {
			if (!wgIsArticle || wgAction != 'view' || param('diff') !== null || param('oldid') !== null || !can_edit() || HotCat.disable()) return;
			getPage ();
		}
	}
 
	// Legacy stuff
 
	function closeForm () {
		// Close all open editors without redirect resolution and other asynchronous stuff.
		for (var i = 0; i < editors.length; i++) {
			if (editors[i].state == CategoryEditor.OPEN) {
				editors[i].cancel();
			} else if (editors[i].state == CategoryEditor.CHANGE_PENDING) {
				editors[i].sanitizeInput ();
				var value = editors[i].text.value.split('|');
				var key   = null;
				if (value.length > 1) key = value[1];
				var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
				if (v.length === 0) {
					editors[i].cancel ();
				} else {
					editors[i].currentCategory = v;
					editors[i].currentKey = key;
					editors[i].currentExists = this.inputExists;
					editors[i].close ();
				}
			}
		}
	}
 
	function getState () {
		var result = null;
		for (var i = 0; i < editors.length; i++) {
			var text = editors[i].currentCategory;
			var key  = editors[i].currentKey;
			if (text && text.length > 0) {
				if (key !== null) text += '|' + key;
				if (result === null)
					result = text;
				else
					result = result + '\n' + text;
			}
		}
		return result;
	}
 
	function setState (state) {
		var cats = state.split ('\n');
		if (cats.length === 0) return null;
		if (initialized && editors.length == 1 && editors[0].isAddCategory) {
			// Insert new spans and create new editors for them.
			var newSpans = [];
			var before = editors.length == 1 ? editors[0].span : null;
			var i;
			for (i = 0; i < cats.length; i++) {
				if (cats[i].length === 0) continue;
				var cat = cats[i].split ('|');
				var key = cat.length > 1 ? cat[1] : null;
				cat = cat[0];
				var lk = make ('a'); lk.href = wikiPagePath (HotCat.category_canonical + ':' + cat);
				lk.appendChild (make (cat, true));
				lk.title = cat;
				var span = make ('span');
				span.appendChild (lk);
				if (i === 0) catLine.insertBefore (make (' ', true), before);
				catLine.insertBefore (span, before);
				if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
				newSpans.push ({element: span, title: cat, 'key': key});
			}
			// And change the last one...
			if (before) {
				before.parentNode.insertBefore (make (' | ', true), before);
			}
			var editor = null;
			for (i = 0; i < newSpans.length; i++) {
				editor = new CategoryEditor (catLine, newSpans[i].element, newSpans[i].title, newSpans[i].key);
			}
		}
		return null;
	}
 
	// Now export these legacy functions
	window.hotcat_get_state  = function () { return getState(); };
	window.hotcat_set_state  = function (state) { return setState (state); };
	window.hotcat_close_form = function () { closeForm (); };
 
	if (window.mediaWiki && window.mediaWiki.config) {
		// Make sure we don't get conflicts with AjaxCategories (core development that should one day
		// replace HotCat).
		window.mediaWiki.config.set('disableAJAXCategories', true);
	}
	// Run as soon as possible. This varies depending on MediaWiki version;
	// window's 'load' event is always safe, but usually we can do better than that.
 
	// Check for version to avoid MediaWiki bug 32537.
	var mwVersion = (window.mediaWiki && mediaWiki.config) ? mediaWiki.config.get('wgVersion') : window.wgVersion;
	if (parseFloat(mwVersion) > 1.20) {
		// We can safely trigger just after user configuration is loaded. Also start HotCat if the user module fails to load.
		// Won't start on IE6 start with this. If you need HotCat with MW > 1.20 on IE6, use window.attachEvent('onload', run);
		var startHotCat = function(){ jQuery(document).ready(run); };
		mediaWiki.loader.using('user', startHotCat, startHotCat);
	} else {
		// mw.loader.using('user', ...) could have unintended side-effects. Fall back to DOMContentLoaded.
		jQuery(document.body).on('DOMContentLoaded', run);
		// And in case we're loaded after DOMContentLoaded fires or in a browser that doesn't support it,
		// fall back to window.onload (which is definitely supported on MW 1.20 and lower).
		// The run function itself protects against double initialization, so it's okay.
		jQuery(window).on('load', run);
	}
})();
 
} // end if (guard)
//</source>