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 soft redirect
8 // * Handle normal redirects
10 // * Fixme on narrow windows
15 var ajaxCategories = function ( options
) {
16 // TODO grab these out of option object.
18 var catLinkWrapper
= '<li/>';
19 var $container
= $( '.catlinks' );
20 var $containerNormal
= $( '#mw-normal-catlinks' );
22 var categoryLinkSelector
= '#mw-normal-catlinks li a';
25 var _catElements
= {};
27 var namespaceIds
= mw
.config
.get( 'wgNamespaceIds' );
28 var categoryNamespaceId
= namespaceIds
['category'];
29 var categoryNamespace
= mw
.config
.get( 'wgFormattedNamespaces' )[categoryNamespaceId
];
32 var _multiEdit
= ( wgUserGroups
.indexOf("user") != -1 );
35 * Helper function for $.fn.suggestion
37 * @param string Query string.
39 _fetchSuggestions = function ( query
) {
41 // ignore bad characters, they will be stripped out
42 var catName
= _stripIllegals( $( this ).val() );
43 var request
= $.ajax( {
44 url
: mw
.util
.wikiScript( 'api' ),
48 'apnamespace': categoryNamespaceId
,
53 success: function( data
) {
54 // Process data.query.allpages into an array of titles
55 var pages
= data
.query
.allpages
;
58 $.each( pages
, function( i
, page
) {
59 var title
= page
.title
.split( ':', 2 )[1];
60 titleArr
.push( title
);
63 $( _this
).suggestions( 'suggestions', titleArr
);
70 _stripIllegals = function ( cat
) {
71 return cat
.replace( /[\x00-\x1f\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f]+/g, '' );
75 * Insert a newly added category into the DOM
77 * @param string category name.
78 * @param boolean isHidden (unused)
80 _insertCatDOM = function ( cat
, isHidden
) {
81 // User can implicitely state a sort key.
82 // Remove before display
83 cat
= cat
.replace(/\|.*/, '');
85 // strip out bad characters
86 cat
= _stripIllegals ( cat
);
88 if ( $.isEmpty( cat
) || this.containsCat( cat
) ) {
92 var $catLinkWrapper
= $( catLinkWrapper
);
93 var $anchor
= $( '<a/>' ).append( cat
);
94 $catLinkWrapper
.append( $anchor
);
95 $anchor
.attr( { target
: "_blank", href
: _catLink( cat
) } );
97 $container
.find( '#mw-hidden-catlinks ul' ).append( $catLinkWrapper
);
99 $container
.find( '#mw-normal-catlinks ul' ).append( $catLinkWrapper
);
101 _createCatButtons( $anchor
.get(0) );
104 _makeSuggestionBox = function ( prefill
, callback
, buttonVal
) {
105 // Create add category prompt
106 var promptContainer
= $( '<div class="mw-addcategory-prompt"/>' );
107 var promptTextbox
= $( '<input type="text" size="45" class="mw-addcategory-input"/>' );
108 if ( prefill
!== '' ) {
109 promptTextbox
.val( prefill
);
111 var addButton
= $( '<input type="button" class="mw-addcategory-button"/>' );
112 addButton
.val( buttonVal
);
114 addButton
.click( callback
);
116 promptTextbox
.suggestions( {
117 'fetch':_fetchSuggestions
,
118 'cancel': function() {
120 // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of "unknown" for typeof
121 if ( req
&& ( typeof req
.abort
!== 'unknown' ) && ( typeof req
.abort
!== 'undefined' ) && req
.abort
) {
127 promptTextbox
.suggestions();
129 promptContainer
.append( promptTextbox
);
130 promptContainer
.append( addButton
);
132 return promptContainer
;
136 * Build URL for passed Category
138 * @param string category name.
139 * @return string Valid URL
141 _catLink = function ( cat
) {
142 return mw
.util
.wikiGetlink( categoryNamespace
+ ':' + $.ucFirst( cat
) );
146 * Parse the DOM $container and build a list of
149 * @return array Array of all categories
151 _getCats = function () {
152 return $container
.find( categoryLinkSelector
).map( function() { return $.trim( $( this ).text() ); } );
156 * Check whether a passed category is present in the DOM
158 * @return boolean True for exists
160 this.containsCat = function ( cat
) {
161 return _getCats().filter( function() { return $.ucFirst(this) == $.ucFirst(cat
); } ).length
!== 0;
165 * This get's called by all action buttons
166 * Displays a dialog to confirm the action
167 * Afterwords do the actual edit
169 * @param function fn text-modifying function
170 * @param string actionSummary Changes done
171 * @param function fn doneFn callback after everything is done
172 * @return boolean True for exists
174 _confirmEdit = function ( fn
, actionSummary
, doneFn
, all
) {
175 // Check whether to use multiEdit mode
176 if ( _multiEdit
&& !all
) {
178 _stash
.summaries
.push( actionSummary
);
179 _stash
.fns
.push( fn
);
181 //TODO add Cancel button
182 _saveAllButton
.show();
183 //_cancelAllButton.show();
185 // This only does visual changes
189 // Produce a confirmation dialog
190 var dialog
= $( '<div/>' );
192 dialog
.addClass( 'mw-ajax-confirm-dialog' );
193 dialog
.attr( 'title', mw
.msg( 'ajax-confirm-title' ) );
196 var confirmIntro
= $( '<p/>' );
197 confirmIntro
.text( mw
.msg( 'ajax-confirm-prompt' ) );
198 dialog
.append( confirmIntro
);
200 // Summary of the action to be taken
201 var summaryHolder
= $( '<p/>' );
202 var summaryLabel
= $( '<strong/>' );
203 summaryLabel
.text( mw
.msg( 'ajax-confirm-actionsummary' ) + " " );
204 summaryHolder
.text( actionSummary
);
205 summaryHolder
.prepend( summaryLabel
);
206 dialog
.append( summaryHolder
);
209 var reasonBox
= $( '<input type="text" size="45" />' );
210 reasonBox
.addClass( 'mw-ajax-confirm-reason' );
211 dialog
.append( reasonBox
);
214 var submitButton
= $( '<input type="button"/>' );
215 submitButton
.val( mw
.msg( 'ajax-confirm-save' ) );
217 var submitFunction = function() {
218 _addProgressIndicator( dialog
);
220 mw
.config
.get( 'wgPageName' ),
225 dialog
.dialog( 'close' );
226 _removeProgressIndicator( dialog
);
232 buttons
[mw
.msg( 'ajax-confirm-save' )] = submitFunction
;
233 var dialogOptions
= {
239 $( '#catlinks' ).prepend( dialog
);
240 dialog
.dialog( dialogOptions
);
244 * When multiEdit mode is enabled,
245 * this is called when the user clicks "save all"
246 * Combines the summaries and edit functions
248 _handleStashedCategories = function() {
253 var summary
= _stash
.summaries
.join('. ');
254 var combinedFn = function( oldtext
) {
255 // Run the text through all action functions
257 for ( var i
= 0; i
< fns
.length
; i
++ ) {
258 newtext
= fns
[i
]( newtext
);
262 var doneFn
= _resetToActual
;
264 _confirmEdit( combinedFn
, summary
, doneFn
, true );
267 _resetToActual = function() {
268 //Remove saveAllButton
269 _saveAllButton
.hide();
270 _cancelAllButton
.hide();
274 _stash
.summaries
= [];
277 $container
.find('.mw-removed-category').parent().remove();
278 // Any link with $link.css('text-decoration', 'line-through');
279 // needs to be removed
283 _doEdit = function ( page
, fn
, summary
, doneFn
) {
284 // Get an edit token for the page.
287 'prop':'info|revisions',
290 'rvprop':'content|timestamp',
294 $.get( mw
.util
.wikiScript( 'api' ), getTokenVars
,
296 var infos
= reply
.query
.pages
;
299 function( pageid
, data
) {
300 var token
= data
.edittoken
;
301 var timestamp
= data
.revisions
[0].timestamp
;
302 var oldText
= data
.revisions
[0]['*'];
304 var newText
= fn( oldText
);
306 if ( newText
=== false ) return;
314 'basetimestamp':timestamp
,
318 $.post( mw
.util
.wikiScript( 'api' ), postEditVars
, doneFn
, 'json' );
326 * Append spinner wheel to element
327 * @param DOMObject element.
329 _addProgressIndicator = function ( elem
) {
330 var indicator
= $( '<div/>' );
332 indicator
.addClass( 'mw-ajax-loader' );
334 elem
.append( indicator
);
338 * Find and remove spinner wheel from inside element
339 * @param DOMObject parent element.
341 _removeProgressIndicator = function ( elem
) {
342 elem
.find( '.mw-ajax-loader' ).remove();
346 * Makes regex string caseinsensitive.
347 * Useful when 'i' flag can't be used.
348 * Return stuff like [Ff][Oo][Oo]
349 * @param string Regex string.
350 * @return string Processed regex string
352 _makeCaseInsensitive = function ( string
) {
354 for (var i
=0; i
< string
.length
; i
++) {
355 newString
+= '[' + string
[i
].toUpperCase() + string
[i
].toLowerCase() + ']';
359 _buildRegex = function ( category
) {
360 // Build a regex that matches legal invocations of that category.
361 var categoryNSFragment
= '';
362 $.each( namespaceIds
, function( name
, id
) {
364 // The parser accepts stuff like cATegORy,
365 // we need to do the same
366 categoryNSFragment
+= '|' + _makeCaseInsensitive ( $.escapeRE(name
) );
369 categoryNSFragment
= categoryNSFragment
.substr( 1 ); // Remove leading |
372 var titleFragment
= $.escapeRE(category
);
374 firstChar
= category
.charAt( 0 );
375 firstChar
= '[' + firstChar
.toUpperCase() + firstChar
.toLowerCase() + ']';
376 titleFragment
= firstChar
+ category
.substr( 1 );
377 var categoryRegex
= '\\[\\[(' + categoryNSFragment
+ '):' + titleFragment
+ '(\\|[^\\]]*)?\\]\\]';
379 return new RegExp( categoryRegex
, 'g' );
382 _handleEditLink = function ( e
) {
384 var $this = $( this );
385 var $link
= $this.parent().find( 'a:not(.icon)' );
386 var category
= $link
.text();
388 var $input
= _makeSuggestionBox( category
,
390 _multiEdit
? mw
.msg( 'ajax-confirm-ok' ) : mw
.msg( 'ajax-confirm-save' )
392 $link
.after( $input
).hide();
393 _catElements
[category
].editButton
.hide();
394 _catElements
[category
].deleteButton
.unbind('click').click( function() {
397 _catElements
[category
].editButton
.show();
398 $( this ).unbind('click').click( _handleDeleteLink
);
402 _handleAddLink = function ( e
) {
405 $container
.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).toggle();
408 _handleDeleteLink = function ( e
) {
411 var $this = $( this );
412 var $link
= $this.parent().find( 'a:not(.icon)' );
413 var category
= $link
.text();
415 var categoryRegex
= _buildRegex( category
);
417 var summary
= mw
.msg( 'ajax-remove-category-summary', category
);
420 function( oldText
) {
421 newText
= _runHooks ( oldText
, 'beforeDelete' );
422 //TODO Cleanup whitespace safely?
423 var newText
= newText
.replace( categoryRegex
, '' );
425 if ( newText
== oldText
) {
426 var error
= mw
.msg( 'ajax-remove-category-error' );
428 _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
429 $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
436 function( unsaved
) {
438 //TODO Make revertable
439 $link
.addClass('.mw-removed-category');
441 $this.parent().remove();
447 _handleCategoryAdd = function ( e
) {
448 // Grab category text
449 var category
= $( this ).parent().find( '.mw-addcategory-input' ).val();
450 category
= $.ucFirst( category
);
452 if ( this.containsCat(category
) ) {
453 _showError( mw
.msg( 'ajax-category-already-present', category
) );
456 var appendText
= "\n[[" + categoryNamespace
+ ":" + category
+ "]]\n";
457 var summary
= mw
.msg( 'ajax-add-category-summary', category
);
460 function( oldText
) {
461 newText
= _runHooks ( oldText
, 'beforeAdd' );
462 return newText
+ appendText
;
466 $container
.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).toggle();
467 _insertCatDOM( category
, false );
472 _handleCategoryEdit = function ( e
) {
473 //FIXME: in MultiEdit Mode handle successive edits to same category
476 // Grab category text
477 var categoryNew
= $( this ).parent().find( '.mw-addcategory-input' ).val();
478 categoryNew
= $.ucFirst( categoryNew
);
480 var $this = $( this );
481 var $link
= $this.parent().parent().find( 'a:not(.icon)' );
482 var category
= $link
.text();
484 // User didn't change anything.
485 if ( category
== categoryNew
) {
486 _catElements
[category
].deleteButton
.click();
489 categoryRegex
= _buildRegex( category
);
491 var summary
= mw
.msg( 'ajax-edit-category-summary', category
, categoryNew
);
494 function( oldText
) {
495 newText
= _runHooks ( oldText
, 'beforeChange' );
497 var matches
= newText
.match( categoryRegex
);
499 //Old cat wasn't found, likely to be transcluded
500 if ( !$.isArray( matches
) ) {
501 var error
= mw
.msg( 'ajax-edit-category-error' );
503 _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
504 $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
507 var sortkey
= matches
[0].replace( categoryRegex
, '$2' );
508 var newCategoryString
= "[[" + categoryNamespace
+ ":" + categoryNew
+ sortkey
+ ']]';
510 if (matches
.length
> 1) {
511 // The category is duplicated.
512 // Remove all but one match
513 for (var i
= 1; i
< matches
.length
; i
++) {
514 oldText
= oldText
.replace( matches
[i
], '');
517 var newText
= oldText
.replace( categoryRegex
, newCategoryString
);
523 // Remove input box & button
524 _catElements
[category
].deleteButton
.click();
525 _catElements
[categoryNew
] = _catElements
[category
];
526 delete _catElements
[category
];
527 // Update link text and href
528 $link
.show().text( categoryNew
).attr( 'href', _catLink( categoryNew
) );
534 * Open a dismissable error dialog
536 * @param string str The error description
538 _showError = function ( str
) {
539 var dialog
= $( '<div/>' );
542 $( '#bodyContent' ).append( dialog
);
545 buttons
[mw
.msg( 'ajax-error-dismiss' )] = function( e
) {
546 dialog
.dialog( 'close' );
548 var dialogOptions
= {
551 'title' : mw
.msg( 'ajax-error-title' )
554 dialog
.dialog( dialogOptions
);
558 * Manufacture iconed button, with or without text
560 * @param string icon The icon class.
561 * @param string title Title attribute.
562 * @param string className (optional) Additional classes to be added to the button.
563 * @param string text (optional) Text of button.
565 * @return jQueryObject The button
567 _createButton = function ( icon
, title
, className
, text
){
568 var $button
= $( '<a>' ).addClass( className
|| '' )
569 .attr('title', title
);
572 var $icon
= $( '<a>' ).addClass( 'icon ' + icon
);
573 $button
.addClass( 'icon-parent' ).append( $icon
).append( text
);
575 $button
.addClass( 'icon ' + icon
);
581 * Append edit and remove buttons to a given category link
583 * @param DOMElement element Anchor element, to which the buttons should be appended.
585 _createCatButtons = function( element
) {
586 // Create remove & edit buttons
587 var deleteButton
= _createButton('icon-close', mw
.msg( 'ajax-remove-category' ) );
588 var editButton
= _createButton('icon-edit', mw
.msg( 'ajax-edit-category' ) );
591 var saveButton
= _createButton('icon-tick', mw
.msg( 'ajax-confirm-save' ) ).hide();
593 deleteButton
.click( _handleDeleteLink
);
594 editButton
.click( _handleEditLink
);
596 $( element
).after( deleteButton
).after( editButton
);
598 //Save references to all links and buttons
599 _catElements
[$( element
).text()] = {
601 parent
: $( element
).parent(),
602 saveButton
: saveButton
,
603 deleteButton
: deleteButton
,
604 editButton
: editButton
607 this.setup = function () {
608 // Could be set by gadgets like HotCat etc.
609 if ( mw
.config
.get('disableAJAXCategories') ) {
612 // Only do it for articles.
613 if ( !mw
.config
.get( 'wgIsArticle' ) ) return;
615 // Unhide hidden category holders.
616 $('#mw-hidden-catlinks').show();
618 // Create [Add Category] link
619 var addLink
= _createButton('icon-add',
620 mw
.msg( 'ajax-add-category' ),
621 'mw-ajax-addcategory',
622 mw
.msg( 'ajax-add-category' )
624 addLink
.click( _handleAddLink
);
625 $containerNormal
.append( addLink
);
627 // Create add category prompt
628 var promptContainer
= _makeSuggestionBox( '', _handleCategoryAdd
, mw
.msg( 'ajax-add-category-submit' ) );
629 promptContainer
.hide();
631 // Create edit & delete link for each category.
632 $( '#catlinks li a' ).each( function( e
) {
633 _createCatButtons( this );
636 $containerNormal
.append( promptContainer
);
638 //TODO Make more clickable
639 _saveAllButton
= _createButton( 'icon-tick',
640 mw
.msg( 'ajax-confirm-save-all' ),
642 mw
.msg( 'ajax-confirm-save-all' )
644 _cancelAllButton
= _createButton( 'icon-tick',
645 mw
.msg( 'ajax-confirm-save-all' ),
647 mw
.msg( 'ajax-confirm-save-all' )
649 _saveAllButton
.click( _handleStashedCategories
).hide();
650 _cancelAllButton
.hide();
652 $containerNormal
.append( _saveAllButton
).append( _cancelAllButton
);
664 _runHooks = function( oldtext
, type
) {
665 // No hooks registered
666 if ( !_hooks
[type
] ) {
669 for (var i
= 0; i
< _hooks
[type
].length
; i
++) {
670 oldtext
= _hooks
[type
][i
]( oldtext
);
677 * Currently available: beforeAdd, beforeChange, beforeDelete
679 * @param string type Type of hook to add
680 * @param function fn Hook function. This function is the old text passed
681 * and it needs to return the modified text
683 this.addHook = function( type
, fn
) {
684 if ( !_hooks
[type
] ) return;
685 else hooks
[type
].push( fn
);
688 // Now make a new version
689 mw
.ajaxCategories
= new ajaxCategories();
691 } )( jQuery
, mediaWiki
);