3 // * The edit summary should contain the added/removed category name too.
4 // Something like: "Category:Foo added. Reason"
5 // Requirement: Be able to get msg with lang option.
6 // * Handle uneditable cats. Needs serverside changes!
7 // * Add Hooks for change, delete, add
8 // * Add Hooks for soft redirect
9 // * Handle normal redirects
10 // * Simple / MultiEditMode
14 var ajaxCategories = function ( options
) {
15 // TODO grab these out of option object.
17 var catLinkWrapper
= '<li/>';
18 var $container
= $( '.catlinks' );
20 var categoryLinkSelector
= '#mw-normal-catlinks li a';
23 var _catElements
= {};
25 var namespaceIds
= mw
.config
.get( 'wgNamespaceIds' )
26 var categoryNamespaceId
= namespaceIds
['category'];
27 var categoryNamespace
= mw
.config
.get( 'wgFormattedNamespaces' )[categoryNamespaceId
];
31 * Helper function for $.fn.suggestion
33 * @param string Query string.
35 _fetchSuggestions = function ( query
) {
37 // ignore bad characters, they will be stripped out
38 var catName
= _stripIllegals( $( this ).val() );
39 var request
= $.ajax( {
40 url
: mw
.util
.wikiScript( 'api' ),
44 'apnamespace': categoryNamespaceId
,
49 success: function( data
) {
50 // Process data.query.allpages into an array of titles
51 var pages
= data
.query
.allpages
;
54 $.each( pages
, function( i
, page
) {
55 var title
= page
.title
.split( ':', 2 )[1];
56 titleArr
.push( title
);
59 $( _this
).suggestions( 'suggestions', titleArr
);
66 _stripIllegals = function ( cat
) {
67 return cat
.replace( /[\x00-\x1f\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f]+/g, '' );
71 * Insert a newly added category into the DOM
73 * @param string category name.
74 * @param boolean isHidden (unused)
76 _insertCatDOM = function ( cat
, isHidden
) {
77 // User can implicitely state a sort key.
78 // Remove before display
79 cat
= cat
.replace(/\|.*/, '');
81 // strip out bad characters
82 cat
= _stripIllegals ( cat
);
84 if ( $.isEmpty( cat
) || _containsCat( cat
) ) {
88 var $catLinkWrapper
= $( catLinkWrapper
);
89 var $anchor
= $( '<a/>' ).append( cat
);
90 $catLinkWrapper
.append( $anchor
);
91 $anchor
.attr( { target
: "_blank", href
: _catLink( cat
) } );
93 $container
.find( '#mw-hidden-catlinks ul' ).append( $catLinkWrapper
);
95 $container
.find( '#mw-normal-catlinks ul' ).append( $catLinkWrapper
);
97 _createCatButtons( $anchor
.get(0) );
100 _makeSuggestionBox = function ( prefill
, callback
, buttonVal
) {
101 // Create add category prompt
102 var promptContainer
= $( '<div class="mw-addcategory-prompt"/>' );
103 var promptTextbox
= $( '<input type="text" size="45" class="mw-addcategory-input"/>' );
104 if ( prefill
!== '' ) {
105 promptTextbox
.val( prefill
);
107 var addButton
= $( '<input type="button" class="mw-addcategory-button"/>' );
108 addButton
.val( buttonVal
);
110 addButton
.click( callback
);
112 promptTextbox
.suggestions( {
113 'fetch':_fetchSuggestions
,
114 'cancel': function() {
116 // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of "unknown" for typeof
117 if ( req
&& ( typeof req
.abort
!== 'unknown' ) && ( typeof req
.abort
!== 'undefined' ) && req
.abort
) {
123 promptTextbox
.suggestions();
125 promptContainer
.append( promptTextbox
);
126 promptContainer
.append( addButton
);
128 return promptContainer
;
132 * Build URL for passed Category
134 * @param string category name.
135 * @return string Valid URL
137 _catLink = function ( cat
) {
138 return mw
.util
.wikiGetlink( categoryNamespace
+ ':' + $.ucFirst( cat
) );
142 * Parse the DOM $container and build a list of
145 * @return array Array of all categories
147 _getCats = function () {
148 return $container
.find( categoryLinkSelector
).map( function() { return $.trim( $( this ).text() ); } );
152 * Check whether a passed category is present in the DOM
154 * @return boolean True for exists
156 _containsCat = function ( cat
) {
157 return _getCats().filter( function() { return $.ucFirst(this) == $.ucFirst(cat
); } ).length
!== 0;
161 * This get's called by all action buttons
162 * Displays a dialog to confirm the action
163 * Afterwords do the actual edit
165 * @param function fn text-modifying function
166 * @param string actionSummary Changes done
167 * @param function fn doneFn callback after everything is done
168 * @return boolean True for exists
170 _confirmEdit = function ( fn
, actionSummary
, doneFn
, all
) {
171 // Check whether to use multiEdit mode
172 if ( mw
.config
.get('AJAXCategoriesMulti') && !all
) {
174 _stash
.summaries
.push( actionSummary
);
175 _stash
.fns
.push( fn
);
176 _stash
.doneFns
.push( doneFn
);
178 // Make sure we have a save button
179 if ( !_saveAllButton
) {
180 //TODO Make more clickable
181 _saveAllButton
= _createButton( 'icon-tick',
182 mw
.msg( 'ajax-confirm-save-all' ),
184 mw
.msg( 'ajax-confirm-save-all' )
186 _saveAllButton
.click( _handleStashedCategories
);
191 // Produce a confirmation dialog
192 var dialog
= $( '<div/>' );
194 dialog
.addClass( 'mw-ajax-confirm-dialog' );
195 dialog
.attr( 'title', mw
.msg( 'ajax-confirm-title' ) );
198 var confirmIntro
= $( '<p/>' );
199 confirmIntro
.text( mw
.msg( 'ajax-confirm-prompt' ) );
200 dialog
.append( confirmIntro
);
202 // Summary of the action to be taken
203 var summaryHolder
= $( '<p/>' );
204 var summaryLabel
= $( '<strong/>' );
205 summaryLabel
.text( mw
.msg( 'ajax-confirm-actionsummary' ) + " " );
206 summaryHolder
.text( actionSummary
);
207 summaryHolder
.prepend( summaryLabel
);
208 dialog
.append( summaryHolder
);
211 var reasonBox
= $( '<input type="text" size="45" />' );
212 reasonBox
.addClass( 'mw-ajax-confirm-reason' );
213 dialog
.append( reasonBox
);
216 var submitButton
= $( '<input type="button"/>' );
217 submitButton
.val( mw
.msg( 'ajax-confirm-save' ) );
219 var submitFunction = function() {
220 _addProgressIndicator( dialog
);
222 mw
.config
.get( 'wgPageName' ),
227 dialog
.dialog( 'close' );
228 _removeProgressIndicator( dialog
);
234 buttons
[mw
.msg( 'ajax-confirm-save' )] = submitFunction
;
235 var dialogOptions
= {
241 $( '#catlinks' ).prepend( dialog
);
242 dialog
.dialog( dialogOptions
);
246 * When multiEdit mode is enabled,
247 * this is called when the user clicks "save all"
248 * Combines the summaries and edit functions
250 _handleStashedCategories = function() {
254 //TODO do I need a space?
255 var summary
= _stash
.summaries
.join(' ');
256 var combinedFn = function( oldtext
) {
257 // Run the text through all action functions
259 for ( var i
= 0; i
< fns
.length
; i
++ ) {
260 newtext
= fns
[i
]( newtext
);
264 var doneFn = function() {
265 //Remove saveAllButton
266 _saveAllButton
.remove();
267 _saveAllButton
= undefined;
272 _doEdit = function ( page
, fn
, summary
, doneFn
) {
273 // Get an edit token for the page.
276 'prop':'info|revisions',
279 'rvprop':'content|timestamp',
283 $.get( mw
.util
.wikiScript( 'api' ), getTokenVars
,
285 var infos
= reply
.query
.pages
;
288 function( pageid
, data
) {
289 var token
= data
.edittoken
;
290 var timestamp
= data
.revisions
[0].timestamp
;
291 var oldText
= data
.revisions
[0]['*'];
293 var newText
= fn( oldText
);
295 if ( newText
=== false ) return;
303 'basetimestamp':timestamp
,
307 $.post( mw
.util
.wikiScript( 'api' ), postEditVars
, doneFn
, 'json' );
315 * Append spinner wheel to element
316 * @param DOMObject element.
318 _addProgressIndicator = function ( elem
) {
319 var indicator
= $( '<div/>' );
321 indicator
.addClass( 'mw-ajax-loader' );
323 elem
.append( indicator
);
327 * Find and remove spinner wheel from inside element
328 * @param DOMObject parent element.
330 _removeProgressIndicator = function ( elem
) {
331 elem
.find( '.mw-ajax-loader' ).remove();
335 * Makes regex string caseinsensitive.
336 * Useful when 'i' flag can't be used.
337 * Return stuff like [Ff][Oo][Oo]
338 * @param string Regex string.
339 * @return string Processed regex string
341 _makeCaseInsensitive = function ( string
) {
343 for (var i
=0; i
< string
.length
; i
++) {
344 newString
+= '[' + string
[i
].toUpperCase() + string
[i
].toLowerCase() + ']';
348 _buildRegex = function ( category
) {
349 // Build a regex that matches legal invocations of that category.
350 var categoryNSFragment
= '';
351 $.each( namespaceIds
, function( name
, id
) {
353 // The parser accepts stuff like cATegORy,
354 // we need to do the same
355 categoryNSFragment
+= '|' + _makeCaseInsensitive ( $.escapeRE(name
) );
358 categoryNSFragment
= categoryNSFragment
.substr( 1 ); // Remove leading |
361 var titleFragment
= $.escapeRE(category
);
363 firstChar
= category
.charAt( 0 );
364 firstChar
= '[' + firstChar
.toUpperCase() + firstChar
.toLowerCase() + ']';
365 titleFragment
= firstChar
+ category
.substr( 1 );
366 var categoryRegex
= '\\[\\[(' + categoryNSFragment
+ '):' + titleFragment
+ '(\\|[^\\]]*)?\\]\\]';
368 return new RegExp( categoryRegex
, 'g' );
371 _handleEditLink = function ( e
) {
373 var $this = $( this );
374 var $link
= $this.parent().find( 'a:not(.icon)' );
375 var category
= $link
.text();
377 var $input
= _makeSuggestionBox( category
, _handleCategoryEdit
, mw
.msg( 'ajax-confirm-save' ) );
378 $link
.after( $input
).hide();
379 _catElements
[category
].editButton
.hide();
380 _catElements
[category
].deleteButton
.unbind('click').click( function() {
383 _catElements
[category
].editButton
.show();
384 $( this ).unbind('click').click( _handleDeleteLink
);
388 _handleAddLink = function ( e
) {
391 $container
.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).toggle();
394 _handleDeleteLink = function ( e
) {
397 var $this = $( this );
398 var $link
= $this.parent().find( 'a:not(.icon)' );
399 var category
= $link
.text();
401 categoryRegex
= _buildRegex( category
);
403 var summary
= mw
.msg( 'ajax-remove-category-summary', category
);
406 function( oldText
) {
407 //TODO Cleanup whitespace safely?
408 var newText
= oldText
.replace( categoryRegex
, '' );
410 if ( newText
== oldText
) {
411 var error
= mw
.msg( 'ajax-remove-category-error' );
413 _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
414 $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
422 $this.parent().remove();
427 _handleCategoryAdd = function ( e
) {
428 // Grab category text
429 var category
= $( this ).parent().find( '.mw-addcategory-input' ).val();
430 category
= $.ucFirst( category
);
432 if ( _containsCat(category
) ) {
433 _showError( mw
.msg( 'ajax-category-already-present' ) );
436 var appendText
= "\n[[" + categoryNamespace
+ ":" + category
+ "]]\n";
437 var summary
= mw
.msg( 'ajax-add-category-summary', category
);
440 function( oldText
) { return oldText
+ appendText
},
443 _insertCatDOM( category
, false );
448 _handleCategoryEdit = function ( e
) {
451 // Grab category text
452 var categoryNew
= $( this ).parent().find( '.mw-addcategory-input' ).val();
453 categoryNew
= $.ucFirst( categoryNew
);
455 var $this = $( this );
456 var $link
= $this.parent().parent().find( 'a:not(.icon)' );
457 var category
= $link
.text();
459 // User didn't change anything. Just close the box
460 if ( category
== categoryNew
) {
461 $this.parent().remove();
465 categoryRegex
= _buildRegex( category
);
467 var summary
= mw
.msg( 'ajax-edit-category-summary', category
, categoryNew
);
470 function( oldText
) {
471 var matches
= oldText
.match( categoryRegex
);
473 //Old cat wasn't found, likely to be transcluded
474 if ( !$.isArray( matches
) ) {
475 var error
= mw
.msg( 'ajax-edit-category-error' );
477 _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
478 $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
481 var sortkey
= matches
[0].replace( categoryRegex
, '$2' );
482 var newCategoryString
= "[[" + categoryNamespace
+ ":" + categoryNew
+ sortkey
+ ']]';
484 if (matches
.length
> 1) {
485 // The category is duplicated.
486 // Remove all but one match
487 for (var i
= 1; i
< matches
.length
; i
++) {
488 oldText
= oldText
.replace( matches
[i
], '');
491 var newText
= oldText
.replace( categoryRegex
, newCategoryString
);
497 // Remove input box & button
498 $this.parent().remove();
500 // Update link text and href
501 $link
.show().text( categoryNew
).attr( 'href', _catLink( categoryNew
) );
507 * Open a dismissable error dialog
509 * @param string str The error description
511 _showError = function ( str
) {
512 var dialog
= $( '<div/>' );
515 $( '#bodyContent' ).append( dialog
);
518 buttons
[mw
.msg( 'ajax-error-dismiss' )] = function( e
) {
519 dialog
.dialog( 'close' );
521 var dialogOptions
= {
524 'title' : mw
.msg( 'ajax-error-title' )
527 dialog
.dialog( dialogOptions
);
531 * Manufacture iconed button, with or without text
533 * @param string icon The icon class.
534 * @param string title Title attribute.
535 * @param string className (optional) Additional classes to be added to the button.
536 * @param string text (optional) Text of button.
538 * @return jQueryObject The button
540 _createButton = function ( icon
, title
, className
, text
){
541 var $button
= $( '<a>' ).addClass( className
|| '' )
542 .attr('title', title
);
545 var $icon
= $( '<a>' ).addClass( 'icon ' + icon
);
546 $button
.addClass( 'icon-parent' ).append( $icon
).append( text
);
548 $button
.addClass( 'icon ' + icon
);
554 * Append edit and remove buttons to a given category link
556 * @param DOMElement element Anchor element, to which the buttons should be appended.
558 _createCatButtons = function( element
) {
559 // Create remove & edit buttons
560 var deleteButton
= _createButton('icon-close', mw
.msg( 'ajax-remove-category' ) );
561 var editButton
= _createButton('icon-edit', mw
.msg( 'ajax-edit-category' ) );
564 var saveButton
= _createButton('icon-tick', mw
.msg( 'ajax-confirm-save' ) ).hide();
566 deleteButton
.click( _handleDeleteLink
);
567 editButton
.click( _handleEditLink
);
569 $( element
).after( deleteButton
).after( editButton
);
571 //Save references to all links and buttons
572 _catElements
[$( element
).text()] = {
574 parent
: $( element
).parent(),
575 saveButton
: saveButton
,
576 deleteButton
: deleteButton
,
577 editButton
: editButton
580 this.setup = function () {
581 // Could be set by gadgets like HotCat etc.
582 if ( mw
.config
.get('disableAJAXCategories') ) {
585 // Only do it for articles.
586 if ( !mw
.config
.get( 'wgIsArticle' ) ) return;
588 var clElement
= $( '#mw-normal-catlinks' );
590 // Unhide hidden category holders.
591 $('#mw-hidden-catlinks').show();
593 // Create [Add Category] link
594 var addLink
= _createButton('icon-add',
595 mw
.msg( 'ajax-add-category' ),
596 'mw-ajax-addcategory',
597 mw
.msg( 'ajax-add-category' )
599 addLink
.click( _handleAddLink
);
600 clElement
.append( addLink
);
602 // Create add category prompt
603 var promptContainer
= _makeSuggestionBox( '', _handleCategoryAdd
, mw
.msg( 'ajax-add-category-submit' ) );
604 promptContainer
.hide();
606 // Create edit & delete link for each category.
607 $( '#catlinks li a' ).each( function( e
) {
608 _createCatButtons( this );
611 clElement
.append( promptContainer
);
620 // Now make a new version
621 mw
.ajaxCategories
= new ajaxCategories();
623 // Executing only on doc.ready, so that everyone
624 // gets a chance to set mw.config.set('disableAJAXCategories')
625 $( document
).ready( mw
.ajaxCategories
.setup() );
627 } )( jQuery
, mediaWiki
);