Automoderated users, Autopatrolled users, Bureaucrats, Comment administrators, Confirmed users, Moderators, Rollbackers, Administrators
213,527
edits
m (1 revision imported: upgrade to 2.34) |
(Upgraded to V2.36 to fix the "It appears your browser does not support Unicode" false-positive error.) |
||
Line 1:
// <nowiki>
/*
HotCat V2.
Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
Line 18 ⟶ 17:
/*
This code
For use with older versions of MediaWiki, use the archived versions below:
<=1.26: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-HotCat.js&oldid=211134664
*/
/* eslint-disable vars-on-top, one-var, camelcase, no-use-before-define, no-alert */
/* global HotCat, mediaWiki, UFUI, JSconfig, UploadForm */
( function ( $, mw ) {
// account for values changing, e.g. wgCurRevisionId after a VE edit
var conf = mw.config.values;
if (
// Guard against double inclusions (in old IE/Opera element ids become window properties)
( window.HotCat && !window.HotCat.nodeName ) ||
// Not on edit pages
) {
Line 40:
}
// Configuration stuff.
window.HotCat = {
// Localize these messages to the main language of your wiki.
// 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
//
using: ' using [[Help:Gadget-HotCat|HotCat]]',
// $1 is replaced by a number. If your language has several plural forms (c.f. [[:en:Dual (grammatical form)]]),
// you can set this to an array of strings suitable for passing to mw.language.configPlural().
// If that function doesn't exist, HotCat will simply fall back to using the last
// entry in the array.
multi_change: '$1 categories',
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
// see localization hook below.
commit: 'Save',
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
// see localization hook below.
ok: 'OK',
// Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
// see localization hook below.
cancel: 'Cancel',
// Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
// see localization hook below.
multi_error: 'Could not retrieve the page text from the server. Therefore, your category changes ' +
'cannot be saved. We apologize for the inconvenience.',
// Defaults to '[[' + category_canonical + ':$1]]'. Can be overridden if in the short edit summaries
// not the standard category name should be used but, say, a shorter namespace alias. $1 is replaced
//
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
// 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)' },
// The tooltips for the above links
tooltips: {
change: 'Modify',
remove: 'Remove',
add: 'Add a new category',
restore: 'Undo changes',
undo: 'Undo changes',
down: 'Open for modifying and display subcategories',
up: 'Open for modifying and display parent categories'
},
// The HTML content of the "enter multi-mode" link at the front.
addmulti: '<span>+<sup>+</sup></span>',
// Tooltip for the "enter multi-mode" link
multi_tooltip: 'Modify several categories',
// Return true to disable HotCat.
disable: function () {
var ns = conf.wgNamespaceNumber;
var nsIds = conf.wgNamespaceIds;
return (
ns < 0 || // Special pages; Special:Upload is handled differently
ns === 2 && /\.(js|css)$/.test( conf.wgTitle ) || // User scripts
nsIds &&
ns ===
},
uncat_regexp: /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}\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'
},
// Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the first letter
// of a page is automatically capitalized ("first-letter"; Category:aa === Category:Aa), or it isn't
// ("case-sensitive"; Category:aa !== Category:Aa). It doesn't currently have a fully case-insensitive mode
// (which would mean Category:aa === Category:Aa === Category:AA === Category:aA)
// HotCat tries to set this correctly automatically using an API query. It's still a good idea to manually
// configure it correctly; either directly here if you copied HotCat, or in the local configuration file
// MediaWiki:Gadget-HotCat.js/local_defaults if you hotlink to the Commons-version, to ensure it is set even
// if that API query should fail for some strange reason.
capitalizePageNames: true,
// 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: '#F8CCB0',
// If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
// the changes; users must always save explicitly.
no_autocommit: false,
// If true, the "category deletion" link "(-)" will never save automatically but always show an
// edit page where the user has to save the edit manually. Is false by default because that's the
// traditional behavior. This setting overrides no_autocommit for "(-)" links.
del_needs_diff: false,
// Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
// server to get suggestions.
suggest_delay: 100,
// Default width, in characters, of the text input field.
editbox_width: 40,
// One of the engine_names above, to be used as the default suggestion engine.
suggestions: 'combined',
// If true, always use the default engine, and never display a selector.
fixed_search: false,
// If false, do not display the "up" and "down" links
use_up_down: true,
// Default list size
list_size: 5,
// 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
// options in his or her preferences set.
dont_add_to_watchlist: false,
window.HotCat.shortcuts = window.HotCat.shortcuts || {};
for ( var k in map ) {
if ( !map.hasOwnProperty
var v = map[ k ];
if ( typeof v !== 'string' ) { continue; }
k = k.replace
v = v.replace
if ( k.length === 0 || v.length === 0 ) { 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
// And even more compatbility. HotCat was developed without jQuery, and anyway current jQuery
// (1.7.1) doesn't seem to support in jquery.getJSON() or jQuery.ajax() the automatic
Line 234 ⟶ 219:
// ajax routines ourselves in any case. We can't rely on the old sajax_init_object(), newer
// MW versions (>= 1.19) might not have it.
var getJSON = ( function () {
function getRequest
var request = null;
try {
request = new window.XMLHttpRequest();
} catch ( anything ) {
if ( window.ActiveXObject ) {
try {
request = new window.ActiveXObject( 'Microsoft.XMLHTTP' );
} catch ( any ) {
}
} // end if IE
Line 250 ⟶ 235:
}
return function ( settings ) {
var req = getRequest();
if ( !req && settings && settings.error ) { settings.error
if ( !req || !settings || !settings.uri ) { return req; }
// eslint-disable-next-line no-use-before-define
var uri = armorUri( settings.uri );
var args = settings.data || null;
var method;
if ( args && uri.length + args.length + 1 > 2000 ) {
// We lose caching, but at least we can make the request
method = 'POST';
req.setRequestHeader
} else {
method = 'GET';
if ( args ) { uri += '?' + args; }
args = null;
}
req.open
req.onreadystatechange = function () {
if ( req.readyState !== 4 ) { return; }
if ( req.status !== 200 || !req.responseText || !( /^\s*[
if ( settings.error ) { settings.error
} else {
// eslint-disable-next-line no-eval
if ( settings.success ) { settings.success( eval( '(' + req.responseText + ')' ) ); }
}
};
req.setRequestHeader
req.setRequestHeader
req.send
return req;
};
}
function armorUri
// Avoid protocol-relative URIs, IE7 has a bug with them in Ajax calls
if ( uri.length >= 2 && uri.substring( 0, 2 ) === '//' ) { return document.location.protocol + uri; }
return uri;
}
function LoadTrigger
this.queue = [];
this.toLoad = needed;
}
LoadTrigger.prototype = {
register
if ( this.toLoad <= 0 ) {
callback
} else {
this.queue[ this.queue.length ] = callback;
}
},
loaded
if ( this.toLoad > 0 ) {
this.toLoad--;
if ( this.toLoad === 0 ) {
// Run queued callbacks once
for ( var i = 0; i < this.queue.length; i++ ) { this.queue[ i ](); }
this.queue = [];
}
Line 314 ⟶ 301:
};
var setupCompleted = new LoadTrigger( 1 );
// Used to run user-registered code once HotCat is fully set up and ready.
HotCat.runWhenReady = function ( callback ) { setupCompleted.register( callback ); };
var loadTrigger = new LoadTrigger( 2 );
// Used to delay running the HotCat setup until /local_defaults and localizations have been loaded.
function load
var head = document.getElementsByTagName
var s = document.createElement
s.setAttribute
s.setAttribute
var done = false;
function afterLoad
if ( done ) { return; }
done = true;
s.onload = s.onreadystatechange = s.onerror = null; // Properly clean up to avoid memory leaks in IE
if ( head && s.parentNode ) { head.removeChild
loadTrigger.loaded();
}
s.onload = s.onreadystatechange = function () { // onreadystatechange for IE, onload for all others
if ( done ) { return; }
if ( !this.readyState || this.readyState === 'loaded' || this.readyState === 'complete' ) {
afterLoad
}
};
s.onerror = afterLoad; // Clean up, but otherwise ignore errors
head.insertBefore
}
function loadJS
load
}
function loadURI
var url = href;
if ( url.substring
url = window.location.protocol + url;
} else if ( url.substring
url = conf.wgServer + url;
}
load
}
Line 363 ⟶ 350:
// from the wiki where this script is executing, even if this script itself is hotlinked from the Commons. This can
// be used to change the default settings, or to provide localized interface texts for edit summaries and so on.
loadJS
// 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 the Commons and don't want that.
if ( typeof 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
);
} else {
// Load translations locally
loadJS
}
} else {
Line 390 ⟶ 377:
// The following regular expression strings are used when searching for categories in wikitext.
var wikiTextBlank
var wikiTextBlankRE = new RegExp
// 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
Line 412 ⟶ 399:
var formattedNamespaces = conf.wgFormattedNamespaces;
var namespaceIds = conf.wgNamespaceIds;
function autoLocalize( namespaceNumber, fallback ) {
function
if ( !name || name.length === 0 ) { return ''; }
var regex_name = '';
var
}
}
return regex_name
.replace( /([\\^$.?*+()])/g, '\\$1' )
.replace( wikiTextBlankRE, wikiTextBlank );
}
namespaceIds[ cat_name ] === namespaceNumber
) {
regexp += '|' + create_regexp_str( cat_name );
}
}
}
return regexp;
}
if ( formattedNamespaces[ '10' ] ) {
HotCat.template_regexp = autoLocalize( 10, 'template' );
}
Line 463 ⟶ 446:
// 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
return literal ? document.createTextNode( arg ) : document.createElement( arg );
}
function
if ( typeof uri === 'undefined' || uri === null ) { uri = document.location.href; }
var re = new RegExp( '[&?]' + name + '=([^&#]*)' );
var m = re.exec( uri );
if ( m && m.length > 1 ) { return decodeURIComponent( m[ 1 ] ); }
return null;
}
function title
if ( !href ) { return null; }
var script = conf.wgScript + '?';
if ( href.indexOf
// href="/w/index.php?title=..."
return param
} else {
// href="/wiki/..."
var prefix = conf.wgArticlePath.replace
if ( href.indexOf(
prefix = conf.wgServer + prefix; // Fully expanded URL?
}
if ( href.indexOf( prefix ) !== 0 && prefix.substring( 0, 2 ) === '//' ) {
prefix = document.location.protocol + prefix; // Protocol-relative wgServer?
}
if ( href.indexOf( prefix ) === 0 ) {
return decodeURIComponent( href.substring( prefix.length ) );
}
}
return null;
}
function hasClass
return ( ' ' + elem.className + ' ' ).indexOf
}
function capitalize
if ( !str || str.length === 0 ) { return str; }
return str.substr( 0, 1 ).toUpperCase() + str.substr
}
function wikiPagePath
// 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
}
function escapeRE( str ) {
return str.replace( /([
}
function substituteFactory
options = options || {};
var lead = options.indicator || '$';
var indicator = escapeRE
var lbrace = escapeRE
var rbrace = escapeRE
var re;
re = new RegExp(
// $$
// $0, $1
'(?:' + 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 ) {
}
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
return HotCat.capitalizePageNames ? capitalize( s ) : s;
};
}
// Text modification
var findCatsRE =
new RegExp
function replaceByBlanks
return match.replace( /(\s|\S)/g, ' ' ); // /./ doesn't match linebreaks. /(\s|\S)/ does.
}
function find_category
var cat_regex = null;
if ( HotCat.template_categories[ category ] ) {
cat_regex = new RegExp(
'\\{\\{' + wikiTextBlankOrBidi + '(' + HotCat.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi +
'(?:' + HotCat.template_categories[ category ] + ')' +
wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}',
'g'
);
} else {
var cat_name
var initial
cat_regex = new RegExp(
'\\[\\[' + wikiTextBlankOrBidi + '(' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi +
( initial === '\\' || !HotCat.capitalizePageNames ?
initial :
'[' + initial.toUpperCase() + initial.toLowerCase() + ']'
) +
cat_name.substring( 1 ).replace( wikiTextBlankRE, wikiTextBlank ) +
wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]',
'g'
);
}
if ( once ) { return cat_regex.exec
var copiedtext = wikitext
var result = [];
var curr_match = null;
while ( ( curr_match = cat_regex.exec
result.push
}
result.re = cat_regex;
return result; // An array containing all matches, with positions, in result[ i ].match
}
var interlanguageRE = null;
function change_category
function find_insertionpoint
var copiedtext = wikitext
// Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element".
var index = -1;
findCatsRE.lastIndex = 0;
while ( findCatsRE.exec( copiedtext ) !== null ) { index = findCatsRE.lastIndex; }
if ( index < 0 ) {
// Find the index of the first interlanguage link...
var match = null;
if ( !interlanguageRE ) {
// Approximation without API: interlanguage links start with 2 to 3 lower case letters, optionally followed by
// a sequence of groups consisting of a dash followed by one or more lower case letters. Exceptions are "simple"
// and "tokipona".
match = /((^|\n\r?)(\[\[\s*(([a-z]{2,3}(-[a-z]+)*)|simple|tokipona)\s*:[^\]]+\]\]\s*))+$/.exec
} else {
match = interlanguageRE.exec( copiedtext );
}
if ( match ) { index = match.index; }
return {
}
return {
}
var summary
var nameSpace = HotCat.category_canonical;
var cat_point = -1; // Position of removed category;
if ( key ) { key = '|' + key; }
var keyChange = ( toRemove && toAdd && toRemove === toAdd && toAdd.length > 0 );
var matches;
if ( toRemove && toRemove.length > 0 ) {
matches = find_category
if ( !matches || matches.length === 0 ) {
return { text: wikitext,
} else {
var before = wikitext.substring
var after
if ( matches.length > 1 ) {
// Remove all occurrences in after
matches.re.lastIndex = 0;
after = after.replace
}
if ( toAdd ) {
nameSpace = matches[ 0 ].match[ 1 ] || nameSpace;
if ( key === null ) { key = matches[ 0 ].match[ 2 ]; } // Remember the category key, if any.
}
// Remove whitespace (properly): strip whitespace, but only up to the next line feed.
Line 643 ⟶ 637:
// whitespace characters, insert a blank.
var i = before.length - 1;
while ( i >= 0 && before.charAt
var j = 0;
while ( j < after.length && after.charAt
if ( i >= 0 && before.charAt( i ) === '\n' && ( after.length === 0 || j < after.length && after.charAt( j ) === '\n' ) ) { i--; }
if ( i >= 0 ) {
before = before.substring( 0, i + 1 );
} else {
before = '';
}
after = after.substring( j );
} else {
after = '';
}
if (
before.length > 0 && before.substring( before.length - 1 ).search( /\S/ ) >= 0 &&
after.length > 0 && after.substr( 0, 1 ).search( /\S/ ) >= 0 ) {
before += ' ';
}
cat_point = before.length;
if ( cat_point === 0 && after.length > 0 && after.substr( 0, 1 ) === '\n' ) {
after = after.substr( 1 );
}
wikitext = before + after;
if ( !keyChange ) {
if ( HotCat.template_categories[ toRemove ] ) {
summary.push
} else {
summary.push
}
}
}
}
if ( toAdd && toAdd.length > 0 ) {
matches = find_category
if ( matches && matches.length > 0 ) {
return { text: wikitext,
} else {
var onCat = false;
if ( cat_point < 0 ) {
var point = find_insertionpoint
cat_point = point.idx;
onCat = point.onCat;
Line 681 ⟶ 683:
onCat = true;
}
var newcatstring = '[[' + nameSpace + ':' + toAdd + ( key ||
if ( cat_point >= 0 ) {
var suffix = wikitext.substring
wikitext = wikitext.substring
if ( suffix.length > 0 && suffix.substr( 0, 1 ) !== '\n' ) {
wikitext += '\n' + suffix;
} else {
Line 691 ⟶ 693:
}
} else {
if ( wikitext.length > 0 && wikitext.substr
wikitext += '\n';
}
wikitext += ( wikitext.length > 0 ? '\n' : '' ) + newcatstring;
}
if ( keyChange ) {
var k = key ||
if ( k.length > 0 ) { k = k.substr
summary.push
} else {
summary.push
}
if ( HotCat.uncat_regexp && !is_hidden ) {
var txt = wikitext.replace
if ( txt.length !== wikitext.length ) {
wikitext = txt;
summary.push
}
}
}
}
return { text: wikitext,
}
// The real HotCat UI
function evtKeys
/* eslint-disable no-bitwise */
e = e || window.event || window.Event; // W3C, IE, Netscape
var code = 0;
if ( typeof e.ctrlKey !== 'undefined' ) { // All modern browsers
// Ctrl-click seems to be overloaded in FF/Mac (it opens a pop-up menu), so treat cmd-click
// as a ctrl-click, too.
if ( e.ctrlKey || e.metaKey ) { code |= 1; }
if ( e.shiftKey ) { code |= 2; }
} else if ( typeof e.modifiers !== 'undefined' ) { // Netscape...
if ( e.modifiers & ( Event.CONTROL_MASK | Event.META_MASK ) ) { code |= 1; }
if ( e.modifiers & Event.SHIFT_MASK ) { code |= 2; }
}
/* eslint-enable no-bitwise */
return code;
}
function evtKill
e = e || window.event || window.Event; // W3C, IE, Netscape
if ( typeof e.preventDefault !== 'undefined' ) {
e.preventDefault
e.stopPropagation
} else {
e.cancelBubble = true;
}
return false;
}
var catLine
var onUpload
var editors
var commitButton = null;
var commitForm
var multiSpan
var pageText
var pageTime
var pageWatched
var watchCreate
var watchEdit
var minorEdits
var editToken
var is_rtl
var serverTime
var lastRevId
var pageTextRevId = null;
var conflictingUser = null;
var newDOM
function setMultiInput
if ( commitButton || onUpload ) { return; }
commitButton = make
commitButton.type
commitButton.value = HotCat.messages.commit;
commitButton.onclick = multiSubmit;
if ( multiSpan ) {
multiSpan.parentNode.replaceChild
} else {
catLine.appendChild
}
}
function checkMultiInput
if ( !commitButton ) { return; }
var has_changes = false;
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ].state !== CategoryEditor.UNCHANGED ) {
has_changes = true;
break;
Line 795:
}
function currentTimestamp
var now = new Date();
var ts
function two
ts = ts +
return ts;
}
var saveInProgress = false;
function initiateEdit
if ( saveInProgress ) { return; }
saveInProgress = true;
var oldButtonState;
if ( commitButton ) {
oldButtonState = commitButton.disabled;
commitButton.disabled = true;
Line 820:
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
setPage( json );
},
error: function ( req ) {
fail( req.status + ' ' + req.statusText );
}
} );
}
function multiChangeMsg
var msg = HotCat.messages.multi_change;
if ( typeof msg !== 'string' && msg.length ) {
if (
msg = mw.language.convertPlural
} else {
msg = msg[ msg.length - 1 ];
}
}
return substitute
}
function performChanges
if ( pageText === null ) {
failure
return;
}
// Backwards compatibility after message change (added $2 to cat_keychange)
if ( HotCat.messages.cat_keychange.indexOf
// More backwards-compatibility with earlier HotCat versions:
if ( !HotCat.messages.short_catchange ) { HotCat.messages.short_catchange = '[[' + HotCat.category_canonical + ':$1]]'; }
// Create a form and submit it. We don't use the edit API (api.php?action=edit) because
// (a) sensibly reporting back errors like edit conflicts is always a hassle, and
Line 871 ⟶ 876:
// current user, then we set the "oldid" value and switch to diff, which gives the "you are editing an old version;
// if you save, any more recent changes will be lost" screen.
var editingOldVersion = lastRevId !== null && lastRevId !== conf.wgCurRevisionId || pageTextRevId !== null && pageTextRevId !== conf.wgCurRevisionId;
var selfEditConflict = editingOldVersion && conflictingUser && conflictingUser === conf.wgUserName;
if ( singleEditor && !singleEditor.noCommit && !HotCat.no_autocommit && editToken && !selfEditConflict ) {
// If we do have an edit conflict, but not with ourself, that's no reason not to attempt to save: the server side may actually be able to
// merge the changes. We just need to make sure that we do present a diff view if it's a self edit conflict.
commitForm.wpEditToken.value = editToken;
action = commitForm.wpDiff;
if ( action ) { action.name = action.value = 'wpSave'; }
} else {
action = commitForm.wpSave;
if ( action ) { action.name = action.value = 'wpDiff'; }
}
var result = { text
var changed = [], added = [], deleted = [], changes = 0;
var toEdit =
var error = null;
var i;
for ( i = 0; i < toEdit.length; i++ ) {
if ( toEdit[ i ].state === CategoryEditor.CHANGED ) {
result = change_category
);
if ( !result.error ) {
changes++;
if ( !toEdit[ i ].originalCategory || toEdit[ i ].originalCategory.length === 0 ) {
added.push
} else {
changed.push(
}
} else if ( error === null ) {
error = result.error;
}
} else if (
toEdit[ i ].originalCategory.length > 0
) {
result = change_category( result.text, toEdit[ i ].originalCategory, null, null, false );
if ( !result.error ) {
changes++;
deleted.push
} 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
Line 928 ⟶ 934:
commitForm.wpMinoredit.checked = minorEdits;
commitForm.wpWatchthis.checked = conf.wgArticleId === 0 && watchCreate || watchEdit || pageWatched;
if ( conf.wgArticleId > 0 || !!singleEditor ) {
if ( changes === 1 ) {
if ( result.summary && result.summary.length > 0 ) {
commitForm.wpSummary.value = HotCat.messages.prefix + result.summary.join
}
commitForm.wpMinoredit.checked = HotCat.single_minor || minorEdits;
} else if ( changes > 1 ) {
var summary = [];
var shortSummary = [];
// Deleted
for ( i = 0; i < deleted.length; i++ ) {
summary.push
}
if ( deleted.length === 1 ) {
shortSummary.push( '-' + substitute( HotCat.messages.short_catchange, [ null, deleted[ 0 ] ] ) );
} else if ( deleted.length > 1 ) {
shortSummary.push( '- ' + multiChangeMsg( deleted.length ) );
}
// Added
for ( i = 0; i < added.length; i++ ) {
summary.push
}
if ( added.length === 1 ) {
shortSummary.push( '+' + substitute( HotCat.messages.short_catchange, [ null, added[ 0 ] ] ) );
} else if ( added.length > 1 ) {
shortSummary.push( '+ ' + multiChangeMsg( added.length ) );
}
// Changed
var arrow = is_rtl ? '\u2190' : '\u2192'; // left and right arrows. Don't use ← and → in the code.
for ( i = 0; i < changed.length; i++ ) {
if ( changed[ i ].from !== changed[ i ].to ) {
summary.push
} else {
summary.push
}
}
if ( changed.length === 1 ) {
if ( changed[ 0 ].from !== changed[ 0 ].to ) {
shortSummary.push
} else {
shortSummary.push
}
} else if ( changed.length > 1 ) {
shortSummary.push
}
if ( summary.length > 0 ) {
summary = summary.join
if ( summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length ) {
summary = shortSummary.join
}
commitForm.wpSummary.value = HotCat.messages.prefix + summary + HotCat.messages.using;
Line 982 ⟶ 991:
}
commitForm.wpTextbox1.value = result.text;
commitForm.wpStarttime.value = serverTime || currentTimestamp
commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
if ( selfEditConflict ) { commitForm.oldid.value =
// Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
commitForm.hcCommit.click();
}
function resolveMulti
var i;
for ( i = 0; i < toResolve.length; i++ ) {
toResolve[ i ].dab = null;
toResolve[ i ].dabInput = toResolve[ i ].lastInput;
}
if ( noSuggestions ) {
callback
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' +
for ( i = 0; i < toResolve.length; i++ ) {
var v = toResolve[ i ].dabInput;
v = replaceShortcuts
toResolve[ i ].dabInputCleaned = v;
args += encodeURIComponent
if ( i + 1 < toResolve.length ) { args += '%7C'; }
}
getJSON( {
} );
}
function resolveOne
var cats
var lks
var is_dab
var is_redir = typeof page.redirect === 'string'; // Hard redirect?
var is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden === 'string';
var is_missing = typeof page.missing === 'string';
var i;
for ( i = 0; i < toResolve.length; i++ ) {
if ( toResolve.length > 1 && toResolve[ i ].dabInputCleaned !== page.title.substring
// Note: the server returns in page an NFC normalized Unicode title. If our input was not NFC normalized, we may not find
// any entry here. If we have only one editor to resolve (the most common case, I presume), we may simply skip the check.
toResolve[ i ].currentHidden = is_hidden;
toResolve[ i ].inputExists = !is_missing;
toResolve[ i ].icon.src = armorUri( is_missing ? HotCat.existsNo : HotCat.existsYes );
}
if ( is_missing ) { return; }
if ( !is_redir && cats && ( HotCat.disambig_category || HotCat.redir_category ) ) {
for ( var c = 0; c < cats.length; c++ ) {
var cat = cats[ c ]
// Strip namespace prefix
if ( cat ) {
cat = cat.substring
if ( cat === HotCat.disambig_category ) {
is_dab = true; break;
} else if ( cat === HotCat.redir_category ) {
is_redir = true; break;
}
Line 1,051 ⟶ 1,060:
}
}
if ( !is_redir && !is_dab ) { return; }
if ( !lks || lks.length === 0 ) { 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 > 0
) {
// Internal link to existing thingy. Extract the page name and remove the namespace.
var match = lks[ i ]
match = match.substring
// Exclude blacklisted categories.
if ( !HotCat.blacklist || !HotCat.blacklist.test
titles.push
}
}
}
if ( titles.length === 0 ) {
return;
}
for ( i = 0; i < toResolve.length; i++ ) {
if ( toResolve.length > 1 && toResolve[ i ].dabInputCleaned !== page.title.substring
toResolve[ i ].inputExists = true; // Might actually be wrong if it's a redirect pointing to a non-existing category
toResolve[ i ].icon.src = armorUri( HotCat.existsYes );
if ( titles.length > 1 ) {
toResolve[ i ].dab = titles;
} else {
toResolve[ i ].text.value =
titles[ 0 ] + ( toResolve[ i ].currentKey !== null ? '|' + toResolve[ i ].currentKey :
}
}
}
function resolveRedirects
if ( !params || !params.query || !params.query.pages ) { return; }
for ( var p in params.query.pages )
}
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
}
}
if ( toResolve.length === 0 ) {
initiateEdit
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 ]; }
}
}
if ( firstDab ) {
showDab( firstDab );
} else if ( !dontChange ) {
initiateEdit( function ( failure ) { performChanges( failure ); }, function ( msg ) { alert( msg ); } );
}
} );
}
Line 1,128 ⟶ 1,138:
var noSuggestions = false;
var suggestionEngines = {
opensearch
}
}
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',
}
return
}
return null;
}
},
exists: {
uri: '/api.php?format=json&action=query&prop=info&titles=Category:$1',
}
}
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
for ( var
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',
}
return titles;
}
}
}
return null;
}
}
};
var suggestionConfigs = {
};
function CategoryEditor
CategoryEditor.UNCHANGED
CategoryEditor.OPEN
CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
CategoryEditor.CHANGED
CategoryEditor.DELETED
// 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
function forceRedraw
if ( dummyElement.parentNode ) {
} else {
document.body.appendChild
}
}
Line 1,265 ⟶ 1,256:
// Event keyCodes that we handle in the text input field/suggestion list.
var BS = 8, TAB = 9, RET = 13, ESC = 27, SPACE = 32, PGUP = 33, PGDOWN = 34, UP = 38, DOWN = 40, DEL = 46, IME = 229;
function makeActive
if ( which.is_active ) { return; }
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ] !== which ) { editors[ i ].inactivate
}
which.is_active = true;
if ( which.dab ) {
showDab
} else {
// Check for programmatic value changes.
var expectedInput = which.lastRealInput || which.lastInput ||
var actualValue = which.text.value ||
if ( expectedInput.length === 0 && actualValue.length > 0 || expectedInput.length > 0 && actualValue.indexOf
// 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
);
} else {
which.setSelection
}
}
Line 1,303 ⟶ 1,294:
}
function showDab
if ( !which.is_active ) {
makeActive( which );
} else {
which.showSuggestions
which.dab = null;
}
Line 1,314 ⟶ 1,305:
CategoryEditor.prototype = {
initialize
// 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
if ( !newDOM ) {
span = make
span.className = 'noprint';
if ( key ) {
span.appendChild
if ( after ) {
after.parentNode.insertBefore
after = after.nextSibling;
} else {
line.appendChild
}
} else if ( line.firstChild ) {
span.appendChild
line.appendChild
}
}
this.linkSpan = make
this.linkSpan.className = 'noprint nopopups hotcatlink';
var lk = make
lk.appendChild
this.linkSpan.appendChild
span = make
span.className = 'noprint';
if ( is_rtl ) { span.dir = 'rtl'; }
span.appendChild
if ( after ) {
after.parentNode.insertBefore
} else {
line.appendChild
}
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
// Create change and del links
this.makeLinkSpan
if ( !this.originalExists && this.upDownLinks ) { this.upDownLinks.style.display = 'none'; }
span.appendChild
}
this.originalHidden
this.line
this.engine
this.span
this.currentCategory
this.currentExists
this.currentHidden
this.currentKey
this.state
this.lastSavedState
this.lastSavedCategory
this.lastSavedKey
this.lastSavedExists
this.lastSavedHidden
if ( this.catLink && this.currentKey ) {
this.catLink.title = this.currentKey;
}
editors[ editors.length ] = this;
},
makeLinkSpan
this.normalLinks = make
var lk = null;
if ( this.originalCategory && this.originalCategory.length > 0 ) {
lk = make
lk.appendChild
this.normalLinks.appendChild
this.normalLinks.appendChild
}
if ( !HotCat.template_categories[ this.originalCategory ] ) {
lk = make
lk.appendChild
this.normalLinks.appendChild
this.normalLinks.appendChild
if ( !noSuggestions && HotCat.use_up_down ) {
this.upDownLinks = make
lk = make
lk.appendChild
this.upDownLinks.appendChild
this.upDownLinks.appendChild
lk = make
lk.appendChild
this.upDownLinks.appendChild
this.upDownLinks.appendChild
this.normalLinks.appendChild
}
}
this.linkSpan = make
this.linkSpan.className = 'noprint nopopups hotcatlink';
this.linkSpan.appendChild
this.undelLink = make
this.undelLink.className = 'nopopups hotcatlink';
this.undelLink.style.display = 'none';
lk = make
lk.appendChild
this.undelLink.appendChild
this.undelLink.appendChild
this.linkSpan.appendChild
},
invokeSuggestions
if ( this.engine && suggestionConfigs[ this.engine ] && suggestionConfigs[ this.engine ].temp && !dont_autocomplete ) {
this.engine = HotCat.suggestions; // Reset to a search upon input
}
this.state = CategoryEditor.CHANGE_PENDING;
var self = this;
window.setTimeout
},
makeForm
var form = make
form.method = 'POST'; form.onsubmit =
this.form = form;
var self = this;
var text = make
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
Line 1,455 ⟶ 1,447:
// detected by a keyDown IME with a keyUp of space, tab, escape, or return. (Example: IE8)
text.onkeyup =
function ( evt ) {
evt = evt || window.event || window.Event; // W3C, IE, Netscape
var key = evt.keyCode || 0;
if ( self.ime && self.lastKey === IME && !self.usesComposition && ( key === TAB || key === RET || key === ESC || key === SPACE ) ) { self.ime = false; }
if ( self.ime ) { return true; }
if ( key === UP || key === DOWN || key === PGUP || key === PGDOWN ) {
// In case a browser doesn't generate keypress events for arrow keys...
if ( self.keyCount === 0 ) { return self.processKey
} else {
if ( key === ESC && self.lastKey !== IME ) {
if ( !self.resetKeySelection
// No undo of key selection: treat ESC as "cancel".
self.cancel
return;
}
Line 1,473 ⟶ 1,465:
// Also do this for ESC as a workaround for Firefox bug 524360
// https://bugzilla.mozilla.org/show_bug.cgi?id=524360
self.invokeSuggestions
}
return true;
};
text.onkeydown =
function ( evt ) {
evt = evt || window.event || window.Event; // W3C, IE, Netscape
var key = evt.keyCode || 0;
self.lastKey = key;
self.keyCount = 0;
// DOM Level < 3 IME input
if ( !self.ime && key === IME && !self.usesComposition ) {
// self.usesComposition catches browsers that may emit spurious keydown IME after a composition has ended
self.ime = true;
} else if ( self.ime && key !== IME && !( key >= 16 && key <= 20 || key >= 91 && key <= 93 || key === 144 ) ) {
// Ignore control keys: ctrl, shift, alt, alt gr, caps lock, windows/apple cmd keys, num lock. Only the windows keys
// terminate IME (apple cmd doesn't), but they also cause a blur, so it's OK to ignore them here.
Line 1,493 ⟶ 1,485:
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
// 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
// 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.
Line 1,507 ⟶ 1,499:
// Don't test for text.selectionStart being defined; FF3.6.4 raises an exception when trying to access that
// property while the element is not being displayed.
,
);
// DOM Level 3 IME handling
Line 1,515 ⟶ 1,507:
// 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.
} catch ( any ) {
// Just in case some browsers might produce exceptions with these DOM Level 3 events
}
}
this.text = text;
this.icon = make
var list = null;
if ( !noSuggestions ) {
list = make
list.onclick
list.ondblclick = function ( e ) { if ( self.highlightSuggestion( 0 ) ) { self.accept
list.onchange = function (
list.onkeyup = function ( evt ) {
evt = evt || window.event || window.Event; // W3C, IE, Netscape
window.setTimeout( function () { self.textchange( true ); }, HotCat.suggest_delay );
} else if ( evt.keyCode === RET ) {
}
if ( !HotCat.fixed_search ) {
var engineSelector = make( 'select' );
for ( var
var opt = make( 'option' );
opt.value = key;
if ( key === this.engine ) { opt.selected = true; }
opt.appendChild
engineSelector.appendChild
}
}
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;
}
Line 1,566 ⟶ 1,556:
this.list = list;
function button_label
var label = null;
if (
onUpload &&
typeof UFUI.getLabel === 'function'
) {
try {
label = UFUI.getLabel
// 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
OK.value = button_label
OK.onclick =
this.ok = OK;
var cancel = make
cancel.value = button_label
cancel.onclick =
this.cancelButton = cancel;
var span = make
span.className = 'hotcatinput';
span.style.position = 'relative';
Line 1,602 ⟶ 1,593:
// suggestions and the selector at the right edge of the screen if display of the input field causes a re-layout
// moving the form to the front of the next line.
span.appendChild
// 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
span.style.whiteSpace = 'nowrap';
if ( list ) { span.appendChild
if ( this.engineSelector ) { span.appendChild
if ( !noSuggestions ) { span.appendChild
span.appendChild
span.appendChild
form.appendChild( span );
form.style.display = 'none';
this.span.appendChild
},
display
if ( this.isAddCategory && !onUpload ) {
// 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;
Line 1,632 ⟶ 1,624:
}
}
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.currentHidden
this.currentKey
this.icon.src = armorUri( this.currentExists ? HotCat.existsYes : HotCat.existsNo );
this.text.value = this.currentCategory + ( this.currentKey !== null ? '|' + this.currentKey :
this.originalState = this.state;
this.lastInput
this.inputExists
this.state
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
this.text.focus();
this.text.readOnly = false;
checkMultiInput
return result;
},
show
var result = this.display
var v = this.lastSavedCategory;
if ( v.length === 0 ) { return result; }
this.text.readOnly = !!readOnly;
this.engine = engine;
this.textchange
forceRedraw
return result;
},
open
return this.show
},
down
return this.show
},
up
return this.show
},
cancel
if ( this.isAddCategory && !onUpload ) {
this.removeEditor(); // We added a new adder when opening
return;
Line 1,693 ⟶ 1,685:
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.currentExists
this.currentHidden
if ( this.catLink ) {
if ( this.currentKey && this.currentKey.length > 0 ) {
this.catLink.title = this.currentKey;
} else {
this.catLink.title =
}
}
if ( this.state === CategoryEditor.UNCHANGED ) {
if ( this.catLink ) { this.catLink.style.backgroundColor = 'transparent'; }
} else {
if ( !onUpload ) {
try {
this.catLink.style.backgroundColor = HotCat.bg_changed;
} catch ( ex ) {}
}
}
checkMultiInput
forceRedraw
},
removeEditor
if ( !newDOM ) {
var next = this.span.nextSibling;
if ( next ) { next.parentNode.removeChild
}
this.span.parentNode.removeChild
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ] === this ) {
editors.splice
break;
}
}
checkMultiInput
var self = this;
// eslint-disable-next-line no-delete-var
window.setTimeout( function () { delete self; }, 10 );
},
rollback
this.undoLink.parentNode.removeChild
this.undoLink = null;
this.currentCategory = this.originalCategory;
Line 1,749 ⟶ 1,742:
this.lastSavedHidden = this.originalHidden;
this.state = CategoryEditor.UNCHANGED;
if ( !this.currentCategory || this.currentCategory.length === 0 ) {
// It was a newly added category. Remove the whole editor.
this.removeEditor();
} else {
// Redisplay the link...
this.catLink.removeChild
this.catLink.appendChild
this.catLink.href = wikiPagePath
this.catLink.title = this.currentKey ||
this.catLink.className = this.currentExists ?
this.catLink.style.backgroundColor = 'transparent';
if ( this.upDownLinks ) { this.upDownLinks.style.display = this.currentExists ?
checkMultiInput
}
return evtKill
},
inactivate
if ( this.list ) { this.list.style.display = 'none'; }
if ( this.engineSelector ) { this.engineSelector.style.display = 'none'; }
this.is_active = false;
},
acceptCheck
this.sanitizeInput
var value = this.text.value.split( '|' );
var key
if ( value.length > 1 ) { key = value[ 1 ]; }
var v = value[ 0 ].replace( /_/g, ' ' ).replace( /^\s+|\s+$/g,
if ( HotCat.capitalizePageNames ) { v = capitalize
this.lastInput = v;
v = replaceShortcuts( v, HotCat.shortcuts );
if ( v.length === 0 ) {
this.cancel
return false;
}
if (
!dontCheck && (
conf.wgNamespaceNumber === 14 && v === conf.wgTitle ||
HotCat.blacklist && HotCat.blacklist.test( v )
) {
this.cancel();
return false;
}
Line 1,799 ⟶ 1,793:
},
accept
// eslint-disable-next-line no-bitwise
var result = evtKill( evt );
var
var original = this.currentCategory;
if ( resolved[ 0 ].acceptCheck( true ) ) {
( resolved[ 0 ].
null
}
} );
}
return result;
},
close
if ( !this.catLink ) {
// Create a catLink
this.catLink = make
this.catLink.appendChild
this.catLink.style.display = 'none';
this.span.insertBefore
}
this.catLink.removeChild
this.catLink.appendChild
this.catLink.href = wikiPagePath
this.catLink.className = this.currentExists ?
this.lastSavedCategory = this.currentCategory;
this.lastSavedKey
this.lastSavedExists
this.lastSavedHidden
// 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 ) {
// 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.makeLinkSpan
this.span.appendChild
}
if ( !this.undoLink ) {
// Append an undo link.
var span = make
var lk = make
lk.appendChild
span.appendChild
span.appendChild
this.normalLinks.appendChild
this.undoLink = span;
if ( !onUpload ) {
try {
this.catLink.style.backgroundColor = HotCat.bg_changed;
} catch ( ex ) {}
}
}
if ( this.upDownLinks ) { this.upDownLinks.style.display = this.lastSavedExists ?
this.linkSpan.style.display =
this.state = CategoryEditor.CHANGED;
checkMultiInput
forceRedraw
},
commit
// Check again to catch problem cases after redirect resolution
if (
(
this.currentKey === this.originalKey ||
)
)
conf.wgNamespaceNumber === 14 && this.currentCategory === conf.wgTitle ||
HotCat.blacklist && HotCat.blacklist.test( this.currentCategory )
) {
this.cancel();
return;
}
if ( commitButton || onUpload ) {
this.close
} else {
this.close
var self = this;
initiateEdit
}
},
remove
// eslint-disable-next-line no-bitwise
return evtKill( evt );
},
doRemove
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;
Line 1,918 ⟶ 1,914:
}
}
if ( commitButton ) {
this.catLink.title =
this.catLink.style.cssText += '; text-decoration : line-through !important;';
try {
this.catLink.style.backgroundColor = HotCat.bg_changed;
} catch ( ex ) {}
this.originalState = this.state;
this.state = CategoryEditor.DELETED;
this.normalLinks.style.display = 'none';
this.undelLink.style.display =
checkMultiInput
} else {
if ( onUpload ) {
// Remove this editor completely
this.removeEditor
} else {
this.originalState = this.state;
Line 1,938 ⟶ 1,934:
this.noCommit = noCommit || HotCat.del_needs_diff;
var self = this;
initiateEdit(
function ( failure ) { performChanges( failure, self ); },
function ( msg ) { self.state = self.originalState; alert( msg ); }
);
}
}
},
restore
// Can occur only if we do have a commit button and are not on the upload form
this.catLink.title = this.currentKey ||
this.catLink.style.textDecoration =
this.state = this.originalState;
if ( this.state === CategoryEditor.UNCHANGED ) {
this.catLink.style.backgroundColor = 'transparent';
} else {
try {
this.catLink.style.backgroundColor = HotCat.bg_changed;
} catch ( ex ) {}
}
this.normalLinks.style.display =
this.undelLink.style.display = 'none';
checkMultiInput
return evtKill
},
// Internal operations
selectEngine
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
var v = this.text.value ||
v = v.replace( /^(\s|_)+/,
var re = new RegExp
if ( re.test
v = v.substring
}
if ( HotCat.capitalizePageNames ) { v = capitalize
// Only update the input field if there is a difference. IE8 appears to reset the selection
// and place the cursor at the front upon reset, which makes our autocompletetion become a
// nuisance. FF and IE6 don't seem to have this problem.
if ( this.text.value !== null && this.text.value !== v ) { this.text.value = v; }
},
makeCall
var cb = callbackObj;
var e
var v
var z
var thisObj = this;
function done
cb.callsMade++;
if ( cb.callsMade === cb.nofCalls ) {
if ( cb.exists ) { cb.allTitles.exists = true; }
if ( cb.normalized ) { cb.allTitles.normalized = cb.normalized; }
if ( !cb.dontCache && !suggestionConfigs[ cb.engineName ].cache[ z ] ) {
suggestionConfigs[ cb.engineName ].cache[ z ] = cb.allTitles;
}
thisObj.text.readOnly = false;
if ( !cb.cancelled ) { thisObj.showSuggestions
if ( cb === thisObj.callbackObj ) { thisObj.callbackObj = null; }
// eslint-disable-next-line no-delete-var
delete cb;
}
}
getJSON
var titles = e.handler
if ( titles && titles.length > 0 ) {
if ( cb.allTitles === null ) {
cb.allTitles = titles;
} else {
cb.allTitles = cb.allTitles.concat
}
if ( titles.exists ) { cb.exists = true; }
if ( titles.normalized ) { cb.normalized = titles.normalized; }
}
done();
},
error: function ( req ) {
if ( !req ) {
noSuggestions = true;
}
done();
}
},
callbackObj
textchange
// Hide all other lists
makeActive
// 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
v = v.substring
} else {
this.currentKey = null;
}
if ( this.lastInput === v && !force ) { return; } // No change
if ( this.lastInput !== v )
this.lastInput = v;
this.lastRealInput = v;
// Mark blacklisted inputs.
this.ok.disabled = v.length > 0 && HotCat.blacklist && HotCat.blacklist.test
if ( noSuggestions ) {
// No Ajax: just make sure the list is hidden
if ( this.list ) { this.list.style.display = 'none'; }
if ( this.engineSelector ) { this.engineSelector.style.display = 'none'; }
if ( this.icon ) { this.icon.style.display = 'none'; }
return;
}
if ( v.length === 0 ) { this.showSuggestions( [] ); return; }
var cleanKey = v.replace( /[\u200E\u200F\u202A-\u202E]/g,
cleanKey = replaceShortcuts( cleanKey, HotCat.shortcuts );
cleanKey = cleanKey.replace( /^\s+|\s+$/g, '' );
if ( cleanKey.length === 0 ) { this.showSuggestions( [] ); return; }
if ( this.callbackObj ) { this.callbackObj.cancelled = true; }
var engineName
dont_autocomplete = dont_autocomplete || suggestionConfigs[ engineName ].noCompletion;
if ( suggestionConfigs[ engineName ].cache[ cleanKey ] ) {
this.showSuggestions
return;
}
var engines = suggestionConfigs[ engineName ].engines;
this.callbackObj =
{ allTitles: null, callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engineName };
this.makeCalls
},
makeCalls
for ( var j = 0; j < engines.length; j++ ) {
var engine = suggestionEngines[ engines[ j ] ];
var url = conf.wgServer + conf.wgScriptPath + engine.uri.replace
this.makeCall
}
},
showSuggestions
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
if ( this.lastQuery && this.lastInput.indexOf
}
this.lastQuery = queryKey;
// Get current input text
var v = this.text.value.split( '|' );
var key = v.length > 1 ? '|' + v[ 1 ] :
v = ( HotCat.capitalizePageNames ? capitalize
var vNormalized = v;
var knownToExist = titles && titles.exists;
var i;
if ( titles ) {
if ( titles.normalized && v.indexOf( queryKey ) === 0 ) {
// We got back a different normalization than what is in the input field
vNormalized = titles.normalized + v.substring( queryKey.length );
}
var vLow = vNormalized.toLowerCase
// Strip blacklisted categories
if ( HotCat.blacklist ) {
for ( i = 0; i < titles.length; i++ ) {
if ( HotCat.blacklist.test
titles.splice( i, 1 );
i--;
}
}
}
titles.sort
function ( a, b ) {
if ( a === b ) { return 0; }
if ( a.indexOf
if ( b.indexOf
// Opensearch may return stuff not beginning with the search prefix!
var prefixMatchA = ( a.indexOf
var prefixMatchB = ( b.indexOf
if ( prefixMatchA !== prefixMatchB ) { return prefixMatchB - prefixMatchA; }
// Case-insensitive prefix match!
var aLow = a.toLowerCase(), bLow = b.toLowerCase();
prefixMatchA = ( aLow.indexOf
prefixMatchB = ( bLow.indexOf
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
)
titles.splice( i, 1 );
i--;
}
}
}
if ( !titles || titles.length === 0 ) {
if ( this.list ) { this.list.style.display = 'none'; }
if ( this.engineSelector ) { this.engineSelector.style.display = 'none'; }
if ( engineName && suggestionConfigs[ engineName ] && !suggestionConfigs[ engineName ].temp ) {
if ( this.icon ) { this.icon.src = armorUri( HotCat.existsNo ); }
this.inputExists = false;
}
Line 2,174 ⟶ 2,177:
}
var firstTitle = titles[ 0 ];
var completed = this.autoComplete
var existing = completed || knownToExist || firstTitle === replaceShortcuts( v, HotCat.shortcuts );
if ( engineName && suggestionConfigs[ engineName ] && !suggestionConfigs[ engineName ].temp ) {
this.icon.src = armorUri( existing ? HotCat.existsYes : HotCat.existsNo );
this.inputExists = existing;
}
if ( completed ) {
this.lastInput = firstTitle;
if ( titles.length === 1 ) {
this.list.style.display = 'none';
if ( this.engineSelector ) { this.engineSelector.style.display = 'none'; }
return;
}
}
// (Re-)fill the list
while ( this.list.firstChild ) { this.list.removeChild
for ( i = 0
var opt = make
opt.appendChild
opt.selected = completed && ( i === 0 );
this.list.appendChild
}
this.displayList();
},
displayList
this.showsList = true;
if ( !this.is_active ) {
this.list.style.display = 'none';
if ( this.engineSelector ) { this.engineSelector.style.display = 'none'; }
return;
}
var nofItems = ( this.list.options.length > HotCat.list_size ? HotCat.list_size : this.list.options.length );
if ( nofItems <= 1 ) { nofItems = 2; }
this.list.size = nofItems;
this.list.style.align
this.list.style.zIndex
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';
Line 2,228 ⟶ 2,231:
// Approximate calculation of maximum list size
var maxListHeight = listh;
if ( nofItems < HotCat.list_size ) { maxListHeight = ( listh / nofItems ) * HotCat.list_size; }
function viewport
if ( is_webkit && !document.evaluate ) {
return window[ 'inner' + what ];
}
var s = 'client' + what;
if ( window.opera )
return
}
return ( document.documentElement ? document.documentElement[ s ] : 0 ) || document.body[ s ] || 0;
}
function scroll_offset
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.
Line 2,249 ⟶ 2,254:
// IE 7: like webkit; IE6: disabled in RTL anyway since too many problems.
// Opera: don't know...
if ( result < 0 ) { result = -
if ( !is_webkit
result = scroll_offset( 'Width' ) - viewport( 'Width' ) - result;
}
// Now all have webkit behavior, i.e. zero if at the leftmost edge.
Line 2,257 ⟶ 2,262:
return result;
}
function position
// Stripped-down simplified position function. It's good enough for our purposes.
if ( node.getBoundingClientRect ) {
var box
return {
y: Math.round( box.top + scroll_offset( 'Top' ) )
};
}
var t = 0, l = 0;
do {
t = t + ( node.offsetTop
l = l + ( node.offsetLeft || 0 );
node = node.offsetParent;
} while ( node );
return {
x: l,
y: t
};
}
var textPos = position
var nl = 0;
var nt = 0;
Line 2,280 ⟶ 2,289:
// Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
var textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
if ( this.engineName ) {
this.engineSelector.style.zIndex = 5;
this.engineSelector.style.position = 'absolute';
this.engineSelector.style.width = textBoxWidth + 'px';
// Figure out the height of this selector: display it off-screen, then hide it again.
if ( this.engineSelector.style.display === 'none' ) {
this.engineSelector.style[ anchor ] = '-10000px';
this.engineSelector.style.top = '0px';
this.engineSelector.style.display =
offset = this.engineSelector.offsetHeight;
this.engineSelector.style.display = 'none';
Line 2,294 ⟶ 2,303:
offset = this.engineSelector.offsetHeight;
}
this.engineSelector.style[ anchor
}
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 = -
if ( this.engineName ) { this.engineSelector.style.top = -
}
this.list.style.top = nt + 'px';
this.list.style.width =
this.list.style[ anchor ] = nl + 'px';
if ( this.engineName ) {
this.selectEngine
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
var view_w = viewport
var w
var l_pos
var left
var right
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 {
Line 2,336 ⟶ 2,345:
}
var relative_offset = 0;
if ( left < scroll ) {
relative_offset = scroll - left;
} else if ( right > scroll + view_w ) {
relative_offset = -
}
if ( is_rtl ) { relative_offset = -
if ( relative_offset !== 0 ) {
this.list.style[ anchor ] = ( nl + relative_offset ) + 'px';
}
}
},
autoComplete
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
// 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 {
Line 2,365 ⟶ 2,374:
this.text.focus();
this.text.value = newVal + key;
this.setSelection
return true;
},
canSelect
return this.text.setSelectionRange ||
},
setSelection
// this.text must be focused (at least on IE)
if ( !this.text.value ) { return; }
if ( this.text.setSelectionRange ) {
this.text.setSelectionRange
} else if ( typeof this.text.selectionStart !== 'undefined' ) {
if ( from > this.text.selectionStart ) {
this.text.selectionEnd
this.text.selectionStart = from;
} else {
this.text.selectionStart = from;
this.text.selectionEnd
}
} else if ( this.text.createTextRange ) { // IE
var new_selection = this.text.createTextRange();
new_selection.move
new_selection.moveEnd
new_selection.select();
}
},
getSelection
var from = 0, to = 0;
// this.text must be focused (at least on IE)
if ( !this.text.value ) {
// No text.
} else if ( typeof this.text.selectionStart !== 'undefined' ) {
from = this.text.selectionStart;
to
} 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
this.lastSelection = this.getSelection
},
processKey
var dir = 0;
switch ( this.lastKey ) {
case UP:
break;
case DOWN:
dir = 1;
break;
case PGUP:
dir = -HotCat.list_size;
break;
case PGDOWN:
break;
case ESC: // Inhibit default behavior (revert to last real input in FF: we do that ourselves)
return evtKill
}
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
if ( noSuggestions || !this.list || this.list.style.display === 'none' ) { return false; }
var curr = this.list.selectedIndex;
var tgt
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[
}
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
if ( !completed || this.list.options[ tgt ].text === this.lastRealInput ) {
this.text.value = this.list.options[ tgt ].text + key;
if ( this.canSelect() )
this.setSelection( this.list.options[ tgt ].text.length, this.list.options[ tgt ].text.length );
}
}
this.lastInput = this.list.options[ tgt ].text;
this.inputExists = true; // Might be wrong if from a dab list...
if ( this.icon ) { this.icon.src = armorUri( HotCat.existsYes ); }
this.state = CategoryEditor.CHANGE_PENDING;
}
Line 2,487 ⟶ 2,508:
},
resetKeySelection
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;
Line 2,510 ⟶ 2,531:
}; // end CategoryEditor.prototype
function initialize
// User configurations. Do this here, called from the onload handler, so that users can
// override it easily in their own user script files by just declaring variables. JSconfig
// is some feature used at Wikimedia Commons.
var config = ( typeof JSconfig !== 'undefined' && JSconfig.keys ) ? JSconfig.keys : {};
HotCat.dont_add_to_watchlist =
( typeof window.hotcat_dont_add_to_watchlist !== 'undefined' ?
);
HotCat.no_autocommit =
( typeof window.hotcat_no_autocommit !== 'undefined' ?
);
HotCat.del_needs_diff =
( typeof window.hotcat_del_needs_diff !== 'undefined' ?
);
HotCat.suggest_delay = window.hotcat_suggestion_delay ||
HotCat.editbox_width = window.hotcat_editbox_width ||
HotCat.suggestions
if ( typeof HotCat.suggestions !== 'string' || !suggestionConfigs[ HotCat.suggestions ] ) {
HotCat.suggestions = 'combined';
}
HotCat.fixed_search =
!!window.hotcat_suggestions_fixed :
config.HotCatFixedSuggestions :
HotCat.fixed_search
)
);
HotCat.single_minor =
( typeof window.hotcat_single_changes_are_minor !== 'undefined' ?
);
HotCat.bg_changed = window.hotcat_changed_background ||
HotCat.use_up_down =
( typeof window.hotcat_use_category_links !== 'undefined' ?
);
HotCat.list_size = window.hotcat_list_size ||
// Numeric input, make sure we have a numeric value
HotCat.list_size = parseInt
if (
if ( HotCat.list_size > 15 ) { HotCat.list_size = 15; }
// Localize search engine names
if ( HotCat.engine_names ) {
for ( var key in HotCat.engine_names ) {
if ( suggestionConfigs[ key ] && HotCat.engine_names[ key ] ) {
suggestionConfigs[ key ].name = HotCat.engine_names[ key ];
}
}
}
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
is_rtl = hasClass
if ( !is_rtl ) {
if ( document.defaultView && document.defaultView.getComputedStyle ) { // Gecko etc.
is_rtl = document.defaultView.getComputedStyle
} else if ( document.body.currentStyle ) { // IE, has subtle differences to getComputedStyle
is_rtl = document.body.currentStyle
} else { // Not exactly right, but best effort
is_rtl = document.body.style
}
is_rtl = ( is_rtl === 'rtl' );
}
}
function can_edit
var container = null;
switch ( mw.config.get( 'skin' ) ) {
case 'cologneblue':
container = document.getElementById
// Fall through
case 'standard':
case 'nostalgia':
if ( !container ) { container = document.getElementById
var lks = container.getElementsByTagName
for ( var i = 0; i < lks.length; i++ ) {
if (
}
return false;
default:
// all modern skins:
return document.getElementById
}
}
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
if ( !ip ) {
ip = document.getElementById
while ( ip && ip.nodeName.toLowerCase() !== 'table' ) { ip = ip.parentNode; }
}
if ( !ip ) { return; }
var reupload = document.getElementById
var destFile = document.getElementById
if (
// Insert a table row with two fields (label and empty category bar)
var labelCell = make
var lineCell
// Create the category line
catLine = make
catLine.className = 'catlinks';
catLine.id = 'catlinks';
Line 2,654 ⟶ 2,673:
catLine.style.margin = '0';
catLine.style.border = 'none';
lineCell.appendChild
// Create the label
var label = null;
if (
)
try {
label = UFUI.getLabel( 'wpCategoriesUploadLbl' );
} catch ( ex ) {
label = null;
}
}
if ( !label ) {
labelCell.id = 'hotcatLabel';
labelCell.appendChild
} else {
labelCell.id = 'hotcatLabelTranslated';
labelCell.appendChild
}
labelCell.className
labelCell.style.textAlign
labelCell.style.verticalAlign = 'middle';
// Change the onsubmit handler
var form = document.getElementById( 'upload' ) || document.getElementById( 'mw-upload-form' );
if ( form ) {
var newRow = ip.insertRow
newRow.appendChild
newRow.appendChild
form.onsubmit = ( function ( oldSubmit ) {
return function () {
var do_submit = true;
if ( oldSubmit ) {
if ( typeof oldSubmit === 'string' ) {
} else if ( typeof oldSubmit === 'function' ) {
do_submit = oldSubmit.apply( form, arguments );
}
}
if ( !do_submit ) {
return false;
}
closeForm();
// Copy the categories
var eb = document.getElementById
var addedOne = false;
for ( var i = 0; i < editors.length; i++ ) {
var t = editors[ i ].currentCategory;
if ( !t )
var key = editors[ i ].currentKey;
var new_cat = '[[' + HotCat.category_canonical + ':' + t + ( key ? '|' + key :
// Only add if not already present
var cleanedText = eb.value
if ( !find_category
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;
};
}
}
}
Line 2,725 ⟶ 2,747:
var cleanedText = null;
function isOnPage
var catTitle = title( span.firstChild.getAttribute( 'href', 2 ) );
catTitle = catTitle.substr( catTitle.indexOf( ':' ) + 1 ).replace( /_/g, ' ' );
var result = { title: catTitle, match: [ '', '', '' ] };
if (
if ( cleanedText === null ) {
cleanedText = pageText
.replace( /<
.replace( /<nowiki
}
result.match = find_category
return result;
}
Line 2,744 ⟶ 2,767:
var setupTimeout = null;
function findByClass
var result = window.jQuery( scope ).find( tag + '.' + className );
return ( result && result.length ) ? result[ 0 ] : null;
}
function setup
if ( initialized ) { return; }
initialized = true;
if ( setupTimeout ) {
window.clearTimeout
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 ||
var hiddenCats = document.getElementById( 'mw-hidden-catlinks' );
if ( !catLine ) {
var footer = null;
if ( !hiddenCats ) {
footer = findByClass(
if ( !footer ) { return; } // Don't know where to insert the category line
}
catLine = make
catLine.id = 'mw-normal-catlinks';
catLine.style.textAlign = is_rtl ? 'right' : 'left';
// Add a label
var label = make
label.href
label.title = HotCat.categories;
label.appendChild
catLine.appendChild
catLine.appendChild
// Insert the new category line
var container = ( hiddenCats ? hiddenCats.parentNode : document.getElementById
if ( !container ) {
container = make
container.id = 'catlinks';
footer.parentNode.insertBefore
}
container.className = 'catlinks noprint';
container.style.display =
if ( !hiddenCats ) {
container.appendChild
} else {
container.insertBefore
}
} // end if catLine exists
if ( is_rtl ) { catLine.dir = 'rtl'; }
// Create editors for all existing categories
function createEditors
var i;
var cats = line.getElementsByTagName
if ( cats.length > 0 ) {
newDOM = true; line = cats[ 0 ].parentNode;
} else {
cats = line.getElementsByTagName
}
// Copy cats, otherwise it'll also magically contain our added spans as it is a live collection!
var copyCats = new Array
for ( i = 0; i < cats.length; i++ ) { copyCats[ i ] = cats[ i ]; }
for ( i = 0; i < copyCats.length; i++ ) {
var test = isOnPage( copyCats[ i ] );
// eslint-disable-next-line no-new
}
}
return copyCats.length > 0 ? copyCats[ copyCats.length - 1 ] : null;
}
var lastSpan = createEditors
// 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 ( 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
enableMulti.className = 'noprint';
if ( is_rtl ) { enableMulti.dir = 'rtl'; }
catLine.insertBefore
enableMulti.appendChild
multiSpan = make
enableMulti.appendChild
multiSpan.innerHTML = '(<a>' + HotCat.addmulti + '</a>)';
var lk = multiSpan.getElementsByTagName
lk.onclick = function ( evt ) {
lk.title = HotCat.multi_tooltip;
lk.style.cursor = 'pointer';
}
cleanedText = null;
if ( typeof additionalWork === 'function' ) { additionalWork(); }
setupCompleted.loaded(); // Trigger signal; execute registered functions
}
function setPage
var startTime = null;
if ( json && json.query ) {
if ( json.query.pages ) {
var page = json.query.pages[ conf.wgArticleId === 0 ?
if ( page ) {
if ( page.revisions && page.revisions.length > 0 ) {
// Revisions are sorted by revision ID, hence [ 0 ] is the one we asked for, and possibly there's a [ 1 ] if we're
// not on the latest revision (edit conflicts and such).
pageText = page.revisions[ 0 ][ '*' ];
if ( page.revisions[ 0 ].timestamp ) { pageTime = page.revisions[ 0 ].timestamp.replace
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
pageWatched = typeof page.watched === 'string';
editToken = page.edittoken;
if ( page.langlinks && ( !json[ 'query-continue' ] || !json[ 'query-continue' ].langlinks ) ) {
// We have interlanguage links, and we got them all.
var re =
for ( var i = 0; i < page.langlinks.length; i++ ) {
re += ( i > 0 ? '|' :
}
if ( re.length > 0 ) {
interlanguageRE = new RegExp
}
}
Line 2,884 ⟶ 2,903:
}
// Siteinfo
if ( json.query.general ) {
// ResourceLoader's JSParser doesn't like .case, so override eslint.
// eslint-disable-next-line dot-notation
HotCat.capitalizePageNames = ( json.query.general[ 'case' ] === 'first-letter' );
if ( json.query.general.time && !startTime ) { startTime = json.query.general.time.replace( /\D/g, '' ); }
}
serverTime = startTime;
// Userinfo
if ( json.query.userinfo && json.query.userinfo.options ) {
watchCreate = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchcreations === '1';
watchEdit
minorEdits
// If the user has the "All edits are minor" preference enabled, we should honor that
// for single category changes, no matter what the site configuration is.
if ( minorEdits ) { HotCat.single_minor = true; }
}
}
}
function createCommitForm
if ( commitForm ) { return; }
var formContainer = make
formContainer.style.display = 'none';
document.body.appendChild
formContainer.innerHTML =
'<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="' +
'<input type="hidden" value="ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ" name="wpUnicodeCheck" />' +
'</form>';
commitForm = document.getElementById( 'hotcatCommitForm' );
}
function getPage
// We know we have an article here.
if ( conf.wgArticleId === 0 ) {
// Doesn't exist yet.
if ( conf.wgNamespaceNumber === 2 ) {
// Disable on non-existing User pages -- might be a global user page.
return;
}
pageText =
pageTime = null;
setup
} else {
var url = conf.wgServer + conf.wgScriptPath + '/api.php?format=json&callback=HotCat.start&action=query&rawcontinue=&titles=' +
var s = make
s.src = armorUri( url );
s.type = 'text/javascript';
HotCat.start = function ( json ) { setPage
document.getElementsByTagName
setupTimeout = window.setTimeout
}
}
function run
if ( HotCat.started ) { return; }
HotCat.started = true;
loadTrigger.register( really_run );
}
function really_run
initialize
if ( !HotCat.upload_disabled && conf.wgNamespaceNumber === -1 && conf.wgCanonicalSpecialPageName === 'Upload' && conf.wgUserName ) {
setup_upload();
// Check for state restoration once the setup is done otherwise, but before signalling setup completion
if (
UploadForm.previous_hotcat_state = setState( UploadForm.previous_hotcat_state );
}
} );
} else {
if ( !conf.wgIsArticle || conf.wgAction !== 'view' || param( 'diff' ) !== null || param( 'oldid' ) !== null || !can_edit() || HotCat.disable() ) { return; }
getPage
}
}
Line 2,982 ⟶ 3,002:
// Legacy stuff
function closeForm
// Close all open editors without redirect resolution and other asynchronous stuff.
for ( var i = 0; i < editors.length; i++ ) {
if ( editors[ i ].state === CategoryEditor.OPEN ) {
editors[ i ].cancel();
} else if ( editors[ i ].state === CategoryEditor.CHANGE_PENDING ) {
editors[ i ].sanitizeInput
var value = editors[ i ].text.value.split( '|' );
var key
if ( value.length > 1 ) { key = value[ 1 ]; }
var v = value[ 0 ].replace( /_/g, ' ' ).replace( /^\s+|\s+$/g,
if ( v.length === 0 ) {
editors[ i ].cancel
} else {
editors[ i ].currentCategory = v;
editors[ i ].currentKey = key;
editors[ i ].currentExists = this.inputExists;
editors[ i ].close
}
}
Line 3,005 ⟶ 3,025:
}
function getState
var result = null;
for ( var i = 0; i < editors.length; i++ ) {
var text = editors[ i ].currentCategory;
var key
if ( text && text.length > 0 ) {
if ( key !== null ) { text += '|' + key; }
if ( result === null ) {
result = text;
} else {
result = result + '\n' + text;
}
}
}
Line 3,021 ⟶ 3,042:
}
function setState
var cats = state.split
if ( cats.length === 0 ) { return null; }
if ( initialized && editors.length === 1 && editors[ 0 ].isAddCategory ) {
// Insert new spans and create new editors for them.
var newSpans = [];
var before = editors.length === 1 ? editors[ 0 ].span : null;
var i;
for ( i = 0; i < cats.length; i++ ) {
if ( cats[ i ].length === 0 ) { continue; }
var cat = cats[ i ].split
var key = cat.length > 1 ? cat[ 1 ] : null;
cat = cat[ 0 ];
var lk = make
lk.appendChild
lk.title = cat;
var span = make
span.appendChild
if ( i === 0 ) { catLine.insertBefore
catLine.insertBefore
if ( before && i + 1 < cats.length ) { parent.insertBefore(
newSpans.push
}
// And change the last one...
if ( before ) {
before.parentNode.insertBefore
}
for ( i = 0; i < newSpans.length; i++ ) {
// eslint-disable-next-line no-new
}
}
Line 3,057 ⟶ 3,078:
// Export legacy functions
window.hotcat_get_state
window.hotcat_set_state
window.hotcat_close_form = function () { closeForm
// Make sure we don't get conflicts with AjaxCategories (core development that should one day
// replace HotCat).
mw.config.set( 'disableAJAXCategories', true );
// Run as soon as possible. This varies depending on MediaWiki version;
// window's 'load' event is always safe, but usually we can do better than that.
if ( conf.wgCanonicalSpecialPageName !== 'Upload' ) {
// Reload HotCat after (VE) edits (bug T103285)
mw.hook( 'postEdit' ).add( function () {
// Reset HotCat in case this is a soft reload (VE edit)
catLine = null;
editors = [];
initialized = false;
HotCat.started = false;
run();
}
var startHotCat = function () {
$( run );
};
// We can safely trigger just after user configuration is loaded. Also start HotCat if the user module fails to load.
// Avoid using Promise methods of mw.loader.using as those aren't supported in older
// MediaWiki versions.
mw.loader.using( 'user', startHotCat, startHotCat );
}( jQuery, mediaWiki ) );
// </nowiki>
|