2b3b021a06c2ae2caac97a0182b3f9172935e568
2 * mediaWiki.page.ajaxCategories
4 * @author Michael Dale, 2009
5 * @author Leo Koppelkamm, 2011
6 * @author Timo Tijhof, 2011
9 * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds,
10 * wgCaseSensitiveNamespaces, wgUserGroups), mw.util.wikiGetlink, mw.user.getId
16 var catNsId
= mw
.config
.get( 'wgNamespaceIds' ).category
,
18 catLinkWrapper
: '<li>',
19 $container
: $( '.catlinks' ),
20 $containerNormal
: $( '#mw-normal-catlinks' ),
21 categoryLinkSelector
: 'li a:not(.icon)',
22 multiEdit
: $.inArray( 'user', mw
.config
.get( 'wgUserGroups' ) ) !== -1,
23 resolveRedirects
: true
25 isCatNsSensitive
= $.inArray( 14, mw
.config
.get( 'wgCaseSensitiveNamespaces' ) ) !== -1;
31 if ( typeof s
=== 'string' ) {
32 return s
.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '' );
38 * Helper function for $.fn.suggestions
41 * @param value {String} Textbox value.
43 function fetchSuggestions( value
) {
46 catName
= clean( value
);
49 url
: mw
.util
.wikiScript( 'api' ),
58 success: function( data
) {
59 // Process data.query.allpages into an array of titles
60 var pages
= data
.query
.allpages
,
61 titleArr
= $.map( pages
, function( page
) {
62 return new mw
.Title( page
.title
).getMainText();
65 $el
.suggestions( 'suggestions', titleArr
);
68 $el
.data( 'suggestions-request', request
);
72 * Replace <nowiki> and comments with unique keys in the page text.
74 * @param text {String}
75 * @param id {String} Unique key for this nowiki replacement layer call.
76 * @param keys {Array} Array where fragments will be stored in.
79 function replaceNowikis( text
, id
, keys
) {
80 var matches
= text
.match( /(<nowiki\>[\s\S]*?<\/nowiki>|<\!--[\s\S]*?--\>)/g );
81 for ( var i
= 0; matches
&& i
< matches
.length
; i
++ ) {
83 text
= text
.replace( matches
[i
], '' + id
+ '-' + i
);
89 * Restore <nowiki> and comments from unique keys in the page text.
91 * @param text {String}
92 * @param id {String} Unique key of the layer to be restored, as passed to replaceNowikis().
93 * @param keys {Array} Array where fragements should be fetched from.
96 function restoreNowikis( text
, id
, keys
) {
97 for ( var i
= 0; i
< keys
.length
; i
++ ) {
98 text
= text
.replace( '' + id
+ '-' + i
, keys
[i
] );
104 * Makes regex string caseinsensitive.
105 * Useful when 'i' flag can't be used.
106 * Return stuff like [Ff][Oo][Oo]
108 * @param string {String} Regex string
109 * @return {String} Processed regex string
111 function makeCaseInsensitive( string
) {
113 for ( var i
= 0; i
< string
.length
; i
++ ) {
114 newString
+= '[' + string
.charAt( i
).toUpperCase() + string
.charAt( i
).toLowerCase() + ']';
120 * Build a regex that matches legal invocations of the passed category.
121 * @param category {String}
122 * @param matchLineBreak {Boolean} Match one following linebreak as well?
125 function buildRegex( category
, matchLineBreak
) {
126 var categoryRegex
, categoryNSFragment
,
127 titleFragment
= $.escapeRE( category
).replace( /( |_)/g, '[ _]' ),
128 firstChar
= titleFragment
.charAt( 0 );
130 // Filter out all names for category namespace
131 categoryNSFragment
= $.map( mw
.config
.get( 'wgNamespaceIds' ), function( id
, name
) {
132 if ( id
=== catNsId
) {
133 name
= $.escapeRE( name
);
134 return !isCatNsSensitive
? makeCaseInsensitive( name
) : name
;
136 // Otherwise don't include in categoryNSFragment
140 firstChar
= '[' + firstChar
.toUpperCase() + firstChar
.toLowerCase() + ']';
141 titleFragment
= firstChar
+ titleFragment
.substr( 1 );
142 categoryRegex
= '\\[\\[(' + categoryNSFragment
+ ')' + '[ _]*' + ':' + '[ _]*' + titleFragment
+ '[ _]*' + '(\\|[^\\]]*)?\\]\\]';
143 if ( matchLineBreak
) {
144 categoryRegex
+= '[ \\t\\r]*\\n?';
146 return new RegExp( categoryRegex
, 'g' );
150 * Manufacture iconed button, with or without text.
152 * @param icon {String} The icon class.
153 * @param title {String} Title attribute.
154 * @param className {String} (optional) Additional classes to be added to the button.
155 * @param text {String} (optional) Text label of button.
156 * @return {jQuery} The button.
158 function createButton( icon
, title
, className
, text
){
159 // We're adding a zero width space for IE7, it's got problems with empty nodes apparently
160 var $button
= $( '<a>' )
161 .addClass( className
|| '' )
162 .attr( 'title', title
)
166 var $icon
= $( '<span>' ).addClass( 'icon ' + icon
).html( '​' );
167 $button
.addClass( 'icon-parent' ).append( $icon
).append( mw
.html
.escape( text
) );
169 $button
.addClass( 'icon ' + icon
);
178 mw
.ajaxCategories = function( options
) {
180 this.options
= options
= $.extend( defaultOptions
, options
);
182 // Save scope in shortcut
185 // Elements tied to this instance
186 this.saveAllButton
= null;
187 this.cancelAllButton
= null;
188 this.addContainer
= null;
194 dialogDescriptions
: [],
210 * Handle add category submit. Not to be called directly.
213 * @param e {jQuery Event}
215 this.handleAddLink = function( e
) {
218 categoryText
= $.ucFirst( $el
.parent().find( '.mw-addcategory-input' ).val() || '' );
221 ajaxcat
.resolveRedirects( categoryText
, function( resolvedCatTitle
) {
222 ajaxcat
.handleCategoryAdd( $link
, resolvedCatTitle
, '', false );
228 * @param e {jQuery Event}
230 this.createEditInterface = function( e
) {
232 $link
= $el
.data( 'link' ),
233 category
= $link
.text(),
234 $input
= ajaxcat
.makeSuggestionBox( category
,
235 ajaxcat
.handleEditLink
,
236 ajaxcat
.options
.multiEdit
? mw
.msg( 'ajax-confirm-ok' ) : mw
.msg( 'ajax-confirm-save' )
239 $link
.after( $input
).hide();
241 $input
.find( '.mw-addcategory-input' ).focus();
243 // Get the editButton associated with this category link,
245 $link
.data( 'editButton' ).hide();
247 // Get the deleteButton associated with this category link,
248 $link
.data( 'deleteButton' )
249 // (re)set click handler
252 // When the delete button is clicked:
253 // - Remove the suggestion box
254 // - Show the link and it's edit button
255 // - (re)set the click handler again
257 $link
.show().data( 'editButton' ).show();
260 .click( ajaxcat
.handleDeleteLink
)
261 .attr( 'title', mw
.msg( 'ajax-remove-category' ) );
263 .attr( 'title', mw
.msg( 'ajax-cancel' ) );
267 * Handle edit category submit. Not to be called directly.
270 * @param e {jQuery Event}
272 this.handleEditLink = function( e
) {
273 var input
, category
, categoryOld
,
274 sortkey
= '', // Wikitext for between '[[Category:Foo' and ']]'.
276 $link
= $el
.parent().parent().find( 'a:not(.icon)' );
278 // Grab category text
279 input
= $el
.parent().find( '.mw-addcategory-input' ).val();
282 var arr
= input
.split( '|', 2 );
283 if ( arr
.length
> 1 ) {
289 var isAdded
= $link
.hasClass( 'mw-added-category' );
290 ajaxcat
.resetCatLink( $link
);
291 categoryOld
= $link
.text();
293 // If something changed and the new cat is already on the page, delete it.
294 if ( categoryOld
!== category
&& ajaxcat
.containsCat( category
) ) {
295 $link
.data( 'deleteButton' ).click();
300 ajaxcat
.resolveRedirects( category
, function( resolvedCatTitle
) {
301 ajaxcat
.handleCategoryEdit( $link
, categoryOld
, resolvedCatTitle
, sortkey
, isAdded
);
306 * Handle delete category submit. Not to be called directly.
309 * @param e {jQuery Event}
311 this.handleDeleteLink = function( e
) {
313 $link
= $el
.parent().find( 'a:not(.icon)' ),
314 category
= $link
.text();
316 if ( $link
.is( '.mw-added-category, .mw-changed-category' ) ) {
317 // We're just cancelling the addition or edit
318 ajaxcat
.resetCatLink( $link
, $link
.hasClass( 'mw-added-category' ) );
320 } else if ( $link
.is( '.mw-removed-category' ) ) {
321 // It's already removed...
324 ajaxcat
.handleCategoryDelete( $link
, category
);
328 * When multiEdit mode is enabled,
329 * this is called when the user clicks "save all"
330 * Combines the dialogDescriptions and edit functions.
335 this.handleStashedCategories = function() {
337 // Remove "holes" in array
338 var dialogDescriptions
= $.grep( ajaxcat
.stash
.dialogDescriptions
, function( n
, i
) {
342 if ( dialogDescriptions
.length
< 1 ) {
343 // Nothing to do here.
344 ajaxcat
.saveAllButton
.hide();
345 ajaxcat
.cancelAllButton
.hide();
348 dialogDescriptions
= dialogDescriptions
.join( '<br/>' );
351 // Remove "holes" in array
352 var summaryShort
= $.grep( ajaxcat
.stash
.editSummaries
, function( n
,i
) {
355 summaryShort
= summaryShort
.join( ', ' );
357 var fns
= ajaxcat
.stash
.fns
;
359 ajaxcat
.doConfirmEdit( {
360 modFn: function( oldtext
) {
361 // Run the text through all action functions
362 var newtext
= oldtext
;
363 for ( var i
= 0; i
< fns
.length
; i
++ ) {
364 if ( $.isFunction( fns
[i
] ) ) {
365 newtext
= fns
[i
]( newtext
);
366 if ( newtext
=== false ) {
373 dialogDescription
: dialogDescriptions
,
374 editSummary
: summaryShort
,
376 ajaxcat
.resetAll( true );
386 mw
.ajaxCategories
.prototype = {
391 // Could be set by gadgets like HotCat etc.
392 if ( mw
.config
.get( 'disableAJAXCategories' ) ) {
395 // Only do it for articles.
396 if ( !mw
.config
.get( 'wgIsArticle' ) ) {
399 var options
= this.options
,
401 // Create [Add Category] link
402 $addLink
= createButton( 'icon-add',
403 mw
.msg( 'ajax-add-category' ),
404 'mw-ajax-addcategory',
405 mw
.msg( 'ajax-add-category' )
406 ).click( function() {
407 $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus();
410 // Create add category prompt
411 this.addContainer
= this.makeSuggestionBox( '', this.handleAddLink
, mw
.msg( 'ajax-add-category-submit' ) );
412 this.addContainer
.children().hide();
413 this.addContainer
.prepend( $addLink
);
415 // Create edit & delete link for each category.
416 $( '#catlinks' ).find( 'li a' ).each( function() {
417 ajaxcat
.createCatButtons( $( this ) );
420 options
.$containerNormal
.append( this.addContainer
);
422 // @todo Make more clickable
423 this.saveAllButton
= createButton( 'icon-tick',
424 mw
.msg( 'ajax-confirm-save-all' ),
426 mw
.msg( 'ajax-confirm-save-all' )
428 this.cancelAllButton
= createButton( 'icon-close',
429 mw
.msg( 'ajax-cancel-all' ),
431 mw
.msg( 'ajax-cancel-all' )
433 this.saveAllButton
.click( this.handleStashedCategories
).hide();
434 this.cancelAllButton
.click( function() {
435 ajaxcat
.resetAll( false );
437 options
.$containerNormal
.append( this.saveAllButton
).append( this.cancelAllButton
);
438 options
.$container
.append( this.addContainer
);
442 * Insert a newly added category into the DOM.
444 * @param catTitle {mw.Title} Category title for which a link should be created.
447 createCatLink: function( catTitle
) {
448 var catName
= catTitle
.getMainText();
450 if ( this.containsCat( catName
) ) {
454 var $catLinkWrapper
= $( this.options
.catLinkWrapper
),
459 href
: catTitle
.getUrl()
462 $catLinkWrapper
.append( $anchor
);
464 this.createCatButtons( $anchor
);
470 * Create a suggestion box for use in edit/add dialogs
471 * @param prefill {String} Prefill input
472 * @param callback {Function} Called on submit
473 * @param buttonVal {String} Button text
475 makeSuggestionBox: function( prefill
, callback
, buttonVal
) {
476 // Create add category prompt
477 var $promptContainer
= $( '<div class="mw-addcategory-prompt"></div>' ),
478 $promptTextbox
= $( '<input type="text" size="30" class="mw-addcategory-input"></input>' ),
479 $addButton
= $( '<input type="button" class="mw-addcategory-button"></input>' ),
482 if ( prefill
!== '' ) {
483 $promptTextbox
.val( prefill
);
491 .keyup( function( e
) {
492 if ( e
.keyCode
=== 13 ) {
497 fetch
: fetchSuggestions
,
499 var req
= this.data( 'suggestions-request' );
500 // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of 'unknown' for typeof
501 if ( req
&& typeof req
.abort
!== 'unknown' && typeof req
.abort
!== 'undefined' && req
.abort
) {
509 .append( $promptTextbox
)
510 .append( $addButton
);
512 return $promptContainer
;
516 * Execute or queue a category addition.
518 * @param $link {jQuery} Anchor tag of category link inside #catlinks.
519 * @param catTitle {mw.Title} Instance of mw.Title of the category to be added.
520 * @param catSortkey {String} sort key (optional)
522 * @return {mw.ajaxCategories}
524 handleCategoryAdd: function( $link
, catTitle
, catSortkey
, noAppend
) {
526 // Suffix is wikitext between '[[Category:Foo' and ']]'.
527 suffix
= catSortkey
? '|' + catSortkey
: '',
528 catName
= catTitle
.getMainText(),
529 catFull
= catTitle
.toText();
531 if ( !$link
.length
) {
532 $link
= this.createCatLink( catTitle
);
535 if ( this.containsCat( catName
) ) {
536 this.showError( mw
.msg( 'ajax-category-already-present', catName
) );
540 // Sometimes createCatLink returns undefined/null, previously caused an exception
541 // in the following lines, catching now.. @todo
543 this.showError( 'Unexpected error occurred. $link undefined.' );
547 // Mark red if missing
548 $link
[(catTitle
.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
550 this.doConfirmEdit( {
551 modFn: function( oldText
) {
552 var newText
= ajaxcat
.runHooks( oldText
, 'beforeAdd', catName
);
553 newText
= newText
+ "\n[[" + catFull
+ suffix
+ "]]\n";
554 return ajaxcat
.runHooks( newText
, 'afterAdd', catName
);
556 dialogDescription
: mw
.message( 'ajax-add-category-summary', catName
).escaped(),
557 editSummary
: '+[[' + catFull
+ ']]',
558 doneFn: function( unsaved
) {
560 ajaxcat
.options
.$container
561 .find( '#mw-normal-catlinks > .mw-addcategory-prompt' ).children( 'input' ).hide();
562 ajaxcat
.options
.$container
563 .find( '#mw-normal-catlinks ul' ).append( $link
.parent() );
565 // Remove input box & button
566 $link
.data( 'deleteButton' ).click();
568 // Update link text and href
569 $link
.show().text( catName
).attr( 'href', catTitle
.getUrl() );
572 $link
.addClass( 'mw-added-category' );
574 $( '.mw-ajax-addcategory' ).click();
583 * Execute or queue a category edit.
585 * @param $link {jQuery} Anchor tag of category link in #catlinks.
586 * @param oldCatName {String} Name of category before edit
587 * @param catTitle {mw.Title} Instance of mw.Title for new category
588 * @param catSortkey {String} Sort key of new category link (optional)
589 * @param isAdded {Boolean} True if this is a new link, false if it changed an existing one
591 handleCategoryEdit: function( $link
, oldCatName
, catTitle
, catSortkey
, isAdded
) {
593 catName
= catTitle
.getMainText();
595 // Category add needs to be handled differently
598 this.handleCategoryAdd( $link
, catTitle
, catSortkey
, true );
602 // User didn't change anything, trigger delete
603 // @todo Document why it's deleted.
604 if ( oldCatName
=== catName
) {
605 $link
.data( 'deleteButton' ).click();
609 // Mark red if missing
610 $link
[(catTitle
.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
612 var categoryRegex
= buildRegex( oldCatName
),
613 editSummary
= '[[' + new mw
.Title( oldCatName
, catNsId
).toText() + ']] -> [[' + catTitle
.toText() + ']]';
615 ajaxcat
.doConfirmEdit({
616 modFn: function( oldText
) {
617 var newText
= ajaxcat
.runHooks( oldText
, 'beforeChange', oldCatName
, catName
),
618 matches
= newText
.match( categoryRegex
);
620 // Old cat wasn't found, likely to be transcluded
621 if ( !$.isArray( matches
) ) {
622 ajaxcat
.showError( mw
.msg( 'ajax-edit-category-error' ) );
626 var suffix
= catSortkey
? '|' + catSortkey
: matches
[0].replace( categoryRegex
, '$2' ),
627 newCategoryWikitext
= '[[' + catTitle
+ suffix
+ ']]';
629 if ( matches
.length
> 1 ) {
630 // The category is duplicated. Remove all but one match
631 for ( var i
= 1; i
< matches
.length
; i
++ ) {
632 oldText
= oldText
.replace( matches
[i
], '' );
635 newText
= oldText
.replace( categoryRegex
, newCategoryWikitext
);
637 return ajaxcat
.runHooks( newText
, 'afterChange', oldCatName
, catName
);
639 dialogDescription
: mw
.message( 'ajax-edit-category-summary', oldCatName
, catName
).escaped(),
640 editSummary
: editSummary
,
641 doneFn: function( unsaved
) {
642 // Remove input box & button
643 $link
.data( 'deleteButton' ).click();
645 // Update link text and href
646 $link
.show().text( catName
).attr( 'href', catTitle
.getUrl() );
648 $link
.data( 'origCat', oldCatName
).addClass( 'mw-changed-category' );
657 * Checks the API whether the category in question is a redirect.
658 * Also returns existance info (to color link red/blue)
659 * @param category {String} Name of category to resolve
660 * @param callback {Function} Called with 1 argument (mw.Title object)
662 resolveRedirects: function( category
, callback
) {
663 if ( !this.options
.resolveRedirects
) {
664 callback( category
, true );
667 var catTitle
= new mw
.Title( category
, catNsId
),
670 titles
: catTitle
.toString(),
675 $.getJSON( mw
.util
.wikiScript( 'api' ), queryVars
, function( json
) {
676 var redirect
= json
.query
.redirects
,
677 exists
= !json
.query
.pages
[-1];
679 // Register existence
680 mw
.Title
.exist
.set( catTitle
.toString(), exists
);
683 catTitle
= new mw
.Title( redirect
[0].to
).getMainText();
684 // Redirect existence as well (non-existant pages can't be redirects)
685 mw
.Title
.exist
.set( catTitle
.toString(), true );
687 callback( catTitle
);
692 * Append edit and remove buttons to a given category link
694 * @param DOMElement element Anchor element, to which the buttons should be appended.
695 * @return {mw.ajaxCategories}
697 createCatButtons: function( $element
) {
698 var deleteButton
= createButton( 'icon-close', mw
.msg( 'ajax-remove-category' ) ),
699 editButton
= createButton( 'icon-edit', mw
.msg( 'ajax-edit-category' ) ),
700 saveButton
= createButton( 'icon-tick', mw
.msg( 'ajax-confirm-save' ) ).hide(),
703 deleteButton
.click( this.handleDeleteLink
);
704 editButton
.click( ajaxcat
.createEditInterface
);
706 $element
.after( deleteButton
).after( editButton
);
708 // Save references to all links and buttons
710 deleteButton
: deleteButton
,
711 editButton
: editButton
,
712 saveButton
: saveButton
721 * Append spinner wheel to element.
722 * @param $el {jQuery}
723 * @return {mw.ajaxCategories}
725 addProgressIndicator: function( $el
) {
726 $el
.append( $( '<div>' ).addClass( 'mw-ajax-loader' ) );
731 * Find and remove spinner wheel from inside element.
732 * @param $el {jQuery}
733 * @return {mw.ajaxCategories}
735 removeProgressIndicator: function( $el
) {
736 $el
.find( '.mw-ajax-loader' ).remove();
741 * Parse the DOM $container and build a list of
742 * present categories.
744 * @return {Array} All categories.
746 getCats: function() {
747 var cats
= this.options
.$container
748 .find( this.options
.categoryLinkSelector
)
750 return $.trim( $( this ).text() );
756 * Check whether a passed category is present in the DOM.
758 * @param newCat {String} Category name to be checked for.
761 containsCat: function( newCat
) {
762 newCat
= $.ucFirst( newCat
);
764 $.each( this.getCats(), function(i
, cat
) {
765 if ( $.ucFirst( cat
) === newCat
) {
767 // Stop once we have a match
775 * Execute or queue a category delete.
777 * @param $link {jQuery}
781 handleCategoryDelete: function( $link
, category
) {
782 var categoryRegex
= buildRegex( category
, true ),
786 modFn: function( oldText
) {
787 var newText
= ajaxcat
.runHooks( oldText
, 'beforeDelete', category
);
788 newText
= newText
.replace( categoryRegex
, '' );
790 if ( newText
=== oldText
) {
791 ajaxcat
.showError( mw
.msg( 'ajax-remove-category-error' ) );
795 return ajaxcat
.runHooks( newText
, 'afterDelete', category
);
797 dialogDescription
: mw
.message( 'ajax-remove-category-summary', category
).escaped(),
798 editSummary
: '-[[' + new mw
.Title( category
, catNsId
) + ']]',
799 doneFn: function( unsaved
) {
801 $link
.addClass( 'mw-removed-category' );
803 $link
.parent().remove();
812 * Takes a category link element
813 * and strips all data from it.
815 * @param $link {jQuery}
816 * @param del {Boolean}
817 * @param dontRestoreText {Boolean}
820 resetCatLink: function( $link
, del
, dontRestoreText
) {
821 $link
.removeClass( 'mw-removed-category mw-added-category mw-changed-category' );
822 var data
= $link
.data();
824 if ( typeof data
.stashIndex
=== 'number' ) {
825 this.removeStashItem( data
.stashIndex
);
828 $link
.parent().remove();
831 if ( data
.origCat
&& !dontRestoreText
) {
832 var catTitle
= new mw
.Title( data
.origCat
, catNsId
);
833 $link
.text( catTitle
.getMainText() );
834 $link
.attr( 'href', catTitle
.getUrl() );
841 saveButton
: data
.saveButton
,
842 deleteButton
: data
.deleteButton
,
843 editButton
: data
.editButton
848 * Do the actual edit.
849 * Gets token & text from api, runs it through fn
850 * and saves it with summary.
851 * @param page {String} Pagename
852 * @param fn {Function} edit function
853 * @param summary {String}
854 * @param doneFn {String} Callback after all is done
856 doEdit: function( page
, fn
, summary
, doneFn
) {
857 // Get an edit token for the page.
860 prop
: 'info|revisions',
863 rvprop
: 'content|timestamp',
868 mw
.util
.wikiScript( 'api' ),
871 var infos
= reply
.query
.pages
;
872 $.each( infos
, function( pageid
, data
) {
873 var token
= data
.edittoken
,
874 timestamp
= data
.revisions
[0].timestamp
,
875 oldText
= data
.revisions
[0]['*'],
876 nowikiKey
= mw
.user
.generateId(), // Unique ID for nowiki replacement
877 nowikiFragments
= []; // Nowiki fragments will be stored here during the changes
879 // Replace all nowiki parts with unique keys..
880 oldText
= replaceNowikis( oldText
, nowikiKey
, nowikiFragments
);
882 // ..then apply the changes to the page text..
883 var newText
= fn( oldText
);
884 if ( newText
=== false ) {
888 // ..and restore the nowiki parts back.
889 newText
= restoreNowikis( newText
, nowikiKey
, nowikiFragments
);
897 basetimestamp
: timestamp
,
902 mw
.util
.wikiScript( 'api' ),
907 .error( function( xhr
, text
, error
) {
908 ajaxcat
.showError( mw
.msg( 'ajax-api-error', text
, error
) );
913 ).error( function( xhr
, text
, error
) {
914 ajaxcat
.showError( mw
.msg( 'ajax-api-error', text
, error
) );
919 * This gets called by all action buttons
920 * Displays a dialog to confirm the action
921 * Afterwards do the actual edit.
923 * @param props {Object}:
924 * - modFn {Function} text-modifying function
925 * - dialogDescription {String} Changes done (HTML in the dialog)
926 * - editSummary {String} Changes done (text for edit summary)
927 * - doneFn {Function} callback after everything is done
930 * @return {mw.ajaxCategories}
932 doConfirmEdit: function( props
) {
933 var summaryHolder
, reasonBox
, dialog
, submitFunction
,
942 // Check whether to use multiEdit mode:
943 if ( this.options
.multiEdit
&& props
.action
!== 'all' ) {
947 .data( 'stashIndex', this.stash
.fns
.length
)
948 .data( 'summary', props
.dialogDescription
);
950 this.stash
.dialogDescriptions
.push( props
.dialogDescription
);
951 this.stash
.editSummaries
.push( props
.editSummary
);
952 this.stash
.fns
.push( props
.modFn
);
954 this.saveAllButton
.show();
955 this.cancelAllButton
.show();
957 // Clear input field after action
958 ajaxcat
.addContainer
.find( '.mw-addcategory-input' ).val( '' );
960 // This only does visual changes, fire done and return.
961 props
.doneFn( true );
965 // Summary of the action to be taken
966 summaryHolder
= $( '<p>' )
967 .html( '<strong>' + mw
.msg( 'ajax-category-question' ) + '</strong><br/>' + props
.dialogDescription
);
970 reasonBox
= $( '<input type="text" size="45"></input>' )
971 .addClass( 'mw-ajax-confirm-reason' );
973 // Produce a confirmation dialog
974 dialog
= $( '<div>' )
975 .addClass( 'mw-ajax-confirm-dialog' )
976 .attr( 'title', mw
.msg( 'ajax-confirm-title' ) )
977 .append( summaryHolder
)
978 .append( reasonBox
);
981 submitFunction = function() {
982 ajaxcat
.addProgressIndicator( dialog
);
984 mw
.config
.get( 'wgPageName' ),
986 props
.editSummary
+ ': ' + reasonBox
.val(),
990 // Clear input field after successful edit
991 ajaxcat
.addContainer
.find( '.mw-addcategory-input' ).val( '' );
993 dialog
.dialog( 'close' );
994 ajaxcat
.removeProgressIndicator( dialog
);
999 buttons
[mw
.msg( 'ajax-confirm-save' )] = submitFunction
;
1001 dialog
.dialog( dialogOptions
).keyup( function( e
) {
1003 if ( e
.keyCode
=== 13 ) {
1012 * @param index {Number|jQuery} Stash index or jQuery object of stash item.
1013 * @return {mw.ajaxCategories}
1015 removeStashItem: function( i
) {
1016 if ( typeof i
!== 'number' ) {
1017 i
= i
.data( 'stashIndex' );
1021 delete this.stash
.fns
[i
];
1022 delete this.stash
.dialogDescriptions
[i
];
1025 if ( $.isEmpty( this.stash
.fns
) ) {
1026 this.stash
.fns
= [];
1027 this.stash
.dialogDescriptions
= [];
1028 this.stash
.editSummaries
= [];
1029 this.saveAllButton
.hide();
1030 this.cancelAllButton
.hide();
1036 * Reset all data from the category links and the stash.
1038 * @param del {Boolean} Delete any category links with .mw-removed-category
1039 * @return {mw.ajaxCategories}
1041 resetAll: function( del
) {
1042 var $links
= this.options
.$container
.find( this.options
.categoryLinkSelector
),
1047 $del
= $links
.filter( '.mw-removed-category' ).parent();
1050 $links
.each( function() {
1051 ajaxcat
.resetCatLink( $( this ), false, del
);
1056 this.options
.$container
.find( '#mw-hidden-catlinks' ).remove();
1063 * Currently available: beforeAdd, beforeChange, beforeDelete,
1064 * afterAdd, afterChange, afterDelete
1065 * If the hook function returns false, all changes are aborted.
1067 * @param string type Type of hook to add
1068 * @param function fn Hook function. The following vars are passed to it:
1069 * 1. oldtext: The wikitext before the hook
1070 * 2. category: The deleted, added, or changed category
1071 * 3. (only for beforeChange/afterChange): newcategory
1073 addHook: function( type
, fn
) {
1074 if ( !this.hooks
[type
] || !$.isFunction( fn
) ) {
1078 this.hooks
[type
].push( fn
);
1084 * Open a dismissable error dialog
1086 * @param string str The error description
1088 showError: function( str
) {
1089 var oldDialog
= $( '.mw-ajax-confirm-dialog' ),
1094 title
: mw
.msg( 'ajax-error-title' )
1097 this.removeProgressIndicator( oldDialog
);
1098 oldDialog
.dialog( 'close' );
1100 var dialog
= $( '<div>' ).text( str
);
1102 mw
.util
.$content
.append( dialog
);
1104 buttons
[mw
.msg( 'ajax-confirm-ok' )] = function( e
) {
1105 dialog
.dialog( 'close' );
1108 dialog
.dialog( dialogOptions
).keyup( function( e
) {
1109 if ( e
.keyCode
=== 13 ) {
1110 dialog
.dialog( 'close' );
1119 * @param categoryNew
1122 runHooks: function( oldtext
, type
, category
, categoryNew
) {
1123 // No hooks registered
1124 if ( !this.hooks
[type
] ) {
1127 for ( var i
= 0; i
< this.hooks
[type
].length
; i
++ ) {
1128 oldtext
= this.hooks
[type
][i
]( oldtext
, category
, categoryNew
);
1129 if ( oldtext
=== false ) {
1130 this.showError( mw
.msg( 'ajax-category-hook-error', category
) );