r93090 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r93089‎ | r93090 | r93091 >
Date:19:03, 25 July 2011
Author:diebuche
Status:deferred (Comments)
Tags:
Comment:
* Ignore any <nowiki> or comment sections
* More natural messages
* Respect wgCaseSensitiveNamespaces
* Regex: Add possible whitespace between "[[Category:" and the category name. ( [[Category: Foo]] )
* Make nearly all functions publicly accessible
* Add "cancel all" button
* Submit on enter keypress
* Check for redirects
* Color links correctly based on existance of category page
* Add a summary of the changes done into the edit summary ('+Category:foo, -Category:Bar: Foo is not correct...')
* Add more error handlers
* Add more hooks ( afterChange/Delete/add ).
* Pass category names to the hooks
* Allow hooks to abort by returning false
* Handle sortkey correctly in all operations
* Move addCategory form below categories.
* Fix any known IE6 and IE7 bugs.
* Add more documentation
Modified paths:
  • /trunk/phase3/languages/messages/MessagesEn.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesQqq.php (modified) (history)
  • /trunk/phase3/maintenance/language/messages.inc (modified) (history)
  • /trunk/phase3/resources/Resources.php (modified) (history)
  • /trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.css (modified) (history)
  • /trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.js (modified) (history)

Diff [purge]

Index: trunk/phase3/maintenance/language/messages.inc
@@ -3479,22 +3479,21 @@
34803480 'ajax-add-category-submit',
34813481 'ajax-confirm-ok',
34823482 'ajax-confirm-title',
3483 - 'ajax-confirm-prompt',
34843483 'ajax-confirm-save',
34853484 'ajax-confirm-save-all',
34863485 'ajax-cancel',
 3486+ 'ajax-cancel-all',
34873487 'ajax-add-category-summary',
34883488 'ajax-edit-category-summary',
34893489 'ajax-remove-category-summary',
3490 - 'ajax-add-category-question',
3491 - 'ajax-edit-category-question',
3492 - 'ajax-remove-category-question',
3493 - 'ajax-confirm-actionsummary',
 3490+ 'ajax-category-question',
 3491+ 'ajax-category-and',
34943492 'ajax-error-title',
3495 - 'ajax-error-dismiss',
34963493 'ajax-remove-category-error',
34973494 'ajax-edit-category-error',
34983495 'ajax-category-already-present',
 3496+ 'ajax-category-hook-error',
 3497+ 'ajax-api-error',
34993498 ),
35003499
35013500 );
Index: trunk/phase3/languages/messages/MessagesQqq.php
@@ -4238,17 +4238,22 @@
42394239 'sqlite-no-fts' => 'Shown on Special:Version, $1 is version',
42404240
42414241 # Add categories per AJAX
4242 -'ajax-remove-category' => 'Tooltip for link to remove a category from the page, displayed after each category at the foot of a page. Refers to the specific category. "Remove this category" is also correct.',
 4242+'ajax-remove-category' => 'Tooltip for link to remove a category from the page, displayed after each category at the foot of a page.
 4243+Refers to the specific category. "Remove this category" is also correct.',
42434244 'ajax-edit-category' => 'Tooltip for the edit link displayed after each category at the foot of a page. Refers to the specific category. "Edit this category" is also correct.',
42444245 'ajax-add-category-submit' => '{{Identical|Add}}',
42454246 'ajax-confirm-ok' => '{{Identical|OK}}',
4246 -'ajax-confirm-save' => 'Submit button
4247 -
4248 -{{Identical|Save}}',
4249 -'ajax-edit-category-summary' => 'Automatic edit summary that can be copied to the summary box. $1 and $2 are both category names.',
4250 -'ajax-confirm-actionsummary' => 'This message is followed by one of the following messages; {{msg-mw|ajax-add-category-summary}}, {{msg-mw|ajax-remove-category-summary}}, {{msg-mw|ajax-edit-category-summary}}.',
 4247+'ajax-confirm-title' => 'Title for a dialog box in which the user is asked for an edit summary',
 4248+'ajax-confirm-save' => 'Submit button {{Identical|Save}}',
 4249+'ajax-confirm-save-all' => 'Submit button to save all changes',
 4250+'ajax-add-category-summary' => 'See {{msg-mw|ajax-category-question}}. $1 is a category name. This is used inside a sentence, make sure that the case is correct.',
 4251+'ajax-edit-category-summary' => 'See {{msg-mw|ajax-category-question}}. $1 and $2 are both category names. This is used inside a sentence, make sure that the case is correct.',
 4252+'ajax-remove-category-summary' => 'See {{msg-mw|ajax-category-question}}. $1 is a category name. This is used inside a sentence, make sure that the case is correct.',
 4253+'ajax-category-question' => 'Question the user is asked before submit. $1 is filled with a list of one or more of the following messages;
 4254+{{msg-mw|ajax-add-category-summary}}, {{msg-mw|ajax-remove-category-summary}}, {{msg-mw|ajax-edit-category-summary}}.
 4255+The last of the inserted messages is prefaced by {{msg-mw|ajax-category-and}}. The result would be something like:
 4256+Why do you want to remove Category:A and add Category:C ?',
 4257+'ajax-category-and' => 'Used to join multiple list items. Eg. "Edit foo, move bar and add foobar"',
42514258 'ajax-error-title' => '{{Identical|Error}}',
4252 -'ajax-error-dismiss' => '{{Identical|OK}}',
42534259 'ajax-category-already-present' => 'Error message. $1 is the category name',
4254 -
42554260 );
Index: trunk/phase3/languages/messages/MessagesEn.php
@@ -4607,24 +4607,21 @@
46084608 'ajax-add-category-submit' => 'Add',
46094609 'ajax-confirm-ok' => 'OK',
46104610 'ajax-confirm-title' => 'Confirm action',
4611 -'ajax-confirm-prompt' => 'You can provide an edit summary below.
4612 -Click "Save" to save your edit.',
46134611 'ajax-confirm-save' => 'Save',
46144612 'ajax-confirm-save-all' => 'Save all changes',
4615 -'ajax-cancel' => 'Cancel edits',
4616 -'ajax-add-category-summary' => 'Add category "$1"',
4617 -'ajax-edit-category-summary' => 'Change category "$1" to "$2"',
4618 -'ajax-remove-category-summary' => 'Remove category "$1"',
4619 -'ajax-add-category-question' => 'Why do you want to add category "$1"?',
4620 -'ajax-edit-category-question' => 'Why do you want to change category "$1" to "$2"?',
4621 -'ajax-remove-category-question' => 'Why do you want to remove category "$1"?',
4622 -'ajax-confirm-actionsummary' => 'Action to take:',
 4613+'ajax-cancel' => 'Cancel edit',
 4614+'ajax-cancel-all' => 'Cancel all changes',
 4615+'ajax-add-category-summary' => 'add category "$1"',
 4616+'ajax-edit-category-summary' => 'change category "$1" to "$2"',
 4617+'ajax-remove-category-summary' => 'remove category "$1"',
 4618+'ajax-category-question' => '<strong>Why</strong> do you want to $1?',
 4619+'ajax-category-and' => ' and ',
46234620 'ajax-error-title' => 'Error',
4624 -'ajax-error-dismiss' => 'OK',
4625 -'ajax-remove-category-error' => 'It was not possible to remove this category.
 4621+'ajax-remove-category-error' => 'It was not possible to remove category "$1".
46264622 This usually occurs when the category has been added to the page in a template.',
4627 -'ajax-edit-category-error' => 'It was not possible to edit this category.
 4623+'ajax-edit-category-error' => 'It was not possible to edit category "$1".
46284624 This usually occurs when the category has been added to the page in a template.',
4629 -'ajax-category-already-present' => 'This page already belongs to the category $1',
4630 -
 4625+'ajax-category-already-present' => 'This page already belongs to the category "$1"',
 4626+'ajax-category-hook-error' => 'A local function prevented the changes from being saved',
 4627+'ajax-api-error' => 'The API returned an error: $1: $2',
46314628 );
Index: trunk/phase3/resources/Resources.php
@@ -501,23 +501,22 @@
502502 'ajax-edit-category',
503503 'ajax-add-category-submit',
504504 'ajax-confirm-ok',
505 - 'ajax-confirm-prompt',
506505 'ajax-confirm-title',
507506 'ajax-confirm-save',
508507 'ajax-confirm-save-all',
509508 'ajax-cancel',
 509+ 'ajax-cancel-all',
510510 'ajax-add-category-summary',
511 - 'ajax-remove-category-summary',
512511 'ajax-edit-category-summary',
513 - 'ajax-add-category-question',
514 - 'ajax-edit-category-question',
515 - 'ajax-remove-category-question',
516 - 'ajax-confirm-actionsummary',
 512+ 'ajax-remove-category-summary',
 513+ 'ajax-category-question',
 514+ 'ajax-category-and',
517515 'ajax-error-title',
518 - 'ajax-error-dismiss',
519516 'ajax-remove-category-error',
520517 'ajax-edit-category-error',
521518 'ajax-category-already-present',
 519+ 'ajax-category-hook-error',
 520+ 'ajax-api-error',
522521 ),
523522 ),
524523 'mediawiki.page.ajaxCategories.init' => array(
Index: trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.js
@@ -1,12 +1,13 @@
2 -// TODO
3 -//
4 -// * The edit summary should contain the added/removed category name too.
5 -// Something like: "Category:Foo added. Reason"
6 -// Requirement: Be able to get msg with lang option.
7 -// * Handle uneditable cats. Needs serverside changes!
8 -// * Handle normal redirects
9 -// * Enter to submit
10 -
 2+/**
 3+ * mediaWiki.page.ajaxCategories
 4+ *
 5+ * @author Michael Dale, 2009
 6+ * @author Leo Koppelkamm, 2011
 7+ * @since 1.18
 8+ *
 9+ * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces, wgUserGroups),
 10+ * mw.util.wikiGetlink, mw.user.getId
 11+ */
1112 ( function( $ ) {
1213
1314 /* Local scope */
@@ -25,33 +26,16 @@
2627 * @param string category name.
2728 * @return string Valid URL
2829 */
29 - catLink = function( cat ) {
 30+ catUrl = function( cat ) {
3031 return mw.util.wikiGetlink( new mw.Title( cat, catNsId ) );
3132 };
3233
33 -mw.ajaxCategories = function( options ) {
34 - //Save scope in shortcut
35 - var aC = this;
36 -
37 - // TODO grab these out of option object.
38 -
39 - var catLinkWrapper = '<li/>';
40 - var $container = $( '.catlinks' );
41 - var $containerNormal = $( '#mw-normal-catlinks' );
42 -
43 - var categoryLinkSelector = '#mw-normal-catlinks li a:not(.icon)';
44 - var _request;
45 -
46 - var _saveAllButton;
47 - var _cancelAllButton;
48 - var _multiEdit = $.inArray( 'user', mw.config.get( 'wgUserGroups' ) ) !== -1;
49 -
5034 /**
5135 * Helper function for $.fn.suggestion
5236 *
5337 * @param string Query string.
5438 */
55 - _fetchSuggestions = function( query ) {
 39+ fetchSuggestions = function( query ) {
5640 var _this = this;
5741 // ignore bad characters, they will be stripped out
5842 var catName = clean( $( this ).val() );
@@ -78,42 +62,188 @@
7963 $( _this ).suggestions( 'suggestions', titleArr );
8064 }
8165 } );
82 - //TODO
8366 _request = request;
8467 };
 68+
 69+ /**
 70+ * Replace <nowiki> and comments with unique keys
 71+ */
 72+ replaceNowikis = function( text, id, array ) {
 73+ var matches = text.match( /(<nowiki\>[\s\S]*?<\/nowiki>|<\!--[\s\S]*?--\>)/g );
 74+ for ( var i = 0; matches && i < matches.length; i++ ) {
 75+ array[i] = matches[i];
 76+ text = text.replace( matches[i], id + i + '-' );
 77+ }
 78+ return text;
 79+ };
 80+
 81+ /**
 82+ * Restore <nowiki> and comments from unique keys
 83+ */
 84+ restoreNowikis = function( text, id, array ) {
 85+ for ( var i = 0; i < array.length; i++ ) {
 86+ text = text.replace( id + i + '-', array[i] );
 87+ }
 88+ return text;
 89+ };
 90+
 91+ /**
 92+ * Makes regex string caseinsensitive.
 93+ * Useful when 'i' flag can't be used.
 94+ * Return stuff like [Ff][Oo][Oo]
 95+ * @param string Regex string.
 96+ * @return string Processed regex string
 97+ */
 98+ makeCaseInsensitive = function( string ) {
 99+ if ( $.inArray( 14, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) + 1 ) {
 100+ return string;
 101+ }
 102+ var newString = '';
 103+ for ( var i=0; i < string.length; i++ ) {
 104+ newString += '[' + string.charAt( i ).toUpperCase() + string.charAt( i ).toLowerCase() + ']';
 105+ }
 106+ return newString;
 107+ };
 108+
 109+ /**
 110+ * Build a regex that matches legal invocations
 111+ * of the passed category.
 112+ * @param string category.
 113+ * @param boolean Match one following linebreak as well?
 114+ * @return Regex
 115+ */
 116+ buildRegex = function( category, matchLineBreak ) {
 117+ var categoryNSFragment = '';
 118+ $.each( mw.config.get( 'wgNamespaceIds' ), function( name, id ) {
 119+ if ( id == 14 ) {
 120+ // The parser accepts stuff like cATegORy,
 121+ // we need to do the same
 122+ // ( Well unless we have wgCaseSensitiveNamespaces, but that's being checked for )
 123+ categoryNSFragment += '|' + makeCaseInsensitive ( $.escapeRE( name ) );
 124+ }
 125+ } );
 126+ categoryNSFragment = categoryNSFragment.substr( 1 ); // Remove leading pipe
85127
 128+ // Build the regex
 129+ var titleFragment = $.escapeRE( category ).replace( /( |_)/g, '[ _]' );
 130+
 131+ firstChar = titleFragment.charAt( 0 );
 132+ firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
 133+ titleFragment = firstChar + titleFragment.substr( 1 );
 134+ var categoryRegex = '\\[\\[(' + categoryNSFragment + '):' + '[ _]*' +titleFragment + '(\\|[^\\]]*)?\\]\\]';
 135+ if ( matchLineBreak ) {
 136+ categoryRegex += '[ \\t\\r]*\\n?';
 137+ }
 138+ return new RegExp( categoryRegex, 'g' );
 139+ };
 140+
 141+
 142+mw.ajaxCategories = function( options ) {
 143+ //Save scope in shortcut
 144+ var that = this, _request, _saveAllButton, _cancelAllButton, _addContainer, defaults;
 145+
 146+ defaults = {
 147+ catLinkWrapper : '<li/>',
 148+ $container : $( '.catlinks' ),
 149+ $containerNormal : $( '#mw-normal-catlinks' ),
 150+ categoryLinkSelector : 'li a:not(.icon)',
 151+ multiEdit : $.inArray( 'user', mw.config.get( 'wgUserGroups' ) ) + 1,
 152+ resolveRedirects : true
 153+ };
 154+ // merge defaults and options, without modifying defaults */
 155+ options = $.extend( {}, defaults, options );
 156+
86157 /**
87158 * Insert a newly added category into the DOM
88159 *
89160 * @param string category name.
90161 * @return jQuery object
91162 */
92 - _createCatDOM = function( cat ) {
 163+ this.createCatLink = function( cat ) {
93164 // User can implicitely state a sort key.
94165 // Remove before display
95 - cat = cat.replace(/\|.*/, '');
 166+ cat = cat.replace(/\|.*/, '' );
96167
97168 // strip out bad characters
98169 cat = clean ( cat );
99170
100 - if ( $.isEmpty( cat ) || aC.containsCat( cat ) ) {
 171+ if ( $.isEmpty( cat ) || that.containsCat( cat ) ) {
101172 return;
102173 }
103174
104 - var $catLinkWrapper = $( catLinkWrapper );
 175+ var $catLinkWrapper = $( options.catLinkWrapper );
105176 var $anchor = $( '<a/>' ).append( cat );
106177 $catLinkWrapper.append( $anchor );
107 - $anchor.attr( { target: "_blank", href: catLink( cat ) } );
 178+ $anchor.attr( { target: "_blank", href: catUrl( cat ) } );
108179
109 - _createCatButtons( $anchor.get(0) );
 180+ _createCatButtons( $anchor );
110181
111182 return $anchor;
112183 };
113184
114 - _makeSuggestionBox = function( prefill, callback, buttonVal ) {
 185+ /**
 186+ * Takes a category link element
 187+ * and strips all data from it.
 188+ *
 189+ * @param jQuery object
 190+ */
 191+ this.resetCatLink = function( $link, del, dontRestoreText ) {
 192+ $link.removeClass( 'mw-removed-category mw-added-category mw-changed-category' );
 193+ var data = $link.data();
 194+
 195+ if ( typeof data.stashIndex == "number" ) {
 196+ _removeStashItem( data.stashIndex );
 197+ }
 198+ if ( del ) {
 199+ $link.parent.remove();
 200+ return;
 201+ }
 202+ if ( data.origCat && !dontRestoreText ) {
 203+ $link.text( data.origCat );
 204+ $link.attr( 'href', catUrl( data.origCat ) );
 205+ }
 206+
 207+ $link.removeData();
 208+
 209+ //Readd static.
 210+ $link.data({
 211+ saveButton : data.saveButton,
 212+ deleteButton: data.deleteButton,
 213+ editButton : data.editButton
 214+ });
 215+ };
 216+
 217+ /**
 218+ * Reset all data from the category links and the stash.
 219+ * @param Boolean del Delete any category links with .mw-removed-category
 220+ */
 221+ this.resetAll = function( del ) {
 222+ var $links = options.$container.find( options.categoryLinkSelector ), $del = $();
 223+ if ( del ) {
 224+ $del = $links.filter( '.mw-removed-category' ).parent();
 225+ }
 226+
 227+ $links.each( function() {
 228+ that.resetCatLink( $( this ), false, del );
 229+ });
 230+
 231+ $del.remove();
 232+
 233+ if ( !options.$container.find( '#mw-hidden-catlinks li' ).length ) {
 234+ options.$container.find( '#mw-hidden-catlinks' ).remove();
 235+ }
 236+ };
 237+
 238+ /**
 239+ * Create a suggestion box for use in edit/add dialogs
 240+ * @param str prefill Prefill input
 241+ * @param function callback on submit
 242+ * @param str buttonVal Button text
 243+ */
 244+ this._makeSuggestionBox = function( prefill, callback, buttonVal ) {
115245 // Create add category prompt
116246 var promptContainer = $( '<div class="mw-addcategory-prompt"/>' );
117 - var promptTextbox = $( '<input type="text" size="45" class="mw-addcategory-input"/>' );
 247+ var promptTextbox = $( '<input type="text" size="30" class="mw-addcategory-input"/>' );
118248 if ( prefill !== '' ) {
119249 promptTextbox.val( prefill );
120250 }
@@ -121,9 +251,11 @@
122252 addButton.val( buttonVal );
123253
124254 addButton.click( callback );
125 -
 255+ promptTextbox.keyup( function( e ) {
 256+ if ( e.keyCode == 13 ) addButton.click();
 257+ });
126258 promptTextbox.suggestions( {
127 - 'fetch':_fetchSuggestions,
 259+ 'fetch': fetchSuggestions,
128260 'cancel': function() {
129261 var req = _request;
130262 // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of "unknown" for typeof
@@ -147,8 +279,8 @@
148280 *
149281 * @return array Array of all categories
150282 */
151 - _getCats = function() {
152 - return $container.find( categoryLinkSelector ).map( function() { return $.trim( $( this ).text() ); } );
 283+ this.getCats = function() {
 284+ return options.$container.find( options.categoryLinkSelector ).map( function() { return $.trim( $( this ).text() ); } );
153285 };
154286
155287 /**
@@ -157,31 +289,32 @@
158290 * @return boolean True for exists
159291 */
160292 this.containsCat = function( cat ) {
161 - return _getCats().filter( function() { return $.ucFirst(this) == $.ucFirst(cat); } ).length !== 0;
 293+ return that.getCats().filter( function() { return $.ucFirst( this ) == $.ucFirst( cat ); } ).length !== 0;
162294 };
163295
164296 /**
165297 * This gets called by all action buttons
166298 * Displays a dialog to confirm the action
167 - * Afterwords do the actual edit
 299+ * Afterwards do the actual edit
168300 *
169301 * @param function fn text-modifying function
170302 * @param string actionSummary Changes done
 303+ * @param string shortSummary Changes, short version
171304 * @param function fn doneFn callback after everything is done
172305 * @return boolean True for exists
173306 */
174 - _confirmEdit = function( fn, actionSummary, doneFn, $link, action ) {
 307+ this._confirmEdit = function( fn, actionSummary, shortSummary, doneFn, $link, action ) {
175308 // Check whether to use multiEdit mode
176 - if ( _multiEdit && action != 'all' ) {
 309+ if ( options.multiEdit && action != 'all' ) {
177310 // Stash away
178 - $link.data('stashIndex', _stash.fns.length );
 311+ $link.data( 'stashIndex', _stash.fns.length );
 312+ $link.data( 'summary', actionSummary );
179313 _stash.summaries.push( actionSummary );
 314+ _stash.shortSum.push( shortSummary );
180315 _stash.fns.push( fn );
181316
182 -
183 - //TODO add Cancel button
184317 _saveAllButton.show();
185 - //_cancelAllButton.show();
 318+ _cancelAllButton.show();
186319
187320 // This only does visual changes
188321 doneFn( true );
@@ -193,17 +326,9 @@
194327 dialog.addClass( 'mw-ajax-confirm-dialog' );
195328 dialog.attr( 'title', mw.msg( 'ajax-confirm-title' ) );
196329
197 - // Intro text.
198 - var confirmIntro = $( '<p/>' );
199 - confirmIntro.text( mw.msg( 'ajax-confirm-prompt' ) );
200 - dialog.append( confirmIntro );
201 -
202330 // Summary of the action to be taken
203331 var summaryHolder = $( '<p/>' );
204 - var summaryLabel = $( '<strong/>' );
205 - summaryLabel.text( mw.msg( 'ajax-confirm-actionsummary' ) + " " );
206 - summaryHolder.text( actionSummary );
207 - summaryHolder.prepend( summaryLabel );
 332+ summaryHolder.html( mw.msg( 'ajax-category-question', actionSummary ) );
208333 dialog.append( summaryHolder );
209334
210335 // Reason textbox.
@@ -216,15 +341,15 @@
217342 submitButton.val( mw.msg( 'ajax-confirm-save' ) );
218343
219344 var submitFunction = function() {
220 - _addProgressIndicator( dialog );
221 - _doEdit(
 345+ that._addProgressIndicator( dialog );
 346+ that._doEdit(
222347 mw.config.get( 'wgPageName' ),
223348 fn,
224 - reasonBox.val(),
 349+ shortSummary + ': ' + reasonBox.val(),
225350 function() {
226351 doneFn();
227352 dialog.dialog( 'close' );
228 - _removeProgressIndicator( dialog );
 353+ that._removeProgressIndicator( dialog );
229354 }
230355 );
231356 };
@@ -239,6 +364,11 @@
240365
241366 $( '#catlinks' ).prepend( dialog );
242367 dialog.dialog( dialogOptions );
 368+
 369+ // Close on enter
 370+ dialog.keyup( function( e ) {
 371+ if ( e.keyCode == 13 ) submitFunction();
 372+ });
243373 };
244374
245375 /**
@@ -246,48 +376,60 @@
247377 * this is called when the user clicks "save all"
248378 * Combines the summaries and edit functions
249379 */
250 - _handleStashedCategories = function() {
251 - // Save fns
252 - fns = _stash.fns;
 380+ this._handleStashedCategories = function() {
 381+ var summary = '', fns = _stash.fns;
253382
254 - //TODO Add spaces in msg
255 - var summary = _stash.summaries.join();
256 - if ( summary == '' ) {
 383+ // Remove "holes" in array
 384+ summary = $.grep( _stash.summaries, function( n, i ) {
 385+ return ( n );
 386+ });
 387+ if ( summary.length < 1 ) {
 388+ // Nothing to do here.
257389 _saveAllButton.hide();
 390+ _cancelAllButton.hide();
258391 return;
 392+ } else if ( summary.length == 1 ) {
 393+ summary = summary.pop();
 394+ } else {
 395+ var lastSummary = summary.pop();
 396+ summary = summary.join( ', ');
 397+ summary += mw.msg( 'ajax-category-and' ) + lastSummary;
 398+ summary = summary.substring( 0, summary.length - 2 );
259399 }
 400+ // Remove "holes" in array
 401+ summaryShort = $.grep( _stash.shortSum, function( n,i ) {
 402+ return ( n );
 403+ });
 404+ summaryShort = summaryShort.join( ', ' );
 405+
260406 var combinedFn = function( oldtext ) {
261407 // Run the text through all action functions
262408 newtext = oldtext;
263409 for ( var i = 0; i < fns.length; i++ ) {
264 - if ( $.isfunction( fns[i] ) ) {
 410+ if ( $.isFunction( fns[i] ) ) {
265411 newtext = fns[i]( newtext );
 412+ if ( newtext === false ) {
 413+ return false;
 414+ }
266415 }
267416 }
268417 return newtext;
269418 };
270 - var doneFn = _resetToActual;
 419+ var doneFn = function() { that.resetAll( true ); };
271420
272 - _confirmEdit( combinedFn, summary, doneFn, '', 'all' );
 421+ that._confirmEdit( combinedFn, summary, shortSummary, doneFn, '', 'all' );
273422 };
274423
275 - _resetToActual = function() {
276 - //Remove saveAllButton
277 - _saveAllButton.hide();
278 - _cancelAllButton.hide();
279 -
280 - // Clean stash
281 - _stash.fns = [];
282 - _stash.summaries = [];
283 -
284 - // TODO
285 - $container.find('.mw-removed-category').parent().remove();
286 - // Any link with $link.css('text-decoration', 'line-through');
287 - // needs to be removed
288 -
289 - };
290 -
291 - _doEdit = function( page, fn, summary, doneFn ) {
 424+ /**
 425+ * Do the actual edit.
 426+ * Gets token & text from api, runs it through fn
 427+ * and saves it with summary.
 428+ * @param str page Pagename
 429+ * @param function fn edit function
 430+ * @param str summary
 431+ * @param str doneFn Callback after all is done
 432+ */
 433+ this._doEdit = function( page, fn, summary, doneFn ) {
292434 // Get an edit token for the page.
293435 var getTokenVars = {
294436 'action':'query',
@@ -298,7 +440,7 @@
299441 'format':'json'
300442 };
301443
302 - $.get( mw.util.wikiScript( 'api' ), getTokenVars,
 444+ $.post( mw.util.wikiScript( 'api' ), getTokenVars,
303445 function( reply ) {
304446 var infos = reply.query.pages;
305447 $.each(
@@ -308,9 +450,17 @@
309451 var timestamp = data.revisions[0].timestamp;
310452 var oldText = data.revisions[0]['*'];
311453
 454+ // Replace all nowiki and comments with unique keys
 455+ var key = mw.user.generateId();
 456+ var nowiki = [];
 457+ oldText = replaceNowikis( oldText, key, nowiki );
 458+
 459+ // Then do the changes
312460 var newText = fn( oldText );
313 -
314461 if ( newText === false ) return;
 462+
 463+ // And restore them back
 464+ newText = restoreNowikis( newText, key, nowiki );
315465
316466 var postEditVars = {
317467 'action':'edit',
@@ -322,241 +472,245 @@
323473 'format':'json'
324474 };
325475
326 - $.post( mw.util.wikiScript( 'api' ), postEditVars, doneFn, 'json' );
 476+ $.post( mw.util.wikiScript( 'api' ), postEditVars, doneFn, 'json' )
 477+ .error( function( xhr, text, error ) {
 478+ _showError( mw.msg( 'ajax-api-error', text, error ) );
 479+ });
327480 }
328481 );
329482 }
330 - , 'json' );
 483+ , 'json' ).error( function( xhr, text, error ) {
 484+ _showError( mw.msg( 'ajax-api-error', text, error ) );
 485+ });
331486 };
332 -
333487 /**
334488 * Append spinner wheel to element
335489 * @param DOMObject element.
336490 */
337 - _addProgressIndicator = function( elem ) {
338 - var indicator = $( '<div/>' );
339 -
340 - indicator.addClass( 'mw-ajax-loader' );
341 -
342 - elem.append( indicator );
 491+ this._addProgressIndicator = function( elem ) {
 492+ elem.append( $( '<div/>' ).addClass( 'mw-ajax-loader' ) );
343493 };
344494
345495 /**
346496 * Find and remove spinner wheel from inside element
347497 * @param DOMObject parent element.
348498 */
349 - _removeProgressIndicator = function( elem ) {
 499+ this._removeProgressIndicator = function( elem ) {
350500 elem.find( '.mw-ajax-loader' ).remove();
351501 };
352 -
 502+
353503 /**
354 - * Makes regex string caseinsensitive.
355 - * Useful when 'i' flag can't be used.
356 - * Return stuff like [Ff][Oo][Oo]
357 - * @param string Regex string.
358 - * @return string Processed regex string
 504+ * Checks the API whether the category in question is a redirect.
 505+ * Also returns existance info ( to color link red/blue )
 506+ * @param string category.
 507+ * @param function callback
359508 */
360 - _makeCaseInsensitive = function( string ) {
361 - var newString = '';
362 - for (var i=0; i < string.length; i++) {
363 - newString += '[' + string[i].toUpperCase() + string[i].toLowerCase() + ']';
364 - }
365 - return newString;
366 - };
367 - _buildRegex = function( category ) {
368 - // Build a regex that matches legal invocations of that category.
369 - var categoryNSFragment = '';
370 - $.each( mw.config.get( 'wgNamespaceIds' ), function( name, id ) {
371 - if ( id == 14 ) {
372 - // The parser accepts stuff like cATegORy,
373 - // we need to do the same
374 - categoryNSFragment += '|' + _makeCaseInsensitive ( $.escapeRE(name) );
375 - }
376 - } );
377 - categoryNSFragment = categoryNSFragment.substr( 1 ); // Remove leading |
378 -
379 - // Build the regex
380 - var titleFragment = $.escapeRE(category);
381 -
382 - firstChar = category.charAt( 0 );
383 - firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
384 - titleFragment = firstChar + category.substr( 1 );
385 - var categoryRegex = '\\[\\[(' + categoryNSFragment + '):' + titleFragment + '(\\|[^\\]]*)?\\]\\]';
386 -
387 - return new RegExp( categoryRegex, 'g' );
388 - };
389 -
390 - _handleEditLink = function( e ) {
391 - e.preventDefault();
392 - var $this = $( this );
393 - var $link = $this.parent().find( 'a:not(.icon)' );
394 - var category = $link.text();
395 -
396 - var $input = _makeSuggestionBox( category,
397 - _handleCategoryEdit,
398 - _multiEdit ? mw.msg( 'ajax-confirm-ok' ) : mw.msg( 'ajax-confirm-save' )
399 - );
400 - $link.after( $input ).hide();
401 - $link.data('editButton').hide();
402 - $link.data('deleteButton').unbind('click').click( function() {
403 - $input.remove();
404 - $link.show();
405 - $link.data('editButton').show();
406 - $( this ).unbind('click').click( _handleDeleteLink );
407 - });
408 - };
409 -
410 - _handleAddLink = function( e ) {
411 - e.preventDefault();
412 -
413 - $container.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).toggle();
414 - };
415 -
416 - _handleDeleteLink = function( e ) {
417 - var $this = $( this );
418 - var $link = $this.parent().find( 'a:not(.icon)' );
419 - var category = $link.text();
420 -
421 - if ( $link.hasClass('mw-added-category') ) {
422 - // We're just cancelling the addition
423 - _removeStashItem ( $link );
424 -
425 - $link.parent().remove();
 509+ this._resolveRedirects = function( category, callback ) {
 510+ if ( !options.resolveRedirects ) {
 511+ callback( category );
426512 return;
427513 }
 514+ var queryVars = {
 515+ 'action':'query',
 516+ 'titles': new mw.Title( category, catNsId ).toString(),
 517+ 'redirects':'',
 518+ 'format' : 'json'
 519+ };
428520
429 - var categoryRegex = _buildRegex( category );
430 -
431 - var summary = mw.msg( 'ajax-remove-category-summary', category );
432 -
433 - _confirmEdit(
434 - function( oldText ) {
435 - newText = _runHooks ( oldText, 'beforeDelete' );
436 - //TODO Cleanup whitespace safely?
437 - var newText = newText.replace( categoryRegex, '' );
438 -
439 - if ( newText == oldText ) {
440 - var error = mw.msg( 'ajax-remove-category-error' );
441 - _showError( error );
442 - _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
443 - $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
444 - return false;
 521+ $.get( mw.util.wikiScript( 'api' ), queryVars,
 522+ function( reply ) {
 523+ var redirect = reply.query.redirects;
 524+ if ( redirect ) {
 525+ category = new mw.Title( redirect[0].to )._name;
445526 }
446 -
447 - return newText;
448 - },
449 - summary,
450 - function( unsaved ) {
451 - if ( unsaved ) {
452 - //TODO Make revertable
453 - $link.addClass('mw-removed-category');
454 - } else {
455 - $this.parent().remove();
456 - }
457 - },
458 - $link,
459 - 'delete'
460 - );
 527+ callback( category, !reply.query.pages[-1] );
 528+ }
 529+ , 'json' );
461530 };
 531+
 532+ /**
 533+ * Handle add category submit. Not to be called directly
 534+ */
 535+ this._handleAddLink = function( e ) {
 536+ var $this = $( this ), $link = $();
462537
463 - _handleCategoryAdd = function( e ) {
464 - var $this = $( this );
465 -
466538 // Grab category text
467539 var category = $this.parent().find( '.mw-addcategory-input' ).val();
468540 category = $.ucFirst( category );
469541
470 - var $link = _createCatDOM( category );
471 -
472 - if ( aC.containsCat(category) ) {
 542+ // Resolve redirects
 543+ that._resolveRedirects( category, function( resolvedCat, exists ) {
 544+ that.handleCategoryAdd( $link, resolvedCat, false, exists );
 545+ } );
 546+ };
 547+ /**
 548+ * Execute or queue an category add
 549+ */
 550+ this.handleCategoryAdd = function( $link, category, noAppend, exists ) {
 551+ if ( !$link.length ) {
 552+ $link = that.createCatLink( category );
 553+ }
 554+ // Mark red if missing
 555+ $link.toggleClass( 'new', exists === false );
 556+
 557+ // Handle sortkey
 558+ var arr = category.split( '|' ), sortkey = '';
 559+
 560+ if ( arr.length > 1 ) {
 561+ category = arr.shift();
 562+ sortkey = '|' + arr.join( '|' );
 563+ if ( sortkey == '|' ) sortkey = '';
 564+ }
 565+
 566+ //Replace underscores
 567+ category = category.replace(/_/g, ' ' );
 568+
 569+ if ( that.containsCat( category ) ) {
473570 _showError( mw.msg( 'ajax-category-already-present', category ) );
474571 return;
475572 }
476 - var appendText = "\n[[" + new mw.Title( category, catNsId ) + "]]\n";
 573+ var catFull = new mw.Title( category, catNsId ).toString().replace(/_/g, ' ' );
 574+ var appendText = "\n[[" + catFull + sortkey + "]]\n";
477575 var summary = mw.msg( 'ajax-add-category-summary', category );
478 -
479 - _confirmEdit(
 576+ var shortSummary = '+[[' + catFull + ']]';
 577+ that._confirmEdit(
480578 function( oldText ) {
481 - newText = _runHooks ( oldText, 'beforeAdd' );
482 - return newText + appendText;
 579+ newText = _runHooks ( oldText, 'beforeAdd', category );
 580+ newText = newText + appendText;
 581+ return _runHooks ( newText, 'afterAdd', category );
483582 },
484583 summary,
 584+ shortSummary,
485585 function( unsaved ) {
486 - $container.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).toggle();
487 - $container.find( '#mw-normal-catlinks ul' ).append( $link.parent() );
 586+ if ( !noAppend ) {
 587+ options.$container.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).children( 'input' ).hide();
 588+ options.$container.find( '#mw-normal-catlinks ul' ).append( $link.parent() );
 589+ } else {
 590+ // Remove input box & button
 591+ $link.data( 'deleteButton' ).click();
 592+
 593+ // Update link text and href
 594+ $link.show().text( category ).attr( 'href', catUrl( category ) );
 595+ }
488596 if ( unsaved ) {
489597 $link.addClass( 'mw-added-category' );
490598 }
 599+ $( '.mw-ajax-addcategory' ).click();
491600 },
492601 $link,
493602 'add'
494603 );
495604 };
 605+ this._createEditInterface = function( e ) {
 606+ var $this = $( this ),
 607+ $link = $this.data( 'link' ),
 608+ category = $link.text();
 609+ var $input = that._makeSuggestionBox( category,
 610+ that._handleEditLink,
 611+ options.multiEdit ? mw.msg( 'ajax-confirm-ok' ) : mw.msg( 'ajax-confirm-save' )
 612+ );
 613+ $link.after( $input ).hide();
 614+ $input.find( '.mw-addcategory-input' ).focus();
 615+ $link.data( 'editButton' ).hide();
 616+ $link.data( 'deleteButton' ).unbind( 'click' ).click( function() {
 617+ $input.remove();
 618+ $link.show();
 619+ $link.data( 'editButton' ).show();
 620+ $( this ).unbind( 'click' ).click( that._handleDeleteLink )
 621+ .attr( 'title', mw.msg( 'ajax-remove-category' ));
 622+ }).attr( 'title', mw.msg( 'ajax-cancel' ));
 623+ };
 624+
 625+ /**
 626+ * Handle edit category submit. Not to be called directly
 627+ */
 628+ this._handleEditLink = function( e ) {
 629+ var $this = $( this ),
 630+ $link = $this.parent().parent().find( 'a:not(.icon)' ),
 631+ categoryNew, sortkey = '';
 632+
 633+ // Grab category text
 634+ categoryNew = $this.parent().find( '.mw-addcategory-input' ).val();
 635+ categoryNew = $.ucFirst( categoryNew.replace(/_/g, ' ' ) );
 636+
 637+ // Strip sortkey
 638+ var arr = categoryNew.split( '|' );
 639+ if ( arr.length > 1 ) {
 640+ categoryNew = arr.shift();
 641+ sortkey = '|' + arr.join( '|' );
 642+ }
496643
497 - _handleCategoryEdit = function( e ) {
498 - var $this = $( this );
 644+ // Grab text
 645+ var added = $link.hasClass( 'mw-added-category' );
 646+ that.resetCatLink ( $link );
 647+ var category = $link.text();
499648
500 - // Grab category text
501 - var categoryNew = $this.parent().find( '.mw-addcategory-input' ).val();
502 - categoryNew = $.ucFirst( categoryNew );
 649+ // Check for dupes ( exluding itself )
 650+ if ( category != categoryNew && that.containsCat( categoryNew ) ) {
 651+ $link.data( 'deleteButton' ).click();
 652+ return;
 653+ }
503654
504 - var $link = $this.parent().parent().find( 'a:not(.icon)' );
505 - if ( $link.hasClass('mw-removed-category') ) {
506 - _removeStashItem ( $link );
507 - $link.removeClass( 'mw-removed-category' );
 655+ // Resolve redirects
 656+ that._resolveRedirects( categoryNew, function( resolvedCat, exists ) {
 657+ that.handleCategoryEdit( $link, category, resolvedCat, sortkey, exists, added );
 658+ });
 659+ };
 660+ /**
 661+ * Execute or queue an category edit
 662+ */
 663+ this.handleCategoryEdit = function( $link, category, categoryNew, sortkeyNew, exists, added ) {
 664+ // Category add needs to be handled differently
 665+ if ( added ) {
 666+ // Pass sortkey back
 667+ that.handleCategoryAdd( $link, categoryNew + sortkeyNew, true );
 668+ return;
508669 }
509 - if ( $link.data( 'origCat' ) ) {
510 - var category = $link.data( 'origCat' );
511 - _removeStashItem ( $link );
512 - } else {
513 - var category = $link.text();
514 - }
515 -
516 -
517670 // User didn't change anything.
518 - if ( category == categoryNew ) {
519 - $link.data('deleteButton').click();
 671+ if ( category == categoryNew + sortkeyNew ) {
 672+ $link.data( 'deleteButton' ).click();
520673 return;
521674 }
522 - categoryRegex = _buildRegex( category );
523 -
 675+ // Mark red if missing
 676+ $link.toggleClass( 'new', exists === false );
 677+
 678+ categoryRegex = buildRegex( category );
 679+
524680 var summary = mw.msg( 'ajax-edit-category-summary', category, categoryNew );
525 -
526 - _confirmEdit(
 681+ var shortSummary = '[[' + new mw.Title( category, catNsId ) + ']] -> [[' + new mw.Title( categoryNew, catNsId ) + ']]';
 682+ that._confirmEdit(
527683 function( oldText ) {
528 - newText = _runHooks ( oldText, 'beforeChange' );
 684+ newText = _runHooks ( oldText, 'beforeChange', category, categoryNew );
529685
530686 var matches = newText.match( categoryRegex );
531687
532688 //Old cat wasn't found, likely to be transcluded
533689 if ( !$.isArray( matches ) ) {
534 - var error = mw.msg( 'ajax-edit-category-error' );
535 - _showError( error );
536 - _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
537 - $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
 690+ _showError( mw.msg( 'ajax-edit-category-error' ) );
538691 return false;
539692 }
540 - var sortkey = matches[0].replace( categoryRegex, '$2' );
541 - var newCategoryString = "[[" + new mw.Titel( categoryNew, catNsId ) + sortkey + ']]';
 693+ var sortkey = sortkeyNew || matches[0].replace( categoryRegex, '$2' );
 694+ var newCategoryString = "[[" + new mw.Title( categoryNew, catNsId ) + sortkey + ']]';
542695
543 - if (matches.length > 1) {
 696+ if ( matches.length > 1 ) {
544697 // The category is duplicated.
545698 // Remove all but one match
546 - for (var i = 1; i < matches.length; i++) {
547 - oldText = oldText.replace( matches[i], '');
 699+ for ( var i = 1; i < matches.length; i++ ) {
 700+ oldText = oldText.replace( matches[i], '' );
548701 }
549702 }
550703 var newText = oldText.replace( categoryRegex, newCategoryString );
551704
552 - return newText;
 705+ return _runHooks ( newText, 'afterChange', category, categoryNew );
553706 },
554707 summary,
 708+ shortSummary,
555709 function( unsaved ) {
556710 // Remove input box & button
557 - $link.data('deleteButton').click();
 711+ $link.data( 'deleteButton' ).click();
558712
559713 // Update link text and href
560 - $link.show().text( categoryNew ).attr( 'href', catLink( categoryNew ) );
 714+ $link.show().text( categoryNew ).attr( 'href', catUrl( categoryNew ) );
561715 if ( unsaved ) {
562716 $link.data( 'origCat', category ).addClass( 'mw-changed-category' );
563717 }
@@ -565,20 +719,79 @@
566720 'edit'
567721 );
568722 };
 723+
 724+ /**
 725+ * Handle delete category submit. Not to be called directly
 726+ */
 727+ this._handleDeleteLink = function() {
 728+ var $this = $( this ),
 729+ $link = $this.parent().find( 'a:not(.icon)' ),
 730+ category = $link.text();
569731
 732+ if ( $link.is( '.mw-added-category, .mw-changed-category' ) ) {
 733+ // We're just cancelling the addition or edit
 734+ that.resetCatLink ( $link, $link.hasClass( 'mw-added-category' ) );
 735+ return;
 736+ } else if ( $link.is( '.mw-removed-category' ) ) {
 737+ // It's already removed...
 738+ return;
 739+ }
 740+ that.handleCategoryDelete( $link, category );
 741+ };
 742+
570743 /**
 744+ * Execute or queue an category delete
 745+ */
 746+ this.handleCategoryDelete = function( $link, category ) {
 747+ var categoryRegex = buildRegex( category, true );
 748+
 749+ var summary = mw.msg( 'ajax-remove-category-summary', category );
 750+ var shortSummary = '-[[' + new mw.Title( category, catNsId ) + ']]';
 751+
 752+ that._confirmEdit(
 753+ function( oldText ) {
 754+ newText = _runHooks ( oldText, 'beforeDelete', category );
 755+ var newText = newText.replace( categoryRegex, '' );
 756+
 757+ if ( newText == oldText ) {
 758+ _showError( mw.msg( 'ajax-remove-category-error' ) );
 759+ return false;
 760+ }
 761+
 762+ return _runHooks ( newText, 'afterDelete', category );
 763+ },
 764+ summary,
 765+ shortSummary,
 766+ function( unsaved ) {
 767+ if ( unsaved ) {
 768+ $link.addClass( 'mw-removed-category' );
 769+ } else {
 770+ $link.parent().remove();
 771+ }
 772+ },
 773+ $link,
 774+ 'delete'
 775+ );
 776+ };
 777+
 778+
 779+ /**
571780 * Open a dismissable error dialog
572781 *
573782 * @param string str The error description
574783 */
575784 _showError = function( str ) {
 785+ var oldDialog = $( '.mw-ajax-confirm-dialog' );
 786+ that._removeProgressIndicator( oldDialog );
 787+ oldDialog.dialog( 'close' );
 788+
576789 var dialog = $( '<div/>' );
577790 dialog.text( str );
578791
579792 mw.util.$content.append( dialog );
580793
581794 var buttons = { };
582 - buttons[mw.msg( 'ajax-error-dismiss' )] = function( e ) {
 795+ buttons[mw.msg( 'ajax-confirm-ok' )] = function( e ) {
583796 dialog.dialog( 'close' );
584797 };
585798 var dialogOptions = {
@@ -588,6 +801,11 @@
589802 };
590803
591804 dialog.dialog( dialogOptions );
 805+
 806+ // Close on enter
 807+ dialog.keyup( function( e ) {
 808+ if ( e.keyCode == 13 ) dialog.dialog( 'close' );
 809+ });
592810 };
593811
594812 /**
@@ -601,11 +819,12 @@
602820 * @return jQueryObject The button
603821 */
604822 _createButton = function( icon, title, className, text ){
 823+ // We're adding a zero width space for IE7, it's got problems with empty nodes apparently
605824 var $button = $( '<a>' ).addClass( className || '' )
606 - .attr('title', title);
 825+ .attr( 'title', title ).html( '&#8203;' );
607826
608827 if ( text ) {
609 - var $icon = $( '<a>' ).addClass( 'icon ' + icon );
 828+ var $icon = $( '<span>' ).addClass( 'icon ' + icon ).html( '&#8203;' );
610829 $button.addClass( 'icon-parent' ).append( $icon ).append( text );
611830 } else {
612831 $button.addClass( 'icon ' + icon );
@@ -618,60 +837,64 @@
619838 *
620839 * @param DOMElement element Anchor element, to which the buttons should be appended.
621840 */
622 - _createCatButtons = function( element ) {
 841+ _createCatButtons = function( $element ) {
623842 // Create remove & edit buttons
624 - var deleteButton = _createButton('icon-close', mw.msg( 'ajax-remove-category' ) );
625 - var editButton = _createButton('icon-edit', mw.msg( 'ajax-edit-category' ) );
 843+ var deleteButton = _createButton( 'icon-close', mw.msg( 'ajax-remove-category' ) );
 844+ var editButton = _createButton( 'icon-edit', mw.msg( 'ajax-edit-category' ) );
626845
627846 //Not yet used
628 - var saveButton = _createButton('icon-tick', mw.msg( 'ajax-confirm-save' ) ).hide();
 847+ var saveButton = _createButton( 'icon-tick', mw.msg( 'ajax-confirm-save' ) ).hide();
629848
630 - deleteButton.click( _handleDeleteLink );
631 - editButton.click( _handleEditLink );
 849+ deleteButton.click( that._handleDeleteLink );
 850+ editButton.click( that._createEditInterface );
632851
633 - $( element ).after( deleteButton ).after( editButton );
 852+ $element.after( deleteButton ).after( editButton );
634853
635854 //Save references to all links and buttons
636 - $( element ).data({
 855+ $element.data({
637856 saveButton : saveButton,
638857 deleteButton: deleteButton,
639858 editButton : editButton
640859 });
 860+ editButton.data({
 861+ link : $element
 862+ });
641863 };
 864+
 865+ /**
 866+ * Create the UI
 867+ */
642868 this.setup = function() {
643869 // Could be set by gadgets like HotCat etc.
644 - if ( mw.config.get('disableAJAXCategories') ) {
645 - return;
 870+ if ( mw.config.get( 'disableAJAXCategories' ) ) {
 871+ return false;
646872 }
647873 // Only do it for articles.
648874 if ( !mw.config.get( 'wgIsArticle' ) ) return;
649875
650 - // Unhide hidden category holders.
651 - $('#mw-hidden-catlinks').show();
652 -
653 - var $li = $('<li>', {
654 - 'class' : 'mw-ajax-addcategory-holder'
655 - });
656 - // $containerNormal.find('ul').append( $li)
657876 // Create [Add Category] link
658 - var addLink = _createButton('icon-add',
 877+ var addLink = _createButton( 'icon-add',
659878 mw.msg( 'ajax-add-category' ),
660879 'mw-ajax-addcategory',
661880 mw.msg( 'ajax-add-category' )
662881 );
663 - addLink.click( _handleAddLink );
664 - $containerNormal.append( addLink );
 882+ addLink.click( function() {
 883+ $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus();
 884+ });
 885+
665886
666887 // Create add category prompt
667 - var promptContainer = _makeSuggestionBox( '', _handleCategoryAdd, mw.msg( 'ajax-add-category-submit' ) );
668 - promptContainer.hide();
 888+ _addContainer = that._makeSuggestionBox( '', that._handleAddLink, mw.msg( 'ajax-add-category-submit' ) );
 889+ _addContainer.children().hide();
669890
 891+ _addContainer.prepend( addLink );
 892+
670893 // Create edit & delete link for each category.
671894 $( '#catlinks li a' ).each( function() {
672 - _createCatButtons( this );
 895+ _createCatButtons( $( this ) );
673896 });
674897
675 - $containerNormal.append( promptContainer );
 898+ options.$containerNormal.append( _addContainer );
676899
677900 //TODO Make more clickable
678901 _saveAllButton = _createButton( 'icon-tick',
@@ -679,53 +902,78 @@
680903 '',
681904 mw.msg( 'ajax-confirm-save-all' )
682905 );
683 - _cancelAllButton = _createButton( 'icon-tick',
684 - mw.msg( 'ajax-confirm-save-all' ),
 906+ _cancelAllButton = _createButton( 'icon-close',
 907+ mw.msg( 'ajax-cancel-all' ),
685908 '',
686 - mw.msg( 'ajax-confirm-save-all' )
 909+ mw.msg( 'ajax-cancel-all' )
687910 );
688 - _saveAllButton.click( _handleStashedCategories ).hide();
689 - _cancelAllButton.hide();
690 - //TODO wrap in div display:inline-block
691 - $containerNormal.append( _saveAllButton ).append( _cancelAllButton );
 911+ _saveAllButton.click( that._handleStashedCategories ).hide();
 912+ _cancelAllButton.click( function() { that.resetAll( false ); } ).hide();
 913+ options.$containerNormal.append( _saveAllButton ).append( _cancelAllButton );
 914+ options.$container.append( _addContainer );
692915 };
693916
694917 _stash = {
695918 summaries : [],
 919+ shortSum : [],
696920 fns : []
697921 };
698 - _removeStashItem = function( $link ) {
699 - var i = $link.data( 'stashIndex' );
 922+ _removeStashItem = function( i ) {
 923+ if ( typeof i != "number" ) {
 924+ i = i.data( 'stashIndex' );
 925+ }
700926 delete _stash.fns[i];
701927 delete _stash.summaries[i];
702 - }
 928+ if ( $.isEmpty( _stash.fns ) ) {
 929+ _stash.fns = [];
 930+ _stash.summaries = [];
 931+ _stash.shortSum = [];
 932+ _saveAllButton.hide();
 933+ _cancelAllButton.hide();
 934+ }
 935+ };
703936 _hooks = {
704937 beforeAdd : [],
705938 beforeChange : [],
706 - beforeDelete : []
 939+ beforeDelete : [],
 940+ afterAdd : [],
 941+ afterChange : [],
 942+ afterDelete : []
707943 };
708 - _runHooks = function( oldtext, type ) {
 944+ _runHooks = function( oldtext, type, category, categoryNew ) {
709945 // No hooks registered
710 - if ( !_hooks[type] ) {
 946+ if ( !_hooks[type] ) {
711947 return oldtext;
712948 } else {
713 - for (var i = 0; i < _hooks[type].length; i++) {
714 - oldtext = _hooks[type][i]( oldtext );
 949+ for ( var i = 0; i < _hooks[type].length; i++ ) {
 950+ oldtext = _hooks[type][i]( oldtext, category, categoryNew );
 951+ if ( oldtext === false ) {
 952+ _showError( mw.msg( 'ajax-category-hook-error', category ) );
 953+ return;
 954+ }
715955 }
716956 return oldtext;
717957 }
718958 };
719959 /**
720960 * Add hooks
721 - * Currently available: beforeAdd, beforeChange, beforeDelete
 961+ * Currently available: beforeAdd, beforeChange, beforeDelete,
 962+ * afterAdd, afterChange, afterDelete
 963+ * If the hook function returns false, all changes are aborted.
722964 *
723965 * @param string type Type of hook to add
724 - * @param function fn Hook function. This function is the old text passed
725 - * and it needs to return the modified text
 966+ * @param function fn Hook function. The following vars are passed to it:
 967+ * 1. oldtext: The wikitext before the hook
 968+ * 2. category: The deleted, added, or changed category
 969+ * 3. (only for beforeChange/afterChange): newcategory
726970 */
727971 this.addHook = function( type, fn ) {
728 - if ( !_hooks[type] ) return;
729 - else hooks[type].push( fn );
 972+ if ( !_hooks[type] || !$.isFunction( fn ) ) {
 973+ return;
 974+ }
 975+ else {
 976+ hooks[type].push( fn );
 977+ }
730978 };
731979 };
732980
Index: trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.css
@@ -17,11 +17,16 @@
1818 #catlinks:hover .icon {
1919 opacity: 1;
2020 }
21 -
 21+#catlinks ul {
 22+ margin-right: 2em;
 23+}
 24+.mw-ajax-addcategory-holder {
 25+ display: inline-block;
 26+}
2227 .mw-ajax-addcategory {
23 - padding-left: 30px;
2428 margin-right: 1em;
2529 cursor: pointer;
 30+ display:inline-block;
2631 }
2732 #catlinks .icon {
2833 cursor: pointer;
@@ -29,9 +34,11 @@
3035 margin: 0;
3136 background: url('images/AJAXCategorySprite.png') 0 0 no-repeat;
3237 opacity: 0.5;
 38+
3339 }
3440 #catlinks .icon-parent {
3541 cursor: pointer;
 42+ margin-right: 1em;
3643 }
3744 #catlinks .icon-parent:hover .icon {
3845 background-position-y: -16px;
@@ -61,4 +68,4 @@
6269 }
6370 #catlinks .icon-add:hover {
6471 background-position: -64px -16px;
65 -}
\ No newline at end of file
 72+}

Follow-up revisions

RevisionCommit summaryAuthorDate
r93196r93090 : Make summaries more modular, Siebrand says there'll be problems with...diebuche15:50, 26 July 2011

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r92112Rewrite ajaxCategories for ResourceLoader. Add some missing functionality (ed...diebuche22:36, 13 July 2011

Comments

#Comment by DieBuche (talk | contribs)   19:06, 25 July 2011

Forgot to mention in commit msg: This is a followup to r92112

#Comment by 😂 (talk | contribs)   19:07, 25 July 2011

Removing 1.18 tag, AjaxCategories was removed from 1.18 and will not be backported.

#Comment by Raymond (talk | contribs)   20:16, 25 July 2011

Please note, trailing spaces will be trimmed automatically:

+'ajax-category-and'             => ' and ',

Isn't it possible to use the core message

'and'           => ' and',

if needed together with the other core message:

'word-separator'      => ' '
#Comment by Raymond (talk | contribs)   20:19, 25 July 2011

This is lego style and makes a lot of problems with many languages;

'ajax-category-question'        => 'Why do you want to $1?',

I suggest to use complete sentences per case. As I see 3 are needed only.

#Comment by DieBuche (talk | contribs)   21:32, 25 July 2011

There more than 3. Theoretically unlimited. "Why do you want to remove category A, add Category C, change Category D to F and remove Category Z?"


I can switch to a simpler system though, if you think that this is impossible. Something like: "Why do you want to make the following changes: * Add X \n * Delete Y \n ... "

#Comment by Raymond (talk | contribs)   15:29, 26 July 2011

Yes, I think the more simple system would improve the translation quality, see mw:Localisation#Avoid patchwork messages too.

#Comment by DieBuche (talk | contribs)   15:51, 26 July 2011

I reverted back to the simpler system in r93196

#Comment by Schnark (talk | contribs)   08:12, 29 July 2011

You don't want to check for wgCaseSensitiveNamespaces in makeCaseInsensitive, but in buildRegex when building firstChar (which by the way you want to be a local variable).

#Comment by Nikerabbit (talk | contribs)   08:14, 4 September 2011
+'ajax-category-hook-error'      => 'A local function prevented the changes from being saved',

This is not very easy to translate nor for the user to understand. Should we cut the edges and say (installed) extension instead of local function?

#Comment by Krinkle (talk | contribs)   20:56, 3 October 2011

AjaxCategories moved out of core. Marking deferred for now.

Status & tagging log