Difference between revisions of "MediaWiki:Gadget-HotCat.js"

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

Latest revision as of 13:40, 16 February 2021

/**
HotCat V2.43

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

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

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

Choose whichever license of these you like best :-)

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

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

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

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

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

		// Override the decision of whether HotCat should help users by automatically
		// capitalising the title in the user input text if the wiki has case-sensitive page names.
		// Basically, this will make an API query to check the MediaWiki configuration and HotCat then sets
		// this to true for most wikis, and to false on Wiktionary.
		// 
		// You can set this directly if there is a problem with it. For example, Georgian Wikipedia (kawiki),
		// is known to have different capitalisation logic between MediaWiki PHP and JavaScript. As such, automatic
		// case changes in JavaScript by HotCat would be wrong.
		capitalizePageNames: null,
		// If upload_disabled is true, HotCat will not be used on the Upload form.
		upload_disabled: false,
		// Single regular expression matching blacklisted categories that cannot be changed or
		// added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
		// or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
		// word "maintenance" in its title.
		blacklist: null,

		// Stuff changeable by users:
		// Background for changed categories in multi-edit mode. Default is a very light salmon pink.
		bg_changed: '#FCA',
		// If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
		// the changes; users must always save explicitly.
		no_autocommit: false,
		// If true, the "category deletion" link "(-)" will never save automatically but always show an
		// edit page where the user has to save the edit manually. Is false by default because that's the
		// traditional behavior. This setting overrides no_autocommit for "(-)" links.
		del_needs_diff: false,
		// Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
		// server to get suggestions.
		suggest_delay: 100,
		// Default width, in characters, of the text input field.
		editbox_width: 40,
		// One of the engine_names above, to be used as the default suggestion engine.
		suggestions: 'combined',
		// If true, always use the default engine, and never display a selector.
		fixed_search: false,
		// If false, do not display the "up" and "down" links
		use_up_down: true,
		// Default list size
		listSize: 10,
		// If true, single category changes are marked as minor edits. If false, they're not.
		single_minor: true,
		// If true, never add a page to the user's watchlist. If false, pages get added to the watchlist if
		// the user has the "Add pages I edit to my watchlist" or the "Add pages I create to my watchlist"
		// options in his or her preferences set.
		dont_add_to_watchlist: false,
		shortcuts: null,
		addShortcuts: function ( map ) {
			if ( !map ) return;
			window.HotCat.shortcuts = window.HotCat.shortcuts || {};
			for ( var k in map ) {
				if ( !map.hasOwnProperty( k ) || typeof k !== 'string' ) continue;

				var v = map[ k ];
				if ( typeof v !== 'string' ) continue;

				k = k.replace( /^\s+|\s+$/g, '' );
				v = v.replace( /^\s+|\s+$/g, '' );
				if ( !k.length || !v.length ) continue;

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

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

	function LoadTrigger( needed ) {
		// Define methods in a closure so that self reference is available,
		// also allows method calls to be detached.
		var self = this;
		self.queue = [];
		self.needed = needed;
		self.register = function ( callback ) {
			if ( self.needed <= 0 ) callback(); // Execute directly
			else self.queue.push( callback );
		};
		self.loaded = function () {
			self.needed--;
			if ( self.needed === 0 ) {
				// Run queued callbacks once
				for ( var i = 0; i < self.queue.length; i++ ) self.queue[ i ]();
				self.queue = [];
			}
		};
	}

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

	function load( uri, callback ) {
		var s = document.createElement( 'script' );
		s.src = uri;
		var called = false;

		s.onload = s.onerror = function () {
			if ( !called && callback ) {
				called = true;
				callback();
			}
			if ( s.parentNode ) {
				s.parentNode.removeChild( s );
			}
		};
		document.head.appendChild( s );
	}

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

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

		load( url, callback );
	}

	// 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 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', loadTrigger.loaded );

	// Load localized UI texts. These are the texts that HotCat displays on the page itself. Texts shown in edit summaries
	// should be localized in /local_defaults above.
	if ( conf.wgUserLanguage !== 'en' ) {
		// Lupo: somebody thought it would be a good idea to add this. So the default is true, and you have to set it to false
		// explicitly if you're not on Commons and don't want that.
		if ( window.hotcat_translations_from_commons === undefined ) window.hotcat_translations_from_commons = true;

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

	// No further changes should be necessary here.

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

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

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

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

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

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

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

		return literal ? document.createTextNode( arg ) : document.createElement( arg );
	}
	function param( name, uri ) {
		uri = uri || document.location.href;
		var re = new RegExp( '[&?]' + name + '=([^&#]*)' );
		var m = re.exec( uri );
		if ( m && m.length > 1 ) return decodeURIComponent( m[ 1 ] );
		return null;
	}
	function title( href ) {
		if ( !href ) return null;

		var script = conf.wgScript + '?';
		if ( href.indexOf( script ) === 0 || href.indexOf( conf.wgServer + script ) === 0 || conf.wgServer.substring( 0, 2 ) === '//' && href.indexOf( document.location.protocol + conf.wgServer + script ) === 0 ) {
			// href="/w/index.php?title=..."
			return param( 'title', href );
		} else {
			// href="/wiki/..."
			var prefix = conf.wgArticlePath.replace( '$1', '' );
			if ( href.indexOf( prefix ) ) prefix = conf.wgServer + prefix; // Fully expanded URL?

			if ( href.indexOf( prefix ) && prefix.substring( 0, 2 ) === '//' ) prefix = document.location.protocol + prefix; // Protocol-relative wgServer?

			if ( href.indexOf( prefix ) === 0 ) return decodeURIComponent( href.substring( prefix.length ) );
		}
		return null;
	}
	function hasClass( elem, name ) {
		return ( ' ' + elem.className + ' ' ).indexOf( ' ' + name + ' ' ) >= 0;
	}
	function capitalize( str ) {
		if ( !str || !str.length ) return str;

		return str.substr( 0, 1 ).toUpperCase() + str.substr( 1 );
	}
	function wikiPagePath( pageName ) {
		// Note: do not simply use encodeURI, it doesn't encode '&', which might break if wgArticlePath actually has the $1 in
		// a query parameter.
		return conf.wgArticlePath.replace( '$1', encodeURIComponent( pageName ).replace( /%3A/g, ':' ).replace( /%2F/g, '/' ) );
	}
	function escapeRE( str ) {
		return str.replace( /([\\^$.?*+()[\]])/g, '\\$1' );
	}

	function substituteFactory( options ) {
		options = options || {};
		var lead = options.indicator || '$';
		var indicator = escapeRE( lead );
		var lbrace = escapeRE( options.lbrace || '{' );
		var rbrace = escapeRE( options.rbrace || '}' );
		var re;

		re = new RegExp(
			// $$
			'(?:' + indicator + '(' + indicator + '))|' +
			// $0, $1
			'(?:' + indicator + '(\\d+))|' +
			// ${key}
			'(?:' + indicator + '(?:' + lbrace + '([^' + lbrace + rbrace + ']+)' + rbrace + '))|' +
			// $key (only if first char after $ is not $, digit, or { )
			'(?:' + indicator + '(?!(?:[' + indicator + lbrace + ']|\\d))(\\S+?)\\b)',
			'g'
		);
		// Replace $1, $2, or ${key1}, ${key2}, or $key1, $key2 by values from map. $$ is replaced by a single $.
		return function ( str, map ) {
			if ( !map ) return str;

			return str.replace( re, function ( match, prefix, idx, key, alpha ) {
				if ( prefix === lead ) return lead;

				var k = alpha || key || idx;
				var replacement = typeof map[ k ] === 'function' ? map[ k ]( match, k ) : map[ k ];
				return typeof replacement === 'string' ? replacement : ( replacement || match );
			} );
		};
	}

	var substitute = substituteFactory();
	var replaceShortcuts = ( function () {
		var replaceHash = substituteFactory( {
			indicator: '#',
			lbrace: '[',
			rbrace: ']'
		} );
		return function ( str, map ) {
			var s = replaceHash( str, map );
			return HC.capitalizePageNames ? capitalize( s ) : s;
		};
	}() );

	// Text modification

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

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

	function find_category( wikitext, category, once ) {
		var cat_regex = null;
		if ( HC.template_categories[ category ] ) {
			cat_regex = new RegExp(
				'\\{\\{' + wikiTextBlankOrBidi + '(' + HC.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi +
				'(?:' + HC.template_categories[ category ] + ')' +
				wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}',
				'g'
			);
		} else {
			var cat_name = escapeRE( category );
			var initial = cat_name.substr( 0, 1 );
			cat_regex = new RegExp(
				'\\[\\[' + wikiTextBlankOrBidi + '(' + HC.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi +
				( initial === '\\' || !HC.capitalizePageNames ?
					initial :
					'[' + initial.toUpperCase() + initial.toLowerCase() + ']' ) +
				cat_name.substring( 1 ).replace( wikiTextBlankRE, wikiTextBlank ) +
				wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]',
				'g'
			);
		}
		if ( once ) return cat_regex.exec( wikitext );

		var copiedtext = wikitext
			.replace( /<!--(\s|\S)*?-->/g, replaceByBlanks )
			.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, replaceByBlanks );
		var result = [];
		var curr_match = null;
		while ( ( curr_match = cat_regex.exec( copiedtext ) ) !== null ) {
			result.push( {
				match: curr_match
			} );
		}
		result.re = cat_regex;
		return result; // An array containing all matches, with positions, in result[ i ].match
	}

	var interlanguageRE = null;

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

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

			if ( index < 0 ) {
				// Find the index of the first interlanguage link...
				var match = null;
				if ( !interlanguageRE ) {
				// Approximation without API: interlanguage links start with 2 to 3 lower case letters, optionally followed by
				// a sequence of groups consisting of a dash followed by one or more lower case letters. Exceptions are "simple"
				// and "tokipona".
					match = /((^|\n\r?)(\[\[\s*(([a-z]{2,3}(-[a-z]+)*)|simple|tokipona)\s*:[^\]]+\]\]\s*))+$/.exec( copiedtext );
				} else {
					match = interlanguageRE.exec( copiedtext );
				}
				if ( match ) index = match.index;

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

		var summary = [],
			nameSpace = HC.category_canonical,
			cat_point = -1, // Position of removed category;
			keyChange = ( toRemove && toAdd && toRemove === toAdd && toAdd.length ),
			matches;
		if ( key ) key = '|' + key;
		// Remove
		if ( toRemove && toRemove.length ) {
			matches = find_category( wikitext, toRemove );
			if ( !matches || !matches.length ) {
				return {
					text: wikitext,
					summary: summary,
					error: HC.messages.cat_notFound.replace( /\$1/g, toRemove )
				};
			} else {
				var before = wikitext.substring( 0, matches[ 0 ].match.index ),
					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; Canonical namespace should be always preferred
					if ( key === null ) key = matches[ 0 ].match[ 2 ];
				// Remember the category key, if any.
				}
				// Remove whitespace (properly): strip whitespace, but only up to the next line feed.
				// If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
				// whitespace characters, insert a blank.
				var i = before.length - 1;
				while ( i >= 0 && before.charAt( i ) !== '\n' && before.substr( i, 1 ).search( /\s/ ) >= 0 ) i--;

				var j = 0;
				while ( j < after.length && after.charAt( j ) !== '\n' && after.substr( j, 1 ).search( /\s/ ) >= 0 ) j++;

				if ( i >= 0 && before.charAt( i ) === '\n' && ( !after.length || j < after.length && after.charAt( j ) === '\n' ) ) i--;

				if ( i >= 0 ) before = before.substring( 0, i + 1 ); else before = '';

				if ( j < after.length ) after = after.substring( j ); else after = '';

				if (
					before.length && before.substring( before.length - 1 ).search( /\S/ ) >= 0 &&
					after.length && after.substr( 0, 1 ).search( /\S/ ) >= 0
				) {
					before += ' ';
				}

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

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

			}
		}
		// Add
		if ( toAdd && toAdd.length ) {
			matches = find_category( wikitext, toAdd );
			if ( matches && matches.length ) {
				// Already exists
				return {
					text: wikitext,
					summary: summary,
					error: HC.messages.cat_exists.replace( /\$1/g, toAdd )
				};
			} else {
				var onCat = false;
				if ( cat_point < 0 ) {
					var point = find_insertionpoint( wikitext );
					cat_point = point.idx;
					onCat = point.onCat;
				} else {
					onCat = true;
				}
				var newcatstring = '[[' + nameSpace + ':' + toAdd + ( key || '' ) + ']]';
				if ( cat_point >= 0 ) {
					var suffix = wikitext.substring( cat_point );
					wikitext = wikitext.substring( 0, cat_point ) + ( cat_point > 0 ? '\n' : '' ) + newcatstring + ( !onCat ? '\n' : '' );
					if ( suffix.length && suffix.substr( 0, 1 ) !== '\n' ) wikitext += '\n' + suffix; else wikitext += suffix;
				} else {
					if ( wikitext.length && wikitext.substr( wikitext.length - 1, 1 ) !== '\n' ) wikitext += '\n';

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

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

	// The real HotCat UI

	function evtKeys( e ) {
		/* eslint-disable no-bitwise */
		var code = 0;
		if ( e.ctrlKey ) { // All modern browsers
		// Ctrl-click seems to be overloaded in FF/Mac (it opens a pop-up menu), so treat cmd-click
		// as a ctrl-click, too.
			if ( e.ctrlKey || e.metaKey ) code |= 1;

			if ( e.shiftKey ) code |= 2;
		}
		return code;
	}
	function evtKill( e ) {
		if ( e.preventDefault ) {
			e.preventDefault();
			e.stopPropagation();
		} else {
			e.cancelBubble = true;
		}
		return false;
	}

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

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

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

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

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

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

	function setPage( json ) {
		var startTime = null;
		if ( json && json.query ) {
			if ( json.query.pages ) {
				var page = json.query.pages[ !conf.wgArticleId ? '-1' : String( conf.wgArticleId ) ];
				if ( page ) {
					if ( page.revisions && page.revisions.length ) {
						// Revisions are sorted by revision ID, hence [ 0 ] is the one we asked for, and possibly there's a [ 1 ] if we're
						// not on the latest revision (edit conflicts and such).
						pageText = page.revisions[ 0 ][ '*' ];
						if ( page.revisions[ 0 ].timestamp ) pageTime = page.revisions[ 0 ].timestamp.replace( /\D/g, '' );
						if ( page.revisions[ 0 ].revid ) pageTextRevId = page.revisions[ 0 ].revid;
						if ( page.revisions.length > 1 ) conflictingUser = page.revisions[ 1 ].user;
					}
					if ( page.lastrevid ) lastRevId = page.lastrevid;
					if ( page.starttimestamp ) startTime = page.starttimestamp.replace( /\D/g, '' );
					pageWatched = typeof page.watched === 'string';
					editToken = page.edittoken;
					if ( page.langlinks && ( !json[ 'query-continue' ] || !json[ 'query-continue' ].langlinks ) ) {
						// We have interlanguage links, and we got them all.
						var re = '';
						for ( var i = 0; i < page.langlinks.length; i++ ) re += ( i > 0 ? '|' : '' ) + page.langlinks[ i ].lang.replace( /([\\^$.?*+()])/g, '\\$1' );
						if ( re.length ) interlanguageRE = new RegExp( '((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$' );
					}
				}
			}
			// Siteinfo
			if ( json.query.general ) {
				if ( json.query.general.time && !startTime ) startTime = json.query.general.time.replace( /\D/g, '' );

				if ( HC.capitalizePageNames === null ) {
					// ResourceLoader's JSParser doesn't like .case, so override eslint.
					// eslint-disable-next-line dot-notation
					HC.capitalizePageNames = ( json.query.general[ 'case' ] === 'first-letter' );
				}
			}
			serverTime = startTime;
			// Userinfo
			if ( json.query.userinfo && json.query.userinfo.options ) {
				watchCreate = !HC.dont_add_to_watchlist && json.query.userinfo.options.watchcreations === '1';
				watchEdit = !HC.dont_add_to_watchlist && json.query.userinfo.options.watchdefault === '1';
				minorEdits = json.query.userinfo.options.minordefault === 1;
				// If the user has the "All edits are minor" preference enabled, we should honor that
				// for single category changes, no matter what the site configuration is.
				if ( minorEdits ) HC.single_minor = true;
			}
		}
	}

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

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

		// Must use Ajax here to get the user options and the edit token.
		$.getJSON(
			conf.wgServer + conf.wgScriptPath + '/api.php?' +
			'format=json&action=query&rawcontinue=&titles=' + encodeURIComponent( conf.wgPageName ) +
			'&prop=info%7Crevisions%7Clanglinks&inprop=watched&intoken=edit&rvprop=content%7Ctimestamp%7Cids%7Cuser&lllimit=500' +
			'&rvlimit=2&rvdir=newer&rvstartid=' + conf.wgCurRevisionId +	'&meta=siteinfo%7Cuserinfo&uiprop=options',
			function ( json ) {
				setPage( json );
				doEdit( fail );
			}
		).fail( function ( req ) {
			fail( req.status + ' ' + req.statusText );
		} );
	}

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

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

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

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

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

		// Create a form and submit it. We don't use the edit API (api.php?action=edit) because
		// (a) sensibly reporting back errors like edit conflicts is always a hassle, and
		// (b) we want to show a diff for multi-edits anyway, and
		// (c) we want to trigger onsubmit events, allowing user code to intercept the edit.
		// Using the form, we can do (b) and (c), and we get (a) for free. And, of course, using the form
		// automatically reloads the page with the updated categories on a successful submit, which
		// we would have to do explicitly if we used the edit API.
		var action;
		// Normally, we don't have to care about edit conflicts. If some other user edited the page in the meantime, the
		// server will take care of it and merge the edit automatically or present an edit conflict screen. However, the
		// server suppresses edit conflicts with oneself. Hence, if we have a conflict, and the conflicting user is the
		// current user, then we set the "oldid" value and switch to diff, which gives the "you are editing an old version;
		// if you save, any more recent changes will be lost" screen.
		var selfEditConflict = ( lastRevId !== null && lastRevId !== conf.wgCurRevisionId || pageTextRevId !== null &&
			pageTextRevId !== conf.wgCurRevisionId ) && conflictingUser && conflictingUser === conf.wgUserName;
		if ( singleEditor && !singleEditor.noCommit && !HC.no_autocommit && editToken && !selfEditConflict ) {
			// If we do have an edit conflict, but not with ourself, that's no reason not to attempt to save: the server side may actually be able to
			// merge the changes. We just need to make sure that we do present a diff view if it's a self edit conflict.
			commitForm.wpEditToken.value = editToken;
			action = commitForm.wpDiff;
			if ( action ) action.name = action.value = 'wpSave';
		} else {
			action = commitForm.wpSave;
			if ( action ) action.name = action.value = 'wpDiff';
		}
		var result = {
				text: pageText
			},
			changed = [],
			added = [],
			deleted = [],
			changes = 0,
			toEdit = singleEditor ? [ singleEditor ] : editors,
			error = null,
			edit,
			i;
		for ( i = 0; i < toEdit.length; i++ ) {
			edit = toEdit[ i ];
			if ( edit.state === CategoryEditor.CHANGED ) {
				result = change_category(
					result.text,
					edit.originalCategory,
					edit.currentCategory,
					edit.currentKey,
					edit.currentHidden );
				if ( !result.error ) {
					changes++;
					if ( !edit.originalCategory || !edit.originalCategory.length ) {
						added.push( edit.currentCategory );
					} else {
						changed.push( {
							from: edit.originalCategory,
							to: edit.currentCategory
						} );
					}
				} else if ( error === null ) {
					error = result.error;
				}
			} else if (
				edit.state === CategoryEditor.DELETED && edit.originalCategory && edit.originalCategory.length ) {
				result = change_category(
					result.text,
					edit.originalCategory,
					null, null, false );
				if ( !result.error ) {
					changes++;
					deleted.push( edit.originalCategory );
				} else if ( error === null ) {
					error = result.error;
				}
			}
		}
		if ( error !== null ) { // Do not commit if there were errors
			action = commitForm.wpSave;
			if ( action ) action.name = action.value = 'wpDiff';
		}
		// Fill in the form and submit it
		commitForm.wpMinoredit.checked = minorEdits;
		commitForm.wpWatchthis.checked = !conf.wgArticleId && watchCreate || watchEdit || pageWatched;
		if ( conf.wgArticleId || !!singleEditor ) {
			// Prepare change-tag save
			if ( action && action.value === 'wpSave' ) {
				if ( HC.changeTag ) {
					commitForm.wpChangeTags.value = HC.changeTag;
					HC.messages.using = '';
					HC.messages.prefix = '';
				}
			} else {
				commitForm.wpAutoSummary.value = HC.changeTag;
			}
			if ( changes === 1 ) {
				if ( result.summary && result.summary.length ) commitForm.wpSummary.value = HC.messages.prefix + result.summary.join( HC.messages.separator ) + HC.messages.using;
				commitForm.wpMinoredit.checked = HC.single_minor || minorEdits;
			} else if ( changes ) {
				var summary = [];
				var shortSummary = [];
				// Deleted
				for ( i = 0; i < deleted.length; i++ ) summary.push( '−' + substitute( HC.messages.short_catchange, [ null, deleted[ i ] ] ) );

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

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

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

				// Changed
				var arrow = is_rtl ? '\u2190' : '\u2192'; // left and right arrows. Don't use ← and → in the code.
				for ( i = 0; i < changed.length; i++ ) {
					if ( changed[ i ].from !== changed[ i ].to ) {
						summary.push(
							'±' + substitute( HC.messages.short_catchange, [ null, changed[ i ].from ] ) + arrow +
							substitute( HC.messages.short_catchange, [ null, changed[ i ].to ] )
						);
					} else {
						summary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ i ].from ] ) );
					}
				}
				if ( changed.length === 1 ) {
					if ( changed[ 0 ].from !== changed[ 0 ].to ) {
						shortSummary.push(
							'±' + substitute( HC.messages.short_catchange, [ null, changed[ 0 ].from ] ) + arrow +
							substitute( HC.messages.short_catchange, [ null, changed[ 0 ].to ] )
						);
					} else {
						shortSummary.push( '±' + substitute( HC.messages.short_catchange, [ null, changed[ 0 ].from ] ) );
					}
				} else if ( changed.length ) {
					shortSummary.push( '± ' + multiChangeMsg( changed.length ) );
				}
				if ( summary.length ) {
					summary = summary.join( HC.messages.separator );
					if ( summary.length > 200 - HC.messages.prefix.length - HC.messages.using.length ) summary = shortSummary.join( HC.messages.separator );

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

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

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

	function resolveOne( page, toResolve ) {
		var cats = page.categories,
			lks = page.links,
			is_dab = false,
			is_redir = typeof page.redirect === 'string', // Hard redirect?
			is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden === 'string',
			is_missing = typeof page.missing === 'string',
			i;
		for ( i = 0; i < toResolve.length; i++ ) {
			if ( i && toResolve[ i ].dabInputCleaned !== page.title.substring( page.title.indexOf( ':' ) + 1 ) ) continue;
			// Note: the server returns in page an NFC normalized Unicode title. If our input was not NFC normalized, we may not find
			// any entry here. If we have only one editor to resolve (the most common case, I presume), we may simply skip the check.
			toResolve[ i ].currentHidden = is_hidden;
			toResolve[ i ].inputExists = !is_missing;
			toResolve[ i ].icon.src = ( is_missing ? HC.existsNo : HC.existsYes );
		}
		if ( is_missing ) return;
		if ( !is_redir && cats && ( HC.disambig_category || HC.redir_category ) ) {
			for ( var c = 0; c < cats.length; c++ ) {
				var cat = cats[ c ].title;
				// Strip namespace prefix
				if ( cat ) {
					cat = cat.substring( cat.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
					if ( cat === HC.disambig_category ) {
						is_dab = true;
						break;
					} else if ( cat === HC.redir_category ) {
						is_redir = true;
						break;
					}
				}
			}
		}
		if ( !is_redir && !is_dab ) return;
		if ( !lks || !lks.length ) return;
		var titles = [];
		for ( i = 0; i < lks.length; i++ ) {
			if (
				// Category namespace -- always true since we ask only for the category links
				lks[ i ].ns === 14 &&
				// Name not empty
				lks[ i ].title && lks[ i ].title.length
			) {
				// Internal link to existing thingy. Extract the page name and remove the namespace.
				var match = lks[ i ].title;
				match = match.substring( match.indexOf( ':' ) + 1 );
				// Exclude blacklisted categories.
				if ( !HC.blacklist || !HC.blacklist.test( match ) ) titles.push( match );
			}
		}
		if ( !titles.length ) return;
		for ( i = 0; i < toResolve.length; i++ ) {
			if ( i && toResolve[ i ].dabInputCleaned !== page.title.substring( page.title.indexOf( ':' ) + 1 ) ) continue;
			toResolve[ i ].inputExists = true; // Might actually be wrong if it's a redirect pointing to a non-existing category
			toResolve[ i ].icon.src = HC.existsYes;
			if ( titles.length > 1 ) {
				toResolve[ i ].dab = titles;
			} else {
				toResolve[ i ].text.value =
					titles[ 0 ] + ( toResolve[ i ].currentKey !== null ? '|' + toResolve[ i ].currentKey : '' );
			}
		}
	}

	function resolveRedirects( toResolve, params ) {
		if ( !params || !params.query || !params.query.pages ) return;
		for ( var p in params.query.pages ) resolveOne( params.query.pages[ p ], toResolve );
	}

	function resolveMulti( toResolve, callback ) {
		var i;
		for ( i = 0; i < toResolve.length; i++ ) {
			toResolve[ i ].dab = null;
			toResolve[ i ].dabInput = toResolve[ i ].lastInput;
		}
		if ( noSuggestions ) {
			callback( toResolve );
			return;
		}
		// Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
		// category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
		var args = 'action=query&prop=info%7Clinks%7Ccategories%7Ccategoryinfo&plnamespace=14' +
			'&pllimit=' + ( toResolve.length * 10 ) +
			'&cllimit=' + ( toResolve.length * 10 ) +
			'&format=json&titles=';
		for ( i = 0; i < toResolve.length; i++ ) {
			var v = toResolve[ i ].dabInput;
			v = replaceShortcuts( v, HC.shortcuts );
			toResolve[ i ].dabInputCleaned = v;
			args += encodeURIComponent( 'Category:' + v );
			if ( i + 1 < toResolve.length ) args += '%7C';
		}
		$.getJSON( conf.wgServer + conf.wgScriptPath + '/api.php?' + args,
			function ( json ) {
				resolveRedirects( toResolve, json );
				callback( toResolve );
			} ).fail( function ( req ) {
			if ( !req ) noSuggestions = true;
			callback( toResolve );
		} );
	}

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

		which.is_active = true;
		if ( which.dab ) {
			// eslint-disable-next-line no-use-before-define
			showDab( which );
		} else {
			// Check for programmatic value changes.
			var expectedInput = which.lastRealInput || which.lastInput || '';
			var actualValue = which.text.value || '';
			if ( !expectedInput.length && actualValue.length || expectedInput.length && actualValue.indexOf( expectedInput ) ) {
				// Somehow the field's value appears to have changed, and which.lastSelection therefore is no longer valid. Try to set the
				// cursor at the end of the category, and do not display the old suggestion list.
				which.showsList = false;
				var v = actualValue.split( '|' );
				which.lastRealInput = which.lastInput = v[ 0 ];
				if ( v.length > 1 ) which.currentKey = v[ 1 ];

				if ( which.lastSelection ) {
					which.lastSelection = {
						start: v[ 0 ].length,
						end: v[ 0 ].length
					};
				}
			}
			if ( which.showsList ) which.displayList();

			if ( which.lastSelection ) {
				if ( is_webkit ) {
					// WebKit (Safari, Chrome) has problems selecting inside focus()
					// See http://code.google.com/p/chromium/issues/detail?id=32865#c6
					window.setTimeout(
						function () {
							which.setSelection( which.lastSelection.start, which.lastSelection.end );
						},
						1 );
				} else {
					which.setSelection( which.lastSelection.start, which.lastSelection.end );
				}
			}
		}
	}

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

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

		if ( !toResolve.length ) {
			initiateEdit( function ( failure ) {
				performChanges( failure );
			}, function ( msg ) {
				alert( msg );
			} );
			return;
		}
		resolveMulti( toResolve, function ( resolved ) {
			var firstDab = null;
			var dontChange = false;
			for ( var i = 0; i < resolved.length; i++ ) {
				if ( resolved[ i ].lastInput !== resolved[ i ].dabInput ) {
					// We didn't disable all the open editors, but we did asynchronous calls. It is
					// theoretically possible that the user changed something...
					dontChange = true;
				} else {
					if ( resolved[ i ].dab ) {
						if ( !firstDab ) firstDab = resolved[ i ];
					} else {
						if ( resolved[ i ].acceptCheck( true ) ) resolved[ i ].commit();
					}
				}
			}
			if ( firstDab ) {
				showDab( firstDab );
			} else if ( !dontChange ) {
				initiateEdit( function ( failure ) {
					performChanges( failure );
				}, function ( msg ) {
					alert( msg );
				} );
			}
		} );
	}

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

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

	var suggestionEngines = {
		opensearch: {
			uri: '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1', // $1 = search term
			// Function to convert result of uri into an array of category names
			handler: function ( queryResult, queryKey ) {
				if ( queryResult && queryResult.length >= 2 ) {
					var key = queryResult[ 0 ].substring( queryResult[ 0 ].indexOf( ':' ) + 1 );
					var titles = queryResult[ 1 ];
					var exists = false;
					if ( !cat_prefix ) cat_prefix = new RegExp( '^(' + HC.category_regexp + '):' );

					for ( var i = 0; i < titles.length; i++ ) {
						cat_prefix.lastIndex = 0;
						var m = cat_prefix.exec( titles[ i ] );
						if ( m && m.length > 1 ) {
							titles[ i ] = titles[ i ].substring( titles[ i ].indexOf( ':' ) + 1 ); // rm namespace
							if ( key === titles[ i ] ) exists = true;
						} else {
							titles.splice( i, 1 ); // Nope, it's not a category after all.
							i--;
						}
					}
					titles.exists = exists;
					if ( queryKey !== key ) titles.normalized = key;
					// Remember the NFC normalized key we got back from the server
					return titles;
				}
				return null;
			}
		},
		internalsearch: {
			uri: '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1&apprefix=$1',
			handler: function ( queryResult ) {
				if ( queryResult && queryResult.query && queryResult.query.allpages ) {
					var titles = queryResult.query.allpages;
					for ( var i = 0; i < titles.length; i++ ) titles[ i ] = titles[ i ].title.substring( titles[ i ].title.indexOf( ':' ) + 1 ); // rm namespace

					return titles;
				}
				return null;
			}
		},
		exists: {
			uri: '/api.php?format=json&action=query&prop=info&titles=Category:$1',
			handler: function ( queryResult, queryKey ) {
				if ( queryResult && queryResult.query && queryResult.query.pages && !queryResult.query.pages[ -1 ] ) {
					// Should have exactly 1
					for ( var p in queryResult.query.pages ) {
						var title = queryResult.query.pages[ p ].title;
						title = title.substring( title.indexOf( ':' ) + 1 );
						var titles = [ title ];
						titles.exists = true;
						if ( queryKey !== title ) titles.normalized = title;
						// NFC
						return titles;
					}
				}
				return null;
			}
		},
		subcategories: {
			uri: '/api.php?format=json&action=query&list=categorymembers&cmtype=subcat&cmlimit=max&cmtitle=Category:$1',
			handler: function ( queryResult ) {
				if ( queryResult && queryResult.query && queryResult.query.categorymembers ) {
					var titles = queryResult.query.categorymembers;
					for ( var i = 0; i < titles.length; i++ ) titles[ i ] = titles[ i ].title.substring( titles[ i ].title.indexOf( ':' ) + 1 ); // rm namespace

					return titles;
				}
				return null;
			}
		},
		parentcategories: {
			uri: '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max',
			handler: function ( queryResult ) {
				if ( queryResult && queryResult.query && queryResult.query.pages ) {
					for ( var p in queryResult.query.pages ) {
						if ( queryResult.query.pages[ p ].categories ) {
							var titles = queryResult.query.pages[ p ].categories;
							for ( var i = 0; i < titles.length; i++ ) titles[ i ] = titles[ i ].title.substring( titles[ i ].title.indexOf( ':' ) + 1 ); // rm namespace

							return titles;
						}
					}
				}
				return null;
			}
		}
	};

	var suggestionConfigs = {
		searchindex: {
			name: 'Search index',
			engines: [ 'opensearch' ],
			cache: {},
			show: true,
			temp: false,
			noCompletion: false
		},
		pagelist: {
			name: 'Page list',
			engines: [ 'internalsearch', 'exists' ],
			cache: {},
			show: true,
			temp: false,
			noCompletion: false
		},
		combined: {
			name: 'Combined search',
			engines: [ 'opensearch', 'internalsearch' ],
			cache: {},
			show: true,
			temp: false,
			noCompletion: false
		},
		subcat: {
			name: 'Subcategories',
			engines: [ 'subcategories' ],
			cache: {},
			show: true,
			temp: true,
			noCompletion: true
		},
		parentcat: {
			name: 'Parent categories',
			engines: [ 'parentcategories' ],
			cache: {},
			show: true,
			temp: true,
			noCompletion: true
		}
	};

	CategoryEditor.UNCHANGED = 0;
	CategoryEditor.OPEN = 1; // Open, but no input yet
	CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
	CategoryEditor.CHANGED = 3;
	CategoryEditor.DELETED = 4;

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

	function forceRedraw() {
		if ( dummyElement.parentNode ) document.body.removeChild( dummyElement ); else document.body.appendChild( dummyElement );
	}

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

	CategoryEditor.prototype = {

		initialize: function ( line, span, after, key, is_hidden ) {
			// If a span is given, 'after' is the category title, otherwise it may be an element after which to
			// insert the new span. 'key' is likewise overloaded; if a span is given, it is the category key (if
			// known), otherwise it is a boolean indicating whether a bar shall be prepended.
			if ( !span ) {
				this.isAddCategory = true;
				// Create add span and append to catLinks
				this.originalCategory = '';
				this.originalKey = null;
				this.originalExists = false;
				if ( !newDOM ) {
					span = make( 'span' );
					span.className = 'noprint';
					if ( key ) {
						span.appendChild( make( ' | ', true ) );
						if ( after ) {
							after.parentNode.insertBefore( span, after.nextSibling );
							after = after.nextSibling;
						} else if (line) {
							line.appendChild( span );
						}
					} else if ( line && line.firstChild ) {
						span.appendChild( make( ' ', true ) );
						line.appendChild( span );
					}
				}
				this.linkSpan = make( 'span' );
				this.linkSpan.className = 'noprint nopopups hotcatlink';
				var lk = make( 'a' );
				lk.href = '#catlinks';
				lk.onclick = this.open.bind( this );
				lk.appendChild( make( HC.links.add, true ) );
				lk.title = HC.tooltips.add;
				this.linkSpan.appendChild( lk );
				span = make( newDOM ? 'li' : 'span' );
				span.className = 'noprint';
				if ( is_rtl ) span.dir = 'rtl';

				span.appendChild( this.linkSpan );
				if ( after ) {
					after.parentNode.insertBefore( span, after.nextSibling );
				} else if ( line ) {
					line.appendChild( span );
				}

				this.normalLinks = null;
				this.undelLink = null;
				this.catLink = null;
			} else {
				if ( is_rtl ) span.dir = 'rtl';

				this.isAddCategory = false;
				this.catLink = span.firstChild;
				this.originalCategory = after;
				this.originalKey = ( key && key.length > 1 ) ? key.substr( 1 ) : null; // > 1 because it includes the leading bar
				this.originalExists = !hasClass( this.catLink, 'new' );
				// Create change and del links
				this.makeLinkSpan();
				if ( !this.originalExists && this.upDownLinks ) this.upDownLinks.style.display = 'none';

				span.appendChild( this.linkSpan );
			}
			this.originalHidden = is_hidden;
			this.line = line;
			this.engine = HC.suggestions;
			this.span = span;
			this.currentCategory = this.originalCategory;
			this.currentExists = this.originalExists;
			this.currentHidden = this.originalHidden;
			this.currentKey = this.originalKey;
			this.state = CategoryEditor.UNCHANGED;
			this.lastSavedState = CategoryEditor.UNCHANGED;
			this.lastSavedCategory = this.originalCategory;
			this.lastSavedKey = this.originalKey;
			this.lastSavedExists = this.originalExists;
			this.lastSavedHidden = this.originalHidden;
			if ( this.catLink && this.currentKey ) this.catLink.title = this.currentKey;

			editors[ editors.length ] = this;
		},

		makeLinkSpan: function () {
			this.normalLinks = make( 'span' );
			var lk = null;
			if ( this.originalCategory && this.originalCategory.length ) {
				lk = make( 'a' );
				lk.href = '#catlinks';
				lk.onclick = this.remove.bind( this );
				lk.appendChild( make( HC.links.remove, true ) );
				lk.title = HC.tooltips.remove;
				this.normalLinks.appendChild( make( ' ', true ) );
				this.normalLinks.appendChild( lk );
			}
			if ( !HC.template_categories[ this.originalCategory ] ) {
				lk = make( 'a' );
				lk.href = '#catlinks';
				lk.onclick = this.open.bind( this );
				lk.appendChild( make( HC.links.change, true ) );
				lk.title = HC.tooltips.change;
				this.normalLinks.appendChild( make( ' ', true ) );
				this.normalLinks.appendChild( lk );
				if ( !noSuggestions && HC.use_up_down ) {
					this.upDownLinks = make( 'span' );
					lk = make( 'a' );
					lk.href = '#catlinks';
					lk.onclick = this.down.bind( this );
					lk.appendChild( make( HC.links.down, true ) );
					lk.title = HC.tooltips.down;
					this.upDownLinks.appendChild( make( ' ', true ) );
					this.upDownLinks.appendChild( lk );
					lk = make( 'a' );
					lk.href = '#catlinks';
					lk.onclick = this.up.bind( this );
					lk.appendChild( make( HC.links.up, true ) );
					lk.title = HC.tooltips.up;
					this.upDownLinks.appendChild( make( ' ', true ) );
					this.upDownLinks.appendChild( lk );
					this.normalLinks.appendChild( this.upDownLinks );
				}
			}
			this.linkSpan = make( 'span' );
			this.linkSpan.className = 'noprint nopopups hotcatlink';
			this.linkSpan.appendChild( this.normalLinks );
			this.undelLink = make( 'span' );
			this.undelLink.className = 'nopopups hotcatlink';
			this.undelLink.style.display = 'none';
			lk = make( 'a' );
			lk.href = '#catlinks';
			lk.onclick = this.restore.bind( this );
			lk.appendChild( make( HC.links.restore, true ) );
			lk.title = HC.tooltips.restore;
			this.undelLink.appendChild( make( ' ', true ) );
			this.undelLink.appendChild( lk );
			this.linkSpan.appendChild( this.undelLink );
		},

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

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

		makeForm: function () {
			var form = make( 'form' );
			form.method = 'POST';
			form.onsubmit = this.accept.bind( this );
			this.form = form;
			var self = this;
			var text = make( 'input' );
			text.type = 'text';
			text.size = HC.editbox_width;
			if ( !noSuggestions ) {
				// Be careful here to handle IME input. This is browser/OS/IME dependent, but basically there are two mechanisms:
				// - Modern (DOM Level 3) browsers use compositionstart/compositionend events to signal composition; if the
				//   composition is not canceled, there'll be a textInput event following. During a composition key events are
				//   either all suppressed (FF/Gecko), or otherwise have keyDown === IME for all keys (Webkit).
				//   - Webkit sends a textInput followed by keyDown === IME and a keyUp with the key that ended composition.
				//   - Gecko doesn't send textInput but just a keyUp with the key that ended composition, without sending keyDown
				//     first. Gecko doesn't send any keydown while IME is active.
				// - Older browsers signal composition by keyDown === IME for the first and subsequent keys for a composition. The
				//   first keyDown !== IME is certainly after the end of the composition. Typically, composition end can also be
				//   detected by a keyDown IME with a keyUp of space, tab, escape, or return.
				text.onkeyup = function ( evt ) {
					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 ) {
					var key = evt.keyCode || 0;
					self.lastKey = key;
					self.keyCount = 0;
					// DOM Level < 3 IME input
					if ( !self.ime && key === IME && !self.usesComposition ) {
						// self.usesComposition catches browsers that may emit spurious keydown IME after a composition has ended
						self.ime = true;
					} else if ( self.ime && key !== IME && !( key >= 16 && key <= 20 || key >= 91 && key <= 93 || key === 144 ) ) {
						// Ignore control keys: ctrl, shift, alt, alt gr, caps lock, windows/apple cmd keys, num lock. Only the windows keys
						// terminate IME (apple cmd doesn't), but they also cause a blur, so it's OK to ignore them here.
						// Note: Safari 4 (530.17) propagates ESC out of an IME composition (observed at least on Win XP).
						self.ime = false;
					}
					if ( self.ime ) return true;

					// Handle return explicitly, to override the default form submission to be able to check for ctrl
					if ( key === RET ) return self.accept( evt );

					// Inhibit default behavior of ESC (revert to last real input in FF: we do that ourselves)
					return ( key === ESC ) ? evtKill( evt ) : true;
				};
				// And handle continued pressing of arrow keys
				text.onkeypress = function ( evt ) {
					self.keyCount++;
					return self.processKey( evt );
				};
				$( text ).on( 'focus', function () {
					makeActive( self );
				} );
				// On IE, blur events are asynchronous, and may thus arrive after the element has lost the focus. Since IE
				// can get the selection only while the element is active (has the focus), we may not always get the selection.
				// Therefore, use an IE-specific synchronous event on IE...
				// Don't test for text.selectionStart being defined;
				$( text ).on(
					( text.onbeforedeactivate !== undefined && text.createTextRange ) ? 'beforedeactivate' : 'blur',
					this.saveView.bind( this ) );
				// DOM Level 3 IME handling
				try {
					// Setting lastKey = IME provides a fake keyDown for Gecko's single keyUp after a cmposition. If we didn't do this,
					// cancelling a composition via ESC would also cancel and close the whole category input editor.
					$( text ).on( 'compositionstart', function () {
						self.lastKey = IME;
						self.usesComposition = true;
						self.ime = true;
					} );
					$( text ).on( 'compositionend', function () {
						self.lastKey = IME;
						self.usesComposition = true;
						self.ime = false;
					} );
					$( text ).on( 'textInput', function () {
						self.ime = false;
						self.invokeSuggestions( false );
					} );
				} catch ( any ) {
					// Just in case some browsers might produce exceptions with these DOM Level 3 events
				}
				$( text ).on( 'blur', function () {
					self.usesComposition = false;
					self.ime = false;
				} );
			}
			this.text = text;

			this.icon = make( 'img' );

			var list = null;
			if ( !noSuggestions ) {
				list = make( 'select' );
				list.onclick = function () {
					if ( self.highlightSuggestion( 0 ) ) self.textchange( false, true );
				};
				list.ondblclick = function ( e ) {
					if ( self.highlightSuggestion( 0 ) ) self.accept( e );
				};
				list.onchange = function () {
					self.highlightSuggestion( 0 );
					self.text.focus();
				};
				list.onkeyup = function ( evt ) {
					if ( evt.keyCode === ESC ) {
						self.resetKeySelection();
						self.text.focus();
						window.setTimeout( function () {
							self.textchange( true );
						}, HC.suggest_delay );
					} else if ( evt.keyCode === RET ) {
						self.accept( evt );
					}
				};
				if ( !HC.fixed_search ) {
					var engineSelector = make( 'select' );
					for ( var key in suggestionConfigs ) {
						if ( suggestionConfigs[ key ].show ) {
							var opt = make( 'option' );
							opt.value = key;
							if ( key === this.engine ) opt.selected = true;

							opt.appendChild( make( suggestionConfigs[ key ].name, true ) );
							engineSelector.appendChild( opt );
						}
					}
					engineSelector.onchange = function () {
						self.engine = self.engineSelector.options[ self.engineSelector.selectedIndex ].value;
						self.text.focus();
						self.textchange( true, true ); // Don't autocomplete, force re-display of list
					};
					this.engineSelector = engineSelector;
				}
			}
			this.list = list;

			function button_label( id, defaultText ) {
				var label = null;
				if (
					onUpload &&
					window.UFUI !== undefined &&
					window.UIElements !== undefined &&
					UFUI.getLabel instanceof Function
				) {
					try {
						label = UFUI.getLabel( id, true );
						// Extract the plain text. IE doesn't know that Node.TEXT_NODE === 3
						while ( label && label.nodeType !== 3 ) label = label.firstChild;
					} catch ( ex ) {
						label = null;
					}
				}
				if ( !label || !label.data ) return defaultText;

				return label.data;
			}

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

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

			var span = make( 'span' );
			span.className = 'hotcatinput';
			span.style.position = 'relative';
			span.appendChild( text );

			// Support: 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 && this.line ) {
				// eslint-disable-next-line no-new
				new CategoryEditor( this.line, null, this.span, true ); // Create a new one
			}
			if ( !commitButton && !onUpload ) {
				for ( var i = 0; i < editors.length; i++ ) {
					if ( editors[ i ].state !== CategoryEditor.UNCHANGED ) {
						setMultiInput();
						break;
					}
				}
			}
			if ( !this.form ) this.makeForm();

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

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

			this.currentCategory = this.lastSavedCategory;
			this.currentExists = this.lastSavedExists;
			this.currentHidden = this.lastSavedHidden;
			this.currentKey = this.lastSavedKey;
			this.icon.src = ( this.currentExists ? HC.existsYes : HC.existsNo );
			this.text.value = this.currentCategory + ( this.currentKey !== null ? '|' + this.currentKey : '' );
			this.originalState = this.state;
			this.lastInput = this.currentCategory;
			this.inputExists = this.currentExists;
			this.state = this.state === CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
			this.lastSelection = {
				start: this.currentCategory.length,
				end: this.currentCategory.length
			};
			this.showsList = false;
			// Display the form
			if ( this.catLink ) this.catLink.style.display = 'none';

			this.linkSpan.style.display = 'none';
			this.form.style.display = 'inline';
			this.ok.disabled = false;
			// Kill the event before focussing, otherwise IE will kill the onfocus event!
			var result = evtKill( evt );
			this.text.focus();
			this.text.readOnly = false;
			checkMultiInput();
			return result;
		},

		show: function ( evt, engine, readOnly ) {
			var result = this.display( evt );
			var v = this.lastSavedCategory;
			if ( !v.length ) return result;

			this.text.readOnly = !!readOnly;
			this.engine = engine;
			this.textchange( false, true ); // do autocompletion, force display of suggestions
			forceRedraw();
			return result;
		},

		open: function ( evt ) {
			return this.show( evt, ( this.engine && suggestionConfigs[ this.engine ].temp ) ? HC.suggestions : this.engine );
		},

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

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

		cancel: function () {
			if ( this.isAddCategory && !onUpload ) {
				this.removeEditor(); // We added a new adder when opening
				return;
			}
			// Close, re-display link
			this.inactivate();
			this.form.style.display = 'none';
			if ( this.catLink ) this.catLink.style.display = '';

			this.linkSpan.style.display = '';
			this.state = this.originalState;
			this.currentCategory = this.lastSavedCategory;
			this.currentKey = this.lastSavedKey;
			this.currentExists = this.lastSavedExists;
			this.currentHidden = this.lastSavedHidden;
			if ( this.catLink )
				if ( this.currentKey && this.currentKey.length ) { this.catLink.title = this.currentKey; } else { this.catLink.title = ''; }

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

		removeEditor: function () {
			if ( !newDOM ) {
				var next = this.span.nextSibling;
				if ( next ) next.parentNode.removeChild( next );
			}
			this.span.parentNode.removeChild( this.span );
			for ( var i = 0; i < editors.length; i++ ) {
				if ( editors[ i ] === this ) {
					editors.splice( i, 1 );
					break;
				}
			}
			checkMultiInput();
		},

		rollback: function ( evt ) {
			this.undoLink.parentNode.removeChild( this.undoLink );
			this.undoLink = null;
			this.currentCategory = this.originalCategory;
			this.currentKey = this.originalKey;
			this.currentExists = this.originalExists;
			this.currentHidden = this.originalHidden;
			this.lastSavedCategory = this.originalCategory;
			this.lastSavedKey = this.originalKey;
			this.lastSavedExists = this.originalExists;
			this.lastSavedHidden = this.originalHidden;
			this.state = CategoryEditor.UNCHANGED;
			if ( !this.currentCategory || !this.currentCategory.length ) {
				// It was a newly added category. Remove the whole editor.
				this.removeEditor();
			} else {
				// Redisplay the link...
				this.catLink.removeChild( this.catLink.firstChild );
				this.catLink.appendChild( make( this.currentCategory, true ) );
				this.catLink.href = wikiPagePath( HC.category_canonical + ':' + this.currentCategory );
				this.catLink.title = this.currentKey || '';
				this.catLink.className = this.currentExists ? '' : 'new';
				this.catLink.style.backgroundColor = 'transparent';
				if ( this.upDownLinks ) this.upDownLinks.style.display = this.currentExists ? '' : 'none';

				checkMultiInput();
			}
			return evtKill( evt );
		},

		inactivate: function () {
			if ( this.list ) this.list.style.display = 'none';

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

			this.is_active = false;
		},

		acceptCheck: function ( dontCheck ) {
			this.sanitizeInput();
			var value = this.text.value.split( '|' );
			var key = null;
			if ( value.length > 1 ) key = value[ 1 ];

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

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

		accept: function ( evt ) {
			// eslint-disable-next-line no-bitwise
			this.noCommit = ( evtKeys( evt ) & 1 ) !== 0;
			var result = evtKill( evt );
			if ( this.acceptCheck() ) {
				var toResolve = [ this ];
				var original = this.currentCategory;
				resolveMulti( toResolve, function ( resolved ) {
					if ( resolved[ 0 ].dab ) {
						showDab( resolved[ 0 ] );
					} else {
						if ( resolved[ 0 ].acceptCheck( true ) ) {
							resolved[ 0 ].commit(
								( resolved[ 0 ].currentCategory !== original ) ?
									HC.messages.cat_resolved.replace( /\$1/g, original ) :
									null );
						}
					}
				} );
			}
			return result;
		},

		close: function () {
			if ( !this.catLink ) {
				// Create a catLink
				this.catLink = make( 'a' );
				this.catLink.appendChild( make( 'foo', true ) );
				this.catLink.style.display = 'none';
				this.span.insertBefore( this.catLink, this.span.firstChild.nextSibling );
			}
			this.catLink.removeChild( this.catLink.firstChild );
			this.catLink.appendChild( make( this.currentCategory, true ) );
			this.catLink.href = wikiPagePath( HC.category_canonical + ':' + this.currentCategory );
			this.catLink.className = this.currentExists ? '' : 'new';
			this.lastSavedCategory = this.currentCategory;
			this.lastSavedKey = this.currentKey;
			this.lastSavedExists = this.currentExists;
			this.lastSavedHidden = this.currentHidden;
			// Close form and redisplay category
			this.inactivate();
			this.form.style.display = 'none';
			this.catLink.title = this.currentKey || '';
			this.catLink.style.display = '';
			if ( this.isAddCategory ) {
				if ( onUpload && this.line ) {
					// eslint-disable-next-line no-new
					new CategoryEditor( this.line, null, this.span, true ); // Create a new one
				}
				this.isAddCategory = false;
				this.linkSpan.parentNode.removeChild( this.linkSpan );
				this.makeLinkSpan();
				this.span.appendChild( this.linkSpan );
			}
			if ( !this.undoLink ) {
				// Append an undo link.
				var span = make( 'span' );
				var lk = make( 'a' );
				lk.href = '#catlinks';
				lk.onclick = this.rollback.bind( this );
				lk.appendChild( make( HC.links.undo, true ) );
				lk.title = HC.tooltips.undo;
				span.appendChild( make( ' ', true ) );
				span.appendChild( lk );
				this.normalLinks.appendChild( span );
				this.undoLink = span;
				if ( !onUpload ) {
					try {
						this.catLink.style.backgroundColor = HC.bg_changed;
					} catch ( ex ) {}
				}
			}
			if ( this.upDownLinks ) this.upDownLinks.style.display = this.lastSavedExists ? '' : 'none';

			this.linkSpan.style.display = '';
			this.state = CategoryEditor.CHANGED;
			checkMultiInput();
			forceRedraw();
		},

		commit: function () {
			// Check again to catch problem cases after redirect resolution
			if (
				(
					this.currentCategory === this.originalCategory &&
					(
						this.currentKey === this.originalKey ||
						this.currentKey === null && !this.originalKey.length
					)
				) ||
				conf.wgNamespaceNumber === 14 && this.currentCategory === conf.wgTitle ||
				HC.blacklist && HC.blacklist.test( this.currentCategory )
			) {
				this.cancel();
				return;
			}
			this.close();
			if ( !commitButton && !onUpload ) {
				var self = this;
				initiateEdit( function ( failure ) {
					performChanges( failure, self );
				}, function ( msg ) {
					alert( msg );
				} );
			}
		},

		remove: function ( evt ) {
			// eslint-disable-next-line no-bitwise
			this.doRemove( evtKeys( evt ) & 1 );
			return evtKill( evt );
		},

		doRemove: function ( noCommit ) {
			if ( this.isAddCategory ) { // Empty input on adding a new category
				this.cancel();
				return;
			}
			if ( !commitButton && !onUpload ) {
				for ( var i = 0; i < editors.length; i++ ) {
					if ( editors[ i ].state !== CategoryEditor.UNCHANGED ) {
						setMultiInput();
						break;
					}
				}
			}
			if ( commitButton ) {
				this.catLink.title = '';
				this.catLink.style.cssText += '; text-decoration : line-through !important;';
				try {
					this.catLink.style.backgroundColor = HC.bg_changed;
				} catch ( ex ) {}
				this.originalState = this.state;
				this.state = CategoryEditor.DELETED;
				this.normalLinks.style.display = 'none';
				this.undelLink.style.display = '';
				checkMultiInput();
			} else {
				if ( onUpload ) {
					// Remove this editor completely
					this.removeEditor();
				} else {
					this.originalState = this.state;
					this.state = CategoryEditor.DELETED;
					this.noCommit = noCommit || HC.del_needs_diff;
					var self = this;
					initiateEdit(
						function ( failure ) {
							performChanges( failure, self );
						},
						function ( msg ) {
							self.state = self.originalState;
							alert( msg );
						} );
				}
			}
		},

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

		// Internal operations

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

		sanitizeInput: function () {
			var v = this.text.value || '';
			v = v.replace( /^(\s|_)+/, '' ); // Trim leading blanks and underscores
			var re = new RegExp( '^(' + HC.category_regexp + '):' );
			if ( re.test( v ) ) v = v.substring( v.indexOf( ':' ) + 1 ).replace( /^(\s|_)+/, '' );
			v = v.replace(/\u200E$/, ''); // Trim ending left-to-right mark
			if ( HC.capitalizePageNames ) v = capitalize( v );

			// Only update the input field if there is a difference. Various browsers otherwise
			// reset the selection and cursor position after each value re-assignment.
			if ( this.text.value !== null && this.text.value !== v ) this.text.value = v;
		},

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

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

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

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

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

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

					cb = undefined;
				}
			}

			$.getJSON( url, function ( json ) {
				var titles = e.handler( json, z );
				if ( titles && titles.length ) {
					if ( cb.allTitles === null ) cb.allTitles = titles; else cb.allTitles = cb.allTitles.concat( titles );
					if ( titles.exists ) cb.exists = true;
					if ( titles.normalized ) cb.normalized = titles.normalized;
				}
				done();
			} ).fail( function ( req ) {
				if ( !req ) noSuggestions = true;
				cb.dontCache = true;
				done();
			} );
		},

		callbackObj: null,

		textchange: function ( dont_autocomplete, force ) {
			// Hide all other lists
			makeActive( this );
			// Get input value, omit sort key, if any
			this.sanitizeInput();
			var v = this.text.value;
			// Disregard anything after a pipe.
			var pipe = v.indexOf( '|' );
			if ( pipe >= 0 ) {
				this.currentKey = v.substring( pipe + 1 );
				v = v.substring( 0, pipe );
			} else {
				this.currentKey = null;
			}
			if ( this.lastInput === v && !force ) return; // No change
			if ( this.lastInput !== v ) checkMultiInput();

			this.lastInput = v;
			this.lastRealInput = v;

			// Mark blacklisted inputs.
			this.ok.disabled = v.length && HC.blacklist && HC.blacklist.test( v );

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

			if ( !v.length ) {
				this.showSuggestions( [] );
				return;
			}
			var cleanKey = v.replace( /[\u200E\u200F\u202A-\u202E]/g, '' ).replace( wikiTextBlankRE, ' ' );
			cleanKey = replaceShortcuts( cleanKey, HC.shortcuts );
			cleanKey = cleanKey.replace( /^\s+|\s+$/g, '' );
			if ( !cleanKey.length ) {
				this.showSuggestions( [] );
				return;
			}

			if ( this.callbackObj ) this.callbackObj.cancelled = true;

			var engineName = suggestionConfigs[ this.engine ] ? this.engine : 'combined';

			dont_autocomplete = dont_autocomplete || suggestionConfigs[ engineName ].noCompletion;
			if ( suggestionConfigs[ engineName ].cache[ cleanKey ] ) {
				this.showSuggestions( suggestionConfigs[ engineName ].cache[ cleanKey ], dont_autocomplete, v, engineName );
				return;
			}

			var engines = suggestionConfigs[ engineName ].engines;
			this.callbackObj = {
				allTitles: null,
				callsMade: 0,
				nofCalls: engines.length,
				noCompletion: dont_autocomplete,
				engineName: engineName
			};
			this.makeCalls( engines, this.callbackObj, v, cleanKey );
		},

		makeCalls: function ( engines, cb, v, cleanKey ) {
			for ( var j = 0; j < engines.length; j++ ) {
				var engine = suggestionEngines[ engines[ j ] ];
				var url = conf.wgServer + conf.wgScriptPath + engine.uri.replace( /\$1/g, encodeURIComponent( cleanKey ) );
				this.makeCall( url, cb, engine, v, cleanKey );
			}
		},

		showSuggestions: function ( titles, dontAutocomplete, queryKey, engineName ) {
			this.text.readOnly = false;
			this.dab = null;
			this.showsList = false;
			if ( !this.list ) return;
			if ( noSuggestions ) {
				if ( this.list ) this.list.style.display = 'none';

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

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

				this.inputExists = true; // Default...
				return;
			}
			this.engineName = engineName;
			if ( engineName ) {
				if ( !this.engineSelector ) this.engineName = null;
			} else {
				if ( this.engineSelector ) this.engineSelector.style.display = 'none';
			}
			if ( queryKey ) {
				if ( this.lastInput.indexOf( queryKey ) ) return;
				if ( this.lastQuery && this.lastInput.indexOf( this.lastQuery ) === 0 && this.lastQuery.length > queryKey.length ) return;
			}
			this.lastQuery = queryKey;

			// Get current input text
			var v = this.text.value.split( '|' );
			var key = v.length > 1 ? '|' + v[ 1 ] : '';
			v = ( HC.capitalizePageNames ? capitalize( v[ 0 ] ) : v[ 0 ] );
			var vNormalized = v;
			var knownToExist = titles && titles.exists;
			var i;
			if ( titles ) {
				if ( titles.normalized && v.indexOf( queryKey ) === 0 ) {
				// We got back a different normalization than what is in the input field
					vNormalized = titles.normalized + v.substring( queryKey.length );
				}
				var vLow = vNormalized.toLowerCase();
				// Strip blacklisted categories
				if ( HC.blacklist ) {
					for ( i = 0; i < titles.length; i++ ) {
						if ( HC.blacklist.test( titles[ i ] ) ) {
							titles.splice( i, 1 );
							i--;
						}
					}
				}
				titles.sort(
					function ( a, b ) {
						if ( a === b ) return 0;

						if ( a.indexOf( b ) === 0 ) return 1;
						// a begins with b: a > b
						if ( b.indexOf( a ) === 0 ) return -1;
						// b begins with a: a < b
						// Opensearch may return stuff not beginning with the search prefix!
						var prefixMatchA = ( a.indexOf( vNormalized ) === 0 ? 1 : 0 );
						var prefixMatchB = ( b.indexOf( vNormalized ) === 0 ? 1 : 0 );
						if ( prefixMatchA !== prefixMatchB ) return prefixMatchB - prefixMatchA;

						// Case-insensitive prefix match!
						var aLow = a.toLowerCase(),
							bLow = b.toLowerCase();
						prefixMatchA = ( aLow.indexOf( vLow ) === 0 ? 1 : 0 );
						prefixMatchB = ( bLow.indexOf( vLow ) === 0 ? 1 : 0 );
						if ( prefixMatchA !== prefixMatchB ) return prefixMatchB - prefixMatchA;

						if ( a < b ) return -1;

						if ( b < a ) return 1;

						return 0;
					} );
				// Remove duplicates and self-references
				for ( i = 0; i < titles.length; i++ ) {
					if (
						i + 1 < titles.length && titles[ i ] === titles[ i + 1 ] ||
						conf.wgNamespaceNumber === 14 && titles[ i ] === conf.wgTitle
					) {
						titles.splice( i, 1 );
						i--;
					}
				}
			}
			if ( !titles || !titles.length ) {
				if ( this.list ) this.list.style.display = 'none';

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

				if ( engineName && suggestionConfigs[ engineName ] && !suggestionConfigs[ engineName ].temp ) {
					if ( this.icon ) this.icon.src = HC.existsNo;

					this.inputExists = false;
				}
				return;
			}

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

					return;
				}
			}
			// (Re-)fill the list
			while ( this.list.firstChild ) this.list.removeChild( this.list.firstChild );

			for ( i = 0; i < titles.length; i++ ) {
				var opt = make( 'option' );
				opt.appendChild( make( titles[ i ], true ) );
				opt.selected = completed && ( i === 0 );
				this.list.appendChild( opt );
			}
			this.displayList();
		},

		displayList: function () {
			this.showsList = true;
			if ( !this.is_active ) {
				this.list.style.display = 'none';
				if ( this.engineSelector ) this.engineSelector.style.display = 'none';

				return;
			}
			var nofItems = ( this.list.options.length > HC.listSize ? HC.listSize : this.list.options.length );
			if ( nofItems <= 1 ) nofItems = 2;

			this.list.size = nofItems;
			this.list.style.align = is_rtl ? 'right' : 'left';
			this.list.style.zIndex = 5;
			this.list.style.position = 'absolute';
			// Compute initial list position. First the height.
			var anchor = is_rtl ? 'right' : 'left';
			var listh = 0;
			if ( this.list.style.display === 'none' ) {
				// Off-screen display to get the height
				this.list.style.top = this.text.offsetTop + 'px';
				this.list.style[ anchor ] = '-10000px';
				this.list.style.display = '';
				listh = this.list.offsetHeight;
				this.list.style.display = 'none';
			} else {
				listh = this.list.offsetHeight;
			}
			// Approximate calculation of maximum list size
			var maxListHeight = listh;
			if ( nofItems < HC.listSize ) maxListHeight = ( listh / nofItems ) * HC.listSize;

			function viewport( what ) {
				if ( is_webkit && !document.evaluate ) {
				// Safari < 3.0
					return window[ 'inner' + what ];
				}
				var s = 'client' + what;
				if ( window.opera ) return document.body[ s ];

				return ( document.documentElement ? document.documentElement[ s ] : 0 ) || document.body[ s ] || 0;
			}
			function scroll_offset( what ) {
				var s = 'scroll' + what;
				var result = ( document.documentElement ? document.documentElement[ s ] : 0 ) || document.body[ s ] || 0;
				if ( is_rtl && what === 'Left' ) {
					// RTL inconsistencies.
					// FF: 0 at the far right, then increasingly negative values.
					// IE >= 8: 0 at the far right, then increasingly positive values.
					// Webkit: scrollWidth - clientWidth at the far right, then down to zero.
					// Opera: don't know...
					if ( result < 0 ) result = -result;

					if ( !is_webkit ) result = scroll_offset( 'Width' ) - viewport( 'Width' ) - result;

					// Now all have webkit behavior, i.e. zero if at the leftmost edge.
				}
				return result;
			}
			function position( node ) {
				// Stripped-down simplified position function. It's good enough for our purposes.
				if ( node.getBoundingClientRect ) {
					var box = node.getBoundingClientRect();
					return {
						x: Math.round( box.left + scroll_offset( 'Left' ) ),
						y: Math.round( box.top + scroll_offset( 'Top' ) )
					};
				}
				var t = 0,
					l = 0;
				do {
					t += ( node.offsetTop || 0 );
					l += ( node.offsetLeft || 0 );
					node = node.offsetParent;
				} while ( node );
				return {
					x: l,
					y: t
				};
			}

			var textPos = position( this.text ),
				nl = 0,
				nt = 0,
				offset = 0,
				// Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
				textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
			if ( this.engineName ) {
				this.engineSelector.style.zIndex = 5;
				this.engineSelector.style.position = 'absolute';
				this.engineSelector.style.width = textBoxWidth + 'px';
				// Figure out the height of this selector: display it off-screen, then hide it again.
				if ( this.engineSelector.style.display === 'none' ) {
					this.engineSelector.style[ anchor ] = '-10000px';
					this.engineSelector.style.top = '0';
					this.engineSelector.style.display = '';
					offset = this.engineSelector.offsetHeight;
					this.engineSelector.style.display = 'none';
				} else {
					offset = this.engineSelector.offsetHeight;
				}
				this.engineSelector.style[ anchor ] = nl + 'px';
			}
			if ( textPos.y < maxListHeight + offset + 1 ) {
			// The list might extend beyond the upper border of the page. Let's avoid that by placing it
			// below the input text field.
				nt = this.text.offsetHeight + offset + 1;
				if ( this.engineName ) this.engineSelector.style.top = this.text.offsetHeight + 'px';
			} else {
				nt = -listh - offset - 1;
				if ( this.engineName ) this.engineSelector.style.top = -( offset + 1 ) + 'px';
			}
			this.list.style.top = nt + 'px';
			this.list.style.width = ''; // No fixed width (yet)
			this.list.style[ anchor ] = nl + 'px';
			if ( this.engineName ) {
				this.selectEngine( this.engineName );
				this.engineSelector.style.display = '';
			}
			this.list.style.display = 'block';
			// Set the width of the list
			if ( this.list.offsetWidth < textBoxWidth ) {
				this.list.style.width = textBoxWidth + 'px';
				return;
			}
			// If the list is wider than the textbox: make sure it fits horizontally into the browser window
			var scroll = scroll_offset( 'Left' );
			var view_w = viewport( 'Width' );
			var w = this.list.offsetWidth;
			var l_pos = position( this.list );
			var left = l_pos.x;
			var right = left + w;
			if ( left < scroll || right > scroll + view_w ) {
				if ( w > view_w ) {
					w = view_w;
					this.list.style.width = w + 'px';
					if ( is_rtl ) left = right - w; else right = left + w;
				}
				var relative_offset = 0;
				if ( left < scroll ) relative_offset = scroll - left; else if ( right > scroll + view_w ) relative_offset = -( right - scroll - view_w );

				if ( is_rtl ) relative_offset = -relative_offset;

				if ( relative_offset ) this.list.style[ anchor ] = ( nl + relative_offset ) + 'px';
			}
		},

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

			if ( dontModify || this.ime || !this.canSelect() ) return false;

			// If we can't select properly or an IME composition is ongoing, autocompletion would be a major annoyance to the user.
			if ( newVal.indexOf( actVal ) ) {
				// Maybe it'll work with the normalized value (NFC)?
				if ( normalizedActVal && newVal.indexOf( normalizedActVal ) === 0 ) {
					if ( this.lastRealInput === actVal ) this.lastRealInput = normalizedActVal;

					actVal = normalizedActVal;
				} else {
					return false;
				}
			}
			// Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
			// such that it can be easily removed by typing backspace if the suggestion is unwanted.
			this.text.focus();
			this.text.value = newVal + key;
			this.setSelection( actVal.length, newVal.length );
			return true;
		},

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

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

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

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

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

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

			var curr = this.list.selectedIndex;
			var tgt = -1;
			if ( dir === 0 ) {
				if ( curr < 0 || curr >= this.list.options.length ) return false;

				tgt = curr;
			} else {
				tgt = curr < 0 ? 0 : curr + dir;
				tgt = tgt < 0 ? 0 : tgt;
				if ( tgt >= this.list.options.length ) tgt = this.list.options.length - 1;
			}
			if ( tgt !== curr || dir === 0 ) {
				if ( curr >= 0 && curr < this.list.options.length && dir !== 0 ) this.list.options[ curr ].selected = false;

				this.list.options[ tgt ].selected = true;
				// Get current input text
				var v = this.text.value.split( '|' );
				var key = v.length > 1 ? '|' + v[ 1 ] : '';
				var completed = this.autoComplete( this.list.options[ tgt ].text, this.lastRealInput, null, key, false );
				if ( !completed || this.list.options[ tgt ].text === this.lastRealInput ) {
					this.text.value = this.list.options[ tgt ].text + key;
					if ( this.canSelect() ) this.setSelection( this.list.options[ tgt ].text.length, this.list.options[ tgt ].text.length );
				}
				this.lastInput = this.list.options[ tgt ].text;
				this.inputExists = true; // Might be wrong if from a dab list...
				if ( this.icon ) this.icon.src = HC.existsYes;

				this.state = CategoryEditor.CHANGE_PENDING;
			}
			return true;
		},

		resetKeySelection: function () {
			if ( noSuggestions || !this.list || this.list.style.display === 'none' ) return false;

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

	function initialize() {
		// User configurations: Do this here, called from the onload handler, so that users can
		// override it easily in their own user script files by just declaring variables. JSconfig
		// is some feature used at Wikimedia Commons.
		var config = ( window.JSconfig !== undefined && JSconfig.keys ) ? JSconfig.keys : {};
		HC.dont_add_to_watchlist = ( window.hotcat_dont_add_to_watchlist !== undefined ?
			!!window.hotcat_dont_add_to_watchlist :
			( config.HotCatDontAddToWatchlist !== undefined ? config.HotCatDontAddToWatchlist :
				HC.dont_add_to_watchlist ) );
		HC.no_autocommit = ( window.hotcat_no_autocommit !== undefined ?
			!!window.hotcat_no_autocommit : ( config.HotCatNoAutoCommit !== undefined ?
				config.HotCatNoAutoCommit :
				// On talk namespace default autocommit off
				( conf.wgNamespaceNumber % 2 ?
					true : HC.no_autocommit ) ) );
		HC.del_needs_diff = ( window.hotcat_del_needs_diff !== undefined ?
			!!window.hotcat_del_needs_diff :
			( config.HotCatDelNeedsDiff !== undefined ?
				config.HotCatDelNeedsDiff :
				HC.del_needs_diff ) );
		HC.suggest_delay = window.hotcat_suggestion_delay || config.HotCatSuggestionDelay || HC.suggest_delay;
		HC.editbox_width = window.hotcat_editbox_width || config.HotCatEditBoxWidth || HC.editbox_width;
		HC.suggestions = window.hotcat_suggestions || config.HotCatSuggestions || HC.suggestions;
		if ( typeof HC.suggestions !== 'string' || !suggestionConfigs[ HC.suggestions ] ) HC.suggestions = 'combined';

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

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

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

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

		HC.listSize = Math.min( HC.listSize, 30 ); // Max size

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

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

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

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

	function setup_upload() {
		onUpload = true;
		// Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
		var ip = document.getElementById( 'mw-htmlform-description' ) || document.getElementById( 'wpDestFile' );
		if ( !ip ) {
			ip = document.getElementById( 'wpDestFile' );
			while ( ip && ip.nodeName.toLowerCase() !== 'table' ) ip = ip.parentNode;
		}
		if ( !ip ) return;
		var reupload = document.getElementById( 'wpForReUpload' );
		var destFile = document.getElementById( 'wpDestFile' );
		if (
			( reupload && !!reupload.value ) ||
			( destFile && ( destFile.disabled || destFile.readOnly ) )
		) {
			return; // re-upload form...
		}
		// Insert a table row with two fields (label and empty category bar)
		var labelCell = make( 'td' );
		var lineCell = make( 'td' );
		// Create the category line
		catLine = make( 'div' );
		catLine.className = 'catlinks';
		catLine.id = 'catlinks';
		catLine.style.textAlign = is_rtl ? 'right' : 'left';
		// We'll be inside a table row. Make sure that we don't have margins or strange borders.
		catLine.style.margin = '0';
		catLine.style.border = 'none';
		lineCell.appendChild( catLine );
		// Create the label
		var label = null;
		if ( window.UFUI && window.UIElements && UFUI.getLabel instanceof Function ) {
			try {
				label = UFUI.getLabel( 'wpCategoriesUploadLbl' );
			} catch ( ex ) {
				label = null;
			}
		}
		if ( !label ) {
			labelCell.id = 'hotcatLabel';
			labelCell.appendChild( make( HC.categories, true ) );
		} else {
			labelCell.id = 'hotcatLabelTranslated';
			labelCell.appendChild( label );
		}
		labelCell.className = 'mw-label';
		labelCell.style.textAlign = 'right';
		labelCell.style.verticalAlign = 'middle';
		// Change the onsubmit handler
		var form = document.getElementById( 'upload' ) || document.getElementById( 'mw-upload-form' );
		if ( form ) {
			var newRow = ip.insertRow( -1 );
			newRow.appendChild( labelCell );
			newRow.appendChild( lineCell );
			form.onsubmit = ( function ( oldSubmit ) {
				return function () {
					var do_submit = true;
					if ( oldSubmit ) {
						if ( typeof oldSubmit === 'string' ) {
						// eslint-disable-next-line no-eval
							do_submit = eval( oldSubmit );
						} else if ( oldSubmit instanceof Function ) {
							do_submit = oldSubmit.apply( form, arguments );
						}
					}
					if ( !do_submit ) return false;
					closeForm();
					// Copy the categories
					var eb = document.getElementById( 'wpUploadDescription' ) || document.getElementById( 'wpDesc' );
					var addedOne = false;
					for ( var i = 0; i < editors.length; i++ ) {
						var t = editors[ i ].currentCategory;
						if ( !t ) continue;
						var key = editors[ i ].currentKey;
						var new_cat = '[[' + HC.category_canonical + ':' + t + ( key ? '|' + key : '' ) + ']]';
						// Only add if not already present
						var cleanedText = eb.value
							.replace( /<!--(\s|\S)*?-->/g, '' )
							.replace( /<nowiki>(\s|\S)*?<\/nowiki>/g, '' );
						if ( !find_category( cleanedText, t, true ) ) {
							eb.value += '\n' + new_cat;
							addedOne = true;
						}
					}
					if ( addedOne ) {
					// Remove "subst:unc" added by Flinfo if it didn't find categories
						eb.value = eb.value.replace( /\{\{subst:unc\}\}/g, '' );
					}
					return true;
				};
			}( form.onsubmit ) );
		}
	}

	var cleanedText = null;

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

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

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

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

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

	var initialized = false;
	var setupTimeout = null;

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

	function setup( additionalWork ) {
		if ( initialized ) return;
		initialized = true;
		if ( setupTimeout ) {
			window.clearTimeout( setupTimeout );
			setupTimeout = null;
		}
		// Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
		// each category, and add the + link.
		catLine =
			// Special:Upload
			catLine ||
			document.getElementById( 'mw-normal-catlinks' );
		var hiddenCats = document.getElementById( 'mw-hidden-catlinks' );
		if ( !catLine ) {
			var footer = null;
			if ( !hiddenCats ) {
				footer = findByClass( document, 'div', 'printfooter' );
				if ( !footer ) return; // Don't know where to insert the category line
			}
			catLine = make( 'div' );
			catLine.id = 'mw-normal-catlinks';
			catLine.style.textAlign = is_rtl ? 'right' : 'left';
			// Add a label
			var label = make( 'a' );
			label.href = conf.wgArticlePath.replace( '$1', 'Special:Categories' );
			label.title = HC.categories;
			label.appendChild( make( HC.categories, true ) );
			catLine.appendChild( label );
			catLine.appendChild( make( ':', true ) );
			// Insert the new category line
			var container = ( hiddenCats ? hiddenCats.parentNode : document.getElementById( 'catlinks' ) );
			if ( !container ) {
				container = make( 'div' );
				container.id = 'catlinks';
				footer.parentNode.insertBefore( container, footer.nextSibling );
			}
			container.className = 'catlinks noprint';
			container.style.display = '';
			if ( !hiddenCats ) container.appendChild( catLine ); else container.insertBefore( catLine, hiddenCats );
		} // end if catLine exists
		if ( is_rtl ) catLine.dir = 'rtl';

		// Create editors for all existing categories

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

		var lastSpan = createEditors( catLine, false );
		// Create one to add a new category
		// eslint-disable-next-line no-new
		new CategoryEditor( newDOM ? catLine.getElementsByTagName( 'ul' )[ 0 ] : catLine, null, null, lastSpan !== null, false );
		if ( !onUpload ) {
			if ( pageText !== null && hiddenCats ) {
				if ( is_rtl ) hiddenCats.dir = 'rtl';
				createEditors( hiddenCats, true );
			}
			// And finally add the "multi-mode" span. (Do this at the end, otherwise it ends up in the list above.)
			var enableMulti = make( 'span' );
			enableMulti.className = 'noprint';
			if ( is_rtl ) enableMulti.dir = 'rtl';
			catLine.insertBefore( enableMulti, catLine.firstChild.nextSibling );
			enableMulti.appendChild( make( '\xa0', true ) ); // nbsp
			multiSpan = make( 'span' );
			enableMulti.appendChild( multiSpan );
			multiSpan.innerHTML = '(<a>' + HC.addmulti + '</a>)';
			var lk = multiSpan.getElementsByTagName( 'a' )[ 0 ];
			lk.onclick = function ( evt ) {
				setMultiInput();
				checkMultiInput();
				return evtKill( evt );
			};
			lk.title = HC.multi_tooltip;
			lk.style.cursor = 'pointer';
		}
		cleanedText = null;
		if ( additionalWork instanceof Function ) additionalWork();
		mw.hook( 'hotcat.ready' ).fire(); // Execute registered callback functions
		$( 'body' ).trigger( 'hotcatSetupCompleted' );
	}

	function createCommitForm() {
		if ( commitForm ) return;
		var formContainer = make( 'div' );
		formContainer.style.display = 'none';
		document.body.appendChild( formContainer );
		formContainer.innerHTML =
			'<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="' +
			conf.wgScript + '?title=' + encodeURIComponent( conf.wgPageName ) + '&action=submit">' +
			'<input type="hidden" name="wpTextbox1">' +
			'<input type="hidden" name="model" value="' + conf.wgPageContentModel + '">' +
			'<input type="hidden" name="format" value="text/x-wiki">' +
			'<input type="hidden" name="wpSummary" value="">' +
			'<input type="checkbox" name="wpMinoredit" value="1">' +
			'<input type="checkbox" name="wpWatchthis" value="1">' +
			'<input type="hidden" name="wpAutoSummary" value="d41d8cd98f00b204e9800998ecf8427e">' +
			'<input type="hidden" name="wpEdittime">' +
			'<input type="hidden" name="wpStarttime">' +
			'<input type="hidden" name="wpDiff" value="wpDiff">' +
			'<input type="hidden" name="oldid" value="0">' +
			'<input type="submit" name="hcCommit" value="hcCommit">' +
			'<input type="hidden" name="wpEditToken">' +
			'<input type="hidden" name="wpUltimateParam" value="1">' +
			'<input type="hidden" name="wpChangeTags">' +
			'<input type="hidden" value="ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ" name="wpUnicodeCheck">' +
			'</form>';
		commitForm = document.getElementById( 'hotcatCommitForm' );
	}

	function getPage() {
		// We know we have an article here.
		if ( !conf.wgArticleId ) {
			// Doesn't exist yet. Disable on non-existing User pages -- might be a global user page.
			if ( conf.wgNamespaceNumber === 2 ) return;
			pageText = '';
			pageTime = null;
			setup( createCommitForm );
		} else {
			var url = conf.wgServer + conf.wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&rawcontinue=&titles=' +
			encodeURIComponent( conf.wgPageName ) +
			'&prop=info%7Crevisions&rvprop=content%7Ctimestamp%7Cids&meta=siteinfo&rvlimit=1&rvstartid=' +
			conf.wgCurRevisionId;
			var s = make( 'script' );
			s.src = url;
			HC.start = function ( json ) {
				setPage( json );
				setup( createCommitForm );
			};
			document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
			setupTimeout = window.setTimeout( function () {
				setup( createCommitForm );
			}, 4000 ); // 4 sec, just in case getting the wikitext takes longer.
		}
	}

	function setState( state ) {
		var cats = state.split( '\n' );
		if ( !cats.length ) return null;

		if ( initialized && editors.length === 1 && editors[ 0 ].isAddCategory ) {
			// Insert new spans and create new editors for them.
			var newSpans = [];
			var before = editors.length === 1 ? editors[ 0 ].span : null;
			var i;
			for ( i = 0; i < cats.length; i++ ) {
				if ( !cats[ i ].length ) continue;
				var cat = cats[ i ].split( '|' );
				var key = cat.length > 1 ? cat[ 1 ] : null;
				cat = cat[ 0 ];
				var lk = make( 'a' );
				lk.href = wikiPagePath( HC.category_canonical + ':' + cat );
				lk.appendChild( make( cat, true ) );
				lk.title = cat;
				var span = make( 'span' );
				span.appendChild( lk );
				if ( !i ) catLine.insertBefore( make( ' ', true ), before );

				catLine.insertBefore( span, before );
				if ( before && i + 1 < cats.length ) parent.insertBefore( make( ' | ', true ), before );

				newSpans.push( {
					element: span,
					title: cat,
					key: key
				} );
			}
			// And change the last one...
			if ( before ) before.parentNode.insertBefore( make( ' | ', true ), before );

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

	function getState() {
		var result = null;
		for ( var i = 0; i < editors.length; i++ ) {
			var text = editors[ i ].currentCategory;
			var key = editors[ i ].currentKey;
			if ( text && text.length ) {
				if ( key !== null ) text += '|' + key;
				if ( result === null ) result = text; else result += '\n' + text;
			}
		}
		return result;
	}

	function really_run() {
		initialize();

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

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

	// Export legacy functions
	window.hotcat_get_state = function () {
		return getState();
	};
	window.hotcat_set_state = function ( state ) {
		return setState( state );
	};
	window.hotcat_close_form = function () {
		closeForm();
	};
	HC.runWhenReady = function ( callback ) {
		// run user-registered code once HotCat is fully set up and ready.
		mw.hook( 'hotcat.ready' ).add( callback );
	};

	// Make sure we don't get conflicts with AjaxCategories (core development that should one day
	// replace HotCat).
	mw.config.set( 'disableAJAXCategories', true );

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

	if ( conf.wgCanonicalSpecialPageName !== 'Upload' ) {
		// Reload HotCat after (VE) edits (bug T103285)
		mw.hook( 'postEdit' ).add( function () {
			// Reset HotCat in case this is a soft reload (VE edit)
			catLine = null;
			editors = [];
			initialized = false;
			HC.started = false;
			run();
		} );
	}

	// We can safely trigger just after user configuration is loaded.
	// Use always() instead of then() to also start HotCat if the user module has problems.
	$.when( mw.loader.using( 'user' ), $.ready ).always( run );
}( jQuery, mediaWiki ) );
// </nowiki>