2 * mediaWiki.page.ajaxCategories
4 * @author Michael Dale, 2009
5 * @author Leo Koppelkamm, 2011
8 * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces, wgUserGroups),
9 * mw.util.wikiGetlink, mw.user.getId
15 var catNsId
= mw
.config
.get( 'wgNamespaceIds' ).category
,
17 clean = function( s
) {
18 if ( s
!== undefined ) {
19 return s
.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '' );
24 * Build URL for passed Category
26 * @param string category name.
27 * @return string Valid URL
29 catUrl = function( cat
) {
30 return mw
.util
.wikiGetlink( new mw
.Title( cat
, catNsId
) );
34 * Helper function for $.fn.suggestion
36 * @param string Query string.
38 fetchSuggestions = function( query
) {
40 // ignore bad characters, they will be stripped out
41 var catName
= clean( $( this ).val() );
42 var request
= $.ajax( {
43 url
: mw
.util
.wikiScript( 'api' ),
47 'apnamespace': catNsId
,
52 success: function( data
) {
53 // Process data.query.allpages into an array of titles
54 var pages
= data
.query
.allpages
;
57 $.each( pages
, function( i
, page
) {
58 var title
= page
.title
.split( ':', 2 )[1];
59 titleArr
.push( title
);
62 $( _this
).suggestions( 'suggestions', titleArr
);
69 * Replace <nowiki> and comments with unique keys
71 replaceNowikis = function( text
, id
, array
) {
72 var matches
= text
.match( /(<nowiki\>[\s\S]*?<\/nowiki>|<\!--[\s\S]*?--\>)/g );
73 for ( var i
= 0; matches
&& i
< matches
.length
; i
++ ) {
74 array
[i
] = matches
[i
];
75 text
= text
.replace( matches
[i
], id
+ i
+ '-' );
81 * Restore <nowiki> and comments from unique keys
83 restoreNowikis = function( text
, id
, array
) {
84 for ( var i
= 0; i
< array
.length
; i
++ ) {
85 text
= text
.replace( id
+ i
+ '-', array
[i
] );
91 * Makes regex string caseinsensitive.
92 * Useful when 'i' flag can't be used.
93 * Return stuff like [Ff][Oo][Oo]
94 * @param string Regex string.
95 * @return string Processed regex string
97 makeCaseInsensitive = function( string
) {
98 if ( $.inArray( 14, mw
.config
.get( 'wgCaseSensitiveNamespaces' ) ) + 1 ) {
102 for ( var i
=0; i
< string
.length
; i
++ ) {
103 newString
+= '[' + string
.charAt( i
).toUpperCase() + string
.charAt( i
).toLowerCase() + ']';
109 * Build a regex that matches legal invocations
110 * of the passed category.
111 * @param string category.
112 * @param boolean Match one following linebreak as well?
115 buildRegex = function( category
, matchLineBreak
) {
116 var categoryNSFragment
= '';
117 $.each( mw
.config
.get( 'wgNamespaceIds' ), function( name
, id
) {
119 // The parser accepts stuff like cATegORy,
120 // we need to do the same
121 // ( Well unless we have wgCaseSensitiveNamespaces, but that's being checked for )
122 categoryNSFragment
+= '|' + makeCaseInsensitive ( $.escapeRE( name
) );
125 categoryNSFragment
= categoryNSFragment
.substr( 1 ); // Remove leading pipe
128 var titleFragment
= $.escapeRE( category
).replace( /( |_)/g, '[ _]' );
130 firstChar
= titleFragment
.charAt( 0 );
131 firstChar
= '[' + firstChar
.toUpperCase() + firstChar
.toLowerCase() + ']';
132 titleFragment
= firstChar
+ titleFragment
.substr( 1 );
133 var categoryRegex
= '\\[\\[(' + categoryNSFragment
+ '):' + '[ _]*' +titleFragment
+ '(\\|[^\\]]*)?\\]\\]';
134 if ( matchLineBreak
) {
135 categoryRegex
+= '[ \\t\\r]*\\n?';
137 return new RegExp( categoryRegex
, 'g' );
141 mw
.ajaxCategories = function( options
) {
142 //Save scope in shortcut
143 var that
= this, _request
, _saveAllButton
, _cancelAllButton
, _addContainer
, defaults
;
146 catLinkWrapper
: '<li/>',
147 $container
: $( '.catlinks' ),
148 $containerNormal
: $( '#mw-normal-catlinks' ),
149 categoryLinkSelector
: 'li a:not(.icon)',
150 multiEdit
: $.inArray( 'user', mw
.config
.get( 'wgUserGroups' ) ) + 1,
151 resolveRedirects
: true
153 // merge defaults and options, without modifying defaults */
154 options
= $.extend( {}, defaults
, options
);
157 * Insert a newly added category into the DOM
159 * @param string category name.
160 * @return jQuery object
162 this.createCatLink = function( cat
) {
163 // User can implicitely state a sort key.
164 // Remove before display
165 cat
= cat
.replace(/\|.*/, '' );
167 // strip out bad characters
170 if ( $.isEmpty( cat
) || that
.containsCat( cat
) ) {
174 var $catLinkWrapper
= $( options
.catLinkWrapper
);
175 var $anchor
= $( '<a/>' ).append( cat
);
176 $catLinkWrapper
.append( $anchor
);
177 $anchor
.attr( { target
: "_blank", href
: catUrl( cat
) } );
179 _createCatButtons( $anchor
);
185 * Takes a category link element
186 * and strips all data from it.
188 * @param jQuery object
190 this.resetCatLink = function( $link
, del
, dontRestoreText
) {
191 $link
.removeClass( 'mw-removed-category mw-added-category mw-changed-category' );
192 var data
= $link
.data();
194 if ( typeof data
.stashIndex
== "number" ) {
195 _removeStashItem( data
.stashIndex
);
198 $link
.parent
.remove();
201 if ( data
.origCat
&& !dontRestoreText
) {
202 $link
.text( data
.origCat
);
203 $link
.attr( 'href', catUrl( data
.origCat
) );
210 saveButton
: data
.saveButton
,
211 deleteButton
: data
.deleteButton
,
212 editButton
: data
.editButton
217 * Reset all data from the category links and the stash.
218 * @param Boolean del Delete any category links with .mw-removed-category
220 this.resetAll = function( del
) {
221 var $links
= options
.$container
.find( options
.categoryLinkSelector
), $del
= $();
223 $del
= $links
.filter( '.mw-removed-category' ).parent();
226 $links
.each( function() {
227 that
.resetCatLink( $( this ), false, del
);
232 if ( !options
.$container
.find( '#mw-hidden-catlinks li' ).length
) {
233 options
.$container
.find( '#mw-hidden-catlinks' ).remove();
238 * Create a suggestion box for use in edit/add dialogs
239 * @param str prefill Prefill input
240 * @param function callback on submit
241 * @param str buttonVal Button text
243 this._makeSuggestionBox = function( prefill
, callback
, buttonVal
) {
244 // Create add category prompt
245 var promptContainer
= $( '<div class="mw-addcategory-prompt"/>' );
246 var promptTextbox
= $( '<input type="text" size="30" class="mw-addcategory-input"/>' );
247 if ( prefill
!== '' ) {
248 promptTextbox
.val( prefill
);
250 var addButton
= $( '<input type="button" class="mw-addcategory-button"/>' );
251 addButton
.val( buttonVal
);
253 addButton
.click( callback
);
254 promptTextbox
.keyup( function( e
) {
255 if ( e
.keyCode
== 13 ) addButton
.click();
257 promptTextbox
.suggestions( {
258 'fetch': fetchSuggestions
,
259 'cancel': function() {
261 // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of "unknown" for typeof
262 if ( req
&& ( typeof req
.abort
!== 'unknown' ) && ( typeof req
.abort
!== 'undefined' ) && req
.abort
) {
268 promptTextbox
.suggestions();
270 promptContainer
.append( promptTextbox
);
271 promptContainer
.append( addButton
);
273 return promptContainer
;
277 * Parse the DOM $container and build a list of
280 * @return array Array of all categories
282 this.getCats = function() {
283 return options
.$container
.find( options
.categoryLinkSelector
).map( function() { return $.trim( $( this ).text() ); } );
287 * Check whether a passed category is present in the DOM
289 * @return boolean True for exists
291 this.containsCat = function( cat
) {
292 return that
.getCats().filter( function() { return $.ucFirst( this ) == $.ucFirst( cat
); } ).length
!== 0;
296 * This gets called by all action buttons
297 * Displays a dialog to confirm the action
298 * Afterwards do the actual edit
300 * @param function fn text-modifying function
301 * @param string actionSummary Changes done
302 * @param string shortSummary Changes, short version
303 * @param function fn doneFn callback after everything is done
304 * @return boolean True for exists
306 this._confirmEdit = function( fn
, actionSummary
, shortSummary
, doneFn
, $link
, action
) {
307 // Check whether to use multiEdit mode
308 if ( options
.multiEdit
&& action
!= 'all' ) {
310 $link
.data( 'stashIndex', _stash
.fns
.length
);
311 $link
.data( 'summary', actionSummary
);
312 _stash
.summaries
.push( actionSummary
);
313 _stash
.shortSum
.push( shortSummary
);
314 _stash
.fns
.push( fn
);
316 _saveAllButton
.show();
317 _cancelAllButton
.show();
319 // This only does visual changes
323 // Produce a confirmation dialog
324 var dialog
= $( '<div/>' );
326 dialog
.addClass( 'mw-ajax-confirm-dialog' );
327 dialog
.attr( 'title', mw
.msg( 'ajax-confirm-title' ) );
329 // Summary of the action to be taken
330 var summaryHolder
= $( '<p/>' );
331 summaryHolder
.html( '<strong>' + mw
.msg( 'ajax-category-question' ) + '</strong><br>' + actionSummary
);
332 dialog
.append( summaryHolder
);
335 var reasonBox
= $( '<input type="text" size="45" />' );
336 reasonBox
.addClass( 'mw-ajax-confirm-reason' );
337 dialog
.append( reasonBox
);
340 var submitButton
= $( '<input type="button"/>' );
341 submitButton
.val( mw
.msg( 'ajax-confirm-save' ) );
343 var submitFunction = function() {
344 that
._addProgressIndicator( dialog
);
346 mw
.config
.get( 'wgPageName' ),
348 shortSummary
+ ': ' + reasonBox
.val(),
351 dialog
.dialog( 'close' );
352 that
._removeProgressIndicator( dialog
);
358 buttons
[mw
.msg( 'ajax-confirm-save' )] = submitFunction
;
359 var dialogOptions
= {
365 $( '#catlinks' ).prepend( dialog
);
366 dialog
.dialog( dialogOptions
);
369 dialog
.keyup( function( e
) {
370 if ( e
.keyCode
== 13 ) submitFunction();
375 * When multiEdit mode is enabled,
376 * this is called when the user clicks "save all"
377 * Combines the summaries and edit functions
379 this._handleStashedCategories = function() {
380 var summary
= '', fns
= _stash
.fns
;
382 // Remove "holes" in array
383 summary
= $.grep( _stash
.summaries
, function( n
, i
) {
386 if ( summary
.length
< 1 ) {
387 // Nothing to do here.
388 _saveAllButton
.hide();
389 _cancelAllButton
.hide();
392 summary
= summary
.join( '<br>' );
394 // Remove "holes" in array
395 summaryShort
= $.grep( _stash
.shortSum
, function( n
,i
) {
398 summaryShort
= summaryShort
.join( ', ' );
400 var combinedFn = function( oldtext
) {
401 // Run the text through all action functions
403 for ( var i
= 0; i
< fns
.length
; i
++ ) {
404 if ( $.isFunction( fns
[i
] ) ) {
405 newtext
= fns
[i
]( newtext
);
406 if ( newtext
=== false ) {
413 var doneFn = function() { that
.resetAll( true ); };
415 that
._confirmEdit( combinedFn
, summary
, summaryShort
, doneFn
, '', 'all' );
419 * Do the actual edit.
420 * Gets token & text from api, runs it through fn
421 * and saves it with summary.
422 * @param str page Pagename
423 * @param function fn edit function
425 * @param str doneFn Callback after all is done
427 this._doEdit = function( page
, fn
, summary
, doneFn
) {
428 // Get an edit token for the page.
431 'prop':'info|revisions',
434 'rvprop':'content|timestamp',
438 $.post( mw
.util
.wikiScript( 'api' ), getTokenVars
,
440 var infos
= reply
.query
.pages
;
443 function( pageid
, data
) {
444 var token
= data
.edittoken
;
445 var timestamp
= data
.revisions
[0].timestamp
;
446 var oldText
= data
.revisions
[0]['*'];
448 // Replace all nowiki and comments with unique keys
449 var key
= mw
.user
.generateId();
451 oldText
= replaceNowikis( oldText
, key
, nowiki
);
453 // Then do the changes
454 var newText
= fn( oldText
);
455 if ( newText
=== false ) return;
457 // And restore them back
458 newText
= restoreNowikis( newText
, key
, nowiki
);
466 'basetimestamp':timestamp
,
470 $.post( mw
.util
.wikiScript( 'api' ), postEditVars
, doneFn
, 'json' )
471 .error( function( xhr
, text
, error
) {
472 _showError( mw
.msg( 'ajax-api-error', text
, error
) );
477 , 'json' ).error( function( xhr
, text
, error
) {
478 _showError( mw
.msg( 'ajax-api-error', text
, error
) );
482 * Append spinner wheel to element
483 * @param DOMObject element.
485 this._addProgressIndicator = function( elem
) {
486 elem
.append( $( '<div/>' ).addClass( 'mw-ajax-loader' ) );
490 * Find and remove spinner wheel from inside element
491 * @param DOMObject parent element.
493 this._removeProgressIndicator = function( elem
) {
494 elem
.find( '.mw-ajax-loader' ).remove();
498 * Checks the API whether the category in question is a redirect.
499 * Also returns existance info ( to color link red/blue )
500 * @param string category.
501 * @param function callback
503 this._resolveRedirects = function( category
, callback
) {
504 if ( !options
.resolveRedirects
) {
505 callback( category
);
510 'titles': new mw
.Title( category
, catNsId
).toString(),
515 $.get( mw
.util
.wikiScript( 'api' ), queryVars
,
517 var redirect
= reply
.query
.redirects
;
519 category
= new mw
.Title( redirect
[0].to
)._name
;
521 callback( category
, !reply
.query
.pages
[-1] );
527 * Handle add category submit. Not to be called directly
529 this._handleAddLink = function( e
) {
530 var $this = $( this ), $link
= $();
532 // Grab category text
533 var category
= $this.parent().find( '.mw-addcategory-input' ).val();
534 category
= $.ucFirst( category
);
537 that
._resolveRedirects( category
, function( resolvedCat
, exists
) {
538 that
.handleCategoryAdd( $link
, resolvedCat
, false, exists
);
542 * Execute or queue an category add
544 this.handleCategoryAdd = function( $link
, category
, noAppend
, exists
) {
545 if ( !$link
.length
) {
546 $link
= that
.createCatLink( category
);
548 // Mark red if missing
549 $link
.toggleClass( 'new', exists
=== false );
552 var arr
= category
.split( '|' ), sortkey
= '';
554 if ( arr
.length
> 1 ) {
555 category
= arr
.shift();
556 sortkey
= '|' + arr
.join( '|' );
557 if ( sortkey
== '|' ) sortkey
= '';
560 //Replace underscores
561 category
= category
.replace(/_
/g
, ' ' );
563 if ( that
.containsCat( category
) ) {
564 _showError( mw
.msg( 'ajax-category-already-present', category
) );
567 var catFull
= new mw
.Title( category
, catNsId
).toString().replace(/_
/g
, ' ' );
568 var appendText
= "\n[[" + catFull
+ sortkey
+ "]]\n";
569 var summary
= mw
.msg( 'ajax-add-category-summary', category
);
570 var shortSummary
= '+[[' + catFull
+ ']]';
572 function( oldText
) {
573 newText
= _runHooks ( oldText
, 'beforeAdd', category
);
574 newText
= newText
+ appendText
;
575 return _runHooks ( newText
, 'afterAdd', category
);
579 function( unsaved
) {
581 options
.$container
.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).children( 'input' ).hide();
582 options
.$container
.find( '#mw-normal-catlinks ul' ).append( $link
.parent() );
584 // Remove input box & button
585 $link
.data( 'deleteButton' ).click();
587 // Update link text and href
588 $link
.show().text( category
).attr( 'href', catUrl( category
) );
591 $link
.addClass( 'mw-added-category' );
593 $( '.mw-ajax-addcategory' ).click();
599 this._createEditInterface = function( e
) {
600 var $this = $( this ),
601 $link
= $this.data( 'link' ),
602 category
= $link
.text();
603 var $input
= that
._makeSuggestionBox( category
,
604 that
._handleEditLink
,
605 options
.multiEdit
? mw
.msg( 'ajax-confirm-ok' ) : mw
.msg( 'ajax-confirm-save' )
607 $link
.after( $input
).hide();
608 $input
.find( '.mw-addcategory-input' ).focus();
609 $link
.data( 'editButton' ).hide();
610 $link
.data( 'deleteButton' ).unbind( 'click' ).click( function() {
613 $link
.data( 'editButton' ).show();
614 $( this ).unbind( 'click' ).click( that
._handleDeleteLink
)
615 .attr( 'title', mw
.msg( 'ajax-remove-category' ));
616 }).attr( 'title', mw
.msg( 'ajax-cancel' ));
620 * Handle edit category submit. Not to be called directly
622 this._handleEditLink = function( e
) {
623 var $this = $( this ),
624 $link
= $this.parent().parent().find( 'a:not(.icon)' ),
625 categoryNew
, sortkey
= '';
627 // Grab category text
628 categoryNew
= $this.parent().find( '.mw-addcategory-input' ).val();
629 categoryNew
= $.ucFirst( categoryNew
.replace(/_
/g
, ' ' ) );
632 var arr
= categoryNew
.split( '|' );
633 if ( arr
.length
> 1 ) {
634 categoryNew
= arr
.shift();
635 sortkey
= '|' + arr
.join( '|' );
639 var added
= $link
.hasClass( 'mw-added-category' );
640 that
.resetCatLink ( $link
);
641 var category
= $link
.text();
643 // Check for dupes ( exluding itself )
644 if ( category
!= categoryNew
&& that
.containsCat( categoryNew
) ) {
645 $link
.data( 'deleteButton' ).click();
650 that
._resolveRedirects( categoryNew
, function( resolvedCat
, exists
) {
651 that
.handleCategoryEdit( $link
, category
, resolvedCat
, sortkey
, exists
, added
);
655 * Execute or queue an category edit
657 this.handleCategoryEdit = function( $link
, category
, categoryNew
, sortkeyNew
, exists
, added
) {
658 // Category add needs to be handled differently
661 that
.handleCategoryAdd( $link
, categoryNew
+ sortkeyNew
, true );
664 // User didn't change anything.
665 if ( category
== categoryNew
+ sortkeyNew
) {
666 $link
.data( 'deleteButton' ).click();
669 // Mark red if missing
670 $link
.toggleClass( 'new', exists
=== false );
672 categoryRegex
= buildRegex( category
);
674 var summary
= mw
.msg( 'ajax-edit-category-summary', category
, categoryNew
);
675 var shortSummary
= '[[' + new mw
.Title( category
, catNsId
) + ']] -> [[' + new mw
.Title( categoryNew
, catNsId
) + ']]';
677 function( oldText
) {
678 newText
= _runHooks ( oldText
, 'beforeChange', category
, categoryNew
);
680 var matches
= newText
.match( categoryRegex
);
682 //Old cat wasn't found, likely to be transcluded
683 if ( !$.isArray( matches
) ) {
684 _showError( mw
.msg( 'ajax-edit-category-error' ) );
687 var sortkey
= sortkeyNew
|| matches
[0].replace( categoryRegex
, '$2' );
688 var newCategoryString
= "[[" + new mw
.Title( categoryNew
, catNsId
) + sortkey
+ ']]';
690 if ( matches
.length
> 1 ) {
691 // The category is duplicated.
692 // Remove all but one match
693 for ( var i
= 1; i
< matches
.length
; i
++ ) {
694 oldText
= oldText
.replace( matches
[i
], '' );
697 var newText
= oldText
.replace( categoryRegex
, newCategoryString
);
699 return _runHooks ( newText
, 'afterChange', category
, categoryNew
);
703 function( unsaved
) {
704 // Remove input box & button
705 $link
.data( 'deleteButton' ).click();
707 // Update link text and href
708 $link
.show().text( categoryNew
).attr( 'href', catUrl( categoryNew
) );
710 $link
.data( 'origCat', category
).addClass( 'mw-changed-category' );
719 * Handle delete category submit. Not to be called directly
721 this._handleDeleteLink = function() {
722 var $this = $( this ),
723 $link
= $this.parent().find( 'a:not(.icon)' ),
724 category
= $link
.text();
726 if ( $link
.is( '.mw-added-category, .mw-changed-category' ) ) {
727 // We're just cancelling the addition or edit
728 that
.resetCatLink ( $link
, $link
.hasClass( 'mw-added-category' ) );
730 } else if ( $link
.is( '.mw-removed-category' ) ) {
731 // It's already removed...
734 that
.handleCategoryDelete( $link
, category
);
738 * Execute or queue an category delete
740 this.handleCategoryDelete = function( $link
, category
) {
741 var categoryRegex
= buildRegex( category
, true );
743 var summary
= mw
.msg( 'ajax-remove-category-summary', category
);
744 var shortSummary
= '-[[' + new mw
.Title( category
, catNsId
) + ']]';
747 function( oldText
) {
748 newText
= _runHooks ( oldText
, 'beforeDelete', category
);
749 var newText
= newText
.replace( categoryRegex
, '' );
751 if ( newText
== oldText
) {
752 _showError( mw
.msg( 'ajax-remove-category-error' ) );
756 return _runHooks ( newText
, 'afterDelete', category
);
760 function( unsaved
) {
762 $link
.addClass( 'mw-removed-category' );
764 $link
.parent().remove();
774 * Open a dismissable error dialog
776 * @param string str The error description
778 _showError = function( str
) {
779 var oldDialog
= $( '.mw-ajax-confirm-dialog' );
780 that
._removeProgressIndicator( oldDialog
);
781 oldDialog
.dialog( 'close' );
783 var dialog
= $( '<div/>' );
786 mw
.util
.$content
.append( dialog
);
789 buttons
[mw
.msg( 'ajax-confirm-ok' )] = function( e
) {
790 dialog
.dialog( 'close' );
792 var dialogOptions
= {
795 'title' : mw
.msg( 'ajax-error-title' )
798 dialog
.dialog( dialogOptions
);
801 dialog
.keyup( function( e
) {
802 if ( e
.keyCode
== 13 ) dialog
.dialog( 'close' );
807 * Manufacture iconed button, with or without text
809 * @param string icon The icon class.
810 * @param string title Title attribute.
811 * @param string className (optional) Additional classes to be added to the button.
812 * @param string text (optional) Text of button.
814 * @return jQueryObject The button
816 _createButton = function( icon
, title
, className
, text
){
817 // We're adding a zero width space for IE7, it's got problems with empty nodes apparently
818 var $button
= $( '<a>' ).addClass( className
|| '' )
819 .attr( 'title', title
).html( '​' );
822 var $icon
= $( '<span>' ).addClass( 'icon ' + icon
).html( '​' );
823 $button
.addClass( 'icon-parent' ).append( $icon
).append( text
);
825 $button
.addClass( 'icon ' + icon
);
831 * Append edit and remove buttons to a given category link
833 * @param DOMElement element Anchor element, to which the buttons should be appended.
835 _createCatButtons = function( $element
) {
836 // Create remove & edit buttons
837 var deleteButton
= _createButton( 'icon-close', mw
.msg( 'ajax-remove-category' ) );
838 var editButton
= _createButton( 'icon-edit', mw
.msg( 'ajax-edit-category' ) );
841 var saveButton
= _createButton( 'icon-tick', mw
.msg( 'ajax-confirm-save' ) ).hide();
843 deleteButton
.click( that
._handleDeleteLink
);
844 editButton
.click( that
._createEditInterface
);
846 $element
.after( deleteButton
).after( editButton
);
848 //Save references to all links and buttons
850 saveButton
: saveButton
,
851 deleteButton
: deleteButton
,
852 editButton
: editButton
862 this.setup = function() {
863 // Could be set by gadgets like HotCat etc.
864 if ( mw
.config
.get( 'disableAJAXCategories' ) ) {
867 // Only do it for articles.
868 if ( !mw
.config
.get( 'wgIsArticle' ) ) return;
870 // Create [Add Category] link
871 var addLink
= _createButton( 'icon-add',
872 mw
.msg( 'ajax-add-category' ),
873 'mw-ajax-addcategory',
874 mw
.msg( 'ajax-add-category' )
876 addLink
.click( function() {
877 $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus();
881 // Create add category prompt
882 _addContainer
= that
._makeSuggestionBox( '', that
._handleAddLink
, mw
.msg( 'ajax-add-category-submit' ) );
883 _addContainer
.children().hide();
885 _addContainer
.prepend( addLink
);
887 // Create edit & delete link for each category.
888 $( '#catlinks li a' ).each( function() {
889 _createCatButtons( $( this ) );
892 options
.$containerNormal
.append( _addContainer
);
894 //TODO Make more clickable
895 _saveAllButton
= _createButton( 'icon-tick',
896 mw
.msg( 'ajax-confirm-save-all' ),
898 mw
.msg( 'ajax-confirm-save-all' )
900 _cancelAllButton
= _createButton( 'icon-close',
901 mw
.msg( 'ajax-cancel-all' ),
903 mw
.msg( 'ajax-cancel-all' )
905 _saveAllButton
.click( that
._handleStashedCategories
).hide();
906 _cancelAllButton
.click( function() { that
.resetAll( false ); } ).hide();
907 options
.$containerNormal
.append( _saveAllButton
).append( _cancelAllButton
);
908 options
.$container
.append( _addContainer
);
916 _removeStashItem = function( i
) {
917 if ( typeof i
!= "number" ) {
918 i
= i
.data( 'stashIndex' );
920 delete _stash
.fns
[i
];
921 delete _stash
.summaries
[i
];
922 if ( $.isEmpty( _stash
.fns
) ) {
924 _stash
.summaries
= [];
925 _stash
.shortSum
= [];
926 _saveAllButton
.hide();
927 _cancelAllButton
.hide();
938 _runHooks = function( oldtext
, type
, category
, categoryNew
) {
939 // No hooks registered
940 if ( !_hooks
[type
] ) {
943 for ( var i
= 0; i
< _hooks
[type
].length
; i
++ ) {
944 oldtext
= _hooks
[type
][i
]( oldtext
, category
, categoryNew
);
945 if ( oldtext
=== false ) {
946 _showError( mw
.msg( 'ajax-category-hook-error', category
) );
955 * Currently available: beforeAdd, beforeChange, beforeDelete,
956 * afterAdd, afterChange, afterDelete
957 * If the hook function returns false, all changes are aborted.
959 * @param string type Type of hook to add
960 * @param function fn Hook function. The following vars are passed to it:
961 * 1. oldtext: The wikitext before the hook
962 * 2. category: The deleted, added, or changed category
963 * 3. (only for beforeChange/afterChange): newcategory
965 this.addHook = function( type
, fn
) {
966 if ( !_hooks
[type
] || !$.isFunction( fn
) ) {
970 hooks
[type
].push( fn
);