|
|
שורה 1: |
שורה 1: |
− | // <nowiki> | + | /** |
− | /* | + | * Cat-a-lot |
− | HotCat V2.35
| + | * Changes category of multiple files |
| + | * |
| + | * Originally by Magnus Manske |
| + | * RegExes by Ilmari Karonen |
| + | * Completely rewritten by DieBuche |
| + | * |
| + | * Requires [[MediaWiki:Gadget-SettingsManager.js]] and [[MediaWiki:Gadget-SettingsUI.js]] (properly registered) for per-user-settings |
| + | * |
| + | * READ THIS PAGE IF YOU WANT TO TRANSLATE OR USE THIS ON ANOTHER SITE: |
| + | * http://commons.wikimedia.org/wiki/MediaWiki:Gadget-Cat-a-lot.js/translating |
| + | * <nowiki> |
| + | */ |
| | | |
− | Ajax-based simple Category manager. Allows adding/removing/changing categories on a page view.
| + | /* global jQuery, mediaWiki, importStylesheet */ |
− | Supports multiple category changes, as well as redirect and disambiguation resolution. Also
| + | /* eslint one-var: 0, vars-on-top: 0, no-underscore-dangle:0 */ // extends: wikimedia |
− | plugs into the upload form. Search engines to use for the suggestion list are configurable, and
| + | /* jshint unused:true, forin:false, smarttabs:true, loopfunc:true, browser:true */ |
− | can be selected interactively.
| |
| | | |
− | Documentation: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat
| + | ( function( $, mw ) { |
− | List of main authors: https://commons.wikimedia.org/wiki/Help:Gadget-HotCat/Version_history
| + | 'use strict'; |
| | | |
− | License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
| + | var NS_CAT = 14, |
| + | formattedNS = mw.config.get( 'wgFormattedNamespaces' ), |
| + | nsIDs = mw.config.get( 'wgNamespaceIds' ); |
| | | |
− | Choose whichever license of these you like best :-)
| + | var msgs = { |
− | */
| + | // Preferences |
| + | // new: added 2012-09-19. Please translate. |
| + | // Use user language for i18n |
| + | 'cat-a-lot-watchlistpref': 'Watchlist preference concerning files edited with Cat-a-lot', |
| + | 'cat-a-lot-watch_pref': 'According to your general preferences', |
| + | 'cat-a-lot-watch_nochange': 'Do not change watchstatus', |
| + | 'cat-a-lot-watch_watch': 'Watch pages edited with Cat-a-lot', |
| + | 'cat-a-lot-watch_unwatch': 'Remove pages while editing with Cat-a-lot from your watchlist', |
| + | 'cat-a-lot-minorpref': 'Mark edits as minor (if you generally mark your edits as minor, this won\'t change anything)', |
| + | 'cat-a-lot-editpagespref': 'Allow categorising pages (including categories) that are not files', |
| + | 'cat-a-lot-docleanuppref': 'Remove {{Check categories}} and other minor cleanup', |
| + | 'cat-a-lot-subcatcountpref': 'Sub-categories to show at most', |
| + | 'cat-a-lot-config-settings': 'Preferences', |
| | | |
− | /* | + | // Progress |
− | This code should run on any MediaWiki installation >= MW 1.27.
| + | 'cat-a-lot-loading': 'Loading...', |
| + | 'cat-a-lot-editing': 'Editing page', |
| + | 'cat-a-lot-of': 'of ', |
| + | 'cat-a-lot-skipped-already': 'The following {{PLURAL:$1|1=page was|$1 pages were}} skipped, because the page was already in the category:', |
| + | 'cat-a-lot-skipped-not-found': 'The following {{PLURAL:$1|1=page was|$1 pages were}} skipped, because the old category could not be found:', |
| + | 'cat-a-lot-skipped-server': 'The following {{PLURAL:$1|1=page|$1 pages}} couldn\'t be changed, since there were problems connecting to the server:', |
| + | 'cat-a-lot-all-done': 'All pages are processed.', |
| + | 'cat-a-lot-done': 'Done!', |
| + | 'cat-a-lot-added-cat': 'Added category $1', |
| + | 'cat-a-lot-copied-cat': 'Copied to category $1', |
| + | 'cat-a-lot-moved-cat': 'Moved to category $1', |
| + | 'cat-a-lot-removed-cat': 'Removed from category $1', |
| + | 'cat-a-lot-return-to-page': 'Return to page', |
| + | 'cat-a-lot-cat-not-found': 'Category not found.', |
| | | |
− | For use with older versions of MediaWiki, use the archived versions below:
| + | // as in 17 files selected |
| + | 'cat-a-lot-files-selected': '{{PLURAL:$1|1=One file|$1 files}} selected.', |
| | | |
− | <=1.26: https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-HotCat.js&oldid=211134664
| + | // Actions |
− | */
| + | 'cat-a-lot-copy': 'Copy', |
− | /* eslint-disable */ // This old code uses too many coding conventions incompatible with eslint.
| + | 'cat-a-lot-move': 'Move', |
− | (function ($, mw) {
| + | 'cat-a-lot-add': 'Add', |
− | // Don't use mw.config.get() as that takes a copy of the config, and so doesn't | + | 'cat-a-lot-remove-from-cat': 'Remove from this category', |
− | // account for values changing, e.g. wgCurRevisionId after a VE edit | + | 'cat-a-lot-enter-name': 'Enter category name', |
− | var conf = mw.config.values; | + | 'cat-a-lot-select': 'Select', |
| + | 'cat-a-lot-all': 'all', |
| + | 'cat-a-lot-none': 'none', |
| + | 'cat-a-lot-none-selected': 'No files selected!', |
| | | |
− | if ( | + | // Summaries: |
− | // Guard against double inclusions (in old IE/Opera element ids become window properties)
| + | 'cat-a-lot-pref-save-summary': '[[c:Help:Gadget-Cat-a-lot|Cat-a-lot]]: updating user preferences', |
− | (window.HotCat && !window.HotCat.nodeName)
| + | 'cat-a-lot-summary-add': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Adding [[Category:$1]]', |
− | // Not on edit pages
| + | 'cat-a-lot-summary-copy': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Copying from [[Category:$1]] to [[Category:$2]]', |
− | || conf.wgAction == 'edit'
| + | 'cat-a-lot-summary-move': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Moving from [[Category:$1]] to [[Category:$2]]', |
| + | 'cat-a-lot-summary-remove': '[[c:Help:Cat-a-lot|Cat-a-lot]]: Removing from [[Category:$1]]' |
| + | }; |
| + | mw.messages.set( msgs ); |
| + | |
| + | function msg( /* params */ ) { |
| + | var args = Array.prototype.slice.call( arguments, 0 ); |
| + | args[0] = 'cat-a-lot-' + args[0]; |
| + | return mw.message.apply( mw.message, args ).parse(); |
| + | } |
| + | function msgPlain( key ) { |
| + | return mw.message( 'cat-a-lot-' + key ).plain(); |
| + | } |
| + | |
| + | // There is only one cat-a-lot on one page |
| + | var $body, $container, $dataContainer, $searchInputContainer, $searchInput, $resultList, $markCounter, |
| + | $selections, $selectAll, $selectNone, $settingsWrapper, $settingsLink, $head, $link; |
| + | var commons_url = 'https://commons.wikimedia.org/w/index.php'; |
| | | |
− | ) { | + | var catALot = window.catALot = { |
− | return; | + | apiUrl: mw.util.wikiScript( 'api' ), |
− | }
| + | origin: false, |
| + | searchmode: false, |
| + | version: 3.7, |
| + | setHeight: 450, |
| + | settings: {}, |
| + | init: function() { |
| + | this._initSettings(); |
| | | |
− | // Configuration stuff.
| + | $body = $( document.body ); |
− | window.HotCat = {
| + | $container = $( '<div>' ) |
− | // Localize these messages to the main language of your wiki.
| + | .attr( 'id', 'cat_a_lot' ) |
− | messages :
| + | .appendTo( $body ); |
− | {cat_removed : 'removed [[Category:$1]]'
| + | $dataContainer = $( '<div>' ) |
− | ,template_removed : 'removed {{[[Category:$1]]}}'
| + | .attr( 'id', 'cat_a_lot_data' ) |
− | ,cat_added : 'added [[Category:$1]]'
| + | .appendTo( $container ); |
− | ,cat_keychange: 'new key for [[Category:$1]]: "$2"' // $2 is the new key
| + | $searchInputContainer = $( '<div>' ) |
− | ,cat_notFound : 'Category "$1" not found'
| + | .appendTo( $dataContainer ); |
− | ,cat_exists : 'Category "$1" already exists; not added.'
| + | $searchInput = $( '<input>' ) |
− | ,cat_resolved : ' (redirect [[Category:$1]] resolved)'
| + | .attr({ |
− | ,uncat_removed: 'removed {{uncategorized}}'
| + | id: 'cat_a_lot_searchcatname', |
− | ,separator : '; '
| + | placeholder: msgPlain( 'enter-name' ), |
− | ,prefix : ""
| + | type: 'text' |
− | // Some text to prefix to the edit summary.
| + | }) |
− | ,using : ' using [[Help:Gadget-HotCat|HotCat]]'
| + | .appendTo( $searchInputContainer ); |
− | // Some text to append to the edit summary. Named 'using' for historical reasons. If you prefer
| + | $resultList = $( '<div>' ) |
− | // to have a marker at the front, use prefix and set this to the empty string. | + | .attr( 'id', 'cat_a_lot_category_list' ) |
− | ,multi_change : '$1 categories'
| + | .appendTo( $dataContainer ); |
− | // $1 is replaced by a number. If your language has several plural forms (c.f. [[:en:Dual (grammatical form)]]),
| + | $markCounter = $( '<div>' ) |
− | // you can set this to an array of strings suitable for passing to mw.language.configPlural().
| + | .attr( 'id', 'cat_a_lot_mark_counter' ) |
− | // If that function doesn't exist, HotCat will simply fall back to using the last | + | .appendTo( $dataContainer ); |
− | // entry in the array.
| + | $selections = $( '<div>' ) |
− | ,commit : 'Save'
| + | .attr( 'id', 'cat_a_lot_selections' ) |
− | // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
| + | .text( msgPlain( 'select' ) ) |
− | // see localization hook below.
| + | .appendTo( $dataContainer ); |
− | ,ok : 'OK'
| + | $selectAll = $( '<a>' ) |
− | // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
| + | .attr( 'id', 'cat_a_lot_select_all' ) |
− | // see localization hook below.
| + | .text( msgPlain( 'all' ) ) |
− | ,cancel : 'Cancel'
| + | .appendTo( $selections.append( ' ' ) ); |
− | // Button text. Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage,
| + | $selectNone = $( '<a>' ) |
− | // see localization hook below. | + | .attr( 'id', 'cat_a_lot_select_none' ) |
− | ,multi_error : 'Could not retrieve the page text from the server. Therefore, your category changes '
| + | .text( msgPlain( 'none' ) ) |
− | +'cannot be saved. We apologize for the inconvenience.'
| + | .appendTo( $selections.append( ' • ' ) ); |
− | // Localize to wgContentLanguage here; localize to wgUserLanguage in a subpage, | + | $settingsWrapper = $( '<div>' ) |
− | // see localization hook below.
| + | .attr( 'id', 'cat_a_lot_settings' ) |
− | ,short_catchange : null
| + | .appendTo( $dataContainer ); |
− | // Defaults to '[[' + category_canonical + ':$1]]'. Can be overridden if in the short edit summaries
| + | $settingsLink = $( '<a>' ) |
− | // not the standard category name should be used but, say, a shorter namespace alias. $1 is replaced
| + | .attr( 'id', 'cat_a_lot_config_settings' ) |
− | // by a category name. | + | .text( msgPlain( 'config-settings' ) ) |
− | }
| + | .appendTo( $settingsWrapper ); |
− | ,categories : 'Categories'
| + | $head = $( '<div>' ) |
− | // Plural of category_canonical.
| + | .attr( 'id', 'cat_a_lot_head' ) |
− | ,disambig_category : 'Disambiguation'
| + | .appendTo( $container ); |
− | // Any category in this category is deemed a disambiguation category; i.e., a category that should not contain
| + | $link = $( '<a>' ) |
− | // any items, but that contains links to other categories where stuff should be categorized. If you don't have
| + | .attr( 'id', 'cat_a_lot_toggle' ) |
− | // that concept on your wiki, set it to null. Use blanks, not underscores.
| + | .text( 'Cat-a-lot' ) |
− | ,redir_category : 'Category redirects'
| + | .appendTo( $head ); |
− | // Any category in this category is deemed a (soft) redirect to some other category defined by a link
| + | $settingsWrapper.append( $( '<a>' ) |
− | // to another non-blacklisted category. If your wiki doesn't have soft category redirects, set this to null.
| + | .attr( { |
− | // If a soft-redirected category contains more than one link to another non-blacklisted category, it's considered
| + | href: commons_url + '?title=Special:MyLanguage/Help:Gadget-Cat-a-lot', |
− | // a disambiguation category instead.
| + | target: '_blank', |
− | ,links : {change: '(±)', remove: '(\u2212)', add: '(+)', restore: '(×)', undo: '(×)', down: '(\u2193)', up: '(\u2191)'}
| + | style: 'float:right', |
− | // The little modification links displayed after category names. U+2212 is a minus sign; U+2193 and U+2191 are
| + | title: $( '#n-help' ).attr( 'title' ) |
− | // downward and upward pointing arrows. Do not use ↓ and ↑ in the code!
| + | } ).text( '?' ) ); |
− | ,tooltips : {
| |
− | change: 'Modify'
| |
− | ,remove: 'Remove'
| |
− | ,add: 'Add a new category'
| |
− | ,restore: 'Undo changes'
| |
− | ,undo: 'Undo changes'
| |
− | ,down: 'Open for modifying and display subcategories'
| |
− | ,up: 'Open for modifying and display parent categories'
| |
− | }
| |
− | // The tooltips for the above links
| |
− | ,addmulti : '<span>+<sup>+</sup></span>'
| |
− | // The HTML content of the "enter multi-mode" link at the front.
| |
− | ,multi_tooltip : 'Modify several categories'
| |
− | // Tooltip for the "enter multi-mode" link
| |
− | ,disable :
| |
− | function () { // Return true to disable HotCat.
| |
− | 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 === 0 // Non-existing file pages
| |
− | || ns === 2 && /\.(js|css)$/.test(conf.wgTitle) // User scripts
| |
− | || nsIds
| |
− | && ( ns === nsIds['creator']
| |
− | || ns === nsIds['timedtext']
| |
− | || ns === nsIds['institution']
| |
− | )
| |
− | );
| |
− | } | |
− | ,uncat_regexp : /\{\{\s*([Uu]ncat(egori[sz]ed( image)?)?|[Nn]ocat|[Nn]eedscategory)[^}]*\}\}\s*(<\!--.*?--\>)?/g
| |
− | // A regexp matching a templates used to mark uncategorized pages, if your wiki does have that.
| |
− | // If not, set it to null.
| |
− | ,existsYes : '//upload.wikimedia.org/wikipedia/commons/thumb/b/be/P_yes.svg/20px-P_yes.svg.png'
| |
− | ,existsNo : '//upload.wikimedia.org/wikipedia/commons/thumb/4/42/P_no.svg/20px-P_no.svg.png'
| |
− | // The images used for the little indication icon. Should not need changing.
| |
− | ,template_categories : {}
| |
− | // a list of categories which can be removed by removing a template
| |
− | // key: the category without namespace
| |
− | // value: A regexp matching the template name, again without namespace
| |
− | // If you don't have this at your wiki, or don't want this, set it to an empty object {}.
| |
− | ,engine_names : {
| |
− | searchindex : 'Search index'
| |
− | ,pagelist : 'Page list'
| |
− | ,combined : 'Combined search'
| |
− | ,subcat : 'Subcategories'
| |
− | ,parentcat : 'Parent categories'
| |
− | }
| |
− | // Names for the search engines
| |
− | ,capitalizePageNames : true
| |
− | // Set to false if your wiki has case-sensitive page names. MediaWiki has two modes: either the first letter
| |
− | // of a page is automatically capitalized ("first-letter"; Category:aa == Category:Aa), or it isn't
| |
− | // ("case-sensitive"; Category:aa != Category:Aa). It doesn't currently have a fully case-insensitive mode
| |
− | // (which would mean Category:aa == Category:Aa == Category:AA == Category:aA)
| |
− | // HotCat tries to set this correctly automatically using an API query. It's still a good idea to manually
| |
− | // configure it correctly; either directly here if you copied HotCat, or in the local configuration file
| |
− | // MediaWiki:Gadget-HotCat.js/local_defaults if you hotlink to the Commons-version, to ensure it is set even
| |
− | // if that API query should fail for some strange reason.
| |
− | ,upload_disabled : false
| |
− | // If upload_disabled is true, HotCat will not be used on the Upload form.
| |
− | ,blacklist : null
| |
− | // Single regular expression matching blacklisted categories that cannot be changed or
| |
− | // added using HotCat. For instance /\bstubs?$/ (any category ending with the word "stub"
| |
− | // or "stubs"), or /(\bstubs?$)|\bmaintenance\b/ (stub categories and any category with the
| |
− | // word "maintenance" in its title.
| |
| | | |
− | // Stuff changeable by users:
| + | if ( this.origin ) { |
− | ,bg_changed : '#F8CCB0'
| + | $( '<a>' ) |
− | // Background for changed categories in multi-edit mode. Default is a very light salmon pink.
| + | .attr( 'id', 'cat_a_lot_remove' ) |
− | ,no_autocommit : false
| + | .html( msg( 'remove-from-cat' ) ) |
− | // If true, HotCat will never automatically submit changes. HotCat will only open an edit page with
| + | .appendTo( $selections ) |
− | // the changes; users must always save explicitly.
| + | .click( function() { |
− | ,del_needs_diff : false
| + | catALot.remove(); |
− | // If true, the "category deletion" link "(-)" will never save automatically but always show an
| + | } ); |
− | // edit page where the user has to save the edit manually. Is false by default because that's the
| |
− | // traditional behavior. This setting overrides no_autocommit for "(-)" links.
| |
− | ,suggest_delay : 100
| |
− | // Time, in milliseconds, that HotCat waits after a keystroke before making a request to the
| |
− | // server to get suggestions.
| |
− | ,editbox_width : 40
| |
− | // Default width, in characters, of the text input field.
| |
− | ,suggestions : 'combined'
| |
− | // One of the engine_names above, to be used as the default suggestion engine.
| |
− | ,fixed_search : false
| |
− | // If true, always use the default engine, and never display a selector.
| |
− | ,use_up_down : true
| |
− | // If false, do not display the "up" and "down" links
| |
− | ,list_size : 5
| |
− | // Default list size
| |
− | ,single_minor : true
| |
− | // If true, single category changes are marked as minor edits. If false, they're not.
| |
− | ,dont_add_to_watchlist : false
| |
− | // If true, never add a page to the user's watchlist. If false, pages get added to the watchlist if
| |
− | // the user has the "Add pages I edit to my watchlist" or the "Add pages I create to my watchlist"
| |
− | // options in his or her preferences set.
| |
− | ,shortcuts : null
| |
− | ,addShortcuts :
| |
− | function (map) { | |
− | if (!map) return;
| |
− | window.HotCat.shortcuts = window.HotCat.shortcuts || {};
| |
− | for (var k in map) { | |
− | if (!map.hasOwnProperty (k) || typeof k != 'string') continue; | |
− | var v = map[k]; | |
− | if (typeof v != 'string') continue;
| |
− | k = k.replace (/^\s+|\s+$/g, ""); | |
− | v = v.replace (/^\s+|\s+$/g, ""); | |
− | if (k.length === 0 || v.length === 0) continue;
| |
− | window.HotCat.shortcuts[k] = v; | |
− | }
| |
| } | | } |
− | };
| |
| | | |
− | // More backwards compatibility. We have a few places where we test for the browser: once for
| + | if ( ( mw.util.getParamValue( 'withJS' ) === 'MediaWiki:Gadget-Cat-a-lot.js' && |
− | // Safari < 3.0, and twice for WebKit (Chrome or Safari, any versions)
| + | !mw.util.getParamValue( 'withCSS' ) ) || |
− | var ua = navigator.userAgent.toLowerCase();
| + | mw.loader.getState( 'ext.gadget.Cat-a-lot' ) === 'registered' ) { |
− | var is_webkit = /applewebkit\/\d+/.test(ua) && ua.indexOf ('spoofer') < 0;
| + | importStylesheet( 'MediaWiki:Gadget-Cat-a-lot.css' ); |
− | // And even more compatbility. HotCat was developed without jQuery, and anyway current jQuery
| |
− | // (1.7.1) doesn't seem to support in jquery.getJSON() or jQuery.ajax() the automatic
| |
− | // switching from GET to POST requests if the query arguments would make the uri too long.
| |
− | // (IE has a hard limit of 2083 bytes, and the servers may have limits around 4 or 8kB.)
| |
− | // Anyway, HotCat is supposed to run on wikis without jQuery, so we'd have to supply some
| |
− | // ajax routines ourselves in any case. We can't rely on the old sajax_init_object(), newer
| |
− | // MW versions (>= 1.19) might not have it.
| |
− | var getJSON = (function () {
| |
− | function getRequest () {
| |
− | var request = null; | |
− | try {
| |
− | request = new window.XMLHttpRequest();
| |
− | } catch (anything) {
| |
− | if (window.ActiveXObject) {
| |
− | try {
| |
− | request = new window.ActiveXObject('Microsoft.XMLHTTP');
| |
− | } catch (any) {
| |
− | }
| |
− | } // end if IE
| |
− | } // end try-catch
| |
− | return request;
| |
| } | | } |
| | | |
− | return function (settings) { | + | var reCat = new RegExp( '^\\s*' + catALot.localizedRegex( NS_CAT, 'Category' ) + ':', '' ); |
− | var req = getRequest();
| |
− | if (!req && settings && settings.error) settings.error (req);
| |
− | if (!req || !settings || !settings.uri) return req;
| |
− | var uri = armorUri (settings.uri);
| |
− | var args = settings.data || null;
| |
− | var method;
| |
− | if (args && uri.length + args.length + 1 > 2000) {
| |
− | // We lose caching, but at least we can make the request
| |
− | method = 'POST';
| |
− | req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
| |
− | } else {
| |
− | method = 'GET';
| |
− | if (args) uri += '?' + args;
| |
− | args = null;
| |
− | }
| |
− | req.open (method, uri, true);
| |
− | req.onreadystatechange = function () {
| |
− | if (req.readyState != 4) return;
| |
− | if (req.status != 200 || !req.responseText || !(/^\s*[\{\[]/.test(req.responseText))) {
| |
− | if (settings.error) settings.error (req);
| |
− | } else {
| |
− | if (settings.success) settings.success (eval ('(' + req.responseText + ')'));
| |
− | }
| |
− | };
| |
− | req.setRequestHeader ('Pragma', 'cache=yes');
| |
− | req.setRequestHeader ('Cache-Control', 'no-transform');
| |
− | req.send (args);
| |
− | return req;
| |
− | };
| |
− | })();
| |
− | | |
− | function armorUri (uri) {
| |
− | // Avoid protocol-relative URIs, IE7 has a bug with them in Ajax calls
| |
− | if (uri.length >= 2 && uri.substring(0, 2) == '//') return document.location.protocol + uri;
| |
− | return uri;
| |
− | }
| |
| | | |
− | function LoadTrigger (needed) {
| + | $searchInput.keypress( function( e ) { |
− | this.queue = []; | + | if ( e.which === 13 ) { |
− | this.toLoad = needed;
| + | catALot.updateCats( $.trim( $( this ).val() ) ); |
− | }
| |
− | LoadTrigger.prototype = {
| |
− | register : function (callback) {
| |
− | if (this.toLoad <= 0) { | |
− | callback (); // Execute directly | |
− | } else {
| |
− | this.queue[this.queue.length] = callback;
| |
| } | | } |
− | }, | + | } ) |
− | | + | .bind( 'input keyup', function() { |
− | loaded : function () {
| + | var oldVal = this.value, |
− | if (this.toLoad > 0) {
| + | newVal = oldVal.replace( reCat, '' ); |
− | this.toLoad--;
| + | if ( newVal !== oldVal ) { |
− | if (this.toLoad === 0) { | + | this.value = newVal; |
− | // Run queued callbacks once
| |
− | for (var i = 0; i < this.queue.length; i++) this.queue[i]();
| |
− | this.queue = []; | |
| } | | } |
− | } | + | } ); |
| + | if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Search' ) { |
| + | $searchInput.val( mw.util.getParamValue( 'search' ) ); |
| } | | } |
− | | + | function initAutocomplete() { |
− | };
| + | if ( catALot.autoCompleteIsEnabled ) { |
− | | + | return; |
− | var setupCompleted = new LoadTrigger(1);
| |
− | // Used to run user-registered code once HotCat is fully set up and ready.
| |
− | HotCat.runWhenReady = function (callback) {setupCompleted.register(callback);};
| |
− | | |
− | var loadTrigger = new LoadTrigger(2);
| |
− | // Used to delay running the HotCat setup until /local_defaults and localizations have been loaded.
| |
− | | |
− | function load (uri) {
| |
− | var head = document.getElementsByTagName ('head')[0];
| |
− | var s = document.createElement ('script');
| |
− | s.setAttribute ('src', armorUri(uri));
| |
− | s.setAttribute ('type', 'text/javascript');
| |
− | var done = false;
| |
− | | |
− | function afterLoad () { | |
− | if (done) return; | |
− | done = true;
| |
− | s.onload = s.onreadystatechange = s.onerror = null; // Properly clean up to avoid memory leaks in IE
| |
− | if (head && s.parentNode) head.removeChild (s);
| |
− | loadTrigger.loaded();
| |
− | }
| |
− | | |
− | s.onload = s.onreadystatechange = function () { // onreadystatechange for IE, onload for all others
| |
− | if (done) return;
| |
− | if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
| |
− | afterLoad ();
| |
| } | | } |
− | };
| + | catALot.autoCompleteIsEnabled = true; |
− | s.onerror = afterLoad; // Clean up, but otherwise ignore errors
| |
− | head.insertBefore (s, head.firstChild); // appendChild may trigger bugs in IE6 here
| |
− | }
| |
| | | |
− | function loadJS (page) {
| + | $searchInput.autocomplete( { |
− | load (conf.wgServer + conf.wgScript + '?title=' + encodeURIComponent (page) + '&action=raw&ctype=text/javascript');
| + | source: function( request, response ) { |
− | }
| + | catALot.doAPICall( { |
− | | + | action: 'opensearch', |
− | function loadURI (href) {
| + | search: request.term, |
− | var url = href;
| + | redirects: 'resolve', |
− | if (url.substring (0, 2) == '//') {
| + | namespace: NS_CAT |
− | url = window.location.protocol + url;
| + | }, function( data ) { |
− | } else if (url.substring (0, 1) == '/') {
| + | if ( data[ 1 ] ) { |
− | url = conf.wgServer + url; | + | response( $( data[ 1 ] ) |
| + | .map( function( index, item ) { |
| + | return item.replace( reCat, '' ); |
| + | } ) ); |
| + | } |
| + | } ); |
| + | }, |
| + | open: function() { |
| + | $( '.ui-autocomplete' ) |
| + | .position( { |
| + | my: $( 'body' ) |
| + | .is( '.rtl' ) ? 'left bottom' : 'right bottom', |
| + | at: $( 'body' ) |
| + | .is( '.rtl' ) ? 'left top' : 'right top', |
| + | of: $searchInput |
| + | } ); |
| + | }, |
| + | appendTo: '#cat_a_lot' |
| + | } ); |
| } | | } |
− | load (url);
| |
− | }
| |
| | | |
− | // Load local configurations, overriding the pre-set default values in the HotCat object above. This is always loaded
| + | $selectAll |
− | // from the wiki where this script is executing, even if this script itself is hotlinked from the Commons. This can
| + | .click( function() { |
− | // be used to change the default settings, or to provide localized interface texts for edit summaries and so on.
| + | catALot.toggleAll( true ); |
− | loadJS ('MediaWiki:Gadget-HotCat.js/local_defaults');
| + | } ); |
− | | + | $selectNone |
− | // Load localized UI texts. These are the texts that HotCat displays on the page itself. Texts shown in edit summaries
| + | .click( function() { |
− | // should be localized in /local_defaults above.
| + | catALot.toggleAll( false ); |
− | 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
| + | $link |
− | // explicitly if you're not on the Commons and don't want that.
| + | .click( function() { |
− | if (typeof window.hotcat_translations_from_commons == 'undefined') {
| + | $( this ).toggleClass( 'cat_a_lot_enabled' ); |
− | window.hotcat_translations_from_commons = true; | + | // Load autocomplete on demand |
− | } | + | mw.loader.using( [ 'jquery.ui.autocomplete' ], initAutocomplete ); |
− | // Localization hook to localize HotCat messages, tooltips, and engine names for wgUserLanguage.
| + | catALot.run(); |
− | if (window.hotcat_translations_from_commons && conf.wgServer.indexOf('//commons') < 0) {
| + | } ); |
− | loadURI ('//commons.wikimedia.org/w/index.php?title='
| + | $settingsLink |
− | + 'MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage | + | .click( function() { |
− | + '&action=raw&ctype=text/javascript'
| + | catALot.manageSettings(); |
− | ); | + | } ); |
− | } else { | |
− | // Load translations locally | |
− | loadJS ('MediaWiki:Gadget-HotCat.js/' + conf.wgUserLanguage);
| |
− | }
| |
− | } else {
| |
− | loadTrigger.loaded();
| |
− | }
| |
| | | |
− | // No further changes should be necessary here. | + | this.localCatName = formattedNS[ NS_CAT ]; |
| + | }, |
| | | |
− | // The following regular expression strings are used when searching for categories in wikitext. | + | findAllLabels: function( searchmode ) { |
− | var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+';
| + | // It's possible to allow any kind of pages as well but what happens if you click on "select all" and don't expect it |
− | var wikiTextBlankRE = new RegExp (wikiTextBlank, 'g');
| + | switch ( searchmode ) { |
− | // Regexp for handling blanks inside a category title or namespace name.
| + | case 'search': |
− | // See http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Title.php?revision=104051&view=markup#l2722
| + | this.labels = this.labels.add( $( 'table.searchResultImage' ).find( 'tr>td:eq(1)' ) ); |
− | // See also http://www.fileformat.info/info/unicode/category/Zs/list.htm
| + | if ( this.settings.editpages ) { |
− | // MediaWiki collapses several contiguous blanks inside a page title to one single blank. It also replace a
| + | this.labels = this.labels.add( 'div.mw-search-result-heading' ); |
− | // 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.
| + | break; |
− | // Note: we _do_ include the horizontal tab in the above list, even though the MediaWiki software for some reason
| + | case 'category': |
− | // appears to not handle it. The zero-width space \u200B is _not_ handled as a space inside titles by MW.
| + | this.findAllLabels( 'gallery' ); |
− | var wikiTextBlankOrBidi = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200B\\u200E\\u200F\\u2028-\\u202F\\u205F\\u3000]*';
| + | this.labels = this.labels.add( $( 'div#mw-category-media' ).find( 'li[class!="gallerybox"]' ) ); |
− | // Whitespace regexp for handling whitespace between link components. Including the horizontal tab, but not \n\r\f\v:
| |
− | // a link must be on one single line.
| |
− | // MediaWiki also removes Unicode bidi override characters in page titles (and namespace names) completely.
| |
− | // This is *not* handled, as it would require us to allow any of [\u200E\u200F\u202A-\u202E] between any two
| |
− | // characters inside a category link. It _could_ be done though... We _do_ handle strange spaces, including the
| |
− | // zero-width space \u200B, and bidi overrides between the components of a category link (adjacent to the colon,
| |
− | // or adjacent to and inside of "[[" and "]]").
| |
| | | |
− | // First auto-localize the regexps for the category and the template namespaces.
| + | if ( this.settings.editpages ) { |
− | var formattedNamespaces = conf.wgFormattedNamespaces;
| + | this.labels = this.labels.add( $( 'div#mw-pages, div#mw-subcategories' ).find( 'li' ) ); |
− | var namespaceIds = conf.wgNamespaceIds;
| |
− | function autoLocalize (namespaceNumber, fallback) {
| |
− | function create_regexp_str (name)
| |
− | {
| |
− | if (!name || name.length === 0) return "";
| |
− | var regex_name = "";
| |
− | for (var i = 0; i < name.length; i++){
| |
− | var initial = name.substr (i, 1);
| |
− | var ll = initial.toLowerCase ();
| |
− | var ul = initial.toUpperCase ();
| |
− | if (ll == ul){
| |
− | regex_name += initial;
| |
− | } else {
| |
− | regex_name += '[' + ll + ul + ']';
| |
| } | | } |
− | } | + | break; |
− | return regex_name | + | case 'contribs': |
− | .replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1')
| + | this.labels = this.labels.add( $( 'ul.mw-contributions-list li' ) ); |
− | .replace (wikiTextBlankRE, wikiTextBlank);
| + | // FIXME: Filter if !this.settings.editpages |
| + | break; |
| + | case 'prefix': |
| + | this.labels = this.labels.add( $( 'ul.mw-prefixindex-list li' ) ); |
| + | break; |
| + | case 'listfiles': |
| + | // this.labels = this.labels.add( $( 'table.listfiles>tbody>tr' ).find( 'td:eq(1)' ) ); |
| + | this.labels = this.labels.add( $( '.TablePager_col_img_name' ) ); |
| + | break; |
| + | case 'gallery': |
| + | this.labels = this.labels.add( 'div.gallerytext' ); |
| + | break; |
| } | | } |
| + | }, |
| | | |
− | fallback = fallback.toLowerCase();
| + | getTitleFromLink: function( href ) { |
− | var canonical = formattedNamespaces["" + namespaceNumber].toLowerCase(); | + | try { |
− | var regexp = create_regexp_str (canonical);
| + | return decodeURIComponent( href ) |
− | if (fallback && canonical != fallback) regexp += '|' + create_regexp_str(fallback);
| + | .match( /wiki\/(.+?)(?:#.+)?$/ )[ 1 ].replace( /_/g, ' ' ); |
− | if (namespaceIds) {
| + | } catch ( ex ) { |
− | for (var cat_name in namespaceIds) {
| + | return ''; |
− | if ( typeof cat_name == 'string'
| |
− | && cat_name.toLowerCase() != canonical
| |
− | && cat_name.toLowerCase() != fallback
| |
− | && namespaceIds[cat_name] == namespaceNumber)
| |
− | {
| |
− | regexp += '|' + create_regexp_str(cat_name);
| |
− | }
| |
− | }
| |
| } | | } |
− | return regexp; | + | }, |
− | }
| + | |
| + | getMarkedLabels: function() { |
| + | this.selectedLabels = this.labels.filter( '.cat_a_lot_selected:visible' ); |
| + | return this.selectedLabels.map( function() { |
| + | var file = $( this ).find( 'a[title][class$="title"]' ); |
| + | file = file.length ? file : $( this ).find( 'a[title]' ); |
| + | var title = file.attr( 'title' ) || |
| + | catALot.getTitleFromLink( file.attr( 'href' ) ) || |
| + | catALot.getTitleFromLink( $( this ).find( 'a' ) |
| + | .attr( 'href' ) ); |
| | | |
− | HotCat.category_canonical = formattedNamespaces['14'];
| + | return [ [ title, $( this ) ] ]; |
− | HotCat.category_regexp = autoLocalize (14, 'category');
| + | } ); |
− | if (formattedNamespaces['10']) {
| + | }, |
− | HotCat.template_regexp = autoLocalize (10, 'template'); | |
− | } | |
| | | |
− | // Utility functions. Yes, this duplicates some functionality that also exists in other places, but | + | updateSelectionCounter: function() { |
− | // to keep this whole stuff in a single file not depending on any other on-wiki Javascripts, we re-do
| + | this.selectedLabels = this.labels.filter( '.cat_a_lot_selected' ); |
− | // these few operations here.
| + | $markCounter |
− | function make (arg, literal) {
| + | .show() |
− | if (!arg) return null;
| + | .html( msg( 'files-selected', this.selectedLabels.length ) ); |
− | return literal ? document.createTextNode (arg) : document.createElement (arg);
| + | }, |
− | }
| |
− | function param (name, uri) {
| |
− | if (typeof uri == 'undefined' || uri === null) uri = document.location.href; | |
− | var re = new RegExp ('[&?]' + name + '=([^&#]*)');
| |
− | var m = re.exec (uri);
| |
− | if (m && m.length > 1) return decodeURIComponent(m[1]);
| |
− | return null;
| |
− | }
| |
− | function title (href) {
| |
− | if (!href) return null;
| |
− | var script = 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) !== 0) 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 (elem, name) {
| |
− | return (' ' + elem.className + ' ').indexOf (' ' + name + ' ') >= 0;
| |
− | }
| |
− | function capitalize (str) {
| |
− | if (!str || str.length === 0) return str;
| |
− | return str.substr(0, 1).toUpperCase() + str.substr (1);
| |
− | } | |
− | function wikiPagePath (pageName) {
| |
− | // Note: do not simply use encodeURI, it doesn't encode '&', which might break if wgArticlePath actually has the $1 in
| |
− | // a query parameter.
| |
− | return conf.wgArticlePath.replace('$1', encodeURIComponent (pageName).replace(/%3A/g, ':').replace(/%2F/g, '/'));
| |
− | }
| |
− | function escapeRE(str) {
| |
− | return str.replace(/([\\\^\$\.\?\*\+\(\)\[\]])/g, '\\$1');
| |
− | }
| |
| | | |
− | function substituteFactory (options) { | + | makeClickable: function() { |
− | options = options || {}; | + | this.labels = $(); |
− | var lead = options.indicator || '$'; | + | this.findAllLabels( this.searchmode ); |
− | var indicator = escapeRE (lead); | + | this.labels.catALotShiftClick( function() { |
− | var lbrace = escapeRE (options.lbrace || '{');
| + | catALot.updateSelectionCounter(); |
− | var rbrace = escapeRE (options.rbrace || '}'); | + | } ) |
− | var re;
| + | .addClass( 'cat_a_lot_label' ); |
| + | }, |
| | | |
− | re = new RegExp(
| + | toggleAll: function( select ) { |
− | '(?:' + indicator + '(' + indicator + '))|' // $$
| + | this.labels.toggleClass( 'cat_a_lot_selected', select ); |
− | +'(?:' + indicator + '(\\d+))|' // $0, $1
| + | this.updateSelectionCounter(); |
− | +'(?:' + indicator + '(?:' + lbrace + '([^' + lbrace + rbrace + ']+)' + rbrace + '))|' // ${key}
| + | }, |
− | +'(?:' + indicator + '(?!(?:[' + indicator + lbrace + ']|\\d))(\\S+?)\\b)' // $key (only if first char after $ is not $, digit, or { )
| |
− | ,'g');
| |
− | // Replace $1, $2, or ${key1}, ${key2}, or $key1, $key2 by values from map. $$ is replaced by a single $. | |
− | return function (str, map) {
| |
− | if (!map) return str;
| |
− | return str.replace(re
| |
− | ,function (match, prefix, idx, key, alpha) {
| |
− | if (prefix == lead) return lead;
| |
− | var k = alpha || key || idx;
| |
− | var replacement = typeof map[k] === 'function' ? map[k](match, k) : map[k];
| |
− | return typeof replacement === 'string' ? replacement : (replacement || match);
| |
− | }
| |
− | );
| |
− | };
| |
− | } | |
| | | |
− | var substitute = substituteFactory(); | + | getSubCats: function() { |
− | var replaceShortcuts = (function () {
| + | var data = { |
− | var replaceHash = substituteFactory({indicator:'#',lbrace:'[',rbrace:']'}); | + | action: 'query', |
− | return function (str, map) {
| + | list: 'categorymembers', |
− | var s = replaceHash (str, map); | + | cmtype: 'subcat', |
− | return HotCat.capitalizePageNames ? capitalize(s) : s; | + | cmlimit: this.settings.subcatcount, |
| + | cmtitle: 'Category:' + this.currentCategory |
| }; | | }; |
− | })();
| |
| | | |
− | // Text modification
| + | this.doAPICall( data, function( result ) { |
| + | var cats = result.query.categorymembers; |
| + | |
| + | catALot.subCats = []; |
| + | for ( var i = 0; i < cats.length; i++ ) { |
| + | catALot.subCats.push( cats[ i ].title.replace( /^[^:]+:/, '' ) ); |
| + | } |
| + | catALot.catCounter++; |
| + | if ( catALot.catCounter === 2 ) { |
| + | catALot.showCategoryList(); |
| + | } |
| + | } ); |
| + | }, |
| | | |
− | var findCatsRE = | + | getParentCats: function() { |
− | new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(?:' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':[^\\]]+\\]\\]', 'g'); | + | var data = { |
| + | action: 'query', |
| + | prop: 'categories', |
| + | titles: 'Category:' + this.currentCategory |
| + | }; |
| + | this.doAPICall( data, function( result ) { |
| + | catALot.parentCats = []; |
| + | var cats, pages = result.query.pages; |
| + | if ( pages[ -1 ] && pages[ -1 ].missing === '' ) { |
| + | $resultList.html( '<span id="cat_a_lot_no_found">' + msg( 'cat-not-found' ) + '</span>' ); |
| + | document.body.style.cursor = 'auto'; |
| | | |
− | function replaceByBlanks (match) {
| + | $resultList.append( '<table>' ); |
− | return match.replace(/(\s|\S)/g, ' '); // /./ doesn't match linebreaks. /(\s|\S)/ does.
| + | catALot.createCatLinks( '→', [ catALot.currentCategory ] ); |
− | }
| + | return; |
| + | } |
| + | // there should be only one, but we don't know its ID |
| + | for ( var id in pages ) { |
| + | cats = pages[ id ].categories; |
| + | } |
| + | for ( var i = 0; i < cats.length; i++ ) { |
| + | catALot.parentCats.push( cats[ i ].title.replace( /^[^:]+:/, '' ) ); |
| + | } |
| | | |
− | function find_category (wikitext, category, once) {
| + | catALot.catCounter++; |
− | var cat_regex = null;
| + | if ( catALot.catCounter === 2 ) { |
− | if(HotCat.template_categories[category]){
| + | catALot.showCategoryList(); |
− | cat_regex = new RegExp ('\\{\\{' + wikiTextBlankOrBidi + '(' + HotCat.template_regexp + '(?=' + wikiTextBlankOrBidi + ':))?' + wikiTextBlankOrBidi | + | } |
− | + '(?:' + HotCat.template_categories[category] + ')'
| + | } ); |
− | + wikiTextBlankOrBidi + '(\\|.*?)?\\}\\}', 'g'
| + | }, |
− | );
| |
− | } else {
| |
− | var cat_name = escapeRE (category); | |
− | var initial = cat_name.substr (0, 1);
| |
− | cat_regex = new RegExp ('\\[\\[' + wikiTextBlankOrBidi + '(' + HotCat.category_regexp + ')' + wikiTextBlankOrBidi + ':' + wikiTextBlankOrBidi
| |
− | + (initial == '\\' || !HotCat.capitalizePageNames
| |
− | ? initial
| |
− | : '[' + initial.toUpperCase() + initial.toLowerCase() + ']')
| |
− | + cat_name.substring (1).replace (wikiTextBlankRE, wikiTextBlank)
| |
− | + wikiTextBlankOrBidi + '(\\|.*?)?\\]\\]', 'g'
| |
− | ); | |
− | }
| |
− | if (once) return cat_regex.exec (wikitext); | |
− | var copiedtext = wikitext
| |
− | .replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
| |
− | .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
| |
− | var result = [];
| |
− | var curr_match = null;
| |
− | while ((curr_match = cat_regex.exec (copiedtext)) !== null) {
| |
− | result.push ({match : curr_match});
| |
− | }
| |
− | result.re = cat_regex;
| |
− | return result; // An array containing all matches, with positions, in result[i].match
| |
− | }
| |
| | | |
− | var interlanguageRE = null; | + | localizedRegex: function( namespaceNumber, fallback ) { |
| + | // Copied from HotCat. Thanks Lupo. |
| + | var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+'; |
| + | var wikiTextBlankRE = new RegExp( wikiTextBlank, 'g' ); |
| | | |
− | function change_category (wikitext, toRemove, toAdd, key, is_hidden) {
| + | var createRegexStr = function( name ) { |
− | | + | if ( !name || name.length === 0 ) { |
− | function find_insertionpoint (wikitext) { | + | return ''; |
− | var copiedtext = wikitext | + | } |
− | .replace(/<\!--(\s|\S)*?--\>/g, replaceByBlanks)
| + | var regexName = ''; |
− | .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, replaceByBlanks);
| + | for ( var i = 0; i < name.length; i++ ) { |
− | // Search in copiedtext to avoid that we insert inside an HTML comment or a nowiki "element". | + | var initial = name.substr( i, 1 ); |
− | var index = -1; | + | var ll = initial.toLowerCase(); |
− | findCatsRE.lastIndex = 0; | + | var ul = initial.toUpperCase(); |
− | while (findCatsRE.exec(copiedtext) !== null) index = findCatsRE.lastIndex;
| + | if ( ll === ul ) { |
− | if (index < 0) {
| + | regexName += initial; |
− | // 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 { | | } else { |
− | match = interlanguageRE.exec(copiedtext); | + | regexName += '[' + ll + ul + ']'; |
| } | | } |
− | if (match) index = match.index;
| |
− | return {idx : index, onCat : false};
| |
| } | | } |
− | return {idx : index, onCat : index >= 0}; | + | return regexName.replace( /([\\\^\$\.\?\*\+\(\)])/g, '\\$1' ) |
− | } | + | .replace( wikiTextBlankRE, wikiTextBlank ); |
| + | }; |
| | | |
− | var summary = []; | + | fallback = fallback.toLowerCase(); |
− | var nameSpace = HotCat.category_canonical; | + | var canonical = formattedNS[ namespaceNumber ].toLowerCase(); |
− | var cat_point = -1; // Position of removed category;
| + | var RegexString = createRegexStr( canonical ); |
− | | + | if ( fallback && canonical !== fallback ) { |
− | if (key) key = '|' + key;
| + | RegexString += '|' + createRegexStr( fallback ); |
− | var keyChange = (toRemove && toAdd && toRemove == toAdd && toAdd.length > 0); | |
− | var matches;
| |
− | if (toRemove && toRemove.length > 0) { | |
− | matches = find_category (wikitext, toRemove);
| |
− | if (!matches || matches.length === 0) {
| |
− | return {text: wikitext, 'summary': summary, error: HotCat.messages.cat_notFound.replace (/\$1/g, toRemove)};
| |
− | } else { | |
− | var before = wikitext.substring (0, matches[0].match.index);
| |
− | var after = wikitext.substring (matches[0].match.index + matches[0].match[0].length);
| |
− | if (matches.length > 1) {
| |
− | // Remove all occurrences in after
| |
− | matches.re.lastIndex = 0;
| |
− | after = after.replace (matches.re, "");
| |
− | }
| |
− | if (toAdd) {
| |
− | nameSpace = matches[0].match[1] || nameSpace;
| |
− | if (key === null) key = matches[0].match[2]; // Remember the category key, if any.
| |
− | }
| |
− | // Remove whitespace (properly): strip whitespace, but only up to the next line feed.
| |
− | // If we then have two linefeeds in a row, remove one. Otherwise, if we have two non-
| |
− | // whitespace characters, insert a blank.
| |
− | var i = before.length - 1;
| |
− | while (i >= 0 && before.charAt (i) != '\n' && before.substr (i, 1).search (/\s/) >= 0) i--;
| |
− | var j = 0;
| |
− | while (j < after.length && after.charAt (j) != '\n' && after.substr (j, 1).search (/\s/) >= 0)
| |
− | j++;
| |
− | if (i >= 0 && before.charAt (i) == '\n' && (after.length === 0 || j < after.length && after.charAt (j) == '\n'))
| |
− | i--;
| |
− | if (i >= 0) before = before.substring (0, i+1); else before = "";
| |
− | if (j < after.length) after = after.substring (j); else after = "";
| |
− | if (before.length > 0 && before.substring (before.length - 1).search (/\S/) >= 0
| |
− | && after.length > 0 && after.substr (0, 1).search (/\S/) >= 0)
| |
− | before += ' ';
| |
− | cat_point = before.length;
| |
− | if (cat_point === 0 && after.length > 0 && after.substr(0,1) == '\n') {
| |
− | after = after.substr(1);
| |
− | }
| |
− | wikitext = before + after;
| |
− | if (!keyChange) {
| |
− | if(HotCat.template_categories[toRemove]) {
| |
− | summary.push (HotCat.messages.template_removed.replace (/\$1/g, toRemove));
| |
− | } else {
| |
− | summary.push (HotCat.messages.cat_removed.replace (/\$1/g, toRemove));
| |
− | }
| |
− | }
| |
− | }
| |
| } | | } |
− | if (toAdd && toAdd.length > 0) { | + | for ( var catName in nsIDs ) { |
− | matches = find_category (wikitext, toAdd);
| + | if ( typeof catName === 'string' && catName.toLowerCase() !== canonical && catName.toLowerCase() !== fallback && nsIDs[ catName ] === namespaceNumber ) { |
− | if (matches && matches.length > 0) { | + | RegexString += '|' + createRegexStr( catName ); |
− | return {text: wikitext, 'summary': summary, error : HotCat.messages.cat_exists.replace (/\$1/g, toAdd)};
| |
− | } else {
| |
− | var onCat = false;
| |
− | if (cat_point < 0) {
| |
− | var point = find_insertionpoint (wikitext);
| |
− | cat_point = point.idx;
| |
− | onCat = point.onCat;
| |
− | } else {
| |
− | onCat = true;
| |
− | }
| |
− | var newcatstring = '[[' + nameSpace + ':' + toAdd + (key || "") + ']]';
| |
− | if (cat_point >= 0) {
| |
− | var suffix = wikitext.substring (cat_point);
| |
− | wikitext = wikitext.substring (0, cat_point) + (cat_point > 0 ? '\n' : "") + newcatstring + (!onCat ? '\n' : "");
| |
− | if (suffix.length > 0 && suffix.substr(0, 1) != '\n') {
| |
− | wikitext += '\n' + suffix;
| |
− | } else {
| |
− | wikitext += suffix;
| |
− | }
| |
− | } else {
| |
− | if (wikitext.length > 0 && wikitext.substr (wikitext.length - 1, 1) != '\n')
| |
− | wikitext += '\n';
| |
− | wikitext += (wikitext.length > 0 ? '\n' : "") + newcatstring;
| |
− | }
| |
− | if (keyChange) {
| |
− | var k = key || "";
| |
− | if (k.length > 0) k = k.substr (1);
| |
− | summary.push (substitute (HotCat.messages.cat_keychange, [null, toAdd, k]));
| |
− | } else {
| |
− | summary.push (HotCat.messages.cat_added.replace (/\$1/g, toAdd));
| |
− | } | |
− | if (HotCat.uncat_regexp && !is_hidden) {
| |
− | var txt = wikitext.replace (HotCat.uncat_regexp, ""); // Remove "uncat" templates
| |
− | if (txt.length != wikitext.length) {
| |
− | wikitext = txt;
| |
− | summary.push (HotCat.messages.uncat_removed);
| |
− | }
| |
− | }
| |
| } | | } |
| } | | } |
− | return {text: wikitext, 'summary': summary, error: null}; | + | return ( '(?:' + RegexString + ')' ); |
− | }
| + | }, |
| + | |
| + | regexBuilder: function( category ) { |
| + | var catname = this.localizedRegex( NS_CAT, 'Category' ); |
| + | |
| + | // Build a regexp string for matching the given category: |
| + | // trim leading/trailing whitespace and underscores |
| + | category = category.replace (/^[\s_]+|[\s_]+$/g, ""); |
| + | |
| + | // escape regexp metacharacters (= any ASCII punctuation except _) |
| + | category = mw.RegExp.escape( category ); |
| | | |
− | // The real HotCat UI
| + | // any sequence of spaces and underscores should match any other |
| + | category = category.replace( /[\s_]+/g, '[\\s_]+' ); |
| | | |
− | function evtKeys (e) {
| + | // Make the first character case-insensitive: |
− | e = e || window.event || window.Event; // W3C, IE, Netscape | + | var first = category.substr( 0, 1 ); |
− | var code = 0; | + | if ( first.toUpperCase() !== first.toLowerCase() ) { |
− | if (typeof e.ctrlKey != 'undefined') { // All modern browsers | + | category = '[' + first.toUpperCase() + first.toLowerCase() + ']' + category.substr( 1 ); |
− | // Ctrl-click seems to be overloaded in FF/Mac (it opens a pop-up menu), so treat cmd-click | |
− | // as a ctrl-click, too.
| |
− | if (e.ctrlKey || e.metaKey) code |= 1;
| |
− | if (e.shiftKey) code |= 2;
| |
− | } else if (typeof e.modifiers != 'undefined') { // Netscape...
| |
− | if (e.modifiers & (Event.CONTROL_MASK | Event.META_MASK)) code |= 1;
| |
− | if (e.modifiers & Event.SHIFT_MASK) code |= 2;
| |
| } | | } |
− | return code;
| |
− | }
| |
− | function evtKill (e) {
| |
− | e = e || window.event || window.Event; // W3C, IE, Netscape
| |
− | if (typeof e.preventDefault != 'undefined') {
| |
− | e.preventDefault ();
| |
− | e.stopPropagation ();
| |
− | } else
| |
− | e.cancelBubble = true;
| |
− | return false;
| |
− | }
| |
| | | |
− | var catLine = null;
| + | // Compile it into a RegExp that matches MediaWiki category syntax (yeah, it looks ugly): |
− | var onUpload = false;
| + | // XXX: the first capturing parens are assumed to match the sortkey, if present, including the | but excluding the ]] |
− | var editors = [];
| + | return new RegExp( '\\[\\[[\\s_]*' + catname + '[\\s_]*:[\\s_]*' + category + '[\\s_]*(\\|[^\\]]*(?:\\][^\\]]+)*)?\\]\\]\\s*', 'g' ); |
| + | }, |
| | | |
− | var commitButton = null; | + | getContent: function( file, targetcat, mode ) { |
− | var commitForm = null;
| + | var data = { |
− | var multiSpan = null;
| + | action: 'query', |
| + | prop: 'info|revisions', |
| + | rvprop: 'content|timestamp', |
| + | intoken: 'edit', |
| + | titles: file[ 0 ] |
| + | }; |
| | | |
− | var pageText = null;
| + | this.doAPICall( data, function( result ) { |
− | var pageTime = null;
| + | catALot.editCategories( result, file, targetcat, mode ); |
− | var pageWatched = false;
| + | } ); |
− | var watchCreate = false;
| + | }, |
− | var watchEdit = false;
| |
− | var minorEdits = false; | |
− | var editToken = null;
| |
| | | |
− | var is_rtl = false; | + | // Remove {{Uncategorized}}. No need to replace it with anything. |
− | var serverTime = null; | + | removeUncat: function( text ) { |
− | var lastRevId = null;
| + | return text.replace( /\{\{\s*[Uu]ncategorized\s*(\|?.*?)\}\}/, '' ); |
− | var pageTextRevId = null; | + | }, |
− | var conflictingUser = null;
| |
| | | |
− | var newDOM = false; // true if MediaWiki serves the new UL-LI DOM for categories | + | doCleanup: function( text ) { |
− | | + | if ( this.settings.docleanup ) { |
− | function setMultiInput () {
| + | return text.replace( /\{\{\s*[Cc]heck categories\s*(\|?.*?)\}\}/, '' ); |
− | if (commitButton || onUpload) return; | |
− | commitButton = make ('input');
| |
− | commitButton.type = 'button';
| |
− | commitButton.value = HotCat.messages.commit;
| |
− | commitButton.onclick = multiSubmit;
| |
− | if (multiSpan) {
| |
− | multiSpan.parentNode.replaceChild (commitButton, multiSpan); | |
| } else { | | } else { |
− | catLine.appendChild (commitButton); | + | return text; |
| } | | } |
− | } | + | }, |
| | | |
− | function checkMultiInput () { | + | editCategories: function( result, file, targetcat, mode ) { |
− | if (!commitButton) return;
| + | var otext, starttimestamp, timestamp; |
− | var has_changes = false; | + | if ( !result ) { |
− | for (var i = 0; i < editors.length; i++) { | + | // Happens on unstable wifi connections.. |
− | if (editors[i].state != CategoryEditor.UNCHANGED) {
| + | this.connectionError.push( file[ 0 ] ); |
− | has_changes = true;
| + | this.updateCounter(); |
− | break;
| + | return; |
− | } | |
− | }
| |
− | commitButton.disabled = !has_changes;
| |
− | }
| |
− | | |
− | function currentTimestamp () {
| |
− | var now = new Date();
| |
− | var ts = "" + now.getUTCFullYear();
| |
− | function two (s) { return s.substr (s.length - 2); }
| |
− | ts = ts
| |
− | + two ('0' + (now.getUTCMonth() + 1))
| |
− | + two ('0' + now.getUTCDate()) | |
− | + two ('00' + now.getUTCHours())
| |
− | + two ('00' + now.getUTCMinutes())
| |
− | + two ('00' + now.getUTCSeconds()); | |
− | return ts;
| |
− | }
| |
− | | |
− | var saveInProgress = false;
| |
− | function initiateEdit (doEdit, failure) {
| |
− | if (saveInProgress) return;
| |
− | saveInProgress = true;
| |
− | var oldButtonState;
| |
− | if (commitButton) {
| |
− | oldButtonState = commitButton.disabled;
| |
− | commitButton.disabled = true;
| |
| } | | } |
| + | var pages = result.query.pages; |
| | | |
− | function fail() { | + | // there should be only one, but we don't know its ID |
− | saveInProgress = false; | + | for ( var id in pages ) { |
− | if (commitButton) commitButton.disabled = oldButtonState; | + | // The edittoken only changes between logins |
− | failure.apply(this, arguments); | + | this.edittoken = pages[ id ].edittoken; |
| + | otext = pages[ id ].revisions[ 0 ][ '*' ]; |
| + | starttimestamp = pages[ id ].starttimestamp; |
| + | timestamp = pages[ id ].revisions[ 0 ].timestamp; |
| } | | } |
| | | |
− | // Must use Ajax here to get the user options and the edit token.
| |
| | | |
− | getJSON ({ | + | var sourcecat = this.origin; |
− | uri : conf.wgServer + conf.wgScriptPath + '/api.php'
| + | // Check if that file is already in that category |
− | ,data : 'format=json&action=query&rawcontinue=&titles=' + encodeURIComponent (conf.wgPageName)
| + | if ( mode !== 'remove' && this.regexBuilder( targetcat ).test( otext ) ) { |
− | + '&prop=info%7Crevisions%7Clanglinks&inprop=watched&intoken=edit&rvprop=content%7Ctimestamp%7Cids%7Cuser&lllimit=500'
| |
− | + '&rvlimit=2&rvdir=newer&rvstartid=' + conf.wgCurRevisionId
| |
− | + '&meta=siteinfo%7Cuserinfo&uiprop=options'
| |
− | ,success : function (json) { setPage(json); doEdit(fail); }
| |
− | ,error : function (req) { fail(req.status + ' ' + req.statusText); }
| |
− | });
| |
− | }
| |
| | | |
− | function multiChangeMsg (count) {
| + | // If the new cat is already there, just remove the old one. |
− | var msg = HotCat.messages.multi_change;
| + | if ( mode === 'move' ) { |
− | if (typeof msg != 'string' && msg.length) {
| + | mode = 'remove'; |
− | if (mw.language && mw.language.convertPlural) {
| |
− | msg = mw.language.convertPlural (count, msg); | |
| } else { | | } else { |
− | msg = msg[msg.length-1]; | + | this.alreadyThere.push( file[ 0 ] ); |
| + | this.updateCounter(); |
| + | return; |
| } | | } |
| } | | } |
− | return substitute (msg, [null, "" + count]);
| |
− | }
| |
| | | |
− | function performChanges (failure, singleEditor) {
| + | var text = otext; |
− | if (pageText === null) { | + | var comment; |
− | failure (HotCat.messages.multi_error);
| + | |
− | return;
| + | // Fix text |
− | }
| + | switch ( mode ) { |
− | // Backwards compatibility after message change (added $2 to cat_keychange) | + | case 'add': |
− | if (HotCat.messages.cat_keychange.indexOf ('$2') < 0) HotCat.messages.cat_keychange += '"$2"'; | + | text += '\n[[' + this.localCatName + ':' + targetcat + ']]\n'; |
− | // More backwards-compatibility with earlier HotCat versions:
| + | comment = msgPlain( 'summary-add' ).replace( '$1', targetcat ); |
− | if (!HotCat.messages.short_catchange) HotCat.messages.short_catchange = '[[' + HotCat.category_canonical + ':$1]]';
| + | break; |
− | // Create a form and submit it. We don't use the edit API (api.php?action=edit) because
| + | case 'copy': |
− | // (a) sensibly reporting back errors like edit conflicts is always a hassle, and
| + | text = text.replace( this.regexBuilder( sourcecat ), '[[' + this.localCatName + ':' + sourcecat + '$1]]\n[[' + this.localCatName + ':' + targetcat + '$1]]\n' ); |
− | // (b) we want to show a diff for multi-edits anyway, and
| + | comment = msgPlain( 'summary-copy' ).replace( '$1', sourcecat ).replace( '$2', targetcat ); |
− | // (c) we want to trigger onsubmit events, allowing user code to intercept the edit.
| + | // If category is added through template: |
− | // Using the form, we can do (b) and (c), and we get (a) for free. And, of course, using the form
| + | if ( otext === text ) { |
− | // automatically reloads the page with the updated categories on a successful submit, which
| + | text += '\n[[' + this.localCatName + ':' + targetcat + ']]'; |
− | // we would have to do explicitly if we used the edit API.
| |
− | var action;
| |
− | // Normally, we don't have to care about edit conflicts. If some other user edited the page in the meantime, the
| |
− | // server will take care of it and merge the edit automatically or present an edit conflict screen. However, the
| |
− | // server suppresses edit conflicts with oneself. Hence, if we have a conflict, and the conflicting user is the
| |
− | // current user, then we set the "oldid" value and switch to diff, which gives the "you are editing an old version;
| |
− | // if you save, any more recent changes will be lost" screen.
| |
− | var editingOldVersion = lastRevId !== null && lastRevId != 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 : pageText };
| |
− | var changed = [], added = [], deleted = [], changes = 0;
| |
− | var toEdit = !!singleEditor ? [singleEditor] : editors;
| |
− | var error = null;
| |
− | var i;
| |
− | for (i=0; i < toEdit.length; i++) {
| |
− | if (toEdit[i].state == CategoryEditor.CHANGED) {
| |
− | result = change_category (
| |
− | result.text
| |
− | , toEdit[i].originalCategory
| |
− | , toEdit[i].currentCategory
| |
− | , toEdit[i].currentKey
| |
− | , toEdit[i].currentHidden
| |
− | );
| |
− | if (!result.error) { | |
− | changes++;
| |
− | if (!toEdit[i].originalCategory || toEdit[i].originalCategory.length === 0) {
| |
− | added.push (toEdit[i].currentCategory);
| |
− | } else {
| |
− | changed.push ({from : toEdit[i].originalCategory, to : toEdit[i].currentCategory});
| |
− | }
| |
− | } else if (error === null) { | |
− | error = result.error; | |
| } | | } |
− | } else if ( toEdit[i].state == CategoryEditor.DELETED
| + | break; |
− | && toEdit[i].originalCategory
| + | case 'move': |
− | && toEdit[i].originalCategory.length > 0)
| + | text = text.replace( this.regexBuilder( sourcecat ), '[[' + this.localCatName + ':' + targetcat + '$1]]\n' ); |
− | {
| + | comment = msgPlain( 'summary-move' ).replace( '$1', sourcecat ).replace( '$2', targetcat ); |
− | result = change_category (result.text, toEdit[i].originalCategory, null, null, false);
| + | break; |
− | if (!result.error) {
| + | case 'remove': |
− | changes++;
| + | text = text.replace( this.regexBuilder( sourcecat ), '' ); |
− | deleted.push (toEdit[i].originalCategory);
| + | comment = msgPlain( 'summary-remove' ).replace( '$1', sourcecat ); |
− | } else if (error === null) {
| + | break; |
− | error = result.error;
| |
− | } | |
− | }
| |
− | }
| |
− | if (error !== null) { // Do not commit if there were errors
| |
− | action = commitForm.wpSave;
| |
− | if (action) action.name = action.value = 'wpDiff'; | |
− | }
| |
− | // Fill in the form and submit it
| |
− | commitForm.wpAutoSummary.value = 'd41d8cd98f00b204e9800998ecf8427e'; // MD5 hash of the empty string
| |
− | commitForm.wpMinoredit.checked = minorEdits;
| |
− | commitForm.wpWatchthis.checked = 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 (HotCat.messages.separator) + HotCat.messages.using;
| |
− | commitForm.wpMinoredit.checked = HotCat.single_minor || minorEdits;
| |
− | } else if (changes > 1) {
| |
− | var summary = [];
| |
− | var shortSummary = [];
| |
− | // Deleted
| |
− | for (i = 0; i < deleted.length; i++) {
| |
− | summary.push ('-' + substitute (HotCat.messages.short_catchange, [null, deleted[i]]));
| |
− | } | |
− | if (deleted.length == 1)
| |
− | shortSummary.push ('-' + substitute (HotCat.messages.short_catchange, [null, deleted[0]]));
| |
− | else if (deleted.length > 1)
| |
− | shortSummary.push ('- ' + multiChangeMsg (deleted.length));
| |
− | // Added
| |
− | for (i = 0; i < added.length; i++) {
| |
− | summary.push ('+' + substitute (HotCat.messages.short_catchange, [null, added[i]]));
| |
− | }
| |
− | if (added.length == 1)
| |
− | shortSummary.push ('+' + substitute (HotCat.messages.short_catchange, [null, added[0]]));
| |
− | else if (added.length > 1) | |
− | shortSummary.push ('+ ' + multiChangeMsg (added.length));
| |
− | // Changed
| |
− | var arrow = is_rtl ? '\u2190' : '\u2192'; // left and right arrows. Don't use ← and → in the code.
| |
− | for (i = 0; i < changed.length; i++) { | |
− | if (changed[i].from != changed[i].to) {
| |
− | summary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[i].from]) + arrow
| |
− | + substitute (HotCat.messages.short_catchange, [null, changed[i].to]));
| |
− | } else {
| |
− | summary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[i].from]));
| |
− | }
| |
− | }
| |
− | if (changed.length == 1) { | |
− | if (changed[0].from != changed[0].to) {
| |
− | shortSummary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[0].from]) + arrow
| |
− | + substitute (HotCat.messages.short_catchange, [null, changed[0].to]));
| |
− | } else {
| |
− | shortSummary.push ('±' + substitute (HotCat.messages.short_catchange, [null, changed[0].from]));
| |
− | }
| |
− | } else if (changed.length > 1) { | |
− | shortSummary.push ('± ' + multiChangeMsg (changed.length));
| |
− | }
| |
− | if (summary.length > 0) {
| |
− | summary = summary.join (HotCat.messages.separator);
| |
− | if (summary.length > 200 - HotCat.messages.prefix.length - HotCat.messages.using.length) {
| |
− | summary = shortSummary.join (HotCat.messages.separator);
| |
− | }
| |
− | commitForm.wpSummary.value = HotCat.messages.prefix + summary + HotCat.messages.using;
| |
− | }
| |
− | }
| |
| } | | } |
− | commitForm.wpTextbox1.value = result.text;
| |
− | commitForm.wpStarttime.value = serverTime || currentTimestamp ();
| |
− | commitForm.wpEdittime.value = pageTime || commitForm.wpStarttime.value;
| |
− | if (selfEditConflict) commitForm.oldid.value = "" + (pageTextRevId || conf.wgCurRevisionId);
| |
− | // Submit the form in a way that triggers onsubmit events: commitForm.submit() doesn't.
| |
− | commitForm.hcCommit.click();
| |
− | }
| |
| | | |
− | function resolveMulti (toResolve, callback) {
| + | if ( text === otext ) { |
− | var i; | + | this.notFound.push( file[ 0 ] ); |
− | for (i = 0; i < toResolve.length; i++) {
| + | this.updateCounter(); |
− | toResolve[i].dab = null; | |
− | toResolve[i].dabInput = toResolve[i].lastInput;
| |
− | }
| |
− | if (noSuggestions) {
| |
− | callback (toResolve); | |
| return; | | return; |
| } | | } |
− | // Use %7C instead of |, otherwise Konqueror insists on re-encoding the arguments, resulting in doubly encoded
| |
− | // category names. (That is a bug in Konqueror. Other browsers don't have this problem.)
| |
− | var args = 'action=query&prop=info%7Clinks%7Ccategories%7Ccategoryinfo&plnamespace=14'
| |
− | + '&pllimit=' + (toResolve.length * 10)
| |
− | + '&cllimit=' + (toResolve.length * 10)
| |
− | + '&format=json&titles=';
| |
− | for (i = 0; i < toResolve.length; i++) {
| |
− | var v = toResolve[i].dabInput;
| |
− | v = replaceShortcuts (v, HotCat.shortcuts);
| |
− | toResolve[i].dabInputCleaned = v;
| |
− | args += encodeURIComponent ('Category:' + v);
| |
− | if (i+1 < toResolve.length) args += '%7C';
| |
− | }
| |
− | getJSON({
| |
− | uri : conf.wgServer + conf.wgScriptPath + '/api.php'
| |
− | ,data : args
| |
− | ,success: function (json) { resolveRedirects (toResolve, json); callback (toResolve); }
| |
− | ,error: function (req) { if (!req) noSuggestions = true; callback (toResolve); }
| |
− | });
| |
− | }
| |
| | | |
− | function resolveOne (page, toResolve) {
| + | // Remove uncat after we checked whether we changed the text successfully. |
− | var cats = page.categories; | + | // Otherwise we might fail to do the changes, but still replace {{uncat}} |
− | var lks = page.links;
| + | if ( mode !== 'remove' ) { |
− | var is_dab = false; | + | text = this.doCleanup( this.removeUncat( text ) ); |
− | var is_redir = typeof page.redirect == 'string'; // Hard redirect?
| |
− | var is_hidden = page.categoryinfo && typeof page.categoryinfo.hidden == 'string'; | |
− | var is_missing = typeof page.missing == 'string';
| |
− | var i;
| |
− | for (i = 0; i < toResolve.length; i++) {
| |
− | if (toResolve.length > 1 && toResolve[i].dabInputCleaned != page.title.substring (page.title.indexOf (':') + 1)) continue; | |
− | // Note: the server returns in page an NFC normalized Unicode title. If our input was not NFC normalized, we may not find
| |
− | // any entry here. If we have only one editor to resolve (the most common case, I presume), we may simply skip the check.
| |
− | toResolve[i].currentHidden = is_hidden;
| |
− | toResolve[i].inputExists = !is_missing;
| |
− | toResolve[i].icon.src = armorUri(is_missing ? HotCat.existsNo : HotCat.existsYes);
| |
| } | | } |
− | if (is_missing) return; | + | var data = { |
− | if (!is_redir && cats && (HotCat.disambig_category || HotCat.redir_category)) {
| + | action: 'edit', |
− | for (var c = 0; c < cats.length; c++) {
| + | assert: 'user', |
− | var cat = cats[c]['title'];
| + | summary: comment, |
− | // Strip namespace prefix
| + | title: file[ 0 ], |
− | if (cat) {
| + | text: text, |
− | cat = cat.substring (cat.indexOf (':') + 1).replace(/_/g, ' ');
| + | bot: true, |
− | if (cat == HotCat.disambig_category) {
| + | starttimestamp: starttimestamp, |
− | is_dab = true; break;
| + | basetimestamp: timestamp, |
− | } else if (cat == HotCat.redir_category) {
| + | watchlist: this.settings.watchlist, |
− | is_redir = true; break;
| + | token: this.edittoken |
− | }
| + | }; |
− | }
| + | if ( this.settings.minor ) { |
− | } | + | data.minor = true; |
− | }
| |
− | if (!is_redir && !is_dab) return;
| |
− | if (!lks || lks.length === 0) return;
| |
− | var titles = [];
| |
− | for (i = 0; i < lks.length; i++) {
| |
− | if ( lks[i]['ns'] == 14 // Category namespace -- always true since we ask only for the category links | |
− | && lks[i]['title'] && lks[i]['title'].length > 0) // Name not empty
| |
− | { | |
− | // Internal link to existing thingy. Extract the page name and remove the namespace.
| |
− | var match = lks[i]['title'];
| |
− | match = match.substring (match.indexOf (':') + 1);
| |
− | // Exclude blacklisted categories.
| |
− | if (!HotCat.blacklist || !HotCat.blacklist.test (match)) {
| |
− | titles.push (match);
| |
− | }
| |
− | }
| |
− | } | |
− | if (titles.length === 0) { | |
− | return;
| |
− | }
| |
− | for (i = 0; i < toResolve.length; i++) {
| |
− | if (toResolve.length > 1 && toResolve[i].dabInputCleaned != page.title.substring (page.title.indexOf (':') + 1)) continue; | |
− | toResolve[i].inputExists = true; // Might actually be wrong if it's a redirect pointing to a non-existing category
| |
− | toResolve[i].icon.src = armorUri(HotCat.existsYes);
| |
− | if (titles.length > 1) {
| |
− | toResolve[i].dab = titles;
| |
− | } else {
| |
− | toResolve[i].text.value =
| |
− | titles[0] + (toResolve[i].currentKey !== null ? '|' + toResolve[i].currentKey : "");
| |
− | }
| |
| } | | } |
− | }
| |
| | | |
− | function resolveRedirects (toResolve, params) {
| + | this.doAPICall( data, function() { |
− | if (!params || !params.query || !params.query.pages) return; | + | catALot.updateCounter(); |
− | for (var p in params.query.pages) resolveOne (params.query.pages[p], toResolve); | + | } ); |
− | } | + | this.markAsDone( file[ 1 ], mode, targetcat ); |
| + | }, |
| | | |
− | function multiSubmit () { | + | markAsDone: function( label, mode, targetcat ) { |
− | var toResolve = []; | + | label.addClass( 'cat_a_lot_markAsDone' ); |
− | for (var i = 0; i < editors.length; i++) { | + | switch ( mode ) { |
− | if (editors[i].state == CategoryEditor.CHANGE_PENDING || editors[i].state == CategoryEditor.OPEN) | + | case 'add': |
− | toResolve.push (editors[i]); | + | label.append( '<br>' + msg( 'added-cat', targetcat ) ); |
− | }
| + | break; |
− | if (toResolve.length === 0) {
| + | case 'copy': |
− | initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);}); | + | label.append( '<br>' + msg( 'copied-cat', targetcat ) ); |
− | return;
| + | break; |
| + | case 'move': |
| + | label.append( '<br>' + msg( 'moved-cat', targetcat ) ); |
| + | break; |
| + | case 'remove': |
| + | label.append( '<br>' + msg( 'removed-cat' ) ); |
| + | break; |
| } | | } |
− | resolveMulti (
| + | }, |
− | toResolve
| |
− | , function (resolved) {
| |
− | var firstDab = null;
| |
− | var dontChange = false;
| |
− | for (var i = 0; i < resolved.length; i++) {
| |
− | if (resolved[i].lastInput != resolved[i].dabInput) {
| |
− | // We didn't disable all the open editors, but we did asynchronous calls. It is
| |
− | // theoretically possible that the user changed something...
| |
− | dontChange = true;
| |
− | } else {
| |
− | if (resolved[i].dab) {
| |
− | if (!firstDab) firstDab = resolved[i];
| |
− | } else {
| |
− | if (resolved[i].acceptCheck(true)) resolved[i].commit();
| |
− | }
| |
− | }
| |
− | }
| |
− | if (firstDab) {
| |
− | showDab (firstDab);
| |
− | } else if (!dontChange) {
| |
− | initiateEdit (function (failure) {performChanges (failure);}, function (msg) {alert (msg);});
| |
− | }
| |
− | }
| |
− | );
| |
− | } | |
− | | |
− | var cat_prefix = null;
| |
− | var noSuggestions = false;
| |
− | var suggestionEngines = {
| |
− | opensearch :
| |
− | { uri : '/api.php?format=json&action=opensearch&namespace=14&limit=30&search=Category:$1' // $1 = search term
| |
− | ,handler : // Function to convert result of uri into an array of category names
| |
− | function (queryResult, queryKey) {
| |
− | if (queryResult && queryResult.length >= 2) {
| |
− | var key = queryResult[0].substring(queryResult[0].indexOf(':') + 1);
| |
− | var titles = queryResult[1];
| |
− | var exists = false;
| |
− | if (!cat_prefix) cat_prefix = new RegExp ('^(' + HotCat.category_regexp + ':)');
| |
− | for (var i = 0; i < titles.length; i++) {
| |
− | cat_prefix.lastIndex = 0;
| |
− | var m = cat_prefix.exec (titles[i]);
| |
− | if (m && m.length > 1) {
| |
− | titles[i] = titles[i].substring (titles[i].indexOf (':') + 1); // rm namespace
| |
− | if (key == titles[i]) exists = true;
| |
− | } else {
| |
− | titles.splice (i, 1); // Nope, it's not a category after all.
| |
− | i--;
| |
− | }
| |
− | }
| |
− | titles.exists = exists;
| |
− | if (queryKey != key) titles.normalized = key; // Remember the NFC normalized key we got back from the server
| |
− | return titles;
| |
− | }
| |
− | return null;
| |
− | }
| |
− | }
| |
− | ,internalsearch :
| |
− | { uri : '/api.php?format=json&action=query&list=allpages&apnamespace=14&aplimit=30&apfrom=$1&apprefix=$1'
| |
− | ,handler :
| |
− | function (queryResult, queryKey) {
| |
− | if (queryResult && queryResult.query && queryResult.query.allpages) {
| |
− | var titles = queryResult.query.allpages;
| |
− | for (var i = 0; i < titles.length; i++) {
| |
− | titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
| |
− | }
| |
− | return titles;
| |
− | }
| |
− | return null;
| |
− | }
| |
− | }
| |
− | ,exists :
| |
− | { uri : '/api.php?format=json&action=query&prop=info&titles=Category:$1'
| |
− | ,handler :
| |
− | function (queryResult, queryKey) {
| |
− | if (queryResult && queryResult.query && queryResult.query.pages && !queryResult.query.pages[-1]) {
| |
− | // Should have exactly 1
| |
− | for (var p in queryResult.query.pages) {
| |
− | var title = queryResult.query.pages[p].title;
| |
− | title = title.substring (title.indexOf (':') + 1);
| |
− | var titles = [title];
| |
− | titles.exists = true;
| |
− | if (queryKey != title) titles.normalized = title; // NFC
| |
− | return titles;
| |
− | }
| |
− | }
| |
− | return null;
| |
− | }
| |
− | }
| |
− | ,subcategories :
| |
− | // I don't understand why they didn't map cmnamespace=14 automatically to cmtype=subcat,
| |
− | // which gives better results and is faster.
| |
− | { uri : '/api.php?format=json&action=query&list=categorymembers&cmtype=subcat&cmlimit=max&cmtitle=Category:$1'
| |
− | ,handler :
| |
− | function (queryResult, queryKey) {
| |
− | if (queryResult && queryResult.query && queryResult.query.categorymembers) {
| |
− | var titles = queryResult.query.categorymembers;
| |
− | for (var i = 0; i < titles.length; i++) {
| |
− | titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
| |
− | }
| |
− | return titles;
| |
− | }
| |
− | return null;
| |
− | }
| |
− | }
| |
− | ,parentcategories :
| |
− | { uri : '/api.php?format=json&action=query&prop=categories&titles=Category:$1&cllimit=max'
| |
− | ,handler :
| |
− | function (queryResult, queryKey) {
| |
− | if (queryResult && queryResult.query && queryResult.query.pages) {
| |
− | for (var p in queryResult.query.pages) {
| |
− | if (queryResult.query.pages[p].categories) {
| |
− | var titles = queryResult.query.pages[p].categories;
| |
− | for (var i = 0; i < titles.length; i++) {
| |
− | titles[i] = titles[i].title.substring (titles[i].title.indexOf (':') + 1); // rm namespace
| |
− | }
| |
− | return titles;
| |
− | }
| |
− | }
| |
− | }
| |
− | return null;
| |
− | }
| |
− | }
| |
− | };
| |
| | | |
− | var suggestionConfigs = { | + | updateCounter: function() { |
− | searchindex : {name: 'Search index', engines: ['opensearch'], cache: {}, show: true, temp: false, noCompletion : false}
| + | this.counterCurrent++; |
− | ,pagelist : {name: 'Page list', engines: ['internalsearch', 'exists'], cache: {}, show: true, temp: false, noCompletion : false}
| + | if ( this.counterCurrent > this.counterNeeded ) { |
− | ,combined : {name: 'Combined search', engines: ['opensearch', 'internalsearch'], cache: {}, show: true, temp: false, noCompletion : false}
| + | this.displayResult(); |
− | ,subcat : {name: 'Subcategories', engines: ['subcategories'], cache: {}, show: true, temp: true, noCompletion : true}
| |
− | ,parentcat : {name: 'Parent categories', engines: ['parentcategories'], cache: {}, show: true, temp: true, noCompletion : true}
| |
− | };
| |
− | | |
− | function CategoryEditor () { this.initialize.apply (this, arguments); }
| |
− | CategoryEditor.UNCHANGED = 0;
| |
− | CategoryEditor.OPEN = 1; // Open, but no input yet
| |
− | CategoryEditor.CHANGE_PENDING = 2; // Open, some input made
| |
− | CategoryEditor.CHANGED = 3;
| |
− | CategoryEditor.DELETED = 4;
| |
− | | |
− | // IE6 sometimes forgets to redraw the list when editors are opened or closed.
| |
− | // Adding/removing a dummy element helps, at least when opening editors.
| |
− | var dummyElement = make ('\xa0', true);
| |
− | | |
− | function forceRedraw () {
| |
− | if (dummyElement.parentNode) { | |
− | document.body.removeChild (dummyElement); | |
| } else { | | } else { |
− | document.body.appendChild (dummyElement); | + | this.domCounter.text( this.counterCurrent ); |
| } | | } |
− | } | + | }, |
| | | |
− | // Event keyCodes that we handle in the text input field/suggestion list. | + | displayResult: function() { |
− | var BS = 8, TAB = 9, RET = 13, ESC = 27, SPACE = 32, PGUP = 33, PGDOWN = 34, UP = 38, DOWN = 40, DEL = 46, IME = 229;
| + | document.body.style.cursor = 'auto'; |
| + | $( '.cat_a_lot_feedback' ) |
| + | .addClass( 'cat_a_lot_done' ); |
| + | $( '.ui-dialog-content' ) |
| + | .height( 'auto' ); |
| + | var rep = this.domCounter.parent(); |
| + | rep.html( '<h3>' + msg( 'done' ) + '</h3>' ); |
| + | rep.append( msg( 'all-done' ) + '<br />' ); |
| | | |
− | function makeActive (which) {
| + | var close = $( '<a>' ) |
− | if (which.is_active) return;
| + | .text( msgPlain( 'return-to-page' ) ); |
− | for (var i = 0; i < editors.length; i++) { | + | close.click( function() { |
− | if (editors[i] !== which) editors[i].inactivate (); | + | catALot.progressDialog.remove(); |
| + | catALot.toggleAll( false ); |
| + | } ); |
| + | rep.append( close ); |
| + | if ( this.alreadyThere.length ) { |
| + | rep.append( '<h5>' + msg( 'skipped-already', this.alreadyThere.length ) + '</h5>' ); |
| + | rep.append( this.alreadyThere.join( '<br>' ) ); |
| } | | } |
− | which.is_active = true;
| + | if ( this.notFound.length ) { |
− | if (which.dab) { | + | rep.append( '<h5>' + msg( 'skipped-not-found', this.notFound.length ) + '</h5>' ); |
− | showDab (which);
| + | rep.append( this.notFound.join( '<br>' ) ); |
− | } else {
| |
− | // Check for programmatic value changes. | |
− | var expectedInput = which.lastRealInput || which.lastInput || "";
| |
− | var actualValue = which.text.value || "";
| |
− | if (expectedInput.length === 0 && actualValue.length > 0 || expectedInput.length > 0 && actualValue.indexOf (expectedInput) !== 0) {
| |
− | // Somehow the field's value appears to have changed, and which.lastSelection therefore is no longer valid. Try to set the
| |
− | // cursor at the end of the category, and do not display the old suggestion list.
| |
− | which.showsList = false;
| |
− | var v = actualValue.split('|');
| |
− | which.lastRealInput = which.lastInput = v[0];
| |
− | if (v.length > 1) which.currentKey = v[1];
| |
− | if (which.lastSelection) which.lastSelection = {start: v[0].length, end: v[0].length};
| |
− | } | |
− | if (which.showsList) which.displayList();
| |
− | if (which.lastSelection) {
| |
− | if (is_webkit) {
| |
− | // WebKit (Safari, Chrome) has problems selecting inside focus()
| |
− | // See http://code.google.com/p/chromium/issues/detail?id=32865#c6
| |
− | window.setTimeout (
| |
− | function () { which.setSelection (which.lastSelection.start, which.lastSelection.end); }
| |
− | ,1
| |
− | );
| |
− | } else {
| |
− | which.setSelection (which.lastSelection.start, which.lastSelection.end);
| |
− | }
| |
− | }
| |
| } | | } |
− | }
| + | if ( this.connectionError.length ) { |
− | | + | rep.append( '<h5>' + msg( 'skipped-server', this.connectionError.length ) + '</h5>' ); |
− | function showDab (which) {
| + | rep.append( this.connectionError.join( '<br>' ) ); |
− | if (!which.is_active) { | |
− | makeActive(which); | |
− | } else {
| |
− | which.showSuggestions (which.dab, false, null, null); // do autocompletion, no key, no engine selector | |
− | which.dab = null;
| |
| } | | } |
− | }
| |
| | | |
− | CategoryEditor.prototype = { | + | }, |
| | | |
− | initialize : function (line, span, after, key, is_hidden) {
| + | moveHere: function( targetcat ) { |
− | // If a span is given, 'after' is the category title, otherwise it may be an element after which to
| + | this.doSomething( targetcat, 'move' ); |
− | // insert the new span. 'key' is likewise overloaded; if a span is given, it is the category key (if
| + | }, |
− | // known), otherwise it is a boolean indicating whether a bar shall be prepended.
| |
− | if (!span) {
| |
− | this.isAddCategory = true;
| |
− | // Create add span and append to catLinks
| |
− | this.originalCategory = "";
| |
− | this.originalKey = null;
| |
− | this.originalExists = false;
| |
− | if (!newDOM) {
| |
− | span = make ('span');
| |
− | span.className = 'noprint';
| |
− | if (key) {
| |
− | span.appendChild (make (' | ', true));
| |
− | if (after) {
| |
− | after.parentNode.insertBefore (span, after.nextSibling);
| |
− | after = after.nextSibling;
| |
− | } else {
| |
− | line.appendChild (span);
| |
− | }
| |
− | } else if (line.firstChild) {
| |
− | span.appendChild (make (' ', true));
| |
− | line.appendChild (span);
| |
− | }
| |
− | }
| |
− | this.linkSpan = make ('span');
| |
− | this.linkSpan.className = 'noprint nopopups hotcatlink';
| |
− | var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.open.bind (this);
| |
− | lk.appendChild (make (HotCat.links.add, true)); lk.title = HotCat.tooltips.add;
| |
− | this.linkSpan.appendChild (lk);
| |
− | span = make (newDOM ? 'li' : 'span');
| |
− | span.className = 'noprint';
| |
− | if (is_rtl) span.dir = 'rtl';
| |
− | span.appendChild (this.linkSpan);
| |
− | if (after)
| |
− | after.parentNode.insertBefore (span, after.nextSibling);
| |
− | else
| |
− | line.appendChild (span);
| |
− | this.normalLinks = null;
| |
− | this.undelLink = null;
| |
− | this.catLink = null;
| |
− | } else {
| |
− | if (is_rtl) span.dir = 'rtl';
| |
− | this.isAddCategory = false;
| |
− | this.catLink = span.firstChild;
| |
− | this.originalCategory = after;
| |
− | this.originalKey = (key && key.length > 1) ? key.substr(1) : null; // > 1 because it includes the leading bar
| |
− | this.originalExists = !hasClass (this.catLink, 'new');
| |
− | // Create change and del links
| |
− | this.makeLinkSpan ();
| |
− | if (!this.originalExists && this.upDownLinks) this.upDownLinks.style.display = 'none';
| |
− | span.appendChild (this.linkSpan);
| |
− | }
| |
− | this.originalHidden = is_hidden;
| |
− | this.line = line;
| |
− | this.engine = HotCat.suggestions;
| |
− | this.span = span;
| |
− | this.currentCategory = this.originalCategory;
| |
− | this.currentExists = this.originalExists;
| |
− | this.currentHidden = this.originalHidden;
| |
− | this.currentKey = this.originalKey;
| |
− | this.state = CategoryEditor.UNCHANGED;
| |
− | this.lastSavedState = CategoryEditor.UNCHANGED;
| |
− | this.lastSavedCategory = this.originalCategory;
| |
− | this.lastSavedKey = this.originalKey;
| |
− | this.lastSavedExists = this.originalExists;
| |
− | this.lastSavedHidden = this.originalHidden;
| |
− | if (this.catLink && this.currentKey) {
| |
− | this.catLink.title = this.currentKey;
| |
− | }
| |
− | editors[editors.length] = this;
| |
− | },
| |
| | | |
− | makeLinkSpan : function () {
| + | copyHere: function( targetcat ) { |
− | this.normalLinks = make ('span');
| + | this.doSomething( targetcat, 'copy' ); |
− | var lk = null;
| + | }, |
− | if (this.originalCategory && this.originalCategory.length > 0) {
| + | |
− | lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.remove.bind (this);
| + | addHere: function( targetcat ) { |
− | lk.appendChild (make (HotCat.links.remove, true)); lk.title = HotCat.tooltips.remove;
| + | this.doSomething( targetcat, 'add' ); |
− | this.normalLinks.appendChild (make (' ', true));
| + | }, |
− | this.normalLinks.appendChild (lk);
| |
− | }
| |
− | if (!HotCat.template_categories[this.originalCategory]) {
| |
− | lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.open.bind (this);
| |
− | lk.appendChild (make (HotCat.links.change, true)); lk.title = HotCat.tooltips.change;
| |
− | this.normalLinks.appendChild (make (' ', true));
| |
− | this.normalLinks.appendChild (lk);
| |
− | if (!noSuggestions && HotCat.use_up_down) {
| |
− | this.upDownLinks = make ('span');
| |
− | lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.down.bind (this);
| |
− | lk.appendChild (make (HotCat.links.down, true)); lk.title = HotCat.tooltips.down;
| |
− | this.upDownLinks.appendChild (make (' ', true));
| |
− | this.upDownLinks.appendChild (lk);
| |
− | lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.up.bind (this);
| |
− | lk.appendChild (make (HotCat.links.up, true)); lk.title = HotCat.tooltips.up;
| |
− | this.upDownLinks.appendChild (make (' ', true));
| |
− | this.upDownLinks.appendChild (lk);
| |
− | this.normalLinks.appendChild (this.upDownLinks);
| |
− | }
| |
− | }
| |
− | this.linkSpan = make ('span');
| |
− | this.linkSpan.className = 'noprint nopopups hotcatlink';
| |
− | this.linkSpan.appendChild (this.normalLinks);
| |
− | this.undelLink = make ('span');
| |
− | this.undelLink.className = 'nopopups hotcatlink';
| |
− | this.undelLink.style.display = 'none';
| |
− | lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.restore.bind (this);
| |
− | lk.appendChild (make (HotCat.links.restore, true)); lk.title = HotCat.tooltips.restore;
| |
− | this.undelLink.appendChild (make (' ', true));
| |
− | this.undelLink.appendChild (lk);
| |
− | this.linkSpan.appendChild (this.undelLink);
| |
− | },
| |
| | | |
− | invokeSuggestions : function (dont_autocomplete) {
| + | remove: function() { |
− | if (this.engine && suggestionConfigs[this.engine] && suggestionConfigs[this.engine].temp && !dont_autocomplete) {
| + | this.doSomething( '', 'remove' ); |
− | this.engine = HotCat.suggestions; // Reset to a search upon input
| + | }, |
− | }
| |
− | this.state = CategoryEditor.CHANGE_PENDING;
| |
− | var self = this;
| |
− | window.setTimeout (function () {self.textchange (dont_autocomplete);}, HotCat.suggest_delay);
| |
− | },
| |
| | | |
− | makeForm : function () {
| + | doSomething: function( targetcat, mode ) { |
− | var form = make ('form');
| + | var files = this.getMarkedLabels(); |
− | form.method = 'POST'; form.onsubmit = this.accept.bind (this);
| + | if ( files.length === 0 ) { |
− | this.form = form;
| + | alert( msgPlain( 'none-selected' ) ); |
− | var self = this;
| + | return; |
− | var text = make ('input'); text.type = 'text'; text.size = HotCat.editbox_width;
| + | } |
− | if (!noSuggestions) { | + | this.notFound = []; |
− | // Be careful here to handle IME input. This is browser/OS/IME dependent, but basically there are two mechanisms:
| + | this.alreadyThere = []; |
− | // - Modern (DOM Level 3) browsers use compositionstart/compositionend events to signal composition; if the
| + | this.connectionError = []; |
− | // composition is not canceled, there'll be a textInput event following. During a composition key events are
| + | this.counterCurrent = 1; |
− | // either all suppressed (FF/Gecko), or otherwise have keyDown === IME for all keys (Webkit).
| + | this.counterNeeded = files.length; |
− | // - Webkit sends a textInput followed by keyDown === IME and a keyUp with the key that ended composition.
| + | mw.loader.using( [ 'jquery.ui.dialog', 'mediawiki.RegExp' ], function() { |
− | // - Gecko doesn't send textInput but just a keyUp with the key that ended composition, without sending keyDown
| + | catALot.showProgress(); |
− | // first. Gecko doesn't send any keydown while IME is active.
| + | for ( var i = 0; i < files.length; i++ ) { |
− | // - Older browsers signal composition by keyDown === IME for the first and subsequent keys for a composition. The
| + | catALot.getContent( files[ i ], targetcat, mode ); |
− | // first keyDown !== IME is certainly after the end of the composition. Typically, composition end can also be
| |
− | // detected by a keyDown IME with a keyUp of space, tab, escape, or return. (Example: IE8)
| |
− | text.onkeyup =
| |
− | function (evt) {
| |
− | evt = evt || window.event || window.Event; // W3C, IE, Netscape
| |
− | var key = evt.keyCode || 0;
| |
− | if (self.ime && self.lastKey === IME && !self.usesComposition && (key === TAB || key === RET || key == ESC || key === SPACE)) self.ime = false;
| |
− | if (self.ime) return true;
| |
− | if (key === UP || key === DOWN || key === PGUP || key === PGDOWN) {
| |
− | // In case a browser doesn't generate keypress events for arrow keys...
| |
− | if (self.keyCount === 0) return self.processKey (evt);
| |
− | } else {
| |
− | if (key === ESC && self.lastKey !== IME) {
| |
− | if (!self.resetKeySelection ()) {
| |
− | // No undo of key selection: treat ESC as "cancel".
| |
− | self.cancel ();
| |
− | return;
| |
− | }
| |
− | }
| |
− | // Also do this for ESC as a workaround for Firefox bug 524360
| |
− | // https://bugzilla.mozilla.org/show_bug.cgi?id=524360
| |
− | self.invokeSuggestions (key === BS || key === DEL || key === ESC);
| |
− | }
| |
− | return true;
| |
− | };
| |
− | text.onkeydown =
| |
− | function (evt) {
| |
− | evt = evt || window.event || window.Event; // W3C, IE, Netscape
| |
− | var key = evt.keyCode || 0;
| |
− | self.lastKey = key;
| |
− | self.keyCount = 0;
| |
− | // DOM Level < 3 IME input
| |
− | if (!self.ime && key === IME && !self.usesComposition) {
| |
− | // self.usesComposition catches browsers that may emit spurious keydown IME after a composition has ended
| |
− | self.ime = true;
| |
− | } else if (self.ime && key !== IME && !(key >= 16 && key <= 20 || key >= 91 && key <= 93 || key === 144)) {
| |
− | // Ignore control keys: ctrl, shift, alt, alt gr, caps lock, windows/apple cmd keys, num lock. Only the windows keys
| |
− | // terminate IME (apple cmd doesn't), but they also cause a blur, so it's OK to ignore them here.
| |
− | // Note: Safari 4 (530.17) propagates ESC out of an IME composition (observed at least on Win XP).
| |
− | self.ime = false;
| |
− | }
| |
− | if (self.ime) return true;
| |
− | // Handle return explicitly, to override the default form submission to be able to check for ctrl
| |
− | if (key === RET) return self.accept (evt);
| |
− | // Inhibit default behavior of ESC (revert to last real input in FF: we do that ourselves)
| |
− | return (key === ESC) ? evtKill(evt) : true;
| |
− | };
| |
− | // And handle continued pressing of arrow keys
| |
− | text.onkeypress = function (evt) {self.keyCount++; return self.processKey (evt);};
| |
− | $(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; FF3.6.4 raises an exception when trying to access that
| |
− | // property while the element is not being displayed.
| |
− | $(text).on (
| |
− | (typeof 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 (evt) { self.lastKey = IME; self.usesComposition = true; self.ime = true; });
| |
− | $(text).on ('compositionend', function (evt) { self.lastKey = IME; self.usesComposition = true; self.ime = false; });
| |
− | $(text).on ('textInput', function (evt) { self.ime = false; self.invokeSuggestions(false); });
| |
− | } catch (any) {
| |
− | // Just in case some browsers might produce exceptions with these DOM Level 3 events
| |
− | } | |
− | $(text).on ('blur', function (evt) { self.usesComposition = false; self.ime = false; });
| |
| } | | } |
− | this.text = text;
| + | } ); |
− | | + | }, |
− | this.icon = make ('img');
| |
| | | |
− | var list = null;
| + | doAPICall: function( params, callback ) { |
− | if (!noSuggestions) {
| + | params.format = 'json'; |
− | list = make ('select');
| + | var i = 0, |
− | list.onclick = function (e) { if (self.highlightSuggestion(0)) self.textchange (false, true); };
| + | apiUrl = this.apiUrl, |
− | list.ondblclick = function (e) { if (self.highlightSuggestion(0)) self.accept (e); };
| + | doCall, |
− | list.onchange = function (e) { self.highlightSuggestion(0); self.text.focus(); };
| + | handleError = function( jqXHR, textStatus, errorThrown ) { |
− | list.onkeyup =
| + | if ( window.console && $.isFunction( window.console.log ) ) { |
− | function (evt) {
| + | window.console.log( 'Error: ', jqXHR, textStatus, errorThrown ); |
− | evt = evt || window.event || window.Event; // W3C, IE, Netscape
| |
− | if (evt.keyCode === ESC) {
| |
− | self.resetKeySelection ();
| |
− | self.text.focus();
| |
− | window.setTimeout (function () {self.textchange (true);}, HotCat.suggest_delay);
| |
− | } else if (evt.keyCode === RET) {
| |
− | self.accept (evt);
| |
− | }
| |
− | }; | |
− | if (!HotCat.fixed_search) {
| |
− | var engineSelector = make ('select');
| |
− | for (var key in suggestionConfigs) {
| |
− | if (suggestionConfigs[key].show) {
| |
− | var opt = make ('option');
| |
− | opt.value = key;
| |
− | if (key == this.engine) opt.selected = true;
| |
− | opt.appendChild (make (suggestionConfigs[key].name, true));
| |
− | engineSelector.appendChild (opt);
| |
− | }
| |
− | }
| |
− | engineSelector.onchange =
| |
− | function () {
| |
− | self.engine = self.engineSelector.options[self.engineSelector.selectedIndex].value;
| |
− | self.text.focus();
| |
− | self.textchange (true, true); // Don't autocomplete, force re-display of list
| |
− | };
| |
− | this.engineSelector = engineSelector;
| |
| } | | } |
− | }
| + | if ( i < 4 ) { |
− | this.list = list;
| + | window.setTimeout( doCall, 300 ); |
− | | + | i++; |
− | function button_label (id, defaultText) {
| + | } else if ( params.title ) { |
− | var label = null;
| + | this.connectionError.push( params.title ); |
− | if ( onUpload | + | this.updateCounter(); |
− | && typeof UFUI != 'undefined'
| + | return; |
− | && typeof UIElements != 'undefined'
| |
− | && typeof UFUI.getLabel == 'function')
| |
− | {
| |
− | try { | |
− | label = UFUI.getLabel (id, true);
| |
− | // Extract the plain text. IE doesn't know that Node.TEXT_NODE === 3
| |
− | while (label && label.nodeType != 3) label = label.firstChild;
| |
− | } catch (ex) { | |
− | label = null;
| |
− | } | |
| } | | } |
− | if (!label || !label.data) return defaultText;
| + | }; |
− | return label.data;
| + | doCall = function() { |
− | }
| + | $.ajax( { |
| + | url: apiUrl, |
| + | cache: false, |
| + | dataType: 'json', |
| + | data: params, |
| + | type: 'POST', |
| + | success: callback, |
| + | error: handleError |
| + | } ); |
| + | }; |
| + | doCall(); |
| + | }, |
| | | |
− | // Do not use type 'submit'; we cannot detect modifier keys if we do
| + | createCatLinks: function( symbol, list ) { |
− | var OK = make ('input'); OK.type = 'button';
| + | list.sort(); |
− | OK.value = button_label ('wpOkUploadLbl', HotCat.messages.ok); | + | var domlist = $resultList.find( 'table' ); |
− | OK.onclick = this.accept.bind (this);
| + | for ( var i = 0; i < list.length; i++ ) { |
− | this.ok = OK;
| + | var $tr = $( '<tr>' ); |
| | | |
− | var cancel = make ('input'); cancel.type = 'button'; | + | var $link = $( '<a>' ), |
− | cancel.value = button_label ('wpCancelUploadLbl', HotCat.messages.cancel);
| + | $add, $move, $copy; |
− | cancel.onclick = this.cancel.bind (this);
| |
− | this.cancelButton = cancel;
| |
| | | |
− | var span = make ('span'); | + | $link.text( list[ i ] ); |
− | span.className = 'hotcatinput';
| + | $tr.data( 'cat', list[ i ] ); |
− | span.style.position = 'relative';
| + | $link.click( function() { |
− | // FF3.6: add the input field first, then the two absolutely positioned elements. Otherwise, FF3.6 may leave the
| + | catALot.updateCats( $( this ).closest( 'tr' ).data( 'cat' ) ); |
− | // suggestions and the selector at the right edge of the screen if display of the input field causes a re-layout
| + | } ); |
− | // moving the form to the front of the next line.
| |
− | span.appendChild (text);
| |
− | | |
− | // IE8/IE9: put some text into this span (a0 is nbsp) and make sure it always stays on the
| |
− | // same line as the input field, otherwise, IE8/9 miscalculates the height of the span and
| |
− | // then the engine selector may overlap the input field.
| |
− | span.appendChild (make ('\xa0', true));
| |
− | span.style.whiteSpace = 'nowrap';
| |
− | | |
− | if (list) span.appendChild (list);
| |
− | if (this.engineSelector) span.appendChild (this.engineSelector);
| |
− | if (!noSuggestions) span.appendChild (this.icon);
| |
− | span.appendChild (OK);
| |
− | span.appendChild (cancel);
| |
− | form.appendChild(span); | |
− | form.style.display = 'none';
| |
− | this.span.appendChild (form);
| |
− | },
| |
− | | |
− | display : function (evt) {
| |
− | if (this.isAddCategory && !onUpload) {
| |
− | var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
| |
− | }
| |
− | if (!commitButton && !onUpload) {
| |
− | for (var i = 0; i < editors.length; i++) {
| |
− | if (editors[i].state != CategoryEditor.UNCHANGED) {
| |
− | setMultiInput();
| |
− | break;
| |
− | }
| |
− | }
| |
− | } | |
− | if (!this.form) {
| |
− | this.makeForm (); | |
− | }
| |
− | if (this.list) this.list.style.display = 'none';
| |
− | if (this.engineSelector) this.engineSelector.style.display = 'none';
| |
− | this.currentCategory = this.lastSavedCategory;
| |
− | this.currentExists = this.lastSavedExists;
| |
− | this.currentHidden = this.lastSavedHidden;
| |
− | this.currentKey = this.lastSavedKey;
| |
− | this.icon.src = armorUri(this.currentExists ? HotCat.existsYes : HotCat.existsNo);
| |
− | this.text.value = this.currentCategory + (this.currentKey !== null ? '|' + this.currentKey : "");
| |
− | this.originalState = this.state;
| |
− | this.lastInput = this.currentCategory;
| |
− | this.inputExists = this.currentExists;
| |
− | this.state = this.state == CategoryEditor.UNCHANGED ? CategoryEditor.OPEN : CategoryEditor.CHANGE_PENDING;
| |
− | this.lastSelection = {start: this.currentCategory.length, end: this.currentCategory.length};
| |
− | this.showsList = false;
| |
− | // Display the form
| |
− | if (this.catLink) this.catLink.style.display = 'none';
| |
− | this.linkSpan.style.display = 'none';
| |
− | this.form.style.display = 'inline';
| |
− | this.ok.disabled = false;
| |
− | // Kill the event before focussing, otherwise IE will kill the onfocus event!
| |
− | var result = evtKill (evt);
| |
− | this.text.focus();
| |
− | this.text.readOnly = false;
| |
− | checkMultiInput ();
| |
− | return result; | |
− | },
| |
− | | |
− | show : function (evt, engine, readOnly) {
| |
− | var result = this.display (evt);
| |
− | var v = this.lastSavedCategory;
| |
− | if (v.length === 0) return result;
| |
− | this.text.readOnly = !!readOnly;
| |
− | this.engine = engine;
| |
− | this.textchange (false, true); // do autocompletion, force display of suggestions
| |
− | forceRedraw ();
| |
− | return result;
| |
− | },
| |
| | | |
− | open : function (evt) {
| + | $tr.append( $( '<td>' ).text( symbol ) ) |
− | return this.show (evt, (this.engine && suggestionConfigs[this.engine].temp) ? HotCat.suggestions : this.engine);
| + | .append( $( '<td>' ).append( $link ) ); |
− | },
| |
| | | |
− | down : function (evt) {
| + | if ( this.origin ) { |
− | return this.show (evt, 'subcat', true);
| + | // Can't move to source category |
− | },
| + | if ( list[ i ] !== this.origin ) { |
| + | $move = $( '<a>' ) |
| + | .addClass( 'cat_a_lot_move' ) |
| + | .text( msgPlain( 'move' ) ) |
| + | .click( function() { |
| + | catALot.moveHere( $( this ).closest( 'tr' ).data( 'cat' ) ); |
| + | } ); |
| | | |
− | up : function (evt) {
| + | $copy = $( '<a>' ) |
− | return this.show (evt, 'parentcat');
| + | .addClass( 'cat_a_lot_action' ) |
− | },
| + | .text( msgPlain( 'copy' ) ) |
| + | .click( function() { |
| + | catALot.copyHere( $( this ).closest( 'tr' ).data( 'cat' ) ); |
| + | } ); |
| | | |
− | cancel : function () {
| + | $tr.append( $( '<td>' ).append( $move ), $( '<td>' ).append( $copy ) ); |
− | if (this.isAddCategory && !onUpload) {
| |
− | this.removeEditor(); // We added a new adder when opening
| |
− | return;
| |
− | }
| |
− | // Close, re-display link
| |
− | this.inactivate();
| |
− | this.form.style.display = 'none';
| |
− | if (this.catLink) this.catLink.style.display = "";
| |
− | this.linkSpan.style.display = "";
| |
− | this.state = this.originalState;
| |
− | this.currentCategory = this.lastSavedCategory;
| |
− | this.currentKey = this.lastSavedKey;
| |
− | this.currentExists = this.lastSavedExists;
| |
− | this.currentHidden = this.lastSavedHidden;
| |
− | if (this.catLink) {
| |
− | if (this.currentKey && this.currentKey.length > 0) {
| |
− | this.catLink.title = this.currentKey;
| |
− | } else {
| |
− | this.catLink.title = "";
| |
| } | | } |
− | }
| |
− | if (this.state == CategoryEditor.UNCHANGED) {
| |
− | if (this.catLink) this.catLink.style.backgroundColor = 'transparent';
| |
| } else { | | } else { |
− | if (!onUpload) { | + | $add = $( '<a>' ) |
− | try {
| + | .addClass( 'cat_a_lot_action' ) |
− | this.catLink.style.backgroundColor = HotCat.bg_changed; | + | .text( msgPlain( 'add' ) ) |
− | } catch (ex) {}
| + | .click( function() { |
− | }
| + | catALot.addHere( $( this ).closest( 'tr' ).data( 'cat' ) ); |
− | }
| + | } ); |
− | checkMultiInput ();
| |
− | forceRedraw ();
| |
− | },
| |
| | | |
− | removeEditor : function () {
| + | $tr.append( $( '<td>' ).append( $add ) ); |
− | if (!newDOM) {
| |
− | var next = this.span.nextSibling;
| |
− | if (next) next.parentNode.removeChild (next);
| |
| } | | } |
− | this.span.parentNode.removeChild (this.span);
| |
− | for (var i = 0; i < editors.length; i++) {
| |
− | if (editors[i] == this) {
| |
− | editors.splice (i, 1);
| |
− | break;
| |
− | }
| |
− | }
| |
− | checkMultiInput ();
| |
− | var self = this;
| |
− | window.setTimeout (function () {delete self;}, 10);
| |
− | },
| |
| | | |
− | rollback : function (evt) {
| + | domlist.append( $tr ); |
− | this.undoLink.parentNode.removeChild (this.undoLink);
| + | } |
− | this.undoLink = null;
| + | }, |
− | this.currentCategory = this.originalCategory;
| |
− | this.currentKey = this.originalKey;
| |
− | this.currentExists = this.originalExists;
| |
− | this.currentHidden = this.originalHidden;
| |
− | this.lastSavedCategory = this.originalCategory;
| |
− | this.lastSavedKey = this.originalKey;
| |
− | this.lastSavedExists = this.originalExists; | |
− | this.lastSavedHidden = this.originalHidden;
| |
− | this.state = CategoryEditor.UNCHANGED;
| |
− | if (!this.currentCategory || this.currentCategory.length === 0) {
| |
− | // It was a newly added category. Remove the whole editor.
| |
− | this.removeEditor();
| |
− | } else {
| |
− | // Redisplay the link...
| |
− | this.catLink.removeChild (this.catLink.firstChild);
| |
− | this.catLink.appendChild (make (this.currentCategory, true));
| |
− | this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
| |
− | this.catLink.title = this.currentKey || "";
| |
− | this.catLink.className = this.currentExists ? "" : 'new';
| |
− | this.catLink.style.backgroundColor = 'transparent';
| |
− | if (this.upDownLinks) this.upDownLinks.style.display = this.currentExists ? "" : 'none';
| |
− | checkMultiInput ();
| |
− | }
| |
− | return evtKill (evt);
| |
− | }, | |
− | | |
− | inactivate : function () {
| |
− | if (this.list) this.list.style.display = 'none';
| |
− | if (this.engineSelector) this.engineSelector.style.display = 'none';
| |
− | this.is_active = false;
| |
− | },
| |
| | | |
− | acceptCheck : function (dontCheck) {
| + | getCategoryList: function() { |
− | this.sanitizeInput ();
| + | this.catCounter = 0; |
− | var value = this.text.value.split('|');
| + | this.getParentCats(); |
− | var key = null;
| + | this.getSubCats(); |
− | if (value.length > 1) key = value[1];
| + | }, |
− | var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
| |
− | if (HotCat.capitalizePageNames) v = capitalize (v);
| |
− | this.lastInput = v;
| |
− | v = replaceShortcuts(v, HotCat.shortcuts);
| |
− | if (v.length === 0) {
| |
− | this.cancel ();
| |
− | return false;
| |
− | }
| |
− | if (!dontCheck
| |
− | && ( conf.wgNamespaceNumber === 14 && v == conf.wgTitle
| |
− | || HotCat.blacklist && HotCat.blacklist.test(v))
| |
− | )
| |
− | {
| |
− | this.cancel ();
| |
− | return false;
| |
− | }
| |
− | this.currentCategory = v;
| |
− | this.currentKey = key;
| |
− | this.currentExists = this.inputExists;
| |
− | return true;
| |
− | },
| |
| | | |
− | accept : function (evt) {
| + | showCategoryList: function() { |
− | this.noCommit = (evtKeys (evt) & 1) !== 0;
| + | var thiscat = [ this.currentCategory ]; |
− | var result = evtKill (evt);
| |
− | if (this.acceptCheck ()) {
| |
− | var toResolve = [this];
| |
− | var original = this.currentCategory;
| |
− | resolveMulti (
| |
− | toResolve
| |
− | ,function (resolved) {
| |
− | if (resolved[0].dab) {
| |
− | showDab (resolved[0]);
| |
− | } else {
| |
− | if (resolved[0].acceptCheck(true)) {
| |
− | resolved[0].commit (
| |
− | (resolved[0].currentCategory != original)
| |
− | ? HotCat.messages.cat_resolved.replace (/\$1/g, original)
| |
− | : null
| |
− | );
| |
− | }
| |
− | }
| |
− | }
| |
− | );
| |
− | }
| |
− | return result;
| |
− | },
| |
| | | |
− | close : function () { | + | $resultList.empty(); |
− | if (!this.catLink) {
| + | $resultList.append( '<table>' ); |
− | // Create a catLink
| |
− | this.catLink = make ('a');
| |
− | this.catLink.appendChild (make ('foo', true));
| |
− | this.catLink.style.display = 'none';
| |
− | this.span.insertBefore (this.catLink, this.span.firstChild.nextSibling);
| |
− | }
| |
− | this.catLink.removeChild (this.catLink.firstChild);
| |
− | this.catLink.appendChild (make (this.currentCategory, true));
| |
− | this.catLink.href = wikiPagePath (HotCat.category_canonical + ':' + this.currentCategory);
| |
− | this.catLink.className = this.currentExists ? "" : 'new';
| |
− | this.lastSavedCategory = this.currentCategory;
| |
− | this.lastSavedKey = this.currentKey;
| |
− | this.lastSavedExists = this.currentExists;
| |
− | this.lastSavedHidden = this.currentHidden;
| |
− | // Close form and redisplay category
| |
− | this.inactivate();
| |
− | this.form.style.display = 'none';
| |
− | this.catLink.title = this.currentKey || "";
| |
− | this.catLink.style.display = "";
| |
− | if (this.isAddCategory) {
| |
− | if (onUpload) {
| |
− | var newAdder = new CategoryEditor (this.line, null, this.span, true); // Create a new one
| |
− | }
| |
− | this.isAddCategory = false;
| |
− | this.linkSpan.parentNode.removeChild (this.linkSpan);
| |
− | this.makeLinkSpan ();
| |
− | this.span.appendChild (this.linkSpan);
| |
− | }
| |
− | if (!this.undoLink) {
| |
− | // Append an undo link.
| |
− | var span = make ('span');
| |
− | var lk = make ('a'); lk.href = '#catlinks'; lk.onclick = this.rollback.bind (this);
| |
− | lk.appendChild (make (HotCat.links.undo, true)); lk.title = HotCat.tooltips.undo;
| |
− | span.appendChild (make (' ', true));
| |
− | span.appendChild (lk);
| |
− | this.normalLinks.appendChild (span);
| |
− | this.undoLink = span;
| |
− | if (!onUpload) {
| |
− | try {
| |
− | this.catLink.style.backgroundColor = HotCat.bg_changed;
| |
− | } catch (ex) {}
| |
− | }
| |
− | }
| |
− | if (this.upDownLinks) this.upDownLinks.style.display = this.lastSavedExists ? "" : 'none';
| |
− | this.linkSpan.style.display = "";
| |
− | this.state = CategoryEditor.CHANGED;
| |
− | checkMultiInput ();
| |
− | forceRedraw ();
| |
− | },
| |
| | | |
− | commit : function (comment) { | + | this.createCatLinks( '↑', this.parentCats ); |
− | // Check again to catch problem cases after redirect resolution
| + | this.createCatLinks( '→', thiscat ); |
− | if ( ( this.currentCategory == this.originalCategory
| + | this.createCatLinks( '↓', this.subCats ); |
− | && (this.currentKey == this.originalKey
| |
− | || this.currentKey === null && this.originalKey.length === 0
| |
− | )
| |
− | )
| |
− | || 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 (function (failure) {performChanges (failure, self);}, function (msg) {alert (msg);});
| |
− | }
| |
− | },
| |
| | | |
− | remove : function (evt) { | + | document.body.style.cursor = 'auto'; |
− | this.doRemove (evtKeys (evt) & 1);
| + | // Reset width |
− | return evtKill (evt);
| + | $container.width( '' ); |
− | },
| + | $container.height( '' ); |
− | | + | $container.width( Math.min( $container.width() * 1.1 + 15, $( window ).width() - 10 ) ); |
− | doRemove : function (noCommit) {
| |
− | if (this.isAddCategory) { // Empty input on adding a new category
| |
− | this.cancel ();
| |
− | return;
| |
− | }
| |
− | if (!commitButton && !onUpload) {
| |
− | for (var i = 0; i < editors.length; i++) {
| |
− | if (editors[i].state != CategoryEditor.UNCHANGED) {
| |
− | setMultiInput();
| |
− | break;
| |
− | }
| |
− | }
| |
− | }
| |
− | if (commitButton) {
| |
− | this.catLink.title = "";
| |
− | this.catLink.style.cssText += '; text-decoration : line-through !important;';
| |
− | try {
| |
− | this.catLink.style.backgroundColor = HotCat.bg_changed;
| |
− | } catch (ex) {}
| |
− | this.originalState = this.state;
| |
− | this.state = CategoryEditor.DELETED;
| |
− | this.normalLinks.style.display = 'none';
| |
− | this.undelLink.style.display = "";
| |
− | checkMultiInput ();
| |
− | } else {
| |
− | if (onUpload) {
| |
− | // Remove this editor completely
| |
− | this.removeEditor ();
| |
− | } else {
| |
− | this.originalState = this.state;
| |
− | this.state = CategoryEditor.DELETED;
| |
− | this.noCommit = noCommit || HotCat.del_needs_diff;
| |
− | var self = this;
| |
− | initiateEdit (function (failure) {performChanges (failure, self);}, function (msg) {self.state = self.originalState; alert (msg);});
| |
− | }
| |
− | }
| |
− | },
| |
| | | |
− | restore : function (evt) { | + | $resultList.css( { |
− | // Can occur only if we do have a commit button and are not on the upload form | + | maxHeight: this.setHeight + 'px', |
− | this.catLink.title = this.currentKey || "";
| + | height: '' |
− | this.catLink.style.textDecoration = "";
| + | } ); |
− | this.state = this.originalState;
| + | }, |
− | if (this.state == CategoryEditor.UNCHANGED) {
| |
− | this.catLink.style.backgroundColor = 'transparent';
| |
− | } else { | |
− | try {
| |
− | this.catLink.style.backgroundColor = HotCat.bg_changed;
| |
− | } catch (ex) {}
| |
− | }
| |
− | this.normalLinks.style.display = "";
| |
− | this.undelLink.style.display = 'none';
| |
− | checkMultiInput ();
| |
− | return evtKill (evt);
| |
− | },
| |
| | | |
− | // Internal operations | + | updateCats: function( newcat ) { |
| + | document.body.style.cursor = 'wait'; |
| | | |
− | selectEngine : function (engineName) { | + | this.currentCategory = newcat; |
− | if (!this.engineSelector) return;
| + | $resultList.html( '<div class="cat_a_lot_loading">' + msgPlain( 'loading' ) + '</div>' ); |
− | for (var i = 0; i < this.engineSelector.options.length; i++) {
| + | this.getCategoryList(); |
− | this.engineSelector.options[i].selected = this.engineSelector.options[i].value == engineName;
| + | }, |
− | }
| + | |
− | },
| + | showProgress: function() { |
| + | document.body.style.cursor = 'wait'; |
| | | |
− | sanitizeInput : function () { | + | this.progressDialog = $( '<div>' ) |
− | var v = this.text.value || ""; | + | .html( msg( 'editing' ) + ' <span id="cat_a_lot_current">' + this.counterCurrent + '</span> ' + msg( 'of' ) + this.counterNeeded ) |
− | v = v.replace(/^(\s|_)+/, ""); // Trim leading blanks and underscores
| + | .dialog( { |
− | var re = new RegExp ('^(' + HotCat.category_regexp + '):');
| + | width: 450, |
− | if (re.test (v)) { | + | height: 90, |
− | v = v.substring (v.indexOf (':') + 1).replace(/^(\s|_)+/, ""); | + | minHeight: 90, |
− | }
| + | modal: true, |
− | if (HotCat.capitalizePageNames) v = capitalize (v);
| + | resizable: false, |
− | // Only update the input field if there is a difference. IE8 appears to reset the selection
| + | draggable: false, |
− | // and place the cursor at the front upon reset, which makes our autocompletetion become a
| + | closeOnEscape: false, |
− | // nuisance. FF and IE6 don't seem to have this problem. | + | dialogClass: 'cat_a_lot_feedback' |
− | if (this.text.value !== null && this.text.value != v) | + | } ); |
− | this.text.value = v;
| + | $( '.ui-dialog-titlebar' ) |
− | },
| + | .hide(); |
| + | this.domCounter = $( '#cat_a_lot_current' ); |
| | | |
− | makeCall : function (url, callbackObj, engine, queryKey, cleanKey) {
| + | }, |
− | var cb = callbackObj;
| |
− | var e = engine;
| |
− | var v = queryKey;
| |
− | var z = cleanKey;
| |
− | var thisObj = this;
| |
| | | |
− | function done () {
| + | run: function() { |
− | cb.callsMade++;
| + | if ( $( '.cat_a_lot_enabled' ).length ) { |
− | if (cb.callsMade === cb.nofCalls) {
| + | this.makeClickable(); |
− | if (cb.exists) cb.allTitles.exists = true;
| + | $dataContainer |
− | if (cb.normalized) cb.allTitles.normalized = cb.normalized;
| + | .show(); |
− | if (!cb.dontCache && !suggestionConfigs[cb.engineName].cache[z]) { | + | $container |
− | suggestionConfigs[cb.engineName].cache[z] = cb.allTitles; | + | .resizable( { |
| + | handles: 'n', |
| + | alsoResize: '#cat_a_lot_category_list', |
| + | resize: function() { |
| + | $( this ) |
| + | .css( { |
| + | left: '', |
| + | top: '' |
| + | } ); |
| + | catALot.setHeight = $( this ).height(); |
| + | $resultList |
| + | .css( { |
| + | maxHeight: '', |
| + | width: '' |
| + | } ); |
| } | | } |
− | thisObj.text.readOnly = false;
| + | } ) |
− | if (!cb.cancelled) thisObj.showSuggestions (cb.allTitles, cb.noCompletion, v, cb.engineName);
| + | /*.draggable( { // FIXME: Box get static if sametime resize |
− | if (cb === thisObj.callbackObj) thisObj.callbackObj = null;
| + | cursor: 'move', |
− | delete cb; | + | start: function() { |
− | }
| + | $( this ).css( 'height', $( this ).height() ); |
− | }
| |
− | | |
− | getJSON ({
| |
− | uri : url
| |
− | ,success : function (json) {
| |
− | var titles = e.handler (json, z);
| |
− | if (titles && titles.length > 0) {
| |
− | if (cb.allTitles === null) {
| |
− | cb.allTitles = titles;
| |
− | } else {
| |
− | cb.allTitles = cb.allTitles.concat (titles);
| |
| } | | } |
− | if (titles.exists) cb.exists = true;
| + | } )*/; |
− | if (titles.normalized) cb.normalized = titles.normalized;
| + | $resultList |
− | } | + | .css( { |
− | done();
| + | maxHeight: '450px' |
− | }
| + | } ); |
− | ,error : function (req) {if (!req) noSuggestions = true; cb.dontCache = true; done(); }
| |
− | });
| |
− | },
| |
| | | |
− | callbackObj : null,
| + | this.updateCats( this.origin || 'Images' ); |
− | | + | $link.text( 'X' ); |
− | textchange : function (dont_autocomplete, force) {
| + | } else { |
− | // Hide all other lists
| + | $dataContainer |
− | makeActive (this);
| + | .hide(); |
− | // Get input value, omit sort key, if any
| + | $container |
− | this.sanitizeInput (); | + | // .draggable( 'destroy' ) |
− | var v = this.text.value;
| + | .resizable( 'destroy' ) |
− | // Disregard anything after a pipe.
| + | .removeAttr( 'style' ); |
− | var pipe = v.indexOf ('|');
| + | // Unbind click handlers |
− | if (pipe >= 0) { | + | this.labels.unbind( 'click.catALot' ); |
− | this.currentKey = v.substring (pipe+1);
| + | $link.text( 'Cat-a-lot' ); |
− | v = v.substring (0, pipe);
| + | } |
− | } else {
| + | }, |
− | this.currentKey = null;
| |
− | } | |
− | if (this.lastInput == v && !force) return; // No change
| |
− | if (this.lastInput != v) checkMultiInput ();
| |
− | this.lastInput = v; | |
− | this.lastRealInput = v;
| |
− | | |
− | // Mark blacklisted inputs.
| |
− | this.ok.disabled = v.length > 0 && HotCat.blacklist && HotCat.blacklist.test (v);
| |
− | | |
− | if (noSuggestions) {
| |
− | // No Ajax: just make sure the list is hidden | |
− | if (this.list) this.list.style.display = 'none';
| |
− | if (this.engineSelector) this.engineSelector.style.display = 'none'; | |
− | if (this.icon) this.icon.style.display = 'none';
| |
− | return;
| |
− | }
| |
− | | |
− | if (v.length === 0) { this.showSuggestions([]); return; }
| |
− | var cleanKey = v.replace(/[\u200E\u200F\u202A-\u202E]/g, "").replace(wikiTextBlankRE, ' '); | |
− | cleanKey = replaceShortcuts(cleanKey, HotCat.shortcuts); | |
− | cleanKey = cleanKey.replace(/^\s+|\s+$/g, '');
| |
− | if (cleanKey.length === 0) { this.showSuggestions([]); return; } | |
− | | |
− | if (this.callbackObj) this.callbackObj.cancelled = true;
| |
− | var engineName = suggestionConfigs[this.engine] ? this.engine : 'combined';
| |
− | | |
− | dont_autocomplete = dont_autocomplete || suggestionConfigs[engineName].noCompletion;
| |
− | if (suggestionConfigs[engineName].cache[cleanKey]) {
| |
− | this.showSuggestions (suggestionConfigs[engineName].cache[cleanKey], dont_autocomplete, v, engineName);
| |
− | return;
| |
− | }
| |
− | | |
− | var engines = suggestionConfigs[engineName].engines;
| |
− | this.callbackObj =
| |
− | {allTitles: null, callsMade: 0, nofCalls: engines.length, noCompletion: dont_autocomplete, engineName: engineName};
| |
− | this.makeCalls (engines, this.callbackObj, v, cleanKey);
| |
− | },
| |
− | | |
− | makeCalls : function (engines, cb, v, cleanKey) { | |
− | for (var j = 0; j < engines.length; j++) {
| |
− | var engine = suggestionEngines[engines[j]];
| |
− | var url = conf.wgServer + conf.wgScriptPath + engine.uri.replace (/\$1/g, encodeURIComponent (cleanKey));
| |
− | this.makeCall (url, cb, engine, v, cleanKey);
| |
− | }
| |
− | },
| |
| | | |
− | showSuggestions : function (titles, dontAutocomplete, queryKey, engineName) {
| + | manageSettings: function() { |
− | this.text.readOnly = false;
| + | mw.loader.using( [ 'ext.gadget.SettingsManager', 'ext.gadget.SettingsUI', 'jquery.ui.progressbar' ], function() { |
− | this.dab = null;
| + | catALot._manageSettings(); |
− | this.showsList = false;
| + | } ); |
− | if (!this.list) return;
| + | }, |
− | if (noSuggestions) {
| + | |
− | if (this.list) this.list.style.display = 'none';
| + | _manageSettings: function() { |
− | if (this.engineSelector) this.engineSelector.style.display = 'none';
| + | mw.libs.SettingsUI( this.defaults, 'Cat-a-lot' ) |
− | if (this.icon) this.icon.style.display = 'none';
| + | .show() |
− | this.inputExists = true; // Default...
| + | .done( function( s, verbose, loc, settingsOut, $dlg ) { |
− | return;
| + | var mustRestart = false, |
− | }
| + | _restart = function() { |
− | this.engineName = engineName;
| + | if ( !mustRestart ) { |
− | if (engineName) {
| + | return; |
− | if (!this.engineSelector) this.engineName = null;
| |
− | } else {
| |
− | if (this.engineSelector) this.engineSelector.style.display = 'none';
| |
− | }
| |
− | if (queryKey) {
| |
− | if (this.lastInput.indexOf (queryKey) !== 0) return;
| |
− | if (this.lastQuery && this.lastInput.indexOf (this.lastQuery) === 0 && this.lastQuery.length > queryKey.length)
| |
− | return;
| |
− | }
| |
− | this.lastQuery = queryKey;
| |
− | | |
− | // Get current input text
| |
− | var v = this.text.value.split('|');
| |
− | var key = v.length > 1 ? '|' + v[1] : "";
| |
− | v = (HotCat.capitalizePageNames ? capitalize (v[0]) : v[0]); | |
− | var vNormalized = v; | |
− | var knownToExist = titles && titles.exists;
| |
− | var i;
| |
− | if (titles) {
| |
− | if (titles.normalized && v.indexOf(queryKey) === 0) {
| |
− | // We got back a different normalization than what is in the input field
| |
− | vNormalized = titles.normalized + v.substring(queryKey.length);
| |
− | }
| |
− | var vLow = vNormalized.toLowerCase (); | |
− | // Strip blacklisted categories
| |
− | if (HotCat.blacklist) {
| |
− | for (i = 0; i < titles.length; i++) { | |
− | if (HotCat.blacklist.test (titles[i])) { | |
− | titles.splice(i, 1); | |
− | i--;
| |
| } | | } |
− | }
| |
− | }
| |
− | titles.sort (
| |
− | function (a, b) {
| |
− | if (a == b) return 0;
| |
− | if (a.indexOf (b) === 0) return 1; // a begins with b: a > b
| |
− | if (b.indexOf (a) === 0) return -1; // b begins with a: a < b
| |
− | // Opensearch may return stuff not beginning with the search prefix!
| |
− | var prefixMatchA = (a.indexOf (vNormalized) === 0 ? 1 : 0);
| |
− | var prefixMatchB = (b.indexOf (vNormalized) === 0 ? 1 : 0);
| |
− | if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
| |
− | // Case-insensitive prefix match!
| |
− | var aLow = a.toLowerCase(), bLow = b.toLowerCase();
| |
− | prefixMatchA = (aLow.indexOf (vLow) === 0 ? 1 : 0);
| |
− | prefixMatchB = (bLow.indexOf (vLow) === 0 ? 1 : 0);
| |
− | if (prefixMatchA != prefixMatchB) return prefixMatchB - prefixMatchA;
| |
− | if (a < b) return -1;
| |
− | if (b < a) return 1;
| |
− | return 0;
| |
− | }
| |
− | );
| |
− | // Remove duplicates and self-references
| |
− | for (i = 0; i < titles.length; i++) {
| |
− | if ( i+1 < titles.length && titles[i] == titles[i+1]
| |
− | || conf.wgNamespaceNumber == 14 && titles[i] == conf.wgTitle
| |
− | )
| |
− | {
| |
− | titles.splice (i, 1);
| |
− | i--;
| |
− | }
| |
− | }
| |
− | }
| |
− | if (!titles || titles.length === 0) {
| |
− | if (this.list) this.list.style.display = 'none';
| |
− | if (this.engineSelector) this.engineSelector.style.display = 'none';
| |
− | if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
| |
− | if (this.icon) this.icon.src = armorUri(HotCat.existsNo);
| |
− | this.inputExists = false;
| |
− | }
| |
− | return;
| |
− | }
| |
| | | |
− | var firstTitle = titles[0];
| + | $container.remove(); |
− | var completed = this.autoComplete (firstTitle, v, vNormalized, key, dontAutocomplete);
| + | catALot.labels.unbind( 'click.catALot' ); |
− | var existing = completed || knownToExist || firstTitle == replaceShortcuts(v, HotCat.shortcuts);
| + | catALot.init(); |
− | if (engineName && suggestionConfigs[engineName] && !suggestionConfigs[engineName].temp) {
| + | }, |
− | this.icon.src = armorUri(existing ? HotCat.existsYes : HotCat.existsNo);
| + | _saveToJS = function() { |
− | this.inputExists = existing;
| + | var opt = mw.libs.settingsManager.option( { |
− | }
| + | optionName: 'catALotPrefs', |
− | if (completed) {
| + | value: catALot.settings, |
− | this.lastInput = firstTitle;
| + | encloseSignature: 'catALot', |
− | if (titles.length === 1) {
| + | encloseBlock: '////////// Cat-a-lot user preferences //////////\n', |
− | this.list.style.display = 'none';
| + | triggerSaveAt: /Cat.?A.?Lot/i, |
− | if (this.engineSelector) this.engineSelector.style.display = 'none';
| + | editSummary: msgPlain( 'pref-save-summary' ) |
− | return;
| + | } ), |
− | }
| + | oldHeight = $dlg.height(), |
− | }
| + | $prog = $( '<div>' ); |
− | // (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 () {
| + | $dlg.css( 'height', oldHeight ) |
− | this.showsList = true;
| + | .html( '' ); |
− | if (!this.is_active) {
| + | $prog.css( { |
− | this.list.style.display = 'none';
| + | height: Math.round( oldHeight / 8 ), |
− | if (this.engineSelector) this.engineSelector.style.display = 'none';
| + | 'margin-top': Math.round( ( 7 * oldHeight ) / 16 ) |
− | return;
| + | } ) |
− | }
| + | .appendTo( $dlg ); |
− | var nofItems = (this.list.options.length > HotCat.list_size ? HotCat.list_size : this.list.options.length);
| |
− | if (nofItems <= 1) nofItems = 2;
| |
− | this.list.size = nofItems;
| |
− | this.list.style.align = is_rtl ? 'right' : 'left';
| |
− | this.list.style.zIndex = 5;
| |
− | this.list.style.position = 'absolute';
| |
− | // Compute initial list position. First the height.
| |
− | var anchor = is_rtl ? 'right' : 'left';
| |
− | var listh = 0;
| |
− | if (this.list.style.display == 'none') {
| |
− | // Off-screen display to get the height
| |
− | this.list.style.top = this.text.offsetTop + 'px';
| |
− | this.list.style[anchor] = '-10000px';
| |
− | this.list.style.display = "";
| |
− | listh = this.list.offsetHeight;
| |
− | this.list.style.display = 'none';
| |
− | } else {
| |
− | listh = this.list.offsetHeight;
| |
− | }
| |
− | // Approximate calculation of maximum list size
| |
− | var maxListHeight = listh;
| |
− | if (nofItems < HotCat.list_size) maxListHeight = (listh / nofItems) * HotCat.list_size;
| |
| | | |
− | function viewport (what) {
| + | $dlg.parent() |
− | if (is_webkit && !document.evaluate)
| + | .find( '.ui-dialog-buttonpane button' ) |
− | return window['inner' + what]; // Safari < 3.0
| + | .button( 'option', 'disabled', true ); |
− | var s = 'client' + what;
| |
− | if (window.opera) return document.body[s];
| |
− | return (document.documentElement ? document.documentElement[s] : 0)
| |
− | || document.body[s] || 0;
| |
− | }
| |
− | function scroll_offset (what) {
| |
− | var s = 'scroll' + what;
| |
− | var result = (document.documentElement ? document.documentElement[s] : 0)
| |
− | || document.body[s] || 0; | |
− | if (is_rtl && what == 'Left') {
| |
− | // RTL inconsistencies.
| |
− | // FF: 0 at the far right, then increasingly negative values.
| |
− | // IE >= 8: 0 at the far right, then increasingly positive values.
| |
− | // Webkit: scrollWidth - clientWidth at the far right, then down to zero.
| |
− | // IE 7: like webkit; IE6: disabled in RTL anyway since too many problems.
| |
− | // Opera: don't know...
| |
− | if (result < 0) result = - result;
| |
− | if (!is_webkit) {
| |
− | result = scroll_offset('Width') - viewport('Width') - result;
| |
− | }
| |
− | // Now all have webkit behavior, i.e. zero if at the leftmost edge.
| |
− | }
| |
− | return result;
| |
− | }
| |
− | function position (node) {
| |
− | // Stripped-down simplified position function. It's good enough for our purposes.
| |
− | if (node.getBoundingClientRect) {
| |
− | var box = node.getBoundingClientRect ();
| |
− | return { x : Math.round (box.left + scroll_offset ('Left'))
| |
− | ,y : Math.round (box.top + scroll_offset ('Top'))
| |
− | };
| |
− | }
| |
− | var t = 0, l = 0;
| |
− | do {
| |
− | t = t + (node.offsetTop || 0);
| |
− | l = l + (node.offsetLeft || 0);
| |
− | node = node.offsetParent;
| |
− | } while (node);
| |
− | return {x : l, y : t};
| |
− | }
| |
| | | |
− | var textPos = position (this.text);
| + | opt.save() |
− | var nl = 0;
| + | .done( function( text, progress ) { |
− | var nt = 0;
| + | $prog.progressbar( { |
− | var offset = 0;
| + | value: progress |
− | // Opera 9.5 somehow has offsetWidth = 0 here?? Use the next best value...
| + | } ); |
− | var textBoxWidth = this.text.offsetWidth || this.text.clientWidth;
| + | $prog.fadeOut( function() { |
− | if (this.engineName) {
| + | $dlg.dialog( 'close' ); |
− | this.engineSelector.style.zIndex = 5;
| + | _restart(); |
− | 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.
| + | .progress( function( text, progress ) { |
− | if (this.engineSelector.style.display == 'none') {
| + | $prog.progressbar( { |
− | this.engineSelector.style[anchor] = '-10000px';
| + | value: progress |
− | this.engineSelector.style.top = '0px';
| + | } ); |
− | this.engineSelector.style.display = "";
| + | // TODO: Add "details" to progressbar |
− | offset = this.engineSelector.offsetHeight;
| + | } ) |
− | this.engineSelector.style.display = 'none';
| + | .fail( function( text ) { |
− | } else {
| + | $prog.addClass( 'ui-state-error' ); |
− | offset = this.engineSelector.offsetHeight;
| + | $dlg.prepend( $( '<p>' ) |
− | }
| + | .text( text ) ); |
− | this.engineSelector.style[anchor] = nl + 'px';
| + | } ); |
− | }
| + | }; |
− | if (textPos.y < maxListHeight + offset + 1) {
| + | $.each( settingsOut, function( n, v ) { |
− | // The list might extend beyond the upper border of the page. Let's avoid that by placing it
| + | if ( v.forcerestart && catALot.settings[ v.name ] !== v.value ) { |
− | // below the input text field.
| + | mustRestart = true; |
− | 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;
| |
| } | | } |
| + | catALot.settings[ v.name ] = v.value; |
| + | window.catALotPrefs[ v.name ] = v.value; |
| + | } ); |
| + | switch ( loc ) { |
| + | case 'page': |
| + | $dlg.dialog( 'close' ); |
| + | _restart(); |
| + | break; |
| + | case 'account-publicly': |
| + | _saveToJS(); |
| + | break; |
| } | | } |
− | var relative_offset = 0;
| + | } ); |
− | if (left < scroll) {
| + | }, |
− | relative_offset = scroll - left;
| + | |
− | } else if (right > scroll + view_w) {
| + | _initSettings: function() { |
− | relative_offset = - (right - scroll - view_w);
| + | if ( this.settings.watchlist ) { |
− | }
| + | return; |
− | if (is_rtl) relative_offset = - relative_offset;
| |
− | if (relative_offset !== 0) {
| |
− | this.list.style[anchor] = (nl + relative_offset) + 'px';
| |
− | }
| |
− | }
| |
− | },
| |
− | | |
− | autoComplete : function (newVal, actVal, normalizedActVal, key, dontModify) {
| |
− | if (newVal == actVal) return true;
| |
− | if (dontModify || this.ime || !this.canSelect()) return false;
| |
− | // If we can't select properly or an IME composition is ongoing, autocompletion would be a major annoyance to the user.
| |
− | if (newVal.indexOf (actVal) !== 0) {
| |
− | // Maybe it'll work with the normalized value (NFC)?
| |
− | if (normalizedActVal && newVal.indexOf(normalizedActVal) === 0) {
| |
− | if (this.lastRealInput == actVal) this.lastRealInput = normalizedActVal;
| |
− | actVal = normalizedActVal;
| |
− | } else {
| |
− | return false;
| |
− | }
| |
− | }
| |
− | // Actual input is a prefix of the new text. Fill in new text, selecting the newly added suffix
| |
− | // such that it can be easily removed by typing backspace if the suggestion is unwanted.
| |
− | this.text.focus();
| |
− | this.text.value = newVal + key;
| |
− | this.setSelection (actVal.length, newVal.length);
| |
− | return true;
| |
− | },
| |
− | | |
− | canSelect : function () {
| |
− | return this.text.setSelectionRange
| |
− | || this.text.createTextRange
| |
− | || typeof this.text.selectionStart != 'undefined'
| |
− | && typeof this.text.selectionEnd != 'undefined';
| |
− | },
| |
− | | |
− | setSelection : function (from, to) {
| |
− | // this.text must be focused (at least on IE)
| |
− | if (!this.text.value) return;
| |
− | if (this.text.setSelectionRange) { // e.g. khtml
| |
− | this.text.setSelectionRange (from, to);
| |
− | } else if (typeof this.text.selectionStart != 'undefined') {
| |
− | if (from > this.text.selectionStart) {
| |
− | this.text.selectionEnd = to;
| |
− | this.text.selectionStart = from;
| |
− | } else {
| |
− | this.text.selectionStart = from;
| |
− | this.text.selectionEnd = to;
| |
− | }
| |
− | } else if (this.text.createTextRange) { // IE
| |
− | var new_selection = this.text.createTextRange();
| |
− | new_selection.move ('character', from);
| |
− | new_selection.moveEnd ('character', to - from);
| |
− | new_selection.select();
| |
− | }
| |
− | },
| |
− | | |
− | getSelection : function () {
| |
− | var from = 0, to = 0;
| |
− | // this.text must be focused (at least on IE)
| |
− | if (!this.text.value) {
| |
− | // No text.
| |
− | } else if (typeof this.text.selectionStart != 'undefined') {
| |
− | from = this.text.selectionStart;
| |
− | to = this.text.selectionEnd;
| |
− | } else if (document.selection && document.selection.createRange) { // IE
| |
− | var rng = document.selection.createRange().duplicate();
| |
− | if (rng.parentElement() === this.text) {
| |
− | try {
| |
− | var textRng = this.text.createTextRange();
| |
− | textRng.move('character', 0);
| |
− | textRng.setEndPoint('EndToEnd', rng);
| |
− | // We're in a single-line input box: no need to care about IE's strange
| |
− | // handling of line ends
| |
− | to = textRng.text.length;
| |
− | textRng.setEndPoint('EndToStart', rng);
| |
− | from = textRng.text.length;
| |
− | } catch (notFocused) {
| |
− | from = this.text.value.length; to = from; // At end of text
| |
− | }
| |
− | }
| |
− | }
| |
− | return {start: from, end: to}; | |
− | },
| |
− | | |
− | saveView : function (evt) {
| |
− | this.lastSelection = this.getSelection ();
| |
− | },
| |
− | | |
− | processKey : function (evt) {
| |
− | var dir = 0;
| |
− | switch (this.lastKey) {
| |
− | case UP: dir = -1;
| |
− | case DOWN: if (dir === 0) dir = 1;
| |
− | case PGUP: if (dir === 0) dir = -HotCat.list_size;
| |
− | case PGDOWN: if (dir === 0) dir = HotCat.list_size;
| |
− | if (this.list.style.display != 'none') {
| |
− | // List is visible, so there are suggestions
| |
− | this.highlightSuggestion (dir);
| |
− | // Kill the event, otherwise some browsers (e.g., Firefox) may additionally treat an up-arrow
| |
− | // as "place the text cursor at the front", which we don't want here.
| |
− | return evtKill (evt);
| |
− | } else if ( this.keyCount <= 1
| |
− | && (!this.callbackObj || this.callbackObj.callsMade == this.callbackObj.nofCalls)
| |
− | )
| |
− | {
| |
− | // If no suggestions displayed, get them, unless we're already getting them.
| |
− | this.textchange ();
| |
− | }
| |
− | break;
| |
− | case ESC: // Inhibit default behavior (revert to last real input in FF: we do that ourselves)
| |
− | return evtKill (evt);
| |
− | }
| |
− | return true;
| |
− | },
| |
− | | |
− | highlightSuggestion : function (dir) {
| |
− | if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
| |
− | var curr = this.list.selectedIndex;
| |
− | var tgt = -1;
| |
− | if (dir === 0) {
| |
− | if (curr < 0 || curr >= this.list.options.length) return false;
| |
− | tgt = curr;
| |
− | } else {
| |
− | tgt = curr < 0 ? 0 : curr + dir;
| |
− | tgt = tgt < 0 ? 0 : tgt;
| |
− | if (tgt >= this.list.options.length) tgt = this.list.options.length - 1;
| |
− | }
| |
− | if (tgt != curr || dir === 0) {
| |
− | if (curr >= 0 && curr < this.list.options.length && dir !== 0) this.list.options[curr].selected = false;
| |
− | this.list.options[tgt].selected = true;
| |
− | // Get current input text
| |
− | var v = this.text.value.split('|');
| |
− | var key = v.length > 1 ? '|' + v[1] : "";
| |
− | var completed = this.autoComplete (this.list.options[tgt].text, this.lastRealInput, null, key, false);
| |
− | if (!completed || this.list.options[tgt].text == this.lastRealInput) {
| |
− | this.text.value = this.list.options[tgt].text + key;
| |
− | if (this.canSelect()) this.setSelection (this.list.options[tgt].text.length, this.list.options[tgt].text.length);
| |
− | }
| |
− | this.lastInput = this.list.options[tgt].text;
| |
− | this.inputExists = true; // Might be wrong if from a dab list...
| |
− | if (this.icon) this.icon.src = armorUri(HotCat.existsYes);
| |
− | this.state = CategoryEditor.CHANGE_PENDING;
| |
− | }
| |
− | return true;
| |
− | },
| |
− | | |
− | resetKeySelection : function () { | |
− | if (noSuggestions || !this.list || this.list.style.display == 'none') return false;
| |
− | var curr = this.list.selectedIndex;
| |
− | if (curr >= 0 && curr < this.list.options.length) {
| |
− | this.list.options[curr].selected = false;
| |
− | // Get current input text
| |
− | var v = this.text.value.split('|');
| |
− | var key = v.length > 1 ? '|' + v[1] : "";
| |
− | // ESC is handled strangely by some browsers (e.g., FF); somehow it resets the input value before
| |
− | // our event handlers ever get a chance to run.
| |
− | var result = v[0] != this.lastInput;
| |
− | if (v[0] != this.lastRealInput) {
| |
− | this.text.value = this.lastRealInput + key;
| |
− | result = true;
| |
− | }
| |
− | this.lastInput = this.lastRealInput;
| |
− | return result;
| |
− | }
| |
− | return false; | |
| } | | } |
− | | + | if ( !window.catALotPrefs ) { |
− | }; // end CategoryEditor.prototype
| + | window.catALotPrefs = {}; |
− | | |
− | function initialize () {
| |
− | // User configurations. Do this here, called from the onload handler, so that users can
| |
− | // override it easily in their own user script files by just declaring variables. JSconfig
| |
− | // is some feature used at Wikimedia Commons.
| |
− | var config = (typeof JSconfig != 'undefined' && JSconfig.keys) ? JSconfig.keys : {};
| |
− | HotCat.dont_add_to_watchlist =
| |
− | (typeof window.hotcat_dont_add_to_watchlist != 'undefined'
| |
− | ? !!window.hotcat_dont_add_to_watchlist
| |
− | : (typeof config.HotCatDontAddToWatchlist != 'undefined'
| |
− | ? config.HotCatDontAddToWatchlist
| |
− | : HotCat.dont_add_to_watchlist
| |
− | )
| |
− | );
| |
− | HotCat.no_autocommit =
| |
− | (typeof window.hotcat_no_autocommit != 'undefined'
| |
− | ? !!window.hotcat_no_autocommit
| |
− | : (typeof config.HotCatNoAutoCommit != 'undefined'
| |
− | ? config.HotCatNoAutoCommit
| |
− | : HotCat.no_autocommit
| |
− | )
| |
− | );
| |
− | HotCat.del_needs_diff =
| |
− | (typeof window.hotcat_del_needs_diff != 'undefined'
| |
− | ? !!window.hotcat_del_needs_diff
| |
− | : (typeof config.HotCatDelNeedsDiff != 'undefined'
| |
− | ? config.HotCatDelNeedsDiff
| |
− | : HotCat.del_needs_diff
| |
− | )
| |
− | );
| |
− | HotCat.suggest_delay = window.hotcat_suggestion_delay
| |
− | || config['HotCatSuggestionDelay']
| |
− | || HotCat.suggest_delay;
| |
− | HotCat.editbox_width = window.hotcat_editbox_width
| |
− | || config['HotCatEditBoxWidth']
| |
− | || HotCat.editbox_width;
| |
− | HotCat.suggestions = window.hotcat_suggestions
| |
− | || config['HotCatSuggestions']
| |
− | || HotCat.suggestions;
| |
− | if (typeof HotCat.suggestions != 'string' || !suggestionConfigs[HotCat.suggestions]) | |
− | HotCat.suggestions = 'combined';
| |
− | HotCat.fixed_search =
| |
− | (typeof window.hotcat_suggestions_fixed != 'undefined'
| |
− | ? !!window.hotcat_suggestions_fixed
| |
− | : (typeof config.HotCatFixedSuggestions != 'undefined'
| |
− | ? config.HotCatFixedSuggestions
| |
− | : HotCat.fixed_search
| |
− | )
| |
− | ); | |
− | HotCat.single_minor =
| |
− | (typeof window.hotcat_single_changes_are_minor != 'undefined'
| |
− | ? !!window.hotcat_single_changes_are_minor
| |
− | : (typeof config.HotCatMinorSingleChanges != 'undefined'
| |
− | ? config.HotCatMinorSingleChanges
| |
− | : HotCat.single_minor
| |
− | )
| |
− | );
| |
− | HotCat.bg_changed = window.hotcat_changed_background
| |
− | || config.HotCatChangedBackground
| |
− | || HotCat.bg_changed;
| |
− | HotCat.use_up_down =
| |
− | (typeof window.hotcat_use_category_links != 'undefined'
| |
− | ? !!window.hotcat_use_category_links
| |
− | : (typeof config.HotCatUseCategoryLinks != 'undefined'
| |
− | ? config.HotCatUseCategoryLinks
| |
− | : HotCat.use_up_down
| |
− | )
| |
− | );
| |
− | HotCat.list_size = window.hotcat_list_size
| |
− | || config.HotCatListSize
| |
− | || HotCat.list_size;
| |
− | // Numeric input, make sure we have a numeric value
| |
− | HotCat.list_size = parseInt (HotCat.list_size, 10);
| |
− | if (isNaN (HotCat.list_size) || HotCat.list_size < 5) HotCat.list_size = 5;
| |
− | if (HotCat.list_size > 15) HotCat.list_size = 15;
| |
− | // Localize search engine names
| |
− | if (HotCat.engine_names) {
| |
− | for (var key in HotCat.engine_names) {
| |
− | if (suggestionConfigs[key] && HotCat.engine_names[key]) {
| |
− | suggestionConfigs[key].name = HotCat.engine_names[key];
| |
− | }
| |
− | }
| |
| } | | } |
− | // Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]] | + | $.each( this.defaults, function( n, v ) { |
− | is_rtl = hasClass (document.body, 'rtl');
| + | v.value = catALot.settings[ v.name ] = ( window.catALotPrefs[ v.name ] || v[ 'default' ] ); |
− | if (!is_rtl) {
| + | v.label = msgPlain( v.label_i18n ); |
− | if (document.defaultView && document.defaultView.getComputedStyle) { // Gecko etc. | + | if ( v.select_i18n ) { |
− | is_rtl = document.defaultView.getComputedStyle (document.body, null).getPropertyValue ('direction'); | + | v.select = {}; |
− | } else if (document.body.currentStyle) { // IE, has subtle differences to getComputedStyle
| + | $.each( v.select_i18n, function( i18nk, val ) { |
− | is_rtl = document.body.currentStyle['direction'];
| + | v.select[ msgPlain( i18nk ) ] = val; |
− | } else { // Not exactly right, but best effort
| + | } ); |
− | is_rtl = document.body.style['direction'];
| |
| } | | } |
− | is_rtl = (is_rtl == 'rtl'); | + | } ); |
| + | }, |
| + | /* eslint-disable camelcase */ |
| + | defaults: [ { |
| + | name: 'watchlist', |
| + | 'default': 'preferences', |
| + | label_i18n: 'watchlistpref', |
| + | select_i18n: { |
| + | watch_pref: 'preferences', |
| + | watch_nochange: 'nochange', |
| + | watch_watch: 'watch', |
| + | watch_unwatch: 'unwatch' |
| } | | } |
− | } | + | }, { |
− | | + | name: 'minor', |
− | function can_edit () {
| + | 'default': false, |
− | var container = null; | + | label_i18n: 'minorpref' |
− | switch (mw.config.get('skin')) {
| + | }, { |
− | case 'cologneblue':
| + | name: 'editpages', |
− | container = document.getElementById ('quickbar');
| + | 'default': false, |
− | // Fall through
| + | label_i18n: 'editpagespref', |
− | case 'standard':
| + | forcerestart: true |
− | case 'nostalgia':
| + | }, { |
− | if (!container) container = document.getElementById ('topbar');
| + | name: 'docleanup', |
− | var lks = container.getElementsByTagName ('a');
| + | 'default': false, |
− | for (var i = 0; i < lks.length; i++) {
| + | label_i18n: 'docleanuppref' |
− | if ( param ('title', lks[i].href) == conf.wgPageName
| + | }, { |
− | && param ('action', lks[i].href) == 'edit')
| + | name: 'subcatcount', |
− | return true;
| + | 'default': 50, |
− | }
| + | min: 5, |
− | return false;
| + | max: 500, |
− | default:
| + | label_i18n: 'subcatcountpref', |
− | // all modern skins:
| + | forcerestart: true |
− | return document.getElementById ('ca-edit') !== null;
| + | } ] |
− | } | + | /* eslint-enable camelcase */ |
− | return false;
| + | }; |
− | }
| |
| | | |
− | function setup_upload () {
| + | // The gadget is not immediately needed, so let the page load normally |
− | onUpload = true;
| + | window.setTimeout( function () { |
− | // Add an empty category bar at the end of the table containing the description, and change the onsubmit handler.
| + | var userGrp = mw.config.get('wgUserGroups'); |
− | var ip = document.getElementById ('mw-htmlform-description') || document.getElementById ('wpDestFile');
| + | var trusted = ( $.inArray( 'sysop', userGrp ) > -1 || |
− | if (!ip) {
| + | $.inArray( 'autoconfirmed', userGrp ) > -1 || |
− | ip = document.getElementById ('wpDestFile');
| + | mw.config.get( 'wgRelevantUserName' ) === mw.config.get( 'wgUserName' ) ); |
− | while (ip && ip.nodeName.toLowerCase() != 'table') ip = ip.parentNode;
| |
− | }
| |
− | if (!ip) return;
| |
− | var reupload = document.getElementById ('wpForReUpload');
| |
− | var destFile = document.getElementById ('wpDestFile');
| |
− | if ( (reupload && !!reupload.value)
| |
− | || (destFile && (destFile.disabled || destFile.readOnly)))
| |
− | return; // re-upload form...
| |
− | // Insert a table row with two fields (label and empty category bar) | |
− | var labelCell = make ('td');
| |
− | var lineCell = make ('td');
| |
− | // Create the category line
| |
− | catLine = make ('div');
| |
− | catLine.className = 'catlinks';
| |
− | catLine.id = 'catlinks';
| |
− | catLine.style.textAlign = is_rtl ? 'right' : 'left';
| |
− | // We'll be inside a table row. Make sure that we don't have margins or strange borders.
| |
− | catLine.style.margin = '0';
| |
− | catLine.style.border = 'none';
| |
− | lineCell.appendChild (catLine);
| |
− | // Create the label
| |
− | var label = null;
| |
− | if ( typeof UFUI != 'undefined'
| |
− | && typeof UIElements != 'undefined'
| |
− | && typeof UFUI.getLabel == 'function'
| |
− | )
| |
− | {
| |
− | try {
| |
− | label = UFUI.getLabel('wpCategoriesUploadLbl');
| |
− | } catch (ex) {
| |
− | label = null;
| |
− | }
| |
− | }
| |
− | if (!label) {
| |
− | labelCell.id = 'hotcatLabel';
| |
− | labelCell.appendChild (make (HotCat.categories, true));
| |
− | } else {
| |
− | labelCell.id = 'hotcatLabelTranslated';
| |
− | labelCell.appendChild (label);
| |
− | }
| |
− | labelCell.className = 'mw-label';
| |
− | labelCell.style.textAlign = 'right';
| |
− | labelCell.style.verticalAlign = 'middle';
| |
− | // Change the onsubmit handler
| |
− | var form = document.getElementById('upload') || document.getElementById('mw-upload-form');
| |
− | if (form) { | |
− | var newRow = ip.insertRow (-1);
| |
− | newRow.appendChild (labelCell);
| |
− | newRow.appendChild (lineCell);
| |
− | form.onsubmit = (function (oldSubmit) {
| |
− | return function () {
| |
− | var do_submit = true;
| |
− | if (oldSubmit) {
| |
− | if (typeof oldSubmit == 'string')
| |
− | do_submit = eval (oldSubmit);
| |
− | else if (typeof oldSubmit == 'function')
| |
− | do_submit = oldSubmit.apply (form, arguments);
| |
− | }
| |
− | if (!do_submit) return false;
| |
− | closeForm ();
| |
− | // Copy the categories
| |
− | var eb = document.getElementById ('wpUploadDescription')
| |
− | || document.getElementById ('wpDesc');
| |
− | var addedOne = false;
| |
− | for (var i = 0; i < editors.length; i++) {
| |
− | var t = editors[i].currentCategory;
| |
− | if (!t) continue ;
| |
− | var key = editors[i].currentKey;
| |
− | var new_cat = '[[' + HotCat.category_canonical + ':' + t + (key ? '|' + key : "") + ']]';
| |
− | // Only add if not already present
| |
− | var cleanedText = eb.value
| |
− | .replace(/<\!--(\s|\S)*?--\>/g, "")
| |
− | .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, "");
| |
− | if (!find_category (cleanedText, t, true)) {
| |
− | eb.value += '\n' + new_cat;
| |
− | addedOne = true;
| |
− | }
| |
− | }
| |
− | if (addedOne) {
| |
− | // Remove "subst:unc" added by Flinfo if it didn't find categories
| |
− | eb.value = eb.value.replace(/\{\{subst:unc\}\}/g, "");
| |
− | }
| |
− | return true;
| |
− | };
| |
− | }) (form.onsubmit);
| |
− | }
| |
− | }
| |
| | | |
− | var cleanedText = null; | + | switch ( mw.config.get( 'wgNamespaceNumber' ) ) { |
− | | + | case NS_CAT: |
− | function isOnPage (span) {
| + | catALot.searchmode = 'category'; |
− | if (span.firstChild.nodeType !== Node.ELEMENT_NODE) return null; | + | catALot.origin = mw.config.get( 'wgTitle' ); |
− | var catTitle = title (span.firstChild.getAttribute ('href', 2));
| + | break; |
− | if (!catTitle) return null;
| + | case -1: |
− | catTitle = catTitle.substr (catTitle.indexOf (':') + 1).replace (/_/g, ' '); | + | catALot.searchmode = { |
− | if (HotCat.blacklist && HotCat.blacklist.test (catTitle)) return null;
| + | // list of accepted special page names mapped to search mode names |
− | var result = { title : catTitle, match : ["", "", ""] };
| + | Contributions: 'contribs', |
− | if (pageText === null) return result;
| + | Listfiles: trusted ? 'listfiles' : null, |
− | if (cleanedText === null) {
| + | Prefixindex: trusted ? 'prefix' : null, |
− | cleanedText = pageText
| + | Search: 'search', |
− | .replace(/<\!--(\s|\S)*?--\>/g, "") | + | Uncategorizedimages: 'gallery' |
− | .replace(/<nowiki\>(\s|\S)*?<\/nowiki>/g, ""); | + | }[ mw.config.get( 'wgCanonicalSpecialPageName' ) ]; |
− | }
| + | break; |
− | result.match = find_category (cleanedText, catTitle, true);
| |
− | return result;
| |
| } | | } |
| | | |
− | var initialized = false; | + | if ( catALot.searchmode ) { |
− | var setupTimeout = null;
| + | var loadingLocalizations = 1; |
− | | + | var loadLocalization = function( lang, cb ) { |
− | function findByClass (scope, tag, className) {
| + | loadingLocalizations++; |
− | var result = window.jQuery(scope).find(tag + '.' + className); | + | switch ( lang ) { |
− | return (result && result.length) ? result[0] : null; | + | case 'zh-hk': |
− | }
| + | case 'zh-mo': |
− | | + | case 'zh-tw': |
− | function setup (additionalWork) {
| + | lang = 'zh-hant'; |
− | if (initialized) return;
| + | break; |
− | initialized = true;
| + | case 'zh': |
− | if (setupTimeout) {
| + | case 'zh-cn': |
− | window.clearTimeout (setupTimeout); | + | case 'zh-my': |
− | setupTimeout = null;
| + | case 'zh-sg': |
− | }
| + | lang = 'zh-hans'; |
− | // Find the category bar, or create an empty one if there isn't one. Then add -/+- links after
| + | break; |
− | // each category, and add the + link.
| |
− | catLine = catLine // Special:Upload
| |
− | || 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 = HotCat.categories;
| |
− | label.appendChild (make (HotCat.categories, true));
| |
− | catLine.appendChild (label);
| |
− | catLine.appendChild (make (':', true));
| |
− | // Insert the new category line
| |
− | var container = (hiddenCats ? hiddenCats.parentNode : document.getElementById ('catlinks'));
| |
− | if (!container) {
| |
− | container = make ('div'); | |
− | container.id = 'catlinks'; | |
− | footer.parentNode.insertBefore (container, footer.nextSibling);
| |
− | }
| |
− | container.className = 'catlinks noprint';
| |
− | container.style.display = "";
| |
− | if (!hiddenCats) {
| |
− | container.appendChild (catLine);
| |
− | } else {
| |
− | container.insertBefore (catLine, hiddenCats);
| |
| } | | } |
− | } // end if catLine exists
| |
− | if (is_rtl) catLine.dir = 'rtl';
| |
| | | |
− | // Create editors for all existing categories
| + | $.ajax( { |
| + | url: commons_url, |
| + | dataType: 'script', |
| + | data: { |
| + | title: 'MediaWiki:Gadget-Cat-a-lot.js/' + lang, |
| + | action: 'raw', |
| + | ctype: 'text/javascript', |
| + | // Allow caching for 28 days |
| + | maxage: 2419200, |
| + | smaxage: 2419200 |
| + | }, |
| + | cache: true, |
| + | success: cb, |
| + | error: cb |
| + | } ); |
| + | }; |
| + | var maybeLaunch = function() { |
| + | loadingLocalizations--; |
| | | |
− | function createEditors (line, is_hidden) {
| + | function init() { |
− | var i;
| + | $( function() { |
− | var cats = line.getElementsByTagName ('li');
| + | catALot.init(); |
− | if (cats.length > 0) {
| + | } ); |
− | newDOM = true; line = cats[0].parentNode;
| |
− | } else {
| |
− | cats = line.getElementsByTagName ('span');
| |
| } | | } |
− | // Copy cats, otherwise it'll also magically contain our added spans as it is a live collection! | + | if ( !loadingLocalizations ) |
− | var copyCats = new Array (cats.length);
| + | mw.loader.using( [ 'user' ], init, init ); |
− | for (i = 0; i < cats.length; i++) copyCats[i] = cats[i];
| + | }; |
− | var editor = null;
| |
− | for (i = 0; i < copyCats.length; i++) {
| |
− | var test = isOnPage (copyCats[i]);
| |
− | if (test !== null && test.match !== null) {
| |
− | editor = new CategoryEditor (line, copyCats[i], test.title, test.match[2], is_hidden);
| |
− | }
| |
− | }
| |
− | return copyCats.length > 0 ? copyCats[copyCats.length-1] : null;
| |
− | }
| |
| | | |
− | var lastSpan = createEditors (catLine, false); | + | if ( mw.config.get( 'wgUserLanguage' ) !== 'en' ) |
− | // Create one to add a new category
| + | loadLocalization( mw.config.get( 'wgUserLanguage' ), maybeLaunch ); |
− | var editor = new CategoryEditor(newDOM ? catLine.getElementsByTagName('ul')[0] : catLine, null, null, lastSpan !== null, false);
| + | if ( mw.config.get( 'wgContentLanguage' ) !== 'en' ) |
− | if (!onUpload) {
| + | loadLocalization( mw.config.get( 'wgContentLanguage' ), maybeLaunch ); |
− | if (pageText !== null && hiddenCats) {
| + | maybeLaunch(); |
− | if (is_rtl) hiddenCats.dir = 'rtl';
| |
− | createEditors (hiddenCats, true);
| |
− | }
| |
− | // And finally add the "multi-mode" span. (Do this at the end, otherwise it ends up in the list above.) | |
− | var enableMulti = make ('span');
| |
− | enableMulti.className = 'noprint';
| |
− | if (is_rtl) enableMulti.dir = 'rtl';
| |
− | catLine.insertBefore (enableMulti, catLine.firstChild.nextSibling);
| |
− | enableMulti.appendChild (make ('\xa0', true)); // nbsp
| |
− | multiSpan = make ('span');
| |
− | enableMulti.appendChild (multiSpan);
| |
− | multiSpan.innerHTML = '(<a>' + HotCat.addmulti + '</a>)';
| |
− | var lk = multiSpan.getElementsByTagName ('a')[0];
| |
− | lk.onclick = function (evt) {setMultiInput (); checkMultiInput (); return evtKill (evt);}; | |
− | lk.title = HotCat.multi_tooltip;
| |
− | lk.style.cursor = 'pointer';
| |
− | }
| |
− | cleanedText = null;
| |
− | if (typeof additionalWork == 'function') additionalWork();
| |
− | setupCompleted.loaded(); // Trigger signal; execute registered functions
| |
− | $('body').trigger('hotcatSetupCompleted'); | |
| } | | } |
| + | }, 400); |
| | | |
− | function setPage (json) {
| + | /** |
− | var startTime = null;
| + | * Derivative work of |
− | if (json && json.query) {
| + | * (replace "checkboxes" with cat-a-lot labels in your mind) |
− | if (json.query.pages) {
| + | */ |
− | var page = json.query.pages[conf.wgArticleId === 0 ? "-1" : "" + conf.wgArticleId];
| + | /** |
− | if (page) {
| + | * jQuery checkboxShiftClick |
− | 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
| + | * This will enable checkboxes to be checked or unchecked in a row by clicking one, holding shift and clicking another one |
− | // not on the latest revision (edit conflicts and such).
| + | * |
− | pageText = page.revisions[0]['*'];
| + | * @author Krinkle <krinklemail@gmail.com> |
− | if (page.revisions[0].timestamp) pageTime = page.revisions[0].timestamp.replace (/\D/g, "");
| + | * @license GPL v2 |
− | if (page.revisions[0].revid) pageTextRevId = page.revisions[0].revid;
| + | */ |
− | if (page.revisions.length > 1) conflictingUser = page.revisions[1].user;
| + | $.fn.catALotShiftClick = function( cb ) { |
− | }
| + | var prevCheckbox = null, |
− | if (page.lastrevid) lastRevId = page.lastrevid;
| + | $box = this; |
− | if (page.starttimestamp) startTime = page.starttimestamp.replace (/\D/g, "");
| + | // When our boxes are clicked.. |
− | pageWatched = typeof page.watched == 'string';
| + | $box.bind( 'click.catALot', function( e ) { |
− | editToken = page.edittoken;
| |
− | if (page.langlinks && (!json['query-continue'] || !json['query-continue'].langlinks)) {
| |
− | // We have interlanguage links, and we got them all.
| |
− | var re = "";
| |
− | for (var i = 0; i < page.langlinks.length; i++) {
| |
− | re += (i > 0 ? '|' : "") + page.langlinks[i].lang.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1');
| |
− | }
| |
− | if (re.length > 0) {
| |
− | interlanguageRE = new RegExp ('((^|\\n\\r?)(\\[\\[\\s*(' + re + ')\\s*:[^\\]]+\\]\\]\\s*))+$');
| |
− | }
| |
− | }
| |
| | | |
− | }
| + | // Prevent following the link and text selection |
− | }
| + | e.preventDefault(); |
− | // Siteinfo
| |
− | if (json.query.general) {
| |
− | HotCat.capitalizePageNames = (json.query.general['case'] == 'first-letter');
| |
− | if (json.query.general.time && !startTime) startTime = json.query.general.time.replace (/\D/g, "");
| |
− | }
| |
− | serverTime = startTime;
| |
− | // Userinfo
| |
− | if (json.query.userinfo && json.query.userinfo.options) {
| |
− | watchCreate = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchcreations == '1';
| |
− | watchEdit = !HotCat.dont_add_to_watchlist && json.query.userinfo.options.watchdefault == '1';
| |
− | minorEdits = json.query.userinfo.options.minordefault == 1;
| |
− | // If the user has the "All edits are minor" preference enabled, we should honor that
| |
− | // for single category changes, no matter what the site configuration is.
| |
− | if (minorEdits) HotCat.single_minor = true;
| |
− | }
| |
− | }
| |
− | }
| |
| | | |
− | function createCommitForm () {
| + | // Highlight last selected |
− | if (commitForm) return; | + | $( '#cat_a_lot_last_selected' ) |
− | var formContainer = make ('div'); | + | .removeAttr( 'id' ); |
− | formContainer.style.display = 'none';
| + | var $thisControl = $( e.target ), |
− | document.body.appendChild (formContainer);
| + | method; |
− | formContainer.innerHTML =
| + | if ( !$thisControl.hasClass( 'cat_a_lot_label' ) ) { |
− | '<form id="hotcatCommitForm" method="post" enctype="multipart/form-data" action="'
| + | $thisControl = $thisControl.parents( '.cat_a_lot_label' ); |
− | + conf.wgScript + '?title=' + encodeURIComponent (conf.wgPageName)
| |
− | + '&action=submit">'
| |
− | + '<input type="hidden" name="wpTextbox1" />'
| |
− | + '<input type="hidden" name="model" value="wikitext" />'
| |
− | + '<input type="hidden" name="format" value="text/x-wiki" />'
| |
− | + '<input type="hidden" name="wpSummary" value="" />'
| |
− | + '<input type="checkbox" name="wpMinoredit" value="1" />'
| |
− | + '<input type="checkbox" name="wpWatchthis" value="1" />'
| |
− | + '<input type="hidden" name="wpAutoSummary" value="" />'
| |
− | + '<input type="hidden" name="wpEdittime" />'
| |
− | + '<input type="hidden" name="wpStarttime" />'
| |
− | + '<input type="hidden" name="wpDiff" value="wpDiff" />'
| |
− | + '<input type="hidden" name="oldid" value="0" />'
| |
− | + '<input type="submit" name="hcCommit" value="hcCommit" />'
| |
− | + '<input type="hidden" name="wpEditToken" />'
| |
− | + '<input type="hidden" name="wpUltimateParam" value="1" />'
| |
− | + '</form>';
| |
− | commitForm = document.getElementById ('hotcatCommitForm');
| |
− | }
| |
− | | |
− | function getPage () {
| |
− | // We know we have an article here. | |
− | if (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 (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 = armorUri(url);
| |
− | s.type = 'text/javascript';
| |
− | HotCat.start = function (json) { setPage (json); setup (createCommitForm); }; | |
− | document.getElementsByTagName ('head')[0].appendChild (s);
| |
− | setupTimeout = window.setTimeout (function () {setup (createCommitForm);}, 4000); // 4 sec, just in case getting the wikitext takes longer.
| |
| } | | } |
− | }
| + | $thisControl.attr( 'id', 'cat_a_lot_last_selected' ) |
− | | + | .toggleClass( 'cat_a_lot_selected' ); |
− | function run () {
| |
− | if (HotCat.started) return; | |
− | HotCat.started = true;
| |
− | loadTrigger.register(really_run);
| |
− | }
| |
| | | |
− | function really_run () {
| + | // And one has been clicked before... |
− | initialize ();
| + | if ( prevCheckbox !== null && e.shiftKey ) { |
| + | method = $thisControl.hasClass( 'cat_a_lot_selected' ) ? 'addClass' : 'removeClass'; |
| | | |
− | if (!HotCat.upload_disabled && conf.wgNamespaceNumber === -1 && conf.wgCanonicalSpecialPageName == 'Upload' && conf.wgUserName) {
| + | // Check or uncheck this one and all in-between checkboxes |
− | setup_upload ();
| + | $box.slice( |
− | setup (function () { | + | Math.min( $box.index( prevCheckbox ), $box.index( $thisControl ) ), |
− | // Check for state restoration once the setup is done otherwise, but before signalling setup completion
| + | Math.max( $box.index( prevCheckbox ), $box.index( $thisControl ) ) + 1 |
− | if ( typeof UploadForm != 'undefined'
| + | )[ method ]( 'cat_a_lot_selected' ); |
− | && typeof UploadForm.previous_hotcat_state != 'undefined'
| |
− | && UploadForm.previous_hotcat_state !== null)
| |
− | {
| |
− | UploadForm.previous_hotcat_state = setState (UploadForm.previous_hotcat_state);
| |
− | } | |
− | });
| |
− | } else {
| |
− | if (!conf.wgIsArticle || conf.wgAction != 'view' || param('diff') !== null || param('oldid') !== null || !can_edit() || HotCat.disable()) return;
| |
− | getPage (); | |
| } | | } |
− | }
| + | // Either way, update the prevCheckbox variable to the one clicked now |
| + | prevCheckbox = $thisControl; |
| | | |
− | // Legacy stuff
| + | if ( $.isFunction( cb ) ) { |
− | | + | cb(); |
− | function closeForm () {
| |
− | // Close all open editors without redirect resolution and other asynchronous stuff. | |
− | for (var i = 0; i < editors.length; i++) {
| |
− | if (editors[i].state == CategoryEditor.OPEN) {
| |
− | editors[i].cancel();
| |
− | } else if (editors[i].state == CategoryEditor.CHANGE_PENDING) {
| |
− | editors[i].sanitizeInput ();
| |
− | var value = editors[i].text.value.split('|');
| |
− | var key = null;
| |
− | if (value.length > 1) key = value[1];
| |
− | var v = value[0].replace(/_/g, ' ').replace(/^\s+|\s+$/g, "");
| |
− | if (v.length === 0) {
| |
− | editors[i].cancel ();
| |
− | } else {
| |
− | editors[i].currentCategory = v;
| |
− | editors[i].currentKey = key;
| |
− | editors[i].currentExists = this.inputExists;
| |
− | editors[i].close ();
| |
− | }
| |
− | }
| |
| } | | } |
− | } | + | } ); |
| + | return $box; |
| + | }; |
| | | |
− | function getState () {
| + | }( jQuery, mediaWiki ) ); |
− | var result = null;
| |
− | for (var i = 0; i < editors.length; i++) {
| |
− | var text = editors[i].currentCategory;
| |
− | var key = editors[i].currentKey;
| |
− | if (text && text.length > 0) {
| |
− | if (key !== null) text += '|' + key;
| |
− | if (result === null)
| |
− | result = text;
| |
− | else
| |
− | result = result + '\n' + text;
| |
− | }
| |
− | }
| |
− | return result;
| |
− | }
| |
− | | |
− | function setState (state) {
| |
− | var cats = state.split ('\n');
| |
− | if (cats.length === 0) return null;
| |
− | if (initialized && editors.length == 1 && editors[0].isAddCategory) {
| |
− | // Insert new spans and create new editors for them.
| |
− | var newSpans = [];
| |
− | var before = editors.length == 1 ? editors[0].span : null;
| |
− | var i;
| |
− | for (i = 0; i < cats.length; i++) {
| |
− | if (cats[i].length === 0) continue;
| |
− | var cat = cats[i].split ('|');
| |
− | var key = cat.length > 1 ? cat[1] : null;
| |
− | cat = cat[0];
| |
− | var lk = make ('a'); lk.href = wikiPagePath (HotCat.category_canonical + ':' + cat);
| |
− | lk.appendChild (make (cat, true));
| |
− | lk.title = cat;
| |
− | var span = make ('span');
| |
− | span.appendChild (lk);
| |
− | if (i === 0) catLine.insertBefore (make (' ', true), before);
| |
− | catLine.insertBefore (span, before);
| |
− | if (before && i+1 < cats.length) parent.insertBefore (make (' | ', true), before);
| |
− | newSpans.push ({element: span, title: cat, 'key': key});
| |
− | }
| |
− | // And change the last one...
| |
− | if (before) {
| |
− | before.parentNode.insertBefore (make (' | ', true), before);
| |
− | }
| |
− | var editor = null;
| |
− | for (i = 0; i < newSpans.length; i++) {
| |
− | editor = new CategoryEditor (catLine, newSpans[i].element, newSpans[i].title, newSpans[i].key);
| |
− | }
| |
− | }
| |
− | return null;
| |
− | }
| |
− | | |
− | // 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 (); };
| |
− | | |
− | // Make sure we don't get conflicts with AjaxCategories (core development that should one day
| |
− | // replace HotCat).
| |
− | mw.config.set('disableAJAXCategories', true);
| |
− | | |
− | if (conf.wgCanonicalSpecialPageName !== 'Upload') {
| |
− | // Use wikipage.content hook so that HotCat reloads after VE edits (bug T103285)
| |
− | var startHotCat = function() {
| |
− | mw.hook('wikipage.content').add( function() {
| |
− | // Reset HotCat in case this is a soft reload (VE edit)
| |
− | catLine = null;
| |
− | editors = [];
| |
− | initialized = false;
| |
− | HotCat.started = false;
| |
− | run ();
| |
− | } );
| |
− | };
| |
− | } else {
| |
− | // We're running on Special:Upload, where the 'wikipage.content' hook is fired for
| |
− | // various previewed wikitext snippets, which shouldn't reload HotCat interface.
| |
− | var startHotCat = function() {
| |
− | $(document).ready(run);
| |
− | };
| |
− | }
| |
− | // We can safely trigger just after user configuration is loaded. Also start HotCat if the user module fails to load.
| |
− | // Avoid using Promise methods of mw.loader.using as those aren't supported in older
| |
− | // MediaWiki versions.
| |
− | mw.loader.using('user', startHotCat, startHotCat);
| |
− | })(jQuery, mediaWiki);
| |
| | | |
| // </nowiki> | | // </nowiki> |