User:Surjection/HotCat.js

// // See also: MediaWiki:Gadget-HotCat.js/local defaults //window.hotcat_translations_from_commons = true; // Make HotCat load its interface from the Commons (the default setting so no need to set it here)

/** HotCat V2.43 en.wiktionary fork (with better support for en.wiktionary templates) upstream: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-HotCat.js&oldid=578342698

The original script is hosted at. See it for more information.

License: Quadruple licensed, any of the following: GFDL GPL LGPL Creative Commons Attribution 3.0 (CC-BY-3.0)

Requires MediaWiki >= MW 1.27. // /* 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 ) {	// BEGINNING OF en.wiktionary EXTRA CODE	var EN_WIKTIONARY_NS0 = document.body.className.split(/\s+/).indexOf("ns-0") >= 0;	var EN_WIKTIONARY_TOPICS = ["C", "c", "topics", "top", "topic", "catlangcode"];	var EN_WIKTIONARY_CATLANGNAME = ["cln", "catlangname", "Cln"];	var EN_WIKTIONARY_CATEGORIZE = ["categorize", "cat"];	var EN_WIKTIONARY_PREFER_TOPICS_FOR_NEW_ADDITIONS = true;	var EN_WIKTIONARY_PREFER_CATLANGNAME_FOR_NEW_ADDITIONS = true;	function regexpEscape(string) {		return string.replace(/[-\/\\^$*+?.|[\]{}]/g, '\\$&');	}	function cleanUpText(wikitext) {		return wikitext		.replace( //g, replaceByBlanks )		.replace( / (\s|\S)*?<\/nowiki>/g, replaceByBlanks );	}

function en_wiktionary_find_category_allMatches(regexp, wikitext, data_key, data) { var copiedtext = cleanUpText(wikitext); var result = []; var curr_match = null; while ((curr_match = regexp.exec(copiedtext)) !== null) { var obj = { match: curr_match }; obj[data_key] = data; result.push(obj); }		result.re = regexp; return result.length ? result : null; }	function en_wiktionary_codeToCanonicalName(callback) { $.get({			"url": mw.util.wikiScript('index'),			"data": {				"action": "raw",				"title": "Module:languages/code to canonical name.json"			},			"cache": true		}).then(function(r) {			var parsedData;			try {				parsedData = JSON.parse(r);				callback({ ok: true, data: parsedData });			} catch (e) {				console.error(e);				callback({ ok: false });			}		}); }	function en_wiktionary_populate_langdata(data) { data.langnames = []; data.name_to_code = {}; for (var key in data.code_to_name) { if (data.code_to_name.hasOwnProperty(key)) { var value = data.code_to_name[key]; data.langnames.push(value); data.name_to_code[value] = key; }		}		var escaped = data.langnames.map(regexpEscape); // sort from longest to shortest to ensure greedy alternation escaped.sort(function (a, b) { return b.length - a.length; }); data.catlangnameRegexp = "^(" + escaped.join("|") + ") (.+)"; }	var EN_WIKTIONARY_LANGUAGE_DATA_STORAGE_KEY = "HotCat-en-wiktionary-language-data"; function en_wiktionary_cache_langdata(callback) { if (!window.sessionStorage) { callback; return; }		if (window.sessionStorage.hasOwnProperty(EN_WIKTIONARY_LANGUAGE_DATA_STORAGE_KEY)) { try { var timeNow = new Date.getTime; var data = JSON.parse(window.sessionStorage.getItem(EN_WIKTIONARY_LANGUAGE_DATA_STORAGE_KEY)); if (timeNow < data.expiry) { EN_WIKTIONARY_LANGDATA = data.data; EN_WIKTIONARY_LANGDATA_READY = true; EN_WIKTIONARY_LANGDATA.catlangnameRegexpCompiled = null; callback; return; }			} catch (e) { } }		en_wiktionary_codeToCanonicalName(function (r) {			if (r.ok) {				EN_WIKTIONARY_LANGDATA = {};				EN_WIKTIONARY_LANGDATA.code_to_name = r.data;				en_wiktionary_populate_langdata(EN_WIKTIONARY_LANGDATA);			}			EN_WIKTIONARY_LANGDATA_READY = true;			if (window.sessionStorage)				window.sessionStorage.setItem(EN_WIKTIONARY_LANGUAGE_DATA_STORAGE_KEY, JSON.stringify({					expiry: new Date.getTime + 30 * 60 * 1000, // expire in 30 minutes					data: EN_WIKTIONARY_LANGDATA				}));			callback;		}); }	var EN_WIKTIONARY_LANGDATA = null; var EN_WIKTIONARY_LANGDATA_READY = false; var EN_WIKTIONARY_LANGDATA_LOCK = false; var EN_WIKTIONARY_LANGDATA_QUEUE = []; function en_wiktionary_get_langdata(callback) { if (EN_WIKTIONARY_LANGDATA_READY) { callback; } else { if (!EN_WIKTIONARY_LANGDATA_LOCK) { EN_WIKTIONARY_LANGDATA_LOCK = true; en_wiktionary_cache_langdata(function {					callback;					EN_WIKTIONARY_LANGDATA_QUEUE.forEach(function(fn) { fn; });				}); return; }			EN_WIKTIONARY_LANGDATA_QUEUE.push(callback); }	}	function EN_WIKTIONARY_TOPICS_START(lang) { return "\\{\\{\\s*(?:" + EN_WIKTIONARY_TOPICS.join("|") + ")\\s*\\|" + lang; }	function EN_WIKTIONARY_CATLANGNAME_START(lang) { return "\\{\\{\\s*(?:" + EN_WIKTIONARY_CATLANGNAME.join("|") + ")\\s*\\|" + lang; }	function EN_WIKTIONARY_CATEGORIZE_START(lang) { return "\\{\\{\\s*(?:" + EN_WIKTIONARY_CATEGORIZE.join("|") + ")\\s*\\|" + (lang == true ? "(" : "") + "[a-z0-9-]+" + (lang == true ? ")" : "");	}	var EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS = "[^}]*"; var EN_WIKTIONARY_TEMPLATE_START_OF_PARAMETER = "?(?<=\\|)"; var EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER = "(?=\\s*[\|\}])"; var EN_WIKTIONARY_TEMPLATE_END = "\\}\\}"; function en_wiktionary_category_name_detect(category) { var topicsMatch = category.match(/^([a-z-]+):(.+)$/); if (topicsMatch) { return { type: "topics", lang: topicsMatch[1], topic: topicsMatch[2] }; }		if (EN_WIKTIONARY_LANGDATA) { var data = EN_WIKTIONARY_LANGDATA; if (!data.catlangnameRegexpCompiled) { data.catlangnameRegexpCompiled = new RegExp(data.catlangnameRegexp); }			var catlangnameMatch = category.match(data.catlangnameRegexpCompiled); if (catlangnameMatch && data.name_to_code[catlangnameMatch[1]]) { return { type: "catlangname", lang: data.name_to_code[catlangnameMatch[1]], topic: catlangnameMatch[2] }; }		}		return null; }	function en_wiktionary_find_category(wikitext, category, once) { if (!EN_WIKTIONARY_NS0) return null; var catname = en_wiktionary_category_name_detect(category); var data, matched; if (catname) { if (catname.type == "topics") { var topicTemplateRegex = new RegExp(					EN_WIKTIONARY_TOPICS_START(catname.lang) +					EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +					EN_WIKTIONARY_TEMPLATE_START_OF_PARAMETER +					regexpEscape(catname.topic) +					EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER +					EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +					EN_WIKTIONARY_TEMPLATE_END, "g"); data = { lang: catname.lang, name: catname.topic }; if (once) { matched = topicTemplateRegex.exec(wikitext); if (matched) { matched.enWiktTopics = data; }					return matched; }				return en_wiktionary_find_category_allMatches(topicTemplateRegex, wikitext, "enWiktTopics", data); } else if (catname.type == "catlangname") { var clnTemplateRegex = new RegExp(					EN_WIKTIONARY_CATLANGNAME_START(catname.lang) +					EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +					EN_WIKTIONARY_TEMPLATE_START_OF_PARAMETER +					regexpEscape(catname.topic) +					EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER +					EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +					EN_WIKTIONARY_TEMPLATE_END, "g"); data = { lang: catname.lang, name: catname.topic }; if (once) { matched = clnTemplateRegex.exec(wikitext); if (matched) { matched.enWiktCatlangname = data; }					return matched; }				return en_wiktionary_find_category_allMatches(clnTemplateRegex, wikitext, "enWiktCatlangname", data); }		}		var catTemplateRegex = new RegExp(EN_WIKTIONARY_CATEGORIZE_START(true) +					EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +					EN_WIKTIONARY_TEMPLATE_START_OF_PARAMETER +					regexpEscape(category) +					EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER +					EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +					EN_WIKTIONARY_TEMPLATE_END, "g"); var catTemplateMatches = wikitext.match(catTemplateRegex); if (catTemplateMatches) { data = { lang: catTemplateMatches[1], name: category }; if (once) { if (catTemplateMatches) { catTemplateMatches.enWiktCategorize = data; }				return catTemplateMatches; }			catTemplateRegex.lastIndex = 0; return en_wiktionary_find_category_allMatches(catTemplateRegex, wikitext, "enWiktCategorize", data); }		return null; }

function en_wiktionary_is_special_match(match) { return !!(match.enWiktTopics || match.enWiktCatlangname || match.enWiktCategorize); }	function en_wiktionary_three_to_two_split(wikitext, match) { var before, after, data, removeTopic; if (match.enWiktTopics) { // ? data = match.enWiktTopics; before = wikitext.substring(0, match.match.index); after = wikitext.substring(match.match.index); var topicTemplateRegex = new RegExp(				EN_WIKTIONARY_TOPICS_START(data.lang) + 				EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +				EN_WIKTIONARY_TEMPLATE_START_OF_PARAMETER +				regexpEscape(data.name) +				EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER +				EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +				EN_WIKTIONARY_TEMPLATE_END + "\\s*?\n?"); removeTopic = function(inner) { // remove parameter from var remaining = inner.replace(new RegExp("\\|" + regexpEscape(data.name) + EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER), ""); // if it is now empty, remove it				remaining = remaining.replace(new RegExp( EN_WIKTIONARY_TOPICS_START(data.lang) + "\\s*\\}\\}\n?"), ""); return remaining; };			return [before, after.replace(topicTemplateRegex, removeTopic)]; } else if (match.enWiktCatlangname) { // ? data = match.enWiktCatlangname; before = wikitext.substring(0, match.match.index); after = wikitext.substring(match.match.index); var clnTemplateRegex = new RegExp(				EN_WIKTIONARY_CATLANGNAME_START(data.lang) + 				EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +				EN_WIKTIONARY_TEMPLATE_START_OF_PARAMETER +				regexpEscape(data.name) +				EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER +				EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +				EN_WIKTIONARY_TEMPLATE_END + "\\s*?\n?"); removeTopic = function(inner) { // remove parameter from var remaining = inner.replace(new RegExp("\\|" + regexpEscape(data.name) + EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER), ""); // if it is now empty, remove it				remaining = remaining.replace(new RegExp( EN_WIKTIONARY_CATLANGNAME_START(data.lang) + "\\s*\\}\\}\n?"), ""); return remaining; };			return [before, after.replace(clnTemplateRegex, removeTopic)]; } else if (match.enWiktCategorize) { // ? data = match.enWiktCategorize; before = wikitext.substring(0, match.match.index); after = wikitext.substring(match.match.index); var catTemplateRegex = new RegExp(				EN_WIKTIONARY_CATEGORIZE_START(data.lang) + 				EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +				EN_WIKTIONARY_TEMPLATE_START_OF_PARAMETER +				regexpEscape(data.name) +				EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER +				EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS +				EN_WIKTIONARY_TEMPLATE_END + "\\s*?\n?"); removeTopic = function(inner) { // remove parameter from var remaining = inner.replace(new RegExp("\\|" + regexpEscape(data.name) + EN_WIKTIONARY_TEMPLATE_END_OF_PARAMETER), ""); // if it is now empty, remove it				remaining = remaining.replace(new RegExp( EN_WIKTIONARY_CATEGORIZE_START(data.lang) + "\\s*\\}\\}\n?"), ""); return remaining; };			return [before, after.replace(catTemplateRegex, removeTopic)]; }		before = wikitext.substring(0, match.match.index); after = wikitext.substring(match.match.index + match.match[0].length); return [before, after]; }	function en_wiktionary_remove_category(wikitext, match) { var before_after = en_wiktionary_three_to_two_split(wikitext, match); return before_after[0] + before_after[1]; }	function en_wiktionary_find_insertionpoint(wikitext, category) { var catname = en_wiktionary_category_name_detect(category); if (catname) { if (catname.lang) { var langname = catname.langname || (EN_WIKTIONARY_LANGDATA ? EN_WIKTIONARY_LANGDATA.code_to_name[catname.lang] : null); if (langname) { // try to put category under the language L2					var l2_heading_this = "^==\\s*" + regexpEscape(langname) + "\\s*==$"; var end_of_l2 = "\\n*(?:^$)?\\n*^==(?!=)\\s*.*\\s*==$"; var clean_wikitext = cleanUpText(wikitext); var finalRegexp = "(" + l2_heading_this + "[\\s\\S]+?)" + end_of_l2; var match = new RegExp(finalRegexp, "m").exec(clean_wikitext); if (match) { var rawPoint = match.index + match[1].length; var noPreLine = false; if (wikitext.substring(rawPoint - 2, rawPoint) == "]]") { return { idx: rawPoint, onCat: true };						}						// find line before horizontal rule var cookedPoint = wikitext.indexOf("", rawPoint); if (cookedPoint > 0) { cookedPoint--; } else { cookedPoint = rawPoint; }						if (wikitext.substring(cookedPoint - 3, cookedPoint) == "}}\n") { // if the preceding line has a topics/cln template, add noPreLine var i = cookedPoint - 2; while (i >= 0 && wikitext[i] !== '\n') { i--; }							i += 2; var previousLine = wikitext.substring(i, cookedPoint - 1); if (previousLine.match(new RegExp("^\\{\\{(?:" + EN_WIKTIONARY_TOPICS.join("|") + "|" + EN_WIKTIONARY_CATLANGNAME.join("|") + ")\\|"))) { noPreLine = true; }						}						return { idx: cookedPoint, onCat: false, noPreLine: noPreLine };					} else { var l2match = new RegExp(l2_heading_this, "m").exec(clean_wikitext); if (l2match) { // assuming there are no more L2s... var l2_regex = new RegExp("^==(?!=)\\s*.*\\s*==$", "gm"); l2_regex.lastIndex = l2match.index + l2match[0].length; var l2nextmatch = l2_regex.exec(clean_wikitext); if (!l2nextmatch) { // no more L2s return { idx: -1, onCat: false };							}						}					}				}			}		}		return null; // fallback }	function en_wiktionary_add_category_hook(wikitext, category, cat_point) { var catname = en_wiktionary_category_name_detect(category); if (catname) { var match, splitPoint; if (catname.type == "topics") { // try to find the first language template that matches and // add the category there var topicTemplateRegex = new RegExp("(?:^|(?<=\\n))(" + EN_WIKTIONARY_TOPICS_START(catname.lang) + EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS + ")" + EN_WIKTIONARY_TEMPLATE_END); match = topicTemplateRegex.exec(cleanUpText(wikitext)); if (match) { splitPoint = match.index + match[1].length; return wikitext.substring(0, splitPoint) + "|" + catname.topic + wikitext.substring(splitPoint); }			}			if (catname.type == "catlangname") { // try to find the first language template that matches and // add the category there var clnTemplateRegex = new RegExp("(?:^|(?<=\\n))(" + EN_WIKTIONARY_CATLANGNAME_START(catname.lang) + EN_WIKTIONARY_TEMPLATE_ANY_PARAMETERS + ")" + EN_WIKTIONARY_TEMPLATE_END); match = clnTemplateRegex.exec(cleanUpText(wikitext)); if (match) { splitPoint = match.index + match[1].length; return wikitext.substring(0, splitPoint) + "|" + catname.topic + wikitext.substring(splitPoint); }			}		}		return null; // fallback }	function en_wiktionary_add_category_string(category, onCat) { var catname = en_wiktionary_category_name_detect(category); if (catname) { if (catname.type == "topics" && EN_WIKTIONARY_PREFER_TOPICS_FOR_NEW_ADDITIONS && !onCat) { return ""; }			if (catname.type == "catlangname" && EN_WIKTIONARY_PREFER_CATLANGNAME_FOR_NEW_ADDITIONS && !onCat) { return ""; }		}		return null; // fallback }		// END OF en.wiktionary EXTRA CODE

// 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 = $.extend( {}, mw.config.values, {           // when running on mobile domain - do not use wgServer.            wgServer: window.location.host.indexOf('.m.') > -1 ?                '//' + window.location.host : mw.config.get( 'wgServer' )        } );

// 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 ', template_removed: 'removed ', cat_added: 'added ', cat_keychange: 'new key for : "$2"', // $2 is the new key cat_notFound: 'Category "$1" not found', cat_exists: 'Category "$1" already exists; not added.', cat_resolved: ' (redirect resolved)', uncat_removed: 'removed ', 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 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: ' ++ ', // 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: true, // 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; var result_hook = en_wiktionary_find_category(wikitext, category, once); if (result_hook) { return result_hook; }

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( //g, replaceByBlanks ) .replace( / (\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, category ) { var copiedtext = wikitext .replace( //g, replaceByBlanks ) .replace( / (\s|\S)*?<\/nowiki>/g, replaceByBlanks ); // Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element". var result = en_wiktionary_find_insertionpoint( wikitext, category ); if (result) { return result; }			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, noOnCatOverride = false; 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 after = wikitext; for (var i = matches.length - 1; i > 0; i--) { after = en_wiktionary_remove_category(after, matches[i]); }				var before_after = en_wiktionary_three_to_two_split(after, matches[0]); noOnCatOverride = en_wiktionary_is_special_match(matches[0]); var before = before_after[0]; after = before_after[1]; 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 hooked = en_wiktionary_add_category_hook( wikitext, toAdd, cat_point ); if (hooked) { wikitext = hooked; summary.push( HC.messages.cat_added.replace( /\$1/g, toAdd ) ); } else { var onCat = false; var noPreLine = false; if ( cat_point < 0 ) { var point = find_insertionpoint ( wikitext, toAdd ); cat_point = point.idx; onCat = point.onCat; noPreLine = point.noPreLine; } else { onCat = true; }					var newcatstring = en_wiktionary_add_category_string(toAdd, onCat && !noOnCatOverride) || ('|  ) + ); if ( cat_point >= 0 ) { var suffix = wikitext.substring( cat_point ); wikitext = wikitext.substring( 0, cat_point ) + ( cat_point > 0 && !noPreLine ? '\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'; if ( json.query.tokens ) editToken = json.query.tokens.csrftoken; 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&rvprop=content%7Ctimestamp%7Cids%7Cuser&lllimit=500' +			'&rvlimit=2&rvdir=newer&rvstartid=' + conf.wgCurRevisionId + '&meta=siteinfo%7Cuserinfo%7Ctokens&type=csrf&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 ); }			if (this.span && this.span.parentNode) { 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 = $( '' ).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 =  + key :  ) + ;						// Only add if not already present						var cleanedText = eb.value							.replace( //g,  ) .replace( / (\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( //g, '' ) .replace( / (\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 = '(' + HC.addmulti + ')'; 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 = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '<input type="hidden" name="wpEditToken">' + '<input type="hidden" name="wpUltimateParam" value="1">' + '<input type="hidden" name="wpChangeTags">' + '<input type="hidden" value="ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ" name="wpUnicodeCheck">' + ' ';		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 { en_wiktionary_get_langdata(function {			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 (e.g. VisualEditor edit), unless the categories			// were not re-rendered and our interface is still there (e.g. DiscussionTools edit)			if ( document.querySelector( '#catlinks .hotcatlink' ) ) {				return;			}			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 ) ); //